diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake index 387385da8d2f0d..9690bc4270662a 100644 --- a/frontend/cmake/ui-utility.cmake +++ b/frontend/cmake/ui-utility.cmake @@ -28,6 +28,11 @@ target_sources( utility/MultitrackVideoOutput.cpp utility/MultitrackVideoOutput.hpp utility/NativeEventFilter.hpp + utility/OBSClipboardService.hpp + utility/OBSClipboardService.cpp + utility/OBSClipboardSerializer.hpp + utility/OBSClipboardSerializer.cpp + utility/OBSClipboardMimeTypes.hpp utility/OBSCanvas.cpp utility/OBSCanvas.hpp utility/OBSEventFilter.hpp diff --git a/frontend/utility/OBSClipboardMimeTypes.hpp b/frontend/utility/OBSClipboardMimeTypes.hpp new file mode 100644 index 00000000000000..4b567782661362 --- /dev/null +++ b/frontend/utility/OBSClipboardMimeTypes.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace OBSClipboard { +inline constexpr char SceneItems[] = "application/x-obs-studio-scene-items+json"; +inline constexpr char SourceFilters[] = "application/x-obs-studio-source-filters+json"; +inline constexpr char SceneItemTransform[] = "application/x-obs-studio-scene-item-transform+json"; +inline constexpr char SceneItemTransition[] = "application/x-obs-studio-scene-item-transition+json"; + +inline constexpr int PayloadVersion = 1; +} // namespace OBSClipboard diff --git a/frontend/utility/OBSClipboardSerializer.cpp b/frontend/utility/OBSClipboardSerializer.cpp new file mode 100644 index 00000000000000..c5a9f0c4f6399c --- /dev/null +++ b/frontend/utility/OBSClipboardSerializer.cpp @@ -0,0 +1,124 @@ +#include "OBSClipboardSerializer.hpp" + +OBSData OBSClipboardSerializer::SerializeSceneItem(OBSSceneItem item) +{ + if (!item) { + return {}; + } + + OBSSource source = obs_sceneitem_get_source(item); + if (!source) { + return {}; + } + OBSDataArrayAutoRelease items = obs_data_array_create(); + obs_sceneitem_save(item, items); + + OBSDataAutoRelease sourceData = obs_save_source(source); + if (!sourceData) { + return {}; + } + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "items", items); + obs_data_set_obj(data, "source", sourceData); + obs_data_set_int(data, "output_flags", obs_source_get_output_flags(source)); + + return OBSData(data); +} + +OBSData OBSClipboardSerializer::SerializeFilters(OBSSource source) +{ + if (!source) { + return {}; + } + OBSDataArrayAutoRelease filters = obs_source_backup_filters(source); + if (!filters) { + return {}; + } + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "filters", filters); + + return OBSData(data); +} + +OBSData OBSClipboardSerializer::SerializeTransform(OBSSceneItem item) +{ + if (!item) { + return {}; + } + + obs_transform_info transform; + obs_sceneitem_crop crop; + + obs_sceneitem_get_info2(item, &transform); + obs_sceneitem_get_crop(item, &crop); + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_vec2(data, "pos", &transform.pos); + obs_data_set_double(data, "rot", transform.rot); + obs_data_set_vec2(data, "scale", &transform.scale); + obs_data_set_int(data, "alignment", transform.alignment); + obs_data_set_int(data, "bounds_type", transform.bounds_type); + obs_data_set_int(data, "bounds_alignment", transform.bounds_alignment); + obs_data_set_vec2(data, "bounds", &transform.bounds); + obs_data_set_bool(data, "crop_to_bounds", transform.crop_to_bounds); + + obs_data_set_int(data, "crop_left", crop.left); + obs_data_set_int(data, "crop_top", crop.top); + obs_data_set_int(data, "crop_right", crop.right); + obs_data_set_int(data, "crop_bottom", crop.bottom); + + return OBSData(data); +} + +OBSData OBSClipboardSerializer::SerializeTransition(OBSSceneItem item, bool show) +{ + if (!item) { + return {}; + } + OBSDataAutoRelease data = obs_sceneitem_transition_save(item, show); + return OBSData(data); +} + +bool OBSClipboardSerializer::DeserializeSceneItem(obs_data_t *data, OBSDataArrayAutoRelease &items, + OBSDataAutoRelease &source, uint32_t &outputFlags) +{ + if (!data) { + return false; + } + items = obs_data_get_array(data, "items"); + source = obs_data_get_obj(data, "source"); + outputFlags = static_cast(obs_data_get_int(data, "output_flags")); + return !!items && obs_data_array_count(items) && !!source; +} + +bool OBSClipboardSerializer::DeserializeFilters(obs_data_t *data, OBSDataArrayAutoRelease &filters) +{ + if (!data) { + return false; + } + filters = obs_data_get_array(data, "filters"); + return !!filters; +} + +bool OBSClipboardSerializer::DeserializeTransform(obs_data_t *data, obs_transform_info &transform, + obs_sceneitem_crop &crop) +{ + if (!data) { + return false; + } + obs_data_get_vec2(data, "pos", &transform.pos); + transform.rot = obs_data_get_double(data, "rot"); + obs_data_get_vec2(data, "scale", &transform.scale); + transform.alignment = obs_data_get_int(data, "alignment"); + transform.bounds_type = static_cast(obs_data_get_int(data, "bounds_type")); + transform.bounds_alignment = obs_data_get_int(data, "bounds_alignment"); + obs_data_get_vec2(data, "bounds", &transform.bounds); + transform.crop_to_bounds = obs_data_get_bool(data, "crop_to_bounds"); + + crop.left = obs_data_get_int(data, "crop_left"); + crop.top = obs_data_get_int(data, "crop_top"); + crop.right = obs_data_get_int(data, "crop_right"); + crop.bottom = obs_data_get_int(data, "crop_bottom"); + + return true; +} diff --git a/frontend/utility/OBSClipboardSerializer.hpp b/frontend/utility/OBSClipboardSerializer.hpp new file mode 100644 index 00000000000000..11045be63aaa06 --- /dev/null +++ b/frontend/utility/OBSClipboardSerializer.hpp @@ -0,0 +1,33 @@ +/****************************************************************************** + Copyright (C) 2026 by Kunal Dubey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class OBSClipboardSerializer { +public: + static OBSData SerializeSceneItem(OBSSceneItem item); + static OBSData SerializeFilters(OBSSource source); + static OBSData SerializeTransform(OBSSceneItem item); + static OBSData SerializeTransition(OBSSceneItem item, bool show); + + static bool DeserializeSceneItem(obs_data_t *data, OBSDataArrayAutoRelease &items, OBSDataAutoRelease &source, + uint32_t &outputFlags); + static bool DeserializeFilters(obs_data_t *data, OBSDataArrayAutoRelease &filters); + static bool DeserializeTransform(obs_data_t *data, obs_transform_info &transform, obs_sceneitem_crop &crop); +}; diff --git a/frontend/utility/OBSClipboardService.cpp b/frontend/utility/OBSClipboardService.cpp new file mode 100644 index 00000000000000..5cfe366cbca2ad --- /dev/null +++ b/frontend/utility/OBSClipboardService.cpp @@ -0,0 +1,353 @@ +#include "OBSClipboardService.hpp" + +#include "OBSClipboardMimeTypes.hpp" +#include "OBSClipboardSerializer.hpp" + +#include +#include +#include + +#include +#include + +namespace { +OBSDataAutoRelease CloneData(obs_data_t *data) +{ + if (!data) { + return {}; + } + + return obs_data_create_from_json(obs_data_get_json(data)); +} + +OBSSourceAutoRelease ResolveReferencedSource(obs_data_t *sourceData) +{ + const char *uuid = obs_data_get_string(sourceData, "uuid"); + if (uuid && *uuid) { + return obs_get_source_by_uuid(uuid); + } + + const char *name = obs_data_get_string(sourceData, "name"); + if (name && *name) { + return obs_get_source_by_name(name); + } + + return {}; +} + +bool CanLoadDuplicateSource(obs_data_t *sourceData) +{ + const char *id = obs_data_get_string(sourceData, "id"); + return id && *id; +} + +std::string GetUniqueFilterName(obs_source_t *destination, const char *name) +{ + std::string uniqueName = name; + int suffix = 1; + + for (;;) { + OBSSourceAutoRelease existing = obs_source_get_filter_by_name(destination, uniqueName.c_str()); + if (!existing) { + break; + } + uniqueName = std::string(name) + " " + std::to_string(++suffix); + } + + return uniqueName; +} + +void PrepareRootSceneItem(obs_data_array_t *items, obs_source_t *source = nullptr) +{ + const size_t count = obs_data_array_count(items); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease item = obs_data_array_item(items, i); + + if (obs_data_get_bool(item, "group_item_backup")) { + continue; + } + + obs_data_erase(item, "id"); + + if (source) { + obs_data_set_string(item, "name", obs_source_get_name(source)); + obs_data_set_string(item, "source_uuid", obs_source_get_uuid(source)); + } + + return; + } +} +} // namespace + +bool OBSClipboardService::canPasteSceneItems(bool duplicate) const +{ + OBSData payload = getMimeData(OBSClipboard::SceneItems); + if (!payload) { + return false; + } + + OBSDataArrayAutoRelease serializedItems = obs_data_get_array(payload, "items"); + if (!serializedItems) { + return false; + } + + const size_t count = obs_data_array_count(serializedItems); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease serializedItem = obs_data_array_item(serializedItems, i); + + OBSDataArrayAutoRelease items; + OBSDataAutoRelease sourceData; + uint32_t outputFlags = 0; + + if (!OBSClipboardSerializer::DeserializeSceneItem(serializedItem, items, sourceData, outputFlags)) { + continue; + } + + const bool requiresReference = !duplicate || (outputFlags & OBS_SOURCE_DO_NOT_DUPLICATE); + + if ((!requiresReference && CanLoadDuplicateSource(sourceData)) || + (requiresReference && ResolveReferencedSource(sourceData))) { + return true; + } + } + + return false; +} + +bool OBSClipboardService::canPasteFilters() const +{ + return !!getMimeData(OBSClipboard::SourceFilters); +} + +bool OBSClipboardService::canPasteTransform() const +{ + return !!getMimeData(OBSClipboard::SceneItemTransform); +} + +bool OBSClipboardService::canPasteTransition() const +{ + return !!getMimeData(OBSClipboard::SceneItemTransition); +} + +void OBSClipboardService::copySceneItems(const std::vector &items) +{ + OBSDataArrayAutoRelease serializedItems = obs_data_array_create(); + for (OBSSceneItem item : items) { + OBSData data = OBSClipboardSerializer::SerializeSceneItem(item); + if (data) { + obs_data_array_push_back(serializedItems, data); + } + } + + if (!obs_data_array_count(serializedItems)) { + return; + } + + OBSDataAutoRelease payload = obs_data_create(); + obs_data_set_array(payload, "items", serializedItems); + setMimeData(OBSClipboard::SceneItems, payload); +} + +void OBSClipboardService::copyFilters(OBSSource source) +{ + if (!source) { + return; + } + OBSData payload = OBSClipboardSerializer::SerializeFilters(source); + setMimeData(OBSClipboard::SourceFilters, payload); +} + +void OBSClipboardService::copyTransform(OBSSceneItem item) +{ + if (!item) { + return; + } + OBSData payload = OBSClipboardSerializer::SerializeTransform(item); + setMimeData(OBSClipboard::SceneItemTransform, payload); +} + +void OBSClipboardService::copyTransition(OBSSceneItem item, bool show) +{ + OBSData payload = OBSClipboardSerializer::SerializeTransition(item, show); + setMimeData(OBSClipboard::SceneItemTransition, payload); +} + +void OBSClipboardService::pasteSceneItems(OBSScene scene, bool duplicate) +{ + if (!scene) { + return; + } + OBSData payload = getMimeData(OBSClipboard::SceneItems); + if (!payload) { + return; + } + OBSDataArrayAutoRelease serializedItems = obs_data_get_array(payload, "items"); + if (!serializedItems) { + return; + } + + for (size_t i = obs_data_array_count(serializedItems); i > 0; i--) { + OBSDataAutoRelease serializedItem = obs_data_array_item(serializedItems, i - 1); + OBSDataAutoRelease copy = CloneData(serializedItem); + if (!copy) { + continue; + } + + OBSDataArrayAutoRelease items; + OBSDataAutoRelease sourceData; + uint32_t outputFlags = 0; + + if (!OBSClipboardSerializer::DeserializeSceneItem(copy, items, sourceData, outputFlags)) { + continue; + } + + const bool useReference = !duplicate || (outputFlags & OBS_SOURCE_DO_NOT_DUPLICATE); + + if (useReference) { + OBSSourceAutoRelease source = ResolveReferencedSource(sourceData); + if (!source) { + continue; + } + + const char *name = obs_source_get_name(source); + + if (obs_scene_get_group(scene, name)) { + continue; + } + + PrepareRootSceneItem(items); + obs_sceneitems_add(scene, items); + continue; + } + + obs_data_erase(sourceData, "uuid"); + + if (!CanLoadDuplicateSource(sourceData)) { + continue; + } + + OBSSourceAutoRelease source = obs_load_source(sourceData); + if (!source) { + continue; + } + + obs_source_load2(source); + PrepareRootSceneItem(items, source); + obs_sceneitems_add(scene, items); + } +} + +void OBSClipboardService::pasteFilters(OBSSource destination) +{ + if (!destination) { + return; + } + OBSData payload = getMimeData(OBSClipboard::SourceFilters); + if (!payload) { + return; + } + OBSDataArrayAutoRelease filters; + if (!OBSClipboardSerializer::DeserializeFilters(payload, filters)) { + return; + } + + for (size_t i = obs_data_array_count(filters); i > 0; i--) { + OBSDataAutoRelease serializedFilter = obs_data_array_item(filters, i - 1); + OBSDataAutoRelease filterData = CloneData(serializedFilter); + if (!filterData) { + continue; + } + + const std::string name = GetUniqueFilterName(destination, obs_data_get_string(filterData, "name")); + obs_data_set_string(filterData, "name", name.c_str()); + obs_data_erase(filterData, "uuid"); + OBSSourceAutoRelease filter = obs_load_private_source(filterData); + if (filter) { + obs_source_filter_add(destination, filter); + } + } +} + +void OBSClipboardService::pasteTransform(const std::vector &items) +{ + OBSData payload = getMimeData(OBSClipboard::SceneItemTransform); + if (!payload) { + return; + } + + obs_transform_info transform = {}; + obs_sceneitem_crop crop = {}; + + if (!OBSClipboardSerializer::DeserializeTransform(payload, transform, crop)) + return; + + for (OBSSceneItem item : items) { + if (!item || obs_sceneitem_locked(item)) + continue; + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &transform); + obs_sceneitem_set_crop(item, &crop); + obs_sceneitem_defer_update_end(item); + } +} + +void OBSClipboardService::pasteTransition(const std::vector &items, bool show) +{ + OBSData payload = getMimeData(OBSClipboard::SceneItemTransition); + if (!payload) { + return; + } + for (OBSSceneItem item : items) { + if (item) { + obs_sceneitem_transition_load(item, payload, show); + } + } +} + +void OBSClipboardService::setMimeData(const char *mimeType, obs_data_t *payload) +{ + if (!mimeType) { + return; + } + if (!payload) { + return; + } + QMimeData *mimeData = new QMimeData(); + + OBSDataAutoRelease envelope = obs_data_create(); + obs_data_set_int(envelope, "version", OBSClipboard::PayloadVersion); + obs_data_set_string(envelope, "type", mimeType); + obs_data_set_string(envelope, "created_with", obs_get_version_string()); + obs_data_set_obj(envelope, "payload", payload); + + mimeData->setData(mimeType, QByteArray(obs_data_get_json(envelope))); + + QApplication::clipboard()->setMimeData(mimeData); +} + +OBSData OBSClipboardService::getMimeData(const char *mimeType) const +{ + if (!mimeType) { + return {}; + } + const QMimeData *mimeData = QApplication::clipboard()->mimeData(); + if (!mimeData || !mimeData->hasFormat(mimeType)) { + return {}; + } + QByteArray data = mimeData->data(mimeType); + OBSDataAutoRelease envelope = obs_data_create_from_json(data.constData()); + if (!envelope) { + return {}; + } + if (obs_data_get_int(envelope, "version") != OBSClipboard::PayloadVersion) { + return {}; + } + if (strcmp(obs_data_get_string(envelope, "type"), mimeType) != 0) { + return {}; + } + OBSDataAutoRelease payload = obs_data_get_obj(envelope, "payload"); + + return OBSData(payload); +} diff --git a/frontend/utility/OBSClipboardService.hpp b/frontend/utility/OBSClipboardService.hpp new file mode 100644 index 00000000000000..11ab7316a20acc --- /dev/null +++ b/frontend/utility/OBSClipboardService.hpp @@ -0,0 +1,43 @@ +/****************************************************************************** + Copyright (C) 2026 by Kunal Dubey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +class OBSClipboardService { +public: + bool canPasteSceneItems(bool duplicate) const; + bool canPasteFilters() const; + bool canPasteTransform() const; + bool canPasteTransition() const; + + void copySceneItems(const std::vector &items); + void copyFilters(OBSSource source); + void copyTransform(OBSSceneItem item); + void copyTransition(OBSSceneItem item, bool show); + + void pasteSceneItems(OBSScene scene, bool duplicate); + void pasteFilters(OBSSource destination); + void pasteTransform(const std::vector &items); + void pasteTransition(const std::vector &items, bool show); + +private: + void setMimeData(const char *mimeType, obs_data_t *payload); + OBSData getMimeData(const char *mimeType) const; +};