From ff42661737c2af2adce5381f027cb33fb28f4768 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Sun, 7 Jul 2024 15:26:38 +0200 Subject: [PATCH] Improve user experience of the struct widget --- src/dbg/commands/cmd-types.cpp | 4 +- src/gui/Src/Gui/StructWidget.cpp | 123 ++++++++++++++++++++++--------- src/gui/Src/Gui/StructWidget.h | 10 +++ src/gui/Src/Gui/StructWidget.ui | 7 +- src/gui/icons/struct.png | Bin 759 -> 630 bytes 5 files changed, 108 insertions(+), 36 deletions(-) diff --git a/src/dbg/commands/cmd-types.cpp b/src/dbg/commands/cmd-types.cpp index ab874796..376583b9 100644 --- a/src/dbg/commands/cmd-types.cpp +++ b/src/dbg/commands/cmd-types.cpp @@ -523,7 +523,7 @@ struct PrintVisitor : TypeManager::Visitor td.userdata = nullptr; auto node = GuiTypeAddNode(mParents.empty() ? nullptr : parent().node, &td); - mPath.push_back((member.name == "visit" ? type.name : member.name) + "."); + mPath.push_back((member.name == "display" ? type.name : member.name) + "."); mParents.push_back(Parent(type.isunion ? Parent::Union : Parent::Struct)); parent().node = node; parent().size = td.size; @@ -635,7 +635,7 @@ bool cbInstrVisitType(int argc, char* argv[]) if(IsArgumentsLessThan(argc, 2)) return false; auto type = argv[1]; - auto name = "visit"; + auto name = "display"; duint addr = 0; auto maxPtrDepth = 0; if(argc > 2) diff --git a/src/gui/Src/Gui/StructWidget.cpp b/src/gui/Src/Gui/StructWidget.cpp index 5082e38f..85574ae0 100644 --- a/src/gui/Src/Gui/StructWidget.cpp +++ b/src/gui/Src/Gui/StructWidget.cpp @@ -1,10 +1,13 @@ +#include +#include +#include + #include "StructWidget.h" #include "ui_StructWidget.h" #include "Configuration.h" #include "MenuBuilder.h" #include "LineEditDialog.h" #include "GotoDialog.h" -#include #include "StringUtil.h" #include "MiscUtil.h" #include "RichTextItemDelegate.h" @@ -32,8 +35,8 @@ StructWidget::StructWidget(QWidget* parent) : connect(Config(), SIGNAL(shortcutsUpdated()), this, SLOT(shortcutsUpdatedSlot())); colorsUpdatedSlot(); fontsUpdatedSlot(); - setupContextMenu(); setupColumns(); + setupContextMenu(); } StructWidget::~StructWidget() @@ -45,28 +48,26 @@ void StructWidget::saveWindowSettings() { auto saveColumn = [this](int column) { - auto settingName = QString("StructWidgetColumn%1").arg(column); + auto settingName = QString("StructWidgetV2Column%1").arg(column); BridgeSettingSetUint("Gui", settingName.toUtf8().constData(), ui->treeWidget->columnWidth(column)); }; - saveColumn(0); - saveColumn(1); - saveColumn(2); - saveColumn(3); + auto columnCount = ui->treeWidget->columnCount(); + for(int i = 0; i < columnCount; i++) + saveColumn(i); } void StructWidget::loadWindowSettings() { auto loadColumn = [this](int column) { - auto settingName = QString("StructWidgetColumn%1").arg(column); + auto settingName = QString("StructWidgetV2Column%1").arg(column); duint width = 0; if(BridgeSettingGetUint("Gui", settingName.toUtf8().constData(), &width)) ui->treeWidget->setColumnWidth(column, width); }; - loadColumn(0); - loadColumn(1); - loadColumn(2); - loadColumn(3); + auto columnCount = ui->treeWidget->columnCount(); + for(int i = 0; i < columnCount; i++) + loadColumn(i); } void StructWidget::colorsUpdatedSlot() @@ -100,7 +101,19 @@ void StructWidget::typeAddNode(void* parent, const TYPEDESCRIPTOR* type) dtype.type = *type; dtype.name = highlightTypeName(dtype.type.name); dtype.type.name = nullptr; - auto text = QStringList() << dtype.name << ToPtrString(dtype.type.addr + dtype.type.offset) << "0x" + ToHexString(dtype.type.size); + QStringList text; + auto columnCount = ui->treeWidget->columnCount(); + for(int i = 0; i < columnCount; i++) + text.append(QString()); + + text[ColOffset] = "+0x" + ToHexString(dtype.type.offset); + text[ColField] = dtype.name; + if(dtype.type.offset == 0 && true) + text[ColAddress] = QString("%1").arg(ToPtrString(dtype.type.addr + dtype.type.offset)); + else + text[ColAddress] = ToPtrString(dtype.type.addr + dtype.type.offset); + text[ColSize] = "0x" + ToHexString(dtype.type.size); + text[ColValue] = ""; // NOTE: filled in later QTreeWidgetItem* item = parent ? new QTreeWidgetItem((QTreeWidgetItem*)parent, text) : new QTreeWidgetItem(ui->treeWidget, text); item->setExpanded(dtype.type.expanded); QVariant var; @@ -125,7 +138,7 @@ void StructWidget::typeUpdateWidget() auto name = type.name.toUtf8(); type.type.name = name.constData(); auto addr = type.type.addr + type.type.offset; - item->setText(1, ToPtrString(addr)); + item->setText(ColAddress, ToPtrString(addr)); QString valueStr; if(type.type.callback) //use the provided callback { @@ -155,7 +168,7 @@ void StructWidget::typeUpdateWidget() else if(type.type.addr) valueStr = "???"; } - item->setText(3, valueStr); + item->setText(ColValue, valueStr); } ui->treeWidget->setUpdatesEnabled(true); } @@ -169,9 +182,14 @@ void StructWidget::dbgStateChangedSlot(DBGSTATE state) void StructWidget::setupColumns() { auto charWidth = ui->treeWidget->fontMetrics().width(' '); - ui->treeWidget->setColumnWidth(0, 4 + charWidth * 60); //Name - ui->treeWidget->setColumnWidth(1, 6 + charWidth * sizeof(duint) * 2); //Address - ui->treeWidget->setColumnWidth(2, 4 + charWidth * 6); //Size + ui->treeWidget->setColumnWidth(ColField, 4 + charWidth * 60); + ui->treeWidget->setColumnWidth(ColOffset, 6 + charWidth * 7); + ui->treeWidget->setColumnWidth(ColAddress, 6 + charWidth * sizeof(duint) * 2); + ui->treeWidget->setColumnWidth(ColSize, 4 + charWidth * 6); + + // NOTE: Trick to display the expander icons in the second column + // Reference: https://stackoverflow.com/a/25887454/1806760 + // ui->treeWidget->header()->moveSection(ColField, ColOffset); } #define hasSelection !!ui->treeWidget->selectedItems().count() @@ -181,6 +199,10 @@ void StructWidget::setupColumns() void StructWidget::setupContextMenu() { mMenuBuilder = new MenuBuilder(this); + mMenuBuilder->addAction(makeAction(DIcon("dump"), tr("&Follow address in Dump"), SLOT(followDumpSlot())), [this](QMenu*) + { + return hasSelection && DbgMemIsValidReadPtr(selectedType.addr + selectedType.offset); + }); mMenuBuilder->addAction(makeAction(DIcon("dump"), tr("Follow value in Dump"), SLOT(followValueDumpSlot())), [this](QMenu*) { return DbgMemIsValidReadPtr(selectedValue()); @@ -189,15 +211,11 @@ void StructWidget::setupContextMenu() { return DbgMemIsValidReadPtr(selectedValue()); }); - mMenuBuilder->addAction(makeAction(DIcon("dump"), tr("&Follow address in Dump"), SLOT(followDumpSlot())), [this](QMenu*) - { - return hasSelection && DbgMemIsValidReadPtr(selectedType.addr + selectedType.offset); - }); mMenuBuilder->addAction(makeAction(DIcon("structaddr"), tr("Change address"), SLOT(changeAddrSlot())), [this](QMenu*) { return hasSelection && !selectedItem->parent() && DbgIsDebugging(); }); - mMenuBuilder->addAction(makeAction(DIcon("visitstruct"), tr("Visit type"), SLOT(visitSlot()))); + mMenuBuilder->addAction(makeAction(DIcon("visitstruct"), tr("Display type"), SLOT(visitSlot()))); mMenuBuilder->addAction(makeAction(DIcon("database-import"), tr("Load JSON"), SLOT(loadJsonSlot()))); mMenuBuilder->addAction(makeAction(DIcon("source"), tr("Parse header"), SLOT(parseFileSlot()))); mMenuBuilder->addAction(makeAction(DIcon("removestruct"), tr("Remove"), SLOT(removeSlot())), [this](QMenu*) @@ -206,6 +224,21 @@ void StructWidget::setupContextMenu() }); mMenuBuilder->addAction(makeAction(DIcon("eraser"), tr("Clear"), SLOT(clearSlot()))); mMenuBuilder->addAction(makeShortcutAction(DIcon("sync"), tr("&Refresh"), SLOT(refreshSlot()), "ActionRefresh")); + + auto copyMenu = new MenuBuilder(this); + auto columnCount = ui->treeWidget->columnCount(); + auto headerItem = ui->treeWidget->headerItem(); + for(int column = 0; column < columnCount; column++) + { + auto action = makeAction(headerItem->text(column), SLOT(copyColumnSlot())); + action->setObjectName(QString("%1").arg(column)); + copyMenu->addAction(action, [this, column](QMenu*) + { + return hasSelection && !selectedItem->text(column).isEmpty(); + }); + } + mMenuBuilder->addMenu(makeMenu(DIcon("copy"), tr("&Copy")), copyMenu); + mMenuBuilder->loadFromConfig(); } @@ -226,7 +259,6 @@ QString StructWidget::highlightTypeName(QString name) const "wchar_t", "int16_t", "uint8_t", - "struct", "double", "size_t", "uint64", @@ -235,7 +267,6 @@ QString StructWidget::highlightTypeName(QString name) const "uint16", "signed", "int8_t", - "union", "const", "float", "duint", @@ -269,14 +300,18 @@ QString StructWidget::highlightTypeName(QString name) const }(); name.replace(re, "\\1"); - return std::move(name); + + static QRegExp sre("^(struct|union|class|enum) ([a-zA-Z0-9_:$]+)"); + name.replace(sre, "\\1 \\2"); + + return name; } duint StructWidget::selectedValue() const { if(!hasSelection) return 0; - QStringList split = selectedItem->text(3).split(','); + QStringList split = selectedItem->text(ColValue).split(','); if(split.length() < 1) return 0; return split[0].toULongLong(nullptr, 0); @@ -325,18 +360,28 @@ void StructWidget::removeSlot() void StructWidget::visitSlot() { - //TODO: replace with a list to pick from - LineEditDialog mLineEdit(this); - mLineEdit.setWindowTitle(tr("Type to visit")); - if(mLineEdit.exec() != QDialog::Accepted || !mLineEdit.editText.length()) + QStringList structs; + DbgFunctions()->EnumStructs([](const char* name, void* userdata) + { + ((QStringList*)userdata)->append(name); + }, &structs); + if(structs.isEmpty()) + { + SimpleErrorBox(this, tr("Error"), tr("No types loaded yet, parse a header first...")); + return; + } + + QString selection; + if(!SimpleChoiceBox(this, tr("Type to display"), "", structs, selection, true, "", &DIcon("struct"), 1) || selection.isEmpty()) return; if(!mGotoDialog) mGotoDialog = new GotoDialog(this); duint addr = 0; - mGotoDialog->setWindowTitle(tr("Address to visit")); + mGotoDialog->setWindowTitle(tr("Address to display %1 at").arg(selection)); if(DbgIsDebugging() && mGotoDialog->exec() == QDialog::Accepted) addr = DbgValFromString(mGotoDialog->expressionText.toUtf8().constData()); - DbgCmdExec(QString("VisitType %1, %2, 2").arg(mLineEdit.editText, ToPtrString(addr))); + DbgCmdExec(QString("VisitType %1, %2, 2").arg(selection, ToPtrString(addr))); + // TODO: show a proper error message on failure } void StructWidget::loadJsonSlot() @@ -388,3 +433,15 @@ void StructWidget::refreshSlot() { typeUpdateWidget(); } + +void StructWidget::copyColumnSlot() +{ + QAction* action = qobject_cast(sender()); + if(action == nullptr || !hasSelection) + return; + auto column = action->objectName().toInt(); + auto text = selectedItem->text(column); + text = QTextDocumentFragment::fromHtml(text).toPlainText(); + if(!text.isEmpty()) + Bridge::CopyToClipboard(text); +} diff --git a/src/gui/Src/Gui/StructWidget.h b/src/gui/Src/Gui/StructWidget.h index 6dda30e5..ecc0f38c 100644 --- a/src/gui/Src/Gui/StructWidget.h +++ b/src/gui/Src/Gui/StructWidget.h @@ -43,6 +43,15 @@ private: QString highlightTypeName(QString name) const; duint selectedValue() const; + enum + { + ColField, + ColOffset, + ColAddress, + ColSize, + ColValue, + }; + private slots: void on_treeWidget_customContextMenuRequested(const QPoint & pos); @@ -56,4 +65,5 @@ private slots: void parseFileSlot(); void changeAddrSlot(); void refreshSlot(); + void copyColumnSlot(); }; diff --git a/src/gui/Src/Gui/StructWidget.ui b/src/gui/Src/Gui/StructWidget.ui index 8e896669..46dc8a10 100644 --- a/src/gui/Src/Gui/StructWidget.ui +++ b/src/gui/Src/Gui/StructWidget.ui @@ -51,7 +51,12 @@ - Name + Field + + + + + Offset diff --git a/src/gui/icons/struct.png b/src/gui/icons/struct.png index 5a39a1aae8e9f482e6f94957bdce706932aa5187..d2aba04ccb19e8f31df5c7bc100f6239660a5c78 100644 GIT binary patch delta 568 zcmV-80>}OL1@;7xNq-ngL_t(|+N6`ui;`gw$LIYOteZb}U(xEIB4J7g7hWjB=+dDs z(y7B*&>`@=ME^kdx`!7}OAzSA!-i}TdFUkW9yVhSA?1?zx^3#~s=fN!cSK#cy!pU~ znR(xtd1mH$9$_>Z(K1V!TxYK+yv!6Uw@y?oPxn1!Dmer#k(+7 zaBNxDd-$D5>eYw4H?FTn<9F8R^t63x@1S~H(bKPmVRM$C)9H|Dnhz3*#8a^4a5$WW zLZRL5b`Rug^(d-E!qo5gNsHJi{q`ujezB6ApA7%2*Xt|caQGf4V_B(GcE&Rhgw_3> zPgUymdc=}#V}E!hH5d$1{EcO^*-RneB$LT}Xti3zl5If3&CM?nQIuyhu&e=QV3zIr z7H^OQsZyy#=-G{)KY{UMo4C*U<$v?}2p4W0LH9WyXuy64`HOBn zIwb$b*_kVfLQbc1O44Stk;mhqSS}N*AYv$0SODQC-)|gi;H8Y%d7+f0ei7n zd^z>eH0?RWD``;zejnT)Nz z461Sg4Kcb4{6_L3`1WrCVe-JF1&DL7pUJmeSoS&p6<`36)dQ|ZkIL`>0000S@^HFM3}R`Te`+h(hY>0y2~j~1 zWQ45fdqF57V-5sChx#e}W>_HmU_(eqqLN&&nQdxBeQ=S6HOr7_G>3D0^KI|l&YjyR zbshNKckg-U_q*qwb1#96H4p+F?2F(5$S=FSisQJ`<%5H51b={rdwU;KN&{MlV@<~O zF-1x93fb@o$qkS*A_3e=YPB^+i>0;LXf%KN37@PKz_<^j&1;r|URqWOCu@SD|wiXy8KmlN2_#$8^*E+8REfJHN7^k|36 zRnAi0a=SNCAedKx8MX;xi3B^N9tB6uR;xi%RVD0%LVwY3e*Y?F^)x4N4u_j1C4fDL zB9#hGh$7TQqmW9a^n~NCp6Ti`s7)q8MxU#{|9*8Sw1O1he=|TcE0i{MPfXa<%gaL2 zT|9Dj+x3#ZeEU2tT`u!L|SJ{%xWk&@->Z4SqU(&AzvwYdp*nwz^XV(cLR_ad$r z!{JP;=)c>Z7X+}V)z&qycRevXOCuHwwUn0HZfG=TujMIH?kZ=p(6;itP1@L?(UB4Q z5a4Yi&tE}*8A5|lBZS?DnvN$~0X!ZQA&wm9xHFTd3JOehiee1P^6V3U1@yCvx`UAR gAc^Gkd;TN90G~fX3$8R9`v3p{07*qoM6N<$f=I