#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------- // Settings tree // ---------------------------------------------------------------- class SettingsTree : public QTreeWidget { Q_OBJECT public: // Emitted when user edits a value cell // key = full slash path, value = new string Q_SIGNAL void valueEdited(const QString &key, const QString &value); explicit SettingsTree(QWidget *parent = nullptr) : QTreeWidget(parent) { setColumnCount(2); setHeaderLabels({"Key", "Value"}); header()->setSectionResizeMode(0, QHeaderView::Interactive); header()->setSectionResizeMode(1, QHeaderView::Stretch); header()->setStretchLastSection(true); setAlternatingRowColors(true); setUniformRowHeights(true); setAnimated(true); setIndentation(16); setColumnWidth(0, 240); connect(this, &QTreeWidget::itemChanged, this, &SettingsTree::onItemChanged); } void loadKeys(const QStringList &keys) { m_loading = true; clear(); m_itemMap.clear(); for (const QString &key : keys) { const QStringList parts = key.split('/'); QTreeWidgetItem *parent = invisibleRootItem(); QString accumulated; for (int i = 0; i < parts.size(); ++i) { if (!accumulated.isEmpty()) accumulated += '/'; accumulated += parts[i]; if (!m_itemMap.contains(accumulated)) { auto *item = new QTreeWidgetItem(parent); item->setText(0, parts[i]); item->setExpanded(false); // Only leaf nodes (last part) are editable const bool isLeaf = (i == parts.size() - 1); if (isLeaf) { item->setFlags(item->flags() | Qt::ItemIsEditable); item->setToolTip(1, "Double-click to edit and send"); } else { item->setFlags(item->flags() & ~Qt::ItemIsEditable); } m_itemMap[accumulated] = item; } parent = m_itemMap[accumulated]; } } resizeColumnToContents(0); m_loading = false; } void setValue(const QString &key, const QString &value) { if (!m_itemMap.contains(key)) return; m_loading = true; m_itemMap[key]->setText(1, value); m_loading = false; } private slots: void onItemChanged(QTreeWidgetItem *item, int column) { if (m_loading || column != 1) return; // Find the full path for this item const QString key = fullPath(item); if (key.isEmpty()) return; emit valueEdited(key, item->text(1)); } private: QString fullPath(QTreeWidgetItem *item) const { // Walk up the tree to reconstruct the full slash-separated path QStringList parts; QTreeWidgetItem *cur = item; while (cur && cur != invisibleRootItem()) { parts.prepend(cur->text(0)); cur = cur->parent(); } return parts.join('/'); } QMap m_itemMap; bool m_loading = false; }; // ---------------------------------------------------------------- // WebSocket controller // ---------------------------------------------------------------- class WebSocketController : public QObject { Q_OBJECT public: explicit WebSocketController(QLineEdit *urlEdit, QLabel *statusLabel, QObject *parent = nullptr) : QObject(parent), m_urlEdit(urlEdit), m_statusLabel(statusLabel) {} QWebSocket *socket() { return &m_socket; } void addLogView(QTextEdit *l) { m_logs.append(l); } void setSettingsTree(SettingsTree *t) { m_settingsTree = t; // When user edits a value, send set command then verify connect(t, &SettingsTree::valueEdited, this, &WebSocketController::onValueEdited); } bool isConnected() const { return m_socket.state() == QAbstractSocket::ConnectedState; } public slots: void startConnection() { const QUrl url(m_urlEdit->text().trimmed()); if (!url.isValid()) { broadcast("Invalid URL"); return; } broadcast(QString("Connecting to %1").arg(url.toString())); m_statusLabel->setText("Connecting..."); m_socket.open(url); } void closeConnection() { broadcast("Closing connection"); m_settingsKeys.clear(); m_socket.close(); } void sendCommand(const QString &cmd) { if (!isConnected()) { broadcast("[not connected]"); return; } m_socket.sendTextMessage(cmd); broadcast(QString("TX: %1").arg(cmd)); } void onConnected() { broadcast("Connected"); m_statusLabel->setText("Connected"); sendCommand(QStringLiteral("GBL List")); } void onDisconnected() { broadcast("Disconnected"); m_statusLabel->setText("Disconnected"); m_settingsKeys.clear(); } void onTextMessageReceived(const QString &msg) { broadcast(QString("RX: %1").arg(msg)); handleProtocol(msg); } void onErrorOccurred(QAbstractSocket::SocketError) { broadcast(QString("Error: %1").arg(m_socket.errorString())); m_statusLabel->setText("Error"); } // Called when user finishes editing a cell in the settings tree void onValueEdited(const QString &key, const QString &newValue) { // Send: GBL key=newvalue sendCommand(QString("GBL %1=%2").arg(key, newValue)); // Verify: GBL key (machine echoes back confirmed value) sendCommand(QString("GBL %1").arg(key)); } private: void handleProtocol(const QString &msg) { if (!m_settingsTree) return; const QStringList tokens = msg.split(' ', Qt::SkipEmptyParts); if (tokens.size() < 2) return; // GBL List key1 key2 ... if (tokens[0] == "GBL" && tokens[1] == "List" && tokens.size() > 2) { m_settingsKeys = tokens.mid(2); m_settingsTree->loadKeys(m_settingsKeys); for (const QString &key : m_settingsKeys) sendCommand(QString("GBL %1").arg(key)); return; } // GBL key=value if (tokens[0] == "GBL" && tokens.size() >= 2) { const QString payload = tokens[1]; const int eqIdx = payload.indexOf('='); if (eqIdx > 0) { const QString key = payload.left(eqIdx); QString value = payload.mid(eqIdx + 1); if (tokens.size() > 2) value += ' ' + tokens.mid(2).join(' '); m_settingsTree->setValue(key, value); } } } void broadcast(const QString &line) { for (auto *log : m_logs) log->append(line); } QLineEdit *m_urlEdit = nullptr; QLabel *m_statusLabel = nullptr; QWebSocket m_socket; QList m_logs; SettingsTree *m_settingsTree = nullptr; QStringList m_settingsKeys; }; // ---------------------------------------------------------------- // Tab builders // ---------------------------------------------------------------- static QWidget *makeSettingsTab(WebSocketController *controller, QWidget *parent = nullptr) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); layout->setContentsMargins(2, 2, 2, 2); auto *tree = new SettingsTree(page); auto *placeholder = new QTreeWidgetItem(tree); placeholder->setText(0, "Connect to load settings..."); layout->addWidget(tree, 1); controller->setSettingsTree(tree); return page; } static QWidget *makeManualTab(WebSocketController *controller, QWidget *parent = nullptr) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); auto *sendRow = new QHBoxLayout(); auto *cmdEdit = new QLineEdit(page); cmdEdit->setPlaceholderText("Enter command to send..."); auto *sendBtn = new QPushButton("Send", page); sendRow->addWidget(new QLabel("Command:", page)); sendRow->addWidget(cmdEdit, 1); sendRow->addWidget(sendBtn); auto *rxLog = new QTextEdit(page); rxLog->setReadOnly(true); rxLog->setPlaceholderText("Received messages will appear here..."); layout->addLayout(sendRow); layout->addWidget(rxLog, 1); controller->addLogView(rxLog); auto send = [cmdEdit, controller]() { const QString cmd = cmdEdit->text().trimmed(); if (!cmd.isEmpty()) { controller->sendCommand(cmd); cmdEdit->clear(); } }; QObject::connect(sendBtn, &QPushButton::clicked, page, send); QObject::connect(cmdEdit, &QLineEdit::returnPressed, page, send); return page; } static QWidget *makePanelsTab(WebSocketController *controller, QWidget *parent = nullptr) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); auto *log = new QTextEdit(page); log->setReadOnly(true); layout->addWidget(log, 1); controller->addLogView(log); return page; } static QWidget *makePlaceholder(const QString &text, QWidget *parent = nullptr) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); auto *label = new QLabel(text, page); label->setAlignment(Qt::AlignCenter); layout->addWidget(label); return page; } // ---------------------------------------------------------------- // main // ---------------------------------------------------------------- int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle("Qt WASM WebSocket Client"); auto *mainLayout = new QVBoxLayout(&window); mainLayout->setContentsMargins(6, 6, 6, 6); mainLayout->setSpacing(4); auto *headerLayout = new QHBoxLayout(); auto *urlLabel = new QLabel("WebSocket URL:", &window); auto *urlEdit = new QLineEdit(&window); urlEdit->setPlaceholderText("ws://127.0.0.1:3491/"); urlEdit->setText("ws://127.0.0.1:3491/"); auto *connectButton = new QPushButton("Connect", &window); auto *disconnectButton = new QPushButton("Disconnect", &window); disconnectButton->setEnabled(false); auto *statusLabel = new QLabel("Disconnected", &window); headerLayout->addWidget(urlLabel); headerLayout->addWidget(urlEdit, 1); headerLayout->addWidget(connectButton); headerLayout->addWidget(disconnectButton); headerLayout->addWidget(statusLabel); mainLayout->addLayout(headerLayout); auto *controller = new WebSocketController(urlEdit, statusLabel, &window); auto *tabs = new QTabWidget(&window); tabs->addTab(makePlaceholder("Games content goes here"), "games"); tabs->addTab(makePlaceholder("Versions content goes here"), "versions"); tabs->addTab(makeManualTab(controller, &window), "manual"); tabs->addTab(makeSettingsTab(controller, &window), "settings"); tabs->addTab(makePlaceholder("Power content goes here"), "power"); tabs->addTab(makePanelsTab(controller, &window), "panels"); mainLayout->addWidget(tabs, 1); QObject::connect(connectButton, &QPushButton::clicked, controller, &WebSocketController::startConnection); QObject::connect(disconnectButton, &QPushButton::clicked, controller, &WebSocketController::closeConnection); QObject::connect(controller->socket(), &QWebSocket::connected, controller, &WebSocketController::onConnected); QObject::connect(controller->socket(), &QWebSocket::disconnected, controller, &WebSocketController::onDisconnected); QObject::connect(controller->socket(), &QWebSocket::textMessageReceived, controller, &WebSocketController::onTextMessageReceived); QObject::connect(controller->socket(), &QWebSocket::errorOccurred, controller, &WebSocketController::onErrorOccurred); QObject::connect(controller->socket(), &QWebSocket::connected, &window, [connectButton, disconnectButton]() { connectButton->setEnabled(false); disconnectButton->setEnabled(true); }); QObject::connect(controller->socket(), &QWebSocket::disconnected, &window, [connectButton, disconnectButton]() { connectButton->setEnabled(true); disconnectButton->setEnabled(false); }); window.resize(950, 580); window.show(); return app.exec(); } #include "main.moc"