diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 8b198fe7..065c8d5b 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -745,6 +745,15 @@ AdvSceneSwitcher.condition.usb.vendorName="Vendor Name:" AdvSceneSwitcher.condition.usb.productName="Product Name:" AdvSceneSwitcher.condition.usb.serialNumber="Serial Number:" AdvSceneSwitcher.condition.noDevicesFoundWarning="No USB devices detected!\nThe plugin might not have the required permissions to check for USB devices." +AdvSceneSwitcher.condition.streamDeck="Stream Deck" +AdvSceneSwitcher.condition.streamDeck.checkKeyState="Check key state" +AdvSceneSwitcher.condition.streamDeck.keyState.pressed="Pressed" +AdvSceneSwitcher.condition.streamDeck.keyState.released="Released" +AdvSceneSwitcher.condition.streamDeck.checkKeyPosition="Check key position" +AdvSceneSwitcher.condition.streamDeck.checkData="Check data field" +AdvSceneSwitcher.condition.streamDeck.startListen="Start listening" +AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening" +AdvSceneSwitcher.condition.streamDeck.pluginDownload="

The Stream Deck plugin can be found here on GitHub.

" # Macro Actions AdvSceneSwitcher.action.unknown="Unknown action" @@ -1881,6 +1890,13 @@ AdvSceneSwitcher.tempVar.media.vlc.disc_number="Disc number (VLC)" AdvSceneSwitcher.tempVar.media.vlc.disc_total="Disc total (VLC)" AdvSceneSwitcher.tempVar.media.vlc.metadata.description="This property is exposed by the VLC Source type.\nNot all of these might be applicable to every file.\nIn case the metadata is not available an empty string will be returned." +AdvSceneSwitcher.tempVar.streamDeck.keyPressed="Key pressed" +AdvSceneSwitcher.tempVar.streamDeck.keyPressed.description="Will return \"0\" when the key is pressed, and \"1\" otherwise." +AdvSceneSwitcher.tempVar.streamDeck.row="Key row" +AdvSceneSwitcher.tempVar.streamDeck.column="Key column" +AdvSceneSwitcher.tempVar.streamDeck.data="Data" +AdvSceneSwitcher.tempVar.streamDeck.data.description="The value of the \"Data\" field configured in the action on the Stream Deck side." + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9c5d8371..8df438e6 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -30,6 +30,7 @@ install_advss_plugin_dependency(...) add_plugin(midi) add_plugin(openvr) +add_plugin(stream-deck) add_plugin(twitch) add_plugin(usb) add_plugin(video) diff --git a/plugins/stream-deck/CMakeLists.txt b/plugins/stream-deck/CMakeLists.txt new file mode 100644 index 00000000..a2a0d24b --- /dev/null +++ b/plugins/stream-deck/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.14) +project(advanced-scene-switcher-stream-deck) + +add_library(${PROJECT_NAME} MODULE) + +get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR) + +target_sources(${PROJECT_NAME} PRIVATE macro-condition-stream-deck.cpp + macro-condition-stream-deck.hpp) +setup_advss_plugin(${PROJECT_NAME}) +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +install_advss_plugin(${PROJECT_NAME}) diff --git a/plugins/stream-deck/macro-condition-stream-deck.cpp b/plugins/stream-deck/macro-condition-stream-deck.cpp new file mode 100644 index 00000000..1adfba18 --- /dev/null +++ b/plugins/stream-deck/macro-condition-stream-deck.cpp @@ -0,0 +1,403 @@ +#include "macro-condition-stream-deck.hpp" +#include "layout-helpers.hpp" +#include "websocket-api.hpp" + +#include + +namespace advss { + +const std::string MacroConditionStreamdeck::id = "streamdeck"; + +bool MacroConditionStreamdeck::_registered = MacroConditionFactory::Register( + MacroConditionStreamdeck::id, + {MacroConditionStreamdeck::Create, MacroConditionStreamdeckEdit::Create, + "AdvSceneSwitcher.condition.streamDeck"}); + +constexpr char streamDeckMessage[] = "StreamDeckKeyEvent"; + +static StreamDeckMessageDispatcher messageDispatcher; + +static bool setup(); +static bool setupDone = setup(); +static void receiveStreamDeckMessage(obs_data_t *request_data, obs_data_t *); + +bool setup() +{ + RegisterWebsocketRequest(streamDeckMessage, receiveStreamDeckMessage); + return true; +} + +static void printParseError(obs_data_t *data) +{ + blog(LOG_INFO, "received unexpected stream deck message: %s", + obs_data_get_json(data) ? obs_data_get_json(data) : "''"); +} + +void receiveStreamDeckMessage(obs_data_t *request_data, obs_data_t *) +{ + StreamDeckMessage message; + OBSDataAutoRelease data = obs_data_get_obj(request_data, "data"); + if (!data) { + printParseError(request_data); + return; + } + + if (!obs_data_has_user_value(data, "isKeyDownEvent")) { + printParseError(request_data); + return; + } + message.keyDown = obs_data_get_bool(data, "isKeyDownEvent"); + + OBSDataAutoRelease coordinates = obs_data_get_obj(data, "coordinates"); + if (!coordinates) { + printParseError(request_data); + return; + } + + message.row = obs_data_get_int(coordinates, "row"); + message.column = obs_data_get_int(coordinates, "column"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + if (!settings) { + printParseError(request_data); + return; + } + message.data = obs_data_get_string(settings, "data"); + + messageDispatcher.DispatchMessage(message); +} + +StreamDeckMessageBuffer RegisterForStreamDeckMessages() +{ + return messageDispatcher.RegisterClient(); +} + +MacroConditionStreamdeck::MacroConditionStreamdeck(Macro *m) + : MacroCondition(m, true) +{ + _messageBuffer = RegisterForStreamDeckMessages(); +} + +bool MacroConditionStreamdeck::MessageMatches(const StreamDeckMessage &message) +{ + const bool keyStateMatches = !_pattern.checkKeyState || + ((message.keyDown && _pattern.keyDown) || + (!message.keyDown && !message.keyDown)); + const bool positionMatches = !_pattern.checkPosition || + (message.row == _pattern.row && + message.column == _pattern.column); + const bool dataMatches = + !_pattern.checkData || + (_pattern.regex.Enabled() + ? _pattern.regex.Matches(message.data, _pattern.data) + : message.data == std::string(_pattern.data)); + return keyStateMatches && positionMatches && dataMatches; +} + +bool MacroConditionStreamdeck::CheckCondition() +{ + while (!_messageBuffer->Empty()) { + auto message = _messageBuffer->ConsumeMessage(); + if (!message) { + continue; + } + + if (!message->keyDown) { + _lastMatchingKeyIsStillPressed = false; + } + + if (MessageMatches(*message)) { + _lastMatchingKeyIsStillPressed = message->keyDown; + SetTempVarValues(*message); + return true; + } + } + + if (_lastMatchingKeyIsStillPressed) { + return true; + } + + return false; +} + +bool MacroConditionStreamdeck::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + _pattern.Save(obj); + return true; +} + +bool MacroConditionStreamdeck::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _pattern.Load(obj); + return true; +} + +void MacroConditionStreamdeck::StreamDeckMessagePattern::Save( + obs_data_t *obj) const +{ + OBSDataAutoRelease dataObj = obs_data_create(); + obs_data_set_bool(dataObj, "checkKeyState", checkKeyState); + obs_data_set_bool(dataObj, "keyDown", keyDown); + obs_data_set_bool(dataObj, "checkPosition", checkPosition); + row.Save(dataObj, "row"); + column.Save(dataObj, "column"); + obs_data_set_bool(dataObj, "checkData", checkData); + data.Save(dataObj, "data"); + regex.Save(dataObj); + obs_data_set_obj(obj, "pattern", dataObj); +} + +void MacroConditionStreamdeck::StreamDeckMessagePattern::Load(obs_data_t *obj) +{ + OBSDataAutoRelease dataObj = obs_data_get_obj(obj, "pattern"); + checkKeyState = obs_data_get_bool(dataObj, "checkKeyState"); + keyDown = obs_data_get_bool(dataObj, "keyDown"); + checkPosition = obs_data_get_bool(dataObj, "checkPosition"); + row.Load(dataObj, "row"); + column.Load(dataObj, "column"); + checkData = obs_data_get_bool(dataObj, "checkData"); + data.Load(dataObj, "data"); + regex.Load(dataObj); + obs_data_set_obj(obj, "pattern", dataObj); +} + +void MacroConditionStreamdeck::SetTempVarValues(const StreamDeckMessage &message) +{ + SetTempVarValue("keyPressed", std::to_string(message.keyDown)); + SetTempVarValue("row", std::to_string(message.row)); + SetTempVarValue("column", std::to_string(message.column)); + SetTempVarValue("data", message.data); +} + +void MacroConditionStreamdeck::SetupTempVars() +{ + AddTempvar( + "keyPressed", + obs_module_text( + "AdvSceneSwitcher.tempVar.streamDeck.keyPressed"), + obs_module_text( + "AdvSceneSwitcher.tempVar.streamDeck.keyPressed.description")); + AddTempvar("row", + obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.row")); + AddTempvar( + "column", + obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.column")); + AddTempvar( + "data", + obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.data"), + obs_module_text( + "AdvSceneSwitcher.tempVar.streamDeck.data.description")); +} + +MacroConditionStreamdeckEdit::MacroConditionStreamdeckEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _checkKeyState(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.checkKeyState"))), + _keyState(new QComboBox(this)), + _checkPosition(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.checkKeyPosition"))), + _row(new VariableSpinBox(this)), + _column(new VariableSpinBox(this)), + _checkData(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.checkData"))), + _data(new VariableTextEdit(this)), + _regex(new RegexConfigWidget(this)), + _listen(new QPushButton( + obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.startListen"), + this)) +{ + _keyState->addItem( + obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.keyState.pressed"), + true); + _keyState->addItem( + obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.keyState.released"), + false); + + connect(_checkKeyState, &QCheckBox::stateChanged, this, + &MacroConditionStreamdeckEdit::CheckKeyStateChanged); + connect(_keyState, QOverload::of(&QComboBox::currentIndexChanged), + this, &MacroConditionStreamdeckEdit::KeyStateChanged); + connect(_checkPosition, &QCheckBox::stateChanged, this, + &MacroConditionStreamdeckEdit::CheckPositionChanged); + connect(_row, + static_cast( + &VariableSpinBox::NumberVariableChanged), + this, &MacroConditionStreamdeckEdit::RowChanged); + connect(_column, + static_cast( + &VariableSpinBox::NumberVariableChanged), + this, &MacroConditionStreamdeckEdit::ColumnChanged); + connect(_checkData, &QCheckBox::stateChanged, this, + &MacroConditionStreamdeckEdit::CheckDataChanged); + connect(_regex, &RegexConfigWidget::RegexConfigChanged, this, + &MacroConditionStreamdeckEdit::RegexChanged); + connect(_data, &VariableTextEdit::textChanged, this, + &MacroConditionStreamdeckEdit::DataChanged); + connect(_listen, &QPushButton::clicked, this, + &MacroConditionStreamdeckEdit::ListenClicked); + + auto keyStateLayout = new QHBoxLayout(); + keyStateLayout->addWidget(_checkKeyState); + keyStateLayout->addWidget(_keyState); + keyStateLayout->addStretch(); + + auto positionLayout = new QHBoxLayout(); + positionLayout->addWidget(_row); + positionLayout->addWidget(_column); + + auto dataLayout = new QHBoxLayout(); + dataLayout->addWidget(_checkData); + dataLayout->addWidget(_regex); + dataLayout->addStretch(); + + auto layout = new QVBoxLayout(); + layout->addLayout(keyStateLayout); + layout->addWidget(_checkPosition); + layout->addLayout(positionLayout); + layout->addLayout(dataLayout); + layout->addWidget(_data); + layout->addWidget(_listen); + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.pluginDownload"))); + setLayout(layout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionStreamdeckEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _checkKeyState->setChecked(_entryData->_pattern.checkKeyState); + _keyState->setCurrentIndex( + _keyState->findData(_entryData->_pattern.keyDown)); + _checkPosition->setChecked(_entryData->_pattern.checkPosition); + _row->SetValue(_entryData->_pattern.row); + _column->SetValue(_entryData->_pattern.column); + _checkData->setChecked(_entryData->_pattern.checkData); + _data->setPlainText(_entryData->_pattern.data); + _regex->SetRegexConfig(_entryData->_pattern.regex); + + SetWidgetVisibility(); +} + +void MacroConditionStreamdeckEdit::CheckKeyStateChanged(int state) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.checkKeyState = state; + SetWidgetVisibility(); +} + +void MacroConditionStreamdeckEdit::KeyStateChanged(int index) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.keyDown = _keyState->itemData(index).toBool(); +} + +void MacroConditionStreamdeckEdit::CheckPositionChanged(int state) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.checkPosition = state; + SetWidgetVisibility(); +} + +void MacroConditionStreamdeckEdit::RowChanged(const IntVariable &value) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.row = value; +} + +void MacroConditionStreamdeckEdit::ColumnChanged(const IntVariable &value) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.column = value; +} + +void MacroConditionStreamdeckEdit::CheckDataChanged(int state) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.checkData = state; + SetWidgetVisibility(); +} + +void MacroConditionStreamdeckEdit::RegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.regex = regex; +} + +void MacroConditionStreamdeckEdit::DataChanged() +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pattern.data = _data->toPlainText().toStdString(); +} + +void MacroConditionStreamdeckEdit::ListenClicked() +{ + _isListening = !_isListening; + + _keyState->setDisabled(_isListening); + _row->setDisabled(_isListening); + _column->setDisabled(_isListening); + _data->setDisabled(_isListening); + _regex->setDisabled(_isListening); + + if (_isListening) { + _messageBuffer = RegisterForStreamDeckMessages(); + _listen->setText(obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.stopListen")); + } else { + _updateListenSettings.stop(); + _messageBuffer.reset(); + _listen->setText(obs_module_text( + "AdvSceneSwitcher.condition.streamDeck.startListen")); + return; + } + + connect(&_updateListenSettings, &QTimer::timeout, this, [this]() { + if (_messageBuffer->Empty()) { + return; + } + + StreamDeckMessage lastMessageInBuffer; + while (!_messageBuffer->Empty()) { + auto message = _messageBuffer->ConsumeMessage(); + if (!message) { + continue; + } + lastMessageInBuffer = *message; + } + + _keyState->setCurrentIndex( + _keyState->findData(lastMessageInBuffer.keyDown)); + _row->SetFixedValue(lastMessageInBuffer.row); + _column->SetFixedValue(lastMessageInBuffer.column); + _data->setPlainText(lastMessageInBuffer.data); + }); + + _updateListenSettings.start(300); +} + +void MacroConditionStreamdeckEdit::SetWidgetVisibility() +{ + _keyState->setVisible(_checkKeyState->isChecked()); + _row->setVisible(_checkPosition->isChecked()); + _column->setVisible(_checkPosition->isChecked()); + _data->setVisible(_checkData->isChecked()); + _regex->setVisible(_checkData->isChecked()); + + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/plugins/stream-deck/macro-condition-stream-deck.hpp b/plugins/stream-deck/macro-condition-stream-deck.hpp new file mode 100644 index 00000000..cfef7513 --- /dev/null +++ b/plugins/stream-deck/macro-condition-stream-deck.hpp @@ -0,0 +1,116 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "message-buffer.hpp" +#include "message-dispatcher.hpp" +#include "regex-config.hpp" +#include "variable-spinbox.hpp" +#include "variable-text-edit.hpp" + +#include + +namespace advss { + +struct StreamDeckMessage { + bool keyDown = true; + int row = 0; + int column = 0; + std::string data = ""; +}; + +using StreamDeckMessageBuffer = + std::shared_ptr>; +using StreamDeckMessageDispatcher = MessageDispatcher; +[[nodiscard]] StreamDeckMessageBuffer RegisterForStreamDeckMessages(); + +class MacroConditionStreamdeck : public MacroCondition { +public: + MacroConditionStreamdeck(Macro *m); + bool CheckCondition(); + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + std::string GetId() const { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + + struct StreamDeckMessagePattern { + void Save(obs_data_t *) const; + void Load(obs_data_t *); + + bool checkKeyState = true; + bool keyDown = true; + bool checkPosition = true; + IntVariable row; + IntVariable column; + bool checkData = true; + StringVariable data; + RegexConfig regex; + }; + StreamDeckMessagePattern _pattern; + +private: + void SetTempVarValues(const StreamDeckMessage &); + void SetupTempVars(); + bool MessageMatches(const StreamDeckMessage &); + + // Keep track of the key state so that the condition can still evaluate + // to true while the key is held down. + bool _lastMatchingKeyIsStillPressed = false; + + StreamDeckMessageBuffer _messageBuffer; + + static bool _registered; + static const std::string id; +}; + +class MacroConditionStreamdeckEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionStreamdeckEdit( + QWidget *parent, + std::shared_ptr cond = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionStreamdeckEdit( + parent, + std::dynamic_pointer_cast( + cond)); + } + +private slots: + void CheckKeyStateChanged(int); + void KeyStateChanged(int); + void CheckPositionChanged(int); + void RowChanged(const IntVariable &); + void ColumnChanged(const IntVariable &); + void CheckDataChanged(int); + void RegexChanged(const RegexConfig &); + void DataChanged(); + void ListenClicked(); + +private: + void SetWidgetVisibility(); + + QCheckBox *_checkKeyState; + QComboBox *_keyState; + QCheckBox *_checkPosition; + VariableSpinBox *_row; + VariableSpinBox *_column; + QCheckBox *_checkData; + VariableTextEdit *_data; + RegexConfigWidget *_regex; + QPushButton *_listen; + + bool _isListening = false; + StreamDeckMessageBuffer _messageBuffer; + QTimer _updateListenSettings; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss