409 lines
13 KiB
C++
409 lines
13 KiB
C++
#include <QApplication>
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QTabWidget>
|
|
#include <QTextEdit>
|
|
#include <QTreeWidget>
|
|
#include <QTreeWidgetItem>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
#include <QMap>
|
|
#include <QStringList>
|
|
|
|
#include <QAbstractSocket>
|
|
#include <QUrl>
|
|
#include <QWebSocket>
|
|
|
|
// ----------------------------------------------------------------
|
|
// 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<QString, QTreeWidgetItem *> 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<QTextEdit *> 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"
|