1 #include <QAction>
2 #include <QGuiApplication>
3 #include <QMouseEvent>
4 #include <QMenu>
5 #include <QScreen>
6 #include "obs-app.hpp"
7 #include "window-basic-main.hpp"
8 #include "display-helpers.hpp"
9 #include "qt-wrappers.hpp"
10 #include "platform.hpp"
11
12 static QList<OBSProjector *> multiviewProjectors;
13 static QList<OBSProjector *> allProjectors;
14
15 static bool updatingMultiview = false, drawLabel, drawSafeArea, mouseSwitching,
16 transitionOnDoubleClick;
17 static MultiviewLayout multiviewLayout;
18 static size_t maxSrcs, numSrcs;
19
OBSProjector(QWidget * widget,obs_source_t * source_,int monitor,ProjectorType type_)20 OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
21 ProjectorType type_)
22 : OBSQTDisplay(widget, Qt::Window),
23 source(source_),
24 removedSignal(obs_source_get_signal_handler(source), "remove",
25 OBSSourceRemoved, this)
26 {
27 isAlwaysOnTop = config_get_bool(GetGlobalConfig(), "BasicWindow",
28 "ProjectorAlwaysOnTop");
29
30 if (isAlwaysOnTop)
31 setWindowFlags(Qt::WindowStaysOnTopHint);
32
33 type = type_;
34 #ifdef __APPLE__
35 setWindowIcon(
36 QIcon::fromTheme("obs", QIcon(":/res/images/obs_256x256.png")));
37 #else
38 setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png")));
39 #endif
40
41 if (monitor == -1)
42 resize(480, 270);
43 else
44 SetMonitor(monitor);
45
46 UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
47
48 QAction *action = new QAction(this);
49 action->setShortcut(Qt::Key_Escape);
50 addAction(action);
51 connect(action, SIGNAL(triggered()), this, SLOT(EscapeTriggered()));
52
53 setAttribute(Qt::WA_DeleteOnClose, true);
54
55 //disable application quit when last window closed
56 setAttribute(Qt::WA_QuitOnClose, false);
57
58 installEventFilter(CreateShortcutFilter());
59
60 auto addDrawCallback = [this]() {
61 bool isMultiview = type == ProjectorType::Multiview;
62 obs_display_add_draw_callback(
63 GetDisplay(),
64 isMultiview ? OBSRenderMultiview : OBSRender, this);
65 obs_display_set_background_color(GetDisplay(), 0x000000);
66 };
67
68 connect(this, &OBSQTDisplay::DisplayCreated, addDrawCallback);
69 connect(App(), &QGuiApplication::screenRemoved, this,
70 &OBSProjector::ScreenRemoved);
71
72 if (type == ProjectorType::Multiview) {
73 InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin,
74 &fourByThreeSafeMargin, &leftLine, &topLine,
75 &rightLine);
76 UpdateMultiview();
77
78 multiviewProjectors.push_back(this);
79 }
80
81 App()->IncrementSleepInhibition();
82
83 if (source)
84 obs_source_inc_showing(source);
85
86 allProjectors.push_back(this);
87
88 ready = true;
89
90 show();
91
92 // We need it here to allow keyboard input in X11 to listen to Escape
93 activateWindow();
94 }
95
~OBSProjector()96 OBSProjector::~OBSProjector()
97 {
98 bool isMultiview = type == ProjectorType::Multiview;
99 obs_display_remove_draw_callback(
100 GetDisplay(), isMultiview ? OBSRenderMultiview : OBSRender,
101 this);
102
103 if (source)
104 obs_source_dec_showing(source);
105
106 if (isMultiview) {
107 for (OBSWeakSource &weakSrc : multiviewScenes) {
108 OBSSource src = OBSGetStrongRef(weakSrc);
109 if (src)
110 obs_source_dec_showing(src);
111 }
112
113 obs_enter_graphics();
114 gs_vertexbuffer_destroy(actionSafeMargin);
115 gs_vertexbuffer_destroy(graphicsSafeMargin);
116 gs_vertexbuffer_destroy(fourByThreeSafeMargin);
117 gs_vertexbuffer_destroy(leftLine);
118 gs_vertexbuffer_destroy(topLine);
119 gs_vertexbuffer_destroy(rightLine);
120 obs_leave_graphics();
121 }
122
123 if (type == ProjectorType::Multiview)
124 multiviewProjectors.removeAll(this);
125
126 App()->DecrementSleepInhibition();
127
128 screen = nullptr;
129 }
130
SetMonitor(int monitor)131 void OBSProjector::SetMonitor(int monitor)
132 {
133 savedMonitor = monitor;
134 screen = QGuiApplication::screens()[monitor];
135 setGeometry(screen->geometry());
136 showFullScreen();
137 SetHideCursor();
138 }
139
SetHideCursor()140 void OBSProjector::SetHideCursor()
141 {
142 if (savedMonitor == -1)
143 return;
144
145 bool hideCursor = config_get_bool(GetGlobalConfig(), "BasicWindow",
146 "HideProjectorCursor");
147
148 if (hideCursor && type != ProjectorType::Multiview)
149 setCursor(Qt::BlankCursor);
150 else
151 setCursor(Qt::ArrowCursor);
152 }
153
CreateLabel(const char * name,size_t h)154 static OBSSource CreateLabel(const char *name, size_t h)
155 {
156 obs_data_t *settings = obs_data_create();
157 obs_data_t *font = obs_data_create();
158
159 std::string text;
160 text += " ";
161 text += name;
162 text += " ";
163
164 #if defined(_WIN32)
165 obs_data_set_string(font, "face", "Arial");
166 #elif defined(__APPLE__)
167 obs_data_set_string(font, "face", "Helvetica");
168 #else
169 obs_data_set_string(font, "face", "Monospace");
170 #endif
171 obs_data_set_int(font, "flags", 1); // Bold text
172 obs_data_set_int(font, "size", int(h / 9.81));
173
174 obs_data_set_obj(settings, "font", font);
175 obs_data_set_string(settings, "text", text.c_str());
176 obs_data_set_bool(settings, "outline", false);
177
178 #ifdef _WIN32
179 const char *text_source_id = "text_gdiplus";
180 #else
181 const char *text_source_id = "text_ft2_source";
182 #endif
183
184 OBSSource txtSource =
185 obs_source_create_private(text_source_id, name, settings);
186 obs_source_release(txtSource);
187
188 obs_data_release(font);
189 obs_data_release(settings);
190
191 return txtSource;
192 }
193
labelOffset(obs_source_t * label,uint32_t cx)194 static inline uint32_t labelOffset(obs_source_t *label, uint32_t cx)
195 {
196 uint32_t w = obs_source_get_width(label);
197
198 int n; // Number of scenes per row
199 switch (multiviewLayout) {
200 case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
201 n = 6;
202 break;
203 case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
204 n = 6;
205 break;
206 default:
207 n = 4;
208 break;
209 }
210
211 w = uint32_t(w * ((1.0f) / n));
212 return (cx / 2) - w;
213 }
214
startRegion(int vX,int vY,int vCX,int vCY,float oL,float oR,float oT,float oB)215 static inline void startRegion(int vX, int vY, int vCX, int vCY, float oL,
216 float oR, float oT, float oB)
217 {
218 gs_projection_push();
219 gs_viewport_push();
220 gs_set_viewport(vX, vY, vCX, vCY);
221 gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f);
222 }
223
endRegion()224 static inline void endRegion()
225 {
226 gs_viewport_pop();
227 gs_projection_pop();
228 }
229
OBSRenderMultiview(void * data,uint32_t cx,uint32_t cy)230 void OBSProjector::OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy)
231 {
232 OBSProjector *window = (OBSProjector *)data;
233
234 if (updatingMultiview || !window->ready)
235 return;
236
237 OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window();
238 uint32_t targetCX, targetCY;
239 int x, y;
240 float scale;
241
242 targetCX = (uint32_t)window->fw;
243 targetCY = (uint32_t)window->fh;
244
245 GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
246
247 OBSSource previewSrc = main->GetCurrentSceneSource();
248 OBSSource programSrc = main->GetProgramSource();
249 bool studioMode = main->IsPreviewProgramMode();
250
251 auto drawBox = [&](float cx, float cy, uint32_t colorVal) {
252 gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
253 gs_eparam_t *color =
254 gs_effect_get_param_by_name(solid, "color");
255
256 gs_effect_set_color(color, colorVal);
257 while (gs_effect_loop(solid, "Solid"))
258 gs_draw_sprite(nullptr, 0, (uint32_t)cx, (uint32_t)cy);
259 };
260
261 auto setRegion = [&](float bx, float by, float cx, float cy) {
262 float vX = int(x + bx * scale);
263 float vY = int(y + by * scale);
264 float vCX = int(cx * scale);
265 float vCY = int(cy * scale);
266
267 float oL = bx;
268 float oT = by;
269 float oR = (bx + cx);
270 float oB = (by + cy);
271
272 startRegion(vX, vY, vCX, vCY, oL, oR, oT, oB);
273 };
274
275 auto calcBaseSource = [&](size_t i) {
276 switch (multiviewLayout) {
277 case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
278 window->sourceX = (i % 6) * window->scenesCX;
279 window->sourceY =
280 window->pvwprgCY + (i / 6) * window->scenesCY;
281 break;
282 case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
283 window->sourceX = (i % 6) * window->scenesCX;
284 window->sourceY =
285 window->pvwprgCY + (i / 6) * window->scenesCY;
286 break;
287 case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
288 window->sourceX = window->pvwprgCX;
289 window->sourceY = (i / 2) * window->scenesCY;
290 if (i % 2 != 0)
291 window->sourceX += window->scenesCX;
292 break;
293 case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
294 window->sourceX = 0;
295 window->sourceY = (i / 2) * window->scenesCY;
296 if (i % 2 != 0)
297 window->sourceX = window->scenesCX;
298 break;
299 case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
300 if (i < 4) {
301 window->sourceX = (float(i) * window->scenesCX);
302 window->sourceY = 0;
303 } else {
304 window->sourceX =
305 (float(i - 4) * window->scenesCX);
306 window->sourceY = window->scenesCY;
307 }
308 break;
309 default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES:
310 if (i < 4) {
311 window->sourceX = (float(i) * window->scenesCX);
312 window->sourceY = window->pvwprgCY;
313 } else {
314 window->sourceX =
315 (float(i - 4) * window->scenesCX);
316 window->sourceY =
317 window->pvwprgCY + window->scenesCY;
318 }
319 }
320 window->siX = window->sourceX + window->thickness;
321 window->siY = window->sourceY + window->thickness;
322 };
323
324 auto calcPreviewProgram = [&](bool program) {
325 switch (multiviewLayout) {
326 case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
327 window->sourceX =
328 window->thickness + window->pvwprgCX / 2;
329 window->sourceY = window->thickness;
330 window->labelX = window->offset + window->pvwprgCX / 2;
331 window->labelY = window->pvwprgCY * 0.85f;
332 if (program) {
333 window->sourceX += window->pvwprgCX;
334 window->labelX += window->pvwprgCX;
335 }
336 break;
337 case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
338 window->sourceX = window->thickness;
339 window->sourceY = window->pvwprgCY + window->thickness;
340 window->labelX = window->offset;
341 window->labelY = window->pvwprgCY * 1.85f;
342 if (program) {
343 window->sourceY = window->thickness;
344 window->labelY = window->pvwprgCY * 0.85f;
345 }
346 break;
347 case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
348 window->sourceX = window->pvwprgCX + window->thickness;
349 window->sourceY = window->pvwprgCY + window->thickness;
350 window->labelX = window->pvwprgCX + window->offset;
351 window->labelY = window->pvwprgCY * 1.85f;
352 if (program) {
353 window->sourceY = window->thickness;
354 window->labelY = window->pvwprgCY * 0.85f;
355 }
356 break;
357 case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
358 window->sourceX = window->thickness;
359 window->sourceY = window->pvwprgCY + window->thickness;
360 window->labelX = window->offset;
361 window->labelY = window->pvwprgCY * 1.85f;
362 if (program) {
363 window->sourceX += window->pvwprgCX;
364 window->labelX += window->pvwprgCX;
365 }
366 break;
367 default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES and 18_SCENES
368 window->sourceX = window->thickness;
369 window->sourceY = window->thickness;
370 window->labelX = window->offset;
371 window->labelY = window->pvwprgCY * 0.85f;
372 if (program) {
373 window->sourceX += window->pvwprgCX;
374 window->labelX += window->pvwprgCX;
375 }
376 }
377 };
378
379 auto paintAreaWithColor = [&](float tx, float ty, float cx, float cy,
380 uint32_t color) {
381 gs_matrix_push();
382 gs_matrix_translate3f(tx, ty, 0.0f);
383 drawBox(cx, cy, color);
384 gs_matrix_pop();
385 };
386
387 // Define the whole usable region for the multiview
388 startRegion(x, y, targetCX * scale, targetCY * scale, 0.0f, window->fw,
389 0.0f, window->fh);
390
391 // Change the background color to highlight all sources
392 drawBox(window->fw, window->fh, outerColor);
393
394 /* ----------------------------- */
395 /* draw sources */
396
397 for (size_t i = 0; i < maxSrcs; i++) {
398 // Handle all the offsets
399 calcBaseSource(i);
400
401 if (i >= numSrcs) {
402 // Just paint the background and continue
403 paintAreaWithColor(window->sourceX, window->sourceY,
404 window->scenesCX, window->scenesCY,
405 outerColor);
406 paintAreaWithColor(window->siX, window->siY,
407 window->siCX, window->siCY,
408 backgroundColor);
409 continue;
410 }
411
412 OBSSource src = OBSGetStrongRef(window->multiviewScenes[i]);
413
414 // We have a source. Now chose the proper highlight color
415 uint32_t colorVal = outerColor;
416 if (src == programSrc)
417 colorVal = programColor;
418 else if (src == previewSrc)
419 colorVal = studioMode ? previewColor : programColor;
420
421 // Paint the background
422 paintAreaWithColor(window->sourceX, window->sourceY,
423 window->scenesCX, window->scenesCY,
424 colorVal);
425 paintAreaWithColor(window->siX, window->siY, window->siCX,
426 window->siCY, backgroundColor);
427
428 /* ----------- */
429
430 // Render the source
431 gs_matrix_push();
432 gs_matrix_translate3f(window->siX, window->siY, 0.0f);
433 gs_matrix_scale3f(window->siScaleX, window->siScaleY, 1.0f);
434 setRegion(window->siX, window->siY, window->siCX, window->siCY);
435 obs_source_video_render(src);
436 endRegion();
437 gs_matrix_pop();
438
439 /* ----------- */
440
441 // Render the label
442 if (!drawLabel)
443 continue;
444
445 obs_source *label = window->multiviewLabels[i + 2];
446 if (!label)
447 continue;
448
449 window->offset = labelOffset(label, window->scenesCX);
450
451 gs_matrix_push();
452 gs_matrix_translate3f(
453 window->sourceX + window->offset,
454 (window->scenesCY * 0.85f) + window->sourceY, 0.0f);
455 gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
456 drawBox(obs_source_get_width(label),
457 obs_source_get_height(label) +
458 int(window->sourceY * 0.015f),
459 labelColor);
460 obs_source_video_render(label);
461 gs_matrix_pop();
462 }
463
464 /* ----------------------------- */
465 /* draw preview */
466
467 obs_source_t *previewLabel = window->multiviewLabels[0];
468 window->offset = labelOffset(previewLabel, window->pvwprgCX);
469 calcPreviewProgram(false);
470
471 // Paint the background
472 paintAreaWithColor(window->sourceX, window->sourceY, window->ppiCX,
473 window->ppiCY, backgroundColor);
474
475 // Scale and Draw the preview
476 gs_matrix_push();
477 gs_matrix_translate3f(window->sourceX, window->sourceY, 0.0f);
478 gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
479 setRegion(window->sourceX, window->sourceY, window->ppiCX,
480 window->ppiCY);
481 if (studioMode)
482 obs_source_video_render(previewSrc);
483 else
484 obs_render_main_texture();
485
486 if (drawSafeArea) {
487 RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY);
488 RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY);
489 RenderSafeAreas(window->fourByThreeSafeMargin, targetCX,
490 targetCY);
491 RenderSafeAreas(window->leftLine, targetCX, targetCY);
492 RenderSafeAreas(window->topLine, targetCX, targetCY);
493 RenderSafeAreas(window->rightLine, targetCX, targetCY);
494 }
495
496 endRegion();
497 gs_matrix_pop();
498
499 /* ----------- */
500
501 // Draw the Label
502 if (drawLabel) {
503 gs_matrix_push();
504 gs_matrix_translate3f(window->labelX, window->labelY, 0.0f);
505 gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
506 drawBox(obs_source_get_width(previewLabel),
507 obs_source_get_height(previewLabel) +
508 int(window->pvwprgCX * 0.015f),
509 labelColor);
510 obs_source_video_render(previewLabel);
511 gs_matrix_pop();
512 }
513
514 /* ----------------------------- */
515 /* draw program */
516
517 obs_source_t *programLabel = window->multiviewLabels[1];
518 window->offset = labelOffset(programLabel, window->pvwprgCX);
519 calcPreviewProgram(true);
520
521 paintAreaWithColor(window->sourceX, window->sourceY, window->ppiCX,
522 window->ppiCY, backgroundColor);
523
524 // Scale and Draw the program
525 gs_matrix_push();
526 gs_matrix_translate3f(window->sourceX, window->sourceY, 0.0f);
527 gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
528 setRegion(window->sourceX, window->sourceY, window->ppiCX,
529 window->ppiCY);
530 obs_render_main_texture();
531 endRegion();
532 gs_matrix_pop();
533
534 /* ----------- */
535
536 // Draw the Label
537 if (drawLabel) {
538 gs_matrix_push();
539 gs_matrix_translate3f(window->labelX, window->labelY, 0.0f);
540 gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f);
541 drawBox(obs_source_get_width(programLabel),
542 obs_source_get_height(programLabel) +
543 int(window->pvwprgCX * 0.015f),
544 labelColor);
545 obs_source_video_render(programLabel);
546 gs_matrix_pop();
547 }
548
549 // Region for future usage with additional info.
550 if (multiviewLayout == MultiviewLayout::HORIZONTAL_TOP_24_SCENES) {
551 // Just paint the background for now
552 paintAreaWithColor(window->thickness, window->thickness,
553 window->siCX,
554 window->siCY * 2 + window->thicknessx2,
555 backgroundColor);
556 paintAreaWithColor(
557 window->thickness +
558 2.5 * (window->thicknessx2 + window->ppiCX),
559 window->thickness, window->siCX,
560 window->siCY * 2 + window->thicknessx2,
561 backgroundColor);
562 }
563
564 endRegion();
565 }
566
OBSRender(void * data,uint32_t cx,uint32_t cy)567 void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy)
568 {
569 OBSProjector *window = reinterpret_cast<OBSProjector *>(data);
570
571 if (!window->ready)
572 return;
573
574 OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
575 OBSSource source = window->source;
576
577 uint32_t targetCX;
578 uint32_t targetCY;
579 int x, y;
580 int newCX, newCY;
581 float scale;
582
583 if (source) {
584 targetCX = std::max(obs_source_get_width(source), 1u);
585 targetCY = std::max(obs_source_get_height(source), 1u);
586 } else {
587 struct obs_video_info ovi;
588 obs_get_video_info(&ovi);
589 targetCX = ovi.base_width;
590 targetCY = ovi.base_height;
591 }
592
593 GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
594
595 newCX = int(scale * float(targetCX));
596 newCY = int(scale * float(targetCY));
597
598 startRegion(x, y, newCX, newCY, 0.0f, float(targetCX), 0.0f,
599 float(targetCY));
600
601 if (window->type == ProjectorType::Preview &&
602 main->IsPreviewProgramMode()) {
603 OBSSource curSource = main->GetCurrentSceneSource();
604
605 if (source != curSource) {
606 obs_source_dec_showing(source);
607 obs_source_inc_showing(curSource);
608 source = curSource;
609 window->source = source;
610 }
611 } else if (window->type == ProjectorType::Preview &&
612 !main->IsPreviewProgramMode()) {
613 window->source = nullptr;
614 }
615
616 if (source)
617 obs_source_video_render(source);
618 else
619 obs_render_main_texture();
620
621 endRegion();
622 }
623
OBSSourceRemoved(void * data,calldata_t * params)624 void OBSProjector::OBSSourceRemoved(void *data, calldata_t *params)
625 {
626 OBSProjector *window = reinterpret_cast<OBSProjector *>(data);
627
628 window->deleteLater();
629
630 UNUSED_PARAMETER(params);
631 }
632
getSourceByPosition(int x,int y,float ratio)633 static int getSourceByPosition(int x, int y, float ratio)
634 {
635 int pos = -1;
636 QWidget *rec = QApplication::activeWindow();
637 if (!rec)
638 return pos;
639 int cx = rec->width();
640 int cy = rec->height();
641 int minX = 0;
642 int minY = 0;
643 int maxX = cx;
644 int maxY = cy;
645
646 switch (multiviewLayout) {
647 case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
648 if (float(cx) / float(cy) > ratio) {
649 int validX = cy * ratio;
650 minX = (cx / 2) - (validX / 2);
651 maxX = (cx / 2) + (validX / 2);
652 } else {
653 int validY = cx / ratio;
654 maxY = (cy / 2) + (validY / 2);
655 }
656 minY = cy / 2;
657
658 if (x < minX || x > maxX || y < minY || y > maxY)
659 break;
660
661 pos = (x - minX) / ((maxX - minX) / 6);
662 pos += ((y - minY) / ((maxY - minY) / 3)) * 6;
663
664 break;
665 case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
666 if (float(cx) / float(cy) > ratio) {
667 int validX = cy * ratio;
668 minX = (cx / 2) - (validX / 2);
669 maxX = (cx / 2) + (validX / 2);
670 minY = cy / 3;
671 } else {
672 int validY = cx / ratio;
673 maxY = (cy / 2) + (validY / 2);
674 minY = (cy / 2) - (validY / 6);
675 }
676
677 if (x < minX || x > maxX || y < minY || y > maxY)
678 break;
679
680 pos = (x - minX) / ((maxX - minX) / 6);
681 pos += ((y - minY) / ((maxY - minY) / 4)) * 6;
682
683 break;
684 case MultiviewLayout::VERTICAL_LEFT_8_SCENES:
685 if (float(cx) / float(cy) > ratio) {
686 int validX = cy * ratio;
687 maxX = (cx / 2) + (validX / 2);
688 } else {
689 int validY = cx / ratio;
690 minY = (cy / 2) - (validY / 2);
691 maxY = (cy / 2) + (validY / 2);
692 }
693
694 minX = cx / 2;
695
696 if (x < minX || x > maxX || y < minY || y > maxY)
697 break;
698
699 pos = 2 * ((y - minY) / ((maxY - minY) / 4));
700 if (x > minX + ((maxX - minX) / 2))
701 pos++;
702 break;
703 case MultiviewLayout::VERTICAL_RIGHT_8_SCENES:
704 if (float(cx) / float(cy) > ratio) {
705 int validX = cy * ratio;
706 minX = (cx / 2) - (validX / 2);
707 } else {
708 int validY = cx / ratio;
709 minY = (cy / 2) - (validY / 2);
710 maxY = (cy / 2) + (validY / 2);
711 }
712
713 maxX = (cx / 2);
714
715 if (x < minX || x > maxX || y < minY || y > maxY)
716 break;
717
718 pos = 2 * ((y - minY) / ((maxY - minY) / 4));
719 if (x > minX + ((maxX - minX) / 2))
720 pos++;
721 break;
722 case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES:
723 if (float(cx) / float(cy) > ratio) {
724 int validX = cy * ratio;
725 minX = (cx / 2) - (validX / 2);
726 maxX = (cx / 2) + (validX / 2);
727 } else {
728 int validY = cx / ratio;
729 minY = (cy / 2) - (validY / 2);
730 }
731
732 maxY = (cy / 2);
733
734 if (x < minX || x > maxX || y < minY || y > maxY)
735 break;
736
737 pos = (x - minX) / ((maxX - minX) / 4);
738 if (y > minY + ((maxY - minY) / 2))
739 pos += 4;
740 break;
741 default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES
742 if (float(cx) / float(cy) > ratio) {
743 int validX = cy * ratio;
744 minX = (cx / 2) - (validX / 2);
745 maxX = (cx / 2) + (validX / 2);
746 } else {
747 int validY = cx / ratio;
748 maxY = (cy / 2) + (validY / 2);
749 }
750
751 minY = (cy / 2);
752
753 if (x < minX || x > maxX || y < minY || y > maxY)
754 break;
755
756 pos = (x - minX) / ((maxX - minX) / 4);
757 if (y > minY + ((maxY - minY) / 2))
758 pos += 4;
759 }
760
761 return pos;
762 }
763
mouseDoubleClickEvent(QMouseEvent * event)764 void OBSProjector::mouseDoubleClickEvent(QMouseEvent *event)
765 {
766 OBSQTDisplay::mouseDoubleClickEvent(event);
767
768 if (!mouseSwitching)
769 return;
770
771 if (!transitionOnDoubleClick)
772 return;
773
774 OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window();
775 if (!main->IsPreviewProgramMode())
776 return;
777
778 if (event->button() == Qt::LeftButton) {
779 int pos = getSourceByPosition(event->x(), event->y(), ratio);
780 if (pos < 0 || pos >= (int)numSrcs)
781 return;
782 OBSSource src = OBSGetStrongRef(multiviewScenes[pos]);
783 if (!src)
784 return;
785
786 if (main->GetProgramSource() != src)
787 main->TransitionToScene(src);
788 }
789 }
790
mousePressEvent(QMouseEvent * event)791 void OBSProjector::mousePressEvent(QMouseEvent *event)
792 {
793 OBSQTDisplay::mousePressEvent(event);
794
795 if (event->button() == Qt::RightButton) {
796 OBSBasic *main =
797 reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
798 QMenu popup(this);
799
800 QMenu *projectorMenu = new QMenu(QTStr("Fullscreen"));
801 main->AddProjectorMenuMonitors(projectorMenu, this,
802 SLOT(OpenFullScreenProjector()));
803 popup.addMenu(projectorMenu);
804
805 if (GetMonitor() > -1) {
806 popup.addAction(QTStr("Windowed"), this,
807 SLOT(OpenWindowedProjector()));
808
809 } else if (!this->isMaximized()) {
810 popup.addAction(QTStr("ResizeProjectorWindowToContent"),
811 this, SLOT(ResizeToContent()));
812 }
813
814 QAction *alwaysOnTopButton =
815 new QAction(QTStr("Basic.MainMenu.AlwaysOnTop"), this);
816 alwaysOnTopButton->setCheckable(true);
817 alwaysOnTopButton->setChecked(isAlwaysOnTop);
818
819 connect(alwaysOnTopButton, &QAction::toggled, this,
820 &OBSProjector::AlwaysOnTopToggled);
821
822 popup.addAction(alwaysOnTopButton);
823
824 popup.addAction(QTStr("Close"), this, SLOT(EscapeTriggered()));
825 popup.exec(QCursor::pos());
826 }
827
828 if (!mouseSwitching)
829 return;
830
831 if (event->button() == Qt::LeftButton) {
832 int pos = getSourceByPosition(event->x(), event->y(), ratio);
833 if (pos < 0 || pos >= (int)numSrcs)
834 return;
835 OBSSource src = OBSGetStrongRef(multiviewScenes[pos]);
836 if (!src)
837 return;
838
839 OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window();
840 if (main->GetCurrentSceneSource() != src)
841 main->SetCurrentScene(src, false);
842 }
843 }
844
EscapeTriggered()845 void OBSProjector::EscapeTriggered()
846 {
847 OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
848 main->DeleteProjector(this);
849
850 allProjectors.removeAll(this);
851 }
852
UpdateMultiview()853 void OBSProjector::UpdateMultiview()
854 {
855 multiviewScenes.clear();
856 multiviewLabels.clear();
857
858 struct obs_video_info ovi;
859 obs_get_video_info(&ovi);
860
861 uint32_t w = ovi.base_width;
862 uint32_t h = ovi.base_height;
863 fw = float(w);
864 fh = float(h);
865 ratio = fw / fh;
866
867 struct obs_frontend_source_list scenes = {};
868 obs_frontend_get_scenes(&scenes);
869
870 multiviewLabels.emplace_back(
871 CreateLabel(Str("StudioMode.Preview"), h / 2));
872 multiviewLabels.emplace_back(
873 CreateLabel(Str("StudioMode.Program"), h / 2));
874
875 multiviewLayout = static_cast<MultiviewLayout>(config_get_int(
876 GetGlobalConfig(), "BasicWindow", "MultiviewLayout"));
877
878 drawLabel = config_get_bool(GetGlobalConfig(), "BasicWindow",
879 "MultiviewDrawNames");
880
881 drawSafeArea = config_get_bool(GetGlobalConfig(), "BasicWindow",
882 "MultiviewDrawAreas");
883
884 mouseSwitching = config_get_bool(GetGlobalConfig(), "BasicWindow",
885 "MultiviewMouseSwitch");
886
887 transitionOnDoubleClick = config_get_bool(
888 GetGlobalConfig(), "BasicWindow", "TransitionOnDoubleClick");
889
890 switch (multiviewLayout) {
891 case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
892 pvwprgCX = fw / 2;
893 pvwprgCY = fh / 2;
894
895 maxSrcs = 18;
896 break;
897 case MultiviewLayout::HORIZONTAL_TOP_24_SCENES:
898 pvwprgCX = fw / 3;
899 pvwprgCY = fh / 3;
900
901 maxSrcs = 24;
902 break;
903 default:
904 pvwprgCX = fw / 2;
905 pvwprgCY = fh / 2;
906
907 maxSrcs = 8;
908 }
909
910 ppiCX = pvwprgCX - thicknessx2;
911 ppiCY = pvwprgCY - thicknessx2;
912 ppiScaleX = (pvwprgCX - thicknessx2) / fw;
913 ppiScaleY = (pvwprgCY - thicknessx2) / fh;
914
915 switch (multiviewLayout) {
916 case MultiviewLayout::HORIZONTAL_TOP_18_SCENES:
917 scenesCX = pvwprgCX / 3;
918 scenesCY = pvwprgCY / 3;
919 break;
920 default:
921 scenesCX = pvwprgCX / 2;
922 scenesCY = pvwprgCY / 2;
923 }
924
925 siCX = scenesCX - thicknessx2;
926 siCY = scenesCY - thicknessx2;
927 siScaleX = (scenesCX - thicknessx2) / fw;
928 siScaleY = (scenesCY - thicknessx2) / fh;
929
930 numSrcs = 0;
931 size_t i = 0;
932 while (i < scenes.sources.num && numSrcs < maxSrcs) {
933 obs_source_t *src = scenes.sources.array[i++];
934 OBSData data = obs_source_get_private_settings(src);
935 obs_data_release(data);
936
937 obs_data_set_default_bool(data, "show_in_multiview", true);
938 if (!obs_data_get_bool(data, "show_in_multiview"))
939 continue;
940
941 // We have a displayable source.
942 numSrcs++;
943
944 multiviewScenes.emplace_back(OBSGetWeakRef(src));
945 obs_source_inc_showing(src);
946
947 std::string name = std::to_string(numSrcs) + " - " +
948 obs_source_get_name(src);
949 multiviewLabels.emplace_back(CreateLabel(name.c_str(), h / 3));
950 }
951
952 obs_frontend_source_list_free(&scenes);
953 }
954
UpdateProjectorTitle(QString name)955 void OBSProjector::UpdateProjectorTitle(QString name)
956 {
957 bool window = (GetMonitor() == -1);
958
959 QString title = nullptr;
960 switch (type) {
961 case ProjectorType::Scene:
962 if (!window)
963 title = QTStr("SceneProjector") + " - " + name;
964 else
965 title = QTStr("SceneWindow") + " - " + name;
966 break;
967 case ProjectorType::Source:
968 if (!window)
969 title = QTStr("SourceProjector") + " - " + name;
970 else
971 title = QTStr("SourceWindow") + " - " + name;
972 break;
973 case ProjectorType::Preview:
974 if (!window)
975 title = QTStr("PreviewProjector");
976 else
977 title = QTStr("PreviewWindow");
978 break;
979 case ProjectorType::StudioProgram:
980 if (!window)
981 title = QTStr("StudioProgramProjector");
982 else
983 title = QTStr("StudioProgramWindow");
984 break;
985 case ProjectorType::Multiview:
986 if (!window)
987 title = QTStr("MultiviewProjector");
988 else
989 title = QTStr("MultiviewWindowed");
990 break;
991 default:
992 title = name;
993 break;
994 }
995
996 setWindowTitle(title);
997 }
998
GetSource()999 OBSSource OBSProjector::GetSource()
1000 {
1001 return source;
1002 }
1003
GetProjectorType()1004 ProjectorType OBSProjector::GetProjectorType()
1005 {
1006 return type;
1007 }
1008
GetMonitor()1009 int OBSProjector::GetMonitor()
1010 {
1011 return savedMonitor;
1012 }
1013
UpdateMultiviewProjectors()1014 void OBSProjector::UpdateMultiviewProjectors()
1015 {
1016 obs_enter_graphics();
1017 updatingMultiview = true;
1018 obs_leave_graphics();
1019
1020 for (auto &projector : multiviewProjectors)
1021 projector->UpdateMultiview();
1022
1023 obs_enter_graphics();
1024 updatingMultiview = false;
1025 obs_leave_graphics();
1026 }
1027
RenameProjector(QString oldName,QString newName)1028 void OBSProjector::RenameProjector(QString oldName, QString newName)
1029 {
1030 if (oldName == newName)
1031 return;
1032
1033 UpdateProjectorTitle(newName);
1034 }
1035
OpenFullScreenProjector()1036 void OBSProjector::OpenFullScreenProjector()
1037 {
1038 if (!isFullScreen())
1039 prevGeometry = geometry();
1040
1041 int monitor = sender()->property("monitor").toInt();
1042 SetMonitor(monitor);
1043
1044 UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
1045 }
1046
OpenWindowedProjector()1047 void OBSProjector::OpenWindowedProjector()
1048 {
1049 showFullScreen();
1050 showNormal();
1051 setCursor(Qt::ArrowCursor);
1052
1053 if (!prevGeometry.isNull())
1054 setGeometry(prevGeometry);
1055 else
1056 resize(480, 270);
1057
1058 savedMonitor = -1;
1059
1060 UpdateProjectorTitle(QT_UTF8(obs_source_get_name(source)));
1061 screen = nullptr;
1062 }
1063
ResizeToContent()1064 void OBSProjector::ResizeToContent()
1065 {
1066 OBSSource source = GetSource();
1067 uint32_t targetCX;
1068 uint32_t targetCY;
1069 int x, y, newX, newY;
1070 float scale;
1071
1072 if (source) {
1073 targetCX = std::max(obs_source_get_width(source), 1u);
1074 targetCY = std::max(obs_source_get_height(source), 1u);
1075 } else {
1076 struct obs_video_info ovi;
1077 obs_get_video_info(&ovi);
1078 targetCX = ovi.base_width;
1079 targetCY = ovi.base_height;
1080 }
1081
1082 QSize size = this->size();
1083 GetScaleAndCenterPos(targetCX, targetCY, size.width(), size.height(), x,
1084 y, scale);
1085
1086 newX = size.width() - (x * 2);
1087 newY = size.height() - (y * 2);
1088 resize(newX, newY);
1089 }
1090
AlwaysOnTopToggled(bool isAlwaysOnTop)1091 void OBSProjector::AlwaysOnTopToggled(bool isAlwaysOnTop)
1092 {
1093 SetIsAlwaysOnTop(isAlwaysOnTop, true);
1094 }
1095
closeEvent(QCloseEvent * event)1096 void OBSProjector::closeEvent(QCloseEvent *event)
1097 {
1098 EscapeTriggered();
1099 event->accept();
1100 }
1101
IsAlwaysOnTop() const1102 bool OBSProjector::IsAlwaysOnTop() const
1103 {
1104 return isAlwaysOnTop;
1105 }
1106
IsAlwaysOnTopOverridden() const1107 bool OBSProjector::IsAlwaysOnTopOverridden() const
1108 {
1109 return isAlwaysOnTopOverridden;
1110 }
1111
SetIsAlwaysOnTop(bool isAlwaysOnTop,bool isOverridden)1112 void OBSProjector::SetIsAlwaysOnTop(bool isAlwaysOnTop, bool isOverridden)
1113 {
1114 this->isAlwaysOnTop = isAlwaysOnTop;
1115 this->isAlwaysOnTopOverridden = isOverridden;
1116
1117 SetAlwaysOnTop(this, isAlwaysOnTop);
1118 }
1119
ScreenRemoved(QScreen * screen_)1120 void OBSProjector::ScreenRemoved(QScreen *screen_)
1121 {
1122 if (GetMonitor() < 0 || !screen)
1123 return;
1124
1125 if (screen == screen_)
1126 EscapeTriggered();
1127 }
1128