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