diff --git a/CMakeLists.txt b/CMakeLists.txt index e58e4a14..1ea86f78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,8 @@ target_sources( ${LIB_NAME} PRIVATE src/macro-core/macro-action-audio.cpp src/macro-core/macro-action-audio.hpp + src/macro-core/macro-action-clipboard.cpp + src/macro-core/macro-action-clipboard.hpp src/macro-core/macro-action-edit.cpp src/macro-core/macro-action-edit.hpp src/macro-core/macro-action-file.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index f2439121..091edc12 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -874,6 +874,12 @@ AdvSceneSwitcher.action.twitch.announcement.blue="Blue" AdvSceneSwitcher.action.twitch.announcement.green="Green" AdvSceneSwitcher.action.twitch.announcement.orange="Orange" AdvSceneSwitcher.action.twitch.announcement.purple="Purple" +AdvSceneSwitcher.action.clipboard="Clipboard" +AdvSceneSwitcher.action.clipboard.type.copy.text="Copy text" +AdvSceneSwitcher.action.clipboard.type.copy.image="Copy image" +AdvSceneSwitcher.action.clipboard.copy.text.text.placeholder="Enter text" +AdvSceneSwitcher.action.clipboard.copy.image.url.placeholder="Enter direct image URL" +AdvSceneSwitcher.action.clipboard.copy.image.url.tooltip="Currently supported formats: PNG, JPG/JPEG, BMP, GIF." ; Hotkey AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher" @@ -1442,6 +1448,11 @@ AdvSceneSwitcher.tempVar.audio.sync_offset="Source audio sync offset" AdvSceneSwitcher.tempVar.audio.monitor="Source audio monitor type" AdvSceneSwitcher.tempVar.audio.balance="Source audio balance" +AdvSceneSwitcher.tempVar.clipboard.mimeType.primary="Primary clipboard item MIME type" +AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description="Highest priority MIME type of the current item stored in clipboard, if available." +AdvSceneSwitcher.tempVar.clipboard.mimeType.all="All clipboard item MIME types" +AdvSceneSwitcher.tempVar.clipboard.mimeType.all.description="All MIME types of the current item stored in clipboard separated with space, if available." + AdvSceneSwitcher.tempVar.scene.current="Current scene" AdvSceneSwitcher.tempVar.scene.previous="Previous scene" AdvSceneSwitcher.tempVar.scene.preview="Preview scene" diff --git a/src/macro-core/macro-action-clipboard.cpp b/src/macro-core/macro-action-clipboard.cpp new file mode 100644 index 00000000..b58d8ef7 --- /dev/null +++ b/src/macro-core/macro-action-clipboard.cpp @@ -0,0 +1,257 @@ +#include "macro-action-clipboard.hpp" +#include "curl-helper.hpp" +#include "switcher-data.hpp" + +#include +#include +#include +#include + +namespace advss { + +const std::string MacroActionClipboard::id = "clipboard"; + +bool MacroActionClipboard::_registered = MacroActionFactory::Register( + MacroActionClipboard::id, + {MacroActionClipboard::Create, MacroActionClipboardEdit::Create, + "AdvSceneSwitcher.action.clipboard"}); + +const static std::map actionTypes = { + {MacroActionClipboard::Action::COPY_TEXT, + "AdvSceneSwitcher.action.clipboard.type.copy.text"}, + {MacroActionClipboard::Action::COPY_IMAGE, + "AdvSceneSwitcher.action.clipboard.type.copy.image"}, +}; + +static size_t writeCallback(void *ptr, size_t size, size_t nmemb, + QByteArray *buffer) +{ + buffer->append((char *)ptr, nmemb); + return size * nmemb; +} + +static std::optional getImageFromUrl(const char *url) +{ + QByteArray response; + + switcher->curl.SetOpt(CURLOPT_URL, url); + switcher->curl.SetOpt(CURLOPT_HTTPGET, 1L); + switcher->curl.SetOpt(CURLOPT_TIMEOUT_MS, 30000); + switcher->curl.SetOpt(CURLOPT_WRITEFUNCTION, writeCallback); + switcher->curl.SetOpt(CURLOPT_WRITEDATA, &response); + auto code = switcher->curl.Perform(); + + if (code != CURLE_OK) { + blog(LOG_WARNING, + "Retrieving image failed in %s with error: %s", __func__, + switcher->curl.GetError(code)); + return {}; + } + + return QImage::fromData(response); +} + +static void setMimeTypeParams(ClipboardQueueParams *params, + QClipboard *clipboard) +{ + auto mimeData = clipboard->mimeData(); + if (!mimeData) { + return; + } + auto mimeTypeList = mimeData->formats(); + if (mimeTypeList.empty()) { + return; + } + params->mimeTypePrimary = mimeTypeList.first().toStdString(); + params->mimeTypeAll = mimeTypeList.join(" ").toStdString(); +} + +static void copyText(void *param) +{ + auto params = static_cast(param); + QClipboard *clipboard = QApplication::clipboard(); + + clipboard->setText(params->text.c_str()); + + setMimeTypeParams(params, clipboard); +} + +static void copyImageFromUrl(void *param) +{ + auto params = static_cast(param); + auto url = params->url.c_str(); + auto image = getImageFromUrl(url); + + if (!image || (*image).isNull()) { + blog(LOG_WARNING, "Failed to convert %s URL to image!", url); + return; + } + + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setImage(*image); + + setMimeTypeParams(params, clipboard); +} + +bool MacroActionClipboard::PerformAction() +{ + switch (_action) { + case Action::COPY_TEXT: { + ClipboardTextQueueParams params{"", "", _text}; + obs_queue_task(OBS_TASK_UI, copyText, ¶ms, true); + SetTempVarValues(params); + break; + } + case Action::COPY_IMAGE: { + ClipboardImageQueueParams params{"", "", _url}; + obs_queue_task(OBS_TASK_UI, copyImageFromUrl, ¶ms, true); + SetTempVarValues(params); + break; + } + default: + break; + } + + return true; +} + +void MacroActionClipboard::SetupTempVars() +{ + MacroAction::SetupTempVars(); + + AddTempvar( + "mimeType.primary", + obs_module_text( + "AdvSceneSwitcher.tempVar.clipboard.mimeType.primary"), + obs_module_text( + "AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description")); + AddTempvar( + "mimeType.all", + obs_module_text( + "AdvSceneSwitcher.tempVar.clipboard.mimeType.all"), + obs_module_text( + "AdvSceneSwitcher.tempVar.clipboard.mimeType.all.description")); +} + +void MacroActionClipboard::SetTempVarValues(const ClipboardQueueParams ¶ms) +{ + SetTempVarValue("mimeType.primary", params.mimeTypePrimary); + SetTempVarValue("mimeType.all", params.mimeTypeAll); +} + +bool MacroActionClipboard::Save(obs_data_t *obj) const +{ + MacroAction::Save(obj); + obs_data_set_int(obj, "action", static_cast(_action)); + _text.Save(obj, "text"); + _url.Save(obj, "url"); + + return true; +} + +bool MacroActionClipboard::Load(obs_data_t *obj) +{ + MacroAction::Load(obj); + _action = static_cast(obs_data_get_int(obj, "action")); + _text.Load(obj, "text"); + _url.Load(obj, "url"); + + return true; +} + +static inline void populateActionSelection(QComboBox *list) +{ + for (const auto &[action, name] : actionTypes) { + list->addItem(obs_module_text(name.c_str()), + static_cast(action)); + } +} + +MacroActionClipboardEdit::MacroActionClipboardEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _actions(new FilterComboBox(this)), + _text(new VariableLineEdit(this)), + _url(new VariableLineEdit(this)) +{ + populateActionSelection(_actions); + _url->setToolTip(obs_module_text( + "AdvSceneSwitcher.action.clipboard.copy.image.url.tooltip")); + + QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this, + SLOT(ActionChanged(int))); + QWidget::connect(_text, SIGNAL(editingFinished()), this, + SLOT(TextChanged())); + QWidget::connect(_url, SIGNAL(editingFinished()), this, + SLOT(UrlChanged())); + + auto mainLayout = new QHBoxLayout(); + mainLayout->addWidget(_actions); + mainLayout->addWidget(_text); + mainLayout->addWidget(_url); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroActionClipboardEdit::ActionChanged(int index) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_action = static_cast( + _actions->itemData(index).toInt()); + + SetWidgetVisibility(); +} + +void MacroActionClipboardEdit::TextChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_text = _text->text().toStdString(); +} + +void MacroActionClipboardEdit::UrlChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_url = _url->text().toStdString(); +} + +void MacroActionClipboardEdit::SetWidgetVisibility() +{ + _text->setVisible(_entryData->_action == + MacroActionClipboard::Action::COPY_TEXT); + _url->setVisible(_entryData->_action == + MacroActionClipboard::Action::COPY_IMAGE); + + adjustSize(); + updateGeometry(); +} + +void MacroActionClipboardEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _actions->setCurrentIndex( + _actions->findData(static_cast(_entryData->_action))); + _text->setText(_entryData->_text); + _url->setText(_entryData->_url); + + SetWidgetVisibility(); +} + +} // namespace advss diff --git a/src/macro-core/macro-action-clipboard.hpp b/src/macro-core/macro-action-clipboard.hpp new file mode 100644 index 00000000..c7f22f5d --- /dev/null +++ b/src/macro-core/macro-action-clipboard.hpp @@ -0,0 +1,89 @@ +#pragma once +#include "macro-action-edit.hpp" +#include "filter-combo-box.hpp" + +#include + +namespace advss { + +struct ClipboardQueueParams { + std::string mimeTypePrimary; + std::string mimeTypeAll; +}; + +struct ClipboardTextQueueParams : ClipboardQueueParams { + StringVariable text; +}; + +struct ClipboardImageQueueParams : ClipboardQueueParams { + StringVariable url; +}; + +class MacroActionClipboard : public MacroAction { +public: + MacroActionClipboard(Macro *m) : MacroAction(m) {} + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + std::string GetId() const { return id; }; + + bool PerformAction(); + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + + enum class Action { + COPY_TEXT, + COPY_IMAGE, + }; + + Action _action = Action::COPY_TEXT; + + StringVariable _text = obs_module_text( + "AdvSceneSwitcher.action.clipboard.copy.text.text.placeholder"); + StringVariable _url = obs_module_text( + "AdvSceneSwitcher.action.clipboard.copy.image.url.placeholder"); + +private: + void SetupTempVars(); + void SetTempVarValues(const ClipboardQueueParams ¶ms); + + static bool _registered; + static const std::string id; +}; + +class MacroActionClipboardEdit : public QWidget { + Q_OBJECT + +public: + MacroActionClipboardEdit( + QWidget *parent, + std::shared_ptr entryData = nullptr); + static QWidget *Create(QWidget *parent, + std::shared_ptr action) + { + return new MacroActionClipboardEdit( + parent, std::dynamic_pointer_cast( + action)); + } + void UpdateEntryData(); + +protected: + std::shared_ptr _entryData; + +private slots: + void ActionChanged(int index); + void TextChanged(); + void UrlChanged(); + +private: + void SetWidgetVisibility(); + + bool _loading = true; + + FilterComboBox *_actions; + VariableLineEdit *_text; + VariableLineEdit *_url; +}; + +} // namespace advss diff --git a/src/utils/curl-helper.cpp b/src/utils/curl-helper.cpp index ba125c95..11122002 100644 --- a/src/utils/curl-helper.cpp +++ b/src/utils/curl-helper.cpp @@ -50,6 +50,15 @@ CURLcode Curlhelper::Perform() return _perform(_curl); } +char *Curlhelper::GetError(CURLcode code) +{ + if (!_initialized) { + return (char *)"CURL initialization failed"; + } + + return _error(code); +} + bool Curlhelper::LoadLib() { _lib = new QLibrary(curl_library_name, nullptr); @@ -101,8 +110,10 @@ bool Curlhelper::Resolve() _slistAppend = (slistAppendFunction)_lib->resolve("curl_slist_append"); _perform = (performFunction)_lib->resolve("curl_easy_perform"); _cleanup = (cleanupFunction)_lib->resolve("curl_easy_cleanup"); + _error = (errorFunction)_lib->resolve("curl_easy_strerror"); - if (_init && _setopt && _slistAppend && _perform && _cleanup) { + if (_init && _setopt && _slistAppend && _perform && _cleanup && + _error) { blog(LOG_INFO, "[adv-ss] curl loaded successfully"); return true; } diff --git a/src/utils/curl-helper.hpp b/src/utils/curl-helper.hpp index 923f273a..560ab21a 100644 --- a/src/utils/curl-helper.hpp +++ b/src/utils/curl-helper.hpp @@ -5,22 +5,25 @@ namespace advss { typedef CURL *(*initFunction)(void); -typedef void (*cleanupFunction)(CURL *); typedef CURLcode (*setOptFunction)(CURL *, CURLoption, ...); typedef struct curl_slist *(*slistAppendFunction)(struct curl_slist *list, const char *string); typedef CURLcode (*performFunction)(CURL *); +typedef void (*cleanupFunction)(CURL *); +typedef char *(*errorFunction)(CURLcode); class Curlhelper { public: Curlhelper(); ~Curlhelper(); + bool Initialized() { return _initialized; } + template CURLcode SetOpt(CURLoption, Args...); struct curl_slist *SlistAppend(struct curl_slist *list, const char *string); CURLcode Perform(); - bool Initialized() { return _initialized; } + char *GetError(CURLcode code); private: bool LoadLib(); @@ -31,6 +34,7 @@ private: slistAppendFunction _slistAppend = nullptr; performFunction _perform = nullptr; cleanupFunction _cleanup = nullptr; + errorFunction _error = nullptr; CURL *_curl = nullptr; QLibrary *_lib; bool _initialized = false;