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.
This commit is contained in:
Ford Smith
2021-03-29 03:06:26 -04:00
committed by Ford Smith
parent 999495ca8c
commit 86eb7aeb69
4 changed files with 378 additions and 11 deletions
+38 -3
View File
@@ -20,6 +20,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QDir> #include <QDir>
#include <QGroupBox> #include <QGroupBox>
#include <QObject>
#include "double-slider.hpp" #include "double-slider.hpp"
#include "slider-ignorewheel.hpp" #include "slider-ignorewheel.hpp"
#include "spinbox-ignorewheel.hpp" #include "spinbox-ignorewheel.hpp"
@@ -31,6 +32,9 @@
#include <cstdlib> #include <cstdlib>
#include <initializer_list> #include <initializer_list>
#include <obs-data.h>
#include <obs.h>
#include <qtimer.h>
#include <string> #include <string>
using namespace std; using namespace std;
@@ -171,13 +175,14 @@ void OBSPropertiesView::GetScrollPos(int &h, int &v)
OBSPropertiesView::OBSPropertiesView(OBSData settings_, void *obj_, OBSPropertiesView::OBSPropertiesView(OBSData settings_, void *obj_,
PropertiesReloadCallback reloadCallback, PropertiesReloadCallback reloadCallback,
PropertiesUpdateCallback callback_, PropertiesUpdateCallback callback_,
int minSize_) PropertiesVisualUpdateCb cb_, int minSize_)
: VScrollArea(nullptr), : VScrollArea(nullptr),
properties(nullptr, obs_properties_destroy), properties(nullptr, obs_properties_destroy),
settings(settings_), settings(settings_),
obj(obj_), obj(obj_),
reloadCallback(reloadCallback), reloadCallback(reloadCallback),
callback(callback_), callback(callback_),
cb(cb_),
minSize(minSize_) minSize(minSize_)
{ {
setFrameShape(QFrame::NoFrame); setFrameShape(QFrame::NoFrame);
@@ -1885,6 +1890,12 @@ void WidgetInfo::ControlChanged()
const char *setting = obs_property_name(property); const char *setting = obs_property_name(property);
obs_property_type type = obs_property_get_type(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) { switch (type) {
case OBS_PROPERTY_INVALID: case OBS_PROPERTY_INVALID:
return; return;
@@ -1933,8 +1944,32 @@ void WidgetInfo::ControlChanged()
break; break;
} }
if (view->callback && !view->deferUpdate) if (!recently_updated) {
view->callback(view->obj, view->settings); 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(); view->SignalChanged();
+23 -3
View File
@@ -1,7 +1,10 @@
#pragma once #pragma once
#include "vertical-scroll-area.hpp" #include "vertical-scroll-area.hpp"
#include <obs-data.h>
#include <obs.hpp> #include <obs.hpp>
#include <qtimer.h>
#include <QPointer>
#include <vector> #include <vector>
#include <memory> #include <memory>
@@ -10,7 +13,9 @@ class OBSPropertiesView;
class QLabel; class QLabel;
typedef obs_properties_t *(*PropertiesReloadCallback)(void *obj); 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; OBSPropertiesView *view;
obs_property_t *property; obs_property_t *property;
QWidget *widget; QWidget *widget;
QPointer<QTimer> update_timer;
bool recently_updated = false;
OBSData old_settings_cache;
void BoolChanged(const char *setting); void BoolChanged(const char *setting);
void IntChanged(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: public slots:
void ControlChanged(); void ControlChanged();
@@ -83,6 +100,7 @@ private:
std::string type; std::string type;
PropertiesReloadCallback reloadCallback; PropertiesReloadCallback reloadCallback;
PropertiesUpdateCallback callback = nullptr; PropertiesUpdateCallback callback = nullptr;
PropertiesVisualUpdateCb cb = nullptr;
int minSize; int minSize;
std::vector<std::unique_ptr<WidgetInfo>> children; std::vector<std::unique_ptr<WidgetInfo>> children;
std::string lastFocused; std::string lastFocused;
@@ -135,13 +153,15 @@ signals:
public: public:
OBSPropertiesView(OBSData settings, void *obj, OBSPropertiesView(OBSData settings, void *obj,
PropertiesReloadCallback reloadCallback, PropertiesReloadCallback reloadCallback,
PropertiesUpdateCallback callback, int minSize = 0); PropertiesUpdateCallback callback,
PropertiesVisualUpdateCb cb = nullptr,
int minSize = 0);
OBSPropertiesView(OBSData settings, const char *type, OBSPropertiesView(OBSData settings, const char *type,
PropertiesReloadCallback reloadCallback, PropertiesReloadCallback reloadCallback,
int minSize = 0); int minSize = 0);
inline obs_data_t *GetSettings() const { return settings; } 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; } inline bool DeferUpdate() const { return deferUpdate; }
}; };
+251 -2
View File
@@ -15,6 +15,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/ ******************************************************************************/
#include "properties-view.hpp"
#include "window-namedialog.hpp" #include "window-namedialog.hpp"
#include "window-basic-main.hpp" #include "window-basic-main.hpp"
#include "window-basic-filters.hpp" #include "window-basic-filters.hpp"
@@ -23,9 +24,13 @@
#include "visibility-item-widget.hpp" #include "visibility-item-widget.hpp"
#include "item-widget-helpers.hpp" #include "item-widget-helpers.hpp"
#include "obs-app.hpp" #include "obs-app.hpp"
#include "undo-stack-obs.hpp"
#include <QMessageBox> #include <QMessageBox>
#include <QCloseEvent> #include <QCloseEvent>
#include <obs-data.h>
#include <obs.h>
#include <util/base.h>
#include <vector> #include <vector>
#include <string> #include <string>
#include <QMenu> #include <QMenu>
@@ -202,10 +207,82 @@ void OBSBasicFilters::UpdatePropertiesView(int row, bool async)
obs_data_t *settings = obs_source_get_settings(filter); 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( view = new OBSPropertiesView(
settings, filter, settings, filter,
(PropertiesReloadCallback)obs_source_properties, (PropertiesReloadCallback)obs_source_properties,
(PropertiesUpdateCallback)obs_source_update); (PropertiesUpdateCallback)filter_change,
(PropertiesVisualUpdateCb)disabled_undo);
updatePropertiesSignal.Connect(obs_source_get_signal_handler(filter), updatePropertiesSignal.Connect(obs_source_get_signal_handler(filter),
"update_properties", "update_properties",
@@ -240,6 +317,7 @@ void OBSBasicFilters::AddFilter(OBSSource filter, bool focus)
list->addItem(item); list->addItem(item);
if (focus) if (focus)
list->setCurrentItem(item); list->setCurrentItem(item);
SetupVisibilityItem(list, item, filter); SetupVisibilityItem(list, item, filter);
} }
@@ -486,6 +564,68 @@ void OBSBasicFilters::AddNewFilter(const char *id)
return; 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_t *filter =
obs_source_create(id, name.c_str(), nullptr, nullptr); obs_source_create(id, name.c_str(), nullptr, nullptr);
if (filter) { if (filter) {
@@ -674,8 +814,75 @@ void OBSBasicFilters::on_removeEffectFilter_clicked()
{ {
OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false); OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
if (filter) { 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_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())); listItem->setText(QT_UTF8(name.c_str()));
obs_source_set_name(filter, 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()); listItem->setText(QString());
+66 -3
View File
@@ -26,6 +26,10 @@
#include <QScreen> #include <QScreen>
#include <QWindow> #include <QWindow>
#include <QMessageBox> #include <QMessageBox>
#include <obs-data.h>
#include <obs.h>
#include <qpointer.h>
#include <util/c99defs.h>
using namespace std; using namespace std;
@@ -74,14 +78,28 @@ OBSBasicProperties::OBSBasicProperties(QWidget *parent, OBSSource source_)
/* The OBSData constructor increments the reference once */ /* The OBSData constructor increments the reference once */
obs_data_release(oldSettings); 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_apply(oldSettings, settings);
obs_data_release(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( view = new OBSPropertiesView(
settings, source, nd_settings, source,
(PropertiesReloadCallback)obs_source_properties, (PropertiesReloadCallback)obs_source_properties,
(PropertiesUpdateCallback)obs_source_update); (PropertiesUpdateCallback)handle_memory,
(PropertiesVisualUpdateCb)obs_source_update);
view->setMinimumHeight(150); view->setMinimumHeight(150);
preview->setMinimumSize(20, 150); preview->setMinimumSize(20, 150);
@@ -341,6 +359,51 @@ void OBSBasicProperties::on_buttonBox_clicked(QAbstractButton *button)
QDialogButtonBox::ButtonRole val = buttonBox->buttonRole(button); QDialogButtonBox::ButtonRole val = buttonBox->buttonRole(button);
if (val == QDialogButtonBox::AcceptRole) { 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; acceptClicked = true;
close(); close();