diff options
| author | Mason Reed <mason@vector35.com> | 2025-08-26 23:59:38 -0400 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2025-10-01 21:38:39 -0400 |
| commit | ede39aee7e00c40a43b67ca18dd8ab80ee863d85 (patch) | |
| tree | 67c5eda347ece2282e3c888f38066b496e06dec8 /plugins/warp/ui/shared/search.cpp | |
| parent | a1c46813e7f279aa4cfdb9dbb91c45b559ebeacd (diff) | |
[WARP] Enhanced network support
Diffstat (limited to 'plugins/warp/ui/shared/search.cpp')
| -rw-r--r-- | plugins/warp/ui/shared/search.cpp | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/plugins/warp/ui/shared/search.cpp b/plugins/warp/ui/shared/search.cpp new file mode 100644 index 00000000..1431e9b6 --- /dev/null +++ b/plugins/warp/ui/shared/search.cpp @@ -0,0 +1,301 @@ +#include "search.h" +#include "misc.h" +#include "../../../../../ui/mainwindow.h" + +QVariant WarpSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size()) + return {}; + const auto &it = m_items[index.row()]; + + // Provide tokenized type for functions via UserRole for TokenDataDelegate + 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())); + return {}; + } + + + if (role == Qt::DisplayRole) + { + // TODO: We might want to run the demangler here on the name. + switch (index.column()) + { + case DisplayCol: return QString::fromStdString(it->GetName()); + case KindCol: + { + switch (it->GetKind()) + { + case WARPContainerSearchItemKindFunction: return "Function"; + case WARPContainerSearchItemKindType: return "Type"; + case WARPContainerSearchItemKindSource: return "Source"; + case WARPContainerSearchItemKindSymbol: return "Symbol"; + default: return {}; + } + } + case SourceCol: return QString::fromStdString(it->GetSource().ToString()); + default: return {}; + } + } + return {}; +} + +QVariant WarpSearchModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case DisplayCol: return "Item"; + case KindCol: return "Kind"; + case SourceCol: return "Source"; + default: return {}; + } + } + return {}; +} + +void WarpSearchDelegate::paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // TODO: We actually still want the function icon, I think? So this should not early return and instead replace the name. + // If model provided TokenData (e.g., for function types), render via TokenDataDelegate + if (index.data(Qt::UserRole).canConvert<TokenData>()) + { + TokenDataDelegate(parent()).paint(p, option, index); + return; + } + + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + // We will draw the background/selection via style, and custom-draw icon (left) + text. + const QWidget *w = option.widget; + QStyle *style = w ? w->style() : QApplication::style(); + + // Let style draw the item background/selection etc., but not the default text + QString originalText = opt.text; + opt.text.clear(); + style->drawControl(QStyle::CE_ItemViewItem, &opt, p, w); + + // Retrieve text for DisplayCol + const QString &text = originalText; + + // Retrieve kind text from the hidden KindCol + const QModelIndex kindIndex = index.sibling(index.row(), WarpSearchModel::KindCol); + const QString kind = kindIndex.isValid() ? kindIndex.data(Qt::DisplayRole).toString() : QString(); + + // Map kind -> icon (emoji for simplicity; replace with QIcon/QPixmap if desired) + const QString icon = kindToIcon(kind); + + // Layout rects + const QRect r = opt.rect; + const int marginH = 8; + const int iconSide = 16; + const int spacing = 8; + + // Reserve space on the left for the icon if we have one + QRect iconRect = QRect(r.left() + marginH, r.center().y() - iconSide / 2, iconSide, iconSide); + QRect textRect = r.adjusted((icon.isEmpty() ? marginH : (marginH + iconSide + spacing)), 0, -marginH, 0); + + // Draw icon first (without clipping), then text with clipping + p->save(); + if (!icon.isEmpty()) + { + QFont iconFont = opt.font; + iconFont.setPointSizeF(iconFont.pointSizeF() + 2); + p->setFont(iconFont); + p->setPen( + opt.palette.color(opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); + p->drawText(iconRect, Qt::AlignCenter, icon); + } + p->restore(); + + // Draw elided text, vertically centered (with clipping so long strings don't overflow) + p->save(); + p->setClipRect(textRect); + const QFontMetrics fm(opt.font); + const QString elided = fm.elidedText(text, Qt::ElideRight, textRect.width()); + p->setPen(opt.palette.color(opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); + p->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, elided); + p->restore(); +} + +void WarpSearchRunner::fetchPage(std::size_t offset, std::size_t limit) +{ + const auto session = currentSession(); + auto container = m_container; + auto q = m_queryText; + auto src = m_source; + + // Runs search on Qt threadpool then moves the result to the main thread to update the UI. + using TaskResult = std::pair<std::optional<Warp::ContainerSearchResponse>, QString>; + QFutureWatcher<TaskResult> *watcher = new QFutureWatcher<TaskResult>(this); + const auto future = QtConcurrent::run([container, q, src, offset, limit]() -> TaskResult { + const Warp::ContainerSearchQuery query(q.toStdString(), offset, limit, src); + auto respOpt = container->Search(query); + // TODO: We may want to provide better errors in the future, e.g. BNWARPGetError + if (!respOpt.has_value()) + return {std::nullopt, QStringLiteral("No response from container")}; + return {std::move(respOpt), QString()}; + }); + connect(watcher, &QFutureWatcher<TaskResult>::finished, this, [this, watcher, session]() { + const TaskResult result = watcher->result(); + watcher->deleteLater(); + if (const auto &err = result.second; !err.isEmpty()) + emit pageError(session, err); + else if (const auto &resp = result.first; resp.has_value()) + emit pageReady(session, *resp); + }); + watcher->setFuture(future); +} + +WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget *parent) : QWidget(parent) +{ + m_model = new WarpSearchModel(this); + m_runner = new WarpSearchRunner(container, this); + auto *layout = new QVBoxLayout(this); + m_query = new QLineEdit(this); + m_status = new QLabel(this); + m_view = new QTableView(this); + + // TODO: I am not necessarily a fan with how this looks. + m_query->setPlaceholderText("Search… (Optionally filter with source:<uuid>)"); + + // This is where we make the table not look like a table but instead a list. + m_view->setShowGrid(false); + m_view->verticalHeader()->setVisible(false); + m_view->horizontalHeader()->setVisible(false); + m_view->setSelectionBehavior(QAbstractItemView::SelectRows); + m_view->setSelectionMode(QAbstractItemView::SingleSelection); + m_view->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_view->setFocusPolicy(Qt::NoFocus); + + layout->addWidget(m_query); + layout->addWidget(m_status); + layout->addWidget(m_view); + + m_view->setModel(m_model); + + // Use a delegate to draw the DisplayCol text with an icon (optionally) aligned to the left. + m_view->setItemDelegateForColumn(WarpSearchModel::DisplayCol, new WarpSearchDelegate(m_view)); + + // Hide extra columns: only show the combined "Item" column; do not display source GUID. + m_view->setColumnHidden(WarpSearchModel::KindCol, true); + m_view->setColumnHidden(WarpSearchModel::SourceCol, true); + m_view->horizontalHeader()->setSectionResizeMode(WarpSearchModel::DisplayCol, QHeaderView::Stretch); + + // Debounce user input + m_debounce.setSingleShot(true); + m_debounce.setInterval(SEARCH_DEBOUNCE_MS); + connect(m_query, &QLineEdit::textChanged, this, [this](const QString &) { m_debounce.start(); }); + + connect(&m_debounce, &QTimer::timeout, this, [this]() { + const QString raw = m_query->text(); + + // Parse generic qualifiers and clean the free-text query + const auto parsed = ParsedQuery(raw); + + std::optional<Warp::Source> inlineSource = std::nullopt; + if (const auto val = parsed.GetValue("source"); val.has_value()) + inlineSource = Warp::Source::FromString(val.value().toStdString()); + const auto effectiveSource = inlineSource.has_value() ? inlineSource : m_source; + // TODO: Filter for tags and function guid. + + m_runner->startSession(parsed.query, effectiveSource); + m_model->beginNewSearch(SEARCH_PAGE_SIZE); + }); + + + // Infinite scroll trigger + connect(m_model, &WarpSearchModel::fetchMoreRequested, this, [this](std::size_t offset, std::size_t limit) { + m_status->setText(QStringLiteral("Loading… (%1/%2)").arg(m_model->currentCount()).arg(m_model->total())); + m_runner->fetchPage(offset, limit); + }); + + // Append results if session still valid + connect(m_runner, &WarpSearchRunner::pageReady, this, + [this](std::uint64_t, const Warp::ContainerSearchResponse &resp) { + m_model->appendResponse(resp); + m_status->setText( + QStringLiteral("Showing %1 of %2").arg(m_model->currentCount()).arg(m_model->total())); + }); + + // In cases like if the network container server goes down. + connect(m_runner, &WarpSearchRunner::pageError, this, + [this](std::uint64_t, const QString &msg) { + m_status->setText(QStringLiteral("Error: %1").arg(msg)); + }); + + // Add a context menu so that we can actually do actionable things with what we find. + m_view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_view, &QTableView::customContextMenuRequested, this, [this](const QPoint &pos) { + const QModelIndex idx = m_view->indexAt(pos); + if (!idx.isValid()) + return; + + // TODO: Getting the current view here is really awful, but i dont care right now. + auto ctx = MainWindow::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 itemFunc = item->GetFunction(); + + QMenu menu(this); + QAction *copySourceId = menu.addAction(tr("Copy Source")); + QAction *searchSource = menu.addAction(tr("Search Source")); + QAction *applyType = menu.addAction(tr("Apply Type")); + QAction *applyFunction = menu.addAction(tr("Apply to Current Function")); + + // 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->setVisible(applyType->isEnabled()); + + applyFunction->setEnabled(func != nullptr && itemFunc); + applyFunction->setVisible(applyFunction->isEnabled()); + + QAction *chosen = menu.exec(m_view->viewport()->mapToGlobal(pos)); + if (!chosen) + return; + if (chosen == copySourceId) + QApplication::clipboard()->setText(QString::fromStdString(item->GetSource().ToString())); + else if (chosen == searchSource) + { + const QString sourceId = QString::fromStdString(item->GetSource().ToString()); + // TODO: This does not preserve the query that existed prior, doing so may result in duplicate source qualifiers. + m_query->setText(QStringLiteral("source:\"%1\"").arg(sourceId)); + } else if (chosen == applyType) + { + if (func && item->GetKind() == WARPContainerSearchItemKindFunction) + { + func->SetUserType(itemType); + binaryView->UpdateAnalysis(); + } + else + binaryView->DefineUserType(item->GetName(), itemType); + } else if (chosen == applyFunction) + { + itemFunc->Apply(*func); + binaryView->UpdateAnalysis(); + } + }); + + // This will start searching with an empty query once the widget is constructed. This is a decent behavior considering + // we should be heavily caching queries such as an empty one. + m_debounce.start(); +} |
