Sfoglia il codice sorgente

Merge pull request #3426 from Programatic/undo_redo

UI: Implement Undo/Redo System
Jim 4 anni fa
parent
commit
5d87f3c00b

+ 4 - 2
UI/CMakeLists.txt

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

+ 78 - 0
UI/context-bar-controls.cpp

@@ -1,3 +1,4 @@
+#include "window-basic-main.hpp"
 #include "context-bar-controls.hpp"
 #include "qt-wrappers.hpp"
 #include "obs-app.hpp"
@@ -33,6 +34,63 @@ SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source)
 {
 }
 
+void SourceToolbar::SaveOldProperties(obs_source_t *source)
+{
+	if (oldData)
+		obs_data_release(oldData);
+
+	oldData = obs_data_create();
+	obs_data_t *oldSettings = obs_source_get_settings(source);
+	obs_data_apply(oldData, oldSettings);
+	obs_data_set_string(oldData, "undo_sname", obs_source_get_name(source));
+	obs_data_release(oldSettings);
+	obs_data_release(oldData);
+}
+
+void SourceToolbar::SetUndoProperties(obs_source_t *source)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	std::string scene_name =
+		obs_source_get_name(main->GetCurrentSceneSource());
+	auto undo_redo = [scene_name,
+			  main = std::move(main)](const std::string &data) {
+		obs_data_t *settings = obs_data_create_from_json(data.c_str());
+		obs_source_t *source = obs_get_source_by_name(
+			obs_data_get_string(settings, "undo_sname"));
+		obs_source_update(source, settings);
+
+		obs_source_t *scene_source =
+			obs_get_source_by_name(scene_name.c_str());
+		main->SetCurrentScene(scene_source);
+		obs_source_release(scene_source);
+
+		obs_data_release(settings);
+		obs_source_release(source);
+
+		main->UpdateContextBar();
+	};
+
+	OBSData new_settings = obs_data_create();
+	OBSData curr_settings = obs_source_get_settings(source);
+	obs_data_apply(new_settings, curr_settings);
+	obs_data_set_string(new_settings, "undo_sname",
+			    obs_source_get_name(source));
+
+	std::string undo_data(obs_data_get_json(oldData));
+	std::string redo_data(obs_data_get_json(new_settings));
+
+	if (undo_data.compare(redo_data) != 0)
+		main->undo_s.add_action(
+			QTStr("Undo.Properties")
+				.arg(obs_source_get_name(source)),
+			undo_redo, undo_redo, undo_data, redo_data, nullptr);
+
+	obs_data_release(new_settings);
+	obs_data_release(curr_settings);
+	obs_data_release(oldData);
+}
+
 /* ========================================================================= */
 
 BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source)
@@ -163,8 +221,10 @@ void ComboSelectToolbar::on_device_currentIndexChanged(int idx)
 		return;
 	}
 
+	SaveOldProperties(source);
 	UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name,
 				      is_int);
+	SetUndoProperties(source);
 }
 
 AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source)
@@ -370,10 +430,12 @@ void GameCaptureToolbar::on_mode_currentIndexChanged(int idx)
 
 	QString id = ui->mode->itemData(idx).toString();
 
+	SaveOldProperties(source);
 	obs_data_t *settings = obs_data_create();
 	obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id));
 	obs_source_update(source, settings);
 	obs_data_release(settings);
+	SetUndoProperties(source);
 
 	UpdateWindowVisibility();
 }
@@ -387,10 +449,12 @@ void GameCaptureToolbar::on_window_currentIndexChanged(int idx)
 
 	QString id = ui->window->itemData(idx).toString();
 
+	SaveOldProperties(source);
 	obs_data_t *settings = obs_data_create();
 	obs_data_set_string(settings, "window", QT_TO_UTF8(id));
 	obs_source_update(source, settings);
 	obs_data_release(settings);
+	SetUndoProperties(source);
 }
 
 /* ========================================================================= */
@@ -434,10 +498,12 @@ void ImageSourceToolbar::on_browse_clicked()
 
 	ui->path->setText(path);
 
+	SaveOldProperties(source);
 	obs_data_t *settings = obs_data_create();
 	obs_data_set_string(settings, "file", QT_TO_UTF8(path));
 	obs_source_update(source, settings);
 	obs_data_release(settings);
+	SetUndoProperties(source);
 }
 
 /* ========================================================================= */
@@ -518,10 +584,14 @@ void ColorSourceToolbar::on_choose_clicked()
 	color = newColor;
 	UpdateColor();
 
+	SaveOldProperties(source);
+
 	obs_data_t *settings = obs_data_create();
 	obs_data_set_int(settings, "color", color_to_int(color));
 	obs_source_update(source, settings);
 	obs_data_release(settings);
+
+	SetUndoProperties(source);
 }
 
 /* ========================================================================= */
@@ -596,6 +666,8 @@ void TextSourceToolbar::on_selectFont_clicked()
 	flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0;
 	obs_data_set_int(font_obj, "flags", flags);
 
+	SaveOldProperties(source);
+
 	obs_data_t *settings = obs_data_create();
 
 	obs_data_set_obj(settings, "font", font_obj);
@@ -603,6 +675,8 @@ void TextSourceToolbar::on_selectFont_clicked()
 
 	obs_source_update(source, settings);
 	obs_data_release(settings);
+
+	SetUndoProperties(source);
 }
 
 void TextSourceToolbar::on_selectColor_clicked()
@@ -628,6 +702,8 @@ void TextSourceToolbar::on_selectColor_clicked()
 
 	color = newColor;
 
+	SaveOldProperties(source);
+
 	obs_data_t *settings = obs_data_create();
 	if (!strncmp(obs_source_get_id(source), "text_ft2_source", 15)) {
 		obs_data_set_int(settings, "color1", color_to_int(color));
@@ -637,6 +713,8 @@ void TextSourceToolbar::on_selectColor_clicked()
 	}
 	obs_source_update(source, settings);
 	obs_data_release(settings);
+
+	SetUndoProperties(source);
 }
 
 void TextSourceToolbar::on_text_textChanged()

+ 4 - 0
UI/context-bar-controls.hpp

@@ -22,6 +22,10 @@ protected:
 		std::unique_ptr<obs_properties_t, properties_delete_t>;
 
 	properties_t props;
+	OBSData oldData;
+
+	void SaveOldProperties(obs_source_t *source);
+	void SetUndoProperties(obs_source_t *source);
 
 public:
 	SourceToolbar(QWidget *parent, OBSSource source);

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

@@ -264,6 +264,34 @@ Basic.SceneTransitions="Scene Transitions"
 Basic.TransitionDuration="Duration"
 Basic.TogglePreviewProgramMode="Studio Mode"
 
+# undo
+Undo.Undo="Undo"
+Undo.Redo="Redo"
+Undo.Add="Add '%1'"
+Undo.Delete="Delete '%1'"
+Undo.Rename="Rename '%1'"
+Undo.SceneCollection.Switch="Switch to '%1'"
+Undo.Item.Undo="Undo %1"
+Undo.Item.Redo="Redo %1"
+Undo.Sources.Multi="Delete %1 Sources"
+Undo.Filters="Filter Changes on '%1'"
+Undo.Transform="Transform source(s) In '%1'"
+Undo.Transform.Paste="Paste Transformation in '%1'"
+Undo.Transform.Rotate="Rotation In '%1'"
+Undo.Transform.Reset="Transform Reset In '%1'"
+Undo.Transform.HFlip="Horizontal Flip In '%1'"
+Undo.Transform.VFlip="Vertical Flip In '%1'"
+Undo.Transform.FitToScren="Fit to Screen In '%1'"
+Undo.Transform.StretchToScreen="Stretch to Screen in '%1'"
+Undo.Transform.Center="Center to Screen in '%1'"
+Undo.Transform.VCenter="Vertical Center to Screen in '%1'"
+Undo.Transform.HCenter="Horizontal Center to Screen in '%1'"
+Undo.Volume.Change="Volume Change on '%1'"
+Undo.Audio="Audio Changes"
+Undo.Properties="Property Change on '%1'"
+Undo.Scene.Duplicate="Duplicate Scene '%1'"
+
+
 # transition name dialog
 TransitionNameDlg.Text="Please enter the name of the transition"
 TransitionNameDlg.Title="Transition Name"

+ 21 - 1
UI/forms/OBSBasic.ui

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

+ 38 - 3
UI/properties-view.cpp

@@ -20,6 +20,7 @@
 #include <QStackedWidget>
 #include <QDir>
 #include <QGroupBox>
+#include <QObject>
 #include "double-slider.hpp"
 #include "slider-ignorewheel.hpp"
 #include "spinbox-ignorewheel.hpp"
@@ -31,6 +32,9 @@
 
 #include <cstdlib>
 #include <initializer_list>
+#include <obs-data.h>
+#include <obs.h>
+#include <qtimer.h>
 #include <string>
 
 using namespace std;
@@ -171,13 +175,14 @@ void OBSPropertiesView::GetScrollPos(int &h, int &v)
 OBSPropertiesView::OBSPropertiesView(OBSData settings_, void *obj_,
 				     PropertiesReloadCallback reloadCallback,
 				     PropertiesUpdateCallback callback_,
-				     int minSize_)
+				     PropertiesVisualUpdateCb cb_, int minSize_)
 	: VScrollArea(nullptr),
 	  properties(nullptr, obs_properties_destroy),
 	  settings(settings_),
 	  obj(obj_),
 	  reloadCallback(reloadCallback),
 	  callback(callback_),
+	  cb(cb_),
 	  minSize(minSize_)
 {
 	setFrameShape(QFrame::NoFrame);
@@ -1885,6 +1890,12 @@ void WidgetInfo::ControlChanged()
 	const char *setting = obs_property_name(property);
 	obs_property_type type = obs_property_get_type(property);
 
+	if (!recently_updated) {
+		old_settings_cache = obs_data_create();
+		obs_data_apply(old_settings_cache, view->settings);
+		obs_data_release(old_settings_cache);
+	}
+
 	switch (type) {
 	case OBS_PROPERTY_INVALID:
 		return;
@@ -1933,8 +1944,32 @@ void WidgetInfo::ControlChanged()
 		break;
 	}
 
-	if (view->callback && !view->deferUpdate)
-		view->callback(view->obj, view->settings);
+	if (!recently_updated) {
+		recently_updated = true;
+		update_timer = new QTimer;
+		connect(update_timer, &QTimer::timeout,
+			[this, &ru = recently_updated]() {
+				if (view->callback && !view->deferUpdate) {
+					view->callback(view->obj,
+						       old_settings_cache,
+						       view->settings);
+				}
+
+				ru = false;
+			});
+		connect(update_timer, &QTimer::timeout, &QTimer::deleteLater);
+		update_timer->setSingleShot(true);
+	}
+
+	if (update_timer) {
+		update_timer->stop();
+		update_timer->start(500);
+	} else {
+		blog(LOG_DEBUG, "No update timer or no callback!");
+	}
+
+	if (view->cb && !view->deferUpdate)
+		view->cb(view->obj, view->settings);
 
 	view->SignalChanged();
 

+ 23 - 3
UI/properties-view.hpp

@@ -1,7 +1,10 @@
 #pragma once
 
 #include "vertical-scroll-area.hpp"
+#include <obs-data.h>
 #include <obs.hpp>
+#include <qtimer.h>
+#include <QPointer>
 #include <vector>
 #include <memory>
 
@@ -10,7 +13,9 @@ class OBSPropertiesView;
 class QLabel;
 
 typedef obs_properties_t *(*PropertiesReloadCallback)(void *obj);
-typedef void (*PropertiesUpdateCallback)(void *obj, obs_data_t *settings);
+typedef void (*PropertiesUpdateCallback)(void *obj, obs_data_t *old_settings,
+					 obs_data_t *new_settings);
+typedef void (*PropertiesVisualUpdateCb)(void *obj, obs_data_t *settings);
 
 /* ------------------------------------------------------------------------- */
 
@@ -23,6 +28,9 @@ private:
 	OBSPropertiesView *view;
 	obs_property_t *property;
 	QWidget *widget;
+	QPointer<QTimer> update_timer;
+	bool recently_updated = false;
+	OBSData old_settings_cache;
 
 	void BoolChanged(const char *setting);
 	void IntChanged(const char *setting);
@@ -47,6 +55,15 @@ public:
 	{
 	}
 
+	~WidgetInfo()
+	{
+		if (update_timer) {
+			update_timer->stop();
+			update_timer->deleteLater();
+			obs_data_release(old_settings_cache);
+		}
+	}
+
 public slots:
 
 	void ControlChanged();
@@ -83,6 +100,7 @@ private:
 	std::string type;
 	PropertiesReloadCallback reloadCallback;
 	PropertiesUpdateCallback callback = nullptr;
+	PropertiesVisualUpdateCb cb = nullptr;
 	int minSize;
 	std::vector<std::unique_ptr<WidgetInfo>> children;
 	std::string lastFocused;
@@ -135,13 +153,15 @@ signals:
 public:
 	OBSPropertiesView(OBSData settings, void *obj,
 			  PropertiesReloadCallback reloadCallback,
-			  PropertiesUpdateCallback callback, int minSize = 0);
+			  PropertiesUpdateCallback callback,
+			  PropertiesVisualUpdateCb cb = nullptr,
+			  int minSize = 0);
 	OBSPropertiesView(OBSData settings, const char *type,
 			  PropertiesReloadCallback reloadCallback,
 			  int minSize = 0);
 
 	inline obs_data_t *GetSettings() const { return settings; }
 
-	inline void UpdateSettings() { callback(obj, settings); }
+	inline void UpdateSettings() { callback(obj, nullptr, settings); }
 	inline bool DeferUpdate() const { return deferUpdate; }
 };

+ 28 - 0
UI/source-tree.cpp

@@ -403,6 +403,34 @@ void SourceTreeItem::ExitEditMode(bool save)
 	/* rename                                    */
 
 	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());
 	label->setText(QT_UTF8(newName.c_str()));
 }

+ 101 - 0
UI/undo-stack-obs.cpp

@@ -0,0 +1,101 @@
+#include "undo-stack-obs.hpp"
+
+#include <util/util.hpp>
+
+undo_stack::undo_stack(ui_ptr ui) : ui(ui) {}
+
+void undo_stack::release()
+{
+	for (auto f : undo_items)
+		if (f.d)
+			f.d(true);
+
+	for (auto f : redo_items)
+		if (f.d)
+			f.d(false);
+}
+
+void undo_stack::add_action(const QString &name, undo_redo_cb undo,
+			    undo_redo_cb redo, std::string undo_data,
+			    std::string redo_data, func d)
+{
+	undo_redo_t n = {name, undo_data, redo_data, undo, redo, d};
+
+	undo_items.push_front(n);
+	clear_redo();
+
+	ui->actionMainUndo->setText(QTStr("Undo.Item.Undo").arg(name));
+	ui->actionMainUndo->setEnabled(true);
+
+	ui->actionMainRedo->setText(QTStr("Undo.Redo"));
+	ui->actionMainRedo->setDisabled(true);
+}
+
+void undo_stack::undo()
+{
+	if (undo_items.size() == 0 || disabled)
+		return;
+
+	undo_redo_t temp = undo_items.front();
+	temp.undo(temp.undo_data);
+	redo_items.push_front(temp);
+	undo_items.pop_front();
+
+	ui->actionMainRedo->setText(QTStr("Undo.Item.Redo").arg(temp.name));
+	ui->actionMainRedo->setEnabled(true);
+
+	if (undo_items.size() == 0) {
+		ui->actionMainUndo->setDisabled(true);
+		ui->actionMainUndo->setText(QTStr("Undo.Undo"));
+	} else {
+		ui->actionMainUndo->setText(
+			QTStr("Undo.Item.Undo").arg(undo_items.front().name));
+	}
+}
+
+void undo_stack::redo()
+{
+	if (redo_items.size() == 0 || disabled)
+		return;
+
+	undo_redo_t temp = redo_items.front();
+	temp.redo(temp.redo_data);
+	undo_items.push_front(temp);
+	redo_items.pop_front();
+
+	ui->actionMainUndo->setText(QTStr("Undo.Item.Undo").arg(temp.name));
+	ui->actionMainUndo->setEnabled(true);
+
+	if (redo_items.size() == 0) {
+		ui->actionMainRedo->setDisabled(true);
+		ui->actionMainRedo->setText(QTStr("Undo.Redo"));
+	} else {
+		ui->actionMainRedo->setText(
+			QTStr("Undo.Item.Redo").arg(redo_items.front().name));
+	}
+}
+
+void undo_stack::enable_undo_redo()
+{
+	disabled = false;
+
+	ui->actionMainUndo->setDisabled(false);
+	ui->actionMainRedo->setDisabled(false);
+}
+
+void undo_stack::disable_undo_redo()
+{
+	disabled = true;
+
+	ui->actionMainUndo->setDisabled(true);
+	ui->actionMainRedo->setDisabled(true);
+}
+
+void undo_stack::clear_redo()
+{
+	for (auto f : redo_items)
+		if (f.d)
+			f.d(false);
+
+	redo_items.clear();
+}

+ 46 - 0
UI/undo-stack-obs.hpp

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <QString>
+
+#include <deque>
+#include <functional>
+#include <string>
+#include <memory>
+
+#include "ui_OBSBasic.h"
+
+typedef std::function<void(const std::string &data)> undo_redo_cb;
+typedef std::function<void(bool is_undo)> func;
+typedef std::unique_ptr<Ui::OBSBasic> &ui_ptr;
+
+struct undo_redo_t {
+	QString name;
+	std::string undo_data;
+	std::string redo_data;
+	undo_redo_cb undo;
+	undo_redo_cb redo;
+	func d;
+};
+
+class undo_stack {
+private:
+	ui_ptr ui;
+	std::deque<undo_redo_t> undo_items;
+	std::deque<undo_redo_t> redo_items;
+	bool disabled = false;
+
+	void clear_redo();
+
+public:
+	undo_stack(ui_ptr ui);
+
+	void enable_undo_redo();
+	void disable_undo_redo();
+
+	void release();
+	void add_action(const QString &name, undo_redo_cb undo,
+			undo_redo_cb redo, std::string undo_data,
+			std::string redo_data, func d);
+	void undo();
+	void redo();
+};

+ 251 - 2
UI/window-basic-filters.cpp

@@ -15,6 +15,7 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
 
+#include "properties-view.hpp"
 #include "window-namedialog.hpp"
 #include "window-basic-main.hpp"
 #include "window-basic-filters.hpp"
@@ -23,9 +24,13 @@
 #include "visibility-item-widget.hpp"
 #include "item-widget-helpers.hpp"
 #include "obs-app.hpp"
+#include "undo-stack-obs.hpp"
 
 #include <QMessageBox>
 #include <QCloseEvent>
+#include <obs-data.h>
+#include <obs.h>
+#include <util/base.h>
 #include <vector>
 #include <string>
 #include <QMenu>
@@ -202,10 +207,82 @@ void OBSBasicFilters::UpdatePropertiesView(int row, bool async)
 
 	obs_data_t *settings = obs_source_get_settings(filter);
 
+	auto filter_change = [](void *vp, obs_data_t *nd_old_settings,
+				obs_data_t *new_settings) {
+		obs_source_t *source = reinterpret_cast<obs_source_t *>(vp);
+		obs_source_t *parent = obs_filter_get_parent(source);
+		const char *source_name = obs_source_get_name(source);
+		OBSBasic *main = OBSBasic::Get();
+
+		obs_data_t *redo_wrapper = obs_data_create();
+		obs_data_set_string(redo_wrapper, "name", source_name);
+		obs_data_set_string(redo_wrapper, "settings",
+				    obs_data_get_json(new_settings));
+		obs_data_set_string(redo_wrapper, "parent",
+				    obs_source_get_name(parent));
+
+		obs_data_t *filter_settings = obs_source_get_settings(source);
+		obs_data_t *old_settings =
+			obs_data_get_defaults(filter_settings);
+		obs_data_apply(old_settings, nd_old_settings);
+
+		obs_data_t *undo_wrapper = obs_data_create();
+		obs_data_set_string(undo_wrapper, "name", source_name);
+		obs_data_set_string(undo_wrapper, "settings",
+				    obs_data_get_json(old_settings));
+		obs_data_set_string(undo_wrapper, "parent",
+				    obs_source_get_name(parent));
+
+		auto undo_redo = [](const std::string &data) {
+			obs_data_t *dat =
+				obs_data_create_from_json(data.c_str());
+			obs_source_t *parent_source = obs_get_source_by_name(
+				obs_data_get_string(dat, "parent"));
+			const char *filter_name =
+				obs_data_get_string(dat, "name");
+			obs_source_t *filter = obs_source_get_filter_by_name(
+				parent_source, filter_name);
+			obs_data_t *settings = obs_data_create_from_json(
+				obs_data_get_string(dat, "settings"));
+
+			obs_source_update(filter, settings);
+			obs_source_update_properties(filter);
+
+			obs_data_release(dat);
+			obs_data_release(settings);
+			obs_source_release(filter);
+			obs_source_release(parent_source);
+		};
+		std::string name = std::string(obs_source_get_name(source));
+		std::string undo_data = obs_data_get_json(undo_wrapper);
+		std::string redo_data = obs_data_get_json(redo_wrapper);
+		main->undo_s.add_action(QTStr("Undo.Filters").arg(name.c_str()),
+					undo_redo, undo_redo, undo_data,
+					redo_data, NULL);
+
+		obs_data_release(redo_wrapper);
+		obs_data_release(undo_wrapper);
+		obs_data_release(old_settings);
+		obs_data_release(filter_settings);
+
+		obs_source_update(source, new_settings);
+
+		main->undo_s.enable_undo_redo();
+	};
+
+	auto disabled_undo = [](void *vp, obs_data_t *settings) {
+		OBSBasic *main =
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+		main->undo_s.disable_undo_redo();
+		obs_source_t *source = reinterpret_cast<obs_source_t *>(vp);
+		obs_source_update(source, settings);
+	};
+
 	view = new OBSPropertiesView(
 		settings, filter,
 		(PropertiesReloadCallback)obs_source_properties,
-		(PropertiesUpdateCallback)obs_source_update);
+		(PropertiesUpdateCallback)filter_change,
+		(PropertiesVisualUpdateCb)disabled_undo);
 
 	updatePropertiesSignal.Connect(obs_source_get_signal_handler(filter),
 				       "update_properties",
@@ -240,6 +317,7 @@ void OBSBasicFilters::AddFilter(OBSSource filter, bool focus)
 	list->addItem(item);
 	if (focus)
 		list->setCurrentItem(item);
+
 	SetupVisibilityItem(list, item, filter);
 }
 
@@ -486,6 +564,68 @@ void OBSBasicFilters::AddNewFilter(const char *id)
 			return;
 		}
 
+		obs_data_t *wrapper = obs_data_create();
+		obs_data_set_string(wrapper, "sname",
+				    obs_source_get_name(source));
+		obs_data_set_string(wrapper, "fname", name.c_str());
+		std::string scene_name = obs_source_get_name(
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+				->GetCurrentSceneSource());
+		auto undo = [scene_name](const std::string &data) {
+			obs_source_t *ssource =
+				obs_get_source_by_name(scene_name.c_str());
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+				->SetCurrentScene(ssource);
+			obs_source_release(ssource);
+
+			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, "sname"));
+			obs_source_t *filter = obs_source_get_filter_by_name(
+				source, obs_data_get_string(dat, "fname"));
+			obs_source_filter_remove(source, filter);
+
+			obs_data_release(dat);
+			obs_source_release(source);
+			obs_source_release(filter);
+		};
+
+		obs_data_t *rwrapper = obs_data_create();
+		obs_data_set_string(rwrapper, "sname",
+				    obs_source_get_name(source));
+		auto redo = [scene_name, id = std::string(id),
+			     name](const std::string &data) {
+			obs_source_t *ssource =
+				obs_get_source_by_name(scene_name.c_str());
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+				->SetCurrentScene(ssource);
+			obs_source_release(ssource);
+
+			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, "sname"));
+
+			obs_source_t *filter = obs_source_create(
+				id.c_str(), name.c_str(), nullptr, nullptr);
+			if (filter) {
+				obs_source_filter_add(source, filter);
+				obs_source_release(filter);
+			}
+
+			obs_data_release(dat);
+			obs_source_release(source);
+		};
+
+		std::string undo_data(obs_data_get_json(wrapper));
+		std::string redo_data(obs_data_get_json(rwrapper));
+		main->undo_s.add_action(QTStr("Undo.Add").arg(name.c_str()),
+					undo, redo, undo_data, redo_data, NULL);
+
+		obs_data_release(wrapper);
+		obs_data_release(rwrapper);
+
 		obs_source_t *filter =
 			obs_source_create(id, name.c_str(), nullptr, nullptr);
 		if (filter) {
@@ -674,8 +814,75 @@ void OBSBasicFilters::on_removeEffectFilter_clicked()
 {
 	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
 	if (filter) {
-		if (QueryRemove(this, filter))
+		if (QueryRemove(this, filter)) {
+			obs_data_t *wrapper = obs_save_source(filter);
+			std::string parent_name(obs_source_get_name(source));
+			obs_data_set_string(wrapper, "undo_name",
+					    parent_name.c_str());
+
+			std::string scene_name = obs_source_get_name(
+				reinterpret_cast<OBSBasic *>(
+					App()->GetMainWindow())
+					->GetCurrentSceneSource());
+			auto undo = [scene_name](const std::string &data) {
+				obs_source_t *ssource = obs_get_source_by_name(
+					scene_name.c_str());
+				reinterpret_cast<OBSBasic *>(
+					App()->GetMainWindow())
+					->SetCurrentScene(ssource);
+				obs_source_release(ssource);
+
+				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, "undo_name"));
+				obs_source_t *filter = obs_load_source(dat);
+				obs_source_filter_add(source, filter);
+
+				obs_data_release(dat);
+				obs_source_release(source);
+				obs_source_release(filter);
+			};
+
+			obs_data_t *rwrapper = obs_data_create();
+			obs_data_set_string(rwrapper, "fname",
+					    obs_source_get_name(filter));
+			obs_data_set_string(rwrapper, "sname",
+					    parent_name.c_str());
+			auto redo = [scene_name](const std::string &data) {
+				obs_source_t *ssource = obs_get_source_by_name(
+					scene_name.c_str());
+				reinterpret_cast<OBSBasic *>(
+					App()->GetMainWindow())
+					->SetCurrentScene(ssource);
+				obs_source_release(ssource);
+
+				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, "sname"));
+				obs_source_t *filter =
+					obs_source_get_filter_by_name(
+						source, obs_data_get_string(
+								dat, "fname"));
+				obs_source_filter_remove(source, filter);
+
+				obs_data_release(dat);
+				obs_source_release(filter);
+				obs_source_release(source);
+			};
+
+			std::string undo_data(obs_data_get_json(wrapper));
+			std::string redo_data(obs_data_get_json(rwrapper));
+			main->undo_s.add_action(
+				QTStr("Undo.Delete")
+					.arg(obs_source_get_name(filter)),
+				undo, redo, undo_data, redo_data, NULL);
 			obs_source_filter_remove(source, filter);
+
+			obs_data_release(wrapper);
+			obs_data_release(rwrapper);
+		}
 	}
 }
 
@@ -918,6 +1125,48 @@ void OBSBasicFilters::FilterNameEdited(QWidget *editor, QListWidget *list)
 
 		listItem->setText(QT_UTF8(name.c_str()));
 		obs_source_set_name(filter, name.c_str());
+
+		std::string scene_name = obs_source_get_name(
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+				->GetCurrentSceneSource());
+		auto undo = [scene_name, prev = std::string(prevName),
+			     name](const std::string &data) {
+			obs_source_t *ssource =
+				obs_get_source_by_name(scene_name.c_str());
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+				->SetCurrentScene(ssource);
+			obs_source_release(ssource);
+
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_source_t *filter = obs_source_get_filter_by_name(
+				source, name.c_str());
+			obs_source_set_name(filter, prev.c_str());
+			obs_source_release(source);
+			obs_source_release(filter);
+		};
+
+		auto redo = [scene_name, prev = std::string(prevName),
+			     name](const std::string &data) {
+			obs_source_t *ssource =
+				obs_get_source_by_name(scene_name.c_str());
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+				->SetCurrentScene(ssource);
+			obs_source_release(ssource);
+
+			obs_source_t *source =
+				obs_get_source_by_name(data.c_str());
+			obs_source_t *filter = obs_source_get_filter_by_name(
+				source, prev.c_str());
+			obs_source_set_name(filter, name.c_str());
+			obs_source_release(source);
+			obs_source_release(filter);
+		};
+
+		std::string undo_data(sourceName);
+		std::string redo_data(sourceName);
+		main->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()),
+					undo, redo, undo_data, redo_data, NULL);
 	}
 
 	listItem->setText(QString());

+ 158 - 10
UI/window-basic-main-scene-collections.cpp

@@ -151,17 +151,55 @@ bool OBSBasic::AddSceneCollection(bool create_new, const QString &qname)
 		}
 	}
 
-	SaveProjectNow();
+	auto new_collection = [this, create_new](const std::string &file,
+						 const std::string &name) {
+		SaveProjectNow();
 
-	config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection",
-			  name.c_str());
-	config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile",
-			  file.c_str());
-	if (create_new) {
-		CreateDefaultScene(false);
-	}
-	SaveProjectNow();
-	RefreshSceneCollections();
+		config_set_string(App()->GlobalConfig(), "Basic",
+				  "SceneCollection", name.c_str());
+		config_set_string(App()->GlobalConfig(), "Basic",
+				  "SceneCollectionFile", file.c_str());
+		if (create_new) {
+			CreateDefaultScene(false);
+		}
+		SaveProjectNow();
+		RefreshSceneCollections();
+	};
+
+	new_collection(file, name);
+
+	auto undo = [this, oldName = name](const std::string &data) {
+		std::string newPath;
+		auto cb = [&](const char *name, const char *filePath) {
+			if (strcmp(oldName.c_str(), name) != 0) {
+				newPath = filePath;
+				return false;
+			}
+			return true;
+		};
+
+		EnumSceneCollections(cb);
+		char path[512];
+		int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/");
+		if (ret <= 0) {
+			blog(LOG_WARNING,
+			     "Failed to get scene collection config path");
+			return;
+		}
+		std::string file = path + data + ".json";
+		os_unlink(file.c_str());
+		file += ".bak";
+		os_unlink(file.c_str());
+		Load(newPath.c_str());
+		RefreshSceneCollections();
+	};
+
+	auto redo = [new_collection, file, name](const std::string &) {
+		new_collection(file, name);
+	};
+
+	undo_s.add_action(QTStr("Undo.Add").arg(name.c_str()), undo, redo, file,
+			  "", NULL);
 
 	blog(LOG_INFO, "Added scene collection '%s' (%s, %s.json)",
 	     name.c_str(), create_new ? "clean" : "duplicate", file.c_str());
@@ -245,11 +283,13 @@ void OBSBasic::on_actionRenameSceneCollection_triggered()
 {
 	std::string name;
 	std::string file;
+	std::string oname;
 
 	std::string oldFile = config_get_string(App()->GlobalConfig(), "Basic",
 						"SceneCollectionFile");
 	const char *oldName = config_get_string(App()->GlobalConfig(), "Basic",
 						"SceneCollection");
+	oname = std::string(oldName);
 
 	bool success = GetSceneCollectionName(this, name, file, oldName);
 	if (!success)
@@ -268,6 +308,59 @@ void OBSBasic::on_actionRenameSceneCollection_triggered()
 		return;
 	}
 
+	auto undo = [name = oname, file = oldFile, of = file,
+		     this](const std::string &) {
+		config_set_string(App()->GlobalConfig(), "Basic",
+				  "SceneCollection", name.c_str());
+		config_set_string(App()->GlobalConfig(), "Basic",
+				  "SceneCollectionFile", file.c_str());
+		SaveProjectNow();
+
+		char path[512];
+		int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/");
+		if (ret <= 0) {
+			blog(LOG_WARNING,
+			     "Failed to get scene collection config path");
+			return;
+		}
+		std::string oldFile = of;
+
+		oldFile.insert(0, path);
+		oldFile += ".json";
+		os_unlink(oldFile.c_str());
+		oldFile += ".bak";
+		os_unlink(oldFile.c_str());
+
+		UpdateTitleBar();
+		RefreshSceneCollections();
+	};
+
+	auto redo = [of = oldFile, name, file, this](const std::string &) {
+		config_set_string(App()->GlobalConfig(), "Basic",
+				  "SceneCollection", name.c_str());
+		config_set_string(App()->GlobalConfig(), "Basic",
+				  "SceneCollectionFile", file.c_str());
+		SaveProjectNow();
+
+		char path[512];
+		int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/");
+		if (ret <= 0) {
+			blog(LOG_WARNING,
+			     "Failed to get scene collection config path");
+			return;
+		}
+		std::string oldFile = of;
+
+		oldFile.insert(0, path);
+		oldFile += ".json";
+		os_unlink(oldFile.c_str());
+		oldFile += ".bak";
+		os_unlink(oldFile.c_str());
+
+		UpdateTitleBar();
+		RefreshSceneCollections();
+	};
+
 	oldFile.insert(0, path);
 	oldFile += ".json";
 	os_unlink(oldFile.c_str());
@@ -282,6 +375,9 @@ void OBSBasic::on_actionRenameSceneCollection_triggered()
 	UpdateTitleBar();
 	RefreshSceneCollections();
 
+	undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo,
+			  "", "", NULL);
+
 	if (api) {
 		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED);
 		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
@@ -331,6 +427,32 @@ void OBSBasic::on_actionRemoveSceneCollection_triggered()
 
 	oldFile.insert(0, path);
 	oldFile += ".json";
+	obs_data_t *data =
+		obs_data_create_from_json_file_safe(oldFile.c_str(), "bak");
+	obs_data_set_string(data, "undo_filename", oldFile.c_str());
+	auto undo = [this](const std::string &data) {
+		obs_data_t *dat = obs_data_create_from_json(data.c_str());
+		LoadData(dat, obs_data_get_string(dat, "undo_filename"));
+		SaveProjectNow();
+		RefreshSceneCollections();
+		obs_data_release(dat);
+	};
+
+	auto redo = [this, of = oldFile, newPath](const std::string &) {
+		std::string oldFile = of;
+		os_unlink(oldFile.c_str());
+		oldFile += ".bak";
+		os_unlink(oldFile.c_str());
+
+		Load(newPath.c_str());
+		RefreshSceneCollections();
+	};
+
+	std::string undo_data = std::string(obs_data_get_json(data));
+	undo_s.add_action(QTStr("Undo.Delete").arg(oldName.c_str()), undo, redo,
+			  undo_data, "", NULL);
+	obs_data_release(data);
+
 	os_unlink(oldFile.c_str());
 	oldFile += ".bak";
 	os_unlink(oldFile.c_str());
@@ -410,6 +532,9 @@ void OBSBasic::ChangeSceneCollection()
 
 	const char *oldName = config_get_string(App()->GlobalConfig(), "Basic",
 						"SceneCollection");
+	std::string oldFile = std::string(config_get_string(
+		App()->GlobalConfig(), "Basic", "SceneCollectionFile"));
+
 	if (action->text().compare(QT_UTF8(oldName)) == 0) {
 		action->setChecked(true);
 		return;
@@ -431,6 +556,29 @@ void OBSBasic::ChangeSceneCollection()
 
 	UpdateTitleBar();
 
+	auto undo = [this, fn = std::string(oldFile)](const std::string &) {
+		string fileName = fn;
+		char path[512];
+		int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/");
+		if (ret <= 0) {
+			blog(LOG_WARNING,
+			     "Failed to get scene collection config path");
+			return;
+		}
+		fileName.insert(0, path);
+		fileName += ".json";
+		Load(fileName.c_str());
+		RefreshSceneCollections();
+	};
+
+	auto redo = [this, fileName](const std::string &) {
+		Load(fileName.c_str());
+		RefreshSceneCollections();
+	};
+
+	undo_s.add_action(QTStr("Undo.SceneCollection.Switch").arg(newName),
+			  undo, redo, "", "", NULL);
+
 	if (api)
 		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
 }

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

@@ -17,7 +17,10 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
 
+#include <cstddef>
 #include <ctime>
+#include <obs-data.h>
+#include <obs.h>
 #include <obs.hpp>
 #include <QGuiApplication>
 #include <QMessageBox>
@@ -58,6 +61,7 @@
 #include "remote-text.hpp"
 #include "ui-validation.hpp"
 #include "media-controls.hpp"
+#include "undo-stack-obs.hpp"
 #include <fstream>
 #include <sstream>
 
@@ -203,7 +207,7 @@ extern void RegisterTwitchAuth();
 extern void RegisterRestreamAuth();
 
 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)
 	qRegisterMetaTypeStreamOperators<SignalContainer<OBSScene>>(
@@ -366,6 +370,16 @@ OBSBasic::OBSBasic(QWidget *parent)
 	assignDockToggle(ui->controlsDock, ui->toggleControls);
 	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
 	ui->toggleScenes->setChecked(false);
 	ui->toggleSources->setChecked(false);
@@ -922,6 +936,11 @@ void OBSBasic::Load(const char *file)
 		return;
 	}
 
+	LoadData(data, file);
+}
+
+void OBSBasic::LoadData(obs_data_t *data, const char *file)
+{
 	ClearSceneData();
 	InitDefaultTransitions();
 	ClearContextBar();
@@ -3622,6 +3641,33 @@ void OBSBasic::DuplicateSelectedScene()
 							 OBS_SCENE_DUP_REFS);
 		source = obs_scene_get_source(scene);
 		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);
 
 		break;
@@ -3631,15 +3677,107 @@ void OBSBasic::DuplicateSelectedScene()
 void OBSBasic::RemoveSelectedScene()
 {
 	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_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 +4468,7 @@ void OBSBasic::closeEvent(QCloseEvent *event)
 
 	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
 	 * sources, etc) so that all references are released before shutdown */
+	undo_s.release();
 	ClearSceneData();
 
 	App()->quit();
@@ -4435,6 +4574,47 @@ void OBSBasic::on_action_Settings_triggered()
 	}
 }
 
+void save_audio_source(int channel, obs_data_t *save)
+{
+	obs_source_t *source = obs_get_output_source(channel);
+	if (!source)
+		return;
+
+	obs_data_t *obj = obs_data_create();
+
+	obs_data_set_double(obj, "vol", obs_source_get_volume(source));
+	obs_data_set_double(obj, "balance",
+			    obs_source_get_balance_value(source));
+	obs_data_set_double(obj, "mixers", obs_source_get_audio_mixers(source));
+	obs_data_set_double(obj, "sync", obs_source_get_sync_offset(source));
+	obs_data_set_double(obj, "flags", obs_source_get_flags(source));
+
+	obs_data_set_obj(save, std::to_string(channel).c_str(), obj);
+	obs_data_release(obj);
+	obs_source_release(source);
+}
+
+void load_audio_source(int channel, obs_data_t *data)
+{
+	obs_source_t *source = obs_get_output_source(channel);
+	if (!source)
+		return;
+
+	obs_data_t *save =
+		obs_data_get_obj(data, std::to_string(channel).c_str());
+
+	obs_source_set_volume(source, obs_data_get_double(save, "vol"));
+	obs_source_set_balance_value(source,
+				     obs_data_get_double(save, "balance"));
+	obs_source_set_audio_mixers(source,
+				    obs_data_get_double(save, "mixers"));
+	obs_source_set_sync_offset(source, obs_data_get_double(save, "sync"));
+	obs_source_set_flags(source, obs_data_get_double(save, "flags"));
+
+	obs_data_release(save);
+	obs_source_release(source);
+}
+
 void OBSBasic::on_actionAdvAudioProperties_triggered()
 {
 	if (advAudioWindow != nullptr) {
@@ -4452,6 +4632,53 @@ void OBSBasic::on_actionAdvAudioProperties_triggered()
 
 	connect(advAudioWindow, SIGNAL(destroyed()), this,
 		SLOT(AdvAudioPropsDestroyed()));
+
+	obs_data_t *wrapper = obs_data_create();
+
+	save_audio_source(1, wrapper);
+	save_audio_source(2, wrapper);
+	save_audio_source(3, wrapper);
+	save_audio_source(4, wrapper);
+	save_audio_source(5, wrapper);
+	save_audio_source(6, wrapper);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+
+	connect(advAudioWindow, &QDialog::finished, [this, undo_data]() {
+		auto undo_redo = [](const std::string &data) {
+			obs_data_t *audio_data =
+				obs_data_create_from_json(data.c_str());
+
+			load_audio_source(1, audio_data);
+			load_audio_source(2, audio_data);
+			load_audio_source(3, audio_data);
+			load_audio_source(4, audio_data);
+			load_audio_source(5, audio_data);
+			load_audio_source(6, audio_data);
+
+			obs_data_release(audio_data);
+		};
+
+		obs_data_t *wrapper = obs_data_create();
+
+		save_audio_source(1, wrapper);
+		save_audio_source(2, wrapper);
+		save_audio_source(3, wrapper);
+		save_audio_source(4, wrapper);
+		save_audio_source(5, wrapper);
+		save_audio_source(6, wrapper);
+
+		std::string redo_data(obs_data_get_json(wrapper));
+
+		if (undo_data.compare(redo_data) != 0)
+			undo_s.add_action(QTStr("Undo.Audio"), undo_redo,
+					  undo_redo, undo_data, redo_data,
+					  NULL);
+
+		obs_data_release(wrapper);
+	});
+
+	obs_data_release(wrapper);
 }
 
 void OBSBasic::AdvAudioPropsClicked()
@@ -4686,20 +4913,34 @@ void OBSBasic::on_actionAddScene_triggered()
 			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());
 		source = obs_scene_get_source(scene);
 		SetCurrentScene(source);
+		RefreshSources(scene);
 		obs_scene_release(scene);
 	}
 }
 
 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)
@@ -5114,10 +5355,11 @@ void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem)
 void OBSBasic::AddSource(const char *id)
 {
 	if (id && *id) {
-		OBSBasicSourceSelect sourceSelect(this, id);
+		OBSBasicSourceSelect sourceSelect(this, id, undo_s);
 		sourceSelect.exec();
-		if (sourceSelect.newSource && strcmp(id, "group") != 0)
+		if (sourceSelect.newSource && strcmp(id, "group") != 0) {
 			CreatePropertiesWindow(sourceSelect.newSource);
+		}
 	}
 }
 
@@ -5258,9 +5500,11 @@ void OBSBasic::on_actionRemoveSource_triggered()
 	if (!items.size())
 		return;
 
-	auto removeMultiple = [this](size_t count) {
+	bool confirmed = false;
+
+	if (items.size() > 1) {
 		QString text = QTStr("ConfirmRemove.TextMultiple")
-				       .arg(QString::number(count));
+				       .arg(QString::number(items.size()));
 
 		QMessageBox remove_items(this);
 		remove_items.setText(text);
@@ -5272,21 +5516,139 @@ void OBSBasic::on_actionRemoveSource_triggered()
 		remove_items.setWindowTitle(QTStr("ConfirmRemove.Title"));
 		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_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);
-	} 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()
@@ -5524,6 +5886,27 @@ static void RenameListItem(OBSBasic *parent, QListWidget *listWidget,
 
 		obs_source_release(foundSource);
 	} 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()));
 		obs_source_set_name(source, name.c_str());
 	}
@@ -6739,8 +7122,23 @@ void OBSBasic::on_actionCopyTransform_triggered()
 	ui->actionPasteTransform->setEnabled(true);
 }
 
+void undo_redo(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, "scene_name"));
+	reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+		->SetCurrentScene(source);
+	obs_source_release(source);
+	obs_data_release(dat);
+
+	obs_scene_load_transform_states(data.c_str());
+}
+
 void OBSBasic::on_actionPasteTransform_triggered()
 {
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) {
 		if (!obs_sceneitem_selected(item))
 			return true;
@@ -6756,6 +7154,19 @@ void OBSBasic::on_actionPasteTransform_triggered()
 	};
 
 	obs_scene_enum_items(GetCurrentScene(), func, nullptr);
+
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(
+		QTStr("Undo.Transform.Paste")
+			.arg(obs_source_get_name(GetCurrentSceneSource())),
+		undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
@@ -6791,6 +7202,22 @@ static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
 
 void OBSBasic::on_actionResetTransform_triggered()
 {
+	obs_scene_t *scene = GetCurrentScene();
+
+	obs_data_t *wrapper = obs_scene_save_transform_states(scene, false);
+	obs_scene_enum_items(scene, reset_tr, nullptr);
+	obs_data_t *rwrapper = obs_scene_save_transform_states(scene, false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(
+		QTStr("Undo.Transform.Reset")
+			.arg(obs_source_get_name(obs_scene_get_source(scene))),
+		undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
+
 	obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr);
 }
 
@@ -6868,19 +7295,61 @@ static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item,
 void OBSBasic::on_actionRotate90CW_triggered()
 {
 	float f90CW = 90.0f;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Rotate")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionRotate90CCW_triggered()
 {
 	float f90CCW = -90.0f;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Rotate")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionRotate180_triggered()
 {
 	float f180 = 180.0f;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Rotate")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item,
@@ -6915,16 +7384,44 @@ void OBSBasic::on_actionFlipHorizontal_triggered()
 {
 	vec2 scale;
 	vec2_set(&scale, -1.0f, 1.0f);
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
 			     &scale);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.HFlip")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionFlipVertical_triggered()
 {
 	vec2 scale;
 	vec2_set(&scale, 1.0f, -1.0f);
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
 			     &scale);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.VFlip")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item,
@@ -6964,15 +7461,43 @@ static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item,
 void OBSBasic::on_actionFitToScreen_triggered()
 {
 	obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
 			     &boundsType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.FitToScreen")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionStretchToScreen_triggered()
 {
 	obs_bounds_type boundsType = OBS_BOUNDS_STRETCH;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
 			     &boundsType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.StretchToScreen")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 enum class CenterType {
@@ -7033,19 +7558,61 @@ static bool center_to_scene(obs_scene_t *, obs_sceneitem_t *item, void *param)
 void OBSBasic::on_actionCenterToScreen_triggered()
 {
 	CenterType centerType = CenterType::Scene;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Center")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionVerticalCenter_triggered()
 {
 	CenterType centerType = CenterType::Vertical;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.VCenter")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionHorizontalCenter_triggered()
 {
 	CenterType centerType = CenterType::Horizontal;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.VCenter")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::EnablePreviewDisplay(bool enable)
@@ -7136,6 +7703,46 @@ void OBSBasic::Nudge(int dist, MoveDir dir)
 		break;
 	}
 
+	if (!recent_nudge) {
+		recent_nudge = true;
+		obs_data_t *wrapper = obs_scene_save_transform_states(
+			GetCurrentScene(), true);
+		std::string undo_data(obs_data_get_json(wrapper));
+
+		nudge_timer = new QTimer;
+		QObject::connect(
+			nudge_timer, &QTimer::timeout,
+			[this, &recent_nudge = recent_nudge, undo_data]() {
+				obs_data_t *rwrapper =
+					obs_scene_save_transform_states(
+						GetCurrentScene(), true);
+				std::string redo_data(
+					obs_data_get_json(rwrapper));
+
+				undo_s.add_action(
+					QTStr("Undo.Transform")
+						.arg(obs_source_get_name(
+							GetCurrentSceneSource())),
+					undo_redo, undo_redo, undo_data,
+					redo_data, NULL);
+
+				recent_nudge = false;
+				obs_data_release(rwrapper);
+			});
+		connect(nudge_timer, &QTimer::timeout, nudge_timer,
+			&QTimer::deleteLater);
+		nudge_timer->setSingleShot(true);
+
+		obs_data_release(wrapper);
+	}
+
+	if (nudge_timer) {
+		nudge_timer->stop();
+		nudge_timer->start(1000);
+	} else {
+		blog(LOG_ERROR, "No nudge timer!");
+	}
+
 	obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset);
 }
 
@@ -7767,6 +8374,16 @@ bool OBSBasic::sysTrayMinimizeToTray()
 			       "SysTrayMinimizeToTray");
 }
 
+void OBSBasic::on_actionMainUndo_triggered()
+{
+	undo_s.undo();
+}
+
+void OBSBasic::on_actionMainRedo_triggered()
+{
+	undo_s.redo();
+}
+
 void OBSBasic::on_actionCopySource_triggered()
 {
 	copyStrings.clear();

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

@@ -36,6 +36,7 @@
 #include "window-basic-about.hpp"
 #include "auth-base.hpp"
 #include "log-viewer.hpp"
+#include "undo-stack-obs.hpp"
 
 #include <obs-frontend-internal.hpp>
 
@@ -156,6 +157,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class OBSBasicPreview;
 	friend class OBSBasicStatusBar;
 	friend class OBSBasicSourceSelect;
+	friend class OBSBasicTransform;
 	friend class OBSBasicSettings;
 	friend class Auth;
 	friend class AutoConfig;
@@ -166,6 +168,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class ExtraBrowsersDelegate;
 	friend class DeviceCaptureToolbar;
 	friend class DeviceToolbarPropertiesThread;
+	friend class OBSBasicSourceSelect;
 	friend struct BasicOutputHandler;
 	friend struct OBSStudioAPI;
 
@@ -220,6 +223,9 @@ private:
 	QPointer<QTimer> cpuUsageTimer;
 	QPointer<QTimer> diskFullTimer;
 
+	QPointer<QTimer> nudge_timer;
+	bool recent_nudge = false;
+
 	os_cpu_usage_info_t *cpuUsageInfo = nullptr;
 
 	OBSService service;
@@ -308,6 +314,7 @@ private:
 	void UploadLog(const char *subdir, const char *file, const bool crash);
 
 	void Save(const char *file);
+	void LoadData(obs_data_t *data, const char *file);
 	void Load(const char *file);
 
 	void InitHotkeys();
@@ -609,6 +616,10 @@ public slots:
 	void UnpauseRecording();
 
 private slots:
+
+	void on_actionMainUndo_triggered();
+	void on_actionMainRedo_triggered();
+
 	void AddSceneItem(OBSSceneItem item);
 	void AddScene(OBSSource source);
 	void RemoveScene(OBSSource source);
@@ -741,6 +752,7 @@ private:
 	OBSSource prevFTBSource = nullptr;
 
 public:
+	undo_stack undo_s;
 	OBSSource GetProgramSource();
 	OBSScene GetCurrentScene();
 

+ 45 - 0
UI/window-basic-preview.cpp

@@ -36,6 +36,9 @@ OBSBasicPreview::~OBSBasicPreview()
 		gs_vertexbuffer_destroy(rectFill);
 
 	obs_leave_graphics();
+
+	if (wrapper)
+		obs_data_release(wrapper);
 }
 
 vec2 OBSBasicPreview::GetMouseEventPos(QMouseEvent *event)
@@ -581,6 +584,11 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
 	vec2_zero(&lastMoveOffset);
 
 	mousePos = startPos;
+	if (wrapper)
+		obs_data_release(wrapper);
+	wrapper =
+		obs_scene_save_transform_states(main->GetCurrentScene(), true);
+	changed = false;
 }
 
 void OBSBasicPreview::UpdateCursor(uint32_t &flags)
@@ -713,6 +721,41 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
 		hoveredPreviewItems.push_back(item);
 		selectedItems.clear();
 	}
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(main->GetCurrentScene(), false);
+
+	auto undo_redo = [](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, "scene_name"));
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+			->SetCurrentScene(source);
+		obs_source_release(source);
+		obs_data_release(dat);
+
+		obs_scene_load_transform_states(data.c_str());
+	};
+
+	if (wrapper && rwrapper) {
+		std::string undo_data(obs_data_get_json(wrapper));
+		std::string redo_data(obs_data_get_json(rwrapper));
+		if (changed && undo_data.compare(redo_data) != 0)
+			main->undo_s.add_action(
+				QTStr("Undo.Transform")
+					.arg(obs_source_get_name(
+						main->GetCurrentSceneSource())),
+				undo_redo, undo_redo, undo_data, redo_data,
+				NULL);
+	}
+
+	if (wrapper)
+		obs_data_release(wrapper);
+
+	if (rwrapper)
+		obs_data_release(rwrapper);
+
+	wrapper = NULL;
 }
 
 struct SelectedItemBounds {
@@ -1434,6 +1477,8 @@ void OBSBasicPreview::StretchItem(const vec2 &pos)
 
 void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
 {
+	changed = true;
+
 	if (scrollMode && event->buttons() == Qt::LeftButton) {
 		scrollingOffset.x += event->x() - scrollingFrom.x;
 		scrollingOffset.y += event->y() - scrollingFrom.y;

+ 3 - 0
UI/window-basic-preview.hpp

@@ -106,6 +106,9 @@ private:
 
 	void ProcessClick(const vec2 &pos);
 
+	obs_data_t *wrapper = NULL;
+	bool changed;
+
 public:
 	OBSBasicPreview(QWidget *parent,
 			Qt::WindowFlags flags = Qt::WindowFlags());

+ 66 - 3
UI/window-basic-properties.cpp

@@ -26,6 +26,10 @@
 #include <QScreen>
 #include <QWindow>
 #include <QMessageBox>
+#include <obs-data.h>
+#include <obs.h>
+#include <qpointer.h>
+#include <util/c99defs.h>
 
 using namespace std;
 
@@ -74,14 +78,28 @@ OBSBasicProperties::OBSBasicProperties(QWidget *parent, OBSSource source_)
 	/* The OBSData constructor increments the reference once */
 	obs_data_release(oldSettings);
 
-	OBSData settings = obs_source_get_settings(source);
+	OBSData nd_settings = obs_source_get_settings(source);
+	OBSData settings = obs_data_get_defaults(nd_settings);
+	obs_data_apply(settings, nd_settings);
 	obs_data_apply(oldSettings, settings);
 	obs_data_release(settings);
+	obs_data_release(nd_settings);
+
+	auto handle_memory = [](void *vp, obs_data_t *old_settings,
+				obs_data_t *new_settings) {
+		obs_source_t *source = reinterpret_cast<obs_source_t *>(vp);
+
+		obs_source_update(source, new_settings);
+
+		UNUSED_PARAMETER(old_settings);
+		UNUSED_PARAMETER(vp);
+	};
 
 	view = new OBSPropertiesView(
-		settings, source,
+		nd_settings, source,
 		(PropertiesReloadCallback)obs_source_properties,
-		(PropertiesUpdateCallback)obs_source_update);
+		(PropertiesUpdateCallback)handle_memory,
+		(PropertiesVisualUpdateCb)obs_source_update);
 	view->setMinimumHeight(150);
 
 	preview->setMinimumSize(20, 150);
@@ -341,6 +359,51 @@ void OBSBasicProperties::on_buttonBox_clicked(QAbstractButton *button)
 	QDialogButtonBox::ButtonRole val = buttonBox->buttonRole(button);
 
 	if (val == QDialogButtonBox::AcceptRole) {
+
+		std::string scene_name =
+			obs_source_get_name(main->GetCurrentSceneSource());
+
+		auto undo_redo = [scene_name](const std::string &data) {
+			obs_data_t *settings =
+				obs_data_create_from_json(data.c_str());
+			obs_source_t *source = obs_get_source_by_name(
+				obs_data_get_string(settings, "undo_sname"));
+			obs_source_update(source, settings);
+
+			obs_source_update_properties(source);
+
+			obs_source_t *scene_source =
+				obs_get_source_by_name(scene_name.c_str());
+
+			OBSBasic::Get()->SetCurrentScene(source);
+
+			obs_source_release(scene_source);
+
+			obs_data_release(settings);
+			obs_source_release(source);
+		};
+
+		obs_data_t *new_settings = obs_data_create();
+		obs_data_t *curr_settings = obs_source_get_settings(source);
+		obs_data_apply(new_settings, curr_settings);
+		obs_data_set_string(new_settings, "undo_sname",
+				    obs_source_get_name(source));
+		obs_data_set_string(oldSettings, "undo_sname",
+				    obs_source_get_name(source));
+
+		std::string undo_data(obs_data_get_json(oldSettings));
+		std::string redo_data(obs_data_get_json(new_settings));
+
+		if (undo_data.compare(redo_data) != 0)
+			main->undo_s.add_action(
+				QTStr("Undo.Properties")
+					.arg(obs_source_get_name(source)),
+				undo_redo, undo_redo, undo_data, redo_data,
+				NULL);
+
+		obs_data_release(new_settings);
+		obs_data_release(curr_settings);
+
 		acceptClicked = true;
 		close();
 

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

@@ -28,6 +28,9 @@ struct AddSourceData {
 
 bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source)
 {
+	if (obs_source_is_hidden(source))
+		return false;
+
 	OBSBasicSourceSelect *window =
 		static_cast<OBSBasicSourceSelect *>(data);
 	const char *name = obs_source_get_name(source);
@@ -179,7 +182,7 @@ bool AddNew(QWidget *parent, const char *id, const char *name,
 		return false;
 
 	obs_source_t *source = obs_get_source_by_name(name);
-	if (source) {
+	if (source && parent) {
 		OBSMessageBox::information(parent, QTStr("NameExists.Title"),
 					   QTStr("NameExists.Text"));
 
@@ -236,6 +239,63 @@ void OBSBasicSourceSelect::on_buttonBox_accepted()
 		if (!AddNew(this, id, QT_TO_UTF8(ui->sourceName->text()),
 			    visible, newSource))
 			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);
@@ -261,8 +321,12 @@ template<typename T> static inline T GetOBSRef(QListWidgetItem *item)
 	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);
 

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

@@ -21,6 +21,7 @@
 #include <memory>
 
 #include "ui_OBSBasicSourceSelect.h"
+#include "undo-stack-obs.hpp"
 
 class OBSBasic;
 
@@ -30,6 +31,7 @@ class OBSBasicSourceSelect : public QDialog {
 private:
 	std::unique_ptr<Ui::OBSBasicSourceSelect> ui;
 	const char *id;
+	undo_stack &undo_s;
 
 	static bool EnumSources(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);
 
 public:
-	OBSBasicSourceSelect(OBSBasic *parent, const char *id);
+	OBSBasicSourceSelect(OBSBasic *parent, const char *id,
+			     undo_stack &undo_s);
 
 	OBSSource newSource;
 

+ 32 - 0
UI/window-basic-transform.cpp

@@ -73,10 +73,42 @@ OBSBasicTransform::OBSBasicTransform(OBSBasic *parent)
 	SetScene(scene);
 	SetItem(item);
 
+	obs_data_t *wrapper = obs_scene_save_transform_states(scene, false);
+	undo_data = std::string(obs_data_get_json(wrapper));
+
+	obs_data_release(wrapper);
+
 	channelChangedSignal.Connect(obs_get_signal_handler(), "channel_change",
 				     OBSChannelChanged, this);
 }
 
+OBSBasicTransform::~OBSBasicTransform()
+{
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(main->GetCurrentScene(), false);
+
+	auto undo_redo = [](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, "scene_name"));
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+			->SetCurrentScene(source);
+		obs_source_release(source);
+		obs_data_release(dat);
+		obs_scene_load_transform_states(data.c_str());
+	};
+
+	std::string redo_data(obs_data_get_json(wrapper));
+	if (undo_data.compare(redo_data) != 0)
+		main->undo_s.add_action(
+			QTStr("Undo.Transform")
+				.arg(obs_source_get_name(obs_scene_get_source(
+					main->GetCurrentScene()))),
+			undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+}
+
 void OBSBasicTransform::SetScene(OBSScene scene)
 {
 	transformSignal.Disconnect();

+ 3 - 0
UI/window-basic-transform.hpp

@@ -21,6 +21,8 @@ private:
 	OBSSignal selectSignal;
 	OBSSignal deselectSignal;
 
+	std::string undo_data;
+
 	bool ignoreTransformSignal = false;
 	bool ignoreItemChange = false;
 
@@ -46,4 +48,5 @@ private slots:
 
 public:
 	OBSBasicTransform(OBSBasic *parent);
+	~OBSBasicTransform();
 };

+ 36 - 2
docs/sphinx/reference-scenes.rst

@@ -298,9 +298,37 @@ Scene Item Functions
 
 ---------------------
 
+.. function:: obs_sceneitem_t *obs_scene_sceneitem_from_source(obs_scene_t *scene, obs_source_t *source)
+
+   This will add a reference to the sceneitem.
+
+   :return: The sceneitem associated with a source in a scene. Returns NULL if not found.
+
+---------------------
+
+.. function:: void obs_sceneitem_set_id(obs_sceneitem_t *item);
+
+   Sets the unique numeric identifier of the sceneitem. This is dangerous function and should not
+   normally be used. It can cause errors within obs.
+
+---------------------
+
 .. function:: int64_t obs_sceneitem_get_id(const obs_sceneitem_t *item)
 
-   :return: The unique numeric identifier of the scene item.
+   This is a dangerous function and should not
+   normally be used. It can cause errors within obs.
+
+   :return: Gets the unique numeric identifier of the scene item.
+
+---------------------
+
+.. function:: obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene, bool all_items)
+.. function:: void obs_scene_load_transform_states(oconst char *states)
+
+   Saves all the transformation states for the sceneitms in scene. When all_items is false, it
+   will only save selected items
+
+   :return: Data containing transformation states for all* sceneitems in scene
 
 ---------------------
 
@@ -354,7 +382,13 @@ Scene Item Functions
 
 .. function:: void obs_sceneitem_set_order_position(obs_sceneitem_t *item, int position)
 
-   Changes the scene item's order index.
+   Changes the sceneitem's order index.
+
+---------------------
+
+.. function:: int obs_sceneitem_get_order_position(obs_sceneitem_t *item)
+
+   :return: Gets position of sceneitem in its scene.
 
 ---------------------
 

+ 10 - 1
docs/sphinx/reference-settings.rst

@@ -182,6 +182,12 @@ Default Value Functions
 Default values are used to determine what value will be given if a value
 is not set.
 
+.. function:: obs_data_t *obs_data_get_defaults(obs_data_t *data);
+
+   :return: obs_data_t * with all default values (recursively for all objects as well).
+
+-----------------------
+
 .. function:: void obs_data_set_default_string(obs_data_t *data, const char *name, const char *val)
               const char *obs_data_get_default_string(obs_data_t *data, const char *name)
 
@@ -207,7 +213,10 @@ is not set.
 
    :return: An incremented reference to a data object.
 
----------------------
+----------------------
+
+.. function:: void obs_data_set_default_array(obs_data_t *data, const char *name, obs_data_array_t *arr)
+              obs_data_array_t *obs_data_get_default_array(obs_data_t *data, const char *name)
 
 
 Autoselect Functions

+ 13 - 0
docs/sphinx/reference-sources.rst

@@ -753,6 +753,19 @@ General Source Functions
 
 ---------------------
 
+.. function:: void obs_source_set_hidden(obs_source_t *source, bool hidden)
+
+   Sets the hidden flag that determines whether it should be hidden from the user.
+   Used when the source is still alive but should not be referenced.
+
+---------------------
+
+.. function:: bool obs_source_is_hidden(obs_source_t *source)
+
+  :return: *true* if source's 'hidden' is set true
+
+---------------------
+
 .. function:: uint32_t obs_source_get_output_flags(const obs_source_t *source)
               uint32_t obs_get_source_output_flags(const char *id)
 

+ 110 - 0
libobs/obs-data.c

@@ -767,6 +767,97 @@ bool obs_data_save_json_safe(obs_data_t *data, const char *file,
 	return false;
 }
 
+static void get_defaults_array_cb(obs_data_t *data, void *vp)
+{
+	obs_data_array_t *defs = (obs_data_array_t *)vp;
+	obs_data_t *obs_defaults = obs_data_get_defaults(data);
+
+	obs_data_array_push_back(defs, obs_defaults);
+
+	obs_data_release(obs_defaults);
+}
+
+obs_data_t *obs_data_get_defaults(obs_data_t *data)
+{
+	obs_data_t *defaults = obs_data_create();
+
+	if (!data)
+		return defaults;
+
+	struct obs_data_item *item = data->first_item;
+
+	while (item) {
+		const char *name = get_item_name(item);
+		switch (item->type) {
+		case OBS_DATA_NULL:
+			break;
+
+		case OBS_DATA_STRING: {
+			const char *str =
+				obs_data_get_default_string(data, name);
+			obs_data_set_string(defaults, name, str);
+			break;
+		}
+
+		case OBS_DATA_NUMBER: {
+			switch (obs_data_item_numtype(item)) {
+			case OBS_DATA_NUM_DOUBLE: {
+				double val =
+					obs_data_get_default_double(data, name);
+				obs_data_set_double(defaults, name, val);
+				break;
+			}
+
+			case OBS_DATA_NUM_INT: {
+				int val = obs_data_get_default_int(data, name);
+				obs_data_set_int(defaults, name, val);
+				break;
+			}
+
+			case OBS_DATA_NUM_INVALID:
+				break;
+			}
+			break;
+		}
+
+		case OBS_DATA_BOOLEAN: {
+			bool val = obs_data_get_default_bool(data, name);
+			obs_data_set_bool(defaults, name, val);
+			break;
+		}
+
+		case OBS_DATA_OBJECT: {
+			obs_data_t *val = obs_data_get_default_obj(data, name);
+			obs_data_t *defs = obs_data_get_defaults(val);
+
+			obs_data_set_obj(defaults, name, defs);
+
+			obs_data_release(defs);
+			obs_data_release(val);
+			break;
+		}
+
+		case OBS_DATA_ARRAY: {
+			obs_data_array_t *arr =
+				obs_data_get_default_array(data, name);
+			obs_data_array_t *defs = obs_data_array_create();
+
+			obs_data_array_enum(arr, get_defaults_array_cb,
+					    (void *)defs);
+			obs_data_set_array(defaults, name, defs);
+
+			obs_data_array_release(defs);
+			obs_data_array_release(arr);
+			break;
+		}
+		}
+
+		item = item->next;
+	}
+
+	return defaults;
+}
+
 static struct obs_data_item *get_item(struct obs_data *data, const char *name)
 {
 	if (!data)
@@ -1134,6 +1225,12 @@ void obs_data_set_default_obj(obs_data_t *data, const char *name,
 	obs_set_obj(data, NULL, name, obj, set_item_def);
 }
 
+void obs_data_set_default_array(obs_data_t *data, const char *name,
+				obs_data_array_t *arr)
+{
+	obs_set_array(data, NULL, name, arr, set_item_def);
+}
+
 void obs_data_set_autoselect_string(obs_data_t *data, const char *name,
 				    const char *val)
 {
@@ -1351,6 +1448,16 @@ void obs_data_array_erase(obs_data_array_t *array, size_t idx)
 	}
 }
 
+void obs_data_array_enum(obs_data_array_t *array,
+			 void (*cb)(obs_data_t *data, void *param), void *param)
+{
+	if (array && cb) {
+		for (size_t i = 0; i < array->objects.num; i++) {
+			cb(array->objects.array[i], param);
+		}
+	}
+}
+
 /* ------------------------------------------------------------------------- */
 /* Item status inspection */
 
@@ -1517,6 +1624,9 @@ enum obs_data_number_type obs_data_item_numtype(obs_data_item_t *item)
 		return OBS_DATA_NUM_INVALID;
 
 	num = get_item_data(item);
+	if (!num)
+		return OBS_DATA_NUM_INVALID;
+
 	return num->type;
 }
 

+ 10 - 0
libobs/obs-data.h

@@ -91,6 +91,11 @@ EXPORT void obs_data_set_obj(obs_data_t *data, const char *name,
 EXPORT void obs_data_set_array(obs_data_t *data, const char *name,
 			       obs_data_array_t *array);
 
+/*
+ * Creates an obs_data_t * filled with all default values.
+ */
+EXPORT obs_data_t *obs_data_get_defaults(obs_data_t *data);
+
 /*
  * Default value functions.
  */
@@ -104,6 +109,8 @@ EXPORT void obs_data_set_default_bool(obs_data_t *data, const char *name,
 				      bool val);
 EXPORT void obs_data_set_default_obj(obs_data_t *data, const char *name,
 				     obs_data_t *obj);
+EXPORT void obs_data_set_default_array(obs_data_t *data, const char *name,
+				       obs_data_array_t *arr);
 
 /*
  * Application overrides
@@ -166,6 +173,9 @@ EXPORT void obs_data_array_insert(obs_data_array_t *array, size_t idx,
 EXPORT void obs_data_array_push_back_array(obs_data_array_t *array,
 					   obs_data_array_t *array2);
 EXPORT void obs_data_array_erase(obs_data_array_t *array, size_t idx);
+EXPORT void obs_data_array_enum(obs_data_array_t *array,
+				void (*cb)(obs_data_t *data, void *param),
+				void *param);
 
 /* ------------------------------------------------------------------------- */
 /* Item status inspection */

+ 3 - 0
libobs/obs-internal.h

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

+ 173 - 0
libobs/obs-scene.c

@@ -1641,6 +1641,33 @@ obs_sceneitem_t *obs_scene_find_source_recursive(obs_scene_t *scene,
 	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)
 {
 	struct obs_scene_item *item;
@@ -2001,6 +2028,22 @@ void obs_sceneitem_remove(obs_sceneitem_t *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)
 {
 	return item ? item->parent : NULL;
@@ -2018,6 +2061,113 @@ static void signal_parent(obs_scene_t *parent, const char *command,
 	signal_handler_signal(parent->source->context.signals, command, params);
 }
 
+struct passthrough {
+	obs_data_array_t *ids;
+	bool all_items;
+};
+
+bool save_transform_states(obs_scene_t *scene, obs_sceneitem_t *item,
+			   void *vp_pass)
+{
+	struct passthrough *pass = (struct passthrough *)vp_pass;
+	if (obs_sceneitem_selected(item) || pass->all_items) {
+		obs_data_t *temp = obs_data_create();
+		obs_data_array_t *item_ids = (obs_data_array_t *)pass->ids;
+
+		struct obs_transform_info info;
+		struct obs_sceneitem_crop crop;
+		obs_sceneitem_get_info(item, &info);
+		obs_sceneitem_get_crop(item, &crop);
+
+		struct vec2 pos = info.pos;
+		struct vec2 scale = info.scale;
+		float rot = info.rot;
+		uint32_t alignment = info.alignment;
+		uint32_t bounds_type = info.bounds_type;
+		uint32_t bounds_alignment = info.bounds_alignment;
+		struct vec2 bounds = info.bounds;
+
+		obs_data_set_int(temp, "id", obs_sceneitem_get_id(item));
+		obs_data_set_vec2(temp, "pos", &pos);
+		obs_data_set_vec2(temp, "scale", &scale);
+		obs_data_set_int(temp, "rot", rot);
+		obs_data_set_int(temp, "alignment", alignment);
+		obs_data_set_int(temp, "bounds_type", bounds_type);
+		obs_data_set_vec2(temp, "bounds", &bounds);
+		obs_data_set_int(temp, "bounds_alignment", bounds_alignment);
+		obs_data_set_int(temp, "top", crop.top);
+		obs_data_set_int(temp, "bottom", crop.bottom);
+		obs_data_set_int(temp, "left", crop.left);
+		obs_data_set_int(temp, "right", crop.right);
+
+		obs_data_array_push_back(item_ids, temp);
+
+		obs_data_release(temp);
+	}
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene, bool all_items)
+{
+	obs_data_t *wrapper = obs_data_create();
+	obs_data_array_t *item_ids = obs_data_array_create();
+	struct passthrough pass = {item_ids, all_items};
+
+	obs_scene_enum_items(scene, save_transform_states, (void *)&pass);
+	obs_data_set_array(wrapper, "item_ids", item_ids);
+	obs_data_set_string(wrapper, "scene_name",
+			    obs_source_get_name(obs_scene_get_source(scene)));
+
+	obs_data_array_release(item_ids);
+
+	return wrapper;
+}
+
+void load_transform_states(obs_data_t *temp, void *vp_scene)
+{
+	obs_scene_t *scene = (obs_scene_t *)vp_scene;
+	int64_t id = obs_data_get_int(temp, "id");
+	obs_sceneitem_t *item = obs_scene_find_sceneitem_by_id(scene, id);
+
+	struct obs_transform_info info;
+	struct obs_sceneitem_crop crop;
+	obs_data_get_vec2(temp, "pos", &info.pos);
+	obs_data_get_vec2(temp, "scale", &info.scale);
+	info.rot = obs_data_get_int(temp, "rot");
+	info.alignment = obs_data_get_int(temp, "alignment");
+	info.bounds_type =
+		(enum obs_bounds_type)obs_data_get_int(temp, "bounds_type");
+	info.bounds_alignment = obs_data_get_int(temp, "bounds_alignment");
+	obs_data_get_vec2(temp, "bounds", &info.bounds);
+	crop.top = obs_data_get_int(temp, "top");
+	crop.bottom = obs_data_get_int(temp, "bottom");
+	crop.left = obs_data_get_int(temp, "left");
+	crop.right = obs_data_get_int(temp, "right");
+
+	obs_sceneitem_defer_update_begin(item);
+
+	obs_sceneitem_set_info(item, &info);
+	obs_sceneitem_set_crop(item, &crop);
+
+	obs_sceneitem_defer_update_end(item);
+}
+
+void obs_scene_load_transform_states(const char *data)
+{
+	obs_data_t *dat = obs_data_create_from_json(data);
+	obs_data_array_t *item_ids = obs_data_get_array(dat, "item_ids");
+	obs_source_t *source =
+		obs_get_source_by_name(obs_data_get_string(dat, "scene_name"));
+	obs_scene_t *scene = obs_scene_from_source(source);
+	obs_data_array_enum(item_ids, load_transform_states, (void *)scene);
+
+	obs_data_release(dat);
+	obs_data_array_release(item_ids);
+	obs_source_release(source);
+}
+
 void obs_sceneitem_select(obs_sceneitem_t *item, bool select)
 {
 	struct calldata params;
@@ -2148,6 +2298,24 @@ void obs_sceneitem_set_order(obs_sceneitem_t *item,
 	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)
 {
 	if (!item)
@@ -2573,6 +2741,11 @@ int64_t obs_sceneitem_get_id(const obs_sceneitem_t *item)
 	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)
 {
 	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"))
 		return;
 
-	if (settings)
+	if (settings) {
 		obs_data_apply(source->context.settings, settings);
+	}
 
 	if (source->info.output_flags & OBS_SOURCE_VIDEO) {
 		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);
 }
 
+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,
 					    const char *name)
 {

+ 31 - 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 */
 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 */
 EXPORT uint32_t obs_source_get_output_flags(const obs_source_t *source);
 
@@ -1581,6 +1589,29 @@ EXPORT void obs_sceneitem_release(obs_sceneitem_t *item);
 /** Removes a scene 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);
+
+/** Save all the transform states for a current scene's sceneitems */
+EXPORT obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene,
+						   bool all_items);
+
+/** Load all the transform states of sceneitems in that scene */
+EXPORT void obs_scene_load_transform_states(const char *state);
+
+/**  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. */
 EXPORT obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item);
 

+ 5 - 0
plugins/text-freetype2/text-freetype2.c

@@ -531,9 +531,14 @@ static void *ft2_source_create(obs_data_t *settings, obs_source_t *source,
 
 	obs_data_set_default_string(font_obj, "face", DEFAULT_FACE);
 	obs_data_set_default_int(font_obj, "size", font_size);
+	obs_data_set_default_int(font_obj, "flags", 0);
+	obs_data_set_default_string(font_obj, "style", "");
 	obs_data_set_default_obj(settings, "font", font_obj);
 
 	obs_data_set_default_bool(settings, "antialiasing", true);
+	obs_data_set_default_bool(settings, "word_wrap", false);
+	obs_data_set_default_bool(settings, "outline", false);
+	obs_data_set_default_bool(settings, "drop_shadow", false);
 
 	obs_data_set_default_int(settings, "log_lines", 6);