mirror of
https://github.com/rizinorg/cutter.git
synced 2024-12-18 19:06:10 +00:00
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:
parent
ca84c3d1dc
commit
e5d7bd660a
@ -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 \
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
65
src/widgets/AddressableDockWidget.cpp
Normal file
65
src/widgets/AddressableDockWidget.cpp
Normal 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;
|
||||
}
|
36
src/widgets/AddressableDockWidget.h
Normal file
36
src/widgets/AddressableDockWidget.h
Normal 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
144
src/widgets/CallGraph.cpp
Normal 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
49
src/widgets/CallGraph.h
Normal 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
|
439
src/widgets/CutterGraphView.cpp
Normal file
439
src/widgets/CutterGraphView.cpp
Normal 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);
|
||||
|
||||
}
|
147
src/widgets/CutterGraphView.h
Normal file
147
src/widgets/CutterGraphView.h
Normal 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
|
@ -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)
|
||||
{
|
||||
|
@ -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:
|
||||
|
@ -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(),
|
||||
|
@ -49,6 +49,7 @@ void GraphHorizontalAdapter::setLayoutConfig(const GraphLayout::LayoutConfig &co
|
||||
{
|
||||
GraphLayout::setLayoutConfig(config);
|
||||
swapLayoutConfigDirection();
|
||||
layout->setLayoutConfig(config);
|
||||
}
|
||||
|
||||
void GraphHorizontalAdapter::swapLayoutConfigDirection()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
120
src/widgets/R2GraphWidget.cpp
Normal file
120
src/widgets/R2GraphWidget.cpp
Normal 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();
|
||||
}
|
||||
}
|
68
src/widgets/R2GraphWidget.h
Normal file
68
src/widgets/R2GraphWidget.h
Normal 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
|
66
src/widgets/R2GraphWidget.ui
Normal file
66
src/widgets/R2GraphWidget.ui
Normal 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>
|
257
src/widgets/SimpleTextGraphView.cpp
Normal file
257
src/widgets/SimpleTextGraphView.cpp
Normal 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(©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<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);
|
||||
}
|
85
src/widgets/SimpleTextGraphView.h
Normal file
85
src/widgets/SimpleTextGraphView.h
Normal 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
|
Loading…
Reference in New Issue
Block a user