diff --git a/CMakeLists.txt b/CMakeLists.txt index 36b08a02..8159b9a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,8 @@ target_sources( src/macro-core/macro-action-macro.hpp src/macro-core/macro-action-media.cpp src/macro-core/macro-action-media.hpp + src/macro-core/macro-action-osc.cpp + src/macro-core/macro-action-osc.hpp src/macro-core/macro-action-plugin-state.cpp src/macro-core/macro-action-plugin-state.hpp src/macro-core/macro-action-profile.cpp @@ -266,6 +268,8 @@ target_sources( src/utils/obs-dock.hpp src/utils/obs-module-helper.cpp src/utils/obs-module-helper.hpp + src/utils/osc-helpers.cpp + src/utils/osc-helpers.hpp src/utils/priority-helper.cpp src/utils/priority-helper.hpp src/utils/process-config.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index c3d4421b..a29adf2f 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -673,6 +673,7 @@ AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}" AdvSceneSwitcher.action.midi="MIDI" AdvSceneSwitcher.action.midi.entry="Send message to {{device}}:" AdvSceneSwitcher.action.midi.entry.listen="Set MIDI message selection to messages incoming on {{listenDevices}}: {{listenButton}}" +AdvSceneSwitcher.action.osc="Open Sound Control" ; Transition Tab AdvSceneSwitcher.transitionTab.title="Transition" @@ -1013,6 +1014,21 @@ AdvSceneSwitcher.midi.stopListen="Stop listening" AdvSceneSwitcher.midi.startListenFail="Device is busy!\nSomething else is already listening!" AdvSceneSwitcher.midi.deviceOpenFail="Failed to initialize MIDI device!" +AdvSceneSwitcher.osc.network="Network" +AdvSceneSwitcher.osc.network.protocol="Protocol:" +AdvSceneSwitcher.osc.network.address="Address:" +AdvSceneSwitcher.osc.network.port="Port:" +AdvSceneSwitcher.osc.message="Message" +AdvSceneSwitcher.osc.message.type.none="None" +AdvSceneSwitcher.osc.message.type.float="Float" +AdvSceneSwitcher.osc.message.type.int="Integer" +AdvSceneSwitcher.osc.message.type.string="String" +AdvSceneSwitcher.osc.message.type.binaryBlob="Binary blob" +AdvSceneSwitcher.osc.message.type.true="True" +AdvSceneSwitcher.osc.message.type.false="False" +AdvSceneSwitcher.osc.message.type.infinity="Infinitum" +AdvSceneSwitcher.osc.message.type.null="Nil" + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/src/macro-core/macro-action-osc.cpp b/src/macro-core/macro-action-osc.cpp new file mode 100644 index 00000000..17b369b9 --- /dev/null +++ b/src/macro-core/macro-action-osc.cpp @@ -0,0 +1,318 @@ +#include "macro-action-osc.hpp" +#include "utility.hpp" + +#include +#include + +namespace advss { + +const std::string MacroActionOSC::id = "osc"; + +bool MacroActionOSC::_registered = MacroActionFactory::Register( + MacroActionOSC::id, {MacroActionOSC::Create, MacroActionOSCEdit::Create, + "AdvSceneSwitcher.action.osc"}); + +MacroActionOSC::MacroActionOSC(Macro *m) + : MacroAction(m), + _ctx(asio::io_context()), + _tcpSocket(asio::ip::tcp::socket(_ctx)), + _udpSocket(asio::ip::udp::socket(_ctx)) +{ +} + +void MacroActionOSC::SendOSCTCPMessage(const asio::mutable_buffer &buffer) +{ + try { + asio::write(_tcpSocket, asio::buffer(buffer)); + } catch (const std::exception &e) { + blog(LOG_WARNING, + "failed to send OSC message \"%s\" via TCP %s %d: %s", + _message.ToString().c_str(), _ip.c_str(), _port.GetValue(), + e.what()); + } +} + +void MacroActionOSC::SendOSCUDPMessage(const asio::mutable_buffer &buffer) +{ + try { + _udpSocket.send_to(buffer, _updEndpoint); + } catch (const std::exception &e) { + blog(LOG_WARNING, + "failed to send OSC message \"%s\" via UDP %s %d: %s", + _message.ToString().c_str(), _ip.c_str(), _port.GetValue(), + e.what()); + } +} + +void MacroActionOSC::UDPReconnect() +{ + asio::error_code ec; + asio::ip::udp::resolver resolver(_ctx); + auto endpoints = resolver.resolve(asio::ip::udp::v4(), _ip.c_str(), + std::to_string(_port.GetValue()), ec); + if (ec) { + endpoints = resolver.resolve(asio::ip::udp::v6(), _ip.c_str(), + std::to_string(_port.GetValue()), + ec); + } + if (ec) { + blog(LOG_WARNING, "failed to get IP for \"%s\": %s", + _ip.c_str(), ec.message().c_str()); + return; + } + + try { + _updEndpoint = endpoints.begin()->endpoint(); + _udpSocket = asio::ip::udp::socket(_ctx); + _udpSocket.open(endpoints.begin()->endpoint().protocol()); + } catch (const std::exception &e) { + blog(LOG_WARNING, "failed to connect to UDP %s %d: %s", + _ip.c_str(), _port.GetValue(), e.what()); + } +} + +void MacroActionOSC::TCPReconnect() +{ + asio::error_code ec; + asio::ip::tcp::resolver resolver(_ctx); + auto endpoints = resolver.resolve(asio::ip::tcp::v4(), _ip.c_str(), + std::to_string(_port.GetValue()), ec); + if (ec) { + endpoints = resolver.resolve(asio::ip::tcp::v6(), _ip.c_str(), + std::to_string(_port.GetValue()), + ec); + } + if (ec) { + blog(LOG_WARNING, "failed to get IP for \"%s\": %s", + _ip.c_str(), ec.message().c_str()); + return; + } + try { + _tcpSocket = asio::ip::tcp::socket(_ctx); + _tcpSocket.connect(endpoints.begin()->endpoint()); + } catch (const std::exception &e) { + blog(LOG_WARNING, "failed to connect to TCP %s %d: %s", + _ip.c_str(), _port.GetValue(), e.what()); + } +} + +void MacroActionOSC::CheckReconnect() +{ + if (_protocol == Protocol::TCP && + (_reconnect || !_tcpSocket.is_open())) { + TCPReconnect(); + } + + if (_protocol == Protocol::UDP && + (_reconnect || !_udpSocket.is_open())) { + UDPReconnect(); + } +} + +bool MacroActionOSC::PerformAction() +{ + auto buffer = _message.GetBuffer(); + if (!buffer.has_value()) { + blog(LOG_WARNING, "failed to create or fill OSC buffer!"); + return true; + } + + CheckReconnect(); + + if (_protocol == Protocol::TCP && + (_reconnect || !_tcpSocket.is_open())) { + TCPReconnect(); + } + + if (_protocol == Protocol::UDP && + (_reconnect || !_udpSocket.is_open())) { + UDPReconnect(); + } + + auto rawMessage = asio::buffer(*buffer); + + switch (_protocol) { + case MacroActionOSC::Protocol::TCP: + SendOSCTCPMessage(rawMessage); + break; + case MacroActionOSC::Protocol::UDP: + SendOSCUDPMessage(rawMessage); + break; + default: + break; + } + + return true; +} + +void MacroActionOSC::LogAction() const +{ + vblog(LOG_INFO, "sending OSC message '%s' to %s %s %d", + _message.ToString().c_str(), + _protocol == Protocol::UDP ? "UDP" : "TCP", _ip.c_str(), + _port.GetValue()); +} + +bool MacroActionOSC::Save(obs_data_t *obj) const +{ + MacroAction::Save(obj); + obs_data_set_int(obj, "protocol", static_cast(_protocol)); + _ip.Save(obj, "ip"); + _port.Save(obj, "port"); + _message.Save(obj); + return true; +} + +bool MacroActionOSC::Load(obs_data_t *obj) +{ + MacroAction::Load(obj); + _protocol = static_cast(obs_data_get_int(obj, "protocol")); + _ip.Load(obj, "ip"); + _port.Load(obj, "port"); + _message.Load(obj); + return true; +} + +void MacroActionOSC::SetProtocol(Protocol p) +{ + _protocol = p; + _reconnect = true; +} + +void MacroActionOSC::SetIP(const std::string &ip) +{ + _ip = ip; + _reconnect = true; +} + +void MacroActionOSC::SetPortNr(IntVariable port) +{ + _port = port; + _reconnect = true; +} + +static void populateProtocolSelection(QComboBox *list) +{ + list->addItem("TCP"); + list->addItem("UDP"); +} + +MacroActionOSCEdit::MacroActionOSCEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _protocol(new QComboBox(this)), + _ip(new VariableLineEdit(this)), + _port(new VariableSpinBox(this)), + _message(new OSCMessageEdit(this)) +{ + populateProtocolSelection(_protocol); + _port->setMaximum(65535); + + auto networkGroup = + new QGroupBox(obs_module_text("AdvSceneSwitcher.osc.network")); + auto networkLayout = new QGridLayout; + int row = 0; + networkLayout->addWidget( + new QLabel(obs_module_text( + "AdvSceneSwitcher.osc.network.protocol")), + row, 0); + networkLayout->addWidget(_protocol, row, 1); + ++row; + networkLayout->addWidget( + new QLabel(obs_module_text( + "AdvSceneSwitcher.osc.network.address")), + row, 0); + networkLayout->addWidget(_ip, row, 1); + ++row; + networkLayout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.osc.network.port")), + row, 0); + networkLayout->addWidget(_port, row, 1); + networkGroup->setLayout(networkLayout); + + auto messageGroup = + new QGroupBox(obs_module_text("AdvSceneSwitcher.osc.message")); + auto messageLayout = new QHBoxLayout(); + messageLayout->addWidget(_message); + messageGroup->setLayout(messageLayout); + + auto mainLayout = new QVBoxLayout; + mainLayout->addWidget(networkGroup); + mainLayout->addWidget(messageGroup); + setLayout(mainLayout); + + QWidget::connect(_ip, SIGNAL(editingFinished()), this, + SLOT(IpChanged())); + QWidget::connect(_protocol, SIGNAL(currentIndexChanged(int)), this, + SLOT(ProtocolChanged(int))); + QWidget::connect( + _port, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(PortChanged(const NumberVariable &))); + QWidget::connect(_message, SIGNAL(MessageChanged(const OSCMessage &)), + this, SLOT(MessageChanged(const OSCMessage &))); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroActionOSCEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _protocol->setCurrentIndex(static_cast(_entryData->GetProtocol())); + _ip->setText(_entryData->GetIP()); + _port->SetValue(_entryData->GetPortNr()); + _message->SetMessage(_entryData->_message); + + adjustSize(); + updateGeometry(); +} + +void MacroActionOSCEdit::IpChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->SetIP(_ip->text().toStdString()); +} + +void MacroActionOSCEdit::ProtocolChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->SetProtocol(static_cast(value)); +} + +void MacroActionOSCEdit::PortChanged(const NumberVariable &value) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->SetPortNr(value); +} + +void MacroActionOSCEdit::MessageChanged(const OSCMessage &m) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_message = m; + + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/src/macro-core/macro-action-osc.hpp b/src/macro-core/macro-action-osc.hpp new file mode 100644 index 00000000..6457cebe --- /dev/null +++ b/src/macro-core/macro-action-osc.hpp @@ -0,0 +1,94 @@ +#pragma once +#include "macro-action-edit.hpp" +#include "osc-helpers.hpp" + +#include +#include + +namespace advss { + +class MacroActionOSC : public MacroAction { +public: + MacroActionOSC(Macro *m); + bool PerformAction(); + void LogAction() const; + 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); + } + + enum class Protocol { + TCP, + UDP, + }; + + void SetProtocol(Protocol); + Protocol GetProtocol() const { return _protocol; } + void SetIP(const std::string &); + StringVariable GetIP() const { return _ip; } + void SetPortNr(IntVariable); + IntVariable GetPortNr() { return _port; } + + OSCMessage _message; + +private: + void SendOSCTCPMessage(const asio::mutable_buffer &); + void SendOSCUDPMessage(const asio::mutable_buffer &); + + void CheckReconnect(); + void TCPReconnect(); + void UDPReconnect(); + + Protocol _protocol = Protocol::UDP; + StringVariable _ip = "localhost"; + IntVariable _port = 12345; + bool _reconnect = true; + + asio::io_context _ctx; + asio::ip::tcp::socket _tcpSocket; + asio::ip::udp::socket _udpSocket; + asio::ip::udp::endpoint _updEndpoint; + + static bool _registered; + static const std::string id; +}; + +class MacroActionOSCEdit : public QWidget { + Q_OBJECT + +public: + MacroActionOSCEdit(QWidget *parent, + std::shared_ptr entryData = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr action) + { + return new MacroActionOSCEdit( + parent, + std::dynamic_pointer_cast(action)); + } + +private slots: + void IpChanged(); + void MessageChanged(const OSCMessage &); + void ProtocolChanged(int); + void PortChanged(const NumberVariable &value); + +signals: + void HeaderInfoChanged(const QString &); + +protected: + std::shared_ptr _entryData; + +private: + QComboBox *_protocol; + VariableLineEdit *_ip; + VariableSpinBox *_port; + OSCMessageEdit *_message; + bool _loading = true; +}; + +} // namespace advss diff --git a/src/utils/mouse-wheel-guard.cpp b/src/utils/mouse-wheel-guard.cpp index d8e55330..1f9afbc6 100644 --- a/src/utils/mouse-wheel-guard.cpp +++ b/src/utils/mouse-wheel-guard.cpp @@ -1,4 +1,5 @@ #include "mouse-wheel-guard.hpp" +#include "osc-helpers.hpp" #include #include @@ -23,12 +24,15 @@ bool MouseWheelWidgetAdjustmentGuard::eventFilter(QObject *o, QEvent *e) void PreventMouseWheelAdjustWithoutFocus(QWidget *w) { - w->setFocusPolicy(Qt::StrongFocus); // Ignore QScrollBar as there is no danger of accidentally modifying anything // and long expanded QComboBox would be difficult to interact with otherwise. - if (qobject_cast(w)) { + // Ignore OSCMessageElementEdit to allow OSCMessageEdit list to up update + // current index correctly. + if (qobject_cast(w) || + qobject_cast(w)) { return; } + w->setFocusPolicy(Qt::StrongFocus); w->installEventFilter(new MouseWheelWidgetAdjustmentGuard(w)); } diff --git a/src/utils/osc-helpers.cpp b/src/utils/osc-helpers.cpp new file mode 100644 index 00000000..0f822134 --- /dev/null +++ b/src/utils/osc-helpers.cpp @@ -0,0 +1,689 @@ +#include "osc-helpers.hpp" +#include "obs-module-helper.hpp" +#include "utility.hpp" + +#include +#include + +namespace advss { + +std::unordered_map + OSCMessageElement::_typeNames = { + {std::variant_npos, + {"AdvSceneSwitcher.osc.message.type.none", "-"}}, + {0, {"AdvSceneSwitcher.osc.message.type.int", "i"}}, + {1, {"AdvSceneSwitcher.osc.message.type.float", "f"}}, + {2, {"AdvSceneSwitcher.osc.message.type.string", "s"}}, + {3, {"AdvSceneSwitcher.osc.message.type.binaryBlob", "b"}}, + {4, {"AdvSceneSwitcher.osc.message.type.true", "T"}}, + {5, {"AdvSceneSwitcher.osc.message.type.false", "F"}}, + {6, {"AdvSceneSwitcher.osc.message.type.infinity", "I"}}, + {7, {"AdvSceneSwitcher.osc.message.type.null", "N"}}, +}; + +// Based on https://github.com/mhroth/tinyosc +struct FillMessageElementBufferVisitor { + std::vector &buffer; + uint32_t &curOffset; + + bool success = false; + + void operator()(const StringVariable &value) + { + std::string string = value; + int length = (int)strlen(string.c_str()); + if (curOffset + length >= buffer.size()) { + buffer.resize(curOffset + length + 1); + } + strncpy(buffer.data() + curOffset, string.c_str(), + buffer.size() - curOffset - length); + curOffset = (curOffset + 4 + length) & ~0x3; + success = true; + } + void operator()(const IntVariable &value) + { + if (curOffset + 4 > buffer.size()) { + buffer.resize(curOffset + 4); + } + int32_t k = value; + *((uint32_t *)(buffer.data() + curOffset)) = htonl(k); + curOffset += 4; + success = true; + } + void operator()(const DoubleVariable &value) + { + if (curOffset + 4 > buffer.size()) { + buffer.resize(curOffset + 4); + } + const float f = value; + *((uint32_t *)(buffer.data() + curOffset)) = + htonl(*((uint32_t *)&f)); + curOffset += 4; + success = true; + } + void operator()(const OSCBlob &value) + { + if (curOffset + 4 > buffer.size()) { + buffer.resize(curOffset + 4); + } + auto blob = value.GetBinary(); + if (!blob.has_value()) { + return; + } + if (curOffset + 4 + blob->size() > buffer.size()) { + buffer.resize(curOffset + 4 + blob->size()); + } + *((uint32_t *)(buffer.data() + curOffset)) = + htonl(blob->size()); + curOffset += 4; + memcpy(buffer.data() + curOffset, blob->data(), blob->size()); + curOffset = (curOffset + 3 + blob->size()) & ~0x3; + success = true; + } + void operator()(const OSCTrue &) { success = true; } + void operator()(const OSCFalse &) { success = true; } + void operator()(const OSCInfinity &) { success = true; } + void operator()(const OSCNull &) { success = true; } +}; + +OSCBlob::OSCBlob(const std::string &stringRepresentation) + : _stringRep(stringRepresentation) +{ +} + +void OSCBlob::SetStringRepresentation(const StringVariable &s) +{ + _stringRep = s; +} + +std::string OSCBlob::GetStringRepresentation() const +{ + return _stringRep; +} + +std::optional> OSCBlob::GetBinary() const +{ + std::vector bytes; + std::string hexString = _stringRep; + + for (std::size_t i = 2; i < hexString.size(); i += 4) { + try { + auto byteString = hexString.substr(i, 2); + int value = std::stoi(byteString, nullptr, 16); + bytes.push_back(static_cast(value)); + } catch (const std::exception &e) { + blog(LOG_WARNING, + "failed to convert hex \"%s\" to binary: %s", + hexString.c_str(), e.what()); + return {}; + } + } + + return bytes; +} + +void OSCBlob::Save(obs_data_t *obj, const char *name) const +{ + _stringRep.Save(obj, name); +} + +void OSCBlob::Load(obs_data_t *obj, const char *name) +{ + _stringRep.Load(obj, name); +} + +void OSCTrue::Save(obs_data_t *obj, const char *name) const +{ + obs_data_set_bool(obj, name, true); +} + +void OSCFalse::Save(obs_data_t *obj, const char *name) const +{ + obs_data_set_bool(obj, name, true); +} + +void OSCInfinity::Save(obs_data_t *obj, const char *name) const +{ + obs_data_set_bool(obj, name, true); +} + +void OSCNull::Save(obs_data_t *obj, const char *name) const +{ + obs_data_set_bool(obj, name, true); +} + +std::optional> OSCMessage::GetBuffer() const +{ + if (std::string(_address).empty()) { + return {}; + } + + std::vector buffer(128, 0); + uint32_t currentOffset = (uint32_t)strlen(_address.c_str()); + if (currentOffset > buffer.size()) { + buffer.resize(buffer.size() * 2); + } + strncpy(buffer.data(), _address.c_str(), buffer.size()); + currentOffset = (currentOffset + 4) & ~0x3; + + std::string typeTags; + for (const auto &e : _elements) { + typeTags += e.GetTypeTag(); + } + + buffer.at(currentOffset++) = ','; + int length = (int)strlen(typeTags.c_str()); + if (currentOffset + length >= buffer.size()) { + buffer.resize(buffer.size() * 2); + } + strncpy(buffer.data() + currentOffset, typeTags.c_str(), + buffer.size() - currentOffset - length); + currentOffset = (currentOffset + 4 + length) & ~0x3; + + for (const auto &e : _elements) { + const auto &value = e._value; + FillMessageElementBufferVisitor visitor{buffer, currentOffset}; + std::visit(visitor, value); + if (!visitor.success) { + return {}; + } + } + + buffer.resize(currentOffset); + return buffer; +} + +const char *OSCMessageElement::GetTypeTag() const +{ + return GetTypeTag(*this); +} + +const char *OSCMessageElement::GetTypeName() const +{ + return GetTypeName(*this); +} + +const char *OSCMessageElement::GetTypeTag(const OSCMessageElement &element) +{ + return _typeNames.at(element._value.index()).tag; +} + +const char *OSCMessageElement::GetTypeName(const OSCMessageElement &element) +{ + return obs_module_text( + _typeNames.at(element._value.index()).localizedName); +} + +void OSCMessageElement::Save(obs_data_t *obj) const +{ + std::visit( + [obj](auto &&arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + arg.Save(obj, "strValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "intValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "floatValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "binaryValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "trueValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "falseValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "infiniteValue"); + } else if constexpr (std::is_same_v) { + arg.Save(obj, "nullValue"); + } else { + blog(LOG_WARNING, + "cannot save unkown OSCMessageElement"); + } + }, + _value); +} + +void OSCMessageElement::Load(obs_data_t *obj) +{ + if (obs_data_has_user_value(obj, "strValue")) { + StringVariable string; + string.Load(obj, "strValue"); + _value = string; + } else if (obs_data_has_user_value(obj, "intValue")) { + NumberVariable intValue; + intValue.Load(obj, "intValue"); + _value = intValue; + } else if (obs_data_has_user_value(obj, "floatValue")) { + NumberVariable floatValue; + floatValue.Load(obj, "floatValue"); + _value = floatValue; + } else if (obs_data_has_user_value(obj, "binaryValue")) { + OSCBlob binValue; + binValue.Load(obj, "binaryValue"); + _value = binValue; + } else if (obs_data_has_user_value(obj, "trueValue")) { + _value = OSCTrue(); + } else if (obs_data_has_user_value(obj, "falseValue")) { + _value = OSCFalse(); + } else if (obs_data_has_user_value(obj, "OSCInfinite")) { + _value = OSCInfinity(); + } else if (obs_data_has_user_value(obj, "nullValue")) { + _value = OSCNull(); + } else { + blog(LOG_WARNING, "cannot load unkown OSCMessageElement"); + } +} + +std::string OSCMessageElement::ToString() const +{ + return std::visit( + [](auto &&arg) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return arg; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return arg.GetStringRepresentation(); + } else { + return std::to_string(arg.GetValue()); + } + }, + _value); +} + +void OSCMessage::Save(obs_data_t *obj) const +{ + auto data = obs_data_create(); + _address.Save(data, "address"); + auto elements = obs_data_array_create(); + for (const auto &e : _elements) { + auto array_obj = obs_data_create(); + e.Save(array_obj); + obs_data_array_push_back(elements, array_obj); + obs_data_release(array_obj); + } + obs_data_set_array(data, "elements", elements); + obs_data_set_obj(obj, "oscMessage", data); + obs_data_array_release(elements); + obs_data_release(data); +} + +void OSCMessage::Load(obs_data_t *obj) +{ + + auto data = obs_data_get_obj(obj, "oscMessage"); + _address.Load(data, "address"); + _elements.clear(); + auto elements = obs_data_get_array(data, "elements"); + size_t count = obs_data_array_count(elements); + for (size_t i = 0; i < count; i++) { + auto array_obj = obs_data_array_item(elements, i); + OSCMessageElement e; + e.Load(array_obj); + _elements.push_back(e); + obs_data_release(array_obj); + } + obs_data_array_release(elements); + obs_data_release(data); +} + +std::string OSCMessage::ToString() const +{ + std::string res = "address: " + std::string(_address) + " message: "; + for (const auto &e : _elements) { + res += "[" + e.ToString() + "]"; + } + return res; +} + +OSCMessageElementEdit::OSCMessageElementEdit(QWidget *parent) + : QWidget(parent), + _type(new QComboBox(this)), + _intValue(new VariableSpinBox(this)), + _doubleValue(new VariableDoubleSpinBox(this)), + _text(new VariableLineEdit(this)), + _binaryText(new VariableLineEdit(this)) +{ + installEventFilter(this); + + _intValue->setMinimum(INT_MIN); + _intValue->setMaximum(INT_MAX); + _doubleValue->setMinimum(-9999999999); + _doubleValue->setMaximum(9999999999); + _doubleValue->setDecimals(10); + + _intValue->hide(); + _doubleValue->hide(); + _text->hide(); + _binaryText->hide(); + + for (int i = 0; i < OSCMessageElement::_typeNames.size() - 1; i++) { + _type->addItem(obs_module_text( + OSCMessageElement::_typeNames.at(i).localizedName)); + } + _type->setCurrentIndex(0); + + QWidget::connect(_type, SIGNAL(currentIndexChanged(int)), this, + SLOT(TypeChanged(int))); + QWidget::connect( + _doubleValue, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(DoubleChanged(const NumberVariable &))); + QWidget::connect( + _intValue, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(IntChanged(const NumberVariable &))); + QWidget::connect(_text, SIGNAL(editingFinished()), this, + SLOT(TextChanged())); + QWidget::connect(_binaryText, SIGNAL(editingFinished()), this, + SLOT(BinaryTextChanged())); + + auto layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_type, 1); + layout->addWidget(_intValue, 4); + layout->addWidget(_doubleValue, 4); + layout->addWidget(_text, 4); + layout->addWidget(_binaryText, 4); + setLayout(layout); +} + +void OSCMessageElementEdit::SetMessageElement(const OSCMessageElement &element) +{ + const QSignalBlocker b(this); + _type->setCurrentText(element.GetTypeName()); + SetVisibility(element); + + if (std::holds_alternative(element._value)) { + _text->setText(std::get(element._value)); + } else if (std::holds_alternative(element._value)) { + _intValue->SetValue(std::get(element._value)); + } else if (std::holds_alternative(element._value)) { + _doubleValue->SetValue( + std::get(element._value)); + } else if (std::holds_alternative(element._value)) { + _binaryText->setText(std::get(element._value) + .GetStringRepresentation()); + } +} + +bool OSCMessageElementEdit::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick) { + emit Focussed(); + } + + return QWidget::eventFilter(obj, event); +} +void OSCMessageElementEdit::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + + QWidgetList childWidgets = findChildren(); + for (QWidget *childWidget : childWidgets) { + childWidget->installEventFilter(this); + } +} + +void OSCMessageElementEdit::DoubleChanged(const DoubleVariable &value) +{ + emit ElementValueChanged(OSCMessageElement(value)); +} + +void OSCMessageElementEdit::IntChanged(const IntVariable &value) +{ + emit ElementValueChanged(OSCMessageElement(value)); +} + +void OSCMessageElementEdit::TextChanged() +{ + emit ElementValueChanged(StringVariable(_text->text().toStdString())); +} + +void OSCMessageElementEdit::BinaryTextChanged() +{ + emit ElementValueChanged(OSCBlob(_binaryText->text().toStdString())); +} + +void OSCMessageElementEdit::SetVisibility(const OSCMessageElement &element) +{ + _intValue->hide(); + _doubleValue->hide(); + _text->hide(); + _binaryText->hide(); + + if (std::holds_alternative(element._value)) { + _text->show(); + } else if (std::holds_alternative(element._value)) { + _intValue->show(); + } else if (std::holds_alternative(element._value)) { + _doubleValue->show(); + } else if (std::holds_alternative(element._value)) { + _binaryText->show(); + } +} + +void OSCMessageElementEdit::TypeChanged(int idx) +{ + OSCMessageElement element; + if (idx == 0) { + element = OSCMessageElement(IntVariable(0)); + } else if (idx == 1) { + element = OSCMessageElement(DoubleVariable(0.0)); + } else if (idx == 2) { + element = OSCMessageElement("value"); + } else if (idx == 3) { + element = OSCMessageElement(OSCBlob("\\x00\\x01\\x02\\x03")); + } else if (idx == 4) { + element = OSCMessageElement(OSCTrue()); + } else if (idx == 5) { + element = OSCMessageElement(OSCFalse()); + } else if (idx == 6) { + element = OSCMessageElement(OSCInfinity()); + } else if (idx == 7) { + element = OSCMessageElement(OSCNull()); + } + SetVisibility(element); + SetMessageElement(element); + emit ElementValueChanged(element); +} + +OSCMessageEdit::OSCMessageEdit(QWidget *parent) + : QWidget(parent), + _address(new VariableLineEdit(this)), + _elements(new QListWidget()), + _add(new QPushButton()), + _remove(new QPushButton()), + _up(new QPushButton()), + _down(new QPushButton()) +{ + _elements->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + _elements->setAutoScroll(false); + + _add->setMaximumWidth(22); + _add->setProperty("themeID", + QVariant(QString::fromUtf8("addIconSmall"))); + _add->setFlat(true); + _remove->setMaximumWidth(22); + _remove->setProperty("themeID", + QVariant(QString::fromUtf8("removeIconSmall"))); + _remove->setFlat(true); + _up->setMaximumWidth(22); + _up->setProperty("themeID", + QVariant(QString::fromUtf8("upArrowIconSmall"))); + _up->setFlat(true); + _down->setMaximumWidth(22); + _down->setProperty("themeID", + QVariant(QString::fromUtf8("downArrowIconSmall"))); + _down->setFlat(true); + + QWidget::connect(_address, SIGNAL(editingFinished()), this, + SLOT(AddressChanged())); + QWidget::connect(_add, SIGNAL(clicked()), this, SLOT(Add())); + QWidget::connect(_remove, SIGNAL(clicked()), this, SLOT(Remove())); + QWidget::connect(_up, SIGNAL(clicked()), this, SLOT(Up())); + QWidget::connect(_down, SIGNAL(clicked()), this, SLOT(Down())); + + auto controlsLayout = new QHBoxLayout(); + controlsLayout->addWidget(_add); + controlsLayout->addWidget(_remove); + QFrame *line = new QFrame(); + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Sunken); + controlsLayout->addWidget(line); + controlsLayout->addWidget(_up); + controlsLayout->addWidget(_down); + controlsLayout->addStretch(); + + auto layout = new QVBoxLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_address); + layout->addWidget(_elements); + layout->addLayout(controlsLayout); + setLayout(layout); +} + +void OSCMessageEdit::InsertElement(const OSCMessageElement &element) +{ + auto item = new QListWidgetItem(_elements); + _elements->addItem(item); + auto elementEdit = new OSCMessageElementEdit(this); + elementEdit->SetMessageElement(element); + item->setSizeHint(elementEdit->minimumSizeHint()); + _elements->setItemWidget(item, elementEdit); + QWidget::connect(elementEdit, + SIGNAL(ElementValueChanged(const OSCMessageElement &)), + this, + SLOT(ElementValueChanged(const OSCMessageElement &))); + QWidget::connect(elementEdit, SIGNAL(Focussed()), this, + SLOT(ElementFocussed())); + _currentSelection._elements.push_back(element); +} + +void OSCMessageEdit::SetMessage(const OSCMessage &message) +{ + _address->setText(message._address); + for (const auto &element : message._elements) { + InsertElement(element); + } + _currentSelection = message; + SetWidgetSize(); +} + +void OSCMessageEdit::AddressChanged() +{ + _currentSelection._address = _address->text().toStdString(); + emit MessageChanged(_currentSelection); +} + +void OSCMessageEdit::Add() +{ + OSCMessageElement element; + InsertElement(element); + emit MessageChanged(_currentSelection); + SetWidgetSize(); +} + +void OSCMessageEdit::Remove() +{ + auto item = _elements->currentItem(); + int idx = _elements->currentRow(); + if (!item || idx == -1) { + return; + } + delete item; + _currentSelection._elements.erase(_currentSelection._elements.begin() + + idx); + emit MessageChanged(_currentSelection); + SetWidgetSize(); +} + +void OSCMessageEdit::Up() +{ + int idx = _elements->currentRow(); + if (!listMoveUp(_elements)) { + return; + } + + iter_swap(_currentSelection._elements.begin() + idx, + _currentSelection._elements.begin() + idx - 1); + + emit MessageChanged(_currentSelection); + SetWidgetSize(); +} + +void OSCMessageEdit::Down() +{ + int idx = _elements->currentRow(); + if (!listMoveDown(_elements)) { + return; + } + + iter_swap(_currentSelection._elements.begin() + idx, + _currentSelection._elements.begin() + idx + 1); + + emit MessageChanged(_currentSelection); + SetWidgetSize(); +} + +static QListWidgetItem *getItemFromWidget(QListWidget *list, QWidget *widget) +{ + for (int i = 0; i < list->count(); i++) { + auto item = list->item(i); + if (!item) { + continue; + } + auto itemWidget = list->itemWidget(item); + if (itemWidget == widget) { + return item; + } + } + return nullptr; +} + +int OSCMessageEdit::GetIndexOfSignal() +{ + auto sender = this->sender(); + if (!sender) { + return -1; + } + auto widget = qobject_cast(sender); + if (!widget) { + return -1; + } + return _elements->row(getItemFromWidget(_elements, widget)); +} + +void OSCMessageEdit::ElementFocussed() +{ + int idx = GetIndexOfSignal(); + if (idx == -1) { + return; + } + _elements->setCurrentRow(idx); +} + +void OSCMessageEdit::ElementValueChanged(const OSCMessageElement &element) +{ + int idx = GetIndexOfSignal(); + if (idx == -1) { + return; + } + + _currentSelection._elements.at(idx) = element; + _elements->setCurrentRow(idx); + emit MessageChanged(_currentSelection); +} + +void OSCMessageEdit::SetWidgetSize() +{ + SetHeightToContentHeight(_elements); + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/src/utils/osc-helpers.hpp b/src/utils/osc-helpers.hpp new file mode 100644 index 00000000..31898b6a --- /dev/null +++ b/src/utils/osc-helpers.hpp @@ -0,0 +1,168 @@ +#pragma once +#include "variable-string.hpp" +#include "variable-number.hpp" +#include "variable-line-edit.hpp" +#include "variable-spinbox.hpp" + +#include +#include +#include + +namespace advss { + +class OSCBlob { +public: + OSCBlob() = default; + OSCBlob(const std::string &stringRepresentation); + void SetStringRepresentation(const StringVariable &); + std::string GetStringRepresentation() const; + std::optional> GetBinary() const; + void Save(obs_data_t *obj, const char *name) const; + void Load(obs_data_t *obj, const char *name); + +private: + StringVariable _stringRep; +}; + +class OSCTrue { +public: + void Save(obs_data_t *obj, const char *name) const; + std::string GetStringRepresentation() const { return "true"; } +}; + +class OSCFalse { +public: + void Save(obs_data_t *obj, const char *name) const; + std::string GetStringRepresentation() const { return "false"; } +}; + +class OSCInfinity { +public: + void Save(obs_data_t *obj, const char *name) const; + std::string GetStringRepresentation() const { return "infinity"; } +}; + +class OSCNull { +public: + void Save(obs_data_t *obj, const char *name) const; + std::string GetStringRepresentation() const { return "null"; } +}; + +class OSCMessageElement { +public: + OSCMessageElement() = default; + OSCMessageElement(const StringVariable &v) : _value(v) {} + OSCMessageElement(const IntVariable &v) : _value(v) {} + OSCMessageElement(const DoubleVariable &v) : _value(v) {} + OSCMessageElement(const OSCBlob &v) : _value(v) {} + OSCMessageElement(const OSCTrue &v) : _value(v) {} + OSCMessageElement(const OSCFalse &v) : _value(v) {} + OSCMessageElement(const OSCInfinity &v) : _value(v) {} + OSCMessageElement(const OSCNull &v) : _value(v) {} + + void Save(obs_data_t *obj) const; + void Load(obs_data_t *obj); + + std::string ToString() const; + const char *GetTypeName() const; + const char *GetTypeTag() const; + static const char *GetTypeName(const OSCMessageElement &); + static const char *GetTypeTag(const OSCMessageElement &); + +private: + struct TypeInfo { + const char *localizedName, *tag; + }; + static std::unordered_map _typeNames; + + std::variant + _value; + + friend class OSCMessage; + friend class OSCMessageElementEdit; +}; + +class OSCMessage { +public: + void Save(obs_data_t *obj) const; + void Load(obs_data_t *obj); + + std::string ToString() const; + std::optional> GetBuffer() const; + +private: + StringVariable _address = "/address"; + std::vector _elements = { + OSCMessageElement("example"), + OSCMessageElement(IntVariable(3))}; + + friend class OSCMessageEdit; +}; + +class OSCMessageElementEdit : public QWidget { + Q_OBJECT + +public: + OSCMessageElementEdit(QWidget *); + void SetMessageElement(const OSCMessageElement &); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void showEvent(QShowEvent *event) override; + +private slots: + void TypeChanged(int); + void DoubleChanged(const NumberVariable &value); + void IntChanged(const NumberVariable &value); + void TextChanged(); + void BinaryTextChanged(); + +signals: + void ElementValueChanged(const OSCMessageElement &); + void Focussed(); + +private: + void SetVisibility(const OSCMessageElement &); + + QComboBox *_type; + VariableSpinBox *_intValue; + VariableDoubleSpinBox *_doubleValue; + VariableLineEdit *_text; + VariableLineEdit *_binaryText; +}; + +class OSCMessageEdit : public QWidget { + Q_OBJECT +public: + OSCMessageEdit(QWidget *); + void SetMessage(const OSCMessage &); + +private slots: + void ElementValueChanged(const OSCMessageElement &); + void ElementFocussed(); + void AddressChanged(); + void Add(); + void Remove(); + void Up(); + void Down(); + +signals: + void MessageChanged(const OSCMessage &); + +private: + void InsertElement(const OSCMessageElement &); + void SetWidgetSize(); + int GetIndexOfSignal(); + + VariableLineEdit *_address; + QListWidget *_elements; + QPushButton *_add; + QPushButton *_remove; + QPushButton *_up; + QPushButton *_down; + + OSCMessage _currentSelection; +}; + +} // namespace advss diff --git a/src/utils/utility.cpp b/src/utils/utility.cpp index 2ba94fe2..eed564d4 100644 --- a/src/utils/utility.cpp +++ b/src/utils/utility.cpp @@ -1138,9 +1138,13 @@ void SetHeightToContentHeight(QListWidget *list) auto nrItems = list->count(); if (nrItems == 0) { list->setMaximumHeight(0); + list->setMinimumHeight(0); } else { - list->setMaximumHeight(list->sizeHintForRow(0) * nrItems + - 2 * list->frameWidth()); + int height = + (list->sizeHintForRow(0) + list->spacing()) * nrItems + + 2 * list->frameWidth(); + list->setMinimumHeight(height); + list->setMaximumHeight(height); } }