diff options
| author | Mason Reed <mason@vector35.com> | 2026-03-13 12:24:18 -0700 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-03-24 18:46:48 -0700 |
| commit | 3c88b11e5df33116580ac008e36092775df66135 (patch) | |
| tree | cd64fbe149f05683ddabcccd0db7b87356f1f8cf /plugins/warp/ui/shared/source.cpp | |
| parent | f325aa7b6026a1daef84931baeb1f50d7da10c10 (diff) | |
[WARP] Improved UX and API
- Exposes WARP type objects directly
- Adds processor API (for generating warp files directly)
- Adds file and chunk API
- Misc cleanup
- Simplified the amount of commands
- Replaced the "Create" commands with a purpose built processor dialog
- Added a native QT viewer for WARP files
- Simplified committing to a remote with a purpose built commit dialog
Diffstat (limited to 'plugins/warp/ui/shared/source.cpp')
| -rw-r--r-- | plugins/warp/ui/shared/source.cpp | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/plugins/warp/ui/shared/source.cpp b/plugins/warp/ui/shared/source.cpp new file mode 100644 index 00000000..c7736151 --- /dev/null +++ b/plugins/warp/ui/shared/source.cpp @@ -0,0 +1,204 @@ +#include "source.h" + +#include <QClipboard> +#include <QFileInfo> +#include <QHeaderView> +#include <QPainter> + +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 {}; +} + +WarpSourcesView::WarpSourcesView(QWidget* parent) : QTableView(parent) +{ + m_model = new WarpSourcesModel(this); + QTableView::setModel(m_model); + + horizontalHeader()->setStretchLastSection(true); + setSelectionBehavior(SelectRows); + setSelectionMode(SingleSelection); + + // Make the table look like a simple list that shows only the source path + setShowGrid(false); + verticalHeader()->setVisible(false); + horizontalHeader()->setVisible(false); + setAlternatingRowColors(false); + setEditTriggers(NoEditTriggers); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setWordWrap(false); + setIconSize(QSize(16, 12)); + // Ensure long paths truncate from the left: "...tail/of/the/path" + setTextElideMode(Qt::ElideLeft); + + // Hide GUID column, keep only the Path column visible + setColumnHidden(WarpSourcesModel::GuidCol, true); + // Also hide boolean columns; their state is shown as an icon next to the path + setColumnHidden(WarpSourcesModel::WritableCol, true); + setColumnHidden(WarpSourcesModel::UncommittedCol, true); + // Ensure the remaining (Path) column fills the width + horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch); + + // Per-item context menu + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { + if (!m_model || !m_container) + return; + + QMenu menu(this); + const QModelIndex index = indexAt(pos); + + if (!index.isValid()) + { + QAction* actAdd = menu.addAction(tr("Add Source")); + QAction* chosen = menu.exec(viewport()->mapToGlobal(pos)); + if (!chosen) + return; + if (chosen == actAdd) + addSource(); + } + else + { + setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol)); + + const int row = index.row(); + const QModelIndex pathIdx = m_model->index(row, WarpSourcesModel::PathCol); + const QModelIndex guidIdx = m_model->index(row, WarpSourcesModel::GuidCol); + const QString path = m_model->data(pathIdx, Qt::DisplayRole).toString(); + const QFileInfo fi(path); + + const QString guid = m_model->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(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())); + } + }); +} + +void WarpSourcesView::setContainer(Warp::Ref<Warp::Container> container) +{ + m_container = std::move(container); + m_model->setContainer(m_container); +} + +bool WarpSourcesView::addSource() +{ + if (!m_model || !m_container) + return false; + + std::string sourceName; + if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source")) + return false; + if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value()) + { + BinaryNinja::LogAlertF("Failed to add source: {}", sourceName); + return false; + } + m_model->reload(); + return true; +} + +std::optional<Warp::Source> WarpSourcesView::sourceFromRow(int row) const +{ + if (!m_model || row < 0 || row >= m_model->rowCount()) + return std::nullopt; + const QModelIndex guidIdx = m_model->index(row, WarpSourcesModel::GuidCol); + std::string guidStr = m_model->data(guidIdx, Qt::DisplayRole).toString().toStdString(); + return Warp::WarpUUID::FromString(guidStr); +}
\ No newline at end of file |
