diff --git a/src/Cutter.pro b/src/Cutter.pro index f2ec81bf..0f0ac1ab 100644 --- a/src/Cutter.pro +++ b/src/Cutter.pro @@ -427,7 +427,12 @@ SOURCES += \ dialogs/LayoutManager.cpp \ common/CutterLayout.cpp \ widgets/GraphHorizontalAdapter.cpp \ - common/ResourcePaths.cpp + common/ResourcePaths.cpp \ + widgets/CutterGraphView.cpp \ + widgets/SimpleTextGraphView.cpp \ + widgets/R2GraphWidget.cpp \ + widgets/CallGraph.cpp \ + widgets/AddressableDockWidget.cpp GRAPHVIZ_SOURCES = \ widgets/GraphvizLayout.cpp @@ -581,7 +586,12 @@ HEADERS += \ common/BinaryTrees.h \ common/LinkedListPool.h \ widgets/GraphHorizontalAdapter.h \ - common/ResourcePaths.h + common/ResourcePaths.h \ + widgets/CutterGraphView.h \ + widgets/SimpleTextGraphView.h \ + widgets/R2GraphWidget.h \ + widgets/CallGraph.h \ + widgets/AddressableDockWidget.h GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h @@ -648,7 +658,8 @@ FORMS += \ widgets/ColorPicker.ui \ dialogs/preferences/ColorThemeEditDialog.ui \ widgets/ListDockWidget.ui \ - dialogs/LayoutManager.ui + dialogs/LayoutManager.ui \ + widgets/R2GraphWidget.ui RESOURCES += \ resources.qrc \ diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index efbe511c..9213f358 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -69,6 +69,8 @@ #include "widgets/HexdumpWidget.h" #include "widgets/DecompilerWidget.h" #include "widgets/HexWidget.h" +#include "widgets/R2GraphWidget.h" +#include "widgets/CallGraph.h" // Qt Headers #include @@ -371,7 +373,10 @@ void MainWindow::initDocks() segmentsDock = new SegmentsWidget(this), symbolsDock = new SymbolsWidget(this), vTablesDock = new VTablesWidget(this), - zignaturesDock = new ZignaturesWidget(this) + zignaturesDock = new ZignaturesWidget(this), + r2GraphDock = new R2GraphWidget(this), + callGraphDock = new CallGraphWidget(this, false), + globalCallGraphDock = new CallGraphWidget(this, true), }; auto makeActionList = [this](QList docks) { @@ -590,6 +595,11 @@ void MainWindow::finalizeOpen() // Add fortune message core->message("\n" + core->cmdRaw("fo")); + // hide all docks before showing window to avoid false positive for refreshDeferrer + for (auto dockWidget : dockWidgets) { + dockWidget->hide(); + } + QSettings settings; auto geometry = settings.value("geometry").toByteArray(); if (!geometry.isEmpty()) { @@ -822,6 +832,9 @@ void MainWindow::restoreDocks() tabifyDockWidget(dashboardDock, memoryMapDock); tabifyDockWidget(dashboardDock, breakpointDock); tabifyDockWidget(dashboardDock, registerRefsDock); + tabifyDockWidget(dashboardDock, r2GraphDock); + tabifyDockWidget(dashboardDock, callGraphDock); + tabifyDockWidget(dashboardDock, globalCallGraphDock); for (const auto &it : dockWidgets) { // Check whether or not current widgets is graph, hexdump or disasm if (isExtraMemoryWidget(it)) { @@ -923,10 +936,26 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type) QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address) { QMenu *menu = new QMenu(parent); + // Memory dock widgets for (auto &dock : dockWidgets) { if (auto memoryWidget = qobject_cast(dock)) { QAction *action = new QAction(memoryWidget->windowTitle(), menu); - connect(action, &QAction::triggered, this, [this, memoryWidget, address]() { + connect(action, &QAction::triggered, this, [memoryWidget, address]() { + memoryWidget->getSeekable()->seek(address); + memoryWidget->raiseMemoryWidget(); + }); + menu->addAction(action); + } + } + menu->addSeparator(); + // Rest of the AddressableDockWidgets that weren't added already + for (auto &dock : dockWidgets) { + if (auto memoryWidget = qobject_cast(dock)) { + if (qobject_cast(dock)) { + continue; + } + QAction *action = new QAction(memoryWidget->windowTitle(), menu); + connect(action, &QAction::triggered, this, [memoryWidget, address]() { memoryWidget->getSeekable()->seek(address); memoryWidget->raiseMemoryWidget(); }); diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index 47664279..039da028 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -52,6 +52,8 @@ class GraphWidget; class HexdumpWidget; class DecompilerWidget; class OverviewWidget; +class R2GraphWidget; +class CallGraphWidget; namespace Ui { class MainWindow; @@ -258,15 +260,18 @@ private: ClassesWidget *classesDock = nullptr; ResourcesWidget *resourcesDock = nullptr; VTablesWidget *vTablesDock = nullptr; - CutterDockWidget *stackDock = nullptr; - CutterDockWidget *threadsDock = nullptr; - CutterDockWidget *processesDock = nullptr; - CutterDockWidget *registersDock = nullptr; - CutterDockWidget *backtraceDock = nullptr; - CutterDockWidget *memoryMapDock = nullptr; + CutterDockWidget *stackDock = nullptr; + CutterDockWidget *threadsDock = nullptr; + CutterDockWidget *processesDock = nullptr; + CutterDockWidget *registersDock = nullptr; + CutterDockWidget *backtraceDock = nullptr; + CutterDockWidget *memoryMapDock = nullptr; NewFileDialog *newFileDialog = nullptr; - CutterDockWidget *breakpointDock = nullptr; - CutterDockWidget *registerRefsDock = nullptr; + CutterDockWidget *breakpointDock = nullptr; + CutterDockWidget *registerRefsDock = nullptr; + R2GraphWidget *r2GraphDock = nullptr; + CallGraphWidget *callGraphDock = nullptr; + CallGraphWidget *globalCallGraphDock = nullptr; QMenu *disassemblyContextMenuExtensions = nullptr; QMenu *addressableContextMenuExtensions = nullptr; diff --git a/src/dialogs/preferences/GraphOptionsWidget.ui b/src/dialogs/preferences/GraphOptionsWidget.ui index f07af461..91467138 100644 --- a/src/dialogs/preferences/GraphOptionsWidget.ui +++ b/src/dialogs/preferences/GraphOptionsWidget.ui @@ -108,7 +108,7 @@ - 100 + 400 10 @@ -121,7 +121,7 @@ - 100 + 400 10 @@ -137,7 +137,7 @@ 1 - 100 + 400 5 @@ -153,7 +153,7 @@ 1 - 100 + 400 5 diff --git a/src/widgets/AddressableDockWidget.cpp b/src/widgets/AddressableDockWidget.cpp new file mode 100644 index 00000000..87cb41af --- /dev/null +++ b/src/widgets/AddressableDockWidget.cpp @@ -0,0 +1,65 @@ +#include "AddressableDockWidget.h" +#include "common/CutterSeekable.h" +#include "MainWindow.h" +#include +#include +#include +#include + +AddressableDockWidget::AddressableDockWidget(MainWindow *parent) + : CutterDockWidget(parent) + , seekable(new CutterSeekable(this)) + , syncAction(tr("Sync/unsync offset"), this) +{ + connect(seekable, &CutterSeekable::syncChanged, this, &AddressableDockWidget::updateWindowTitle); + connect(&syncAction, &QAction::triggered, seekable, &CutterSeekable::toggleSynchronization); + + dockMenu = new QMenu(this); + dockMenu->addAction(&syncAction); + + setContextMenuPolicy(Qt::ContextMenuPolicy::DefaultContextMenu); +} + +void AddressableDockWidget::raiseMemoryWidget() +{ + show(); + raise(); + widgetToFocusOnRaise()->setFocus(Qt::FocusReason::TabFocusReason); +} + +QVariantMap AddressableDockWidget::serializeViewProprties() +{ + auto result = CutterDockWidget::serializeViewProprties(); + result["synchronized"] = seekable->isSynchronized(); + return result; +} + +void AddressableDockWidget::deserializeViewProperties(const QVariantMap &properties) +{ + QVariant synchronized = properties.value("synchronized", true); + seekable->setSynchronization(synchronized.toBool()); +} + +void AddressableDockWidget::updateWindowTitle() +{ + QString name = getWindowTitle(); + QString id = getDockNumber(); + if (!id.isEmpty()) { + name += " " + id; + } + if (!seekable->isSynchronized()) { + name += CutterSeekable::tr(" (unsynced)"); + } + setWindowTitle(name); +} + +void AddressableDockWidget::contextMenuEvent(QContextMenuEvent *event) +{ + event->accept(); + dockMenu->exec(mapToGlobal(event->pos())); +} + +CutterSeekable *AddressableDockWidget::getSeekable() const +{ + return seekable; +} diff --git a/src/widgets/AddressableDockWidget.h b/src/widgets/AddressableDockWidget.h new file mode 100644 index 00000000..a5b28d75 --- /dev/null +++ b/src/widgets/AddressableDockWidget.h @@ -0,0 +1,36 @@ +#ifndef ADDRESSABLE_DOCK_WIDGET_H +#define ADDRESSABLE_DOCK_WIDGET_H + +#include "CutterDockWidget.h" +#include "core/Cutter.h" + +#include + +class CutterSeekable; + +class AddressableDockWidget : public CutterDockWidget +{ + Q_OBJECT +public: + AddressableDockWidget(MainWindow *parent); + ~AddressableDockWidget() override {} + + CutterSeekable *getSeekable() const; + + void raiseMemoryWidget(); + + QVariantMap serializeViewProprties() override; + void deserializeViewProperties(const QVariantMap &properties) override; +public slots: + void updateWindowTitle(); + +protected: + CutterSeekable *seekable = nullptr; + QAction syncAction; + QMenu *dockMenu = nullptr; + + virtual QString getWindowTitle() const = 0; + void contextMenuEvent(QContextMenuEvent *event) override; +}; + +#endif // ADDRESSABLE_DOCK_WIDGET_H diff --git a/src/widgets/CallGraph.cpp b/src/widgets/CallGraph.cpp new file mode 100644 index 00000000..ddfb57f6 --- /dev/null +++ b/src/widgets/CallGraph.cpp @@ -0,0 +1,144 @@ +#include "CallGraph.h" + +#include "MainWindow.h" + +#include +#include +#include + +CallGraphWidget::CallGraphWidget(MainWindow *main, bool global) + : AddressableDockWidget(main) + , graphView(new CallGraphView(this, main, global)) + , global(global) +{ + setObjectName(main->getUniqueObjectName("CallGraphWidget")); + this->setWindowTitle(getWindowTitle()); + connect(seekable, &CutterSeekable::seekableSeekChanged, this, &CallGraphWidget::onSeekChanged); + + setWidget(graphView); +} + +CallGraphWidget::~CallGraphWidget() +{ +} + +QString CallGraphWidget::getWindowTitle() const +{ + return global ? tr("Global Callgraph") : tr("Callgraph"); +} + +void CallGraphWidget::onSeekChanged(RVA address) +{ + if (auto function = Core()->functionIn(address)) { + graphView->showAddress(function->addr); + } +} + +CallGraphView::CallGraphView(CutterDockWidget *parent, MainWindow *main, bool global) + : SimpleTextGraphView(parent, main) + , global(global) + , refreshDeferrer(nullptr, this) +{ + enableAddresses(true); + refreshDeferrer.registerFor(parent); + connect(&refreshDeferrer, &RefreshDeferrer::refreshNow, this, &CallGraphView::refreshView); + connect(Core(), &CutterCore::refreshAll, this, &SimpleTextGraphView::refreshView); +} + +void CallGraphView::showExportDialog() +{ + QString defaultName; + if (global) { + defaultName = "global_callgraph"; + } else { + defaultName = QString("callgraph_%1").arg(RAddressString(address)); + } + showExportGraphDialog(defaultName, global ? "agC" : "agc", address); +} + +void CallGraphView::showAddress(RVA address) +{ + if (global) { + auto addressMappingIt = addressMapping.find(address); + if (addressMappingIt != addressMapping.end()) { + selectBlockWithId(addressMappingIt->second); + showBlock(blocks[addressMappingIt->second]); + } + } else if (address != this->address) { + this->address = address; + refreshView(); + } +} + +void CallGraphView::refreshView() +{ + if (!refreshDeferrer.attemptRefresh(nullptr)) { + return; + } + SimpleTextGraphView::refreshView(); +} + +void CallGraphView::loadCurrentGraph() +{ + blockContent.clear(); + blocks.clear(); + + QJsonDocument functionsDoc = Core()->cmdj(global ? "agCj" : QString("agcj @ %1").arg(address)); + auto nodes = functionsDoc.array(); + + QHash idMapping; + + auto getId = [&](const QString &name) -> uint64_t { + auto nextId = idMapping.size(); + auto &itemId = idMapping[name]; + if (idMapping.size() != nextId) { + itemId = nextId; + } + return itemId; + }; + + for (const QJsonValueRef &value : nodes) { + QJsonObject block = value.toObject(); + QString name = block["name"].toVariant().toString(); + + auto edges = block["imports"].toArray(); + GraphLayout::GraphBlock layoutBlock; + layoutBlock.entry = getId(name); + for (auto edge : edges) { + auto targetName = edge.toString(); + auto targetId = getId(targetName); + layoutBlock.edges.emplace_back(targetId); + } + + // it would be good if address came directly from json instead of having to lookup by name + addBlock(std::move(layoutBlock), name, Core()->num(name)); + } + for (auto it = idMapping.begin(), end = idMapping.end(); it != end; ++it) { + if (blocks.find(it.value()) == blocks.end()) { + GraphLayout::GraphBlock block; + block.entry = it.value(); + addBlock(std::move(block), it.key(), Core()->num(it.key())); + } + } + if (blockContent.empty() && !global) { + addBlock({}, RAddressString(address), address); + } + + addressMapping.clear(); + for (auto &it : blockContent) { + addressMapping[it.second.address] = it.first; + } + + computeGraphPlacement(); +} + +void CallGraphView::restoreCurrentBlock() +{ + if (!global && lastLoadedAddress != address) { + selectedBlock = NO_BLOCK_SELECTED; + lastLoadedAddress = address; + center(); + } else { + SimpleTextGraphView::restoreCurrentBlock(); + } +} diff --git a/src/widgets/CallGraph.h b/src/widgets/CallGraph.h new file mode 100644 index 00000000..f9c15814 --- /dev/null +++ b/src/widgets/CallGraph.h @@ -0,0 +1,49 @@ +#ifndef CALL_GRAPH_WIDGET_H +#define CALL_GRAPH_WIDGET_H + +#include "core/Cutter.h" +#include "AddressableDockWidget.h" +#include "widgets/SimpleTextGraphView.h" +#include "common/RefreshDeferrer.h" + +class MainWindow; +/** + * @brief Graphview displaying either global or function callgraph. + */ +class CallGraphView : public SimpleTextGraphView +{ + Q_OBJECT +public: + CallGraphView(CutterDockWidget *parent, MainWindow *main, bool global); + void showExportDialog() override; + void showAddress(RVA address); + void refreshView() override; +protected: + bool global; ///< is this a global or function callgraph + RVA address = RVA_INVALID; ///< function address if this is not a global callgraph + std::unordered_map addressMapping; ///< mapping from addresses to block id + void loadCurrentGraph() override; + void restoreCurrentBlock() override; +private: + RefreshDeferrer refreshDeferrer; + RVA lastLoadedAddress = RVA_INVALID; +}; + + +class CallGraphWidget : public AddressableDockWidget +{ + Q_OBJECT + +public: + explicit CallGraphWidget(MainWindow *main, bool global); + ~CallGraphWidget(); +protected: + QString getWindowTitle() const override; +private: + CallGraphView *graphView; + bool global; + + void onSeekChanged(RVA address); +}; + +#endif // CALL_GRAPH_WIDGET_H diff --git a/src/widgets/CutterGraphView.cpp b/src/widgets/CutterGraphView.cpp new file mode 100644 index 00000000..6addab41 --- /dev/null +++ b/src/widgets/CutterGraphView.cpp @@ -0,0 +1,439 @@ +#include "CutterGraphView.h" + +#include "core/Cutter.h" +#include "common/Configuration.h" +#include "dialogs/MultitypeFileSaveDialog.h" +#include "TempConfig.h" + +#include + +#include + +static const int KEY_ZOOM_IN = Qt::Key_Plus + Qt::ControlModifier; +static const int KEY_ZOOM_OUT = Qt::Key_Minus + Qt::ControlModifier; +static const int KEY_ZOOM_RESET = Qt::Key_Equal + Qt::ControlModifier; + +static const uint64_t BITMPA_EXPORT_WARNING_SIZE = 32 * 1024 * 1024; + +#ifndef NDEBUG +#define GRAPH_GRID_DEBUG_MODES true +#else +#define GRAPH_GRID_DEBUG_MODES false +#endif + +CutterGraphView::CutterGraphView(QWidget *parent) + : GraphView(parent) + , mFontMetrics(nullptr) + , actionExportGraph(tr("Export Graph"), this) + , graphLayout(GraphView::Layout::GridMedium) +{ + connect(Core(), &CutterCore::graphOptionsChanged, this, &CutterGraphView::refreshView); + connect(Config(), &Configuration::colorsUpdated, this, &CutterGraphView::colorsUpdatedSlot); + connect(Config(), &Configuration::fontsUpdated, this, &CutterGraphView::fontsUpdatedSlot); + + initFont(); + updateColors(); + + connect(&actionExportGraph, &QAction::triggered, this, &CutterGraphView::showExportDialog); + + layoutMenu = new QMenu(tr("Layout"), this); + horizontalLayoutAction = layoutMenu->addAction(tr("Horizontal")); + horizontalLayoutAction->setCheckable(true); + + static const std::pair LAYOUT_CONFIG[] = { + {tr("Grid narrow"), GraphView::Layout::GridNarrow} + , {tr("Grid medium"), GraphView::Layout::GridMedium} + , {tr("Grid wide"), GraphView::Layout::GridWide} +#if GRAPH_GRID_DEBUG_MODES + , {"GridAAA", GraphView::Layout::GridAAA} + , {"GridAAB", GraphView::Layout::GridAAB} + , {"GridABA", GraphView::Layout::GridABA} + , {"GridABB", GraphView::Layout::GridABB} + , {"GridBAA", GraphView::Layout::GridBAA} + , {"GridBAB", GraphView::Layout::GridBAB} + , {"GridBBA", GraphView::Layout::GridBBA} + , {"GridBBB", GraphView::Layout::GridBBB} +#endif +#ifdef CUTTER_ENABLE_GRAPHVIZ + , {tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline} + , {tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho} + , {tr("Graphviz sfdp"), GraphView::Layout::GraphvizSfdp} + , {tr("Graphviz neato"), GraphView::Layout::GraphvizNeato} + , {tr("Graphviz twopi"), GraphView::Layout::GraphvizTwoPi} + , {tr("Graphviz circo"), GraphView::Layout::GraphvizCirco} +#endif + }; + layoutMenu->addSeparator(); + connect(horizontalLayoutAction, &QAction::toggled, this, &CutterGraphView::updateLayout); + QActionGroup *layoutGroup = new QActionGroup(layoutMenu); + for (auto &item : LAYOUT_CONFIG) { + auto action = layoutGroup->addAction(item.first); + action->setCheckable(true); + GraphView::Layout layout = item.second; + if (layout == this->graphLayout) { + action->setChecked(true); + } + connect(action, &QAction::triggered, this, [this, layout]() { + this->graphLayout = layout; + updateLayout(); + }); + + } + layoutMenu->addActions(layoutGroup->actions()); +} + +QPoint CutterGraphView::getTextOffset(int line) const +{ + int padding = static_cast(2 * charWidth); + return QPoint(padding, padding + line * charHeight); +} + +void CutterGraphView::initFont() +{ + setFont(Config()->getFont()); + QFontMetricsF metrics(font()); + baseline = int(metrics.ascent()); + charWidth = metrics.width('X'); + charHeight = static_cast(metrics.height()); + charOffset = 0; + mFontMetrics.reset(new CachedFontMetrics(font())); +} + +void CutterGraphView::zoom(QPointF mouseRelativePos, double velocity) +{ + qreal newScale = getViewScale() * std::pow(1.25, velocity); + setZoom(mouseRelativePos, newScale); +} + +void CutterGraphView::setZoom(QPointF mouseRelativePos, double scale) +{ + mouseRelativePos.rx() *= size().width(); + mouseRelativePos.ry() *= size().height(); + mouseRelativePos /= getViewScale(); + + auto globalMouse = mouseRelativePos + getViewOffset(); + mouseRelativePos *= getViewScale(); + qreal newScale = scale; + newScale = std::max(newScale, 0.05); + mouseRelativePos /= newScale; + setViewScale(newScale); + + // Adjusting offset, so that zooming will be approaching to the cursor. + setViewOffset(globalMouse.toPoint() - mouseRelativePos.toPoint()); + + viewport()->update(); + emit viewZoomed(); +} + +void CutterGraphView::zoomIn() +{ + zoom(QPointF(0.5, 0.5), 1); +} + +void CutterGraphView::zoomOut() +{ + zoom(QPointF(0.5, 0.5), -1); +} + +void CutterGraphView::zoomReset() +{ + setZoom(QPointF(0.5, 0.5), 1); +} + +void CutterGraphView::showExportDialog() +{ + showExportGraphDialog("graph", "", RVA_INVALID); +} + +void CutterGraphView::updateColors() +{ + disassemblyBackgroundColor = ConfigColor("gui.alt_background"); + disassemblySelectedBackgroundColor = ConfigColor("gui.disass_selected"); + mDisabledBreakpointColor = disassemblyBackgroundColor; + graphNodeColor = ConfigColor("gui.border"); + backgroundColor = ConfigColor("gui.background"); + disassemblySelectionColor = ConfigColor("lineHighlight"); + PCSelectionColor = ConfigColor("highlightPC"); + + jmpColor = ConfigColor("graph.trufae"); + brtrueColor = ConfigColor("graph.true"); + brfalseColor = ConfigColor("graph.false"); + + mCommentColor = ConfigColor("comment"); +} + +void CutterGraphView::colorsUpdatedSlot() +{ + updateColors(); + refreshView(); +} + +GraphLayout::LayoutConfig CutterGraphView::getLayoutConfig() +{ + auto blockSpacing = Config()->getGraphBlockSpacing(); + auto edgeSpacing = Config()->getGraphEdgeSpacing(); + GraphLayout::LayoutConfig layoutConfig; + layoutConfig.blockHorizontalSpacing = blockSpacing.x(); + layoutConfig.blockVerticalSpacing = blockSpacing.y(); + layoutConfig.edgeHorizontalSpacing = edgeSpacing.x(); + layoutConfig.edgeVerticalSpacing = edgeSpacing.y(); + return layoutConfig; +} + +void CutterGraphView::updateLayout() +{ + setGraphLayout(GraphView::makeGraphLayout(graphLayout, horizontalLayoutAction->isChecked())); + saveCurrentBlock(); + setLayoutConfig(getLayoutConfig()); + computeGraphPlacement(); + restoreCurrentBlock(); + emit viewRefreshed(); +} + +void CutterGraphView::fontsUpdatedSlot() +{ + initFont(); + refreshView(); +} + +bool CutterGraphView::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::ShortcutOverride: { + QKeyEvent *keyEvent = static_cast(event); + int key = keyEvent->key() + keyEvent->modifiers(); + if (key == KEY_ZOOM_OUT || key == KEY_ZOOM_RESET + || key == KEY_ZOOM_IN || (key == (KEY_ZOOM_IN | Qt::ShiftModifier))) { + event->accept(); + return true; + } + break; + } + case QEvent::KeyPress: { + QKeyEvent *keyEvent = static_cast(event); + int key = keyEvent->key() + keyEvent->modifiers(); + if (key == KEY_ZOOM_IN || (key == (KEY_ZOOM_IN | Qt::ShiftModifier))) { + zoomIn(); + return true; + } else if (key == KEY_ZOOM_OUT) { + zoomOut(); + return true; + } else if (key == KEY_ZOOM_RESET) { + zoomReset(); + return true; + } + break; + } + default: + break; + } + return GraphView::event(event); +} + +void CutterGraphView::refreshView() +{ + initFont(); + setLayoutConfig(getLayoutConfig()); +} + + +void CutterGraphView::wheelEvent(QWheelEvent *event) +{ + // when CTRL is pressed, we zoom in/out with mouse wheel + if (Qt::ControlModifier == event->modifiers()) { + const QPoint numDegrees = event->angleDelta() / 8; + if (!numDegrees.isNull()) { + int numSteps = numDegrees.y() / 15; + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + QPointF relativeMousePos = event->pos(); +#else + QPointF relativeMousePos = event->position(); +#endif + relativeMousePos.rx() /= size().width(); + relativeMousePos.ry() /= size().height(); + + zoom(relativeMousePos, numSteps); + } + event->accept(); + } else { + // use mouse wheel for scrolling when CTRL is not pressed + GraphView::wheelEvent(event); + } + emit graphMoved(); +} + +void CutterGraphView::resizeEvent(QResizeEvent *event) +{ + GraphView::resizeEvent(event); + emit resized(); +} + +void CutterGraphView::saveCurrentBlock() +{ +} + +void CutterGraphView::restoreCurrentBlock() +{ +} + + +void CutterGraphView::mousePressEvent(QMouseEvent *event) +{ + GraphView::mousePressEvent(event); + emit graphMoved(); +} + +void CutterGraphView::mouseMoveEvent(QMouseEvent *event) +{ + GraphView::mouseMoveEvent(event); + emit graphMoved(); +} + +void CutterGraphView::exportGraph(QString filePath, GraphExportType type, QString graphCommand, + RVA address) +{ + bool graphTransparent = Config()->getBitmapTransparentState(); + double graphScaleFactor = Config()->getBitmapExportScaleFactor(); + switch (type) { + case GraphExportType::Png: + this->saveAsBitmap(filePath, "png", graphScaleFactor, graphTransparent); + break; + case GraphExportType::Jpeg: + this->saveAsBitmap(filePath, "jpg", graphScaleFactor, false); + break; + case GraphExportType::Svg: + this->saveAsSvg(filePath); + break; + + case GraphExportType::GVDot: + exportR2TextGraph(filePath, graphCommand + "d", address); + break; + case GraphExportType::R2Json: + exportR2TextGraph(filePath, graphCommand + "j", address); + break; + case GraphExportType::R2Gml: + exportR2TextGraph(filePath, graphCommand + "g", address); + break; + case GraphExportType::R2SDBKeyValue: + exportR2TextGraph(filePath, graphCommand + "k", address); + break; + + case GraphExportType::GVJson: + exportR2GraphvizGraph(filePath, "json", graphCommand, address); + break; + case GraphExportType::GVGif: + exportR2GraphvizGraph(filePath, "gif", graphCommand, address); + break; + case GraphExportType::GVPng: + exportR2GraphvizGraph(filePath, "png", graphCommand, address); + break; + case GraphExportType::GVJpeg: + exportR2GraphvizGraph(filePath, "jpg", graphCommand, address); + break; + case GraphExportType::GVPostScript: + exportR2GraphvizGraph(filePath, "ps", graphCommand, address); + break; + case GraphExportType::GVSvg: + exportR2GraphvizGraph(filePath, "svg", graphCommand, address); + break; + } +} + +void CutterGraphView::exportR2GraphvizGraph(QString filePath, QString type, QString graphCommand, + RVA address) +{ + TempConfig tempConfig; + tempConfig.set("graph.gv.format", type); + qWarning() << Core()->cmdRawAt(QString("%0w \"%1\"").arg(graphCommand).arg(filePath), address); +} + +void CutterGraphView::exportR2TextGraph(QString filePath, QString graphCommand, RVA address) +{ + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Can't open file"; + return; + } + QTextStream fileOut(&file); + fileOut << Core()->cmdRawAt(QString("%0").arg(graphCommand), address); +} + +bool CutterGraphView::graphIsBitamp(CutterGraphView::GraphExportType type) +{ + switch (type) { + case GraphExportType::Png: + case GraphExportType::Jpeg: + case GraphExportType::GVGif: + case GraphExportType::GVPng: + case GraphExportType::GVJpeg: + return true; + default: + return false; + } +} + + +Q_DECLARE_METATYPE(CutterGraphView::GraphExportType); + +void CutterGraphView::showExportGraphDialog(QString defaultName, QString graphCommand, RVA address) +{ + QVector types = { + {tr("PNG (*.png)"), "png", QVariant::fromValue(GraphExportType::Png)}, + {tr("JPEG (*.jpg)"), "jpg", QVariant::fromValue(GraphExportType::Jpeg)}, + {tr("SVG (*.svg)"), "svg", QVariant::fromValue(GraphExportType::Svg)} + }; + + bool r2GraphExports = !graphCommand.isEmpty(); + if (r2GraphExports) { + types.append({ + {tr("Graphviz dot (*.dot)"), "dot", QVariant::fromValue(GraphExportType::GVDot)}, + {tr("Graph Modelling Language (*.gml)"), "gml", QVariant::fromValue(GraphExportType::R2Gml)}, + {tr("R2 JSON (*.json)"), "json", QVariant::fromValue(GraphExportType::R2Json)}, + {tr("SDB key-value (*.txt)"), "txt", QVariant::fromValue(GraphExportType::R2SDBKeyValue)}, + }); + bool hasGraphviz = !QStandardPaths::findExecutable("dot").isEmpty() + || !QStandardPaths::findExecutable("xdot").isEmpty(); + if (hasGraphviz) { + types.append({ + {tr("Graphviz json (*.json)"), "json", QVariant::fromValue(GraphExportType::GVJson)}, + {tr("Graphviz gif (*.gif)"), "gif", QVariant::fromValue(GraphExportType::GVGif)}, + {tr("Graphviz png (*.png)"), "png", QVariant::fromValue(GraphExportType::GVPng)}, + {tr("Graphviz jpg (*.jpg)"), "jpg", QVariant::fromValue(GraphExportType::GVJpeg)}, + {tr("Graphviz PostScript (*.ps)"), "ps", QVariant::fromValue(GraphExportType::GVPostScript)}, + {tr("Graphviz svg (*.svg)"), "svg", QVariant::fromValue(GraphExportType::GVSvg)} + }); + } + } + + MultitypeFileSaveDialog dialog(this, tr("Export Graph")); + dialog.setTypes(types); + dialog.selectFile(defaultName); + if (!dialog.exec()) { + return; + } + + auto selectedType = dialog.selectedType(); + if (!selectedType.data.canConvert()) { + qWarning() << "Bad selected type, should not happen."; + return; + } + auto exportType = selectedType.data.value(); + + if (graphIsBitamp(exportType)) { + uint64_t bitmapSize = uint64_t(width) * uint64_t(height); + if (bitmapSize > BITMPA_EXPORT_WARNING_SIZE) { + auto answer = QMessageBox::question(this, + tr("Graph Export"), + tr("Do you really want to export %1 x %2 = %3 pixel bitmap image? Consider using different format.") + .arg(width).arg(height).arg(bitmapSize)); + if (answer != QMessageBox::Yes) { + return; + } + } + } + + QString filePath = dialog.selectedFiles().first(); + exportGraph(filePath, exportType, graphCommand, address); + +} diff --git a/src/widgets/CutterGraphView.h b/src/widgets/CutterGraphView.h new file mode 100644 index 00000000..4403802a --- /dev/null +++ b/src/widgets/CutterGraphView.h @@ -0,0 +1,147 @@ +#ifndef CUTTER_GRAPHVIEW_H +#define CUTTER_GRAPHVIEW_H + + +#include +#include +#include +#include + +#include "widgets/GraphView.h" +#include "common/CachedFontMetrics.h" + +/** + * @brief Common Cutter specific graph functionality. + */ +class CutterGraphView : public GraphView +{ + Q_OBJECT +public: + CutterGraphView(QWidget *parent); + virtual bool event(QEvent *event) override; + + enum class GraphExportType { + Png, Jpeg, Svg, GVDot, GVJson, + GVGif, GVPng, GVJpeg, GVPostScript, GVSvg, + R2Gml, R2SDBKeyValue, R2Json + }; + /** + * @brief Export graph to a file in the specified format + * @param filePath + * @param type export type, GV* and R2* types require \p graphCommand + * @param graphCommand r2 graph printing command without type, not required for direct image export + * @param address object address for commands like agf + */ + void exportGraph(QString filePath, GraphExportType type, QString graphCommand = "", RVA address = RVA_INVALID); + /** + * @brief Export image using r2 ag*w command and graphviz. + * Requires graphviz dot executable in the path. + * + * @param filePath output file path + * @param type image format as expected by "e graph.gv.format" + * @param graphCommand r2 command without type, for example agf + * @param address object address if required by command + */ + void exportR2GraphvizGraph(QString filePath, QString type, QString graphCommand, RVA address); + /** + * @brief Export graph in one of the text formats supported by r2 json, gml, SDB key-value + * @param filePath output file path + * @param graphCommand graph command including the format, example "agfd" or "agfg" + * @param address object address if required by command + */ + void exportR2TextGraph(QString filePath, QString graphCommand, RVA address); + static bool graphIsBitamp(GraphExportType type); + /** + * @brief Show graph export dialog. + * @param defaultName - default file name in the export dialog + * @param graphCommand - R2 graph commmand with graph type and without export type, for example afC. Leave empty + * for non-r2 graphs. In such case only direct image export will be available. + * @param address - object address if relevant for \p graphCommand + */ + void showExportGraphDialog(QString defaultName, QString graphCommand = "", + RVA address = RVA_INVALID); + +public slots: + virtual void refreshView(); + void updateColors(); + void fontsUpdatedSlot(); + + void zoom(QPointF mouseRelativePos, double velocity); + void setZoom(QPointF mouseRelativePos, double scale); + void zoomIn(); + void zoomOut(); + void zoomReset(); + + /** + * @brief Show the export file dialog. Override this to support r2 based export formats. + */ + virtual void showExportDialog(); +signals: + void viewRefreshed(); + void viewZoomed(); + void graphMoved(); + void resized(); +protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + /** + * @brief Save the the currently viewed or displayed block. + * Called before reloading graph. Override to this to implement graph specific logic for what block is selected. + * Default implementation does nothing. + */ + virtual void saveCurrentBlock(); + /** + * @brief Restore view focus and block last saved using saveCurrentBlock(). + * Called after the graph is reloaded. Default implementation does nothing. Can center the view if the new graph + * displays completely different content and the matching node doesn't exist. + */ + virtual void restoreCurrentBlock(); + + void initFont(); + QPoint getTextOffset(int line) const; + GraphLayout::LayoutConfig getLayoutConfig(); + virtual void updateLayout(); + + // Font data + std::unique_ptr> mFontMetrics; + qreal charWidth; + int charHeight; + int charOffset; + int baseline; + + // colors + QColor disassemblyBackgroundColor; + QColor disassemblySelectedBackgroundColor; + QColor disassemblySelectionColor; + QColor PCSelectionColor; + QColor jmpColor; + QColor brtrueColor; + QColor brfalseColor; + QColor retShadowColor; + QColor indirectcallShadowColor; + QColor mAutoCommentColor; + QColor mAutoCommentBackgroundColor; + QColor mCommentColor; + QColor mCommentBackgroundColor; + QColor mLabelColor; + QColor mLabelBackgroundColor; + QColor graphNodeColor; + QColor mAddressColor; + QColor mAddressBackgroundColor; + QColor mCipColor; + QColor mBreakpointColor; + QColor mDisabledBreakpointColor; + + QAction actionExportGraph; + + GraphView::Layout graphLayout; + QMenu *layoutMenu; + QAction *horizontalLayoutAction; +private: + void colorsUpdatedSlot(); +}; + +#endif // CUTTER_GRAPHVIEW_H diff --git a/src/widgets/DisassemblerGraphView.cpp b/src/widgets/DisassemblerGraphView.cpp index 7d40be70..19b9aa8b 100644 --- a/src/widgets/DisassemblerGraphView.cpp +++ b/src/widgets/DisassemblerGraphView.cpp @@ -5,12 +5,10 @@ #include "core/MainWindow.h" #include "common/Colors.h" #include "common/Configuration.h" -#include "common/CachedFontMetrics.h" #include "common/TempConfig.h" #include "common/SyntaxHighlighter.h" #include "common/BasicBlockHighlighter.h" #include "common/BasicInstructionHighlighter.h" -#include "dialogs/MultitypeFileSaveDialog.h" #include "common/Helpers.h" #include @@ -23,36 +21,20 @@ #include #include #include -#include -#include #include #include -#include #include #include #include #include -const int DisassemblerGraphView::KEY_ZOOM_IN = Qt::Key_Plus + Qt::ControlModifier; -const int DisassemblerGraphView::KEY_ZOOM_OUT = Qt::Key_Minus + Qt::ControlModifier; -const int DisassemblerGraphView::KEY_ZOOM_RESET = Qt::Key_Equal + Qt::ControlModifier; - -#ifndef NDEBUG -#define GRAPH_GRID_DEBUG_MODES true -#else -#define GRAPH_GRID_DEBUG_MODES false -#endif - DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *seekable, MainWindow *mainWindow, QList additionalMenuActions) - : GraphView(parent), - mFontMetrics(nullptr), - graphLayout(GraphView::Layout::GridMedium), + : CutterGraphView(parent), blockMenu(new DisassemblyContextMenu(this, mainWindow)), contextMenu(new QMenu(this)), seekable(seekable), - actionExportGraph(this), actionUnhighlight(this), actionUnhighlightInstruction(this) { @@ -67,12 +49,9 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se connect(Core(), SIGNAL(varsChanged()), this, SLOT(refreshView())); connect(Core(), SIGNAL(instructionChanged(RVA)), this, SLOT(refreshView())); connect(Core(), SIGNAL(functionsChanged()), this, SLOT(refreshView())); - connect(Core(), SIGNAL(graphOptionsChanged()), this, SLOT(refreshView())); connect(Core(), SIGNAL(asmOptionsChanged()), this, SLOT(refreshView())); connect(Core(), SIGNAL(refreshCodeViews()), this, SLOT(refreshView())); - connect(Config(), SIGNAL(colorsUpdated()), this, SLOT(colorsUpdatedSlot())); - connect(Config(), SIGNAL(fontsUpdated()), this, SLOT(fontsUpdatedSlot())); connectSeekChanged(false); // ESC for previous @@ -99,53 +78,9 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se shortcuts.append(shortcut_next_instr); shortcuts.append(shortcut_prev_instr); - // Export Graph menu - actionExportGraph.setText(tr("Export Graph")); - connect(&actionExportGraph, SIGNAL(triggered(bool)), this, SLOT(on_actionExportGraph_triggered())); - // Context menu that applies to everything contextMenu->addAction(&actionExportGraph); - static const std::pair LAYOUT_CONFIG[] = { - {tr("Grid narrow"), GraphView::Layout::GridNarrow} - , {tr("Grid medium"), GraphView::Layout::GridMedium} - , {tr("Grid wide"), GraphView::Layout::GridWide} - -#if GRAPH_GRID_DEBUG_MODES - , {"GridAAA", GraphView::Layout::GridAAA} - , {"GridAAB", GraphView::Layout::GridAAB} - , {"GridABA", GraphView::Layout::GridABA} - , {"GridABB", GraphView::Layout::GridABB} - , {"GridBAA", GraphView::Layout::GridBAA} - , {"GridBAB", GraphView::Layout::GridBAB} - , {"GridBBA", GraphView::Layout::GridBBA} - , {"GridBBB", GraphView::Layout::GridBBB} -#endif - -#ifdef CUTTER_ENABLE_GRAPHVIZ - , {tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline} - , {tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho} -#endif - }; - auto layoutMenu = contextMenu->addMenu(tr("Layout")); - horizontalLayoutAction = layoutMenu->addAction(tr("Horizontal")); - horizontalLayoutAction->setCheckable(true); - layoutMenu->addSeparator(); - connect(horizontalLayoutAction, &QAction::toggled, this, &DisassemblerGraphView::updateLayout); - QActionGroup *layoutGroup = new QActionGroup(layoutMenu); - for (auto &item : LAYOUT_CONFIG) { - auto action = layoutGroup->addAction(item.first); - action->setCheckable(true); - GraphView::Layout layout = item.second; - connect(action, &QAction::triggered, this, [this, layout]() { - this->graphLayout = layout; - updateLayout(); - }); - if (layout == this->graphLayout) { - action->setChecked(true); - } - } - layoutMenu->addActions(layoutGroup->actions()); - + contextMenu->addMenu(layoutMenu); contextMenu->addSeparator(); contextMenu->addActions(additionalMenuActions); @@ -199,10 +134,6 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se blockMenu->addSeparator(); blockMenu->addActions(contextMenu->actions()); - - initFont(); - colorsUpdatedSlot(); - connect(blockMenu, &DisassemblyContextMenu::copy, this, &DisassemblerGraphView::copySelection); // Add header as widget to layout so it stretches to the layout width @@ -232,8 +163,7 @@ DisassemblerGraphView::~DisassemblerGraphView() void DisassemblerGraphView::refreshView() { - initFont(); - setLayoutConfig(getLayoutConfig()); + CutterGraphView::refreshView(); loadCurrentGraph(); emit viewRefreshed(); } @@ -370,7 +300,7 @@ void DisassemblerGraphView::loadCurrentGraph() addBlock(gb); } - cleanupEdges(); + cleanupEdges(blocks); if (!func["blocks"].toArray().isEmpty()) { computeGraphPlacement(); @@ -416,36 +346,6 @@ void DisassemblerGraphView::prepareGraphNode(GraphBlock &block) block.height = (height * charHeight) + extra; } -void DisassemblerGraphView::cleanupEdges() -{ - for (auto &blockIt : blocks) { - auto &block = blockIt.second; - auto outIt = block.edges.begin(); - std::unordered_set seenEdges; - for (auto it = block.edges.begin(), end = block.edges.end(); it != end; ++it) { - // remove edges going to different functions - // and remove duplicate edges, common in switch statements - if (blocks.find(it->target) != blocks.end() && - seenEdges.find(it->target) == seenEdges.end()) { - *outIt++ = *it; - seenEdges.insert(it->target); - } - } - block.edges.erase(outIt, block.edges.end()); - } -} - -void DisassemblerGraphView::initFont() -{ - setFont(Config()->getFont()); - QFontMetricsF metrics(font()); - baseline = int(metrics.ascent()); - charWidth = metrics.width('X'); - charHeight = static_cast(metrics.height()); - charOffset = 0; - mFontMetrics.reset(new CachedFontMetrics(font())); -} - void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) { QRectF blockRect(block.x, block.y, block.width, block.height); @@ -721,33 +621,6 @@ void DisassemblerGraphView::showInstruction(GraphView::GraphBlock &block, RVA ad showRectangle(QRect(rect.x(), rect.y(), rect.width(), rect.height()), true); } -// Public Slots - -void DisassemblerGraphView::colorsUpdatedSlot() -{ - disassemblyBackgroundColor = ConfigColor("gui.alt_background"); - disassemblySelectedBackgroundColor = ConfigColor("gui.disass_selected"); - mDisabledBreakpointColor = disassemblyBackgroundColor; - graphNodeColor = ConfigColor("gui.border"); - backgroundColor = ConfigColor("gui.background"); - disassemblySelectionColor = ConfigColor("lineHighlight"); - PCSelectionColor = ConfigColor("highlightPC"); - - jmpColor = ConfigColor("graph.trufae"); - brtrueColor = ConfigColor("graph.true"); - brfalseColor = ConfigColor("graph.false"); - - mCommentColor = ConfigColor("comment"); - initFont(); - refreshView(); -} - -void DisassemblerGraphView::fontsUpdatedSlot() -{ - initFont(); - refreshView(); -} - DisassemblerGraphView::DisassemblyBlock *DisassemblerGraphView::blockForAddress(RVA addr) { for (auto &blockIt : disassembly_blocks) { @@ -794,51 +667,11 @@ void DisassemblerGraphView::onSeekChanged(RVA addr) if (db) { // This is a local address! We animated to it. transition_dont_seek = true; - showBlock(&blocks[db->entry], !switchFunction); + showBlock(blocks[db->entry], !switchFunction); showInstruction(blocks[db->entry], addr); } } -void DisassemblerGraphView::zoom(QPointF mouseRelativePos, double velocity) -{ - qreal newScale = getViewScale() * std::pow(1.25, velocity); - setZoom(mouseRelativePos, newScale); -} - -void DisassemblerGraphView::setZoom(QPointF mouseRelativePos, double scale) -{ - mouseRelativePos.rx() *= size().width(); - mouseRelativePos.ry() *= size().height(); - mouseRelativePos /= getViewScale(); - - auto globalMouse = mouseRelativePos + getViewOffset(); - mouseRelativePos *= getViewScale(); - qreal newScale = scale; - newScale = std::max(newScale, 0.05); - mouseRelativePos /= newScale; - setViewScale(newScale); - - // Adjusting offset, so that zooming will be approaching to the cursor. - setViewOffset(globalMouse.toPoint() - mouseRelativePos.toPoint()); - - viewport()->update(); - emit viewZoomed(); -} - -void DisassemblerGraphView::zoomIn() -{ - zoom(QPointF(0.5, 0.5), 1); -} - -void DisassemblerGraphView::zoomOut() -{ - zoom(QPointF(0.5, 0.5), -1); -} - -void DisassemblerGraphView::zoomReset() -{ - setZoom(QPointF(0.5, 0.5), 1); -} void DisassemblerGraphView::takeTrue() { @@ -895,18 +728,6 @@ void DisassemblerGraphView::seekInstruction(bool previous_instr) } } -GraphLayout::LayoutConfig DisassemblerGraphView::getLayoutConfig() -{ - auto blockSpacing = Config()->getGraphBlockSpacing(); - auto edgeSpacing = Config()->getGraphEdgeSpacing(); - GraphLayout::LayoutConfig layoutConfig; - layoutConfig.blockHorizontalSpacing = blockSpacing.x(); - layoutConfig.blockVerticalSpacing = blockSpacing.y(); - layoutConfig.edgeHorizontalSpacing = edgeSpacing.x(); - layoutConfig.edgeVerticalSpacing = edgeSpacing.y(); - return layoutConfig; -} - void DisassemblerGraphView::nextInstr() { seekInstruction(false); @@ -971,12 +792,6 @@ DisassemblerGraphView::Token *DisassemblerGraphView::getToken(Instr *instr, int return nullptr; } -QPoint DisassemblerGraphView::getTextOffset(int line) const -{ - int padding = static_cast(2 * charWidth); - return QPoint(padding, padding + line * charHeight); -} - QPoint DisassemblerGraphView::getInstructionOffset(const DisassemblyBlock &block, int line) const { return getTextOffset(line + static_cast(block.header_text.lines.size())); @@ -1006,7 +821,7 @@ void DisassemblerGraphView::blockClicked(GraphView::GraphBlock &block, QMouseEve } void DisassemblerGraphView::blockContextMenuRequested(GraphView::GraphBlock &block, - QContextMenuEvent *event, QPoint pos) + QContextMenuEvent *event, QPoint /*pos*/) { const RVA offset = this->seekable->getOffset(); actionUnhighlight.setVisible(Core()->getBBHighlighter()->getBasicBlock(block.entry)); @@ -1025,6 +840,21 @@ void DisassemblerGraphView::contextMenuEvent(QContextMenuEvent *event) } } +void DisassemblerGraphView::showExportDialog() +{ + QString defaultName = "graph"; + if (auto f = Core()->functionIn(currentFcnAddr)) { + QString functionName = f->name; + // don't confuse image type guessing and make c++ names somewhat usable + functionName.replace(QRegularExpression("[.:]"), "_"); + functionName.remove(QRegularExpression("[^a-zA-Z0-9_].*")); + if (!functionName.isEmpty()) { + defaultName = functionName; + } + } + showExportGraphDialog(defaultName, "agf", currentFcnAddr); +} + void DisassemblerGraphView::blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos) { @@ -1065,91 +895,6 @@ void DisassemblerGraphView::blockTransitionedTo(GraphView::GraphBlock *to) seekLocal(to->entry); } -bool DisassemblerGraphView::event(QEvent *event) -{ - switch (event->type()) { - case QEvent::ShortcutOverride: { - QKeyEvent *keyEvent = static_cast(event); - int key = keyEvent->key() + keyEvent->modifiers(); - if (key == KEY_ZOOM_OUT || key == KEY_ZOOM_RESET - || key == KEY_ZOOM_IN || (key == (KEY_ZOOM_IN | Qt::ShiftModifier))) { - event->accept(); - return true; - } - break; - } - case QEvent::KeyPress: { - QKeyEvent *keyEvent = static_cast(event); - int key = keyEvent->key() + keyEvent->modifiers(); - if (key == KEY_ZOOM_IN || (key == (KEY_ZOOM_IN | Qt::ShiftModifier))) { - zoomIn(); - return true; - } else if (key == KEY_ZOOM_OUT) { - zoomOut(); - return true; - } else if (key == KEY_ZOOM_RESET) { - zoomReset(); - return true; - } - break; - } - default: - break; - } - return GraphView::event(event); -} - - -Q_DECLARE_METATYPE(DisassemblerGraphView::GraphExportType); - -void DisassemblerGraphView::on_actionExportGraph_triggered() -{ - QVector types = { - {tr("PNG (*.png)"), "png", QVariant::fromValue(GraphExportType::Png)}, - {tr("JPEG (*.jpg)"), "jpg", QVariant::fromValue(GraphExportType::Jpeg)}, - {tr("SVG (*.svg)"), "svg", QVariant::fromValue(GraphExportType::Svg)} - }; - bool hasGraphviz = !QStandardPaths::findExecutable("dot").isEmpty() - || !QStandardPaths::findExecutable("xdot").isEmpty(); - if (hasGraphviz) { - types.append({ - {tr("Graphviz dot (*.dot)"), "dot", QVariant::fromValue(GraphExportType::GVDot)}, - {tr("Graphviz json (*.json)"), "json", QVariant::fromValue(GraphExportType::GVJson)}, - {tr("Graphviz gif (*.gif)"), "gif", QVariant::fromValue(GraphExportType::GVGif)}, - {tr("Graphviz png (*.png)"), "png", QVariant::fromValue(GraphExportType::GVPng)}, - {tr("Graphviz jpg (*.jpg)"), "jpg", QVariant::fromValue(GraphExportType::GVJpeg)}, - {tr("Graphviz PostScript (*.ps)"), "ps", QVariant::fromValue(GraphExportType::GVPostScript)}, - {tr("Graphviz svg (*.svg)"), "svg", QVariant::fromValue(GraphExportType::GVSvg)} - }); - } - - QString defaultName = "graph"; - if (auto f = Core()->functionIn(currentFcnAddr)) { - QString functionName = f->name; - // don't confuse image type guessing and make c++ names somewhat usable - functionName.replace(QRegularExpression("[.:]"), "_"); - functionName.remove(QRegularExpression("[^a-zA-Z0-9_].*")); - if (!functionName.isEmpty()) { - defaultName = functionName; - } - } - - - MultitypeFileSaveDialog dialog(this, tr("Export Graph")); - dialog.setTypes(types); - dialog.selectFile(defaultName); - if (!dialog.exec()) - return; - - auto selectedType = dialog.selectedType(); - if (!selectedType.data.canConvert()) { - qWarning() << "Bad selected type, should not happen."; - return; - } - QString filePath = dialog.selectedFiles().first(); - exportGraph(filePath, selectedType.data.value()); - -} void DisassemblerGraphView::onActionHighlightBITriggered() { @@ -1188,114 +933,11 @@ void DisassemblerGraphView::onActionUnhighlightBITriggered() Config()->colorsUpdated(); } -void DisassemblerGraphView::updateLayout() +void DisassemblerGraphView::restoreCurrentBlock() { - setGraphLayout(GraphView::makeGraphLayout(graphLayout, horizontalLayoutAction->isChecked())); - setLayoutConfig(getLayoutConfig()); - computeGraphPlacement(); - emit viewRefreshed(); onSeekChanged(this->seekable->getOffset()); // try to keep the view on current block } -void DisassemblerGraphView::exportGraph(QString filePath, GraphExportType type) -{ - bool graphTransparent = Config()->getBitmapTransparentState(); - double graphScaleFactor = Config()->getBitmapExportScaleFactor(); - switch (type) { - case GraphExportType::Png: - this->saveAsBitmap(filePath, "png", graphScaleFactor, graphTransparent); - break; - case GraphExportType::Jpeg: - this->saveAsBitmap(filePath, "jpg", graphScaleFactor, false); - break; - case GraphExportType::Svg: - this->saveAsSvg(filePath); - break; - - case GraphExportType::GVDot: { - QFile file(filePath); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Can't open file"; - return; - } - QTextStream fileOut(&file); - fileOut << Core()->cmdRaw(QString("agfd 0x%1").arg(currentFcnAddr, 0, 16)); - } - break; - - case GraphExportType::GVJson: - exportR2GraphvizGraph(filePath, "json"); - break; - case GraphExportType::GVGif: - exportR2GraphvizGraph(filePath, "gif"); - break; - case GraphExportType::GVPng: - exportR2GraphvizGraph(filePath, "png"); - break; - case GraphExportType::GVJpeg: - exportR2GraphvizGraph(filePath, "jpg"); - break; - case GraphExportType::GVPostScript: - exportR2GraphvizGraph(filePath, "ps"); - break; - case GraphExportType::GVSvg: - exportR2GraphvizGraph(filePath, "svg"); - break; - } -} - -void DisassemblerGraphView::exportR2GraphvizGraph(QString filePath, QString type) -{ - TempConfig tempConfig; - tempConfig.set("graph.gv.format", type); - qWarning() << Core()->cmdRawAt(QString("agfw \"%1\"") - .arg(filePath), - currentFcnAddr); -} - -void DisassemblerGraphView::mousePressEvent(QMouseEvent *event) -{ - GraphView::mousePressEvent(event); - emit graphMoved(); -} - -void DisassemblerGraphView::mouseMoveEvent(QMouseEvent *event) -{ - GraphView::mouseMoveEvent(event); - emit graphMoved(); -} - -void DisassemblerGraphView::wheelEvent(QWheelEvent *event) -{ - // when CTRL is pressed, we zoom in/out with mouse wheel - if (Qt::ControlModifier == event->modifiers()) { - const QPoint numDegrees = event->angleDelta() / 8; - if (!numDegrees.isNull()) { - int numSteps = numDegrees.y() / 15; - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QPointF relativeMousePos = event->pos(); -#else - QPointF relativeMousePos = event->position(); -#endif - relativeMousePos.rx() /= size().width(); - relativeMousePos.ry() /= size().height(); - - zoom(relativeMousePos, numSteps); - } - event->accept(); - } else { - // use mouse wheel for scrolling when CTRL is not pressed - GraphView::wheelEvent(event); - } - emit graphMoved(); -} - -void DisassemblerGraphView::resizeEvent(QResizeEvent *event) -{ - GraphView::resizeEvent(event); - emit resized(); -} void DisassemblerGraphView::paintEvent(QPaintEvent *event) { diff --git a/src/widgets/DisassemblerGraphView.h b/src/widgets/DisassemblerGraphView.h index 8837487d..3f8697c2 100644 --- a/src/widgets/DisassemblerGraphView.h +++ b/src/widgets/DisassemblerGraphView.h @@ -8,7 +8,7 @@ #include #include -#include "widgets/GraphView.h" +#include "widgets/CutterGraphView.h" #include "menus/DisassemblyContextMenu.h" #include "common/RichTextPainter.h" #include "common/CutterSeekable.h" @@ -16,7 +16,7 @@ class QTextEdit; class FallbackSyntaxHighlighter; -class DisassemblerGraphView : public GraphView +class DisassemblerGraphView : public CutterGraphView { Q_OBJECT @@ -101,7 +101,6 @@ public: GraphView::GraphBlock *to, bool interactive) override; virtual void blockTransitionedTo(GraphView::GraphBlock *to) override; - virtual bool event(QEvent *event) override; void loadCurrentGraph(); QString windowTitle; @@ -112,13 +111,6 @@ public: using EdgeConfigurationMapping = std::map, EdgeConfiguration>; EdgeConfigurationMapping getEdgeConfigurations(); - enum class GraphExportType { - Png, Jpeg, Svg, GVDot, GVJson, - GVGif, GVPng, GVJpeg, GVPostScript, GVSvg - }; - void exportGraph(QString filePath, GraphExportType type); - void exportR2GraphvizGraph(QString filePath, QString type); - /** * @brief keep the current addr of the fcn of Graph * Everytime overview updates its contents, it compares this value with the one in Graph @@ -126,16 +118,9 @@ public: */ ut64 currentFcnAddr = RVA_INVALID; // TODO: make this less public public slots: - void refreshView(); - void colorsUpdatedSlot(); - void fontsUpdatedSlot(); - void onSeekChanged(RVA addr); - void zoom(QPointF mouseRelativePos, double velocity); - void setZoom(QPointF mouseRelativePos, double scale); - void zoomIn(); - void zoomOut(); - void zoomReset(); + void refreshView() override; + void onSeekChanged(RVA addr); void takeTrue(); void takeFalse(); @@ -145,48 +130,31 @@ public slots: void copySelection(); protected: - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; void blockContextMenuRequested(GraphView::GraphBlock &block, QContextMenuEvent *event, QPoint pos) override; void contextMenuEvent(QContextMenuEvent *event) override; - + void restoreCurrentBlock() override; private slots: - void on_actionExportGraph_triggered(); + void showExportDialog() override; void onActionHighlightBITriggered(); void onActionUnhighlightBITriggered(); - void updateLayout(); private: bool transition_dont_seek = false; Token *highlight_token; - // Font data - std::unique_ptr> mFontMetrics; - qreal charWidth; - int charHeight; - int charOffset; - int baseline; bool emptyGraph; ut64 currentBlockAddress = RVA_INVALID; - GraphView::Layout graphLayout; - DisassemblyContextMenu *blockMenu; QMenu *contextMenu; - QAction* horizontalLayoutAction; void connectSeekChanged(bool disconnect); - void initFont(); void prepareGraphNode(GraphBlock &block); - void cleanupEdges(); Token *getToken(Instr *instr, int x); - QPoint getTextOffset(int line) const; + QPoint getInstructionOffset(const DisassemblyBlock &block, int line) const; RVA getAddrForMouseEvent(GraphBlock &block, QPoint *point); Instr *getInstrForMouseEvent(GraphBlock &block, QPoint *point, bool force = false); @@ -203,48 +171,17 @@ private: DisassemblyBlock *blockForAddress(RVA addr); void seekLocal(RVA addr, bool update_viewport = true); void seekInstruction(bool previous_instr); - GraphLayout::LayoutConfig getLayoutConfig(); CutterSeekable *seekable = nullptr; QList shortcuts; QList breakpoints; - QColor disassemblyBackgroundColor; - QColor disassemblySelectedBackgroundColor; - QColor disassemblySelectionColor; - QColor PCSelectionColor; - QColor jmpColor; - QColor brtrueColor; - QColor brfalseColor; - QColor retShadowColor; - QColor indirectcallShadowColor; - QColor mAutoCommentColor; - QColor mAutoCommentBackgroundColor; - QColor mCommentColor; - QColor mCommentBackgroundColor; - QColor mLabelColor; - QColor mLabelBackgroundColor; - QColor graphNodeColor; - QColor mAddressColor; - QColor mAddressBackgroundColor; - QColor mCipColor; - QColor mBreakpointColor; - QColor mDisabledBreakpointColor; - - QAction actionExportGraph; QAction actionUnhighlight; QAction actionUnhighlightInstruction; QLabel *emptyText = nullptr; - static const int KEY_ZOOM_IN; - static const int KEY_ZOOM_OUT; - static const int KEY_ZOOM_RESET; signals: - void viewRefreshed(); - void viewZoomed(); - void graphMoved(); - void resized(); void nameChanged(const QString &name); public: diff --git a/src/widgets/GraphGridLayout.cpp b/src/widgets/GraphGridLayout.cpp index 3d18eef4..b762fb20 100644 --- a/src/widgets/GraphGridLayout.cpp +++ b/src/widgets/GraphGridLayout.cpp @@ -262,6 +262,12 @@ void GraphGridLayout::CalculateLayout(GraphLayout::Graph &blocks, ut64 entry, in { LayoutState layoutState; layoutState.blocks = &blocks; + if (blocks.empty()) { + return; + } + if (blocks.find(entry) == blocks.end()) { + entry = blocks.begin()->first; + } for (auto &it : blocks) { GridBlock block; @@ -492,7 +498,7 @@ void GraphGridLayout::computeAllBlockPlacement(const std::vector &blockOrd if (block.row == 0) { // place all the roots first auto offset = -block.leftPosition; block.col += nextEmptyColumn + offset; - nextEmptyColumn = block.rightPosition + offset; + nextEmptyColumn = block.rightPosition + offset + nextEmptyColumn; } } // Visit all nodes top to bottom, converting relative positions to absolute. @@ -824,13 +830,11 @@ void calculateSegmentOffsets( * @param segmentOffsets offsets relative to the left side edge column. * @param edgeColumnWidth widths of edge columns * @param segments either all horizontal or all vertical edge segments - * @param minSpacing spacing between segments */ static void centerEdges( std::vector &segmentOffsets, const std::vector &edgeColumnWidth, - const std::vector &segments, - int minSpacing) + const std::vector &segments) { /* Split segments in each edge column into non intersecting chunks. Center each chunk separately. * @@ -864,18 +868,19 @@ static void centerEdges( auto it = events.begin(); while (it != events.end()) { + int left, right; + left = right = segmentOffsets[it->index]; auto chunkStart = it++; int activeSegmentCount = 1; - int chunkWidth = 0; + while (activeSegmentCount > 0) { activeSegmentCount += it->start ? 1 : -1; - chunkWidth = std::max(chunkWidth, segmentOffsets[it->index]); + int offset = segmentOffsets[it->index]; + left = std::min(left, offset); + right = std::max(right, offset); it++; } - // leftMost segment position includes padding on the left side so add it on the right side as well - chunkWidth += minSpacing; - - int spacing = (std::max(edgeColumnWidth[chunkStart->x], minSpacing) - chunkWidth) / 2; + int spacing = (edgeColumnWidth[chunkStart->x] - (right - left)) / 2 - left; for (auto segment = chunkStart; segment != it; segment++) { if (segment->start) { segmentOffsets[segment->index] += spacing; @@ -977,7 +982,7 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state edgeOffsets.resize(edgeIndex); calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides, state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing); - centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.edgeHorizontalSpacing); + centerEdges(edgeOffsets, state.edgeColumnWidth, segments); edgeIndex = 0; auto copySegmentsToEdges = [&](bool col) { @@ -985,7 +990,22 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state for (auto &edgeListIt : state.edge) { for (auto &edge : edgeListIt.second) { for (size_t j = col ? 1 : 2; j < edge.points.size(); j += 2) { - edge.points[j].offset = edgeOffsets[edgeIndex++]; + int offset = edgeOffsets[edgeIndex++]; + if (col) { + GraphBlock *block = nullptr; + if (j == 1) { + block = &(*state.blocks)[edgeListIt.first]; + } else if (j + 1 == edge.points.size()) { + block = &(*state.blocks)[edge.dest]; + } + if (block) { + int blockWidth = block->width; + int edgeColumWidth = state.edgeColumnWidth[edge.points[j].col]; + offset = std::max(-blockWidth / 2 + edgeColumWidth/ 2, offset); + offset = std::min(edgeColumWidth / 2 + std::min(blockWidth, edgeColumWidth) / 2, offset); + } + } + edge.points[j].offset = offset; } } } @@ -1203,7 +1223,7 @@ using Constraint = std::pair, int>; * @param objectiveFunction coefficients for function \f$\sum c_i x_i\f$ which needs to be minimized * @param inequalities inequality constraints \f$x_{e_i} - x_{f_i} \leq b_i\f$ * @param equalities equality constraints \f$x_{e_i} - x_{f_i} = b_i\f$ - * @param solution input/output argument, returns results, needs to be initialized with a viable solution + * @param solution input/output argument, returns results, needs to be initialized with a feasible solution * @param stickWhenNotMoving variable grouping strategy */ static void optimizeLinearProgramPass( @@ -1283,7 +1303,7 @@ static void optimizeLinearProgramPass( }; for (auto &equality : equalities) { - // process equalities, assumes that initial solution is viable solution and matches equality constraints + // process equalities, assumes that initial solution is feasible solution and matches equality constraints int a = getGroup(equality.first.first); int b = getGroup(equality.first.second); if (a == b) { @@ -1363,6 +1383,11 @@ static void optimizeLinearProgramPass( } } assert(smallestMove != INT_MAX); + if (smallestMove == INT_MAX) { + // Unbound variable, this means that linear program wasn't set up correctly. + // Better don't change it instead of stretching the graph to infinity. + smallestMove = 0; + } solution[g] += smallestMove; if (smallestMove == 0 && stickWhenNotMoving == false) { continue; @@ -1388,7 +1413,7 @@ static void optimizeLinearProgramPass( * @param objectiveFunction coefficients for function \f$\sum c_i x_i\f$ which needs to be minimized * @param inequalities inequality constraints \f$x_{e_i} - x_{f_i} \leq b_i\f$ * @param equalities equality constraints \f$x_{e_i} - x_{f_i} = b_i\f$ - * @param solution input/output argument, returns results, needs to be initialized with a viable solution + * @param solution input/output argument, returns results, needs to be initialized with a feasible solution */ static void optimizeLinearProgram( size_t n, @@ -1527,12 +1552,15 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const int blockVariable = blockMapping[blockId]; equalities.push_back({{blockVariable, edgeVariable}, blockPos - edgeVariablePos}); }; - auto setViableSolution = [&](size_t variable, int value) { + auto setFeasibleSolution = [&](size_t variable, int value) { solution.resize(std::max(solution.size(), variable + 1)); solution[variable] = value; }; auto copyVariablesToPositions = [&](const std::vector &solution, bool horizontal = false) { + for (auto v : solution) { + assert(v >= 0); + } size_t variableIndex = blockMapping.size(); for (auto &blockIt : *state.blocks) { auto &block = blockIt.second; @@ -1582,7 +1610,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const int x = edge.polyline[i].y(); segments.push_back({x, int(variableIndex), y0, y1}); variableGroups.push_back(blockMapping.size() + edgeIndex); - setViableSolution(variableIndex, x); + setFeasibleSolution(variableIndex, x); if (i > 2) { int prevX = edge.polyline[i - 2].y(); addObjective(variableIndex, x, variableIndex - 1, prevX); @@ -1593,7 +1621,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const } segments.push_back({block.y, blockVariable, block.x, block.x + block.width}); segments.push_back({block.y + block.height, blockVariable, block.x, block.x + block.width}); - setViableSolution(blockVariable, block.y); + setFeasibleSolution(blockVariable, block.y); } createInequalitiesFromSegments(std::move(segments), solution, variableGroups, blockMapping.size(), @@ -1601,9 +1629,6 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const objectiveFunction.resize(solution.size()); optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution); - for (auto v : solution) { - assert(v >= 0); - } copyVariablesToPositions(solution, true); connectEdgeEnds(*state.blocks); @@ -1632,7 +1657,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const int x = edge.polyline[i].x(); segments.push_back({x, int(variableIndex), y0, y1}); variableGroups.push_back(blockMapping.size() + edgeIndex); - setViableSolution(variableIndex, x); + setFeasibleSolution(variableIndex, x); if (i > 2) { int prevX = edge.polyline[i - 2].x(); addObjective(variableIndex, x, variableIndex - 1, prevX); @@ -1647,7 +1672,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const int blockVariable = blockMapping[blockIt.first]; segments.push_back({block.x, blockVariable, block.y, block.y + block.height}); segments.push_back({block.x + block.width, blockVariable, block.y, block.y + block.height}); - setViableSolution(blockVariable, block.x); + setFeasibleSolution(blockVariable, block.x); } createInequalitiesFromSegments(std::move(segments), solution, variableGroups, blockMapping.size(), diff --git a/src/widgets/GraphHorizontalAdapter.cpp b/src/widgets/GraphHorizontalAdapter.cpp index 343eb5a3..929db8e7 100644 --- a/src/widgets/GraphHorizontalAdapter.cpp +++ b/src/widgets/GraphHorizontalAdapter.cpp @@ -49,6 +49,7 @@ void GraphHorizontalAdapter::setLayoutConfig(const GraphLayout::LayoutConfig &co { GraphLayout::setLayoutConfig(config); swapLayoutConfigDirection(); + layout->setLayoutConfig(config); } void GraphHorizontalAdapter::swapLayoutConfigDirection() diff --git a/src/widgets/GraphView.cpp b/src/widgets/GraphView.cpp index 2a44d6fd..08b84dba 100644 --- a/src/widgets/GraphView.cpp +++ b/src/widgets/GraphView.cpp @@ -45,20 +45,12 @@ GraphView::~GraphView() } // Callbacks -void GraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) -{ - Q_UNUSED(p) - Q_UNUSED(block) - Q_UNUSED(interactive) - qWarning() << "Draw block not overriden!"; -} void GraphView::blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos) { Q_UNUSED(block); Q_UNUSED(event); Q_UNUSED(pos); - qWarning() << "Block clicked not overridden!"; } void GraphView::blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos) @@ -66,7 +58,6 @@ void GraphView::blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *ev Q_UNUSED(block); Q_UNUSED(event); Q_UNUSED(pos); - qWarning() << "Block double clicked not overridden!"; } void GraphView::blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos) @@ -89,7 +80,6 @@ bool GraphView::helpEvent(QHelpEvent *event) void GraphView::blockTransitionedTo(GraphView::GraphBlock *to) { Q_UNUSED(to); - qWarning() << "blockTransitionedTo not overridden!"; } GraphView::EdgeConfiguration GraphView::edgeConfiguration(GraphView::GraphBlock &from, @@ -134,10 +124,29 @@ void GraphView::computeGraphPlacement() { graphLayoutSystem->CalculateLayout(blocks, entry, width, height); setCacheDirty(); - ready = true; + clampViewOffset(); viewport()->update(); } +void GraphView::cleanupEdges(GraphLayout::Graph &graph) +{ + for (auto &blockIt : graph) { + auto &block = blockIt.second; + auto outIt = block.edges.begin(); + std::unordered_set seenEdges; + for (auto it = block.edges.begin(), end = block.edges.end(); it != end; ++it) { + // remove edges going to different functions + // and remove duplicate edges, common in switch statements + if (graph.find(it->target) != graph.end() && + seenEdges.find(it->target) == seenEdges.end()) { + *outIt++ = *it; + seenEdges.insert(it->target); + } + } + block.edges.erase(outIt, block.edges.end()); + } +} + void GraphView::beginMouseDrag(QMouseEvent *event) { scroll_base_x = event->x(); @@ -399,10 +408,10 @@ void GraphView::paint(QPainter &p, QPoint offset, QRect viewport, qreal scale, b void GraphView::saveAsBitmap(QString path, const char *format, double scaler, bool transparent) { - QImage image(width*scaler, height*scaler, QImage::Format_ARGB32); - if(transparent){ + QImage image(width * scaler, height * scaler, QImage::Format_ARGB32); + if (transparent) { image.fill(qRgba(0, 0, 0, 0)); - }else{ + } else { image.fill(backgroundColor); } QPainter p; @@ -456,13 +465,8 @@ void GraphView::centerY(bool emitSignal) void GraphView::showBlock(GraphBlock &block, bool anywhere) { - showBlock(&block, anywhere); -} - -void GraphView::showBlock(GraphBlock *block, bool anywhere) -{ - showRectangle(QRect(block->x, block->y, block->width, block->height), anywhere); - blockTransitionedTo(block); + showRectangle(QRect(block.x, block.y, block.width, block.height), anywhere); + blockTransitionedTo(&block); } void GraphView::showRectangle(const QRect &block, bool anywhere) @@ -512,6 +516,11 @@ QPoint GraphView::viewToLogicalCoordinates(QPoint p) return p / current_scale + offset; } +QPoint GraphView::logicalToViewCoordinates(QPoint p) +{ + return (p - offset) * current_scale; +} + void GraphView::setGraphLayout(std::unique_ptr layout) { graphLayoutSystem = std::move(layout); @@ -529,6 +538,15 @@ std::unique_ptr GraphView::makeGraphLayout(GraphView::Layout layout { std::unique_ptr result; bool needAdapter = true; + +#ifdef CUTTER_ENABLE_GRAPHVIZ + auto makeGraphvizLayout = [&](GraphvizLayout::LayoutType type) { + result.reset(new GraphvizLayout(type, + horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB)); + needAdapter = false; + }; +#endif + switch (layout) { case Layout::GridNarrow: result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Narrow)); @@ -546,8 +564,7 @@ std::unique_ptr GraphView::makeGraphLayout(GraphView::Layout layout case Layout::GridBAA: case Layout::GridBAB: case Layout::GridBBA: - case Layout::GridBBB: - { + case Layout::GridBBB: { int options = static_cast(layout) - static_cast(Layout::GridAAA); std::unique_ptr gridLayout(new GraphGridLayout()); gridLayout->setTightSubtreePlacement((options & 1) == 0); @@ -558,14 +575,22 @@ std::unique_ptr GraphView::makeGraphLayout(GraphView::Layout layout } #ifdef CUTTER_ENABLE_GRAPHVIZ case Layout::GraphvizOrtho: - result.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho, - horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB)); - needAdapter = false; + makeGraphvizLayout(GraphvizLayout::LayoutType::DotOrtho); break; case Layout::GraphvizPolyline: - result.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline, - horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB)); - needAdapter = false; + makeGraphvizLayout(GraphvizLayout::LayoutType::DotPolyline); + break; + case Layout::GraphvizSfdp: + makeGraphvizLayout(GraphvizLayout::LayoutType::Sfdp); + break; + case Layout::GraphvizNeato: + makeGraphvizLayout(GraphvizLayout::LayoutType::Neato); + break; + case Layout::GraphvizTwoPi: + makeGraphvizLayout(GraphvizLayout::LayoutType::TwoPi); + break; + case Layout::GraphvizCirco: + makeGraphvizLayout(GraphvizLayout::LayoutType::Circo); break; #endif } diff --git a/src/widgets/GraphView.h b/src/widgets/GraphView.h index 35823a7b..36c921fb 100644 --- a/src/widgets/GraphView.h +++ b/src/widgets/GraphView.h @@ -53,6 +53,10 @@ public: #ifdef CUTTER_ENABLE_GRAPHVIZ , GraphvizOrtho , GraphvizPolyline + , GraphvizSfdp + , GraphvizNeato + , GraphvizTwoPi + , GraphvizCirco #endif }; static std::unique_ptr makeGraphLayout(Layout layout, bool horizontal = false); @@ -69,7 +73,6 @@ public: ~GraphView() override; void showBlock(GraphBlock &block, bool anywhere = false); - void showBlock(GraphBlock *block, bool anywhere = false); /** * @brief Move view so that area is visible. * @param rect Rectangle to show @@ -83,19 +86,28 @@ public: */ GraphView::GraphBlock *getBlockContaining(QPoint p); QPoint viewToLogicalCoordinates(QPoint p); + QPoint logicalToViewCoordinates(QPoint p); void setGraphLayout(std::unique_ptr layout); - GraphLayout& getGraphLayout() const { return *graphLayoutSystem; } - void setLayoutConfig(const GraphLayout::LayoutConfig& config); + GraphLayout &getGraphLayout() const { return *graphLayoutSystem; } + void setLayoutConfig(const GraphLayout::LayoutConfig &config); void paint(QPainter &p, QPoint offset, QRect area, qreal scale = 1.0, bool interactive = true); - void saveAsBitmap(QString path, const char *format = nullptr, double scaler = 1.0, bool transparent = false); + void saveAsBitmap(QString path, const char *format = nullptr, double scaler = 1.0, + bool transparent = false); void saveAsSvg(QString path); void computeGraphPlacement(); + + /** + * @brief Remove duplicate edges and edges without target in graph. + * @param graph + */ + static void cleanupEdges(GraphLayout::Graph &graph); protected: std::unordered_map blocks; + /// image background color QColor backgroundColor = QColor(Qt::white); // Padding inside the block @@ -113,7 +125,7 @@ protected: * @param block * @param interactive - can be used for disabling elemnts during export */ - virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive = true); + virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive = true) = 0; virtual void blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos); virtual void blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos); virtual void blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos); @@ -122,6 +134,14 @@ protected: virtual void wheelEvent(QWheelEvent *event) override; virtual EdgeConfiguration edgeConfiguration(GraphView::GraphBlock &from, GraphView::GraphBlock *to, bool interactive = true); + /** + * @brief Called when user requested context menu for a block. Should open a block specific contextmenu. + * Typically triggered by right click. + * @param block - the block that was clicked on + * @param event - context menu event that triggered the callback, can be used to display context menu + * at correct position + * @param pos - mouse click position in logical coordinates of the drawing, set only if event reason is mouse + */ virtual void blockContextMenuRequested(GraphView::GraphBlock &block, QContextMenuEvent *event, QPoint pos); @@ -159,12 +179,10 @@ private: QPoint offset = QPoint(0, 0); - ut64 entry; + ut64 entry = 0; std::unique_ptr graphLayoutSystem; - bool ready = false; - // Scrolling data int scroll_base_x = 0; int scroll_base_y = 0; diff --git a/src/widgets/GraphvizLayout.cpp b/src/widgets/GraphvizLayout.cpp index c5468e8f..ea6bc24d 100644 --- a/src/widgets/GraphvizLayout.cpp +++ b/src/widgets/GraphvizLayout.cpp @@ -11,10 +11,10 @@ #include -GraphvizLayout::GraphvizLayout(LineType lineType, Direction direction) +GraphvizLayout::GraphvizLayout(LayoutType lineType, Direction direction) : GraphLayout({}) , direction(direction) - , lineType(lineType) + , layoutType(lineType) { } @@ -103,7 +103,7 @@ void GraphvizLayout::CalculateLayout(std::unordered_map &block strc.reserve(2 * blocks.size()); std::map, Agedge_t *> edges; - agsafeset(g, STR("splines"), lineType == LineType::Ortho ? STR("ortho") : STR("polyline"), STR("")); + agsafeset(g, STR("splines"), layoutType == LayoutType::DotOrtho ? STR("ortho") : STR("polyline"), STR("")); switch (direction) { case Direction::LR: agsafeset(g, STR("rankdir"), STR("LR"), STR("")); @@ -154,7 +154,27 @@ void GraphvizLayout::CalculateLayout(std::unordered_map &block setFloatingPointAttr(u, heightAatr, block.height / dpi); } - gvLayout(gvc, g, "dot"); + const char *layoutEngine = "dot"; + switch (layoutType) { + case LayoutType::DotOrtho: + case LayoutType::DotPolyline: + layoutEngine = "dot"; + break; + case LayoutType::Sfdp: + layoutEngine = "sfdp"; + break; + case LayoutType::Neato: + layoutEngine = "neato"; + break; + case LayoutType::TwoPi: + layoutEngine = "twopi"; + break; + case LayoutType::Circo: + layoutEngine = "circo"; + break; + } + + gvLayout(gvc, g, layoutEngine); for (auto &blockIt : blocks) { auto &block = blockIt.second; @@ -198,7 +218,7 @@ void GraphvizLayout::CalculateLayout(std::unordered_map &block auto it = std::prev(edge.polyline.end()); QPointF direction = *it; direction -= *(--it); - edge.arrow = getArrowDirection(direction, lineType == LineType::Polyline); + edge.arrow = getArrowDirection(direction, layoutType == LayoutType::DotPolyline); } else { edge.arrow = GraphEdge::Down; diff --git a/src/widgets/GraphvizLayout.h b/src/widgets/GraphvizLayout.h index 531ff31e..bc6475e1 100644 --- a/src/widgets/GraphvizLayout.h +++ b/src/widgets/GraphvizLayout.h @@ -7,22 +7,26 @@ class GraphvizLayout : public GraphLayout { public: - enum class LineType { - Ortho, - Polyline + enum class LayoutType { + DotOrtho, + DotPolyline, + Sfdp, + Neato, + TwoPi, + Circo, }; enum class Direction { TB, LR }; - GraphvizLayout(LineType lineType, Direction direction = Direction::TB); + GraphvizLayout(LayoutType layoutType, Direction direction = Direction::TB); virtual void CalculateLayout(std::unordered_map &blocks, ut64 entry, int &width, int &height) const override; private: Direction direction; - LineType lineType; + LayoutType layoutType; }; #endif // GRAPHVIZLAYOUT_H diff --git a/src/widgets/MemoryDockWidget.cpp b/src/widgets/MemoryDockWidget.cpp index bf5bc3d4..657ffb3d 100644 --- a/src/widgets/MemoryDockWidget.cpp +++ b/src/widgets/MemoryDockWidget.cpp @@ -7,21 +7,12 @@ #include MemoryDockWidget::MemoryDockWidget(MemoryWidgetType type, MainWindow *parent) - : CutterDockWidget(parent) + : AddressableDockWidget(parent) , mType(type) - , seekable(new CutterSeekable(this)) - , syncAction(tr("Sync/unsync offset"), this) { if (parent) { parent->addMemoryDockWidget(this); } - connect(seekable, &CutterSeekable::syncChanged, this, &MemoryDockWidget::updateWindowTitle); - connect(&syncAction, &QAction::triggered, seekable, &CutterSeekable::toggleSynchronization); - - dockMenu = new QMenu(this); - dockMenu->addAction(&syncAction); - - setContextMenuPolicy(Qt::ContextMenuPolicy::DefaultContextMenu); } bool MemoryDockWidget::tryRaiseMemoryWidget() @@ -38,13 +29,6 @@ bool MemoryDockWidget::tryRaiseMemoryWidget() return true; } -void MemoryDockWidget::raiseMemoryWidget() -{ - show(); - raise(); - widgetToFocusOnRaise()->setFocus(Qt::FocusReason::TabFocusReason); -} - bool MemoryDockWidget::eventFilter(QObject *object, QEvent *event) { if (mainWindow && event->type() == QEvent::FocusIn) { @@ -52,40 +36,3 @@ bool MemoryDockWidget::eventFilter(QObject *object, QEvent *event) } return CutterDockWidget::eventFilter(object, event); } - -QVariantMap MemoryDockWidget::serializeViewProprties() -{ - auto result = CutterDockWidget::serializeViewProprties(); - result["synchronized"] = seekable->isSynchronized(); - return result; -} - -void MemoryDockWidget::deserializeViewProperties(const QVariantMap &properties) -{ - QVariant synchronized = properties.value("synchronized", true); - seekable->setSynchronization(synchronized.toBool()); -} - -void MemoryDockWidget::updateWindowTitle() -{ - QString name = getWindowTitle(); - QString id = getDockNumber(); - if (!id.isEmpty()) { - name += " " + id; - } - if (!seekable->isSynchronized()) { - name += CutterSeekable::tr(" (unsynced)"); - } - setWindowTitle(name); -} - -void MemoryDockWidget::contextMenuEvent(QContextMenuEvent *event) -{ - event->accept(); - dockMenu->exec(mapToGlobal(event->pos())); -} - -CutterSeekable *MemoryDockWidget::getSeekable() const -{ - return seekable; -} diff --git a/src/widgets/MemoryDockWidget.h b/src/widgets/MemoryDockWidget.h index e0533f6e..44472f96 100644 --- a/src/widgets/MemoryDockWidget.h +++ b/src/widgets/MemoryDockWidget.h @@ -1,49 +1,30 @@ #ifndef MEMORYDOCKWIDGET_H #define MEMORYDOCKWIDGET_H -#include "CutterDockWidget.h" +#include "AddressableDockWidget.h" #include "core/Cutter.h" #include -class CutterSeekable; - /* Disassembly/Graph/Hexdump/Decompiler view priority */ enum class MemoryWidgetType { Disassembly, Graph, Hexdump, Decompiler }; -class MemoryDockWidget : public CutterDockWidget +class MemoryDockWidget : public AddressableDockWidget { Q_OBJECT public: MemoryDockWidget(MemoryWidgetType type, MainWindow *parent); ~MemoryDockWidget() override {} - CutterSeekable *getSeekable() const; - bool tryRaiseMemoryWidget(); - void raiseMemoryWidget(); MemoryWidgetType getType() const { return mType; } bool eventFilter(QObject *object, QEvent *event) override; - - QVariantMap serializeViewProprties() override; - void deserializeViewProperties(const QVariantMap &properties) override; private: MemoryWidgetType mType; - -public slots: - void updateWindowTitle(); - -protected: - CutterSeekable *seekable = nullptr; - QAction syncAction; - QMenu *dockMenu = nullptr; - - virtual QString getWindowTitle() const = 0; - void contextMenuEvent(QContextMenuEvent *event) override; }; #endif // MEMORYDOCKWIDGET_H diff --git a/src/widgets/R2GraphWidget.cpp b/src/widgets/R2GraphWidget.cpp new file mode 100644 index 00000000..09fcd557 --- /dev/null +++ b/src/widgets/R2GraphWidget.cpp @@ -0,0 +1,120 @@ +#include "R2GraphWidget.h" +#include "ui_R2GraphWidget.h" + +#include +#include +#include + +R2GraphWidget::R2GraphWidget(MainWindow *main) + : CutterDockWidget(main) + , ui(new Ui::R2GraphWidget) + , graphView(new GenericR2GraphView(this, main)) +{ + ui->setupUi(this); + ui->verticalLayout->addWidget(graphView); + connect(ui->refreshButton, &QPushButton::pressed, this, [this]() { + graphView->refreshView(); + }); + struct GraphType { + QChar commandChar; + QString label; + } types[] = { + {'a', tr("aga - Data reference graph")}, + {'A', tr("agA - Global data references graph")}, + // {'c', tr("c - Function callgraph")}, + // {'C', tr("C - Global callgraph")}, + // {'f', tr("f - Basic blocks function graph")}, + {'i', tr("agi - Imports graph")}, + {'r', tr("agr - References graph")}, + {'R', tr("agR - Global references graph")}, + {'x', tr("agx - Cross references graph")}, + {'g', tr("agg - Custom graph")}, + }; + for (auto &graphType : types) { + ui->graphType->addItem(graphType.label, graphType.commandChar); + } + connect(ui->graphType, &QComboBox::currentTextChanged, this, &R2GraphWidget::typeChanged); +} + +R2GraphWidget::~R2GraphWidget() +{ +} + +void R2GraphWidget::typeChanged() +{ + auto currentData = ui->graphType->currentData(); + if (currentData.isNull()) { + graphView->setGraphCommand(ui->graphType->currentText()); + } else { + auto command = QString("ag%1").arg(currentData.toChar()); + graphView->setGraphCommand(command); + } +} + +GenericR2GraphView::GenericR2GraphView(R2GraphWidget *parent, MainWindow *main) + : SimpleTextGraphView(parent, main) + , refreshDeferrer(nullptr, this) +{ + refreshDeferrer.registerFor(parent); + connect(&refreshDeferrer, &RefreshDeferrer::refreshNow, this, &GenericR2GraphView::refreshView); +} + +void GenericR2GraphView::setGraphCommand(QString cmd) +{ + graphCommand = cmd; +} + +void GenericR2GraphView::refreshView() +{ + if (!refreshDeferrer.attemptRefresh(nullptr)) { + return; + } + SimpleTextGraphView::refreshView(); +} + +void GenericR2GraphView::loadCurrentGraph() +{ + blockContent.clear(); + blocks.clear(); + + if (graphCommand.isEmpty()) { + return; + } + + QJsonDocument functionsDoc = Core()->cmdj(QString("%1j").arg(graphCommand)); + auto nodes = functionsDoc.object()["nodes"].toArray(); + + for (const QJsonValueRef &value : nodes) { + QJsonObject block = value.toObject(); + uint64_t id = block["id"].toVariant().toULongLong(); + + QString content; + QString title = block["title"].toString(); + QString body = block["body"].toString(); + if (!title.isEmpty() && !body.isEmpty()) { + content = title + "/n" + body; + } else { + content = title + body; + } + + auto edges = block["out_nodes"].toArray(); + GraphLayout::GraphBlock layoutBlock; + layoutBlock.entry = id; + for (auto edge : edges) { + auto targetId = edge.toVariant().toULongLong(); + layoutBlock.edges.emplace_back(targetId); + } + + addBlock(std::move(layoutBlock), content); + } + + cleanupEdges(blocks); + + computeGraphPlacement(); + + if (graphCommand != lastShownCommand) { + selectedBlock = NO_BLOCK_SELECTED; + lastShownCommand = graphCommand; + center(); + } +} diff --git a/src/widgets/R2GraphWidget.h b/src/widgets/R2GraphWidget.h new file mode 100644 index 00000000..576f676f --- /dev/null +++ b/src/widgets/R2GraphWidget.h @@ -0,0 +1,68 @@ +#ifndef R2_GRAPH_WIDGET_H +#define R2_GRAPH_WIDGET_H + +#include + +#include "core/Cutter.h" +#include "CutterDockWidget.h" +#include "widgets/SimpleTextGraphView.h" +#include "common/RefreshDeferrer.h" + +class MainWindow; + +namespace Ui { +class R2GraphWidget; +} + +class R2GraphWidget; + +/** + * @brief Generic graph view for r2 graphs. + * Not all r2 graph commands output the same kind of json. Only those that have following format + * @code{.json} + * { "nodes": [ + * { + * "id": 0, + * "tittle": "node_0_tittle", + * "body": "". + * "out_nodes": [1, 2, 3] + * }, + * ... + * ]} + * @endcode + * Id don't have to be sequential. Simple text label is displayed containing concatenation of + * label and body. No r2 builtin graph uses both. Duplicate edges and edges with target id + * not present in the list of nodes are removed. + */ +class GenericR2GraphView : public SimpleTextGraphView +{ + Q_OBJECT +public: + GenericR2GraphView(R2GraphWidget *parent, MainWindow *main); + void setGraphCommand(QString cmd); + void refreshView() override; +protected: + void loadCurrentGraph() override; +private: + RefreshDeferrer refreshDeferrer; + QString graphCommand; + QString lastShownCommand; +}; + + +class R2GraphWidget : public CutterDockWidget +{ + Q_OBJECT + +public: + explicit R2GraphWidget(MainWindow *main); + ~R2GraphWidget(); + +private: + std::unique_ptr ui; + GenericR2GraphView *graphView; + + void typeChanged(); +}; + +#endif // R2_GRAPH_WIDGET_H diff --git a/src/widgets/R2GraphWidget.ui b/src/widgets/R2GraphWidget.ui new file mode 100644 index 00000000..5a05bc5c --- /dev/null +++ b/src/widgets/R2GraphWidget.ui @@ -0,0 +1,66 @@ + + + R2GraphWidget + + + + 0 + 0 + 439 + 162 + + + + R2 graphs + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + ag + + + + + + + + 0 + 0 + + + + true + + + + + + + Refresh + + + + + + + + + + + diff --git a/src/widgets/SimpleTextGraphView.cpp b/src/widgets/SimpleTextGraphView.cpp new file mode 100644 index 00000000..11f95515 --- /dev/null +++ b/src/widgets/SimpleTextGraphView.cpp @@ -0,0 +1,257 @@ + +#include "SimpleTextGraphView.h" +#include "core/Cutter.h" +#include "core/MainWindow.h" +#include "common/Configuration.h" +#include "common/SyntaxHighlighter.h" +#include "common/Helpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +SimpleTextGraphView::SimpleTextGraphView(QWidget *parent, MainWindow *mainWindow) + : CutterGraphView(parent), + contextMenu(new QMenu(this)), + addressableItemContextMenu(this, mainWindow), + copyAction(tr("Copy"), this) +{ + copyAction.setShortcut(QKeySequence::StandardKey::Copy); + copyAction.setShortcutContext(Qt::WidgetShortcut); + connect(©Action, &QAction::triggered, this, &SimpleTextGraphView::copyBlockText); + + contextMenu->addAction(©Action); + contextMenu->addAction(&actionExportGraph); + contextMenu->addMenu(layoutMenu); + + addressableItemContextMenu.insertAction(addressableItemContextMenu.actions().first(), ©Action); + addressableItemContextMenu.addSeparator(); + addressableItemContextMenu.addAction(&actionExportGraph); + addressableItemContextMenu.addMenu(layoutMenu); + + addActions(addressableItemContextMenu.actions()); + addAction(©Action); + enableAddresses(haveAddresses); +} + +SimpleTextGraphView::~SimpleTextGraphView() +{ + for (QShortcut *shortcut : shortcuts) { + delete shortcut; + } +} + +void SimpleTextGraphView::refreshView() +{ + initFont(); + setLayoutConfig(getLayoutConfig()); + saveCurrentBlock(); + loadCurrentGraph(); + if (blocks.find(selectedBlock) == blocks.end()) { + selectedBlock = NO_BLOCK_SELECTED; + } + restoreCurrentBlock(); + emit viewRefreshed(); +} + +void SimpleTextGraphView::selectBlockWithId(ut64 blockId) +{ + if (!enableBlockSelection) { + return; + } + auto contentIt = blockContent.find(blockId); + if (contentIt != blockContent.end()) { + selectedBlock = blockId; + if (haveAddresses) { + addressableItemContextMenu.setTarget(contentIt->second.address, contentIt->second.text); + } + viewport()->update(); + } else { + selectedBlock = NO_BLOCK_SELECTED; + } +} + +void SimpleTextGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) +{ + QRectF blockRect(block.x, block.y, block.width, block.height); + + const qreal padding = charWidth; + + p.setPen(Qt::black); + p.setBrush(Qt::gray); + p.setFont(Config()->getFont()); + p.drawRect(blockRect); + + // Render node + auto &content = blockContent[block.entry]; + + p.setPen(QColor(0, 0, 0, 0)); + p.setBrush(QColor(0, 0, 0, 100)); + p.setPen(QPen(graphNodeColor, 1)); + + bool blockSelected = interactive && (block.entry == selectedBlock); + if (blockSelected) { + p.setBrush(disassemblySelectedBackgroundColor); + } else { + p.setBrush(disassemblyBackgroundColor); + } + // Draw basic block background + p.drawRect(blockRect); + + // Stop rendering text when it's too small + auto transform = p.combinedTransform(); + QRect screenChar = transform.mapRect(QRect(0, 0, charWidth, charHeight)); + + if (screenChar.width() * qhelpers::devicePixelRatio(p.device()) < 4) { + return; + } + + p.setPen(palette().color(QPalette::WindowText)); + // Render node text + auto x = block.x + padding; + int y = block.y + padding + p.fontMetrics().ascent(); + p.drawText(QPoint(x, y), content.text); +} + +GraphView::EdgeConfiguration SimpleTextGraphView::edgeConfiguration(GraphView::GraphBlock &from, + GraphView::GraphBlock *to, + bool interactive) +{ + EdgeConfiguration ec; + ec.color = jmpColor; + ec.start_arrow = false; + ec.end_arrow = true; + if (interactive && (selectedBlock == from.entry || selectedBlock == to->entry)) { + ec.width_scale = 2.0; + } + return ec; +} + +void SimpleTextGraphView::setBlockSelectionEnabled(bool value) +{ + enableBlockSelection = value; + if (!value) { + selectedBlock = NO_BLOCK_SELECTED; + } +} + +void SimpleTextGraphView::addBlock(GraphLayout::GraphBlock block, const QString &text, RVA address) +{ + auto &content = blockContent[block.entry]; + content.text = text; + content.address = address; + + int height = 1; + int width = mFontMetrics->width(text); + int extra = static_cast(2 * charWidth); + block.width = static_cast(width + extra); + block.height = (height * charHeight) + extra; + GraphView::addBlock(std::move(block)); +} + +void SimpleTextGraphView::enableAddresses(bool enabled) +{ + haveAddresses = enabled; + if (!enabled) { + addressableItemContextMenu.clearTarget(); + // Clearing addreassable item context menu disables all the actions inside it including extra ones added + // by SimpleTextGraphView. Re-enable them because they are in the regular context menu as well and shouldn't be + // disabled. + for (auto action : contextMenu->actions()) { + action->setEnabled(true); + } + }; +} + +void SimpleTextGraphView::copyBlockText() +{ + auto blockIt = blockContent.find(selectedBlock); + if (blockIt != blockContent.end()) { + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(blockIt->second.text); + } +} + +void SimpleTextGraphView::contextMenuEvent(QContextMenuEvent *event) +{ + GraphView::contextMenuEvent(event); + if (!event->isAccepted() && + event->reason() != QContextMenuEvent::Mouse && + enableBlockSelection && selectedBlock != NO_BLOCK_SELECTED) { + auto blockIt = blocks.find(selectedBlock); + if (blockIt != blocks.end()) { + blockContextMenuRequested(blockIt->second, event, {}); + } + } + if (!event->isAccepted()) { + contextMenu->exec(event->globalPos()); + event->accept(); + } +} + +void SimpleTextGraphView::blockContextMenuRequested(GraphView::GraphBlock &block, + QContextMenuEvent *event, QPoint /*pos*/) +{ + if (haveAddresses) { + const auto &content = blockContent[block.entry]; + addressableItemContextMenu.setTarget(content.address, content.text); + QPoint pos = event->globalPos(); + + if (event->reason() != QContextMenuEvent::Mouse) { + QPoint blockPosition(block.x + block.width / 2, block.y + block.height / 2); + blockPosition = logicalToViewCoordinates(blockPosition); + if (viewport()->rect().contains(blockPosition)) { + pos = mapToGlobal(blockPosition); + } + } + addressableItemContextMenu.exec(pos); + event->accept(); + } + +} + +void SimpleTextGraphView::blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, + QPoint /*pos*/) +{ + if (haveAddresses) { + QToolTip::showText(event->globalPos(), RAddressString(blockContent[block.entry].address)); + } +} + +void SimpleTextGraphView::blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, + QPoint /*pos*/) +{ + if ((event->button() == Qt::LeftButton || event->button() == Qt::RightButton) + && enableBlockSelection) { + selectBlockWithId(block.entry); + } +} + +void SimpleTextGraphView::restoreCurrentBlock() +{ + if (enableBlockSelection) { + auto blockIt = blocks.find(selectedBlock); + if (blockIt != blocks.end()) { + showBlock(blockIt->second, true); + } + } +} + +void SimpleTextGraphView::paintEvent(QPaintEvent *event) +{ + // SimpleTextGraphView is always dirty + setCacheDirty(); + GraphView::paintEvent(event); +} diff --git a/src/widgets/SimpleTextGraphView.h b/src/widgets/SimpleTextGraphView.h new file mode 100644 index 00000000..cb13a9f4 --- /dev/null +++ b/src/widgets/SimpleTextGraphView.h @@ -0,0 +1,85 @@ +#ifndef SIMPLE_TEXT_GRAPHVIEW_H +#define SIMPLE_TEXT_GRAPHVIEW_H + +// Based on the DisassemblerGraphView from x64dbg + +#include +#include +#include +#include + +#include "widgets/CutterGraphView.h" +#include "menus/AddressableItemContextMenu.h" +#include "common/RichTextPainter.h" +#include "common/CutterSeekable.h" + +/** + * @brief Graphview with nodes containing simple plaintext labels. + */ +class SimpleTextGraphView : public CutterGraphView +{ + Q_OBJECT +public: + SimpleTextGraphView(QWidget *parent, MainWindow *mainWindow); + ~SimpleTextGraphView() override; + virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) override; + virtual GraphView::EdgeConfiguration edgeConfiguration(GraphView::GraphBlock &from, + GraphView::GraphBlock *to, + bool interactive) override; + + /** + * @brief Enable or disable block selection. + * Selecting a block highlights it and allows copying the label. Enabled by default. + * @param value + */ + void setBlockSelectionEnabled(bool value); +public slots: + void refreshView() override; + /** + * @brief Select a given block. Requires block selection to be enabled. + */ + void selectBlockWithId(ut64 blockId); +protected: + void paintEvent(QPaintEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + void blockContextMenuRequested(GraphView::GraphBlock &block, QContextMenuEvent *event, + QPoint pos) override; + void blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos)override; + void blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos) override; + + void restoreCurrentBlock() override; + + /** + * @brief Load the graph to be displayed. + * Needs to cleanup the old graph and use addBlock() to create new nodes. + */ + virtual void loadCurrentGraph() = 0; + void addBlock(GraphLayout::GraphBlock block, const QString &content, RVA address = RVA_INVALID); + /** + * @brief Enable or disable address interactions for nodes. + * If enabled node addresses need to be specified when calling addBlock(). Adds address related + * items to the node context menu. By default disabled. + * @param enabled + */ + void enableAddresses(bool enabled); + + struct BlockContent { + QString text; + RVA address; + }; + std::unordered_map blockContent; + + QList shortcuts; + QMenu *contextMenu; + AddressableItemContextMenu addressableItemContextMenu; + QAction copyAction; + + static const ut64 NO_BLOCK_SELECTED = RVA_INVALID; + ut64 selectedBlock = NO_BLOCK_SELECTED; + bool enableBlockSelection = true; + bool haveAddresses = false; +private: + void copyBlockText(); +}; + +#endif // SIMPLE_TEXT_GRAPHVIEW_H