Browse Source

UI: Add Multiview projector

Jim note:

- Refactored code significantly
- Added a context menu option to exclude specific scenes from projectors
- Made it so multiview projectors update when scenes are
  added/removed/renamed
- Increased text quality
- Removed the color sources and replaced them with simple solid
  rectangles
- Increased the border size of "program" and "preview" scenes in the
  lower scene list

Closes jp9000/obs-studio#1068
Shaolin 8 years ago
parent
commit
721cb3dea5
5 changed files with 658 additions and 33 deletions
  1. 3 0
      UI/data/locale/en-US.ini
  2. 155 19
      UI/window-basic-main.cpp
  3. 19 3
      UI/window-basic-main.hpp
  4. 464 9
      UI/window-projector.cpp
  5. 17 2
      UI/window-projector.hpp

+ 3 - 0
UI/data/locale/en-US.ini

@@ -78,6 +78,9 @@ Defaults="Defaults"
 HideMixer="Hide in Mixer"
 TransitionOverride="Transition Override"
 None="None"
+StudioMode.Preview="Preview"
+StudioMode.Program="Program"
+ShowInMultiview="Show in Multiview"
 
 # warning if program already open
 AlreadyRunning.Title="OBS is already running"

+ 155 - 19
UI/window-basic-main.cpp

@@ -139,6 +139,7 @@ OBSBasic::OBSBasic(QWidget *parent)
 
 	projectorArray.resize(10, "");
 	previewProjectorArray.resize(10, 0);
+	multiviewProjectorArray.resize(10, 0);
 	studioProgramProjectorArray.resize(10, 0);
 
 	setAcceptDrops(true);
@@ -320,7 +321,8 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder,
 		OBSScene &scene, OBSSource &curProgramScene,
 		obs_data_array_t *savedProjectorList,
 		obs_data_array_t *savedPreviewProjectorList,
-		obs_data_array_t *savedStudioProgramProjectorList)
+		obs_data_array_t *savedStudioProgramProjectorList,
+		obs_data_array_t *savedMultiviewProjectorList)
 {
 	obs_data_t *saveData = obs_data_create();
 
@@ -366,6 +368,8 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder,
 			savedPreviewProjectorList);
 	obs_data_set_array(saveData, "saved_studio_preview_projectors",
 			savedStudioProgramProjectorList);
+	obs_data_set_array(saveData, "saved_multiview_projectors",
+			savedMultiviewProjectorList);
 	obs_data_array_release(sourcesArray);
 
 	obs_data_set_string(saveData, "current_transition",
@@ -468,6 +472,21 @@ obs_data_array_t *OBSBasic::SaveStudioProgramProjectors()
 	return saveProjector;
 }
 
+obs_data_array_t *OBSBasic::SaveMultiviewProjectors()
+{
+	obs_data_array_t *saveProjector = obs_data_array_create();
+
+	for (size_t i = 0; i < multiviewProjectorArray.size(); i++) {
+		obs_data_t *data = obs_data_create();
+		obs_data_set_int(data, "saved_multiview_projectors",
+			multiviewProjectorArray.at(i));
+		obs_data_array_push_back(saveProjector, data);
+		obs_data_release(data);
+	}
+
+	return saveProjector;
+}
+
 void OBSBasic::Save(const char *file)
 {
 	OBSScene scene = GetCurrentScene();
@@ -482,11 +501,14 @@ void OBSBasic::Save(const char *file)
 	obs_data_array_t *savedPreviewProjectorList = SavePreviewProjectors();
 	obs_data_array_t *savedStudioProgramProjectorList =
 			SaveStudioProgramProjectors();
+	obs_data_array_t *savedMultiviewProjectorList =
+			SaveMultiviewProjectors();
 	obs_data_t *saveData = GenerateSaveData(sceneOrder, quickTrData,
 			ui->transitionDuration->value(), transitions,
 			scene, curProgramScene, savedProjectorList,
 			savedPreviewProjectorList,
-			savedStudioProgramProjectorList);
+			savedStudioProgramProjectorList,
+			savedMultiviewProjectorList);
 
 	obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
 	obs_data_set_bool(saveData, "scaling_enabled",
@@ -515,6 +537,7 @@ void OBSBasic::Save(const char *file)
 	obs_data_array_release(savedProjectorList);
 	obs_data_array_release(savedPreviewProjectorList);
 	obs_data_array_release(savedStudioProgramProjectorList);
+	obs_data_array_release(savedMultiviewProjectorList);
 }
 
 static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent)
@@ -653,6 +676,19 @@ void OBSBasic::LoadSavedStudioProgramProjectors(obs_data_array_t *array)
 	}
 }
 
+void OBSBasic::LoadSavedMultiviewProjectors(obs_data_array_t *array)
+{
+	size_t num = obs_data_array_count(array);
+
+	for (size_t i = 0; i < num; i++) {
+		obs_data_t *data = obs_data_array_item(array, i);
+		multiviewProjectorArray.at(i) = obs_data_get_int(data,
+				"saved_multiview_projectors");
+
+		obs_data_release(data);
+	}
+}
+
 static void LogFilter(obs_source_t*, obs_source_t *filter, void *v_val)
 {
 	const char *name = obs_source_get_name(filter);
@@ -784,6 +820,8 @@ void OBSBasic::Load(const char *file)
 	ui->transitionDuration->setValue(newDuration);
 	SetTransition(curTransition);
 
+	/* ------------------- */
+
 	obs_data_array_t *savedProjectors = obs_data_get_array(data,
 			"saved_projectors");
 
@@ -792,6 +830,8 @@ void OBSBasic::Load(const char *file)
 
 	obs_data_array_release(savedProjectors);
 
+	/* ------------------- */
+
 	obs_data_array_t *savedPreviewProjectors = obs_data_get_array(data,
 			"saved_preview_projectors");
 
@@ -800,6 +840,8 @@ void OBSBasic::Load(const char *file)
 
 	obs_data_array_release(savedPreviewProjectors);
 
+	/* ------------------- */
+
 	obs_data_array_t *savedStudioProgramProjectors = obs_data_get_array(data,
 			"saved_studio_preview_projectors");
 
@@ -808,6 +850,16 @@ void OBSBasic::Load(const char *file)
 
 	obs_data_array_release(savedStudioProgramProjectors);
 
+	/* ------------------- */
+
+	obs_data_array_t *savedMultiviewProjectors = obs_data_get_array(data,
+			"saved_multiview_projectors");
+
+	if (savedMultiviewProjectors)
+		LoadSavedMultiviewProjectors(savedMultiviewProjectors);
+
+	obs_data_array_release(savedMultiviewProjectors);
+
 
 retryScene:
 	curScene = obs_get_source_by_name(sceneName);
@@ -1927,6 +1979,11 @@ void OBSBasic::SaveProjectDeferred()
 	Save(savePath);
 }
 
+OBSSource OBSBasic::GetProgramSource()
+{
+	return OBSGetStrongRef(programScene);
+}
+
 OBSScene OBSBasic::GetCurrentScene()
 {
 	QListWidgetItem *item = ui->scenes->currentItem();
@@ -2088,6 +2145,8 @@ void OBSBasic::AddScene(OBSSource source)
 		obs_source_t *source = obs_scene_get_source(scene);
 		blog(LOG_INFO, "User added scene '%s'",
 				obs_source_get_name(source));
+
+		OBSProjector::UpdateMultiviewProjectors();
 	}
 
 	if (api)
@@ -2122,6 +2181,8 @@ void OBSBasic::RemoveScene(OBSSource source)
 	if (!disableSaving) {
 		blog(LOG_INFO, "User Removed scene '%s'",
 				obs_source_get_name(source));
+
+		OBSProjector::UpdateMultiviewProjectors();
 	}
 
 	if (api)
@@ -2203,7 +2264,8 @@ static void RenameListValues(QListWidget *listWidget, const QString &newName,
 		items[i]->setText(newName);
 }
 
-void OBSBasic::RenameSources(QString newName, QString prevName)
+void OBSBasic::RenameSources(OBSSource source, QString newName,
+		QString prevName)
 {
 	RenameListValues(ui->scenes,  newName, prevName);
 
@@ -2221,6 +2283,10 @@ void OBSBasic::RenameSources(QString newName, QString prevName)
 	}
 
 	SaveProject();
+
+	obs_scene_t *scene = obs_scene_from_source(source);
+	if (scene)
+		OBSProjector::UpdateMultiviewProjectors();
 }
 
 void OBSBasic::SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select)
@@ -2816,11 +2882,13 @@ void OBSBasic::SourceDeactivated(void *data, calldata_t *params)
 
 void OBSBasic::SourceRenamed(void *data, calldata_t *params)
 {
+	obs_source_t *source = (obs_source_t*)calldata_ptr(params, "source");
 	const char *newName  = calldata_string(params, "new_name");
 	const char *prevName = calldata_string(params, "prev_name");
 
 	QMetaObject::invokeMethod(static_cast<OBSBasic*>(data),
 			"RenameSources",
+			Q_ARG(OBSSource, source),
 			Q_ARG(QString, QT_UTF8(newName)),
 			Q_ARG(QString, QT_UTF8(prevName)));
 
@@ -3457,6 +3525,7 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
 		popup.addMenu(&order);
 
 		popup.addSeparator();
+
 		sceneProjectorMenu = new QMenu(QTStr("SceneProjector"));
 		AddProjectorMenuMonitors(sceneProjectorMenu, this,
 				SLOT(OpenSceneProjector()));
@@ -3475,6 +3544,36 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
 
 		QMenu *transitionMenu = CreatePerSceneTransitionMenu();
 		popup.addMenu(transitionMenu);
+
+		/* ---------------------- */
+
+		if (IsPreviewProgramMode()) {
+			QAction *multiviewAction = popup.addAction(
+					QTStr("ShowInMultiview"));
+
+			OBSSource source = GetCurrentSceneSource();
+			OBSData data = obs_source_get_private_settings(source);
+			obs_data_release(data);
+
+			obs_data_set_default_bool(data, "show_in_multiview",
+					true);
+			bool show = obs_data_get_bool(data, "show_in_multiview");
+
+			multiviewAction->setCheckable(true);
+			multiviewAction->setChecked(show);
+
+			auto showInMultiview = [this] (OBSData data)
+			{
+				bool show = obs_data_get_bool(data,
+						"show_in_multiview");
+				obs_data_set_bool(data, "show_in_multiview",
+						!show);
+				OBSProjector::UpdateMultiviewProjectors();
+			};
+
+			connect(multiviewAction, &QAction::triggered,
+					std::bind(showInMultiview, data));
+		}
 	}
 
 	popup.exec(QCursor::pos());
@@ -3719,6 +3818,7 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
 	QMenu popup(this);
 	QPointer<QMenu> previewProjector;
 	QPointer<QMenu> sourceProjector;
+	QPointer<QMenu> multiviewProjectorMenu;
 
 	if (preview) {
 		QAction *action = popup.addAction(
@@ -3733,6 +3833,20 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
 		popup.addAction(ui->actionLockPreview);
 		popup.addMenu(ui->scalingMenu);
 
+		if (IsPreviewProgramMode()) {
+			multiviewProjectorMenu = new QMenu(
+					"Multiview Projector");
+			AddProjectorMenuMonitors(multiviewProjectorMenu, this,
+					SLOT(OpenMultiviewProjector()));
+			popup.addMenu(multiviewProjectorMenu);
+
+			QAction *multiviewWindow = popup.addAction(
+					"Multiview Windowed",
+					this, SLOT(OpenMultiviewWindow()));
+
+			popup.addAction(multiviewWindow);
+		}
+
 		previewProjector = new QMenu(QTStr("PreviewProjector"));
 		AddProjectorMenuMonitors(previewProjector, this,
 				SLOT(OpenPreviewProjector()));
@@ -5417,17 +5531,12 @@ void OBSBasic::NudgeLeft()     {Nudge(1,  MoveDir::Left);}
 void OBSBasic::NudgeRight()    {Nudge(1,  MoveDir::Right);}
 
 void OBSBasic::OpenProjector(obs_source_t *source, int monitor, bool window,
-		QString title, bool studioProgram)
+		QString title, ProjectorType type)
 {
 	/* seriously?  10 monitors? */
 	if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1)
 		return;
 
-	bool isPreview = false;
-
-	if (source == nullptr)
-		isPreview = true;
-
 	if (!window) {
 		delete projectors[monitor];
 		projectors[monitor].clear();
@@ -5438,20 +5547,22 @@ void OBSBasic::OpenProjector(obs_source_t *source, int monitor, bool window,
 	const char *name = obs_source_get_name(source);
 
 	if (!window) {
-		if (studioProgram) {
+		if (type == ProjectorType::StudioProgram) {
 			studioProgramProjectorArray.at((size_t)monitor) = 1;
-		} else if (isPreview) {
+		} else if (type == ProjectorType::Preview) {
 			previewProjectorArray.at((size_t)monitor) = 1;
+		} else if (type == ProjectorType::Multiview) {
+			multiviewProjectorArray.at((size_t)monitor) = 1;
 		} else {
 			projectorArray.at((size_t)monitor) = name;
 		}
 	}
 
 	if (!window) {
-		projector->Init(monitor, false, nullptr, studioProgram);
+		projector->Init(monitor, false, nullptr, type);
 		projectors[monitor] = projector;
 	} else {
-		projector->Init(monitor, true, title, studioProgram);
+		projector->Init(monitor, true, title, type);
 
 		for (auto &projPtr : windowProjectors) {
 			if (!projPtr) {
@@ -5468,13 +5579,14 @@ void OBSBasic::OpenProjector(obs_source_t *source, int monitor, bool window,
 void OBSBasic::OpenStudioProgramProjector()
 {
 	int monitor = sender()->property("monitor").toInt();
-	OpenProjector(nullptr, monitor, false, nullptr, true);
+	OpenProjector(nullptr, monitor, false, nullptr,
+			ProjectorType::StudioProgram);
 }
 
 void OBSBasic::OpenPreviewProjector()
 {
 	int monitor = sender()->property("monitor").toInt();
-	OpenProjector(nullptr, monitor, false);
+	OpenProjector(nullptr, monitor, false, nullptr, ProjectorType::Preview);
 }
 
 void OBSBasic::OpenSourceProjector()
@@ -5487,6 +5599,13 @@ void OBSBasic::OpenSourceProjector()
 	OpenProjector(obs_sceneitem_get_source(item), monitor, false);
 }
 
+void OBSBasic::OpenMultiviewProjector()
+{
+	int monitor = sender()->property("monitor").toInt();
+	OpenProjector(nullptr, monitor, false, nullptr,
+			ProjectorType::Multiview);
+}
+
 void OBSBasic::OpenSceneProjector()
 {
 	int monitor = sender()->property("monitor").toInt();
@@ -5501,14 +5620,15 @@ void OBSBasic::OpenStudioProgramWindow()
 {
 	int monitor = sender()->property("monitor").toInt();
 	QString title = QTStr("StudioProgramWindow");
-	OpenProjector(nullptr, monitor, true, title, true);
+	OpenProjector(nullptr, monitor, true, title,
+			ProjectorType::StudioProgram);
 }
 
 void OBSBasic::OpenPreviewWindow()
 {
 	int monitor = sender()->property("monitor").toInt();
 	QString title = QTStr("PreviewWindow");
-	OpenProjector(nullptr, monitor, true, title);
+	OpenProjector(nullptr, monitor, true, nullptr, ProjectorType::Preview);
 }
 
 void OBSBasic::OpenSourceWindow()
@@ -5526,6 +5646,13 @@ void OBSBasic::OpenSourceWindow()
 	OpenProjector(obs_sceneitem_get_source(item), monitor, true, title);
 }
 
+void OBSBasic::OpenMultiviewWindow()
+{
+	int monitor = sender()->property("monitor").toInt();
+	OpenProjector(nullptr, monitor, true, "Multiview",
+			ProjectorType::Multiview);
+}
+
 void OBSBasic::OpenSceneWindow()
 {
 	int monitor = sender()->property("monitor").toInt();
@@ -5566,13 +5693,21 @@ void OBSBasic::OpenSavedProjectors()
 		for (size_t i = 0; i < studioProgramProjectorArray.size(); i++) {
 			if (studioProgramProjectorArray.at(i) == 1) {
 				OpenProjector(nullptr, (int)i, false, nullptr,
-						true);
+						ProjectorType::StudioProgram);
 			}
 		}
 
 		for (size_t i = 0; i < previewProjectorArray.size(); i++) {
 			if (previewProjectorArray.at(i) == 1) {
-				OpenProjector(nullptr, (int)i, false);
+				OpenProjector(nullptr, (int)i, false, nullptr,
+						ProjectorType::Preview);
+			}
+		}
+
+		for (size_t i = 0; i < multiviewProjectorArray.size(); i++) {
+			if (multiviewProjectorArray.at(i) == 1) {
+				OpenProjector(nullptr, (int)i, false, nullptr,
+						ProjectorType::Multiview);
 			}
 		}
 	}
@@ -5581,6 +5716,7 @@ void OBSBasic::OpenSavedProjectors()
 void OBSBasic::RemoveSavedProjectors(int monitor)
 {
 	studioProgramProjectorArray.at((size_t)monitor) = 0;
+	multiviewProjectorArray.at((size_t)monitor) = 0;
 	previewProjectorArray.at((size_t)monitor) = 0;
 	projectorArray.at((size_t)monitor) = "";
 }

+ 19 - 3
UI/window-basic-main.hpp

@@ -67,6 +67,13 @@ enum class QtDataRole {
 	OBSSignals,
 };
 
+enum class ProjectorType {
+	Source,
+	Preview,
+	StudioProgram,
+	Multiview
+};
+
 struct QuickTransition {
 	QPushButton *button = nullptr;
 	OBSSource source;
@@ -115,6 +122,7 @@ private:
 
 	std::vector<std::string> projectorArray;
 	std::vector<int> studioProgramProjectorArray;
+	std::vector<int> multiviewProjectorArray;
 	std::vector<int> previewProjectorArray;
 
 	bool loaded = false;
@@ -241,7 +249,8 @@ private:
 
 	void Nudge(int dist, MoveDir dir);
 	void OpenProjector(obs_source_t *source, int monitor, bool window,
-			QString title = nullptr, bool studioProgram = false);
+			QString title = nullptr,
+			ProjectorType type = ProjectorType::Source);
 
 	void GetAudioSourceFilters();
 	void GetAudioSourceProperties();
@@ -361,6 +370,10 @@ private:
 	void LoadSavedStudioProgramProjectors(
 		obs_data_array_t *savedStudioProgramProjectors);
 
+	obs_data_array_t *SaveMultiviewProjectors();
+	void LoadSavedMultiviewProjectors(
+		obs_data_array_t *savedMultiviewProjectors);
+
 public slots:
 	void StartStreaming();
 	void StopStreaming();
@@ -404,7 +417,7 @@ private slots:
 	void RemoveSceneItem(OBSSceneItem item);
 	void AddScene(OBSSource source);
 	void RemoveScene(OBSSource source);
-	void RenameSources(QString newName, QString prevName);
+	void RenameSources(OBSSource source, QString newName, QString prevName);
 
 	void SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select);
 
@@ -477,7 +490,8 @@ private:
 	static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed);
 
 public:
-	OBSScene      GetCurrentScene();
+	OBSSource GetProgramSource();
+	OBSScene GetCurrentScene();
 
 	void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n);
 
@@ -689,11 +703,13 @@ private slots:
 	void OpenStudioProgramProjector();
 	void OpenPreviewProjector();
 	void OpenSourceProjector();
+	void OpenMultiviewProjector();
 	void OpenSceneProjector();
 
 	void OpenStudioProgramWindow();
 	void OpenPreviewWindow();
 	void OpenSourceWindow();
+	void OpenMultiviewWindow();
 	void OpenSceneWindow();
 
 public slots:

+ 464 - 9
UI/window-projector.cpp

@@ -7,7 +7,9 @@
 #include "display-helpers.hpp"
 #include "qt-wrappers.hpp"
 #include "platform.hpp"
-#include "obs-app.hpp"
+
+static QList<OBSProjector *> multiviewProjectors;
+static bool updatingMultiview = false;
 
 OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, bool window)
 	: OBSQTDisplay                 (widget,
@@ -30,7 +32,10 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, bool window)
 
 	auto addDrawCallback = [this] ()
 	{
-		obs_display_add_draw_callback(GetDisplay(), OBSRender, this);
+		bool isMultiview = type == ProjectorType::Multiview;
+		obs_display_add_draw_callback(GetDisplay(),
+				isMultiview ? OBSRenderMultiview : OBSRender,
+				this);
 		obs_display_set_background_color(GetDisplay(), 0x000000);
 	};
 
@@ -50,13 +55,73 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, bool window)
 
 OBSProjector::~OBSProjector()
 {
+	bool isMultiview = type == ProjectorType::Multiview;
+	obs_display_remove_draw_callback(GetDisplay(),
+			isMultiview ? OBSRenderMultiview : OBSRender, this);
+
 	if (source)
 		obs_source_dec_showing(source);
+
+	if (isMultiview) {
+		for (OBSWeakSource &weakSrc : multiviewScenes) {
+			OBSSource src = OBSGetStrongRef(weakSrc);
+			if (src)
+				obs_source_dec_showing(src);
+		}
+
+		obs_enter_graphics();
+		gs_vertexbuffer_destroy(outerBox);
+		gs_vertexbuffer_destroy(innerBox);
+		gs_vertexbuffer_destroy(leftVLine);
+		gs_vertexbuffer_destroy(rightVLine);
+		gs_vertexbuffer_destroy(leftLine);
+		gs_vertexbuffer_destroy(topLine);
+		gs_vertexbuffer_destroy(rightLine);
+		obs_leave_graphics();
+	}
+
+	if (type == ProjectorType::Multiview)
+		multiviewProjectors.removeAll(this);
+
 	App()->DecrementSleepInhibition();
 }
 
+static OBSSource CreateLabel(const char *name, size_t h)
+{
+	obs_data_t *settings = obs_data_create();
+	obs_data_t *font     = obs_data_create();
+
+	std::string text;
+	text += " ";
+	text += name;
+	text += " ";
+
+#if defined(_WIN32)
+	obs_data_set_string(font, "face", "Arial");
+#elif defined(__APPLE__)
+	obs_data_set_string(font, "face", "Helvetica");
+#else
+	obs_data_set_string(font, "face", "Monospace");
+#endif
+	obs_data_set_int(font, "flags", 0);
+	obs_data_set_int(font, "size", int(h / 9.81));
+
+	obs_data_set_obj(settings, "font", font);
+	obs_data_set_string(settings, "text", text.c_str());
+	obs_data_set_bool(settings, "outline", true);
+
+	OBSSource txtSource = obs_source_create_private("text_ft2_source", name,
+			settings);
+	obs_source_release(txtSource);
+
+	obs_data_release(font);
+	obs_data_release(settings);
+
+	return txtSource;
+}
+
 void OBSProjector::Init(int monitor, bool window, QString title,
-		bool studioProgram)
+		ProjectorType type_)
 {
 	QScreen *screen = QGuiApplication::screens()[monitor];
 
@@ -86,13 +151,342 @@ void OBSProjector::Init(int monitor, bool window, QString title,
 	}
 
 	savedMonitor = monitor;
-	isWindow = window;
-	useStudioProgram = studioProgram;
+	isWindow     = window;
+	type         = type_;
+
+	if (type == ProjectorType::Multiview) {
+		obs_enter_graphics();
+		gs_render_start(true);
+		gs_vertex2f(0.001f, 0.001f);
+		gs_vertex2f(0.001f, 0.997f);
+		gs_vertex2f(0.997f, 0.997f);
+		gs_vertex2f(0.997f, 0.001f);
+		gs_vertex2f(0.001f, 0.001f);
+		outerBox = gs_render_save();
+
+		gs_render_start(true);
+		gs_vertex2f(0.04f, 0.04f);
+		gs_vertex2f(0.04f, 0.96f);
+		gs_vertex2f(0.96f, 0.96f);
+		gs_vertex2f(0.96f, 0.04f);
+		gs_vertex2f(0.04f, 0.04f);
+		innerBox = gs_render_save();
+
+		gs_render_start(true);
+		gs_vertex2f(0.15f, 0.04f);
+		gs_vertex2f(0.15f, 0.96f);
+		leftVLine = gs_render_save();
+
+		gs_render_start(true);
+		gs_vertex2f(0.85f, 0.04f);
+		gs_vertex2f(0.85f, 0.96f);
+		rightVLine = gs_render_save();
+
+		gs_render_start(true);
+		gs_vertex2f(0.0f, 0.5f);
+		gs_vertex2f(0.075f, 0.5f);
+		leftLine = gs_render_save();
+
+		gs_render_start(true);
+		gs_vertex2f(0.5f, 0.0f);
+		gs_vertex2f(0.5f, 0.09f);
+		topLine = gs_render_save();
+
+		gs_render_start(true);
+		gs_vertex2f(0.925f, 0.5f);
+		gs_vertex2f(1.0f, 0.5f);
+		rightLine = gs_render_save();
+		obs_leave_graphics();
+
+		UpdateMultiview();
+
+		multiviewProjectors.push_back(this);
+	}
+
+	ready = true;
+}
+
+static inline void renderVB(gs_effect_t *effect, gs_vertbuffer_t *vb,
+		int cx, int cy)
+{
+	if (!vb)
+		return;
+
+	matrix4 transform;
+	matrix4_identity(&transform);
+	transform.x.x = cx;
+	transform.y.y = cy;
+
+	gs_load_vertexbuffer(vb);
+
+	gs_matrix_push();
+	gs_matrix_mul(&transform);
+
+	while (gs_effect_loop(effect, "Solid"))
+		gs_draw(GS_LINESTRIP, 0, 0);
+
+	gs_matrix_pop();
+}
+
+static inline uint32_t labelOffset(obs_source_t *label, uint32_t cx)
+{
+	uint32_t w = obs_source_get_width(label);
+	w = uint32_t(float(w) * 0.5f);
+	return (cx / 2) - w;
+}
+
+void OBSProjector::OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy)
+{
+	OBSProjector *window = (OBSProjector *)data;
+
+	if (updatingMultiview || !window->ready)
+		return;
+
+	OBSBasic     *main   = (OBSBasic *)obs_frontend_get_main_window();
+	uint32_t     targetCX, targetCY;
+	int          x, y;
+	float        fX, fY, halfCX, halfCY, sourceX, sourceY,
+	             quarterCX, quarterCY, scale, targetCXF, targetCYF,
+		     hiCX, hiCY, qiX, qiY, qiCX, qiCY,
+		     hiScaleX, hiScaleY, qiScaleX, qiScaleY;
+	uint32_t     offset;
+	gs_rect      rect;
+
+	gs_effect_t  *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
+	gs_eparam_t  *color = gs_effect_get_param_by_name(solid, "color");
+
+	struct obs_video_info ovi;
+	obs_get_video_info(&ovi);
+	targetCX = ovi.base_width;
+	targetCY = ovi.base_height;
+
+	GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
+
+	targetCXF = float(targetCX);
+	targetCYF = float(targetCY);
+	fX        = float(x);
+	fY        = float(y);
+
+	halfCX    = (targetCXF + 1) / 2;
+	halfCY    = (targetCYF + 1) / 2;
+	hiCX      = (halfCX - 4.0);
+	hiCY      = (halfCY - 4.0);
+	hiScaleX  = hiCX / targetCXF;
+	hiScaleY  = hiCY / targetCYF;
+
+	quarterCX = (halfCX + 1) / 2;
+	quarterCY = (halfCY + 1) / 2;
+	qiCX      = (quarterCX - 8.0);
+	qiCY      = (quarterCY - 8.0);
+	qiScaleX  = qiCX / targetCXF;
+	qiScaleY  = qiCY / targetCYF;
+
+	OBSSource previewSrc = main->GetCurrentSceneSource();
+	OBSSource programSrc = main->GetProgramSource();
+
+	bool studioMode = main->IsPreviewProgramMode();
+
+	auto drawBox = [solid, color] (float cx, float cy,
+			uint32_t colorVal)
+	{
+		gs_effect_set_color(color, colorVal);
+		while (gs_effect_loop(solid, "Solid"))
+			gs_draw_sprite(nullptr, 0, (uint32_t)cx, (uint32_t)cy);
+	};
+
+	/* ----------------------------- */
+	/* draw sources                  */
+
+	gs_projection_push();
+	gs_viewport_push();
+	gs_set_viewport(x, y, targetCX * scale, targetCY * scale);
+	gs_ortho(0.0f, targetCXF, 0.0f, targetCYF, -100.0f, 100.0f);
+
+	for (size_t i = 0; i < 8; i++) {
+		OBSSource src = OBSGetStrongRef(window->multiviewScenes[i]);
+		obs_source *label = window->multiviewLabels[i + 2];
+
+		if (!src)
+			continue;
+		if (!label)
+			continue;
+
+		if (i < 4) {
+			sourceX = (float(i) * quarterCX);
+			sourceY = halfCY;
+		} else {
+			sourceX = (float(i - 4) * quarterCX);
+			sourceY = halfCY + quarterCY;
+		}
+
+		qiX = sourceX + 4.0f;
+		qiY = sourceY + 4.0f;
+
+		rect.x = int(fX + qiX * scale);
+		rect.y = int(fY + qiY * scale);
+		rect.cx = int(qiCX * scale);
+		rect.cy = int(qiCY * scale);
+
+		/* ----------- */
+
+		if (src == previewSrc || src == programSrc) {
+			uint32_t colorVal = src == programSrc
+				? 0xFFFF0000
+				: 0xFF00FF00;
+
+			gs_matrix_push();
+			gs_matrix_translate3f(sourceX, sourceY, 0.0f);
+			drawBox(quarterCX, quarterCY, colorVal);
+			gs_matrix_pop();
+
+			gs_matrix_push();
+			gs_matrix_translate3f(qiX, qiY, 0.0f);
+			drawBox(qiCX, qiCY, 0xFF000000);
+			gs_matrix_pop();
+		}
+
+		/* ----------- */
+
+		gs_matrix_push();
+		gs_matrix_translate3f(qiX, qiY, 0.0f);
+		gs_matrix_scale3f(qiScaleX, qiScaleY, 1.0f);
+
+		gs_effect_set_color(color, 0xFF000000);
+		gs_set_scissor_rect(&rect);
+		obs_source_video_render(src);
+		gs_set_scissor_rect(nullptr);
+		gs_effect_set_color(color, 0xFFFFFFFF);
+		renderVB(solid, window->outerBox, targetCX, targetCY);
+
+		gs_matrix_pop();
+
+		/* ----------- */
+
+
+		offset = labelOffset(label, quarterCX);
+		cx = obs_source_get_width(label);
+		cy = obs_source_get_height(label);
+
+		gs_matrix_push();
+		gs_matrix_translate3f(sourceX + offset,
+				(quarterCY * 0.8f) + sourceY, 0.0f);
+
+		drawBox(cx, cy + int(quarterCX * 0.015f), 0xD91F1F1F);
+		obs_source_video_render(label);
+
+		gs_matrix_pop();
+	}
+
+	gs_effect_set_color(color, 0xFFFFFFFF);
+
+	/* ----------------------------- */
+	/* draw preview                  */
+
+	gs_matrix_push();
+	gs_matrix_translate3f(2.0f, 2.0f, 0.0f);
+	gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f);
+
+	rect.x = int(fX + 2.0f * scale);
+	rect.y = int(fY + 2.0f * scale);
+	rect.cx = int(hiCX * scale);
+	rect.cy = int(hiCY * scale);
+	gs_set_scissor_rect(&rect);
+
+	if (studioMode) {
+		obs_source_video_render(previewSrc);
+	} else {
+		obs_render_main_view();
+	}
+
+	gs_set_scissor_rect(nullptr);
+
+	gs_matrix_pop();
+
+	/* ----------- */
+
+	gs_matrix_push();
+	gs_matrix_scale3f(0.5f, 0.5f, 1.0f);
+
+	renderVB(solid, window->outerBox, targetCX, targetCY);
+	renderVB(solid, window->innerBox, targetCX, targetCY);
+	renderVB(solid, window->leftVLine, targetCX, targetCY);
+	renderVB(solid, window->rightVLine, targetCX, targetCY);
+	renderVB(solid, window->leftLine, targetCX, targetCY);
+	renderVB(solid, window->topLine, targetCX, targetCY);
+	renderVB(solid, window->rightLine, targetCX, targetCY);
+
+	gs_matrix_pop();
+
+	/* ----------- */
+
+	obs_source_t *previewLabel = window->multiviewLabels[0];
+	offset = labelOffset(previewLabel, halfCX);
+	cx = obs_source_get_width(previewLabel);
+	cy = obs_source_get_height(previewLabel);
+
+	gs_matrix_push();
+	gs_matrix_translate3f(offset, (halfCY * 0.8f), 0.0f);
+
+	drawBox(cx, cy + int(halfCX * 0.015f), 0xD91F1F1F);
+	obs_source_video_render(previewLabel);
+
+	gs_matrix_pop();
+
+	/* ----------------------------- */
+	/* draw program                  */
+
+	gs_matrix_push();
+	gs_matrix_translate3f(halfCX + 2.0, 2.0f, 0.0f);
+	gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f);
+
+	rect.x = int(fX + (halfCX + 2.0f) * scale);
+	rect.y = int(fY + 2.0f * scale);
+	gs_set_scissor_rect(&rect);
+
+	obs_render_main_view();
+
+	gs_set_scissor_rect(nullptr);
+
+	gs_matrix_pop();
+
+	/* ----------- */
+
+	gs_matrix_push();
+	gs_matrix_translate3f(halfCX, 0.0f, 0.0f);
+	gs_matrix_scale3f(0.5f, 0.5f, 1.0f);
+
+	renderVB(solid, window->outerBox, targetCX, targetCY);
+
+	gs_matrix_pop();
+
+	/* ----------- */
+
+	obs_source_t *programLabel = window->multiviewLabels[1];
+	offset = labelOffset(programLabel, halfCX);
+	cx = obs_source_get_width(programLabel);
+	cy = obs_source_get_height(programLabel);
+
+	gs_matrix_push();
+	gs_matrix_translate3f(halfCX + offset, (halfCY * 0.8f), 0.0f);
+
+	drawBox(cx, cy + int(halfCX * 0.015f), 0xD91F1F1F);
+	obs_source_video_render(programLabel);
+
+	gs_matrix_pop();
+
+	/* ----------------------------- */
+
+	gs_viewport_pop();
+	gs_projection_pop();
 }
 
 void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy)
 {
 	OBSProjector *window = reinterpret_cast<OBSProjector*>(data);
+
+	if (!window->ready)
+		return;
+
 	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
 
 	uint32_t targetCX;
@@ -123,7 +517,8 @@ void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy)
 
 	OBSSource source = window->source;
 
-	if (!window->useStudioProgram && main->IsPreviewProgramMode()) {
+	if (window->type == ProjectorType::Preview &&
+	    main->IsPreviewProgramMode()) {
 		OBSSource curSource = main->GetCurrentSceneSource();
 
 		if (window->source != curSource) {
@@ -166,11 +561,71 @@ void OBSProjector::mousePressEvent(QMouseEvent *event)
 void OBSProjector::EscapeTriggered()
 {
 	if (!isWindow) {
-		OBSBasic *main =
-			reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
-
+		OBSBasic *main = (OBSBasic*)obs_frontend_get_main_window();
 		main->RemoveSavedProjectors(savedMonitor);
 	}
 
 	deleteLater();
 }
+
+void OBSProjector::UpdateMultiview()
+{
+	for (OBSWeakSource &val : multiviewScenes)
+		val = nullptr;
+	for (OBSSource &val : multiviewLabels)
+		val = nullptr;
+
+	struct obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	uint32_t h = ovi.base_height;
+
+	struct obs_frontend_source_list scenes = {};
+	obs_frontend_get_scenes(&scenes);
+
+	int curIdx = 0;
+
+	multiviewLabels[0] = CreateLabel(Str("StudioMode.Preview"), h / 2);
+	multiviewLabels[1] = CreateLabel(Str("StudioMode.Program"), h / 2);
+
+	for (size_t i = 0; i < scenes.sources.num && curIdx < 8; i++) {
+		obs_source_t *src = scenes.sources.array[i];
+		OBSData data = obs_source_get_private_settings(src);
+		obs_data_release(data);
+
+		obs_data_set_default_bool(data, "show_in_multiview", true);
+		if (!obs_data_get_bool(data, "show_in_multiview"))
+			continue;
+
+		multiviewScenes[curIdx] = OBSGetWeakRef(src);
+		obs_source_inc_showing(src);
+
+		std::string name;
+		name += std::to_string(curIdx + 1);
+		name += " - ";
+		name += obs_source_get_name(src);
+
+		if (name.size() > 15)
+			name.resize(15);
+
+		multiviewLabels[curIdx + 2] = CreateLabel(name.c_str(), h / 4);
+
+		curIdx++;
+	}
+
+	obs_frontend_source_list_free(&scenes);
+}
+
+void OBSProjector::UpdateMultiviewProjectors()
+{
+	obs_enter_graphics();
+	updatingMultiview = true;
+	obs_leave_graphics();
+
+	for (auto &projector : multiviewProjectors)
+		projector->UpdateMultiview();
+
+	obs_enter_graphics();
+	updatingMultiview = false;
+	obs_leave_graphics();
+}

+ 17 - 2
UI/window-projector.hpp

@@ -13,6 +13,7 @@ private:
 	OBSSource source;
 	OBSSignal removedSignal;
 
+	static void OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy);
 	static void OBSRender(void *data, uint32_t cx, uint32_t cy);
 	static void OBSSourceRemoved(void *data, calldata_t *params);
 
@@ -20,7 +21,19 @@ private:
 
 	int savedMonitor = 0;
 	bool isWindow = false;
-	bool useStudioProgram = false;
+	ProjectorType type = ProjectorType::Source;
+	OBSWeakSource multiviewScenes[8];
+	OBSSource     multiviewLabels[10];
+	gs_vertbuffer_t *outerBox = nullptr;
+	gs_vertbuffer_t *innerBox = nullptr;
+	gs_vertbuffer_t *leftVLine = nullptr;
+	gs_vertbuffer_t *rightVLine = nullptr;
+	gs_vertbuffer_t *leftLine = nullptr;
+	gs_vertbuffer_t *topLine = nullptr;
+	gs_vertbuffer_t *rightLine = nullptr;
+	bool ready = false;
+
+	void UpdateMultiview();
 
 private slots:
 	void EscapeTriggered();
@@ -30,5 +43,7 @@ public:
 	~OBSProjector();
 
 	void Init(int monitor, bool window, QString title,
-			bool studioProgram = false);
+			ProjectorType type = ProjectorType::Source);
+
+	static void UpdateMultiviewProjectors();
 };