Add more graph widgets (#2273)

* Add generic r2 graph.
* Add Callgraph widgets
* Add more graphviz layouts.
* Fix some edge cases in graphGridLayout that were more likely to appear in callgraphs
* Refactor the code moving some of the logic out of disassemblyGraphWidget making it more reusable
This commit is contained in:
karliss 2020-07-16 11:05:10 +03:00 committed by GitHub
parent ca84c3d1dc
commit e5d7bd660a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1733 additions and 612 deletions

View File

@ -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 \

View File

@ -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 <QApplication>
@ -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<CutterDockWidget *> 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<MemoryDockWidget *>(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<AddressableDockWidget *>(dock)) {
if (qobject_cast<MemoryDockWidget *>(dock)) {
continue;
}
QAction *action = new QAction(memoryWidget->windowTitle(), menu);
connect(action, &QAction::triggered, this, [memoryWidget, address]() {
memoryWidget->getSeekable()->seek(address);
memoryWidget->raiseMemoryWidget();
});

View File

@ -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;

View File

@ -108,7 +108,7 @@
<item row="1" column="1">
<widget class="QSpinBox" name="horizontalBlockSpacing">
<property name="maximum">
<number>100</number>
<number>400</number>
</property>
<property name="singleStep">
<number>10</number>
@ -121,7 +121,7 @@
<item row="1" column="2">
<widget class="QSpinBox" name="verticalBlockSpacing">
<property name="maximum">
<number>100</number>
<number>400</number>
</property>
<property name="singleStep">
<number>10</number>
@ -137,7 +137,7 @@
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
<number>400</number>
</property>
<property name="singleStep">
<number>5</number>
@ -153,7 +153,7 @@
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
<number>400</number>
</property>
<property name="singleStep">
<number>5</number>

View File

@ -0,0 +1,65 @@
#include "AddressableDockWidget.h"
#include "common/CutterSeekable.h"
#include "MainWindow.h"
#include <QAction>
#include <QEvent>
#include <QMenu>
#include <QContextMenuEvent>
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;
}

View File

@ -0,0 +1,36 @@
#ifndef ADDRESSABLE_DOCK_WIDGET_H
#define ADDRESSABLE_DOCK_WIDGET_H
#include "CutterDockWidget.h"
#include "core/Cutter.h"
#include <QAction>
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

144
src/widgets/CallGraph.cpp Normal file
View File

@ -0,0 +1,144 @@
#include "CallGraph.h"
#include "MainWindow.h"
#include <QJsonValue>
#include <QJsonArray>
#include <QJsonObject>
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<QString, uint64_t> 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();
}
}

49
src/widgets/CallGraph.h Normal file
View File

@ -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<RVA, ut64> 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

View File

@ -0,0 +1,439 @@
#include "CutterGraphView.h"
#include "core/Cutter.h"
#include "common/Configuration.h"
#include "dialogs/MultitypeFileSaveDialog.h"
#include "TempConfig.h"
#include <cmath>
#include <QStandardPaths>
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<QString, GraphView::Layout> 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<int>(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<int>(metrics.height());
charOffset = 0;
mFontMetrics.reset(new CachedFontMetrics<qreal>(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<QKeyEvent *>(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<QKeyEvent *>(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<MultitypeFileSaveDialog::TypeDescription> 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<GraphExportType>()) {
qWarning() << "Bad selected type, should not happen.";
return;
}
auto exportType = selectedType.data.value<GraphExportType>();
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);
}

View File

@ -0,0 +1,147 @@
#ifndef CUTTER_GRAPHVIEW_H
#define CUTTER_GRAPHVIEW_H
#include <QWidget>
#include <QPainter>
#include <QShortcut>
#include <QLabel>
#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<CachedFontMetrics<qreal>> 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

View File

@ -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 <QColorDialog>
@ -23,36 +21,20 @@
#include <QToolTip>
#include <QTextDocument>
#include <QTextEdit>
#include <QFileDialog>
#include <QFile>
#include <QVBoxLayout>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QClipboard>
#include <QApplication>
#include <QAction>
#include <cmath>
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<QAction *> 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<QString, GraphView::Layout> 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<ut64> 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<int>(metrics.height());
charOffset = 0;
mFontMetrics.reset(new CachedFontMetrics<qreal>(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<int>(2 * charWidth);
return QPoint(padding, padding + line * charHeight);
}
QPoint DisassemblerGraphView::getInstructionOffset(const DisassemblyBlock &block, int line) const
{
return getTextOffset(line + static_cast<int>(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<QKeyEvent *>(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<QKeyEvent *>(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<MultitypeFileSaveDialog::TypeDescription> 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<GraphExportType>()) {
qWarning() << "Bad selected type, should not happen.";
return;
}
QString filePath = dialog.selectedFiles().first();
exportGraph(filePath, selectedType.data.value<GraphExportType>());
}
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)
{

View File

@ -8,7 +8,7 @@
#include <QShortcut>
#include <QLabel>
#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<std::pair<ut64, ut64>, 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<CachedFontMetrics<qreal>> 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<QShortcut *> shortcuts;
QList<RVA> 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:

View File

@ -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<ut64> &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<int> &segmentOffsets,
const std::vector<int> &edgeColumnWidth,
const std::vector<EdgeSegment> &segments,
int minSpacing)
const std::vector<EdgeSegment> &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<std::pair<int, int>, 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<int> &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(),

View File

@ -49,6 +49,7 @@ void GraphHorizontalAdapter::setLayoutConfig(const GraphLayout::LayoutConfig &co
{
GraphLayout::setLayoutConfig(config);
swapLayoutConfigDirection();
layout->setLayoutConfig(config);
}
void GraphHorizontalAdapter::swapLayoutConfigDirection()

View File

@ -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<ut64> 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<GraphLayout> layout)
{
graphLayoutSystem = std::move(layout);
@ -529,6 +538,15 @@ std::unique_ptr<GraphLayout> GraphView::makeGraphLayout(GraphView::Layout layout
{
std::unique_ptr<GraphLayout> 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<GraphLayout> GraphView::makeGraphLayout(GraphView::Layout layout
case Layout::GridBAA:
case Layout::GridBAB:
case Layout::GridBBA:
case Layout::GridBBB:
{
case Layout::GridBBB: {
int options = static_cast<int>(layout) - static_cast<int>(Layout::GridAAA);
std::unique_ptr<GraphGridLayout> gridLayout(new GraphGridLayout());
gridLayout->setTightSubtreePlacement((options & 1) == 0);
@ -558,14 +575,22 @@ std::unique_ptr<GraphLayout> 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
}

View File

@ -53,6 +53,10 @@ public:
#ifdef CUTTER_ENABLE_GRAPHVIZ
, GraphvizOrtho
, GraphvizPolyline
, GraphvizSfdp
, GraphvizNeato
, GraphvizTwoPi
, GraphvizCirco
#endif
};
static std::unique_ptr<GraphLayout> 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<GraphLayout> 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<ut64, GraphBlock> 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<GraphLayout> graphLayoutSystem;
bool ready = false;
// Scrolling data
int scroll_base_x = 0;
int scroll_base_y = 0;

View File

@ -11,10 +11,10 @@
#include <gvc.h>
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<ut64, GraphBlock> &block
strc.reserve(2 * blocks.size());
std::map<std::pair<ut64, ut64>, 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<ut64, GraphBlock> &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<ut64, GraphBlock> &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;

View File

@ -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<ut64, GraphBlock> &blocks,
ut64 entry,
int &width,
int &height) const override;
private:
Direction direction;
LineType lineType;
LayoutType layoutType;
};
#endif // GRAPHVIZLAYOUT_H

View File

@ -7,21 +7,12 @@
#include <QContextMenuEvent>
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;
}

View File

@ -1,49 +1,30 @@
#ifndef MEMORYDOCKWIDGET_H
#define MEMORYDOCKWIDGET_H
#include "CutterDockWidget.h"
#include "AddressableDockWidget.h"
#include "core/Cutter.h"
#include <QAction>
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

View File

@ -0,0 +1,120 @@
#include "R2GraphWidget.h"
#include "ui_R2GraphWidget.h"
#include <QJsonValue>
#include <QJsonArray>
#include <QJsonObject>
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();
}
}

View File

@ -0,0 +1,68 @@
#ifndef R2_GRAPH_WIDGET_H
#define R2_GRAPH_WIDGET_H
#include <memory>
#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::R2GraphWidget> ui;
GenericR2GraphView *graphView;
void typeChanged();
};
#endif // R2_GRAPH_WIDGET_H

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>R2GraphWidget</class>
<widget class="QDockWidget" name="R2GraphWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>439</width>
<height>162</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">R2 graphs</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>ag</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="graphType">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshButton">
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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 <QPainter>
#include <QJsonObject>
#include <QJsonArray>
#include <QMouseEvent>
#include <QPropertyAnimation>
#include <QShortcut>
#include <QToolTip>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QStandardPaths>
#include <QClipboard>
#include <QApplication>
#include <QAction>
#include <cmath>
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(&copyAction, &QAction::triggered, this, &SimpleTextGraphView::copyBlockText);
contextMenu->addAction(&copyAction);
contextMenu->addAction(&actionExportGraph);
contextMenu->addMenu(layoutMenu);
addressableItemContextMenu.insertAction(addressableItemContextMenu.actions().first(), &copyAction);
addressableItemContextMenu.addSeparator();
addressableItemContextMenu.addAction(&actionExportGraph);
addressableItemContextMenu.addMenu(layoutMenu);
addActions(addressableItemContextMenu.actions());
addAction(&copyAction);
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<int>(2 * charWidth);
block.width = static_cast<int>(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);
}

View File

@ -0,0 +1,85 @@
#ifndef SIMPLE_TEXT_GRAPHVIEW_H
#define SIMPLE_TEXT_GRAPHVIEW_H
// Based on the DisassemblerGraphView from x64dbg
#include <QWidget>
#include <QPainter>
#include <QShortcut>
#include <QLabel>
#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<ut64, BlockContent> blockContent;
QList<QShortcut *> 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