This uses the SetWindowDisplayAffinity API to hide windows from capture applications (including OBS). This is not perfect - internal windows such as context menus, combo box dropdowns, etc will still be displayed. Even with these limitations, it should help people with single monitors capture content with less interference from the OBS window. This implementation is for Windows only but the code is generic enough that adding other platforms should be straightforward.
1129 lines
28 KiB
C++
1129 lines
28 KiB
C++
#include <QAction>
|
|
#include <QGuiApplication>
|
|
#include <QMouseEvent>
|
|
#include <QMenu>
|
|
#include <QScreen>
|
|
#include "obs-app.hpp"
|
|
#include "window-basic-main.hpp"
|
|
#include "display-helpers.hpp"
|
|
#include "qt-wrappers.hpp"
|
|
#include "platform.hpp"
|
|
|
|
static QList<OBSProjector *> multiviewProjectors;
|
|
static QList<OBSProjector *> allProjectors;
|
|
|
|
static bool updatingMultiview = false, drawLabel, drawSafeArea, mouseSwitching,
|
|
transitionOnDoubleClick;
|
|
static MultiviewLayout multiviewLayout;
|
|
static size_t maxSrcs, numSrcs;
|
|
|
|
OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
|
|
ProjectorType type_)
|
|
: OBSQTDisplay(widget, Qt::Window),
|
|
source(source_),
|
|
removedSignal(obs_source_get_signal_handler(source), "remove",
|
|
OBSSourceRemoved, this)
|
|
{
|
|
isAlwaysOnTop = config_get_bool(GetGlobalConfig(), "BasicWindow",
|
|
"ProjectorAlwaysOnTop");
|
|
|
|
if (isAlwaysOnTop)
|
|
setWindowFlags(Qt::WindowStaysOnTopHint);
|
|
|
|
// Mark the window as a projector so SetDisplayAffinity
|
|
// can skip it
|
|
windowHandle()->setProperty("isOBSProjectorWindow", true);
|
|
|
|
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__)
|
|
// Prevents resizing of projector windows
|
|
setAttribute(Qt::WA_PaintOnScreen, false);
|
|
#endif
|
|
|
|
type = type_;
|
|
#ifdef __APPLE__
|
|
setWindowIcon(
|
|
QIcon::fromTheme("obs", QIcon(":/res/images/obs_256x256.png")));
|
|
#else
|
|
setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png")));
|
|
#endif
|
|
|
|
if (monitor == -1)
|
|
resize(480, 270);
|
|
else
|
|
SetMonitor(monitor);
|
|
|
|
UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
|
|
|
|
QAction *action = new QAction(this);
|
|
action->setShortcut(Qt::Key_Escape);
|
|
addAction(action);
|
|
connect(action, SIGNAL(triggered()), this, SLOT(EscapeTriggered()));
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
|
|
//disable application quit when last window closed
|
|
setAttribute(Qt::WA_QuitOnClose, false);
|
|
|
|
installEventFilter(CreateShortcutFilter());
|
|
|
|
auto addDrawCallback = [this]() {
|
|
bool isMultiview = type == ProjectorType::Multiview;
|
|
obs_display_add_draw_callback(
|
|
GetDisplay(),
|
|
isMultiview ? OBSRenderMultiview : OBSRender, this);
|
|
obs_display_set_background_color(GetDisplay(), 0x000000);
|
|
};
|
|
|
|
connect(this, &OBSQTDisplay::DisplayCreated, addDrawCallback);
|
|
connect(App(), &QGuiApplication::screenRemoved, this,
|
|
&OBSProjector::ScreenRemoved);
|
|
|
|
if (type == ProjectorType::Multiview) {
|
|
InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin,
|
|
&fourByThreeSafeMargin, &leftLine, &topLine,
|
|
&rightLine);
|
|
UpdateMultiview();
|
|
|
|
multiviewProjectors.push_back(this);
|
|
}
|
|
|
|
App()->IncrementSleepInhibition();
|
|
|
|
if (source)
|
|
obs_source_inc_showing(source);
|
|
|
|
allProjectors.push_back(this);
|
|
|
|
ready = true;
|
|
|
|
show();
|
|
|
|
// We need it here to allow keyboard input in X11 to listen to Escape
|
|
activateWindow();
|
|
}
|
|
|
|
OBSProjector::~OBSProjector()
|
|
{
|
|
bool isMultiview = type == ProjectorType::Multiview;
|
|
obs_display_remove_draw_callback(
|
|
GetDisplay(), isMultiview ? OBSRenderMultiview : OBSRender,
|
|
this);
|
|
|
|
if (source)
|
|
obs_source_dec_showing(source);
|
|
|
|
if (isMultiview) {
|
|
for (OBSWeakSource &weakSrc : multiviewScenes) {
|
|
OBSSource src = OBSGetStrongRef(weakSrc);
|
|
if (src)
|
|
obs_source_dec_showing(src);
|
|
}
|
|
|
|
obs_enter_graphics();
|
|
gs_vertexbuffer_destroy(actionSafeMargin);
|
|
gs_vertexbuffer_destroy(graphicsSafeMargin);
|
|
gs_vertexbuffer_destroy(fourByThreeSafeMargin);
|
|
gs_vertexbuffer_destroy(leftLine);
|
|
gs_vertexbuffer_destroy(topLine);
|
|
gs_vertexbuffer_destroy(rightLine);
|
|
obs_leave_graphics();
|
|
}
|
|
|
|
if (type == ProjectorType::Multiview)
|
|
multiviewProjectors.removeAll(this);
|
|
|
|
App()->DecrementSleepInhibition();
|
|
|
|
screen = nullptr;
|
|
}
|
|
|
|
void OBSProjector::SetMonitor(int monitor)
|
|
{
|
|
savedMonitor = monitor;
|
|
screen = QGuiApplication::screens()[monitor];
|
|
setGeometry(screen->geometry());
|
|
showFullScreen();
|
|
SetHideCursor();
|
|
}
|
|
|
|
void OBSProjector::SetHideCursor()
|
|
{
|
|
if (savedMonitor == -1)
|
|
return;
|
|
|
|
bool hideCursor = config_get_bool(GetGlobalConfig(), "BasicWindow",
|
|
"HideProjectorCursor");
|
|
|
|
if (hideCursor && type != ProjectorType::Multiview)
|
|
setCursor(Qt::BlankCursor);
|
|
else
|
|
setCursor(Qt::ArrowCursor);
|
|
}
|
|
|
|
static OBSSource CreateLabel(const char *name, size_t h)
|
|
{
|
|
OBSDataAutoRelease settings = obs_data_create();
|
|
OBSDataAutoRelease font = obs_data_create();
|
|
|
|
std::string text;
|
|
text += " ";
|
|
text += name;
|
|
text += " ";
|
|
|
|
#if defined(_WIN32)
|
|
obs_data_set_string(font, "face", "Arial");
|
|
#elif defined(__APPLE__)
|
|
obs_data_set_string(font, "face", "Helvetica");
|
|
#else
|
|
obs_data_set_string(font, "face", "Monospace");
|
|
#endif
|
|
obs_data_set_int(font, "flags", 1); // Bold text
|
|
obs_data_set_int(font, "size", int(h / 9.81));
|
|
|
|
obs_data_set_obj(settings, "font", font);
|
|
obs_data_set_string(settings, "text", text.c_str());
|
|
obs_data_set_bool(settings, "outline", false);
|
|
|
|
#ifdef _WIN32
|
|
const char *text_source_id = "text_gdiplus";
|
|
#else
|
|
const char *text_source_id = "text_ft2_source";
|
|
#endif
|
|
|
|
OBSSourceAutoRelease txtSource =
|
|
obs_source_create_private(text_source_id, name, settings);
|
|
|
|
return txtSource.Get();
|
|
}
|
|
|
|
static inline uint32_t labelOffset(obs_source_t *label, uint32_t cx)
|
|
{
|
|
uint32_t w = obs_source_get_width(label);
|
|
|
|
int n; // Twice of scale factor of preview and program scenes
|
|
switch (multiviewLayout) {
|
|
case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
|
|
n = 6;
|
|
break;
|
|
default:
|
|
n = 4;
|
|
break;
|
|
}
|
|
|
|
w = uint32_t(w * ((1.0f) / n));
|
|
return (cx / 2) - w;
|
|
}
|
|
|
|
static inline void startRegion(int vX, int vY, int vCX, int vCY, float oL,
|
|
float oR, float oT, float oB)
|
|
{
|
|
gs_projection_push();
|
|
gs_viewport_push();
|
|
gs_set_viewport(vX, vY, vCX, vCY);
|
|
gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f);
|
|
}
|
|
|
|
static inline void endRegion()
|
|
{
|
|
gs_viewport_pop();
|
|
gs_projection_pop();
|
|
}
|
|
|
|
void OBSProjector::OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy)
|
|
{
|
|
OBSProjector *window = (OBSProjector *)data;
|
|
|
|
if (updatingMultiview || !window->ready)
|
|
return;
|
|
|
|
OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window();
|
|
uint32_t targetCX, targetCY;
|
|
int x, y;
|
|
float scale;
|
|
|
|
targetCX = (uint32_t)window->fw;
|
|
targetCY = (uint32_t)window->fh;
|
|
|
|
GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
|
|
|
|
OBSSource previewSrc = main->GetCurrentSceneSource();
|
|
OBSSource programSrc = main->GetProgramSource();
|
|
bool studioMode = main->IsPreviewProgramMode();
|
|
|
|
auto drawBox = [&](float cx, float cy, uint32_t colorVal) {
|
|
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
|
|
gs_eparam_t *color =
|
|
gs_effect_get_param_by_name(solid, "color");
|
|
|
|
gs_effect_set_color(color, colorVal);
|
|
while (gs_effect_loop(solid, "Solid"))
|
|
gs_draw_sprite(nullptr, 0, (uint32_t)cx, (uint32_t)cy);
|
|
};
|
|
|
|
auto setRegion = [&](float bx, float by, float cx, float cy) {
|
|
float vX = int(x + bx * scale);
|
|
float vY = int(y + by * scale);
|
|
float vCX = int(cx * scale);
|
|
float vCY = int(cy * scale);
|
|
|
|
float oL = bx;
|
|
float oT = by;
|
|
float oR = (bx + cx);
|
|
float oB = (by + cy);
|
|
|
|
startRegion(vX, vY, vCX, vCY, oL, oR, oT, oB);
|
|
};
|
|
|
|
auto calcBaseSource = [&](size_t i) {
|
|
switch (multiviewLayout) {
|
|
case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
|
|
window->sourceX = (i % 6) * window->scenesCX;
|
|
window->sourceY =
|
|
window->pvwprgCY + (i / 6) * window->scenesCY;
|
|
break;
|
|
case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
|
|
window->sourceX = (i % 6) * window->scenesCX;
|
|
window->sourceY =
|
|
window->pvwprgCY + (i / 6) * window->scenesCY;
|
|
break;
|
|
case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
|
|
window->sourceX = window->pvwprgCX;
|
|
window->sourceY = (i / 2) * window->scenesCY;
|
|
if (i % 2 != 0)
|
|
window->sourceX += window->scenesCX;
|
|
break;
|
|
case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
|
|
window->sourceX = 0;
|
|
window->sourceY = (i / 2) * window->scenesCY;
|
|
if (i % 2 != 0)
|
|
window->sourceX = window->scenesCX;
|
|
break;
|
|
case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
|
|
if (i < 4) {
|
|
window->sourceX = (float(i) * window->scenesCX);
|
|
window->sourceY = 0;
|
|
} else {
|
|
window->sourceX =
|
|
(float(i - 4) * window->scenesCX);
|
|
window->sourceY = window->scenesCY;
|
|
}
|
|
break;
|
|
default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES:
|
|
if (i < 4) {
|
|
window->sourceX = (float(i) * window->scenesCX);
|
|
window->sourceY = window->pvwprgCY;
|
|
} else {
|
|
window->sourceX =
|
|
(float(i - 4) * window->scenesCX);
|
|
window->sourceY =
|
|
window->pvwprgCY + window->scenesCY;
|
|
}
|
|
}
|
|
window->siX = window->sourceX + window->thickness;
|
|
window->siY = window->sourceY + window->thickness;
|
|
};
|
|
|
|
auto calcPreviewProgram = [&](bool program) {
|
|
switch (multiviewLayout) {
|
|
case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
|
|
window->sourceX =
|
|
window->thickness + window->pvwprgCX / 2;
|
|
window->sourceY = window->thickness;
|
|
window->labelX = window->offset + window->pvwprgCX / 2;
|
|
window->labelY = window->pvwprgCY * 0.85f;
|
|
if (program) {
|
|
window->sourceX += window->pvwprgCX;
|
|
window->labelX += window->pvwprgCX;
|
|
}
|
|
break;
|
|
case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
|
|
window->sourceX = window->thickness;
|
|
window->sourceY = window->pvwprgCY + window->thickness;
|
|
window->labelX = window->offset;
|
|
window->labelY = window->pvwprgCY * 1.85f;
|
|
if (program) {
|
|
window->sourceY = window->thickness;
|
|
window->labelY = window->pvwprgCY * 0.85f;
|
|
}
|
|
break;
|
|
case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
|
|
window->sourceX = window->pvwprgCX + window->thickness;
|
|
window->sourceY = window->pvwprgCY + window->thickness;
|
|
window->labelX = window->pvwprgCX + window->offset;
|
|
window->labelY = window->pvwprgCY * 1.85f;
|
|
if (program) {
|
|
window->sourceY = window->thickness;
|
|
window->labelY = window->pvwprgCY * 0.85f;
|
|
}
|
|
break;
|
|
case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
|
|
window->sourceX = window->thickness;
|
|
window->sourceY = window->pvwprgCY + window->thickness;
|
|
window->labelX = window->offset;
|
|
window->labelY = window->pvwprgCY * 1.85f;
|
|
if (program) {
|
|
window->sourceX += window->pvwprgCX;
|
|
window->labelX += window->pvwprgCX;
|
|
}
|
|
break;
|
|
default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES and 18_SCENES
|
|
window->sourceX = window->thickness;
|
|
window->sourceY = window->thickness;
|
|
window->labelX = window->offset;
|
|
window->labelY = window->pvwprgCY * 0.85f;
|
|
if (program) {
|
|
window->sourceX += window->pvwprgCX;
|
|
window->labelX += window->pvwprgCX;
|
|
}
|
|
}
|
|
};
|
|
|
|
auto paintAreaWithColor = [&](float tx, float ty, float cx, float cy,
|
|
uint32_t color) {
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(tx, ty, 0.0f);
|
|
drawBox(cx, cy, color);
|
|
gs_matrix_pop();
|
|
};
|
|
|
|
// Define the whole usable region for the multiview
|
|
startRegion(x, y, targetCX * scale, targetCY * scale, 0.0f, window->fw,
|
|
0.0f, window->fh);
|
|
|
|
// Change the background color to highlight all sources
|
|
drawBox(window->fw, window->fh, outerColor);
|
|
|
|
/* ----------------------------- */
|
|
/* draw sources */
|
|
|
|
for (size_t i = 0; i < maxSrcs; i++) {
|
|
// Handle all the offsets
|
|
calcBaseSource(i);
|
|
|
|
if (i >= numSrcs) {
|
|
// Just paint the background and continue
|
|
paintAreaWithColor(window->sourceX, window->sourceY,
|
|
window->scenesCX, window->scenesCY,
|
|
outerColor);
|
|
paintAreaWithColor(window->siX, window->siY,
|
|
window->siCX, window->siCY,
|
|
backgroundColor);
|
|
continue;
|
|
}
|
|
|
|
OBSSource src = OBSGetStrongRef(window->multiviewScenes[i]);
|
|
|
|
// We have a source. Now chose the proper highlight color
|
|
uint32_t colorVal = outerColor;
|
|
if (src == programSrc)
|
|
colorVal = programColor;
|
|
else if (src == previewSrc)
|
|
colorVal = studioMode ? previewColor : programColor;
|
|
|
|
// Paint the background
|
|
paintAreaWithColor(window->sourceX, window->sourceY,
|
|
window->scenesCX, window->scenesCY,
|
|
colorVal);
|
|
paintAreaWithColor(window->siX, window->siY, window->siCX,
|
|
window->siCY, backgroundColor);
|
|
|
|
/* ----------- */
|
|
|
|
// Render the source
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(window->siX, window->siY, 0.0f);
|
|
gs_matrix_scale3f(window->siScaleX, window->siScaleY, 1.0f);
|
|
setRegion(window->siX, window->siY, window->siCX, window->siCY);
|
|
obs_source_video_render(src);
|
|
endRegion();
|
|
gs_matrix_pop();
|
|
|
|
/* ----------- */
|
|
|
|
// Render the label
|
|
if (!drawLabel)
|
|
continue;
|
|
|
|
obs_source *label = window->multiviewLabels[i + 2];
|
|
if (!label)
|
|
continue;
|
|
|
|
window->offset = labelOffset(label, window->scenesCX);
|
|
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(
|
|
window->sourceX + window->offset,
|
|
(window->scenesCY * 0.85f) + window->sourceY, 0.0f);
|
|
gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
|
|
drawBox(obs_source_get_width(label),
|
|
obs_source_get_height(label) +
|
|
int(window->sourceY * 0.015f),
|
|
labelColor);
|
|
obs_source_video_render(label);
|
|
gs_matrix_pop();
|
|
}
|
|
|
|
/* ----------------------------- */
|
|
/* draw preview */
|
|
|
|
obs_source_t *previewLabel = window->multiviewLabels[0];
|
|
window->offset = labelOffset(previewLabel, window->pvwprgCX);
|
|
calcPreviewProgram(false);
|
|
|
|
// Paint the background
|
|
paintAreaWithColor(window->sourceX, window->sourceY, window->ppiCX,
|
|
window->ppiCY, backgroundColor);
|
|
|
|
// Scale and Draw the preview
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(window->sourceX, window->sourceY, 0.0f);
|
|
gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
|
|
setRegion(window->sourceX, window->sourceY, window->ppiCX,
|
|
window->ppiCY);
|
|
if (studioMode)
|
|
obs_source_video_render(previewSrc);
|
|
else
|
|
obs_render_main_texture();
|
|
|
|
if (drawSafeArea) {
|
|
RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY);
|
|
RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY);
|
|
RenderSafeAreas(window->fourByThreeSafeMargin, targetCX,
|
|
targetCY);
|
|
RenderSafeAreas(window->leftLine, targetCX, targetCY);
|
|
RenderSafeAreas(window->topLine, targetCX, targetCY);
|
|
RenderSafeAreas(window->rightLine, targetCX, targetCY);
|
|
}
|
|
|
|
endRegion();
|
|
gs_matrix_pop();
|
|
|
|
/* ----------- */
|
|
|
|
// Draw the Label
|
|
if (drawLabel) {
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(window->labelX, window->labelY, 0.0f);
|
|
gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
|
|
drawBox(obs_source_get_width(previewLabel),
|
|
obs_source_get_height(previewLabel) +
|
|
int(window->pvwprgCX * 0.015f),
|
|
labelColor);
|
|
obs_source_video_render(previewLabel);
|
|
gs_matrix_pop();
|
|
}
|
|
|
|
/* ----------------------------- */
|
|
/* draw program */
|
|
|
|
obs_source_t *programLabel = window->multiviewLabels[1];
|
|
window->offset = labelOffset(programLabel, window->pvwprgCX);
|
|
calcPreviewProgram(true);
|
|
|
|
paintAreaWithColor(window->sourceX, window->sourceY, window->ppiCX,
|
|
window->ppiCY, backgroundColor);
|
|
|
|
// Scale and Draw the program
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(window->sourceX, window->sourceY, 0.0f);
|
|
gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
|
|
setRegion(window->sourceX, window->sourceY, window->ppiCX,
|
|
window->ppiCY);
|
|
obs_render_main_texture();
|
|
endRegion();
|
|
gs_matrix_pop();
|
|
|
|
/* ----------- */
|
|
|
|
// Draw the Label
|
|
if (drawLabel) {
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(window->labelX, window->labelY, 0.0f);
|
|
gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
|
|
drawBox(obs_source_get_width(programLabel),
|
|
obs_source_get_height(programLabel) +
|
|
int(window->pvwprgCX * 0.015f),
|
|
labelColor);
|
|
obs_source_video_render(programLabel);
|
|
gs_matrix_pop();
|
|
}
|
|
|
|
// Region for future usage with additional info.
|
|
if (multiviewLayout == MultiviewLayout::HORIZONTAL_TOP_24_SCENES) {
|
|
// Just paint the background for now
|
|
paintAreaWithColor(window->thickness, window->thickness,
|
|
window->siCX,
|
|
window->siCY * 2 + window->thicknessx2,
|
|
backgroundColor);
|
|
paintAreaWithColor(
|
|
window->thickness +
|
|
2.5 * (window->thicknessx2 + window->ppiCX),
|
|
window->thickness, window->siCX,
|
|
window->siCY * 2 + window->thicknessx2,
|
|
backgroundColor);
|
|
}
|
|
|
|
endRegion();
|
|
}
|
|
|
|
void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy)
|
|
{
|
|
OBSProjector *window = reinterpret_cast<OBSProjector *>(data);
|
|
|
|
if (!window->ready)
|
|
return;
|
|
|
|
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
|
|
OBSSource source = window->source;
|
|
|
|
uint32_t targetCX;
|
|
uint32_t targetCY;
|
|
int x, y;
|
|
int newCX, newCY;
|
|
float scale;
|
|
|
|
if (source) {
|
|
targetCX = std::max(obs_source_get_width(source), 1u);
|
|
targetCY = std::max(obs_source_get_height(source), 1u);
|
|
} else {
|
|
struct obs_video_info ovi;
|
|
obs_get_video_info(&ovi);
|
|
targetCX = ovi.base_width;
|
|
targetCY = ovi.base_height;
|
|
}
|
|
|
|
GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
|
|
|
|
newCX = int(scale * float(targetCX));
|
|
newCY = int(scale * float(targetCY));
|
|
|
|
startRegion(x, y, newCX, newCY, 0.0f, float(targetCX), 0.0f,
|
|
float(targetCY));
|
|
|
|
if (window->type == ProjectorType::Preview &&
|
|
main->IsPreviewProgramMode()) {
|
|
OBSSource curSource = main->GetCurrentSceneSource();
|
|
|
|
if (source != curSource) {
|
|
obs_source_dec_showing(source);
|
|
obs_source_inc_showing(curSource);
|
|
source = curSource;
|
|
window->source = source;
|
|
}
|
|
} else if (window->type == ProjectorType::Preview &&
|
|
!main->IsPreviewProgramMode()) {
|
|
window->source = nullptr;
|
|
}
|
|
|
|
if (source)
|
|
obs_source_video_render(source);
|
|
else
|
|
obs_render_main_texture();
|
|
|
|
endRegion();
|
|
}
|
|
|
|
void OBSProjector::OBSSourceRemoved(void *data, calldata_t *params)
|
|
{
|
|
OBSProjector *window = reinterpret_cast<OBSProjector *>(data);
|
|
|
|
window->deleteLater();
|
|
|
|
UNUSED_PARAMETER(params);
|
|
}
|
|
|
|
static int getSourceByPosition(int x, int y, float ratio)
|
|
{
|
|
int pos = -1;
|
|
QWidget *rec = QApplication::activeWindow();
|
|
if (!rec)
|
|
return pos;
|
|
int cx = rec->width();
|
|
int cy = rec->height();
|
|
int minX = 0;
|
|
int minY = 0;
|
|
int maxX = cx;
|
|
int maxY = cy;
|
|
|
|
switch (multiviewLayout) {
|
|
case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
|
|
if (float(cx) / float(cy) > ratio) {
|
|
int validX = cy * ratio;
|
|
minX = (cx / 2) - (validX / 2);
|
|
maxX = (cx / 2) + (validX / 2);
|
|
} else {
|
|
int validY = cx / ratio;
|
|
maxY = (cy / 2) + (validY / 2);
|
|
}
|
|
minY = cy / 2;
|
|
|
|
if (x < minX || x > maxX || y < minY || y > maxY)
|
|
break;
|
|
|
|
pos = (x - minX) / ((maxX - minX) / 6);
|
|
pos += ((y - minY) / ((maxY - minY) / 3)) * 6;
|
|
|
|
break;
|
|
case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
|
|
if (float(cx) / float(cy) > ratio) {
|
|
int validX = cy * ratio;
|
|
minX = (cx / 2) - (validX / 2);
|
|
maxX = (cx / 2) + (validX / 2);
|
|
minY = cy / 3;
|
|
} else {
|
|
int validY = cx / ratio;
|
|
maxY = (cy / 2) + (validY / 2);
|
|
minY = (cy / 2) - (validY / 6);
|
|
}
|
|
|
|
if (x < minX || x > maxX || y < minY || y > maxY)
|
|
break;
|
|
|
|
pos = (x - minX) / ((maxX - minX) / 6);
|
|
pos += ((y - minY) / ((maxY - minY) / 4)) * 6;
|
|
|
|
break;
|
|
case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
|
|
if (float(cx) / float(cy) > ratio) {
|
|
int validX = cy * ratio;
|
|
maxX = (cx / 2) + (validX / 2);
|
|
} else {
|
|
int validY = cx / ratio;
|
|
minY = (cy / 2) - (validY / 2);
|
|
maxY = (cy / 2) + (validY / 2);
|
|
}
|
|
|
|
minX = cx / 2;
|
|
|
|
if (x < minX || x > maxX || y < minY || y > maxY)
|
|
break;
|
|
|
|
pos = 2 * ((y - minY) / ((maxY - minY) / 4));
|
|
if (x > minX + ((maxX - minX) / 2))
|
|
pos++;
|
|
break;
|
|
case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
|
|
if (float(cx) / float(cy) > ratio) {
|
|
int validX = cy * ratio;
|
|
minX = (cx / 2) - (validX / 2);
|
|
} else {
|
|
int validY = cx / ratio;
|
|
minY = (cy / 2) - (validY / 2);
|
|
maxY = (cy / 2) + (validY / 2);
|
|
}
|
|
|
|
maxX = (cx / 2);
|
|
|
|
if (x < minX || x > maxX || y < minY || y > maxY)
|
|
break;
|
|
|
|
pos = 2 * ((y - minY) / ((maxY - minY) / 4));
|
|
if (x > minX + ((maxX - minX) / 2))
|
|
pos++;
|
|
break;
|
|
case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
|
|
if (float(cx) / float(cy) > ratio) {
|
|
int validX = cy * ratio;
|
|
minX = (cx / 2) - (validX / 2);
|
|
maxX = (cx / 2) + (validX / 2);
|
|
} else {
|
|
int validY = cx / ratio;
|
|
minY = (cy / 2) - (validY / 2);
|
|
}
|
|
|
|
maxY = (cy / 2);
|
|
|
|
if (x < minX || x > maxX || y < minY || y > maxY)
|
|
break;
|
|
|
|
pos = (x - minX) / ((maxX - minX) / 4);
|
|
if (y > minY + ((maxY - minY) / 2))
|
|
pos += 4;
|
|
break;
|
|
default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES
|
|
if (float(cx) / float(cy) > ratio) {
|
|
int validX = cy * ratio;
|
|
minX = (cx / 2) - (validX / 2);
|
|
maxX = (cx / 2) + (validX / 2);
|
|
} else {
|
|
int validY = cx / ratio;
|
|
maxY = (cy / 2) + (validY / 2);
|
|
}
|
|
|
|
minY = (cy / 2);
|
|
|
|
if (x < minX || x > maxX || y < minY || y > maxY)
|
|
break;
|
|
|
|
pos = (x - minX) / ((maxX - minX) / 4);
|
|
if (y > minY + ((maxY - minY) / 2))
|
|
pos += 4;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
void OBSProjector::mouseDoubleClickEvent(QMouseEvent *event)
|
|
{
|
|
OBSQTDisplay::mouseDoubleClickEvent(event);
|
|
|
|
if (!mouseSwitching)
|
|
return;
|
|
|
|
if (!transitionOnDoubleClick)
|
|
return;
|
|
|
|
OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window();
|
|
if (!main->IsPreviewProgramMode())
|
|
return;
|
|
|
|
if (event->button() == Qt::LeftButton) {
|
|
int pos = getSourceByPosition(event->x(), event->y(), ratio);
|
|
if (pos < 0 || pos >= (int)numSrcs)
|
|
return;
|
|
OBSSource src = OBSGetStrongRef(multiviewScenes[pos]);
|
|
if (!src)
|
|
return;
|
|
|
|
if (main->GetProgramSource() != src)
|
|
main->TransitionToScene(src);
|
|
}
|
|
}
|
|
|
|
void OBSProjector::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
OBSQTDisplay::mousePressEvent(event);
|
|
|
|
if (event->button() == Qt::RightButton) {
|
|
OBSBasic *main =
|
|
reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
|
|
QMenu popup(this);
|
|
|
|
QMenu *projectorMenu = new QMenu(QTStr("Fullscreen"));
|
|
main->AddProjectorMenuMonitors(projectorMenu, this,
|
|
SLOT(OpenFullScreenProjector()));
|
|
popup.addMenu(projectorMenu);
|
|
|
|
if (GetMonitor() > -1) {
|
|
popup.addAction(QTStr("Windowed"), this,
|
|
SLOT(OpenWindowedProjector()));
|
|
|
|
} else if (!this->isMaximized()) {
|
|
popup.addAction(QTStr("ResizeProjectorWindowToContent"),
|
|
this, SLOT(ResizeToContent()));
|
|
}
|
|
|
|
QAction *alwaysOnTopButton =
|
|
new QAction(QTStr("Basic.MainMenu.AlwaysOnTop"), this);
|
|
alwaysOnTopButton->setCheckable(true);
|
|
alwaysOnTopButton->setChecked(isAlwaysOnTop);
|
|
|
|
connect(alwaysOnTopButton, &QAction::toggled, this,
|
|
&OBSProjector::AlwaysOnTopToggled);
|
|
|
|
popup.addAction(alwaysOnTopButton);
|
|
|
|
popup.addAction(QTStr("Close"), this, SLOT(EscapeTriggered()));
|
|
popup.exec(QCursor::pos());
|
|
}
|
|
|
|
if (!mouseSwitching)
|
|
return;
|
|
|
|
if (event->button() == Qt::LeftButton) {
|
|
int pos = getSourceByPosition(event->x(), event->y(), ratio);
|
|
if (pos < 0 || pos >= (int)numSrcs)
|
|
return;
|
|
OBSSource src = OBSGetStrongRef(multiviewScenes[pos]);
|
|
if (!src)
|
|
return;
|
|
|
|
OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window();
|
|
if (main->GetCurrentSceneSource() != src)
|
|
main->SetCurrentScene(src, false);
|
|
}
|
|
}
|
|
|
|
void OBSProjector::EscapeTriggered()
|
|
{
|
|
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
|
|
main->DeleteProjector(this);
|
|
|
|
allProjectors.removeAll(this);
|
|
}
|
|
|
|
void OBSProjector::UpdateMultiview()
|
|
{
|
|
multiviewScenes.clear();
|
|
multiviewLabels.clear();
|
|
|
|
struct obs_video_info ovi;
|
|
obs_get_video_info(&ovi);
|
|
|
|
uint32_t w = ovi.base_width;
|
|
uint32_t h = ovi.base_height;
|
|
fw = float(w);
|
|
fh = float(h);
|
|
ratio = fw / fh;
|
|
|
|
struct obs_frontend_source_list scenes = {};
|
|
obs_frontend_get_scenes(&scenes);
|
|
|
|
multiviewLabels.emplace_back(
|
|
CreateLabel(Str("StudioMode.Preview"), h / 2));
|
|
multiviewLabels.emplace_back(
|
|
CreateLabel(Str("StudioMode.Program"), h / 2));
|
|
|
|
multiviewLayout = static_cast<MultiviewLayout>(config_get_int(
|
|
GetGlobalConfig(), "BasicWindow", "MultiviewLayout"));
|
|
|
|
drawLabel = config_get_bool(GetGlobalConfig(), "BasicWindow",
|
|
"MultiviewDrawNames");
|
|
|
|
drawSafeArea = config_get_bool(GetGlobalConfig(), "BasicWindow",
|
|
"MultiviewDrawAreas");
|
|
|
|
mouseSwitching = config_get_bool(GetGlobalConfig(), "BasicWindow",
|
|
"MultiviewMouseSwitch");
|
|
|
|
transitionOnDoubleClick = config_get_bool(
|
|
GetGlobalConfig(), "BasicWindow", "TransitionOnDoubleClick");
|
|
|
|
switch (multiviewLayout) {
|
|
case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
|
|
pvwprgCX = fw / 2;
|
|
pvwprgCY = fh / 2;
|
|
|
|
maxSrcs = 18;
|
|
break;
|
|
case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
|
|
pvwprgCX = fw / 3;
|
|
pvwprgCY = fh / 3;
|
|
|
|
maxSrcs = 24;
|
|
break;
|
|
default:
|
|
pvwprgCX = fw / 2;
|
|
pvwprgCY = fh / 2;
|
|
|
|
maxSrcs = 8;
|
|
}
|
|
|
|
ppiCX = pvwprgCX - thicknessx2;
|
|
ppiCY = pvwprgCY - thicknessx2;
|
|
ppiScaleX = (pvwprgCX - thicknessx2) / fw;
|
|
ppiScaleY = (pvwprgCY - thicknessx2) / fh;
|
|
|
|
switch (multiviewLayout) {
|
|
case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
|
|
scenesCX = pvwprgCX / 3;
|
|
scenesCY = pvwprgCY / 3;
|
|
break;
|
|
default:
|
|
scenesCX = pvwprgCX / 2;
|
|
scenesCY = pvwprgCY / 2;
|
|
}
|
|
|
|
siCX = scenesCX - thicknessx2;
|
|
siCY = scenesCY - thicknessx2;
|
|
siScaleX = (scenesCX - thicknessx2) / fw;
|
|
siScaleY = (scenesCY - thicknessx2) / fh;
|
|
|
|
numSrcs = 0;
|
|
size_t i = 0;
|
|
while (i < scenes.sources.num && numSrcs < maxSrcs) {
|
|
obs_source_t *src = scenes.sources.array[i++];
|
|
OBSDataAutoRelease data = obs_source_get_private_settings(src);
|
|
|
|
obs_data_set_default_bool(data, "show_in_multiview", true);
|
|
if (!obs_data_get_bool(data, "show_in_multiview"))
|
|
continue;
|
|
|
|
// We have a displayable source.
|
|
numSrcs++;
|
|
|
|
multiviewScenes.emplace_back(OBSGetWeakRef(src));
|
|
obs_source_inc_showing(src);
|
|
|
|
std::string name = std::to_string(numSrcs) + " - " +
|
|
obs_source_get_name(src);
|
|
multiviewLabels.emplace_back(CreateLabel(name.c_str(), h / 3));
|
|
}
|
|
|
|
obs_frontend_source_list_free(&scenes);
|
|
}
|
|
|
|
void OBSProjector::UpdateProjectorTitle(QString name)
|
|
{
|
|
bool window = (GetMonitor() == -1);
|
|
|
|
QString title = nullptr;
|
|
switch (type) {
|
|
case ProjectorType::Scene:
|
|
if (!window)
|
|
title = QTStr("SceneProjector") + " - " + name;
|
|
else
|
|
title = QTStr("SceneWindow") + " - " + name;
|
|
break;
|
|
case ProjectorType::Source:
|
|
if (!window)
|
|
title = QTStr("SourceProjector") + " - " + name;
|
|
else
|
|
title = QTStr("SourceWindow") + " - " + name;
|
|
break;
|
|
case ProjectorType::Preview:
|
|
if (!window)
|
|
title = QTStr("PreviewProjector");
|
|
else
|
|
title = QTStr("PreviewWindow");
|
|
break;
|
|
case ProjectorType::StudioProgram:
|
|
if (!window)
|
|
title = QTStr("StudioProgramProjector");
|
|
else
|
|
title = QTStr("StudioProgramWindow");
|
|
break;
|
|
case ProjectorType::Multiview:
|
|
if (!window)
|
|
title = QTStr("MultiviewProjector");
|
|
else
|
|
title = QTStr("MultiviewWindowed");
|
|
break;
|
|
default:
|
|
title = name;
|
|
break;
|
|
}
|
|
|
|
setWindowTitle(title);
|
|
}
|
|
|
|
OBSSource OBSProjector::GetSource()
|
|
{
|
|
return source;
|
|
}
|
|
|
|
ProjectorType OBSProjector::GetProjectorType()
|
|
{
|
|
return type;
|
|
}
|
|
|
|
int OBSProjector::GetMonitor()
|
|
{
|
|
return savedMonitor;
|
|
}
|
|
|
|
void OBSProjector::UpdateMultiviewProjectors()
|
|
{
|
|
obs_enter_graphics();
|
|
updatingMultiview = true;
|
|
obs_leave_graphics();
|
|
|
|
for (auto &projector : multiviewProjectors)
|
|
projector->UpdateMultiview();
|
|
|
|
obs_enter_graphics();
|
|
updatingMultiview = false;
|
|
obs_leave_graphics();
|
|
}
|
|
|
|
void OBSProjector::RenameProjector(QString oldName, QString newName)
|
|
{
|
|
if (oldName == newName)
|
|
return;
|
|
|
|
UpdateProjectorTitle(newName);
|
|
}
|
|
|
|
void OBSProjector::OpenFullScreenProjector()
|
|
{
|
|
if (!isFullScreen())
|
|
prevGeometry = geometry();
|
|
|
|
int monitor = sender()->property("monitor").toInt();
|
|
SetMonitor(monitor);
|
|
|
|
UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
|
|
}
|
|
|
|
void OBSProjector::OpenWindowedProjector()
|
|
{
|
|
showFullScreen();
|
|
showNormal();
|
|
setCursor(Qt::ArrowCursor);
|
|
|
|
if (!prevGeometry.isNull())
|
|
setGeometry(prevGeometry);
|
|
else
|
|
resize(480, 270);
|
|
|
|
savedMonitor = -1;
|
|
|
|
UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
|
|
screen = nullptr;
|
|
}
|
|
|
|
void OBSProjector::ResizeToContent()
|
|
{
|
|
OBSSource source = GetSource();
|
|
uint32_t targetCX;
|
|
uint32_t targetCY;
|
|
int x, y, newX, newY;
|
|
float scale;
|
|
|
|
if (source) {
|
|
targetCX = std::max(obs_source_get_width(source), 1u);
|
|
targetCY = std::max(obs_source_get_height(source), 1u);
|
|
} else {
|
|
struct obs_video_info ovi;
|
|
obs_get_video_info(&ovi);
|
|
targetCX = ovi.base_width;
|
|
targetCY = ovi.base_height;
|
|
}
|
|
|
|
QSize size = this->size();
|
|
GetScaleAndCenterPos(targetCX, targetCY, size.width(), size.height(), x,
|
|
y, scale);
|
|
|
|
newX = size.width() - (x * 2);
|
|
newY = size.height() - (y * 2);
|
|
resize(newX, newY);
|
|
}
|
|
|
|
void OBSProjector::AlwaysOnTopToggled(bool isAlwaysOnTop)
|
|
{
|
|
SetIsAlwaysOnTop(isAlwaysOnTop, true);
|
|
}
|
|
|
|
void OBSProjector::closeEvent(QCloseEvent *event)
|
|
{
|
|
EscapeTriggered();
|
|
event->accept();
|
|
}
|
|
|
|
bool OBSProjector::IsAlwaysOnTop() const
|
|
{
|
|
return isAlwaysOnTop;
|
|
}
|
|
|
|
bool OBSProjector::IsAlwaysOnTopOverridden() const
|
|
{
|
|
return isAlwaysOnTopOverridden;
|
|
}
|
|
|
|
void OBSProjector::SetIsAlwaysOnTop(bool isAlwaysOnTop, bool isOverridden)
|
|
{
|
|
this->isAlwaysOnTop = isAlwaysOnTop;
|
|
this->isAlwaysOnTopOverridden = isOverridden;
|
|
|
|
SetAlwaysOnTop(this, isAlwaysOnTop);
|
|
}
|
|
|
|
void OBSProjector::ScreenRemoved(QScreen *screen_)
|
|
{
|
|
if (GetMonitor() < 0 || !screen)
|
|
return;
|
|
|
|
if (screen == screen_)
|
|
EscapeTriggered();
|
|
}
|