summaryrefslogtreecommitdiff
path: root/plugins/warp/ui/containers.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/warp/ui/containers.cpp')
-rw-r--r--plugins/warp/ui/containers.cpp274
1 files changed, 274 insertions, 0 deletions
diff --git a/plugins/warp/ui/containers.cpp b/plugins/warp/ui/containers.cpp
new file mode 100644
index 00000000..689d695f
--- /dev/null
+++ b/plugins/warp/ui/containers.cpp
@@ -0,0 +1,274 @@
+#include "containers.h"
+
+QVariant WarpSourcesModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return {};
+ if (index.row() < 0 || index.row() >= rowCount())
+ return {};
+
+ const auto &r = m_rows[static_cast<size_t>(index.row())];
+
+ // Build a small two-dot status icon (left: writable, right: uncommitted)
+ auto statusIcon = [](bool writable, bool uncommitted) -> QIcon {
+ static QIcon cache[2][2]; // [writable][uncommitted]
+ QIcon &cached = cache[writable ? 1 : 0][uncommitted ? 1 : 0];
+ if (!cached.isNull())
+ return cached;
+
+ const int w = 16, h = 12, radius = 4;
+ QPixmap pm(w, h);
+ pm.fill(Qt::transparent);
+ QPainter p(&pm);
+ p.setRenderHint(QPainter::Antialiasing, true);
+
+ // Colors
+ QColor writableOn(76, 175, 80); // green
+ QColor writableOff(158, 158, 158); // grey
+ QColor uncommittedOn(255, 193, 7); // amber
+ QColor uncommittedOff(158, 158, 158); // grey
+
+ // Left dot: writable
+ p.setBrush(writable ? writableOn : writableOff);
+ p.setPen(Qt::NoPen);
+ p.drawEllipse(QPoint(4, h / 2), radius, radius);
+
+ // Right dot: uncommitted
+ p.setBrush(uncommitted ? uncommittedOn : uncommittedOff);
+ p.drawEllipse(QPoint(w - 6, h / 2), radius, radius);
+
+ p.end();
+ cached = QIcon(pm);
+ return cached;
+ };
+
+ if (role == Qt::DecorationRole && index.column() == PathCol)
+ {
+ return statusIcon(r.writable, r.uncommitted);
+ }
+
+ if (role == Qt::ToolTipRole && index.column() == PathCol)
+ {
+ QStringList parts;
+ parts << (r.writable ? "Writable" : "Read-only");
+ parts << (r.uncommitted ? "Uncommitted changes" : "No uncommitted changes");
+ return parts.join(" • ");
+ }
+
+ if (role == Qt::DisplayRole)
+ {
+ switch (index.column())
+ {
+ case GuidCol: return r.guid;
+ case PathCol: return r.path;
+ case WritableCol: return r.writable ? "Yes" : "No";
+ case UncommittedCol: return r.uncommitted ? "Yes" : "No";
+ default: return {};
+ }
+ }
+
+ if (role == Qt::CheckStateRole)
+ {
+ // Optional: expose as checkboxes if someone ever shows these columns
+ switch (index.column())
+ {
+ case WritableCol: return r.writable ? Qt::Checked : Qt::Unchecked;
+ case UncommittedCol: return r.uncommitted ? Qt::Checked : Qt::Unchecked;
+ default: break;
+ }
+ }
+
+ return {};
+}
+
+WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, QWidget *parent) : QWidget(parent)
+{
+ m_container = std::move(container);
+ auto *layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ m_tabs = new QTabWidget(this);
+ layout->addWidget(m_tabs);
+
+ // Sources tab
+ m_sourcesPage = new QWidget(this);
+ auto *sourcesLayout = new QVBoxLayout(m_sourcesPage);
+ m_sourcesView = new QTableView(m_sourcesPage);
+ m_sourcesModel = new WarpSourcesModel(m_sourcesPage);
+ m_sourcesModel->setContainer(m_container);
+ m_sourcesView->setModel(m_sourcesModel);
+ m_sourcesView->horizontalHeader()->setStretchLastSection(true);
+ m_sourcesView->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_sourcesView->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ // Make the table look like a simple list that shows only the source path
+ m_sourcesView->setShowGrid(false);
+ m_sourcesView->verticalHeader()->setVisible(false);
+ m_sourcesView->horizontalHeader()->setVisible(false);
+ m_sourcesView->setAlternatingRowColors(false);
+ m_sourcesView->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_sourcesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_sourcesView->setWordWrap(false);
+ m_sourcesView->setIconSize(QSize(16, 12));
+ // Ensure long paths truncate from the left: "...tail/of/the/path"
+ m_sourcesView->setTextElideMode(Qt::ElideLeft);
+ // Hide GUID column, keep only the Path column visible
+ m_sourcesView->setColumnHidden(WarpSourcesModel::GuidCol, true);
+ // Also hide boolean columns; their state is shown as an icon next to the path
+ m_sourcesView->setColumnHidden(WarpSourcesModel::WritableCol, true);
+ m_sourcesView->setColumnHidden(WarpSourcesModel::UncommittedCol, true);
+ // Ensure the remaining (Path) column fills the width
+ m_sourcesView->horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch);
+
+ // Per-item context menu
+ m_sourcesView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(m_sourcesView, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
+ QMenu menu(m_sourcesView);
+ const QModelIndex index = m_sourcesView->indexAt(pos);
+
+ if (!index.isValid())
+ {
+ QAction *actAdd = menu.addAction(tr("Add Source"));
+ QAction *chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == actAdd)
+ {
+ std::string sourceName;
+ if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source"))
+ return;
+ if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value())
+ {
+ BinaryNinja::LogAlertF("Failed to add source: {}", sourceName);
+ return;
+ }
+ m_sourcesModel->reload();
+ }
+ } else
+ {
+ m_sourcesView->setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol));
+
+ const int row = index.row();
+ const QModelIndex pathIdx = m_sourcesModel->index(row, WarpSourcesModel::PathCol);
+ const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol);
+ const QString path = m_sourcesModel->data(pathIdx, Qt::DisplayRole).toString();
+ const QFileInfo fi(path);
+
+ const QString guid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
+
+ QAction *actReveal = menu.addAction(tr("Reveal in File Browser"));
+ actReveal->setEnabled(fi.exists());
+ QAction *actCopyPath = menu.addAction(tr("Copy Path"));
+ QAction *actCopyGuid = menu.addAction(tr("Copy GUID"));
+
+ QAction *chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == actCopyPath)
+ QGuiApplication::clipboard()->setText(path);
+ else if (chosen == actCopyGuid)
+ QGuiApplication::clipboard()->setText(guid);
+ else if (chosen == actReveal)
+ QDesktopServices::openUrl(QUrl::fromLocalFile(fi.absoluteFilePath()));
+ }
+ });
+
+
+ sourcesLayout->addWidget(m_sourcesView);
+ m_tabs->addTab(m_sourcesPage, tr("Sources"));
+
+ // Search tab
+ m_searchTab = new WarpSearchWidget(m_container, this);
+ m_tabs->addTab(m_searchTab, tr("Search"));
+
+ // Periodic refresh timer for the Sources view
+ m_refreshTimer = new QTimer(this);
+ m_refreshTimer->setInterval(5000);
+ connect(m_refreshTimer, &QTimer::timeout, this, [this]() {
+ // Only refresh if the widget and the Sources page are actually visible
+ if (!this->isVisible() || !m_sourcesPage || !m_sourcesPage->isVisible())
+ return;
+
+ // Preserve selection by GUID across reloads
+ QString currentGuid;
+ if (const QModelIndex currentIdx = m_sourcesView->currentIndex(); currentIdx.isValid())
+ {
+ const int row = currentIdx.row();
+ const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol);
+ currentGuid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
+ }
+
+ m_sourcesModel->reload();
+
+ if (!currentGuid.isEmpty())
+ {
+ for (int r = 0; r < m_sourcesModel->rowCount(); ++r)
+ {
+ const QModelIndex gIdx = m_sourcesModel->index(r, WarpSourcesModel::GuidCol);
+ if (m_sourcesModel->data(gIdx, Qt::DisplayRole).toString() == currentGuid)
+ {
+ m_sourcesView->setCurrentIndex(m_sourcesModel->index(r, WarpSourcesModel::PathCol));
+ break;
+ }
+ }
+ }
+ });
+ m_refreshTimer->start();
+
+ // Optional: force a refresh when switching back to the Sources tab
+ connect(m_tabs, &QTabWidget::currentChanged, this, [this](const int idx) {
+ QWidget *w = m_tabs->widget(idx);
+ if (w == m_sourcesPage)
+ m_sourcesModel->reload();
+ });
+}
+
+WarpContainersPane::WarpContainersPane(QWidget *parent) : QWidget(parent)
+{
+ auto *splitter = new QSplitter(Qt::Vertical, this);
+ splitter->setContentsMargins(0, 0, 0, 0);
+ auto *mainLayout = new QVBoxLayout(this);
+ mainLayout->setContentsMargins(0, 0, 0, 0);
+ mainLayout->setSpacing(0);
+ mainLayout->addWidget(splitter);
+ auto newPalette = palette();
+ newPalette.setColor(QPalette::Window, getThemeColor(SidebarWidgetBackgroundColor));
+ setAutoFillBackground(true);
+ setPalette(newPalette);
+
+ // List on top
+ m_list = new QListWidget(splitter);
+ m_list->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_list->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_list->setUniformItemSizes(true);
+
+ // Make names larger and show end of long strings (elide at the start)
+ {
+ QFont f = m_list->font();
+ f.setPointSizeF(f.pointSizeF() + 2.0); // bump size
+ m_list->setFont(f);
+ m_list->setTextElideMode(Qt::ElideLeft);
+ }
+
+ // Container view (tabs) below
+ m_stack = new QStackedWidget(splitter);
+ m_stack->setContentsMargins(0, 0, 0, 0);
+
+ splitter->setStretchFactor(0, 0); // list: minimal growth
+ splitter->setStretchFactor(1, 1); // stack: takes remaining space
+ splitter->setCollapsible(0, false);
+ splitter->setCollapsible(1, false);
+
+ populate();
+
+ connect(m_list, &QListWidget::currentRowChanged, this, [this](int row) {
+ if (row >= 0 && row < m_stack->count())
+ m_stack->setCurrentIndex(row);
+ });
+
+ // Select the first container if available
+ if (m_list->count() > 0)
+ {
+ m_list->setCurrentRow(0);
+ }
+}