Make ClassesWidget a ListDockWidget (#3152)

Adds Quick Filter and generic address-based context menu entries to the
existing ClassesWidget. The original ClassesWidget.ui is not used
anymore as the layout is provided by ListDockWidget and only adjusted.
The AddressableItemContextMenu may now also optionally be shown when
there no currently selected item by using
setShowItemContextMenuWithoutAddress(). This is used e.g. to display the
"Create Class" option when nothing is present in the list.

Fixes #2237

Co-authored-by: Tristan Crawford <tristanthtcrawford@gmail.com>
This commit is contained in:
Florian Märkl 2023-03-30 19:33:31 +02:00 committed by GitHub
parent d3ee310a21
commit 1f133741ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 241 deletions

View File

@ -338,7 +338,6 @@ set(UI_FILES
dialogs/preferences/InitializationFileEditor.ui
widgets/QuickFilterView.ui
widgets/DecompilerWidget.ui
widgets/ClassesWidget.ui
widgets/VTablesWidget.ui
widgets/TypesWidget.ui
widgets/SearchWidget.ui

View File

@ -13,11 +13,17 @@ AddressableFilterProxyModel::AddressableFilterProxyModel(AddressableItemModelI *
RVA AddressableFilterProxyModel::address(const QModelIndex &index) const
{
if (!addressableSourceModel) {
return RVA_INVALID;
}
return addressableSourceModel->address(this->mapToSource(index));
}
QString AddressableFilterProxyModel::name(const QModelIndex &index) const
{
if (!addressableSourceModel) {
return QString();
}
return addressableSourceModel->name(this->mapToSource(index));
}
@ -28,6 +34,6 @@ void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
void AddressableFilterProxyModel::setSourceModel(AddressableItemModelI *sourceModel)
{
ParentClass::setSourceModel(sourceModel->asItemModel());
ParentClass::setSourceModel(sourceModel ? sourceModel->asItemModel() : nullptr);
addressableSourceModel = sourceModel;
}

View File

@ -112,7 +112,8 @@ void AddressableItemContextMenu::aboutToShowSlot()
void AddressableItemContextMenu::setHasTarget(bool hasTarget)
{
this->hasTarget = hasTarget;
for (const auto &action : this->actions()) {
action->setEnabled(hasTarget);
}
actionShowInMenu->setEnabled(hasTarget);
actionCopyAddress->setEnabled(hasTarget);
actionShowXrefs->setEnabled(hasTarget);
actionAddcomment->setEnabled(hasTarget);
}

View File

@ -57,16 +57,30 @@ public:
itemContextMenu = menu;
}
/**
* If this is set to true, the context menu will also be shown if no item
* is currently selected.
*/
void setShowItemContextMenuWithoutAddress(bool val) { showItemContextMenuWithoutAddress = val; }
protected:
virtual void showItemContextMenu(const QPoint &pt)
{
if (!itemContextMenu) {
return;
}
auto index = this->currentIndex();
if (index.isValid() && itemContextMenu) {
if (index.isValid()) {
auto offset = addressableModel->address(index);
auto name = addressableModel->name(index);
itemContextMenu->setTarget(offset, name);
itemContextMenu->exec(this->mapToGlobal(pt));
} else {
if (!showItemContextMenuWithoutAddress) {
return;
}
itemContextMenu->clearTarget();
}
itemContextMenu->exec(this->mapToGlobal(pt));
}
virtual void onItemActivated(const QModelIndex &index)
@ -90,6 +104,7 @@ protected:
}
private:
bool showItemContextMenuWithoutAddress = false;
AddressableItemModelI *addressableModel = nullptr;
AddressableItemContextMenu *itemContextMenu = nullptr;
MainWindow *mainWindow = nullptr;

View File

@ -1,6 +1,6 @@
#include "ClassesWidget.h"
#include "core/MainWindow.h"
#include "ui_ClassesWidget.h"
#include "ui_ListDockWidget.h"
#include "common/Helpers.h"
#include "common/SvgIconEngine.h"
#include "dialogs/EditMethodDialog.h"
@ -9,6 +9,8 @@
#include <QMenu>
#include <QMouseEvent>
#include <QInputDialog>
#include <QShortcut>
#include <QComboBox>
QVariant ClassesModel::headerData(int section, Qt::Orientation, int role) const
{
@ -33,6 +35,17 @@ QVariant ClassesModel::headerData(int section, Qt::Orientation, int role) const
}
}
RVA ClassesModel::address(const QModelIndex &index) const
{
QVariant v = data(index, OffsetRole);
return v.isValid() ? v.toULongLong() : RVA_INVALID;
}
QString ClassesModel::name(const QModelIndex &index) const
{
return data(index, NameRole).toString();
}
BinClassesModel::BinClassesModel(QObject *parent) : ClassesModel(parent) {}
void BinClassesModel::setClasses(const QList<BinClassDescription> &classes)
@ -526,12 +539,17 @@ QVariant AnalysisClassesModel::data(const QModelIndex &index, int role) const
}
ClassesSortFilterProxyModel::ClassesSortFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
: AddressableFilterProxyModel(nullptr, parent)
{
setFilterCaseSensitivity(Qt::CaseInsensitive);
setSortCaseSensitivity(Qt::CaseInsensitive);
}
bool ClassesSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (parent.isValid())
return true;
QModelIndex index = sourceModel()->index(row, 0, parent);
return qhelpers::filterStringContains(index.data(ClassesModel::NameRole).toString(), this);
}
@ -576,23 +594,63 @@ bool ClassesSortFilterProxyModel::hasChildren(const QModelIndex &parent) const
return !parent.isValid() || !parent.parent().isValid();
}
ClassesWidget::ClassesWidget(MainWindow *main) : CutterDockWidget(main), ui(new Ui::ClassesWidget)
ClassesWidget::ClassesWidget(MainWindow *main)
: ListDockWidget(main),
seekToVTableAction(tr("Seek to VTable"), this),
editMethodAction(tr("Edit Method"), this),
addMethodAction(tr("Add Method"), this),
newClassAction(tr("Create new Class"), this),
renameClassAction(tr("Rename Class"), this),
deleteClassAction(tr("Delete Class"), this)
{
ui->setupUi(this);
setWindowTitle(tr("Classes"));
setObjectName("ClassesWidget");
ui->classesTreeView->setIconSize(QSize(10, 10));
ui->treeView->setIconSize(QSize(10, 10));
proxy_model = new ClassesSortFilterProxyModel(this);
ui->classesTreeView->setModel(proxy_model);
ui->classesTreeView->sortByColumn(ClassesModel::TYPE, Qt::AscendingOrder);
ui->classesTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
setModels(proxy_model);
ui->classSourceCombo->setCurrentIndex(1);
classSourceCombo = new QComboBox(this);
// User an intermediate single-child layout to contain the combo box, otherwise
// when the combo box is inserted directly, the entire vertical layout gets a
// weird horizontal padding on macOS.
QBoxLayout *comboLayout = new QBoxLayout(QBoxLayout::Direction::LeftToRight, nullptr);
comboLayout->addWidget(classSourceCombo);
ui->verticalLayout->insertLayout(ui->verticalLayout->indexOf(ui->quickFilterView), comboLayout);
classSourceCombo->addItem(tr("Binary Info (Fixed)"));
classSourceCombo->addItem(tr("Analysis (Editable)"));
classSourceCombo->setCurrentIndex(1);
connect<void (QComboBox::*)(int)>(ui->classSourceCombo, &QComboBox::currentIndexChanged, this,
connect<void (QComboBox::*)(int)>(classSourceCombo, &QComboBox::currentIndexChanged, this,
&ClassesWidget::refreshClasses);
connect(ui->classesTreeView, &QTreeView::customContextMenuRequested, this,
&ClassesWidget::showContextMenu);
connect(&seekToVTableAction, &QAction::triggered, this,
&ClassesWidget::seekToVTableActionTriggered);
connect(&editMethodAction, &QAction::triggered, this,
&ClassesWidget::editMethodActionTriggered);
connect(&addMethodAction, &QAction::triggered, this, &ClassesWidget::addMethodActionTriggered);
connect(&newClassAction, &QAction::triggered, this, &ClassesWidget::newClassActionTriggered);
connect(&renameClassAction, &QAction::triggered, this,
&ClassesWidget::renameClassActionTriggered);
connect(&deleteClassAction, &QAction::triggered, this,
&ClassesWidget::deleteClassActionTriggered);
// Build context menu like this:
// class-related actions
// -- classesMethodsSeparator
// method-related actions
// -- separator
// default actions from AddressableItemList
auto contextMenu = ui->treeView->getItemContextMenu();
contextMenu->insertSeparator(contextMenu->actions().first());
contextMenu->insertActions(contextMenu->actions().first(),
{ &addMethodAction, &editMethodAction, &seekToVTableAction });
classesMethodsSeparator = contextMenu->insertSeparator(contextMenu->actions().first());
contextMenu->insertActions(classesMethodsSeparator,
{ &newClassAction, &renameClassAction, &deleteClassAction });
connect(contextMenu, &QMenu::aboutToShow, this, &ClassesWidget::updateActions);
ui->treeView->setShowItemContextMenuWithoutAddress(true);
refreshClasses();
}
@ -601,7 +659,7 @@ ClassesWidget::~ClassesWidget() {}
ClassesWidget::Source ClassesWidget::getSource()
{
switch (ui->classSourceCombo->currentIndex()) {
switch (classSourceCombo->currentIndex()) {
case 0:
return Source::BIN;
default:
@ -614,88 +672,68 @@ void ClassesWidget::refreshClasses()
switch (getSource()) {
case Source::BIN:
if (!bin_model) {
proxy_model->setSourceModel(nullptr);
proxy_model->setSourceModel(static_cast<AddressableItemModelI *>(nullptr));
delete analysis_model;
analysis_model = nullptr;
bin_model = new BinClassesModel(this);
proxy_model->setSourceModel(bin_model);
proxy_model->setSourceModel(static_cast<AddressableItemModelI *>(bin_model));
}
bin_model->setClasses(Core()->getAllClassesFromBin());
break;
case Source::ANALYSIS:
if (!analysis_model) {
proxy_model->setSourceModel(nullptr);
proxy_model->setSourceModel(static_cast<AddressableItemModelI *>(nullptr));
delete bin_model;
bin_model = nullptr;
analysis_model = new AnalysisClassesModel(this);
proxy_model->setSourceModel(analysis_model);
proxy_model->setSourceModel(static_cast<AddressableItemModelI *>(analysis_model));
}
break;
}
qhelpers::adjustColumns(ui->classesTreeView, 3, 0);
qhelpers::adjustColumns(ui->treeView, 3, 0);
ui->classesTreeView->setColumnWidth(0, 200);
ui->treeView->setColumnWidth(0, 200);
}
void ClassesWidget::on_classesTreeView_doubleClicked(const QModelIndex &index)
void ClassesWidget::updateActions()
{
if (!index.isValid())
return;
bool isAnalysis = !!analysis_model;
newClassAction.setVisible(isAnalysis);
addMethodAction.setVisible(isAnalysis);
QVariant offsetData = index.data(ClassesModel::OffsetRole);
if (!offsetData.isValid()) {
return;
}
RVA offset = offsetData.value<RVA>();
Core()->seekAndShow(offset);
}
void ClassesWidget::showContextMenu(const QPoint &pt)
{
if (!analysis_model) {
// no context menu for bin classes
return;
bool rowIsAnalysisClass = false;
bool rowIsAnalysisMethod = false;
QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (isAnalysis && index.isValid()) {
auto type = static_cast<ClassesModel::RowType>(index.data(ClassesModel::TypeRole).toInt());
rowIsAnalysisClass = type == ClassesModel::RowType::Class;
rowIsAnalysisMethod = type == ClassesModel::RowType::Method;
}
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
return;
}
auto type = static_cast<ClassesModel::RowType>(index.data(ClassesModel::TypeRole).toInt());
renameClassAction.setVisible(rowIsAnalysisClass);
deleteClassAction.setVisible(rowIsAnalysisClass);
QMenu menu(ui->classesTreeView);
menu.addAction(ui->newClassAction);
if (type == ClassesModel::RowType::Class) {
menu.addAction(ui->renameClassAction);
menu.addAction(ui->deleteClassAction);
}
menu.addSeparator();
menu.addAction(ui->addMethodAction);
if (type == ClassesModel::RowType::Method) {
menu.addAction(ui->editMethodAction);
classesMethodsSeparator->setVisible(rowIsAnalysisClass || rowIsAnalysisMethod);
editMethodAction.setVisible(rowIsAnalysisMethod);
bool rowHasVTable = false;
if (rowIsAnalysisMethod) {
QString className = index.parent().data(ClassesModel::NameRole).toString();
QString methodName = index.data(ClassesModel::NameRole).toString();
AnalysisMethodDescription desc;
if (Core()->getAnalysisMethod(className, methodName, &desc)) {
if (desc.vtableOffset >= 0) {
menu.addAction(ui->seekToVTableAction);
rowHasVTable = true;
}
}
}
menu.exec(ui->classesTreeView->mapToGlobal(pt));
seekToVTableAction.setVisible(rowHasVTable);
}
void ClassesWidget::on_seekToVTableAction_triggered()
void ClassesWidget::seekToVTableActionTriggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = ui->treeView->selectionModel()->currentIndex();
QString className = index.parent().data(ClassesModel::NameRole).toString();
QList<AnalysisVTableDescription> vtables = Core()->getAnalysisClassVTables(className);
@ -714,9 +752,9 @@ void ClassesWidget::on_seekToVTableAction_triggered()
Core()->seekAndShow(vtables[0].addr + desc.vtableOffset);
}
void ClassesWidget::on_addMethodAction_triggered()
void ClassesWidget::addMethodActionTriggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (!index.isValid()) {
return;
}
@ -732,9 +770,9 @@ void ClassesWidget::on_addMethodAction_triggered()
EditMethodDialog::newMethod(className, QString(), this);
}
void ClassesWidget::on_editMethodAction_triggered()
void ClassesWidget::editMethodActionTriggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Method)) {
@ -745,7 +783,7 @@ void ClassesWidget::on_editMethodAction_triggered()
EditMethodDialog::editMethod(className, methName, this);
}
void ClassesWidget::on_newClassAction_triggered()
void ClassesWidget::newClassActionTriggered()
{
bool ok;
QString name = QInputDialog::getText(this, tr("Create new Class"), tr("Class Name:"),
@ -755,9 +793,9 @@ void ClassesWidget::on_newClassAction_triggered()
}
}
void ClassesWidget::on_deleteClassAction_triggered()
void ClassesWidget::deleteClassActionTriggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Class)) {
@ -772,9 +810,9 @@ void ClassesWidget::on_deleteClassAction_triggered()
Core()->deleteClass(className);
}
void ClassesWidget::on_renameClassAction_triggered()
void ClassesWidget::renameClassActionTriggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Class)) {

View File

@ -5,6 +5,7 @@
#include "core/Cutter.h"
#include "CutterDockWidget.h"
#include "widgets/ListDockWidget.h"
#include <QAbstractListModel>
#include <QSortFilterProxyModel>
@ -21,7 +22,7 @@ class ClassesWidget;
/**
* @brief Common abstract base class for Bin and Anal classes models
*/
class ClassesModel : public QAbstractItemModel
class ClassesModel : public AddressableItemModel<>
{
public:
enum Columns { NAME = 0, REAL_NAME, TYPE, OFFSET, VTABLE, COUNT };
@ -69,10 +70,13 @@ public:
*/
static const int RealNameRole = Qt::UserRole + 4;
explicit ClassesModel(QObject *parent = nullptr) : QAbstractItemModel(parent) {}
explicit ClassesModel(QObject *parent = nullptr) : AddressableItemModel(parent) {}
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &index) const override;
};
Q_DECLARE_METATYPE(ClassesModel::RowType)
@ -163,7 +167,7 @@ public slots:
void classAttrsChanged(const QString &cls);
};
class ClassesSortFilterProxyModel : public QSortFilterProxyModel
class ClassesSortFilterProxyModel : public AddressableFilterProxyModel
{
Q_OBJECT
@ -176,7 +180,7 @@ protected:
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
};
class ClassesWidget : public CutterDockWidget
class ClassesWidget : public ListDockWidget
{
Q_OBJECT
@ -185,29 +189,34 @@ public:
~ClassesWidget();
private slots:
void on_classesTreeView_doubleClicked(const QModelIndex &index);
void on_seekToVTableAction_triggered();
void on_addMethodAction_triggered();
void on_editMethodAction_triggered();
void on_newClassAction_triggered();
void on_deleteClassAction_triggered();
void on_renameClassAction_triggered();
void showContextMenu(const QPoint &pt);
void seekToVTableActionTriggered();
void editMethodActionTriggered();
void addMethodActionTriggered();
void newClassActionTriggered();
void renameClassActionTriggered();
void deleteClassActionTriggered();
void refreshClasses();
void updateActions();
private:
enum class Source { BIN, ANALYSIS };
Source getSource();
std::unique_ptr<Ui::ClassesWidget> ui;
BinClassesModel *bin_model = nullptr;
AnalysisClassesModel *analysis_model = nullptr;
ClassesSortFilterProxyModel *proxy_model;
QComboBox *classSourceCombo;
QAction seekToVTableAction;
QAction editMethodAction;
QAction addMethodAction;
QAction newClassAction;
QAction renameClassAction;
QAction deleteClassAction;
QAction *classesMethodsSeparator;
};
#endif // CLASSESWIDGET_H

View File

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ClassesWidget</class>
<widget class="QDockWidget" name="ClassesWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Classes</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="CutterTreeView" name="classesTreeView">
<property name="styleSheet">
<string notr="true">CutterTreeView::item
{
padding-top: 1px;
padding-bottom: 1px;
}</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_17">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="classSourceLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Source:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="classSourceCombo">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Binary Info (Fixed)</string>
</property>
</item>
<item>
<property name="text">
<string>Analysis (Editable)</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<action name="seekToVTableAction">
<property name="text">
<string>Seek to VTable</string>
</property>
</action>
<action name="editMethodAction">
<property name="text">
<string>Edit Method</string>
</property>
</action>
<action name="addMethodAction">
<property name="text">
<string>Add Method</string>
</property>
</action>
<action name="newClassAction">
<property name="text">
<string>Create new Class</string>
</property>
</action>
<action name="renameClassAction">
<property name="text">
<string>Rename Class</string>
</property>
</action>
<action name="deleteClassAction">
<property name="text">
<string>Delete Class</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>CutterTreeView</class>
<extends>QTreeView</extends>
<header>widgets/CutterTreeView.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -2,7 +2,6 @@
#include "ui_ListDockWidget.h"
#include "core/MainWindow.h"
#include "common/Helpers.h"
#include "menus/AddressableItemContextMenu.h"
#include <QMenu>
#include <QResizeEvent>