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-22 01:47:19 -04:00 committed by Ford Smith
parent 999495ca8c
commit 86eb7aeb69
4 changed files with 378 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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