mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-05-14 05:12:09 -04:00
Meshtastic demod: make key dialog an object on its own
This commit is contained in:
parent
23e17844d6
commit
748f024771
84
modemmeshtastic/README.md
Normal file
84
modemmeshtastic/README.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Meshtastic port payload reference
|
||||
|
||||
This note summarizes Meshtastic port names used by SDRangel packet decoding and what payload kind to expect.
|
||||
|
||||
Scope:
|
||||
- Port names and IDs come from the local map in `meshtasticpacket.cpp` (`kPortMap`).
|
||||
- Payload types are practical expectations based on Meshtastic app semantics.
|
||||
- SDRangel currently does **generic** data decode only (header + protobuf envelope fields + raw payload), not per-port protobuf decoding.
|
||||
|
||||
## How to interpret payload display in SDRangel
|
||||
|
||||
- Text is shown only when payload bytes look like valid UTF-8.
|
||||
- Otherwise payload is shown as hex.
|
||||
- For most ports (POSITION, NODEINFO, TELEMETRY, ROUTING, etc.), payload is protobuf/binary, so hex is the normal representation.
|
||||
- Seeing a mix of readable text and binary garbage (for example NODEINFO_APP showing the node name) is normal: protobuf stores `string` fields as raw UTF-8 bytes inline in the binary stream. The readable parts are string field values; the surrounding bytes are protobuf field tags (varints), numeric IDs, and other binary-encoded fields.
|
||||
|
||||
## Port list (current SDRangel map)
|
||||
|
||||
| Port name | ID | Expected payload kind | Typical content | SDRangel decode level |
|
||||
|---|---:|---|---|---|
|
||||
| UNKNOWN_APP | 0 | binary/unknown | unspecified | envelope + raw payload |
|
||||
| TEXT / TEXT_MESSAGE_APP | 1 | UTF-8 text | plain chat text | envelope + text/hex payload |
|
||||
| REMOTE_HARDWARE_APP | 2 | protobuf/binary | remote hardware control messages | envelope + raw payload |
|
||||
| POSITION_APP | 3 | protobuf/binary | GPS position fields | envelope + raw payload |
|
||||
| NODEINFO_APP | 4 | protobuf/binary | node metadata / identity (long name, short name, hardware model, node ID) | envelope + raw payload. Node name strings appear in clear because protobuf stores `string` fields as raw UTF-8 bytes; surrounding bytes are binary (field tags, varint IDs, enums). |
|
||||
| ROUTING_APP | 5 | protobuf/binary | routing/control data | envelope + raw payload |
|
||||
| ADMIN_APP | 6 | protobuf/binary | admin/config operations | envelope + raw payload |
|
||||
| TEXT_MESSAGE_COMPRESSED_APP | 7 | compressed/binary | compressed text payload | envelope + raw payload |
|
||||
| WAYPOINT_APP | 8 | protobuf/binary | waypoint structures | envelope + raw payload |
|
||||
| AUDIO_APP | 9 | binary | encoded audio frames | envelope + raw payload |
|
||||
| DETECTION_SENSOR_APP | 10 | protobuf/binary | sensor detection events | envelope + raw payload |
|
||||
| ALERT_APP | 11 | protobuf/binary | alert/notification message | envelope + raw payload |
|
||||
| KEY_VERIFICATION_APP | 12 | protobuf/binary | key verification/signaling | envelope + raw payload |
|
||||
| REPLY_APP | 32 | protobuf/binary | reply wrapper/ack data | envelope + raw payload |
|
||||
| IP_TUNNEL_APP | 33 | binary | tunneled IP packet bytes | envelope + raw payload |
|
||||
| PAXCOUNTER_APP | 34 | protobuf/binary | pax counter telemetry | envelope + raw payload |
|
||||
| STORE_FORWARD_PLUSPLUS_APP | 35 | protobuf/binary | store-and-forward control/data | envelope + raw payload |
|
||||
| NODE_STATUS_APP | 36 | protobuf/binary | node status information | envelope + raw payload |
|
||||
| SERIAL_APP | 64 | binary | serial tunnel bytes | envelope + raw payload |
|
||||
| STORE_FORWARD_APP | 65 | protobuf/binary | store-and-forward messages | envelope + raw payload |
|
||||
| RANGE_TEST_APP | 66 | protobuf/binary | range test metadata/results | envelope + raw payload |
|
||||
| TELEMETRY_APP | 67 | protobuf/binary | telemetry records | envelope + raw payload |
|
||||
| ZPS_APP | 68 | protobuf/binary | ZPS-specific messages | envelope + raw payload |
|
||||
| SIMULATOR_APP | 69 | protobuf/binary | simulator-generated messages | envelope + raw payload |
|
||||
| TRACEROUTE_APP | 70 | protobuf/binary | traceroute path/hops | envelope + raw payload |
|
||||
| NEIGHBORINFO_APP | 71 | protobuf/binary | neighbor table entries | envelope + raw payload |
|
||||
| ATAK_PLUGIN | 72 | protobuf/binary | ATAK integration payloads | envelope + raw payload |
|
||||
|
||||
## Notes
|
||||
|
||||
- This is a practical operator reference, not a protocol authority.
|
||||
- For authoritative and exhaustive app payload schemas, use Meshtastic upstream protobuf definitions (for example `portnums.proto` and related message protos).
|
||||
- If SDRangel later adds per-port protobuf decoding, this table should be updated with "decoded fields available" per port.
|
||||
|
||||
## Protocol availability and per-port decoding
|
||||
|
||||
The Meshtastic protocol is fully public and open-source (GPL v3, same license as SDRangel).
|
||||
|
||||
- Full `.proto` definitions: https://github.com/meshtastic/protobufs
|
||||
- Browsable API reference with all message types and fields: https://buf.build/meshtastic/protobufs
|
||||
- Protocol architecture doc: https://meshtastic.org/docs/overview/mesh-algo/
|
||||
|
||||
Key message types relevant to SDRangel decoding:
|
||||
|
||||
| Proto message | Port | Fields of interest |
|
||||
|---|---|---|
|
||||
| `Position` | POSITION_APP (3) | latitude, longitude, altitude, time, speed, heading |
|
||||
| `User` | NODEINFO_APP (4) | id (node ID string), long_name, short_name, hw_model, is_licensed |
|
||||
| `Routing` | ROUTING_APP (5) | route success/failure error codes |
|
||||
| `Data` | (envelope, already decoded) | portnum, payload, want_response, dest, source |
|
||||
| `Telemetry` | TELEMETRY_APP (67) | device metrics (battery, voltage, utilization), environment (temp, humidity), air quality |
|
||||
| `RouteDiscovery` | TRACEROUTE_APP (70) | route (node ID list), snr_towards, snr_back |
|
||||
| `NeighborInfo` | NEIGHBORINFO_APP (71) | node_id, node_broadcast_interval_secs, neighbors list |
|
||||
| `Waypoint` | WAYPOINT_APP (8) | id, latitude, longitude, expire, locked_to, name, description |
|
||||
|
||||
### Adding per-port decoding to SDRangel
|
||||
|
||||
Three practical approaches (in order of fit for this codebase):
|
||||
|
||||
1. **Extend the existing hand-parser** — `meshtasticpacket.cpp` already contains a minimal protobuf varint/length-delimited parser for the `Data` envelope. The same approach can be extended per-port for common types (POSITION, NODEINFO, TELEMETRY) with no extra dependency. Best choice for portability and minimal build impact.
|
||||
|
||||
2. **nanopb** — the C library used by Meshtastic firmware itself; tiny footprint, no external runtime, generates `.pb.c`/`.pb.h` from the upstream `.proto` files. Would require adding it to `CMakeLists.txt` as a dependency.
|
||||
|
||||
3. **Qt Protobuf** (`QProtobufSerializer`) — available in Qt 6.5+. Adds a Qt version constraint that may conflict with Qt5 builds.
|
||||
@ -34,10 +34,13 @@ if(NOT SERVER_MODE)
|
||||
${meshtastic_SOURCES}
|
||||
meshtasticdemodgui.cpp
|
||||
meshtasticdemodgui.ui
|
||||
meshtastickeysdialog.cpp
|
||||
meshtastickeysdialog.ui
|
||||
)
|
||||
set(meshtastic_HEADERS
|
||||
${meshtastic_HEADERS}
|
||||
meshtasticdemodgui.h
|
||||
meshtastickeysdialog.h
|
||||
)
|
||||
set(TARGET_NAME ${PLUGINS_PREFIX}demodmeshtastic)
|
||||
set(TARGET_LIB "Qt::Widgets")
|
||||
|
||||
@ -21,8 +21,6 @@
|
||||
|
||||
#include "device/deviceuiset.h"
|
||||
#include "device/deviceapi.h"
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QScrollBar>
|
||||
#include <QDebug>
|
||||
#include <QCoreApplication>
|
||||
@ -30,10 +28,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QAbstractItemView>
|
||||
#include <QList>
|
||||
@ -41,10 +36,8 @@
|
||||
#include <QPlainTextEdit>
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
@ -69,6 +62,7 @@
|
||||
#include "meshtasticdemod.h"
|
||||
#include "meshtasticdemodmsg.h"
|
||||
#include "meshtasticdemodgui.h"
|
||||
#include "meshtastickeysdialog.h"
|
||||
#include "meshtasticpacket.h"
|
||||
|
||||
namespace
|
||||
@ -940,100 +934,31 @@ void MeshtasticDemodGUI::advanceMeshAutoLock()
|
||||
|
||||
void MeshtasticDemodGUI::editMeshtasticKeys()
|
||||
{
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(tr("Meshtastic Keys"));
|
||||
dialog.resize(760, 460);
|
||||
MeshtasticKeysDialog dialog(this);
|
||||
dialog.setKeySpecList(m_settings.m_meshtasticKeySpecList);
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout(&dialog);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
QLabel* helpLabel = new QLabel(tr(
|
||||
"One key entry per line (or comma/semicolon separated).\n"
|
||||
"Formats: default, none, simple1..simple10, hex:<hex>, b64:<base64>, base64:<base64>, raw hex, raw base64.\n"
|
||||
"Optional channel mapping: channelName=keySpec (example: LongFast=default)."),
|
||||
&dialog);
|
||||
helpLabel->setWordWrap(true);
|
||||
layout->addWidget(helpLabel);
|
||||
m_settings.m_meshtasticKeySpecList = dialog.getKeySpecList();
|
||||
|
||||
QTextEdit* keyEditor = new QTextEdit(&dialog);
|
||||
keyEditor->setPlainText(m_settings.m_meshtasticKeySpecList);
|
||||
keyEditor->setToolTip(tr("Enter one or more key specs used to decrypt Meshtastic packets."));
|
||||
keyEditor->setPlaceholderText("LongFast=default\nnone\nLongSlow=hex:00112233445566778899aabbccddeeff");
|
||||
layout->addWidget(keyEditor, 1);
|
||||
if (m_meshKeysButton)
|
||||
{
|
||||
const bool hasCustomKeys = !m_settings.m_meshtasticKeySpecList.isEmpty();
|
||||
m_meshKeysButton->setText(hasCustomKeys ? tr("Keys*") : tr("Keys..."));
|
||||
m_meshKeysButton->setToolTip(hasCustomKeys ?
|
||||
tr("Custom Meshtastic decode keys configured. Click to edit.") :
|
||||
tr("Open Meshtastic key manager."));
|
||||
}
|
||||
|
||||
QHBoxLayout* statusLayout = new QHBoxLayout();
|
||||
QPushButton* validateButton = new QPushButton(tr("Validate"), &dialog);
|
||||
validateButton->setToolTip(tr("Validate key syntax and count without saving."));
|
||||
QLabel* statusLabel = new QLabel(&dialog);
|
||||
statusLabel->setToolTip(tr("Validation status for the current key list."));
|
||||
statusLabel->setWordWrap(true);
|
||||
statusLayout->addWidget(validateButton);
|
||||
statusLayout->addWidget(statusLabel, 1);
|
||||
layout->addLayout(statusLayout);
|
||||
applySettings();
|
||||
|
||||
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
auto validateInput = [keyEditor, statusLabel]() -> bool {
|
||||
const QString keyText = keyEditor->toPlainText().trimmed();
|
||||
|
||||
if (keyText.isEmpty())
|
||||
{
|
||||
statusLabel->setStyleSheet("QLabel { color: #bbbbbb; }");
|
||||
statusLabel->setText(QObject::tr("No custom keys set. Decoder will use environment/default keys."));
|
||||
return true;
|
||||
}
|
||||
|
||||
QString error;
|
||||
int keyCount = 0;
|
||||
|
||||
if (!modemmeshtastic::Packet::validateKeySpecList(keyText, error, &keyCount))
|
||||
{
|
||||
statusLabel->setStyleSheet("QLabel { color: #ff5555; }");
|
||||
statusLabel->setText(QObject::tr("Invalid key list: %1").arg(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
statusLabel->setStyleSheet("QLabel { color: #7cd67c; }");
|
||||
statusLabel->setText(QObject::tr("Valid: %1 key(s) parsed").arg(keyCount));
|
||||
return true;
|
||||
};
|
||||
|
||||
QObject::connect(validateButton, &QPushButton::clicked, &dialog, [validateInput]() {
|
||||
validateInput();
|
||||
});
|
||||
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, [this, &dialog, keyEditor, validateInput]() {
|
||||
if (!validateInput())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Keys"), tr("Fix the Meshtastic key list before saving."));
|
||||
return;
|
||||
}
|
||||
|
||||
m_settings.m_meshtasticKeySpecList = keyEditor->toPlainText().trimmed();
|
||||
|
||||
if (m_meshKeysButton)
|
||||
{
|
||||
const bool hasCustomKeys = !m_settings.m_meshtasticKeySpecList.isEmpty();
|
||||
m_meshKeysButton->setText(hasCustomKeys ? tr("Keys*") : tr("Keys..."));
|
||||
m_meshKeysButton->setToolTip(hasCustomKeys ?
|
||||
tr("Custom Meshtastic decode keys configured. Click to edit.") :
|
||||
tr("Open Meshtastic key manager."));
|
||||
}
|
||||
|
||||
applySettings();
|
||||
|
||||
if (m_settings.m_meshtasticKeySpecList.isEmpty()) {
|
||||
displayStatus(tr("MESH KEYS|using environment/default key set"));
|
||||
} else {
|
||||
displayStatus(tr("MESH KEYS|custom key set saved"));
|
||||
}
|
||||
|
||||
dialog.accept();
|
||||
});
|
||||
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
validateInput();
|
||||
dialog.exec();
|
||||
if (m_settings.m_meshtasticKeySpecList.isEmpty()) {
|
||||
displayStatus(tr("MESH KEYS|using environment/default key set"));
|
||||
} else {
|
||||
displayStatus(tr("MESH KEYS|custom key set saved"));
|
||||
}
|
||||
}
|
||||
|
||||
int MeshtasticDemodGUI::findBandwidthIndex(int bandwidthHz) const
|
||||
|
||||
71
plugins/channelrx/demodmeshtastic/meshtastickeysdialog.cpp
Normal file
71
plugins/channelrx/demodmeshtastic/meshtastickeysdialog.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "meshtastickeysdialog.h"
|
||||
#include "ui_meshtastickeysdialog.h"
|
||||
#include "meshtasticpacket.h"
|
||||
|
||||
MeshtasticKeysDialog::MeshtasticKeysDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::MeshtasticKeysDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
validateCurrentInput();
|
||||
}
|
||||
|
||||
MeshtasticKeysDialog::~MeshtasticKeysDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void MeshtasticKeysDialog::setKeySpecList(const QString& keySpecList)
|
||||
{
|
||||
ui->keyEditor->setPlainText(keySpecList);
|
||||
validateCurrentInput();
|
||||
}
|
||||
|
||||
QString MeshtasticKeysDialog::getKeySpecList() const
|
||||
{
|
||||
return ui->keyEditor->toPlainText().trimmed();
|
||||
}
|
||||
|
||||
void MeshtasticKeysDialog::on_validate_clicked()
|
||||
{
|
||||
validateCurrentInput();
|
||||
}
|
||||
|
||||
void MeshtasticKeysDialog::accept()
|
||||
{
|
||||
if (!validateCurrentInput())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Keys"), tr("Fix the Meshtastic key list before saving."));
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
bool MeshtasticKeysDialog::validateCurrentInput()
|
||||
{
|
||||
const QString keyText = ui->keyEditor->toPlainText().trimmed();
|
||||
|
||||
if (keyText.isEmpty())
|
||||
{
|
||||
ui->statusLabel->setStyleSheet("QLabel { color: #bbbbbb; }");
|
||||
ui->statusLabel->setText(tr("No custom keys set. Decoder will use environment/default keys."));
|
||||
return true;
|
||||
}
|
||||
|
||||
QString error;
|
||||
int keyCount = 0;
|
||||
|
||||
if (!modemmeshtastic::Packet::validateKeySpecList(keyText, error, &keyCount))
|
||||
{
|
||||
ui->statusLabel->setStyleSheet("QLabel { color: #ff5555; }");
|
||||
ui->statusLabel->setText(tr("Invalid key list: %1").arg(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
ui->statusLabel->setStyleSheet("QLabel { color: #7cd67c; }");
|
||||
ui->statusLabel->setText(tr("Valid: %1 key(s) parsed").arg(keyCount));
|
||||
return true;
|
||||
}
|
||||
31
plugins/channelrx/demodmeshtastic/meshtastickeysdialog.h
Normal file
31
plugins/channelrx/demodmeshtastic/meshtastickeysdialog.h
Normal file
@ -0,0 +1,31 @@
|
||||
#ifndef INCLUDE_MESHTASTICKEYSDIALOG_H
|
||||
#define INCLUDE_MESHTASTICKEYSDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
namespace Ui {
|
||||
class MeshtasticKeysDialog;
|
||||
}
|
||||
|
||||
class MeshtasticKeysDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MeshtasticKeysDialog(QWidget* parent = nullptr);
|
||||
~MeshtasticKeysDialog();
|
||||
|
||||
void setKeySpecList(const QString& keySpecList);
|
||||
QString getKeySpecList() const;
|
||||
|
||||
private slots:
|
||||
void on_validate_clicked();
|
||||
void accept() override;
|
||||
|
||||
private:
|
||||
bool validateCurrentInput();
|
||||
|
||||
Ui::MeshtasticKeysDialog* ui;
|
||||
};
|
||||
|
||||
#endif // INCLUDE_MESHTASTICKEYSDIALOG_H
|
||||
112
plugins/channelrx/demodmeshtastic/meshtastickeysdialog.ui
Normal file
112
plugins/channelrx/demodmeshtastic/meshtastickeysdialog.ui
Normal file
@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MeshtasticKeysDialog</class>
|
||||
<widget class="QDialog" name="MeshtasticKeysDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>760</width>
|
||||
<height>460</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Meshtastic Keys</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="helpLabel">
|
||||
<property name="text">
|
||||
<string>One key entry per line (or comma/semicolon separated).
|
||||
Formats: default, none, simple1..simple10, hex:<hex>, b64:<base64>, base64:<base64>, raw hex, raw base64.
|
||||
Optional channel mapping: channelName=keySpec (example: LongFast=default).</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="keyEditor">
|
||||
<property name="toolTip">
|
||||
<string>Enter one or more key specs used to decrypt Meshtastic packets.</string>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>LongFast=default
|
||||
none
|
||||
LongSlow=hex:00112233445566778899aabbccddeeff</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="statusLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="validate">
|
||||
<property name="toolTip">
|
||||
<string>Validate key syntax and count without saving.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Validate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="statusLabel">
|
||||
<property name="toolTip">
|
||||
<string>Validation status for the current key list.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>MeshtasticKeysDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>MeshtasticKeysDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
Loading…
x
Reference in New Issue
Block a user