浏览代码

UI/libobs: Undo/Redo Sources and Scenes

Implements the Undo/Redo for scenes and sources, ranging from renaming,
deletion, addition. It also adds several elements to libobs that were
designed to facilitate undo/redo, and should not affect the rest of
libobs.
Ford Smith 4 年之前
父节点
当前提交
60d95cb5bd

+ 4 - 2
UI/CMakeLists.txt

@@ -256,7 +256,8 @@ set(obs_SOURCES
 	obs-proxy-style.cpp
 	obs-proxy-style.cpp
 	locked-checkbox.cpp
 	locked-checkbox.cpp
 	visibility-checkbox.cpp
 	visibility-checkbox.cpp
-	media-slider.cpp)
+	media-slider.cpp
+	undo-stack-obs.cpp)
 
 
 set(obs_HEADERS
 set(obs_HEADERS
 	${obs_PLATFORM_HEADERS}
 	${obs_PLATFORM_HEADERS}
@@ -327,7 +328,8 @@ set(obs_HEADERS
 	log-viewer.hpp
 	log-viewer.hpp
 	obs-proxy-style.hpp
 	obs-proxy-style.hpp
 	obs-proxy-style.hpp
 	obs-proxy-style.hpp
-	media-slider.hpp)
+	media-slider.hpp
+	undo-stack-obs.hpp)
 
 
 set(obs_importers_HEADERS
 set(obs_importers_HEADERS
 	importers/importers.hpp)
 	importers/importers.hpp)

+ 21 - 1
UI/forms/OBSBasic.ui

@@ -586,6 +586,9 @@
       <string>Paste.Filters</string>
       <string>Paste.Filters</string>
      </property>
      </property>
     </action>
     </action>
+    <addaction name="actionMainUndo"/>
+    <addaction name="actionMainRedo"/>
+    <addaction name="separator"/>
     <addaction name="actionCopySource"/>
     <addaction name="actionCopySource"/>
     <addaction name="actionPasteRef"/>
     <addaction name="actionPasteRef"/>
     <addaction name="actionPasteDup"/>
     <addaction name="actionPasteDup"/>
@@ -599,6 +602,7 @@
     <addaction name="actionLockPreview"/>
     <addaction name="actionLockPreview"/>
     <addaction name="separator"/>
     <addaction name="separator"/>
     <addaction name="actionAdvAudioProperties"/>
     <addaction name="actionAdvAudioProperties"/>
+    <addaction name="separator"/>
    </widget>
    </widget>
    <widget class="QMenu" name="profileMenu">
    <widget class="QMenu" name="profileMenu">
     <property name="title">
     <property name="title">
@@ -2034,7 +2038,23 @@
     <string>Basic.MainMenu.View.ContextBar</string>
     <string>Basic.MainMenu.View.ContextBar</string>
    </property>
    </property>
   </action>
   </action>
- </widget>
+  <action name="actionMainUndo">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+    <property name="text">
+      <string>Undo</string>
+    </property>
+  </action>
+  <action name="actionMainRedo">
+   <property name="enabled">
+    <bool>false</bool>
+    </property>
+    <property name="text">
+      <string>Redo</string>
+    </property>
+    </action>
+  </widget>
  <customwidgets>
  <customwidgets>
   <customwidget>
   <customwidget>
    <class>OBSBasicPreview</class>
    <class>OBSBasicPreview</class>

+ 28 - 0
UI/source-tree.cpp

@@ -403,6 +403,34 @@ void SourceTreeItem::ExitEditMode(bool save)
 	/* rename                                    */
 	/* rename                                    */
 
 
 	SignalBlocker sourcesSignalBlocker(this);
 	SignalBlocker sourcesSignalBlocker(this);
+	std::string prevName(obs_source_get_name(source));
+	std::string scene_name =
+		obs_source_get_name(main->GetCurrentSceneSource());
+	auto undo = [scene_name, prevName, main](const std::string &data) {
+		obs_source_t *source = obs_get_source_by_name(data.c_str());
+		obs_source_set_name(source, prevName.c_str());
+		obs_source_release(source);
+
+		obs_source_t *scene_source =
+			obs_get_source_by_name(scene_name.c_str());
+		main->SetCurrentScene(scene_source);
+		obs_source_release(scene_source);
+	};
+
+	auto redo = [scene_name, main, newName](const std::string &data) {
+		obs_source_t *source = obs_get_source_by_name(data.c_str());
+		obs_source_set_name(source, newName.c_str());
+		obs_source_release(source);
+
+		obs_source_t *scene_source =
+			obs_get_source_by_name(scene_name.c_str());
+		main->SetCurrentScene(scene_source);
+		obs_source_release(scene_source);
+	};
+
+	main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo,
+				redo, newName, prevName, NULL);
+
 	obs_source_set_name(source, newName.c_str());
 	obs_source_set_name(source, newName.c_str());
 	label->setText(QT_UTF8(newName.c_str()));
 	label->setText(QT_UTF8(newName.c_str()));
 }
 }

+ 326 - 26
UI/window-basic-main.cpp

@@ -17,7 +17,10 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
 ******************************************************************************/
 
 
+#include <cstddef>
 #include <ctime>
 #include <ctime>
+#include <obs-data.h>
+#include <obs.h>
 #include <obs.hpp>
 #include <obs.hpp>
 #include <QGuiApplication>
 #include <QGuiApplication>
 #include <QMessageBox>
 #include <QMessageBox>
@@ -58,6 +61,7 @@
 #include "remote-text.hpp"
 #include "remote-text.hpp"
 #include "ui-validation.hpp"
 #include "ui-validation.hpp"
 #include "media-controls.hpp"
 #include "media-controls.hpp"
+#include "undo-stack-obs.hpp"
 #include <fstream>
 #include <fstream>
 #include <sstream>
 #include <sstream>
 
 
@@ -203,7 +207,7 @@ extern void RegisterTwitchAuth();
 extern void RegisterRestreamAuth();
 extern void RegisterRestreamAuth();
 
 
 OBSBasic::OBSBasic(QWidget *parent)
 OBSBasic::OBSBasic(QWidget *parent)
-	: OBSMainWindow(parent), ui(new Ui::OBSBasic)
+	: OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic)
 {
 {
 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
 	qRegisterMetaTypeStreamOperators<SignalContainer<OBSScene>>(
 	qRegisterMetaTypeStreamOperators<SignalContainer<OBSScene>>(
@@ -366,6 +370,16 @@ OBSBasic::OBSBasic(QWidget *parent)
 	assignDockToggle(ui->controlsDock, ui->toggleControls);
 	assignDockToggle(ui->controlsDock, ui->toggleControls);
 	assignDockToggle(statsDock, ui->toggleStats);
 	assignDockToggle(statsDock, ui->toggleStats);
 
 
+	// Register shortcuts for Undo/Redo
+	ui->actionMainUndo->setShortcut(Qt::CTRL + Qt::Key_Z);
+	QList<QKeySequence> shrt;
+	shrt << QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z)
+	     << QKeySequence(Qt::CTRL + Qt::Key_Y);
+	ui->actionMainRedo->setShortcuts(shrt);
+
+	ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut);
+	ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut);
+
 	//hide all docking panes
 	//hide all docking panes
 	ui->toggleScenes->setChecked(false);
 	ui->toggleScenes->setChecked(false);
 	ui->toggleSources->setChecked(false);
 	ui->toggleSources->setChecked(false);
@@ -3622,6 +3636,33 @@ void OBSBasic::DuplicateSelectedScene()
 							 OBS_SCENE_DUP_REFS);
 							 OBS_SCENE_DUP_REFS);
 		source = obs_scene_get_source(scene);
 		source = obs_scene_get_source(scene);
 		SetCurrentScene(source, true);
 		SetCurrentScene(source, true);
+
+		auto undo = [](const std::string &data) {
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_source_remove(source);
+			obs_source_release(source);
+		};
+
+		auto redo = [this, name](const std::string &data) {
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_scene_t *scene = obs_scene_from_source(source);
+			obs_source_release(source);
+			scene = obs_scene_duplicate(scene, name.c_str(),
+						    OBS_SCENE_DUP_REFS);
+			source = obs_scene_get_source(scene);
+			SetCurrentScene(source, true);
+			obs_scene_release(scene);
+		};
+
+		undo_s.add_action(
+			QTStr("Undo.Scene.Duplicate")
+				.arg(obs_source_get_name(source)),
+			undo, redo, obs_source_get_name(source),
+			obs_source_get_name(obs_scene_get_source(curScene)),
+			NULL);
+
 		obs_scene_release(scene);
 		obs_scene_release(scene);
 
 
 		break;
 		break;
@@ -3631,15 +3672,107 @@ void OBSBasic::DuplicateSelectedScene()
 void OBSBasic::RemoveSelectedScene()
 void OBSBasic::RemoveSelectedScene()
 {
 {
 	OBSScene scene = GetCurrentScene();
 	OBSScene scene = GetCurrentScene();
-	if (scene) {
-		obs_source_t *source = obs_scene_get_source(scene);
-		if (QueryRemoveSource(source)) {
+	obs_source_t *source = obs_scene_get_source(scene);
+
+	OBSSource curProgramScene = OBSGetStrongRef(programScene);
+
+	if (source && QueryRemoveSource(source)) {
+		vector<std::string> item_ids;
+		obs_data_t *wrapper = obs_save_source(source);
+		obs_data_array_t *arr = obs_data_array_create();
+		struct wrap {
+			obs_data_array_t *arr;
+			vector<std::string> &items;
+		};
+
+		wrap passthrough = {arr, item_ids};
+		obs_scene_enum_items(
+			scene,
+			[](obs_scene_t *, obs_sceneitem_t *item,
+			   void *vp_wrap) {
+				wrap *passthrough = (wrap *)vp_wrap;
+				passthrough->items.push_back(obs_source_get_name(
+					obs_sceneitem_get_source(item)));
+				obs_data_array_t *arr = passthrough->arr;
+				obs_sceneitem_save(item, arr);
+				obs_source_addref(
+					obs_sceneitem_get_source(item));
+				return true;
+			},
+			(void *)&passthrough);
+		obs_data_array_t *list_order = SaveSceneListOrder();
+		obs_data_set_array(wrapper, "arr", arr);
+		obs_data_set_array(wrapper, "list_order", list_order);
+		obs_data_set_string(wrapper, "name",
+				    obs_source_get_name(source));
+
+		auto d = [item_ids](bool remove_ref) {
+			for (auto &item : item_ids) {
+				obs_source_t *source =
+					obs_get_source_by_name(item.c_str());
+				blog(LOG_INFO, "%s", item.c_str());
+				if (remove_ref) {
+					obs_source_release(source);
+					obs_source_release(source);
+				}
+			}
+		};
+
+		auto undo = [this, d](const std::string &data) {
+			obs_data_t *dat =
+				obs_data_create_from_json(data.c_str());
+			obs_source_release(obs_load_source(dat));
+			obs_data_array_t *arr = obs_data_get_array(dat, "arr");
+			obs_data_array_t *list_order =
+				obs_data_get_array(dat, "list_order");
+			const char *sname = obs_data_get_string(dat, "name");
+			obs_source_t *source = obs_get_source_by_name(sname);
+			obs_scene_t *scene = obs_scene_from_source(source);
+
+			obs_sceneitems_add(scene, arr);
+			LoadSceneListOrder(list_order);
+			SetCurrentScene(source);
+
+			obs_data_release(dat);
+			obs_data_array_release(arr);
+			obs_data_array_release(list_order);
+			obs_source_release(source);
+
+			d(true);
+		};
+		obs_data_t *rwrapper = obs_data_create();
+		obs_data_set_string(rwrapper, "name",
+				    obs_source_get_name(source));
+		auto redo = [d](const std::string &data) {
+			obs_data_t *dat =
+				obs_data_create_from_json(data.c_str());
+			obs_source_t *source = obs_get_source_by_name(
+				obs_data_get_string(dat, "name"));
 			obs_source_remove(source);
 			obs_source_remove(source);
+			obs_source_release(source);
+			obs_data_release(dat);
 
 
-			if (api)
-				api->on_event(
-					OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
-		}
+			d(false);
+		};
+
+		std::string undo_data = obs_data_get_json(wrapper);
+		std::string redo_data = obs_data_get_json(wrapper);
+		undo_s.add_action(
+			QTStr("Undo.Delete").arg(obs_source_get_name(source)),
+			undo, redo, undo_data, redo_data, [d](bool undo) {
+				if (undo) {
+					d(true);
+				}
+			});
+
+		obs_source_remove(source);
+		obs_data_release(wrapper);
+		obs_data_release(rwrapper);
+		obs_data_array_release(arr);
+		obs_data_array_release(list_order);
+
+		if (api)
+			api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
 	}
 	}
 }
 }
 
 
@@ -4330,6 +4463,7 @@ void OBSBasic::closeEvent(QCloseEvent *event)
 
 
 	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
 	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
 	 * sources, etc) so that all references are released before shutdown */
 	 * sources, etc) so that all references are released before shutdown */
+	undo_s.release();
 	ClearSceneData();
 	ClearSceneData();
 
 
 	App()->quit();
 	App()->quit();
@@ -4686,20 +4820,34 @@ void OBSBasic::on_actionAddScene_triggered()
 			return;
 			return;
 		}
 		}
 
 
+		auto undo_fn = [](const std::string &data) {
+			obs_source_t *t = obs_get_source_by_name(data.c_str());
+			if (t) {
+				obs_source_release(t);
+				obs_source_remove(t);
+			}
+		};
+
+		auto redo_fn = [this](const std::string &data) {
+			obs_scene_t *scene = obs_scene_create(data.c_str());
+			obs_source_t *source = obs_scene_get_source(scene);
+			SetCurrentScene(source);
+			obs_scene_release(scene);
+		};
+		undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())),
+				  undo_fn, redo_fn, name, name, NULL);
+
 		obs_scene_t *scene = obs_scene_create(name.c_str());
 		obs_scene_t *scene = obs_scene_create(name.c_str());
 		source = obs_scene_get_source(scene);
 		source = obs_scene_get_source(scene);
 		SetCurrentScene(source);
 		SetCurrentScene(source);
+		RefreshSources(scene);
 		obs_scene_release(scene);
 		obs_scene_release(scene);
 	}
 	}
 }
 }
 
 
 void OBSBasic::on_actionRemoveScene_triggered()
 void OBSBasic::on_actionRemoveScene_triggered()
 {
 {
-	OBSScene scene = GetCurrentScene();
-	obs_source_t *source = obs_scene_get_source(scene);
-
-	if (source && QueryRemoveSource(source))
-		obs_source_remove(source);
+	RemoveSelectedScene();
 }
 }
 
 
 void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx)
 void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx)
@@ -5109,10 +5257,11 @@ void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem)
 void OBSBasic::AddSource(const char *id)
 void OBSBasic::AddSource(const char *id)
 {
 {
 	if (id && *id) {
 	if (id && *id) {
-		OBSBasicSourceSelect sourceSelect(this, id);
+		OBSBasicSourceSelect sourceSelect(this, id, undo_s);
 		sourceSelect.exec();
 		sourceSelect.exec();
-		if (sourceSelect.newSource && strcmp(id, "group") != 0)
+		if (sourceSelect.newSource && strcmp(id, "group") != 0) {
 			CreatePropertiesWindow(sourceSelect.newSource);
 			CreatePropertiesWindow(sourceSelect.newSource);
+		}
 	}
 	}
 }
 }
 
 
@@ -5253,9 +5402,11 @@ void OBSBasic::on_actionRemoveSource_triggered()
 	if (!items.size())
 	if (!items.size())
 		return;
 		return;
 
 
-	auto removeMultiple = [this](size_t count) {
+	bool confirmed = false;
+
+	if (items.size() > 1) {
 		QString text = QTStr("ConfirmRemove.TextMultiple")
 		QString text = QTStr("ConfirmRemove.TextMultiple")
-				       .arg(QString::number(count));
+				       .arg(QString::number(items.size()));
 
 
 		QMessageBox remove_items(this);
 		QMessageBox remove_items(this);
 		remove_items.setText(text);
 		remove_items.setText(text);
@@ -5267,21 +5418,139 @@ void OBSBasic::on_actionRemoveSource_triggered()
 		remove_items.setWindowTitle(QTStr("ConfirmRemove.Title"));
 		remove_items.setWindowTitle(QTStr("ConfirmRemove.Title"));
 		remove_items.exec();
 		remove_items.exec();
 
 
-		return Yes == remove_items.clickedButton();
+		confirmed = remove_items.clickedButton();
+	} else {
+		OBSSceneItem &item = items[0];
+		obs_source_t *source = obs_sceneitem_get_source(item);
+		if (source && QueryRemoveSource(source))
+			confirmed = true;
+	}
+	if (!confirmed)
+		return;
+
+	struct source_save {
+		std::string name;
+		std::string scene_name;
+		int pos;
+		bool in_group = false;
+		int64_t group_id;
 	};
 	};
+	vector<source_save> item_save;
 
 
-	if (items.size() == 1) {
-		OBSSceneItem &item = items[0];
+	obs_data_t *wrapper = obs_data_create();
+	obs_data_array_t *data = obs_data_array_create();
+	for (const auto &item : items) {
+		obs_sceneitem_save(item, data);
 		obs_source_t *source = obs_sceneitem_get_source(item);
 		obs_source_t *source = obs_sceneitem_get_source(item);
+		obs_source_addref(source);
+		obs_source_set_hidden(source, true);
+
+		obs_sceneitem_t *grp =
+			obs_sceneitem_get_group(GetCurrentScene(), item);
+		obs_scene_t *scene = obs_sceneitem_get_scene(item);
+		source_save save = {
+			obs_source_get_name(source),
+			obs_source_get_name(obs_scene_get_source(scene)),
+			obs_sceneitem_get_order_position(item),
+			grp ? true : false, obs_sceneitem_get_id(grp)};
+
+		item_save.push_back(save);
+	}
+
+	obs_scene_t *scene = GetCurrentScene();
+	const char *name = obs_source_get_name(obs_scene_get_source(scene));
+	obs_data_set_array(wrapper, "data_array", data);
+	obs_data_set_string(wrapper, "name", name);
+	std::string undo_data(obs_data_get_json(wrapper));
+
+	auto undo_fn = [this, item_save](const std::string &data) {
+		obs_data_t *dat = obs_data_create_from_json(data.c_str());
+		obs_data_array_t *sources_data =
+			obs_data_get_array(dat, "data_array");
+		const char *name = obs_data_get_string(dat, "name");
+		obs_source_t *src = obs_get_source_by_name(name);
+		obs_scene_t *scene = obs_scene_from_source(src);
+
+		obs_sceneitems_add(scene, sources_data);
+		SetCurrentScene(scene);
+
+		for (const auto &save : item_save) {
+			obs_source_t *source =
+				obs_get_source_by_name(save.name.c_str());
+			obs_source_set_hidden(source, false);
+			if (save.in_group) {
+				obs_sceneitem_t *grp =
+					obs_scene_find_sceneitem_by_id(
+						scene, save.group_id);
+				obs_sceneitem_t *item =
+					obs_scene_sceneitem_from_source(scene,
+									source);
+				obs_sceneitem_group_add_item(grp, item);
+				obs_sceneitem_set_order_position(item,
+								 save.pos);
+
+				obs_sceneitem_release(item);
+			}
 
 
-		if (source && QueryRemoveSource(source))
+			obs_source_release(source);
+			obs_source_release(source);
+		}
+
+		obs_source_release(src);
+		obs_data_array_release(sources_data);
+		obs_data_release(dat);
+	};
+
+	auto redo_fn = [item_save](const std::string &) {
+		for (const auto &save : item_save) {
+			obs_source_t *source =
+				obs_get_source_by_name(save.name.c_str());
+			obs_source_t *scene_source =
+				obs_get_source_by_name(save.scene_name.c_str());
+			obs_scene_t *scene =
+				obs_scene_from_source(scene_source);
+			if (!scene)
+				scene = obs_group_from_source(scene_source);
+
+			obs_sceneitem_t *item =
+				obs_scene_sceneitem_from_source(scene, source);
 			obs_sceneitem_remove(item);
 			obs_sceneitem_remove(item);
-	} else {
-		if (removeMultiple(items.size())) {
-			for (auto &item : items)
-				obs_sceneitem_remove(item);
+			obs_source_set_hidden(source, true);
+
+			obs_sceneitem_release(item);
+			obs_source_release(scene_source);
+			/*  usually want to release source, but redo needs to add a reference to keep alive */
 		}
 		}
-	}
+	};
+
+	auto d = [item_save](bool is_undo) {
+		if (!is_undo)
+			return;
+
+		for (const auto &item : item_save) {
+			obs_source_t *source =
+				obs_get_source_by_name(item.name.c_str());
+			obs_source_release(source);
+			obs_source_release(source);
+		}
+	};
+
+	QString action_name;
+	if (items.size() > 1)
+		action_name = QTStr("Undo.Sources.Multi")
+				      .arg(QString::number(items.size()));
+	else
+		action_name =
+			QTStr("Undo.Delete")
+				.arg(QString(obs_source_get_name(
+					obs_sceneitem_get_source(items[0]))));
+	undo_s.add_action(action_name, undo_fn, redo_fn, undo_data, "", d);
+
+	obs_data_array_release(data);
+	obs_data_release(wrapper);
+
+	for (auto &item : items)
+		obs_sceneitem_remove(item);
 }
 }
 
 
 void OBSBasic::on_actionInteract_triggered()
 void OBSBasic::on_actionInteract_triggered()
@@ -5519,6 +5788,27 @@ static void RenameListItem(OBSBasic *parent, QListWidget *listWidget,
 
 
 		obs_source_release(foundSource);
 		obs_source_release(foundSource);
 	} else {
 	} else {
+		auto undo = [prev = std::string(prevName)](
+				    const std::string &data) {
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_source_set_name(source, prev.c_str());
+			obs_source_release(source);
+		};
+
+		auto redo = [name](const std::string &data) {
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_source_set_name(source, name.c_str());
+			obs_source_release(source);
+		};
+
+		std::string undo_data(name);
+		std::string redo_data(prevName);
+		parent->undo_s.add_action(
+			QTStr("Undo.Rename").arg(name.c_str()), undo, redo,
+			undo_data, redo_data, NULL);
+
 		listItem->setText(QT_UTF8(name.c_str()));
 		listItem->setText(QT_UTF8(name.c_str()));
 		obs_source_set_name(source, name.c_str());
 		obs_source_set_name(source, name.c_str());
 	}
 	}
@@ -7762,6 +8052,16 @@ bool OBSBasic::sysTrayMinimizeToTray()
 			       "SysTrayMinimizeToTray");
 			       "SysTrayMinimizeToTray");
 }
 }
 
 
+void OBSBasic::on_actionMainUndo_triggered()
+{
+	undo_s.undo();
+}
+
+void OBSBasic::on_actionMainRedo_triggered()
+{
+	undo_s.redo();
+}
+
 void OBSBasic::on_actionCopySource_triggered()
 void OBSBasic::on_actionCopySource_triggered()
 {
 {
 	copyStrings.clear();
 	copyStrings.clear();

+ 7 - 0
UI/window-basic-main.hpp

@@ -36,6 +36,7 @@
 #include "window-basic-about.hpp"
 #include "window-basic-about.hpp"
 #include "auth-base.hpp"
 #include "auth-base.hpp"
 #include "log-viewer.hpp"
 #include "log-viewer.hpp"
+#include "undo-stack-obs.hpp"
 
 
 #include <obs-frontend-internal.hpp>
 #include <obs-frontend-internal.hpp>
 
 
@@ -166,6 +167,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class ExtraBrowsersDelegate;
 	friend class ExtraBrowsersDelegate;
 	friend class DeviceCaptureToolbar;
 	friend class DeviceCaptureToolbar;
 	friend class DeviceToolbarPropertiesThread;
 	friend class DeviceToolbarPropertiesThread;
+	friend class OBSBasicSourceSelect;
 	friend struct BasicOutputHandler;
 	friend struct BasicOutputHandler;
 	friend struct OBSStudioAPI;
 	friend struct OBSStudioAPI;
 
 
@@ -608,6 +610,10 @@ public slots:
 	void UnpauseRecording();
 	void UnpauseRecording();
 
 
 private slots:
 private slots:
+
+	void on_actionMainUndo_triggered();
+	void on_actionMainRedo_triggered();
+
 	void AddSceneItem(OBSSceneItem item);
 	void AddSceneItem(OBSSceneItem item);
 	void AddScene(OBSSource source);
 	void AddScene(OBSSource source);
 	void RemoveScene(OBSSource source);
 	void RemoveScene(OBSSource source);
@@ -740,6 +746,7 @@ private:
 	OBSSource prevFTBSource = nullptr;
 	OBSSource prevFTBSource = nullptr;
 
 
 public:
 public:
+	undo_stack undo_s;
 	OBSSource GetProgramSource();
 	OBSSource GetProgramSource();
 	OBSScene GetCurrentScene();
 	OBSScene GetCurrentScene();
 
 

+ 67 - 3
UI/window-basic-source-select.cpp

@@ -28,6 +28,9 @@ struct AddSourceData {
 
 
 bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source)
 bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source)
 {
 {
+	if (obs_source_is_hidden(source))
+		return false;
+
 	OBSBasicSourceSelect *window =
 	OBSBasicSourceSelect *window =
 		static_cast<OBSBasicSourceSelect *>(data);
 		static_cast<OBSBasicSourceSelect *>(data);
 	const char *name = obs_source_get_name(source);
 	const char *name = obs_source_get_name(source);
@@ -179,7 +182,7 @@ bool AddNew(QWidget *parent, const char *id, const char *name,
 		return false;
 		return false;
 
 
 	obs_source_t *source = obs_get_source_by_name(name);
 	obs_source_t *source = obs_get_source_by_name(name);
-	if (source) {
+	if (source && parent) {
 		OBSMessageBox::information(parent, QTStr("NameExists.Title"),
 		OBSMessageBox::information(parent, QTStr("NameExists.Title"),
 					   QTStr("NameExists.Text"));
 					   QTStr("NameExists.Text"));
 
 
@@ -236,6 +239,63 @@ void OBSBasicSourceSelect::on_buttonBox_accepted()
 		if (!AddNew(this, id, QT_TO_UTF8(ui->sourceName->text()),
 		if (!AddNew(this, id, QT_TO_UTF8(ui->sourceName->text()),
 			    visible, newSource))
 			    visible, newSource))
 			return;
 			return;
+
+		OBSBasic *main =
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+		std::string scene_name =
+			obs_source_get_name(main->GetCurrentSceneSource());
+		auto undo = [scene_name, main](const std::string &data) {
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_source_release(source);
+			obs_source_remove(source);
+
+			obs_source_t *scene_source =
+				obs_get_source_by_name(scene_name.c_str());
+			main->SetCurrentScene(scene_source);
+			obs_source_release(scene_source);
+
+			main->RefreshSources(main->GetCurrentScene());
+		};
+		obs_data_t *wrapper = obs_data_create();
+		obs_data_set_string(wrapper, "id", id);
+		obs_sceneitem_t *item = obs_scene_sceneitem_from_source(
+			main->GetCurrentScene(), newSource);
+		obs_data_set_int(wrapper, "item_id",
+				 obs_sceneitem_get_id(item));
+		obs_data_set_string(
+			wrapper, "name",
+			ui->sourceName->text().toUtf8().constData());
+		obs_data_set_bool(wrapper, "visible", visible);
+
+		auto redo = [scene_name, main](const std::string &data) {
+			obs_data_t *dat =
+				obs_data_create_from_json(data.c_str());
+			OBSSource source;
+			AddNew(NULL, obs_data_get_string(dat, "id"),
+			       obs_data_get_string(dat, "name"),
+			       obs_data_get_bool(dat, "visible"), source);
+			obs_sceneitem_t *item = obs_scene_sceneitem_from_source(
+				main->GetCurrentScene(), source);
+			obs_sceneitem_set_id(item, (int64_t)obs_data_get_int(
+							   dat, "item_id"));
+
+			obs_source_t *scene_source =
+				obs_get_source_by_name(scene_name.c_str());
+			main->SetCurrentScene(scene_source);
+			obs_source_release(scene_source);
+
+			main->RefreshSources(main->GetCurrentScene());
+			obs_data_release(dat);
+			obs_sceneitem_release(item);
+		};
+		undo_s.add_action(QTStr("Undo.Add").arg(ui->sourceName->text()),
+				  undo, redo,
+				  std::string(obs_source_get_name(newSource)),
+				  std::string(obs_data_get_json(wrapper)),
+				  NULL);
+		obs_data_release(wrapper);
+		obs_sceneitem_release(item);
 	}
 	}
 
 
 	done(DialogCode::Accepted);
 	done(DialogCode::Accepted);
@@ -261,8 +321,12 @@ template<typename T> static inline T GetOBSRef(QListWidgetItem *item)
 	return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
 	return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
 }
 }
 
 
-OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_)
-	: QDialog(parent), ui(new Ui::OBSBasicSourceSelect), id(id_)
+OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_,
+					   undo_stack &undo_s)
+	: QDialog(parent),
+	  ui(new Ui::OBSBasicSourceSelect),
+	  id(id_),
+	  undo_s(undo_s)
 {
 {
 	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
 	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
 
 

+ 4 - 1
UI/window-basic-source-select.hpp

@@ -21,6 +21,7 @@
 #include <memory>
 #include <memory>
 
 
 #include "ui_OBSBasicSourceSelect.h"
 #include "ui_OBSBasicSourceSelect.h"
+#include "undo-stack-obs.hpp"
 
 
 class OBSBasic;
 class OBSBasic;
 
 
@@ -30,6 +31,7 @@ class OBSBasicSourceSelect : public QDialog {
 private:
 private:
 	std::unique_ptr<Ui::OBSBasicSourceSelect> ui;
 	std::unique_ptr<Ui::OBSBasicSourceSelect> ui;
 	const char *id;
 	const char *id;
+	undo_stack &undo_s;
 
 
 	static bool EnumSources(void *data, obs_source_t *source);
 	static bool EnumSources(void *data, obs_source_t *source);
 	static bool EnumGroups(void *data, obs_source_t *source);
 	static bool EnumGroups(void *data, obs_source_t *source);
@@ -45,7 +47,8 @@ private slots:
 	void SourceRemoved(OBSSource source);
 	void SourceRemoved(OBSSource source);
 
 
 public:
 public:
-	OBSBasicSourceSelect(OBSBasic *parent, const char *id);
+	OBSBasicSourceSelect(OBSBasic *parent, const char *id,
+			     undo_stack &undo_s);
 
 
 	OBSSource newSource;
 	OBSSource newSource;
 
 

+ 3 - 0
libobs/obs-internal.h

@@ -621,6 +621,9 @@ struct obs_source {
 	 * to handle things but it's the best option) */
 	 * to handle things but it's the best option) */
 	bool removed;
 	bool removed;
 
 
+	/*  used to indicate if the source should show up when queried for user ui */
+	bool temp_removed;
+
 	bool active;
 	bool active;
 	bool showing;
 	bool showing;
 
 

+ 66 - 0
libobs/obs-scene.c

@@ -1477,6 +1477,33 @@ obs_sceneitem_t *obs_scene_find_source_recursive(obs_scene_t *scene,
 	return item;
 	return item;
 }
 }
 
 
+struct sceneitem_check {
+	obs_source_t *source_in;
+	obs_sceneitem_t *item_out;
+};
+
+bool check_sceneitem_exists(obs_scene_t *scene, obs_sceneitem_t *item,
+			    void *vp_check)
+{
+	UNUSED_PARAMETER(scene);
+	struct sceneitem_check *check = (struct sceneitem_check *)vp_check;
+	if (obs_sceneitem_get_source(item) == check->source_in) {
+		check->item_out = item;
+		obs_sceneitem_addref(item);
+		return false;
+	}
+
+	return true;
+}
+
+obs_sceneitem_t *obs_scene_sceneitem_from_source(obs_scene_t *scene,
+						 obs_source_t *source)
+{
+	struct sceneitem_check check = {source, NULL};
+	obs_scene_enum_items(scene, check_sceneitem_exists, (void *)&check);
+	return check.item_out;
+}
+
 obs_sceneitem_t *obs_scene_find_sceneitem_by_id(obs_scene_t *scene, int64_t id)
 obs_sceneitem_t *obs_scene_find_sceneitem_by_id(obs_scene_t *scene, int64_t id)
 {
 {
 	struct obs_scene_item *item;
 	struct obs_scene_item *item;
@@ -1830,6 +1857,22 @@ void obs_sceneitem_remove(obs_sceneitem_t *item)
 	obs_sceneitem_release(item);
 	obs_sceneitem_release(item);
 }
 }
 
 
+void obs_sceneitem_save(obs_sceneitem_t *item, obs_data_array_t *arr)
+{
+	scene_save_item(arr, item, NULL);
+}
+
+void sceneitem_restore(obs_data_t *data, void *vp)
+{
+	obs_scene_t *scene = (obs_scene_t *)vp;
+	scene_load_item(scene, data);
+}
+
+void obs_sceneitems_add(obs_scene_t *scene, obs_data_array_t *data)
+{
+	obs_data_array_enum(data, sceneitem_restore, scene);
+}
+
 obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item)
 obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item)
 {
 {
 	return item ? item->parent : NULL;
 	return item ? item->parent : NULL;
@@ -1977,6 +2020,24 @@ void obs_sceneitem_set_order(obs_sceneitem_t *item,
 	obs_scene_release(scene);
 	obs_scene_release(scene);
 }
 }
 
 
+int obs_sceneitem_get_order_position(obs_sceneitem_t *item)
+{
+	struct obs_scene *scene = item->parent;
+	struct obs_scene_item *next = scene->first_item;
+
+	full_lock(scene);
+
+	int index = 0;
+	while (next && next != item) {
+		next = next->next;
+		++index;
+	}
+
+	full_unlock(scene);
+
+	return index;
+}
+
 void obs_sceneitem_set_order_position(obs_sceneitem_t *item, int position)
 void obs_sceneitem_set_order_position(obs_sceneitem_t *item, int position)
 {
 {
 	if (!item)
 	if (!item)
@@ -2385,6 +2446,11 @@ int64_t obs_sceneitem_get_id(const obs_sceneitem_t *item)
 	return item->id;
 	return item->id;
 }
 }
 
 
+void obs_sceneitem_set_id(obs_sceneitem_t *item, int64_t id)
+{
+	item->id = id;
+}
+
 obs_data_t *obs_sceneitem_get_private_settings(obs_sceneitem_t *item)
 obs_data_t *obs_sceneitem_get_private_settings(obs_sceneitem_t *item)
 {
 {
 	if (!obs_ptr_valid(item, "obs_sceneitem_get_private_settings"))
 	if (!obs_ptr_valid(item, "obs_sceneitem_get_private_settings"))

+ 12 - 1
libobs/obs-source.c

@@ -929,8 +929,9 @@ void obs_source_update(obs_source_t *source, obs_data_t *settings)
 	if (!obs_source_valid(source, "obs_source_update"))
 	if (!obs_source_valid(source, "obs_source_update"))
 		return;
 		return;
 
 
-	if (settings)
+	if (settings) {
 		obs_data_apply(source->context.settings, settings);
 		obs_data_apply(source->context.settings, settings);
+	}
 
 
 	if (source->info.output_flags & OBS_SOURCE_VIDEO) {
 	if (source->info.output_flags & OBS_SOURCE_VIDEO) {
 		os_atomic_inc_long(&source->defer_update_count);
 		os_atomic_inc_long(&source->defer_update_count);
@@ -4283,6 +4284,16 @@ void obs_source_enum_filters(obs_source_t *source,
 	pthread_mutex_unlock(&source->filter_mutex);
 	pthread_mutex_unlock(&source->filter_mutex);
 }
 }
 
 
+void obs_source_set_hidden(obs_source_t *source, bool hidden)
+{
+	source->temp_removed = hidden;
+}
+
+bool obs_source_is_hidden(obs_source_t *source)
+{
+	return source->temp_removed;
+}
+
 obs_source_t *obs_source_get_filter_by_name(obs_source_t *source,
 obs_source_t *obs_source_get_filter_by_name(obs_source_t *source,
 					    const char *name)
 					    const char *name)
 {
 {

+ 23 - 0
libobs/obs.h

@@ -898,6 +898,14 @@ EXPORT void obs_source_remove(obs_source_t *source);
 /** Returns true if the source should be released */
 /** Returns true if the source should be released */
 EXPORT bool obs_source_removed(const obs_source_t *source);
 EXPORT bool obs_source_removed(const obs_source_t *source);
 
 
+/** The 'hidden' flag is not the same as a sceneitem's visibility. It is a
+  * property the determines if it can be found through searches. **/
+/** Simply sets a 'hidden' flag when the source is still alive but shouldn't be found */
+EXPORT void obs_source_set_hidden(obs_source_t *source, bool hidden);
+
+/** Returns the current 'hidden' state on the source */
+EXPORT bool obs_source_is_hidden(obs_source_t *source);
+
 /** Returns capability flags of a source */
 /** Returns capability flags of a source */
 EXPORT uint32_t obs_source_get_output_flags(const obs_source_t *source);
 EXPORT uint32_t obs_source_get_output_flags(const obs_source_t *source);
 
 
@@ -1578,6 +1586,21 @@ EXPORT void obs_sceneitem_release(obs_sceneitem_t *item);
 /** Removes a scene item. */
 /** Removes a scene item. */
 EXPORT void obs_sceneitem_remove(obs_sceneitem_t *item);
 EXPORT void obs_sceneitem_remove(obs_sceneitem_t *item);
 
 
+/** Adds a scene item. */
+EXPORT void obs_sceneitems_add(obs_scene_t *scene, obs_data_array_t *data);
+
+/** Saves Sceneitem into an array, arr **/
+EXPORT void obs_sceneitem_save(obs_sceneitem_t *item, obs_data_array_t *arr);
+
+/** Set the ID of a sceneitem */
+EXPORT void obs_sceneitem_set_id(obs_sceneitem_t *sceneitem, int64_t id);
+
+/** Tries to find the sceneitem of the source in a given scene. Returns NULL if not found */
+EXPORT obs_sceneitem_t *obs_scene_sceneitem_from_source(obs_scene_t *scene,
+							obs_source_t *source);
+/**  Gets a sceneitem's order in its scene */
+EXPORT int obs_sceneitem_get_order_position(obs_sceneitem_t *item);
+
 /** Gets the scene parent associated with the scene item. */
 /** Gets the scene parent associated with the scene item. */
 EXPORT obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item);
 EXPORT obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item);