From e97f41a35670b53a3a861e254867022f2d9e69fa Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Wed, 17 Aug 2022 20:44:58 +0200 Subject: [PATCH] Add connection-manager Used to add, remove, and modify websocket connections --- src/advanced-scene-switcher.hpp | 3 + src/utils/connection-manager.cpp | 585 +++++++++++++++++++++++++++++++ src/utils/connection-manager.hpp | 114 ++++++ 3 files changed, 702 insertions(+) create mode 100644 src/utils/connection-manager.cpp create mode 100644 src/utils/connection-manager.hpp diff --git a/src/advanced-scene-switcher.hpp b/src/advanced-scene-switcher.hpp index b078090d..0688c8f9 100644 --- a/src/advanced-scene-switcher.hpp +++ b/src/advanced-scene-switcher.hpp @@ -92,6 +92,9 @@ signals: void SceneGroupAdded(const QString &name); void SceneGroupRemoved(const QString &name); void SceneGroupRenamed(const QString &oldName, const QString newName); + void ConnectionAdded(const QString &); + void ConnectionRenamed(const QString &oldName, const QString &newName); + void ConnectionRemoved(const QString &); public slots: void on_windowUp_clicked(); diff --git a/src/utils/connection-manager.cpp b/src/utils/connection-manager.cpp new file mode 100644 index 00000000..fed6c6e9 --- /dev/null +++ b/src/utils/connection-manager.cpp @@ -0,0 +1,585 @@ +#include "connection-manager.hpp" +#include "utility.hpp" +#include "name-dialog.hpp" +#include "switcher-data-structs.hpp" + +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(Connection *); + +void SwitcherData::saveConnections(obs_data_t *obj) +{ + obs_data_array_t *connectionArray = obs_data_array_create(); + for (Connection &c : connections) { + obs_data_t *array_obj = obs_data_create(); + c.Save(array_obj); + obs_data_array_push_back(connectionArray, array_obj); + obs_data_release(array_obj); + } + obs_data_set_array(obj, "connections", connectionArray); + obs_data_array_release(connectionArray); +} + +void SwitcherData::loadConnections(obs_data_t *obj) +{ + connections.clear(); + + obs_data_array_t *connectionArray = + obs_data_get_array(obj, "connections"); + size_t count = obs_data_array_count(connectionArray); + + for (size_t i = 0; i < count; i++) { + obs_data_t *array_obj = obs_data_array_item(connectionArray, i); + connections.emplace_back(); + connections.back().Load(array_obj); + obs_data_release(array_obj); + } + obs_data_array_release(connectionArray); +} + +Connection::Connection(std::string name, std::string address, uint64_t port, + std::string pass, bool connectOnStart, bool reconnect, + int reconnectDelay) + : _name(name), + _address(address), + _port(port), + _password(pass), + _connectOnStart(connectOnStart), + _reconnect(reconnect), + _reconnectDelay(reconnectDelay) +{ +} + +Connection::Connection(const Connection &other) +{ + _name = other._name; + _address = other._address; + _port = other._port; + _password = other._password; + _connectOnStart = other._connectOnStart; + _reconnect = other._reconnect; + _reconnectDelay = other._reconnectDelay; +} + +Connection &Connection::operator=(const Connection &other) +{ + if (this != &other) { + _name = other._name; + _address = other._address; + _port = other._port; + _password = other._password; + _connectOnStart = other._connectOnStart; + _reconnect = other._reconnect; + _reconnectDelay = other._reconnectDelay; + _client.Disconnect(); + } + return *this; +} + +Connection::~Connection() +{ + _client.Disconnect(); +} + +std::string GetUri(std::string addr, int port) +{ + return "ws://" + addr + ":" + std::to_string(port); +} + +std::string Connection::GetURI() +{ + return GetUri(_address, _port); +} + +void Connection::Reconnect() +{ + _client.Disconnect(); + _client.Connect(GetURI(), _password, _reconnect, _reconnectDelay); +} + +void Connection::SendMsg(const std::string &msg) +{ + const auto status = _client.GetStatus(); + if (status == WSConnection::Status::DISCONNECTED) { + _client.Connect(GetURI(), _password, _reconnect, + _reconnectDelay); + blog(LOG_WARNING, + "could not send message '%s' (connection to '%s' not established)", + msg.c_str(), GetURI().c_str()); + return; + } + + if (status == WSConnection::Status::AUTHENTICATED) { + _client.SendRequest(msg); + } +} + +Connection *GetConnectionByName(const QString &name) +{ + return GetConnectionByName(name.toStdString()); +} +Connection *GetConnectionByName(const std::string &name) +{ + for (auto &con : switcher->connections) { + if (con.GetName() == name) { + return &con; + } + } + return nullptr; +} + +bool ConnectionNameAvailable(const QString &name) +{ + return !GetConnectionByName(name); +} + +bool ConnectionNameAvailable(const std::string &name) +{ + return ConnectionNameAvailable(QString::fromStdString(name)); +} + +ConnectionSelection::ConnectionSelection(QWidget *parent) + : QWidget(parent), _selection(new QComboBox), _modify(new QPushButton) +{ + _modify->setMaximumSize(22, 22); + QIcon icon; + icon.addFile( + QString::fromUtf8(":/settings/images/settings/general.svg"), + QSize(), QIcon::Normal, QIcon::Off); + _modify->setIcon(icon); + _modify->setFlat(true); + + // Connect to slots + QWidget::connect(_selection, + SIGNAL(currentTextChanged(const QString &)), this, + SLOT(ChangeSelection(const QString &))); + QWidget::connect(_modify, SIGNAL(clicked()), this, + SLOT(ModifyButtonClicked())); + QWidget::connect( + window(), + SIGNAL(ConnectionRenamed(const QString &, const QString &)), + this, SLOT(RenameConnection(const QString &, const QString &))); + QWidget::connect(window(), SIGNAL(ConnectionAdded(const QString &)), + this, SLOT(AddConnection(const QString &))); + QWidget::connect(window(), SIGNAL(ConnectionRemoved(const QString &)), + this, SLOT(RemoveConnection(const QString &))); + + // Forward signals + QWidget::connect( + this, + SIGNAL(ConnectionRenamed(const QString &, const QString &)), + window(), + SIGNAL(ConnectionRenamed(const QString &, const QString &))); + QWidget::connect(this, SIGNAL(ConnectionAdded(const QString &)), + window(), SIGNAL(ConnectionAdded(const QString &))); + QWidget::connect(this, SIGNAL(ConnectionRemoved(const QString &)), + window(), SIGNAL(ConnectionRemoved(const QString &))); + + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(_selection); + layout->addWidget(_modify); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + for (const auto &c : switcher->connections) { + _selection->addItem(QString::fromStdString(c._name)); + } + _selection->model()->sort(0); + addSelectionEntry( + _selection, + obs_module_text("AdvSceneSwitcher.connection.select")); + _selection->insertSeparator(_selection->count()); + _selection->addItem(obs_module_text("AdvSceneSwitcher.connection.add")); +} + +void ConnectionSelection::SetConnection(const std::string &con) +{ + const QSignalBlocker blocker(_selection); + if (!!GetConnectionByName(con)) { + _selection->setCurrentText(QString::fromStdString(con)); + } else { + _selection->setCurrentIndex(0); + } +} + +void ConnectionSelection::ChangeSelection(const QString &sel) +{ + if (sel == obs_module_text("AdvSceneSwitcher.connection.add")) { + Connection connection; + bool accepted = ConnectionSettingsDialog::AskForSettings( + this, connection); + if (!accepted) { + _selection->setCurrentIndex(0); + return; + } + switcher->connections.emplace_back(connection); + const QSignalBlocker b(_selection); + const QString name = QString::fromStdString(connection._name); + AddConnection(name); + _selection->setCurrentText(name); + emit ConnectionAdded(name); + emit SelectionChanged(name); + return; + } + auto connection = GetCurrentConnection(); + if (connection) { + emit SelectionChanged( + QString::fromStdString(connection->_name)); + } else { + emit SelectionChanged(""); + } +} + +void ConnectionSelection::ModifyButtonClicked() +{ + auto connection = GetCurrentConnection(); + if (!connection) { + return; + } + auto properties = [&]() { + const auto oldName = connection->_name; + bool accepted = ConnectionSettingsDialog::AskForSettings( + this, *connection); + if (!accepted) { + return; + } + if (oldName != connection->_name) { + emit ConnectionRenamed( + QString::fromStdString(oldName), + QString::fromStdString(connection->_name)); + } + }; + + QMenu menu(this); + + QAction *action = new QAction( + obs_module_text("AdvSceneSwitcher.connection.rename"), &menu); + connect(action, SIGNAL(triggered()), this, SLOT(RenameConnection())); + action->setProperty("connetion", QVariant::fromValue(connection)); + menu.addAction(action); + + action = new QAction( + obs_module_text("AdvSceneSwitcher.connection.remove"), &menu); + connect(action, SIGNAL(triggered()), this, SLOT(RemoveConnection())); + menu.addAction(action); + + action = new QAction( + obs_module_text("AdvSceneSwitcher.connection.properties"), + &menu); + connect(action, &QAction::triggered, properties); + menu.addAction(action); + + menu.exec(QCursor::pos()); +} + +void ConnectionSelection::RenameConnection() +{ + QAction *action = reinterpret_cast(sender()); + QVariant variant = action->property("connetion"); + Connection *connection = variant.value(); + + std::string name; + bool accepted = AdvSSNameDialog::AskForName( + this, obs_module_text("AdvSceneSwitcher.windowTitle"), + obs_module_text("AdvSceneSwitcher.connection.newName"), name, + QString::fromStdString(name)); + if (!accepted) { + return; + } + if (name.empty()) { + DisplayMessage("AdvSceneSwitcher.connection.emptyName"); + return; + } + if (_selection->currentText().toStdString() != name && + !ConnectionNameAvailable(name)) { + DisplayMessage("AdvSceneSwitcher.connection.nameNotAvailable"); + return; + } + + const auto oldName = connection->_name; + connection->_name = name; + emit ConnectionRenamed(QString::fromStdString(oldName), + QString::fromStdString(name)); +} + +void ConnectionSelection::RenameConnection(const QString &oldName, + const QString &name) +{ + int idx = _selection->findText(oldName); + if (idx == -1) { + return; + } + _selection->setItemText(idx, name); +} + +void ConnectionSelection::AddConnection(const QString &name) +{ + if (_selection->findText(name) == -1) { + _selection->insertItem(1, name); + } +} + +void ConnectionSelection::RemoveConnection() +{ + auto connection = GetCurrentConnection(); + if (!connection) { + return; + } + + int idx = + _selection->findText(QString::fromStdString(connection->_name)); + if (idx == -1 || idx == _selection->count()) { + return; + } + + emit ConnectionRemoved(QString::fromStdString(connection->_name)); + + for (auto it = switcher->connections.begin(); + it != switcher->connections.end(); ++it) { + if (it->_name == connection->_name) { + switcher->connections.erase(it); + break; + } + } +} + +void ConnectionSelection::RemoveConnection(const QString &name) +{ + const int idx = _selection->findText(name); + if (idx == _selection->currentIndex()) { + _selection->setCurrentIndex(0); + } + _selection->removeItem(idx); +} + +Connection *ConnectionSelection::GetCurrentConnection() +{ + return GetConnectionByName(_selection->currentText()); +} + +ConnectionSettingsDialog::ConnectionSettingsDialog(QWidget *parent, + const Connection &settings) + : QDialog(parent), + _name(new QLineEdit()), + _nameHint(new QLabel), + _address(new QLineEdit()), + _port(new QSpinBox()), + _password(new QLineEdit()), + _connectOnStart(new QCheckBox()), + _reconnect(new QCheckBox()), + _reconnectDelay(new QSpinBox()), + _test(new QPushButton( + obs_module_text("AdvSceneSwitcher.connection.test"))), + _status(new QLabel()), + _buttonbox(new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel)) +{ + setModal(true); + setWindowModality(Qt::WindowModality::WindowModal); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setFixedWidth(555); + setMinimumHeight(100); + + _port->setMaximum(65535); + _reconnectDelay->setMaximum(9999); + _reconnectDelay->setSuffix("s"); + _buttonbox->setCenterButtons(true); + _buttonbox->button(QDialogButtonBox::Ok)->setDisabled(true); + + _name->setText(QString::fromStdString(settings._name)); + _address->setText(QString::fromStdString(settings._address)); + _port->setValue(settings._port); + _password->setText(QString::fromStdString(settings._password)); + _connectOnStart->setChecked(settings._connectOnStart); + _reconnect->setChecked(settings._reconnect); + _reconnectDelay->setValue(settings._reconnectDelay); + + QWidget::connect(_name, SIGNAL(textEdited(const QString &)), this, + SLOT(NameChanged(const QString &))); + QWidget::connect(_reconnect, SIGNAL(stateChanged(int)), this, + SLOT(ReconnectChanged(int))); + QWidget::connect(_test, SIGNAL(clicked()), this, + SLOT(TestConnection())); + + QWidget::connect(_buttonbox, &QDialogButtonBox::accepted, this, + &QDialog::accept); + QWidget::connect(_buttonbox, &QDialogButtonBox::rejected, this, + &QDialog::reject); + + QGridLayout *layout = new QGridLayout; + int row = 0; + layout->addWidget( + new QLabel(obs_module_text("AdvSceneSwitcher.connection.name")), + row, 0); + QHBoxLayout *nameLayout = new QHBoxLayout; + nameLayout->addWidget(_name); + nameLayout->addWidget(_nameHint); + layout->addLayout(nameLayout, row, 1); + ++row; + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.connection.address")), + row, 0); + layout->addWidget(_address, row, 1); + ++row; + layout->addWidget( + new QLabel(obs_module_text("AdvSceneSwitcher.connection.port")), + row, 0); + layout->addWidget(_port, row, 1); + ++row; + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.connection.password")), + row, 0); + layout->addWidget(_password, row, 1); + ++row; + layout->addWidget( + new QLabel(obs_module_text( + "AdvSceneSwitcher.connection.connectOnStart")), + row, 0); + layout->addWidget(_connectOnStart, row, 1); + ++row; + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.connection.reconnect")), + row, 0); + layout->addWidget(_reconnect, row, 1); + ++row; + layout->addWidget( + new QLabel(obs_module_text( + "AdvSceneSwitcher.connection.reconnectDelay")), + row, 0); + layout->addWidget(_reconnectDelay, row, 1); + ++row; + layout->addWidget(_test, row, 0); + layout->addWidget(_status, row, 1); + ++row; + layout->addWidget(_buttonbox, row, 0, 1, -1); + setLayout(layout); + + NameChanged(_name->text()); + ReconnectChanged(_reconnect->isChecked()); +} + +void ConnectionSettingsDialog::NameChanged(const QString &text) +{ + + if (text != _name->text() && !ConnectionNameAvailable(text)) { + SetNameWarning(obs_module_text( + "AdvSceneSwitcher.connection.nameNotAvailable")); + return; + } + if (text.isEmpty()) { + SetNameWarning(obs_module_text( + "AdvSceneSwitcher.connection.emptyName")); + return; + } + if (text == obs_module_text("AdvSceneSwitcher.connection.select") || + text == obs_module_text("AdvSceneSwitcher.connection.add")) { + SetNameWarning(obs_module_text( + "AdvSceneSwitcher.connection.nameReserved")); + return; + } + SetNameWarning(""); +} + +void ConnectionSettingsDialog::ReconnectChanged(int state) +{ + _reconnectDelay->setEnabled(state); +} + +void ConnectionSettingsDialog::SetStatus() +{ + switch (_testConnection.GetStatus()) { + case WSConnection::Status::DISCONNECTED: + _status->setText(obs_module_text( + "AdvSceneSwitcher.connection.status.disconnected")); + break; + case WSConnection::Status::CONNECTING: + _status->setText(obs_module_text( + "AdvSceneSwitcher.connection.status.connecting")); + break; + case WSConnection::Status::CONNECTED: + _status->setText(obs_module_text( + "AdvSceneSwitcher.connection.status.connected")); + break; + case WSConnection::Status::AUTHENTICATED: + _status->setText(obs_module_text( + "AdvSceneSwitcher.connection.status.authenticated")); + break; + default: + break; + } +} + +void ConnectionSettingsDialog::TestConnection() +{ + _testConnection.Disconnect(); + _testConnection.Connect(GetUri(_address->text().toStdString(), + _port->value()), + _password->text().toStdString(), false); + _statusTimer.setInterval(1000); + QWidget::connect(&_statusTimer, &QTimer::timeout, this, + &ConnectionSettingsDialog::SetStatus); + _statusTimer.start(); +} + +bool ConnectionSettingsDialog::AskForSettings(QWidget *parent, + Connection &settings) +{ + ConnectionSettingsDialog dialog(parent, settings); + dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); + if (dialog.exec() != DialogCode::Accepted) { + return false; + } + + settings._name = dialog._name->text().toStdString(); + settings._address = dialog._address->text().toStdString(); + settings._port = dialog._port->value(); + settings._password = dialog._password->text().toStdString(); + settings._connectOnStart = dialog._connectOnStart->isChecked(); + settings._reconnect = dialog._reconnect->isChecked(); + settings._reconnectDelay = dialog._reconnectDelay->value(); + settings.Reconnect(); + return true; +} + +void ConnectionSettingsDialog::SetNameWarning(const QString warn) +{ + if (warn.isEmpty()) { + _nameHint->hide(); + _buttonbox->button(QDialogButtonBox::Ok)->setDisabled(false); + return; + } + _nameHint->setText(warn); + _nameHint->show(); + _buttonbox->button(QDialogButtonBox::Ok)->setDisabled(true); +} + +void Connection::Load(obs_data_t *obj) +{ + _name = obs_data_get_string(obj, "name"); + _address = obs_data_get_string(obj, "address"); + _port = obs_data_get_int(obj, "port"); + _password = obs_data_get_string(obj, "password"); + _connectOnStart = obs_data_get_bool(obj, "connectOnStart"); + _reconnect = obs_data_get_bool(obj, "reconnect"); + _reconnectDelay = obs_data_get_int(obj, "reconnectDelay"); + + if (_connectOnStart) { + _client.Connect(GetURI(), _password, _reconnect, + _reconnectDelay); + } +} + +void Connection::Save(obs_data_t *obj) +{ + obs_data_set_string(obj, "name", _name.c_str()); + obs_data_set_string(obj, "address", _address.c_str()); + obs_data_set_int(obj, "port", _port); + obs_data_set_string(obj, "password", _password.c_str()); + obs_data_set_bool(obj, "connectOnStart", _connectOnStart); + obs_data_set_bool(obj, "reconnect", _reconnect); + obs_data_set_int(obj, "reconnectDelay", _reconnectDelay); +} diff --git a/src/utils/connection-manager.hpp b/src/utils/connection-manager.hpp new file mode 100644 index 00000000..2d7816bb --- /dev/null +++ b/src/utils/connection-manager.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ConnectionSelection; +class ConnectionSettingsDialog; + +class Connection { +public: + Connection(std::string name, std::string address, uint64_t port, + std::string pass, bool connectOnStart, bool reconnect, + int reconnectDelay); + Connection() = default; + Connection(const Connection &); + Connection &operator=(const Connection &); + ~Connection(); + + void Reconnect(); + void SendMsg(const std::string &msg); + void Load(obs_data_t *obj); + void Save(obs_data_t *obj); + std::string GetName() { return _name; } + +private: + std::string _name = ""; + std::string _address = "localhost"; + uint64_t _port = 4455; + std::string _password = "password"; + bool _connectOnStart = true; + bool _reconnect = true; + int _reconnectDelay = 3; + + std::string GetURI(); + WSConnection _client; + + friend ConnectionSelection; + friend ConnectionSettingsDialog; +}; + +Connection *GetConnectionByName(const QString &); +Connection *GetConnectionByName(const std::string &); + +class ConnectionSettingsDialog : public QDialog { + Q_OBJECT + +public: + ConnectionSettingsDialog(QWidget *parent, const Connection &); + static bool AskForSettings(QWidget *parent, Connection &settings); + +private slots: + void NameChanged(const QString &); + void ReconnectChanged(int); + void SetStatus(); + void TestConnection(); + +private: + void SetNameWarning(const QString); + + QLineEdit *_name; + QLabel *_nameHint; + QLineEdit *_address; + QSpinBox *_port; + QLineEdit *_password; + QCheckBox *_connectOnStart; + QCheckBox *_reconnect; + QSpinBox *_reconnectDelay; + QPushButton *_test; + QLabel *_status; + QDialogButtonBox *_buttonbox; + + QTimer _statusTimer; + WSConnection _testConnection; +}; + +class ConnectionSelection : public QWidget { + Q_OBJECT + +public: + ConnectionSelection(QWidget *parent = 0); + void SetConnection(const std::string &); + +private slots: + void ModifyButtonClicked(); + void ChangeSelection(const QString &); + void RenameConnection(); + void RenameConnection(const QString &oldName, const QString &name); + void AddConnection(const QString &); + void RemoveConnection(); + void RemoveConnection(const QString &); +signals: + void SelectionChanged(const QString &); + void ConnectionAdded(const QString &); + void ConnectionRemoved(const QString &); + void ConnectionRenamed(const QString &oldName, const QString &name); + +private: + Connection *GetCurrentConnection(); + + QComboBox *_selection; + QPushButton *_modify; +};