mirror of
https://github.com/rizinorg/cutter.git
synced 2024-12-20 03:46:11 +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 \
|
dialogs/LayoutManager.cpp \
|
||||||
common/CutterLayout.cpp \
|
common/CutterLayout.cpp \
|
||||||
widgets/GraphHorizontalAdapter.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 = \
|
GRAPHVIZ_SOURCES = \
|
||||||
widgets/GraphvizLayout.cpp
|
widgets/GraphvizLayout.cpp
|
||||||
@ -581,7 +586,12 @@ HEADERS += \
|
|||||||
common/BinaryTrees.h \
|
common/BinaryTrees.h \
|
||||||
common/LinkedListPool.h \
|
common/LinkedListPool.h \
|
||||||
widgets/GraphHorizontalAdapter.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
|
GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h
|
||||||
|
|
||||||
@ -648,7 +658,8 @@ FORMS += \
|
|||||||
widgets/ColorPicker.ui \
|
widgets/ColorPicker.ui \
|
||||||
dialogs/preferences/ColorThemeEditDialog.ui \
|
dialogs/preferences/ColorThemeEditDialog.ui \
|
||||||
widgets/ListDockWidget.ui \
|
widgets/ListDockWidget.ui \
|
||||||
dialogs/LayoutManager.ui
|
dialogs/LayoutManager.ui \
|
||||||
|
widgets/R2GraphWidget.ui
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc \
|
resources.qrc \
|
||||||
|
@ -69,6 +69,8 @@
|
|||||||
#include "widgets/HexdumpWidget.h"
|
#include "widgets/HexdumpWidget.h"
|
||||||
#include "widgets/DecompilerWidget.h"
|
#include "widgets/DecompilerWidget.h"
|
||||||
#include "widgets/HexWidget.h"
|
#include "widgets/HexWidget.h"
|
||||||
|
#include "widgets/R2GraphWidget.h"
|
||||||
|
#include "widgets/CallGraph.h"
|
||||||
|
|
||||||
// Qt Headers
|
// Qt Headers
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
@ -371,7 +373,10 @@ void MainWindow::initDocks()
|
|||||||
segmentsDock = new SegmentsWidget(this),
|
segmentsDock = new SegmentsWidget(this),
|
||||||
symbolsDock = new SymbolsWidget(this),
|
symbolsDock = new SymbolsWidget(this),
|
||||||
vTablesDock = new VTablesWidget(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) {
|
auto makeActionList = [this](QList<CutterDockWidget *> docks) {
|
||||||
@ -590,6 +595,11 @@ void MainWindow::finalizeOpen()
|
|||||||
// Add fortune message
|
// Add fortune message
|
||||||
core->message("\n" + core->cmdRaw("fo"));
|
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;
|
QSettings settings;
|
||||||
auto geometry = settings.value("geometry").toByteArray();
|
auto geometry = settings.value("geometry").toByteArray();
|
||||||
if (!geometry.isEmpty()) {
|
if (!geometry.isEmpty()) {
|
||||||
@ -822,6 +832,9 @@ void MainWindow::restoreDocks()
|
|||||||
tabifyDockWidget(dashboardDock, memoryMapDock);
|
tabifyDockWidget(dashboardDock, memoryMapDock);
|
||||||
tabifyDockWidget(dashboardDock, breakpointDock);
|
tabifyDockWidget(dashboardDock, breakpointDock);
|
||||||
tabifyDockWidget(dashboardDock, registerRefsDock);
|
tabifyDockWidget(dashboardDock, registerRefsDock);
|
||||||
|
tabifyDockWidget(dashboardDock, r2GraphDock);
|
||||||
|
tabifyDockWidget(dashboardDock, callGraphDock);
|
||||||
|
tabifyDockWidget(dashboardDock, globalCallGraphDock);
|
||||||
for (const auto &it : dockWidgets) {
|
for (const auto &it : dockWidgets) {
|
||||||
// Check whether or not current widgets is graph, hexdump or disasm
|
// Check whether or not current widgets is graph, hexdump or disasm
|
||||||
if (isExtraMemoryWidget(it)) {
|
if (isExtraMemoryWidget(it)) {
|
||||||
@ -923,10 +936,26 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
|
|||||||
QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address)
|
QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address)
|
||||||
{
|
{
|
||||||
QMenu *menu = new QMenu(parent);
|
QMenu *menu = new QMenu(parent);
|
||||||
|
// Memory dock widgets
|
||||||
for (auto &dock : dockWidgets) {
|
for (auto &dock : dockWidgets) {
|
||||||
if (auto memoryWidget = qobject_cast<MemoryDockWidget *>(dock)) {
|
if (auto memoryWidget = qobject_cast<MemoryDockWidget *>(dock)) {
|
||||||
QAction *action = new QAction(memoryWidget->windowTitle(), menu);
|
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->getSeekable()->seek(address);
|
||||||
memoryWidget->raiseMemoryWidget();
|
memoryWidget->raiseMemoryWidget();
|
||||||
});
|
});
|
||||||
|
@ -52,6 +52,8 @@ class GraphWidget;
|
|||||||
class HexdumpWidget;
|
class HexdumpWidget;
|
||||||
class DecompilerWidget;
|
class DecompilerWidget;
|
||||||
class OverviewWidget;
|
class OverviewWidget;
|
||||||
|
class R2GraphWidget;
|
||||||
|
class CallGraphWidget;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class MainWindow;
|
class MainWindow;
|
||||||
@ -267,6 +269,9 @@ private:
|
|||||||
NewFileDialog *newFileDialog = nullptr;
|
NewFileDialog *newFileDialog = nullptr;
|
||||||
CutterDockWidget *breakpointDock = nullptr;
|
CutterDockWidget *breakpointDock = nullptr;
|
||||||
CutterDockWidget *registerRefsDock = nullptr;
|
CutterDockWidget *registerRefsDock = nullptr;
|
||||||
|
R2GraphWidget *r2GraphDock = nullptr;
|
||||||
|
CallGraphWidget *callGraphDock = nullptr;
|
||||||
|
CallGraphWidget *globalCallGraphDock = nullptr;
|
||||||
|
|
||||||
QMenu *disassemblyContextMenuExtensions = nullptr;
|
QMenu *disassemblyContextMenuExtensions = nullptr;
|
||||||
QMenu *addressableContextMenuExtensions = nullptr;
|
QMenu *addressableContextMenuExtensions = nullptr;
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QSpinBox" name="horizontalBlockSpacing">
|
<widget class="QSpinBox" name="horizontalBlockSpacing">
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>100</number>
|
<number>400</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<number>10</number>
|
<number>10</number>
|
||||||
@ -121,7 +121,7 @@
|
|||||||
<item row="1" column="2">
|
<item row="1" column="2">
|
||||||
<widget class="QSpinBox" name="verticalBlockSpacing">
|
<widget class="QSpinBox" name="verticalBlockSpacing">
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>100</number>
|
<number>400</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<number>10</number>
|
<number>10</number>
|
||||||
@ -137,7 +137,7 @@
|
|||||||
<number>1</number>
|
<number>1</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>100</number>
|
<number>400</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<number>5</number>
|
<number>5</number>
|
||||||
@ -153,7 +153,7 @@
|
|||||||
<number>1</number>
|
<number>1</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>100</number>
|
<number>400</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<number>5</number>
|
<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 "core/MainWindow.h"
|
||||||
#include "common/Colors.h"
|
#include "common/Colors.h"
|
||||||
#include "common/Configuration.h"
|
#include "common/Configuration.h"
|
||||||
#include "common/CachedFontMetrics.h"
|
|
||||||
#include "common/TempConfig.h"
|
#include "common/TempConfig.h"
|
||||||
#include "common/SyntaxHighlighter.h"
|
#include "common/SyntaxHighlighter.h"
|
||||||
#include "common/BasicBlockHighlighter.h"
|
#include "common/BasicBlockHighlighter.h"
|
||||||
#include "common/BasicInstructionHighlighter.h"
|
#include "common/BasicInstructionHighlighter.h"
|
||||||
#include "dialogs/MultitypeFileSaveDialog.h"
|
|
||||||
#include "common/Helpers.h"
|
#include "common/Helpers.h"
|
||||||
|
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
@ -23,36 +21,20 @@
|
|||||||
#include <QToolTip>
|
#include <QToolTip>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
||||||
#include <cmath>
|
#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,
|
DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *seekable,
|
||||||
MainWindow *mainWindow, QList<QAction *> additionalMenuActions)
|
MainWindow *mainWindow, QList<QAction *> additionalMenuActions)
|
||||||
: GraphView(parent),
|
: CutterGraphView(parent),
|
||||||
mFontMetrics(nullptr),
|
|
||||||
graphLayout(GraphView::Layout::GridMedium),
|
|
||||||
blockMenu(new DisassemblyContextMenu(this, mainWindow)),
|
blockMenu(new DisassemblyContextMenu(this, mainWindow)),
|
||||||
contextMenu(new QMenu(this)),
|
contextMenu(new QMenu(this)),
|
||||||
seekable(seekable),
|
seekable(seekable),
|
||||||
actionExportGraph(this),
|
|
||||||
actionUnhighlight(this),
|
actionUnhighlight(this),
|
||||||
actionUnhighlightInstruction(this)
|
actionUnhighlightInstruction(this)
|
||||||
{
|
{
|
||||||
@ -67,12 +49,9 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se
|
|||||||
connect(Core(), SIGNAL(varsChanged()), this, SLOT(refreshView()));
|
connect(Core(), SIGNAL(varsChanged()), this, SLOT(refreshView()));
|
||||||
connect(Core(), SIGNAL(instructionChanged(RVA)), this, SLOT(refreshView()));
|
connect(Core(), SIGNAL(instructionChanged(RVA)), this, SLOT(refreshView()));
|
||||||
connect(Core(), SIGNAL(functionsChanged()), 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(asmOptionsChanged()), this, SLOT(refreshView()));
|
||||||
connect(Core(), SIGNAL(refreshCodeViews()), 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);
|
connectSeekChanged(false);
|
||||||
|
|
||||||
// ESC for previous
|
// ESC for previous
|
||||||
@ -99,53 +78,9 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se
|
|||||||
shortcuts.append(shortcut_next_instr);
|
shortcuts.append(shortcut_next_instr);
|
||||||
shortcuts.append(shortcut_prev_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
|
// Context menu that applies to everything
|
||||||
contextMenu->addAction(&actionExportGraph);
|
contextMenu->addAction(&actionExportGraph);
|
||||||
static const std::pair<QString, GraphView::Layout> LAYOUT_CONFIG[] = {
|
contextMenu->addMenu(layoutMenu);
|
||||||
{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->addSeparator();
|
contextMenu->addSeparator();
|
||||||
contextMenu->addActions(additionalMenuActions);
|
contextMenu->addActions(additionalMenuActions);
|
||||||
|
|
||||||
@ -199,10 +134,6 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se
|
|||||||
blockMenu->addSeparator();
|
blockMenu->addSeparator();
|
||||||
blockMenu->addActions(contextMenu->actions());
|
blockMenu->addActions(contextMenu->actions());
|
||||||
|
|
||||||
|
|
||||||
initFont();
|
|
||||||
colorsUpdatedSlot();
|
|
||||||
|
|
||||||
connect(blockMenu, &DisassemblyContextMenu::copy, this, &DisassemblerGraphView::copySelection);
|
connect(blockMenu, &DisassemblyContextMenu::copy, this, &DisassemblerGraphView::copySelection);
|
||||||
|
|
||||||
// Add header as widget to layout so it stretches to the layout width
|
// Add header as widget to layout so it stretches to the layout width
|
||||||
@ -232,8 +163,7 @@ DisassemblerGraphView::~DisassemblerGraphView()
|
|||||||
|
|
||||||
void DisassemblerGraphView::refreshView()
|
void DisassemblerGraphView::refreshView()
|
||||||
{
|
{
|
||||||
initFont();
|
CutterGraphView::refreshView();
|
||||||
setLayoutConfig(getLayoutConfig());
|
|
||||||
loadCurrentGraph();
|
loadCurrentGraph();
|
||||||
emit viewRefreshed();
|
emit viewRefreshed();
|
||||||
}
|
}
|
||||||
@ -370,7 +300,7 @@ void DisassemblerGraphView::loadCurrentGraph()
|
|||||||
|
|
||||||
addBlock(gb);
|
addBlock(gb);
|
||||||
}
|
}
|
||||||
cleanupEdges();
|
cleanupEdges(blocks);
|
||||||
|
|
||||||
if (!func["blocks"].toArray().isEmpty()) {
|
if (!func["blocks"].toArray().isEmpty()) {
|
||||||
computeGraphPlacement();
|
computeGraphPlacement();
|
||||||
@ -416,36 +346,6 @@ void DisassemblerGraphView::prepareGraphNode(GraphBlock &block)
|
|||||||
block.height = (height * charHeight) + extra;
|
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)
|
void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive)
|
||||||
{
|
{
|
||||||
QRectF blockRect(block.x, block.y, block.width, block.height);
|
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);
|
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)
|
DisassemblerGraphView::DisassemblyBlock *DisassemblerGraphView::blockForAddress(RVA addr)
|
||||||
{
|
{
|
||||||
for (auto &blockIt : disassembly_blocks) {
|
for (auto &blockIt : disassembly_blocks) {
|
||||||
@ -794,51 +667,11 @@ void DisassemblerGraphView::onSeekChanged(RVA addr)
|
|||||||
if (db) {
|
if (db) {
|
||||||
// This is a local address! We animated to it.
|
// This is a local address! We animated to it.
|
||||||
transition_dont_seek = true;
|
transition_dont_seek = true;
|
||||||
showBlock(&blocks[db->entry], !switchFunction);
|
showBlock(blocks[db->entry], !switchFunction);
|
||||||
showInstruction(blocks[db->entry], addr);
|
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()
|
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()
|
void DisassemblerGraphView::nextInstr()
|
||||||
{
|
{
|
||||||
seekInstruction(false);
|
seekInstruction(false);
|
||||||
@ -971,12 +792,6 @@ DisassemblerGraphView::Token *DisassemblerGraphView::getToken(Instr *instr, int
|
|||||||
return nullptr;
|
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
|
QPoint DisassemblerGraphView::getInstructionOffset(const DisassemblyBlock &block, int line) const
|
||||||
{
|
{
|
||||||
return getTextOffset(line + static_cast<int>(block.header_text.lines.size()));
|
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,
|
void DisassemblerGraphView::blockContextMenuRequested(GraphView::GraphBlock &block,
|
||||||
QContextMenuEvent *event, QPoint pos)
|
QContextMenuEvent *event, QPoint /*pos*/)
|
||||||
{
|
{
|
||||||
const RVA offset = this->seekable->getOffset();
|
const RVA offset = this->seekable->getOffset();
|
||||||
actionUnhighlight.setVisible(Core()->getBBHighlighter()->getBasicBlock(block.entry));
|
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,
|
void DisassemblerGraphView::blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event,
|
||||||
QPoint pos)
|
QPoint pos)
|
||||||
{
|
{
|
||||||
@ -1065,91 +895,6 @@ void DisassemblerGraphView::blockTransitionedTo(GraphView::GraphBlock *to)
|
|||||||
seekLocal(to->entry);
|
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()
|
void DisassemblerGraphView::onActionHighlightBITriggered()
|
||||||
{
|
{
|
||||||
@ -1188,114 +933,11 @@ void DisassemblerGraphView::onActionUnhighlightBITriggered()
|
|||||||
Config()->colorsUpdated();
|
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
|
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)
|
void DisassemblerGraphView::paintEvent(QPaintEvent *event)
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
||||||
#include "widgets/GraphView.h"
|
#include "widgets/CutterGraphView.h"
|
||||||
#include "menus/DisassemblyContextMenu.h"
|
#include "menus/DisassemblyContextMenu.h"
|
||||||
#include "common/RichTextPainter.h"
|
#include "common/RichTextPainter.h"
|
||||||
#include "common/CutterSeekable.h"
|
#include "common/CutterSeekable.h"
|
||||||
@ -16,7 +16,7 @@
|
|||||||
class QTextEdit;
|
class QTextEdit;
|
||||||
class FallbackSyntaxHighlighter;
|
class FallbackSyntaxHighlighter;
|
||||||
|
|
||||||
class DisassemblerGraphView : public GraphView
|
class DisassemblerGraphView : public CutterGraphView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@ -101,7 +101,6 @@ public:
|
|||||||
GraphView::GraphBlock *to,
|
GraphView::GraphBlock *to,
|
||||||
bool interactive) override;
|
bool interactive) override;
|
||||||
virtual void blockTransitionedTo(GraphView::GraphBlock *to) override;
|
virtual void blockTransitionedTo(GraphView::GraphBlock *to) override;
|
||||||
virtual bool event(QEvent *event) override;
|
|
||||||
|
|
||||||
void loadCurrentGraph();
|
void loadCurrentGraph();
|
||||||
QString windowTitle;
|
QString windowTitle;
|
||||||
@ -112,13 +111,6 @@ public:
|
|||||||
using EdgeConfigurationMapping = std::map<std::pair<ut64, ut64>, EdgeConfiguration>;
|
using EdgeConfigurationMapping = std::map<std::pair<ut64, ut64>, EdgeConfiguration>;
|
||||||
EdgeConfigurationMapping getEdgeConfigurations();
|
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
|
* @brief keep the current addr of the fcn of Graph
|
||||||
* Everytime overview updates its contents, it compares this value with the one in 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
|
ut64 currentFcnAddr = RVA_INVALID; // TODO: make this less public
|
||||||
public slots:
|
public slots:
|
||||||
void refreshView();
|
void refreshView() override;
|
||||||
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 onSeekChanged(RVA addr);
|
||||||
void takeTrue();
|
void takeTrue();
|
||||||
void takeFalse();
|
void takeFalse();
|
||||||
|
|
||||||
@ -145,48 +130,31 @@ public slots:
|
|||||||
void copySelection();
|
void copySelection();
|
||||||
|
|
||||||
protected:
|
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 paintEvent(QPaintEvent *event) override;
|
||||||
void blockContextMenuRequested(GraphView::GraphBlock &block, QContextMenuEvent *event,
|
void blockContextMenuRequested(GraphView::GraphBlock &block, QContextMenuEvent *event,
|
||||||
QPoint pos) override;
|
QPoint pos) override;
|
||||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
|
void restoreCurrentBlock() override;
|
||||||
private slots:
|
private slots:
|
||||||
void on_actionExportGraph_triggered();
|
void showExportDialog() override;
|
||||||
void onActionHighlightBITriggered();
|
void onActionHighlightBITriggered();
|
||||||
void onActionUnhighlightBITriggered();
|
void onActionUnhighlightBITriggered();
|
||||||
void updateLayout();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool transition_dont_seek = false;
|
bool transition_dont_seek = false;
|
||||||
|
|
||||||
Token *highlight_token;
|
Token *highlight_token;
|
||||||
// Font data
|
|
||||||
std::unique_ptr<CachedFontMetrics<qreal>> mFontMetrics;
|
|
||||||
qreal charWidth;
|
|
||||||
int charHeight;
|
|
||||||
int charOffset;
|
|
||||||
int baseline;
|
|
||||||
bool emptyGraph;
|
bool emptyGraph;
|
||||||
ut64 currentBlockAddress = RVA_INVALID;
|
ut64 currentBlockAddress = RVA_INVALID;
|
||||||
|
|
||||||
GraphView::Layout graphLayout;
|
|
||||||
|
|
||||||
DisassemblyContextMenu *blockMenu;
|
DisassemblyContextMenu *blockMenu;
|
||||||
QMenu *contextMenu;
|
QMenu *contextMenu;
|
||||||
QAction* horizontalLayoutAction;
|
|
||||||
|
|
||||||
void connectSeekChanged(bool disconnect);
|
void connectSeekChanged(bool disconnect);
|
||||||
|
|
||||||
void initFont();
|
|
||||||
void prepareGraphNode(GraphBlock &block);
|
void prepareGraphNode(GraphBlock &block);
|
||||||
void cleanupEdges();
|
|
||||||
Token *getToken(Instr *instr, int x);
|
Token *getToken(Instr *instr, int x);
|
||||||
QPoint getTextOffset(int line) const;
|
|
||||||
QPoint getInstructionOffset(const DisassemblyBlock &block, int line) const;
|
QPoint getInstructionOffset(const DisassemblyBlock &block, int line) const;
|
||||||
RVA getAddrForMouseEvent(GraphBlock &block, QPoint *point);
|
RVA getAddrForMouseEvent(GraphBlock &block, QPoint *point);
|
||||||
Instr *getInstrForMouseEvent(GraphBlock &block, QPoint *point, bool force = false);
|
Instr *getInstrForMouseEvent(GraphBlock &block, QPoint *point, bool force = false);
|
||||||
@ -203,48 +171,17 @@ private:
|
|||||||
DisassemblyBlock *blockForAddress(RVA addr);
|
DisassemblyBlock *blockForAddress(RVA addr);
|
||||||
void seekLocal(RVA addr, bool update_viewport = true);
|
void seekLocal(RVA addr, bool update_viewport = true);
|
||||||
void seekInstruction(bool previous_instr);
|
void seekInstruction(bool previous_instr);
|
||||||
GraphLayout::LayoutConfig getLayoutConfig();
|
|
||||||
|
|
||||||
CutterSeekable *seekable = nullptr;
|
CutterSeekable *seekable = nullptr;
|
||||||
QList<QShortcut *> shortcuts;
|
QList<QShortcut *> shortcuts;
|
||||||
QList<RVA> breakpoints;
|
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 actionUnhighlight;
|
||||||
QAction actionUnhighlightInstruction;
|
QAction actionUnhighlightInstruction;
|
||||||
|
|
||||||
QLabel *emptyText = nullptr;
|
QLabel *emptyText = nullptr;
|
||||||
|
|
||||||
static const int KEY_ZOOM_IN;
|
|
||||||
static const int KEY_ZOOM_OUT;
|
|
||||||
static const int KEY_ZOOM_RESET;
|
|
||||||
signals:
|
signals:
|
||||||
void viewRefreshed();
|
|
||||||
void viewZoomed();
|
|
||||||
void graphMoved();
|
|
||||||
void resized();
|
|
||||||
void nameChanged(const QString &name);
|
void nameChanged(const QString &name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -262,6 +262,12 @@ void GraphGridLayout::CalculateLayout(GraphLayout::Graph &blocks, ut64 entry, in
|
|||||||
{
|
{
|
||||||
LayoutState layoutState;
|
LayoutState layoutState;
|
||||||
layoutState.blocks = &blocks;
|
layoutState.blocks = &blocks;
|
||||||
|
if (blocks.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (blocks.find(entry) == blocks.end()) {
|
||||||
|
entry = blocks.begin()->first;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto &it : blocks) {
|
for (auto &it : blocks) {
|
||||||
GridBlock block;
|
GridBlock block;
|
||||||
@ -492,7 +498,7 @@ void GraphGridLayout::computeAllBlockPlacement(const std::vector<ut64> &blockOrd
|
|||||||
if (block.row == 0) { // place all the roots first
|
if (block.row == 0) { // place all the roots first
|
||||||
auto offset = -block.leftPosition;
|
auto offset = -block.leftPosition;
|
||||||
block.col += nextEmptyColumn + offset;
|
block.col += nextEmptyColumn + offset;
|
||||||
nextEmptyColumn = block.rightPosition + offset;
|
nextEmptyColumn = block.rightPosition + offset + nextEmptyColumn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Visit all nodes top to bottom, converting relative positions to absolute.
|
// 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 segmentOffsets offsets relative to the left side edge column.
|
||||||
* @param edgeColumnWidth widths of edge columns
|
* @param edgeColumnWidth widths of edge columns
|
||||||
* @param segments either all horizontal or all vertical edge segments
|
* @param segments either all horizontal or all vertical edge segments
|
||||||
* @param minSpacing spacing between segments
|
|
||||||
*/
|
*/
|
||||||
static void centerEdges(
|
static void centerEdges(
|
||||||
std::vector<int> &segmentOffsets,
|
std::vector<int> &segmentOffsets,
|
||||||
const std::vector<int> &edgeColumnWidth,
|
const std::vector<int> &edgeColumnWidth,
|
||||||
const std::vector<EdgeSegment> &segments,
|
const std::vector<EdgeSegment> &segments)
|
||||||
int minSpacing)
|
|
||||||
{
|
{
|
||||||
/* Split segments in each edge column into non intersecting chunks. Center each chunk separately.
|
/* 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();
|
auto it = events.begin();
|
||||||
while (it != events.end()) {
|
while (it != events.end()) {
|
||||||
|
int left, right;
|
||||||
|
left = right = segmentOffsets[it->index];
|
||||||
auto chunkStart = it++;
|
auto chunkStart = it++;
|
||||||
int activeSegmentCount = 1;
|
int activeSegmentCount = 1;
|
||||||
int chunkWidth = 0;
|
|
||||||
while (activeSegmentCount > 0) {
|
while (activeSegmentCount > 0) {
|
||||||
activeSegmentCount += it->start ? 1 : -1;
|
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++;
|
it++;
|
||||||
}
|
}
|
||||||
// leftMost segment position includes padding on the left side so add it on the right side as well
|
int spacing = (edgeColumnWidth[chunkStart->x] - (right - left)) / 2 - left;
|
||||||
chunkWidth += minSpacing;
|
|
||||||
|
|
||||||
int spacing = (std::max(edgeColumnWidth[chunkStart->x], minSpacing) - chunkWidth) / 2;
|
|
||||||
for (auto segment = chunkStart; segment != it; segment++) {
|
for (auto segment = chunkStart; segment != it; segment++) {
|
||||||
if (segment->start) {
|
if (segment->start) {
|
||||||
segmentOffsets[segment->index] += spacing;
|
segmentOffsets[segment->index] += spacing;
|
||||||
@ -977,7 +982,7 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state
|
|||||||
edgeOffsets.resize(edgeIndex);
|
edgeOffsets.resize(edgeIndex);
|
||||||
calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides,
|
calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides,
|
||||||
state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing);
|
state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing);
|
||||||
centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.edgeHorizontalSpacing);
|
centerEdges(edgeOffsets, state.edgeColumnWidth, segments);
|
||||||
edgeIndex = 0;
|
edgeIndex = 0;
|
||||||
|
|
||||||
auto copySegmentsToEdges = [&](bool col) {
|
auto copySegmentsToEdges = [&](bool col) {
|
||||||
@ -985,7 +990,22 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state
|
|||||||
for (auto &edgeListIt : state.edge) {
|
for (auto &edgeListIt : state.edge) {
|
||||||
for (auto &edge : edgeListIt.second) {
|
for (auto &edge : edgeListIt.second) {
|
||||||
for (size_t j = col ? 1 : 2; j < edge.points.size(); j += 2) {
|
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 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 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 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
|
* @param stickWhenNotMoving variable grouping strategy
|
||||||
*/
|
*/
|
||||||
static void optimizeLinearProgramPass(
|
static void optimizeLinearProgramPass(
|
||||||
@ -1283,7 +1303,7 @@ static void optimizeLinearProgramPass(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (auto &equality : equalities) {
|
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 a = getGroup(equality.first.first);
|
||||||
int b = getGroup(equality.first.second);
|
int b = getGroup(equality.first.second);
|
||||||
if (a == b) {
|
if (a == b) {
|
||||||
@ -1363,6 +1383,11 @@ static void optimizeLinearProgramPass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert(smallestMove != INT_MAX);
|
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;
|
solution[g] += smallestMove;
|
||||||
if (smallestMove == 0 && stickWhenNotMoving == false) {
|
if (smallestMove == 0 && stickWhenNotMoving == false) {
|
||||||
continue;
|
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 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 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 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(
|
static void optimizeLinearProgram(
|
||||||
size_t n,
|
size_t n,
|
||||||
@ -1527,12 +1552,15 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const
|
|||||||
int blockVariable = blockMapping[blockId];
|
int blockVariable = blockMapping[blockId];
|
||||||
equalities.push_back({{blockVariable, edgeVariable}, blockPos - edgeVariablePos});
|
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.resize(std::max(solution.size(), variable + 1));
|
||||||
solution[variable] = value;
|
solution[variable] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto copyVariablesToPositions = [&](const std::vector<int> &solution, bool horizontal = false) {
|
auto copyVariablesToPositions = [&](const std::vector<int> &solution, bool horizontal = false) {
|
||||||
|
for (auto v : solution) {
|
||||||
|
assert(v >= 0);
|
||||||
|
}
|
||||||
size_t variableIndex = blockMapping.size();
|
size_t variableIndex = blockMapping.size();
|
||||||
for (auto &blockIt : *state.blocks) {
|
for (auto &blockIt : *state.blocks) {
|
||||||
auto &block = blockIt.second;
|
auto &block = blockIt.second;
|
||||||
@ -1582,7 +1610,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const
|
|||||||
int x = edge.polyline[i].y();
|
int x = edge.polyline[i].y();
|
||||||
segments.push_back({x, int(variableIndex), y0, y1});
|
segments.push_back({x, int(variableIndex), y0, y1});
|
||||||
variableGroups.push_back(blockMapping.size() + edgeIndex);
|
variableGroups.push_back(blockMapping.size() + edgeIndex);
|
||||||
setViableSolution(variableIndex, x);
|
setFeasibleSolution(variableIndex, x);
|
||||||
if (i > 2) {
|
if (i > 2) {
|
||||||
int prevX = edge.polyline[i - 2].y();
|
int prevX = edge.polyline[i - 2].y();
|
||||||
addObjective(variableIndex, x, variableIndex - 1, prevX);
|
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, blockVariable, block.x, block.x + block.width});
|
||||||
segments.push_back({block.y + block.height, 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(),
|
createInequalitiesFromSegments(std::move(segments), solution, variableGroups, blockMapping.size(),
|
||||||
@ -1601,9 +1629,6 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const
|
|||||||
|
|
||||||
objectiveFunction.resize(solution.size());
|
objectiveFunction.resize(solution.size());
|
||||||
optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution);
|
optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution);
|
||||||
for (auto v : solution) {
|
|
||||||
assert(v >= 0);
|
|
||||||
}
|
|
||||||
copyVariablesToPositions(solution, true);
|
copyVariablesToPositions(solution, true);
|
||||||
connectEdgeEnds(*state.blocks);
|
connectEdgeEnds(*state.blocks);
|
||||||
|
|
||||||
@ -1632,7 +1657,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const
|
|||||||
int x = edge.polyline[i].x();
|
int x = edge.polyline[i].x();
|
||||||
segments.push_back({x, int(variableIndex), y0, y1});
|
segments.push_back({x, int(variableIndex), y0, y1});
|
||||||
variableGroups.push_back(blockMapping.size() + edgeIndex);
|
variableGroups.push_back(blockMapping.size() + edgeIndex);
|
||||||
setViableSolution(variableIndex, x);
|
setFeasibleSolution(variableIndex, x);
|
||||||
if (i > 2) {
|
if (i > 2) {
|
||||||
int prevX = edge.polyline[i - 2].x();
|
int prevX = edge.polyline[i - 2].x();
|
||||||
addObjective(variableIndex, x, variableIndex - 1, prevX);
|
addObjective(variableIndex, x, variableIndex - 1, prevX);
|
||||||
@ -1647,7 +1672,7 @@ void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const
|
|||||||
int blockVariable = blockMapping[blockIt.first];
|
int blockVariable = blockMapping[blockIt.first];
|
||||||
segments.push_back({block.x, blockVariable, block.y, block.y + block.height});
|
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});
|
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(),
|
createInequalitiesFromSegments(std::move(segments), solution, variableGroups, blockMapping.size(),
|
||||||
|
@ -49,6 +49,7 @@ void GraphHorizontalAdapter::setLayoutConfig(const GraphLayout::LayoutConfig &co
|
|||||||
{
|
{
|
||||||
GraphLayout::setLayoutConfig(config);
|
GraphLayout::setLayoutConfig(config);
|
||||||
swapLayoutConfigDirection();
|
swapLayoutConfigDirection();
|
||||||
|
layout->setLayoutConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphHorizontalAdapter::swapLayoutConfigDirection()
|
void GraphHorizontalAdapter::swapLayoutConfigDirection()
|
||||||
|
@ -45,20 +45,12 @@ GraphView::~GraphView()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Callbacks
|
// 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)
|
void GraphView::blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos)
|
||||||
{
|
{
|
||||||
Q_UNUSED(block);
|
Q_UNUSED(block);
|
||||||
Q_UNUSED(event);
|
Q_UNUSED(event);
|
||||||
Q_UNUSED(pos);
|
Q_UNUSED(pos);
|
||||||
qWarning() << "Block clicked not overridden!";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphView::blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos)
|
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(block);
|
||||||
Q_UNUSED(event);
|
Q_UNUSED(event);
|
||||||
Q_UNUSED(pos);
|
Q_UNUSED(pos);
|
||||||
qWarning() << "Block double clicked not overridden!";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphView::blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos)
|
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)
|
void GraphView::blockTransitionedTo(GraphView::GraphBlock *to)
|
||||||
{
|
{
|
||||||
Q_UNUSED(to);
|
Q_UNUSED(to);
|
||||||
qWarning() << "blockTransitionedTo not overridden!";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphView::EdgeConfiguration GraphView::edgeConfiguration(GraphView::GraphBlock &from,
|
GraphView::EdgeConfiguration GraphView::edgeConfiguration(GraphView::GraphBlock &from,
|
||||||
@ -134,10 +124,29 @@ void GraphView::computeGraphPlacement()
|
|||||||
{
|
{
|
||||||
graphLayoutSystem->CalculateLayout(blocks, entry, width, height);
|
graphLayoutSystem->CalculateLayout(blocks, entry, width, height);
|
||||||
setCacheDirty();
|
setCacheDirty();
|
||||||
ready = true;
|
clampViewOffset();
|
||||||
viewport()->update();
|
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)
|
void GraphView::beginMouseDrag(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
scroll_base_x = event->x();
|
scroll_base_x = event->x();
|
||||||
@ -456,13 +465,8 @@ void GraphView::centerY(bool emitSignal)
|
|||||||
|
|
||||||
void GraphView::showBlock(GraphBlock &block, bool anywhere)
|
void GraphView::showBlock(GraphBlock &block, bool anywhere)
|
||||||
{
|
{
|
||||||
showBlock(&block, anywhere);
|
showRectangle(QRect(block.x, block.y, block.width, block.height), anywhere);
|
||||||
}
|
blockTransitionedTo(&block);
|
||||||
|
|
||||||
void GraphView::showBlock(GraphBlock *block, bool anywhere)
|
|
||||||
{
|
|
||||||
showRectangle(QRect(block->x, block->y, block->width, block->height), anywhere);
|
|
||||||
blockTransitionedTo(block);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphView::showRectangle(const QRect &block, bool anywhere)
|
void GraphView::showRectangle(const QRect &block, bool anywhere)
|
||||||
@ -512,6 +516,11 @@ QPoint GraphView::viewToLogicalCoordinates(QPoint p)
|
|||||||
return p / current_scale + offset;
|
return p / current_scale + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPoint GraphView::logicalToViewCoordinates(QPoint p)
|
||||||
|
{
|
||||||
|
return (p - offset) * current_scale;
|
||||||
|
}
|
||||||
|
|
||||||
void GraphView::setGraphLayout(std::unique_ptr<GraphLayout> layout)
|
void GraphView::setGraphLayout(std::unique_ptr<GraphLayout> layout)
|
||||||
{
|
{
|
||||||
graphLayoutSystem = std::move(layout);
|
graphLayoutSystem = std::move(layout);
|
||||||
@ -529,6 +538,15 @@ std::unique_ptr<GraphLayout> GraphView::makeGraphLayout(GraphView::Layout layout
|
|||||||
{
|
{
|
||||||
std::unique_ptr<GraphLayout> result;
|
std::unique_ptr<GraphLayout> result;
|
||||||
bool needAdapter = true;
|
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) {
|
switch (layout) {
|
||||||
case Layout::GridNarrow:
|
case Layout::GridNarrow:
|
||||||
result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Narrow));
|
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::GridBAA:
|
||||||
case Layout::GridBAB:
|
case Layout::GridBAB:
|
||||||
case Layout::GridBBA:
|
case Layout::GridBBA:
|
||||||
case Layout::GridBBB:
|
case Layout::GridBBB: {
|
||||||
{
|
|
||||||
int options = static_cast<int>(layout) - static_cast<int>(Layout::GridAAA);
|
int options = static_cast<int>(layout) - static_cast<int>(Layout::GridAAA);
|
||||||
std::unique_ptr<GraphGridLayout> gridLayout(new GraphGridLayout());
|
std::unique_ptr<GraphGridLayout> gridLayout(new GraphGridLayout());
|
||||||
gridLayout->setTightSubtreePlacement((options & 1) == 0);
|
gridLayout->setTightSubtreePlacement((options & 1) == 0);
|
||||||
@ -558,14 +575,22 @@ std::unique_ptr<GraphLayout> GraphView::makeGraphLayout(GraphView::Layout layout
|
|||||||
}
|
}
|
||||||
#ifdef CUTTER_ENABLE_GRAPHVIZ
|
#ifdef CUTTER_ENABLE_GRAPHVIZ
|
||||||
case Layout::GraphvizOrtho:
|
case Layout::GraphvizOrtho:
|
||||||
result.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho,
|
makeGraphvizLayout(GraphvizLayout::LayoutType::DotOrtho);
|
||||||
horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB));
|
|
||||||
needAdapter = false;
|
|
||||||
break;
|
break;
|
||||||
case Layout::GraphvizPolyline:
|
case Layout::GraphvizPolyline:
|
||||||
result.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline,
|
makeGraphvizLayout(GraphvizLayout::LayoutType::DotPolyline);
|
||||||
horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB));
|
break;
|
||||||
needAdapter = false;
|
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;
|
break;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,10 @@ public:
|
|||||||
#ifdef CUTTER_ENABLE_GRAPHVIZ
|
#ifdef CUTTER_ENABLE_GRAPHVIZ
|
||||||
, GraphvizOrtho
|
, GraphvizOrtho
|
||||||
, GraphvizPolyline
|
, GraphvizPolyline
|
||||||
|
, GraphvizSfdp
|
||||||
|
, GraphvizNeato
|
||||||
|
, GraphvizTwoPi
|
||||||
|
, GraphvizCirco
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
static std::unique_ptr<GraphLayout> makeGraphLayout(Layout layout, bool horizontal = false);
|
static std::unique_ptr<GraphLayout> makeGraphLayout(Layout layout, bool horizontal = false);
|
||||||
@ -69,7 +73,6 @@ public:
|
|||||||
~GraphView() override;
|
~GraphView() override;
|
||||||
|
|
||||||
void showBlock(GraphBlock &block, bool anywhere = false);
|
void showBlock(GraphBlock &block, bool anywhere = false);
|
||||||
void showBlock(GraphBlock *block, bool anywhere = false);
|
|
||||||
/**
|
/**
|
||||||
* @brief Move view so that area is visible.
|
* @brief Move view so that area is visible.
|
||||||
* @param rect Rectangle to show
|
* @param rect Rectangle to show
|
||||||
@ -83,6 +86,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
GraphView::GraphBlock *getBlockContaining(QPoint p);
|
GraphView::GraphBlock *getBlockContaining(QPoint p);
|
||||||
QPoint viewToLogicalCoordinates(QPoint p);
|
QPoint viewToLogicalCoordinates(QPoint p);
|
||||||
|
QPoint logicalToViewCoordinates(QPoint p);
|
||||||
|
|
||||||
void setGraphLayout(std::unique_ptr<GraphLayout> layout);
|
void setGraphLayout(std::unique_ptr<GraphLayout> layout);
|
||||||
GraphLayout &getGraphLayout() const { return *graphLayoutSystem; }
|
GraphLayout &getGraphLayout() const { return *graphLayoutSystem; }
|
||||||
@ -90,12 +94,20 @@ public:
|
|||||||
|
|
||||||
void paint(QPainter &p, QPoint offset, QRect area, qreal scale = 1.0, bool interactive = true);
|
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 saveAsSvg(QString path);
|
||||||
|
|
||||||
void computeGraphPlacement();
|
void computeGraphPlacement();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove duplicate edges and edges without target in graph.
|
||||||
|
* @param graph
|
||||||
|
*/
|
||||||
|
static void cleanupEdges(GraphLayout::Graph &graph);
|
||||||
protected:
|
protected:
|
||||||
std::unordered_map<ut64, GraphBlock> blocks;
|
std::unordered_map<ut64, GraphBlock> blocks;
|
||||||
|
/// image background color
|
||||||
QColor backgroundColor = QColor(Qt::white);
|
QColor backgroundColor = QColor(Qt::white);
|
||||||
|
|
||||||
// Padding inside the block
|
// Padding inside the block
|
||||||
@ -113,7 +125,7 @@ protected:
|
|||||||
* @param block
|
* @param block
|
||||||
* @param interactive - can be used for disabling elemnts during export
|
* @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 blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos);
|
||||||
virtual void blockDoubleClicked(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);
|
virtual void blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos);
|
||||||
@ -122,6 +134,14 @@ protected:
|
|||||||
virtual void wheelEvent(QWheelEvent *event) override;
|
virtual void wheelEvent(QWheelEvent *event) override;
|
||||||
virtual EdgeConfiguration edgeConfiguration(GraphView::GraphBlock &from, GraphView::GraphBlock *to,
|
virtual EdgeConfiguration edgeConfiguration(GraphView::GraphBlock &from, GraphView::GraphBlock *to,
|
||||||
bool interactive = true);
|
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,
|
virtual void blockContextMenuRequested(GraphView::GraphBlock &block, QContextMenuEvent *event,
|
||||||
QPoint pos);
|
QPoint pos);
|
||||||
|
|
||||||
@ -159,12 +179,10 @@ private:
|
|||||||
|
|
||||||
QPoint offset = QPoint(0, 0);
|
QPoint offset = QPoint(0, 0);
|
||||||
|
|
||||||
ut64 entry;
|
ut64 entry = 0;
|
||||||
|
|
||||||
std::unique_ptr<GraphLayout> graphLayoutSystem;
|
std::unique_ptr<GraphLayout> graphLayoutSystem;
|
||||||
|
|
||||||
bool ready = false;
|
|
||||||
|
|
||||||
// Scrolling data
|
// Scrolling data
|
||||||
int scroll_base_x = 0;
|
int scroll_base_x = 0;
|
||||||
int scroll_base_y = 0;
|
int scroll_base_y = 0;
|
||||||
|
@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
#include <gvc.h>
|
#include <gvc.h>
|
||||||
|
|
||||||
GraphvizLayout::GraphvizLayout(LineType lineType, Direction direction)
|
GraphvizLayout::GraphvizLayout(LayoutType lineType, Direction direction)
|
||||||
: GraphLayout({})
|
: GraphLayout({})
|
||||||
, direction(direction)
|
, direction(direction)
|
||||||
, lineType(lineType)
|
, layoutType(lineType)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ void GraphvizLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &block
|
|||||||
strc.reserve(2 * blocks.size());
|
strc.reserve(2 * blocks.size());
|
||||||
std::map<std::pair<ut64, ut64>, Agedge_t *> edges;
|
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) {
|
switch (direction) {
|
||||||
case Direction::LR:
|
case Direction::LR:
|
||||||
agsafeset(g, STR("rankdir"), STR("LR"), STR(""));
|
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);
|
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) {
|
for (auto &blockIt : blocks) {
|
||||||
auto &block = blockIt.second;
|
auto &block = blockIt.second;
|
||||||
@ -198,7 +218,7 @@ void GraphvizLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &block
|
|||||||
auto it = std::prev(edge.polyline.end());
|
auto it = std::prev(edge.polyline.end());
|
||||||
QPointF direction = *it;
|
QPointF direction = *it;
|
||||||
direction -= *(--it);
|
direction -= *(--it);
|
||||||
edge.arrow = getArrowDirection(direction, lineType == LineType::Polyline);
|
edge.arrow = getArrowDirection(direction, layoutType == LayoutType::DotPolyline);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
edge.arrow = GraphEdge::Down;
|
edge.arrow = GraphEdge::Down;
|
||||||
|
@ -7,22 +7,26 @@
|
|||||||
class GraphvizLayout : public GraphLayout
|
class GraphvizLayout : public GraphLayout
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum class LineType {
|
enum class LayoutType {
|
||||||
Ortho,
|
DotOrtho,
|
||||||
Polyline
|
DotPolyline,
|
||||||
|
Sfdp,
|
||||||
|
Neato,
|
||||||
|
TwoPi,
|
||||||
|
Circo,
|
||||||
};
|
};
|
||||||
enum class Direction {
|
enum class Direction {
|
||||||
TB,
|
TB,
|
||||||
LR
|
LR
|
||||||
};
|
};
|
||||||
GraphvizLayout(LineType lineType, Direction direction = Direction::TB);
|
GraphvizLayout(LayoutType layoutType, Direction direction = Direction::TB);
|
||||||
virtual void CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks,
|
virtual void CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks,
|
||||||
ut64 entry,
|
ut64 entry,
|
||||||
int &width,
|
int &width,
|
||||||
int &height) const override;
|
int &height) const override;
|
||||||
private:
|
private:
|
||||||
Direction direction;
|
Direction direction;
|
||||||
LineType lineType;
|
LayoutType layoutType;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // GRAPHVIZLAYOUT_H
|
#endif // GRAPHVIZLAYOUT_H
|
||||||
|
@ -7,21 +7,12 @@
|
|||||||
#include <QContextMenuEvent>
|
#include <QContextMenuEvent>
|
||||||
|
|
||||||
MemoryDockWidget::MemoryDockWidget(MemoryWidgetType type, MainWindow *parent)
|
MemoryDockWidget::MemoryDockWidget(MemoryWidgetType type, MainWindow *parent)
|
||||||
: CutterDockWidget(parent)
|
: AddressableDockWidget(parent)
|
||||||
, mType(type)
|
, mType(type)
|
||||||
, seekable(new CutterSeekable(this))
|
|
||||||
, syncAction(tr("Sync/unsync offset"), this)
|
|
||||||
{
|
{
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent->addMemoryDockWidget(this);
|
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()
|
bool MemoryDockWidget::tryRaiseMemoryWidget()
|
||||||
@ -38,13 +29,6 @@ bool MemoryDockWidget::tryRaiseMemoryWidget()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MemoryDockWidget::raiseMemoryWidget()
|
|
||||||
{
|
|
||||||
show();
|
|
||||||
raise();
|
|
||||||
widgetToFocusOnRaise()->setFocus(Qt::FocusReason::TabFocusReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MemoryDockWidget::eventFilter(QObject *object, QEvent *event)
|
bool MemoryDockWidget::eventFilter(QObject *object, QEvent *event)
|
||||||
{
|
{
|
||||||
if (mainWindow && event->type() == QEvent::FocusIn) {
|
if (mainWindow && event->type() == QEvent::FocusIn) {
|
||||||
@ -52,40 +36,3 @@ bool MemoryDockWidget::eventFilter(QObject *object, QEvent *event)
|
|||||||
}
|
}
|
||||||
return CutterDockWidget::eventFilter(object, 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
|
#ifndef MEMORYDOCKWIDGET_H
|
||||||
#define MEMORYDOCKWIDGET_H
|
#define MEMORYDOCKWIDGET_H
|
||||||
|
|
||||||
#include "CutterDockWidget.h"
|
#include "AddressableDockWidget.h"
|
||||||
#include "core/Cutter.h"
|
#include "core/Cutter.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
||||||
class CutterSeekable;
|
|
||||||
|
|
||||||
/* Disassembly/Graph/Hexdump/Decompiler view priority */
|
/* Disassembly/Graph/Hexdump/Decompiler view priority */
|
||||||
enum class MemoryWidgetType { Disassembly, Graph, Hexdump, Decompiler };
|
enum class MemoryWidgetType { Disassembly, Graph, Hexdump, Decompiler };
|
||||||
|
|
||||||
class MemoryDockWidget : public CutterDockWidget
|
class MemoryDockWidget : public AddressableDockWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
MemoryDockWidget(MemoryWidgetType type, MainWindow *parent);
|
MemoryDockWidget(MemoryWidgetType type, MainWindow *parent);
|
||||||
~MemoryDockWidget() override {}
|
~MemoryDockWidget() override {}
|
||||||
|
|
||||||
CutterSeekable *getSeekable() const;
|
|
||||||
|
|
||||||
bool tryRaiseMemoryWidget();
|
bool tryRaiseMemoryWidget();
|
||||||
void raiseMemoryWidget();
|
|
||||||
MemoryWidgetType getType() const
|
MemoryWidgetType getType() const
|
||||||
{
|
{
|
||||||
return mType;
|
return mType;
|
||||||
}
|
}
|
||||||
bool eventFilter(QObject *object, QEvent *event) override;
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
|
|
||||||
QVariantMap serializeViewProprties() override;
|
|
||||||
void deserializeViewProperties(const QVariantMap &properties) override;
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
MemoryWidgetType mType;
|
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
|
#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