diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index eca37cf2..7e652e69 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -623,6 +623,15 @@ AdvSceneSwitcher.condition.queue.type.stopped="Stopped" AdvSceneSwitcher.condition.queue.type.size="Size" AdvSceneSwitcher.condition.queue.entry.startStop="Queue{{queues}}is{{conditions}}" AdvSceneSwitcher.condition.queue.entry.size="Queue{{queues}}{{conditions}}is less than{{size}}" +AdvSceneSwitcher.condition.clipboard="Clipboard" +AdvSceneSwitcher.condition.clipboard.placeholder="Clipboard text" +AdvSceneSwitcher.condition.clipboard.urlTooltip="Depending on the OS files might be represented as URLs also!" +AdvSceneSwitcher.condition.clipboard.condition.changed="Clipboard content changed" +AdvSceneSwitcher.condition.clipboard.condition.isText="Clipboard contains text" +AdvSceneSwitcher.condition.clipboard.condition.isImage="Clipboard contains an image" +AdvSceneSwitcher.condition.clipboard.condition.isURL="Clipboard contains an URL" +AdvSceneSwitcher.condition.clipboard.condition.matches="Clipboard content matches" +AdvSceneSwitcher.condition.clipboard.condition.entry="{{conditions}}{{regex}}{{urlInfo}}" ; Macro Actions AdvSceneSwitcher.action.scene="Switch scene" @@ -1639,6 +1648,9 @@ AdvSceneSwitcher.tempVar.streaming.keyframeInterval.description="Stream keyframe AdvSceneSwitcher.tempVar.streaming.durationSeconds="Stream duration" AdvSceneSwitcher.tempVar.streaming.durationSeconds.description="Seconds passed since the stream was started.\nThis value will be zero if the stream is stopped." +AdvSceneSwitcher.tempVar.clipboard.text="Clipboard text" +AdvSceneSwitcher.tempVar.clipboard.text.description="The text contained in the clipboard.\nWill be empty if the clipboard does not contain text." + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/plugins/base/CMakeLists.txt b/plugins/base/CMakeLists.txt index 9f2b8b7b..de4495f5 100644 --- a/plugins/base/CMakeLists.txt +++ b/plugins/base/CMakeLists.txt @@ -75,6 +75,8 @@ target_sources( macro-action-window.hpp macro-condition-audio.cpp macro-condition-audio.hpp + macro-condition-clipboard.cpp + macro-condition-clipboard.hpp macro-condition-cursor.cpp macro-condition-cursor.hpp macro-condition-date.cpp diff --git a/plugins/base/macro-condition-clipboard.cpp b/plugins/base/macro-condition-clipboard.cpp new file mode 100644 index 00000000..dc4cf8f0 --- /dev/null +++ b/plugins/base/macro-condition-clipboard.cpp @@ -0,0 +1,312 @@ +#include "macro-condition-clipboard.hpp" +#include "layout-helpers.hpp" +#include "ui-helpers.hpp" + +#include +#include +#include + +namespace advss { + +const std::string MacroConditionClipboard::id = "clipboard"; + +bool MacroConditionClipboard::_registered = MacroConditionFactory::Register( + MacroConditionClipboard::id, + {MacroConditionClipboard::Create, MacroConditionClipboardEdit::Create, + "AdvSceneSwitcher.condition.clipboard"}); + +const static std::map + conditionTypes = { + {MacroConditionClipboard::Condition::CHANGED, + "AdvSceneSwitcher.condition.clipboard.condition.changed"}, + {MacroConditionClipboard::Condition::IS_TEXT, + "AdvSceneSwitcher.condition.clipboard.condition.isText"}, + {MacroConditionClipboard::Condition::IS_IMAGE, + "AdvSceneSwitcher.condition.clipboard.condition.isImage"}, + {MacroConditionClipboard::Condition::IS_URL, + "AdvSceneSwitcher.condition.clipboard.condition.isURL"}, + {MacroConditionClipboard::Condition::MATCHES, + "AdvSceneSwitcher.condition.clipboard.condition.matches"}, +}; + +ClipboardMessageBuffer ClipboardListener::RegisterForClipboardChanges() +{ + return Instance()->_dispatcher.RegisterClient(); +} + +ClipboardListener *ClipboardListener::Instance() +{ + static ClipboardListener instance; + return &instance; +} + +ClipboardListener::ClipboardListener() : QObject(nullptr) +{ + if (!QApplication::clipboard()) { + return; + } + QObject::connect(QApplication::clipboard(), &QClipboard::dataChanged, + this, &ClipboardListener::ClipboardDataChanged); +} + +void ClipboardListener::ClipboardDataChanged() +{ + auto text = QApplication::clipboard()->text(); + _dispatcher.DispatchMessage(text.toStdString()); +} + +MacroConditionClipboard::MacroConditionClipboard(Macro *m) : MacroCondition(m) +{ + _messageBuffer = ClipboardListener::RegisterForClipboardChanges(); +} + +std::shared_ptr MacroConditionClipboard::Create(Macro *m) +{ + return std::make_shared(m); +} + +static bool clipboardContainsImage() +{ + const QClipboard *clipboard = QApplication::clipboard(); + const QMimeData *mimeData = clipboard->mimeData(); + return mimeData->hasImage(); +} + +static bool clipboardContainsText() +{ + const QClipboard *clipboard = QApplication::clipboard(); + const QMimeData *mimeData = clipboard->mimeData(); + return mimeData->hasText(); +} + +static bool clipboardContainsUrl() +{ + const QClipboard *clipboard = QApplication::clipboard(); + const QMimeData *mimeData = clipboard->mimeData(); + return mimeData->hasUrls(); +} + +static std::string getCurrentClipboardText() +{ + auto clipboard = QApplication::clipboard(); + if (!clipboard) { + return ""; + } + return clipboard->text().toStdString(); +} + +bool MacroConditionClipboard::CheckCondition() +{ + switch (_condition) { + case Condition::CHANGED: + while (!_messageBuffer->Empty()) { + auto message = _messageBuffer->ConsumeMessage(); + if (!message) { + continue; + } + SetTempVarValue("text", *message); + return true; + } + return false; + case Condition::IS_TEXT: + if (clipboardContainsText()) { + SetTempVarValue("text", getCurrentClipboardText()); + return true; + } + return false; + case Condition::IS_IMAGE: + return clipboardContainsImage(); + case Condition::IS_URL: + if (clipboardContainsUrl()) { + SetTempVarValue("text", getCurrentClipboardText()); + return true; + } + return false; + case Condition::MATCHES: + if (_regex.Enabled()) { + auto text = getCurrentClipboardText(); + if (_regex.Matches(text, _text)) { + SetTempVarValue("text", text); + return true; + } + } else { + auto text = getCurrentClipboardText(); + if (text == std::string(_text)) { + SetTempVarValue("text", text); + return true; + } + } + return false; + default: + break; + } + + return false; +} + +void MacroConditionClipboard::SetCondition(Condition condition) +{ + _condition = condition; + _messageBuffer.reset(); + if (_condition == Condition::CHANGED) { + _messageBuffer = + ClipboardListener::RegisterForClipboardChanges(); + } +} + +void MacroConditionClipboard::SetupTempVars() +{ + MacroCondition::SetupTempVars(); + + AddTempvar( + "text", + obs_module_text("AdvSceneSwitcher.tempVar.clipboard.text"), + obs_module_text( + "AdvSceneSwitcher.tempVar.clipboard.text.description")); +} + +bool MacroConditionClipboard::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + obs_data_set_int(obj, "condition", static_cast(_condition)); + _text.Save(obj, "text"); + _regex.Save(obj); + return true; +} + +bool MacroConditionClipboard::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _text.Load(obj, "text"); + _regex.Load(obj); + SetCondition( + static_cast(obs_data_get_int(obj, "condition"))); + return true; +} + +static void populateCompareModeselection(QComboBox *list) +{ + for (const auto &[_, name] : conditionTypes) { + list->addItem(obs_module_text(name.c_str())); + } +} + +MacroConditionClipboardEdit::MacroConditionClipboardEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _conditions(new QComboBox(this)), + _text(new VariableTextEdit(this)), + _regex(new RegexConfigWidget(parent)), + _urlInfo(new QLabel()) +{ + populateCompareModeselection(_conditions); + + QString path = GetThemeTypeName() == "Light" + ? ":/res/images/help.svg" + : ":/res/images/help_light.svg"; + QIcon icon(path); + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + _urlInfo->setPixmap(pixmap); + _urlInfo->hide(); + _urlInfo->setToolTip(obs_module_text( + "AdvSceneSwitcher.condition.clipboard.urlTooltip")); + + QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this, + SLOT(ConditionChanged(int))); + QWidget::connect(_text, SIGNAL(textChanged()), this, + SLOT(TextChanged())); + QWidget::connect(_regex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(RegexChanged(const RegexConfig &))); + + auto layout = new QHBoxLayout(); + PlaceWidgets( + obs_module_text( + "AdvSceneSwitcher.condition.clipboard.condition.entry"), + layout, + {{"{{conditions}}", _conditions}, + {"{{regex}}", _regex}, + {"{{urlInfo}}", _urlInfo}}); + + auto mainLayout = new QVBoxLayout(); + mainLayout->addLayout(layout); + mainLayout->addWidget(_text); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +QWidget * +MacroConditionClipboardEdit::Create(QWidget *parent, + std::shared_ptr action) +{ + return new MacroConditionClipboardEdit( + parent, + std::dynamic_pointer_cast(action)); +} + +void MacroConditionClipboardEdit::ConditionChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->SetCondition( + static_cast(value)); + SetWidgetVisibility(); +} + +void MacroConditionClipboardEdit::TextChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_text = _text->toPlainText().toStdString(); + adjustSize(); + updateGeometry(); +} + +void MacroConditionClipboardEdit::RegexChanged(const RegexConfig &conf) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_regex = conf; + adjustSize(); + updateGeometry(); +} + +void MacroConditionClipboardEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _conditions->setCurrentIndex( + static_cast(_entryData->GetCondition())); + _text->setPlainText(_entryData->_text); + _regex->SetRegexConfig(_entryData->_regex); + SetWidgetVisibility(); +} + +void MacroConditionClipboardEdit::SetWidgetVisibility() +{ + const bool requiresTextInput = + _entryData->GetCondition() == + MacroConditionClipboard::Condition::MATCHES; + _regex->setVisible(requiresTextInput); + _text->setVisible(requiresTextInput); + _urlInfo->setVisible(_entryData->GetCondition() == + MacroConditionClipboard::Condition::IS_URL); + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/plugins/base/macro-condition-clipboard.hpp b/plugins/base/macro-condition-clipboard.hpp new file mode 100644 index 00000000..ec4e9994 --- /dev/null +++ b/plugins/base/macro-condition-clipboard.hpp @@ -0,0 +1,92 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "filter-combo-box.hpp" +#include "message-buffer.hpp" +#include "message-dispatcher.hpp" +#include "regex-config.hpp" +#include "variable-text-edit.hpp" + +namespace advss { + +using ClipboardMessageBuffer = std::shared_ptr>; +using ClipboardMessageDispatcher = MessageDispatcher; + +class ClipboardListener : public QObject { + Q_OBJECT +public: + [[nodiscard]] static ClipboardMessageBuffer + RegisterForClipboardChanges(); + +private slots: + void ClipboardDataChanged(); + +private: + ClipboardListener(); + static ClipboardListener *Instance(); + + ClipboardMessageDispatcher _dispatcher; +}; + +class MacroConditionClipboard : public MacroCondition { +public: + MacroConditionClipboard(Macro *m); + static std::shared_ptr Create(Macro *m); + std::string GetId() const { return id; }; + bool CheckCondition(); + + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + + enum class Condition { + CHANGED, + IS_TEXT, + IS_IMAGE, + IS_URL, + MATCHES, + }; + void SetCondition(Condition); + Condition GetCondition() const { return _condition; } + + StringVariable _text = obs_module_text( + "AdvSceneSwitcher.condition.clipboard.placeholder"); + RegexConfig _regex; + +private: + void SetupTempVars(); + + Condition _condition = Condition::CHANGED; + ClipboardMessageBuffer _messageBuffer; + + static bool _registered; + static const std::string id; +}; + +class MacroConditionClipboardEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionClipboardEdit( + QWidget *parent, + std::shared_ptr entryData = nullptr); + static QWidget *Create(QWidget *parent, + std::shared_ptr action); + +private slots: + void ConditionChanged(int); + void TextChanged(); + void RegexChanged(const RegexConfig &conf); + +private: + void UpdateEntryData(); + void SetWidgetVisibility(); + + QComboBox *_conditions; + VariableTextEdit *_text; + RegexConfigWidget *_regex; + QLabel *_urlInfo; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss