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