Browse Source

frontend-tools: Add scene switcher plugin

jp9000 9 years ago
parent
commit
39592ff5eb

+ 2 - 0
UI/CMakeLists.txt

@@ -239,3 +239,5 @@ if (UNIX AND UNIX_STRUCTURE AND NOT APPLE)
 	install(FILES forms/images/obs.png
 		DESTINATION ${CMAKE_INSTALL_FULL_DATAROOTDIR}/icons/hicolor/256x256/apps)
 endif()
+
+add_subdirectory(frontend-plugins)

+ 3 - 0
UI/frontend-plugins/CMakeLists.txt

@@ -0,0 +1,3 @@
+if(WIN32 OR APPLE)
+	add_subdirectory(frontend-tools)
+endif()

+ 46 - 0
UI/frontend-plugins/frontend-tools/CMakeLists.txt

@@ -0,0 +1,46 @@
+project(frontend-tools)
+
+if(APPLE)
+	find_library(COCOA Cocoa)
+	include_directories(${COCOA})
+endif()
+
+set(frontend-tools_HEADERS
+	auto-scene-switcher.hpp
+	)
+set(frontend-tools_SOURCES
+	frontend-tools.c
+	auto-scene-switcher.cpp
+	)
+set(frontend-tools_UI
+	forms/auto-scene-switcher.ui
+	)
+
+if(WIN32)
+	set(frontend-tools_PLATFORM_SOURCES
+		auto-scene-switcher-win.cpp)
+elseif(APPLE)
+	set(frontend-tools_PLATFORM_SOURCES
+		auto-scene-switcher-osx.mm)
+	set_source_files_properties(auto-scene-switcher-osx.mm
+		PROPERTIES COMPILE_FLAGS "-fobjc-arc")
+
+	set(frontend-tools_PLATFORM_LIBS
+		${COCOA})
+endif()
+
+qt5_wrap_ui(frontend-tools_UI_HEADERS ${frontend-tools_UI})
+
+add_library(frontend-tools MODULE
+	${frontend-tools_HEADERS}
+	${frontend-tools_SOURCES}
+	${frontend-tools_PLATFORM_SOURCES}
+	${frontend-tools_UI_HEADERS}
+	)
+target_link_libraries(frontend-tools
+	${frontend-tools_PLATFORM_LIBS}
+	obs-frontend-api
+	Qt5::Widgets
+	libobs)
+
+install_obs_plugin_with_data(frontend-tools data)

+ 43 - 0
UI/frontend-plugins/frontend-tools/auto-scene-switcher-osx.mm

@@ -0,0 +1,43 @@
+#import <AppKit/AppKit.h>
+#include <util/platform.h>
+#include "auto-scene-switcher.hpp"
+
+using namespace std;
+
+void GetWindowList(vector<string> &windows)
+{
+	windows.resize(0);
+
+	@autoreleasepool {
+		NSWorkspace *ws = [NSWorkspace sharedWorkspace];
+		NSArray *array = [ws runningApplications];
+		for (NSRunningApplication *app in array) {
+			NSString *name = app.localizedName;
+			if (!name)
+				continue;
+
+			const char *str = name.UTF8String;
+			if (str && *str)
+				windows.emplace_back(str);
+		}
+	}
+}
+
+void GetCurrentWindowTitle(string &title)
+{
+	title.resize(0);
+
+	@autoreleasepool {
+		NSWorkspace *ws = [NSWorkspace sharedWorkspace];
+		NSRunningApplication *app = [ws frontmostApplication];
+		if (app) {
+			NSString *name = app.localizedName;
+			if (!name)
+				return;
+
+			const char *str = name.UTF8String;
+			if (str && *str)
+				title = str;
+		}
+	}
+}

+ 69 - 0
UI/frontend-plugins/frontend-tools/auto-scene-switcher-win.cpp

@@ -0,0 +1,69 @@
+#include <windows.h>
+#include <util/platform.h>
+#include "auto-scene-switcher.hpp"
+
+using namespace std;
+
+static bool GetWindowTitle(HWND window, string &title)
+{
+	size_t len = (size_t)GetWindowTextLengthW(window);
+	wstring wtitle;
+
+	wtitle.resize(len);
+	if (!GetWindowTextW(window, &wtitle[0], (int)len + 1))
+		return false;
+
+	len = os_wcs_to_utf8(wtitle.c_str(), 0, nullptr, 0);
+	title.resize(len);
+	os_wcs_to_utf8(wtitle.c_str(), 0, &title[0], len + 1);
+	return true;
+}
+
+static bool WindowValid(HWND window)
+{
+	LONG_PTR styles, ex_styles;
+	RECT rect;
+	DWORD id;
+
+	if (!IsWindowVisible(window))
+		return false;
+	GetWindowThreadProcessId(window, &id);
+	if (id == GetCurrentProcessId())
+		return false;
+
+	GetClientRect(window, &rect);
+	styles    = GetWindowLongPtr(window, GWL_STYLE);
+	ex_styles = GetWindowLongPtr(window, GWL_EXSTYLE);
+
+	if (ex_styles & WS_EX_TOOLWINDOW)
+		return false;
+	if (styles & WS_CHILD)
+		return false;
+
+	return true;
+}
+
+void GetWindowList(vector<string> &windows)
+{
+	HWND window = GetWindow(GetDesktopWindow(), GW_CHILD);
+
+	while (window) {
+		string title;
+		if (WindowValid(window) && GetWindowTitle(window, title))
+			windows.emplace_back(title);
+		window = GetNextWindow(window, GW_HWNDNEXT);
+	}
+}
+
+void GetCurrentWindowTitle(string &title)
+{
+	HWND window = GetForegroundWindow();
+	DWORD id;
+
+	GetWindowThreadProcessId(window, &id);
+	if (id == GetCurrentProcessId()) {
+		title = "";
+		return;
+	}
+	GetWindowTitle(window, title);
+}

+ 588 - 0
UI/frontend-plugins/frontend-tools/auto-scene-switcher.cpp

@@ -0,0 +1,588 @@
+#include <obs-frontend-api.h>
+#include <obs-module.h>
+#include <obs.hpp>
+#include <util/util.hpp>
+#include <QMainWindow>
+#include <QAction>
+#include "auto-scene-switcher.hpp"
+
+#include <condition_variable>
+#include <chrono>
+#include <string>
+#include <vector>
+#include <thread>
+#include <regex>
+#include <mutex>
+
+using namespace std;
+
+#define DEFAULT_INTERVAL 300
+
+struct SceneSwitch {
+	OBSWeakSource scene;
+	string window;
+	regex re;
+
+	inline SceneSwitch(OBSWeakSource scene_, const char *window_)
+		: scene(scene_), window(window_), re(window_)
+	{
+	}
+};
+
+static inline bool WeakSourceValid(obs_weak_source_t *ws)
+{
+	obs_source_t *source = obs_weak_source_get_source(ws);
+	if (source)
+		obs_source_release(source);
+	return !!source;
+}
+
+struct SwitcherData {
+	thread th;
+	condition_variable cv;
+	mutex m;
+	bool stop = false;
+
+	vector<SceneSwitch> switches;
+	OBSWeakSource nonMatchingScene;
+	int interval = DEFAULT_INTERVAL;
+	bool switchIfNotMatching = false;
+	bool startAtLaunch = false;
+
+	void Thread();
+	void Start();
+	void Stop();
+
+	void Prune()
+	{
+		for (size_t i = 0; i < switches.size(); i++) {
+			SceneSwitch &s = switches[i];
+			if (!WeakSourceValid(s.scene))
+				switches.erase(switches.begin() + i--);
+		}
+
+		if (nonMatchingScene && !WeakSourceValid(nonMatchingScene)) {
+			switchIfNotMatching = false;
+			nonMatchingScene = nullptr;
+		}
+	}
+
+	inline ~SwitcherData()
+	{
+		Stop();
+	}
+};
+
+static SwitcherData *switcher = nullptr;
+
+static inline QString MakeSwitchName(const QString &scene,
+		const QString &window)
+{
+	return QStringLiteral("[") + scene + QStringLiteral("]: ") + window;
+}
+
+static inline string GetWeakSourceName(obs_weak_source_t *weak_source)
+{
+	string name;
+
+	obs_source_t *source = obs_weak_source_get_source(weak_source);
+	if (source) {
+		name = obs_source_get_name(source);
+		obs_source_release(source);
+	}
+
+	return name;
+}
+
+static inline OBSWeakSource GetWeakSourceByName(const char *name)
+{
+	OBSWeakSource weak;
+	obs_source_t *source = obs_get_source_by_name(name);
+	if (source) {
+		weak = obs_source_get_weak_source(source);
+		obs_weak_source_release(weak);
+		obs_source_release(source);
+	}
+
+	return weak;
+}
+
+static inline OBSWeakSource GetWeakSourceByQString(const QString &name)
+{
+	return GetWeakSourceByName(name.toUtf8().constData());
+}
+
+SceneSwitcher::SceneSwitcher(QWidget *parent)
+	: QDialog(parent),
+	  ui(new Ui_SceneSwitcher)
+{
+	ui->setupUi(this);
+
+	lock_guard<mutex> lock(switcher->m);
+
+	switcher->Prune();
+
+	BPtr<char*> scenes = obs_frontend_get_scene_names();
+	char **temp = scenes;
+	while (*temp) {
+		const char *name = *temp;
+		ui->scenes->addItem(name);
+		ui->noMatchSwitchScene->addItem(name);
+		temp++;
+	}
+
+	if (switcher->switchIfNotMatching)
+		ui->noMatchSwitch->setChecked(true);
+	else
+		ui->noMatchDontSwitch->setChecked(true);
+
+	ui->noMatchSwitchScene->setCurrentText(
+			GetWeakSourceName(switcher->nonMatchingScene).c_str());
+	ui->checkInterval->setValue(switcher->interval);
+
+	vector<string> windows;
+	GetWindowList(windows);
+
+	for (string &window : windows)
+		ui->windows->addItem(window.c_str());
+
+	for (auto &s : switcher->switches) {
+		string sceneName = GetWeakSourceName(s.scene);
+		QString text = MakeSwitchName(sceneName.c_str(),
+				s.window.c_str());
+
+		QListWidgetItem *item = new QListWidgetItem(text,
+				ui->switches);
+		item->setData(Qt::UserRole, s.window.c_str());
+	}
+
+	if (switcher->th.joinable())
+		SetStarted();
+	else
+		SetStopped();
+
+	loading = false;
+}
+
+void SceneSwitcher::closeEvent(QCloseEvent*)
+{
+	obs_frontend_save();
+}
+
+int SceneSwitcher::FindByData(const QString &window)
+{
+	int count = ui->switches->count();
+	int idx = -1;
+
+	for (int i = 0; i < count; i++) {
+		QListWidgetItem *item = ui->switches->item(i);
+		QString itemWindow =
+			item->data(Qt::UserRole).toString();
+
+		if (itemWindow == window) {
+			idx = i;
+			break;
+		}
+	}
+
+	return idx;
+}
+
+void SceneSwitcher::on_switches_currentRowChanged(int idx)
+{
+	if (loading)
+		return;
+	if (idx == -1)
+		return;
+
+	QListWidgetItem *item = ui->switches->item(idx);
+
+	QString window = item->data(Qt::UserRole).toString();
+
+	lock_guard<mutex> lock(switcher->m);
+	for (auto &s : switcher->switches) {
+		if (window.compare(s.window.c_str()) == 0) {
+			string name = GetWeakSourceName(s.scene);
+			ui->scenes->setCurrentText(name.c_str());
+			ui->windows->setCurrentText(window);
+			break;
+		}
+	}
+}
+
+void SceneSwitcher::on_close_clicked()
+{
+	done(0);
+}
+
+void SceneSwitcher::on_add_clicked()
+{
+	QString sceneName = ui->scenes->currentText();
+	QString windowName = ui->windows->currentText();
+
+	if (windowName.isEmpty())
+		return;
+
+	OBSWeakSource source = GetWeakSourceByQString(sceneName);
+	QVariant v = QVariant::fromValue(windowName);
+
+	QString text = MakeSwitchName(sceneName, windowName);
+
+	int idx = FindByData(windowName);
+
+	if (idx == -1) {
+		QListWidgetItem *item = new QListWidgetItem(text,
+				ui->switches);
+		item->setData(Qt::UserRole, v);
+
+		lock_guard<mutex> lock(switcher->m);
+		switcher->switches.emplace_back(source,
+				windowName.toUtf8().constData());
+	} else {
+		QListWidgetItem *item = ui->switches->item(idx);
+		item->setText(text);
+
+		string window = windowName.toUtf8().constData();
+
+		{
+			lock_guard<mutex> lock(switcher->m);
+			for (auto &s : switcher->switches) {
+				if (s.window == window) {
+					s.scene = source;
+					break;
+				}
+			}
+		}
+
+		ui->switches->sortItems();
+	}
+}
+
+void SceneSwitcher::on_remove_clicked()
+{
+	QListWidgetItem *item = ui->switches->currentItem();
+	if (!item)
+		return;
+
+	string window =
+		item->data(Qt::UserRole).toString().toUtf8().constData();
+
+	{
+		lock_guard<mutex> lock(switcher->m);
+		auto &switches = switcher->switches;
+
+		for (auto it = switches.begin(); it != switches.end(); ++it) {
+			auto &s = *it;
+
+			if (s.window == window) {
+				switches.erase(it);
+				break;
+			}
+		}
+	}
+
+	delete item;
+}
+
+void SceneSwitcher::on_startAtLaunch_toggled(bool value)
+{
+	if (loading)
+		return;
+
+	lock_guard<mutex> lock(switcher->m);
+	switcher->startAtLaunch = value;
+}
+
+void SceneSwitcher::UpdateNonMatchingScene(const QString &name)
+{
+	obs_source_t *scene = obs_get_source_by_name(
+			name.toUtf8().constData());
+	obs_weak_source_t *ws = obs_source_get_weak_source(scene);
+
+	switcher->nonMatchingScene = ws;
+
+	obs_weak_source_release(ws);
+	obs_source_release(scene);
+}
+
+void SceneSwitcher::on_noMatchDontSwitch_clicked()
+{
+	if (loading)
+		return;
+
+	lock_guard<mutex> lock(switcher->m);
+	switcher->switchIfNotMatching = false;
+}
+
+void SceneSwitcher::on_noMatchSwitch_clicked()
+{
+	if (loading)
+		return;
+
+	lock_guard<mutex> lock(switcher->m);
+	switcher->switchIfNotMatching = true;
+	UpdateNonMatchingScene(ui->noMatchSwitchScene->currentText());
+}
+
+void SceneSwitcher::on_noMatchSwitchScene_currentTextChanged(
+		const QString &text)
+{
+	if (loading)
+		return;
+
+	lock_guard<mutex> lock(switcher->m);
+	UpdateNonMatchingScene(text);
+}
+
+void SceneSwitcher::on_checkInterval_valueChanged(int value)
+{
+	if (loading)
+		return;
+
+	lock_guard<mutex> lock(switcher->m);
+	switcher->interval = value;
+}
+
+void SceneSwitcher::SetStarted()
+{
+	ui->toggleStartButton->setText(obs_module_text("Stop"));
+	ui->pluginRunningText->setText(obs_module_text("Active"));
+}
+
+void SceneSwitcher::SetStopped()
+{
+	ui->toggleStartButton->setText(obs_module_text("Start"));
+	ui->pluginRunningText->setText(obs_module_text("Inactive"));
+}
+
+void SceneSwitcher::on_toggleStartButton_clicked()
+{
+	if (switcher->th.joinable()) {
+		switcher->Stop();
+		SetStopped();
+	} else {
+		switcher->Start();
+		SetStarted();
+	}
+}
+
+static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *)
+{
+	if (saving) {
+		lock_guard<mutex> lock(switcher->m);
+		obs_data_t *obj = obs_data_create();
+		obs_data_array_t *array = obs_data_array_create();
+
+		switcher->Prune();
+
+		for (SceneSwitch &s : switcher->switches) {
+			obs_data_t *array_obj = obs_data_create();
+
+			obs_source_t *source = obs_weak_source_get_source(
+					s.scene);
+			if (source) {
+				const char *n = obs_source_get_name(source);
+				obs_data_set_string(array_obj, "scene", n);
+				obs_data_set_string(array_obj, "window_title",
+						s.window.c_str());
+				obs_data_array_push_back(array, array_obj);
+				obs_source_release(source);
+			}
+
+			obs_data_release(array_obj);
+		}
+
+		string nonMatchingSceneName =
+			GetWeakSourceName(switcher->nonMatchingScene);
+
+		obs_data_set_int(obj, "interval", switcher->interval);
+		obs_data_set_string(obj, "non_matching_scene",
+				nonMatchingSceneName.c_str());
+		obs_data_set_bool(obj, "switch_if_not_matching",
+				switcher->switchIfNotMatching);
+		obs_data_set_bool(obj, "active", switcher->th.joinable());
+		obs_data_set_array(obj, "switches", array);
+
+		obs_data_set_obj(save_data, "auto-scene-switcher", obj);
+
+		obs_data_array_release(array);
+		obs_data_release(obj);
+	} else {
+		switcher->m.lock();
+
+		obs_data_t *obj = obs_data_get_obj(save_data,
+				"auto-scene-switcher");
+		obs_data_array_t *array = obs_data_get_array(obj, "switches");
+		size_t count = obs_data_array_count(array);
+
+		if (!obj)
+			obj = obs_data_create();
+
+		obs_data_set_default_int(obj, "interval", DEFAULT_INTERVAL);
+
+		switcher->interval = obs_data_get_int(obj, "interval");
+		switcher->switchIfNotMatching =
+			obs_data_get_bool(obj, "switch_if_not_matching");
+		string nonMatchingScene =
+			obs_data_get_string(obj, "non_matching_scene");
+		bool active = obs_data_get_bool(obj, "active");
+
+		switcher->nonMatchingScene =
+			GetWeakSourceByName(nonMatchingScene.c_str());
+
+		switcher->switches.clear();
+
+		for (size_t i = 0; i < count; i++) {
+			obs_data_t *array_obj = obs_data_array_item(array, i);
+
+			const char *scene =
+				obs_data_get_string(array_obj, "scene");
+			const char *window =
+				obs_data_get_string(array_obj, "window_title");
+
+			switcher->switches.emplace_back(
+					GetWeakSourceByName(scene),
+					window);
+
+			obs_data_release(array_obj);
+		}
+
+		obs_data_array_release(array);
+		obs_data_release(obj);
+
+		switcher->m.unlock();
+
+		if (active)
+			switcher->Start();
+		else
+			switcher->Stop();
+	}
+}
+
+void SwitcherData::Thread()
+{
+	chrono::duration<long long, milli> duration =
+		chrono::milliseconds(interval);
+	string lastTitle;
+	string title;
+
+	for (;;) {
+		unique_lock<mutex> lock(m);
+		OBSWeakSource scene;
+		bool match = false;
+
+		cv.wait_for(lock, duration);
+		if (switcher->stop) {
+			switcher->stop = false;
+			break;
+		}
+
+		duration = chrono::milliseconds(interval);
+
+		GetCurrentWindowTitle(title);
+
+		if (lastTitle != title) {
+			switcher->Prune();
+
+			for (SceneSwitch &s : switches) {
+				if (s.window == title) {
+					match = true;
+					scene = s.scene;
+					break;
+				}
+			}
+
+			/* try regex */
+			if (!match) {
+				for (SceneSwitch &s : switches) {
+					try {
+						bool matches = regex_match(
+								title, s.re);
+						if (matches) {
+							match = true;
+							scene = s.scene;
+							break;
+						}
+					} catch (const regex_error &) {}
+				}
+			}
+
+			if (!match && switchIfNotMatching &&
+					nonMatchingScene) {
+				match = true;
+				scene = nonMatchingScene;
+			}
+
+			if (match) {
+				obs_source_t *source =
+					obs_weak_source_get_source(scene);
+				obs_source_t *currentSource =
+					obs_frontend_get_current_scene();
+
+				if (source && source != currentSource)
+					obs_frontend_set_current_scene(source);
+
+				obs_source_release(currentSource);
+				obs_source_release(source);
+			}
+		}
+
+		lastTitle = title;
+	}
+}
+
+void SwitcherData::Start()
+{
+	if (!switcher->th.joinable())
+		switcher->th = thread([] () {switcher->Thread();});
+}
+
+void SwitcherData::Stop()
+{
+	if (th.joinable()) {
+		{
+			lock_guard<mutex> lock(m);
+			stop = true;
+		}
+		cv.notify_one();
+		th.join();
+	}
+}
+
+extern "C" void FreeSceneSwitcher()
+{
+	delete switcher;
+	switcher = nullptr;
+}
+
+static void OBSEvent(enum obs_frontend_event event, void *)
+{
+	if (event == OBS_FRONTEND_EVENT_EXIT)
+		FreeSceneSwitcher();
+}
+
+extern "C" void InitSceneSwitcher()
+{
+	QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction(
+			obs_module_text("SceneSwitcher"));
+
+	switcher = new SwitcherData;
+
+	auto cb = [] ()
+	{
+		obs_frontend_push_ui_translation(obs_module_get_string);
+
+		QMainWindow *window =
+			(QMainWindow*)obs_frontend_get_main_window();
+
+		SceneSwitcher ss(window);
+		ss.exec();
+
+		obs_frontend_pop_ui_translation();
+	};
+
+	obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
+	obs_frontend_add_event_callback(OBSEvent, nullptr);
+
+	action->connect(action, &QAction::triggered, cb);
+}

+ 47 - 0
UI/frontend-plugins/frontend-tools/auto-scene-switcher.hpp

@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QDialog>
+#include <memory>
+#include <vector>
+#include <string>
+
+#include "ui_auto-scene-switcher.h"
+
+struct obs_weak_source;
+typedef struct obs_weak_source obs_weak_source_t;
+
+class QCloseEvent;
+
+class SceneSwitcher : public QDialog {
+	Q_OBJECT
+
+public:
+	std::unique_ptr<Ui_SceneSwitcher> ui;
+	bool loading = true;
+
+	SceneSwitcher(QWidget *parent);
+
+	void closeEvent(QCloseEvent *event) override;
+
+	void SetStarted();
+	void SetStopped();
+
+	int FindByData(const QString &window);
+
+	void UpdateNonMatchingScene(const QString &name);
+
+public slots:
+	void on_switches_currentRowChanged(int idx);
+	void on_close_clicked();
+	void on_add_clicked();
+	void on_remove_clicked();
+	void on_noMatchDontSwitch_clicked();
+	void on_noMatchSwitch_clicked();
+	void on_startAtLaunch_toggled(bool value);
+	void on_noMatchSwitchScene_currentTextChanged(const QString &text);
+	void on_checkInterval_valueChanged(int value);
+	void on_toggleStartButton_clicked();
+};
+
+void GetWindowList(std::vector<std::string> &windows);
+void GetCurrentWindowTitle(std::string &title);

+ 11 - 0
UI/frontend-plugins/frontend-tools/data/locale/en-US.ini

@@ -0,0 +1,11 @@
+SceneSwitcher="Automatic Scene Switcher"
+SceneSwitcher.OnNoMatch="When no window matches:"
+SceneSwitcher.OnNoMatch.DontSwitch="Don't switch"
+SceneSwitcher.OnNoMatch.SwitchTo="Switch to:"
+SceneSwitcher.CheckInterval="Check active window title every:"
+SceneSwitcher.StartAtLaunch="Automatically start for this scene collection"
+SceneSwitcher.ActiveOrNotActive="Scene Switcher is:"
+Active="Active"
+Inactive="Inactive"
+Start="Start"
+Stop="Stop"

+ 310 - 0
UI/frontend-plugins/frontend-tools/forms/auto-scene-switcher.ui

@@ -0,0 +1,310 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SceneSwitcher</class>
+ <widget class="QDialog" name="SceneSwitcher">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>743</width>
+    <height>563</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>SceneSwitcher</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QComboBox" name="windows">
+       <property name="editable">
+        <bool>true</bool>
+       </property>
+       <property name="maxVisibleItems">
+        <number>20</number>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="scenes">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>100</width>
+         <height>0</height>
+        </size>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QListWidget" name="switches">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <property name="spacing">
+      <number>4</number>
+     </property>
+     <item>
+      <widget class="QPushButton" name="add">
+       <property name="maximumSize">
+        <size>
+         <width>22</width>
+         <height>22</height>
+        </size>
+       </property>
+       <property name="flat">
+        <bool>true</bool>
+       </property>
+       <property name="themeID" stdset="0">
+        <string notr="true">addIconSmall</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="remove">
+       <property name="maximumSize">
+        <size>
+         <width>22</width>
+         <height>22</height>
+        </size>
+       </property>
+       <property name="flat">
+        <bool>true</bool>
+       </property>
+       <property name="themeID" stdset="0">
+        <string notr="true">removeIconSmall</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <property name="fieldGrowthPolicy">
+      <enum>QFormLayout::ExpandingFieldsGrow</enum>
+     </property>
+     <property name="labelAlignment">
+      <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
+     </property>
+     <item row="0" column="0">
+      <widget class="QLabel" name="label">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>SceneSwitcher.OnNoMatch</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QRadioButton" name="noMatchDontSwitch">
+         <property name="text">
+          <string>SceneSwitcher.OnNoMatch.DontSwitch</string>
+         </property>
+         <property name="checked">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_3">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QRadioButton" name="noMatchSwitch">
+           <property name="text">
+            <string>SceneSwitcher.OnNoMatch.SwitchTo</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QComboBox" name="noMatchSwitchScene">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>100</width>
+             <height>0</height>
+            </size>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>SceneSwitcher.CheckInterval</string>
+       </property>
+       <property name="buddy">
+        <cstring>checkInterval</cstring>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QSpinBox" name="checkInterval">
+       <property name="minimumSize">
+        <size>
+         <width>100</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="suffix">
+        <string notr="true">ms</string>
+       </property>
+       <property name="minimum">
+        <number>50</number>
+       </property>
+       <property name="maximum">
+        <number>20000</number>
+       </property>
+       <property name="value">
+        <number>300</number>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_4">
+       <property name="text">
+        <string>SceneSwitcher.ActiveOrNotActive</string>
+       </property>
+      </widget>
+     </item>
+     <item row="3" column="0">
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>200</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item row="3" column="1">
+      <widget class="QPushButton" name="toggleStartButton">
+       <property name="text">
+        <string>Start</string>
+       </property>
+      </widget>
+     </item>
+     <item row="4" column="1">
+      <spacer name="verticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Preferred</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>40</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item row="2" column="1">
+      <widget class="QLabel" name="pluginRunningText">
+       <property name="text">
+        <string notr="true">Not Active</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item alignment="Qt::AlignRight">
+    <widget class="QPushButton" name="close">
+     <property name="text">
+      <string>Close</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>noMatchSwitch</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>noMatchSwitchScene</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>286</x>
+     <y>347</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>483</x>
+     <y>352</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

+ 18 - 0
UI/frontend-plugins/frontend-tools/frontend-tools.c

@@ -0,0 +1,18 @@
+#include <obs-module.h>
+
+OBS_DECLARE_MODULE()
+OBS_MODULE_USE_DEFAULT_LOCALE("frontend-tools", "en-US")
+
+void InitSceneSwitcher();
+void FreeSceneSwitcher();
+
+bool obs_module_load(void)
+{
+	InitSceneSwitcher();
+	return true;
+}
+
+void obs_module_unload(void)
+{
+	FreeSceneSwitcher();
+}