mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-12-23 01:55:48 -05:00
1176 lines
44 KiB
C++
1176 lines
44 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
|
|
// //
|
|
// This program is free software; you can redistribute it and/or modify //
|
|
// it under the terms of the GNU General Public License as published by //
|
|
// the Free Software Foundation as version 3 of the License, or //
|
|
// (at your option) any later version. //
|
|
// //
|
|
// This program is distributed in the hope that it will be useful, //
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
|
// GNU General Public License V3 for more details. //
|
|
// //
|
|
// You should have received a copy of the GNU General Public License //
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "feature/featureuiset.h"
|
|
#include "gui/basicfeaturesettingsdialog.h"
|
|
#include "gui/flowlayout.h"
|
|
#include "gui/scidoublespinbox.h"
|
|
#include "gui/dialogpositioner.h"
|
|
|
|
#include "ui_remotecontrolgui.h"
|
|
#include "remotecontrol.h"
|
|
#include "remotecontrolgui.h"
|
|
#include "remotecontrolsettingsdialog.h"
|
|
|
|
RemoteControlGUI* RemoteControlGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature)
|
|
{
|
|
RemoteControlGUI* gui = new RemoteControlGUI(pluginAPI, featureUISet, feature);
|
|
return gui;
|
|
}
|
|
|
|
void RemoteControlGUI::destroy()
|
|
{
|
|
delete this;
|
|
}
|
|
|
|
void RemoteControlGUI::resetToDefaults()
|
|
{
|
|
m_settings.resetToDefaults();
|
|
displaySettings();
|
|
applySettings(true);
|
|
}
|
|
|
|
QByteArray RemoteControlGUI::serialize() const
|
|
{
|
|
return m_settings.serialize();
|
|
}
|
|
|
|
bool RemoteControlGUI::deserialize(const QByteArray& data)
|
|
{
|
|
if (m_settings.deserialize(data))
|
|
{
|
|
m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex);
|
|
displaySettings();
|
|
applySettings(true);
|
|
on_update_clicked();
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
resetToDefaults();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool RemoteControlGUI::handleMessage(const Message& message)
|
|
{
|
|
if (RemoteControl::MsgConfigureRemoteControl::match(message))
|
|
{
|
|
qDebug("RemoteControlGUI::handleMessage: RemoteControl::MsgConfigureRemoteControl");
|
|
const RemoteControl::MsgConfigureRemoteControl& cfg = (RemoteControl::MsgConfigureRemoteControl&) message;
|
|
m_settings = cfg.getSettings();
|
|
blockApplySettings(true);
|
|
displaySettings();
|
|
blockApplySettings(false);
|
|
|
|
return true;
|
|
}
|
|
else if (RemoteControl::MsgDeviceStatus::match(message))
|
|
{
|
|
const RemoteControl::MsgDeviceStatus& msg = (RemoteControl::MsgDeviceStatus&) message;
|
|
deviceUpdated(msg.getProtocol(), msg.getDeviceId(), msg.getStatus());
|
|
return true;
|
|
}
|
|
else if (RemoteControl::MsgDeviceError::match(message))
|
|
{
|
|
const RemoteControl::MsgDeviceError& msg = (RemoteControl::MsgDeviceError&) message;
|
|
QMessageBox::critical(this, "Remote Control Error", msg.getErrorMessage());
|
|
return true;
|
|
}
|
|
else if (RemoteControl::MsgDeviceUnavailable::match(message))
|
|
{
|
|
const RemoteControl::MsgDeviceUnavailable& msg = (RemoteControl::MsgDeviceUnavailable&) message;
|
|
deviceUnavailable(msg.getProtocol(), msg.getDeviceId());
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RemoteControlGUI::handleInputMessages()
|
|
{
|
|
Message* message;
|
|
|
|
while ((message = getInputMessageQueue()->pop()))
|
|
{
|
|
if (handleMessage(*message)) {
|
|
delete message;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::onWidgetRolled(QWidget* widget, bool rollDown)
|
|
{
|
|
(void) widget;
|
|
(void) rollDown;
|
|
|
|
getRollupContents()->saveState(m_rollupState);
|
|
applySettings();
|
|
}
|
|
|
|
RemoteControlGUI::RemoteControlGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) :
|
|
FeatureGUI(parent),
|
|
ui(new Ui::RemoteControlGUI),
|
|
m_pluginAPI(pluginAPI),
|
|
m_featureUISet(featureUISet),
|
|
m_doApplySettings(true)
|
|
{
|
|
m_feature = feature;
|
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
m_helpURL = "plugins/feature/remotecontrol/readme.md";
|
|
RollupContents *rollupContents = getRollupContents();
|
|
ui->setupUi(rollupContents);
|
|
rollupContents->arrangeRollups();
|
|
connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
|
|
|
|
ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"
|
|
"QToolButton:checked { background-color : green; }"
|
|
"QToolButton:disabled { background-color : gray; }");
|
|
|
|
m_startStopIcon.addFile(":/play.png", QSize(16, 16), QIcon::Normal, QIcon::Off);
|
|
m_startStopIcon.addFile(":/stop.png", QSize(16, 16), QIcon::Normal, QIcon::On);
|
|
|
|
m_remoteControl = reinterpret_cast<RemoteControl*>(feature);
|
|
m_remoteControl->setMessageQueueToGUI(&m_inputMessageQueue);
|
|
|
|
m_settings.setRollupState(&m_rollupState);
|
|
|
|
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
|
|
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
|
|
|
|
displaySettings();
|
|
applySettings(true);
|
|
makeUIConnections();
|
|
m_resizer.enableChildMouseTracking();
|
|
}
|
|
|
|
RemoteControlGUI::~RemoteControlGUI()
|
|
{
|
|
qDeleteAll(m_deviceGUIs);
|
|
m_deviceGUIs.clear();
|
|
delete ui;
|
|
}
|
|
|
|
void RemoteControlGUI::setWorkspaceIndex(int index)
|
|
{
|
|
m_settings.m_workspaceIndex = index;
|
|
m_feature->setWorkspaceIndex(index);
|
|
}
|
|
|
|
void RemoteControlGUI::blockApplySettings(bool block)
|
|
{
|
|
m_doApplySettings = !block;
|
|
}
|
|
|
|
void RemoteControlGUI::displaySettings()
|
|
{
|
|
setTitleColor(m_settings.m_rgbColor);
|
|
setWindowTitle(m_settings.m_title);
|
|
setTitle(m_settings.m_title);
|
|
createGUI();
|
|
blockApplySettings(true);
|
|
getRollupContents()->restoreState(m_rollupState);
|
|
blockApplySettings(false);
|
|
getRollupContents()->arrangeRollups();
|
|
}
|
|
|
|
void RemoteControlGUI::onMenuDialogCalled(const QPoint &p)
|
|
{
|
|
if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings)
|
|
{
|
|
BasicFeatureSettingsDialog dialog(this);
|
|
dialog.setTitle(m_settings.m_title);
|
|
dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
|
|
dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
|
|
dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
|
|
dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex);
|
|
dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex);
|
|
dialog.setDefaultTitle(m_displayedName);
|
|
|
|
dialog.move(p);
|
|
new DialogPositioner(&dialog, false);
|
|
dialog.exec();
|
|
|
|
m_settings.m_title = dialog.getTitle();
|
|
m_settings.m_useReverseAPI = dialog.useReverseAPI();
|
|
m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
|
|
m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
|
|
m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex();
|
|
m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex();
|
|
|
|
setTitle(m_settings.m_title);
|
|
setTitleColor(m_settings.m_rgbColor);
|
|
|
|
applySettings();
|
|
}
|
|
|
|
resetContextMenuType();
|
|
}
|
|
|
|
void RemoteControlGUI::createControls(RemoteControlDeviceGUI *gui, QBoxLayout *vBox, FlowLayout *flow, int &widgetCnt)
|
|
{
|
|
// Create buttons to control the device
|
|
QGridLayout *controlsGrid = nullptr;
|
|
|
|
if (gui->m_rcDevice->m_verticalControls)
|
|
{
|
|
controlsGrid = new QGridLayout();
|
|
vBox->addLayout(controlsGrid);
|
|
}
|
|
else if (!flow)
|
|
{
|
|
flow = new FlowLayout(2, 6, 6);
|
|
vBox->addItem(flow);
|
|
}
|
|
|
|
int row = 0;
|
|
for (auto const &control : gui->m_rcDevice->m_controls)
|
|
{
|
|
if (!gui->m_rcDevice->m_verticalControls && (widgetCnt > 0))
|
|
{
|
|
QFrame *line = new QFrame();
|
|
line->setFrameShape(QFrame::VLine);
|
|
line->setFrameShadow(QFrame::Sunken);
|
|
flow->addWidget(line);
|
|
}
|
|
|
|
DeviceDiscoverer::ControlInfo *info = gui->m_rcDevice->m_info.getControl(control.m_id);
|
|
if (!info)
|
|
{
|
|
qDebug() << "RemoteControlGUI::createControls: Info missing for " << control.m_id;
|
|
continue;
|
|
}
|
|
|
|
if (!control.m_labelLeft.isEmpty())
|
|
{
|
|
QLabel *controlLabelLeft = new QLabel(control.m_labelLeft);
|
|
if (gui->m_rcDevice->m_verticalControls)
|
|
{
|
|
controlsGrid->addWidget(controlLabelLeft, row, 0);
|
|
controlsGrid->setColumnStretch(row, 0);
|
|
}
|
|
else
|
|
{
|
|
flow->addWidget(controlLabelLeft);
|
|
}
|
|
}
|
|
|
|
QList<QWidget *> widgets;
|
|
QWidget *widget = nullptr;
|
|
|
|
switch (info->m_type)
|
|
{
|
|
case DeviceDiscoverer::BOOL:
|
|
{
|
|
ButtonSwitch *button = new ButtonSwitch();
|
|
button->setToolTip("Start/stop " + info->m_name);
|
|
button->setIcon(m_startStopIcon);
|
|
button->setStyleSheet("QToolButton { background-color : blue; }"
|
|
"QToolButton:checked { background-color : green; }"
|
|
"QToolButton:disabled { background-color : gray; }");
|
|
connect(button, &ButtonSwitch::toggled,
|
|
[=] (bool toggled)
|
|
{
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
toggled);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(button);
|
|
widget = button;
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::INT:
|
|
{
|
|
QSpinBox *spinBox = new QSpinBox();
|
|
|
|
spinBox->setToolTip("Set value for " + info->m_name);
|
|
spinBox->setMinimum((int)info->m_min);
|
|
spinBox->setMaximum((int)info->m_max);
|
|
connect(spinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
|
|
[=] (int value)
|
|
{
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
value);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(spinBox);
|
|
widget = spinBox;
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::FLOAT:
|
|
{
|
|
switch (info->m_widgetType)
|
|
{
|
|
case DeviceDiscoverer::SPIN_BOX:
|
|
{
|
|
QDoubleSpinBox *spinBox = new SciDoubleSpinBox();
|
|
|
|
spinBox->setToolTip("Set value for " + info->m_name);
|
|
spinBox->setMinimum(info->m_min);
|
|
spinBox->setMaximum(info->m_max);
|
|
spinBox->setDecimals(info->m_precision);
|
|
connect(spinBox, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
|
|
[=] (double value)
|
|
{
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
(float)value * info->m_scale);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(spinBox);
|
|
widget = spinBox;
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::DIAL:
|
|
{
|
|
widget = new QWidget();
|
|
QHBoxLayout *layout = new QHBoxLayout();
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
widget->setLayout(layout);
|
|
|
|
QDial *dial = new QDial();
|
|
dial->setMaximumSize(24, 24);
|
|
dial->setToolTip("Set value for " + info->m_name);
|
|
dial->setMinimum(info->m_min);
|
|
dial->setMaximum(info->m_max);
|
|
|
|
connect(dial, static_cast<void (QDial::*)(int)>(&QDial::valueChanged),
|
|
[=] (int value)
|
|
{
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
((float)value) * info->m_scale);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(dial);
|
|
layout->addWidget(dial);
|
|
|
|
QLabel *label = new QLabel(QString::number(dial->value()));
|
|
label->setToolTip("Value for " + info->m_name);
|
|
widgets.append(label);
|
|
layout->addWidget(label);
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::SLIDER:
|
|
{
|
|
widget = new QWidget();
|
|
QHBoxLayout *layout = new QHBoxLayout();
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
widget->setLayout(layout);
|
|
|
|
QSlider *slider = new QSlider(Qt::Horizontal);
|
|
slider->setToolTip("Set value for " + info->m_name);
|
|
slider->setMinimum(info->m_min);
|
|
slider->setMaximum(info->m_max);
|
|
|
|
connect(slider, static_cast<void (QSlider::*)(int)>(&QSlider::valueChanged),
|
|
[=] (int value)
|
|
{
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
((float)value) * info->m_scale);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(slider);
|
|
layout->addWidget(slider);
|
|
|
|
QLabel *label = new QLabel(QString::number(slider->value()));
|
|
label->setToolTip("Value for " + info->m_name);
|
|
widgets.append(label);
|
|
layout->addWidget(label);
|
|
}
|
|
break;
|
|
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::STRING:
|
|
{
|
|
QLineEdit *lineEdit = new QLineEdit();
|
|
|
|
lineEdit->setToolTip("Set value for " + info->m_name);
|
|
|
|
connect(lineEdit, &QLineEdit::editingFinished,
|
|
[=] ()
|
|
{
|
|
QString text = lineEdit->text();
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
text);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(lineEdit);
|
|
widget = lineEdit;
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::LIST:
|
|
{
|
|
QComboBox *combo = new QComboBox();
|
|
|
|
combo->setToolTip("Set value for " + info->m_name);
|
|
combo->insertItems(0, info->m_values);
|
|
connect(combo, &QComboBox::currentTextChanged,
|
|
[=] (const QString &text)
|
|
{
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
text);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(combo);
|
|
widget = combo;
|
|
}
|
|
break;
|
|
|
|
case DeviceDiscoverer::BUTTON:
|
|
{
|
|
QString label = info->m_name;
|
|
if (info->m_values.size() > 0) {
|
|
label = info->m_values[0];
|
|
}
|
|
QToolButton *button = new QToolButton();
|
|
button->setText(label);
|
|
button->setToolTip("Trigger " + info->m_name);
|
|
|
|
connect(button, &QToolButton::clicked,
|
|
[=] (bool checked)
|
|
{
|
|
(void) checked;
|
|
RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
|
|
gui->m_rcDevice->m_info.m_id,
|
|
control.m_id,
|
|
1);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
);
|
|
widgets.append(button);
|
|
widget = button;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
qDebug() << "RemoteControlGUI::createControls: Unexpected type for control.";
|
|
break;
|
|
|
|
}
|
|
gui->m_controls.insert(control.m_id, widgets);
|
|
if (gui->m_rcDevice->m_verticalControls) {
|
|
controlsGrid->addWidget(widget, row, 1);
|
|
} else {
|
|
flow->addWidget(widget);
|
|
}
|
|
|
|
if (!control.m_labelRight.isEmpty())
|
|
{
|
|
QLabel *controlLabelRight = new QLabel(control.m_labelRight);
|
|
if (gui->m_rcDevice->m_verticalControls)
|
|
{
|
|
controlsGrid->addWidget(controlLabelRight, row, 2);
|
|
controlsGrid->setColumnStretch(row, 2);
|
|
}
|
|
else
|
|
{
|
|
flow->addWidget(controlLabelRight);
|
|
}
|
|
}
|
|
|
|
widgetCnt++;
|
|
row++;
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::createChart(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, const QString &id, const QString &units)
|
|
{
|
|
if (gui->m_chart == nullptr)
|
|
{
|
|
// Create a chart to plot the sensor data
|
|
gui->m_chart = new QChart();
|
|
gui->m_chart->setTitle("");
|
|
gui->m_chart->legend()->hide();
|
|
gui->m_chart->layout()->setContentsMargins(0, 0, 0, 0);
|
|
gui->m_chart->setMargins(QMargins(1, 1, 1, 1));
|
|
gui->m_chart->setTheme(QChart::ChartThemeDark);
|
|
QLineSeries *series = new QLineSeries();
|
|
gui->m_series.insert(id, series);
|
|
QLineSeries *onePointSeries = new QLineSeries();
|
|
gui->m_onePointSeries.insert(id, onePointSeries);
|
|
gui->m_chart->addSeries(series);
|
|
QValueAxis *yAxis = new QValueAxis();
|
|
QDateTimeAxis *xAxis = new QDateTimeAxis();
|
|
xAxis->setFormat(QString("hh:mm:ss"));
|
|
yAxis->setTitleText(units);
|
|
gui->m_chart->addAxis(xAxis, Qt::AlignBottom);
|
|
gui->m_chart->addAxis(yAxis, Qt::AlignLeft);
|
|
series->attachAxis(xAxis);
|
|
series->attachAxis(yAxis);
|
|
gui->m_chartView = new QChartView();
|
|
gui->m_chartView->setChart(gui->m_chart);
|
|
if (m_settings.m_chartHeightFixed)
|
|
{
|
|
gui->m_chartView->setMinimumSize(300, m_settings.m_chartHeightPixels);
|
|
gui->m_chartView->setMaximumSize(16777215, m_settings.m_chartHeightPixels);
|
|
gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
}
|
|
else
|
|
{
|
|
gui->m_chartView->setMinimumSize(300, 130); // 130 is enough to display axis labels
|
|
gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
gui->m_chartView->setSceneRect(0, 0, 300, 130); // This determines m_chartView->sizeHint() - default is 640x480, which is a bit big
|
|
}
|
|
QBoxLayout *chartLayout = new QVBoxLayout();
|
|
gui->m_chartView->setLayout(chartLayout);
|
|
|
|
vBox->addWidget(gui->m_chartView);
|
|
}
|
|
else
|
|
{
|
|
// Add new series
|
|
QLineSeries *series = new QLineSeries();
|
|
gui->m_series.insert(id, series);
|
|
QLineSeries *onePointSeries = new QLineSeries();
|
|
gui->m_onePointSeries.insert(id, onePointSeries);
|
|
gui->m_chart->addSeries(series);
|
|
if (!gui->m_rcDevice->m_commonYAxis)
|
|
{
|
|
// Use per series Y axis
|
|
QValueAxis *yAxis = new QValueAxis();
|
|
yAxis->setTitleText(units);
|
|
gui->m_chart->addAxis(yAxis, Qt::AlignRight);
|
|
series->attachAxis(yAxis);
|
|
}
|
|
else
|
|
{
|
|
// Use common y axis
|
|
QAbstractAxis *yAxis = gui->m_chart->axes(Qt::Vertical)[0];
|
|
// Only display units if all the same
|
|
if (yAxis->titleText() != units) {
|
|
yAxis->setTitleText("");
|
|
}
|
|
series->attachAxis(yAxis);
|
|
}
|
|
series->attachAxis(gui->m_chart->axes(Qt::Horizontal)[0]);
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::createSensors(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, FlowLayout *flow, int &widgetCnt, bool &hasCharts)
|
|
{
|
|
// Table doesn't seem to expand in a QHBoxLayout, so we have to use a GridLayout
|
|
QGridLayout *grid = nullptr;
|
|
QTableWidget *table = nullptr;
|
|
if (gui->m_rcDevice->m_verticalSensors)
|
|
{
|
|
grid = new QGridLayout();
|
|
grid->setColumnStretch(0, 1);
|
|
vBox->addLayout(grid);
|
|
table = new QTableWidget(gui->m_rcDevice->m_sensors.size(), 3);
|
|
table->verticalHeader()->setVisible(false);
|
|
table->horizontalHeader()->setVisible(false);
|
|
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
|
table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
|
table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
|
table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
table->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); // Needed so table->sizeHint matches minimumSize set below
|
|
}
|
|
else if (!flow)
|
|
{
|
|
flow = new FlowLayout(2, 6, 6);
|
|
vBox->addItem(flow);
|
|
}
|
|
|
|
int row = 0;
|
|
bool hasUnits = false;
|
|
for (auto const &sensor : gui->m_rcDevice->m_sensors)
|
|
{
|
|
// For vertical layout, we use a table
|
|
// For horizontal, we use HBox of labels separated with bars
|
|
if (gui->m_rcDevice->m_verticalSensors)
|
|
{
|
|
if (!sensor.m_labelLeft.isEmpty())
|
|
{
|
|
QTableWidgetItem *sensorLabel = new QTableWidgetItem(sensor.m_labelLeft);
|
|
sensorLabel->setFlags(Qt::ItemIsEnabled);
|
|
table->setItem(row, COL_LABEL, sensorLabel);
|
|
}
|
|
QTableWidgetItem *valueItem = new QTableWidgetItem("-");
|
|
table->setItem(row, COL_VALUE, valueItem);
|
|
valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
valueItem->setFlags(Qt::ItemIsEnabled);
|
|
if (!sensor.m_labelRight.isEmpty())
|
|
{
|
|
QTableWidgetItem *unitsItem = new QTableWidgetItem(sensor.m_labelRight);
|
|
unitsItem->setFlags(Qt::ItemIsEnabled);
|
|
table->setItem(row, COL_UNITS, unitsItem);
|
|
hasUnits = true;
|
|
}
|
|
gui->m_sensorValueItems.insert(sensor.m_id, valueItem);
|
|
grid->addWidget(table, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
if (widgetCnt > 0)
|
|
{
|
|
QFrame *line = new QFrame();
|
|
line->setFrameShape(QFrame::VLine);
|
|
line->setFrameShadow(QFrame::Sunken);
|
|
flow->addWidget(line);
|
|
}
|
|
if (!sensor.m_labelLeft.isEmpty())
|
|
{
|
|
QLabel *sensorLabel = new QLabel(sensor.m_labelLeft);
|
|
flow->addWidget(sensorLabel);
|
|
}
|
|
QLabel *sensorValue = new QLabel("-");
|
|
flow->addWidget(sensorValue);
|
|
if (!sensor.m_labelRight.isEmpty())
|
|
{
|
|
QLabel *sensorUnits = new QLabel(sensor.m_labelRight);
|
|
flow->addWidget(sensorUnits);
|
|
}
|
|
gui->m_sensorValueLabels.insert(sensor.m_id, sensorValue);
|
|
}
|
|
|
|
if (sensor.m_plot)
|
|
{
|
|
createChart(gui, vBox, sensor.m_id, sensor.m_labelRight);
|
|
hasCharts = true;
|
|
}
|
|
|
|
widgetCnt++;
|
|
row++;
|
|
}
|
|
|
|
if (table)
|
|
{
|
|
table->resizeColumnToContents(COL_LABEL);
|
|
if (hasUnits) {
|
|
table->resizeColumnToContents(COL_UNITS);
|
|
} else {
|
|
table->hideColumn(COL_UNITS);
|
|
}
|
|
|
|
int tableWidth = 0;
|
|
for (int i = 0; i < table->columnCount(); i++){
|
|
tableWidth += table->columnWidth(i);
|
|
}
|
|
int tableHeight = 0;
|
|
for (int i = 0; i < table->rowCount(); i++){
|
|
tableHeight += table->rowHeight(i);
|
|
}
|
|
table->setMinimumWidth(tableWidth);
|
|
table->setMinimumHeight(tableHeight+2);
|
|
}
|
|
}
|
|
|
|
RemoteControlGUI::RemoteControlDeviceGUI *RemoteControlGUI::createDeviceGUI(RemoteControlDevice *rcDevice)
|
|
{
|
|
// Create the UI for the device
|
|
RemoteControlDeviceGUI *gui = new RemoteControlDeviceGUI(rcDevice);
|
|
|
|
bool hasCharts = false;
|
|
|
|
gui->m_container = new QWidget(getRollupContents());
|
|
gui->m_container->setWindowTitle(gui->m_rcDevice->m_label);
|
|
bool vertical = gui->m_rcDevice->m_verticalControls || gui->m_rcDevice->m_verticalSensors;
|
|
QVBoxLayout *vBox = new QVBoxLayout();
|
|
vBox->setContentsMargins(2, 2, 2, 2);
|
|
FlowLayout *flow = nullptr;
|
|
|
|
if (!vertical)
|
|
{
|
|
flow = new FlowLayout(2, 6, 6);
|
|
vBox->addItem(flow);
|
|
}
|
|
int widgetCnt = 0;
|
|
|
|
// Create buttons to control the device
|
|
createControls(gui, vBox, flow, widgetCnt);
|
|
|
|
if (gui->m_rcDevice->m_verticalControls) {
|
|
widgetCnt = 0;
|
|
}
|
|
|
|
// Create widgets to display the sensor label and its value
|
|
createSensors(gui, vBox, flow, widgetCnt, hasCharts);
|
|
|
|
gui->m_container->setLayout(vBox);
|
|
|
|
if (hasCharts && !m_settings.m_chartHeightFixed) {
|
|
gui->m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
}
|
|
|
|
gui->m_container->show();
|
|
return gui;
|
|
}
|
|
|
|
void RemoteControlGUI::createGUI()
|
|
{
|
|
// Delete existing elements
|
|
for (auto gui : m_deviceGUIs)
|
|
{
|
|
delete gui->m_container;
|
|
gui->m_container = nullptr;
|
|
}
|
|
qDeleteAll(m_deviceGUIs);
|
|
m_deviceGUIs.clear();
|
|
|
|
// Create new GUIs for each device
|
|
bool expanding = false;
|
|
for (auto device : m_settings.m_devices)
|
|
{
|
|
RemoteControlDeviceGUI *gui = createDeviceGUI(device);
|
|
m_deviceGUIs.append(gui);
|
|
if (gui->m_container->sizePolicy().verticalPolicy() == QSizePolicy::Expanding) {
|
|
expanding = true;
|
|
}
|
|
}
|
|
if (expanding)
|
|
{
|
|
getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
}
|
|
else
|
|
{
|
|
getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
}
|
|
|
|
// FIXME: Why are these three steps needed to get the window
|
|
// to resize to the newly added widgets?
|
|
getRollupContents()->arrangeRollups(); // Recalc rollup size
|
|
layout()->activate(); // Get QMdiSubWindow to recalc its sizeHint
|
|
resize(sizeHint());
|
|
|
|
// Need to do it twice when FlowLayout is used!
|
|
getRollupContents()->arrangeRollups();
|
|
layout()->activate();
|
|
resize(sizeHint());
|
|
}
|
|
|
|
void RemoteControlGUI::on_startStop_toggled(bool checked)
|
|
{
|
|
if (m_doApplySettings)
|
|
{
|
|
RemoteControl::MsgStartStop *message = RemoteControl::MsgStartStop::create(checked);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::on_update_clicked()
|
|
{
|
|
RemoteControl::MsgDeviceGetState *message = RemoteControl::MsgDeviceGetState::create();
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
|
|
void RemoteControlGUI::on_settings_clicked()
|
|
{
|
|
// Display settings dialog
|
|
RemoteControlSettingsDialog dialog(&m_settings);
|
|
if (dialog.exec() == QDialog::Accepted)
|
|
{
|
|
createGUI();
|
|
applySettings();
|
|
on_update_clicked();
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::on_clearData_clicked()
|
|
{
|
|
// Clear data in all charts
|
|
for (auto deviceGUI : m_deviceGUIs)
|
|
{
|
|
for (auto series : deviceGUI->m_series) {
|
|
series->clear();
|
|
}
|
|
for (auto series : deviceGUI->m_onePointSeries) {
|
|
series->clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update a control widget with latest state value
|
|
void RemoteControlGUI::updateControl(QWidget *widget, const DeviceDiscoverer::ControlInfo *controlInfo, const QString &key, const QVariant &value)
|
|
{
|
|
if (ButtonSwitch *button = qobject_cast<ButtonSwitch *>(widget))
|
|
{
|
|
if ((QMetaType::Type)value.type() == QMetaType::QString)
|
|
{
|
|
if (value.toString() == "unavailable")
|
|
{
|
|
button->setStyleSheet("QToolButton { background-color : gray; }"
|
|
"QToolButton:checked { background-color : gray; }"
|
|
"QToolButton:disabled { background-color : gray; }");
|
|
}
|
|
else if (value.toString() == "error")
|
|
{
|
|
button->setStyleSheet("QToolButton { background-color : red; }"
|
|
"QToolButton:checked { background-color : red; }"
|
|
"QToolButton:disabled { background-color : red; }");
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "RemoteControlGUI::updateControl: String value for button " << key << value;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int state = value.toInt();
|
|
int prev = button->blockSignals(true);
|
|
button->setChecked(state != 0);
|
|
button->blockSignals(prev);
|
|
button->setStyleSheet("QToolButton { background-color : blue; }"
|
|
"QToolButton:checked { background-color : green; }"
|
|
"QToolButton:disabled { background-color : gray; }");
|
|
}
|
|
}
|
|
else if (QSpinBox *spinBox = qobject_cast<QSpinBox *>(widget))
|
|
{
|
|
int prev = spinBox->blockSignals(true);
|
|
if (value.toString() == "unavailable")
|
|
{
|
|
spinBox->setStyleSheet("QSpinBox { background-color : gray; }");
|
|
}
|
|
else if (value.toString() == "error")
|
|
{
|
|
spinBox->setStyleSheet("QSpinBox { background-color : red; }");
|
|
}
|
|
else
|
|
{
|
|
int state = value.toInt();
|
|
bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum());
|
|
spinBox->setValue(state);
|
|
if (outOfRange) {
|
|
spinBox->setStyleSheet("QSpinBox { background-color : red; }");
|
|
} else {
|
|
spinBox->setStyleSheet("");
|
|
}
|
|
}
|
|
spinBox->blockSignals(prev);
|
|
}
|
|
else if (QDoubleSpinBox *spinBox = qobject_cast<QDoubleSpinBox *>(widget))
|
|
{
|
|
int prev = spinBox->blockSignals(true);
|
|
if (value.toString() == "unavailable")
|
|
{
|
|
spinBox->setStyleSheet("QDoubleSpinBox { background-color : gray; }");
|
|
}
|
|
else if (value.toString() == "error")
|
|
{
|
|
spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }");
|
|
}
|
|
else
|
|
{
|
|
double state = value.toDouble();
|
|
if (controlInfo) {
|
|
state = state / controlInfo->m_scale;
|
|
}
|
|
bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum());
|
|
spinBox->setValue(state);
|
|
if (outOfRange) {
|
|
spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }");
|
|
} else {
|
|
spinBox->setStyleSheet("");
|
|
}
|
|
}
|
|
spinBox->blockSignals(prev);
|
|
}
|
|
else if (QDial *dial = qobject_cast<QDial *>(widget))
|
|
{
|
|
int prev = dial->blockSignals(true);
|
|
if (value.toString() == "unavailable")
|
|
{
|
|
dial->setStyleSheet("QDial { background-color : gray; }");
|
|
}
|
|
else if (value.toString() == "error")
|
|
{
|
|
dial->setStyleSheet("QDial { background-color : red; }");
|
|
}
|
|
else
|
|
{
|
|
double state = value.toDouble();
|
|
if (controlInfo) {
|
|
state = state / controlInfo->m_scale;
|
|
}
|
|
bool outOfRange = (state < dial->minimum()) || (state > dial->maximum());
|
|
dial->setValue(state);
|
|
if (outOfRange) {
|
|
dial->setStyleSheet("QDial { background-color : red; }");
|
|
} else {
|
|
dial->setStyleSheet("");
|
|
}
|
|
}
|
|
dial->blockSignals(prev);
|
|
}
|
|
else if (QSlider *slider = qobject_cast<QSlider *>(widget))
|
|
{
|
|
int prev = slider->blockSignals(true);
|
|
if (value.toString() == "unavailable")
|
|
{
|
|
slider->setStyleSheet("QSlider { background-color : gray; }");
|
|
}
|
|
else if (value.toString() == "error")
|
|
{
|
|
slider->setStyleSheet("QSlider { background-color : red; }");
|
|
}
|
|
else
|
|
{
|
|
double state = value.toDouble();
|
|
if (controlInfo) {
|
|
state = state / controlInfo->m_scale;
|
|
}
|
|
bool outOfRange = (state < slider->minimum()) || (state > slider->maximum());
|
|
slider->setValue(state);
|
|
if (outOfRange) {
|
|
slider->setStyleSheet("QSlider { background-color : red; }");
|
|
} else {
|
|
slider->setStyleSheet("");
|
|
}
|
|
}
|
|
slider->blockSignals(prev);
|
|
}
|
|
else if (QComboBox *comboBox = qobject_cast<QComboBox *>(widget))
|
|
{
|
|
int prev = comboBox->blockSignals(true);
|
|
QString string = value.toString();
|
|
int index = comboBox->findText(string);
|
|
if (index != -1)
|
|
{
|
|
comboBox->setCurrentIndex(index);
|
|
comboBox->setStyleSheet("");
|
|
}
|
|
else
|
|
{
|
|
comboBox->setStyleSheet("QComboBox { background-color : red; }");
|
|
}
|
|
comboBox->blockSignals(prev);
|
|
}
|
|
else if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(widget))
|
|
{
|
|
lineEdit->setText(value.toString());
|
|
}
|
|
else if (QLabel *label = qobject_cast<QLabel *>(widget))
|
|
{
|
|
label->setText(value.toString());
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "RemoteControlGUI::updateControl: Unexpected widget type";
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::updateChart(RemoteControlDeviceGUI *deviceGUI, const QString &key, const QVariant &value)
|
|
{
|
|
// Format the value for display
|
|
bool ok = false;
|
|
double d = value.toDouble(&ok);
|
|
bool iOk = false;
|
|
int iValue = value.toInt(&iOk);
|
|
QString formattedValue;
|
|
RemoteControlSensor *sensor = deviceGUI->m_rcDevice->getSensor(key);
|
|
QString format = sensor->m_format.trimmed();
|
|
if (format.contains("%s"))
|
|
{
|
|
formattedValue = QString::asprintf(format.toUtf8(), value.toString().toUtf8().data());
|
|
}
|
|
else if (format.contains("%d") || format.contains("%u") || format.contains("%x") || format.contains("%X"))
|
|
{
|
|
formattedValue = QString::asprintf(format.toUtf8(), value.toInt());
|
|
}
|
|
else if (((QMetaType::Type)value.type() == QMetaType::Double) || ((QMetaType::Type)value.type() == QMetaType::Float))
|
|
{
|
|
if (format.isEmpty()) {
|
|
format = "%.1f";
|
|
}
|
|
formattedValue = QString::asprintf(format.toUtf8(), value.toDouble());
|
|
}
|
|
else if (iOk)
|
|
{
|
|
formattedValue = QString::asprintf("%d", iValue);
|
|
}
|
|
else
|
|
{
|
|
formattedValue = value.toString();
|
|
}
|
|
|
|
// Update sensor value widget to display the latest value
|
|
if (deviceGUI->m_sensorValueLabels.contains(key)) {
|
|
deviceGUI->m_sensorValueLabels.value(key)->setText(formattedValue);
|
|
} else {
|
|
deviceGUI->m_sensorValueItems.value(key)->setText(formattedValue);
|
|
}
|
|
|
|
// Plot value on chart
|
|
if (deviceGUI->m_series.contains(key))
|
|
{
|
|
QLineSeries *onePointSeries = deviceGUI->m_onePointSeries.value(key);
|
|
QLineSeries *series = deviceGUI->m_series.value(key);
|
|
QDateTime dt = QDateTime::currentDateTime();
|
|
if (ok)
|
|
{
|
|
// Charts aren't displayed properly if series has only one point,
|
|
// so we save the first point in an additional series: onePointSeries
|
|
if (onePointSeries->count() == 0)
|
|
{
|
|
onePointSeries->append(dt.toMSecsSinceEpoch(), d);
|
|
}
|
|
else
|
|
{
|
|
if (series->count() == 0) {
|
|
series->append(onePointSeries->at(0));
|
|
}
|
|
series->append(dt.toMSecsSinceEpoch(), d);
|
|
QList<QAbstractAxis *> axes = deviceGUI->m_chart->axes(Qt::Horizontal, series);
|
|
QDateTimeAxis *dtAxis = (QDateTimeAxis *)axes[0];
|
|
QDateTime start = QDateTime::fromMSecsSinceEpoch(series->at(0).x());
|
|
QDateTime end = QDateTime::fromMSecsSinceEpoch(series->at(series->count() - 1).x());
|
|
if (start.date() == end.date())
|
|
{
|
|
if (start.secsTo(end) < 60*5) {
|
|
dtAxis->setFormat(QString("hh:mm:ss"));
|
|
} else {
|
|
dtAxis->setFormat(QString("hh:mm"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dtAxis->setFormat(QString("%1 hh:mm").arg(QLocale::system().dateFormat(QLocale::ShortFormat)));
|
|
}
|
|
dtAxis->setRange(start, end);
|
|
axes = deviceGUI->m_chart->axes(Qt::Vertical, series);
|
|
QValueAxis *yAxis = (QValueAxis *)axes[0];
|
|
if (series->count() == 2)
|
|
{
|
|
double y1 = series->at(0).y();
|
|
double y2 = series->at(1).y();
|
|
double yMin = std::min(y1, y2);
|
|
double yMax = std::max(y1, y2);
|
|
double min = (yMin >= 0.0) ? yMin * 0.9 : yMin * 1.1;
|
|
double max = (yMax >= 0.0) ? yMax * 1.1 : yMax * 0.9;
|
|
yAxis->setRange(min, max);
|
|
}
|
|
else
|
|
{
|
|
double min = (d >= 0.0) ? d * 0.9 : d * 1.1;
|
|
double max = (d >= 0.0) ? d * 1.1 : d * 0.9;
|
|
if (min < yAxis->min()) {
|
|
yAxis->setMin(min);
|
|
}
|
|
if (max > yAxis->max()) {
|
|
yAxis->setMax(max);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "RemoteControlGUI::deviceUpdated: Error converting " << key << value;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::deviceUpdated(const QString &protocol, const QString &deviceId, const QHash<QString, QVariant> &status)
|
|
{
|
|
for (auto deviceGUI : m_deviceGUIs)
|
|
{
|
|
if ( (protocol == deviceGUI->m_rcDevice->m_protocol)
|
|
&& (deviceId == deviceGUI->m_rcDevice->m_info.m_id))
|
|
{
|
|
deviceGUI->m_container->setEnabled(true);
|
|
|
|
QHashIterator<QString, QVariant> itr(status);
|
|
|
|
while (itr.hasNext())
|
|
{
|
|
itr.next();
|
|
QString key = itr.key();
|
|
QVariant value = itr.value();
|
|
|
|
if (deviceGUI->m_controls.contains(key))
|
|
{
|
|
// Update control(s) to display latest state
|
|
QList<QWidget *> widgets = deviceGUI->m_controls.value(key);
|
|
DeviceDiscoverer::ControlInfo *control = deviceGUI->m_rcDevice->m_info.getControl(key);
|
|
|
|
for (auto widget : widgets) {
|
|
updateControl(widget, control, key, value);
|
|
}
|
|
}
|
|
else if (deviceGUI->m_sensorValueLabels.contains(key) || deviceGUI->m_sensorValueItems.contains(key))
|
|
{
|
|
// Plot on chart
|
|
updateChart(deviceGUI, key, value);
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "RemoteControlGUI::deviceUpdated: Unexpected status key " << key << value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::deviceUnavailable(const QString &protocol, const QString &deviceId)
|
|
{
|
|
for (auto deviceGUI : m_deviceGUIs)
|
|
{
|
|
if ( (protocol == deviceGUI->m_rcDevice->m_protocol)
|
|
&& (deviceId == deviceGUI->m_rcDevice->m_info.m_id))
|
|
{
|
|
deviceGUI->m_container->setEnabled(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::applySettings(bool force)
|
|
{
|
|
if (m_doApplySettings)
|
|
{
|
|
RemoteControl::MsgConfigureRemoteControl* message = RemoteControl::MsgConfigureRemoteControl::create(m_settings, force);
|
|
m_remoteControl->getInputMessageQueue()->push(message);
|
|
}
|
|
}
|
|
|
|
void RemoteControlGUI::makeUIConnections()
|
|
{
|
|
QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &RemoteControlGUI::on_startStop_toggled);
|
|
QObject::connect(ui->update, &QToolButton::clicked, this, &RemoteControlGUI::on_update_clicked);
|
|
QObject::connect(ui->settings, &QToolButton::clicked, this, &RemoteControlGUI::on_settings_clicked);
|
|
QObject::connect(ui->clearData, &QToolButton::clicked, this, &RemoteControlGUI::on_clearData_clicked);
|
|
}
|