Browse Source

UI: Undo/Redo Properties and Filters

Implements undo/redo for both properties and filters. Works by creating
a new callback that gets called to save undo/redo states after a timer
is fired. Also disabled undo/redo until the actions have completed to
prevent a user from being able to disrupt the stack by perfoming actions
before others have finished.
Ford Smith 4 years ago
parent
commit
86eb7aeb69
4 changed files with 378 additions and 11 deletions
  1. 38 3
      UI/properties-view.cpp
  2. 23 3
      UI/properties-view.hpp
  3. 251 2
      UI/window-basic-filters.cpp
  4. 66 3
      UI/window-basic-properties.cpp

+ 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; }
 };

+ 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());

+ 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();