diff --git a/.gitignore b/.gitignore index 9415fe5cab..de40f52401 100755 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .clangd ./*.cache *.zip -build-* AppRun* *.AppImage *.txz diff --git a/src/plugins/score-plugin-remotecontrol/CMakeLists.txt b/src/plugins/score-plugin-remotecontrol/CMakeLists.txt index 0bebb7917c..b79fef6a62 100644 --- a/src/plugins/score-plugin-remotecontrol/CMakeLists.txt +++ b/src/plugins/score-plugin-remotecontrol/CMakeLists.txt @@ -37,9 +37,12 @@ set(HDRS "${CMAKE_CURRENT_SOURCE_DIR}/i-score-remote/RemoteApplication.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/score_plugin_remotecontrol.hpp" + + "${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/HttpServer/DocumentPlugin.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/HttpServer/HttpServer.hpp" ) -set(SRCS +set(SRCS "${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/Controller/RemoteControlProvider.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/Controller/DocumentPlugin.cpp" @@ -57,6 +60,9 @@ set(SRCS "${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/ApplicationPlugin.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/HttpServer/DocumentPlugin.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/HttpServer/HttpServer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/score_plugin_remotecontrol.cpp" ) diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/ApplicationPlugin.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/ApplicationPlugin.cpp index 43f56064d2..3e2af491bb 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/ApplicationPlugin.cpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/ApplicationPlugin.cpp @@ -7,8 +7,11 @@ #include #include + #include #include +#include + namespace RemoteControl { @@ -22,6 +25,8 @@ void ApplicationPlugin::on_createdDocument(score::Document& doc) doc.model().addPluginModel(new WS::DocumentPlugin{doc.context(), &doc.model()}); doc.model().addPluginModel( new Controller::DocumentPlugin{doc.context(), &doc.model()}); + doc.model().addPluginModel( + new HttpServer::DocumentPlugin{doc.context(), &doc.model()}); } } diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/DocumentPlugin.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/DocumentPlugin.cpp new file mode 100644 index 0000000000..73aabf00ea --- /dev/null +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/DocumentPlugin.cpp @@ -0,0 +1,43 @@ +#include "DocumentPlugin.hpp" + +#include + +#include + +namespace RemoteControl::HttpServer +{ +DocumentPlugin::DocumentPlugin(const score::DocumentContext& doc, QObject* parent) + : score::DocumentPlugin{doc, "RemoteControl::HttpServer::DocumentPlugin", parent} +{ + auto& model{m_context.app.settings()}; + + m_server.set_path(model.getWebUiPath().toStdString()); + m_server.set_address(model.getServerAddress().toStdString()); + m_server.set_port(model.getServerPort()); + + if(model.getEnabled() && model.getServerEnabled()) + m_server.start_thread(); + + con(model, &Settings::Model::EnabledChanged, this, [this, &model](bool e) { + e&& model.getServerEnabled() ? m_server.start_thread() : m_server.stop_thread(); + }, Qt::QueuedConnection); + + con(model, &Settings::Model::WebUiPathChanged, this, [this](const QString& s) { + m_server.set_path(s.toStdString()); + }, Qt::QueuedConnection); + + con(model, &Settings::Model::ServerAddressChanged, this, [this](const QString& s) { + m_server.set_address(s.toStdString().c_str()); + }, Qt::QueuedConnection); + + con(model, &Settings::Model::ServerPortChanged, this, + [this](unsigned short s) { m_server.set_port(s); }, Qt::QueuedConnection); + + con(model, &Settings::Model::ServerEnabledChanged, this, [this, &model](bool e) { + e&& model.getEnabled() ? m_server.start_thread() : m_server.stop_thread(); + }, Qt::QueuedConnection); +} + +DocumentPlugin::~DocumentPlugin() { } + +} \ No newline at end of file diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/DocumentPlugin.hpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/DocumentPlugin.hpp new file mode 100644 index 0000000000..b147aa47bd --- /dev/null +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/DocumentPlugin.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "HttpServer.hpp" + +#include + +namespace RemoteControl::HttpServer +{ +struct DocumentPlugin : score::DocumentPlugin +{ +public: + DocumentPlugin(const score::DocumentContext& doc, QObject* parent); + ~DocumentPlugin(); + +private: + HttpServer m_server; +}; + +} \ No newline at end of file diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/HttpServer.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/HttpServer.cpp new file mode 100644 index 0000000000..b9628b8f2d --- /dev/null +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/HttpServer.cpp @@ -0,0 +1,342 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +#include "HttpServer.hpp" + +#include + +#include +#include + +namespace RemoteControl::HttpServer +{ +HttpServer::HttpServer() { } + +HttpServer::~HttpServer() +{ + stop_thread(); +} + +// Return a reasonable mime type based on the extension of a file. +beast::string_view HttpServer::mime_type(beast::string_view path) +{ + using beast::iequals; + auto const ext = [&path] { + auto const pos = path.rfind("."); + if(pos == beast::string_view::npos) + return beast::string_view{}; + return path.substr(pos); + }(); + if(iequals(ext, ".htm")) + return "text/html"; + if(iequals(ext, ".html")) + return "text/html"; + if(iequals(ext, ".php")) + return "text/html"; + if(iequals(ext, ".css")) + return "text/css"; + if(iequals(ext, ".txt")) + return "text/plain"; + if(iequals(ext, ".js")) + return "application/javascript"; + if(iequals(ext, ".json")) + return "application/json"; + if(iequals(ext, ".xml")) + return "application/xml"; + if(iequals(ext, ".swf")) + return "application/x-shockwave-flash"; + if(iequals(ext, ".wasm")) + return "application/wasm"; + if(iequals(ext, ".flv")) + return "video/x-flv"; + if(iequals(ext, ".png")) + return "image/png"; + if(iequals(ext, ".jpe")) + return "image/jpeg"; + if(iequals(ext, ".jpeg")) + return "image/jpeg"; + if(iequals(ext, ".jpg")) + return "image/jpeg"; + if(iequals(ext, ".gif")) + return "image/gif"; + if(iequals(ext, ".bmp")) + return "image/bmp"; + if(iequals(ext, ".ico")) + return "image/vnd.microsoft.icon"; + if(iequals(ext, ".tiff")) + return "image/tiff"; + if(iequals(ext, ".tif")) + return "image/tiff"; + if(iequals(ext, ".svg")) + return "image/svg+xml"; + if(iequals(ext, ".svgz")) + return "image/svg+xml"; + return "application/text"; +} + +// Append an HTTP rel-path to a local filesystem path. +// The returned path is normalized for the platform. +std::string HttpServer::path_cat(beast::string_view path) +{ + std::lock_guard lock{mtx}; + if(m_buildWasmPath.empty()) + return std::string(path); + std::string result(m_buildWasmPath); +#ifdef BOOST_MSVC + char constexpr path_separator = '\\'; + if(result.back() == path_separator) + result.resize(result.size() - 1); + result.append(path.data(), path.size()); + for(auto& c : result) + if(c == '/') + c = path_separator; +#else + char constexpr path_separator = '/'; + if(result.back() == path_separator) + result.resize(result.size() - 1); + result.append(path.data(), path.size()); +#endif + return result; +} + +// This function produces an HTTP response for the given +// request. The type of the response object depends on the +// contents of the request, so the interface requires the +// caller to pass a generic lambda for receiving the response. +template +void HttpServer::handle_request( + http::request>&& req, const Send& send) +{ + using namespace score; + + // Returns a bad request response + auto const bad_request = [&req](beast::string_view why) { + http::response res{http::status::bad_request, req.version()}; + res.set(http::field::server, ossia_score_verison_agent()); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = std::string(why); + res.prepare_payload(); + return res; + }; + + // Returns a not found response + auto const not_found = [&req](beast::string_view target) { + http::response res{http::status::not_found, req.version()}; + res.set(http::field::server, ossia_score_verison_agent()); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "The resource '" + std::string(target) + "' was not found.
Go to the following address : http://ip_address:port/ossia_remote.html."; + res.prepare_payload(); + return res; + }; + + // Returns a server error response + auto const server_error = [&req](beast::string_view what) { + http::response res{ + http::status::internal_server_error, req.version()}; + res.set(http::field::server, ossia_score_verison_agent()); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + // Make sure we can handle the method + if(req.method() != http::verb::get && req.method() != http::verb::head) + return send(bad_request("Unknown HTTP-method")); + + // Request path must be absolute and not contain "..". + if(req.target().empty() || req.target()[0] != '/' + || req.target().find("..") != beast::string_view::npos) + return send(bad_request("Illegal request-target")); + + // Build the path to the requested file + std::string path = path_cat(req.target()); + if(req.target().back() == '/') + path.append("index.html"); + + // Attempt to open the file + beast::error_code ec; + http::file_body::value_type body; + + body.open(path.c_str(), beast::file_mode::scan, ec); + + // Handle the case where the file doesn't exist + if(ec == beast::errc::no_such_file_or_directory) + return send(not_found(req.target())); + + // Handle an unknown error + if(ec) + return send(server_error(ec.message())); + + // Cache the size since we need it after the move + auto const size = body.size(); + + // Respond to HEAD request + if(req.method() == http::verb::head) + { + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, ossia_score_verison_agent()); + res.set(http::field::content_type, mime_type(path)); + res.content_length(size); + res.keep_alive(false); + return send(std::move(res)); + } + + // Respond to GET request + http::response res{ + std::piecewise_construct, std::make_tuple(std::move(body)), + std::make_tuple(http::status::ok, req.version())}; + + // Allow Cross-Origin Resource Sharing (CORS). + res.set(http::field::access_control_allow_headers, "*"); + res.set(http::field::access_control_allow_origin, "*"); + res.set(http::field::access_control_allow_methods, "GET"); + + res.set(http::field::server, ossia_score_verison_agent()); + res.set(http::field::content_type, mime_type(path)); + res.content_length(size); + res.keep_alive(false); + return send(std::move(res)); +} + +// Report a failure +void HttpServer::fail(beast::error_code ec, char const* what) +{ + qDebug() << what << ": " << ec.message(); +} + +// Handles an HTTP server connection +void HttpServer::do_session(tcp::socket& socket) +{ + bool close = false; + beast::error_code ec; + + // This buffer is required to persist across reads + beast::flat_buffer buffer; + + // This lambda is used to send messages + const auto lambda{[&close, &socket, &ec]( + http::message&& msg) { + // Determine if we should close the connection after + close = msg.need_eof(); + + // We need the serializer here because the serializer requires + // a non-const file_body, and the message oriented version of + // http::write only works with const messages. + http::serializer sr{msg}; + http::write(socket, sr, ec); + }}; + + for(;;) + { + // Read a request + http::request req; + http::read(socket, buffer, req, ec); + if(ec == http::error::end_of_stream) + break; + if(ec) + HttpServer::fail(ec, "read"); + + // Send the response + HttpServer::handle_request(std::move(req), lambda); + + if(ec) + HttpServer::fail(ec, "write"); + if(close) + { + // This means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + break; + } + } + + // Send a TCP shutdown + socket.shutdown(tcp::socket::shutdown_send, ec); + + // At this point the connection is closed gracefully +} + +// Launch the open_server function in a thread +void HttpServer::start_thread() +{ + m_serverThread = std::thread{[this] { open_server(); }}; +} + +void HttpServer::stop_thread() +{ + if(!running()) + return; + + shutdown(m_listenSocket, SHUT_RDWR); + m_ioc.stop(); + m_serverThread.join(); +} + +void HttpServer::set_path(const std::string& str) +{ + std::lock_guard lock{mtx}; + m_buildWasmPath = str; +} + +void HttpServer::set_address(const std::string& str) +{ + bool is_running{running()}; + if(is_running) + stop_thread(); + m_endpoint.address(net::ip::make_address(str.c_str())); + if(is_running) + start_thread(); +} + +void HttpServer::set_port(unsigned short prt) +{ + bool is_running{running()}; + if(is_running) + stop_thread(); + m_endpoint.port(prt); + if(is_running) + start_thread(); +} + +// Open a server using sockets +int HttpServer::open_server() +{ + try + { + // The acceptor receives incoming connections + tcp::acceptor acceptor{m_ioc, m_endpoint}; + m_listenSocket = acceptor.native_handle(); + for(;;) + { + // This will receive the new connection + tcp::socket socket{m_ioc}; + + // Block until we get a connection + acceptor.accept(socket); + + // Launch the session, transferring ownership of the socket + do_session(socket); + } + } + catch(const std::exception& e) + { + qDebug() << "Error: " << e.what(); + return EXIT_FAILURE; + } +} + +bool HttpServer::running() +{ + return m_serverThread.joinable(); +} + +} diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/HttpServer.hpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/HttpServer.hpp new file mode 100644 index 0000000000..b0e1c66b69 --- /dev/null +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/HttpServer/HttpServer.hpp @@ -0,0 +1,95 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +#pragma once + +#define BOOST_DATE_TIME_NO_LIB 1 + +#include +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#define SHUT_RDWR 2 +#endif + +#include + +namespace score +{ +static std::string ossia_score_verison_agent() +{ + static QString agent{QCoreApplication::organizationDomain() + + '.' + + QCoreApplication::applicationName() + + '/' + + QCoreApplication::applicationVersion()}; + + return agent.toStdString(); +}; +} + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +namespace RemoteControl::HttpServer +{ +class HttpServer +{ +public: + HttpServer(); + ~HttpServer(); + + void start_thread(); + void stop_thread(); + void set_path(const std::string& str); + void set_address(const std::string& str); + void set_port(unsigned short prt); + +private: + // Return a reasonable mime type based on the extension of a file. + beast::string_view mime_type(beast::string_view path); + + // Append an HTTP rel-path to a local filesystem path. + // The returned path is normalized for the platform. + std::string path_cat(beast::string_view path); + + // This function produces an HTTP response for the given + // request. The type of the response object depends on the + // contents of the request, so the interface requires the + // caller to pass a generic lambda for receiving the response. + template + void handle_request( + http::request>&& req, const Send& send); + + // Report a failure + void fail(beast::error_code ec, char const* what); + + // Handles an HTTP server connection + void do_session(tcp::socket& socket); + + // Open a server using sockets + int open_server(); + bool running(); + + net::io_context m_ioc; + tcp::endpoint m_endpoint{}; + std::thread m_serverThread; + std::string m_buildWasmPath; + int m_listenSocket{}; + std::mutex mtx{}; +}; +} diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.cpp index 034945b651..4a4aa4ddd1 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.cpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.cpp @@ -1,6 +1,9 @@ #include "Model.hpp" +#include + #include +#include #include W_OBJECT_IMPL(RemoteControl::Settings::Model) @@ -8,13 +11,20 @@ namespace RemoteControl { namespace Settings { - namespace Parameters { SETTINGS_PARAMETER_IMPL(Enabled){QStringLiteral("RemoteControl/Enabled"), false}; +SETTINGS_PARAMETER_IMPL(WebUiPath){QStringLiteral("RemoteControl/WebUiPath"), ""}; +SETTINGS_PARAMETER_IMPL(ServerAddress){QStringLiteral("RemoteControl/ServerAddress"), "0.0.0.0"}; +SETTINGS_PARAMETER_IMPL(ServerPort){QStringLiteral("RemoteControl/ServerPort"), 8080}; +SETTINGS_PARAMETER_IMPL(ServerEnabled){QStringLiteral("RemoteControl/ServerEnabled"), false}; static auto list() { - return std::tie(Enabled); + return std::tie(Enabled + , WebUiPath + , ServerAddress + , ServerPort + , ServerEnabled); } } @@ -24,8 +34,23 @@ Model::Model( : score::SettingsDelegateModel{k, nullptr} { score::setupDefaultSettings(set, Parameters::list(), *this); + + if (m_WebUiPath.isEmpty()) + { + const auto path{score::AppContext() + .settings() + .getPackagesPath() + + "/wasm-remote/"}; + + if (QDir{path}.exists()) + setWebUiPath(path); + } } SCORE_SETTINGS_PARAMETER_CPP(bool, Model, Enabled) +SCORE_SETTINGS_PARAMETER_CPP(QString, Model, WebUiPath) +SCORE_SETTINGS_PARAMETER_CPP(QString, Model, ServerAddress) +SCORE_SETTINGS_PARAMETER_CPP(unsigned short, Model, ServerPort) +SCORE_SETTINGS_PARAMETER_CPP(bool, Model, ServerEnabled) } } diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.hpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.hpp index 49c01155f1..ad0bd1fa28 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.hpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Model.hpp @@ -10,7 +10,11 @@ namespace Settings class SCORE_PLUGIN_REMOTECONTROL_EXPORT Model : public score::SettingsDelegateModel { W_OBJECT(Model) - bool m_Enabled = false; + bool m_Enabled{false}; + bool m_ServerEnabled{false}; + QString m_WebUiPath{}; + QString m_ServerAddress{"0.0.0.0"}; + unsigned short m_ServerPort{8080}; public: Model( @@ -18,9 +22,16 @@ class SCORE_PLUGIN_REMOTECONTROL_EXPORT Model : public score::SettingsDelegateMo const score::ApplicationContext& ctx); SCORE_SETTINGS_PARAMETER_HPP(SCORE_PLUGIN_REMOTECONTROL_EXPORT, bool, Enabled) + SCORE_SETTINGS_PARAMETER_HPP(SCORE_PLUGIN_REMOTECONTROL_EXPORT, QString, WebUiPath) + SCORE_SETTINGS_PARAMETER_HPP(SCORE_PLUGIN_REMOTECONTROL_EXPORT, QString, ServerAddress) + SCORE_SETTINGS_PARAMETER_HPP(SCORE_PLUGIN_REMOTECONTROL_EXPORT, unsigned short, ServerPort) + SCORE_SETTINGS_PARAMETER_HPP(SCORE_PLUGIN_REMOTECONTROL_EXPORT, bool, ServerEnabled) }; SCORE_SETTINGS_PARAMETER(Model, Enabled) - +SCORE_SETTINGS_PARAMETER(Model, WebUiPath) +SCORE_SETTINGS_PARAMETER(Model, ServerAddress) +SCORE_SETTINGS_PARAMETER(Model, ServerPort) +SCORE_SETTINGS_PARAMETER(Model, ServerEnabled) } } diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Presenter.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Presenter.cpp index d079eb9728..bc60258824 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Presenter.cpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/Presenter.cpp @@ -27,11 +27,47 @@ Presenter::Presenter(Model& m, View& v, QObject* parent) } }); + con(v, &View::webUiPathChanged, this, [&](auto val) { + if(val != m.getWebUiPath()) + { + m_disp.submit(this->model(this), val); + } + }); + + con(v, &View::serverAddressChanged, this, [&](auto val) { + if(val != m.getServerAddress()) + { + m_disp.submit(this->model(this), val); + } + }); + + con(v, &View::serverPortChanged, this, [&](auto val) { + if(val != m.getServerPort()) + { + m_disp.submit(this->model(this), val); + } + }); + + con(v, &View::serverEnabledChanged, this, [&](auto val) { + if(val != m.getServerEnabled()) + { + m_disp.submit(this->model(this), val); + } + }); + // model -> view con(m, &Model::EnabledChanged, &v, &View::setEnabled); + con(m, &Model::WebUiPathChanged, &v, &View::setWebUiPath); + con(m, &Model::ServerAddressChanged, &v, &View::setServerAddress); + con(m, &Model::ServerPortChanged, &v, &View::setServerPort); + con(m, &Model::ServerEnabledChanged, &v, &View::setServerEnabled); // initial value v.setEnabled(m.getEnabled()); + v.setWebUiPath(m.getWebUiPath()); + v.setServerAddress(m.getServerAddress()); + v.setServerPort(m.getServerPort()); + v.setServerEnabled(m.getServerEnabled()); } } diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.cpp index 3de1d02c8f..2eb033e922 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.cpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.cpp @@ -1,10 +1,14 @@ #include "View.hpp" +#include "score/widgets/MarginLess.hpp" #include #include #include #include +#include +#include +#include #include W_OBJECT_IMPL(RemoteControl::Settings::View) @@ -21,14 +25,19 @@ View::View() { m_enabled = new QCheckBox{tr("Enabled")}; - connect(m_enabled, SignalUtils::QCheckBox_checkStateChanged(), this, [&](int t) { + connect(m_enabled + , SignalUtils::QCheckBox_checkStateChanged() + , this + , [&](int t) { switch(t) { case Qt::Unchecked: enabledChanged(false); + m_web_ui->setEnabled(false); break; case Qt::Checked: enabledChanged(true); + m_web_ui->setEnabled(true); break; default: break; @@ -37,6 +46,75 @@ View::View() lay->addRow(m_enabled); } + + { + m_web_ui = new score::FormWidget{tr("Web UI")}; + auto web_lay = m_web_ui->layout(); + + auto subw{new QWidget}; + auto sublay{new score::MarginLess{subw}}; + m_web_ui_path = new QLineEdit{}; + + auto browse{new QPushButton{tr("Browse...")}}; + browse->setMaximumWidth(100); + + connect(browse, &QPushButton::clicked, this, [this] + { + auto f{QFileDialog::getExistingDirectory( + nullptr, tr("Web UI folder"), m_web_ui_path->displayText())}; + if (!f.isEmpty()) webUiPathChanged(f); + }); + + sublay->addWidget(m_web_ui_path); + sublay->addWidget(browse); + web_lay->addRow(tr("Web UI folder"), subw); + + subw = new QWidget; + sublay = new score::MarginLess{subw}; + m_server_address = new QLineEdit{}; + m_server_address->setPlaceholderText("0.0.0.0"); + + connect(m_server_address + , &QLineEdit::textEdited + , this, [&](const QString& str) + { if (!str.isEmpty()) serverAddressChanged(str); }); + + m_server_port = new QSpinBox{}; + m_server_port->setRange(0, 9999); + m_server_port->setMaximumWidth(100); + + connect(m_server_port + , SignalUtils::QSpinBox_valueChanged_int() + , this + , [&](int t) + { serverPortChanged(t); }); + + sublay->addWidget(m_server_address); + sublay->addWidget(m_server_port); + web_lay->addRow(tr("HTTP server address/port"), subw); + + m_server_enabled = new QCheckBox{tr("Enable HTTP server")}; + + connect(m_server_enabled + , SignalUtils::QCheckBox_checkStateChanged() + , this + , [&](int t) { + switch(t) + { + case Qt::Unchecked: + serverEnabledChanged(false); + break; + case Qt::Checked: + serverEnabledChanged(true); + break; + default: + break; + } + }); + + web_lay->addRow(m_server_enabled); + lay->addRow(m_web_ui); + } } void View::setEnabled(bool val) @@ -56,6 +134,41 @@ void View::setEnabled(bool val) } } +void View::setWebUiPath(const QString& val) +{ + if (val != m_web_ui_path->displayText()) + m_web_ui_path->setText(val); +} + +void View::setServerAddress(const QString& val) +{ + if (val != m_server_address->displayText()) + m_server_address->setText(val); +} + +void View::setServerPort(unsigned short val) +{ + if (val != m_server_port->value()) + m_server_port->setValue(val); +} + +void View::setServerEnabled(bool val) +{ + switch(m_server_enabled->checkState()) + { + case Qt::Unchecked: + if(val) + m_server_enabled->setChecked(true); + break; + case Qt::Checked: + if(!val) + m_server_enabled->setChecked(false); + break; + default: + break; + } +} + QWidget* View::getWidget() { return m_widg; diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.hpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.hpp index 93171f6b70..cdf0d4b8ca 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.hpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/Settings/View.hpp @@ -3,6 +3,8 @@ #include class QCheckBox; +class QLineEdit; +class QVBoxLayout; namespace score { @@ -19,14 +21,28 @@ class View : public score::GlobalSettingsView public: View(); void setEnabled(bool); + void setWebUiPath(const QString&); + void setServerAddress(const QString&); + void setServerPort(unsigned short); + void setServerEnabled(bool); void enabledChanged(bool b) W_SIGNAL(enabledChanged, b); + void webUiPathChanged(QString s) W_SIGNAL(webUiPathChanged, s); + void serverAddressChanged(QString s) W_SIGNAL(serverAddressChanged, s); + void serverPortChanged(unsigned short s) W_SIGNAL(serverPortChanged, s); + void serverEnabledChanged(bool b) W_SIGNAL(serverEnabledChanged, b); private: QWidget* getWidget() override; score::FormWidget* m_widg{}; QCheckBox* m_enabled{}; + + score::FormWidget* m_web_ui{}; + QLineEdit* m_web_ui_path{}; + QLineEdit* m_server_address{}; + QSpinBox* m_server_port{}; + QCheckBox* m_server_enabled{}; }; }