1 /******************************************************************************
2     Copyright (C) 2013-2015 by Hugh Bailey <obs.jim@gmail.com>
3                                Zachary Lund <admin@computerquip.com>
4                                Philippe Groarke <philippe.groarke@gmail.com>
5 
6     This program is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 2 of the License, or
9     (at your option) any later version.
10 
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 ******************************************************************************/
19 #include "ui-config.h"
20 
21 #include <cstddef>
22 #include <ctime>
23 #include <functional>
24 #include <obs-data.h>
25 #include <obs.h>
26 #include <obs.hpp>
27 #include <QGuiApplication>
28 #include <QMessageBox>
29 #include <QShowEvent>
30 #include <QDesktopServices>
31 #include <QFileDialog>
32 #include <QScreen>
33 #include <QColorDialog>
34 #include <QSizePolicy>
35 #include <QScrollBar>
36 #include <QTextStream>
37 
38 #include <util/dstr.h>
39 #include <util/util.hpp>
40 #include <util/platform.h>
41 #include <util/profiler.hpp>
42 #include <util/dstr.hpp>
43 
44 #include "obs-app.hpp"
45 #include "platform.hpp"
46 #include "visibility-item-widget.hpp"
47 #include "item-widget-helpers.hpp"
48 #include "window-basic-settings.hpp"
49 #include "window-namedialog.hpp"
50 #include "window-basic-auto-config.hpp"
51 #include "window-basic-source-select.hpp"
52 #include "window-basic-main.hpp"
53 #include "window-basic-stats.hpp"
54 #include "window-basic-main-outputs.hpp"
55 #include "window-log-reply.hpp"
56 #include "window-projector.hpp"
57 #include "window-remux.hpp"
58 #if YOUTUBE_ENABLED
59 #include "auth-youtube.hpp"
60 #include "window-youtube-actions.hpp"
61 #include "youtube-api-wrappers.hpp"
62 #endif
63 #include "qt-wrappers.hpp"
64 #include "context-bar-controls.hpp"
65 #include "obs-proxy-style.hpp"
66 #include "display-helpers.hpp"
67 #include "volume-control.hpp"
68 #include "remote-text.hpp"
69 #include "ui-validation.hpp"
70 #include "media-controls.hpp"
71 #include "undo-stack-obs.hpp"
72 #include <fstream>
73 #include <sstream>
74 
75 #ifdef _WIN32
76 #include "win-update/win-update.hpp"
77 #include "windows.h"
78 #endif
79 
80 #include "ui_OBSBasic.h"
81 #include "ui_ColorSelect.h"
82 
83 #include <QWindow>
84 
85 #include <json11.hpp>
86 
87 #ifdef ENABLE_WAYLAND
88 #include <obs-nix-platform.h>
89 #endif
90 
91 using namespace json11;
92 using namespace std;
93 
94 #ifdef BROWSER_AVAILABLE
95 #include <browser-panel.hpp>
96 #endif
97 
98 #include "ui-config.h"
99 
100 struct QCef;
101 struct QCefCookieManager;
102 
103 QCef *cef = nullptr;
104 QCefCookieManager *panel_cookies = nullptr;
105 
106 void DestroyPanelCookieManager();
107 
108 namespace {
109 
110 template<typename OBSRef> struct SignalContainer {
111 	OBSRef ref;
112 	vector<shared_ptr<OBSSignal>> handlers;
113 };
114 }
115 
116 extern volatile long insideEventLoop;
117 
118 Q_DECLARE_METATYPE(OBSScene);
119 Q_DECLARE_METATYPE(OBSSceneItem);
120 Q_DECLARE_METATYPE(OBSSource);
121 Q_DECLARE_METATYPE(obs_order_movement);
122 Q_DECLARE_METATYPE(SignalContainer<OBSScene>);
123 
operator <<(QDataStream & out,const SignalContainer<OBSScene> & v)124 QDataStream &operator<<(QDataStream &out, const SignalContainer<OBSScene> &v)
125 {
126 	out << v.ref;
127 	return out;
128 }
129 
operator >>(QDataStream & in,SignalContainer<OBSScene> & v)130 QDataStream &operator>>(QDataStream &in, SignalContainer<OBSScene> &v)
131 {
132 	in >> v.ref;
133 	return in;
134 }
135 
GetOBSRef(QListWidgetItem * item)136 template<typename T> static T GetOBSRef(QListWidgetItem *item)
137 {
138 	return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
139 }
140 
SetOBSRef(QListWidgetItem * item,T && val)141 template<typename T> static void SetOBSRef(QListWidgetItem *item, T &&val)
142 {
143 	item->setData(static_cast<int>(QtDataRole::OBSRef),
144 		      QVariant::fromValue(val));
145 }
146 
AddExtraModulePaths()147 static void AddExtraModulePaths()
148 {
149 	char *plugins_path = getenv("OBS_PLUGINS_PATH");
150 	char *plugins_data_path = getenv("OBS_PLUGINS_DATA_PATH");
151 	if (plugins_path && plugins_data_path) {
152 		string data_path_with_module_suffix;
153 		data_path_with_module_suffix += plugins_data_path;
154 		data_path_with_module_suffix += "/%module%";
155 		obs_add_module_path(plugins_path,
156 				    data_path_with_module_suffix.c_str());
157 	}
158 
159 	char base_module_dir[512];
160 #if defined(_WIN32) || defined(__APPLE__)
161 	int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir),
162 				     "obs-studio/plugins/%module%");
163 #else
164 	int ret = GetConfigPath(base_module_dir, sizeof(base_module_dir),
165 				"obs-studio/plugins/%module%");
166 #endif
167 
168 	if (ret <= 0)
169 		return;
170 
171 	string path = base_module_dir;
172 #if defined(__APPLE__)
173 	obs_add_module_path((path + "/bin").c_str(), (path + "/data").c_str());
174 
175 	BPtr<char> config_bin =
176 		os_get_config_path_ptr("obs-studio/plugins/%module%/bin");
177 	BPtr<char> config_data =
178 		os_get_config_path_ptr("obs-studio/plugins/%module%/data");
179 	obs_add_module_path(config_bin, config_data);
180 
181 #elif ARCH_BITS == 64
182 	obs_add_module_path((path + "/bin/64bit").c_str(),
183 			    (path + "/data").c_str());
184 #else
185 	obs_add_module_path((path + "/bin/32bit").c_str(),
186 			    (path + "/data").c_str());
187 #endif
188 }
189 
190 extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main);
191 
assignDockToggle(QDockWidget * dock,QAction * action)192 void assignDockToggle(QDockWidget *dock, QAction *action)
193 {
194 	auto handleWindowToggle = [action](bool vis) {
195 		action->blockSignals(true);
196 		action->setChecked(vis);
197 		action->blockSignals(false);
198 	};
199 	auto handleMenuToggle = [dock](bool check) {
200 		dock->blockSignals(true);
201 		dock->setVisible(check);
202 		dock->blockSignals(false);
203 	};
204 
205 	dock->connect(dock->toggleViewAction(), &QAction::toggled,
206 		      handleWindowToggle);
207 	dock->connect(action, &QAction::toggled, handleMenuToggle);
208 }
209 
210 extern void RegisterTwitchAuth();
211 extern void RegisterRestreamAuth();
212 #if YOUTUBE_ENABLED
213 extern void RegisterYoutubeAuth();
214 #endif
215 
OBSBasic(QWidget * parent)216 OBSBasic::OBSBasic(QWidget *parent)
217 	: OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic)
218 {
219 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
220 	qRegisterMetaTypeStreamOperators<SignalContainer<OBSScene>>(
221 		"SignalContainer<OBSScene>");
222 #endif
223 
224 	setAttribute(Qt::WA_NativeWindow);
225 
226 #if TWITCH_ENABLED
227 	RegisterTwitchAuth();
228 #endif
229 #if RESTREAM_ENABLED
230 	RegisterRestreamAuth();
231 #endif
232 #if YOUTUBE_ENABLED
233 	RegisterYoutubeAuth();
234 #endif
235 
236 	setAcceptDrops(true);
237 
238 	setContextMenuPolicy(Qt::CustomContextMenu);
239 	connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this,
240 		SLOT(on_customContextMenuRequested(const QPoint &)));
241 
242 	api = InitializeAPIInterface(this);
243 
244 	ui->setupUi(this);
245 	ui->previewDisabledWidget->setVisible(false);
246 	ui->contextContainer->setStyle(new OBSProxyStyle);
247 	ui->broadcastButton->setVisible(false);
248 
249 	startingDockLayout = saveState();
250 
251 	statsDock = new OBSDock();
252 	statsDock->setObjectName(QStringLiteral("statsDock"));
253 	statsDock->setFeatures(QDockWidget::DockWidgetClosable |
254 			       QDockWidget::DockWidgetMovable |
255 			       QDockWidget::DockWidgetFloatable);
256 	statsDock->setWindowTitle(QTStr("Basic.Stats"));
257 	addDockWidget(Qt::BottomDockWidgetArea, statsDock);
258 	statsDock->setVisible(false);
259 	statsDock->setFloating(true);
260 	statsDock->resize(700, 200);
261 
262 	copyActionsDynamicProperties();
263 
264 	char styleSheetPath[512];
265 	int ret = GetProfilePath(styleSheetPath, sizeof(styleSheetPath),
266 				 "stylesheet.qss");
267 	if (ret > 0) {
268 		if (QFile::exists(styleSheetPath)) {
269 			QString path =
270 				QString("file:///") + QT_UTF8(styleSheetPath);
271 			App()->setStyleSheet(path);
272 		}
273 	}
274 
275 	qRegisterMetaType<int64_t>("int64_t");
276 	qRegisterMetaType<uint32_t>("uint32_t");
277 	qRegisterMetaType<OBSScene>("OBSScene");
278 	qRegisterMetaType<OBSSceneItem>("OBSSceneItem");
279 	qRegisterMetaType<OBSSource>("OBSSource");
280 	qRegisterMetaType<obs_hotkey_id>("obs_hotkey_id");
281 	qRegisterMetaType<SavedProjectorInfo *>("SavedProjectorInfo *");
282 
283 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
284 	qRegisterMetaTypeStreamOperators<std::vector<std::shared_ptr<OBSSignal>>>(
285 		"std::vector<std::shared_ptr<OBSSignal>>");
286 	qRegisterMetaTypeStreamOperators<OBSScene>("OBSScene");
287 	qRegisterMetaTypeStreamOperators<OBSSceneItem>("OBSSceneItem");
288 #endif
289 
290 	ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false);
291 	ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false);
292 
293 	bool sceneGrid = config_get_bool(App()->GlobalConfig(), "BasicWindow",
294 					 "gridMode");
295 	ui->scenes->SetGridMode(sceneGrid);
296 
297 	ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes));
298 
299 	auto displayResize = [this]() {
300 		struct obs_video_info ovi;
301 
302 		if (obs_get_video_info(&ovi))
303 			ResizePreview(ovi.base_width, ovi.base_height);
304 	};
305 
306 	connect(windowHandle(), &QWindow::screenChanged, displayResize);
307 	connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize);
308 
309 	delete shortcutFilter;
310 	shortcutFilter = CreateShortcutFilter();
311 	installEventFilter(shortcutFilter);
312 
313 	stringstream name;
314 	name << "OBS " << App()->GetVersionString();
315 	blog(LOG_INFO, "%s", name.str().c_str());
316 	blog(LOG_INFO, "---------------------------------");
317 
318 	UpdateTitleBar();
319 
320 	connect(ui->scenes->itemDelegate(),
321 		SIGNAL(closeEditor(QWidget *,
322 				   QAbstractItemDelegate::EndEditHint)),
323 		this,
324 		SLOT(SceneNameEdited(QWidget *,
325 				     QAbstractItemDelegate::EndEditHint)));
326 
327 	cpuUsageInfo = os_cpu_usage_info_start();
328 	cpuUsageTimer = new QTimer(this);
329 	connect(cpuUsageTimer.data(), SIGNAL(timeout()), ui->statusbar,
330 		SLOT(UpdateCPUUsage()));
331 	cpuUsageTimer->start(3000);
332 
333 	diskFullTimer = new QTimer(this);
334 	connect(diskFullTimer, SIGNAL(timeout()), this,
335 		SLOT(CheckDiskSpaceRemaining()));
336 
337 	renameScene = new QAction(ui->scenesDock);
338 	renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut);
339 	connect(renameScene, SIGNAL(triggered()), this, SLOT(EditSceneName()));
340 	ui->scenesDock->addAction(renameScene);
341 
342 	renameSource = new QAction(ui->sourcesDock);
343 	renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut);
344 	connect(renameSource, SIGNAL(triggered()), this,
345 		SLOT(EditSceneItemName()));
346 	ui->sourcesDock->addAction(renameSource);
347 
348 #ifdef __APPLE__
349 	renameScene->setShortcut({Qt::Key_Return});
350 	renameSource->setShortcut({Qt::Key_Return});
351 
352 	ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace});
353 	ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace});
354 
355 	ui->action_Settings->setMenuRole(QAction::PreferencesRole);
356 	ui->actionE_xit->setMenuRole(QAction::QuitRole);
357 #else
358 	renameScene->setShortcut({Qt::Key_F2});
359 	renameSource->setShortcut({Qt::Key_F2});
360 #endif
361 
362 #ifdef __linux__
363 	ui->actionE_xit->setShortcut(Qt::CTRL + Qt::Key_Q);
364 #endif
365 
366 	auto addNudge = [this](const QKeySequence &seq, const char *s) {
367 		QAction *nudge = new QAction(ui->preview);
368 		nudge->setShortcut(seq);
369 		nudge->setShortcutContext(Qt::WidgetShortcut);
370 		ui->preview->addAction(nudge);
371 		connect(nudge, SIGNAL(triggered()), this, s);
372 	};
373 
374 	addNudge(Qt::Key_Up, SLOT(NudgeUp()));
375 	addNudge(Qt::Key_Down, SLOT(NudgeDown()));
376 	addNudge(Qt::Key_Left, SLOT(NudgeLeft()));
377 	addNudge(Qt::Key_Right, SLOT(NudgeRight()));
378 
379 	assignDockToggle(ui->scenesDock, ui->toggleScenes);
380 	assignDockToggle(ui->sourcesDock, ui->toggleSources);
381 	assignDockToggle(ui->mixerDock, ui->toggleMixer);
382 	assignDockToggle(ui->transitionsDock, ui->toggleTransitions);
383 	assignDockToggle(ui->controlsDock, ui->toggleControls);
384 	assignDockToggle(statsDock, ui->toggleStats);
385 
386 	// Register shortcuts for Undo/Redo
387 	ui->actionMainUndo->setShortcut(Qt::CTRL + Qt::Key_Z);
388 	QList<QKeySequence> shrt;
389 	shrt << QKeySequence(Qt::CTRL | Qt::SHIFT + Qt::Key_Z)
390 	     << QKeySequence(Qt::CTRL + Qt::Key_Y);
391 	ui->actionMainRedo->setShortcuts(shrt);
392 
393 	ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut);
394 	ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut);
395 
396 	//hide all docking panes
397 	ui->toggleScenes->setChecked(false);
398 	ui->toggleSources->setChecked(false);
399 	ui->toggleMixer->setChecked(false);
400 	ui->toggleTransitions->setChecked(false);
401 	ui->toggleControls->setChecked(false);
402 	ui->toggleStats->setChecked(false);
403 
404 	QPoint curPos;
405 
406 	//restore parent window geometry
407 	const char *geometry = config_get_string(App()->GlobalConfig(),
408 						 "BasicWindow", "geometry");
409 	if (geometry != NULL) {
410 		QByteArray byteArray =
411 			QByteArray::fromBase64(QByteArray(geometry));
412 		restoreGeometry(byteArray);
413 
414 		QRect windowGeometry = normalGeometry();
415 		if (!WindowPositionValid(windowGeometry)) {
416 			QRect rect =
417 				QGuiApplication::primaryScreen()->geometry();
418 			setGeometry(QStyle::alignedRect(Qt::LeftToRight,
419 							Qt::AlignCenter, size(),
420 							rect));
421 		}
422 
423 		curPos = pos();
424 	} else {
425 		QRect desktopRect =
426 			QGuiApplication::primaryScreen()->geometry();
427 		QSize adjSize = desktopRect.size() / 2 - size() / 2;
428 		curPos = QPoint(adjSize.width(), adjSize.height());
429 	}
430 
431 	QPoint curSize(width(), height());
432 
433 	QPoint statsDockSize(statsDock->width(), statsDock->height());
434 	QPoint statsDockPos = curSize / 2 - statsDockSize / 2;
435 	QPoint newPos = curPos + statsDockPos;
436 	statsDock->move(newPos);
437 
438 	ui->previewLabel->setProperty("themeID", "previewProgramLabels");
439 	ui->previewLabel->style()->polish(ui->previewLabel);
440 
441 	bool labels = config_get_bool(GetGlobalConfig(), "BasicWindow",
442 				      "StudioModeLabels");
443 
444 	if (!previewProgramMode)
445 		ui->previewLabel->setHidden(true);
446 	else
447 		ui->previewLabel->setHidden(!labels);
448 
449 	ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu);
450 	connect(ui->previewDisabledWidget,
451 		SIGNAL(customContextMenuRequested(const QPoint &)), this,
452 		SLOT(PreviewDisabledMenu(const QPoint &)));
453 	connect(ui->enablePreviewButton, SIGNAL(clicked()), this,
454 		SLOT(TogglePreview()));
455 
456 	connect(ui->scenes, SIGNAL(scenesReordered()), this,
457 		SLOT(ScenesReordered()));
458 
459 	connect(ui->broadcastButton, &QPushButton::clicked, this,
460 		&OBSBasic::BroadcastButtonClicked);
461 
462 	UpdatePreviewSafeAreas();
463 }
464 
SaveAudioDevice(const char * name,int channel,obs_data_t * parent,vector<OBSSource> & audioSources)465 static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent,
466 			    vector<OBSSource> &audioSources)
467 {
468 	obs_source_t *source = obs_get_output_source(channel);
469 	if (!source)
470 		return;
471 
472 	audioSources.push_back(source);
473 
474 	obs_data_t *data = obs_save_source(source);
475 
476 	obs_data_set_obj(parent, name, data);
477 
478 	obs_data_release(data);
479 	obs_source_release(source);
480 }
481 
GenerateSaveData(obs_data_array_t * sceneOrder,obs_data_array_t * quickTransitionData,int transitionDuration,obs_data_array_t * transitions,OBSScene & scene,OBSSource & curProgramScene,obs_data_array_t * savedProjectorList)482 static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder,
483 				    obs_data_array_t *quickTransitionData,
484 				    int transitionDuration,
485 				    obs_data_array_t *transitions,
486 				    OBSScene &scene, OBSSource &curProgramScene,
487 				    obs_data_array_t *savedProjectorList)
488 {
489 	obs_data_t *saveData = obs_data_create();
490 
491 	vector<OBSSource> audioSources;
492 	audioSources.reserve(6);
493 
494 	SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources);
495 	SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources);
496 	SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources);
497 	SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources);
498 	SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources);
499 	SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources);
500 
501 	/* -------------------------------- */
502 	/* save non-group sources           */
503 
504 	auto FilterAudioSources = [&](obs_source_t *source) {
505 		if (obs_source_is_group(source))
506 			return false;
507 
508 		return find(begin(audioSources), end(audioSources), source) ==
509 		       end(audioSources);
510 	};
511 	using FilterAudioSources_t = decltype(FilterAudioSources);
512 
513 	obs_data_array_t *sourcesArray = obs_save_sources_filtered(
514 		[](void *data, obs_source_t *source) {
515 			return (*static_cast<FilterAudioSources_t *>(data))(
516 				source);
517 		},
518 		static_cast<void *>(&FilterAudioSources));
519 
520 	/* -------------------------------- */
521 	/* save group sources separately    */
522 
523 	/* saving separately ensures they won't be loaded in older versions */
524 	obs_data_array_t *groupsArray = obs_save_sources_filtered(
525 		[](void *, obs_source_t *source) {
526 			return obs_source_is_group(source);
527 		},
528 		nullptr);
529 
530 	/* -------------------------------- */
531 
532 	obs_source_t *transition = obs_get_output_source(0);
533 	obs_source_t *currentScene = obs_scene_get_source(scene);
534 	const char *sceneName = obs_source_get_name(currentScene);
535 	const char *programName = obs_source_get_name(curProgramScene);
536 
537 	const char *sceneCollection = config_get_string(
538 		App()->GlobalConfig(), "Basic", "SceneCollection");
539 
540 	obs_data_set_string(saveData, "current_scene", sceneName);
541 	obs_data_set_string(saveData, "current_program_scene", programName);
542 	obs_data_set_array(saveData, "scene_order", sceneOrder);
543 	obs_data_set_string(saveData, "name", sceneCollection);
544 	obs_data_set_array(saveData, "sources", sourcesArray);
545 	obs_data_set_array(saveData, "groups", groupsArray);
546 	obs_data_set_array(saveData, "quick_transitions", quickTransitionData);
547 	obs_data_set_array(saveData, "transitions", transitions);
548 	obs_data_set_array(saveData, "saved_projectors", savedProjectorList);
549 	obs_data_array_release(sourcesArray);
550 	obs_data_array_release(groupsArray);
551 
552 	obs_data_set_string(saveData, "current_transition",
553 			    obs_source_get_name(transition));
554 	obs_data_set_int(saveData, "transition_duration", transitionDuration);
555 	obs_source_release(transition);
556 
557 	return saveData;
558 }
559 
copyActionsDynamicProperties()560 void OBSBasic::copyActionsDynamicProperties()
561 {
562 	// Themes need the QAction dynamic properties
563 	for (QAction *x : ui->scenesToolbar->actions()) {
564 		QWidget *temp = ui->scenesToolbar->widgetForAction(x);
565 
566 		for (QByteArray &y : x->dynamicPropertyNames()) {
567 			temp->setProperty(y, x->property(y));
568 		}
569 	}
570 
571 	for (QAction *x : ui->sourcesToolbar->actions()) {
572 		QWidget *temp = ui->sourcesToolbar->widgetForAction(x);
573 
574 		for (QByteArray &y : x->dynamicPropertyNames()) {
575 			temp->setProperty(y, x->property(y));
576 		}
577 	}
578 }
579 
UpdateVolumeControlsDecayRate()580 void OBSBasic::UpdateVolumeControlsDecayRate()
581 {
582 	double meterDecayRate =
583 		config_get_double(basicConfig, "Audio", "MeterDecayRate");
584 
585 	for (size_t i = 0; i < volumes.size(); i++) {
586 		volumes[i]->SetMeterDecayRate(meterDecayRate);
587 	}
588 }
589 
UpdateVolumeControlsPeakMeterType()590 void OBSBasic::UpdateVolumeControlsPeakMeterType()
591 {
592 	uint32_t peakMeterTypeIdx =
593 		config_get_uint(basicConfig, "Audio", "PeakMeterType");
594 
595 	enum obs_peak_meter_type peakMeterType;
596 	switch (peakMeterTypeIdx) {
597 	case 0:
598 		peakMeterType = SAMPLE_PEAK_METER;
599 		break;
600 	case 1:
601 		peakMeterType = TRUE_PEAK_METER;
602 		break;
603 	default:
604 		peakMeterType = SAMPLE_PEAK_METER;
605 		break;
606 	}
607 
608 	for (size_t i = 0; i < volumes.size(); i++) {
609 		volumes[i]->setPeakMeterType(peakMeterType);
610 	}
611 }
612 
ClearVolumeControls()613 void OBSBasic::ClearVolumeControls()
614 {
615 	for (VolControl *vol : volumes)
616 		delete vol;
617 
618 	volumes.clear();
619 }
620 
SaveSceneListOrder()621 obs_data_array_t *OBSBasic::SaveSceneListOrder()
622 {
623 	obs_data_array_t *sceneOrder = obs_data_array_create();
624 
625 	for (int i = 0; i < ui->scenes->count(); i++) {
626 		obs_data_t *data = obs_data_create();
627 		obs_data_set_string(data, "name",
628 				    QT_TO_UTF8(ui->scenes->item(i)->text()));
629 		obs_data_array_push_back(sceneOrder, data);
630 		obs_data_release(data);
631 	}
632 
633 	return sceneOrder;
634 }
635 
SaveProjectors()636 obs_data_array_t *OBSBasic::SaveProjectors()
637 {
638 	obs_data_array_t *savedProjectors = obs_data_array_create();
639 
640 	auto saveProjector = [savedProjectors](OBSProjector *projector) {
641 		if (!projector)
642 			return;
643 
644 		obs_data_t *data = obs_data_create();
645 		ProjectorType type = projector->GetProjectorType();
646 
647 		switch (type) {
648 		case ProjectorType::Scene:
649 		case ProjectorType::Source: {
650 			obs_source_t *source = projector->GetSource();
651 			const char *name = obs_source_get_name(source);
652 			obs_data_set_string(data, "name", name);
653 			break;
654 		}
655 		default:
656 			break;
657 		}
658 
659 		obs_data_set_int(data, "monitor", projector->GetMonitor());
660 		obs_data_set_int(data, "type", static_cast<int>(type));
661 		obs_data_set_string(
662 			data, "geometry",
663 			projector->saveGeometry().toBase64().constData());
664 
665 		if (projector->IsAlwaysOnTopOverridden())
666 			obs_data_set_bool(data, "alwaysOnTop",
667 					  projector->IsAlwaysOnTop());
668 
669 		obs_data_set_bool(data, "alwaysOnTopOverridden",
670 				  projector->IsAlwaysOnTopOverridden());
671 
672 		obs_data_array_push_back(savedProjectors, data);
673 		obs_data_release(data);
674 	};
675 
676 	for (size_t i = 0; i < projectors.size(); i++)
677 		saveProjector(static_cast<OBSProjector *>(projectors[i]));
678 
679 	return savedProjectors;
680 }
681 
Save(const char * file)682 void OBSBasic::Save(const char *file)
683 {
684 	OBSScene scene = GetCurrentScene();
685 	OBSSource curProgramScene = OBSGetStrongRef(programScene);
686 	if (!curProgramScene)
687 		curProgramScene = obs_scene_get_source(scene);
688 
689 	obs_data_array_t *sceneOrder = SaveSceneListOrder();
690 	obs_data_array_t *transitions = SaveTransitions();
691 	obs_data_array_t *quickTrData = SaveQuickTransitions();
692 	obs_data_array_t *savedProjectorList = SaveProjectors();
693 	obs_data_t *saveData = GenerateSaveData(
694 		sceneOrder, quickTrData, ui->transitionDuration->value(),
695 		transitions, scene, curProgramScene, savedProjectorList);
696 
697 	obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
698 	obs_data_set_bool(saveData, "scaling_enabled",
699 			  ui->preview->IsFixedScaling());
700 	obs_data_set_int(saveData, "scaling_level",
701 			 ui->preview->GetScalingLevel());
702 	obs_data_set_double(saveData, "scaling_off_x",
703 			    ui->preview->GetScrollX());
704 	obs_data_set_double(saveData, "scaling_off_y",
705 			    ui->preview->GetScrollY());
706 
707 	if (api) {
708 		obs_data_t *moduleObj = obs_data_create();
709 		api->on_save(moduleObj);
710 		obs_data_set_obj(saveData, "modules", moduleObj);
711 		obs_data_release(moduleObj);
712 	}
713 
714 	if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
715 		blog(LOG_ERROR, "Could not save scene data to %s", file);
716 
717 	obs_data_release(saveData);
718 	obs_data_array_release(sceneOrder);
719 	obs_data_array_release(quickTrData);
720 	obs_data_array_release(transitions);
721 	obs_data_array_release(savedProjectorList);
722 }
723 
DeferSaveBegin()724 void OBSBasic::DeferSaveBegin()
725 {
726 	os_atomic_inc_long(&disableSaving);
727 }
728 
DeferSaveEnd()729 void OBSBasic::DeferSaveEnd()
730 {
731 	long result = os_atomic_dec_long(&disableSaving);
732 	if (result == 0) {
733 		SaveProject();
734 	}
735 }
736 
737 static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val);
738 
LoadAudioDevice(const char * name,int channel,obs_data_t * parent)739 static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent)
740 {
741 	obs_data_t *data = obs_data_get_obj(parent, name);
742 	if (!data)
743 		return;
744 
745 	obs_source_t *source = obs_load_source(data);
746 	if (source) {
747 		obs_set_output_source(channel, source);
748 
749 		const char *name = obs_source_get_name(source);
750 		blog(LOG_INFO, "[Loaded global audio device]: '%s'", name);
751 		obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1);
752 		obs_monitoring_type monitoring_type =
753 			obs_source_get_monitoring_type(source);
754 		if (monitoring_type != OBS_MONITORING_TYPE_NONE) {
755 			const char *type = (monitoring_type ==
756 					    OBS_MONITORING_TYPE_MONITOR_ONLY)
757 						   ? "monitor only"
758 						   : "monitor and output";
759 
760 			blog(LOG_INFO, "    - monitoring: %s", type);
761 		}
762 		obs_source_release(source);
763 	}
764 
765 	obs_data_release(data);
766 }
767 
HasAudioDevices(const char * source_id)768 static inline bool HasAudioDevices(const char *source_id)
769 {
770 	const char *output_id = source_id;
771 	obs_properties_t *props = obs_get_source_properties(output_id);
772 	size_t count = 0;
773 
774 	if (!props)
775 		return false;
776 
777 	obs_property_t *devices = obs_properties_get(props, "device_id");
778 	if (devices)
779 		count = obs_property_list_item_count(devices);
780 
781 	obs_properties_destroy(props);
782 
783 	return count != 0;
784 }
785 
CreateFirstRunSources()786 void OBSBasic::CreateFirstRunSources()
787 {
788 	bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource());
789 	bool hasInputAudio = HasAudioDevices(App()->InputAudioSource());
790 
791 	if (hasDesktopAudio)
792 		ResetAudioDevice(App()->OutputAudioSource(), "default",
793 				 Str("Basic.DesktopDevice1"), 1);
794 	if (hasInputAudio)
795 		ResetAudioDevice(App()->InputAudioSource(), "default",
796 				 Str("Basic.AuxDevice1"), 3);
797 }
798 
CreateDefaultScene(bool firstStart)799 void OBSBasic::CreateDefaultScene(bool firstStart)
800 {
801 	disableSaving++;
802 
803 	ClearSceneData();
804 	InitDefaultTransitions();
805 	CreateDefaultQuickTransitions();
806 	ui->transitionDuration->setValue(300);
807 	SetTransition(fadeTransition);
808 
809 	obs_scene_t *scene = obs_scene_create(Str("Basic.Scene"));
810 
811 	if (firstStart)
812 		CreateFirstRunSources();
813 
814 	SetCurrentScene(scene, true);
815 	obs_scene_release(scene);
816 
817 	disableSaving--;
818 }
819 
ReorderItemByName(QListWidget * lw,const char * name,int newIndex)820 static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex)
821 {
822 	for (int i = 0; i < lw->count(); i++) {
823 		QListWidgetItem *item = lw->item(i);
824 
825 		if (strcmp(name, QT_TO_UTF8(item->text())) == 0) {
826 			if (newIndex != i) {
827 				item = lw->takeItem(i);
828 				lw->insertItem(newIndex, item);
829 			}
830 			break;
831 		}
832 	}
833 }
834 
LoadSceneListOrder(obs_data_array_t * array)835 void OBSBasic::LoadSceneListOrder(obs_data_array_t *array)
836 {
837 	size_t num = obs_data_array_count(array);
838 
839 	for (size_t i = 0; i < num; i++) {
840 		obs_data_t *data = obs_data_array_item(array, i);
841 		const char *name = obs_data_get_string(data, "name");
842 
843 		ReorderItemByName(ui->scenes, name, (int)i);
844 
845 		obs_data_release(data);
846 	}
847 }
848 
LoadSavedProjectors(obs_data_array_t * array)849 void OBSBasic::LoadSavedProjectors(obs_data_array_t *array)
850 {
851 	for (SavedProjectorInfo *info : savedProjectorsArray) {
852 		delete info;
853 	}
854 	savedProjectorsArray.clear();
855 
856 	size_t num = obs_data_array_count(array);
857 
858 	for (size_t i = 0; i < num; i++) {
859 		obs_data_t *data = obs_data_array_item(array, i);
860 
861 		SavedProjectorInfo *info = new SavedProjectorInfo();
862 		info->monitor = obs_data_get_int(data, "monitor");
863 		info->type = static_cast<ProjectorType>(
864 			obs_data_get_int(data, "type"));
865 		info->geometry =
866 			std::string(obs_data_get_string(data, "geometry"));
867 		info->name = std::string(obs_data_get_string(data, "name"));
868 		info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop");
869 		info->alwaysOnTopOverridden =
870 			obs_data_get_bool(data, "alwaysOnTopOverridden");
871 
872 		savedProjectorsArray.emplace_back(info);
873 
874 		obs_data_release(data);
875 	}
876 }
877 
LogFilter(obs_source_t *,obs_source_t * filter,void * v_val)878 static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val)
879 {
880 	const char *name = obs_source_get_name(filter);
881 	const char *id = obs_source_get_id(filter);
882 	int val = (int)(intptr_t)v_val;
883 	string indent;
884 
885 	for (int i = 0; i < val; i++)
886 		indent += "    ";
887 
888 	blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id);
889 }
890 
LogSceneItem(obs_scene_t *,obs_sceneitem_t * item,void * v_val)891 static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val)
892 {
893 	obs_source_t *source = obs_sceneitem_get_source(item);
894 	const char *name = obs_source_get_name(source);
895 	const char *id = obs_source_get_id(source);
896 	int indent_count = (int)(intptr_t)v_val;
897 	string indent;
898 
899 	for (int i = 0; i < indent_count; i++)
900 		indent += "    ";
901 
902 	blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id);
903 
904 	obs_monitoring_type monitoring_type =
905 		obs_source_get_monitoring_type(source);
906 
907 	if (monitoring_type != OBS_MONITORING_TYPE_NONE) {
908 		const char *type =
909 			(monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY)
910 				? "monitor only"
911 				: "monitor and output";
912 
913 		blog(LOG_INFO, "    %s- monitoring: %s", indent.c_str(), type);
914 	}
915 	int child_indent = 1 + indent_count;
916 	obs_source_enum_filters(source, LogFilter,
917 				(void *)(intptr_t)child_indent);
918 
919 	obs_source_t *show_tn = obs_sceneitem_get_show_transition(item);
920 	obs_source_t *hide_tn = obs_sceneitem_get_hide_transition(item);
921 	if (show_tn)
922 		blog(LOG_INFO, "    %s- show: '%s' (%s)", indent.c_str(),
923 		     obs_source_get_name(show_tn), obs_source_get_id(show_tn));
924 	if (hide_tn)
925 		blog(LOG_INFO, "    %s- hide: '%s' (%s)", indent.c_str(),
926 		     obs_source_get_name(hide_tn), obs_source_get_id(hide_tn));
927 
928 	if (obs_sceneitem_is_group(item))
929 		obs_sceneitem_group_enum_items(item, LogSceneItem,
930 					       (void *)(intptr_t)child_indent);
931 	return true;
932 }
933 
LogScenes()934 void OBSBasic::LogScenes()
935 {
936 	blog(LOG_INFO, "------------------------------------------------");
937 	blog(LOG_INFO, "Loaded scenes:");
938 
939 	for (int i = 0; i < ui->scenes->count(); i++) {
940 		QListWidgetItem *item = ui->scenes->item(i);
941 		OBSScene scene = GetOBSRef<OBSScene>(item);
942 
943 		obs_source_t *source = obs_scene_get_source(scene);
944 		const char *name = obs_source_get_name(source);
945 
946 		blog(LOG_INFO, "- scene '%s':", name);
947 		obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1);
948 		obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1);
949 	}
950 
951 	blog(LOG_INFO, "------------------------------------------------");
952 }
953 
Load(const char * file)954 void OBSBasic::Load(const char *file)
955 {
956 	disableSaving++;
957 
958 	obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak");
959 	if (!data) {
960 		disableSaving--;
961 		blog(LOG_INFO, "No scene file found, creating default scene");
962 		CreateDefaultScene(true);
963 		SaveProject();
964 		return;
965 	}
966 
967 	LoadData(data, file);
968 }
969 
LoadData(obs_data_t * data,const char * file)970 void OBSBasic::LoadData(obs_data_t *data, const char *file)
971 {
972 	ClearSceneData();
973 	InitDefaultTransitions();
974 	ClearContextBar();
975 
976 	if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
977 		devicePropertiesThread->wait();
978 		devicePropertiesThread.reset();
979 	}
980 
981 	QApplication::sendPostedEvents(this);
982 
983 	obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
984 	if (api)
985 		api->on_preload(modulesObj);
986 
987 	obs_data_array_t *sceneOrder = obs_data_get_array(data, "scene_order");
988 	obs_data_array_t *sources = obs_data_get_array(data, "sources");
989 	obs_data_array_t *groups = obs_data_get_array(data, "groups");
990 	obs_data_array_t *transitions = obs_data_get_array(data, "transitions");
991 	const char *sceneName = obs_data_get_string(data, "current_scene");
992 	const char *programSceneName =
993 		obs_data_get_string(data, "current_program_scene");
994 	const char *transitionName =
995 		obs_data_get_string(data, "current_transition");
996 
997 	if (!opt_starting_scene.empty()) {
998 		programSceneName = opt_starting_scene.c_str();
999 		if (!IsPreviewProgramMode())
1000 			sceneName = opt_starting_scene.c_str();
1001 	}
1002 
1003 	int newDuration = obs_data_get_int(data, "transition_duration");
1004 	if (!newDuration)
1005 		newDuration = 300;
1006 
1007 	if (!transitionName)
1008 		transitionName = obs_source_get_name(fadeTransition);
1009 
1010 	const char *curSceneCollection = config_get_string(
1011 		App()->GlobalConfig(), "Basic", "SceneCollection");
1012 
1013 	obs_data_set_default_string(data, "name", curSceneCollection);
1014 
1015 	const char *name = obs_data_get_string(data, "name");
1016 	obs_source_t *curScene;
1017 	obs_source_t *curProgramScene;
1018 	obs_source_t *curTransition;
1019 
1020 	if (!name || !*name)
1021 		name = curSceneCollection;
1022 
1023 	LoadAudioDevice(DESKTOP_AUDIO_1, 1, data);
1024 	LoadAudioDevice(DESKTOP_AUDIO_2, 2, data);
1025 	LoadAudioDevice(AUX_AUDIO_1, 3, data);
1026 	LoadAudioDevice(AUX_AUDIO_2, 4, data);
1027 	LoadAudioDevice(AUX_AUDIO_3, 5, data);
1028 	LoadAudioDevice(AUX_AUDIO_4, 6, data);
1029 
1030 	if (!sources) {
1031 		sources = groups;
1032 		groups = nullptr;
1033 	} else {
1034 		obs_data_array_push_back_array(sources, groups);
1035 	}
1036 
1037 	obs_missing_files_t *files = obs_missing_files_create();
1038 
1039 	auto cb = [](void *private_data, obs_source_t *source) {
1040 		obs_missing_files_t *f = (obs_missing_files_t *)private_data;
1041 		obs_missing_files_t *sf = obs_source_get_missing_files(source);
1042 
1043 		obs_missing_files_append(f, sf);
1044 		obs_missing_files_destroy(sf);
1045 
1046 		UNUSED_PARAMETER(source);
1047 	};
1048 
1049 	obs_load_sources(sources, cb, files);
1050 
1051 	if (transitions)
1052 		LoadTransitions(transitions, cb, files);
1053 	if (sceneOrder)
1054 		LoadSceneListOrder(sceneOrder);
1055 
1056 	obs_data_array_release(transitions);
1057 
1058 	curTransition = FindTransition(transitionName);
1059 	if (!curTransition)
1060 		curTransition = fadeTransition;
1061 
1062 	ui->transitionDuration->setValue(newDuration);
1063 	SetTransition(curTransition);
1064 
1065 retryScene:
1066 	curScene = obs_get_source_by_name(sceneName);
1067 	curProgramScene = obs_get_source_by_name(programSceneName);
1068 
1069 	/* if the starting scene command line parameter is bad at all,
1070 	 * fall back to original settings */
1071 	if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) {
1072 		sceneName = obs_data_get_string(data, "current_scene");
1073 		programSceneName =
1074 			obs_data_get_string(data, "current_program_scene");
1075 		obs_source_release(curScene);
1076 		obs_source_release(curProgramScene);
1077 		opt_starting_scene.clear();
1078 		goto retryScene;
1079 	}
1080 
1081 	if (!curProgramScene) {
1082 		curProgramScene = curScene;
1083 		obs_source_addref(curScene);
1084 	}
1085 
1086 	SetCurrentScene(curScene, true);
1087 	if (IsPreviewProgramMode())
1088 		TransitionToScene(curProgramScene, true);
1089 	obs_source_release(curScene);
1090 	obs_source_release(curProgramScene);
1091 
1092 	obs_data_array_release(sources);
1093 	obs_data_array_release(groups);
1094 	obs_data_array_release(sceneOrder);
1095 
1096 	/* ------------------- */
1097 
1098 	bool projectorSave = config_get_bool(GetGlobalConfig(), "BasicWindow",
1099 					     "SaveProjectors");
1100 
1101 	if (projectorSave) {
1102 		obs_data_array_t *savedProjectors =
1103 			obs_data_get_array(data, "saved_projectors");
1104 
1105 		if (savedProjectors) {
1106 			LoadSavedProjectors(savedProjectors);
1107 			OpenSavedProjectors();
1108 			activateWindow();
1109 		}
1110 
1111 		obs_data_array_release(savedProjectors);
1112 	}
1113 
1114 	/* ------------------- */
1115 
1116 	std::string file_base = strrchr(file, '/') + 1;
1117 	file_base.erase(file_base.size() - 5, 5);
1118 
1119 	config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection",
1120 			  name);
1121 	config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile",
1122 			  file_base.c_str());
1123 
1124 	obs_data_array_t *quickTransitionData =
1125 		obs_data_get_array(data, "quick_transitions");
1126 	LoadQuickTransitions(quickTransitionData);
1127 	obs_data_array_release(quickTransitionData);
1128 
1129 	RefreshQuickTransitions();
1130 
1131 	bool previewLocked = obs_data_get_bool(data, "preview_locked");
1132 	ui->preview->SetLocked(previewLocked);
1133 	ui->actionLockPreview->setChecked(previewLocked);
1134 
1135 	/* ---------------------- */
1136 
1137 	bool fixedScaling = obs_data_get_bool(data, "scaling_enabled");
1138 	int scalingLevel = (int)obs_data_get_int(data, "scaling_level");
1139 	float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x");
1140 	float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y");
1141 
1142 	if (fixedScaling) {
1143 		ui->preview->SetScalingLevel(scalingLevel);
1144 		ui->preview->SetScrollingOffset(scrollOffX, scrollOffY);
1145 	}
1146 	ui->preview->SetFixedScaling(fixedScaling);
1147 	emit ui->preview->DisplayResized();
1148 
1149 	/* ---------------------- */
1150 
1151 	if (api)
1152 		api->on_load(modulesObj);
1153 
1154 	obs_data_release(modulesObj);
1155 	obs_data_release(data);
1156 
1157 	if (!opt_starting_scene.empty())
1158 		opt_starting_scene.clear();
1159 
1160 	if (opt_start_streaming) {
1161 		blog(LOG_INFO, "Starting stream due to command line parameter");
1162 		QMetaObject::invokeMethod(this, "StartStreaming",
1163 					  Qt::QueuedConnection);
1164 		opt_start_streaming = false;
1165 	}
1166 
1167 	if (opt_start_recording) {
1168 		blog(LOG_INFO,
1169 		     "Starting recording due to command line parameter");
1170 		QMetaObject::invokeMethod(this, "StartRecording",
1171 					  Qt::QueuedConnection);
1172 		opt_start_recording = false;
1173 	}
1174 
1175 	if (opt_start_replaybuffer) {
1176 		QMetaObject::invokeMethod(this, "StartReplayBuffer",
1177 					  Qt::QueuedConnection);
1178 		opt_start_replaybuffer = false;
1179 	}
1180 
1181 	if (opt_start_virtualcam) {
1182 		QMetaObject::invokeMethod(this, "StartVirtualCam",
1183 					  Qt::QueuedConnection);
1184 		opt_start_virtualcam = false;
1185 	}
1186 
1187 	LogScenes();
1188 
1189 	if (obs_missing_files_count(files) > 0 &&
1190 	    !App()->IsMissingFilesCheckDisabled()) {
1191 		/* the window hasn't fully initialized by this point on macOS,
1192 		 * so put this at the end of the current task queue. Fixes a
1193 		 * bug where the window be behind OBS on startup */
1194 		QTimer::singleShot(0, [this, files] {
1195 			missDialog = new OBSMissingFiles(files, this);
1196 			missDialog->setAttribute(Qt::WA_DeleteOnClose, true);
1197 			missDialog->show();
1198 			missDialog->raise();
1199 		});
1200 	} else {
1201 		obs_missing_files_destroy(files);
1202 	}
1203 
1204 	disableSaving--;
1205 
1206 	if (api) {
1207 		api->on_event(OBS_FRONTEND_EVENT_SCENE_CHANGED);
1208 		api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED);
1209 	}
1210 }
1211 
1212 #define SERVICE_PATH "service.json"
1213 
SaveService()1214 void OBSBasic::SaveService()
1215 {
1216 	if (!service)
1217 		return;
1218 
1219 	char serviceJsonPath[512];
1220 	int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath),
1221 				 SERVICE_PATH);
1222 	if (ret <= 0)
1223 		return;
1224 
1225 	obs_data_t *data = obs_data_create();
1226 	obs_data_t *settings = obs_service_get_settings(service);
1227 
1228 	obs_data_set_string(data, "type", obs_service_get_type(service));
1229 	obs_data_set_obj(data, "settings", settings);
1230 
1231 	if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak"))
1232 		blog(LOG_WARNING, "Failed to save service");
1233 
1234 	obs_data_release(settings);
1235 	obs_data_release(data);
1236 }
1237 
LoadService()1238 bool OBSBasic::LoadService()
1239 {
1240 	const char *type;
1241 
1242 	char serviceJsonPath[512];
1243 	int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath),
1244 				 SERVICE_PATH);
1245 	if (ret <= 0)
1246 		return false;
1247 
1248 	obs_data_t *data =
1249 		obs_data_create_from_json_file_safe(serviceJsonPath, "bak");
1250 
1251 	if (!data)
1252 		return false;
1253 
1254 	obs_data_set_default_string(data, "type", "rtmp_common");
1255 	type = obs_data_get_string(data, "type");
1256 
1257 	obs_data_t *settings = obs_data_get_obj(data, "settings");
1258 	obs_data_t *hotkey_data = obs_data_get_obj(data, "hotkeys");
1259 
1260 	service = obs_service_create(type, "default_service", settings,
1261 				     hotkey_data);
1262 	obs_service_release(service);
1263 
1264 	obs_data_release(hotkey_data);
1265 	obs_data_release(settings);
1266 	obs_data_release(data);
1267 
1268 	return !!service;
1269 }
1270 
InitService()1271 bool OBSBasic::InitService()
1272 {
1273 	ProfileScope("OBSBasic::InitService");
1274 
1275 	if (LoadService())
1276 		return true;
1277 
1278 	service = obs_service_create("rtmp_common", "default_service", nullptr,
1279 				     nullptr);
1280 	if (!service)
1281 		return false;
1282 	obs_service_release(service);
1283 
1284 	return true;
1285 }
1286 
1287 static const double scaled_vals[] = {1.0,         1.25, (1.0 / 0.75), 1.5,
1288 				     (1.0 / 0.6), 1.75, 2.0,          2.25,
1289 				     2.5,         2.75, 3.0,          0.0};
1290 
1291 extern void CheckExistingCookieId();
1292 
InitBasicConfigDefaults()1293 bool OBSBasic::InitBasicConfigDefaults()
1294 {
1295 	QList<QScreen *> screens = QGuiApplication::screens();
1296 
1297 	if (!screens.size()) {
1298 		OBSErrorBox(NULL, "There appears to be no monitors.  Er, this "
1299 				  "technically shouldn't be possible.");
1300 		return false;
1301 	}
1302 
1303 	QScreen *primaryScreen = QGuiApplication::primaryScreen();
1304 
1305 	uint32_t cx = primaryScreen->size().width();
1306 	uint32_t cy = primaryScreen->size().height();
1307 
1308 	cx *= devicePixelRatioF();
1309 	cy *= devicePixelRatioF();
1310 
1311 	bool oldResolutionDefaults = config_get_bool(
1312 		App()->GlobalConfig(), "General", "Pre19Defaults");
1313 
1314 	/* use 1920x1080 for new default base res if main monitor is above
1315 	 * 1920x1080, but don't apply for people from older builds -- only to
1316 	 * new users */
1317 	if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) {
1318 		cx = 1920;
1319 		cy = 1080;
1320 	}
1321 
1322 	bool changed = false;
1323 
1324 	/* ----------------------------------------------------- */
1325 	/* move over old FFmpeg track settings                   */
1326 	if (config_has_user_value(basicConfig, "AdvOut", "FFAudioTrack") &&
1327 	    !config_has_user_value(basicConfig, "AdvOut", "Pre22.1Settings")) {
1328 
1329 		int track = (int)config_get_int(basicConfig, "AdvOut",
1330 						"FFAudioTrack");
1331 		config_set_int(basicConfig, "AdvOut", "FFAudioMixes",
1332 			       1LL << (track - 1));
1333 		config_set_bool(basicConfig, "AdvOut", "Pre22.1Settings", true);
1334 		changed = true;
1335 	}
1336 
1337 	/* ----------------------------------------------------- */
1338 	/* move over mixer values in advanced if older config */
1339 	if (config_has_user_value(basicConfig, "AdvOut", "RecTrackIndex") &&
1340 	    !config_has_user_value(basicConfig, "AdvOut", "RecTracks")) {
1341 
1342 		uint64_t track =
1343 			config_get_uint(basicConfig, "AdvOut", "RecTrackIndex");
1344 		track = 1ULL << (track - 1);
1345 		config_set_uint(basicConfig, "AdvOut", "RecTracks", track);
1346 		config_remove_value(basicConfig, "AdvOut", "RecTrackIndex");
1347 		changed = true;
1348 	}
1349 
1350 	/* ----------------------------------------------------- */
1351 	/* set twitch chat extensions to "both" if prev version  */
1352 	/* is under 24.1                                         */
1353 	if (config_get_bool(GetGlobalConfig(), "General", "Pre24.1Defaults") &&
1354 	    !config_has_user_value(basicConfig, "Twitch", "AddonChoice")) {
1355 		config_set_int(basicConfig, "Twitch", "AddonChoice", 3);
1356 		changed = true;
1357 	}
1358 
1359 	/* ----------------------------------------------------- */
1360 	/* move bitrate enforcement setting to new value         */
1361 	if (config_has_user_value(basicConfig, "SimpleOutput",
1362 				  "EnforceBitrate") &&
1363 	    !config_has_user_value(basicConfig, "Stream1",
1364 				   "IgnoreRecommended") &&
1365 	    !config_has_user_value(basicConfig, "Stream1", "MovedOldEnforce")) {
1366 		bool enforce = config_get_bool(basicConfig, "SimpleOutput",
1367 					       "EnforceBitrate");
1368 		config_set_bool(basicConfig, "Stream1", "IgnoreRecommended",
1369 				!enforce);
1370 		config_set_bool(basicConfig, "Stream1", "MovedOldEnforce",
1371 				true);
1372 		changed = true;
1373 	}
1374 
1375 	/* ----------------------------------------------------- */
1376 	/* enforce minimum retry delay of 1 second prior to 27.1 */
1377 	if (config_has_user_value(basicConfig, "Output", "RetryDelay")) {
1378 		int retryDelay =
1379 			config_get_uint(basicConfig, "Output", "RetryDelay");
1380 		if (retryDelay < 1) {
1381 			config_set_uint(basicConfig, "Output", "RetryDelay", 1);
1382 			changed = true;
1383 		}
1384 	}
1385 
1386 	/* ----------------------------------------------------- */
1387 
1388 	if (changed)
1389 		config_save_safe(basicConfig, "tmp", nullptr);
1390 
1391 	/* ----------------------------------------------------- */
1392 
1393 	config_set_default_string(basicConfig, "Output", "Mode", "Simple");
1394 
1395 	config_set_default_bool(basicConfig, "Stream1", "IgnoreRecommended",
1396 				false);
1397 
1398 	config_set_default_string(basicConfig, "SimpleOutput", "FilePath",
1399 				  GetDefaultVideoSavePath().c_str());
1400 	config_set_default_string(basicConfig, "SimpleOutput", "RecFormat",
1401 				  "mkv");
1402 	config_set_default_uint(basicConfig, "SimpleOutput", "VBitrate", 2500);
1403 	config_set_default_uint(basicConfig, "SimpleOutput", "ABitrate", 160);
1404 	config_set_default_bool(basicConfig, "SimpleOutput", "UseAdvanced",
1405 				false);
1406 	config_set_default_string(basicConfig, "SimpleOutput", "Preset",
1407 				  "veryfast");
1408 	config_set_default_string(basicConfig, "SimpleOutput", "NVENCPreset",
1409 				  "hq");
1410 	config_set_default_string(basicConfig, "SimpleOutput", "RecQuality",
1411 				  "Stream");
1412 	config_set_default_bool(basicConfig, "SimpleOutput", "RecRB", false);
1413 	config_set_default_int(basicConfig, "SimpleOutput", "RecRBTime", 20);
1414 	config_set_default_int(basicConfig, "SimpleOutput", "RecRBSize", 512);
1415 	config_set_default_string(basicConfig, "SimpleOutput", "RecRBPrefix",
1416 				  "Replay");
1417 
1418 	config_set_default_bool(basicConfig, "AdvOut", "ApplyServiceSettings",
1419 				true);
1420 	config_set_default_bool(basicConfig, "AdvOut", "UseRescale", false);
1421 	config_set_default_uint(basicConfig, "AdvOut", "TrackIndex", 1);
1422 	config_set_default_uint(basicConfig, "AdvOut", "VodTrackIndex", 2);
1423 	config_set_default_string(basicConfig, "AdvOut", "Encoder", "obs_x264");
1424 
1425 	config_set_default_string(basicConfig, "AdvOut", "RecType", "Standard");
1426 
1427 	config_set_default_string(basicConfig, "AdvOut", "RecFilePath",
1428 				  GetDefaultVideoSavePath().c_str());
1429 	config_set_default_string(basicConfig, "AdvOut", "RecFormat", "mkv");
1430 	config_set_default_bool(basicConfig, "AdvOut", "RecUseRescale", false);
1431 	config_set_default_uint(basicConfig, "AdvOut", "RecTracks", (1 << 0));
1432 	config_set_default_string(basicConfig, "AdvOut", "RecEncoder", "none");
1433 	config_set_default_uint(basicConfig, "AdvOut", "FLVTrack", 1);
1434 
1435 	config_set_default_bool(basicConfig, "AdvOut", "FFOutputToFile", true);
1436 	config_set_default_string(basicConfig, "AdvOut", "FFFilePath",
1437 				  GetDefaultVideoSavePath().c_str());
1438 	config_set_default_string(basicConfig, "AdvOut", "FFExtension", "mp4");
1439 	config_set_default_uint(basicConfig, "AdvOut", "FFVBitrate", 2500);
1440 	config_set_default_uint(basicConfig, "AdvOut", "FFVGOPSize", 250);
1441 	config_set_default_bool(basicConfig, "AdvOut", "FFUseRescale", false);
1442 	config_set_default_bool(basicConfig, "AdvOut", "FFIgnoreCompat", false);
1443 	config_set_default_uint(basicConfig, "AdvOut", "FFABitrate", 160);
1444 	config_set_default_uint(basicConfig, "AdvOut", "FFAudioMixes", 1);
1445 
1446 	config_set_default_uint(basicConfig, "AdvOut", "Track1Bitrate", 160);
1447 	config_set_default_uint(basicConfig, "AdvOut", "Track2Bitrate", 160);
1448 	config_set_default_uint(basicConfig, "AdvOut", "Track3Bitrate", 160);
1449 	config_set_default_uint(basicConfig, "AdvOut", "Track4Bitrate", 160);
1450 	config_set_default_uint(basicConfig, "AdvOut", "Track5Bitrate", 160);
1451 	config_set_default_uint(basicConfig, "AdvOut", "Track6Bitrate", 160);
1452 
1453 	config_set_default_bool(basicConfig, "AdvOut", "RecRB", false);
1454 	config_set_default_uint(basicConfig, "AdvOut", "RecRBTime", 20);
1455 	config_set_default_int(basicConfig, "AdvOut", "RecRBSize", 512);
1456 
1457 	config_set_default_uint(basicConfig, "Video", "BaseCX", cx);
1458 	config_set_default_uint(basicConfig, "Video", "BaseCY", cy);
1459 
1460 	/* don't allow BaseCX/BaseCY to be susceptible to defaults changing */
1461 	if (!config_has_user_value(basicConfig, "Video", "BaseCX") ||
1462 	    !config_has_user_value(basicConfig, "Video", "BaseCY")) {
1463 		config_set_uint(basicConfig, "Video", "BaseCX", cx);
1464 		config_set_uint(basicConfig, "Video", "BaseCY", cy);
1465 		config_save_safe(basicConfig, "tmp", nullptr);
1466 	}
1467 
1468 	config_set_default_string(basicConfig, "Output", "FilenameFormatting",
1469 				  "%CCYY-%MM-%DD %hh-%mm-%ss");
1470 
1471 	config_set_default_bool(basicConfig, "Output", "DelayEnable", false);
1472 	config_set_default_uint(basicConfig, "Output", "DelaySec", 20);
1473 	config_set_default_bool(basicConfig, "Output", "DelayPreserve", true);
1474 
1475 	config_set_default_bool(basicConfig, "Output", "Reconnect", true);
1476 	config_set_default_uint(basicConfig, "Output", "RetryDelay", 10);
1477 	config_set_default_uint(basicConfig, "Output", "MaxRetries", 20);
1478 
1479 	config_set_default_string(basicConfig, "Output", "BindIP", "default");
1480 	config_set_default_bool(basicConfig, "Output", "NewSocketLoopEnable",
1481 				false);
1482 	config_set_default_bool(basicConfig, "Output", "LowLatencyEnable",
1483 				false);
1484 
1485 	int i = 0;
1486 	uint32_t scale_cx = cx;
1487 	uint32_t scale_cy = cy;
1488 
1489 	/* use a default scaled resolution that has a pixel count no higher
1490 	 * than 1280x720 */
1491 	while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) {
1492 		double scale = scaled_vals[i++];
1493 		scale_cx = uint32_t(double(cx) / scale);
1494 		scale_cy = uint32_t(double(cy) / scale);
1495 	}
1496 
1497 	config_set_default_uint(basicConfig, "Video", "OutputCX", scale_cx);
1498 	config_set_default_uint(basicConfig, "Video", "OutputCY", scale_cy);
1499 
1500 	/* don't allow OutputCX/OutputCY to be susceptible to defaults
1501 	 * changing */
1502 	if (!config_has_user_value(basicConfig, "Video", "OutputCX") ||
1503 	    !config_has_user_value(basicConfig, "Video", "OutputCY")) {
1504 		config_set_uint(basicConfig, "Video", "OutputCX", scale_cx);
1505 		config_set_uint(basicConfig, "Video", "OutputCY", scale_cy);
1506 		config_save_safe(basicConfig, "tmp", nullptr);
1507 	}
1508 
1509 	config_set_default_uint(basicConfig, "Video", "FPSType", 0);
1510 	config_set_default_string(basicConfig, "Video", "FPSCommon", "30");
1511 	config_set_default_uint(basicConfig, "Video", "FPSInt", 30);
1512 	config_set_default_uint(basicConfig, "Video", "FPSNum", 30);
1513 	config_set_default_uint(basicConfig, "Video", "FPSDen", 1);
1514 	config_set_default_string(basicConfig, "Video", "ScaleType", "bicubic");
1515 	config_set_default_string(basicConfig, "Video", "ColorFormat", "NV12");
1516 	config_set_default_string(basicConfig, "Video", "ColorSpace", "709");
1517 	config_set_default_string(basicConfig, "Video", "ColorRange",
1518 				  "Partial");
1519 
1520 	config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId",
1521 				  "default");
1522 	config_set_default_string(
1523 		basicConfig, "Audio", "MonitoringDeviceName",
1524 		Str("Basic.Settings.Advanced.Audio.MonitoringDevice"
1525 		    ".Default"));
1526 	config_set_default_uint(basicConfig, "Audio", "SampleRate", 48000);
1527 	config_set_default_string(basicConfig, "Audio", "ChannelSetup",
1528 				  "Stereo");
1529 	config_set_default_double(basicConfig, "Audio", "MeterDecayRate",
1530 				  VOLUME_METER_DECAY_FAST);
1531 	config_set_default_uint(basicConfig, "Audio", "PeakMeterType", 0);
1532 
1533 	CheckExistingCookieId();
1534 
1535 	return true;
1536 }
1537 
1538 extern bool EncoderAvailable(const char *encoder);
1539 
InitBasicConfigDefaults2()1540 void OBSBasic::InitBasicConfigDefaults2()
1541 {
1542 	bool oldEncDefaults = config_get_bool(App()->GlobalConfig(), "General",
1543 					      "Pre23Defaults");
1544 	bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults;
1545 
1546 	config_set_default_string(basicConfig, "SimpleOutput", "StreamEncoder",
1547 				  useNV ? SIMPLE_ENCODER_NVENC
1548 					: SIMPLE_ENCODER_X264);
1549 	config_set_default_string(basicConfig, "SimpleOutput", "RecEncoder",
1550 				  useNV ? SIMPLE_ENCODER_NVENC
1551 					: SIMPLE_ENCODER_X264);
1552 }
1553 
InitBasicConfig()1554 bool OBSBasic::InitBasicConfig()
1555 {
1556 	ProfileScope("OBSBasic::InitBasicConfig");
1557 
1558 	char configPath[512];
1559 
1560 	int ret = GetProfilePath(configPath, sizeof(configPath), "");
1561 	if (ret <= 0) {
1562 		OBSErrorBox(nullptr, "Failed to get profile path");
1563 		return false;
1564 	}
1565 
1566 	if (os_mkdir(configPath) == MKDIR_ERROR) {
1567 		OBSErrorBox(nullptr, "Failed to create profile path");
1568 		return false;
1569 	}
1570 
1571 	ret = GetProfilePath(configPath, sizeof(configPath), "basic.ini");
1572 	if (ret <= 0) {
1573 		OBSErrorBox(nullptr, "Failed to get basic.ini path");
1574 		return false;
1575 	}
1576 
1577 	int code = basicConfig.Open(configPath, CONFIG_OPEN_ALWAYS);
1578 	if (code != CONFIG_SUCCESS) {
1579 		OBSErrorBox(NULL, "Failed to open basic.ini: %d", code);
1580 		return false;
1581 	}
1582 
1583 	if (config_get_string(basicConfig, "General", "Name") == nullptr) {
1584 		const char *curName = config_get_string(App()->GlobalConfig(),
1585 							"Basic", "Profile");
1586 
1587 		config_set_string(basicConfig, "General", "Name", curName);
1588 		basicConfig.SaveSafe("tmp");
1589 	}
1590 
1591 	return InitBasicConfigDefaults();
1592 }
1593 
InitOBSCallbacks()1594 void OBSBasic::InitOBSCallbacks()
1595 {
1596 	ProfileScope("OBSBasic::InitOBSCallbacks");
1597 
1598 	signalHandlers.reserve(signalHandlers.size() + 7);
1599 	signalHandlers.emplace_back(obs_get_signal_handler(), "source_create",
1600 				    OBSBasic::SourceCreated, this);
1601 	signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove",
1602 				    OBSBasic::SourceRemoved, this);
1603 	signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate",
1604 				    OBSBasic::SourceActivated, this);
1605 	signalHandlers.emplace_back(obs_get_signal_handler(),
1606 				    "source_deactivate",
1607 				    OBSBasic::SourceDeactivated, this);
1608 	signalHandlers.emplace_back(obs_get_signal_handler(),
1609 				    "source_audio_activate",
1610 				    OBSBasic::SourceAudioActivated, this);
1611 	signalHandlers.emplace_back(obs_get_signal_handler(),
1612 				    "source_audio_deactivate",
1613 				    OBSBasic::SourceAudioDeactivated, this);
1614 	signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename",
1615 				    OBSBasic::SourceRenamed, this);
1616 }
1617 
InitPrimitives()1618 void OBSBasic::InitPrimitives()
1619 {
1620 	ProfileScope("OBSBasic::InitPrimitives");
1621 
1622 	obs_enter_graphics();
1623 
1624 	gs_render_start(true);
1625 	gs_vertex2f(0.0f, 0.0f);
1626 	gs_vertex2f(0.0f, 1.0f);
1627 	gs_vertex2f(1.0f, 0.0f);
1628 	gs_vertex2f(1.0f, 1.0f);
1629 	box = gs_render_save();
1630 
1631 	gs_render_start(true);
1632 	gs_vertex2f(0.0f, 0.0f);
1633 	gs_vertex2f(0.0f, 1.0f);
1634 	boxLeft = gs_render_save();
1635 
1636 	gs_render_start(true);
1637 	gs_vertex2f(0.0f, 0.0f);
1638 	gs_vertex2f(1.0f, 0.0f);
1639 	boxTop = gs_render_save();
1640 
1641 	gs_render_start(true);
1642 	gs_vertex2f(1.0f, 0.0f);
1643 	gs_vertex2f(1.0f, 1.0f);
1644 	boxRight = gs_render_save();
1645 
1646 	gs_render_start(true);
1647 	gs_vertex2f(0.0f, 1.0f);
1648 	gs_vertex2f(1.0f, 1.0f);
1649 	boxBottom = gs_render_save();
1650 
1651 	gs_render_start(true);
1652 	for (int i = 0; i <= 360; i += (360 / 20)) {
1653 		float pos = RAD(float(i));
1654 		gs_vertex2f(cosf(pos), sinf(pos));
1655 	}
1656 	circle = gs_render_save();
1657 
1658 	InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin,
1659 		      &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine);
1660 	obs_leave_graphics();
1661 }
1662 
ReplayBufferClicked()1663 void OBSBasic::ReplayBufferClicked()
1664 {
1665 	if (outputHandler->ReplayBufferActive())
1666 		StopReplayBuffer();
1667 	else
1668 		StartReplayBuffer();
1669 };
1670 
AddVCamButton()1671 void OBSBasic::AddVCamButton()
1672 {
1673 	vcamButton = new ReplayBufferButton(QTStr("Basic.Main.StartVirtualCam"),
1674 					    this);
1675 	vcamButton->setCheckable(true);
1676 	connect(vcamButton.data(), &QPushButton::clicked, this,
1677 		&OBSBasic::VCamButtonClicked);
1678 
1679 	vcamButton->setProperty("themeID", "vcamButton");
1680 	ui->buttonsVLayout->insertWidget(2, vcamButton);
1681 	setTabOrder(ui->recordButton, vcamButton);
1682 	setTabOrder(vcamButton, ui->modeSwitch);
1683 }
1684 
ResetOutputs()1685 void OBSBasic::ResetOutputs()
1686 {
1687 	ProfileScope("OBSBasic::ResetOutputs");
1688 
1689 	const char *mode = config_get_string(basicConfig, "Output", "Mode");
1690 	bool advOut = astrcmpi(mode, "Advanced") == 0;
1691 
1692 	if (!outputHandler || !outputHandler->Active()) {
1693 		outputHandler.reset();
1694 		outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this)
1695 					   : CreateSimpleOutputHandler(this));
1696 
1697 		delete replayBufferButton;
1698 		delete replayLayout;
1699 
1700 		if (outputHandler->replayBuffer) {
1701 			replayBufferButton = new ReplayBufferButton(
1702 				QTStr("Basic.Main.StartReplayBuffer"), this);
1703 			replayBufferButton->setCheckable(true);
1704 			connect(replayBufferButton.data(),
1705 				&QPushButton::clicked, this,
1706 				&OBSBasic::ReplayBufferClicked);
1707 
1708 			replayBufferButton->setSizePolicy(QSizePolicy::Ignored,
1709 							  QSizePolicy::Fixed);
1710 
1711 			replayLayout = new QHBoxLayout(this);
1712 			replayLayout->addWidget(replayBufferButton);
1713 
1714 			replayBufferButton->setProperty("themeID",
1715 							"replayBufferButton");
1716 			ui->buttonsVLayout->insertLayout(2, replayLayout);
1717 			setTabOrder(ui->recordButton, replayBufferButton);
1718 			setTabOrder(replayBufferButton,
1719 				    ui->buttonsVLayout->itemAt(3)->widget());
1720 		}
1721 
1722 		if (sysTrayReplayBuffer)
1723 			sysTrayReplayBuffer->setEnabled(
1724 				!!outputHandler->replayBuffer);
1725 	} else {
1726 		outputHandler->Update();
1727 	}
1728 }
1729 
1730 static void AddProjectorMenuMonitors(QMenu *parent, QObject *target,
1731 				     const char *slot);
1732 
1733 #define STARTUP_SEPARATOR \
1734 	"==== Startup complete ==============================================="
1735 #define SHUTDOWN_SEPARATOR \
1736 	"==== Shutting down =================================================="
1737 
1738 #define UNSUPPORTED_ERROR                                                     \
1739 	"Failed to initialize video:\n\nRequired graphics API functionality " \
1740 	"not found.  Your GPU may not be supported."
1741 
1742 #define UNKNOWN_ERROR                                                  \
1743 	"Failed to initialize video.  Your GPU may not be supported, " \
1744 	"or your graphics drivers may need to be updated."
1745 
OBSInit()1746 void OBSBasic::OBSInit()
1747 {
1748 	ProfileScope("OBSBasic::OBSInit");
1749 
1750 	const char *sceneCollection = config_get_string(
1751 		App()->GlobalConfig(), "Basic", "SceneCollectionFile");
1752 	char savePath[1024];
1753 	char fileName[1024];
1754 	int ret;
1755 
1756 	if (!sceneCollection)
1757 		throw "Failed to get scene collection name";
1758 
1759 	ret = snprintf(fileName, sizeof(fileName),
1760 		       "obs-studio/basic/scenes/%s.json", sceneCollection);
1761 	if (ret <= 0)
1762 		throw "Failed to create scene collection file name";
1763 
1764 	ret = GetConfigPath(savePath, sizeof(savePath), fileName);
1765 	if (ret <= 0)
1766 		throw "Failed to get scene collection json file path";
1767 
1768 	if (!InitBasicConfig())
1769 		throw "Failed to load basic.ini";
1770 	if (!ResetAudio())
1771 		throw "Failed to initialize audio";
1772 
1773 	ret = ResetVideo();
1774 
1775 	switch (ret) {
1776 	case OBS_VIDEO_MODULE_NOT_FOUND:
1777 		throw "Failed to initialize video:  Graphics module not found";
1778 	case OBS_VIDEO_NOT_SUPPORTED:
1779 		throw UNSUPPORTED_ERROR;
1780 	case OBS_VIDEO_INVALID_PARAM:
1781 		throw "Failed to initialize video:  Invalid parameters";
1782 	default:
1783 		if (ret != OBS_VIDEO_SUCCESS)
1784 			throw UNKNOWN_ERROR;
1785 	}
1786 
1787 	/* load audio monitoring */
1788 #if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO
1789 	const char *device_name =
1790 		config_get_string(basicConfig, "Audio", "MonitoringDeviceName");
1791 	const char *device_id =
1792 		config_get_string(basicConfig, "Audio", "MonitoringDeviceId");
1793 
1794 	obs_set_audio_monitoring_device(device_name, device_id);
1795 
1796 	blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s",
1797 	     device_name, device_id);
1798 #endif
1799 
1800 	InitOBSCallbacks();
1801 	InitHotkeys();
1802 
1803 	/* hack to prevent elgato from loading its own QtNetwork that it tries
1804 	 * to ship with */
1805 #if defined(_WIN32) && !defined(_DEBUG)
1806 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1807 	LoadLibraryW(L"Qt5Network");
1808 #else
1809 	LoadLibraryW(L"Qt6Network");
1810 #endif
1811 #endif
1812 
1813 	AddExtraModulePaths();
1814 	blog(LOG_INFO, "---------------------------------");
1815 	obs_load_all_modules();
1816 	blog(LOG_INFO, "---------------------------------");
1817 	obs_log_loaded_modules();
1818 	blog(LOG_INFO, "---------------------------------");
1819 	obs_post_load_modules();
1820 
1821 #ifdef BROWSER_AVAILABLE
1822 	cef = obs_browser_init_panel();
1823 #endif
1824 
1825 	obs_data_t *obsData = obs_get_private_data();
1826 	vcamEnabled = obs_data_get_bool(obsData, "vcamEnabled");
1827 	if (vcamEnabled) {
1828 		AddVCamButton();
1829 	}
1830 	obs_data_release(obsData);
1831 
1832 	InitBasicConfigDefaults2();
1833 
1834 	CheckForSimpleModeX264Fallback();
1835 
1836 	blog(LOG_INFO, STARTUP_SEPARATOR);
1837 
1838 	ResetOutputs();
1839 	CreateHotkeys();
1840 
1841 	if (!InitService())
1842 		throw "Failed to initialize service";
1843 
1844 	InitPrimitives();
1845 
1846 	sceneDuplicationMode = config_get_bool(
1847 		App()->GlobalConfig(), "BasicWindow", "SceneDuplicationMode");
1848 	swapScenesMode = config_get_bool(App()->GlobalConfig(), "BasicWindow",
1849 					 "SwapScenesMode");
1850 	editPropertiesMode = config_get_bool(
1851 		App()->GlobalConfig(), "BasicWindow", "EditPropertiesMode");
1852 
1853 	if (!opt_studio_mode) {
1854 		SetPreviewProgramMode(config_get_bool(App()->GlobalConfig(),
1855 						      "BasicWindow",
1856 						      "PreviewProgramMode"));
1857 	} else {
1858 		SetPreviewProgramMode(true);
1859 		opt_studio_mode = false;
1860 	}
1861 
1862 #define SET_VISIBILITY(name, control)                                         \
1863 	do {                                                                  \
1864 		if (config_has_user_value(App()->GlobalConfig(),              \
1865 					  "BasicWindow", name)) {             \
1866 			bool visible = config_get_bool(App()->GlobalConfig(), \
1867 						       "BasicWindow", name);  \
1868 			ui->control->setChecked(visible);                     \
1869 		}                                                             \
1870 	} while (false)
1871 
1872 	SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars);
1873 	SET_VISIBILITY("ShowStatusBar", toggleStatusBar);
1874 #undef SET_VISIBILITY
1875 
1876 	bool sourceIconsVisible = config_get_bool(
1877 		GetGlobalConfig(), "BasicWindow", "ShowSourceIcons");
1878 	ui->toggleSourceIcons->setChecked(sourceIconsVisible);
1879 
1880 	bool contextVisible = config_get_bool(
1881 		App()->GlobalConfig(), "BasicWindow", "ShowContextToolbars");
1882 	ui->toggleContextBar->setChecked(contextVisible);
1883 	ui->contextContainer->setVisible(contextVisible);
1884 	if (contextVisible)
1885 		UpdateContextBar(true);
1886 
1887 	{
1888 		ProfileScope("OBSBasic::Load");
1889 		disableSaving--;
1890 		Load(savePath);
1891 		disableSaving++;
1892 	}
1893 
1894 	TimedCheckForUpdates();
1895 	loaded = true;
1896 
1897 	previewEnabled = config_get_bool(App()->GlobalConfig(), "BasicWindow",
1898 					 "PreviewEnabled");
1899 
1900 	if (!previewEnabled && !IsPreviewProgramMode())
1901 		QMetaObject::invokeMethod(this, "EnablePreviewDisplay",
1902 					  Qt::QueuedConnection,
1903 					  Q_ARG(bool, previewEnabled));
1904 
1905 #ifdef _WIN32
1906 	uint32_t winVer = GetWindowsVersion();
1907 	if (winVer > 0 && winVer < 0x602) {
1908 		bool disableAero =
1909 			config_get_bool(basicConfig, "Video", "DisableAero");
1910 		SetAeroEnabled(!disableAero);
1911 	}
1912 #endif
1913 
1914 	RefreshSceneCollections();
1915 	RefreshProfiles();
1916 	disableSaving--;
1917 
1918 	auto addDisplay = [this](OBSQTDisplay *window) {
1919 		obs_display_add_draw_callback(window->GetDisplay(),
1920 					      OBSBasic::RenderMain, this);
1921 
1922 		struct obs_video_info ovi;
1923 		if (obs_get_video_info(&ovi))
1924 			ResizePreview(ovi.base_width, ovi.base_height);
1925 	};
1926 
1927 	connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay);
1928 
1929 #ifdef _WIN32
1930 	SetWin32DropStyle(this);
1931 	show();
1932 #endif
1933 
1934 	bool alwaysOnTop = config_get_bool(App()->GlobalConfig(), "BasicWindow",
1935 					   "AlwaysOnTop");
1936 
1937 #ifdef ENABLE_WAYLAND
1938 	bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND;
1939 #else
1940 	bool isWayland = false;
1941 #endif
1942 
1943 	if (!isWayland && (alwaysOnTop || opt_always_on_top)) {
1944 		SetAlwaysOnTop(this, true);
1945 		ui->actionAlwaysOnTop->setChecked(true);
1946 	} else if (isWayland) {
1947 		if (opt_always_on_top)
1948 			blog(LOG_INFO,
1949 			     "Always On Top not available on Wayland, ignoring.");
1950 		ui->actionAlwaysOnTop->setEnabled(false);
1951 		ui->actionAlwaysOnTop->setVisible(false);
1952 	}
1953 
1954 #ifndef _WIN32
1955 	show();
1956 #endif
1957 
1958 	/* setup stats dock */
1959 	OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false);
1960 	statsDock->setWidget(statsDlg);
1961 
1962 	/* ----------------------------- */
1963 	/* add custom browser docks      */
1964 
1965 #ifdef BROWSER_AVAILABLE
1966 	if (cef) {
1967 		QAction *action = new QAction(QTStr("Basic.MainMenu."
1968 						    "View.Docks."
1969 						    "CustomBrowserDocks"),
1970 					      this);
1971 		ui->viewMenuDocks->insertAction(ui->toggleScenes, action);
1972 		connect(action, &QAction::triggered, this,
1973 			&OBSBasic::ManageExtraBrowserDocks);
1974 		ui->viewMenuDocks->insertSeparator(ui->toggleScenes);
1975 
1976 		LoadExtraBrowserDocks();
1977 	}
1978 #endif
1979 
1980 	const char *dockStateStr = config_get_string(
1981 		App()->GlobalConfig(), "BasicWindow", "DockState");
1982 
1983 	if (!dockStateStr) {
1984 		on_resetUI_triggered();
1985 	} else {
1986 		QByteArray dockState =
1987 			QByteArray::fromBase64(QByteArray(dockStateStr));
1988 		if (!restoreState(dockState))
1989 			on_resetUI_triggered();
1990 	}
1991 
1992 	bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General",
1993 					     "Pre23Defaults");
1994 	if (pre23Defaults) {
1995 		bool resetDockLock23 = config_get_bool(
1996 			App()->GlobalConfig(), "General", "ResetDockLock23");
1997 		if (!resetDockLock23) {
1998 			config_set_bool(App()->GlobalConfig(), "General",
1999 					"ResetDockLock23", true);
2000 			config_remove_value(App()->GlobalConfig(),
2001 					    "BasicWindow", "DocksLocked");
2002 			config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
2003 		}
2004 	}
2005 
2006 	bool docksLocked = config_get_bool(App()->GlobalConfig(), "BasicWindow",
2007 					   "DocksLocked");
2008 	on_lockUI_toggled(docksLocked);
2009 	ui->lockUI->blockSignals(true);
2010 	ui->lockUI->setChecked(docksLocked);
2011 	ui->lockUI->blockSignals(false);
2012 
2013 #ifndef __APPLE__
2014 	SystemTray(true);
2015 #endif
2016 
2017 #ifdef __APPLE__
2018 	disableColorSpaceConversion(this);
2019 #endif
2020 
2021 	bool has_last_version = config_has_user_value(App()->GlobalConfig(),
2022 						      "General", "LastVersion");
2023 	bool first_run =
2024 		config_get_bool(App()->GlobalConfig(), "General", "FirstRun");
2025 
2026 	if (!first_run) {
2027 		config_set_bool(App()->GlobalConfig(), "General", "FirstRun",
2028 				true);
2029 		config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
2030 	}
2031 
2032 	if (!first_run && !has_last_version && !Active())
2033 		QMetaObject::invokeMethod(this, "on_autoConfigure_triggered",
2034 					  Qt::QueuedConnection);
2035 
2036 	ToggleMixerLayout(config_get_bool(App()->GlobalConfig(), "BasicWindow",
2037 					  "VerticalVolControl"));
2038 
2039 	if (config_get_bool(basicConfig, "General", "OpenStatsOnStartup"))
2040 		on_stats_triggered();
2041 
2042 	OBSBasicStats::InitializeValues();
2043 
2044 	/* ----------------------- */
2045 	/* Add multiview menu      */
2046 
2047 	ui->viewMenu->addSeparator();
2048 
2049 	multiviewProjectorMenu = new QMenu(QTStr("MultiviewProjector"));
2050 	ui->viewMenu->addMenu(multiviewProjectorMenu);
2051 	AddProjectorMenuMonitors(multiviewProjectorMenu, this,
2052 				 SLOT(OpenMultiviewProjector()));
2053 	connect(ui->viewMenu->menuAction(), &QAction::hovered, this,
2054 		&OBSBasic::UpdateMultiviewProjectorMenu);
2055 	ui->viewMenu->addAction(QTStr("MultiviewWindowed"), this,
2056 				SLOT(OpenMultiviewWindow()));
2057 
2058 	ui->sources->UpdateIcons();
2059 
2060 #if !defined(_WIN32)
2061 	delete ui->actionShowCrashLogs;
2062 	delete ui->actionUploadLastCrashLog;
2063 	delete ui->menuCrashLogs;
2064 	ui->actionShowCrashLogs = nullptr;
2065 	ui->actionUploadLastCrashLog = nullptr;
2066 	ui->menuCrashLogs = nullptr;
2067 #if !defined(__APPLE__)
2068 	delete ui->actionCheckForUpdates;
2069 	ui->actionCheckForUpdates = nullptr;
2070 #endif
2071 #endif
2072 
2073 #ifdef __APPLE__
2074 	/* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */
2075 	delete ui->actionFullscreenInterface;
2076 	ui->actionFullscreenInterface = nullptr;
2077 #endif
2078 
2079 #if defined(_WIN32) || defined(__APPLE__)
2080 	if (App()->IsUpdaterDisabled())
2081 		ui->actionCheckForUpdates->setEnabled(false);
2082 #endif
2083 
2084 	OnFirstLoad();
2085 
2086 	activateWindow();
2087 
2088 #ifdef __APPLE__
2089 	QMetaObject::invokeMethod(this, "DeferredSysTrayLoad",
2090 				  Qt::QueuedConnection, Q_ARG(int, 10));
2091 #endif
2092 }
2093 
OnFirstLoad()2094 void OBSBasic::OnFirstLoad()
2095 {
2096 	if (api)
2097 		api->on_event(OBS_FRONTEND_EVENT_FINISHED_LOADING);
2098 
2099 #if defined(BROWSER_AVAILABLE) && defined(_WIN32)
2100 	/* Attempt to load init screen if available */
2101 	if (cef) {
2102 		WhatsNewInfoThread *wnit = new WhatsNewInfoThread();
2103 		connect(wnit, &WhatsNewInfoThread::Result, this,
2104 			&OBSBasic::ReceivedIntroJson);
2105 
2106 		introCheckThread.reset(wnit);
2107 		introCheckThread->start();
2108 	}
2109 #endif
2110 
2111 	Auth::Load();
2112 
2113 	bool showLogViewerOnStartup = config_get_bool(
2114 		App()->GlobalConfig(), "LogViewer", "ShowLogStartup");
2115 
2116 	if (showLogViewerOnStartup)
2117 		on_actionViewCurrentLog_triggered();
2118 }
2119 
DeferredSysTrayLoad(int requeueCount)2120 void OBSBasic::DeferredSysTrayLoad(int requeueCount)
2121 {
2122 	if (--requeueCount > 0) {
2123 		QMetaObject::invokeMethod(this, "DeferredSysTrayLoad",
2124 					  Qt::QueuedConnection,
2125 					  Q_ARG(int, requeueCount));
2126 		return;
2127 	}
2128 
2129 	/* Minimizng to tray on initial startup does not work on mac
2130 	 * unless it is done in the deferred load */
2131 	SystemTray(true);
2132 }
2133 
2134 /* shows a "what's new" page on startup of new versions using CEF */
ReceivedIntroJson(const QString & text)2135 void OBSBasic::ReceivedIntroJson(const QString &text)
2136 {
2137 #ifdef BROWSER_AVAILABLE
2138 #ifdef _WIN32
2139 	if (closing)
2140 		return;
2141 
2142 	std::string err;
2143 	Json json = Json::parse(QT_TO_UTF8(text), err);
2144 	if (!err.empty())
2145 		return;
2146 
2147 	std::string info_url;
2148 	int info_increment = -1;
2149 
2150 	/* check to see if there's an info page for this version */
2151 	const Json::array &items = json.array_items();
2152 	for (const Json &item : items) {
2153 		const std::string &version = item["version"].string_value();
2154 		const std::string &url = item["url"].string_value();
2155 		int increment = item["increment"].int_value();
2156 		int rc = item["RC"].int_value();
2157 
2158 		int major = 0;
2159 		int minor = 0;
2160 
2161 		sscanf(version.c_str(), "%d.%d", &major, &minor);
2162 #if OBS_RELEASE_CANDIDATE > 0
2163 		if (major == OBS_RELEASE_CANDIDATE_MAJOR &&
2164 		    minor == OBS_RELEASE_CANDIDATE_MINOR &&
2165 		    rc == OBS_RELEASE_CANDIDATE) {
2166 #else
2167 		if (major == LIBOBS_API_MAJOR_VER &&
2168 		    minor == LIBOBS_API_MINOR_VER && rc == 0) {
2169 #endif
2170 			info_url = url;
2171 			info_increment = increment;
2172 		}
2173 	}
2174 
2175 	/* this version was not found, or no info for this version */
2176 	if (info_increment == -1) {
2177 		return;
2178 	}
2179 
2180 #if OBS_RELEASE_CANDIDATE > 0
2181 	uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General",
2182 					      "LastRCVersion");
2183 #else
2184 	uint32_t lastVersion =
2185 		config_get_int(App()->GlobalConfig(), "General", "LastVersion");
2186 #endif
2187 
2188 	int current_version_increment = -1;
2189 
2190 #if OBS_RELEASE_CANDIDATE > 0
2191 	if (lastVersion < OBS_RELEASE_CANDIDATE_VER) {
2192 #else
2193 	if ((lastVersion & ~0xFFFF) < (LIBOBS_API_VER & ~0xFFFF)) {
2194 #endif
2195 		config_set_int(App()->GlobalConfig(), "General",
2196 			       "InfoIncrement", -1);
2197 	} else {
2198 		current_version_increment = config_get_int(
2199 			App()->GlobalConfig(), "General", "InfoIncrement");
2200 	}
2201 
2202 	if (info_increment <= current_version_increment) {
2203 		return;
2204 	}
2205 
2206 	config_set_int(App()->GlobalConfig(), "General", "InfoIncrement",
2207 		       info_increment);
2208 
2209 	/* Don't show What's New dialog for new users */
2210 #if !defined(OBS_RELEASE_CANDIDATE) || OBS_RELEASE_CANDIDATE == 0
2211 	if (!lastVersion) {
2212 		return;
2213 	}
2214 #endif
2215 	cef->init_browser();
2216 
2217 	WhatsNewBrowserInitThread *wnbit =
2218 		new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str()));
2219 
2220 	connect(wnbit, &WhatsNewBrowserInitThread::Result, this,
2221 		&OBSBasic::ShowWhatsNew);
2222 
2223 	whatsNewInitThread.reset(wnbit);
2224 	whatsNewInitThread->start();
2225 
2226 #else
2227 	UNUSED_PARAMETER(text);
2228 #endif
2229 #else
2230 	UNUSED_PARAMETER(text);
2231 #endif
2232 }
2233 
2234 void OBSBasic::ShowWhatsNew(const QString &url)
2235 {
2236 #ifdef BROWSER_AVAILABLE
2237 #ifdef _WIN32
2238 	if (closing)
2239 		return;
2240 
2241 	std::string info_url = QT_TO_UTF8(url);
2242 
2243 	QDialog *dlg = new QDialog(this);
2244 	dlg->setAttribute(Qt::WA_DeleteOnClose, true);
2245 	dlg->setWindowTitle("What's New");
2246 	dlg->resize(700, 600);
2247 
2248 	Qt::WindowFlags flags = dlg->windowFlags();
2249 	Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint;
2250 	dlg->setWindowFlags(flags & (~helpFlag));
2251 
2252 	QCefWidget *cefWidget = cef->create_widget(nullptr, info_url);
2253 	if (!cefWidget) {
2254 		return;
2255 	}
2256 
2257 	connect(cefWidget, SIGNAL(titleChanged(const QString &)), dlg,
2258 		SLOT(setWindowTitle(const QString &)));
2259 
2260 	QPushButton *close = new QPushButton(QTStr("Close"));
2261 	connect(close, &QAbstractButton::clicked, dlg, &QDialog::accept);
2262 
2263 	QHBoxLayout *bottomLayout = new QHBoxLayout();
2264 	bottomLayout->addStretch();
2265 	bottomLayout->addWidget(close);
2266 	bottomLayout->addStretch();
2267 
2268 	QVBoxLayout *topLayout = new QVBoxLayout(dlg);
2269 	topLayout->addWidget(cefWidget);
2270 	topLayout->addLayout(bottomLayout);
2271 
2272 	dlg->show();
2273 #else
2274 	UNUSED_PARAMETER(url);
2275 #endif
2276 #else
2277 	UNUSED_PARAMETER(url);
2278 #endif
2279 }
2280 
2281 void OBSBasic::UpdateMultiviewProjectorMenu()
2282 {
2283 	multiviewProjectorMenu->clear();
2284 	AddProjectorMenuMonitors(multiviewProjectorMenu, this,
2285 				 SLOT(OpenMultiviewProjector()));
2286 }
2287 
2288 void OBSBasic::InitHotkeys()
2289 {
2290 	ProfileScope("OBSBasic::InitHotkeys");
2291 
2292 	struct obs_hotkeys_translations t = {};
2293 	t.insert = Str("Hotkeys.Insert");
2294 	t.del = Str("Hotkeys.Delete");
2295 	t.home = Str("Hotkeys.Home");
2296 	t.end = Str("Hotkeys.End");
2297 	t.page_up = Str("Hotkeys.PageUp");
2298 	t.page_down = Str("Hotkeys.PageDown");
2299 	t.num_lock = Str("Hotkeys.NumLock");
2300 	t.scroll_lock = Str("Hotkeys.ScrollLock");
2301 	t.caps_lock = Str("Hotkeys.CapsLock");
2302 	t.backspace = Str("Hotkeys.Backspace");
2303 	t.tab = Str("Hotkeys.Tab");
2304 	t.print = Str("Hotkeys.Print");
2305 	t.pause = Str("Hotkeys.Pause");
2306 	t.left = Str("Hotkeys.Left");
2307 	t.right = Str("Hotkeys.Right");
2308 	t.up = Str("Hotkeys.Up");
2309 	t.down = Str("Hotkeys.Down");
2310 #ifdef _WIN32
2311 	t.meta = Str("Hotkeys.Windows");
2312 #else
2313 	t.meta = Str("Hotkeys.Super");
2314 #endif
2315 	t.menu = Str("Hotkeys.Menu");
2316 	t.space = Str("Hotkeys.Space");
2317 	t.numpad_num = Str("Hotkeys.NumpadNum");
2318 	t.numpad_multiply = Str("Hotkeys.NumpadMultiply");
2319 	t.numpad_divide = Str("Hotkeys.NumpadDivide");
2320 	t.numpad_plus = Str("Hotkeys.NumpadAdd");
2321 	t.numpad_minus = Str("Hotkeys.NumpadSubtract");
2322 	t.numpad_decimal = Str("Hotkeys.NumpadDecimal");
2323 	t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum");
2324 	t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply");
2325 	t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide");
2326 	t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd");
2327 	t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract");
2328 	t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal");
2329 	t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual");
2330 	t.mouse_num = Str("Hotkeys.MouseButton");
2331 	t.escape = Str("Hotkeys.Escape");
2332 	obs_hotkeys_set_translations(&t);
2333 
2334 	obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"),
2335 						   Str("Push-to-mute"),
2336 						   Str("Push-to-talk"));
2337 
2338 	obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"),
2339 						       Str("SceneItemHide"));
2340 
2341 	obs_hotkey_enable_callback_rerouting(true);
2342 	obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this);
2343 }
2344 
2345 void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed)
2346 {
2347 	obs_hotkey_trigger_routed_callback(id, pressed);
2348 }
2349 
2350 void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed)
2351 {
2352 	OBSBasic &basic = *static_cast<OBSBasic *>(data);
2353 	QMetaObject::invokeMethod(&basic, "ProcessHotkey",
2354 				  Q_ARG(obs_hotkey_id, id),
2355 				  Q_ARG(bool, pressed));
2356 }
2357 
2358 void OBSBasic::CreateHotkeys()
2359 {
2360 	ProfileScope("OBSBasic::CreateHotkeys");
2361 
2362 	auto LoadHotkeyData = [&](const char *name) -> OBSData {
2363 		const char *info =
2364 			config_get_string(basicConfig, "Hotkeys", name);
2365 		if (!info)
2366 			return {};
2367 
2368 		obs_data_t *data = obs_data_create_from_json(info);
2369 		if (!data)
2370 			return {};
2371 
2372 		OBSData res = data;
2373 		obs_data_release(data);
2374 		return res;
2375 	};
2376 
2377 	auto LoadHotkey = [&](obs_hotkey_id id, const char *name) {
2378 		obs_data_array_t *array =
2379 			obs_data_get_array(LoadHotkeyData(name), "bindings");
2380 
2381 		obs_hotkey_load(id, array);
2382 		obs_data_array_release(array);
2383 	};
2384 
2385 	auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0,
2386 				  const char *name1) {
2387 		obs_data_array_t *array0 =
2388 			obs_data_get_array(LoadHotkeyData(name0), "bindings");
2389 		obs_data_array_t *array1 =
2390 			obs_data_get_array(LoadHotkeyData(name1), "bindings");
2391 
2392 		obs_hotkey_pair_load(id, array0, array1);
2393 		obs_data_array_release(array0);
2394 		obs_data_array_release(array1);
2395 	};
2396 
2397 #define MAKE_CALLBACK(pred, method, log_action)                            \
2398 	[](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \
2399 		OBSBasic &basic = *static_cast<OBSBasic *>(data);          \
2400 		if ((pred) && pressed) {                                   \
2401 			blog(LOG_INFO, log_action " due to hotkey");       \
2402 			method();                                          \
2403 			return true;                                       \
2404 		}                                                          \
2405 		return false;                                              \
2406 	}
2407 
2408 	streamingHotkeys = obs_hotkey_pair_register_frontend(
2409 		"OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"),
2410 		"OBSBasic.StopStreaming", Str("Basic.Main.StopStreaming"),
2411 		MAKE_CALLBACK(!basic.outputHandler->StreamingActive() &&
2412 				      basic.ui->streamButton->isEnabled(),
2413 			      basic.StartStreaming, "Starting stream"),
2414 		MAKE_CALLBACK(basic.outputHandler->StreamingActive() &&
2415 				      basic.ui->streamButton->isEnabled(),
2416 			      basic.StopStreaming, "Stopping stream"),
2417 		this, this);
2418 	LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming",
2419 		       "OBSBasic.StopStreaming");
2420 
2421 	auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
2422 		OBSBasic &basic = *static_cast<OBSBasic *>(data);
2423 		if (basic.outputHandler->StreamingActive() && pressed) {
2424 			basic.ForceStopStreaming();
2425 		}
2426 	};
2427 
2428 	forceStreamingStopHotkey = obs_hotkey_register_frontend(
2429 		"OBSBasic.ForceStopStreaming",
2430 		Str("Basic.Main.ForceStopStreaming"), cb, this);
2431 	LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming");
2432 
2433 	recordingHotkeys = obs_hotkey_pair_register_frontend(
2434 		"OBSBasic.StartRecording", Str("Basic.Main.StartRecording"),
2435 		"OBSBasic.StopRecording", Str("Basic.Main.StopRecording"),
2436 		MAKE_CALLBACK(!basic.outputHandler->RecordingActive() &&
2437 				      !basic.ui->recordButton->isChecked(),
2438 			      basic.StartRecording, "Starting recording"),
2439 		MAKE_CALLBACK(basic.outputHandler->RecordingActive() &&
2440 				      basic.ui->recordButton->isChecked(),
2441 			      basic.StopRecording, "Stopping recording"),
2442 		this, this);
2443 	LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording",
2444 		       "OBSBasic.StopRecording");
2445 
2446 	pauseHotkeys = obs_hotkey_pair_register_frontend(
2447 		"OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"),
2448 		"OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"),
2449 		MAKE_CALLBACK(basic.pause && !basic.pause->isChecked(),
2450 			      basic.PauseRecording, "Pausing recording"),
2451 		MAKE_CALLBACK(basic.pause && basic.pause->isChecked(),
2452 			      basic.UnpauseRecording, "Unpausing recording"),
2453 		this, this);
2454 	LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording",
2455 		       "OBSBasic.UnpauseRecording");
2456 
2457 	replayBufHotkeys = obs_hotkey_pair_register_frontend(
2458 		"OBSBasic.StartReplayBuffer",
2459 		Str("Basic.Main.StartReplayBuffer"),
2460 		"OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"),
2461 		MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(),
2462 			      basic.StartReplayBuffer,
2463 			      "Starting replay buffer"),
2464 		MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(),
2465 			      basic.StopReplayBuffer, "Stopping replay buffer"),
2466 		this, this);
2467 	LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer",
2468 		       "OBSBasic.StopReplayBuffer");
2469 
2470 	if (vcamEnabled) {
2471 		vcamHotkeys = obs_hotkey_pair_register_frontend(
2472 			"OBSBasic.StartVirtualCam",
2473 			Str("Basic.Main.StartVirtualCam"),
2474 			"OBSBasic.StopVirtualCam",
2475 			Str("Basic.Main.StopVirtualCam"),
2476 			MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(),
2477 				      basic.StartVirtualCam,
2478 				      "Starting virtual camera"),
2479 			MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(),
2480 				      basic.StopVirtualCam,
2481 				      "Stopping virtual camera"),
2482 			this, this);
2483 		LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam",
2484 			       "OBSBasic.StopVirtualCam");
2485 	}
2486 
2487 	togglePreviewHotkeys = obs_hotkey_pair_register_frontend(
2488 		"OBSBasic.EnablePreview",
2489 		Str("Basic.Main.PreviewConextMenu.Enable"),
2490 		"OBSBasic.DisablePreview", Str("Basic.Main.Preview.Disable"),
2491 		MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview,
2492 			      "Enabling preview"),
2493 		MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview,
2494 			      "Disabling preview"),
2495 		this, this);
2496 	LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview",
2497 		       "OBSBasic.DisablePreview");
2498 
2499 	contextBarHotkeys = obs_hotkey_pair_register_frontend(
2500 		"OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"),
2501 		"OBSBasic.HideContextBar", Str("Basic.Main.HideContextBar"),
2502 		MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(),
2503 			      basic.ShowContextBar, "Showing Context Bar"),
2504 		MAKE_CALLBACK(basic.ui->contextContainer->isVisible(),
2505 			      basic.HideContextBar, "Hiding Context Bar"),
2506 		this, this);
2507 	LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar",
2508 		       "OBSBasic.HideContextBar");
2509 #undef MAKE_CALLBACK
2510 
2511 	auto togglePreviewProgram = [](void *data, obs_hotkey_id,
2512 				       obs_hotkey_t *, bool pressed) {
2513 		if (pressed)
2514 			QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2515 						  "on_modeSwitch_clicked",
2516 						  Qt::QueuedConnection);
2517 	};
2518 
2519 	togglePreviewProgramHotkey = obs_hotkey_register_frontend(
2520 		"OBSBasic.TogglePreviewProgram",
2521 		Str("Basic.TogglePreviewProgramMode"), togglePreviewProgram,
2522 		this);
2523 	LoadHotkey(togglePreviewProgramHotkey, "OBSBasic.TogglePreviewProgram");
2524 
2525 	auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2526 			     bool pressed) {
2527 		if (pressed)
2528 			QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2529 						  "TransitionClicked",
2530 						  Qt::QueuedConnection);
2531 	};
2532 
2533 	transitionHotkey = obs_hotkey_register_frontend(
2534 		"OBSBasic.Transition", Str("Transition"), transition, this);
2535 	LoadHotkey(transitionHotkey, "OBSBasic.Transition");
2536 
2537 	auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2538 			     bool pressed) {
2539 		if (pressed)
2540 			QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2541 						  "ResetStatsHotkey",
2542 						  Qt::QueuedConnection);
2543 	};
2544 
2545 	statsHotkey = obs_hotkey_register_frontend(
2546 		"OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"),
2547 		resetStats, this);
2548 	LoadHotkey(statsHotkey, "OBSBasic.ResetStats");
2549 
2550 	auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2551 			     bool pressed) {
2552 		if (pressed)
2553 			QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2554 						  "Screenshot",
2555 						  Qt::QueuedConnection);
2556 	};
2557 
2558 	screenshotHotkey = obs_hotkey_register_frontend(
2559 		"OBSBasic.Screenshot", Str("Screenshot"), screenshot, this);
2560 	LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot");
2561 
2562 	auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *,
2563 				   bool pressed) {
2564 		if (pressed)
2565 			QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
2566 						  "ScreenshotSelectedSource",
2567 						  Qt::QueuedConnection);
2568 	};
2569 
2570 	sourceScreenshotHotkey = obs_hotkey_register_frontend(
2571 		"OBSBasic.SelectedSourceScreenshot",
2572 		Str("Screenshot.SourceHotkey"), screenshotSource, this);
2573 	LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot");
2574 }
2575 
2576 void OBSBasic::ClearHotkeys()
2577 {
2578 	obs_hotkey_pair_unregister(streamingHotkeys);
2579 	obs_hotkey_pair_unregister(recordingHotkeys);
2580 	obs_hotkey_pair_unregister(pauseHotkeys);
2581 	obs_hotkey_pair_unregister(replayBufHotkeys);
2582 	obs_hotkey_pair_unregister(vcamHotkeys);
2583 	obs_hotkey_pair_unregister(togglePreviewHotkeys);
2584 	obs_hotkey_pair_unregister(contextBarHotkeys);
2585 	obs_hotkey_unregister(forceStreamingStopHotkey);
2586 	obs_hotkey_unregister(togglePreviewProgramHotkey);
2587 	obs_hotkey_unregister(transitionHotkey);
2588 	obs_hotkey_unregister(statsHotkey);
2589 	obs_hotkey_unregister(screenshotHotkey);
2590 	obs_hotkey_unregister(sourceScreenshotHotkey);
2591 }
2592 
2593 OBSBasic::~OBSBasic()
2594 {
2595 	/* clear out UI event queue */
2596 	QApplication::sendPostedEvents(App());
2597 
2598 	if (updateCheckThread && updateCheckThread->isRunning())
2599 		updateCheckThread->wait();
2600 
2601 	delete screenshotData;
2602 	delete multiviewProjectorMenu;
2603 	delete previewProjector;
2604 	delete studioProgramProjector;
2605 	delete previewProjectorSource;
2606 	delete previewProjectorMain;
2607 	delete sourceProjector;
2608 	delete sceneProjectorMenu;
2609 	delete scaleFilteringMenu;
2610 	delete colorMenu;
2611 	delete colorWidgetAction;
2612 	delete colorSelect;
2613 	delete deinterlaceMenu;
2614 	delete perSceneTransitionMenu;
2615 	delete shortcutFilter;
2616 	delete trayMenu;
2617 	delete programOptions;
2618 	delete program;
2619 
2620 	/* XXX: any obs data must be released before calling obs_shutdown.
2621 	 * currently, we can't automate this with C++ RAII because of the
2622 	 * delicate nature of obs_shutdown needing to be freed before the UI
2623 	 * can be freed, and we have no control over the destruction order of
2624 	 * the Qt UI stuff, so we have to manually clear any references to
2625 	 * libobs. */
2626 	delete cpuUsageTimer;
2627 	os_cpu_usage_info_destroy(cpuUsageInfo);
2628 
2629 	obs_hotkey_set_callback_routing_func(nullptr, nullptr);
2630 	ClearHotkeys();
2631 
2632 	service = nullptr;
2633 	outputHandler.reset();
2634 
2635 	if (interaction)
2636 		delete interaction;
2637 
2638 	if (properties)
2639 		delete properties;
2640 
2641 	if (filters)
2642 		delete filters;
2643 
2644 	if (transformWindow)
2645 		delete transformWindow;
2646 
2647 	if (advAudioWindow)
2648 		delete advAudioWindow;
2649 
2650 	if (about)
2651 		delete about;
2652 
2653 	obs_display_remove_draw_callback(ui->preview->GetDisplay(),
2654 					 OBSBasic::RenderMain, this);
2655 
2656 	obs_enter_graphics();
2657 	gs_vertexbuffer_destroy(box);
2658 	gs_vertexbuffer_destroy(boxLeft);
2659 	gs_vertexbuffer_destroy(boxTop);
2660 	gs_vertexbuffer_destroy(boxRight);
2661 	gs_vertexbuffer_destroy(boxBottom);
2662 	gs_vertexbuffer_destroy(circle);
2663 	gs_vertexbuffer_destroy(actionSafeMargin);
2664 	gs_vertexbuffer_destroy(graphicsSafeMargin);
2665 	gs_vertexbuffer_destroy(fourByThreeSafeMargin);
2666 	gs_vertexbuffer_destroy(leftLine);
2667 	gs_vertexbuffer_destroy(topLine);
2668 	gs_vertexbuffer_destroy(rightLine);
2669 	obs_leave_graphics();
2670 
2671 	/* When shutting down, sometimes source references can get in to the
2672 	 * event queue, and if we don't forcibly process those events they
2673 	 * won't get processed until after obs_shutdown has been called.  I
2674 	 * really wish there were a more elegant way to deal with this via C++,
2675 	 * but Qt doesn't use C++ in a normal way, so you can't really rely on
2676 	 * normal C++ behavior for your data to be freed in the order that you
2677 	 * expect or want it to. */
2678 	QApplication::sendPostedEvents(this);
2679 
2680 	config_set_int(App()->GlobalConfig(), "General", "LastVersion",
2681 		       LIBOBS_API_VER);
2682 #if OBS_RELEASE_CANDIDATE > 0
2683 	config_set_int(App()->GlobalConfig(), "General", "LastRCVersion",
2684 		       OBS_RELEASE_CANDIDATE_VER);
2685 #endif
2686 
2687 	bool alwaysOnTop = IsAlwaysOnTop(this);
2688 
2689 	config_set_bool(App()->GlobalConfig(), "BasicWindow", "PreviewEnabled",
2690 			previewEnabled);
2691 	config_set_bool(App()->GlobalConfig(), "BasicWindow", "AlwaysOnTop",
2692 			alwaysOnTop);
2693 	config_set_bool(App()->GlobalConfig(), "BasicWindow",
2694 			"SceneDuplicationMode", sceneDuplicationMode);
2695 	config_set_bool(App()->GlobalConfig(), "BasicWindow", "SwapScenesMode",
2696 			swapScenesMode);
2697 	config_set_bool(App()->GlobalConfig(), "BasicWindow",
2698 			"EditPropertiesMode", editPropertiesMode);
2699 	config_set_bool(App()->GlobalConfig(), "BasicWindow",
2700 			"PreviewProgramMode", IsPreviewProgramMode());
2701 	config_set_bool(App()->GlobalConfig(), "BasicWindow", "DocksLocked",
2702 			ui->lockUI->isChecked());
2703 	config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
2704 
2705 #ifdef _WIN32
2706 	uint32_t winVer = GetWindowsVersion();
2707 	if (winVer > 0 && winVer < 0x602) {
2708 		bool disableAero =
2709 			config_get_bool(basicConfig, "Video", "DisableAero");
2710 		if (disableAero) {
2711 			SetAeroEnabled(true);
2712 		}
2713 	}
2714 #endif
2715 
2716 #ifdef BROWSER_AVAILABLE
2717 	DestroyPanelCookieManager();
2718 	delete cef;
2719 	cef = nullptr;
2720 #endif
2721 }
2722 
2723 void OBSBasic::SaveProjectNow()
2724 {
2725 	if (disableSaving)
2726 		return;
2727 
2728 	projectChanged = true;
2729 	SaveProjectDeferred();
2730 }
2731 
2732 void OBSBasic::SaveProject()
2733 {
2734 	if (disableSaving)
2735 		return;
2736 
2737 	projectChanged = true;
2738 	QMetaObject::invokeMethod(this, "SaveProjectDeferred",
2739 				  Qt::QueuedConnection);
2740 }
2741 
2742 void OBSBasic::SaveProjectDeferred()
2743 {
2744 	if (disableSaving)
2745 		return;
2746 
2747 	if (!projectChanged)
2748 		return;
2749 
2750 	projectChanged = false;
2751 
2752 	const char *sceneCollection = config_get_string(
2753 		App()->GlobalConfig(), "Basic", "SceneCollectionFile");
2754 
2755 	char savePath[1024];
2756 	char fileName[1024];
2757 	int ret;
2758 
2759 	if (!sceneCollection)
2760 		return;
2761 
2762 	ret = snprintf(fileName, sizeof(fileName),
2763 		       "obs-studio/basic/scenes/%s.json", sceneCollection);
2764 	if (ret <= 0)
2765 		return;
2766 
2767 	ret = GetConfigPath(savePath, sizeof(savePath), fileName);
2768 	if (ret <= 0)
2769 		return;
2770 
2771 	Save(savePath);
2772 }
2773 
2774 OBSSource OBSBasic::GetProgramSource()
2775 {
2776 	return OBSGetStrongRef(programScene);
2777 }
2778 
2779 OBSScene OBSBasic::GetCurrentScene()
2780 {
2781 	QListWidgetItem *item = ui->scenes->currentItem();
2782 	return item ? GetOBSRef<OBSScene>(item) : nullptr;
2783 }
2784 
2785 OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item)
2786 {
2787 	return item ? GetOBSRef<OBSSceneItem>(item) : nullptr;
2788 }
2789 
2790 OBSSceneItem OBSBasic::GetCurrentSceneItem()
2791 {
2792 	return ui->sources->Get(GetTopSelectedSourceItem());
2793 }
2794 
2795 void OBSBasic::UpdatePreviewScalingMenu()
2796 {
2797 	bool fixedScaling = ui->preview->IsFixedScaling();
2798 	float scalingAmount = ui->preview->GetScalingAmount();
2799 	if (!fixedScaling) {
2800 		ui->actionScaleWindow->setChecked(true);
2801 		ui->actionScaleCanvas->setChecked(false);
2802 		ui->actionScaleOutput->setChecked(false);
2803 		return;
2804 	}
2805 
2806 	obs_video_info ovi;
2807 	obs_get_video_info(&ovi);
2808 
2809 	ui->actionScaleWindow->setChecked(false);
2810 	ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f);
2811 	ui->actionScaleOutput->setChecked(scalingAmount ==
2812 					  float(ovi.output_width) /
2813 						  float(ovi.base_width));
2814 }
2815 
2816 void OBSBasic::CreateInteractionWindow(obs_source_t *source)
2817 {
2818 	bool closed = true;
2819 	if (interaction)
2820 		closed = interaction->close();
2821 
2822 	if (!closed)
2823 		return;
2824 
2825 	interaction = new OBSBasicInteraction(this, source);
2826 	interaction->Init();
2827 	interaction->setAttribute(Qt::WA_DeleteOnClose, true);
2828 }
2829 
2830 void OBSBasic::CreatePropertiesWindow(obs_source_t *source)
2831 {
2832 	bool closed = true;
2833 	if (properties)
2834 		closed = properties->close();
2835 
2836 	if (!closed)
2837 		return;
2838 
2839 	properties = new OBSBasicProperties(this, source);
2840 	properties->Init();
2841 	properties->setAttribute(Qt::WA_DeleteOnClose, true);
2842 }
2843 
2844 void OBSBasic::CreateFiltersWindow(obs_source_t *source)
2845 {
2846 	bool closed = true;
2847 	if (filters)
2848 		closed = filters->close();
2849 
2850 	if (!closed)
2851 		return;
2852 
2853 	filters = new OBSBasicFilters(this, source);
2854 	filters->Init();
2855 	filters->setAttribute(Qt::WA_DeleteOnClose, true);
2856 }
2857 
2858 /* Qt callbacks for invokeMethod */
2859 
2860 void OBSBasic::AddScene(OBSSource source)
2861 {
2862 	const char *name = obs_source_get_name(source);
2863 	obs_scene_t *scene = obs_scene_from_source(source);
2864 
2865 	QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name));
2866 	SetOBSRef(item, OBSScene(scene));
2867 	ui->scenes->addItem(item);
2868 
2869 	obs_hotkey_register_source(
2870 		source, "OBSBasic.SelectScene",
2871 		Str("Basic.Hotkeys.SelectScene"),
2872 		[](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
2873 			OBSBasic *main = reinterpret_cast<OBSBasic *>(
2874 				App()->GetMainWindow());
2875 
2876 			auto potential_source =
2877 				static_cast<obs_source_t *>(data);
2878 			auto source = obs_source_get_ref(potential_source);
2879 			if (source && pressed)
2880 				main->SetCurrentScene(source);
2881 			obs_source_release(source);
2882 		},
2883 		static_cast<obs_source_t *>(source));
2884 
2885 	signal_handler_t *handler = obs_source_get_signal_handler(source);
2886 
2887 	SignalContainer<OBSScene> container;
2888 	container.ref = scene;
2889 	container.handlers.assign({
2890 		std::make_shared<OBSSignal>(handler, "item_add",
2891 					    OBSBasic::SceneItemAdded, this),
2892 		std::make_shared<OBSSignal>(handler, "reorder",
2893 					    OBSBasic::SceneReordered, this),
2894 		std::make_shared<OBSSignal>(handler, "refresh",
2895 					    OBSBasic::SceneRefreshed, this),
2896 	});
2897 
2898 	item->setData(static_cast<int>(QtDataRole::OBSSignals),
2899 		      QVariant::fromValue(container));
2900 
2901 	/* if the scene already has items (a duplicated scene) add them */
2902 	auto addSceneItem = [this](obs_sceneitem_t *item) {
2903 		AddSceneItem(item);
2904 	};
2905 
2906 	using addSceneItem_t = decltype(addSceneItem);
2907 
2908 	obs_scene_enum_items(
2909 		scene,
2910 		[](obs_scene_t *, obs_sceneitem_t *item, void *param) {
2911 			addSceneItem_t *func;
2912 			func = reinterpret_cast<addSceneItem_t *>(param);
2913 			(*func)(item);
2914 			return true;
2915 		},
2916 		&addSceneItem);
2917 
2918 	SaveProject();
2919 
2920 	if (!disableSaving) {
2921 		obs_source_t *source = obs_scene_get_source(scene);
2922 		blog(LOG_INFO, "User added scene '%s'",
2923 		     obs_source_get_name(source));
2924 
2925 		OBSProjector::UpdateMultiviewProjectors();
2926 	}
2927 
2928 	if (api)
2929 		api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
2930 }
2931 
2932 void OBSBasic::RemoveScene(OBSSource source)
2933 {
2934 	obs_scene_t *scene = obs_scene_from_source(source);
2935 
2936 	QListWidgetItem *sel = nullptr;
2937 	int count = ui->scenes->count();
2938 
2939 	for (int i = 0; i < count; i++) {
2940 		auto item = ui->scenes->item(i);
2941 		auto cur_scene = GetOBSRef<OBSScene>(item);
2942 		if (cur_scene != scene)
2943 			continue;
2944 
2945 		sel = item;
2946 		break;
2947 	}
2948 
2949 	if (sel != nullptr) {
2950 		if (sel == ui->scenes->currentItem())
2951 			ui->sources->Clear();
2952 		delete sel;
2953 	}
2954 
2955 	SaveProject();
2956 
2957 	if (!disableSaving) {
2958 		blog(LOG_INFO, "User Removed scene '%s'",
2959 		     obs_source_get_name(source));
2960 
2961 		OBSProjector::UpdateMultiviewProjectors();
2962 	}
2963 
2964 	if (api)
2965 		api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
2966 }
2967 
2968 static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
2969 {
2970 	obs_sceneitem_t *selectedItem =
2971 		reinterpret_cast<obs_sceneitem_t *>(param);
2972 	if (obs_sceneitem_is_group(item))
2973 		obs_sceneitem_group_enum_items(item, select_one, param);
2974 
2975 	obs_sceneitem_select(item, (selectedItem == item));
2976 
2977 	UNUSED_PARAMETER(scene);
2978 	return true;
2979 }
2980 
2981 void OBSBasic::AddSceneItem(OBSSceneItem item)
2982 {
2983 	obs_scene_t *scene = obs_sceneitem_get_scene(item);
2984 
2985 	if (GetCurrentScene() == scene)
2986 		ui->sources->Add(item);
2987 
2988 	SaveProject();
2989 
2990 	if (!disableSaving) {
2991 		obs_source_t *sceneSource = obs_scene_get_source(scene);
2992 		obs_source_t *itemSource = obs_sceneitem_get_source(item);
2993 		blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'",
2994 		     obs_source_get_name(itemSource),
2995 		     obs_source_get_id(itemSource),
2996 		     obs_source_get_name(sceneSource));
2997 
2998 		obs_scene_enum_items(scene, select_one,
2999 				     (obs_sceneitem_t *)item);
3000 	}
3001 }
3002 
3003 static void RenameListValues(QListWidget *listWidget, const QString &newName,
3004 			     const QString &prevName)
3005 {
3006 	QList<QListWidgetItem *> items =
3007 		listWidget->findItems(prevName, Qt::MatchExactly);
3008 
3009 	for (int i = 0; i < items.count(); i++)
3010 		items[i]->setText(newName);
3011 }
3012 
3013 void OBSBasic::RenameSources(OBSSource source, QString newName,
3014 			     QString prevName)
3015 {
3016 	RenameListValues(ui->scenes, newName, prevName);
3017 
3018 	for (size_t i = 0; i < volumes.size(); i++) {
3019 		if (volumes[i]->GetName().compare(prevName) == 0)
3020 			volumes[i]->SetName(newName);
3021 	}
3022 
3023 	for (size_t i = 0; i < projectors.size(); i++) {
3024 		if (projectors[i]->GetSource() == source)
3025 			projectors[i]->RenameProjector(prevName, newName);
3026 	}
3027 
3028 	SaveProject();
3029 
3030 	obs_scene_t *scene = obs_scene_from_source(source);
3031 	if (scene)
3032 		OBSProjector::UpdateMultiviewProjectors();
3033 
3034 	UpdateContextBar();
3035 }
3036 
3037 void OBSBasic::ClearContextBar()
3038 {
3039 	QLayoutItem *la = ui->emptySpace->layout()->itemAt(0);
3040 	if (la) {
3041 		delete la->widget();
3042 		ui->emptySpace->layout()->removeItem(la);
3043 	}
3044 }
3045 
3046 static bool is_network_media_source(obs_source_t *source, const char *id)
3047 {
3048 	if (strcmp(id, "ffmpeg_source") != 0)
3049 		return false;
3050 
3051 	obs_data_t *s = obs_source_get_settings(source);
3052 	bool is_local_file = obs_data_get_bool(s, "is_local_file");
3053 	obs_data_release(s);
3054 
3055 	return !is_local_file;
3056 }
3057 
3058 void OBSBasic::UpdateContextBarDeferred(bool force)
3059 {
3060 	QMetaObject::invokeMethod(this, "UpdateContextBar",
3061 				  Qt::QueuedConnection, Q_ARG(bool, force));
3062 }
3063 
3064 void OBSBasic::UpdateContextBar(bool force)
3065 {
3066 	if (!ui->contextContainer->isVisible() && !force)
3067 		return;
3068 
3069 	OBSSceneItem item = GetCurrentSceneItem();
3070 
3071 	ClearContextBar();
3072 
3073 	if (item) {
3074 		obs_source_t *source = obs_sceneitem_get_source(item);
3075 		const char *id = obs_source_get_unversioned_id(source);
3076 		uint32_t flags = obs_source_get_output_flags(source);
3077 
3078 		ui->sourceInteractButton->setVisible(flags &
3079 						     OBS_SOURCE_INTERACTION);
3080 
3081 		if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) {
3082 			if (!is_network_media_source(source, id)) {
3083 				MediaControls *mediaControls =
3084 					new MediaControls(ui->emptySpace);
3085 				mediaControls->SetSource(source);
3086 
3087 				ui->emptySpace->layout()->addWidget(
3088 					mediaControls);
3089 			}
3090 		} else if (strcmp(id, "browser_source") == 0) {
3091 			BrowserToolbar *c =
3092 				new BrowserToolbar(ui->emptySpace, source);
3093 			ui->emptySpace->layout()->addWidget(c);
3094 
3095 		} else if (strcmp(id, "wasapi_input_capture") == 0 ||
3096 			   strcmp(id, "wasapi_output_capture") == 0 ||
3097 			   strcmp(id, "coreaudio_input_capture") == 0 ||
3098 			   strcmp(id, "coreaudio_output_capture") == 0 ||
3099 			   strcmp(id, "pulse_input_capture") == 0 ||
3100 			   strcmp(id, "pulse_output_capture") == 0 ||
3101 			   strcmp(id, "alsa_input_capture") == 0) {
3102 			AudioCaptureToolbar *c =
3103 				new AudioCaptureToolbar(ui->emptySpace, source);
3104 			c->Init();
3105 			ui->emptySpace->layout()->addWidget(c);
3106 
3107 		} else if (strcmp(id, "window_capture") == 0 ||
3108 			   strcmp(id, "xcomposite_input") == 0) {
3109 			WindowCaptureToolbar *c = new WindowCaptureToolbar(
3110 				ui->emptySpace, source);
3111 			c->Init();
3112 			ui->emptySpace->layout()->addWidget(c);
3113 
3114 		} else if (strcmp(id, "monitor_capture") == 0 ||
3115 			   strcmp(id, "display_capture") == 0 ||
3116 			   strcmp(id, "xshm_input") == 0) {
3117 			DisplayCaptureToolbar *c = new DisplayCaptureToolbar(
3118 				ui->emptySpace, source);
3119 			c->Init();
3120 			ui->emptySpace->layout()->addWidget(c);
3121 
3122 		} else if (strcmp(id, "dshow_input") == 0) {
3123 			DeviceCaptureToolbar *c = new DeviceCaptureToolbar(
3124 				ui->emptySpace, source);
3125 			ui->emptySpace->layout()->addWidget(c);
3126 
3127 		} else if (strcmp(id, "game_capture") == 0) {
3128 			GameCaptureToolbar *c =
3129 				new GameCaptureToolbar(ui->emptySpace, source);
3130 			ui->emptySpace->layout()->addWidget(c);
3131 
3132 		} else if (strcmp(id, "image_source") == 0) {
3133 			ImageSourceToolbar *c =
3134 				new ImageSourceToolbar(ui->emptySpace, source);
3135 			ui->emptySpace->layout()->addWidget(c);
3136 
3137 		} else if (strcmp(id, "color_source") == 0) {
3138 			ColorSourceToolbar *c =
3139 				new ColorSourceToolbar(ui->emptySpace, source);
3140 			ui->emptySpace->layout()->addWidget(c);
3141 
3142 		} else if (strcmp(id, "text_ft2_source") == 0 ||
3143 			   strcmp(id, "text_gdiplus") == 0) {
3144 			TextSourceToolbar *c =
3145 				new TextSourceToolbar(ui->emptySpace, source);
3146 			ui->emptySpace->layout()->addWidget(c);
3147 		}
3148 
3149 		QIcon icon;
3150 
3151 		if (strcmp(id, "scene") == 0)
3152 			icon = GetSceneIcon();
3153 		else if (strcmp(id, "group") == 0)
3154 			icon = GetGroupIcon();
3155 		else
3156 			icon = GetSourceIcon(id);
3157 
3158 		QPixmap pixmap = icon.pixmap(QSize(16, 16));
3159 		ui->contextSourceIcon->setPixmap(pixmap);
3160 		ui->contextSourceIconSpacer->hide();
3161 		ui->contextSourceIcon->show();
3162 
3163 		const char *name = obs_source_get_name(source);
3164 		ui->contextSourceLabel->setText(name);
3165 
3166 		ui->sourceFiltersButton->setEnabled(true);
3167 		ui->sourcePropertiesButton->setEnabled(
3168 			obs_source_configurable(source));
3169 	} else {
3170 		ui->contextSourceIcon->hide();
3171 		ui->contextSourceIconSpacer->show();
3172 		ui->contextSourceLabel->setText(
3173 			QTStr("ContextBar.NoSelectedSource"));
3174 
3175 		ui->sourceFiltersButton->setEnabled(false);
3176 		ui->sourcePropertiesButton->setEnabled(false);
3177 		ui->sourceInteractButton->setVisible(false);
3178 	}
3179 }
3180 
3181 static inline bool SourceMixerHidden(obs_source_t *source)
3182 {
3183 	obs_data_t *priv_settings = obs_source_get_private_settings(source);
3184 	bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden");
3185 	obs_data_release(priv_settings);
3186 
3187 	return hidden;
3188 }
3189 
3190 static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden)
3191 {
3192 	obs_data_t *priv_settings = obs_source_get_private_settings(source);
3193 	obs_data_set_bool(priv_settings, "mixer_hidden", hidden);
3194 	obs_data_release(priv_settings);
3195 }
3196 
3197 void OBSBasic::GetAudioSourceFilters()
3198 {
3199 	QAction *action = reinterpret_cast<QAction *>(sender());
3200 	VolControl *vol = action->property("volControl").value<VolControl *>();
3201 	obs_source_t *source = vol->GetSource();
3202 
3203 	CreateFiltersWindow(source);
3204 }
3205 
3206 void OBSBasic::GetAudioSourceProperties()
3207 {
3208 	QAction *action = reinterpret_cast<QAction *>(sender());
3209 	VolControl *vol = action->property("volControl").value<VolControl *>();
3210 	obs_source_t *source = vol->GetSource();
3211 
3212 	CreatePropertiesWindow(source);
3213 }
3214 
3215 void OBSBasic::HideAudioControl()
3216 {
3217 	QAction *action = reinterpret_cast<QAction *>(sender());
3218 	VolControl *vol = action->property("volControl").value<VolControl *>();
3219 	obs_source_t *source = vol->GetSource();
3220 
3221 	if (!SourceMixerHidden(source)) {
3222 		SetSourceMixerHidden(source, true);
3223 		DeactivateAudioSource(source);
3224 	}
3225 }
3226 
3227 void OBSBasic::UnhideAllAudioControls()
3228 {
3229 	auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */
3230 	{
3231 		if (!obs_source_active(source))
3232 			return true;
3233 		if (!SourceMixerHidden(source))
3234 			return true;
3235 
3236 		SetSourceMixerHidden(source, false);
3237 		ActivateAudioSource(source);
3238 		return true;
3239 	};
3240 
3241 	using UnhideAudioMixer_t = decltype(UnhideAudioMixer);
3242 
3243 	auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */
3244 	{ return (*reinterpret_cast<UnhideAudioMixer_t *>(data))(source); };
3245 
3246 	obs_enum_sources(PreEnum, &UnhideAudioMixer);
3247 }
3248 
3249 void OBSBasic::ToggleHideMixer()
3250 {
3251 	OBSSceneItem item = GetCurrentSceneItem();
3252 	OBSSource source = obs_sceneitem_get_source(item);
3253 
3254 	if (!SourceMixerHidden(source)) {
3255 		SetSourceMixerHidden(source, true);
3256 		DeactivateAudioSource(source);
3257 	} else {
3258 		SetSourceMixerHidden(source, false);
3259 		ActivateAudioSource(source);
3260 	}
3261 }
3262 
3263 void OBSBasic::MixerRenameSource()
3264 {
3265 	QAction *action = reinterpret_cast<QAction *>(sender());
3266 	VolControl *vol = action->property("volControl").value<VolControl *>();
3267 	OBSSource source = vol->GetSource();
3268 
3269 	const char *prevName = obs_source_get_name(source);
3270 
3271 	for (;;) {
3272 		string name;
3273 		bool accepted = NameDialog::AskForName(
3274 			this, QTStr("Basic.Main.MixerRename.Title"),
3275 			QTStr("Basic.Main.MixerRename.Text"), name,
3276 			QT_UTF8(prevName));
3277 		if (!accepted)
3278 			return;
3279 
3280 		if (name.empty()) {
3281 			OBSMessageBox::warning(this,
3282 					       QTStr("NoNameEntered.Title"),
3283 					       QTStr("NoNameEntered.Text"));
3284 			continue;
3285 		}
3286 
3287 		OBSSource sourceTest = obs_get_source_by_name(name.c_str());
3288 		obs_source_release(sourceTest);
3289 
3290 		if (sourceTest) {
3291 			OBSMessageBox::warning(this, QTStr("NameExists.Title"),
3292 					       QTStr("NameExists.Text"));
3293 			continue;
3294 		}
3295 
3296 		obs_source_set_name(source, name.c_str());
3297 		break;
3298 	}
3299 }
3300 
3301 static inline bool SourceVolumeLocked(obs_source_t *source)
3302 {
3303 	obs_data_t *priv_settings = obs_source_get_private_settings(source);
3304 	bool lock = obs_data_get_bool(priv_settings, "volume_locked");
3305 	obs_data_release(priv_settings);
3306 
3307 	return lock;
3308 }
3309 
3310 void OBSBasic::LockVolumeControl(bool lock)
3311 {
3312 	QAction *action = reinterpret_cast<QAction *>(sender());
3313 	VolControl *vol = action->property("volControl").value<VolControl *>();
3314 	obs_source_t *source = vol->GetSource();
3315 
3316 	obs_data_t *priv_settings = obs_source_get_private_settings(source);
3317 	obs_data_set_bool(priv_settings, "volume_locked", lock);
3318 	obs_data_release(priv_settings);
3319 
3320 	vol->EnableSlider(!lock);
3321 }
3322 
3323 void OBSBasic::VolControlContextMenu()
3324 {
3325 	VolControl *vol = reinterpret_cast<VolControl *>(sender());
3326 
3327 	/* ------------------- */
3328 
3329 	QAction lockAction(QTStr("LockVolume"), this);
3330 	lockAction.setCheckable(true);
3331 	lockAction.setChecked(SourceVolumeLocked(vol->GetSource()));
3332 
3333 	QAction hideAction(QTStr("Hide"), this);
3334 	QAction unhideAllAction(QTStr("UnhideAll"), this);
3335 	QAction mixerRenameAction(QTStr("Rename"), this);
3336 
3337 	QAction copyFiltersAction(QTStr("Copy.Filters"), this);
3338 	QAction pasteFiltersAction(QTStr("Paste.Filters"), this);
3339 
3340 	QAction filtersAction(QTStr("Filters"), this);
3341 	QAction propertiesAction(QTStr("Properties"), this);
3342 	QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this);
3343 
3344 	QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this);
3345 	toggleControlLayoutAction.setCheckable(true);
3346 	toggleControlLayoutAction.setChecked(config_get_bool(
3347 		GetGlobalConfig(), "BasicWindow", "VerticalVolControl"));
3348 
3349 	/* ------------------- */
3350 
3351 	connect(&hideAction, &QAction::triggered, this,
3352 		&OBSBasic::HideAudioControl, Qt::DirectConnection);
3353 	connect(&unhideAllAction, &QAction::triggered, this,
3354 		&OBSBasic::UnhideAllAudioControls, Qt::DirectConnection);
3355 	connect(&lockAction, &QAction::toggled, this,
3356 		&OBSBasic::LockVolumeControl, Qt::DirectConnection);
3357 	connect(&mixerRenameAction, &QAction::triggered, this,
3358 		&OBSBasic::MixerRenameSource, Qt::DirectConnection);
3359 
3360 	connect(&copyFiltersAction, &QAction::triggered, this,
3361 		&OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection);
3362 	connect(&pasteFiltersAction, &QAction::triggered, this,
3363 		&OBSBasic::AudioMixerPasteFilters, Qt::DirectConnection);
3364 
3365 	connect(&filtersAction, &QAction::triggered, this,
3366 		&OBSBasic::GetAudioSourceFilters, Qt::DirectConnection);
3367 	connect(&propertiesAction, &QAction::triggered, this,
3368 		&OBSBasic::GetAudioSourceProperties, Qt::DirectConnection);
3369 	connect(&advPropAction, &QAction::triggered, this,
3370 		&OBSBasic::on_actionAdvAudioProperties_triggered,
3371 		Qt::DirectConnection);
3372 
3373 	/* ------------------- */
3374 
3375 	connect(&toggleControlLayoutAction, &QAction::changed, this,
3376 		&OBSBasic::ToggleVolControlLayout, Qt::DirectConnection);
3377 
3378 	/* ------------------- */
3379 
3380 	hideAction.setProperty("volControl",
3381 			       QVariant::fromValue<VolControl *>(vol));
3382 	lockAction.setProperty("volControl",
3383 			       QVariant::fromValue<VolControl *>(vol));
3384 	mixerRenameAction.setProperty("volControl",
3385 				      QVariant::fromValue<VolControl *>(vol));
3386 
3387 	copyFiltersAction.setProperty("volControl",
3388 				      QVariant::fromValue<VolControl *>(vol));
3389 	pasteFiltersAction.setProperty("volControl",
3390 				       QVariant::fromValue<VolControl *>(vol));
3391 
3392 	filtersAction.setProperty("volControl",
3393 				  QVariant::fromValue<VolControl *>(vol));
3394 	propertiesAction.setProperty("volControl",
3395 				     QVariant::fromValue<VolControl *>(vol));
3396 
3397 	/* ------------------- */
3398 
3399 	copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) >
3400 				     0);
3401 
3402 	if (copyFiltersString == nullptr)
3403 		pasteFiltersAction.setEnabled(false);
3404 	else
3405 		pasteFiltersAction.setEnabled(true);
3406 
3407 	QMenu popup;
3408 	vol->SetContextMenu(&popup);
3409 	popup.addAction(&lockAction);
3410 	popup.addSeparator();
3411 	popup.addAction(&unhideAllAction);
3412 	popup.addAction(&hideAction);
3413 	popup.addAction(&mixerRenameAction);
3414 	popup.addSeparator();
3415 	popup.addAction(&copyFiltersAction);
3416 	popup.addAction(&pasteFiltersAction);
3417 	popup.addSeparator();
3418 	popup.addAction(&toggleControlLayoutAction);
3419 	popup.addSeparator();
3420 	popup.addAction(&filtersAction);
3421 	popup.addAction(&propertiesAction);
3422 	popup.addAction(&advPropAction);
3423 	popup.exec(QCursor::pos());
3424 	vol->SetContextMenu(nullptr);
3425 }
3426 
3427 void OBSBasic::on_hMixerScrollArea_customContextMenuRequested()
3428 {
3429 	StackedMixerAreaContextMenuRequested();
3430 }
3431 
3432 void OBSBasic::on_vMixerScrollArea_customContextMenuRequested()
3433 {
3434 	StackedMixerAreaContextMenuRequested();
3435 }
3436 
3437 void OBSBasic::StackedMixerAreaContextMenuRequested()
3438 {
3439 	QAction unhideAllAction(QTStr("UnhideAll"), this);
3440 
3441 	QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this);
3442 
3443 	QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this);
3444 	toggleControlLayoutAction.setCheckable(true);
3445 	toggleControlLayoutAction.setChecked(config_get_bool(
3446 		GetGlobalConfig(), "BasicWindow", "VerticalVolControl"));
3447 
3448 	/* ------------------- */
3449 
3450 	connect(&unhideAllAction, &QAction::triggered, this,
3451 		&OBSBasic::UnhideAllAudioControls, Qt::DirectConnection);
3452 
3453 	connect(&advPropAction, &QAction::triggered, this,
3454 		&OBSBasic::on_actionAdvAudioProperties_triggered,
3455 		Qt::DirectConnection);
3456 
3457 	/* ------------------- */
3458 
3459 	connect(&toggleControlLayoutAction, &QAction::changed, this,
3460 		&OBSBasic::ToggleVolControlLayout, Qt::DirectConnection);
3461 
3462 	/* ------------------- */
3463 
3464 	QMenu popup;
3465 	popup.addAction(&unhideAllAction);
3466 	popup.addSeparator();
3467 	popup.addAction(&toggleControlLayoutAction);
3468 	popup.addSeparator();
3469 	popup.addAction(&advPropAction);
3470 	popup.exec(QCursor::pos());
3471 }
3472 
3473 void OBSBasic::ToggleMixerLayout(bool vertical)
3474 {
3475 	if (vertical) {
3476 		ui->stackedMixerArea->setMinimumSize(180, 220);
3477 		ui->stackedMixerArea->setCurrentIndex(1);
3478 	} else {
3479 		ui->stackedMixerArea->setMinimumSize(220, 0);
3480 		ui->stackedMixerArea->setCurrentIndex(0);
3481 	}
3482 }
3483 
3484 void OBSBasic::ToggleVolControlLayout()
3485 {
3486 	bool vertical = !config_get_bool(GetGlobalConfig(), "BasicWindow",
3487 					 "VerticalVolControl");
3488 	config_set_bool(GetGlobalConfig(), "BasicWindow", "VerticalVolControl",
3489 			vertical);
3490 	ToggleMixerLayout(vertical);
3491 
3492 	// We need to store it so we can delete current and then add
3493 	// at the right order
3494 	vector<OBSSource> sources;
3495 	for (size_t i = 0; i != volumes.size(); i++)
3496 		sources.emplace_back(volumes[i]->GetSource());
3497 
3498 	ClearVolumeControls();
3499 
3500 	for (const auto &source : sources)
3501 		ActivateAudioSource(source);
3502 }
3503 
3504 void OBSBasic::ActivateAudioSource(OBSSource source)
3505 {
3506 	if (SourceMixerHidden(source))
3507 		return;
3508 	if (!obs_source_audio_active(source))
3509 		return;
3510 
3511 	bool vertical = config_get_bool(GetGlobalConfig(), "BasicWindow",
3512 					"VerticalVolControl");
3513 	VolControl *vol = new VolControl(source, true, vertical);
3514 
3515 	vol->EnableSlider(!SourceVolumeLocked(source));
3516 
3517 	double meterDecayRate =
3518 		config_get_double(basicConfig, "Audio", "MeterDecayRate");
3519 	vol->SetMeterDecayRate(meterDecayRate);
3520 
3521 	uint32_t peakMeterTypeIdx =
3522 		config_get_uint(basicConfig, "Audio", "PeakMeterType");
3523 
3524 	enum obs_peak_meter_type peakMeterType;
3525 	switch (peakMeterTypeIdx) {
3526 	case 0:
3527 		peakMeterType = SAMPLE_PEAK_METER;
3528 		break;
3529 	case 1:
3530 		peakMeterType = TRUE_PEAK_METER;
3531 		break;
3532 	default:
3533 		peakMeterType = SAMPLE_PEAK_METER;
3534 		break;
3535 	}
3536 
3537 	vol->setPeakMeterType(peakMeterType);
3538 
3539 	vol->setContextMenuPolicy(Qt::CustomContextMenu);
3540 
3541 	connect(vol, &QWidget::customContextMenuRequested, this,
3542 		&OBSBasic::VolControlContextMenu);
3543 	connect(vol, &VolControl::ConfigClicked, this,
3544 		&OBSBasic::VolControlContextMenu);
3545 
3546 	InsertQObjectByName(volumes, vol);
3547 
3548 	for (auto volume : volumes) {
3549 		if (vertical)
3550 			ui->vVolControlLayout->addWidget(volume);
3551 		else
3552 			ui->hVolControlLayout->addWidget(volume);
3553 	}
3554 }
3555 
3556 void OBSBasic::DeactivateAudioSource(OBSSource source)
3557 {
3558 	for (size_t i = 0; i < volumes.size(); i++) {
3559 		if (volumes[i]->GetSource() == source) {
3560 			delete volumes[i];
3561 			volumes.erase(volumes.begin() + i);
3562 			break;
3563 		}
3564 	}
3565 }
3566 
3567 bool OBSBasic::QueryRemoveSource(obs_source_t *source)
3568 {
3569 	if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE &&
3570 	    !obs_source_is_group(source)) {
3571 		int count = ui->scenes->count();
3572 
3573 		if (count == 1) {
3574 			OBSMessageBox::information(this,
3575 						   QTStr("FinalScene.Title"),
3576 						   QTStr("FinalScene.Text"));
3577 			return false;
3578 		}
3579 	}
3580 
3581 	const char *name = obs_source_get_name(source);
3582 
3583 	QString text = QTStr("ConfirmRemove.Text");
3584 	text.replace("$1", QT_UTF8(name));
3585 
3586 	QMessageBox remove_source(this);
3587 	remove_source.setText(text);
3588 	QPushButton *Yes =
3589 		remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole);
3590 	remove_source.setDefaultButton(Yes);
3591 	remove_source.addButton(QTStr("No"), QMessageBox::NoRole);
3592 	remove_source.setIcon(QMessageBox::Question);
3593 	remove_source.setWindowTitle(QTStr("ConfirmRemove.Title"));
3594 	remove_source.exec();
3595 
3596 	return Yes == remove_source.clickedButton();
3597 }
3598 
3599 #define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */
3600 
3601 #ifdef UPDATE_SPARKLE
3602 void init_sparkle_updater(bool update_to_undeployed);
3603 void trigger_sparkle_update();
3604 #endif
3605 
3606 void OBSBasic::TimedCheckForUpdates()
3607 {
3608 	if (App()->IsUpdaterDisabled())
3609 		return;
3610 	if (!config_get_bool(App()->GlobalConfig(), "General",
3611 			     "EnableAutoUpdates"))
3612 		return;
3613 
3614 #ifdef UPDATE_SPARKLE
3615 	init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General",
3616 					     "UpdateToUndeployed"));
3617 #elif _WIN32
3618 	long long lastUpdate = config_get_int(App()->GlobalConfig(), "General",
3619 					      "LastUpdateCheck");
3620 	uint32_t lastVersion =
3621 		config_get_int(App()->GlobalConfig(), "General", "LastVersion");
3622 
3623 	if (lastVersion < LIBOBS_API_VER) {
3624 		lastUpdate = 0;
3625 		config_set_int(App()->GlobalConfig(), "General",
3626 			       "LastUpdateCheck", 0);
3627 	}
3628 
3629 	long long t = (long long)time(nullptr);
3630 	long long secs = t - lastUpdate;
3631 
3632 	if (secs > UPDATE_CHECK_INTERVAL)
3633 		CheckForUpdates(false);
3634 #endif
3635 }
3636 
3637 void OBSBasic::CheckForUpdates(bool manualUpdate)
3638 {
3639 #ifdef __FreeBSD__
3640 	// Update check seg faults on FreeBSD
3641 	return;
3642 #endif
3643 #ifdef UPDATE_SPARKLE
3644 	trigger_sparkle_update();
3645 #elif _WIN32
3646 	ui->actionCheckForUpdates->setEnabled(false);
3647 
3648 	if (updateCheckThread && updateCheckThread->isRunning())
3649 		return;
3650 
3651 	updateCheckThread.reset(new AutoUpdateThread(manualUpdate));
3652 	updateCheckThread->start();
3653 #endif
3654 
3655 	UNUSED_PARAMETER(manualUpdate);
3656 }
3657 
3658 void OBSBasic::updateCheckFinished()
3659 {
3660 	ui->actionCheckForUpdates->setEnabled(true);
3661 }
3662 
3663 void OBSBasic::DuplicateSelectedScene()
3664 {
3665 	OBSScene curScene = GetCurrentScene();
3666 
3667 	if (!curScene)
3668 		return;
3669 
3670 	OBSSource curSceneSource = obs_scene_get_source(curScene);
3671 	QString format{obs_source_get_name(curSceneSource)};
3672 	format += " %1";
3673 
3674 	int i = 2;
3675 	QString placeHolderText = format.arg(i);
3676 	obs_source_t *source = nullptr;
3677 	while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) {
3678 		obs_source_release(source);
3679 		placeHolderText = format.arg(++i);
3680 	}
3681 
3682 	for (;;) {
3683 		string name;
3684 		bool accepted = NameDialog::AskForName(
3685 			this, QTStr("Basic.Main.AddSceneDlg.Title"),
3686 			QTStr("Basic.Main.AddSceneDlg.Text"), name,
3687 			placeHolderText);
3688 		if (!accepted)
3689 			return;
3690 
3691 		if (name.empty()) {
3692 			OBSMessageBox::warning(this,
3693 					       QTStr("NoNameEntered.Title"),
3694 					       QTStr("NoNameEntered.Text"));
3695 			continue;
3696 		}
3697 
3698 		obs_source_t *source = obs_get_source_by_name(name.c_str());
3699 		if (source) {
3700 			OBSMessageBox::warning(this, QTStr("NameExists.Title"),
3701 					       QTStr("NameExists.Text"));
3702 
3703 			obs_source_release(source);
3704 			continue;
3705 		}
3706 
3707 		obs_scene_t *scene = obs_scene_duplicate(curScene, name.c_str(),
3708 							 OBS_SCENE_DUP_REFS);
3709 		source = obs_scene_get_source(scene);
3710 		SetCurrentScene(source, true);
3711 
3712 		auto undo = [](const std::string &data) {
3713 			obs_source_t *source =
3714 				obs_get_source_by_name(data.c_str());
3715 			obs_source_remove(source);
3716 			obs_source_release(source);
3717 		};
3718 
3719 		auto redo = [this, name](const std::string &data) {
3720 			obs_source_t *source =
3721 				obs_get_source_by_name(data.c_str());
3722 			obs_scene_t *scene = obs_scene_from_source(source);
3723 			obs_source_release(source);
3724 			scene = obs_scene_duplicate(scene, name.c_str(),
3725 						    OBS_SCENE_DUP_REFS);
3726 			source = obs_scene_get_source(scene);
3727 			SetCurrentScene(source, true);
3728 			obs_scene_release(scene);
3729 		};
3730 
3731 		undo_s.add_action(
3732 			QTStr("Undo.Scene.Duplicate")
3733 				.arg(obs_source_get_name(source)),
3734 			undo, redo, obs_source_get_name(source),
3735 			obs_source_get_name(obs_scene_get_source(curScene)));
3736 
3737 		obs_scene_release(scene);
3738 
3739 		break;
3740 	}
3741 }
3742 
3743 static bool save_undo_source_enum(obs_scene_t *scene, obs_sceneitem_t *item,
3744 				  void *p)
3745 {
3746 	UNUSED_PARAMETER(scene);
3747 
3748 	obs_source_t *source = obs_sceneitem_get_source(item);
3749 	if (obs_obj_is_private(source) && !obs_source_removed(source))
3750 		return true;
3751 
3752 	obs_data_array_t *array = (obs_data_array_t *)p;
3753 
3754 	/* check if the source is already stored in the array */
3755 	const char *name = obs_source_get_name(source);
3756 	const size_t count = obs_data_array_count(array);
3757 	for (size_t i = 0; i < count; i++) {
3758 		obs_data_t *sourceData = obs_data_array_item(array, i);
3759 		if (strcmp(name, obs_data_get_string(sourceData, "name")) ==
3760 		    0) {
3761 			obs_data_release(sourceData);
3762 			return true;
3763 		}
3764 		obs_data_release(sourceData);
3765 	}
3766 
3767 	if (obs_source_is_group(source))
3768 		obs_scene_enum_items(obs_group_from_source(source),
3769 				     save_undo_source_enum, p);
3770 
3771 	obs_data_t *source_data = obs_save_source(source);
3772 	obs_data_array_push_back(array, source_data);
3773 	obs_data_release(source_data);
3774 	return true;
3775 }
3776 
3777 void OBSBasic::RemoveSelectedScene()
3778 {
3779 	OBSScene scene = GetCurrentScene();
3780 	obs_source_t *source = obs_scene_get_source(scene);
3781 
3782 	OBSSource curProgramScene = OBSGetStrongRef(programScene);
3783 
3784 	if (!source || !QueryRemoveSource(source)) {
3785 		return;
3786 	}
3787 
3788 	/* ------------------------------ */
3789 	/* save all sources in scene      */
3790 
3791 	obs_data_array_t *array = obs_data_array_create();
3792 
3793 	obs_scene_enum_items(scene, save_undo_source_enum, array);
3794 
3795 	obs_data_t *scene_data = obs_save_source(source);
3796 	obs_data_array_push_back(array, scene_data);
3797 	obs_data_release(scene_data);
3798 
3799 	/* ----------------------------------------------- */
3800 	/* save all scenes and groups the scene is used in */
3801 
3802 	for (int i = 0; i < ui->scenes->count(); i++) {
3803 		QListWidgetItem *widget_item = ui->scenes->item(i);
3804 		auto item_scene = GetOBSRef<OBSScene>(widget_item);
3805 		if (scene == item_scene)
3806 			continue;
3807 		auto *item = obs_scene_find_source_recursive(
3808 			item_scene, obs_source_get_name(source));
3809 		if (item) {
3810 			scene_data = obs_save_source(obs_scene_get_source(
3811 				obs_sceneitem_get_scene(item)));
3812 			obs_data_array_push_back(array, scene_data);
3813 			obs_data_release(scene_data);
3814 		}
3815 	}
3816 
3817 	/* --------------------------- */
3818 	/* undo/redo                   */
3819 
3820 	auto undo = [this](const std::string &json) {
3821 		obs_data_t *base = obs_data_create_from_json(json.c_str());
3822 		obs_data_array_t *array = obs_data_get_array(base, "array");
3823 		int savedIndex = (int)obs_data_get_int(base, "index");
3824 		std::vector<obs_source_t *> sources;
3825 
3826 		/* create missing sources */
3827 		size_t count = obs_data_array_count(array);
3828 		sources.reserve(count);
3829 
3830 		for (size_t i = 0; i < count; i++) {
3831 			obs_data_t *data = obs_data_array_item(array, i);
3832 			const char *name = obs_data_get_string(data, "name");
3833 
3834 			obs_source_t *source = obs_get_source_by_name(name);
3835 			if (!source)
3836 				source = obs_load_source(data);
3837 			sources.push_back(source);
3838 
3839 			obs_data_release(data);
3840 		}
3841 
3842 		/* actually load sources now */
3843 		for (obs_source_t *source : sources)
3844 			obs_source_load2(source);
3845 
3846 		obs_source_t *scene_source = sources.back();
3847 		OBSScene scene = obs_scene_from_source(scene_source);
3848 		SetCurrentScene(scene, true);
3849 
3850 		/* set original index in list box */
3851 		ui->scenes->blockSignals(true);
3852 		int curIndex = ui->scenes->currentRow();
3853 		QListWidgetItem *item = ui->scenes->takeItem(curIndex);
3854 		ui->scenes->insertItem(savedIndex, item);
3855 		ui->scenes->setCurrentRow(savedIndex);
3856 		ui->scenes->blockSignals(false);
3857 
3858 		/* release sources */
3859 		for (obs_source_t *source : sources)
3860 			obs_source_release(source);
3861 
3862 		obs_data_array_release(array);
3863 		obs_data_release(base);
3864 	};
3865 
3866 	auto redo = [](const std::string &name) {
3867 		obs_source_t *source = obs_get_source_by_name(name.c_str());
3868 		obs_source_remove(source);
3869 		obs_source_release(source);
3870 	};
3871 
3872 	obs_data_t *data = obs_data_create();
3873 	obs_data_set_array(data, "array", array);
3874 	obs_data_set_int(data, "index", ui->scenes->currentRow());
3875 
3876 	const char *scene_name = obs_source_get_name(source);
3877 	undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo,
3878 			  obs_data_get_json(data), scene_name);
3879 
3880 	obs_data_array_release(array);
3881 	obs_data_release(data);
3882 
3883 	/* --------------------------- */
3884 	/* remove                      */
3885 
3886 	obs_source_remove(source);
3887 
3888 	if (api)
3889 		api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
3890 }
3891 
3892 void OBSBasic::ReorderSources(OBSScene scene)
3893 {
3894 	if (scene != GetCurrentScene() || ui->sources->IgnoreReorder())
3895 		return;
3896 
3897 	ui->sources->ReorderItems();
3898 	SaveProject();
3899 }
3900 
3901 void OBSBasic::RefreshSources(OBSScene scene)
3902 {
3903 	if (scene != GetCurrentScene() || ui->sources->IgnoreReorder())
3904 		return;
3905 
3906 	ui->sources->RefreshItems();
3907 	SaveProject();
3908 }
3909 
3910 /* OBS Callbacks */
3911 
3912 void OBSBasic::SceneReordered(void *data, calldata_t *params)
3913 {
3914 	OBSBasic *window = static_cast<OBSBasic *>(data);
3915 
3916 	obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
3917 
3918 	QMetaObject::invokeMethod(window, "ReorderSources",
3919 				  Q_ARG(OBSScene, OBSScene(scene)));
3920 }
3921 
3922 void OBSBasic::SceneRefreshed(void *data, calldata_t *params)
3923 {
3924 	OBSBasic *window = static_cast<OBSBasic *>(data);
3925 
3926 	obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene");
3927 
3928 	QMetaObject::invokeMethod(window, "RefreshSources",
3929 				  Q_ARG(OBSScene, OBSScene(scene)));
3930 }
3931 
3932 void OBSBasic::SceneItemAdded(void *data, calldata_t *params)
3933 {
3934 	OBSBasic *window = static_cast<OBSBasic *>(data);
3935 
3936 	obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item");
3937 
3938 	QMetaObject::invokeMethod(window, "AddSceneItem",
3939 				  Q_ARG(OBSSceneItem, OBSSceneItem(item)));
3940 }
3941 
3942 void OBSBasic::SourceCreated(void *data, calldata_t *params)
3943 {
3944 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3945 
3946 	if (obs_scene_from_source(source) != NULL)
3947 		QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3948 					  "AddScene", WaitConnection(),
3949 					  Q_ARG(OBSSource, OBSSource(source)));
3950 }
3951 
3952 void OBSBasic::SourceRemoved(void *data, calldata_t *params)
3953 {
3954 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3955 
3956 	if (obs_scene_from_source(source) != NULL)
3957 		QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3958 					  "RemoveScene",
3959 					  Q_ARG(OBSSource, OBSSource(source)));
3960 }
3961 
3962 void OBSBasic::SourceActivated(void *data, calldata_t *params)
3963 {
3964 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3965 	uint32_t flags = obs_source_get_output_flags(source);
3966 
3967 	if (flags & OBS_SOURCE_AUDIO)
3968 		QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3969 					  "ActivateAudioSource",
3970 					  Q_ARG(OBSSource, OBSSource(source)));
3971 }
3972 
3973 void OBSBasic::SourceDeactivated(void *data, calldata_t *params)
3974 {
3975 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3976 	uint32_t flags = obs_source_get_output_flags(source);
3977 
3978 	if (flags & OBS_SOURCE_AUDIO)
3979 		QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3980 					  "DeactivateAudioSource",
3981 					  Q_ARG(OBSSource, OBSSource(source)));
3982 }
3983 
3984 void OBSBasic::SourceAudioActivated(void *data, calldata_t *params)
3985 {
3986 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3987 
3988 	if (obs_source_active(source))
3989 		QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3990 					  "ActivateAudioSource",
3991 					  Q_ARG(OBSSource, OBSSource(source)));
3992 }
3993 
3994 void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params)
3995 {
3996 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
3997 	QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
3998 				  "DeactivateAudioSource",
3999 				  Q_ARG(OBSSource, OBSSource(source)));
4000 }
4001 
4002 void OBSBasic::SourceRenamed(void *data, calldata_t *params)
4003 {
4004 	obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source");
4005 	const char *newName = calldata_string(params, "new_name");
4006 	const char *prevName = calldata_string(params, "prev_name");
4007 
4008 	QMetaObject::invokeMethod(static_cast<OBSBasic *>(data),
4009 				  "RenameSources", Q_ARG(OBSSource, source),
4010 				  Q_ARG(QString, QT_UTF8(newName)),
4011 				  Q_ARG(QString, QT_UTF8(prevName)));
4012 
4013 	blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName);
4014 }
4015 
4016 void OBSBasic::DrawBackdrop(float cx, float cy)
4017 {
4018 	if (!box)
4019 		return;
4020 
4021 	GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop");
4022 
4023 	gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
4024 	gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color");
4025 	gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
4026 
4027 	vec4 colorVal;
4028 	vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f);
4029 	gs_effect_set_vec4(color, &colorVal);
4030 
4031 	gs_technique_begin(tech);
4032 	gs_technique_begin_pass(tech, 0);
4033 	gs_matrix_push();
4034 	gs_matrix_identity();
4035 	gs_matrix_scale3f(float(cx), float(cy), 1.0f);
4036 
4037 	gs_load_vertexbuffer(box);
4038 	gs_draw(GS_TRISTRIP, 0, 0);
4039 
4040 	gs_matrix_pop();
4041 	gs_technique_end_pass(tech);
4042 	gs_technique_end(tech);
4043 
4044 	gs_load_vertexbuffer(nullptr);
4045 
4046 	GS_DEBUG_MARKER_END();
4047 }
4048 
4049 void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy)
4050 {
4051 	GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain");
4052 
4053 	OBSBasic *window = static_cast<OBSBasic *>(data);
4054 	obs_video_info ovi;
4055 
4056 	obs_get_video_info(&ovi);
4057 
4058 	window->previewCX = int(window->previewScale * float(ovi.base_width));
4059 	window->previewCY = int(window->previewScale * float(ovi.base_height));
4060 
4061 	gs_viewport_push();
4062 	gs_projection_push();
4063 
4064 	obs_display_t *display = window->ui->preview->GetDisplay();
4065 	uint32_t width, height;
4066 	obs_display_size(display, &width, &height);
4067 	float right = float(width) - window->previewX;
4068 	float bottom = float(height) - window->previewY;
4069 
4070 	gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f,
4071 		 100.0f);
4072 
4073 	window->ui->preview->DrawOverflow();
4074 
4075 	/* --------------------------------------- */
4076 
4077 	gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height),
4078 		 -100.0f, 100.0f);
4079 	gs_set_viewport(window->previewX, window->previewY, window->previewCX,
4080 			window->previewCY);
4081 
4082 	if (window->IsPreviewProgramMode()) {
4083 		window->DrawBackdrop(float(ovi.base_width),
4084 				     float(ovi.base_height));
4085 
4086 		OBSScene scene = window->GetCurrentScene();
4087 		obs_source_t *source = obs_scene_get_source(scene);
4088 		if (source)
4089 			obs_source_video_render(source);
4090 	} else {
4091 		obs_render_main_texture_src_color_only();
4092 	}
4093 	gs_load_vertexbuffer(nullptr);
4094 
4095 	/* --------------------------------------- */
4096 
4097 	gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f,
4098 		 100.0f);
4099 	gs_reset_viewport();
4100 
4101 	window->ui->preview->DrawSceneEditing();
4102 
4103 	uint32_t targetCX = window->previewCX;
4104 	uint32_t targetCY = window->previewCY;
4105 
4106 	if (window->drawSafeAreas) {
4107 		RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY);
4108 		RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY);
4109 		RenderSafeAreas(window->fourByThreeSafeMargin, targetCX,
4110 				targetCY);
4111 		RenderSafeAreas(window->leftLine, targetCX, targetCY);
4112 		RenderSafeAreas(window->topLine, targetCX, targetCY);
4113 		RenderSafeAreas(window->rightLine, targetCX, targetCY);
4114 	}
4115 
4116 	/* --------------------------------------- */
4117 
4118 	gs_projection_pop();
4119 	gs_viewport_pop();
4120 
4121 	GS_DEBUG_MARKER_END();
4122 
4123 	UNUSED_PARAMETER(cx);
4124 	UNUSED_PARAMETER(cy);
4125 }
4126 
4127 /* Main class functions */
4128 
4129 obs_service_t *OBSBasic::GetService()
4130 {
4131 	if (!service) {
4132 		service =
4133 			obs_service_create("rtmp_common", NULL, NULL, nullptr);
4134 		obs_service_release(service);
4135 	}
4136 	return service;
4137 }
4138 
4139 void OBSBasic::SetService(obs_service_t *newService)
4140 {
4141 	if (newService)
4142 		service = newService;
4143 }
4144 
4145 int OBSBasic::GetTransitionDuration()
4146 {
4147 	return ui->transitionDuration->value();
4148 }
4149 
4150 bool OBSBasic::StreamingActive() const
4151 {
4152 	if (!outputHandler)
4153 		return false;
4154 	return outputHandler->StreamingActive();
4155 }
4156 
4157 bool OBSBasic::Active() const
4158 {
4159 	if (!outputHandler)
4160 		return false;
4161 	return outputHandler->Active();
4162 }
4163 
4164 #ifdef _WIN32
4165 #define IS_WIN32 1
4166 #else
4167 #define IS_WIN32 0
4168 #endif
4169 
4170 static inline int AttemptToResetVideo(struct obs_video_info *ovi)
4171 {
4172 	return obs_reset_video(ovi);
4173 }
4174 
4175 static inline enum obs_scale_type GetScaleType(ConfigFile &basicConfig)
4176 {
4177 	const char *scaleTypeStr =
4178 		config_get_string(basicConfig, "Video", "ScaleType");
4179 
4180 	if (astrcmpi(scaleTypeStr, "bilinear") == 0)
4181 		return OBS_SCALE_BILINEAR;
4182 	else if (astrcmpi(scaleTypeStr, "lanczos") == 0)
4183 		return OBS_SCALE_LANCZOS;
4184 	else if (astrcmpi(scaleTypeStr, "area") == 0)
4185 		return OBS_SCALE_AREA;
4186 	else
4187 		return OBS_SCALE_BICUBIC;
4188 }
4189 
4190 static inline enum video_format GetVideoFormatFromName(const char *name)
4191 {
4192 	if (astrcmpi(name, "I420") == 0)
4193 		return VIDEO_FORMAT_I420;
4194 	else if (astrcmpi(name, "NV12") == 0)
4195 		return VIDEO_FORMAT_NV12;
4196 	else if (astrcmpi(name, "I444") == 0)
4197 		return VIDEO_FORMAT_I444;
4198 #if 0 //currently unsupported
4199 	else if (astrcmpi(name, "YVYU") == 0)
4200 		return VIDEO_FORMAT_YVYU;
4201 	else if (astrcmpi(name, "YUY2") == 0)
4202 		return VIDEO_FORMAT_YUY2;
4203 	else if (astrcmpi(name, "UYVY") == 0)
4204 		return VIDEO_FORMAT_UYVY;
4205 #endif
4206 	else
4207 		return VIDEO_FORMAT_RGBA;
4208 }
4209 
4210 void OBSBasic::ResetUI()
4211 {
4212 	bool studioPortraitLayout = config_get_bool(
4213 		GetGlobalConfig(), "BasicWindow", "StudioPortraitLayout");
4214 
4215 	bool labels = config_get_bool(GetGlobalConfig(), "BasicWindow",
4216 				      "StudioModeLabels");
4217 
4218 	if (studioPortraitLayout)
4219 		ui->previewLayout->setDirection(QBoxLayout::TopToBottom);
4220 	else
4221 		ui->previewLayout->setDirection(QBoxLayout::LeftToRight);
4222 
4223 	if (previewProgramMode)
4224 		ui->previewLabel->setHidden(!labels);
4225 
4226 	if (programLabel)
4227 		programLabel->setHidden(!labels);
4228 }
4229 
4230 int OBSBasic::ResetVideo()
4231 {
4232 	if (outputHandler && outputHandler->Active())
4233 		return OBS_VIDEO_CURRENTLY_ACTIVE;
4234 
4235 	ProfileScope("OBSBasic::ResetVideo");
4236 
4237 	struct obs_video_info ovi;
4238 	int ret;
4239 
4240 	GetConfigFPS(ovi.fps_num, ovi.fps_den);
4241 
4242 	const char *colorFormat =
4243 		config_get_string(basicConfig, "Video", "ColorFormat");
4244 	const char *colorSpace =
4245 		config_get_string(basicConfig, "Video", "ColorSpace");
4246 	const char *colorRange =
4247 		config_get_string(basicConfig, "Video", "ColorRange");
4248 
4249 	ovi.graphics_module = App()->GetRenderModule();
4250 	ovi.base_width =
4251 		(uint32_t)config_get_uint(basicConfig, "Video", "BaseCX");
4252 	ovi.base_height =
4253 		(uint32_t)config_get_uint(basicConfig, "Video", "BaseCY");
4254 	ovi.output_width =
4255 		(uint32_t)config_get_uint(basicConfig, "Video", "OutputCX");
4256 	ovi.output_height =
4257 		(uint32_t)config_get_uint(basicConfig, "Video", "OutputCY");
4258 	ovi.output_format = GetVideoFormatFromName(colorFormat);
4259 	ovi.colorspace = astrcmpi(colorSpace, "601") == 0
4260 				 ? VIDEO_CS_601
4261 				 : (astrcmpi(colorSpace, "709") == 0
4262 					    ? VIDEO_CS_709
4263 					    : VIDEO_CS_SRGB);
4264 	ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL
4265 						      : VIDEO_RANGE_PARTIAL;
4266 	ovi.adapter =
4267 		config_get_uint(App()->GlobalConfig(), "Video", "AdapterIdx");
4268 	ovi.gpu_conversion = true;
4269 	ovi.scale_type = GetScaleType(basicConfig);
4270 
4271 	if (ovi.base_width < 8 || ovi.base_height < 8) {
4272 		ovi.base_width = 1920;
4273 		ovi.base_height = 1080;
4274 		config_set_uint(basicConfig, "Video", "BaseCX", 1920);
4275 		config_set_uint(basicConfig, "Video", "BaseCY", 1080);
4276 	}
4277 
4278 	if (ovi.output_width < 8 || ovi.output_height < 8) {
4279 		ovi.output_width = ovi.base_width;
4280 		ovi.output_height = ovi.base_height;
4281 		config_set_uint(basicConfig, "Video", "OutputCX",
4282 				ovi.base_width);
4283 		config_set_uint(basicConfig, "Video", "OutputCY",
4284 				ovi.base_height);
4285 	}
4286 
4287 	ret = AttemptToResetVideo(&ovi);
4288 	if (IS_WIN32 && ret != OBS_VIDEO_SUCCESS) {
4289 		if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) {
4290 			blog(LOG_WARNING, "Tried to reset when "
4291 					  "already active");
4292 			return ret;
4293 		}
4294 
4295 		/* Try OpenGL if DirectX fails on windows */
4296 		if (astrcmpi(ovi.graphics_module, DL_OPENGL) != 0) {
4297 			blog(LOG_WARNING,
4298 			     "Failed to initialize obs video (%d) "
4299 			     "with graphics_module='%s', retrying "
4300 			     "with graphics_module='%s'",
4301 			     ret, ovi.graphics_module, DL_OPENGL);
4302 			ovi.graphics_module = DL_OPENGL;
4303 			ret = AttemptToResetVideo(&ovi);
4304 		}
4305 	} else if (ret == OBS_VIDEO_SUCCESS) {
4306 		ResizePreview(ovi.base_width, ovi.base_height);
4307 		if (program)
4308 			ResizeProgram(ovi.base_width, ovi.base_height);
4309 	}
4310 
4311 	if (ret == OBS_VIDEO_SUCCESS) {
4312 		OBSBasicStats::InitializeValues();
4313 		OBSProjector::UpdateMultiviewProjectors();
4314 	}
4315 
4316 	return ret;
4317 }
4318 
4319 bool OBSBasic::ResetAudio()
4320 {
4321 	ProfileScope("OBSBasic::ResetAudio");
4322 
4323 	struct obs_audio_info ai;
4324 	ai.samples_per_sec =
4325 		config_get_uint(basicConfig, "Audio", "SampleRate");
4326 
4327 	const char *channelSetupStr =
4328 		config_get_string(basicConfig, "Audio", "ChannelSetup");
4329 
4330 	if (strcmp(channelSetupStr, "Mono") == 0)
4331 		ai.speakers = SPEAKERS_MONO;
4332 	else if (strcmp(channelSetupStr, "2.1") == 0)
4333 		ai.speakers = SPEAKERS_2POINT1;
4334 	else if (strcmp(channelSetupStr, "4.0") == 0)
4335 		ai.speakers = SPEAKERS_4POINT0;
4336 	else if (strcmp(channelSetupStr, "4.1") == 0)
4337 		ai.speakers = SPEAKERS_4POINT1;
4338 	else if (strcmp(channelSetupStr, "5.1") == 0)
4339 		ai.speakers = SPEAKERS_5POINT1;
4340 	else if (strcmp(channelSetupStr, "7.1") == 0)
4341 		ai.speakers = SPEAKERS_7POINT1;
4342 	else
4343 		ai.speakers = SPEAKERS_STEREO;
4344 
4345 	return obs_reset_audio(&ai);
4346 }
4347 
4348 void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId,
4349 				const char *deviceDesc, int channel)
4350 {
4351 	bool disable = deviceId && strcmp(deviceId, "disabled") == 0;
4352 	obs_source_t *source;
4353 	obs_data_t *settings;
4354 
4355 	source = obs_get_output_source(channel);
4356 	if (source) {
4357 		if (disable) {
4358 			obs_set_output_source(channel, nullptr);
4359 		} else {
4360 			settings = obs_source_get_settings(source);
4361 			const char *oldId =
4362 				obs_data_get_string(settings, "device_id");
4363 			if (strcmp(oldId, deviceId) != 0) {
4364 				obs_data_set_string(settings, "device_id",
4365 						    deviceId);
4366 				obs_source_update(source, settings);
4367 			}
4368 			obs_data_release(settings);
4369 		}
4370 
4371 		obs_source_release(source);
4372 
4373 	} else if (!disable) {
4374 		settings = obs_data_create();
4375 		obs_data_set_string(settings, "device_id", deviceId);
4376 		source = obs_source_create(sourceId, deviceDesc, settings,
4377 					   nullptr);
4378 		obs_data_release(settings);
4379 
4380 		obs_set_output_source(channel, source);
4381 		obs_source_release(source);
4382 	}
4383 }
4384 
4385 void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
4386 {
4387 	QSize targetSize;
4388 	bool isFixedScaling;
4389 	obs_video_info ovi;
4390 
4391 	/* resize preview panel to fix to the top section of the window */
4392 	targetSize = GetPixelSize(ui->preview);
4393 
4394 	isFixedScaling = ui->preview->IsFixedScaling();
4395 	obs_get_video_info(&ovi);
4396 
4397 	if (isFixedScaling) {
4398 		previewScale = ui->preview->GetScalingAmount();
4399 		GetCenterPosFromFixedScale(
4400 			int(cx), int(cy),
4401 			targetSize.width() - PREVIEW_EDGE_SIZE * 2,
4402 			targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX,
4403 			previewY, previewScale);
4404 		previewX += ui->preview->GetScrollX();
4405 		previewY += ui->preview->GetScrollY();
4406 
4407 	} else {
4408 		GetScaleAndCenterPos(int(cx), int(cy),
4409 				     targetSize.width() - PREVIEW_EDGE_SIZE * 2,
4410 				     targetSize.height() -
4411 					     PREVIEW_EDGE_SIZE * 2,
4412 				     previewX, previewY, previewScale);
4413 	}
4414 
4415 	previewX += float(PREVIEW_EDGE_SIZE);
4416 	previewY += float(PREVIEW_EDGE_SIZE);
4417 }
4418 
4419 void OBSBasic::CloseDialogs()
4420 {
4421 	QList<QDialog *> childDialogs = this->findChildren<QDialog *>();
4422 	if (!childDialogs.isEmpty()) {
4423 		for (int i = 0; i < childDialogs.size(); ++i) {
4424 			childDialogs.at(i)->close();
4425 		}
4426 	}
4427 
4428 	if (!stats.isNull())
4429 		stats->close(); //call close to save Stats geometry
4430 	if (!remux.isNull())
4431 		remux->close();
4432 }
4433 
4434 void OBSBasic::EnumDialogs()
4435 {
4436 	visDialogs.clear();
4437 	modalDialogs.clear();
4438 	visMsgBoxes.clear();
4439 
4440 	/* fill list of Visible dialogs and Modal dialogs */
4441 	QList<QDialog *> dialogs = findChildren<QDialog *>();
4442 	for (QDialog *dialog : dialogs) {
4443 		if (dialog->isVisible())
4444 			visDialogs.append(dialog);
4445 		if (dialog->isModal())
4446 			modalDialogs.append(dialog);
4447 	}
4448 
4449 	/* fill list of Visible message boxes */
4450 	QList<QMessageBox *> msgBoxes = findChildren<QMessageBox *>();
4451 	for (QMessageBox *msgbox : msgBoxes) {
4452 		if (msgbox->isVisible())
4453 			visMsgBoxes.append(msgbox);
4454 	}
4455 }
4456 
4457 void OBSBasic::ClearProjectors()
4458 {
4459 	for (size_t i = 0; i < projectors.size(); i++) {
4460 		if (projectors[i])
4461 			delete projectors[i];
4462 	}
4463 
4464 	projectors.clear();
4465 }
4466 
4467 void OBSBasic::ClearSceneData()
4468 {
4469 	disableSaving++;
4470 
4471 	CloseDialogs();
4472 
4473 	ClearVolumeControls();
4474 	ClearListItems(ui->scenes);
4475 	ui->sources->Clear();
4476 	ClearQuickTransitions();
4477 	ui->transitions->clear();
4478 
4479 	ClearProjectors();
4480 
4481 	for (int i = 0; i < MAX_CHANNELS; i++)
4482 		obs_set_output_source(i, nullptr);
4483 
4484 	lastScene = nullptr;
4485 	swapScene = nullptr;
4486 	programScene = nullptr;
4487 	prevFTBSource = nullptr;
4488 
4489 	copyStrings.clear();
4490 	copyFiltersString = nullptr;
4491 	copyFilter = nullptr;
4492 
4493 	auto cb = [](void *unused, obs_source_t *source) {
4494 		obs_source_remove(source);
4495 		UNUSED_PARAMETER(unused);
4496 		return true;
4497 	};
4498 
4499 	obs_enum_scenes(cb, nullptr);
4500 	obs_enum_sources(cb, nullptr);
4501 
4502 	if (api)
4503 		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP);
4504 
4505 	undo_s.clear();
4506 
4507 	disableSaving--;
4508 
4509 	blog(LOG_INFO, "All scene data cleared");
4510 	blog(LOG_INFO, "------------------------------------------------");
4511 }
4512 
4513 void OBSBasic::closeEvent(QCloseEvent *event)
4514 {
4515 	/* Do not close window if inside of a temporary event loop because we
4516 	 * could be inside of an Auth::LoadUI call.  Keep trying once per
4517 	 * second until we've exit any known sub-loops. */
4518 	if (os_atomic_load_long(&insideEventLoop) != 0) {
4519 		QTimer::singleShot(1000, this, SLOT(close()));
4520 		event->ignore();
4521 		return;
4522 	}
4523 
4524 #if YOUTUBE_ENABLED
4525 	/* Also don't close the window if the youtube stream check is active */
4526 	if (youtubeStreamCheckThread) {
4527 		QTimer::singleShot(1000, this, SLOT(close()));
4528 		event->ignore();
4529 		return;
4530 	}
4531 #endif
4532 
4533 	if (isVisible())
4534 		config_set_string(App()->GlobalConfig(), "BasicWindow",
4535 				  "geometry",
4536 				  saveGeometry().toBase64().constData());
4537 
4538 	if (outputHandler && outputHandler->Active()) {
4539 		SetShowing(true);
4540 
4541 		QMessageBox::StandardButton button = OBSMessageBox::question(
4542 			this, QTStr("ConfirmExit.Title"),
4543 			QTStr("ConfirmExit.Text"));
4544 
4545 		if (button == QMessageBox::No) {
4546 			event->ignore();
4547 			restart = false;
4548 			return;
4549 		}
4550 	}
4551 
4552 	QWidget::closeEvent(event);
4553 	if (!event->isAccepted())
4554 		return;
4555 
4556 	blog(LOG_INFO, SHUTDOWN_SEPARATOR);
4557 
4558 	closing = true;
4559 
4560 	if (introCheckThread)
4561 		introCheckThread->wait();
4562 	if (whatsNewInitThread)
4563 		whatsNewInitThread->wait();
4564 	if (updateCheckThread)
4565 		updateCheckThread->wait();
4566 	if (logUploadThread)
4567 		logUploadThread->wait();
4568 	if (devicePropertiesThread && devicePropertiesThread->isRunning()) {
4569 		devicePropertiesThread->wait();
4570 		devicePropertiesThread.reset();
4571 	}
4572 
4573 	QApplication::sendPostedEvents(this);
4574 
4575 	signalHandlers.clear();
4576 
4577 	Auth::Save();
4578 	SaveProjectNow();
4579 	auth.reset();
4580 
4581 	delete extraBrowsers;
4582 
4583 	config_set_string(App()->GlobalConfig(), "BasicWindow", "DockState",
4584 			  saveState().toBase64().constData());
4585 
4586 #ifdef BROWSER_AVAILABLE
4587 	SaveExtraBrowserDocks();
4588 	ClearExtraBrowserDocks();
4589 #endif
4590 
4591 	disableSaving++;
4592 
4593 	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
4594 	 * sources, etc) so that all references are released before shutdown */
4595 	ClearSceneData();
4596 
4597 	if (api)
4598 		api->on_event(OBS_FRONTEND_EVENT_EXIT);
4599 
4600 	App()->quit();
4601 }
4602 
4603 void OBSBasic::changeEvent(QEvent *event)
4604 {
4605 	if (event->type() == QEvent::WindowStateChange) {
4606 		QWindowStateChangeEvent *stateEvent =
4607 			(QWindowStateChangeEvent *)event;
4608 
4609 		if (isMinimized()) {
4610 			if (trayIcon && trayIcon->isVisible() &&
4611 			    sysTrayMinimizeToTray()) {
4612 				ToggleShowHide();
4613 			}
4614 
4615 			if (previewEnabled)
4616 				EnablePreviewDisplay(false);
4617 		} else if (stateEvent->oldState() & Qt::WindowMinimized &&
4618 			   isVisible()) {
4619 			if (previewEnabled)
4620 				EnablePreviewDisplay(true);
4621 		}
4622 	}
4623 }
4624 
4625 void OBSBasic::on_actionShow_Recordings_triggered()
4626 {
4627 	const char *mode = config_get_string(basicConfig, "Output", "Mode");
4628 	const char *type = config_get_string(basicConfig, "AdvOut", "RecType");
4629 	const char *adv_path =
4630 		strcmp(type, "Standard")
4631 			? config_get_string(basicConfig, "AdvOut", "FFFilePath")
4632 			: config_get_string(basicConfig, "AdvOut",
4633 					    "RecFilePath");
4634 	const char *path = strcmp(mode, "Advanced")
4635 				   ? config_get_string(basicConfig,
4636 						       "SimpleOutput",
4637 						       "FilePath")
4638 				   : adv_path;
4639 	QDesktopServices::openUrl(QUrl::fromLocalFile(path));
4640 }
4641 
4642 void OBSBasic::on_actionRemux_triggered()
4643 {
4644 	if (!remux.isNull()) {
4645 		remux->show();
4646 		remux->raise();
4647 		return;
4648 	}
4649 
4650 	const char *mode = config_get_string(basicConfig, "Output", "Mode");
4651 	const char *path = strcmp(mode, "Advanced")
4652 				   ? config_get_string(basicConfig,
4653 						       "SimpleOutput",
4654 						       "FilePath")
4655 				   : config_get_string(basicConfig, "AdvOut",
4656 						       "RecFilePath");
4657 
4658 	OBSRemux *remuxDlg;
4659 	remuxDlg = new OBSRemux(path, this);
4660 	remuxDlg->show();
4661 	remux = remuxDlg;
4662 }
4663 
4664 void OBSBasic::on_action_Settings_triggered()
4665 {
4666 	static bool settings_already_executing = false;
4667 
4668 	/* Do not load settings window if inside of a temporary event loop
4669 	 * because we could be inside of an Auth::LoadUI call.  Keep trying
4670 	 * once per second until we've exit any known sub-loops. */
4671 	if (os_atomic_load_long(&insideEventLoop) != 0) {
4672 		QTimer::singleShot(1000, this,
4673 				   SLOT(on_action_Settings_triggered()));
4674 		return;
4675 	}
4676 
4677 	if (settings_already_executing) {
4678 		return;
4679 	}
4680 
4681 	settings_already_executing = true;
4682 
4683 	{
4684 		OBSBasicSettings settings(this);
4685 		settings.exec();
4686 	}
4687 
4688 	SystemTray(false);
4689 
4690 	settings_already_executing = false;
4691 
4692 	if (restart) {
4693 		QMessageBox::StandardButton button = OBSMessageBox::question(
4694 			this, QTStr("Restart"), QTStr("NeedsRestart"));
4695 
4696 		if (button == QMessageBox::Yes)
4697 			close();
4698 		else
4699 			restart = false;
4700 	}
4701 }
4702 
4703 static inline void AddMissingFiles(void *data, obs_source_t *source)
4704 {
4705 	obs_missing_files_t *f = (obs_missing_files_t *)data;
4706 	obs_missing_files_t *sf = obs_source_get_missing_files(source);
4707 
4708 	obs_missing_files_append(f, sf);
4709 	obs_missing_files_destroy(sf);
4710 }
4711 
4712 void OBSBasic::on_actionShowMissingFiles_triggered()
4713 {
4714 	obs_missing_files_t *files = obs_missing_files_create();
4715 
4716 	auto cb_sources = [](void *data, obs_source_t *source) {
4717 		AddMissingFiles(data, source);
4718 		return true;
4719 	};
4720 	obs_enum_sources(cb_sources, files);
4721 
4722 	auto cb_transitions = [](void *data, obs_source_t *source) {
4723 		if (obs_source_get_type(source) != OBS_SOURCE_TYPE_TRANSITION)
4724 			return true;
4725 
4726 		AddMissingFiles(data, source);
4727 		return true;
4728 	};
4729 	obs_enum_all_sources(cb_transitions, files);
4730 
4731 	if (obs_missing_files_count(files) > 0) {
4732 		missDialog = new OBSMissingFiles(files, this);
4733 		missDialog->setAttribute(Qt::WA_DeleteOnClose, true);
4734 		missDialog->show();
4735 		missDialog->raise();
4736 	} else {
4737 		obs_missing_files_destroy(files);
4738 		OBSMessageBox::information(
4739 			this, QTStr("MissingFiles.NoMissing.Title"),
4740 			QTStr("MissingFiles.NoMissing.Text"));
4741 	}
4742 }
4743 
4744 void save_audio_source(int channel, obs_data_t *save)
4745 {
4746 	obs_source_t *source = obs_get_output_source(channel);
4747 	if (!source)
4748 		return;
4749 
4750 	obs_data_t *obj = obs_data_create();
4751 
4752 	obs_data_set_double(obj, "vol", obs_source_get_volume(source));
4753 	obs_data_set_double(obj, "balance",
4754 			    obs_source_get_balance_value(source));
4755 	obs_data_set_double(obj, "mixers", obs_source_get_audio_mixers(source));
4756 	obs_data_set_double(obj, "sync", obs_source_get_sync_offset(source));
4757 	obs_data_set_double(obj, "flags", obs_source_get_flags(source));
4758 
4759 	obs_data_set_obj(save, std::to_string(channel).c_str(), obj);
4760 	obs_data_release(obj);
4761 	obs_source_release(source);
4762 }
4763 
4764 void load_audio_source(int channel, obs_data_t *data)
4765 {
4766 	obs_source_t *source = obs_get_output_source(channel);
4767 	if (!source)
4768 		return;
4769 
4770 	obs_data_t *save =
4771 		obs_data_get_obj(data, std::to_string(channel).c_str());
4772 
4773 	obs_source_set_volume(source, obs_data_get_double(save, "vol"));
4774 	obs_source_set_balance_value(source,
4775 				     obs_data_get_double(save, "balance"));
4776 	obs_source_set_audio_mixers(source,
4777 				    obs_data_get_double(save, "mixers"));
4778 	obs_source_set_sync_offset(source, obs_data_get_double(save, "sync"));
4779 	obs_source_set_flags(source, obs_data_get_double(save, "flags"));
4780 
4781 	obs_data_release(save);
4782 	obs_source_release(source);
4783 }
4784 
4785 void OBSBasic::on_actionAdvAudioProperties_triggered()
4786 {
4787 	if (advAudioWindow != nullptr) {
4788 		advAudioWindow->raise();
4789 		return;
4790 	}
4791 
4792 	bool iconsVisible = config_get_bool(App()->GlobalConfig(),
4793 					    "BasicWindow", "ShowSourceIcons");
4794 
4795 	advAudioWindow = new OBSBasicAdvAudio(this);
4796 	advAudioWindow->show();
4797 	advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true);
4798 	advAudioWindow->SetIconsVisible(iconsVisible);
4799 
4800 	connect(advAudioWindow, SIGNAL(destroyed()), this,
4801 		SLOT(AdvAudioPropsDestroyed()));
4802 }
4803 
4804 void OBSBasic::AdvAudioPropsClicked()
4805 {
4806 	on_actionAdvAudioProperties_triggered();
4807 }
4808 
4809 void OBSBasic::AdvAudioPropsDestroyed()
4810 {
4811 	advAudioWindow = nullptr;
4812 }
4813 
4814 void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current,
4815 					    QListWidgetItem *prev)
4816 {
4817 	obs_source_t *source = NULL;
4818 
4819 	if (current) {
4820 		obs_scene_t *scene;
4821 
4822 		scene = GetOBSRef<OBSScene>(current);
4823 		source = obs_scene_get_source(scene);
4824 	}
4825 
4826 	SetCurrentScene(source);
4827 
4828 	if (api)
4829 		api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED);
4830 
4831 	UpdateContextBar();
4832 
4833 	UNUSED_PARAMETER(prev);
4834 }
4835 
4836 void OBSBasic::EditSceneName()
4837 {
4838 	ui->scenesDock->removeAction(renameScene);
4839 	QListWidgetItem *item = ui->scenes->currentItem();
4840 	Qt::ItemFlags flags = item->flags();
4841 
4842 	item->setFlags(flags | Qt::ItemIsEditable);
4843 	ui->scenes->editItem(item);
4844 	item->setFlags(flags);
4845 }
4846 
4847 void OBSBasic::AddProjectorMenuMonitors(QMenu *parent, QObject *target,
4848 					const char *slot)
4849 {
4850 	QAction *action;
4851 	QList<QScreen *> screens = QGuiApplication::screens();
4852 	for (int i = 0; i < screens.size(); i++) {
4853 		QScreen *screen = screens[i];
4854 		QRect screenGeometry = screen->geometry();
4855 		qreal ratio = screen->devicePixelRatio();
4856 		QString name = "";
4857 #ifdef _WIN32
4858 		QTextStream fullname(&name);
4859 		fullname << GetMonitorName(screen->name());
4860 		fullname << " (";
4861 		fullname << (i + 1);
4862 		fullname << ")";
4863 #elif defined(__APPLE__)
4864 		name = screen->name();
4865 #else
4866 		name = screen->model().simplified();
4867 
4868 		if (name.length() > 1 && name.endsWith("-"))
4869 			name.chop(1);
4870 #endif
4871 		name = name.simplified();
4872 
4873 		if (name.length() == 0) {
4874 			name = QString("%1 %2")
4875 				       .arg(QTStr("Display"))
4876 				       .arg(QString::number(i + 1));
4877 		}
4878 		QString str =
4879 			QString("%1: %2x%3 @ %4,%5")
4880 				.arg(name,
4881 				     QString::number(screenGeometry.width() *
4882 						     ratio),
4883 				     QString::number(screenGeometry.height() *
4884 						     ratio),
4885 				     QString::number(screenGeometry.x()),
4886 				     QString::number(screenGeometry.y()));
4887 
4888 		action = parent->addAction(str, target, slot);
4889 		action->setProperty("monitor", i);
4890 	}
4891 }
4892 
4893 void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
4894 {
4895 	QListWidgetItem *item = ui->scenes->itemAt(pos);
4896 
4897 	QMenu popup(this);
4898 	QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this);
4899 
4900 	popup.addAction(QTStr("Add"), this,
4901 			SLOT(on_actionAddScene_triggered()));
4902 
4903 	if (item) {
4904 		QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this);
4905 		copyFilters->setEnabled(false);
4906 		connect(copyFilters, SIGNAL(triggered()), this,
4907 			SLOT(SceneCopyFilters()));
4908 		QAction *pasteFilters =
4909 			new QAction(QTStr("Paste.Filters"), this);
4910 		pasteFilters->setEnabled(copyFiltersString);
4911 		connect(pasteFilters, SIGNAL(triggered()), this,
4912 			SLOT(ScenePasteFilters()));
4913 
4914 		popup.addSeparator();
4915 		popup.addAction(QTStr("Duplicate"), this,
4916 				SLOT(DuplicateSelectedScene()));
4917 		popup.addAction(copyFilters);
4918 		popup.addAction(pasteFilters);
4919 		popup.addSeparator();
4920 		popup.addAction(QTStr("Rename"), this, SLOT(EditSceneName()));
4921 		popup.addAction(QTStr("Remove"), this,
4922 				SLOT(RemoveSelectedScene()));
4923 		popup.addSeparator();
4924 
4925 		order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this,
4926 				SLOT(on_actionSceneUp_triggered()));
4927 		order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"),
4928 				this, SLOT(on_actionSceneDown_triggered()));
4929 		order.addSeparator();
4930 		order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"),
4931 				this, SLOT(MoveSceneToTop()));
4932 		order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"),
4933 				this, SLOT(MoveSceneToBottom()));
4934 		popup.addMenu(&order);
4935 
4936 		popup.addSeparator();
4937 
4938 		delete sceneProjectorMenu;
4939 		sceneProjectorMenu = new QMenu(QTStr("SceneProjector"));
4940 		AddProjectorMenuMonitors(sceneProjectorMenu, this,
4941 					 SLOT(OpenSceneProjector()));
4942 		popup.addMenu(sceneProjectorMenu);
4943 
4944 		QAction *sceneWindow = popup.addAction(
4945 			QTStr("SceneWindow"), this, SLOT(OpenSceneWindow()));
4946 
4947 		popup.addAction(sceneWindow);
4948 		popup.addAction(QTStr("Screenshot.Scene"), this,
4949 				SLOT(ScreenshotScene()));
4950 		popup.addSeparator();
4951 		popup.addAction(QTStr("Filters"), this,
4952 				SLOT(OpenSceneFilters()));
4953 
4954 		popup.addSeparator();
4955 
4956 		delete perSceneTransitionMenu;
4957 		perSceneTransitionMenu = CreatePerSceneTransitionMenu();
4958 		popup.addMenu(perSceneTransitionMenu);
4959 
4960 		/* ---------------------- */
4961 
4962 		QAction *multiviewAction =
4963 			popup.addAction(QTStr("ShowInMultiview"));
4964 
4965 		OBSSource source = GetCurrentSceneSource();
4966 		OBSData data = obs_source_get_private_settings(source);
4967 		obs_data_release(data);
4968 
4969 		obs_data_set_default_bool(data, "show_in_multiview", true);
4970 		bool show = obs_data_get_bool(data, "show_in_multiview");
4971 
4972 		multiviewAction->setCheckable(true);
4973 		multiviewAction->setChecked(show);
4974 
4975 		auto showInMultiview = [](OBSData data) {
4976 			bool show =
4977 				obs_data_get_bool(data, "show_in_multiview");
4978 			obs_data_set_bool(data, "show_in_multiview", !show);
4979 			OBSProjector::UpdateMultiviewProjectors();
4980 		};
4981 
4982 		connect(multiviewAction, &QAction::triggered,
4983 			std::bind(showInMultiview, data));
4984 
4985 		copyFilters->setEnabled(obs_source_filter_count(source) > 0);
4986 	}
4987 
4988 	popup.addSeparator();
4989 
4990 	bool grid = ui->scenes->GetGridMode();
4991 
4992 	QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode")
4993 					       : QTStr("Basic.Main.GridMode"),
4994 					  this);
4995 	connect(gridAction, SIGNAL(triggered()), this,
4996 		SLOT(GridActionClicked()));
4997 	popup.addAction(gridAction);
4998 
4999 	popup.exec(QCursor::pos());
5000 }
5001 
5002 void OBSBasic::GridActionClicked()
5003 {
5004 	bool gridMode = !ui->scenes->GetGridMode();
5005 	ui->scenes->SetGridMode(gridMode);
5006 }
5007 
5008 void OBSBasic::on_actionAddScene_triggered()
5009 {
5010 	string name;
5011 	QString format{QTStr("Basic.Main.DefaultSceneName.Text")};
5012 
5013 	int i = 2;
5014 	QString placeHolderText = format.arg(i);
5015 	obs_source_t *source = nullptr;
5016 	while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) {
5017 		obs_source_release(source);
5018 		placeHolderText = format.arg(++i);
5019 	}
5020 
5021 	bool accepted = NameDialog::AskForName(
5022 		this, QTStr("Basic.Main.AddSceneDlg.Title"),
5023 		QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText);
5024 
5025 	if (accepted) {
5026 		if (name.empty()) {
5027 			OBSMessageBox::warning(this,
5028 					       QTStr("NoNameEntered.Title"),
5029 					       QTStr("NoNameEntered.Text"));
5030 			on_actionAddScene_triggered();
5031 			return;
5032 		}
5033 
5034 		obs_source_t *source = obs_get_source_by_name(name.c_str());
5035 		if (source) {
5036 			OBSMessageBox::warning(this, QTStr("NameExists.Title"),
5037 					       QTStr("NameExists.Text"));
5038 
5039 			obs_source_release(source);
5040 			on_actionAddScene_triggered();
5041 			return;
5042 		}
5043 
5044 		auto undo_fn = [](const std::string &data) {
5045 			obs_source_t *t = obs_get_source_by_name(data.c_str());
5046 			if (t) {
5047 				obs_source_release(t);
5048 				obs_source_remove(t);
5049 			}
5050 		};
5051 
5052 		auto redo_fn = [this](const std::string &data) {
5053 			obs_scene_t *scene = obs_scene_create(data.c_str());
5054 			obs_source_t *source = obs_scene_get_source(scene);
5055 			SetCurrentScene(source, true);
5056 			obs_scene_release(scene);
5057 		};
5058 		undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())),
5059 				  undo_fn, redo_fn, name, name);
5060 
5061 		obs_scene_t *scene = obs_scene_create(name.c_str());
5062 		source = obs_scene_get_source(scene);
5063 		SetCurrentScene(source);
5064 		RefreshSources(scene);
5065 		obs_scene_release(scene);
5066 	}
5067 }
5068 
5069 void OBSBasic::on_actionRemoveScene_triggered()
5070 {
5071 	RemoveSelectedScene();
5072 }
5073 
5074 void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx)
5075 {
5076 	int idx = ui->scenes->currentRow();
5077 	if (idx == -1 || idx == invalidIdx)
5078 		return;
5079 
5080 	ui->scenes->blockSignals(true);
5081 	QListWidgetItem *item = ui->scenes->takeItem(idx);
5082 
5083 	if (!relative)
5084 		idx = 0;
5085 
5086 	ui->scenes->insertItem(idx + offset, item);
5087 	ui->scenes->setCurrentRow(idx + offset);
5088 	item->setSelected(true);
5089 	ui->scenes->blockSignals(false);
5090 
5091 	OBSProjector::UpdateMultiviewProjectors();
5092 }
5093 
5094 void OBSBasic::on_actionSceneUp_triggered()
5095 {
5096 	ChangeSceneIndex(true, -1, 0);
5097 }
5098 
5099 void OBSBasic::on_actionSceneDown_triggered()
5100 {
5101 	ChangeSceneIndex(true, 1, ui->scenes->count() - 1);
5102 }
5103 
5104 void OBSBasic::MoveSceneToTop()
5105 {
5106 	ChangeSceneIndex(false, 0, 0);
5107 }
5108 
5109 void OBSBasic::MoveSceneToBottom()
5110 {
5111 	ChangeSceneIndex(false, ui->scenes->count() - 1,
5112 			 ui->scenes->count() - 1);
5113 }
5114 
5115 void OBSBasic::EditSceneItemName()
5116 {
5117 	int idx = GetTopSelectedSourceItem();
5118 	ui->sources->Edit(idx);
5119 }
5120 
5121 void OBSBasic::SetDeinterlacingMode()
5122 {
5123 	QAction *action = reinterpret_cast<QAction *>(sender());
5124 	obs_deinterlace_mode mode =
5125 		(obs_deinterlace_mode)action->property("mode").toInt();
5126 	OBSSceneItem sceneItem = GetCurrentSceneItem();
5127 	obs_source_t *source = obs_sceneitem_get_source(sceneItem);
5128 
5129 	obs_source_set_deinterlace_mode(source, mode);
5130 }
5131 
5132 void OBSBasic::SetDeinterlacingOrder()
5133 {
5134 	QAction *action = reinterpret_cast<QAction *>(sender());
5135 	obs_deinterlace_field_order order =
5136 		(obs_deinterlace_field_order)action->property("order").toInt();
5137 	OBSSceneItem sceneItem = GetCurrentSceneItem();
5138 	obs_source_t *source = obs_sceneitem_get_source(sceneItem);
5139 
5140 	obs_source_set_deinterlace_field_order(source, order);
5141 }
5142 
5143 QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source)
5144 {
5145 	obs_deinterlace_mode deinterlaceMode =
5146 		obs_source_get_deinterlace_mode(source);
5147 	obs_deinterlace_field_order deinterlaceOrder =
5148 		obs_source_get_deinterlace_field_order(source);
5149 	QAction *action;
5150 
5151 #define ADD_MODE(name, mode)                                    \
5152 	action = menu->addAction(QTStr("" name), this,          \
5153 				 SLOT(SetDeinterlacingMode())); \
5154 	action->setProperty("mode", (int)mode);                 \
5155 	action->setCheckable(true);                             \
5156 	action->setChecked(deinterlaceMode == mode);
5157 
5158 	ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE);
5159 	ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD);
5160 	ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO);
5161 	ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND);
5162 	ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X);
5163 	ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR);
5164 	ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X);
5165 	ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF);
5166 	ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X);
5167 #undef ADD_MODE
5168 
5169 	menu->addSeparator();
5170 
5171 #define ADD_ORDER(name, order)                                       \
5172 	action = menu->addAction(QTStr("Deinterlacing." name), this, \
5173 				 SLOT(SetDeinterlacingOrder()));     \
5174 	action->setProperty("order", (int)order);                    \
5175 	action->setCheckable(true);                                  \
5176 	action->setChecked(deinterlaceOrder == order);
5177 
5178 	ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP);
5179 	ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM);
5180 #undef ADD_ORDER
5181 
5182 	return menu;
5183 }
5184 
5185 void OBSBasic::SetScaleFilter()
5186 {
5187 	QAction *action = reinterpret_cast<QAction *>(sender());
5188 	obs_scale_type mode = (obs_scale_type)action->property("mode").toInt();
5189 	OBSSceneItem sceneItem = GetCurrentSceneItem();
5190 
5191 	obs_sceneitem_set_scale_filter(sceneItem, mode);
5192 }
5193 
5194 QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item)
5195 {
5196 	obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item);
5197 	QAction *action;
5198 
5199 #define ADD_MODE(name, mode)                                                   \
5200 	action =                                                               \
5201 		menu->addAction(QTStr("" name), this, SLOT(SetScaleFilter())); \
5202 	action->setProperty("mode", (int)mode);                                \
5203 	action->setCheckable(true);                                            \
5204 	action->setChecked(scaleFilter == mode);
5205 
5206 	ADD_MODE("Disable", OBS_SCALE_DISABLE);
5207 	ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT);
5208 	ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR);
5209 	ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC);
5210 	ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS);
5211 	ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA);
5212 #undef ADD_MODE
5213 
5214 	return menu;
5215 }
5216 
5217 QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu,
5218 					QWidgetAction *widgetAction,
5219 					ColorSelect *select,
5220 					obs_sceneitem_t *item)
5221 {
5222 	QAction *action;
5223 
5224 	menu->setStyleSheet(QString(
5225 		"*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}"
5226 		"*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}"
5227 		"*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}"
5228 		"*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}"
5229 		"*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}"
5230 		"*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}"
5231 		"*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}"
5232 		"*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}"));
5233 
5234 	obs_data_t *privData = obs_sceneitem_get_private_settings(item);
5235 	obs_data_release(privData);
5236 
5237 	obs_data_set_default_int(privData, "color-preset", 0);
5238 	int preset = obs_data_get_int(privData, "color-preset");
5239 
5240 	action = menu->addAction(QTStr("Clear"), this, +SLOT(ColorChange()));
5241 	action->setCheckable(true);
5242 	action->setProperty("bgColor", 0);
5243 	action->setChecked(preset == 0);
5244 
5245 	action = menu->addAction(QTStr("CustomColor"), this,
5246 				 +SLOT(ColorChange()));
5247 	action->setCheckable(true);
5248 	action->setProperty("bgColor", 1);
5249 	action->setChecked(preset == 1);
5250 
5251 	menu->addSeparator();
5252 
5253 	widgetAction->setDefaultWidget(select);
5254 
5255 	for (int i = 1; i < 9; i++) {
5256 		stringstream button;
5257 		button << "preset" << i;
5258 		QPushButton *colorButton =
5259 			select->findChild<QPushButton *>(button.str().c_str());
5260 		if (preset == i + 1)
5261 			colorButton->setStyleSheet("border: 2px solid black");
5262 
5263 		colorButton->setProperty("bgColor", i);
5264 		select->connect(colorButton, SIGNAL(released()), this,
5265 				SLOT(ColorChange()));
5266 	}
5267 
5268 	menu->addAction(widgetAction);
5269 
5270 	return menu;
5271 }
5272 
5273 ColorSelect::ColorSelect(QWidget *parent)
5274 	: QWidget(parent), ui(new Ui::ColorSelect)
5275 {
5276 	ui->setupUi(this);
5277 }
5278 
5279 void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
5280 {
5281 	QMenu popup(this);
5282 	delete previewProjectorSource;
5283 	delete sourceProjector;
5284 	delete scaleFilteringMenu;
5285 	delete colorMenu;
5286 	delete colorWidgetAction;
5287 	delete colorSelect;
5288 	delete deinterlaceMenu;
5289 
5290 	if (preview) {
5291 		QAction *action = popup.addAction(
5292 			QTStr("Basic.Main.PreviewConextMenu.Enable"), this,
5293 			SLOT(TogglePreview()));
5294 		action->setCheckable(true);
5295 		action->setChecked(
5296 			obs_display_enabled(ui->preview->GetDisplay()));
5297 		if (IsPreviewProgramMode())
5298 			action->setEnabled(false);
5299 
5300 		popup.addAction(ui->actionLockPreview);
5301 		popup.addMenu(ui->scalingMenu);
5302 
5303 		previewProjectorSource = new QMenu(QTStr("PreviewProjector"));
5304 		AddProjectorMenuMonitors(previewProjectorSource, this,
5305 					 SLOT(OpenPreviewProjector()));
5306 
5307 		popup.addMenu(previewProjectorSource);
5308 
5309 		QAction *previewWindow =
5310 			popup.addAction(QTStr("PreviewWindow"), this,
5311 					SLOT(OpenPreviewWindow()));
5312 
5313 		popup.addAction(previewWindow);
5314 
5315 		popup.addAction(QTStr("Screenshot.Preview"), this,
5316 				SLOT(ScreenshotScene()));
5317 
5318 		popup.addSeparator();
5319 	}
5320 
5321 	QPointer<QMenu> addSourceMenu = CreateAddSourcePopupMenu();
5322 	if (addSourceMenu)
5323 		popup.addMenu(addSourceMenu);
5324 
5325 	ui->actionCopyFilters->setEnabled(false);
5326 	ui->actionCopySource->setEnabled(false);
5327 
5328 	if (ui->sources->MultipleBaseSelected()) {
5329 		popup.addSeparator();
5330 		popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources,
5331 				SLOT(GroupSelectedItems()));
5332 
5333 	} else if (ui->sources->GroupsSelected()) {
5334 		popup.addSeparator();
5335 		popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources,
5336 				SLOT(UngroupSelectedGroups()));
5337 	}
5338 
5339 	popup.addSeparator();
5340 	popup.addAction(ui->actionCopySource);
5341 	popup.addAction(ui->actionPasteRef);
5342 	popup.addAction(ui->actionPasteDup);
5343 	popup.addSeparator();
5344 
5345 	popup.addSeparator();
5346 	popup.addAction(ui->actionCopyFilters);
5347 	popup.addAction(ui->actionPasteFilters);
5348 	popup.addSeparator();
5349 
5350 	if (idx != -1) {
5351 		if (addSourceMenu)
5352 			popup.addSeparator();
5353 
5354 		OBSSceneItem sceneItem = ui->sources->Get(idx);
5355 		bool lock = obs_sceneitem_locked(sceneItem);
5356 		obs_source_t *source = obs_sceneitem_get_source(sceneItem);
5357 		uint32_t flags = obs_source_get_output_flags(source);
5358 		bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) ==
5359 				    OBS_SOURCE_ASYNC_VIDEO;
5360 		bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO;
5361 		QAction *action;
5362 
5363 		colorMenu = new QMenu(QTStr("ChangeBG"));
5364 		colorWidgetAction = new QWidgetAction(colorMenu);
5365 		colorSelect = new ColorSelect(colorMenu);
5366 		popup.addMenu(AddBackgroundColorMenu(
5367 			colorMenu, colorWidgetAction, colorSelect, sceneItem));
5368 		popup.addAction(QTStr("Rename"), this,
5369 				SLOT(EditSceneItemName()));
5370 		popup.addAction(QTStr("Remove"), this,
5371 				SLOT(on_actionRemoveSource_triggered()));
5372 		popup.addSeparator();
5373 		popup.addMenu(ui->orderMenu);
5374 		popup.addMenu(ui->transformMenu);
5375 
5376 		ui->actionResetTransform->setEnabled(!lock);
5377 		ui->actionRotate90CW->setEnabled(!lock);
5378 		ui->actionRotate90CCW->setEnabled(!lock);
5379 		ui->actionRotate180->setEnabled(!lock);
5380 		ui->actionFlipHorizontal->setEnabled(!lock);
5381 		ui->actionFlipVertical->setEnabled(!lock);
5382 		ui->actionFitToScreen->setEnabled(!lock);
5383 		ui->actionStretchToScreen->setEnabled(!lock);
5384 		ui->actionCenterToScreen->setEnabled(!lock);
5385 		ui->actionVerticalCenter->setEnabled(!lock);
5386 		ui->actionHorizontalCenter->setEnabled(!lock);
5387 
5388 		sourceProjector = new QMenu(QTStr("SourceProjector"));
5389 		AddProjectorMenuMonitors(sourceProjector, this,
5390 					 SLOT(OpenSourceProjector()));
5391 
5392 		QAction *sourceWindow = popup.addAction(
5393 			QTStr("SourceWindow"), this, SLOT(OpenSourceWindow()));
5394 
5395 		popup.addAction(sourceWindow);
5396 
5397 		popup.addSeparator();
5398 
5399 		if (hasAudio) {
5400 			QAction *actionHideMixer =
5401 				popup.addAction(QTStr("HideMixer"), this,
5402 						SLOT(ToggleHideMixer()));
5403 			actionHideMixer->setCheckable(true);
5404 			actionHideMixer->setChecked(SourceMixerHidden(source));
5405 		}
5406 
5407 		if (isAsyncVideo) {
5408 			deinterlaceMenu = new QMenu(QTStr("Deinterlacing"));
5409 			popup.addMenu(
5410 				AddDeinterlacingMenu(deinterlaceMenu, source));
5411 			popup.addSeparator();
5412 		}
5413 
5414 		QAction *resizeOutput =
5415 			popup.addAction(QTStr("ResizeOutputSizeOfSource"), this,
5416 					SLOT(ResizeOutputSizeOfSource()));
5417 
5418 		int width = obs_source_get_width(source);
5419 		int height = obs_source_get_height(source);
5420 
5421 		resizeOutput->setEnabled(!obs_video_active());
5422 
5423 		if (width < 8 || height < 8)
5424 			resizeOutput->setEnabled(false);
5425 
5426 		scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering"));
5427 		popup.addMenu(
5428 			AddScaleFilteringMenu(scaleFilteringMenu, sceneItem));
5429 		popup.addSeparator();
5430 
5431 		popup.addMenu(sourceProjector);
5432 		popup.addAction(sourceWindow);
5433 		popup.addAction(QTStr("Screenshot.Source"), this,
5434 				SLOT(ScreenshotSelectedSource()));
5435 		popup.addSeparator();
5436 
5437 		popup.addMenu(CreateVisibilityTransitionMenu(true));
5438 		popup.addMenu(CreateVisibilityTransitionMenu(false));
5439 		popup.addSeparator();
5440 
5441 		action = popup.addAction(QTStr("Interact"), this,
5442 					 SLOT(on_actionInteract_triggered()));
5443 
5444 		action->setEnabled(obs_source_get_output_flags(source) &
5445 				   OBS_SOURCE_INTERACTION);
5446 
5447 		popup.addAction(QTStr("Filters"), this, SLOT(OpenFilters()));
5448 		popup.addAction(QTStr("Properties"), this,
5449 				SLOT(on_actionSourceProperties_triggered()));
5450 
5451 		ui->actionCopyFilters->setEnabled(
5452 			obs_source_filter_count(source) > 0);
5453 		ui->actionCopySource->setEnabled(true);
5454 	}
5455 	ui->actionPasteFilters->setEnabled(copyFiltersString && idx != -1);
5456 
5457 	popup.exec(QCursor::pos());
5458 }
5459 
5460 void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos)
5461 {
5462 	if (ui->scenes->count()) {
5463 		QModelIndex idx = ui->sources->indexAt(pos);
5464 		CreateSourcePopupMenu(idx.row(), false);
5465 	}
5466 }
5467 
5468 void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem)
5469 {
5470 	if (!witem)
5471 		return;
5472 
5473 	if (IsPreviewProgramMode()) {
5474 		bool doubleClickSwitch =
5475 			config_get_bool(App()->GlobalConfig(), "BasicWindow",
5476 					"TransitionOnDoubleClick");
5477 
5478 		if (doubleClickSwitch)
5479 			TransitionClicked();
5480 	}
5481 }
5482 
5483 void OBSBasic::AddSource(const char *id)
5484 {
5485 	if (id && *id) {
5486 		OBSBasicSourceSelect sourceSelect(this, id, undo_s);
5487 		sourceSelect.exec();
5488 		if (sourceSelect.newSource && strcmp(id, "group") != 0) {
5489 			CreatePropertiesWindow(sourceSelect.newSource);
5490 		}
5491 	}
5492 }
5493 
5494 QMenu *OBSBasic::CreateAddSourcePopupMenu()
5495 {
5496 	const char *unversioned_type;
5497 	const char *type;
5498 	bool foundValues = false;
5499 	bool foundDeprecated = false;
5500 	size_t idx = 0;
5501 
5502 	QMenu *popup = new QMenu(QTStr("Add"), this);
5503 	QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup);
5504 
5505 	auto getActionAfter = [](QMenu *menu, const QString &name) {
5506 		QList<QAction *> actions = menu->actions();
5507 
5508 		for (QAction *menuAction : actions) {
5509 			if (menuAction->text().compare(name) >= 0)
5510 				return menuAction;
5511 		}
5512 
5513 		return (QAction *)nullptr;
5514 	};
5515 
5516 	auto addSource = [this, getActionAfter](QMenu *popup, const char *type,
5517 						const char *name) {
5518 		QString qname = QT_UTF8(name);
5519 		QAction *popupItem = new QAction(qname, this);
5520 		popupItem->setData(QT_UTF8(type));
5521 		connect(popupItem, SIGNAL(triggered(bool)), this,
5522 			SLOT(AddSourceFromAction()));
5523 
5524 		QIcon icon;
5525 
5526 		if (strcmp(type, "scene") == 0)
5527 			icon = GetSceneIcon();
5528 		else
5529 			icon = GetSourceIcon(type);
5530 
5531 		popupItem->setIcon(icon);
5532 
5533 		QAction *after = getActionAfter(popup, qname);
5534 		popup->insertAction(after, popupItem);
5535 	};
5536 
5537 	while (obs_enum_input_types2(idx++, &type, &unversioned_type)) {
5538 		const char *name = obs_source_get_display_name(type);
5539 		uint32_t caps = obs_get_source_output_flags(type);
5540 
5541 		if ((caps & OBS_SOURCE_CAP_DISABLED) != 0)
5542 			continue;
5543 
5544 		if ((caps & OBS_SOURCE_DEPRECATED) == 0) {
5545 			addSource(popup, unversioned_type, name);
5546 		} else {
5547 			addSource(deprecated, unversioned_type, name);
5548 			foundDeprecated = true;
5549 		}
5550 		foundValues = true;
5551 	}
5552 
5553 	addSource(popup, "scene", Str("Basic.Scene"));
5554 
5555 	popup->addSeparator();
5556 	QAction *addGroup = new QAction(QTStr("Group"), this);
5557 	addGroup->setData(QT_UTF8("group"));
5558 	addGroup->setIcon(GetGroupIcon());
5559 	connect(addGroup, SIGNAL(triggered(bool)), this,
5560 		SLOT(AddSourceFromAction()));
5561 	popup->addAction(addGroup);
5562 
5563 	if (!foundDeprecated) {
5564 		delete deprecated;
5565 		deprecated = nullptr;
5566 	}
5567 
5568 	if (!foundValues) {
5569 		delete popup;
5570 		popup = nullptr;
5571 
5572 	} else if (foundDeprecated) {
5573 		popup->addSeparator();
5574 		popup->addMenu(deprecated);
5575 	}
5576 
5577 	return popup;
5578 }
5579 
5580 void OBSBasic::AddSourceFromAction()
5581 {
5582 	QAction *action = qobject_cast<QAction *>(sender());
5583 	if (!action)
5584 		return;
5585 
5586 	AddSource(QT_TO_UTF8(action->data().toString()));
5587 }
5588 
5589 void OBSBasic::AddSourcePopupMenu(const QPoint &pos)
5590 {
5591 	if (!GetCurrentScene()) {
5592 		// Tell the user he needs a scene first (help beginners).
5593 		OBSMessageBox::information(
5594 			this, QTStr("Basic.Main.AddSourceHelp.Title"),
5595 			QTStr("Basic.Main.AddSourceHelp.Text"));
5596 		return;
5597 	}
5598 
5599 	QScopedPointer<QMenu> popup(CreateAddSourcePopupMenu());
5600 	if (popup)
5601 		popup->exec(pos);
5602 }
5603 
5604 void OBSBasic::on_actionAddSource_triggered()
5605 {
5606 	AddSourcePopupMenu(QCursor::pos());
5607 }
5608 
5609 static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param)
5610 {
5611 	vector<OBSSceneItem> &items =
5612 		*reinterpret_cast<vector<OBSSceneItem> *>(param);
5613 
5614 	if (obs_sceneitem_selected(item)) {
5615 		items.emplace_back(item);
5616 	} else if (obs_sceneitem_is_group(item)) {
5617 		obs_sceneitem_group_enum_items(item, remove_items, &items);
5618 	}
5619 	return true;
5620 };
5621 
5622 OBSData OBSBasic::BackupScene(obs_scene_t *scene,
5623 			      std::vector<obs_source_t *> *sources)
5624 {
5625 	obs_data_array_t *undo_array = obs_data_array_create();
5626 
5627 	if (!sources) {
5628 		obs_scene_enum_items(scene, save_undo_source_enum, undo_array);
5629 	} else {
5630 		for (obs_source_t *source : *sources) {
5631 			obs_data_t *source_data = obs_save_source(source);
5632 			obs_data_array_push_back(undo_array, source_data);
5633 			obs_data_release(source_data);
5634 		}
5635 	}
5636 
5637 	obs_data_t *scene_data = obs_save_source(obs_scene_get_source(scene));
5638 	obs_data_array_push_back(undo_array, scene_data);
5639 	obs_data_release(scene_data);
5640 
5641 	OBSData data = obs_data_create();
5642 	obs_data_release(data);
5643 
5644 	obs_data_set_array(data, "array", undo_array);
5645 	obs_data_get_json(data);
5646 	obs_data_array_release(undo_array);
5647 	return data;
5648 }
5649 
5650 static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p)
5651 {
5652 	auto sources = static_cast<std::vector<obs_source_t *> *>(p);
5653 	sources->push_back(obs_sceneitem_get_source(item));
5654 	return true;
5655 }
5656 
5657 void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name,
5658 					 OBSData undo_data, OBSData redo_data)
5659 {
5660 	auto undo_redo = [this](const std::string &json) {
5661 		obs_data_t *base = obs_data_create_from_json(json.c_str());
5662 		obs_data_array_t *array = obs_data_get_array(base, "array");
5663 		std::vector<obs_source_t *> sources;
5664 		std::vector<obs_source_t *> old_sources;
5665 
5666 		/* create missing sources */
5667 		const size_t count = obs_data_array_count(array);
5668 		sources.reserve(count);
5669 
5670 		for (size_t i = 0; i < count; i++) {
5671 			obs_data_t *data = obs_data_array_item(array, i);
5672 			const char *name = obs_data_get_string(data, "name");
5673 
5674 			obs_source_t *source = obs_get_source_by_name(name);
5675 			if (!source)
5676 				source = obs_load_source(data);
5677 
5678 			sources.push_back(source);
5679 
5680 			/* update scene/group settings to restore their
5681 			* contents to their saved settings */
5682 			obs_scene_t *scene =
5683 				obs_group_or_scene_from_source(source);
5684 			if (scene) {
5685 				obs_scene_enum_items(scene, add_source_enum,
5686 						     &old_sources);
5687 				obs_data_t *scene_settings =
5688 					obs_data_get_obj(data, "settings");
5689 				obs_source_update(source, scene_settings);
5690 				obs_data_release(scene_settings);
5691 			}
5692 
5693 			obs_data_release(data);
5694 		}
5695 		for (obs_source_t *source : old_sources)
5696 			obs_source_addref(source);
5697 
5698 		/* actually load sources now */
5699 		for (obs_source_t *source : sources)
5700 			obs_source_load2(source);
5701 
5702 		/* release sources */
5703 		for (obs_source_t *source : sources)
5704 			obs_source_release(source);
5705 		for (obs_source_t *source : old_sources)
5706 			obs_source_release(source);
5707 
5708 		obs_data_array_release(array);
5709 		obs_data_release(base);
5710 
5711 		ui->sources->RefreshItems();
5712 	};
5713 
5714 	const char *undo_json = obs_data_get_last_json(undo_data);
5715 	const char *redo_json = obs_data_get_last_json(redo_data);
5716 
5717 	undo_s.add_action(action_name, undo_redo, undo_redo, undo_json,
5718 			  redo_json);
5719 }
5720 
5721 void OBSBasic::on_actionRemoveSource_triggered()
5722 {
5723 	vector<OBSSceneItem> items;
5724 	OBSScene scene = GetCurrentScene();
5725 	obs_source_t *scene_source = obs_scene_get_source(scene);
5726 
5727 	obs_scene_enum_items(scene, remove_items, &items);
5728 
5729 	if (!items.size())
5730 		return;
5731 
5732 	/* ------------------------------------- */
5733 	/* confirm action with user              */
5734 
5735 	bool confirmed = false;
5736 
5737 	if (items.size() > 1) {
5738 		QString text = QTStr("ConfirmRemove.TextMultiple")
5739 				       .arg(QString::number(items.size()));
5740 
5741 		QMessageBox remove_items(this);
5742 		remove_items.setText(text);
5743 		QPushButton *Yes = remove_items.addButton(QTStr("Yes"),
5744 							  QMessageBox::YesRole);
5745 		remove_items.setDefaultButton(Yes);
5746 		remove_items.addButton(QTStr("No"), QMessageBox::NoRole);
5747 		remove_items.setIcon(QMessageBox::Question);
5748 		remove_items.setWindowTitle(QTStr("ConfirmRemove.Title"));
5749 		remove_items.exec();
5750 
5751 		confirmed = Yes == remove_items.clickedButton();
5752 	} else {
5753 		OBSSceneItem &item = items[0];
5754 		obs_source_t *source = obs_sceneitem_get_source(item);
5755 		if (source && QueryRemoveSource(source))
5756 			confirmed = true;
5757 	}
5758 	if (!confirmed)
5759 		return;
5760 
5761 	/* ----------------------------------------------- */
5762 	/* save undo data                                  */
5763 
5764 	OBSData undo_data = BackupScene(scene_source);
5765 
5766 	/* ----------------------------------------------- */
5767 	/* remove items                                    */
5768 
5769 	for (auto &item : items)
5770 		obs_sceneitem_remove(item);
5771 
5772 	/* ----------------------------------------------- */
5773 	/* save redo data                                  */
5774 
5775 	OBSData redo_data = BackupScene(scene_source);
5776 
5777 	/* ----------------------------------------------- */
5778 	/* add undo/redo action                            */
5779 
5780 	QString action_name;
5781 	if (items.size() > 1) {
5782 		action_name = QTStr("Undo.Sources.Multi")
5783 				      .arg(QString::number(items.size()));
5784 	} else {
5785 		QString str = QTStr("Undo.Delete");
5786 		action_name = str.arg(obs_source_get_name(
5787 			obs_sceneitem_get_source(items[0])));
5788 	}
5789 
5790 	CreateSceneUndoRedoAction(action_name, undo_data, redo_data);
5791 }
5792 
5793 void OBSBasic::on_actionInteract_triggered()
5794 {
5795 	OBSSceneItem item = GetCurrentSceneItem();
5796 	OBSSource source = obs_sceneitem_get_source(item);
5797 
5798 	if (source)
5799 		CreateInteractionWindow(source);
5800 }
5801 
5802 void OBSBasic::on_actionSourceProperties_triggered()
5803 {
5804 	OBSSceneItem item = GetCurrentSceneItem();
5805 	OBSSource source = obs_sceneitem_get_source(item);
5806 
5807 	if (source)
5808 		CreatePropertiesWindow(source);
5809 }
5810 
5811 void OBSBasic::MoveSceneItem(enum obs_order_movement movement,
5812 			     const QString &action_name)
5813 {
5814 	OBSSceneItem item = GetCurrentSceneItem();
5815 	obs_source_t *source = obs_sceneitem_get_source(item);
5816 
5817 	if (!source)
5818 		return;
5819 
5820 	OBSScene scene = GetCurrentScene();
5821 	std::vector<obs_source_t *> sources;
5822 	if (scene != obs_sceneitem_get_scene(item))
5823 		sources.push_back(
5824 			obs_scene_get_source(obs_sceneitem_get_scene(item)));
5825 
5826 	OBSData undo_data = BackupScene(scene, &sources);
5827 
5828 	obs_sceneitem_set_order(item, movement);
5829 
5830 	const char *source_name = obs_source_get_name(source);
5831 	const char *scene_name =
5832 		obs_source_get_name(obs_scene_get_source(scene));
5833 
5834 	OBSData redo_data = BackupScene(scene, &sources);
5835 	CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name),
5836 				  undo_data, redo_data);
5837 }
5838 
5839 void OBSBasic::on_actionSourceUp_triggered()
5840 {
5841 	MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp"));
5842 }
5843 
5844 void OBSBasic::on_actionSourceDown_triggered()
5845 {
5846 	MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown"));
5847 }
5848 
5849 void OBSBasic::on_actionMoveUp_triggered()
5850 {
5851 	MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp"));
5852 }
5853 
5854 void OBSBasic::on_actionMoveDown_triggered()
5855 {
5856 	MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown"));
5857 }
5858 
5859 void OBSBasic::on_actionMoveToTop_triggered()
5860 {
5861 	MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop"));
5862 }
5863 
5864 void OBSBasic::on_actionMoveToBottom_triggered()
5865 {
5866 	MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom"));
5867 }
5868 
5869 static BPtr<char> ReadLogFile(const char *subdir, const char *log)
5870 {
5871 	char logDir[512];
5872 	if (GetConfigPath(logDir, sizeof(logDir), subdir) <= 0)
5873 		return nullptr;
5874 
5875 	string path = logDir;
5876 	path += "/";
5877 	path += log;
5878 
5879 	BPtr<char> file = os_quick_read_utf8_file(path.c_str());
5880 	if (!file)
5881 		blog(LOG_WARNING, "Failed to read log file %s", path.c_str());
5882 
5883 	return file;
5884 }
5885 
5886 void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash)
5887 {
5888 	BPtr<char> fileString{ReadLogFile(subdir, file)};
5889 
5890 	if (!fileString)
5891 		return;
5892 
5893 	if (!*fileString)
5894 		return;
5895 
5896 	ui->menuLogFiles->setEnabled(false);
5897 #if defined(_WIN32)
5898 	ui->menuCrashLogs->setEnabled(false);
5899 #endif
5900 
5901 	stringstream ss;
5902 	ss << "OBS " << App()->GetVersionString() << " log file uploaded at "
5903 	   << CurrentDateTimeString() << "\n\n"
5904 	   << fileString;
5905 
5906 	if (logUploadThread) {
5907 		logUploadThread->wait();
5908 	}
5909 
5910 	RemoteTextThread *thread =
5911 		new RemoteTextThread("https://obsproject.com/logs/upload",
5912 				     "text/plain", ss.str().c_str());
5913 
5914 	logUploadThread.reset(thread);
5915 	if (crash) {
5916 		connect(thread, &RemoteTextThread::Result, this,
5917 			&OBSBasic::crashUploadFinished);
5918 	} else {
5919 		connect(thread, &RemoteTextThread::Result, this,
5920 			&OBSBasic::logUploadFinished);
5921 	}
5922 	logUploadThread->start();
5923 }
5924 
5925 void OBSBasic::on_actionShowLogs_triggered()
5926 {
5927 	char logDir[512];
5928 	if (GetConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0)
5929 		return;
5930 
5931 	QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir));
5932 	QDesktopServices::openUrl(url);
5933 }
5934 
5935 void OBSBasic::on_actionUploadCurrentLog_triggered()
5936 {
5937 	UploadLog("obs-studio/logs", App()->GetCurrentLog(), false);
5938 }
5939 
5940 void OBSBasic::on_actionUploadLastLog_triggered()
5941 {
5942 	UploadLog("obs-studio/logs", App()->GetLastLog(), false);
5943 }
5944 
5945 void OBSBasic::on_actionViewCurrentLog_triggered()
5946 {
5947 	if (!logView)
5948 		logView = new OBSLogViewer();
5949 
5950 	logView->show();
5951 	logView->setWindowState(
5952 		(logView->windowState() & ~Qt::WindowMinimized) |
5953 		Qt::WindowActive);
5954 	logView->activateWindow();
5955 	logView->raise();
5956 }
5957 
5958 void OBSBasic::on_actionShowCrashLogs_triggered()
5959 {
5960 	char logDir[512];
5961 	if (GetConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0)
5962 		return;
5963 
5964 	QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir));
5965 	QDesktopServices::openUrl(url);
5966 }
5967 
5968 void OBSBasic::on_actionUploadLastCrashLog_triggered()
5969 {
5970 	UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true);
5971 }
5972 
5973 void OBSBasic::on_actionCheckForUpdates_triggered()
5974 {
5975 	CheckForUpdates(true);
5976 }
5977 
5978 void OBSBasic::logUploadFinished(const QString &text, const QString &error)
5979 {
5980 	ui->menuLogFiles->setEnabled(true);
5981 #if defined(_WIN32)
5982 	ui->menuCrashLogs->setEnabled(true);
5983 #endif
5984 
5985 	if (text.isEmpty()) {
5986 		OBSMessageBox::critical(
5987 			this, QTStr("LogReturnDialog.ErrorUploadingLog"),
5988 			error);
5989 		return;
5990 	}
5991 	openLogDialog(text, false);
5992 }
5993 
5994 void OBSBasic::crashUploadFinished(const QString &text, const QString &error)
5995 {
5996 	ui->menuLogFiles->setEnabled(true);
5997 #if defined(_WIN32)
5998 	ui->menuCrashLogs->setEnabled(true);
5999 #endif
6000 
6001 	if (text.isEmpty()) {
6002 		OBSMessageBox::critical(
6003 			this, QTStr("LogReturnDialog.ErrorUploadingLog"),
6004 			error);
6005 		return;
6006 	}
6007 	openLogDialog(text, true);
6008 }
6009 
6010 void OBSBasic::openLogDialog(const QString &text, const bool crash)
6011 {
6012 
6013 	obs_data_t *returnData = obs_data_create_from_json(QT_TO_UTF8(text));
6014 	string resURL = obs_data_get_string(returnData, "url");
6015 	QString logURL = resURL.c_str();
6016 	obs_data_release(returnData);
6017 
6018 	OBSLogReply logDialog(this, logURL, crash);
6019 	logDialog.exec();
6020 }
6021 
6022 static void RenameListItem(OBSBasic *parent, QListWidget *listWidget,
6023 			   obs_source_t *source, const string &name)
6024 {
6025 	const char *prevName = obs_source_get_name(source);
6026 	if (name == prevName)
6027 		return;
6028 
6029 	obs_source_t *foundSource = obs_get_source_by_name(name.c_str());
6030 	QListWidgetItem *listItem = listWidget->currentItem();
6031 
6032 	if (foundSource || name.empty()) {
6033 		listItem->setText(QT_UTF8(prevName));
6034 
6035 		if (foundSource) {
6036 			OBSMessageBox::warning(parent,
6037 					       QTStr("NameExists.Title"),
6038 					       QTStr("NameExists.Text"));
6039 		} else if (name.empty()) {
6040 			OBSMessageBox::warning(parent,
6041 					       QTStr("NoNameEntered.Title"),
6042 					       QTStr("NoNameEntered.Text"));
6043 		}
6044 
6045 		obs_source_release(foundSource);
6046 	} else {
6047 		auto undo = [prev = std::string(prevName)](
6048 				    const std::string &data) {
6049 			obs_source_t *source =
6050 				obs_get_source_by_name(data.c_str());
6051 			obs_source_set_name(source, prev.c_str());
6052 			obs_source_release(source);
6053 		};
6054 
6055 		auto redo = [name](const std::string &data) {
6056 			obs_source_t *source =
6057 				obs_get_source_by_name(data.c_str());
6058 			obs_source_set_name(source, name.c_str());
6059 			obs_source_release(source);
6060 		};
6061 
6062 		std::string undo_data(name);
6063 		std::string redo_data(prevName);
6064 		parent->undo_s.add_action(
6065 			QTStr("Undo.Rename").arg(name.c_str()), undo, redo,
6066 			undo_data, redo_data);
6067 
6068 		listItem->setText(QT_UTF8(name.c_str()));
6069 		obs_source_set_name(source, name.c_str());
6070 	}
6071 }
6072 
6073 void OBSBasic::SceneNameEdited(QWidget *editor,
6074 			       QAbstractItemDelegate::EndEditHint endHint)
6075 {
6076 	OBSScene scene = GetCurrentScene();
6077 	QLineEdit *edit = qobject_cast<QLineEdit *>(editor);
6078 	string text = QT_TO_UTF8(edit->text().trimmed());
6079 
6080 	if (!scene)
6081 		return;
6082 
6083 	obs_source_t *source = obs_scene_get_source(scene);
6084 	RenameListItem(this, ui->scenes, source, text);
6085 
6086 	ui->scenesDock->addAction(renameScene);
6087 
6088 	if (api)
6089 		api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
6090 
6091 	UNUSED_PARAMETER(endHint);
6092 }
6093 
6094 void OBSBasic::OpenFilters(OBSSource source)
6095 {
6096 	if (source == nullptr) {
6097 		OBSSceneItem item = GetCurrentSceneItem();
6098 		source = obs_sceneitem_get_source(item);
6099 	}
6100 	CreateFiltersWindow(source);
6101 }
6102 
6103 void OBSBasic::OpenProperties(OBSSource source)
6104 {
6105 	if (source == nullptr) {
6106 		OBSSceneItem item = GetCurrentSceneItem();
6107 		source = obs_sceneitem_get_source(item);
6108 	}
6109 	CreatePropertiesWindow(source);
6110 }
6111 
6112 void OBSBasic::OpenSceneFilters()
6113 {
6114 	OBSScene scene = GetCurrentScene();
6115 	OBSSource source = obs_scene_get_source(scene);
6116 
6117 	CreateFiltersWindow(source);
6118 }
6119 
6120 #define RECORDING_START \
6121 	"==== Recording Start ==============================================="
6122 #define RECORDING_STOP \
6123 	"==== Recording Stop ================================================"
6124 #define REPLAY_BUFFER_START \
6125 	"==== Replay Buffer Start ==========================================="
6126 #define REPLAY_BUFFER_STOP \
6127 	"==== Replay Buffer Stop ============================================"
6128 #define STREAMING_START \
6129 	"==== Streaming Start ==============================================="
6130 #define STREAMING_STOP \
6131 	"==== Streaming Stop ================================================"
6132 #define VIRTUAL_CAM_START \
6133 	"==== Virtual Camera Start =========================================="
6134 #define VIRTUAL_CAM_STOP \
6135 	"==== Virtual Camera Stop ==========================================="
6136 
6137 void OBSBasic::DisplayStreamStartError()
6138 {
6139 	QString message = !outputHandler->lastError.empty()
6140 				  ? QTStr(outputHandler->lastError.c_str())
6141 				  : QTStr("Output.StartFailedGeneric");
6142 	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
6143 	ui->streamButton->setEnabled(true);
6144 	ui->streamButton->setChecked(false);
6145 
6146 	if (sysTrayStream) {
6147 		sysTrayStream->setText(ui->streamButton->text());
6148 		sysTrayStream->setEnabled(true);
6149 	}
6150 
6151 	QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message);
6152 }
6153 
6154 #if YOUTUBE_ENABLED
6155 void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key,
6156 				     bool autostart, bool autostop,
6157 				     bool start_now)
6158 {
6159 	//blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key));
6160 	obs_service_t *service_obj = GetService();
6161 	obs_data_t *settings = obs_service_get_settings(service_obj);
6162 
6163 	const std::string a_key = QT_TO_UTF8(key);
6164 	obs_data_set_string(settings, "key", a_key.c_str());
6165 
6166 	const std::string an_id = QT_TO_UTF8(id);
6167 	obs_data_set_string(settings, "stream_id", an_id.c_str());
6168 
6169 	obs_service_update(service_obj, settings);
6170 	autoStartBroadcast = autostart;
6171 	autoStopBroadcast = autostop;
6172 	broadcastReady = true;
6173 
6174 	obs_data_release(settings);
6175 
6176 	if (start_now)
6177 		QMetaObject::invokeMethod(this, "StartStreaming");
6178 }
6179 
6180 void OBSBasic::YoutubeStreamCheck(const std::string &key)
6181 {
6182 	YoutubeApiWrappers *apiYouTube(
6183 		dynamic_cast<YoutubeApiWrappers *>(GetAuth()));
6184 	if (!apiYouTube) {
6185 		/* technically we should never get here -Jim */
6186 		QMetaObject::invokeMethod(this, "ForceStopStreaming",
6187 					  Qt::QueuedConnection);
6188 		youtubeStreamCheckThread->deleteLater();
6189 		blog(LOG_ERROR, "==========================================");
6190 		blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__);
6191 		blog(LOG_ERROR, "==========================================");
6192 		return;
6193 	}
6194 
6195 	int timeout = 0;
6196 	json11::Json json;
6197 	QString id = key.c_str();
6198 
6199 	while (StreamingActive()) {
6200 		if (timeout == 14) {
6201 			QMetaObject::invokeMethod(this, "ForceStopStreaming",
6202 						  Qt::QueuedConnection);
6203 			break;
6204 		}
6205 
6206 		if (!apiYouTube->FindStream(id, json)) {
6207 			QMetaObject::invokeMethod(this,
6208 						  "DisplayStreamStartError",
6209 						  Qt::QueuedConnection);
6210 			QMetaObject::invokeMethod(this, "StopStreaming",
6211 						  Qt::QueuedConnection);
6212 			break;
6213 		}
6214 
6215 		auto item = json["items"][0];
6216 		auto status = item["status"]["streamStatus"].string_value();
6217 		if (status == "active") {
6218 			QMetaObject::invokeMethod(ui->broadcastButton,
6219 						  "setEnabled",
6220 						  Q_ARG(bool, true));
6221 			break;
6222 		} else {
6223 			QThread::sleep(1);
6224 			timeout++;
6225 		}
6226 	}
6227 
6228 	youtubeStreamCheckThread->deleteLater();
6229 }
6230 
6231 void OBSBasic::ShowYouTubeAutoStartWarning()
6232 {
6233 	auto msgBox = []() {
6234 		QMessageBox msgbox(App()->GetMainWindow());
6235 		msgbox.setWindowTitle(QTStr(
6236 			"YouTube.Actions.AutoStartStreamingWarning.Title"));
6237 		msgbox.setText(
6238 			QTStr("YouTube.Actions.AutoStartStreamingWarning"));
6239 		msgbox.setIcon(QMessageBox::Icon::Information);
6240 		msgbox.addButton(QMessageBox::Ok);
6241 
6242 		QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain"));
6243 		msgbox.setCheckBox(cb);
6244 
6245 		msgbox.exec();
6246 
6247 		if (cb->isChecked()) {
6248 			config_set_bool(App()->GlobalConfig(), "General",
6249 					"WarnedAboutYouTubeAutoStart", true);
6250 			config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
6251 		}
6252 	};
6253 
6254 	bool warned = config_get_bool(App()->GlobalConfig(), "General",
6255 				      "WarnedAboutYouTubeAutoStart");
6256 	if (!warned) {
6257 		QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection,
6258 					  Q_ARG(VoidFunc, msgBox));
6259 	}
6260 }
6261 #endif
6262 
6263 void OBSBasic::StartStreaming()
6264 {
6265 	if (outputHandler->StreamingActive())
6266 		return;
6267 	if (disableOutputsRef)
6268 		return;
6269 
6270 	if (auth && auth->broadcastFlow()) {
6271 		if (!broadcastActive && !broadcastReady) {
6272 			ui->streamButton->setChecked(false);
6273 
6274 			QMessageBox no_broadcast(this);
6275 			no_broadcast.setText(QTStr("Output.NoBroadcast.Text"));
6276 			QPushButton *SetupBroadcast = no_broadcast.addButton(
6277 				QTStr("Basic.Main.SetupBroadcast"),
6278 				QMessageBox::YesRole);
6279 			no_broadcast.setDefaultButton(SetupBroadcast);
6280 			no_broadcast.addButton(QTStr("Close"),
6281 					       QMessageBox::NoRole);
6282 			no_broadcast.setIcon(QMessageBox::Information);
6283 			no_broadcast.setWindowTitle(
6284 				QTStr("Output.NoBroadcast.Title"));
6285 			no_broadcast.exec();
6286 
6287 			if (no_broadcast.clickedButton() == SetupBroadcast)
6288 				QMetaObject::invokeMethod(this,
6289 							  "SetupBroadcast");
6290 			return;
6291 		}
6292 	}
6293 
6294 	if (!outputHandler->SetupStreaming(service)) {
6295 		DisplayStreamStartError();
6296 		return;
6297 	}
6298 
6299 	if (api)
6300 		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTING);
6301 
6302 	SaveProject();
6303 
6304 	ui->streamButton->setEnabled(false);
6305 	ui->streamButton->setChecked(false);
6306 	ui->streamButton->setText(QTStr("Basic.Main.Connecting"));
6307 	ui->broadcastButton->setChecked(false);
6308 
6309 	if (sysTrayStream) {
6310 		sysTrayStream->setEnabled(false);
6311 		sysTrayStream->setText(ui->streamButton->text());
6312 	}
6313 
6314 	if (!outputHandler->StartStreaming(service)) {
6315 		DisplayStreamStartError();
6316 		return;
6317 	}
6318 
6319 	if (!autoStartBroadcast) {
6320 		ui->broadcastButton->setText(
6321 			QTStr("Basic.Main.StartBroadcast"));
6322 		ui->broadcastButton->setProperty("broadcastState", "ready");
6323 		ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6324 		ui->broadcastButton->style()->polish(ui->broadcastButton);
6325 		// well, we need to disable button while stream is not active
6326 		ui->broadcastButton->setEnabled(false);
6327 	} else if (!autoStopBroadcast) {
6328 		broadcastActive = true;
6329 		ui->broadcastButton->setText(QTStr("Basic.Main.StopBroadcast"));
6330 		ui->broadcastButton->setProperty("broadcastState", "active");
6331 		ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6332 		ui->broadcastButton->style()->polish(ui->broadcastButton);
6333 	} else {
6334 		ui->broadcastButton->setEnabled(false);
6335 	}
6336 
6337 	bool recordWhenStreaming = config_get_bool(
6338 		GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
6339 	if (recordWhenStreaming)
6340 		StartRecording();
6341 
6342 	bool replayBufferWhileStreaming = config_get_bool(
6343 		GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
6344 	if (replayBufferWhileStreaming)
6345 		StartReplayBuffer();
6346 
6347 #if YOUTUBE_ENABLED
6348 	if (!autoStartBroadcast)
6349 		OBSBasic::ShowYouTubeAutoStartWarning();
6350 #endif
6351 }
6352 
6353 void OBSBasic::BroadcastButtonClicked()
6354 {
6355 	if (!broadcastReady ||
6356 	    !broadcastActive && !outputHandler->StreamingActive()) {
6357 		SetupBroadcast();
6358 		if (broadcastReady)
6359 			ui->broadcastButton->setChecked(true);
6360 		return;
6361 	}
6362 
6363 	if (!autoStartBroadcast) {
6364 #if YOUTUBE_ENABLED
6365 		std::shared_ptr<YoutubeApiWrappers> ytAuth =
6366 			dynamic_pointer_cast<YoutubeApiWrappers>(auth);
6367 		if (ytAuth.get()) {
6368 			ytAuth->StartLatestBroadcast();
6369 		}
6370 #endif
6371 		broadcastActive = true;
6372 		autoStartBroadcast = true; // and clear the flag
6373 
6374 		if (!autoStopBroadcast) {
6375 			ui->broadcastButton->setText(
6376 				QTStr("Basic.Main.StopBroadcast"));
6377 		} else {
6378 			ui->broadcastButton->setText(
6379 				QTStr("Basic.Main.AutoStopEnabled"));
6380 			ui->broadcastButton->setEnabled(false);
6381 		}
6382 
6383 		ui->broadcastButton->setProperty("broadcastState", "active");
6384 		ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6385 		ui->broadcastButton->style()->polish(ui->broadcastButton);
6386 	} else if (!autoStopBroadcast) {
6387 #if YOUTUBE_ENABLED
6388 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
6389 					       "WarnBeforeStoppingStream");
6390 		if (confirm && isVisible()) {
6391 			QMessageBox::StandardButton button = OBSMessageBox::question(
6392 				this, QTStr("ConfirmStop.Title"),
6393 				QTStr("YouTube.Actions.AutoStopStreamingWarning"),
6394 				QMessageBox::Yes | QMessageBox::No,
6395 				QMessageBox::No);
6396 
6397 			if (button == QMessageBox::No) {
6398 				ui->broadcastButton->setChecked(true);
6399 				return;
6400 			}
6401 		}
6402 
6403 		std::shared_ptr<YoutubeApiWrappers> ytAuth =
6404 			dynamic_pointer_cast<YoutubeApiWrappers>(auth);
6405 		if (ytAuth.get()) {
6406 			ytAuth->StopLatestBroadcast();
6407 		}
6408 #endif
6409 		broadcastActive = false;
6410 		broadcastReady = false;
6411 
6412 		autoStopBroadcast = true;
6413 		QMetaObject::invokeMethod(this, "StopStreaming");
6414 		SetBroadcastFlowEnabled(true);
6415 	}
6416 }
6417 
6418 void OBSBasic::SetBroadcastFlowEnabled(bool enabled)
6419 {
6420 	ui->broadcastButton->setEnabled(enabled);
6421 	ui->broadcastButton->setVisible(enabled);
6422 	ui->broadcastButton->setChecked(broadcastReady);
6423 	ui->broadcastButton->setProperty("broadcastState", "idle");
6424 	ui->broadcastButton->style()->unpolish(ui->broadcastButton);
6425 	ui->broadcastButton->style()->polish(ui->broadcastButton);
6426 	ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast"));
6427 }
6428 
6429 void OBSBasic::SetupBroadcast()
6430 {
6431 	Auth *auth = GetAuth();
6432 #if YOUTUBE_ENABLED
6433 	if (IsYouTubeService(auth->service())) {
6434 		OBSYoutubeActions *dialog;
6435 		dialog = new OBSYoutubeActions(this, auth, broadcastReady);
6436 		connect(dialog, &OBSYoutubeActions::ok, this,
6437 			&OBSBasic::YouTubeActionDialogOk);
6438 		int result = dialog->Valid() ? dialog->exec()
6439 					     : QDialog::Rejected;
6440 		if (result != QDialog::Accepted) {
6441 			if (!broadcastReady)
6442 				ui->broadcastButton->setChecked(false);
6443 		}
6444 	}
6445 #endif
6446 }
6447 
6448 #ifdef _WIN32
6449 static inline void UpdateProcessPriority()
6450 {
6451 	const char *priority = config_get_string(App()->GlobalConfig(),
6452 						 "General", "ProcessPriority");
6453 	if (priority && strcmp(priority, "Normal") != 0)
6454 		SetProcessPriority(priority);
6455 }
6456 
6457 static inline void ClearProcessPriority()
6458 {
6459 	const char *priority = config_get_string(App()->GlobalConfig(),
6460 						 "General", "ProcessPriority");
6461 	if (priority && strcmp(priority, "Normal") != 0)
6462 		SetProcessPriority("Normal");
6463 }
6464 #else
6465 #define UpdateProcessPriority() \
6466 	do {                    \
6467 	} while (false)
6468 #define ClearProcessPriority() \
6469 	do {                   \
6470 	} while (false)
6471 #endif
6472 
6473 inline void OBSBasic::OnActivate()
6474 {
6475 	if (ui->profileMenu->isEnabled()) {
6476 		ui->profileMenu->setEnabled(false);
6477 		ui->autoConfigure->setEnabled(false);
6478 		App()->IncrementSleepInhibition();
6479 		UpdateProcessPriority();
6480 
6481 		if (trayIcon && trayIcon->isVisible()) {
6482 #ifdef __APPLE__
6483 			QIcon trayMask =
6484 				QIcon(":/res/images/tray_active_macos.png");
6485 			trayMask.setIsMask(true);
6486 			trayIcon->setIcon(
6487 				QIcon::fromTheme("obs-tray", trayMask));
6488 #else
6489 			trayIcon->setIcon(QIcon::fromTheme(
6490 				"obs-tray-active",
6491 				QIcon(":/res/images/tray_active.png")));
6492 #endif
6493 		}
6494 	}
6495 }
6496 
6497 extern volatile bool recording_paused;
6498 extern volatile bool replaybuf_active;
6499 
6500 inline void OBSBasic::OnDeactivate()
6501 {
6502 	if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) {
6503 		ui->profileMenu->setEnabled(true);
6504 		ui->autoConfigure->setEnabled(true);
6505 		App()->DecrementSleepInhibition();
6506 		ClearProcessPriority();
6507 
6508 		if (trayIcon && trayIcon->isVisible()) {
6509 #ifdef __APPLE__
6510 			QIcon trayIconFile =
6511 				QIcon(":/res/images/obs_macos.png");
6512 			trayIconFile.setIsMask(true);
6513 #else
6514 			QIcon trayIconFile = QIcon(":/res/images/obs.png");
6515 #endif
6516 			trayIcon->setIcon(
6517 				QIcon::fromTheme("obs-tray", trayIconFile));
6518 		}
6519 	} else if (outputHandler->Active() && trayIcon &&
6520 		   trayIcon->isVisible()) {
6521 		if (os_atomic_load_bool(&recording_paused)) {
6522 #ifdef __APPLE__
6523 			QIcon trayIconFile =
6524 				QIcon(":/res/images/obs_paused_macos.png");
6525 			trayIconFile.setIsMask(true);
6526 #else
6527 			QIcon trayIconFile =
6528 				QIcon(":/res/images/obs_paused.png");
6529 #endif
6530 			trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused",
6531 							   trayIconFile));
6532 		} else {
6533 #ifdef __APPLE__
6534 			QIcon trayIconFile =
6535 				QIcon(":/res/images/tray_active_macos.png");
6536 			trayIconFile.setIsMask(true);
6537 #else
6538 			QIcon trayIconFile =
6539 				QIcon(":/res/images/tray_active.png");
6540 #endif
6541 			trayIcon->setIcon(QIcon::fromTheme("obs-tray-active",
6542 							   trayIconFile));
6543 		}
6544 	}
6545 }
6546 
6547 void OBSBasic::StopStreaming()
6548 {
6549 	SaveProject();
6550 
6551 	if (outputHandler->StreamingActive())
6552 		outputHandler->StopStreaming(streamingStopping);
6553 
6554 	// special case: force reset broadcast state if
6555 	// no autostart and no autostop selected
6556 	if (!autoStartBroadcast && !broadcastActive) {
6557 		broadcastActive = false;
6558 		autoStartBroadcast = true;
6559 		autoStopBroadcast = true;
6560 		broadcastReady = false;
6561 	}
6562 
6563 	if (autoStopBroadcast) {
6564 		broadcastActive = false;
6565 		broadcastReady = false;
6566 	}
6567 
6568 	OnDeactivate();
6569 
6570 	bool recordWhenStreaming = config_get_bool(
6571 		GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
6572 	bool keepRecordingWhenStreamStops =
6573 		config_get_bool(GetGlobalConfig(), "BasicWindow",
6574 				"KeepRecordingWhenStreamStops");
6575 	if (recordWhenStreaming && !keepRecordingWhenStreamStops)
6576 		StopRecording();
6577 
6578 	bool replayBufferWhileStreaming = config_get_bool(
6579 		GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
6580 	bool keepReplayBufferStreamStops =
6581 		config_get_bool(GetGlobalConfig(), "BasicWindow",
6582 				"KeepReplayBufferStreamStops");
6583 	if (replayBufferWhileStreaming && !keepReplayBufferStreamStops)
6584 		StopReplayBuffer();
6585 }
6586 
6587 void OBSBasic::ForceStopStreaming()
6588 {
6589 	SaveProject();
6590 
6591 	if (outputHandler->StreamingActive())
6592 		outputHandler->StopStreaming(true);
6593 
6594 	// special case: force reset broadcast state if
6595 	// no autostart and no autostop selected
6596 	if (!autoStartBroadcast && !broadcastActive) {
6597 		broadcastActive = false;
6598 		autoStartBroadcast = true;
6599 		autoStopBroadcast = true;
6600 		broadcastReady = false;
6601 	}
6602 
6603 	if (autoStopBroadcast) {
6604 		broadcastActive = false;
6605 		broadcastReady = false;
6606 	}
6607 
6608 	OnDeactivate();
6609 
6610 	bool recordWhenStreaming = config_get_bool(
6611 		GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
6612 	bool keepRecordingWhenStreamStops =
6613 		config_get_bool(GetGlobalConfig(), "BasicWindow",
6614 				"KeepRecordingWhenStreamStops");
6615 	if (recordWhenStreaming && !keepRecordingWhenStreamStops)
6616 		StopRecording();
6617 
6618 	bool replayBufferWhileStreaming = config_get_bool(
6619 		GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
6620 	bool keepReplayBufferStreamStops =
6621 		config_get_bool(GetGlobalConfig(), "BasicWindow",
6622 				"KeepReplayBufferStreamStops");
6623 	if (replayBufferWhileStreaming && !keepReplayBufferStreamStops)
6624 		StopReplayBuffer();
6625 }
6626 
6627 void OBSBasic::StreamDelayStarting(int sec)
6628 {
6629 	ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
6630 	ui->streamButton->setEnabled(true);
6631 	ui->streamButton->setChecked(true);
6632 
6633 	if (sysTrayStream) {
6634 		sysTrayStream->setText(ui->streamButton->text());
6635 		sysTrayStream->setEnabled(true);
6636 	}
6637 
6638 	if (!startStreamMenu.isNull())
6639 		startStreamMenu->deleteLater();
6640 
6641 	startStreamMenu = new QMenu();
6642 	startStreamMenu->addAction(QTStr("Basic.Main.StopStreaming"), this,
6643 				   SLOT(StopStreaming()));
6644 	startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), this,
6645 				   SLOT(ForceStopStreaming()));
6646 	ui->streamButton->setMenu(startStreamMenu);
6647 
6648 	ui->statusbar->StreamDelayStarting(sec);
6649 
6650 	OnActivate();
6651 }
6652 
6653 void OBSBasic::StreamDelayStopping(int sec)
6654 {
6655 	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
6656 	ui->streamButton->setEnabled(true);
6657 	ui->streamButton->setChecked(false);
6658 
6659 	if (sysTrayStream) {
6660 		sysTrayStream->setText(ui->streamButton->text());
6661 		sysTrayStream->setEnabled(true);
6662 	}
6663 
6664 	if (!startStreamMenu.isNull())
6665 		startStreamMenu->deleteLater();
6666 
6667 	startStreamMenu = new QMenu();
6668 	startStreamMenu->addAction(QTStr("Basic.Main.StartStreaming"), this,
6669 				   SLOT(StartStreaming()));
6670 	startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), this,
6671 				   SLOT(ForceStopStreaming()));
6672 	ui->streamButton->setMenu(startStreamMenu);
6673 
6674 	ui->statusbar->StreamDelayStopping(sec);
6675 
6676 	if (api)
6677 		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPING);
6678 }
6679 
6680 void OBSBasic::StreamingStart()
6681 {
6682 	ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
6683 	ui->streamButton->setEnabled(true);
6684 	ui->streamButton->setChecked(true);
6685 	ui->statusbar->StreamStarted(outputHandler->streamOutput);
6686 
6687 	if (sysTrayStream) {
6688 		sysTrayStream->setText(ui->streamButton->text());
6689 		sysTrayStream->setEnabled(true);
6690 	}
6691 
6692 #if YOUTUBE_ENABLED
6693 	if (!autoStartBroadcast) {
6694 		// get a current stream key
6695 		obs_service_t *service_obj = GetService();
6696 		obs_data_t *settings = obs_service_get_settings(service_obj);
6697 		std::string key = obs_data_get_string(settings, "stream_id");
6698 		if (!key.empty() && !youtubeStreamCheckThread) {
6699 			youtubeStreamCheckThread = CreateQThread(
6700 				[this, key] { YoutubeStreamCheck(key); });
6701 			youtubeStreamCheckThread->setObjectName(
6702 				"YouTubeStreamCheckThread");
6703 			youtubeStreamCheckThread->start();
6704 		}
6705 	}
6706 #endif
6707 
6708 	if (api)
6709 		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTED);
6710 
6711 	OnActivate();
6712 
6713 	blog(LOG_INFO, STREAMING_START);
6714 }
6715 
6716 void OBSBasic::StreamStopping()
6717 {
6718 	ui->streamButton->setText(QTStr("Basic.Main.StoppingStreaming"));
6719 
6720 	if (sysTrayStream)
6721 		sysTrayStream->setText(ui->streamButton->text());
6722 
6723 	streamingStopping = true;
6724 	if (api)
6725 		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPING);
6726 }
6727 
6728 void OBSBasic::StreamingStop(int code, QString last_error)
6729 {
6730 	const char *errorDescription = "";
6731 	DStr errorMessage;
6732 	bool use_last_error = false;
6733 	bool encode_error = false;
6734 
6735 	switch (code) {
6736 	case OBS_OUTPUT_BAD_PATH:
6737 		errorDescription = Str("Output.ConnectFail.BadPath");
6738 		break;
6739 
6740 	case OBS_OUTPUT_CONNECT_FAILED:
6741 		use_last_error = true;
6742 		errorDescription = Str("Output.ConnectFail.ConnectFailed");
6743 		break;
6744 
6745 	case OBS_OUTPUT_INVALID_STREAM:
6746 		errorDescription = Str("Output.ConnectFail.InvalidStream");
6747 		break;
6748 
6749 	case OBS_OUTPUT_ENCODE_ERROR:
6750 		encode_error = true;
6751 		break;
6752 
6753 	default:
6754 	case OBS_OUTPUT_ERROR:
6755 		use_last_error = true;
6756 		errorDescription = Str("Output.ConnectFail.Error");
6757 		break;
6758 
6759 	case OBS_OUTPUT_DISCONNECTED:
6760 		/* doesn't happen if output is set to reconnect.  note that
6761 		 * reconnects are handled in the output, not in the UI */
6762 		use_last_error = true;
6763 		errorDescription = Str("Output.ConnectFail.Disconnected");
6764 	}
6765 
6766 	if (use_last_error && !last_error.isEmpty())
6767 		dstr_printf(errorMessage, "%s\n\n%s", errorDescription,
6768 			    QT_TO_UTF8(last_error));
6769 	else
6770 		dstr_copy(errorMessage, errorDescription);
6771 
6772 	ui->statusbar->StreamStopped();
6773 
6774 	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
6775 	ui->streamButton->setEnabled(true);
6776 	ui->streamButton->setChecked(false);
6777 
6778 	if (sysTrayStream) {
6779 		sysTrayStream->setText(ui->streamButton->text());
6780 		sysTrayStream->setEnabled(true);
6781 	}
6782 
6783 	streamingStopping = false;
6784 	if (api)
6785 		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPED);
6786 
6787 	OnDeactivate();
6788 
6789 	blog(LOG_INFO, STREAMING_STOP);
6790 
6791 	if (encode_error) {
6792 		OBSMessageBox::information(
6793 			this, QTStr("Output.StreamEncodeError.Title"),
6794 			QTStr("Output.StreamEncodeError.Msg"));
6795 
6796 	} else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
6797 		OBSMessageBox::information(this,
6798 					   QTStr("Output.ConnectFail.Title"),
6799 					   QT_UTF8(errorMessage));
6800 
6801 	} else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
6802 		SysTrayNotify(QT_UTF8(errorDescription),
6803 			      QSystemTrayIcon::Warning);
6804 	}
6805 
6806 	if (!startStreamMenu.isNull()) {
6807 		ui->streamButton->setMenu(nullptr);
6808 		startStreamMenu->deleteLater();
6809 		startStreamMenu = nullptr;
6810 	}
6811 
6812 	// Reset broadcast button state/text
6813 	if (!broadcastActive)
6814 		SetBroadcastFlowEnabled(auth && auth->broadcastFlow());
6815 }
6816 
6817 void OBSBasic::AutoRemux(QString input)
6818 {
6819 	bool autoRemux = config_get_bool(Config(), "Video", "AutoRemux");
6820 
6821 	if (!autoRemux)
6822 		return;
6823 
6824 	const char *recType = config_get_string(Config(), "AdvOut", "RecType");
6825 
6826 	bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0;
6827 
6828 	if (ffmpegOutput)
6829 		return;
6830 
6831 	if (input.isEmpty())
6832 		return;
6833 
6834 	QFileInfo fi(input);
6835 	QString suffix = fi.suffix();
6836 
6837 	/* do not remux if lossless */
6838 	if (suffix.compare("avi", Qt::CaseInsensitive) == 0) {
6839 		return;
6840 	}
6841 
6842 	QString path = fi.path();
6843 
6844 	QString output = input;
6845 	output.resize(output.size() - suffix.size());
6846 	output += "mp4";
6847 
6848 	OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true);
6849 	remux->show();
6850 	remux->AutoRemux(input, output);
6851 }
6852 
6853 void OBSBasic::StartRecording()
6854 {
6855 	if (outputHandler->RecordingActive())
6856 		return;
6857 	if (disableOutputsRef)
6858 		return;
6859 
6860 	if (!OutputPathValid()) {
6861 		OutputPathInvalidMessage();
6862 		ui->recordButton->setChecked(false);
6863 		return;
6864 	}
6865 
6866 	if (LowDiskSpace()) {
6867 		DiskSpaceMessage();
6868 		ui->recordButton->setChecked(false);
6869 		return;
6870 	}
6871 
6872 	if (api)
6873 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTING);
6874 
6875 	SaveProject();
6876 
6877 	if (!outputHandler->StartRecording())
6878 		ui->recordButton->setChecked(false);
6879 }
6880 
6881 void OBSBasic::RecordStopping()
6882 {
6883 	ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
6884 
6885 	if (sysTrayRecord)
6886 		sysTrayRecord->setText(ui->recordButton->text());
6887 
6888 	recordingStopping = true;
6889 	if (api)
6890 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STOPPING);
6891 }
6892 
6893 void OBSBasic::StopRecording()
6894 {
6895 	SaveProject();
6896 
6897 	if (outputHandler->RecordingActive())
6898 		outputHandler->StopRecording(recordingStopping);
6899 
6900 	OnDeactivate();
6901 }
6902 
6903 void OBSBasic::RecordingStart()
6904 {
6905 	ui->statusbar->RecordingStarted(outputHandler->fileOutput);
6906 	ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
6907 	ui->recordButton->setChecked(true);
6908 
6909 	if (sysTrayRecord)
6910 		sysTrayRecord->setText(ui->recordButton->text());
6911 
6912 	recordingStopping = false;
6913 	if (api)
6914 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTED);
6915 
6916 	if (!diskFullTimer->isActive())
6917 		diskFullTimer->start(1000);
6918 
6919 	OnActivate();
6920 	UpdatePause();
6921 
6922 	blog(LOG_INFO, RECORDING_START);
6923 }
6924 
6925 void OBSBasic::RecordingStop(int code, QString last_error)
6926 {
6927 	ui->statusbar->RecordingStopped();
6928 	ui->recordButton->setText(QTStr("Basic.Main.StartRecording"));
6929 	ui->recordButton->setChecked(false);
6930 
6931 	if (sysTrayRecord)
6932 		sysTrayRecord->setText(ui->recordButton->text());
6933 
6934 	blog(LOG_INFO, RECORDING_STOP);
6935 
6936 	if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
6937 		OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"),
6938 					QTStr("Output.RecordFail.Unsupported"));
6939 
6940 	} else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) {
6941 		OBSMessageBox::warning(
6942 			this, QTStr("Output.RecordError.Title"),
6943 			QTStr("Output.RecordError.EncodeErrorMsg"));
6944 
6945 	} else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) {
6946 		OBSMessageBox::warning(this,
6947 				       QTStr("Output.RecordNoSpace.Title"),
6948 				       QTStr("Output.RecordNoSpace.Msg"));
6949 
6950 	} else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
6951 
6952 		const char *errorDescription;
6953 		DStr errorMessage;
6954 		bool use_last_error = true;
6955 
6956 		errorDescription = Str("Output.RecordError.Msg");
6957 
6958 		if (use_last_error && !last_error.isEmpty())
6959 			dstr_printf(errorMessage, "%s\n\n%s", errorDescription,
6960 				    QT_TO_UTF8(last_error));
6961 		else
6962 			dstr_copy(errorMessage, errorDescription);
6963 
6964 		OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"),
6965 					QT_UTF8(errorMessage));
6966 
6967 	} else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) {
6968 		SysTrayNotify(QTStr("Output.RecordFail.Unsupported"),
6969 			      QSystemTrayIcon::Warning);
6970 
6971 	} else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) {
6972 		SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"),
6973 			      QSystemTrayIcon::Warning);
6974 
6975 	} else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
6976 		SysTrayNotify(QTStr("Output.RecordError.Msg"),
6977 			      QSystemTrayIcon::Warning);
6978 	} else if (code == OBS_OUTPUT_SUCCESS) {
6979 		if (outputHandler) {
6980 			std::string path = outputHandler->lastRecordingPath;
6981 			QString str = QTStr("Basic.StatusBar.RecordingSavedTo");
6982 			ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str())));
6983 		}
6984 	}
6985 
6986 	if (api)
6987 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STOPPED);
6988 
6989 	if (diskFullTimer->isActive())
6990 		diskFullTimer->stop();
6991 
6992 	AutoRemux(outputHandler->lastRecordingPath.c_str());
6993 
6994 	OnDeactivate();
6995 	UpdatePause(false);
6996 }
6997 
6998 void OBSBasic::ShowReplayBufferPauseWarning()
6999 {
7000 	auto msgBox = []() {
7001 		QMessageBox msgbox(App()->GetMainWindow());
7002 		msgbox.setWindowTitle(QTStr("Output.ReplayBuffer."
7003 					    "PauseWarning.Title"));
7004 		msgbox.setText(QTStr("Output.ReplayBuffer."
7005 				     "PauseWarning.Text"));
7006 		msgbox.setIcon(QMessageBox::Icon::Information);
7007 		msgbox.addButton(QMessageBox::Ok);
7008 
7009 		QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain"));
7010 		msgbox.setCheckBox(cb);
7011 
7012 		msgbox.exec();
7013 
7014 		if (cb->isChecked()) {
7015 			config_set_bool(App()->GlobalConfig(), "General",
7016 					"WarnedAboutReplayBufferPausing", true);
7017 			config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
7018 		}
7019 	};
7020 
7021 	bool warned = config_get_bool(App()->GlobalConfig(), "General",
7022 				      "WarnedAboutReplayBufferPausing");
7023 	if (!warned) {
7024 		QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection,
7025 					  Q_ARG(VoidFunc, msgBox));
7026 	}
7027 }
7028 
7029 void OBSBasic::StartReplayBuffer()
7030 {
7031 	if (!outputHandler || !outputHandler->replayBuffer)
7032 		return;
7033 	if (outputHandler->ReplayBufferActive())
7034 		return;
7035 	if (disableOutputsRef)
7036 		return;
7037 
7038 	if (!UIValidation::NoSourcesConfirmation(this)) {
7039 		replayBufferButton->setChecked(false);
7040 		return;
7041 	}
7042 
7043 	if (LowDiskSpace()) {
7044 		DiskSpaceMessage();
7045 		replayBufferButton->setChecked(false);
7046 		return;
7047 	}
7048 
7049 	if (api)
7050 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING);
7051 
7052 	SaveProject();
7053 
7054 	if (!outputHandler->StartReplayBuffer()) {
7055 		replayBufferButton->setChecked(false);
7056 	} else if (os_atomic_load_bool(&recording_paused)) {
7057 		ShowReplayBufferPauseWarning();
7058 	}
7059 }
7060 
7061 void OBSBasic::ReplayBufferStopping()
7062 {
7063 	if (!outputHandler || !outputHandler->replayBuffer)
7064 		return;
7065 
7066 	replayBufferButton->setText(QTStr("Basic.Main.StoppingReplayBuffer"));
7067 
7068 	if (sysTrayReplayBuffer)
7069 		sysTrayReplayBuffer->setText(replayBufferButton->text());
7070 
7071 	replayBufferStopping = true;
7072 	if (api)
7073 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING);
7074 }
7075 
7076 void OBSBasic::StopReplayBuffer()
7077 {
7078 	if (!outputHandler || !outputHandler->replayBuffer)
7079 		return;
7080 
7081 	SaveProject();
7082 
7083 	if (outputHandler->ReplayBufferActive())
7084 		outputHandler->StopReplayBuffer(replayBufferStopping);
7085 
7086 	OnDeactivate();
7087 }
7088 
7089 void OBSBasic::ReplayBufferStart()
7090 {
7091 	if (!outputHandler || !outputHandler->replayBuffer)
7092 		return;
7093 
7094 	replayBufferButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
7095 	replayBufferButton->setChecked(true);
7096 
7097 	if (sysTrayReplayBuffer)
7098 		sysTrayReplayBuffer->setText(replayBufferButton->text());
7099 
7100 	replayBufferStopping = false;
7101 	if (api)
7102 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED);
7103 
7104 	OnActivate();
7105 	UpdateReplayBuffer();
7106 
7107 	blog(LOG_INFO, REPLAY_BUFFER_START);
7108 }
7109 
7110 void OBSBasic::ReplayBufferSave()
7111 {
7112 	if (!outputHandler || !outputHandler->replayBuffer)
7113 		return;
7114 	if (!outputHandler->ReplayBufferActive())
7115 		return;
7116 
7117 	calldata_t cd = {0};
7118 	proc_handler_t *ph =
7119 		obs_output_get_proc_handler(outputHandler->replayBuffer);
7120 	proc_handler_call(ph, "save", &cd);
7121 	calldata_free(&cd);
7122 }
7123 
7124 void OBSBasic::ReplayBufferSaved()
7125 {
7126 	if (!outputHandler || !outputHandler->replayBuffer)
7127 		return;
7128 	if (!outputHandler->ReplayBufferActive())
7129 		return;
7130 
7131 	calldata_t cd = {0};
7132 	proc_handler_t *ph =
7133 		obs_output_get_proc_handler(outputHandler->replayBuffer);
7134 	proc_handler_call(ph, "get_last_replay", &cd);
7135 	QString path = QT_UTF8(calldata_string(&cd, "path"));
7136 	QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(path);
7137 	ShowStatusBarMessage(msg);
7138 	calldata_free(&cd);
7139 
7140 	if (api)
7141 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED);
7142 
7143 	AutoRemux(path);
7144 }
7145 
7146 void OBSBasic::ReplayBufferStop(int code)
7147 {
7148 	if (!outputHandler || !outputHandler->replayBuffer)
7149 		return;
7150 
7151 	replayBufferButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
7152 	replayBufferButton->setChecked(false);
7153 
7154 	if (sysTrayReplayBuffer)
7155 		sysTrayReplayBuffer->setText(replayBufferButton->text());
7156 
7157 	blog(LOG_INFO, REPLAY_BUFFER_STOP);
7158 
7159 	if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
7160 		OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"),
7161 					QTStr("Output.RecordFail.Unsupported"));
7162 
7163 	} else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) {
7164 		OBSMessageBox::warning(this,
7165 				       QTStr("Output.RecordNoSpace.Title"),
7166 				       QTStr("Output.RecordNoSpace.Msg"));
7167 
7168 	} else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
7169 		OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"),
7170 					QTStr("Output.RecordError.Msg"));
7171 
7172 	} else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) {
7173 		SysTrayNotify(QTStr("Output.RecordFail.Unsupported"),
7174 			      QSystemTrayIcon::Warning);
7175 
7176 	} else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) {
7177 		SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"),
7178 			      QSystemTrayIcon::Warning);
7179 
7180 	} else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
7181 		SysTrayNotify(QTStr("Output.RecordError.Msg"),
7182 			      QSystemTrayIcon::Warning);
7183 	}
7184 
7185 	if (api)
7186 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED);
7187 
7188 	OnDeactivate();
7189 	UpdateReplayBuffer(false);
7190 }
7191 
7192 void OBSBasic::StartVirtualCam()
7193 {
7194 	if (!outputHandler || !outputHandler->virtualCam)
7195 		return;
7196 	if (outputHandler->VirtualCamActive())
7197 		return;
7198 	if (disableOutputsRef)
7199 		return;
7200 
7201 	SaveProject();
7202 
7203 	if (!outputHandler->StartVirtualCam()) {
7204 		vcamButton->setChecked(false);
7205 	}
7206 }
7207 
7208 void OBSBasic::StopVirtualCam()
7209 {
7210 	if (!outputHandler || !outputHandler->virtualCam)
7211 		return;
7212 
7213 	SaveProject();
7214 
7215 	if (outputHandler->VirtualCamActive())
7216 		outputHandler->StopVirtualCam();
7217 
7218 	OnDeactivate();
7219 }
7220 
7221 void OBSBasic::OnVirtualCamStart()
7222 {
7223 	if (!outputHandler || !outputHandler->virtualCam)
7224 		return;
7225 
7226 	vcamButton->setText(QTStr("Basic.Main.StopVirtualCam"));
7227 	if (sysTrayVirtualCam)
7228 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam"));
7229 	vcamButton->setChecked(true);
7230 
7231 	if (api)
7232 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED);
7233 
7234 	OnActivate();
7235 
7236 	blog(LOG_INFO, VIRTUAL_CAM_START);
7237 }
7238 
7239 void OBSBasic::OnVirtualCamStop(int)
7240 {
7241 	if (!outputHandler || !outputHandler->virtualCam)
7242 		return;
7243 
7244 	vcamButton->setText(QTStr("Basic.Main.StartVirtualCam"));
7245 	if (sysTrayVirtualCam)
7246 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam"));
7247 	vcamButton->setChecked(false);
7248 
7249 	if (api)
7250 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED);
7251 
7252 	blog(LOG_INFO, VIRTUAL_CAM_STOP);
7253 
7254 	OnDeactivate();
7255 }
7256 
7257 void OBSBasic::on_streamButton_clicked()
7258 {
7259 	if (outputHandler->StreamingActive()) {
7260 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
7261 					       "WarnBeforeStoppingStream");
7262 
7263 #if YOUTUBE_ENABLED
7264 		if (isVisible() && auth && IsYouTubeService(auth->service()) &&
7265 		    autoStopBroadcast) {
7266 			QMessageBox::StandardButton button = OBSMessageBox::question(
7267 				this, QTStr("ConfirmStop.Title"),
7268 				QTStr("YouTube.Actions.AutoStopStreamingWarning"),
7269 				QMessageBox::Yes | QMessageBox::No,
7270 				QMessageBox::No);
7271 
7272 			if (button == QMessageBox::No) {
7273 				ui->streamButton->setChecked(true);
7274 				return;
7275 			}
7276 
7277 			confirm = false;
7278 		}
7279 #endif
7280 		if (confirm && isVisible()) {
7281 			QMessageBox::StandardButton button =
7282 				OBSMessageBox::question(
7283 					this, QTStr("ConfirmStop.Title"),
7284 					QTStr("ConfirmStop.Text"),
7285 					QMessageBox::Yes | QMessageBox::No,
7286 					QMessageBox::No);
7287 
7288 			if (button == QMessageBox::No) {
7289 				ui->streamButton->setChecked(true);
7290 				return;
7291 			}
7292 		}
7293 
7294 		StopStreaming();
7295 	} else {
7296 		if (!UIValidation::NoSourcesConfirmation(this)) {
7297 			ui->streamButton->setChecked(false);
7298 			return;
7299 		}
7300 
7301 		Auth *auth = GetAuth();
7302 
7303 		auto action =
7304 			(auth && auth->external())
7305 				? StreamSettingsAction::ContinueStream
7306 				: UIValidation::StreamSettingsConfirmation(
7307 					  this, service);
7308 		switch (action) {
7309 		case StreamSettingsAction::ContinueStream:
7310 			break;
7311 		case StreamSettingsAction::OpenSettings:
7312 			on_action_Settings_triggered();
7313 			ui->streamButton->setChecked(false);
7314 			return;
7315 		case StreamSettingsAction::Cancel:
7316 			ui->streamButton->setChecked(false);
7317 			return;
7318 		}
7319 
7320 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
7321 					       "WarnBeforeStartingStream");
7322 
7323 		bool bwtest = false;
7324 
7325 		if (this->auth) {
7326 			obs_data_t *settings =
7327 				obs_service_get_settings(service);
7328 			bwtest = obs_data_get_bool(settings, "bwtest");
7329 			obs_data_release(settings);
7330 			// Disable confirmation if this is going to open broadcast setup
7331 			if (auth && auth->broadcastFlow() && !broadcastReady &&
7332 			    !broadcastActive)
7333 				confirm = false;
7334 		}
7335 
7336 		if (bwtest && isVisible()) {
7337 			QMessageBox::StandardButton button =
7338 				OBSMessageBox::question(
7339 					this, QTStr("ConfirmBWTest.Title"),
7340 					QTStr("ConfirmBWTest.Text"));
7341 
7342 			if (button == QMessageBox::No) {
7343 				ui->streamButton->setChecked(false);
7344 				return;
7345 			}
7346 		} else if (confirm && isVisible()) {
7347 			QMessageBox::StandardButton button =
7348 				OBSMessageBox::question(
7349 					this, QTStr("ConfirmStart.Title"),
7350 					QTStr("ConfirmStart.Text"),
7351 					QMessageBox::Yes | QMessageBox::No,
7352 					QMessageBox::No);
7353 
7354 			if (button == QMessageBox::No) {
7355 				ui->streamButton->setChecked(false);
7356 				return;
7357 			}
7358 		}
7359 
7360 		StartStreaming();
7361 	}
7362 }
7363 
7364 void OBSBasic::on_recordButton_clicked()
7365 {
7366 	if (outputHandler->RecordingActive()) {
7367 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
7368 					       "WarnBeforeStoppingRecord");
7369 
7370 		if (confirm && isVisible()) {
7371 			QMessageBox::StandardButton button =
7372 				OBSMessageBox::question(
7373 					this, QTStr("ConfirmStopRecord.Title"),
7374 					QTStr("ConfirmStopRecord.Text"),
7375 					QMessageBox::Yes | QMessageBox::No,
7376 					QMessageBox::No);
7377 
7378 			if (button == QMessageBox::No) {
7379 				ui->recordButton->setChecked(true);
7380 				return;
7381 			}
7382 		}
7383 		StopRecording();
7384 	} else {
7385 		if (!UIValidation::NoSourcesConfirmation(this)) {
7386 			ui->recordButton->setChecked(false);
7387 			return;
7388 		}
7389 
7390 		StartRecording();
7391 	}
7392 }
7393 
7394 void OBSBasic::VCamButtonClicked()
7395 {
7396 	if (outputHandler->VirtualCamActive()) {
7397 		StopVirtualCam();
7398 	} else {
7399 		if (!UIValidation::NoSourcesConfirmation(this)) {
7400 			vcamButton->setChecked(false);
7401 			return;
7402 		}
7403 
7404 		StartVirtualCam();
7405 	}
7406 }
7407 
7408 void OBSBasic::on_settingsButton_clicked()
7409 {
7410 	on_action_Settings_triggered();
7411 }
7412 
7413 void OBSBasic::on_actionHelpPortal_triggered()
7414 {
7415 	QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode);
7416 	QDesktopServices::openUrl(url);
7417 }
7418 
7419 void OBSBasic::on_actionWebsite_triggered()
7420 {
7421 	QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode);
7422 	QDesktopServices::openUrl(url);
7423 }
7424 
7425 void OBSBasic::on_actionDiscord_triggered()
7426 {
7427 	QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode);
7428 	QDesktopServices::openUrl(url);
7429 }
7430 
7431 void OBSBasic::on_actionShowSettingsFolder_triggered()
7432 {
7433 	char path[512];
7434 	int ret = GetConfigPath(path, 512, "obs-studio");
7435 	if (ret <= 0)
7436 		return;
7437 
7438 	QDesktopServices::openUrl(QUrl::fromLocalFile(path));
7439 }
7440 
7441 void OBSBasic::on_actionShowProfileFolder_triggered()
7442 {
7443 	char path[512];
7444 	int ret = GetProfilePath(path, 512, "");
7445 	if (ret <= 0)
7446 		return;
7447 
7448 	QDesktopServices::openUrl(QUrl::fromLocalFile(path));
7449 }
7450 
7451 int OBSBasic::GetTopSelectedSourceItem()
7452 {
7453 	QModelIndexList selectedItems =
7454 		ui->sources->selectionModel()->selectedIndexes();
7455 	return selectedItems.count() ? selectedItems[0].row() : -1;
7456 }
7457 
7458 QModelIndexList OBSBasic::GetAllSelectedSourceItems()
7459 {
7460 	return ui->sources->selectionModel()->selectedIndexes();
7461 }
7462 
7463 void OBSBasic::on_preview_customContextMenuRequested(const QPoint &pos)
7464 {
7465 	CreateSourcePopupMenu(GetTopSelectedSourceItem(), true);
7466 
7467 	UNUSED_PARAMETER(pos);
7468 }
7469 
7470 void OBSBasic::ProgramViewContextMenuRequested(const QPoint &)
7471 {
7472 	QMenu popup(this);
7473 	QPointer<QMenu> studioProgramProjector;
7474 
7475 	studioProgramProjector = new QMenu(QTStr("StudioProgramProjector"));
7476 	AddProjectorMenuMonitors(studioProgramProjector, this,
7477 				 SLOT(OpenStudioProgramProjector()));
7478 
7479 	popup.addMenu(studioProgramProjector);
7480 
7481 	QAction *studioProgramWindow =
7482 		popup.addAction(QTStr("StudioProgramWindow"), this,
7483 				SLOT(OpenStudioProgramWindow()));
7484 
7485 	popup.addAction(studioProgramWindow);
7486 
7487 	popup.addAction(QTStr("Screenshot.StudioProgram"), this,
7488 			SLOT(ScreenshotProgram()));
7489 
7490 	popup.exec(QCursor::pos());
7491 }
7492 
7493 void OBSBasic::PreviewDisabledMenu(const QPoint &pos)
7494 {
7495 	QMenu popup(this);
7496 	delete previewProjectorMain;
7497 
7498 	QAction *action =
7499 		popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"),
7500 				this, SLOT(TogglePreview()));
7501 	action->setCheckable(true);
7502 	action->setChecked(obs_display_enabled(ui->preview->GetDisplay()));
7503 
7504 	previewProjectorMain = new QMenu(QTStr("PreviewProjector"));
7505 	AddProjectorMenuMonitors(previewProjectorMain, this,
7506 				 SLOT(OpenPreviewProjector()));
7507 
7508 	QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this,
7509 						 SLOT(OpenPreviewWindow()));
7510 
7511 	popup.addMenu(previewProjectorMain);
7512 	popup.addAction(previewWindow);
7513 	popup.exec(QCursor::pos());
7514 
7515 	UNUSED_PARAMETER(pos);
7516 }
7517 
7518 void OBSBasic::on_actionAlwaysOnTop_triggered()
7519 {
7520 #ifndef _WIN32
7521 	/* Make sure all dialogs are safely and successfully closed before
7522 	 * switching the always on top mode due to the fact that windows all
7523 	 * have to be recreated, so queue the actual toggle to happen after
7524 	 * all events related to closing the dialogs have finished */
7525 	CloseDialogs();
7526 #endif
7527 
7528 	QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop",
7529 				  Qt::QueuedConnection);
7530 }
7531 
7532 void OBSBasic::ToggleAlwaysOnTop()
7533 {
7534 	bool isAlwaysOnTop = IsAlwaysOnTop(this);
7535 
7536 	ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop);
7537 	SetAlwaysOnTop(this, !isAlwaysOnTop);
7538 
7539 	show();
7540 }
7541 
7542 void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const
7543 {
7544 	const char *val = config_get_string(basicConfig, "Video", "FPSCommon");
7545 
7546 	if (strcmp(val, "10") == 0) {
7547 		num = 10;
7548 		den = 1;
7549 	} else if (strcmp(val, "20") == 0) {
7550 		num = 20;
7551 		den = 1;
7552 	} else if (strcmp(val, "24 NTSC") == 0) {
7553 		num = 24000;
7554 		den = 1001;
7555 	} else if (strcmp(val, "25 PAL") == 0) {
7556 		num = 25;
7557 		den = 1;
7558 	} else if (strcmp(val, "29.97") == 0) {
7559 		num = 30000;
7560 		den = 1001;
7561 	} else if (strcmp(val, "48") == 0) {
7562 		num = 48;
7563 		den = 1;
7564 	} else if (strcmp(val, "50 PAL") == 0) {
7565 		num = 50;
7566 		den = 1;
7567 	} else if (strcmp(val, "59.94") == 0) {
7568 		num = 60000;
7569 		den = 1001;
7570 	} else if (strcmp(val, "60") == 0) {
7571 		num = 60;
7572 		den = 1;
7573 	} else {
7574 		num = 30;
7575 		den = 1;
7576 	}
7577 }
7578 
7579 void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const
7580 {
7581 	num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSInt");
7582 	den = 1;
7583 }
7584 
7585 void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const
7586 {
7587 	num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNum");
7588 	den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSDen");
7589 }
7590 
7591 void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const
7592 {
7593 	num = 1000000000;
7594 	den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNS");
7595 }
7596 
7597 void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const
7598 {
7599 	uint32_t type = config_get_uint(basicConfig, "Video", "FPSType");
7600 
7601 	if (type == 1) //"Integer"
7602 		GetFPSInteger(num, den);
7603 	else if (type == 2) //"Fraction"
7604 		GetFPSFraction(num, den);
7605 	else if (false) //"Nanoseconds", currently not implemented
7606 		GetFPSNanoseconds(num, den);
7607 	else
7608 		GetFPSCommon(num, den);
7609 }
7610 
7611 config_t *OBSBasic::Config() const
7612 {
7613 	return basicConfig;
7614 }
7615 
7616 void OBSBasic::on_actionEditTransform_triggered()
7617 {
7618 	if (transformWindow)
7619 		transformWindow->close();
7620 
7621 	if (!GetCurrentSceneItem())
7622 		return;
7623 
7624 	transformWindow = new OBSBasicTransform(this);
7625 	transformWindow->show();
7626 	transformWindow->setAttribute(Qt::WA_DeleteOnClose, true);
7627 }
7628 
7629 static obs_transform_info copiedTransformInfo;
7630 static obs_sceneitem_crop copiedCropInfo;
7631 
7632 void OBSBasic::on_actionCopyTransform_triggered()
7633 {
7634 	auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) {
7635 		if (!obs_sceneitem_selected(item))
7636 			return true;
7637 
7638 		obs_sceneitem_defer_update_begin(item);
7639 		obs_sceneitem_get_info(item, &copiedTransformInfo);
7640 		obs_sceneitem_get_crop(item, &copiedCropInfo);
7641 		obs_sceneitem_defer_update_end(item);
7642 
7643 		UNUSED_PARAMETER(scene);
7644 		UNUSED_PARAMETER(param);
7645 		return true;
7646 	};
7647 
7648 	obs_scene_enum_items(GetCurrentScene(), func, nullptr);
7649 	ui->actionPasteTransform->setEnabled(true);
7650 }
7651 
7652 void undo_redo(const std::string &data)
7653 {
7654 	obs_data_t *dat = obs_data_create_from_json(data.c_str());
7655 	obs_source_t *source =
7656 		obs_get_source_by_name(obs_data_get_string(dat, "scene_name"));
7657 	reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
7658 		->SetCurrentScene(source, true);
7659 	obs_source_release(source);
7660 	obs_data_release(dat);
7661 
7662 	obs_scene_load_transform_states(data.c_str());
7663 }
7664 
7665 void OBSBasic::on_actionPasteTransform_triggered()
7666 {
7667 	obs_data_t *wrapper =
7668 		obs_scene_save_transform_states(GetCurrentScene(), false);
7669 	auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) {
7670 		if (!obs_sceneitem_selected(item))
7671 			return true;
7672 
7673 		obs_sceneitem_defer_update_begin(item);
7674 		obs_sceneitem_set_info(item, &copiedTransformInfo);
7675 		obs_sceneitem_set_crop(item, &copiedCropInfo);
7676 		obs_sceneitem_defer_update_end(item);
7677 
7678 		UNUSED_PARAMETER(scene);
7679 		UNUSED_PARAMETER(param);
7680 		return true;
7681 	};
7682 
7683 	obs_scene_enum_items(GetCurrentScene(), func, nullptr);
7684 
7685 	obs_data_t *rwrapper =
7686 		obs_scene_save_transform_states(GetCurrentScene(), false);
7687 
7688 	std::string undo_data(obs_data_get_json(wrapper));
7689 	std::string redo_data(obs_data_get_json(rwrapper));
7690 	undo_s.add_action(
7691 		QTStr("Undo.Transform.Paste")
7692 			.arg(obs_source_get_name(GetCurrentSceneSource())),
7693 		undo_redo, undo_redo, undo_data, redo_data);
7694 
7695 	obs_data_release(wrapper);
7696 	obs_data_release(rwrapper);
7697 }
7698 
7699 static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
7700 {
7701 	if (obs_sceneitem_is_group(item))
7702 		obs_sceneitem_group_enum_items(item, reset_tr, nullptr);
7703 	if (!obs_sceneitem_selected(item))
7704 		return true;
7705 	if (obs_sceneitem_locked(item))
7706 		return true;
7707 
7708 	obs_sceneitem_defer_update_begin(item);
7709 
7710 	obs_transform_info info;
7711 	vec2_set(&info.pos, 0.0f, 0.0f);
7712 	vec2_set(&info.scale, 1.0f, 1.0f);
7713 	info.rot = 0.0f;
7714 	info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT;
7715 	info.bounds_type = OBS_BOUNDS_NONE;
7716 	info.bounds_alignment = OBS_ALIGN_CENTER;
7717 	vec2_set(&info.bounds, 0.0f, 0.0f);
7718 	obs_sceneitem_set_info(item, &info);
7719 
7720 	obs_sceneitem_crop crop = {};
7721 	obs_sceneitem_set_crop(item, &crop);
7722 
7723 	obs_sceneitem_defer_update_end(item);
7724 
7725 	UNUSED_PARAMETER(scene);
7726 	UNUSED_PARAMETER(param);
7727 	return true;
7728 }
7729 
7730 void OBSBasic::on_actionResetTransform_triggered()
7731 {
7732 	obs_scene_t *scene = GetCurrentScene();
7733 
7734 	obs_data_t *wrapper = obs_scene_save_transform_states(scene, false);
7735 	obs_scene_enum_items(scene, reset_tr, nullptr);
7736 	obs_data_t *rwrapper = obs_scene_save_transform_states(scene, false);
7737 
7738 	std::string undo_data(obs_data_get_json(wrapper));
7739 	std::string redo_data(obs_data_get_json(rwrapper));
7740 	undo_s.add_action(
7741 		QTStr("Undo.Transform.Reset")
7742 			.arg(obs_source_get_name(obs_scene_get_source(scene))),
7743 		undo_redo, undo_redo, undo_data, redo_data);
7744 
7745 	obs_data_release(wrapper);
7746 	obs_data_release(rwrapper);
7747 
7748 	obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr);
7749 }
7750 
7751 static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br)
7752 {
7753 	matrix4 boxTransform;
7754 	obs_sceneitem_get_box_transform(item, &boxTransform);
7755 
7756 	vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f);
7757 	vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f);
7758 
7759 	auto GetMinPos = [&](float x, float y) {
7760 		vec3 pos;
7761 		vec3_set(&pos, x, y, 0.0f);
7762 		vec3_transform(&pos, &pos, &boxTransform);
7763 		vec3_min(&tl, &tl, &pos);
7764 		vec3_max(&br, &br, &pos);
7765 	};
7766 
7767 	GetMinPos(0.0f, 0.0f);
7768 	GetMinPos(1.0f, 0.0f);
7769 	GetMinPos(0.0f, 1.0f);
7770 	GetMinPos(1.0f, 1.0f);
7771 }
7772 
7773 static vec3 GetItemTL(obs_sceneitem_t *item)
7774 {
7775 	vec3 tl, br;
7776 	GetItemBox(item, tl, br);
7777 	return tl;
7778 }
7779 
7780 static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl)
7781 {
7782 	vec3 newTL;
7783 	vec2 pos;
7784 
7785 	obs_sceneitem_get_pos(item, &pos);
7786 	newTL = GetItemTL(item);
7787 	pos.x += tl.x - newTL.x;
7788 	pos.y += tl.y - newTL.y;
7789 	obs_sceneitem_set_pos(item, &pos);
7790 }
7791 
7792 static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item,
7793 				  void *param)
7794 {
7795 	if (obs_sceneitem_is_group(item))
7796 		obs_sceneitem_group_enum_items(item, RotateSelectedSources,
7797 					       param);
7798 	if (!obs_sceneitem_selected(item))
7799 		return true;
7800 	if (obs_sceneitem_locked(item))
7801 		return true;
7802 
7803 	float rot = *reinterpret_cast<float *>(param);
7804 
7805 	vec3 tl = GetItemTL(item);
7806 
7807 	rot += obs_sceneitem_get_rot(item);
7808 	if (rot >= 360.0f)
7809 		rot -= 360.0f;
7810 	else if (rot <= -360.0f)
7811 		rot += 360.0f;
7812 	obs_sceneitem_set_rot(item, rot);
7813 
7814 	obs_sceneitem_force_update_transform(item);
7815 
7816 	SetItemTL(item, tl);
7817 
7818 	UNUSED_PARAMETER(scene);
7819 	return true;
7820 };
7821 
7822 void OBSBasic::on_actionRotate90CW_triggered()
7823 {
7824 	float f90CW = 90.0f;
7825 	obs_data_t *wrapper =
7826 		obs_scene_save_transform_states(GetCurrentScene(), false);
7827 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW);
7828 	obs_data_t *rwrapper =
7829 		obs_scene_save_transform_states(GetCurrentScene(), false);
7830 
7831 	std::string undo_data(obs_data_get_json(wrapper));
7832 	std::string redo_data(obs_data_get_json(rwrapper));
7833 	undo_s.add_action(QTStr("Undo.Transform.Rotate")
7834 				  .arg(obs_source_get_name(obs_scene_get_source(
7835 					  GetCurrentScene()))),
7836 			  undo_redo, undo_redo, undo_data, redo_data);
7837 
7838 	obs_data_release(wrapper);
7839 	obs_data_release(rwrapper);
7840 }
7841 
7842 void OBSBasic::on_actionRotate90CCW_triggered()
7843 {
7844 	float f90CCW = -90.0f;
7845 	obs_data_t *wrapper =
7846 		obs_scene_save_transform_states(GetCurrentScene(), false);
7847 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW);
7848 	obs_data_t *rwrapper =
7849 		obs_scene_save_transform_states(GetCurrentScene(), false);
7850 
7851 	std::string undo_data(obs_data_get_json(wrapper));
7852 	std::string redo_data(obs_data_get_json(rwrapper));
7853 	undo_s.add_action(QTStr("Undo.Transform.Rotate")
7854 				  .arg(obs_source_get_name(obs_scene_get_source(
7855 					  GetCurrentScene()))),
7856 			  undo_redo, undo_redo, undo_data, redo_data);
7857 
7858 	obs_data_release(wrapper);
7859 	obs_data_release(rwrapper);
7860 }
7861 
7862 void OBSBasic::on_actionRotate180_triggered()
7863 {
7864 	float f180 = 180.0f;
7865 	obs_data_t *wrapper =
7866 		obs_scene_save_transform_states(GetCurrentScene(), false);
7867 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180);
7868 	obs_data_t *rwrapper =
7869 		obs_scene_save_transform_states(GetCurrentScene(), false);
7870 
7871 	std::string undo_data(obs_data_get_json(wrapper));
7872 	std::string redo_data(obs_data_get_json(rwrapper));
7873 	undo_s.add_action(QTStr("Undo.Transform.Rotate")
7874 				  .arg(obs_source_get_name(obs_scene_get_source(
7875 					  GetCurrentScene()))),
7876 			  undo_redo, undo_redo, undo_data, redo_data);
7877 
7878 	obs_data_release(wrapper);
7879 	obs_data_release(rwrapper);
7880 }
7881 
7882 static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item,
7883 				      void *param)
7884 {
7885 	vec2 &mul = *reinterpret_cast<vec2 *>(param);
7886 
7887 	if (obs_sceneitem_is_group(item))
7888 		obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale,
7889 					       param);
7890 	if (!obs_sceneitem_selected(item))
7891 		return true;
7892 	if (obs_sceneitem_locked(item))
7893 		return true;
7894 
7895 	vec3 tl = GetItemTL(item);
7896 
7897 	vec2 scale;
7898 	obs_sceneitem_get_scale(item, &scale);
7899 	vec2_mul(&scale, &scale, &mul);
7900 	obs_sceneitem_set_scale(item, &scale);
7901 
7902 	obs_sceneitem_force_update_transform(item);
7903 
7904 	SetItemTL(item, tl);
7905 
7906 	UNUSED_PARAMETER(scene);
7907 	return true;
7908 }
7909 
7910 void OBSBasic::on_actionFlipHorizontal_triggered()
7911 {
7912 	vec2 scale;
7913 	vec2_set(&scale, -1.0f, 1.0f);
7914 	obs_data_t *wrapper =
7915 		obs_scene_save_transform_states(GetCurrentScene(), false);
7916 	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
7917 			     &scale);
7918 	obs_data_t *rwrapper =
7919 		obs_scene_save_transform_states(GetCurrentScene(), false);
7920 
7921 	std::string undo_data(obs_data_get_json(wrapper));
7922 	std::string redo_data(obs_data_get_json(rwrapper));
7923 	undo_s.add_action(QTStr("Undo.Transform.HFlip")
7924 				  .arg(obs_source_get_name(obs_scene_get_source(
7925 					  GetCurrentScene()))),
7926 			  undo_redo, undo_redo, undo_data, redo_data);
7927 
7928 	obs_data_release(wrapper);
7929 	obs_data_release(rwrapper);
7930 }
7931 
7932 void OBSBasic::on_actionFlipVertical_triggered()
7933 {
7934 	vec2 scale;
7935 	vec2_set(&scale, 1.0f, -1.0f);
7936 	obs_data_t *wrapper =
7937 		obs_scene_save_transform_states(GetCurrentScene(), false);
7938 	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
7939 			     &scale);
7940 	obs_data_t *rwrapper =
7941 		obs_scene_save_transform_states(GetCurrentScene(), false);
7942 
7943 	std::string undo_data(obs_data_get_json(wrapper));
7944 	std::string redo_data(obs_data_get_json(rwrapper));
7945 	undo_s.add_action(QTStr("Undo.Transform.VFlip")
7946 				  .arg(obs_source_get_name(obs_scene_get_source(
7947 					  GetCurrentScene()))),
7948 			  undo_redo, undo_redo, undo_data, redo_data);
7949 
7950 	obs_data_release(wrapper);
7951 	obs_data_release(rwrapper);
7952 }
7953 
7954 static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item,
7955 				     void *param)
7956 {
7957 	obs_bounds_type boundsType =
7958 		*reinterpret_cast<obs_bounds_type *>(param);
7959 
7960 	if (obs_sceneitem_is_group(item))
7961 		obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems,
7962 					       param);
7963 	if (!obs_sceneitem_selected(item))
7964 		return true;
7965 	if (obs_sceneitem_locked(item))
7966 		return true;
7967 
7968 	obs_video_info ovi;
7969 	obs_get_video_info(&ovi);
7970 
7971 	obs_transform_info itemInfo;
7972 	vec2_set(&itemInfo.pos, 0.0f, 0.0f);
7973 	vec2_set(&itemInfo.scale, 1.0f, 1.0f);
7974 	itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP;
7975 	itemInfo.rot = 0.0f;
7976 
7977 	vec2_set(&itemInfo.bounds, float(ovi.base_width),
7978 		 float(ovi.base_height));
7979 	itemInfo.bounds_type = boundsType;
7980 	itemInfo.bounds_alignment = OBS_ALIGN_CENTER;
7981 
7982 	obs_sceneitem_set_info(item, &itemInfo);
7983 
7984 	UNUSED_PARAMETER(scene);
7985 	return true;
7986 }
7987 
7988 void OBSBasic::on_actionFitToScreen_triggered()
7989 {
7990 	obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER;
7991 	obs_data_t *wrapper =
7992 		obs_scene_save_transform_states(GetCurrentScene(), false);
7993 	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
7994 			     &boundsType);
7995 	obs_data_t *rwrapper =
7996 		obs_scene_save_transform_states(GetCurrentScene(), false);
7997 
7998 	std::string undo_data(obs_data_get_json(wrapper));
7999 	std::string redo_data(obs_data_get_json(rwrapper));
8000 	undo_s.add_action(QTStr("Undo.Transform.FitToScreen")
8001 				  .arg(obs_source_get_name(obs_scene_get_source(
8002 					  GetCurrentScene()))),
8003 			  undo_redo, undo_redo, undo_data, redo_data);
8004 
8005 	obs_data_release(wrapper);
8006 	obs_data_release(rwrapper);
8007 }
8008 
8009 void OBSBasic::on_actionStretchToScreen_triggered()
8010 {
8011 	obs_bounds_type boundsType = OBS_BOUNDS_STRETCH;
8012 	obs_data_t *wrapper =
8013 		obs_scene_save_transform_states(GetCurrentScene(), false);
8014 	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
8015 			     &boundsType);
8016 	obs_data_t *rwrapper =
8017 		obs_scene_save_transform_states(GetCurrentScene(), false);
8018 
8019 	std::string undo_data(obs_data_get_json(wrapper));
8020 	std::string redo_data(obs_data_get_json(rwrapper));
8021 	undo_s.add_action(QTStr("Undo.Transform.StretchToScreen")
8022 				  .arg(obs_source_get_name(obs_scene_get_source(
8023 					  GetCurrentScene()))),
8024 			  undo_redo, undo_redo, undo_data, redo_data);
8025 
8026 	obs_data_release(wrapper);
8027 	obs_data_release(rwrapper);
8028 }
8029 
8030 enum class CenterType {
8031 	Scene,
8032 	Vertical,
8033 	Horizontal,
8034 };
8035 
8036 static bool center_to_scene(obs_scene_t *, obs_sceneitem_t *item, void *param)
8037 {
8038 	CenterType centerType = *reinterpret_cast<CenterType *>(param);
8039 
8040 	vec3 tl, br, itemCenter, screenCenter, offset;
8041 	obs_video_info ovi;
8042 	obs_transform_info oti;
8043 
8044 	if (obs_sceneitem_is_group(item))
8045 		obs_sceneitem_group_enum_items(item, center_to_scene,
8046 					       &centerType);
8047 	if (!obs_sceneitem_selected(item))
8048 		return true;
8049 	if (obs_sceneitem_locked(item))
8050 		return true;
8051 
8052 	obs_get_video_info(&ovi);
8053 	obs_sceneitem_get_info(item, &oti);
8054 
8055 	if (centerType == CenterType::Scene)
8056 		vec3_set(&screenCenter, float(ovi.base_width),
8057 			 float(ovi.base_height), 0.0f);
8058 	else if (centerType == CenterType::Vertical)
8059 		vec3_set(&screenCenter, float(oti.bounds.x),
8060 			 float(ovi.base_height), 0.0f);
8061 	else if (centerType == CenterType::Horizontal)
8062 		vec3_set(&screenCenter, float(ovi.base_width),
8063 			 float(oti.bounds.y), 0.0f);
8064 
8065 	vec3_mulf(&screenCenter, &screenCenter, 0.5f);
8066 
8067 	GetItemBox(item, tl, br);
8068 
8069 	vec3_sub(&itemCenter, &br, &tl);
8070 	vec3_mulf(&itemCenter, &itemCenter, 0.5f);
8071 	vec3_add(&itemCenter, &itemCenter, &tl);
8072 
8073 	vec3_sub(&offset, &screenCenter, &itemCenter);
8074 	vec3_add(&tl, &tl, &offset);
8075 
8076 	if (centerType == CenterType::Vertical)
8077 		tl.x = oti.pos.x;
8078 	else if (centerType == CenterType::Horizontal)
8079 		tl.y = oti.pos.y;
8080 
8081 	SetItemTL(item, tl);
8082 	return true;
8083 };
8084 
8085 void OBSBasic::on_actionCenterToScreen_triggered()
8086 {
8087 	CenterType centerType = CenterType::Scene;
8088 	obs_data_t *wrapper =
8089 		obs_scene_save_transform_states(GetCurrentScene(), false);
8090 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
8091 	obs_data_t *rwrapper =
8092 		obs_scene_save_transform_states(GetCurrentScene(), false);
8093 
8094 	std::string undo_data(obs_data_get_json(wrapper));
8095 	std::string redo_data(obs_data_get_json(rwrapper));
8096 	undo_s.add_action(QTStr("Undo.Transform.Center")
8097 				  .arg(obs_source_get_name(obs_scene_get_source(
8098 					  GetCurrentScene()))),
8099 			  undo_redo, undo_redo, undo_data, redo_data);
8100 
8101 	obs_data_release(wrapper);
8102 	obs_data_release(rwrapper);
8103 }
8104 
8105 void OBSBasic::on_actionVerticalCenter_triggered()
8106 {
8107 	CenterType centerType = CenterType::Vertical;
8108 	obs_data_t *wrapper =
8109 		obs_scene_save_transform_states(GetCurrentScene(), false);
8110 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
8111 	obs_data_t *rwrapper =
8112 		obs_scene_save_transform_states(GetCurrentScene(), false);
8113 
8114 	std::string undo_data(obs_data_get_json(wrapper));
8115 	std::string redo_data(obs_data_get_json(rwrapper));
8116 	undo_s.add_action(QTStr("Undo.Transform.VCenter")
8117 				  .arg(obs_source_get_name(obs_scene_get_source(
8118 					  GetCurrentScene()))),
8119 			  undo_redo, undo_redo, undo_data, redo_data);
8120 
8121 	obs_data_release(wrapper);
8122 	obs_data_release(rwrapper);
8123 }
8124 
8125 void OBSBasic::on_actionHorizontalCenter_triggered()
8126 {
8127 	CenterType centerType = CenterType::Horizontal;
8128 	obs_data_t *wrapper =
8129 		obs_scene_save_transform_states(GetCurrentScene(), false);
8130 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
8131 	obs_data_t *rwrapper =
8132 		obs_scene_save_transform_states(GetCurrentScene(), false);
8133 
8134 	std::string undo_data(obs_data_get_json(wrapper));
8135 	std::string redo_data(obs_data_get_json(rwrapper));
8136 	undo_s.add_action(QTStr("Undo.Transform.HCenter")
8137 				  .arg(obs_source_get_name(obs_scene_get_source(
8138 					  GetCurrentScene()))),
8139 			  undo_redo, undo_redo, undo_data, redo_data);
8140 
8141 	obs_data_release(wrapper);
8142 	obs_data_release(rwrapper);
8143 }
8144 
8145 void OBSBasic::EnablePreviewDisplay(bool enable)
8146 {
8147 	obs_display_set_enabled(ui->preview->GetDisplay(), enable);
8148 	ui->preview->setVisible(enable);
8149 	ui->previewDisabledWidget->setVisible(!enable);
8150 }
8151 
8152 void OBSBasic::TogglePreview()
8153 {
8154 	previewEnabled = !previewEnabled;
8155 	EnablePreviewDisplay(previewEnabled);
8156 }
8157 
8158 void OBSBasic::EnablePreview()
8159 {
8160 	if (previewProgramMode)
8161 		return;
8162 
8163 	previewEnabled = true;
8164 	EnablePreviewDisplay(true);
8165 }
8166 
8167 void OBSBasic::DisablePreview()
8168 {
8169 	if (previewProgramMode)
8170 		return;
8171 
8172 	previewEnabled = false;
8173 	EnablePreviewDisplay(false);
8174 }
8175 
8176 static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param)
8177 {
8178 	if (obs_sceneitem_locked(item))
8179 		return true;
8180 
8181 	struct vec2 &offset = *reinterpret_cast<struct vec2 *>(param);
8182 	struct vec2 pos;
8183 
8184 	if (!obs_sceneitem_selected(item)) {
8185 		if (obs_sceneitem_is_group(item)) {
8186 			struct vec3 offset3;
8187 			vec3_set(&offset3, offset.x, offset.y, 0.0f);
8188 
8189 			struct matrix4 matrix;
8190 			obs_sceneitem_get_draw_transform(item, &matrix);
8191 			vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f);
8192 			matrix4_inv(&matrix, &matrix);
8193 			vec3_transform(&offset3, &offset3, &matrix);
8194 
8195 			struct vec2 new_offset;
8196 			vec2_set(&new_offset, offset3.x, offset3.y);
8197 			obs_sceneitem_group_enum_items(item, nudge_callback,
8198 						       &new_offset);
8199 		}
8200 
8201 		return true;
8202 	}
8203 
8204 	obs_sceneitem_get_pos(item, &pos);
8205 	vec2_add(&pos, &pos, &offset);
8206 	obs_sceneitem_set_pos(item, &pos);
8207 	return true;
8208 }
8209 
8210 void OBSBasic::Nudge(int dist, MoveDir dir)
8211 {
8212 	if (ui->preview->Locked())
8213 		return;
8214 
8215 	struct vec2 offset;
8216 	vec2_set(&offset, 0.0f, 0.0f);
8217 
8218 	switch (dir) {
8219 	case MoveDir::Up:
8220 		offset.y = (float)-dist;
8221 		break;
8222 	case MoveDir::Down:
8223 		offset.y = (float)dist;
8224 		break;
8225 	case MoveDir::Left:
8226 		offset.x = (float)-dist;
8227 		break;
8228 	case MoveDir::Right:
8229 		offset.x = (float)dist;
8230 		break;
8231 	}
8232 
8233 	if (!recent_nudge) {
8234 		recent_nudge = true;
8235 		obs_data_t *wrapper = obs_scene_save_transform_states(
8236 			GetCurrentScene(), true);
8237 		std::string undo_data(obs_data_get_json(wrapper));
8238 
8239 		nudge_timer = new QTimer;
8240 		QObject::connect(
8241 			nudge_timer, &QTimer::timeout,
8242 			[this, &recent_nudge = recent_nudge, undo_data]() {
8243 				obs_data_t *rwrapper =
8244 					obs_scene_save_transform_states(
8245 						GetCurrentScene(), true);
8246 				std::string redo_data(
8247 					obs_data_get_json(rwrapper));
8248 
8249 				undo_s.add_action(
8250 					QTStr("Undo.Transform")
8251 						.arg(obs_source_get_name(
8252 							GetCurrentSceneSource())),
8253 					undo_redo, undo_redo, undo_data,
8254 					redo_data);
8255 
8256 				recent_nudge = false;
8257 				obs_data_release(rwrapper);
8258 			});
8259 		connect(nudge_timer, &QTimer::timeout, nudge_timer,
8260 			&QTimer::deleteLater);
8261 		nudge_timer->setSingleShot(true);
8262 
8263 		obs_data_release(wrapper);
8264 	}
8265 
8266 	if (nudge_timer) {
8267 		nudge_timer->stop();
8268 		nudge_timer->start(1000);
8269 	} else {
8270 		blog(LOG_ERROR, "No nudge timer!");
8271 	}
8272 
8273 	obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset);
8274 }
8275 
8276 void OBSBasic::NudgeUp()
8277 {
8278 	Nudge(1, MoveDir::Up);
8279 }
8280 void OBSBasic::NudgeDown()
8281 {
8282 	Nudge(1, MoveDir::Down);
8283 }
8284 void OBSBasic::NudgeLeft()
8285 {
8286 	Nudge(1, MoveDir::Left);
8287 }
8288 void OBSBasic::NudgeRight()
8289 {
8290 	Nudge(1, MoveDir::Right);
8291 }
8292 
8293 void OBSBasic::DeleteProjector(OBSProjector *projector)
8294 {
8295 	for (size_t i = 0; i < projectors.size(); i++) {
8296 		if (projectors[i] == projector) {
8297 			projectors[i]->deleteLater();
8298 			projectors.erase(projectors.begin() + i);
8299 			break;
8300 		}
8301 	}
8302 }
8303 
8304 OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor,
8305 				      ProjectorType type)
8306 {
8307 	/* seriously?  10 monitors? */
8308 	if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1)
8309 		return nullptr;
8310 
8311 	OBSProjector *projector =
8312 		new OBSProjector(nullptr, source, monitor, type);
8313 
8314 	projectors.emplace_back(projector);
8315 
8316 	return projector;
8317 }
8318 
8319 void OBSBasic::OpenStudioProgramProjector()
8320 {
8321 	int monitor = sender()->property("monitor").toInt();
8322 	OpenProjector(nullptr, monitor, ProjectorType::StudioProgram);
8323 }
8324 
8325 void OBSBasic::OpenPreviewProjector()
8326 {
8327 	int monitor = sender()->property("monitor").toInt();
8328 	OpenProjector(nullptr, monitor, ProjectorType::Preview);
8329 }
8330 
8331 void OBSBasic::OpenSourceProjector()
8332 {
8333 	int monitor = sender()->property("monitor").toInt();
8334 	OBSSceneItem item = GetCurrentSceneItem();
8335 	if (!item)
8336 		return;
8337 
8338 	OpenProjector(obs_sceneitem_get_source(item), monitor,
8339 		      ProjectorType::Source);
8340 }
8341 
8342 void OBSBasic::OpenMultiviewProjector()
8343 {
8344 	int monitor = sender()->property("monitor").toInt();
8345 	OpenProjector(nullptr, monitor, ProjectorType::Multiview);
8346 }
8347 
8348 void OBSBasic::OpenSceneProjector()
8349 {
8350 	int monitor = sender()->property("monitor").toInt();
8351 	OBSScene scene = GetCurrentScene();
8352 	if (!scene)
8353 		return;
8354 
8355 	OpenProjector(obs_scene_get_source(scene), monitor,
8356 		      ProjectorType::Scene);
8357 }
8358 
8359 void OBSBasic::OpenStudioProgramWindow()
8360 {
8361 	OpenProjector(nullptr, -1, ProjectorType::StudioProgram);
8362 }
8363 
8364 void OBSBasic::OpenPreviewWindow()
8365 {
8366 	OpenProjector(nullptr, -1, ProjectorType::Preview);
8367 }
8368 
8369 void OBSBasic::OpenSourceWindow()
8370 {
8371 	OBSSceneItem item = GetCurrentSceneItem();
8372 	if (!item)
8373 		return;
8374 
8375 	OBSSource source = obs_sceneitem_get_source(item);
8376 
8377 	OpenProjector(obs_sceneitem_get_source(item), -1,
8378 		      ProjectorType::Source);
8379 }
8380 
8381 void OBSBasic::OpenMultiviewWindow()
8382 {
8383 	OpenProjector(nullptr, -1, ProjectorType::Multiview);
8384 }
8385 
8386 void OBSBasic::OpenSceneWindow()
8387 {
8388 	OBSScene scene = GetCurrentScene();
8389 	if (!scene)
8390 		return;
8391 
8392 	OBSSource source = obs_scene_get_source(scene);
8393 
8394 	OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene);
8395 }
8396 
8397 void OBSBasic::OpenSavedProjectors()
8398 {
8399 	for (SavedProjectorInfo *info : savedProjectorsArray) {
8400 		OpenSavedProjector(info);
8401 	}
8402 }
8403 
8404 void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info)
8405 {
8406 	if (info) {
8407 		OBSProjector *projector = nullptr;
8408 		switch (info->type) {
8409 		case ProjectorType::Source:
8410 		case ProjectorType::Scene: {
8411 			OBSSource source =
8412 				obs_get_source_by_name(info->name.c_str());
8413 			if (!source)
8414 				return;
8415 
8416 			projector = OpenProjector(source, info->monitor,
8417 						  info->type);
8418 
8419 			obs_source_release(source);
8420 			break;
8421 		}
8422 		default: {
8423 			projector = OpenProjector(nullptr, info->monitor,
8424 						  info->type);
8425 			break;
8426 		}
8427 		}
8428 
8429 		if (projector && !info->geometry.empty() && info->monitor < 0) {
8430 			QByteArray byteArray = QByteArray::fromBase64(
8431 				QByteArray(info->geometry.c_str()));
8432 			projector->restoreGeometry(byteArray);
8433 
8434 			if (!WindowPositionValid(projector->normalGeometry())) {
8435 				QRect rect = QGuiApplication::primaryScreen()
8436 						     ->geometry();
8437 				projector->setGeometry(QStyle::alignedRect(
8438 					Qt::LeftToRight, Qt::AlignCenter,
8439 					size(), rect));
8440 			}
8441 
8442 			if (info->alwaysOnTopOverridden)
8443 				projector->SetIsAlwaysOnTop(info->alwaysOnTop,
8444 							    true);
8445 		}
8446 	}
8447 }
8448 
8449 void OBSBasic::on_actionFullscreenInterface_triggered()
8450 {
8451 	if (!isFullScreen())
8452 		showFullScreen();
8453 	else
8454 		showNormal();
8455 }
8456 
8457 void OBSBasic::UpdateTitleBar()
8458 {
8459 	stringstream name;
8460 
8461 	const char *profile =
8462 		config_get_string(App()->GlobalConfig(), "Basic", "Profile");
8463 	const char *sceneCollection = config_get_string(
8464 		App()->GlobalConfig(), "Basic", "SceneCollection");
8465 
8466 	name << "OBS ";
8467 	if (previewProgramMode)
8468 		name << "Studio ";
8469 
8470 	name << App()->GetVersionString();
8471 	if (App()->IsPortableMode())
8472 		name << " - Portable Mode";
8473 
8474 	name << " - " << Str("TitleBar.Profile") << ": " << profile;
8475 	name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection;
8476 
8477 	setWindowTitle(QT_UTF8(name.str().c_str()));
8478 }
8479 
8480 int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const
8481 {
8482 	char profiles_path[512];
8483 	const char *profile =
8484 		config_get_string(App()->GlobalConfig(), "Basic", "ProfileDir");
8485 	int ret;
8486 
8487 	if (!profile)
8488 		return -1;
8489 	if (!path)
8490 		return -1;
8491 	if (!file)
8492 		file = "";
8493 
8494 	ret = GetConfigPath(profiles_path, 512, "obs-studio/basic/profiles");
8495 	if (ret <= 0)
8496 		return ret;
8497 
8498 	if (!*file)
8499 		return snprintf(path, size, "%s/%s", profiles_path, profile);
8500 
8501 	return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file);
8502 }
8503 
8504 void OBSBasic::on_resetUI_triggered()
8505 {
8506 	/* prune deleted extra docks */
8507 	for (int i = extraDocks.size() - 1; i >= 0; i--) {
8508 		if (!extraDocks[i]) {
8509 			extraDocks.removeAt(i);
8510 		}
8511 	}
8512 
8513 	if (extraDocks.size()) {
8514 		QMessageBox::StandardButton button = QMessageBox::question(
8515 			this, QTStr("ResetUIWarning.Title"),
8516 			QTStr("ResetUIWarning.Text"));
8517 
8518 		if (button == QMessageBox::No)
8519 			return;
8520 	}
8521 
8522 	/* undock/hide/center extra docks */
8523 	for (int i = extraDocks.size() - 1; i >= 0; i--) {
8524 		if (extraDocks[i]) {
8525 			extraDocks[i]->setVisible(true);
8526 			extraDocks[i]->setFloating(true);
8527 			extraDocks[i]->move(frameGeometry().topLeft() +
8528 					    rect().center() -
8529 					    extraDocks[i]->rect().center());
8530 			extraDocks[i]->setVisible(false);
8531 		}
8532 	}
8533 
8534 	restoreState(startingDockLayout);
8535 
8536 	int cx = width();
8537 	int cy = height();
8538 
8539 	int cx22_5 = cx * 225 / 1000;
8540 	int cx5 = cx * 5 / 100;
8541 
8542 	cy = cy * 225 / 1000;
8543 
8544 	int mixerSize = cx - (cx22_5 * 2 + cx5 * 2);
8545 
8546 	QList<QDockWidget *> docks{ui->scenesDock, ui->sourcesDock,
8547 				   ui->mixerDock, ui->transitionsDock,
8548 				   ui->controlsDock};
8549 
8550 	QList<int> sizes{cx22_5, cx22_5, mixerSize, cx5, cx5};
8551 
8552 	ui->scenesDock->setVisible(true);
8553 	ui->sourcesDock->setVisible(true);
8554 	ui->mixerDock->setVisible(true);
8555 	ui->transitionsDock->setVisible(true);
8556 	ui->controlsDock->setVisible(true);
8557 	statsDock->setVisible(false);
8558 	statsDock->setFloating(true);
8559 
8560 	resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical);
8561 	resizeDocks(docks, sizes, Qt::Horizontal);
8562 
8563 	activateWindow();
8564 }
8565 
8566 void OBSBasic::on_lockUI_toggled(bool lock)
8567 {
8568 	QDockWidget::DockWidgetFeatures features =
8569 		lock ? QDockWidget::NoDockWidgetFeatures
8570 		     : (QDockWidget::DockWidgetClosable |
8571 			QDockWidget::DockWidgetMovable |
8572 			QDockWidget::DockWidgetFloatable);
8573 
8574 	QDockWidget::DockWidgetFeatures mainFeatures = features;
8575 	mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable;
8576 
8577 	ui->scenesDock->setFeatures(mainFeatures);
8578 	ui->sourcesDock->setFeatures(mainFeatures);
8579 	ui->mixerDock->setFeatures(mainFeatures);
8580 	ui->transitionsDock->setFeatures(mainFeatures);
8581 	ui->controlsDock->setFeatures(mainFeatures);
8582 	statsDock->setFeatures(features);
8583 
8584 	for (int i = extraDocks.size() - 1; i >= 0; i--) {
8585 		if (!extraDocks[i]) {
8586 			extraDocks.removeAt(i);
8587 		} else {
8588 			extraDocks[i]->setFeatures(features);
8589 		}
8590 	}
8591 }
8592 
8593 void OBSBasic::on_toggleListboxToolbars_toggled(bool visible)
8594 {
8595 	ui->sourcesToolbar->setVisible(visible);
8596 	ui->scenesToolbar->setVisible(visible);
8597 
8598 	config_set_bool(App()->GlobalConfig(), "BasicWindow",
8599 			"ShowListboxToolbars", visible);
8600 }
8601 
8602 void OBSBasic::ShowContextBar()
8603 {
8604 	on_toggleContextBar_toggled(true);
8605 }
8606 
8607 void OBSBasic::HideContextBar()
8608 {
8609 	on_toggleContextBar_toggled(false);
8610 }
8611 
8612 void OBSBasic::on_toggleContextBar_toggled(bool visible)
8613 {
8614 	config_set_bool(App()->GlobalConfig(), "BasicWindow",
8615 			"ShowContextToolbars", visible);
8616 	this->ui->contextContainer->setVisible(visible);
8617 	UpdateContextBar(true);
8618 }
8619 
8620 void OBSBasic::on_toggleStatusBar_toggled(bool visible)
8621 {
8622 	ui->statusbar->setVisible(visible);
8623 
8624 	config_set_bool(App()->GlobalConfig(), "BasicWindow", "ShowStatusBar",
8625 			visible);
8626 }
8627 
8628 void OBSBasic::on_toggleSourceIcons_toggled(bool visible)
8629 {
8630 	ui->sources->SetIconsVisible(visible);
8631 	if (advAudioWindow != nullptr)
8632 		advAudioWindow->SetIconsVisible(visible);
8633 
8634 	config_set_bool(App()->GlobalConfig(), "BasicWindow", "ShowSourceIcons",
8635 			visible);
8636 }
8637 
8638 void OBSBasic::on_actionLockPreview_triggered()
8639 {
8640 	ui->preview->ToggleLocked();
8641 	ui->actionLockPreview->setChecked(ui->preview->Locked());
8642 }
8643 
8644 void OBSBasic::on_scalingMenu_aboutToShow()
8645 {
8646 	obs_video_info ovi;
8647 	obs_get_video_info(&ovi);
8648 
8649 	QAction *action = ui->actionScaleCanvas;
8650 	QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas");
8651 	text = text.arg(QString::number(ovi.base_width),
8652 			QString::number(ovi.base_height));
8653 	action->setText(text);
8654 
8655 	action = ui->actionScaleOutput;
8656 	text = QTStr("Basic.MainMenu.Edit.Scale.Output");
8657 	text = text.arg(QString::number(ovi.output_width),
8658 			QString::number(ovi.output_height));
8659 	action->setText(text);
8660 	action->setVisible(!(ovi.output_width == ovi.base_width &&
8661 			     ovi.output_height == ovi.base_height));
8662 
8663 	UpdatePreviewScalingMenu();
8664 }
8665 
8666 void OBSBasic::on_actionScaleWindow_triggered()
8667 {
8668 	ui->preview->SetFixedScaling(false);
8669 	ui->preview->ResetScrollingOffset();
8670 	emit ui->preview->DisplayResized();
8671 }
8672 
8673 void OBSBasic::on_actionScaleCanvas_triggered()
8674 {
8675 	ui->preview->SetFixedScaling(true);
8676 	ui->preview->SetScalingLevel(0);
8677 	emit ui->preview->DisplayResized();
8678 }
8679 
8680 void OBSBasic::on_actionScaleOutput_triggered()
8681 {
8682 	obs_video_info ovi;
8683 	obs_get_video_info(&ovi);
8684 
8685 	ui->preview->SetFixedScaling(true);
8686 	float scalingAmount = float(ovi.output_width) / float(ovi.base_width);
8687 	// log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY)
8688 	int32_t approxScalingLevel =
8689 		int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY)));
8690 	ui->preview->SetScalingLevel(approxScalingLevel);
8691 	ui->preview->SetScalingAmount(scalingAmount);
8692 	emit ui->preview->DisplayResized();
8693 }
8694 
8695 void OBSBasic::SetShowing(bool showing)
8696 {
8697 	if (!showing && isVisible()) {
8698 		config_set_string(App()->GlobalConfig(), "BasicWindow",
8699 				  "geometry",
8700 				  saveGeometry().toBase64().constData());
8701 
8702 		/* hide all visible child dialogs */
8703 		visDlgPositions.clear();
8704 		if (!visDialogs.isEmpty()) {
8705 			for (QDialog *dlg : visDialogs) {
8706 				visDlgPositions.append(dlg->pos());
8707 				dlg->hide();
8708 			}
8709 		}
8710 
8711 		if (showHide)
8712 			showHide->setText(QTStr("Basic.SystemTray.Show"));
8713 		QTimer::singleShot(250, this, SLOT(hide()));
8714 
8715 		setVisible(false);
8716 
8717 #ifdef __APPLE__
8718 		EnableOSXDockIcon(false);
8719 #endif
8720 
8721 	} else if (showing && !isVisible()) {
8722 		if (showHide)
8723 			showHide->setText(QTStr("Basic.SystemTray.Hide"));
8724 		QTimer::singleShot(250, this, SLOT(show()));
8725 
8726 		setVisible(true);
8727 
8728 #ifdef __APPLE__
8729 		EnableOSXDockIcon(true);
8730 #endif
8731 
8732 		/* raise and activate window to ensure it is on top */
8733 		raise();
8734 		activateWindow();
8735 
8736 		/* show all child dialogs that was visible earlier */
8737 		if (!visDialogs.isEmpty()) {
8738 			for (int i = 0; i < visDialogs.size(); ++i) {
8739 				QDialog *dlg = visDialogs[i];
8740 				dlg->move(visDlgPositions[i]);
8741 				dlg->show();
8742 			}
8743 		}
8744 
8745 		/* Unminimize window if it was hidden to tray instead of task
8746 		 * bar. */
8747 		if (sysTrayMinimizeToTray()) {
8748 			Qt::WindowStates state;
8749 			state = windowState() & ~Qt::WindowMinimized;
8750 			state |= Qt::WindowActive;
8751 			setWindowState(state);
8752 		}
8753 	}
8754 }
8755 
8756 void OBSBasic::ToggleShowHide()
8757 {
8758 	bool showing = isVisible();
8759 	if (showing) {
8760 		/* check for modal dialogs */
8761 		EnumDialogs();
8762 		if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty())
8763 			return;
8764 	}
8765 	SetShowing(!showing);
8766 }
8767 
8768 void OBSBasic::SystemTrayInit()
8769 {
8770 #ifdef __APPLE__
8771 	QIcon trayIconFile = QIcon(":/res/images/obs_macos.png");
8772 	trayIconFile.setIsMask(true);
8773 #else
8774 	QIcon trayIconFile = QIcon(":/res/images/obs.png");
8775 #endif
8776 	trayIcon.reset(new QSystemTrayIcon(
8777 		QIcon::fromTheme("obs-tray", trayIconFile), this));
8778 	trayIcon->setToolTip("OBS Studio");
8779 
8780 	showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data());
8781 	sysTrayStream = new QAction(QTStr("Basic.Main.StartStreaming"),
8782 				    trayIcon.data());
8783 	sysTrayRecord = new QAction(QTStr("Basic.Main.StartRecording"),
8784 				    trayIcon.data());
8785 	sysTrayReplayBuffer = new QAction(QTStr("Basic.Main.StartReplayBuffer"),
8786 					  trayIcon.data());
8787 	sysTrayVirtualCam = new QAction(QTStr("Basic.Main.StartVirtualCam"),
8788 					trayIcon.data());
8789 	exit = new QAction(QTStr("Exit"), trayIcon.data());
8790 
8791 	trayMenu = new QMenu;
8792 	previewProjector = new QMenu(QTStr("PreviewProjector"));
8793 	studioProgramProjector = new QMenu(QTStr("StudioProgramProjector"));
8794 	AddProjectorMenuMonitors(previewProjector, this,
8795 				 SLOT(OpenPreviewProjector()));
8796 	AddProjectorMenuMonitors(studioProgramProjector, this,
8797 				 SLOT(OpenStudioProgramProjector()));
8798 	trayMenu->addAction(showHide);
8799 	trayMenu->addMenu(previewProjector);
8800 	trayMenu->addMenu(studioProgramProjector);
8801 	trayMenu->addAction(sysTrayStream);
8802 	trayMenu->addAction(sysTrayRecord);
8803 	trayMenu->addAction(sysTrayReplayBuffer);
8804 	trayMenu->addAction(sysTrayVirtualCam);
8805 	trayMenu->addAction(exit);
8806 	trayIcon->setContextMenu(trayMenu);
8807 	trayIcon->show();
8808 
8809 	if (outputHandler && !outputHandler->replayBuffer)
8810 		sysTrayReplayBuffer->setEnabled(false);
8811 
8812 	sysTrayVirtualCam->setEnabled(vcamEnabled);
8813 
8814 	connect(trayIcon.data(),
8815 		SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this,
8816 		SLOT(IconActivated(QSystemTrayIcon::ActivationReason)));
8817 	connect(showHide, SIGNAL(triggered()), this, SLOT(ToggleShowHide()));
8818 	connect(sysTrayStream, SIGNAL(triggered()), this,
8819 		SLOT(on_streamButton_clicked()));
8820 	connect(sysTrayRecord, SIGNAL(triggered()), this,
8821 		SLOT(on_recordButton_clicked()));
8822 	connect(sysTrayReplayBuffer.data(), &QAction::triggered, this,
8823 		&OBSBasic::ReplayBufferClicked);
8824 	connect(sysTrayVirtualCam.data(), &QAction::triggered, this,
8825 		&OBSBasic::VCamButtonClicked);
8826 	connect(exit, SIGNAL(triggered()), this, SLOT(close()));
8827 }
8828 
8829 void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason)
8830 {
8831 	// Refresh projector list
8832 	previewProjector->clear();
8833 	studioProgramProjector->clear();
8834 	AddProjectorMenuMonitors(previewProjector, this,
8835 				 SLOT(OpenPreviewProjector()));
8836 	AddProjectorMenuMonitors(studioProgramProjector, this,
8837 				 SLOT(OpenStudioProgramProjector()));
8838 
8839 #ifdef __APPLE__
8840 	UNUSED_PARAMETER(reason);
8841 #else
8842 	if (reason == QSystemTrayIcon::Trigger) {
8843 		EnablePreviewDisplay(previewEnabled && !isVisible());
8844 		ToggleShowHide();
8845 	}
8846 #endif
8847 }
8848 
8849 void OBSBasic::SysTrayNotify(const QString &text,
8850 			     QSystemTrayIcon::MessageIcon n)
8851 {
8852 	if (trayIcon && trayIcon->isVisible() &&
8853 	    QSystemTrayIcon::supportsMessages()) {
8854 		QSystemTrayIcon::MessageIcon icon =
8855 			QSystemTrayIcon::MessageIcon(n);
8856 		trayIcon->showMessage("OBS Studio", text, icon, 10000);
8857 	}
8858 }
8859 
8860 void OBSBasic::SystemTray(bool firstStarted)
8861 {
8862 	if (!QSystemTrayIcon::isSystemTrayAvailable())
8863 		return;
8864 	if (!trayIcon && !firstStarted)
8865 		return;
8866 
8867 	bool sysTrayWhenStarted = config_get_bool(
8868 		GetGlobalConfig(), "BasicWindow", "SysTrayWhenStarted");
8869 	bool sysTrayEnabled = config_get_bool(GetGlobalConfig(), "BasicWindow",
8870 					      "SysTrayEnabled");
8871 
8872 	if (firstStarted)
8873 		SystemTrayInit();
8874 
8875 	if (!sysTrayEnabled) {
8876 		trayIcon->hide();
8877 	} else {
8878 		trayIcon->show();
8879 		if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) {
8880 			QTimer::singleShot(50, this, SLOT(hide()));
8881 			EnablePreviewDisplay(false);
8882 			setVisible(false);
8883 #ifdef __APPLE__
8884 			EnableOSXDockIcon(false);
8885 #endif
8886 			opt_minimize_tray = false;
8887 		}
8888 	}
8889 
8890 	if (isVisible())
8891 		showHide->setText(QTStr("Basic.SystemTray.Hide"));
8892 	else
8893 		showHide->setText(QTStr("Basic.SystemTray.Show"));
8894 }
8895 
8896 bool OBSBasic::sysTrayMinimizeToTray()
8897 {
8898 	return config_get_bool(GetGlobalConfig(), "BasicWindow",
8899 			       "SysTrayMinimizeToTray");
8900 }
8901 
8902 void OBSBasic::on_actionMainUndo_triggered()
8903 {
8904 	undo_s.undo();
8905 }
8906 
8907 void OBSBasic::on_actionMainRedo_triggered()
8908 {
8909 	undo_s.redo();
8910 }
8911 
8912 void OBSBasic::on_actionCopySource_triggered()
8913 {
8914 	copyStrings.clear();
8915 	bool allowPastingDuplicate = true;
8916 
8917 	for (auto &selectedSource : GetAllSelectedSourceItems()) {
8918 		OBSSceneItem item = ui->sources->Get(selectedSource.row());
8919 		if (!item)
8920 			continue;
8921 
8922 		on_actionCopyTransform_triggered();
8923 
8924 		OBSSource source = obs_sceneitem_get_source(item);
8925 
8926 		copyStrings.push_front(obs_source_get_name(source));
8927 
8928 		copyVisible = obs_sceneitem_visible(item);
8929 
8930 		uint32_t output_flags = obs_source_get_output_flags(source);
8931 		if (output_flags & OBS_SOURCE_DO_NOT_DUPLICATE)
8932 			allowPastingDuplicate = false;
8933 	}
8934 
8935 	ui->actionPasteRef->setEnabled(true);
8936 	ui->actionPasteDup->setEnabled(allowPastingDuplicate);
8937 }
8938 
8939 void OBSBasic::on_actionPasteRef_triggered()
8940 {
8941 	OBSSource scene_source = GetCurrentSceneSource();
8942 	OBSData undo_data = BackupScene(scene_source);
8943 
8944 	undo_s.push_disabled();
8945 
8946 	for (auto &copyString : copyStrings) {
8947 		/* do not allow duplicate refs of the same group in the same scene */
8948 		OBSScene scene = GetCurrentScene();
8949 		if (!!obs_scene_get_group(scene, copyString))
8950 			continue;
8951 
8952 		OBSBasicSourceSelect::SourcePaste(copyString, copyVisible,
8953 						  false);
8954 		on_actionPasteTransform_triggered();
8955 	}
8956 
8957 	undo_s.pop_disabled();
8958 
8959 	QString action_name = QTStr("Undo.PasteSourceRef");
8960 	const char *scene_name = obs_source_get_name(scene_source);
8961 
8962 	OBSData redo_data = BackupScene(scene_source);
8963 	CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data,
8964 				  redo_data);
8965 }
8966 
8967 void OBSBasic::on_actionPasteDup_triggered()
8968 {
8969 	OBSSource scene_source = GetCurrentSceneSource();
8970 	OBSData undo_data = BackupScene(scene_source);
8971 
8972 	undo_s.push_disabled();
8973 
8974 	for (auto &copyString : copyStrings) {
8975 		OBSBasicSourceSelect::SourcePaste(copyString, copyVisible,
8976 						  true);
8977 		on_actionPasteTransform_triggered();
8978 	}
8979 
8980 	undo_s.pop_disabled();
8981 
8982 	QString action_name = QTStr("Undo.PasteSource");
8983 	const char *scene_name = obs_source_get_name(scene_source);
8984 
8985 	OBSData redo_data = BackupScene(scene_source);
8986 	CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data,
8987 				  redo_data);
8988 }
8989 
8990 void OBSBasic::AudioMixerCopyFilters()
8991 {
8992 	QAction *action = reinterpret_cast<QAction *>(sender());
8993 	VolControl *vol = action->property("volControl").value<VolControl *>();
8994 	obs_source_t *source = vol->GetSource();
8995 
8996 	copyFiltersString = obs_source_get_name(source);
8997 }
8998 
8999 void OBSBasic::AudioMixerPasteFilters()
9000 {
9001 	QAction *action = reinterpret_cast<QAction *>(sender());
9002 	VolControl *vol = action->property("volControl").value<VolControl *>();
9003 	obs_source_t *dstSource = vol->GetSource();
9004 
9005 	OBSSource source = obs_get_source_by_name(copyFiltersString);
9006 	obs_source_release(source);
9007 
9008 	if (source == dstSource)
9009 		return;
9010 
9011 	obs_source_copy_filters(dstSource, source);
9012 }
9013 
9014 void OBSBasic::SceneCopyFilters()
9015 {
9016 	copyFiltersString = obs_source_get_name(GetCurrentSceneSource());
9017 }
9018 
9019 void OBSBasic::ScenePasteFilters()
9020 {
9021 	OBSSource source = obs_get_source_by_name(copyFiltersString);
9022 	obs_source_release(source);
9023 
9024 	OBSSource dstSource = GetCurrentSceneSource();
9025 
9026 	if (source == dstSource)
9027 		return;
9028 
9029 	obs_source_copy_filters(dstSource, source);
9030 }
9031 
9032 void OBSBasic::on_actionCopyFilters_triggered()
9033 {
9034 	OBSSceneItem item = GetCurrentSceneItem();
9035 
9036 	if (!item)
9037 		return;
9038 
9039 	OBSSource source = obs_sceneitem_get_source(item);
9040 
9041 	copyFiltersString = obs_source_get_name(source);
9042 
9043 	ui->actionPasteFilters->setEnabled(true);
9044 }
9045 
9046 void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text,
9047 					       obs_source_t *source,
9048 					       obs_data_array_t *undo_array,
9049 					       obs_data_array_t *redo_array)
9050 {
9051 	auto undo_redo = [this](const std::string &json) {
9052 		obs_data_t *data = obs_data_create_from_json(json.c_str());
9053 		obs_data_array_t *array = obs_data_get_array(data, "array");
9054 		const char *name = obs_data_get_string(data, "name");
9055 		obs_source_t *source = obs_get_source_by_name(name);
9056 
9057 		obs_source_restore_filters(source, array);
9058 		obs_source_release(source);
9059 
9060 		obs_data_array_release(array);
9061 		obs_data_release(data);
9062 
9063 		if (filters)
9064 			filters->UpdateSource(source);
9065 	};
9066 
9067 	const char *name = obs_source_get_name(source);
9068 
9069 	obs_data_t *undo_data = obs_data_create();
9070 	obs_data_t *redo_data = obs_data_create();
9071 	obs_data_set_array(undo_data, "array", undo_array);
9072 	obs_data_set_array(redo_data, "array", redo_array);
9073 	obs_data_set_string(undo_data, "name", name);
9074 	obs_data_set_string(redo_data, "name", name);
9075 
9076 	undo_s.add_action(text, undo_redo, undo_redo,
9077 			  obs_data_get_json(undo_data),
9078 			  obs_data_get_json(redo_data));
9079 
9080 	obs_data_release(undo_data);
9081 	obs_data_release(redo_data);
9082 }
9083 
9084 void OBSBasic::on_actionPasteFilters_triggered()
9085 {
9086 	OBSSource source = obs_get_source_by_name(copyFiltersString);
9087 	obs_source_release(source);
9088 
9089 	OBSSceneItem sceneItem = GetCurrentSceneItem();
9090 	OBSSource dstSource = obs_sceneitem_get_source(sceneItem);
9091 
9092 	if (source == dstSource)
9093 		return;
9094 
9095 	obs_data_array_t *undo_array = obs_source_backup_filters(dstSource);
9096 	obs_source_copy_filters(dstSource, source);
9097 	obs_data_array_t *redo_array = obs_source_backup_filters(dstSource);
9098 
9099 	const char *srcName = obs_source_get_name(source);
9100 	const char *dstName = obs_source_get_name(dstSource);
9101 	QString text =
9102 		QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName);
9103 
9104 	CreateFilterPasteUndoRedoAction(text, dstSource, undo_array,
9105 					redo_array);
9106 
9107 	obs_data_array_release(undo_array);
9108 	obs_data_array_release(redo_array);
9109 }
9110 
9111 static void ConfirmColor(SourceTree *sources, const QColor &color,
9112 			 QModelIndexList selectedItems)
9113 {
9114 	for (int x = 0; x < selectedItems.count(); x++) {
9115 		SourceTreeItem *treeItem =
9116 			sources->GetItemWidget(selectedItems[x].row());
9117 		treeItem->setStyleSheet("background: " +
9118 					color.name(QColor::HexArgb));
9119 		treeItem->style()->unpolish(treeItem);
9120 		treeItem->style()->polish(treeItem);
9121 
9122 		OBSSceneItem sceneItem = sources->Get(selectedItems[x].row());
9123 		obs_data_t *privData =
9124 			obs_sceneitem_get_private_settings(sceneItem);
9125 		obs_data_set_int(privData, "color-preset", 1);
9126 		obs_data_set_string(privData, "color",
9127 				    QT_TO_UTF8(color.name(QColor::HexArgb)));
9128 		obs_data_release(privData);
9129 	}
9130 }
9131 
9132 void OBSBasic::ColorChange()
9133 {
9134 	QModelIndexList selectedItems =
9135 		ui->sources->selectionModel()->selectedIndexes();
9136 	QAction *action = qobject_cast<QAction *>(sender());
9137 	QPushButton *colorButton = qobject_cast<QPushButton *>(sender());
9138 
9139 	if (selectedItems.count() == 0)
9140 		return;
9141 
9142 	if (colorButton) {
9143 		int preset = colorButton->property("bgColor").value<int>();
9144 
9145 		for (int x = 0; x < selectedItems.count(); x++) {
9146 			SourceTreeItem *treeItem = ui->sources->GetItemWidget(
9147 				selectedItems[x].row());
9148 			treeItem->setStyleSheet("");
9149 			treeItem->setProperty("bgColor", preset);
9150 			treeItem->style()->unpolish(treeItem);
9151 			treeItem->style()->polish(treeItem);
9152 
9153 			OBSSceneItem sceneItem =
9154 				ui->sources->Get(selectedItems[x].row());
9155 			obs_data_t *privData =
9156 				obs_sceneitem_get_private_settings(sceneItem);
9157 			obs_data_set_int(privData, "color-preset", preset + 1);
9158 			obs_data_set_string(privData, "color", "");
9159 			obs_data_release(privData);
9160 		}
9161 
9162 		for (int i = 1; i < 9; i++) {
9163 			stringstream button;
9164 			button << "preset" << i;
9165 			QPushButton *cButton =
9166 				colorButton->parentWidget()
9167 					->findChild<QPushButton *>(
9168 						button.str().c_str());
9169 			cButton->setStyleSheet("border: 1px solid black");
9170 		}
9171 
9172 		colorButton->setStyleSheet("border: 2px solid black");
9173 	} else if (action) {
9174 		int preset = action->property("bgColor").value<int>();
9175 
9176 		if (preset == 1) {
9177 			OBSSceneItem curSceneItem = GetCurrentSceneItem();
9178 			SourceTreeItem *curTreeItem =
9179 				GetItemWidgetFromSceneItem(curSceneItem);
9180 			obs_data_t *curPrivData =
9181 				obs_sceneitem_get_private_settings(
9182 					curSceneItem);
9183 
9184 			int oldPreset =
9185 				obs_data_get_int(curPrivData, "color-preset");
9186 			const QString oldSheet = curTreeItem->styleSheet();
9187 
9188 			auto liveChangeColor = [=](const QColor &color) {
9189 				if (color.isValid()) {
9190 					curTreeItem->setStyleSheet(
9191 						"background: " +
9192 						color.name(QColor::HexArgb));
9193 				}
9194 			};
9195 
9196 			auto changedColor = [=](const QColor &color) {
9197 				if (color.isValid()) {
9198 					ConfirmColor(ui->sources, color,
9199 						     selectedItems);
9200 				}
9201 			};
9202 
9203 			auto rejected = [=]() {
9204 				if (oldPreset == 1) {
9205 					curTreeItem->setStyleSheet(oldSheet);
9206 					curTreeItem->setProperty("bgColor", 0);
9207 				} else if (oldPreset == 0) {
9208 					curTreeItem->setStyleSheet(
9209 						"background: none");
9210 					curTreeItem->setProperty("bgColor", 0);
9211 				} else {
9212 					curTreeItem->setStyleSheet("");
9213 					curTreeItem->setProperty("bgColor",
9214 								 oldPreset - 1);
9215 				}
9216 
9217 				curTreeItem->style()->unpolish(curTreeItem);
9218 				curTreeItem->style()->polish(curTreeItem);
9219 			};
9220 
9221 			QColorDialog::ColorDialogOptions options =
9222 				QColorDialog::ShowAlphaChannel;
9223 
9224 			const char *oldColor =
9225 				obs_data_get_string(curPrivData, "color");
9226 			const char *customColor = *oldColor != 0 ? oldColor
9227 								 : "#55FF0000";
9228 #ifndef _WIN32
9229 			options |= QColorDialog::DontUseNativeDialog;
9230 #endif
9231 
9232 			QColorDialog *colorDialog = new QColorDialog(this);
9233 			colorDialog->setOptions(options);
9234 			colorDialog->setCurrentColor(QColor(customColor));
9235 			connect(colorDialog, &QColorDialog::currentColorChanged,
9236 				liveChangeColor);
9237 			connect(colorDialog, &QColorDialog::colorSelected,
9238 				changedColor);
9239 			connect(colorDialog, &QColorDialog::rejected, rejected);
9240 			colorDialog->open();
9241 
9242 			obs_data_release(curPrivData);
9243 		} else {
9244 			for (int x = 0; x < selectedItems.count(); x++) {
9245 				SourceTreeItem *treeItem =
9246 					ui->sources->GetItemWidget(
9247 						selectedItems[x].row());
9248 				treeItem->setStyleSheet("background: none");
9249 				treeItem->setProperty("bgColor", preset);
9250 				treeItem->style()->unpolish(treeItem);
9251 				treeItem->style()->polish(treeItem);
9252 
9253 				OBSSceneItem sceneItem = ui->sources->Get(
9254 					selectedItems[x].row());
9255 				obs_data_t *privData =
9256 					obs_sceneitem_get_private_settings(
9257 						sceneItem);
9258 				obs_data_set_int(privData, "color-preset",
9259 						 preset);
9260 				obs_data_set_string(privData, "color", "");
9261 				obs_data_release(privData);
9262 			}
9263 		}
9264 	}
9265 }
9266 
9267 SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem)
9268 {
9269 	int i = 0;
9270 	SourceTreeItem *treeItem = ui->sources->GetItemWidget(i);
9271 	OBSSceneItem item = ui->sources->Get(i);
9272 	int64_t id = obs_sceneitem_get_id(sceneItem);
9273 	while (treeItem && obs_sceneitem_get_id(item) != id) {
9274 		i++;
9275 		treeItem = ui->sources->GetItemWidget(i);
9276 		item = ui->sources->Get(i);
9277 	}
9278 	if (treeItem)
9279 		return treeItem;
9280 
9281 	return nullptr;
9282 }
9283 
9284 void OBSBasic::on_autoConfigure_triggered()
9285 {
9286 	AutoConfig test(this);
9287 	test.setModal(true);
9288 	test.show();
9289 	test.exec();
9290 }
9291 
9292 void OBSBasic::on_stats_triggered()
9293 {
9294 	if (!stats.isNull()) {
9295 		stats->show();
9296 		stats->raise();
9297 		return;
9298 	}
9299 
9300 	OBSBasicStats *statsDlg;
9301 	statsDlg = new OBSBasicStats(nullptr);
9302 	statsDlg->show();
9303 	stats = statsDlg;
9304 }
9305 
9306 void OBSBasic::on_actionShowAbout_triggered()
9307 {
9308 	if (about)
9309 		about->close();
9310 
9311 	about = new OBSAbout(this);
9312 	about->show();
9313 
9314 	about->setAttribute(Qt::WA_DeleteOnClose, true);
9315 }
9316 
9317 void OBSBasic::ResizeOutputSizeOfSource()
9318 {
9319 	if (obs_video_active())
9320 		return;
9321 
9322 	QMessageBox resize_output(this);
9323 	resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" +
9324 			      QTStr("ResizeOutputSizeOfSource.Continue"));
9325 	QAbstractButton *Yes =
9326 		resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole);
9327 	resize_output.addButton(QTStr("No"), QMessageBox::NoRole);
9328 	resize_output.setIcon(QMessageBox::Warning);
9329 	resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource"));
9330 	resize_output.exec();
9331 
9332 	if (resize_output.clickedButton() != Yes)
9333 		return;
9334 
9335 	OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem());
9336 
9337 	int width = obs_source_get_width(source);
9338 	int height = obs_source_get_height(source);
9339 
9340 	config_set_uint(basicConfig, "Video", "BaseCX", width);
9341 	config_set_uint(basicConfig, "Video", "BaseCY", height);
9342 	config_set_uint(basicConfig, "Video", "OutputCX", width);
9343 	config_set_uint(basicConfig, "Video", "OutputCY", height);
9344 
9345 	ResetVideo();
9346 	ResetOutputs();
9347 	config_save_safe(basicConfig, "tmp", nullptr);
9348 	on_actionFitToScreen_triggered();
9349 }
9350 
9351 QAction *OBSBasic::AddDockWidget(QDockWidget *dock)
9352 {
9353 	QAction *action = ui->viewMenuDocks->addAction(dock->windowTitle());
9354 	action->setCheckable(true);
9355 	assignDockToggle(dock, action);
9356 	extraDocks.push_back(dock);
9357 
9358 	bool lock = ui->lockUI->isChecked();
9359 	QDockWidget::DockWidgetFeatures features =
9360 		lock ? QDockWidget::NoDockWidgetFeatures
9361 		     : (QDockWidget::DockWidgetClosable |
9362 			QDockWidget::DockWidgetMovable |
9363 			QDockWidget::DockWidgetFloatable);
9364 
9365 	dock->setFeatures(features);
9366 
9367 	/* prune deleted docks */
9368 	for (int i = extraDocks.size() - 1; i >= 0; i--) {
9369 		if (!extraDocks[i]) {
9370 			extraDocks.removeAt(i);
9371 		}
9372 	}
9373 
9374 	return action;
9375 }
9376 
9377 OBSBasic *OBSBasic::Get()
9378 {
9379 	return reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
9380 }
9381 
9382 bool OBSBasic::StreamingActive()
9383 {
9384 	if (!outputHandler)
9385 		return false;
9386 	return outputHandler->StreamingActive();
9387 }
9388 
9389 bool OBSBasic::RecordingActive()
9390 {
9391 	if (!outputHandler)
9392 		return false;
9393 	return outputHandler->RecordingActive();
9394 }
9395 
9396 bool OBSBasic::ReplayBufferActive()
9397 {
9398 	if (!outputHandler)
9399 		return false;
9400 	return outputHandler->ReplayBufferActive();
9401 }
9402 
9403 SceneRenameDelegate::SceneRenameDelegate(QObject *parent)
9404 	: QStyledItemDelegate(parent)
9405 {
9406 }
9407 
9408 void SceneRenameDelegate::setEditorData(QWidget *editor,
9409 					const QModelIndex &index) const
9410 {
9411 	QStyledItemDelegate::setEditorData(editor, index);
9412 	QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor);
9413 	if (lineEdit)
9414 		lineEdit->selectAll();
9415 }
9416 
9417 bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event)
9418 {
9419 	if (event->type() == QEvent::KeyPress) {
9420 		QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
9421 		if (keyEvent->key() == Qt::Key_Escape) {
9422 			QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor);
9423 			if (lineEdit)
9424 				lineEdit->undo();
9425 		}
9426 	}
9427 
9428 	return QStyledItemDelegate::eventFilter(editor, event);
9429 }
9430 
9431 void OBSBasic::UpdatePatronJson(const QString &text, const QString &error)
9432 {
9433 	if (!error.isEmpty())
9434 		return;
9435 
9436 	patronJson = QT_TO_UTF8(text);
9437 }
9438 
9439 void OBSBasic::PauseRecording()
9440 {
9441 	if (!pause || !outputHandler || !outputHandler->fileOutput ||
9442 	    os_atomic_load_bool(&recording_paused))
9443 		return;
9444 
9445 	obs_output_t *output = outputHandler->fileOutput;
9446 
9447 	if (obs_output_pause(output, true)) {
9448 		pause->setAccessibleName(QTStr("Basic.Main.UnpauseRecording"));
9449 		pause->setToolTip(QTStr("Basic.Main.UnpauseRecording"));
9450 		pause->blockSignals(true);
9451 		pause->setChecked(true);
9452 		pause->blockSignals(false);
9453 
9454 		ui->statusbar->RecordingPaused();
9455 
9456 		if (trayIcon && trayIcon->isVisible()) {
9457 #ifdef __APPLE__
9458 			QIcon trayIconFile =
9459 				QIcon(":/res/images/obs_paused_macos.png");
9460 			trayIconFile.setIsMask(true);
9461 #else
9462 			QIcon trayIconFile =
9463 				QIcon(":/res/images/obs_paused.png");
9464 #endif
9465 			trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused",
9466 							   trayIconFile));
9467 		}
9468 
9469 		os_atomic_set_bool(&recording_paused, true);
9470 
9471 		if (api)
9472 			api->on_event(OBS_FRONTEND_EVENT_RECORDING_PAUSED);
9473 
9474 		if (os_atomic_load_bool(&replaybuf_active))
9475 			ShowReplayBufferPauseWarning();
9476 	}
9477 }
9478 
9479 void OBSBasic::UnpauseRecording()
9480 {
9481 	if (!pause || !outputHandler || !outputHandler->fileOutput ||
9482 	    !os_atomic_load_bool(&recording_paused))
9483 		return;
9484 
9485 	obs_output_t *output = outputHandler->fileOutput;
9486 
9487 	if (obs_output_pause(output, false)) {
9488 		pause->setAccessibleName(QTStr("Basic.Main.PauseRecording"));
9489 		pause->setToolTip(QTStr("Basic.Main.PauseRecording"));
9490 		pause->blockSignals(true);
9491 		pause->setChecked(false);
9492 		pause->blockSignals(false);
9493 
9494 		ui->statusbar->RecordingUnpaused();
9495 
9496 		if (trayIcon && trayIcon->isVisible()) {
9497 #ifdef __APPLE__
9498 			QIcon trayIconFile =
9499 				QIcon(":/res/images/tray_active_macos.png");
9500 			trayIconFile.setIsMask(true);
9501 #else
9502 			QIcon trayIconFile =
9503 				QIcon(":/res/images/tray_active.png");
9504 #endif
9505 			trayIcon->setIcon(QIcon::fromTheme("obs-tray-active",
9506 							   trayIconFile));
9507 		}
9508 
9509 		os_atomic_set_bool(&recording_paused, false);
9510 
9511 		if (api)
9512 			api->on_event(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED);
9513 	}
9514 }
9515 
9516 void OBSBasic::PauseToggled()
9517 {
9518 	if (!pause || !outputHandler || !outputHandler->fileOutput)
9519 		return;
9520 
9521 	obs_output_t *output = outputHandler->fileOutput;
9522 	bool enable = !obs_output_paused(output);
9523 
9524 	if (enable)
9525 		PauseRecording();
9526 	else
9527 		UnpauseRecording();
9528 }
9529 
9530 void OBSBasic::UpdatePause(bool activate)
9531 {
9532 	if (!activate || !outputHandler || !outputHandler->RecordingActive()) {
9533 		pause.reset();
9534 		return;
9535 	}
9536 
9537 	const char *mode = config_get_string(basicConfig, "Output", "Mode");
9538 	bool adv = astrcmpi(mode, "Advanced") == 0;
9539 	bool shared;
9540 
9541 	if (adv) {
9542 		const char *recType =
9543 			config_get_string(basicConfig, "AdvOut", "RecType");
9544 
9545 		if (astrcmpi(recType, "FFmpeg") == 0) {
9546 			shared = config_get_bool(basicConfig, "AdvOut",
9547 						 "FFOutputToFile");
9548 		} else {
9549 			const char *recordEncoder = config_get_string(
9550 				basicConfig, "AdvOut", "RecEncoder");
9551 			shared = astrcmpi(recordEncoder, "none") == 0;
9552 		}
9553 	} else {
9554 		const char *quality = config_get_string(
9555 			basicConfig, "SimpleOutput", "RecQuality");
9556 		shared = strcmp(quality, "Stream") == 0;
9557 	}
9558 
9559 	if (!shared) {
9560 		pause.reset(new QPushButton());
9561 		pause->setAccessibleName(QTStr("Basic.Main.PauseRecording"));
9562 		pause->setToolTip(QTStr("Basic.Main.PauseRecording"));
9563 		pause->setCheckable(true);
9564 		pause->setChecked(false);
9565 		pause->setProperty("themeID",
9566 				   QVariant(QStringLiteral("pauseIconSmall")));
9567 
9568 		QSizePolicy sp;
9569 		sp.setHeightForWidth(true);
9570 		pause->setSizePolicy(sp);
9571 
9572 		connect(pause.data(), &QAbstractButton::clicked, this,
9573 			&OBSBasic::PauseToggled);
9574 		ui->recordingLayout->addWidget(pause.data());
9575 	} else {
9576 		pause.reset();
9577 	}
9578 }
9579 
9580 void OBSBasic::UpdateReplayBuffer(bool activate)
9581 {
9582 	if (!activate || !outputHandler ||
9583 	    !outputHandler->ReplayBufferActive()) {
9584 		replay.reset();
9585 		return;
9586 	}
9587 
9588 	replay.reset(new QPushButton());
9589 	replay->setAccessibleName(QTStr("Basic.Main.SaveReplay"));
9590 	replay->setToolTip(QTStr("Basic.Main.SaveReplay"));
9591 	replay->setChecked(false);
9592 	replay->setProperty("themeID",
9593 			    QVariant(QStringLiteral("replayIconSmall")));
9594 
9595 	QSizePolicy sp;
9596 	sp.setHeightForWidth(true);
9597 	replay->setSizePolicy(sp);
9598 
9599 	connect(replay.data(), &QAbstractButton::clicked, this,
9600 		&OBSBasic::ReplayBufferSave);
9601 	replayLayout->addWidget(replay.data());
9602 	setTabOrder(replayLayout->itemAt(0)->widget(),
9603 		    replayLayout->itemAt(1)->widget());
9604 	setTabOrder(replayLayout->itemAt(1)->widget(),
9605 		    ui->buttonsVLayout->itemAt(3)->widget());
9606 }
9607 
9608 #define MBYTE (1024ULL * 1024ULL)
9609 #define MBYTES_LEFT_STOP_REC 50ULL
9610 #define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE)
9611 
9612 const char *OBSBasic::GetCurrentOutputPath()
9613 {
9614 	const char *path = nullptr;
9615 	const char *mode = config_get_string(Config(), "Output", "Mode");
9616 
9617 	if (strcmp(mode, "Advanced") == 0) {
9618 		const char *advanced_mode =
9619 			config_get_string(Config(), "AdvOut", "RecType");
9620 
9621 		if (strcmp(advanced_mode, "FFmpeg") == 0) {
9622 			path = config_get_string(Config(), "AdvOut",
9623 						 "FFFilePath");
9624 		} else {
9625 			path = config_get_string(Config(), "AdvOut",
9626 						 "RecFilePath");
9627 		}
9628 	} else {
9629 		path = config_get_string(Config(), "SimpleOutput", "FilePath");
9630 	}
9631 
9632 	return path;
9633 }
9634 
9635 void OBSBasic::OutputPathInvalidMessage()
9636 {
9637 	blog(LOG_ERROR, "Recording stopped because of bad output path");
9638 
9639 	OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"),
9640 				QTStr("Output.BadPath.Text"));
9641 }
9642 
9643 bool OBSBasic::OutputPathValid()
9644 {
9645 	const char *mode = config_get_string(Config(), "Output", "Mode");
9646 	if (strcmp(mode, "Advanced") == 0) {
9647 		const char *advanced_mode =
9648 			config_get_string(Config(), "AdvOut", "RecType");
9649 		if (strcmp(advanced_mode, "FFmpeg") == 0) {
9650 			bool is_local = config_get_bool(Config(), "AdvOut",
9651 							"FFOutputToFile");
9652 			if (!is_local)
9653 				return true;
9654 		}
9655 	}
9656 
9657 	const char *path = GetCurrentOutputPath();
9658 	return path && *path && QDir(path).exists();
9659 }
9660 
9661 void OBSBasic::DiskSpaceMessage()
9662 {
9663 	blog(LOG_ERROR, "Recording stopped because of low disk space");
9664 
9665 	OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"),
9666 				QTStr("Output.RecordNoSpace.Msg"));
9667 }
9668 
9669 bool OBSBasic::LowDiskSpace()
9670 {
9671 	const char *path;
9672 
9673 	path = GetCurrentOutputPath();
9674 	if (!path)
9675 		return false;
9676 
9677 	uint64_t num_bytes = os_get_free_disk_space(path);
9678 
9679 	if (num_bytes < (MAX_BYTES_LEFT))
9680 		return true;
9681 	else
9682 		return false;
9683 }
9684 
9685 void OBSBasic::CheckDiskSpaceRemaining()
9686 {
9687 	if (LowDiskSpace()) {
9688 		StopRecording();
9689 		StopReplayBuffer();
9690 
9691 		DiskSpaceMessage();
9692 	}
9693 }
9694 
9695 void OBSBasic::ScenesReordered()
9696 {
9697 	OBSProjector::UpdateMultiviewProjectors();
9698 }
9699 
9700 void OBSBasic::ResetStatsHotkey()
9701 {
9702 	QList<OBSBasicStats *> list = findChildren<OBSBasicStats *>();
9703 
9704 	foreach(OBSBasicStats * s, list) s->Reset();
9705 }
9706 
9707 void OBSBasic::on_customContextMenuRequested(const QPoint &pos)
9708 {
9709 	QWidget *widget = childAt(pos);
9710 	const char *className = nullptr;
9711 	if (widget != nullptr)
9712 		className = widget->metaObject()->className();
9713 
9714 	if (!className || strstr(className, "Dock") != nullptr)
9715 		ui->viewMenuDocks->exec(mapToGlobal(pos));
9716 }
9717 
9718 void OBSBasic::UpdateProjectorHideCursor()
9719 {
9720 	for (size_t i = 0; i < projectors.size(); i++)
9721 		projectors[i]->SetHideCursor();
9722 }
9723 
9724 void OBSBasic::UpdateProjectorAlwaysOnTop(bool top)
9725 {
9726 	for (size_t i = 0; i < projectors.size(); i++)
9727 		SetAlwaysOnTop(projectors[i], top);
9728 }
9729 
9730 void OBSBasic::ResetProjectors()
9731 {
9732 	obs_data_array_t *savedProjectorList = SaveProjectors();
9733 	ClearProjectors();
9734 	LoadSavedProjectors(savedProjectorList);
9735 	OpenSavedProjectors();
9736 	obs_data_array_release(savedProjectorList);
9737 }
9738 
9739 void OBSBasic::on_sourcePropertiesButton_clicked()
9740 {
9741 	on_actionSourceProperties_triggered();
9742 }
9743 
9744 void OBSBasic::on_sourceFiltersButton_clicked()
9745 {
9746 	OpenFilters();
9747 }
9748 
9749 void OBSBasic::on_sourceInteractButton_clicked()
9750 {
9751 	on_actionInteract_triggered();
9752 }
9753 
9754 void OBSBasic::ShowStatusBarMessage(const QString &message)
9755 {
9756 	ui->statusbar->clearMessage();
9757 	ui->statusbar->showMessage(message, 10000);
9758 }
9759 
9760 void OBSBasic::UpdatePreviewSafeAreas()
9761 {
9762 	drawSafeAreas = config_get_bool(App()->GlobalConfig(), "BasicWindow",
9763 					"ShowSafeAreas");
9764 }
9765