/** * @file SourceSidebar.cpp */ #include "SourceSidebar.h" #include "Hub.h" #include "Theme.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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(srcIdx) << static_cast(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 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(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(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 */