summaryrefslogtreecommitdiff
path: root/plugins/warp/ui/shared
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2026-03-13 12:24:18 -0700
committerMason Reed <35282038+emesare@users.noreply.github.com>2026-03-24 18:46:48 -0700
commit3c88b11e5df33116580ac008e36092775df66135 (patch)
treecd64fbe149f05683ddabcccd0db7b87356f1f8cf /plugins/warp/ui/shared
parentf325aa7b6026a1daef84931baeb1f50d7da10c10 (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')
-rw-r--r--plugins/warp/ui/shared/chunk.cpp159
-rw-r--r--plugins/warp/ui/shared/chunk.h28
-rw-r--r--plugins/warp/ui/shared/commitdialog.cpp141
-rw-r--r--plugins/warp/ui/shared/commitdialog.h74
-rw-r--r--plugins/warp/ui/shared/fetchdialog.cpp69
-rw-r--r--plugins/warp/ui/shared/fetchdialog.h2
-rw-r--r--plugins/warp/ui/shared/fetcher.cpp3
-rw-r--r--plugins/warp/ui/shared/fetcher.h1
-rw-r--r--plugins/warp/ui/shared/file.cpp71
-rw-r--r--plugins/warp/ui/shared/file.h25
-rw-r--r--plugins/warp/ui/shared/function.cpp7
-rw-r--r--plugins/warp/ui/shared/function.h7
-rw-r--r--plugins/warp/ui/shared/misc.cpp74
-rw-r--r--plugins/warp/ui/shared/misc.h22
-rw-r--r--plugins/warp/ui/shared/processordialog.cpp451
-rw-r--r--plugins/warp/ui/shared/processordialog.h127
-rw-r--r--plugins/warp/ui/shared/search.cpp22
-rw-r--r--plugins/warp/ui/shared/selectprojectfilesdialog.cpp141
-rw-r--r--plugins/warp/ui/shared/selectprojectfilesdialog.h26
-rw-r--r--plugins/warp/ui/shared/source.cpp204
-rw-r--r--plugins/warp/ui/shared/source.h116
21 files changed, 1698 insertions, 72 deletions
diff --git a/plugins/warp/ui/shared/chunk.cpp b/plugins/warp/ui/shared/chunk.cpp
new file mode 100644
index 00000000..911628eb
--- /dev/null
+++ b/plugins/warp/ui/shared/chunk.cpp
@@ -0,0 +1,159 @@
+#include "chunk.h"
+
+#include <QAction>
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QHeaderView>
+#include <QMenu>
+#include <QVBoxLayout>
+
+#include "misc.h"
+
+ChunkWidget::ChunkWidget(QWidget* parent) : QWidget(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(4);
+
+ // Search Box
+ m_searchBox = new QLineEdit(this);
+ m_searchBox->setPlaceholderText("Search chunk contents...");
+ m_searchBox->setClearButtonEnabled(true);
+ connect(m_searchBox, &QLineEdit::textChanged, this, &ChunkWidget::onSearchTextChanged);
+ layout->addWidget(m_searchBox);
+
+ m_countLabel = new QLabel(this);
+ m_countLabel->setContentsMargins(4, 0, 4, 0);
+ layout->addWidget(m_countLabel);
+
+ // Table Widget (Styled as a list)
+ m_table = new QTableWidget(this);
+ m_table->setColumnCount(3);
+ m_table->setHorizontalHeaderLabels({"Type", "Name", "ID"});
+ m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ m_table->setItemDelegateForColumn(1, new TokenDataDelegate(this));
+ m_table->setColumnHidden(2, true);
+
+ // Visual tweaks to make it look like a nice list
+ m_table->verticalHeader()->setVisible(false);
+ m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_table->setShowGrid(false);
+ m_table->setAlternatingRowColors(false);
+ m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_table->setStyleSheet("QTableWidget::item { padding: 10px; }");
+
+ layout->addWidget(m_table);
+}
+
+void ChunkWidget::setChunk(Warp::Ref<Warp::Chunk> chunk)
+{
+ m_chunk = chunk;
+ m_searchBox->clear();
+ populateTable();
+}
+
+void ChunkWidget::populateTable()
+{
+ m_table->setRowCount(0);
+ if (!m_chunk)
+ {
+ updateCountLabel();
+ return;
+ }
+
+ auto functions = m_chunk->GetFunctions();
+ auto types = m_chunk->GetTypes();
+
+ m_table->setRowCount(functions.size() + types.size());
+ int row = 0;
+
+ for (const auto& func : functions)
+ {
+ m_table->setItem(row, 0, new QTableWidgetItem("Function"));
+
+ auto* nameItem = new QTableWidgetItem();
+ std::string symbolName = func->GetSymbolName();
+ TokenData tokenData(symbolName);
+
+ if (auto warpType = func->GetType())
+ {
+ if (auto analysisType = warpType->GetAnalysisType())
+ tokenData = TokenData(*analysisType, symbolName);
+ }
+
+ nameItem->setText(QString::fromStdString(symbolName)); // Fallback text for search
+ nameItem->setData(Qt::UserRole, QVariant::fromValue(tokenData));
+ m_table->setItem(row, 1, nameItem);
+
+ auto* idItem = new QTableWidgetItem(QString::fromStdString(func->GetGUID().ToString()));
+ m_table->setItem(row, 2, idItem);
+
+ row++;
+ }
+
+ for (const auto& type : types)
+ {
+ m_table->setItem(row, 0, new QTableWidgetItem("Type"));
+
+ auto* nameItem = new QTableWidgetItem();
+ std::string typeName = type->GetName().value_or("");
+ TokenData tokenData(typeName);
+
+ if (auto analysisType = type->GetAnalysisType())
+ {
+ tokenData = TokenData(*analysisType, typeName);
+ }
+
+ nameItem->setText(QString::fromStdString(typeName)); // Fallback text for search
+ nameItem->setData(Qt::UserRole, QVariant::fromValue(tokenData));
+ m_table->setItem(row, 1, nameItem);
+
+ m_table->setItem(row, 2, new QTableWidgetItem(""));
+
+ row++;
+ }
+
+ updateCountLabel();
+}
+
+void ChunkWidget::onSearchTextChanged(const QString& text)
+{
+ for (int i = 0; i < m_table->rowCount(); ++i)
+ {
+ bool match = false;
+ for (int j = 0; j < m_table->columnCount(); ++j)
+ {
+ auto* item = m_table->item(i, j);
+ if (item && item->text().contains(text, Qt::CaseInsensitive))
+ {
+ match = true;
+ break;
+ }
+ }
+ m_table->setRowHidden(i, !match);
+ }
+ updateCountLabel();
+}
+
+void ChunkWidget::updateCountLabel()
+{
+ int totalCount = m_table->rowCount();
+ int visibleCount = 0;
+ for (int i = 0; i < totalCount; ++i)
+ {
+ if (!m_table->isRowHidden(i))
+ visibleCount++;
+ }
+
+ if (m_searchBox->text().isEmpty())
+ {
+ m_countLabel->setText(QString::number(totalCount) + " items");
+ }
+ else
+ {
+ m_countLabel->setText(QString::number(visibleCount) + " of " + QString::number(totalCount) + " items");
+ }
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/chunk.h b/plugins/warp/ui/shared/chunk.h
new file mode 100644
index 00000000..a03a8eae
--- /dev/null
+++ b/plugins/warp/ui/shared/chunk.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <QWidget>
+#include <QTableWidget>
+#include <QLineEdit>
+#include <QLabel>
+#include "warp.h"
+
+class ChunkWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ChunkWidget(QWidget* parent = nullptr);
+ void setChunk(Warp::Ref<Warp::Chunk> chunk);
+
+private slots:
+ void onSearchTextChanged(const QString& text);
+
+private:
+ void populateTable();
+ void updateCountLabel();
+
+ Warp::Ref<Warp::Chunk> m_chunk;
+ QLineEdit* m_searchBox;
+ QLabel* m_countLabel;
+ QTableWidget* m_table;
+}; \ No newline at end of file
diff --git a/plugins/warp/ui/shared/commitdialog.cpp b/plugins/warp/ui/shared/commitdialog.cpp
new file mode 100644
index 00000000..1ea6f4ba
--- /dev/null
+++ b/plugins/warp/ui/shared/commitdialog.cpp
@@ -0,0 +1,141 @@
+#include "commitdialog.h"
+#include "file.h"
+#include "misc.h"
+
+#include <QHBoxLayout>
+#include <QFormLayout>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QTimer>
+
+CommitDialog::CommitDialog(Warp::Ref<Warp::File> file, QWidget* parent) : QDialog(parent), m_file(file)
+{
+ setWindowModality(Qt::NonModal);
+ setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
+ setWindowTitle("Commit to Source");
+ setMinimumSize(300, 200);
+
+ auto* mainLayout = new QVBoxLayout(this);
+
+ auto* commitFormLayout = new QFormLayout();
+
+ m_containerCombo = new QComboBox(this);
+ connect(
+ m_containerCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CommitDialog::onContainerChanged);
+
+ m_sourcesView = new WarpSourcesView(this);
+ m_proxyModel = new QSortFilterProxyModel(this);
+ m_proxyModel->setSourceModel(m_sourcesView->sourceModel());
+ m_proxyModel->setFilterKeyColumn(WarpSourcesModel::PathCol);
+ m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_sourcesView->setModel(m_proxyModel);
+
+ auto* sourceLayout = new QVBoxLayout();
+
+ auto* filterLayout = new QHBoxLayout();
+ m_sourceFilter = new QLineEdit(this);
+ m_sourceFilter->setPlaceholderText("Filter sources...");
+ connect(m_sourceFilter, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString);
+ filterLayout->addWidget(m_sourceFilter);
+
+ m_addSourceButton = new QPushButton("+", this);
+ m_addSourceButton->setFixedWidth(30);
+ m_addSourceButton->setToolTip("Add source");
+ connect(m_addSourceButton, &QPushButton::clicked, this, &CommitDialog::onCreateNewSource);
+ filterLayout->addWidget(m_addSourceButton);
+
+ sourceLayout->addLayout(filterLayout);
+ sourceLayout->addWidget(m_sourcesView);
+
+ commitFormLayout->addRow("Container:", m_containerCombo);
+ commitFormLayout->addRow("Source:", sourceLayout);
+
+ auto commitBtnLabel = QString("Commit %1 chunks").arg(m_file->GetChunks().size());
+ m_commitButton = new QPushButton(commitBtnLabel, this);
+ connect(m_commitButton, &QPushButton::clicked, this, &CommitDialog::onCommit);
+
+ mainLayout->addLayout(commitFormLayout);
+ mainLayout->addWidget(m_commitButton, 0, Qt::AlignRight);
+
+ populateContainers();
+
+ if (!m_containers.empty())
+ m_sourcesView->setContainer(m_containers[m_containerCombo->currentIndex()]);
+}
+
+void CommitDialog::populateContainers()
+{
+ m_containers = Warp::Container::All();
+ m_containerCombo->clear();
+ for (const auto& container : m_containers)
+ m_containerCombo->addItem(QString::fromStdString(container->GetName()));
+}
+
+void CommitDialog::onContainerChanged(int index)
+{
+ if (index >= 0 && index < m_containers.size())
+ m_sourcesView->setContainer(m_containers[index]);
+}
+
+void CommitDialog::onCreateNewSource()
+{
+ if (m_sourcesView->addSource())
+ {
+ // Select the newly added source
+ int rowCount = m_sourcesView->sourceModel()->rowCount();
+ if (rowCount > 0)
+ {
+ QModelIndex sourceIdx = m_sourcesView->sourceModel()->index(rowCount - 1, WarpSourcesModel::PathCol);
+ m_sourcesView->setCurrentIndex(m_proxyModel->mapFromSource(sourceIdx));
+ }
+ }
+}
+
+void CommitDialog::onCommit()
+{
+ if (!m_file)
+ return;
+ int containerIdx = m_containerCombo->currentIndex();
+ QModelIndex proxyIdx = m_sourcesView->currentIndex();
+
+ if (m_file->GetChunks().empty())
+ {
+ QMessageBox::critical(this, "Error", "No chunks to commit.");
+ return;
+ }
+
+ if (containerIdx < 0 || !proxyIdx.isValid())
+ {
+ QMessageBox::critical(this, "Error", "No source selected, please select a source.");
+ return;
+ }
+
+ auto container = m_containers[containerIdx];
+ QModelIndex sourceIdx = m_proxyModel->mapToSource(proxyIdx);
+ auto optSource = m_sourcesView->sourceFromRow(sourceIdx.row());
+ if (!optSource.has_value())
+ {
+ QMessageBox::critical(this, "Error", "Failed to retrieve the selected source.");
+ return;
+ }
+ auto source = optSource.value();
+
+ m_commitButton->setEnabled(false);
+ m_commitButton->setText("Committing...");
+
+ auto* worker = new WarpCommitWorker(container, source, m_file);
+ connect(worker, &WarpCommitWorker::finishedCommitting, this, &CommitDialog::onCommitFinished);
+ connect(worker, &WarpCommitWorker::finished, worker, &QObject::deleteLater);
+ worker->start();
+}
+
+void CommitDialog::onCommitFinished(bool success)
+{
+ m_commitButton->setEnabled(true);
+ m_commitButton->setText("Commit");
+
+ if (success)
+ QMessageBox::information(this, "Success", "Successfully committed to the source.");
+ else
+ QMessageBox::critical(this, "Error", "Failed to commit to the source.");
+}
diff --git a/plugins/warp/ui/shared/commitdialog.h b/plugins/warp/ui/shared/commitdialog.h
new file mode 100644
index 00000000..ccbe8da1
--- /dev/null
+++ b/plugins/warp/ui/shared/commitdialog.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include <QDialog>
+#include <QStackedWidget>
+#include <QComboBox>
+#include <QProgressBar>
+#include <QThread>
+#include <QVBoxLayout>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+
+#include "binaryninjaapi.h"
+#include "source.h"
+#include "warp.h"
+#include "source.h"
+
+// Worker to commit a file to a container
+class WarpCommitWorker : public QThread
+{
+ Q_OBJECT
+
+ Warp::Ref<Warp::Container> m_container;
+ Warp::Source m_source;
+ Warp::Ref<Warp::File> m_file;
+
+public:
+ WarpCommitWorker(Warp::Ref<Warp::Container> container, Warp::Source source, Warp::Ref<Warp::File> file,
+ QObject* parent = nullptr) : QThread(parent), m_container(container), m_source(source), m_file(file)
+ {}
+
+ void run() override
+ {
+ for (const auto& chunk : m_file->GetChunks())
+ {
+ if (auto target = chunk->GetTarget())
+ m_container->AddFunctions(*target, m_source, chunk->GetFunctions());
+ m_container->AddTypes(m_source, chunk->GetTypes());
+ }
+
+ const bool result = m_container->CommitSource(m_source);
+ emit finishedCommitting(result);
+ }
+
+signals:
+ void finishedCommitting(bool success);
+};
+
+class CommitDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CommitDialog(Warp::Ref<Warp::File> file, QWidget* parent = nullptr);
+
+private slots:
+ void onContainerChanged(int index);
+ void onCreateNewSource();
+ void onCommit();
+ void onCommitFinished(bool success);
+
+private:
+ void populateContainers();
+
+ Warp::Ref<Warp::File> m_file;
+ std::vector<Warp::Ref<Warp::Container>> m_containers;
+
+ QComboBox* m_containerCombo;
+ QLineEdit* m_sourceFilter;
+ QPushButton* m_addSourceButton;
+ QSortFilterProxyModel* m_proxyModel;
+ WarpSourcesView* m_sourcesView;
+ QPushButton* m_commitButton;
+}; \ No newline at end of file
diff --git a/plugins/warp/ui/shared/fetchdialog.cpp b/plugins/warp/ui/shared/fetchdialog.cpp
index 44d7c237..41dea3e0 100644
--- a/plugins/warp/ui/shared/fetchdialog.cpp
+++ b/plugins/warp/ui/shared/fetchdialog.cpp
@@ -25,7 +25,7 @@ static void AddListItem(QListWidget* list, const QString& value)
WarpFetchDialog::WarpFetchDialog(BinaryViewRef bv, std::shared_ptr<WarpFetcher> fetcher, QWidget* parent) :
QDialog(parent), m_fetchProcessor(std::move(fetcher)), m_bv(std::move(bv))
{
- setWindowTitle("Fetch WARP Functions");
+ setWindowTitle("WARP Fetcher");
auto form = new QFormLayout();
m_containerCombo = new QComboBox(this);
@@ -160,8 +160,8 @@ void WarpFetchDialog::onReject()
reject();
}
-void WarpFetchDialog::runBatchedFetch(const std::optional<size_t>& containerIndex,
- const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher)
+void WarpFetchDialog::runBatchedFetch(
+ const std::optional<size_t>& containerIndex, const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher)
{
if (!m_bv)
return;
@@ -178,48 +178,27 @@ void WarpFetchDialog::runBatchedFetch(const std::optional<size_t>& containerInde
auto bv = m_bv;
// TODO: Too many captures in this thing lol.
- WorkerInteractiveEnqueue(
- [fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable {
- const auto batchSize = GetBatchSizeFromView(bv);
- size_t processed = 0;
- while (processed < funcs.size())
- {
- if (task->IsCancelled())
- break;
- const size_t remaining = funcs.size() - processed;
- const size_t thisBatchCount = std::min(batchSize, remaining);
- for (size_t i = 0; i < thisBatchCount; ++i)
- fetcher->AddPendingFunction(funcs[processed + i]);
- fetcher->FetchPendingFunctions(allowedTags);
- processed += thisBatchCount;
- task->SetProgressText("Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")");
- }
+ WorkerInteractiveEnqueue([fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable {
+ const auto batchSize = GetBatchSizeFromView(bv);
+ size_t processed = 0;
+ while (processed < funcs.size())
+ {
+ if (task->IsCancelled())
+ break;
+ const size_t remaining = funcs.size() - processed;
+ const size_t thisBatchCount = std::min(batchSize, remaining);
+ for (size_t i = 0; i < thisBatchCount; ++i)
+ fetcher->AddPendingFunction(funcs[processed + i]);
+ fetcher->FetchPendingFunctions(allowedTags);
+ processed += thisBatchCount;
+ task->SetProgressText(
+ "Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")");
+ }
- task->Finish();
- Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds());
+ task->Finish();
+ Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds());
- if (rerunMatcher && bv)
- Warp::RunMatcher(*bv);
- });
-}
-
-void RegisterWarpFetchFunctionsCommand()
-{
- // Register a UI action and bind it globally. Add it to the Tools menu.
- const QString actionName = "WARP\\Fetch";
- if (!UIAction::isActionRegistered(actionName))
- UIAction::registerAction(actionName);
-
- UIActionHandler::globalActions()->bindAction(actionName,
- UIAction(
- [](const UIActionContext& context) {
- if (const BinaryViewRef bv = context.binaryView; bv)
- {
- WarpFetchDialog dlg(bv, WarpFetcher::Global(), nullptr);
- dlg.exec();
- }
- },
- [](const UIActionContext& context) { return context.binaryView != nullptr; }));
-
- Menu::mainMenu("Plugins")->addAction(actionName, "Plugins");
+ if (rerunMatcher && bv)
+ Warp::RunMatcher(*bv);
+ });
}
diff --git a/plugins/warp/ui/shared/fetchdialog.h b/plugins/warp/ui/shared/fetchdialog.h
index 72b8c57e..68fca6c1 100644
--- a/plugins/warp/ui/shared/fetchdialog.h
+++ b/plugins/warp/ui/shared/fetchdialog.h
@@ -52,5 +52,3 @@ private:
void runBatchedFetch(const std::optional<size_t>& containerIndex, const std::vector<Warp::SourceTag>& allowedTags,
bool rerunMatcher);
};
-
-void RegisterWarpFetchFunctionsCommand();
diff --git a/plugins/warp/ui/shared/fetcher.cpp b/plugins/warp/ui/shared/fetcher.cpp
index 2eab910a..f5aa626a 100644
--- a/plugins/warp/ui/shared/fetcher.cpp
+++ b/plugins/warp/ui/shared/fetcher.cpp
@@ -73,7 +73,8 @@ void WarpFetcher::FetchPendingFunctions(const std::vector<Warp::SourceTag>& allo
auto platform = func->GetPlatform();
platformMappedGuidSet[platform].insert(warpFunc->GetGUID());
- // We want to keep track of the guids so we can constrain the server response to only return functions with any of them.
+ // We want to keep track of the guids so we can constrain the server response to only return functions with any
+ // of them.
const auto constraints = warpFunc->GetConstraints();
std::vector<Warp::ConstraintGUID> constraintGuids;
constraintGuids.reserve(constraints.size());
diff --git a/plugins/warp/ui/shared/fetcher.h b/plugins/warp/ui/shared/fetcher.h
index 3464ecd4..ad38ed60 100644
--- a/plugins/warp/ui/shared/fetcher.h
+++ b/plugins/warp/ui/shared/fetcher.h
@@ -24,6 +24,7 @@ class WarpFetcher
std::mutex m_requestMutex;
std::vector<FunctionRef> m_pendingRequests;
std::unordered_set<Warp::FunctionGUID> m_processedGuids;
+
public:
using CallbackId = uint64_t;
using CompletionCallback = std::function<WarpFetchCompletionStatus()>;
diff --git a/plugins/warp/ui/shared/file.cpp b/plugins/warp/ui/shared/file.cpp
new file mode 100644
index 00000000..70a169fe
--- /dev/null
+++ b/plugins/warp/ui/shared/file.cpp
@@ -0,0 +1,71 @@
+#include "file.h"
+
+#include <QVBoxLayout>
+
+FileWidget::FileWidget(QWidget* parent) : QWidget(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ auto* splitter = new QSplitter(Qt::Horizontal, this);
+
+ // Left side: List of chunks
+ m_list = new QListWidget(this);
+ m_list->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_list->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_list->setUniformItemSizes(true);
+ connect(m_list, &QListWidget::itemSelectionChanged, this, &FileWidget::onListSelectionChanged);
+ splitter->addWidget(m_list);
+
+ // Right side: Chunk Widget
+ m_chunkWidget = new ChunkWidget(this);
+ splitter->addWidget(m_chunkWidget);
+
+ splitter->setSizes({100, 900});
+ layout->addWidget(splitter);
+}
+
+void FileWidget::setFile(Warp::Ref<Warp::File> file)
+{
+ m_file = file;
+ m_list->clear();
+ m_chunkWidget->setChunk(nullptr);
+ m_currentChunks.clear();
+
+ if (!m_file)
+ return;
+
+ m_currentChunks = m_file->GetChunks();
+ for (size_t i = 0; i < m_currentChunks.size(); ++i)
+ {
+ auto* listItem = new QListWidgetItem(m_list);
+ listItem->setText(QString("Chunk #%1").arg(i + 1));
+ // Store the chunk index in the item's data for easy retrieval
+ listItem->setData(Qt::UserRole, QVariant::fromValue(static_cast<qulonglong>(i)));
+ }
+
+ if (m_list->count() > 0)
+ m_list->setCurrentRow(0);
+}
+
+void FileWidget::onListSelectionChanged()
+{
+ auto selectedItems = m_list->selectedItems();
+ if (selectedItems.isEmpty())
+ {
+ m_chunkWidget->setChunk(nullptr);
+ return;
+ }
+
+ auto* item = selectedItems.first();
+ QVariant data = item->data(Qt::UserRole);
+ if (data.isValid())
+ {
+ size_t index = data.value<qulonglong>();
+ if (index < m_currentChunks.size())
+ {
+ m_chunkWidget->setChunk(m_currentChunks[index]);
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/file.h b/plugins/warp/ui/shared/file.h
new file mode 100644
index 00000000..057fd2cd
--- /dev/null
+++ b/plugins/warp/ui/shared/file.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QWidget>
+#include <QListWidget>
+#include <QSplitter>
+#include "warp.h"
+#include "chunk.h"
+
+class FileWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit FileWidget(QWidget* parent = nullptr);
+ void setFile(Warp::Ref<Warp::File> file);
+
+private slots:
+ void onListSelectionChanged();
+
+private:
+ Warp::Ref<Warp::File> m_file;
+ QListWidget* m_list;
+ ChunkWidget* m_chunkWidget;
+ std::vector<Warp::Ref<Warp::Chunk>> m_currentChunks;
+}; \ No newline at end of file
diff --git a/plugins/warp/ui/shared/function.cpp b/plugins/warp/ui/shared/function.cpp
index 3b2451ab..fd403d77 100644
--- a/plugins/warp/ui/shared/function.cpp
+++ b/plugins/warp/ui/shared/function.cpp
@@ -21,8 +21,8 @@ WarpFunctionItem::WarpFunctionItem(
// Serialize the tokens to make it accessible via QModelIndex.
// We will take these tokens and then user them in our custom item delegate.
TokenData tokenData = TokenData(symbolName);
- if (BinaryNinja::Ref<BinaryNinja::Type> type = m_function->GetType(*analysisFunction))
- tokenData = TokenData(*type, symbolName);
+ if (Warp::Ref<Warp::Type> warpType = m_function->GetType())
+ tokenData = TokenData(*warpType->GetAnalysisType(analysisFunction->GetArchitecture()), symbolName);
setData(QVariant::fromValue(tokenData), Qt::UserRole);
}
@@ -256,8 +256,7 @@ void WarpFunctionTableWidget::RegisterContextMenuAction(
m_contextMenuActions[name] = callback;
}
-void WarpFunctionTableWidget::RegisterContextMenuAction(
- const QString& name,
+void WarpFunctionTableWidget::RegisterContextMenuAction(const QString& name,
const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback,
const std::function<bool(WarpFunctionItem*, std::optional<uint64_t>)>& isValid)
{
diff --git a/plugins/warp/ui/shared/function.h b/plugins/warp/ui/shared/function.h
index c6e5ba63..4cf226c7 100644
--- a/plugins/warp/ui/shared/function.h
+++ b/plugins/warp/ui/shared/function.h
@@ -112,10 +112,9 @@ public:
void RegisterContextMenuAction(
const QString& name, const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback);
- void RegisterContextMenuAction(
- const QString &name,
- const std::function<void(WarpFunctionItem *, std::optional<uint64_t>)> &callback,
- const std::function<bool(WarpFunctionItem *, std::optional<uint64_t>)> &isValid);
+ void RegisterContextMenuAction(const QString& name,
+ const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback,
+ const std::function<bool(WarpFunctionItem*, std::optional<uint64_t>)>& isValid);
void SetFunctions(QVector<WarpFunctionItem*> functions);
diff --git a/plugins/warp/ui/shared/misc.cpp b/plugins/warp/ui/shared/misc.cpp
index 6de314b1..c88cee10 100644
--- a/plugins/warp/ui/shared/misc.cpp
+++ b/plugins/warp/ui/shared/misc.cpp
@@ -72,6 +72,75 @@ void AddressColorDelegate::paint(QPainter* painter, const QStyleOptionViewItem&
QStyledItemDelegate::paint(painter, opt, index);
}
+void SourcePathDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+
+ // Draw background and selection highlights
+ opt.widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
+
+ QString text = index.data(Qt::DisplayRole).toString();
+ int sepIdx = qMax(text.lastIndexOf('/'), text.lastIndexOf('\\'));
+ QString dirPart = sepIdx != -1 ? text.left(sepIdx + 1) : "";
+ QString filePart = sepIdx != -1 ? text.mid(sepIdx + 1) : text;
+
+ QFont regularFont = opt.font;
+ QFont boldFont = regularFont;
+ boldFont.setBold(true);
+
+ QFontMetrics fmReg(regularFont);
+ QFontMetrics fmBold(boldFont);
+
+ // Basic padding inside the list item
+ QRect textRect = opt.rect.adjusted(3, 0, -3, 0);
+
+ int fileWidth = fmBold.horizontalAdvance(filePart);
+ int dirWidth = fmReg.horizontalAdvance(dirPart);
+
+ QString textToDrawDir;
+ QString textToDrawFile = filePart;
+
+ if (dirWidth + fileWidth > textRect.width())
+ {
+ if (fileWidth > textRect.width())
+ {
+ // The file name itself is too long, elide it
+ textToDrawDir = "";
+ textToDrawFile = fmBold.elidedText(filePart, Qt::ElideLeft, textRect.width());
+ }
+ else
+ {
+ // Elide the directory part so the bold file name fits
+ textToDrawDir = fmReg.elidedText(dirPart, Qt::ElideLeft, textRect.width() - fileWidth);
+ }
+ }
+ else
+ {
+ textToDrawDir = dirPart;
+ }
+
+ painter->save();
+
+ // Set the proper text color based on selection state
+ if (opt.state & QStyle::State_Selected)
+ painter->setPen(opt.palette.highlightedText().color());
+ else
+ painter->setPen(opt.palette.text().color());
+
+ // Draw the directory part
+ painter->setFont(regularFont);
+ painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, textToDrawDir);
+
+ // Draw the file part
+ painter->setFont(boldFont);
+ int dirAdvance = fmReg.horizontalAdvance(textToDrawDir);
+ QRect fileRect = textRect.adjusted(dirAdvance, 0, 0, 0);
+ painter->drawText(fileRect, Qt::AlignLeft | Qt::AlignVCenter, textToDrawFile);
+
+ painter->restore();
+}
+
bool GenericTextFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
auto filterString = filterRegularExpression().pattern();
@@ -142,13 +211,14 @@ ParsedQuery::ParsedQuery(const QString& rawQuery)
query = query.simplified();
}
-WarpRemoveMatchDialog::WarpRemoveMatchDialog(QWidget *parent, FunctionRef func) : QDialog(parent), m_func(func)
+WarpRemoveMatchDialog::WarpRemoveMatchDialog(QWidget* parent, FunctionRef func) : QDialog(parent), m_func(func)
{
setWindowTitle("Remove Matching Function");
setModal(true);
auto* vbox = new QVBoxLayout(this);
- auto* text = new QLabel("Remove the match for this function? You can also mark it as ignored to prevent future automatic matches.");
+ auto* text = new QLabel(
+ "Remove the match for this function? You can also mark it as ignored to prevent future automatic matches.");
text->setWordWrap(true);
vbox->addWidget(text);
diff --git a/plugins/warp/ui/shared/misc.h b/plugins/warp/ui/shared/misc.h
index 92e27fd8..e9d6fd43 100644
--- a/plugins/warp/ui/shared/misc.h
+++ b/plugins/warp/ui/shared/misc.h
@@ -67,6 +67,14 @@ public:
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
};
+class SourcePathDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit SourcePathDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {}
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+};
class GenericTextFilterModel : public QSortFilterProxyModel
{
@@ -109,6 +117,7 @@ struct ParsedQuery
class WarpRemoveMatchDialog : public QDialog
{
Q_OBJECT
+
public:
explicit WarpRemoveMatchDialog(QWidget* parent, FunctionRef func);
@@ -116,7 +125,7 @@ public:
private:
FunctionRef m_func;
- QCheckBox* m_ignoreCheck{nullptr};
+ QCheckBox* m_ignoreCheck {nullptr};
};
constexpr const char* ALLOWED_TAGS_SETTING = "warp.fetcher.allowedSourceTags";
@@ -144,4 +153,15 @@ inline size_t GetBatchSizeFromView(const BinaryViewRef& view)
if (!settings->Contains(BATCH_SIZE_SETTING))
return 10000;
return settings->Get<uint64_t>(BATCH_SIZE_SETTING, view);
+}
+
+inline void RegisterPluginAction(
+ std::string name, std::function<void(const UIActionContext&)> action,
+ std::function<bool(const UIActionContext&)> isValid = [](const UIActionContext&) { return true; })
+{
+ const QString actionName = QString("WARP\\%1").arg(QString::fromStdString(name));
+ if (!UIAction::isActionRegistered(actionName))
+ UIAction::registerAction(actionName);
+ UIActionHandler::globalActions()->bindAction(actionName, UIAction(action, isValid));
+ Menu::mainMenu("Plugins")->addAction(actionName, "Plugins");
} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/processordialog.cpp b/plugins/warp/ui/shared/processordialog.cpp
new file mode 100644
index 00000000..9e602b79
--- /dev/null
+++ b/plugins/warp/ui/shared/processordialog.cpp
@@ -0,0 +1,451 @@
+#include "processordialog.h"
+#include "commitdialog.h"
+#include "misc.h"
+#include "selectprojectfilesdialog.h"
+
+#include <QHBoxLayout>
+#include <QFormLayout>
+#include <QFileDialog>
+#include <QFile>
+#include <QMessageBox>
+#include <QMenu>
+#include <QContextMenuEvent>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace BinaryNinja;
+
+ProcessorDialog::ProcessorDialog(QWidget* parent) : QDialog(parent)
+{
+ setWindowModality(Qt::NonModal);
+ setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
+ setWindowTitle("WARP Processor");
+ setMinimumSize(400, 300);
+
+ auto* mainLayout = new QVBoxLayout(this);
+ m_stack = new QStackedWidget(this);
+ mainLayout->addWidget(m_stack);
+
+ // Page 1: Configuration
+ auto* configPage = new QWidget(this);
+ auto* configLayout = new QVBoxLayout(configPage);
+
+ auto* entrySearchLayout = new QHBoxLayout();
+ entrySearchLayout->setContentsMargins(0, 0, 0, 0);
+ m_entrySearch = new QLineEdit(this);
+ m_entrySearch->setPlaceholderText("Search entries...");
+ connect(m_entrySearch, &QLineEdit::textChanged, this, &ProcessorDialog::onSearchItems);
+ entrySearchLayout->addWidget(m_entrySearch);
+
+ m_addButton = new QPushButton("+", this);
+ m_addButton->setFixedWidth(30);
+ m_addButton->setToolTip("Add entries");
+ connect(m_addButton, &QPushButton::clicked, this, &ProcessorDialog::onAddEntryMenu);
+ entrySearchLayout->addWidget(m_addButton);
+ configLayout->addLayout(entrySearchLayout);
+
+ m_entryList = new QListWidget(this);
+ m_entryList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_entryList->setContextMenuPolicy(Qt::CustomContextMenu);
+ m_entryList->setTextElideMode(Qt::ElideLeft);
+ m_entryList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_entryList->setStyleSheet("QListWidget::item { padding: 2px; }");
+ connect(m_entryList, &QListWidget::customContextMenuRequested, this, &ProcessorDialog::showContextMenu);
+ configLayout->addWidget(m_entryList);
+
+ auto* formLayout = new QFormLayout();
+ m_includedDataCombo = new QComboBox(this);
+ m_includedDataCombo->addItem("Symbols", WARPProcessorIncludedDataSymbols);
+ m_includedDataCombo->addItem("Signatures", WARPProcessorIncludedDataSignatures);
+ m_includedDataCombo->addItem("Types", WARPProcessorIncludedDataTypes);
+ m_includedDataCombo->addItem("All", WARPProcessorIncludedDataAll);
+ m_includedDataCombo->setCurrentIndex(3);
+
+ m_includedFunctionsCombo = new QComboBox(this);
+ m_includedFunctionsCombo->addItem("Selected", WARPProcessorIncludedFunctionsSelected);
+ m_includedFunctionsCombo->addItem("Annotated", WARPProcessorIncludedFunctionsAnnotated);
+ m_includedFunctionsCombo->addItem("All", WARPProcessorIncludedFunctionsAll);
+ m_includedFunctionsCombo->setCurrentIndex(1);
+
+ m_workerCountSpinBox = new QSpinBox(this);
+ m_workerCountSpinBox->setMinimum(2);
+ m_workerCountSpinBox->setValue(GetWorkerThreadCount());
+
+ formLayout->addRow("Included Data:", m_includedDataCombo);
+ formLayout->addRow("Included Functions:", m_includedFunctionsCombo);
+ formLayout->addRow("Worker Count:", m_workerCountSpinBox);
+ configLayout->addLayout(formLayout);
+
+ m_processButton = new QPushButton("Process", this);
+ m_processButton->setEnabled(false);
+ connect(m_processButton, &QPushButton::clicked, this, &ProcessorDialog::onStartProcessing);
+ configLayout->addWidget(m_processButton, 0, Qt::AlignRight);
+ m_stack->addWidget(configPage);
+
+ // Page 2: Processing
+ auto* processPage = new QWidget(this);
+ auto* processLayout = new QVBoxLayout(processPage);
+ m_processingLabel = new QLabel("Processing...", this);
+ m_processingLabel->setAlignment(Qt::AlignCenter);
+ m_progressBar = new QProgressBar(this);
+ m_progressBar->setRange(0, 0);
+
+ m_stateList = new QListWidget(this);
+ m_stateList->setSelectionMode(QAbstractItemView::NoSelection);
+ m_stateList->setFocusPolicy(Qt::NoFocus);
+ m_stateList->setTextElideMode(Qt::ElideLeft);
+ m_stateList->setWordWrap(false);
+ m_stateList->setStyleSheet(
+ "QListWidget { background: transparent; border: none; } QListWidget::item { padding: 1px; }");
+ m_stateList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_stateList->setMinimumHeight(100);
+
+ m_cancelButton = new QPushButton("Cancel", this);
+ connect(m_cancelButton, &QPushButton::clicked, this, &ProcessorDialog::onCancelProcessing);
+
+ m_updateTimer = new QTimer(this);
+ connect(m_updateTimer, &QTimer::timeout, this, &ProcessorDialog::onUpdateState);
+
+ processLayout->addStretch();
+ processLayout->addWidget(m_processingLabel);
+ processLayout->addWidget(m_progressBar);
+ processLayout->addWidget(m_stateList);
+ processLayout->addStretch();
+ processLayout->addWidget(m_cancelButton, 0, Qt::AlignRight);
+ m_stack->addWidget(processPage);
+
+ // Page 3: Results
+ auto* resultsPage = new QWidget(this);
+ auto* resultsLayout = new QVBoxLayout(resultsPage);
+ m_fileWidget = new FileWidget(this);
+ resultsLayout->addWidget(m_fileWidget);
+
+ auto* buttonLayout = new QHBoxLayout();
+ m_saveButton = new QPushButton("Save to File", this);
+ connect(m_saveButton, &QPushButton::clicked, this, &ProcessorDialog::onSaveToFile);
+ m_elapsedLabel = new QLabel(this);
+ m_commitButton = new QPushButton("Commit", this);
+ connect(m_commitButton, &QPushButton::clicked, this, &ProcessorDialog::onCommit);
+ buttonLayout->addWidget(m_elapsedLabel);
+ buttonLayout->addStretch();
+ buttonLayout->addWidget(m_saveButton);
+ buttonLayout->addWidget(m_commitButton);
+ resultsLayout->addLayout(buttonLayout);
+ m_stack->addWidget(resultsPage);
+
+ m_stack->setCurrentIndex(ConfigurationPage);
+}
+
+ProcessorDialog::~ProcessorDialog()
+{
+ m_updateTimer->stop();
+ if (m_processor)
+ m_processor->Cancel();
+}
+
+void ProcessorDialog::onStartProcessing()
+{
+ if (m_toProcess.empty())
+ return;
+
+ auto includedData = static_cast<BNWARPProcessorIncludedData>(m_includedDataCombo->currentData().toInt());
+ auto includedFunctions =
+ static_cast<BNWARPProcessorIncludedFunctions>(m_includedFunctionsCombo->currentData().toInt());
+ auto workerCount = static_cast<size_t>(m_workerCountSpinBox->value());
+
+ m_processor = std::make_shared<Warp::Processor>(includedData, includedFunctions, workerCount);
+
+ for (const auto& item : m_toProcess)
+ {
+ switch (item.type)
+ {
+ case ToProcessEntry::ViewMode:
+ m_processor->AddBinaryView(*item.view);
+ break;
+ case ToProcessEntry::PathMode:
+ m_processor->AddPath(item.path);
+ break;
+ case ToProcessEntry::ProjectMode:
+ m_processor->AddProject(*item.project);
+ break;
+ case ToProcessEntry::ProjectFileMode:
+ m_processor->AddProjectFile(*item.projectFile);
+ break;
+ }
+ }
+
+ m_stack->setCurrentIndex(ProcessingPage);
+ m_processTimer.start();
+ auto* worker = new WarpProcessorWorker(m_processor);
+ connect(worker, &WarpProcessorWorker::finishedProcessing, this, &ProcessorDialog::onProcessingFinished);
+ connect(worker, &WarpProcessorWorker::finished, worker, &QObject::deleteLater);
+ worker->start();
+
+ m_cancelButton->setEnabled(true);
+ m_updateTimer->start(100);
+}
+
+void ProcessorDialog::onAddEntryMenu()
+{
+ QMenu menu(nullptr);
+ addAddActionsToMenu(&menu);
+ menu.exec(m_addButton->mapToGlobal(QPoint(0, m_addButton->height())));
+}
+
+void ProcessorDialog::addAddActionsToMenu(QMenu* menu)
+{
+ menu->addAction("Add Files...", this, &ProcessorDialog::onAddPath);
+ menu->addAction("Add Directory...", this, &ProcessorDialog::onAddDirectory);
+ menu->addAction("Add Project Files", this, &ProcessorDialog::onAddProjectFiles);
+}
+
+void ProcessorDialog::showContextMenu(const QPoint& pos)
+{
+ QMenu menu(nullptr);
+ addAddActionsToMenu(&menu);
+
+ QListWidgetItem* item = m_entryList->itemAt(pos);
+ if (item)
+ {
+ menu.addSeparator();
+ menu.addAction("Remove", this, &ProcessorDialog::onRemoveItem);
+ }
+
+ menu.exec(m_entryList->mapToGlobal(pos));
+}
+
+void ProcessorDialog::onAddBinaryView(Ref<BinaryView> view)
+{
+ ToProcessEntry item;
+ item.type = ToProcessEntry::ViewMode;
+ item.view = view;
+ item.displayName = QString("View: %1").arg(QString::fromStdString(view->GetFile()->GetFilename()));
+
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+ onSearchItems();
+ m_processButton->setEnabled(true);
+}
+
+void ProcessorDialog::onAddPath()
+{
+ QStringList paths = QFileDialog::getOpenFileNames(this, "Select Files", "", "All Files (*)");
+ if (paths.isEmpty())
+ return;
+
+ for (const auto& path : paths)
+ addPathRecursive(path);
+
+ onSearchItems();
+ m_processButton->setEnabled(true);
+}
+
+void ProcessorDialog::onAddDirectory()
+{
+ QString path = QFileDialog::getExistingDirectory(this, "Select Directory", "");
+ if (path.isEmpty())
+ return;
+
+ addPathRecursive(path);
+
+ onSearchItems();
+ m_processButton->setEnabled(true);
+}
+
+void ProcessorDialog::addPathRecursive(const QString& path)
+{
+ QFileInfo info(path);
+ if (info.isDir())
+ {
+ QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ addSinglePath(it.next());
+ }
+ }
+ else
+ {
+ addSinglePath(path);
+ }
+}
+
+void ProcessorDialog::addSinglePath(const QString& path)
+{
+ ToProcessEntry item;
+ item.type = ToProcessEntry::PathMode;
+ item.path = path.toStdString();
+ item.displayName = QString("Path: %1").arg(path);
+
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+}
+
+void ProcessorDialog::onAddProjectFiles()
+{
+ auto projects = Project::GetOpenProjects();
+ if (projects.empty())
+ {
+ QMessageBox::information(this, "Add Project Files", "No projects are currently open.");
+ return;
+ }
+
+ SelectProjectFilesDialog dlg(this);
+ if (dlg.exec() == Accepted)
+ {
+ auto selectedFiles = dlg.getSelectedFiles();
+ if (selectedFiles.empty())
+ {
+ // If no files selected, add the entire project
+ auto project = dlg.getSelectedProject();
+ ToProcessEntry item;
+ item.type = ToProcessEntry::ProjectMode;
+ item.project = project;
+ item.displayName = QString("Project: %1").arg(QString::fromStdString(project->GetName()));
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+ }
+ else
+ {
+ for (auto& file : selectedFiles)
+ {
+ ToProcessEntry item;
+ item.type = ToProcessEntry::ProjectFileMode;
+ item.projectFile = file;
+ item.displayName = QString("File: %1").arg(QString::fromStdString(file->GetName()));
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+ }
+ }
+ onSearchItems();
+ m_processButton->setEnabled(true);
+ }
+}
+
+void ProcessorDialog::onRemoveItem()
+{
+ auto selectedItems = m_entryList->selectedItems();
+ if (selectedItems.isEmpty())
+ {
+ int row = m_entryList->currentRow();
+ if (row >= 0 && row < (int)m_toProcess.size())
+ {
+ m_toProcess.erase(m_toProcess.begin() + row);
+ delete m_entryList->takeItem(row);
+ }
+ }
+ else
+ {
+ for (auto* item : selectedItems)
+ {
+ int row = m_entryList->row(item);
+ if (row >= 0 && row < (int)m_toProcess.size())
+ {
+ m_toProcess.erase(m_toProcess.begin() + row);
+ delete m_entryList->takeItem(row);
+ }
+ }
+ }
+ m_processButton->setEnabled(!m_toProcess.empty());
+}
+
+void ProcessorDialog::onSearchItems()
+{
+ QString filter = m_entrySearch->text().toLower();
+ for (int i = 0; i < m_entryList->count(); ++i)
+ {
+ auto* item = m_entryList->item(i);
+ item->setHidden(!item->text().toLower().contains(filter));
+ }
+}
+
+void ProcessorDialog::onProcessingFinished(Warp::Ref<Warp::File> file)
+{
+ m_updateTimer->stop();
+
+ if (!file)
+ {
+ QMessageBox::critical(this, "Error", "Failed to process the selected input.");
+ m_stack->setCurrentIndex(ConfigurationPage);
+ return;
+ }
+
+ auto elapsed = m_processTimer.elapsed();
+ if (elapsed < 1000)
+ m_elapsedLabel->setText(QString("Processing took: %1ms").arg(elapsed));
+ else
+ m_elapsedLabel->setText(QString("Processing took: %1s").arg(elapsed / 1000.0, 0, 'f', 2));
+
+ m_file = file;
+ m_fileWidget->setFile(m_file);
+ m_stack->setCurrentIndex(ResultsPage);
+}
+
+void ProcessorDialog::onCancelProcessing()
+{
+ if (m_processor)
+ m_processor->Cancel();
+ m_cancelButton->setEnabled(false);
+ m_processingLabel->setText("Cancelling...");
+}
+
+void ProcessorDialog::onUpdateState()
+{
+ if (!m_processor)
+ return;
+
+ auto state = m_processor->GetState();
+ size_t total = state.processedFilesCount + state.unprocessedFilesCount;
+ if (total > 0)
+ {
+ m_progressBar->setMaximum(total);
+ m_progressBar->setValue(state.processedFilesCount);
+ }
+
+ m_stateList->clear();
+
+ auto addToList = [&](const std::vector<std::string>& files, const QString& prefix) {
+ for (auto it = files.rbegin(); it != files.rend(); ++it)
+ {
+ auto* item = new QListWidgetItem(prefix + QString::fromStdString(*it));
+ item->setTextAlignment(Qt::AlignCenter);
+ m_stateList->addItem(item);
+ }
+ };
+
+ addToList(state.processingFiles, "Processing: ");
+ addToList(state.analyzingFiles, "Analyzing: ");
+}
+
+void ProcessorDialog::onSaveToFile()
+{
+ if (!m_file)
+ return;
+
+ QString fileName = QFileDialog::getSaveFileName(this, "Save WARP File", "", "WARP Files (*.warp)");
+ if (!fileName.isEmpty())
+ {
+ DataBuffer buffer = m_file->ToDataBuffer();
+ QFile file(fileName);
+ if (file.open(QIODevice::WriteOnly))
+ {
+ file.write(static_cast<const char*>(buffer.GetData()), buffer.GetLength());
+ file.close();
+ QMessageBox::information(this, "Success", "File saved successfully.");
+ }
+ else
+ {
+ QMessageBox::critical(this, "Error", "Failed to open file for writing.");
+ }
+ }
+}
+
+void ProcessorDialog::onCommit()
+{
+ if (!m_file)
+ return;
+
+ auto* dialog = new CommitDialog(m_file, this);
+ dialog->setAttribute(Qt::WA_DeleteOnClose);
+ dialog->show();
+}
diff --git a/plugins/warp/ui/shared/processordialog.h b/plugins/warp/ui/shared/processordialog.h
new file mode 100644
index 00000000..3f69601d
--- /dev/null
+++ b/plugins/warp/ui/shared/processordialog.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include <QDialog>
+#include <QStackedWidget>
+#include <QComboBox>
+#include <QProgressBar>
+#include <QThread>
+#include <QTimer>
+#include <QMenu>
+#include <QSpinBox>
+#include <QElapsedTimer>
+#include <utility>
+
+#include "binaryninjaapi.h"
+#include "warp.h"
+#include "file.h"
+
+// TODO: Both of these are bothersome but I don't really want to do a ID lookup.
+Q_DECLARE_METATYPE(BinaryNinja::Ref<BinaryNinja::Project>)
+Q_DECLARE_METATYPE(BinaryNinja::Ref<BinaryNinja::ProjectFile>)
+
+// Worker to run the processor
+class WarpProcessorWorker : public QThread
+{
+ Q_OBJECT
+
+ std::shared_ptr<Warp::Processor> m_processor;
+
+public:
+ WarpProcessorWorker(std::shared_ptr<Warp::Processor> processor, QObject* parent = nullptr) :
+ QThread(parent), m_processor(std::move(processor))
+ {}
+
+ void run() override
+ {
+ Warp::Ref<Warp::File> file = m_processor->Start();
+ emit finishedProcessing(file);
+ }
+
+signals:
+ void finishedProcessing(Warp::Ref<Warp::File> file);
+};
+
+class ProcessorDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ProcessorDialog(QWidget* parent = nullptr);
+ ~ProcessorDialog() override;
+
+ void onAddBinaryView(BinaryNinja::Ref<BinaryNinja::BinaryView> view);
+ void onAddProjectFiles();
+
+private slots:
+ void onStartProcessing();
+ void onProcessingFinished(Warp::Ref<Warp::File> file);
+ void onCancelProcessing();
+ void onUpdateState();
+ void onSaveToFile();
+ void onCommit();
+ void onAddPath();
+ void onAddDirectory();
+ void onRemoveItem();
+ void onSearchItems();
+ void onAddEntryMenu();
+ void showContextMenu(const QPoint& pos);
+
+private:
+ void addAddActionsToMenu(QMenu* menu);
+ void addPathRecursive(const QString& path);
+ void addSinglePath(const QString& path);
+ struct ToProcessEntry
+ {
+ enum Type
+ {
+ ViewMode,
+ PathMode,
+ ProjectMode,
+ ProjectFileMode
+ } type;
+ BinaryNinja::Ref<BinaryNinja::BinaryView> view;
+ std::string path;
+ BinaryNinja::Ref<BinaryNinja::Project> project;
+ BinaryNinja::Ref<BinaryNinja::ProjectFile> projectFile;
+ QString displayName;
+ };
+
+ Warp::Ref<Warp::File> m_file;
+ std::shared_ptr<Warp::Processor> m_processor;
+ std::vector<ToProcessEntry> m_toProcess;
+
+ enum Page
+ {
+ ConfigurationPage = 0,
+ ProcessingPage = 1,
+ ResultsPage = 2
+ };
+
+ QStackedWidget* m_stack;
+
+ // Page 1: Configuration
+ QLineEdit* m_entrySearch;
+ QPushButton* m_addButton;
+ QListWidget* m_entryList;
+
+ // Global Config
+ QComboBox* m_includedDataCombo;
+ QComboBox* m_includedFunctionsCombo;
+ QSpinBox* m_workerCountSpinBox;
+ QPushButton* m_processButton;
+
+ // Page 2: Processing
+ QLabel* m_processingLabel;
+ QProgressBar* m_progressBar;
+ QListWidget* m_stateList;
+ QPushButton* m_cancelButton;
+ QTimer* m_updateTimer;
+
+ // Page 3: Results
+ FileWidget* m_fileWidget;
+ QPushButton* m_saveButton;
+ QLabel* m_elapsedLabel;
+ QPushButton* m_commitButton;
+
+ QElapsedTimer m_processTimer;
+};
diff --git a/plugins/warp/ui/shared/search.cpp b/plugins/warp/ui/shared/search.cpp
index a852605e..801aa23d 100644
--- a/plugins/warp/ui/shared/search.cpp
+++ b/plugins/warp/ui/shared/search.cpp
@@ -1,6 +1,6 @@
#include "search.h"
#include "misc.h"
-#include "../../../../../ui/mainwindow.h"
+#include "viewframe.h"
QVariant WarpSearchModel::data(const QModelIndex& index, int role) const
{
@@ -12,8 +12,8 @@ QVariant WarpSearchModel::data(const QModelIndex& index, int role) const
if (role == Qt::UserRole && index.column() == DisplayCol)
{
if (it && it->GetKind() == WARPContainerSearchItemKindFunction)
- if (auto itemType = it->GetType(nullptr))
- return QVariant::fromValue(TokenData(*itemType, it->GetName()));
+ if (auto itemType = it->GetType())
+ return QVariant::fromValue(TokenData(*itemType->GetAnalysisType(), it->GetName()));
return {};
}
@@ -246,21 +246,16 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget
return;
// TODO: Getting the current view here is really awful, but i dont care right now.
- auto ctx = MainWindow::activeContext();
+ auto ctx = UIContext::activeContext();
auto view = ctx->getCurrentView();
auto binaryView = view->getData();
auto viewFrame = ctx->getCurrentViewFrame();
auto viewLocation = viewFrame->getViewLocation();
auto func = viewLocation.getFunction();
- // Retrieve the current architecture from the current function or try the current view.
- auto arch = binaryView->GetDefaultArchitecture();
- if (func)
- arch = func->GetArchitecture();
-
const int row = idx.row();
const auto item = m_model->itemAt(row);
- const auto itemType = item->GetType(arch);
+ const auto itemType = item->GetType();
const auto itemFunc = item->GetFunction();
QMenu menu(this);
@@ -272,7 +267,7 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget
// We let users apply the type for types and functions (assuming the function has one)
// if the user applies a function, we actually will set the user type for the current view location function.
// For types, we will just throw it in the user types.
- applyType->setEnabled(itemType != nullptr);
+ applyType->setEnabled(itemType->m_object != nullptr);
applyType->setVisible(applyType->isEnabled());
applyFunction->setEnabled(func != nullptr && itemFunc);
@@ -294,11 +289,12 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget
{
if (func && item->GetKind() == WARPContainerSearchItemKindFunction)
{
- func->SetUserType(itemType);
+ func->SetUserType(itemType->GetAnalysisType(func->GetArchitecture()));
binaryView->UpdateAnalysis();
}
else
- binaryView->DefineUserType(item->GetName(), itemType);
+ binaryView->DefineUserType(
+ item->GetName(), itemType->GetAnalysisType(binaryView->GetDefaultArchitecture()));
}
else if (chosen == applyFunction)
{
diff --git a/plugins/warp/ui/shared/selectprojectfilesdialog.cpp b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp
new file mode 100644
index 00000000..e8454cff
--- /dev/null
+++ b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp
@@ -0,0 +1,141 @@
+#include "selectprojectfilesdialog.h"
+
+#include <QFormLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+using namespace BinaryNinja;
+
+SelectProjectFilesDialog::SelectProjectFilesDialog(QWidget* parent) : QDialog(parent)
+{
+ setWindowTitle("Select Project Files");
+ setMinimumSize(700, 400);
+ auto* layout = new QVBoxLayout(this);
+
+ auto* topLayout = new QFormLayout();
+ m_projectCombo = new QComboBox(this);
+ auto projects = Project::GetOpenProjects();
+ for (auto& project : projects)
+ {
+ m_projectCombo->addItem(QString::fromStdString(project->GetName()), QVariant::fromValue(project));
+ }
+ topLayout->addRow("Project:", m_projectCombo);
+ layout->addLayout(topLayout);
+
+ m_searchBar = new QLineEdit(this);
+ m_searchBar->setPlaceholderText("Search files...");
+ connect(m_searchBar, &QLineEdit::textChanged, this, &SelectProjectFilesDialog::filterLists);
+ layout->addWidget(m_searchBar);
+
+ auto* listsLayout = new QHBoxLayout();
+
+ auto* notAddingBox = new QVBoxLayout();
+ notAddingBox->addWidget(new QLabel("Available:"));
+ m_notAddingList = new QListWidget(this);
+ m_notAddingList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_notAddingList->setTextElideMode(Qt::ElideLeft);
+ m_notAddingList->setStyleSheet("QListWidget::item { padding: 2px; }");
+ notAddingBox->addWidget(m_notAddingList);
+ listsLayout->addLayout(notAddingBox);
+
+ auto* middleButtons = new QVBoxLayout();
+ middleButtons->addStretch();
+ auto* addButton = new QPushButton(">>", this);
+ connect(addButton, &QPushButton::clicked, [this]() { moveSelected(m_notAddingList, m_addingList); });
+ middleButtons->addWidget(addButton);
+ auto* removeButton = new QPushButton("<<", this);
+ connect(removeButton, &QPushButton::clicked, [this]() { moveSelected(m_addingList, m_notAddingList); });
+ middleButtons->addWidget(removeButton);
+ middleButtons->addStretch();
+ listsLayout->addLayout(middleButtons);
+
+ auto* addingBox = new QVBoxLayout();
+ addingBox->addWidget(new QLabel("Selected:"));
+ m_addingList = new QListWidget(this);
+ m_addingList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_addingList->setTextElideMode(Qt::ElideLeft);
+ m_addingList->setStyleSheet("QListWidget::item { padding: 2px; }");
+ addingBox->addWidget(m_addingList);
+ listsLayout->addLayout(addingBox);
+
+ layout->addLayout(listsLayout);
+
+ auto* buttons = new QHBoxLayout(this);
+ auto* ok = new QPushButton("Add", this);
+ connect(ok, &QPushButton::clicked, this, &QDialog::accept);
+ auto* cancel = new QPushButton("Cancel", this);
+ connect(cancel, &QPushButton::clicked, this, &QDialog::reject);
+ buttons->addStretch();
+ buttons->addWidget(ok);
+ buttons->addWidget(cancel);
+ layout->addLayout(buttons);
+
+ connect(m_projectCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+ &SelectProjectFilesDialog::updateFileList);
+
+ connect(m_notAddingList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) {
+ m_addingList->addItem(m_notAddingList->takeItem(m_notAddingList->row(item)));
+ filterLists();
+ });
+ connect(m_addingList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) {
+ m_notAddingList->addItem(m_addingList->takeItem(m_addingList->row(item)));
+ filterLists();
+ });
+
+ updateFileList();
+}
+
+void SelectProjectFilesDialog::updateFileList()
+{
+ m_notAddingList->clear();
+ m_addingList->clear();
+ m_currentProject = m_projectCombo->currentData().value<Ref<Project>>();
+ if (m_currentProject)
+ {
+ for (auto& file : m_currentProject->GetFiles())
+ {
+ auto* item = new QListWidgetItem(QString::fromStdString(file->GetPathInProject()));
+ item->setData(Qt::UserRole, QVariant::fromValue(file));
+ m_notAddingList->addItem(item);
+ }
+ }
+ filterLists();
+}
+
+void SelectProjectFilesDialog::filterLists()
+{
+ QString filter = m_searchBar->text().toLower();
+ for (int i = 0; i < m_notAddingList->count(); ++i)
+ {
+ auto* item = m_notAddingList->item(i);
+ item->setHidden(!item->text().toLower().contains(filter));
+ }
+ for (int i = 0; i < m_addingList->count(); ++i)
+ {
+ auto* item = m_addingList->item(i);
+ item->setHidden(!item->text().toLower().contains(filter));
+ }
+}
+
+void SelectProjectFilesDialog::moveSelected(QListWidget* from, QListWidget* to)
+{
+ QList<QListWidgetItem*> items = from->selectedItems();
+ for (auto* item : items)
+ to->addItem(from->takeItem(from->row(item)));
+ filterLists();
+}
+
+std::vector<Ref<ProjectFile>> SelectProjectFilesDialog::getSelectedFiles() const
+{
+ std::vector<Ref<ProjectFile>> files;
+ for (int i = 0; i < m_addingList->count(); ++i)
+ files.push_back(m_addingList->item(i)->data(Qt::UserRole).value<Ref<ProjectFile>>());
+ return files;
+}
+
+Ref<Project> SelectProjectFilesDialog::getSelectedProject() const
+{
+ return m_currentProject;
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/selectprojectfilesdialog.h b/plugins/warp/ui/shared/selectprojectfilesdialog.h
new file mode 100644
index 00000000..ce5e8093
--- /dev/null
+++ b/plugins/warp/ui/shared/selectprojectfilesdialog.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <QComboBox>
+#include <QDialog>
+#include <QListWidget>
+
+#include "binaryninjaapi.h"
+
+class SelectProjectFilesDialog : public QDialog
+{
+ Q_OBJECT
+ BinaryNinja::Ref<BinaryNinja::Project> m_currentProject;
+ QComboBox* m_projectCombo;
+ QLineEdit* m_searchBar;
+ QListWidget* m_notAddingList;
+ QListWidget* m_addingList;
+
+public:
+ SelectProjectFilesDialog(QWidget* parent = nullptr);
+
+ void updateFileList();
+ void filterLists();
+ void moveSelected(QListWidget* from, QListWidget* to);
+ [[nodiscard]] std::vector<BinaryNinja::Ref<BinaryNinja::ProjectFile>> getSelectedFiles() const;
+ [[nodiscard]] BinaryNinja::Ref<BinaryNinja::Project> getSelectedProject() const;
+};
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
diff --git a/plugins/warp/ui/shared/source.h b/plugins/warp/ui/shared/source.h
new file mode 100644
index 00000000..7dad9333
--- /dev/null
+++ b/plugins/warp/ui/shared/source.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include <QWidget>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QListWidget>
+#include <QTableView>
+
+#include "theme.h"
+#include "warp.h"
+
+class WarpSourcesModel final : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum Columns : int
+ {
+ GuidCol = 0,
+ PathCol,
+ WritableCol,
+ UncommittedCol,
+ ColumnCount
+ };
+
+ explicit WarpSourcesModel(QObject* parent = nullptr) : QAbstractTableModel(parent) {}
+
+ void setContainer(Warp::Ref<Warp::Container> container)
+ {
+ m_container = std::move(container);
+ reload();
+ }
+
+ void reload()
+ {
+ // Fetch synchronously (can be adapted to async if needed)
+ beginResetModel();
+ m_rows.clear();
+ for (const auto& src : m_container->GetSources())
+ {
+ QString guid = QString::fromStdString(src.ToString());
+ QString path = QString::fromStdString(m_container->SourcePath(src).value_or(std::string {}));
+ bool writable = m_container->IsSourceWritable(src);
+ bool uncommitted = m_container->IsSourceUncommitted(src);
+ m_rows.push_back({guid, path, writable, uncommitted});
+ }
+ endResetModel();
+ }
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ if (parent.isValid())
+ return 0;
+ return static_cast<int>(m_rows.size());
+ }
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ Q_UNUSED(parent);
+ return ColumnCount;
+ }
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override
+ {
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case GuidCol:
+ return "Source GUID";
+ case PathCol:
+ return "Path";
+ case WritableCol:
+ return "Writable";
+ case UncommittedCol:
+ return "Uncommitted";
+ default:
+ return {};
+ }
+ }
+ return {};
+ }
+
+private:
+ struct Row
+ {
+ QString guid;
+ QString path;
+ bool writable;
+ bool uncommitted;
+ };
+
+ std::vector<Row> m_rows;
+ Warp::Ref<Warp::Container> m_container;
+};
+
+
+class WarpSourcesView : public QTableView
+{
+ Q_OBJECT
+
+public:
+ explicit WarpSourcesView(QWidget* parent = nullptr);
+
+ void setContainer(Warp::Ref<Warp::Container> container);
+ bool addSource();
+
+ [[nodiscard]] WarpSourcesModel* sourceModel() const { return m_model; }
+ [[nodiscard]] std::optional<Warp::Source> sourceFromRow(int row) const;
+
+private:
+ WarpSourcesModel* m_model = nullptr;
+ Warp::Ref<Warp::Container> m_container;
+}; \ No newline at end of file