diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index a734b9e5a..6a9c23f4e 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -260,7 +260,8 @@ set(obs_SOURCES obs-proxy-style.cpp locked-checkbox.cpp visibility-checkbox.cpp - media-slider.cpp) + media-slider.cpp + undo-stack-obs.cpp) set(obs_HEADERS ${obs_PLATFORM_HEADERS} @@ -331,7 +332,8 @@ set(obs_HEADERS log-viewer.hpp obs-proxy-style.hpp obs-proxy-style.hpp - media-slider.hpp) + media-slider.hpp + undo-stack-obs.hpp) set(obs_importers_HEADERS importers/importers.hpp) 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); diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 1174aa1e9..5cdc5ab8c 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -264,6 +264,34 @@ Basic.SceneTransitions="Scene Transitions" Basic.TransitionDuration="Duration" Basic.TogglePreviewProgramMode="Studio Mode" +# undo +Undo.Undo="Undo" +Undo.Redo="Redo" +Undo.Add="Add '%1'" +Undo.Delete="Delete '%1'" +Undo.Rename="Rename '%1'" +Undo.SceneCollection.Switch="Switch to '%1'" +Undo.Item.Undo="Undo %1" +Undo.Item.Redo="Redo %1" +Undo.Sources.Multi="Delete %1 Sources" +Undo.Filters="Filter Changes on '%1'" +Undo.Transform="Transform source(s) In '%1'" +Undo.Transform.Paste="Paste Transformation in '%1'" +Undo.Transform.Rotate="Rotation In '%1'" +Undo.Transform.Reset="Transform Reset In '%1'" +Undo.Transform.HFlip="Horizontal Flip In '%1'" +Undo.Transform.VFlip="Vertical Flip In '%1'" +Undo.Transform.FitToScren="Fit to Screen In '%1'" +Undo.Transform.StretchToScreen="Stretch to Screen in '%1'" +Undo.Transform.Center="Center to Screen in '%1'" +Undo.Transform.VCenter="Vertical Center to Screen in '%1'" +Undo.Transform.HCenter="Horizontal Center to Screen in '%1'" +Undo.Volume.Change="Volume Change on '%1'" +Undo.Audio="Audio Changes" +Undo.Properties="Property Change on '%1'" +Undo.Scene.Duplicate="Duplicate Scene '%1'" + + # transition name dialog TransitionNameDlg.Text="Please enter the name of the transition" TransitionNameDlg.Title="Transition Name" 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/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/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/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(); +}; 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-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 85ea5fb2f..bd39e6d3c 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); @@ -922,6 +936,11 @@ void OBSBasic::Load(const char *file) return; } + LoadData(data, file); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file) +{ ClearSceneData(); InitDefaultTransitions(); ClearContextBar(); @@ -3622,6 +3641,33 @@ void OBSBasic::DuplicateSelectedScene() OBS_SCENE_DUP_REFS); source = obs_scene_get_source(scene); SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + obs_source_t *source = + obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + obs_source_release(source); + }; + + auto redo = [this, name](const std::string &data) { + obs_source_t *source = + obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + obs_source_release(source); + scene = obs_scene_duplicate(scene, name.c_str(), + OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + obs_scene_release(scene); + }; + + undo_s.add_action( + QTStr("Undo.Scene.Duplicate") + .arg(obs_source_get_name(source)), + undo, redo, obs_source_get_name(source), + obs_source_get_name(obs_scene_get_source(curScene)), + NULL); + obs_scene_release(scene); break; @@ -3631,15 +3677,107 @@ void OBSBasic::DuplicateSelectedScene() void OBSBasic::RemoveSelectedScene() { OBSScene scene = GetCurrentScene(); - if (scene) { - obs_source_t *source = obs_scene_get_source(scene); - if (QueryRemoveSource(source)) { - obs_source_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 +4468,7 @@ void OBSBasic::closeEvent(QCloseEvent *event) /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, * sources, etc) so that all references are released before shutdown */ + undo_s.release(); ClearSceneData(); App()->quit(); @@ -4435,6 +4574,47 @@ void OBSBasic::on_action_Settings_triggered() } } +void save_audio_source(int channel, obs_data_t *save) +{ + obs_source_t *source = obs_get_output_source(channel); + if (!source) + return; + + obs_data_t *obj = obs_data_create(); + + obs_data_set_double(obj, "vol", obs_source_get_volume(source)); + obs_data_set_double(obj, "balance", + obs_source_get_balance_value(source)); + obs_data_set_double(obj, "mixers", obs_source_get_audio_mixers(source)); + obs_data_set_double(obj, "sync", obs_source_get_sync_offset(source)); + obs_data_set_double(obj, "flags", obs_source_get_flags(source)); + + obs_data_set_obj(save, std::to_string(channel).c_str(), obj); + obs_data_release(obj); + obs_source_release(source); +} + +void load_audio_source(int channel, obs_data_t *data) +{ + obs_source_t *source = obs_get_output_source(channel); + if (!source) + return; + + obs_data_t *save = + obs_data_get_obj(data, std::to_string(channel).c_str()); + + obs_source_set_volume(source, obs_data_get_double(save, "vol")); + obs_source_set_balance_value(source, + obs_data_get_double(save, "balance")); + obs_source_set_audio_mixers(source, + obs_data_get_double(save, "mixers")); + obs_source_set_sync_offset(source, obs_data_get_double(save, "sync")); + obs_source_set_flags(source, obs_data_get_double(save, "flags")); + + obs_data_release(save); + obs_source_release(source); +} + void OBSBasic::on_actionAdvAudioProperties_triggered() { if (advAudioWindow != nullptr) { @@ -4452,6 +4632,53 @@ void OBSBasic::on_actionAdvAudioProperties_triggered() connect(advAudioWindow, SIGNAL(destroyed()), this, SLOT(AdvAudioPropsDestroyed())); + + obs_data_t *wrapper = obs_data_create(); + + save_audio_source(1, wrapper); + save_audio_source(2, wrapper); + save_audio_source(3, wrapper); + save_audio_source(4, wrapper); + save_audio_source(5, wrapper); + save_audio_source(6, wrapper); + + std::string undo_data(obs_data_get_json(wrapper)); + + connect(advAudioWindow, &QDialog::finished, [this, undo_data]() { + auto undo_redo = [](const std::string &data) { + obs_data_t *audio_data = + obs_data_create_from_json(data.c_str()); + + load_audio_source(1, audio_data); + load_audio_source(2, audio_data); + load_audio_source(3, audio_data); + load_audio_source(4, audio_data); + load_audio_source(5, audio_data); + load_audio_source(6, audio_data); + + obs_data_release(audio_data); + }; + + obs_data_t *wrapper = obs_data_create(); + + save_audio_source(1, wrapper); + save_audio_source(2, wrapper); + save_audio_source(3, wrapper); + save_audio_source(4, wrapper); + save_audio_source(5, wrapper); + save_audio_source(6, wrapper); + + std::string redo_data(obs_data_get_json(wrapper)); + + if (undo_data.compare(redo_data) != 0) + undo_s.add_action(QTStr("Undo.Audio"), undo_redo, + undo_redo, undo_data, redo_data, + NULL); + + obs_data_release(wrapper); + }); + + obs_data_release(wrapper); } void OBSBasic::AdvAudioPropsClicked() @@ -4686,20 +4913,34 @@ void OBSBasic::on_actionAddScene_triggered() return; } + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_release(t); + obs_source_remove(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + obs_scene_t *scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source); + obs_scene_release(scene); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), + undo_fn, redo_fn, name, name, NULL); + obs_scene_t *scene = obs_scene_create(name.c_str()); source = obs_scene_get_source(scene); SetCurrentScene(source); + RefreshSources(scene); obs_scene_release(scene); } } void OBSBasic::on_actionRemoveScene_triggered() { - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (source && QueryRemoveSource(source)) - obs_source_remove(source); + RemoveSelectedScene(); } void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) @@ -5114,10 +5355,11 @@ void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) void OBSBasic::AddSource(const char *id) { if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id); + OBSBasicSourceSelect sourceSelect(this, id, undo_s); sourceSelect.exec(); - if (sourceSelect.newSource && strcmp(id, "group") != 0) + if (sourceSelect.newSource && strcmp(id, "group") != 0) { CreatePropertiesWindow(sourceSelect.newSource); + } } } @@ -5258,9 +5500,11 @@ void OBSBasic::on_actionRemoveSource_triggered() if (!items.size()) return; - auto removeMultiple = [this](size_t count) { + bool confirmed = false; + + if (items.size() > 1) { QString text = QTStr("ConfirmRemove.TextMultiple") - .arg(QString::number(count)); + .arg(QString::number(items.size())); QMessageBox remove_items(this); remove_items.setText(text); @@ -5272,21 +5516,139 @@ void OBSBasic::on_actionRemoveSource_triggered() remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); remove_items.exec(); - return Yes == remove_items.clickedButton(); - }; - - 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() @@ -5524,6 +5886,27 @@ static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_release(foundSource); } else { + auto undo = [prev = std::string(prevName)]( + const std::string &data) { + obs_source_t *source = + obs_get_source_by_name(data.c_str()); + obs_source_set_name(source, prev.c_str()); + obs_source_release(source); + }; + + auto redo = [name](const std::string &data) { + obs_source_t *source = + obs_get_source_by_name(data.c_str()); + obs_source_set_name(source, name.c_str()); + obs_source_release(source); + }; + + std::string undo_data(name); + std::string redo_data(prevName); + parent->undo_s.add_action( + QTStr("Undo.Rename").arg(name.c_str()), undo, redo, + undo_data, redo_data, NULL); + listItem->setText(QT_UTF8(name.c_str())); obs_source_set_name(source, name.c_str()); } @@ -6739,8 +7122,23 @@ void OBSBasic::on_actionCopyTransform_triggered() ui->actionPasteTransform->setEnabled(true); } +void undo_redo(const std::string &data) +{ + obs_data_t *dat = obs_data_create_from_json(data.c_str()); + obs_source_t *source = + obs_get_source_by_name(obs_data_get_string(dat, "scene_name")); + reinterpret_cast(App()->GetMainWindow()) + ->SetCurrentScene(source); + obs_source_release(source); + obs_data_release(dat); + + obs_scene_load_transform_states(data.c_str()); +} + void OBSBasic::on_actionPasteTransform_triggered() { + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) { if (!obs_sceneitem_selected(item)) return true; @@ -6756,6 +7154,19 @@ void OBSBasic::on_actionPasteTransform_triggered() }; obs_scene_enum_items(GetCurrentScene(), func, nullptr); + + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Paste") + .arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param) @@ -6791,6 +7202,22 @@ static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param) void OBSBasic::on_actionResetTransform_triggered() { + obs_scene_t *scene = GetCurrentScene(); + + obs_data_t *wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + obs_data_t *rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Reset") + .arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); } @@ -6868,19 +7295,61 @@ static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item, void OBSBasic::on_actionRotate90CW_triggered() { float f90CW = 90.0f; + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Rotate") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } void OBSBasic::on_actionRotate90CCW_triggered() { float f90CCW = -90.0f; + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Rotate") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } void OBSBasic::on_actionRotate180_triggered() { float f180 = 180.0f; + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Rotate") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item, @@ -6915,16 +7384,44 @@ void OBSBasic::on_actionFlipHorizontal_triggered() { vec2 scale; vec2_set(&scale, -1.0f, 1.0f); + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.HFlip") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } void OBSBasic::on_actionFlipVertical_triggered() { vec2 scale; vec2_set(&scale, 1.0f, -1.0f); + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.VFlip") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item, @@ -6964,15 +7461,43 @@ static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item, void OBSBasic::on_actionFitToScreen_triggered() { obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.FitToScreen") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } void OBSBasic::on_actionStretchToScreen_triggered() { obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + obs_data_t *rwrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source( + GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data, NULL); + + obs_data_release(wrapper); + obs_data_release(rwrapper); } enum class CenterType { @@ -7033,19 +7558,61 @@ static bool center_to_scene(obs_scene_t *, obs_sceneitem_t *item, void *param) void OBSBasic::on_actionCenterToScreen_triggered() { CenterType centerType = CenterType::Scene; + obs_data_t *wrapper = + obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), center_to_scene, ¢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) @@ -7136,6 +7703,46 @@ void OBSBasic::Nudge(int dist, MoveDir dir) break; } + if (!recent_nudge) { + recent_nudge = true; + obs_data_t *wrapper = obs_scene_save_transform_states( + GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect( + nudge_timer, &QTimer::timeout, + [this, &recent_nudge = recent_nudge, undo_data]() { + obs_data_t *rwrapper = + obs_scene_save_transform_states( + GetCurrentScene(), true); + std::string redo_data( + obs_data_get_json(rwrapper)); + + undo_s.add_action( + QTStr("Undo.Transform") + .arg(obs_source_get_name( + GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, + redo_data, NULL); + + recent_nudge = false; + obs_data_release(rwrapper); + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, + &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + + obs_data_release(wrapper); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); } @@ -7767,6 +8374,16 @@ bool OBSBasic::sysTrayMinimizeToTray() "SysTrayMinimizeToTray"); } +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + void OBSBasic::on_actionCopySource_triggered() { copyStrings.clear(); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 6c1da6bd5..c7a715afc 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 @@ -156,6 +157,7 @@ class OBSBasic : public OBSMainWindow { friend class OBSBasicPreview; friend class OBSBasicStatusBar; friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; friend class OBSBasicSettings; friend class Auth; friend class AutoConfig; @@ -166,6 +168,7 @@ class OBSBasic : public OBSMainWindow { friend class ExtraBrowsersDelegate; friend class DeviceCaptureToolbar; friend class DeviceToolbarPropertiesThread; + friend class OBSBasicSourceSelect; friend struct BasicOutputHandler; friend struct OBSStudioAPI; @@ -220,6 +223,9 @@ private: QPointer cpuUsageTimer; QPointer diskFullTimer; + QPointer nudge_timer; + bool recent_nudge = false; + os_cpu_usage_info_t *cpuUsageInfo = nullptr; OBSService service; @@ -308,6 +314,7 @@ private: void UploadLog(const char *subdir, const char *file, const bool crash); void Save(const char *file); + void LoadData(obs_data_t *data, const char *file); void Load(const char *file); void InitHotkeys(); @@ -609,6 +616,10 @@ public slots: void UnpauseRecording(); private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + void AddSceneItem(OBSSceneItem item); void AddScene(OBSSource source); void RemoveScene(OBSSource source); @@ -741,6 +752,7 @@ private: OBSSource prevFTBSource = nullptr; public: + undo_stack undo_s; OBSSource GetProgramSource(); OBSScene GetCurrentScene(); 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-properties.cpp b/UI/window-basic-properties.cpp index 4ffa46343..b6a161ebe 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(); 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/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/docs/sphinx/reference-scenes.rst b/docs/sphinx/reference-scenes.rst index 0ef6445e0..72b11d721 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-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/docs/sphinx/reference-sources.rst b/docs/sphinx/reference-sources.rst index 2d376d374..937b20545 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) diff --git a/libobs/obs-data.c b/libobs/obs-data.c index 2d8cefeb4..3f5d27ec1 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 */ @@ -1517,6 +1624,9 @@ enum obs_data_number_type obs_data_item_numtype(obs_data_item_t *item) return OBS_DATA_NUM_INVALID; num = get_item_data(item); + if (!num) + return OBS_DATA_NUM_INVALID; + return num->type; } 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 */ 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 d5d23cd0c..9fab01e3c 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -1641,6 +1641,33 @@ obs_sceneitem_t *obs_scene_find_source_recursive(obs_scene_t *scene, return item; } +struct sceneitem_check { + obs_source_t *source_in; + obs_sceneitem_t *item_out; +}; + +bool check_sceneitem_exists(obs_scene_t *scene, obs_sceneitem_t *item, + void *vp_check) +{ + UNUSED_PARAMETER(scene); + struct sceneitem_check *check = (struct sceneitem_check *)vp_check; + if (obs_sceneitem_get_source(item) == check->source_in) { + check->item_out = item; + obs_sceneitem_addref(item); + return false; + } + + return true; +} + +obs_sceneitem_t *obs_scene_sceneitem_from_source(obs_scene_t *scene, + obs_source_t *source) +{ + struct sceneitem_check check = {source, NULL}; + obs_scene_enum_items(scene, check_sceneitem_exists, (void *)&check); + return check.item_out; +} + obs_sceneitem_t *obs_scene_find_sceneitem_by_id(obs_scene_t *scene, int64_t id) { struct obs_scene_item *item; @@ -2001,6 +2028,22 @@ void obs_sceneitem_remove(obs_sceneitem_t *item) obs_sceneitem_release(item); } +void obs_sceneitem_save(obs_sceneitem_t *item, obs_data_array_t *arr) +{ + scene_save_item(arr, item, NULL); +} + +void sceneitem_restore(obs_data_t *data, void *vp) +{ + obs_scene_t *scene = (obs_scene_t *)vp; + scene_load_item(scene, data); +} + +void obs_sceneitems_add(obs_scene_t *scene, obs_data_array_t *data) +{ + obs_data_array_enum(data, sceneitem_restore, scene); +} + obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item) { return item ? item->parent : NULL; @@ -2018,6 +2061,113 @@ static void signal_parent(obs_scene_t *parent, const char *command, signal_handler_signal(parent->source->context.signals, command, params); } +struct passthrough { + obs_data_array_t *ids; + bool all_items; +}; + +bool save_transform_states(obs_scene_t *scene, obs_sceneitem_t *item, + void *vp_pass) +{ + struct passthrough *pass = (struct passthrough *)vp_pass; + if (obs_sceneitem_selected(item) || pass->all_items) { + obs_data_t *temp = obs_data_create(); + obs_data_array_t *item_ids = (obs_data_array_t *)pass->ids; + + struct obs_transform_info info; + struct obs_sceneitem_crop crop; + obs_sceneitem_get_info(item, &info); + obs_sceneitem_get_crop(item, &crop); + + struct vec2 pos = info.pos; + struct vec2 scale = info.scale; + float rot = info.rot; + uint32_t alignment = info.alignment; + uint32_t bounds_type = info.bounds_type; + uint32_t bounds_alignment = info.bounds_alignment; + struct vec2 bounds = info.bounds; + + obs_data_set_int(temp, "id", obs_sceneitem_get_id(item)); + obs_data_set_vec2(temp, "pos", &pos); + obs_data_set_vec2(temp, "scale", &scale); + obs_data_set_int(temp, "rot", rot); + obs_data_set_int(temp, "alignment", alignment); + obs_data_set_int(temp, "bounds_type", bounds_type); + obs_data_set_vec2(temp, "bounds", &bounds); + obs_data_set_int(temp, "bounds_alignment", bounds_alignment); + obs_data_set_int(temp, "top", crop.top); + obs_data_set_int(temp, "bottom", crop.bottom); + obs_data_set_int(temp, "left", crop.left); + obs_data_set_int(temp, "right", crop.right); + + obs_data_array_push_back(item_ids, temp); + + obs_data_release(temp); + } + + UNUSED_PARAMETER(scene); + return true; +} + +obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene, bool all_items) +{ + obs_data_t *wrapper = obs_data_create(); + obs_data_array_t *item_ids = obs_data_array_create(); + struct passthrough pass = {item_ids, all_items}; + + obs_scene_enum_items(scene, save_transform_states, (void *)&pass); + obs_data_set_array(wrapper, "item_ids", item_ids); + obs_data_set_string(wrapper, "scene_name", + obs_source_get_name(obs_scene_get_source(scene))); + + obs_data_array_release(item_ids); + + return wrapper; +} + +void load_transform_states(obs_data_t *temp, void *vp_scene) +{ + obs_scene_t *scene = (obs_scene_t *)vp_scene; + int64_t id = obs_data_get_int(temp, "id"); + obs_sceneitem_t *item = obs_scene_find_sceneitem_by_id(scene, id); + + struct obs_transform_info info; + struct obs_sceneitem_crop crop; + obs_data_get_vec2(temp, "pos", &info.pos); + obs_data_get_vec2(temp, "scale", &info.scale); + info.rot = obs_data_get_int(temp, "rot"); + info.alignment = obs_data_get_int(temp, "alignment"); + info.bounds_type = + (enum obs_bounds_type)obs_data_get_int(temp, "bounds_type"); + info.bounds_alignment = obs_data_get_int(temp, "bounds_alignment"); + obs_data_get_vec2(temp, "bounds", &info.bounds); + crop.top = obs_data_get_int(temp, "top"); + crop.bottom = obs_data_get_int(temp, "bottom"); + crop.left = obs_data_get_int(temp, "left"); + crop.right = obs_data_get_int(temp, "right"); + + obs_sceneitem_defer_update_begin(item); + + obs_sceneitem_set_info(item, &info); + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); +} + +void obs_scene_load_transform_states(const char *data) +{ + obs_data_t *dat = obs_data_create_from_json(data); + obs_data_array_t *item_ids = obs_data_get_array(dat, "item_ids"); + obs_source_t *source = + obs_get_source_by_name(obs_data_get_string(dat, "scene_name")); + obs_scene_t *scene = obs_scene_from_source(source); + obs_data_array_enum(item_ids, load_transform_states, (void *)scene); + + obs_data_release(dat); + obs_data_array_release(item_ids); + obs_source_release(source); +} + void obs_sceneitem_select(obs_sceneitem_t *item, bool select) { struct calldata params; @@ -2148,6 +2298,24 @@ void obs_sceneitem_set_order(obs_sceneitem_t *item, obs_scene_release(scene); } +int obs_sceneitem_get_order_position(obs_sceneitem_t *item) +{ + struct obs_scene *scene = item->parent; + struct obs_scene_item *next = scene->first_item; + + full_lock(scene); + + int index = 0; + while (next && next != item) { + next = next->next; + ++index; + } + + full_unlock(scene); + + return index; +} + void obs_sceneitem_set_order_position(obs_sceneitem_t *item, int position) { if (!item) @@ -2573,6 +2741,11 @@ int64_t obs_sceneitem_get_id(const obs_sceneitem_t *item) return item->id; } +void obs_sceneitem_set_id(obs_sceneitem_t *item, int64_t id) +{ + item->id = id; +} + obs_data_t *obs_sceneitem_get_private_settings(obs_sceneitem_t *item) { if (!obs_ptr_valid(item, "obs_sceneitem_get_private_settings")) diff --git a/libobs/obs-source.c b/libobs/obs-source.c index e1ad9bb48..504b4b62e 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 efb5f5ec8..1bdce59c4 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); @@ -1581,6 +1589,29 @@ EXPORT void obs_sceneitem_release(obs_sceneitem_t *item); /** Removes a scene item. */ EXPORT void obs_sceneitem_remove(obs_sceneitem_t *item); +/** Adds a scene item. */ +EXPORT void obs_sceneitems_add(obs_scene_t *scene, obs_data_array_t *data); + +/** Saves Sceneitem into an array, arr **/ +EXPORT void obs_sceneitem_save(obs_sceneitem_t *item, obs_data_array_t *arr); + +/** Set the ID of a sceneitem */ +EXPORT void obs_sceneitem_set_id(obs_sceneitem_t *sceneitem, int64_t id); + +/** Tries to find the sceneitem of the source in a given scene. Returns NULL if not found */ +EXPORT obs_sceneitem_t *obs_scene_sceneitem_from_source(obs_scene_t *scene, + obs_source_t *source); + +/** Save all the transform states for a current scene's sceneitems */ +EXPORT obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene, + bool all_items); + +/** Load all the transform states of sceneitems in that scene */ +EXPORT void obs_scene_load_transform_states(const char *state); + +/** Gets a sceneitem's order in its scene */ +EXPORT int obs_sceneitem_get_order_position(obs_sceneitem_t *item); + /** Gets the scene parent associated with the scene item. */ EXPORT obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item); 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);