summaryrefslogtreecommitdiff
path: root/plugins/warp/ui
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/warp/ui')
-rw-r--r--plugins/warp/ui/CMakeLists.txt51
-rw-r--r--plugins/warp/ui/matched.cpp83
-rw-r--r--plugins/warp/ui/matched.h27
-rw-r--r--plugins/warp/ui/matches.cpp164
-rw-r--r--plugins/warp/ui/matches.h29
-rw-r--r--plugins/warp/ui/plugin.cpp198
-rw-r--r--plugins/warp/ui/plugin.h53
-rw-r--r--plugins/warp/ui/shared/constraint.cpp118
-rw-r--r--plugins/warp/ui/shared/constraint.h78
-rw-r--r--plugins/warp/ui/shared/function.cpp365
-rw-r--r--plugins/warp/ui/shared/function.h168
-rw-r--r--plugins/warp/ui/shared/misc.cpp81
-rw-r--r--plugins/warp/ui/shared/misc.h84
13 files changed, 1499 insertions, 0 deletions
diff --git a/plugins/warp/ui/CMakeLists.txt b/plugins/warp/ui/CMakeLists.txt
new file mode 100644
index 00000000..ebfaddc4
--- /dev/null
+++ b/plugins/warp/ui/CMakeLists.txt
@@ -0,0 +1,51 @@
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+
+project(warp_ui CXX C)
+
+file(GLOB SOURCES CONFIGURE_DEPENDS
+ plugin.cpp plugin.h
+ matches.cpp matches.h
+ matched.cpp matched.h
+ shared/misc.cpp shared/misc.h
+ shared/constraint.cpp shared/constraint.h
+ shared/function.cpp shared/function.h
+ shared/container.cpp shared/container.h)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED)
+
+add_library(${PROJECT_NAME} SHARED ${SOURCES} ${MOCS})
+
+target_include_directories(${PROJECT_NAME} PRIVATE ../api)
+
+if (NOT BN_API_BUILD_EXAMPLES AND NOT BN_INTERNAL_BUILD)
+ # Out-of-tree build
+ find_path(
+ BN_API_PATH
+ NAMES binaryninjaapi.h
+ HINTS ../.. binaryninjaapi $ENV{BN_API_PATH}
+ REQUIRED
+ )
+ add_subdirectory(${BN_API_PATH} api)
+endif ()
+
+target_link_libraries(${PROJECT_NAME} binaryninjaui warp_api Qt6::Core Qt6::Gui Qt6::Widgets)
+
+set_target_properties(${PROJECT_NAME} PROPERTIES
+ CXX_STANDARD 17
+ CXX_VISIBILITY_PRESET hidden
+ CXX_STANDARD_REQUIRED ON
+ VISIBILITY_INLINES_HIDDEN ON
+ POSITION_INDEPENDENT_CODE ON)
+
+if (BN_INTERNAL_BUILD)
+ ui_plugin_rpath(${PROJECT_NAME})
+
+ set_target_properties(${PROJECT_NAME} PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR}
+ RUNTIME_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR})
+else ()
+ bn_install_plugin(${PROJECT_NAME})
+endif ()
diff --git a/plugins/warp/ui/matched.cpp b/plugins/warp/ui/matched.cpp
new file mode 100644
index 00000000..475e0cdd
--- /dev/null
+++ b/plugins/warp/ui/matched.cpp
@@ -0,0 +1,83 @@
+#include "matched.h"
+
+#include <QGridLayout>
+
+#include "theme.h"
+
+const char *WARP_APPLY_ACTIVITY = "analysis.warp.apply";
+
+WarpMatchedWidget::WarpMatchedWidget(BinaryViewRef current)
+{
+ m_current = current;
+ // Create the QT stuff
+ QGridLayout *layout = new QGridLayout(this);
+ layout->setContentsMargins(2, 2, 2, 2);
+ layout->setSpacing(2);
+ auto newPalette = palette();
+ newPalette.setColor(QPalette::Window, getThemeColor(SidebarWidgetBackgroundColor));
+ setAutoFillBackground(true);
+ setPalette(newPalette);
+
+ // TODO: Split horizontally if the widget is displayed in a sidebar that is vertically challenged.
+ m_splitter = new QSplitter(Qt::Vertical);
+ m_splitter->setContentsMargins(0, 0, 0, 0);
+
+ // Add a widget to display the matches.
+ m_tableWidget = new WarpFunctionTableWidget(this);
+ m_tableWidget->setContentsMargins(0, 0, 0, 0);
+ m_splitter->addWidget(m_tableWidget);
+
+ // Toggle the applying workflow, this workflow sets all the data for the function based on the matched function data.
+ m_tableWidget->RegisterContextMenuAction("Toggle Application",
+ [this](WarpFunctionItem *, std::optional<uint64_t> address) {
+ if (!address.has_value())
+ return;
+ for (const auto &func: m_current->GetAnalysisFunctionsForAddress(
+ *address))
+ {
+ const bool previous = BinaryNinja::Settings::Instance()->Get<bool>(
+ WARP_APPLY_ACTIVITY, func);
+ BinaryNinja::Settings::Instance()->Set(
+ WARP_APPLY_ACTIVITY, !previous, func);
+ func->Reanalyze();
+ }
+ });
+
+ layout->addWidget(m_splitter, 1, 0, 1, 5);
+ setLayout(layout);
+
+ Update();
+
+ connect(m_tableWidget->GetTableView(), &QTableView::clicked, this,
+ [this](const QModelIndex &index) {
+ if (m_current == nullptr)
+ return;
+ if (!index.isValid())
+ return;
+ const QModelIndex sourceIndex = m_tableWidget->GetProxyModel()->mapToSource(index);
+ if (!sourceIndex.isValid())
+ return;
+ auto selectedItem = m_tableWidget->GetModel()->GetAddress(sourceIndex);
+ if (!selectedItem.has_value())
+ return;
+ // Navigate to the address in the view, so the user feels like they are doing something.
+ auto currentView = m_current->GetCurrentView();
+ m_current->Navigate(currentView, selectedItem.value());
+ });
+}
+
+void WarpMatchedWidget::Update()
+{
+ m_tableWidget->GetTableView()->setSortingEnabled(false);
+ m_tableWidget->GetTableView()->setEnabled(false);
+ for (const auto &analysisFunction: m_current->GetAnalysisFunctionList())
+ {
+ if (const auto &matchedFunction = Warp::Function::GetMatched(*analysisFunction))
+ {
+ uint64_t startAddress = analysisFunction->GetStart();
+ m_tableWidget->InsertFunction(startAddress, new WarpFunctionItem(matchedFunction, analysisFunction));
+ }
+ }
+ m_tableWidget->GetTableView()->setEnabled(true);
+ m_tableWidget->GetTableView()->setSortingEnabled(true);
+}
diff --git a/plugins/warp/ui/matched.h b/plugins/warp/ui/matched.h
new file mode 100644
index 00000000..5e333e25
--- /dev/null
+++ b/plugins/warp/ui/matched.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <QSplitter>
+
+#include "uitypes.h"
+#include "shared/function.h"
+
+class WarpMatchedFunctionTableWidget : public WarpFunctionTableWidget
+{
+ Q_OBJECT
+};
+
+class WarpMatchedWidget : public QWidget
+{
+ Q_OBJECT
+ BinaryViewRef m_current;
+
+ QSplitter *m_splitter;
+
+ WarpFunctionTableWidget *m_tableWidget;
+
+public:
+ explicit WarpMatchedWidget(BinaryViewRef current);
+
+ ~WarpMatchedWidget() override = default;
+
+ void Update();
+};
diff --git a/plugins/warp/ui/matches.cpp b/plugins/warp/ui/matches.cpp
new file mode 100644
index 00000000..fad729d6
--- /dev/null
+++ b/plugins/warp/ui/matches.cpp
@@ -0,0 +1,164 @@
+#include <QGridLayout>
+#include <QHeaderView>
+
+#include "matches.h"
+
+#include <QClipboard>
+#include <QFormLayout>
+
+#include "theme.h"
+#include "warp.h"
+#include "shared/misc.h"
+
+WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(FunctionRef current)
+{
+ // NOTE: Might be nullptr if the no selected function.
+ m_current = current;
+
+ // Create the QT stuff
+ QGridLayout *layout = new QGridLayout(this);
+ layout->setContentsMargins(2, 2, 2, 2);
+ layout->setSpacing(2);
+ auto newPalette = palette();
+ newPalette.setColor(QPalette::Window, getThemeColor(SidebarWidgetBackgroundColor));
+ setAutoFillBackground(true);
+ setPalette(newPalette);
+
+ // TODO: Split horizontally if the widget is displayed in a sidebar that is vertically challenged.
+ m_splitter = new QSplitter(Qt::Vertical);
+ m_splitter->setContentsMargins(0, 0, 0, 0);
+
+ // Add a widget to display the matches.
+ m_tableWidget = new WarpFunctionTableWidget(this);
+ m_tableWidget->setContentsMargins(0, 0, 0, 0);
+ m_splitter->addWidget(m_tableWidget);
+
+ // Add a widget to display the info about the selected function match.
+ m_infoWidget = new WarpFunctionInfoWidget(this);
+ m_infoWidget->setContentsMargins(0, 0, 0, 0);
+ m_splitter->addWidget(m_infoWidget);
+
+ layout->addWidget(m_splitter, 1, 0, 1, 5);
+ setLayout(layout);
+
+ m_tableWidget->RegisterContextMenuAction("Apply", [this](WarpFunctionItem *item, std::optional<uint64_t>) {
+ if (item == nullptr)
+ return;
+ Warp::Ref<Warp::Function> selectedFunction = item->GetFunction();
+ if (!selectedFunction)
+ return;
+ selectedFunction->Apply(*m_current);
+ // Update analysis so that the selected function shows.
+ m_current->GetView()->UpdateAnalysis();
+ // So it shows visually as selected.
+ m_tableWidget->GetModel()->SetMatchedFunction(selectedFunction);
+ });
+ m_tableWidget->RegisterContextMenuAction("Search for Source",
+ [this](WarpFunctionItem *item, std::optional<uint64_t>) {
+ // Apply the source as the filter.
+ if (const auto source = item->GetSource(); source)
+ m_tableWidget->setFilter(source->ToString());
+ });
+
+ connect(m_tableWidget->GetTableView(), &QTableView::clicked, this,
+ [this](const QModelIndex &index) {
+ if (m_current == nullptr)
+ return;
+ if (!index.isValid())
+ return;
+ const QModelIndex sourceIndex = m_tableWidget->GetProxyModel()->mapToSource(index);
+ if (!sourceIndex.isValid())
+ return;
+ auto selectedItem = m_tableWidget->GetModel()->GetItem(sourceIndex);
+ // Access the first column in the row
+ if (!selectedItem)
+ return;
+ m_infoWidget->SetFunction(selectedItem->GetFunction());
+ m_infoWidget->UpdateInfo();
+ });
+
+
+ connect(m_tableWidget->GetTableView(), &QTableView::doubleClicked, this, [=](const QModelIndex &index) {
+ if (m_current == nullptr)
+ return;
+ // Get the selected row for the given index.
+ if (!index.isValid())
+ return;
+ const QModelIndex sourceIndex = m_tableWidget->GetProxyModel()->mapToSource(index);
+ if (!sourceIndex.isValid())
+ return;
+ auto selectedItem = m_tableWidget->GetModel()->GetItem(sourceIndex);
+ // Access the first column in the row
+ if (!selectedItem)
+ return;
+ Warp::Ref<Warp::Function> selectedFunction = selectedItem->GetFunction();
+
+ // Actually apply the newly selected function.
+ selectedFunction->Apply(*m_current);
+
+ // Update analysis so that the selected function shows.
+ m_current->GetView()->UpdateAnalysis();
+
+ // So it shows visually as selected.
+ m_tableWidget->GetModel()->SetMatchedFunction(selectedFunction);
+ });
+}
+
+void WarpCurrentFunctionWidget::SetCurrentFunction(FunctionRef current)
+{
+ if (m_current == current)
+ return;
+ m_current = current;
+ m_infoWidget->SetAnalysisFunction(m_current);
+ UpdateMatches();
+}
+
+void WarpCurrentFunctionWidget::UpdateMatches()
+{
+ if (!m_current)
+ return;
+ const auto guid = Warp::GetAnalysisFunctionGUID(*m_current);
+ if (!guid.has_value())
+ return;
+
+ // Set the matched function for highlighting.
+ Warp::Ref<Warp::Function> matchedFunction = Warp::Function::GetMatched(*m_current);
+ m_tableWidget->GetModel()->SetMatchedFunction(matchedFunction);
+
+ // We swapped functions, reset the info widget to the default state with new analysis function.
+ m_infoWidget->SetFunction(matchedFunction);
+ m_infoWidget->UpdateInfo();
+
+ Warp::Ref<Warp::Target> target = Warp::Target::FromPlatform(*m_current->GetPlatform());
+
+ // Add all the possible matches for the current function to the model.
+ QVector<WarpFunctionItem *> matches;
+ bool matchedFuncAdded = false;
+ // TODO: When we add in the networked container we need to update this stuff on a separate thread and show a spinny thing.
+ for (const auto &container: Warp::Container::All())
+ {
+ for (const auto &source: container->GetSourcesWithFunctionGUID(*target, guid.value()))
+ {
+ for (const auto &function: container->GetFunctionsWithGUID(*target, source, guid.value()))
+ {
+ // TODO: This does not work.
+ if (matchedFunction && BNWARPFunctionsEqual(function->m_object, matchedFunction->m_object))
+ matchedFuncAdded = true;
+ auto item = new WarpFunctionItem(function, m_current);
+ item->SetContainer(container);
+ item->SetSource(source);
+ matches.emplace_back(item);
+ }
+ }
+ }
+
+ // Add the matched function unconditionally, assuming it has not been found in a container.
+ // NOTE: This happens when you load from a database for example.
+ if (matchedFunction && !matchedFuncAdded)
+ {
+ auto item = new WarpFunctionItem(matchedFunction, m_current);
+ matches.emplace_back(item);
+ }
+
+ m_tableWidget->SetFunctions(matches);
+}
diff --git a/plugins/warp/ui/matches.h b/plugins/warp/ui/matches.h
new file mode 100644
index 00000000..4021f10c
--- /dev/null
+++ b/plugins/warp/ui/matches.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QSplitter>
+
+#include "filter.h"
+#include "render.h"
+#include "shared/function.h"
+
+class WarpCurrentFunctionWidget : public QWidget
+{
+ Q_OBJECT
+ FunctionRef m_current;
+
+ QSplitter *m_splitter;
+
+ WarpFunctionTableWidget *m_tableWidget;
+ WarpFunctionInfoWidget *m_infoWidget;
+
+public:
+ explicit WarpCurrentFunctionWidget(FunctionRef current);
+
+ ~WarpCurrentFunctionWidget() override = default;
+
+ void SetCurrentFunction(FunctionRef current);
+
+ FunctionRef GetCurrentFunction() { return m_current; };
+
+ void UpdateMatches();
+};
diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp
new file mode 100644
index 00000000..69aaaf34
--- /dev/null
+++ b/plugins/warp/ui/plugin.cpp
@@ -0,0 +1,198 @@
+#include "plugin.h"
+
+#include <QToolBar>
+
+#include "matched.h"
+#include "matches.h"
+#include "symbollist.h"
+#include "viewframe.h"
+
+using namespace BinaryNinja;
+
+QIcon GetColoredIcon(const QString &iconPath, const QColor &color)
+{
+ auto pixmap = QPixmap(iconPath);
+ auto mask = pixmap.createMaskFromColor(QColor(0, 0, 0), Qt::MaskInColor);
+ pixmap.fill(color);
+ pixmap.setMask(mask);
+ return QIcon(pixmap);
+}
+
+Ref<BackgroundTask> GetMatcherTask()
+{
+ // TODO: What happens if we have multiple views open matching? This fails.
+ // Look for the matcher background task to determine if we are stopping or starting it.
+ Ref<BackgroundTask> matcherTask = nullptr;
+ for (const auto &task: BackgroundTask::GetRunningTasks())
+ {
+ std::string progressText = task->GetProgressText();
+ if (progressText.find("Matching on WARP") != std::string::npos)
+ matcherTask = task;
+ }
+ return matcherTask;
+}
+
+WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP"), m_data(data)
+{
+ m_logger = LogRegistry::CreateLogger("WARPUI");
+ m_currentFrame = nullptr;
+
+ m_headerWidget = new QWidget();
+ QHBoxLayout *headerLayout = new QHBoxLayout();
+ headerLayout->setContentsMargins(0, 0, 0, 0);
+ headerLayout->setSpacing(0);
+
+ QToolBar *headerToolbar = new QToolBar(this);
+ headerToolbar->setContentsMargins(0, 0, 0, 0);
+ headerToolbar->setIconSize(QSize(20, 20));
+
+ static auto matcherStopIcon = GetColoredIcon(":/icons/images/stop.png", getThemeColor(RedStandardHighlightColor));
+ static auto matcherStartIcon = GetColoredIcon(":/icons/images/start.png",
+ getThemeColor(GreenStandardHighlightColor));
+ m_matcherAction = headerToolbar->addAction(matcherStartIcon, "Run Matcher", [this]() {
+ UIActionHandler *handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
+ if (Ref<BackgroundTask> matcherTask = GetMatcherTask())
+ matcherTask->Cancel();
+ else if (!isMatcherRunning)
+ {
+ handler->executeAction("WARP\\Run Matcher");
+ setMatcherActionIcon(true);
+ }
+ });
+ m_matcherAction->setToolTip("Run the matcher on all functions");
+
+ auto loadIcon = GetColoredIcon(":/icons/images/file-add.png", getThemeColor(BlueStandardHighlightColor));
+ auto loadAction = headerToolbar->addAction(loadIcon, "Load Signature File", [this]() {
+ UIActionHandler *handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
+ handler->executeAction("WARP\\Load File");
+ });
+ loadAction->setToolTip("Load a signature file to match against");
+
+ auto saveIcon = GetColoredIcon(":/icons/images/edit.png", getThemeColor(BlueStandardHighlightColor));
+ auto saveAction = headerToolbar->addAction(saveIcon, "Create Signature File", [this]() {
+ UIActionHandler *handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
+ handler->executeAction("WARP\\Create\\From Current View");
+ });
+ saveAction->setToolTip("Save data to a signature file");
+
+ auto refreshIcon = GetColoredIcon(":/icons/images/refresh.png", getThemeColor(BlueStandardHighlightColor));
+ auto refreshAction = headerToolbar->addAction(refreshIcon, "Refresh the view data", [this]() {
+ Update();
+ });
+ refreshAction->setToolTip("Refresh the sidebar data");
+
+ // TODO: Add action for pushing to network sources.
+
+ // Push the toolbar to the right using a stretch space.
+ headerLayout->addStretch();
+ headerLayout->addWidget(headerToolbar, 0);
+ m_headerWidget->setLayout(headerLayout);
+
+ QFrame *currentFunctionFrame = new QFrame(this);
+ m_currentFunctionWidget = new WarpCurrentFunctionWidget(nullptr);
+ QVBoxLayout *currentFunctionLayout = new QVBoxLayout();
+ currentFunctionLayout->setContentsMargins(0, 0, 0, 0);
+ currentFunctionLayout->setSpacing(0);
+ currentFunctionLayout->addWidget(m_currentFunctionWidget);
+ currentFunctionFrame->setLayout(currentFunctionLayout);
+
+ QFrame *matchedFrame = new QFrame(this);
+ m_matchedWidget = new WarpMatchedWidget(m_data);
+ QVBoxLayout *matchedLayout = new QVBoxLayout();
+ matchedLayout->setContentsMargins(0, 0, 0, 0);
+ matchedLayout->setSpacing(0);
+ matchedLayout->addWidget(m_matchedWidget);
+ matchedFrame->setLayout(matchedLayout);
+
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(0);
+
+ auto tabWidget = new QTabWidget(this);
+ tabWidget->addTab(currentFunctionFrame, "Current Function");
+ tabWidget->addTab(matchedFrame, "Matched Functions");
+
+ m_analysisEvent = new AnalysisCompletionEvent(m_data, [this]() {
+ ExecuteOnMainThread([this]() {
+ Update();
+ });
+ });
+
+ layout->addWidget(tabWidget);
+ this->setLayout(layout);
+}
+
+WarpSidebarWidget::~WarpSidebarWidget()
+{
+ m_analysisEvent->Cancel();
+}
+
+void WarpSidebarWidget::focus()
+{
+}
+
+void WarpSidebarWidget::Update()
+{
+ m_matchedWidget->Update();
+ if (!GetMatcherTask())
+ setMatcherActionIcon(false);
+}
+
+void WarpSidebarWidget::setMatcherActionIcon(bool running)
+{
+ static auto matcherStopIcon = GetColoredIcon(":/icons/images/stop.png", getThemeColor(RedStandardHighlightColor));
+ static auto matcherStartIcon = GetColoredIcon(":/icons/images/start.png",
+ getThemeColor(GreenStandardHighlightColor));
+ isMatcherRunning = running;
+ if (running)
+ {
+ m_matcherAction->setIcon(matcherStopIcon);
+ m_matcherAction->setToolTip("Stop the matcher");
+ m_matcherAction->setIconText("Stop Matcher");
+ } else
+ {
+ m_matcherAction->setIcon(matcherStartIcon);
+ m_matcherAction->setToolTip("Run the matcher on all functions");
+ m_matcherAction->setIconText("Run Matcher");
+ }
+}
+
+void WarpSidebarWidget::notifyViewChanged(ViewFrame *view)
+{
+ if (!view)
+ return;
+
+ if (view == m_currentFrame)
+ return;
+ m_currentFrame = view;
+ // TODO: We need to set some stuff here prolly.
+}
+
+void WarpSidebarWidget::notifyViewLocationChanged(View *view, const ViewLocation &location)
+{
+ auto function = location.getFunction();
+ // TODO: Only update if the function exists?
+ // NOTE: The function called will exit early if it is the same function.
+ m_currentFunctionWidget->SetCurrentFunction(function);
+}
+
+WarpSidebarWidgetType::WarpSidebarWidgetType() : SidebarWidgetType(QImage(":/icons/images/warp.png"), "WARP")
+{
+}
+
+
+extern "C" {
+BN_DECLARE_UI_ABI_VERSION
+
+BINARYNINJAPLUGIN void CorePluginDependencies()
+{
+ // We must have WARP to enable this plugin!
+ AddRequiredPluginDependency("warp_ninja");
+}
+
+BINARYNINJAPLUGIN bool UIPluginInit()
+{
+ Sidebar::addSidebarWidgetType(new WarpSidebarWidgetType());
+ return true;
+}
+}
diff --git a/plugins/warp/ui/plugin.h b/plugins/warp/ui/plugin.h
new file mode 100644
index 00000000..18525218
--- /dev/null
+++ b/plugins/warp/ui/plugin.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "matched.h"
+#include "matches.h"
+#include "sidebar.h"
+#include "sidebarwidget.h"
+
+class WarpSidebarWidget : public SidebarWidget
+{
+ Q_OBJECT
+ BinaryNinja::Ref<BinaryNinja::Logger> m_logger;
+ BinaryViewRef m_data;
+ ViewFrame *m_currentFrame;
+ QWidget *m_headerWidget;
+
+ BinaryNinja::Ref<BinaryNinja::AnalysisCompletionEvent> m_analysisEvent;
+ QAction *m_matcherAction;
+ bool isMatcherRunning = false;
+
+ WarpCurrentFunctionWidget *m_currentFunctionWidget;
+ WarpMatchedWidget *m_matchedWidget;
+
+public:
+ explicit WarpSidebarWidget(BinaryViewRef data);
+
+ ~WarpSidebarWidget() override;
+
+ QWidget *headerWidget() override { return m_headerWidget; }
+
+ void focus() override;
+
+ void Update();
+
+ void setMatcherActionIcon(bool running);
+
+ void notifyViewChanged(ViewFrame *) override;
+
+ void notifyViewLocationChanged(View *, const ViewLocation &) override;
+};
+
+class WarpSidebarWidgetType : public SidebarWidgetType
+{
+public:
+ WarpSidebarWidgetType();
+
+ SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightContent; }
+ SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; }
+
+ WarpSidebarWidget *createWidget(ViewFrame *viewFrame, BinaryViewRef data) override
+ {
+ return new WarpSidebarWidget(data);
+ }
+};
diff --git a/plugins/warp/ui/shared/constraint.cpp b/plugins/warp/ui/shared/constraint.cpp
new file mode 100644
index 00000000..1d369bcd
--- /dev/null
+++ b/plugins/warp/ui/shared/constraint.cpp
@@ -0,0 +1,118 @@
+#include "constraint.h"
+
+#include <QGridLayout>
+#include <QHeaderView>
+
+WarpConstraintItem::WarpConstraintItem(const Warp::Constraint &constraint) : m_constraint(constraint)
+{
+ QString guidStr = QString::fromStdString(constraint.guid.ToString());
+ if (const auto offset = constraint.offset; offset)
+ guidStr += QString(" @ %1").arg(*offset, 0, 16);
+ setText(guidStr);
+}
+
+WarpConstraintItemModel::WarpConstraintItemModel(const QStringList &labels, QObject *parent)
+{
+ this->setHorizontalHeaderLabels(labels);
+}
+
+void WarpConstraintItemModel::AddItem(WarpConstraintItem *item)
+{
+ QList<QStandardItem *> row = {};
+ row.insert(COL_CONSTRAINT_ITEM, item);
+ appendRow(row);
+}
+
+WarpConstraintItem *WarpConstraintItemModel::GetItem(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return nullptr;
+ return dynamic_cast<WarpConstraintItem *>(item(index.row(), COL_CONSTRAINT_ITEM));
+}
+
+QVariant WarpConstraintItemModel::data(const QModelIndex &index, int role) const
+{
+ // Highlight constraints that are found in analysis.
+ if (role == Qt::BackgroundRole)
+ {
+ if (const auto item = GetItem(index); item)
+ {
+ auto itemConstraint = item->GetConstraint();
+ // TODO: We really should store the guid in a hashmap or something instead of looping over it for every item.
+ // TODO: A less intense green?
+ // TODO: Take into account the constraint offset.
+ for (const auto &constraint: m_matchedConstraints)
+ if (constraint.guid == itemConstraint.guid)
+ return QBrush(Qt::green);
+ }
+ }
+
+ return QStandardItemModel::data(index, role);
+}
+
+WarpConstraintTableWidget::WarpConstraintTableWidget(QWidget *parent)
+{
+ QGridLayout *layout = new QGridLayout(this);
+ layout->setContentsMargins(2, 2, 2, 2);
+ layout->setVerticalSpacing(4);
+
+ m_table = new QTableView(this);
+ m_model = new WarpConstraintItemModel({"Constraint"}, this);
+ m_proxyModel = new GenericTextFilterModel(this);
+ m_proxyModel->setSourceModel(m_model);
+ m_table->setModel(m_proxyModel);
+
+ m_filterEdit = new FilterEdit(this);
+ m_filterView = new FilteredView(this, m_table, this, m_filterEdit);
+ m_filterView->setFilterPlaceholderText("Search constraints");
+
+ layout->addWidget(m_filterEdit, 0, 0, 1, 5);
+ layout->addWidget(m_table, 1, 0, 1, 5);
+
+ // Make the table look nice.
+ m_table->horizontalHeader()->setStretchLastSection(true);
+ m_table->verticalHeader()->hide();
+ m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_table->setFocusPolicy(Qt::NoFocus);
+ m_table->setShowGrid(false);
+ m_table->setAlternatingRowColors(false);
+ m_table->setSortingEnabled(true);
+ // NOTE: We only have a single column right now, so disable the header.
+ m_table->horizontalHeader()->hide();
+ // Decrease row height to make it look nice.
+ m_table->verticalHeader()->setDefaultSectionSize(30);
+}
+
+void WarpConstraintTableWidget::SetConstraints(QVector<WarpConstraintItem *> constraints)
+{
+ // Clear matches as they are no longer valid.
+ m_model->clear();
+ m_model->setRowCount(0);
+
+ // Temporarily disable sorting so we can add rows faster
+ m_table->setModel(m_model);
+ m_table->setSortingEnabled(false);
+ m_table->setEnabled(false);
+
+ for (const auto &constraint: constraints)
+ m_model->AddItem(constraint);
+
+ // We are done, re-enable table.
+ m_table->setEnabled(true);
+ m_table->setModel(m_proxyModel);
+ m_table->setSortingEnabled(true);
+}
+
+void WarpConstraintTableWidget::SetMatchedConstraints(
+ const std::vector<Warp::Constraint> &analysisConstraints)
+{
+ m_model->SetMatchedConstraints(analysisConstraints);
+}
+
+void WarpConstraintTableWidget::setFilter(const std::string &filter)
+{
+ m_proxyModel->setFilterFixedString(QString::fromStdString(filter));
+ m_filterView->showFilter(QString::fromStdString(filter));
+}
diff --git a/plugins/warp/ui/shared/constraint.h b/plugins/warp/ui/shared/constraint.h
new file mode 100644
index 00000000..2f064836
--- /dev/null
+++ b/plugins/warp/ui/shared/constraint.h
@@ -0,0 +1,78 @@
+#pragma once
+#include <qstandarditemmodel.h>
+#include <QTableView>
+#include <QWidget>
+
+#include "filter.h"
+#include "misc.h"
+#include "warp.h"
+
+class WarpConstraintItem : public QStandardItem
+{
+ Warp::Constraint m_constraint;
+
+public:
+ WarpConstraintItem(const Warp::Constraint &constraint);
+
+ Warp::Constraint GetConstraint() { return m_constraint; }
+};
+
+class WarpConstraintItemModel : public QStandardItemModel
+{
+ Q_OBJECT
+
+ // The current analysis constraints used to highlight matching constraints.
+ std::vector<Warp::Constraint> m_matchedConstraints;
+
+public:
+ WarpConstraintItemModel(const QStringList &labels, QObject *parent);
+
+ static constexpr int COL_CONSTRAINT_ITEM = 0;
+
+ void AddItem(WarpConstraintItem *item);
+
+ WarpConstraintItem *GetItem(const QModelIndex &index) const;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void SetMatchedConstraints(const std::vector<Warp::Constraint> &analysisConstraints)
+ {
+ m_matchedConstraints = analysisConstraints;
+ }
+};
+
+class WarpConstraintTableWidget : public QWidget, public FilterTarget
+{
+ Q_OBJECT
+
+ QTableView *m_table;
+ WarpConstraintItemModel *m_model;
+ GenericTextFilterModel *m_proxyModel;
+ FilterEdit *m_filterEdit;
+ FilteredView *m_filterView;
+
+public:
+ explicit WarpConstraintTableWidget(QWidget *parent = nullptr);
+
+ void SetConstraints(QVector<WarpConstraintItem *> constraints);
+
+ void SetMatchedConstraints(const std::vector<Warp::Constraint> &analysisConstraints);
+
+ void setFilter(const std::string &) override;
+
+ void scrollToFirstItem() override
+ {
+ }
+
+ void scrollToCurrentItem() override
+ {
+ }
+
+ void selectFirstItem() override
+ {
+ }
+
+ void activateFirstItem() override
+ {
+ }
+};
diff --git a/plugins/warp/ui/shared/function.cpp b/plugins/warp/ui/shared/function.cpp
new file mode 100644
index 00000000..2202fb4e
--- /dev/null
+++ b/plugins/warp/ui/shared/function.cpp
@@ -0,0 +1,365 @@
+#include "theme.h"
+
+#include "function.h"
+
+#include <QClipboard>
+#include <QGridLayout>
+#include <QHeaderView>
+
+#include "constraint.h"
+#include "misc.h"
+
+WarpFunctionItem::WarpFunctionItem(Warp::Ref<Warp::Function> function,
+ BinaryNinja::Ref<BinaryNinja::Function> analysisFunction)
+{
+ m_function = function;
+
+ // TODO: This needs to be better. Symbol can be nullptr.
+ BinaryNinja::Ref<BinaryNinja::Symbol> symbol = m_function->GetSymbol(*analysisFunction);
+ std::string symbolName = symbol->GetShortName();
+ setText(QString::fromStdString(symbolName));
+ BinaryNinja::InstructionTextToken nameToken = {255, TextToken, symbolName};
+
+ // 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 = {};
+
+ // TODO: Make this not look like garbage
+ BinaryNinja::Ref<BinaryNinja::Type> type = m_function->GetType(*analysisFunction);
+ if (type)
+ {
+ BinaryNinja::Ref<BinaryNinja::Platform> platform = analysisFunction->GetPlatform();
+ std::vector<BinaryNinja::InstructionTextToken> beforeTokens = type->GetTokensBeforeName(platform);
+ std::vector<BinaryNinja::InstructionTextToken> afterTokens = type->GetTokensAfterName(platform);
+
+ for (const auto &token: beforeTokens)
+ tokenData.tokens.emplace_back(token);
+ tokenData.tokens.emplace_back(255, TextToken, " ");
+ tokenData.tokens.emplace_back(nameToken);
+ for (const auto &token: afterTokens)
+ tokenData.tokens.emplace_back(token);
+ } else
+ {
+ tokenData.tokens.emplace_back(nameToken);
+ }
+
+ setData(QVariant::fromValue(tokenData), Qt::UserRole);
+}
+
+void WarpFunctionItem::SetContainer(const Warp::Ref<Warp::Container> &container)
+{
+ m_container = container;
+
+ // Add the container string to data so the filter model picks it up.
+ auto containerName = m_container->GetName();
+ setData(QString::fromStdString(containerName), Qt::UserRole + 2);
+}
+
+void WarpFunctionItem::SetSource(Warp::Source source)
+{
+ m_source = source;
+
+ // Add the source string to data so the filter model picks it up.
+ std::string sourceStr = m_source->ToString();
+ setData(QString::fromStdString(sourceStr), Qt::UserRole + 1);
+}
+
+WarpFunctionItemModel::WarpFunctionItemModel(const QStringList &labels, QObject *parent)
+{
+ this->setHorizontalHeaderLabels(labels);
+}
+
+void WarpFunctionItemModel::AppendFunction(WarpFunctionItem *item)
+{
+ QList<QStandardItem *> row = {};
+ row.insert(COL_FUNCTION_ITEM, item);
+ appendRow(row);
+}
+
+void WarpFunctionItemModel::InsertFunction(uint64_t address, WarpFunctionItem *item)
+{
+ // Update item if already available, this lets us keep the model
+ const auto iter = m_insertableFunctionRows.find(address);
+ if (iter != m_insertableFunctionRows.end())
+ {
+ setItem(iter->second, COL_FUNCTION_ITEM, item);
+ return;
+ }
+
+ AppendFunction(item);
+ m_insertableFunctionRows[address] = rowCount() - 1;
+}
+
+WarpFunctionItem *WarpFunctionItemModel::GetItem(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return nullptr;
+ return dynamic_cast<WarpFunctionItem *>(item(index.row(), COL_FUNCTION_ITEM));
+}
+
+std::optional<uint64_t> WarpFunctionItemModel::GetAddress(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return std::nullopt;
+ // TODO: This is a hack, this means we must enumerate all rows to get the address.
+ for (const auto &[addr, row]: m_insertableFunctionRows)
+ if (row == index.row())
+ return addr;
+ return std::nullopt;
+}
+
+QVariant WarpFunctionItemModel::data(const QModelIndex &index, int role) const
+{
+ if (role == Qt::BackgroundRole)
+ {
+ auto itemFunction = GetItem(index);
+ // Check if we have a valid item and it's the matched function
+ if (m_matchedFunction && itemFunction)
+ {
+ // TODO: Why wont == go to the correct call???
+ if (BNWARPFunctionsEqual(itemFunction->GetFunction()->m_object, m_matchedFunction->m_object))
+ {
+ // TODO: Better color?
+ QColor matchedColor = getThemeColor(BlueStandardHighlightColor);
+ matchedColor.setAlpha(128);
+ return matchedColor;
+ }
+ }
+ }
+
+ if (role == Qt::DisplayRole)
+ {
+ // We really only use this for searching as we have TokenData for our delegate.
+ WarpFunctionItem *item = GetItem(index);
+ if (!item)
+ return QVariant();
+ TokenData tokenData = item->data(Qt::UserRole).value<TokenData>();
+ // Add the function guid so we can filter by that.
+ QString text = tokenData.toString() + " " + QString::fromStdString(item->GetFunction()->GetGUID().ToString());
+ if (auto source = item->GetSource(); source)
+ {
+ // Add the source guid so we can also filter by that.
+ std::string sourceStr = source->ToString();
+ text = text + " " + QString::fromStdString(sourceStr);
+ }
+ return text;
+ }
+
+ return QStandardItemModel::data(index, role);
+}
+
+bool WarpFunctionFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ const QString filterString = filterRegularExpression().pattern();
+ if (filterString.isEmpty())
+ return true;
+
+ // Filter on the first column only, this contains our actual function.
+ auto index = sourceModel()->index(sourceRow, 0, sourceParent);
+ auto data = QRegularExpression::escape(index.data().toString());
+ if (data.contains(filterString, Qt::CaseInsensitive))
+ return true;
+ return false;
+}
+
+bool WarpFunctionFilterModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
+{
+ // TODO: When we make the stuff _actually_ sortable.
+ return sourceLeft.row() < sourceRight.row();
+}
+
+WarpFunctionTableWidget::WarpFunctionTableWidget(QWidget *parent) : QWidget(parent)
+{
+ QGridLayout *layout = new QGridLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(2);
+
+ m_table = new QTableView(this);
+ m_model = new WarpFunctionItemModel({"Function"}, this);
+ m_proxyModel = new WarpFunctionFilterModel(this);
+ m_proxyModel->setSourceModel(m_model);
+ m_table->setModel(m_proxyModel);
+
+ m_filterEdit = new FilterEdit(this);
+ m_filterView = new FilteredView(this, m_table, this, m_filterEdit);
+ m_filterView->setFilterPlaceholderText("Search functions (By GUID, name or source)");
+
+ layout->addWidget(m_filterEdit, 0, 0, 1, 5);
+ layout->addWidget(m_table, 1, 0, 1, 5);
+
+ // Make the table look nice.
+ m_table->horizontalHeader()->setStretchLastSection(true);
+ m_table->verticalHeader()->hide();
+ m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_table->setFocusPolicy(Qt::NoFocus);
+ m_table->setShowGrid(false);
+ m_table->setAlternatingRowColors(false);
+ m_table->setSortingEnabled(true);
+ // NOTE: We only have a single column right now, so disable header.
+ m_table->horizontalHeader()->hide();
+ // Decrease row height to make it look nice.
+ m_table->verticalHeader()->setDefaultSectionSize(30);
+
+ TokenDataDelegate *tokenDelegate = new TokenDataDelegate(this);
+ // NOTE: Column 0 is assumed to be the function with the token data.
+ m_table->setItemDelegateForColumn(0, tokenDelegate);
+
+ AddressColorDelegate *addressDelegate = new AddressColorDelegate(this);
+ // NOTE: Column 1 is assumed to be the function address.
+ m_table->setItemDelegateForColumn(1, addressDelegate);
+
+ // Add a dynamic context menu to the table.
+ // NOTE: This is a bit stupid, I am sure there is a better way to do this in QT.
+ m_contextMenu = new QMenu(this);
+ RegisterContextMenuAction("Copy Name", [](WarpFunctionItem *item, std::optional<uint64_t>) {
+ QClipboard *clipboard = QGuiApplication::clipboard();
+ clipboard->setText(item->text());
+ });
+ RegisterContextMenuAction("Copy GUID", [](WarpFunctionItem *item, std::optional<uint64_t>) {
+ QClipboard *clipboard = QGuiApplication::clipboard();
+ Warp::Ref<Warp::Function> function = item->GetFunction();
+ std::string guidStr = function->GetGUID().ToString();
+ clipboard->setText(QString::fromStdString(guidStr));
+ });
+
+ m_table->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(m_table, &QTableView::customContextMenuRequested, this, [&](QPoint pos) {
+ const QModelIndex index = m_table->indexAt(pos);
+ if (!index.isValid())
+ return;
+ const QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
+ WarpFunctionItem *item = m_model->GetItem(sourceIndex);
+ if (!item || !item->GetFunction())
+ return;
+
+ // Execute the menu and get the selected action
+ const QAction *selectedAction = m_contextMenu->exec(m_table->viewport()->mapToGlobal(pos));
+ if (!selectedAction)
+ return;
+
+ const auto name = selectedAction->text();
+ const auto iter = m_contextMenuActions.find(name);
+ if (iter != m_contextMenuActions.end())
+ iter->second(item, m_model->GetAddress(sourceIndex));
+ });
+}
+
+void WarpFunctionTableWidget::RegisterContextMenuAction(const QString &name,
+ const std::function<void(
+ WarpFunctionItem *, std::optional<uint64_t>)> &callback)
+{
+ m_contextMenu->addAction(name);
+ m_contextMenuActions[name] = callback;
+}
+
+void WarpFunctionTableWidget::SetFunctions(QVector<WarpFunctionItem *> functions)
+{
+ // Clear matches as they are no longer valid.
+ m_model->clear();
+ m_model->setRowCount(0);
+
+ // Temporarily disable sorting so we can add rows faster
+ m_table->setModel(m_model);
+ m_table->setSortingEnabled(false);
+ m_table->setEnabled(false);
+
+ for (const auto &function: functions)
+ m_model->AppendFunction(function);
+
+ // We are done, re-enable table.
+ m_table->setEnabled(true);
+ m_table->setModel(m_proxyModel);
+ m_table->setSortingEnabled(true);
+
+ // Update the filter text with the new count of functions.
+ m_filterView->setFilterPlaceholderText(QString("Search %1 functions").arg(m_model->rowCount()));
+}
+
+void WarpFunctionTableWidget::InsertFunction(uint64_t address, WarpFunctionItem *function)
+{
+ m_model->InsertFunction(address, function);
+}
+
+void WarpFunctionTableWidget::setFilter(const std::string &filter)
+{
+ m_proxyModel->setFilterFixedString(QString::fromStdString(filter));
+ m_filterView->showFilter(QString::fromStdString(filter));
+}
+
+WarpFunctionInfoWidget::WarpFunctionInfoWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ // Create a tab widget
+ QTabWidget *tabWidget = new QTabWidget(this);
+ tabWidget->setContentsMargins(0, 0, 0, 0);
+
+ // Create tables for the "Constraints", "Comments", and "Variables" tabs
+ m_commentsTable = new QTableView(this);
+ // m_variablesTable = new QTableView(this);
+
+ // TODO: On click navigate to where the constraint is located.
+ m_constraintsTable = new WarpConstraintTableWidget(this);
+ tabWidget->addTab(m_constraintsTable, "Constraints");
+
+ // Set up comments tab
+ m_commentsModel = new QStandardItemModel(this);
+ m_commentsModel->setHorizontalHeaderLabels({"Offset", "Text"});
+ m_commentsModel->setColumnCount(2);
+ m_commentsTable->setModel(m_commentsModel);
+ m_commentsTable->horizontalHeader()->setStretchLastSection(true);
+ m_commentsTable->horizontalHeader()->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_commentsTable->horizontalHeader()->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_commentsTable->horizontalHeader()->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_commentsTable->verticalHeader()->hide();
+ m_commentsTable->horizontalHeader()->hide();
+ tabWidget->addTab(m_commentsTable, "Comments");
+
+ // Set up variables tab
+ // m_variablesTable->setModel(new QStandardItemModel(this));
+ // TODO: Add variables to data.
+ // tabWidget->addTab(m_variablesTable, "Variables");
+
+ // Add the tab widget to this widget's layout
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(0);
+ layout->addWidget(tabWidget);
+
+ setLayout(layout);
+}
+
+void WarpFunctionInfoWidget::UpdateInfo()
+{
+ m_commentsModel->clear();
+ m_commentsModel->setRowCount(0);
+ m_constraintsTable->SetMatchedConstraints({});
+ m_constraintsTable->SetConstraints({});
+
+ Warp::Ref<Warp::Function> function = GetFunction();
+ if (!function)
+ return;
+
+ // Set the analysis constraints if there is an analysis function.
+ if (const auto analysisFunction = GetAnalysisFunction())
+ {
+ const auto analysisConstraints = Warp::Function::Get(*analysisFunction)->GetConstraints();
+ m_constraintsTable->SetMatchedConstraints(analysisConstraints);
+ }
+
+ // Add all the constraints for the current function to the model.
+ QVector<WarpConstraintItem *> constraints;
+ for (const auto &constraint: function->GetConstraints())
+ constraints.push_back(new WarpConstraintItem(constraint));
+ m_constraintsTable->SetConstraints(constraints);
+
+ // Add all the comments to the model.
+ for (const auto &comment: function->GetComments())
+ {
+ m_commentsModel->appendRow({
+ new QStandardItem(QString("0x%1").arg(comment.offset, 0, 16)),
+ new QStandardItem(QString::fromStdString(comment.text))
+ });
+ }
+}
diff --git a/plugins/warp/ui/shared/function.h b/plugins/warp/ui/shared/function.h
new file mode 100644
index 00000000..90d52092
--- /dev/null
+++ b/plugins/warp/ui/shared/function.h
@@ -0,0 +1,168 @@
+#pragma once
+#include <QStandardItemModel>
+#include <QTableView>
+
+#include "binaryninjaapi.h"
+#include "constraint.h"
+#include "filter.h"
+#include "misc.h"
+#include "warp.h"
+
+class WarpFunctionItem : public QStandardItem
+{
+ Warp::Ref<Warp::Function> m_function;
+
+ // Optional attached data used to show/manage the function.
+ Warp::Ref<Warp::Container> m_container;
+ std::optional<Warp::Source> m_source;
+
+public:
+ WarpFunctionItem(Warp::Ref<Warp::Function> function,
+ BinaryNinja::Ref<BinaryNinja::Function> analysisFunction);
+
+ void SetContainer(const Warp::Ref<Warp::Container> &container);
+
+ void SetSource(Warp::Source source);
+
+ Warp::Ref<Warp::Function> GetFunction() { return m_function; }
+ Warp::Ref<Warp::Container> GetContainer() { return m_container; }
+ std::optional<Warp::Source> GetSource() { return m_source; }
+};
+
+class WarpFunctionItemModel : public QStandardItemModel
+{
+ Q_OBJECT
+
+ // The current matched function, used to highlight currently.
+ Warp::Ref<Warp::Function> m_matchedFunction;
+
+ // Mapping of function start address to the row index.
+ // This is used to identify unique functions for updating instead of resetting the entire model.
+ std::unordered_map<uint64_t, int> m_insertableFunctionRows;
+
+public:
+ WarpFunctionItemModel(const QStringList &labels, QObject *parent);
+
+ static constexpr int COL_FUNCTION_ITEM = 0;
+ static constexpr int COL_ADDRESS_ITEM = 1;
+
+ void AppendFunction(WarpFunctionItem *item);
+
+ void InsertFunction(uint64_t address, WarpFunctionItem *item);
+
+ WarpFunctionItem *GetItem(const QModelIndex &index) const;
+
+ std::optional<uint64_t> GetAddress(const QModelIndex &index) const;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void SetMatchedFunction(const Warp::Ref<Warp::Function> &matchedFunction)
+ {
+ Warp::Ref<Warp::Function> previousMatchedFunction = m_matchedFunction;
+ m_matchedFunction = matchedFunction;
+
+ // Make sure to refresh the highlights so we don't keep the highlights from the previous function.
+ if (previousMatchedFunction)
+ {
+ const QModelIndex topLeft = index(0, 0);
+ const QModelIndex bottomRight = index(rowCount() - 1, 0);
+ emit dataChanged(topLeft, bottomRight);
+ }
+ }
+};
+
+class WarpFunctionFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ WarpFunctionFilterModel(QObject *parent): QSortFilterProxyModel(parent)
+ {
+ }
+
+ ~WarpFunctionFilterModel() override = default;
+
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+
+ bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
+};
+
+class WarpFunctionTableWidget : public QWidget, public FilterTarget
+{
+ Q_OBJECT
+
+ QTableView *m_table;
+ WarpFunctionItemModel *m_model;
+ WarpFunctionFilterModel *m_proxyModel;
+ FilterEdit *m_filterEdit;
+ FilteredView *m_filterView;
+ QMenu *m_contextMenu;
+ std::map<QString, std::function<void(WarpFunctionItem *, std::optional<uint64_t>)> > m_contextMenuActions;
+
+public:
+ explicit WarpFunctionTableWidget(QWidget *parent = nullptr);
+
+ // TODO: Invert this and provide OnCallback functions that wrap the connect call.
+ QTableView *GetTableView() const { return m_table; }
+ WarpFunctionItemModel *GetModel() const { return m_model; }
+ WarpFunctionFilterModel *GetProxyModel() const { return m_proxyModel; }
+
+ void RegisterContextMenuAction(const QString &name,
+ const std::function<void(WarpFunctionItem *, std::optional<uint64_t>)> &callback);
+
+ void SetFunctions(QVector<WarpFunctionItem *> functions);
+
+ void InsertFunction(uint64_t address, WarpFunctionItem *function);
+
+ void setFilter(const std::string &) override;
+
+ void scrollToFirstItem() override
+ {
+ }
+
+ void scrollToCurrentItem() override
+ {
+ }
+
+ void selectFirstItem() override
+ {
+ }
+
+ void activateFirstItem() override
+ {
+ }
+};
+
+class WarpFunctionInfoWidget : public QWidget
+{
+ Q_OBJECT
+
+ Warp::Ref<Warp::Function> m_function;
+ BinaryNinja::Ref<BinaryNinja::Function> m_analysisFunction;
+
+ // Optionally provide this information to show the source information.
+ Warp::Ref<Warp::Container> m_container;
+ std::string source;
+
+ WarpConstraintTableWidget *m_constraintsTable;
+
+ QTableView *m_commentsTable;
+ QStandardItemModel *m_commentsModel;
+
+ QTableView *m_variablesTable;
+
+public:
+ explicit WarpFunctionInfoWidget(QWidget *parent = nullptr);
+
+ Warp::Ref<Warp::Function> GetFunction() { return m_function; }
+ void SetFunction(Warp::Ref<Warp::Function> function) { m_function = function; };
+
+ void SetAnalysisFunction(BinaryNinja::Ref<BinaryNinja::Function> analysisFunction)
+ {
+ m_analysisFunction = analysisFunction;
+ };
+ BinaryNinja::Ref<BinaryNinja::Function> GetAnalysisFunction() { return m_analysisFunction; }
+
+ // TODO: Make this private?
+ void UpdateInfo();
+};
diff --git a/plugins/warp/ui/shared/misc.cpp b/plugins/warp/ui/shared/misc.cpp
new file mode 100644
index 00000000..71147079
--- /dev/null
+++ b/plugins/warp/ui/shared/misc.cpp
@@ -0,0 +1,81 @@
+#include "misc.h"
+
+#include <QGridLayout>
+#include <QHeaderView>
+
+#include "action.h"
+#include "fontsettings.h"
+#include "render.h"
+#include "theme.h"
+
+void TokenDataDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ painter->save();
+
+ auto tokenData = index.data(Qt::UserRole).value<TokenData>();
+
+ // Draw either the selected row or background color.
+ QVariant background = index.data(Qt::BackgroundRole);
+ if (background.canConvert<QBrush>())
+ painter->fillRect(option.rect, background.value<QBrush>());
+ else if (option.state & QStyle::State_Selected)
+ painter->fillRect(option.rect, option.palette.highlight());
+ painter->translate(option.rect.topLeft());
+
+
+ auto renderContext = RenderContext((QWidget *) option.widget);
+ renderContext.init(*painter);
+ HighlightTokenState highlightState;
+ renderContext.drawDisassemblyLine(*painter, 5, 5, {tokenData.tokens.begin(), tokenData.tokens.end()},
+ highlightState);
+
+ painter->restore();
+}
+
+QSize TokenDataDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ auto tokenData = index.data(Qt::UserRole).value<TokenData>();
+ auto renderContext = RenderContext((QWidget *) option.widget);
+ QFontMetrics fontMetrics = QFontMetrics(renderContext.getFont());
+ QString line = "";
+ for (const auto &token: tokenData.tokens)
+ line += token.text;
+ int width = qMax(0, fontMetrics.horizontalAdvance(line));
+ return QSize(width, renderContext.getFontHeight());
+}
+
+void AddressColorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+
+ opt.font = getMonospaceFont(qobject_cast<QWidget *>(parent()));
+ opt.palette.setColor(QPalette::Text, getThemeColor(BNThemeColor::AddressColor));
+ opt.displayAlignment = Qt::AlignCenter | Qt::AlignVCenter;
+
+ QStyledItemDelegate::paint(painter, opt, index);
+}
+
+bool GenericTextFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ auto filterString = filterRegularExpression().pattern();
+ if (filterString.isEmpty())
+ return true;
+
+ for (int i = 0; i < sourceModel()->columnCount(); i++)
+ {
+ auto index = sourceModel()->index(sourceRow, i, sourceParent);
+ auto data = QRegularExpression::escape(index.data().toString());
+ if (data.contains(filterString, Qt::CaseInsensitive))
+ return true;
+ }
+
+ return false;
+}
+
+bool GenericTextFilterModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
+{
+ auto leftData = sourceLeft.data().toString();
+ auto rightData = sourceRight.data().toString();
+ return QString::localeAwareCompare(leftData, rightData) < 0;
+}
diff --git a/plugins/warp/ui/shared/misc.h b/plugins/warp/ui/shared/misc.h
new file mode 100644
index 00000000..d2e6a1a8
--- /dev/null
+++ b/plugins/warp/ui/shared/misc.h
@@ -0,0 +1,84 @@
+#pragma once
+#include <qmetatype.h>
+#include <QSortFilterProxyModel>
+#include <qstandarditemmodel.h>
+#include <QStyledItemDelegate>
+#include <QTableView>
+#include <QVector>
+
+#include "binaryninjaapi.h"
+#include "filter.h"
+
+// Used to serialize into the item data for rendering with TokenDataDelegate.
+struct TokenData
+{
+ QVector<BinaryNinja::InstructionTextToken> tokens{};
+
+ TokenData() = default;
+
+ TokenData(const std::vector<BinaryNinja::InstructionTextToken> &tokens)
+ {
+ for (const auto &token: tokens)
+ this->tokens.push_back(token);
+ }
+
+ TokenData(const BinaryNinja::InstructionTextToken &token)
+ {
+ this->tokens.push_back(token);
+ }
+
+ QString toString() const
+ {
+ QStringList tokenStrings;
+ for (const auto &token: tokens)
+ {
+ tokenStrings.append(QString::fromStdString(token.text));
+ }
+ return tokenStrings.join("");
+ }
+};
+
+Q_DECLARE_METATYPE(TokenData)
+
+class TokenDataDelegate final : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit TokenDataDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent)
+ {
+ }
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+};
+
+class AddressColorDelegate final : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit AddressColorDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent)
+ {
+ }
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+};
+
+
+class GenericTextFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ GenericTextFilterModel(QObject *parent): QSortFilterProxyModel(parent)
+ {
+ }
+
+ ~GenericTextFilterModel() override = default;
+
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+
+ bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
+};