From 31e5d2e5e35fa8e5b6f00124becb3efb8319275b Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Sun, 14 Mar 2021 01:51:07 -0500 Subject: [PATCH 01/11] libobs: Implement obs_data_get_defaults Implements obs_data_get_defaults and updates the documentation. This is supposed to allow someone to access all the defaults of an object. Should help in cases where the full data is needed, and not just the currently set. --- docs/sphinx/reference-settings.rst | 11 ++- libobs/obs-data.c | 107 +++++++++++++++++++++++++++++ libobs/obs-data.h | 10 +++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/reference-settings.rst b/docs/sphinx/reference-settings.rst index 91cd4ba87..edc9d04c6 100644 --- a/docs/sphinx/reference-settings.rst +++ b/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 diff --git a/libobs/obs-data.c b/libobs/obs-data.c index 2d8cefeb4..79e971590 100644 --- a/libobs/obs-data.c +++ b/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 */ diff --git a/libobs/obs-data.h b/libobs/obs-data.h index 202190d2d..20ea19d57 100644 --- a/libobs/obs-data.h +++ b/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 */ From 7fc539c3c4ebaa8094f37ca8c6c275541e81831f Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Sat, 13 Mar 2021 22:39:47 -0500 Subject: [PATCH 02/11] text-freetype2: Updated defaults Adds several more settings to the defaults for the text objects. --- plugins/text-freetype2/text-freetype2.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/text-freetype2/text-freetype2.c b/plugins/text-freetype2/text-freetype2.c index cf831ea99..88744d85b 100644 --- a/plugins/text-freetype2/text-freetype2.c +++ b/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); From eaf992119ff610b444f2b745b0e0e9e190be85df Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Sun, 21 Mar 2021 22:21:03 -0400 Subject: [PATCH 03/11] libobs: Fix obs_data_item_numtype returning null in some cases --- libobs/obs-data.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libobs/obs-data.c b/libobs/obs-data.c index 79e971590..3f5d27ec1 100644 --- a/libobs/obs-data.c +++ b/libobs/obs-data.c @@ -1624,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; } From eced5a320bc9dfc7ef62185eb0cc433a890e2ab4 Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Sun, 21 Mar 2021 22:16:17 -0400 Subject: [PATCH 04/11] UI: Initial Undo/Redo Starting to develop Undo/Redo. This just implmements the undo/redo stack and its api. --- UI/data/locale/en-US.ini | 28 +++++++++++ UI/undo-stack-obs.cpp | 101 +++++++++++++++++++++++++++++++++++++++ UI/undo-stack-obs.hpp | 46 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 UI/undo-stack-obs.cpp create mode 100644 UI/undo-stack-obs.hpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 1aba3994e..3397900e1 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -262,6 +262,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" diff --git a/UI/undo-stack-obs.cpp b/UI/undo-stack-obs.cpp new file mode 100644 index 000000000..7d1365c1c --- /dev/null +++ b/UI/undo-stack-obs.cpp @@ -0,0 +1,101 @@ +#include "undo-stack-obs.hpp" + +#include + +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(); +} diff --git a/UI/undo-stack-obs.hpp b/UI/undo-stack-obs.hpp new file mode 100644 index 000000000..e1643b13d --- /dev/null +++ b/UI/undo-stack-obs.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "ui_OBSBasic.h" + +typedef std::function undo_redo_cb; +typedef std::function func; +typedef std::unique_ptr &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_items; + std::deque 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(); +}; From 60d95cb5bd65598600eb24a711d46694db9b44ab Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 00:39:02 -0400 Subject: [PATCH 05/11] UI/libobs: Undo/Redo Sources and Scenes Implements the Undo/Redo for scenes and sources, ranging from renaming, deletion, addition. It also adds several elements to libobs that were designed to facilitate undo/redo, and should not affect the rest of libobs. --- UI/CMakeLists.txt | 6 +- UI/forms/OBSBasic.ui | 22 +- UI/source-tree.cpp | 28 +++ UI/window-basic-main.cpp | 358 +++++++++++++++++++++++++++--- UI/window-basic-main.hpp | 7 + UI/window-basic-source-select.cpp | 70 +++++- UI/window-basic-source-select.hpp | 5 +- libobs/obs-internal.h | 3 + libobs/obs-scene.c | 66 ++++++ libobs/obs-source.c | 13 +- libobs/obs.h | 23 ++ 11 files changed, 564 insertions(+), 37 deletions(-) diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 4478e2b5c..503025e90 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -256,7 +256,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} @@ -327,7 +328,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) diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 5556d2691..261e2d39e 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -586,6 +586,9 @@ Paste.Filters + + + @@ -599,6 +602,7 @@ + @@ -2034,7 +2038,23 @@ Basic.MainMenu.View.ContextBar - + + + false + + + Undo + + + + + false + + + Redo + + + OBSBasicPreview diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp index f7dd79ce2..cf2229531 100644 --- a/UI/source-tree.cpp +++ b/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())); } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index af3a84842..6a5a17fc4 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -17,7 +17,10 @@ along with this program. If not, see . ******************************************************************************/ +#include #include +#include +#include #include #include #include @@ -58,6 +61,7 @@ #include "remote-text.hpp" #include "ui-validation.hpp" #include "media-controls.hpp" +#include "undo-stack-obs.hpp" #include #include @@ -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>( @@ -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 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); @@ -3622,6 +3636,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 +3672,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_remove(source); + obs_source_t *source = obs_scene_get_source(scene); - if (api) - api->on_event( - OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); - } + OBSSource curProgramScene = OBSGetStrongRef(programScene); + + if (source && QueryRemoveSource(source)) { + vector 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 &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); + + d(false); + }; + + std::string undo_data = obs_data_get_json(wrapper); + std::string redo_data = obs_data_get_json(wrapper); + undo_s.add_action( + QTStr("Undo.Delete").arg(obs_source_get_name(source)), + undo, redo, undo_data, redo_data, [d](bool undo) { + if (undo) { + d(true); + } + }); + + obs_source_remove(source); + obs_data_release(wrapper); + obs_data_release(rwrapper); + obs_data_array_release(arr); + obs_data_array_release(list_order); + + if (api) + api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); } } @@ -4330,6 +4463,7 @@ void OBSBasic::closeEvent(QCloseEvent *event) /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, * sources, etc) so that all references are released before shutdown */ + undo_s.release(); ClearSceneData(); App()->quit(); @@ -4686,20 +4820,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) @@ -5109,10 +5257,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); + } } } @@ -5253,9 +5402,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); @@ -5267,21 +5418,139 @@ void OBSBasic::on_actionRemoveSource_triggered() remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); remove_items.exec(); - return Yes == remove_items.clickedButton(); - }; - - if (items.size() == 1) { + confirmed = remove_items.clickedButton(); + } else { OBSSceneItem &item = items[0]; obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - obs_sceneitem_remove(item); - } else { - if (removeMultiple(items.size())) { - for (auto &item : items) - obs_sceneitem_remove(item); - } + 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 item_save; + + 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); + } + + obs_source_release(source); + obs_source_release(source); + } + + obs_source_release(src); + obs_data_array_release(sources_data); + obs_data_release(dat); + }; + + auto redo_fn = [item_save](const std::string &) { + for (const auto &save : item_save) { + obs_source_t *source = + obs_get_source_by_name(save.name.c_str()); + obs_source_t *scene_source = + obs_get_source_by_name(save.scene_name.c_str()); + obs_scene_t *scene = + obs_scene_from_source(scene_source); + if (!scene) + scene = obs_group_from_source(scene_source); + + obs_sceneitem_t *item = + obs_scene_sceneitem_from_source(scene, source); + obs_sceneitem_remove(item); + obs_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() @@ -5519,6 +5788,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()); } @@ -7762,6 +8052,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(); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 0ee5ec96b..40fd716ce 100644 --- a/UI/window-basic-main.hpp +++ b/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 @@ -166,6 +167,7 @@ class OBSBasic : public OBSMainWindow { friend class ExtraBrowsersDelegate; friend class DeviceCaptureToolbar; friend class DeviceToolbarPropertiesThread; + friend class OBSBasicSourceSelect; friend struct BasicOutputHandler; friend struct OBSStudioAPI; @@ -608,6 +610,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); @@ -740,6 +746,7 @@ private: OBSSource prevFTBSource = nullptr; public: + undo_stack undo_s; OBSSource GetProgramSource(); OBSScene GetCurrentScene(); diff --git a/UI/window-basic-source-select.cpp b/UI/window-basic-source-select.cpp index 57b7b121f..cee30e520 100644 --- a/UI/window-basic-source-select.cpp +++ b/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(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(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 static inline T GetOBSRef(QListWidgetItem *item) return item->data(static_cast(QtDataRole::OBSRef)).value(); } -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); diff --git a/UI/window-basic-source-select.hpp b/UI/window-basic-source-select.hpp index 90496798c..089dc60f3 100644 --- a/UI/window-basic-source-select.hpp +++ b/UI/window-basic-source-select.hpp @@ -21,6 +21,7 @@ #include #include "ui_OBSBasicSourceSelect.h" +#include "undo-stack-obs.hpp" class OBSBasic; @@ -30,6 +31,7 @@ class OBSBasicSourceSelect : public QDialog { private: std::unique_ptr 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; diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 142c01235..83037fc25 100644 --- a/libobs/obs-internal.h +++ b/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; diff --git a/libobs/obs-scene.c b/libobs/obs-scene.c index 049a71d40..37eb40eea 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -1477,6 +1477,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; @@ -1830,6 +1857,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; @@ -1977,6 +2020,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) @@ -2385,6 +2446,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")) diff --git a/libobs/obs-source.c b/libobs/obs-source.c index e8c33be23..d148d17f2 100644 --- a/libobs/obs-source.c +++ b/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) { diff --git a/libobs/obs.h b/libobs/obs.h index dd2a69fde..fd7f88e5f 100644 --- a/libobs/obs.h +++ b/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); @@ -1578,6 +1586,21 @@ 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); +/** 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); From 3a620c485e5ec09a628b8adc169446150e6eeebc Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 01:14:37 -0400 Subject: [PATCH 06/11] UI: Undo/Redo Transformations Implements undo/redo for transformations of sources, both through preview and the transformations properties. --- UI/window-basic-main.cpp | 226 +++++++++++++++++++++++++++++++++- UI/window-basic-main.hpp | 4 + UI/window-basic-preview.cpp | 45 +++++++ UI/window-basic-preview.hpp | 3 + UI/window-basic-transform.cpp | 32 +++++ UI/window-basic-transform.hpp | 3 + libobs/obs-scene.c | 107 ++++++++++++++++ libobs/obs.h | 8 ++ 8 files changed, 426 insertions(+), 2 deletions(-) diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 6a5a17fc4..2ac4236e0 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -925,8 +925,6 @@ void OBSBasic::LogScenes() void OBSBasic::Load(const char *file) { - disableSaving++; - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); if (!data) { disableSaving--; @@ -7024,8 +7022,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(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; @@ -7041,6 +7054,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) @@ -7076,6 +7102,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); } @@ -7153,19 +7195,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, @@ -7200,16 +7284,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, @@ -7249,15 +7361,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 { @@ -7318,19 +7458,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, ¢erType); + 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, ¢erType); + 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, ¢erType); + 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) @@ -7421,6 +7603,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); } diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 40fd716ce..d47a77d9d 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -157,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; @@ -222,6 +223,9 @@ private: QPointer cpuUsageTimer; QPointer diskFullTimer; + QPointer nudge_timer; + bool recent_nudge = false; + os_cpu_usage_info_t *cpuUsageInfo = nullptr; OBSService service; diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp index 29e4d7152..ac03f3850 100644 --- a/UI/window-basic-preview.cpp +++ b/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(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(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; diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp index 01386b8d9..c273a1e6c 100644 --- a/UI/window-basic-preview.hpp +++ b/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()); diff --git a/UI/window-basic-transform.cpp b/UI/window-basic-transform.cpp index eaf0594cd..4eb38cb18 100644 --- a/UI/window-basic-transform.cpp +++ b/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(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(); diff --git a/UI/window-basic-transform.hpp b/UI/window-basic-transform.hpp index bc0528ded..65f01e041 100644 --- a/UI/window-basic-transform.hpp +++ b/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(); }; diff --git a/libobs/obs-scene.c b/libobs/obs-scene.c index 37eb40eea..1b07e88b3 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -1890,6 +1890,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; diff --git a/libobs/obs.h b/libobs/obs.h index fd7f88e5f..3c245dd7d 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -1598,6 +1598,14 @@ 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); From 3c5e2ddc37f40fe3f44788ca8000ebe258abcd00 Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 01:20:11 -0400 Subject: [PATCH 07/11] UI: Undo/Redo audio Implements undo/redo for advanced audio properties. --- UI/window-basic-main.cpp | 95 ++++++++++++++++++++++++++++++++++++++++ UI/window-basic-main.hpp | 1 + 2 files changed, 96 insertions(+) diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 2ac4236e0..ccf4d277f 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -934,6 +934,13 @@ void OBSBasic::Load(const char *file) return; } + LoadData(data, file); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file) +{ + disableSaving++; + ClearSceneData(); InitDefaultTransitions(); ClearContextBar(); @@ -4567,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) { @@ -4584,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() diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index d47a77d9d..4682de1cf 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -314,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(); From a374c023a1d0041059cf10d88c9d4290a13cb51a Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 01:27:10 -0400 Subject: [PATCH 08/11] UI: Undo/Redo Scene Collections Implements undo/redo for scene collections. This includes operations including rename, delete, and addition. --- UI/window-basic-main-scene-collections.cpp | 168 +++++++++++++++++++-- UI/window-basic-main.cpp | 4 +- 2 files changed, 160 insertions(+), 12 deletions(-) diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp index 420f2b7a4..26f2d7cbe 100644 --- a/UI/window-basic-main-scene-collections.cpp +++ b/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); } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index ccf4d277f..0ffe61a15 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -925,6 +925,8 @@ void OBSBasic::LogScenes() void OBSBasic::Load(const char *file) { + disableSaving++; + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); if (!data) { disableSaving--; @@ -939,8 +941,6 @@ void OBSBasic::Load(const char *file) void OBSBasic::LoadData(obs_data_t *data, const char *file) { - disableSaving++; - ClearSceneData(); InitDefaultTransitions(); ClearContextBar(); From 999495ca8cb467d21c0e37d8650ab7ca0b9084ba Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 01:46:46 -0400 Subject: [PATCH 09/11] UI: Undo/Redo context bar properties Implements undo/redo for changing of properties done through the context bar. --- UI/context-bar-controls.cpp | 78 +++++++++++++++++++++++++++++++++++++ UI/context-bar-controls.hpp | 4 ++ 2 files changed, 82 insertions(+) diff --git a/UI/context-bar-controls.cpp b/UI/context-bar-controls.cpp index 9ef850b6d..1af2599ab 100644 --- a/UI/context-bar-controls.cpp +++ b/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(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() diff --git a/UI/context-bar-controls.hpp b/UI/context-bar-controls.hpp index 5848121fa..9671dd2f0 100644 --- a/UI/context-bar-controls.hpp +++ b/UI/context-bar-controls.hpp @@ -22,6 +22,10 @@ protected: std::unique_ptr; properties_t props; + OBSData oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source); public: SourceToolbar(QWidget *parent, OBSSource source); From 86eb7aeb6935260725f91ff0b034995d4e7b4154 Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 01:47:19 -0400 Subject: [PATCH 10/11] 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. --- UI/properties-view.cpp | 41 +++++- UI/properties-view.hpp | 26 +++- UI/window-basic-filters.cpp | 253 ++++++++++++++++++++++++++++++++- UI/window-basic-properties.cpp | 69 ++++++++- 4 files changed, 378 insertions(+), 11 deletions(-) diff --git a/UI/properties-view.cpp b/UI/properties-view.cpp index 35d433388..eba6b180a 100644 --- a/UI/properties-view.cpp +++ b/UI/properties-view.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "double-slider.hpp" #include "slider-ignorewheel.hpp" #include "spinbox-ignorewheel.hpp" @@ -31,6 +32,9 @@ #include #include +#include +#include +#include #include 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(); diff --git a/UI/properties-view.hpp b/UI/properties-view.hpp index 143e37100..ea51f7cc9 100644 --- a/UI/properties-view.hpp +++ b/UI/properties-view.hpp @@ -1,7 +1,10 @@ #pragma once #include "vertical-scroll-area.hpp" +#include #include +#include +#include #include #include @@ -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 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> 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; } }; diff --git a/UI/window-basic-filters.cpp b/UI/window-basic-filters.cpp index 52c2c2876..a4a55d054 100644 --- a/UI/window-basic-filters.cpp +++ b/UI/window-basic-filters.cpp @@ -15,6 +15,7 @@ along with this program. If not, see . ******************************************************************************/ +#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 #include +#include +#include +#include #include #include #include @@ -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(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(App()->GetMainWindow()); + main->undo_s.disable_undo_redo(); + obs_source_t *source = reinterpret_cast(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(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(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(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( + 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( + 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( + 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(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(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(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()); diff --git a/UI/window-basic-properties.cpp b/UI/window-basic-properties.cpp index 4f149667d..4e9e2cde0 100644 --- a/UI/window-basic-properties.cpp +++ b/UI/window-basic-properties.cpp @@ -26,6 +26,10 @@ #include #include #include +#include +#include +#include +#include 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(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(); From d0dfae6a3cc9829cf83e2fe07bea5e34d2a7ee7e Mon Sep 17 00:00:00 2001 From: Ford Smith Date: Mon, 22 Mar 2021 01:48:09 -0400 Subject: [PATCH 11/11] docs: Update documentation about utility functions for undo/redo --- docs/sphinx/reference-scenes.rst | 38 +++++++++++++++++++++++++++++-- docs/sphinx/reference-sources.rst | 13 +++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/reference-scenes.rst b/docs/sphinx/reference-scenes.rst index 437259f39..a9bb18c39 100644 --- a/docs/sphinx/reference-scenes.rst +++ b/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. --------------------- diff --git a/docs/sphinx/reference-sources.rst b/docs/sphinx/reference-sources.rst index b9e06b079..85c663d53 100644 --- a/docs/sphinx/reference-sources.rst +++ b/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)