#pragma once #include #include /*! \defgroup metadatachoicedialog MetadataChoiceDialog \ingroup uiapi MetadataChoiceDialog aims to provide a fairly extensible dialog for displaying selection info, without requiring reimplementation or extra UI code whenever complex behavior is required. It can optionally display metadata to the right, a 2nd column with extra information about entries, and can have a checkbox-toggleable second filter (the first one being a regular text filter bar). These are all described in the class documentation below. */ /*! \ingroup metadatachoicedialog */ struct EntryItem { size_t idx; QString entryText; }; /*! \ingroup metadatachoicedialog */ class BINARYNINJAUIAPI ManagedTableDelegate { public: virtual ~ManagedTableDelegate(); // model virtual size_t ManagedTableColumnCount() = 0; virtual size_t ManagedTableRowCount() = 0; virtual QString ManagedTableColumnName(size_t) = 0; virtual QString ManagedTableDisplayText(size_t row, size_t col) = 0; // view virtual std::vector>> GetTableContextMenuItems() = 0; virtual void ExecuteContextMenuItem(size_t menuItemIndex, size_t idx) = 0; virtual std::vector GetAllItems() const = 0; virtual bool EntryItemPassesExtraFilters(EntryItem& item) = 0; }; // Model that is blind and queries info from its delegate. class BINARYNINJAUIAPI ManagedTableModel : public QAbstractTableModel { ManagedTableDelegate* m_delegate; QVariant headerData(int, Qt::Orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override { return m_delegate->ManagedTableRowCount(); }; int columnCount(const QModelIndex& parent = QModelIndex()) const override { return m_delegate->ManagedTableColumnCount(); }; public: void resetModel(); QModelIndex index(int row, int column, const QModelIndex& parentIndex) const override; void SetDelegate(ManagedTableDelegate* delegate) { m_delegate = delegate; }; }; class BINARYNINJAUIAPI ManagedTableView : public QTableView, public FilterTarget { Q_OBJECT ManagedTableModel* m_model; std::string m_filter; FilterOptions m_filterOptions; ManagedTableDelegate* m_delegate; QWidget* m_tabChainPrefixWidget; QWidget* m_tabChainSuffixWidget; public: ManagedTableView(QWidget* parent, const QStringList& entries); int getCurrentSelection(); void forceFilterUpdate() { setFilter(m_filter, m_filterOptions); }; void resetModel(); void setFocusPrefixWidget(QWidget* wdgt) { m_tabChainPrefixWidget = wdgt; }; void setFocusSuffixWidget(QWidget* wdgt) { m_tabChainSuffixWidget = wdgt; }; virtual void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; virtual void setFilter(const std::string& filter, FilterOptions options) override; virtual void scrollToFirstItem() override; virtual void scrollToCurrentItem() override; virtual void ensureSelection() override; virtual void activateSelection() override; virtual void focusInEvent(QFocusEvent *event) override; bool focusNextPrevChild(bool next) override; void contextMenuEvent(QContextMenuEvent *event) override; void SetDelegate(ManagedTableDelegate*); signals: void selectionModified(size_t idx); }; constexpr size_t maximumMetadataTextLineCount = 506; // Enough for 500 entries and 6 lines for any truncation indications. enum MetadataMode { PlaintextMetadataMode, DisassemblyTextLineMetadataMode }; class BINARYNINJAUIAPI EntryItemMetadataViewDelegate { public: virtual ~EntryItemMetadataViewDelegate(); virtual MetadataMode GetCurrentMode() = 0; virtual std::vector LinesForEntryItem(EntryItem& item) = 0; virtual QString PlaintextForEntryItem(EntryItem& item) = 0; }; /*! \ingroup metadatachoicedialog */ class BINARYNINJAUIAPI EntryItemMetadataView : public QTextEdit { EntryItemMetadataViewDelegate* m_delegate; protected: bool m_currentItemIsTruncated; public: explicit EntryItemMetadataView(QWidget* parent); void SetDelegate(EntryItemMetadataViewDelegate* delegate) { m_delegate = delegate; }; virtual void DisplayPlainText(EntryItem& item); virtual void DisplayTextLines(EntryItem& item); }; /*! MetadataChoiceDialog is a dynamic UI View and Controller that allows the user to make a selection given a list of entries, and metadata about those entries. The Model's data is left up to the developer using the dialog to manage. A developer can implement a second checkbox-toggled filter, custom metadata shown in a box to the right of the table, and/or a custom second column with additional info about entries. Content from all of these callbacks is cached, however these caches can be evicted if data has changed or been added. This dialog can to scale to very large amounts of arbitrary info by lazily loading it from provided callbacks, without requiring bespoke dialogs being built for those applications. \b Example: \code{.cpp} // In this example, we display a list of all defined types to the user and allow them to pick one. QStringList entries; std::vector entryTypes; for (auto type : bv->GetTypes()) { entries.push_back(QString::fromStdString(type.first.GetString())); entryTypes.push_back(type.second); } auto choiceDialog = new MetadataChoiceDialog(parent, "Pick Type", entries); // For our metadata, we'll just print the type definition. You could also have this callback return just a QString, // but we want the type tokens to have syntax highlighting here. std::function(EntryItem item)> getMetadata = [&](EntryItem item) { TypeRef type = entryTypes.at(item.idx); std::vector metadata; for (auto line : type->GetLines(data, entries.at(item.idx).toStdString())) { DisassemblyTextLine l; l.tokens = line.tokens; metadata.push_back(l); } return metadata; }; choiceDialog->SetMetadataCallback(getMetadata); // For our checkbox filter, we'll allow the user to filter out non-enum types. std::function isEntryEnum = [&](EntryItem item) { TypeRef type = entryTypes.at(item.idx); return (type.second->IsEnumeration() || type.second->IsEnumReference()); }; choiceDialog->SetExtraFilter("Hide non-enums", isEntryEnum); // For our second column, we'll show the width of the type. std::function secondColumnText = [&](EntryItem item) { TypeRef type = entryTypes.at(item.idx); return "0x" + QString::number(type->GetWidth(), 16); }; choiceDialog->SetSecondColumnTextCallback("Type Width", secondColumnText); choiceDialog->exec(); if (choiceDialog->GetChosenEntry().has_value()) return entryTypes.at(choiceDialog->GetChosenEntry().value().idx); else return nullptr; \endcode AddContextButton and AddContextMenuItem can also be used for more complex UI behavior if required, and examples are provided for using those in their respective documentation. There are also several methods for adding or editing entries, evicting caches, and so on, if you need to modify the dialog's contents on the fly via a button or context menu item. \ingroup metadatachoicedialog */ class BINARYNINJAUIAPI MetadataChoiceDialog : public QDialog, public ManagedTableDelegate, public EntryItemMetadataViewDelegate { Q_OBJECT protected: std::vector m_entries; QHBoxLayout* m_midRowLayout; // ManagedTableDelegate virtual size_t ManagedTableColumnCount() override; virtual size_t ManagedTableRowCount() override; virtual QString ManagedTableColumnName(size_t) override; virtual QString ManagedTableDisplayText(size_t row, size_t col) override; virtual std::vector>> GetTableContextMenuItems() override { return m_contextMenuItems; }; virtual void ExecuteContextMenuItem(size_t menuItemIndex, size_t idx) override; virtual bool EntryItemPassesExtraFilters(EntryItem& item) override; virtual std::vector GetAllItems() const override { return m_entries; }; struct ExtraFilterState { bool exists = false; bool enabled = false; bool cacheEnabled = true; QString title; std::unordered_map cache; std::function callback; }; struct MetadataState { bool exists = false; bool cacheEnabled = true; MetadataMode mode; std::unordered_map preloadedPlaintext; std::unordered_map> preloadedTextLines; std::unordered_map plaintextCache; std::unordered_map> disassemblyTextLineCache; std::function plaintextCallback; std::function(EntryItem item)> disassemblyTextLineCallback; }; // EntryItemMetadataViewDelegate virtual MetadataMode GetCurrentMode() override { return m_metadata.mode; }; virtual std::vector LinesForEntryItem(EntryItem& item) override; virtual QString PlaintextForEntryItem(EntryItem& item) override; bool m_secondColumnCacheEnabled = true; struct InfoColumn { QString name; std::unordered_map textCache; std::function textCallback; }; std::vector m_infoColumns; std::vector>> m_contextMenuItems; ManagedTableView* m_entryListView; EntryItemMetadataView* m_metadataView; FilteredView* m_filterView; FilterEdit* m_edit; MetadataState m_metadata; // Allows a custom checkbox to toggle some condition that items must also pass after passing the search filter. QCheckBox* m_extraFilterCheckbox; ExtraFilterState m_extraFilter; QLabel* m_selectedText; QPushButton* m_cancel; QPushButton* m_choose; bool ExtraFilterEnabled() { return m_extraFilter.exists && m_extraFilter.enabled; }; public: void AddWidthRequiredByItem(void* item, size_t widthRequired); void RemoveWidthRequiredByItem(QWidget* item); void AddHeightRequiredByItem(void* item, size_t widthRequired); void RemoveHeightRequiredByItem(QWidget* item); protected: void UpdateMinimumSpace(); std::optional m_chosenEntry; std::optional m_selectedEntry; private: void UpdateMetadataBox(EntryItem& item); QHBoxLayout* m_bottomLeftRowLayout; QHBoxLayout* m_entryTableContextButtonLayout; std::unordered_map m_layoutWidthRequiringItems; std::unordered_map m_layoutHeightRequiringItems; public: /*! Create a choice selection dialog with a Title, list of entries, and pre-built set of metadata for those entries. \note For large amounts of entries, where computing the metadata for all of them may take a while, it is highly recommended that instead of passing prebuilt metadata, a callback that lazily loads it should be used instead \see SetMetadataCallback \param parent Parent Widget \param title Title of the dialog \param prompt Text of the button to select a choice \param entries List of entries \param metadata Map of indices to the metadata for those entries. */ MetadataChoiceDialog(QWidget* parent, const QString& title, const QString& prompt, const QStringList& entries, std::unordered_map metadata); /*! Create a choice selection dialog with a Title, list of entries, and pre-built set of metadata for those entries. \note For large amounts of entries, where computing the metadata for all of them may take a while, it is highly recommended that instead of passing prebuilt metadata, a callback that lazily loads it should be used instead \see SetMetadataCallback \param parent Parent Widget \param title Title of the dialog \param prompt Text of the button to select a choice \param entries List of entries \param metadata Map of indices to the metadata (as InstructionTextTokens) for those entries. */ MetadataChoiceDialog(QWidget* parent, const QString& title, const QString& prompt, const QStringList& entries, std::unordered_map> metadata); /*! Create a choice selection dialog with a Title and list of entries. \note Without setting a Metadata Callback, metadata will not be displayed for the entries in the view. \param parent Parent Widget \param title Title of the dialog \param prompt Text of the button to select a choice \param entries List of entries */ MetadataChoiceDialog(QWidget* parent, const QString& title, const QString& prompt, const QStringList& entries); MetadataChoiceDialog(QWidget* parent, const QString& title, const QString& prompt = "Select") : MetadataChoiceDialog(parent, title, prompt, {}) {} /*! Set the callback the dialog will execute to retrieve metadata for a given item. \param callback Non nullable function (can be a lambda) that returns a QString containing metadata about the given EntryItem */ void SetMetadataCallback(const std::function& callback); /*! Set the callback the dialog will execute to retrieve metadata for a given item. \param callback Non nullable function (can be a lambda) that returns a vector of DisassemblyTextLines. */ void SetMetadataCallback(const std::function(EntryItem item)>& callback); /*! Set the callback the dialog will execute to retrieve second column text for a given item. \code{.cpp} auto choiceDialog = new MetadataChoiceDialog(parent, "Select Enum", entries); // Set up everything else with dialog // ... uint64_t targetValue = 0x64; std::function getSecondColumnText = [&](EntryItem item) { TypeRef type = entryTypes.at(item.idx); for (auto member : type->GetEnumeration()->GetMembers()) // Look for an enum member with a value of 0x64 if (member.value == targetValue) return QString::fromStdString(member.name); // If we cant find a member with a value of 0x64, just show 0x64 in the second column. return "0x" + QString::number(canTruncate ? constrainedValue : constValue, 16); }; choiceDialog->SetSecondColumnTextCallback("Enum Member Name", getSecondColumnText); \endcode \param secondColumnTitle Title for the 2nd column \param callback Non nullable function (can be a lambda) that returns a QString containing optional second column text for the given index */ void AddColumn(QString columnTitle, const std::function& callback); /*! Set a callback for any optional extra conditions that entries must pass to be displayed. The user will be able to toggle this filter via a checkbox. \param filterTitle Title for the checkbox to toggle this filter condition. \param callback Callback function that checks whether a given entry index passes the extra filter. */ void SetExtraFilter(QString filterTitle, const std::function callback); /*! Set whether metadata should be cached after calling the metadata callback once. By default this is enabled. \param s Whether to cache metadata. */ void SetShouldCacheMetadata(bool s) { m_metadata.cacheEnabled = s; }; /*! Set whether the entries that pass the extra filter should be cached. By default this is enabled. If the qualifications of the extra filter cannot change while this dialog is visible, this should be kept enabled. \param s Whether to cache extra filter results. */ void SetShouldCacheExtraFilterResults(bool s) { m_extraFilter.cacheEnabled = s; }; /*! Get the chosen entry. Should be called after ->exec() has finished. \code{.cpp} // Do all of the initial view setup (see top of this page for a full overview.) choiceDialog->exec(); if (choiceDialog->GetChosenEntry().has_value()) { // EntryItem contains a string (with the entry) and an index. // But since we know the dialog maintains order of indices, we can skip string comparisons // and just look it up by index. return entryTypes.at(choiceDialog->GetChosenEntry().value().idx); } return nullptr; \endcode \return The chosen entry, if one was chosen, or std::nullopt if selection was canceled. */ std::optional GetChosenEntry() const { return m_chosenEntry; }; /*! Add entries to the initial set. \note Don't use this for initial setup; it should primarily be used for any updates made while the dialog is open (via a button or other means). \note This will invalidate all caches, including any metadata initially passed in. If you need to add entries after opening the dialog and want metadata, you will need to use a callback. \param entries List of entries to add. */ void AddEntries(QStringList& entries); /*! Clear all caches. \note Calling AddEntries will also clear the caches. */ void InvalidateAllCaches(); /*! Add a button (or other widget) to the right side of the search bar. You are responsible for the behavior of this widget. \b Example: \code{.cpp} // The icon we're using here is just the "add types" icon from the Types sidebar widget. ClickableIcon* addIcon = new ClickableIcon(QImage(":/icons/images/add.png"), QSize(16, 16)); // This assumes we're not running within a class. If you are within another widget, use normal (icon, &method, this, &method2) syntax. QObject::connect(addIcon, &ClickableIcon::clicked, [&](){ // Do something interesting. Here we're just going to ask for a new entry. std::string result; GetTextLineInput(result, "Enter a new entry:", "Enter Entry"); // If you need to update your callbacks/info with the new entry, do so here, before you add it to the dialog. // Add the entry to the choice dialog. QStringList newEntries = { QString::fromStdString(result) }; choiceDialog->AddEntries(newEntries); } ); \endcode \param button Widget to add. */ void AddContextButton(QWidget* button); void AddContextMenuItem(QString title, const std::functionaction); void SelectFirstValidEntry(); public slots: virtual void selectionChanged(std::optional idx); void accept() override; void reject() override; };