diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 31addcbe0..c8aad986b 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -61,6 +61,14 @@ Deprecated="Deprecated"
ReplayBuffer="Replay Buffer"
Import="Import"
Export="Export"
+Copy="Copy"
+Paste="Paste"
+PasteReference="Paste (Reference)"
+PasteDuplicate="Paste (Duplicate)"
+
+# copy filters
+Copy.Filters="Copy Filters"
+Paste.Filters="Paste Filters"
# updater
Updater.Title="New update available"
diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui
index 92aa8ed16..92b8252a7 100644
--- a/UI/forms/OBSBasic.ui
+++ b/UI/forms/OBSBasic.ui
@@ -892,6 +892,51 @@
+
+
+ Copy
+
+
+ Ctrl+C
+
+
+
+
+ false
+
+
+ PasteReference
+
+
+ PasteReference
+
+
+ PasteReference
+
+
+ Ctrl+V
+
+
+
+
+ Copy.Filters
+
+
+
+
+ false
+
+
+ Paste.Filters
+
+
+
+
+
+
+
+
+
@@ -1409,6 +1454,11 @@
Basic.MainMenu.Edit.Scale.Output
+
+
+ PasteDuplicate
+
+
diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp
index a7154419b..03753c191 100644
--- a/UI/window-basic-main-scene-collections.cpp
+++ b/UI/window-basic-main-scene-collections.cpp
@@ -225,6 +225,8 @@ void OBSBasic::RefreshSceneCollections()
OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
main->OpenSavedProjectors();
+ main->ui->actionPasteRef->setEnabled(false);
+ main->ui->actionPasteDup->setEnabled(false);
}
void OBSBasic::on_actionNewSceneCollection_triggered()
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index b8486c147..420d126e8 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -3347,6 +3347,19 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
if (addSourceMenu)
popup.addMenu(addSourceMenu);
+ ui->actionCopyFilters->setEnabled(false);
+
+ popup.addSeparator();
+ popup.addAction(ui->actionCopySource);
+ popup.addAction(ui->actionPasteRef);
+ popup.addAction(ui->actionPasteDup);
+ popup.addSeparator();
+
+ popup.addSeparator();
+ popup.addAction(ui->actionCopyFilters);
+ popup.addAction(ui->actionPasteFilters);
+ popup.addSeparator();
+
if (item) {
if (addSourceMenu)
popup.addSeparator();
@@ -3393,6 +3406,8 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
SLOT(OpenFilters()));
popup.addAction(QTStr("Properties"), this,
SLOT(on_actionSourceProperties_triggered()));
+
+ ui->actionCopyFilters->setEnabled(true);
}
popup.exec(QCursor::pos());
@@ -5270,3 +5285,60 @@ bool OBSBasic::sysTrayMinimizeToTray()
return config_get_bool(GetGlobalConfig(),
"BasicWindow", "SysTrayMinimizeToTray");
}
+
+void OBSBasic::on_actionCopySource_triggered()
+{
+ on_actionCopyTransform_triggered();
+
+ OBSSceneItem item = GetCurrentSceneItem();
+
+ if (!item)
+ return;
+
+ OBSSource source = obs_sceneitem_get_source(item);
+
+ copyString = obs_source_get_name(source);
+ copyVisible = obs_sceneitem_visible(item);
+
+ ui->actionPasteRef->setEnabled(true);
+ ui->actionPasteDup->setEnabled(true);
+}
+
+void OBSBasic::on_actionPasteRef_triggered()
+{
+ OBSBasicSourceSelect::SourcePaste(copyString, copyVisible, false);
+ on_actionPasteTransform_triggered();
+}
+
+void OBSBasic::on_actionPasteDup_triggered()
+{
+ OBSBasicSourceSelect::SourcePaste(copyString, copyVisible, true);
+ on_actionPasteTransform_triggered();
+}
+
+void OBSBasic::on_actionCopyFilters_triggered()
+{
+ OBSSceneItem item = GetCurrentSceneItem();
+
+ if (!item)
+ return;
+
+ OBSSource source = obs_sceneitem_get_source(item);
+
+ copyFiltersString = obs_source_get_name(source);
+
+ ui->actionPasteFilters->setEnabled(true);
+}
+
+void OBSBasic::on_actionPasteFilters_triggered()
+{
+ OBSSource source = obs_get_source_by_name(copyFiltersString);
+ OBSSceneItem sceneItem = GetCurrentSceneItem();
+
+ OBSSource dstSource = obs_sceneitem_get_source(sceneItem);
+
+ if (source == dstSource)
+ return;
+
+ obs_source_copy_filters(dstSource, source);
+}
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index d89be1383..a4e18a87c 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -119,6 +119,10 @@ private:
bool projectChanged = false;
bool previewEnabled = true;
+ const char *copyString;
+ const char *copyFiltersString;
+ bool copyVisible = true;
+
QPointer updateCheckThread;
QPointer logUploadThread;
@@ -413,6 +417,13 @@ private slots:
void ToggleShowHide();
+ void on_actionCopySource_triggered();
+ void on_actionPasteRef_triggered();
+ void on_actionPasteDup_triggered();
+
+ void on_actionCopyFilters_triggered();
+ void on_actionPasteFilters_triggered();
+
private:
/* OBS Callbacks */
static void SceneReordered(void *data, calldata_t *params);
diff --git a/UI/window-basic-source-select.cpp b/UI/window-basic-source-select.cpp
index b66ba088d..de6db69fd 100644
--- a/UI/window-basic-source-select.cpp
+++ b/UI/window-basic-source-select.cpp
@@ -93,7 +93,28 @@ static void AddSource(void *_data, obs_scene_t *scene)
obs_sceneitem_set_visible(sceneitem, data->visible);
}
-static void AddExisting(const char *name, const bool visible)
+static char *get_new_source_name(const char *name)
+{
+ struct dstr new_name = {0};
+ int inc = 0;
+
+ dstr_copy(&new_name, name);
+
+ for (;;) {
+ obs_source_t *existing_source = obs_get_source_by_name(
+ new_name.array);
+ if (!existing_source)
+ break;
+
+ obs_source_release(existing_source);
+
+ dstr_printf(&new_name, "%s %d", name, ++inc + 1);
+ }
+
+ return new_name.array;
+}
+
+static void AddExisting(const char *name, bool visible, bool duplicate)
{
OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
OBSScene scene = main->GetCurrentScene();
@@ -102,6 +123,17 @@ static void AddExisting(const char *name, const bool visible)
obs_source_t *source = obs_get_source_by_name(name);
if (source) {
+ if (duplicate) {
+ obs_source_t *from = source;
+ char *new_name = get_new_source_name(name);
+ source = obs_source_duplicate(from, new_name, false);
+ bfree(new_name);
+ obs_source_release(from);
+
+ if (!source)
+ return;
+ }
+
AddSourceData data;
data.source = source;
data.visible = visible;
@@ -155,7 +187,7 @@ void OBSBasicSourceSelect::on_buttonBox_accepted()
if (!item)
return;
- AddExisting(QT_TO_UTF8(item->text()), visible);
+ AddExisting(QT_TO_UTF8(item->text()), visible, false);
} else {
if (ui->sourceName->text().isEmpty()) {
QMessageBox::information(this,
@@ -243,3 +275,8 @@ OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_)
obs_enum_sources(EnumSources, this);
}
}
+
+void OBSBasicSourceSelect::SourcePaste(const char *name, bool visible, bool dup)
+{
+ AddExisting(name, visible, dup);
+}
diff --git a/UI/window-basic-source-select.hpp b/UI/window-basic-source-select.hpp
index bdfdc21e1..7bab4768c 100644
--- a/UI/window-basic-source-select.hpp
+++ b/UI/window-basic-source-select.hpp
@@ -47,4 +47,6 @@ public:
OBSBasicSourceSelect(OBSBasic *parent, const char *id);
OBSSource newSource;
+
+ static void SourcePaste(const char *name, bool visible, bool duplicate);
};