Files
2026-06-26 09:11:10 +02:00

168 lines
5.3 KiB
C++

/**
* @file SourceSidebar.cpp
*/
#include "SourceSidebar.h"
#include "Hub.h"
#include "Theme.h"
#include <QVBoxLayout>
#include <QTreeWidget>
#include <QHeaderView>
#include <QPushButton>
#include <QMenu>
#include <QMimeData>
#include <QDrag>
#include <QPainter>
#include <QPixmap>
#include <QIcon>
#include <QSet>
#include <QDataStream>
#include <QApplication>
namespace shq {
const char* const kMimeSignal = "application/x-shq-signal";
/* Roles for tree items. */
enum { RoleSrcIdx = Qt::UserRole + 1, RoleSigIdx, RoleSrcId };
/** QTreeWidget subclass that emits a signal drag with the qint32[2] payload. */
class SourceTree : public QTreeWidget {
public:
explicit SourceTree(QWidget* parent = nullptr) : QTreeWidget(parent) {
setDragEnabled(true);
setDragDropMode(QAbstractItemView::DragOnly);
}
protected:
void startDrag(Qt::DropActions /*supported*/) override {
QTreeWidgetItem* it = currentItem();
if (!it) { return; }
bool ok = false;
int sigIdx = it->data(0, RoleSigIdx).toInt(&ok);
if (!ok || sigIdx < 0) { return; } /* only signal leaves drag */
int srcIdx = it->data(0, RoleSrcIdx).toInt();
QByteArray payload;
{
QDataStream ds(&payload, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::LittleEndian);
ds << static_cast<qint32>(srcIdx) << static_cast<qint32>(sigIdx);
}
auto* mime = new QMimeData;
mime->setData(kMimeSignal, payload);
auto* drag = new QDrag(this);
drag->setMimeData(mime);
drag->exec(Qt::CopyAction);
}
};
SourceSidebar::SourceSidebar(Hub* hub, QWidget* parent)
: QWidget(parent), hub_(hub) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(4, 4, 4, 4);
lay->setSpacing(4);
tree_ = new SourceTree(this);
tree_->setHeaderHidden(true);
tree_->setColumnCount(1);
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
tree_->setIndentation(12);
connect(tree_, &QTreeWidget::customContextMenuRequested,
this, &SourceSidebar::onContextMenu);
lay->addWidget(tree_, 1);
auto* addBtn = new QPushButton("+ Add Source", this);
connect(addBtn, &QPushButton::clicked, this,
&SourceSidebar::addSourceRequested);
lay->addWidget(addBtn);
auto* saveBtn = new QPushButton("Save Sources", this);
connect(saveBtn, &QPushButton::clicked, this,
[this]() { hub_->sendSaveSources(); });
lay->addWidget(saveBtn);
refresh();
}
/** Small square color swatch icon. */
static QIcon swatch(const QColor& c) {
QPixmap pm(12, 12);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawRect(0, 0, 12, 12);
return QIcon(pm);
}
void SourceSidebar::refresh() {
/* Preserve expansion state by source id. */
QSet<QString> expanded;
for (int i = 0; i < tree_->topLevelItemCount(); i++) {
QTreeWidgetItem* it = tree_->topLevelItem(i);
if (it->isExpanded()) {
expanded.insert(it->data(0, RoleSrcId).toString());
}
}
tree_->clear();
const auto& sources = hub_->sources();
for (int s = 0; s < static_cast<int>(sources.size()); s++) {
const Source& src = sources[s];
QString label = QString::fromStdString(
src.label.empty() ? src.id : src.label);
bool connected = (src.state == "connected");
QString dot = connected ? QString::fromUtf8("\u25cf ")
: QString::fromUtf8("\u25cb ");
auto* top = new QTreeWidgetItem(tree_);
top->setText(0, dot + label);
top->setForeground(0, connected ? col::green() : col::red());
top->setData(0, RoleSrcIdx, s);
top->setData(0, RoleSigIdx, -1);
top->setData(0, RoleSrcId, QString::fromStdString(src.id));
top->setFlags(Qt::ItemIsEnabled);
for (int g = 0; g < static_cast<int>(src.signals.size()); g++) {
const SignalView& sig = src.signals[g];
QString name = QString::fromStdString(sig.meta.name);
if (sig.meta.numElements > 1) {
name += QString(" [%1]").arg(sig.meta.numElements);
}
if (!sig.meta.unit.empty()) {
name += " (" + QString::fromStdString(sig.meta.unit) + ")";
}
auto* leaf = new QTreeWidgetItem(top);
leaf->setText(0, name);
leaf->setIcon(0, swatch(sig.color));
leaf->setData(0, RoleSrcIdx, s);
leaf->setData(0, RoleSigIdx, g);
leaf->setData(0, RoleSrcId, QString::fromStdString(src.id));
leaf->setForeground(0, col::text());
leaf->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
Qt::ItemIsDragEnabled);
}
if (expanded.isEmpty() ||
expanded.contains(QString::fromStdString(src.id))) {
top->setExpanded(true);
}
}
}
void SourceSidebar::onContextMenu(const QPoint& pos) {
QTreeWidgetItem* it = tree_->itemAt(pos);
if (!it) { return; }
QString srcId = it->data(0, RoleSrcId).toString();
if (srcId.isEmpty()) { return; }
QMenu menu(this);
QAction* rm = menu.addAction("Remove source");
if (menu.exec(tree_->viewport()->mapToGlobal(pos)) == rm) {
hub_->sendRemoveSource(srcId.toStdString());
}
}
} /* namespace shq */