From 8287e426baebaa2555afa54d7ad655fe0704563a Mon Sep 17 00:00:00 2001 From: karliss Date: Thu, 19 Sep 2019 08:19:50 +0300 Subject: [PATCH] Graph export without graphviz (#1773) --- src/Cutter.pro | 6 +- src/dialogs/MultitypeFileSaveDialog.cpp | 103 +++++++++++ src/dialogs/MultitypeFileSaveDialog.h | 35 ++++ src/widgets/DisassemblerGraphView.cpp | 226 ++++++++++++++---------- src/widgets/DisassemblerGraphView.h | 22 ++- src/widgets/GraphView.cpp | 176 +++++++++--------- src/widgets/GraphView.h | 36 ++-- src/widgets/OverviewView.cpp | 18 +- src/widgets/OverviewView.h | 11 +- 9 files changed, 430 insertions(+), 203 deletions(-) create mode 100644 src/dialogs/MultitypeFileSaveDialog.cpp create mode 100644 src/dialogs/MultitypeFileSaveDialog.h diff --git a/src/Cutter.pro b/src/Cutter.pro index 918c83d9..5539a536 100644 --- a/src/Cutter.pro +++ b/src/Cutter.pro @@ -374,7 +374,8 @@ SOURCES += \ common/Decompiler.cpp \ menus/AddressableItemContextMenu.cpp \ common/AddressableItemModel.cpp \ - widgets/ListDockWidget.cpp + widgets/ListDockWidget.cpp \ + dialogs/MultitypeFileSaveDialog.cpp GRAPHVIZ_SOURCES = \ widgets/GraphvizLayout.cpp @@ -508,7 +509,8 @@ HEADERS += \ menus/AddressableItemContextMenu.h \ common/AddressableItemModel.h \ widgets/ListDockWidget.h \ - widgets/AddressableItemList.h + widgets/AddressableItemList.h \ + dialogs/MultitypeFileSaveDialog.h GRAPHVIZ_HEADERS = widgets/GraphGridLayout.h diff --git a/src/dialogs/MultitypeFileSaveDialog.cpp b/src/dialogs/MultitypeFileSaveDialog.cpp new file mode 100644 index 00000000..5ac6aa1e --- /dev/null +++ b/src/dialogs/MultitypeFileSaveDialog.cpp @@ -0,0 +1,103 @@ +#include "CutterConfig.h" + +#include "MultitypeFileSaveDialog.h" + +#include + + +MultitypeFileSaveDialog::MultitypeFileSaveDialog(QWidget *parent, + const QString &caption, + const QString &directory) + : QFileDialog(parent, caption, directory) +{ + this->setAcceptMode(AcceptMode::AcceptSave); + this->setFileMode(QFileDialog::AnyFile); + + connect(this, &QFileDialog::filterSelected, this, &MultitypeFileSaveDialog::onFilterSelected); +} + +void MultitypeFileSaveDialog::setTypes(const QVector + types, bool useDetection) +{ + this->hasTypeDetection = useDetection; + this->types.clear(); + this->types.reserve(types.size() + (useDetection ? 1 : 0)); + if (useDetection) { + this->types.push_back(TypeDescription{tr("Detect type (*)"), "", QVariant()}); + } + this->types.append(types); + QStringList filters; + for (auto &type : this->types) { + filters.append(type.description); + } + setNameFilters(filters); + onFilterSelected(this->types.first().description); +} + +MultitypeFileSaveDialog::TypeDescription MultitypeFileSaveDialog::selectedType() const +{ + auto filterIt = findType(this->selectedNameFilter()); + if (filterIt == this->types.end()) { + return {}; + } + if (hasTypeDetection && filterIt == this->types.begin()) { + QFileInfo info(this->selectedFiles().first()); + QString currentSuffix = info.suffix(); + filterIt = std::find_if(types.begin(), types.end(), [¤tSuffix](const TypeDescription & v) { + return currentSuffix == v.extension; + }); + if (filterIt != types.end()) { + return *filterIt; + } + return {}; + } else { + return *filterIt; + } +} + +void MultitypeFileSaveDialog::done(int r) +{ + if (r == QDialog::Accepted) { + QFileInfo info(selectedFiles().first()); + auto selectedType = this->selectedType(); + if (selectedType.extension.isEmpty()) { + QMessageBox::warning(this, tr("File save error"), + tr("Unrecognized extension '%1'").arg(info.suffix())); + return; + } + } + QFileDialog::done(r); +} + +void MultitypeFileSaveDialog::onFilterSelected(const QString &filter) +{ + auto it = findType(filter); + if (it == types.end()) { + return; + } + bool detectionSelected = hasTypeDetection && it == types.begin(); + if (detectionSelected) { + setDefaultSuffix(types[1].extension); + } else { + setDefaultSuffix(it->extension); + } + if (!this->selectedFiles().empty()) { + QString currentSelection = this->selectedFiles().first(); + QFileInfo info(currentSelection); + if (!detectionSelected) { + QString currentSuffix = info.suffix(); + if (currentSuffix != it->extension) { + selectFile(info.dir().filePath(info.completeBaseName() + "." + it->extension)); + } + } + } +} + +QVector::const_iterator +MultitypeFileSaveDialog::findType(const QString &description) const +{ + return std::find_if(types.begin(), types.end(), + [&description](const TypeDescription & v) { + return v.description == description; + }); +} diff --git a/src/dialogs/MultitypeFileSaveDialog.h b/src/dialogs/MultitypeFileSaveDialog.h new file mode 100644 index 00000000..f92fe3fb --- /dev/null +++ b/src/dialogs/MultitypeFileSaveDialog.h @@ -0,0 +1,35 @@ +#ifndef MULTITYPEFILESAVEDIALOG_H +#define MULTITYPEFILESAVEDIALOG_H + +#include +#include +#include + +class MultitypeFileSaveDialog : public QFileDialog +{ + Q_OBJECT + +public: + struct TypeDescription { + QString description; + QString extension; + QVariant data; + }; + + explicit MultitypeFileSaveDialog(QWidget *parent = nullptr, + const QString &caption = QString(), + const QString &directory = QString()); + + void setTypes(const QVector types, bool useDetection = true); + TypeDescription selectedType() const; +protected: + void done(int r) override; +private: + void onFilterSelected(const QString &filter); + QVector::const_iterator findType(const QString &description) const; + + QVector types; + bool hasTypeDetection; +}; + +#endif // MULTITYPEFILESAVEDIALOG_H diff --git a/src/widgets/DisassemblerGraphView.cpp b/src/widgets/DisassemblerGraphView.cpp index 7d3439c0..04a89bea 100644 --- a/src/widgets/DisassemblerGraphView.cpp +++ b/src/widgets/DisassemblerGraphView.cpp @@ -8,6 +8,7 @@ #include "common/TempConfig.h" #include "common/SyntaxHighlighter.h" #include "common/BasicBlockHighlighter.h" +#include "dialogs/MultitypeFileSaveDialog.h" #include #include @@ -29,7 +30,8 @@ #include -DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable* seekable, MainWindow* mainWindow) +DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *seekable, + MainWindow *mainWindow) : GraphView(parent), mFontMetrics(nullptr), blockMenu(new DisassemblyContextMenu(this, mainWindow)), @@ -105,17 +107,17 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable* se contextMenu->addAction(&actionExportGraph); static const std::pair LAYOUT_CONFIG[] = { {tr("Grid narrow"), GraphView::Layout::GridNarrow} - ,{tr("Grid medium"), GraphView::Layout::GridMedium} - ,{tr("Grid wide"), GraphView::Layout::GridWide} + , {tr("Grid medium"), GraphView::Layout::GridMedium} + , {tr("Grid wide"), GraphView::Layout::GridWide} #ifdef CUTTER_ENABLE_GRAPHVIZ - ,{tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline} - ,{tr("Graphviz polyline LR"), GraphView::Layout::GraphvizPolylineLR} - ,{tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho} - ,{tr("Graphviz ortho LR"), GraphView::Layout::GraphvizOrthoLR} + , {tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline} + , {tr("Graphviz polyline LR"), GraphView::Layout::GraphvizPolylineLR} + , {tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho} + , {tr("Graphviz ortho LR"), GraphView::Layout::GraphvizOrthoLR} #endif }; auto layoutMenu = contextMenu->addMenu(tr("Layout")); - QActionGroup* layoutGroup = new QActionGroup(layoutMenu); + QActionGroup *layoutGroup = new QActionGroup(layoutMenu); for (auto &item : LAYOUT_CONFIG) { auto action = layoutGroup->addAction(item.first); action->setCheckable(true); @@ -353,7 +355,7 @@ DisassemblerGraphView::EdgeConfigurationMapping DisassemblerGraphView::getEdgeCo EdgeConfigurationMapping result; for (auto &block : blocks) { for (const auto &edge : block.second.edges) { - result[ {block.first, edge.target}] = edgeConfiguration(block.second, &blocks[edge.target]); + result[ {block.first, edge.target}] = edgeConfiguration(block.second, &blocks[edge.target], false); } } return result; @@ -417,17 +419,16 @@ void DisassemblerGraphView::initFont() mFontMetrics.reset(new CachedFontMetrics(font())); } -void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) +void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) { - int blockX = block.x - getViewOffset().x(); - int blockY = block.y - getViewOffset().y(); + QRectF blockRect(block.x, block.y, block.width, block.height); const qreal padding = 2 * charWidth; p.setPen(Qt::black); p.setBrush(Qt::gray); p.setFont(Config()->getFont()); - p.drawRect(blockX, blockY, block.width, block.height); + p.drawRect(blockRect); breakpoints = Core()->getBreakpointsAddresses(); @@ -441,7 +442,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) RVA addr = seekable->getOffset(); RVA PCAddr = Core()->getProgramCounterValue(); for (const Instr &instr : db.instrs) { - if (instr.contains(addr)) { + if (instr.contains(addr) && interactive) { block_selected = true; selected_instruction = instr.addr; } @@ -470,17 +471,22 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) } // Draw basic block background - p.drawRect(blockX, blockY, - block.width, block.height); + p.drawRect(blockRect); auto bb = Core()->getBBHighlighter()->getBasicBlock(block.entry); if (bb) { QColor color(bb->color); p.setBrush(color); - p.drawRect(blockX, blockY, - block.width, block.height); + p.drawRect(blockRect); } - const int firstInstructionY = blockY + getInstructionOffset(db, 0).y(); + const int firstInstructionY = block.y + getInstructionOffset(db, 0).y(); + + // Stop rendering text when it's too small + auto transform = p.combinedTransform(); + QRect screenChar = transform.mapRect(QRect(0, 0, charWidth, charHeight)); + if (screenChar.width() * p.device()->devicePixelRatioF() < 4) { + return; + } // Draw different background for selected instruction if (selected_instruction != RVA_INVALID) { @@ -491,7 +497,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) } auto selected = instr.addr == selected_instruction; if (selected) { - p.fillRect(QRect(static_cast(blockX + charWidth), y, + p.fillRect(QRect(static_cast(block.x + charWidth), y, static_cast(block.width - (10 + padding)), int(instr.text.lines.size()) * charHeight), disassemblySelectionColor); } @@ -527,8 +533,8 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) QColor selectionColor = ConfigColor("wordHighlight"); - p.fillRect(QRectF(blockX + charWidth * 3 + widthBefore, y, highlightWidth, - charHeight), selectionColor); + p.fillRect(QRectF(block.x + charWidth * 3 + widthBefore, y, highlightWidth, + charHeight), selectionColor); } y += int(instr.text.lines.size()) * charHeight; @@ -544,7 +550,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) } auto PC = instr.addr == PCAddr; if (PC) { - p.fillRect(QRect(static_cast(blockX + charWidth), y, + p.fillRect(QRect(static_cast(block.x + charWidth), y, static_cast(block.width - (10 + padding)), int(instr.text.lines.size()) * charHeight), PCSelectionColor); } @@ -552,52 +558,27 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) } } - qreal render_height = viewport()->size().height(); - - // Stop rendering text when it's too small - if (charHeight * getViewScale() * p.device()->devicePixelRatioF() < 4) { - return; - } - // Render node text - auto x = blockX + padding; - int y = blockY + getTextOffset(0).y(); - qreal lineHeightRender = charHeight; + auto x = block.x + padding; + int y = block.y + getTextOffset(0).y(); for (auto &line : db.header_text.lines) { - qreal lineYRender = y; - lineYRender *= getViewScale(); - // Check if line does NOT intersects with view area - if (0 > lineYRender + lineHeightRender - || render_height < lineYRender) { - y += charHeight; - continue; - } - RichTextPainter::paintRichText(&p, x, y, block.width, charHeight, 0, line, - mFontMetrics.get()); + mFontMetrics.get()); y += charHeight; } for (const Instr &instr : db.instrs) { if (Core()->isBreakpoint(breakpoints, instr.addr)) { - p.fillRect(QRect(static_cast(blockX + charWidth), y, + p.fillRect(QRect(static_cast(block.x + charWidth), y, static_cast(block.width - (10 + padding)), int(instr.text.lines.size()) * charHeight), ConfigColor("gui.breakpoint_background")); if (instr.addr == selected_instruction) { - p.fillRect(QRect(static_cast(blockX + charWidth), y, + p.fillRect(QRect(static_cast(block.x + charWidth), y, static_cast(block.width - (10 + padding)), int(instr.text.lines.size()) * charHeight), disassemblySelectionColor); } } for (auto &line : instr.text.lines) { - qreal lineYRender = y; - lineYRender *= getViewScale(); - if (0 > lineYRender + lineHeightRender - || render_height < lineYRender) { - y += charHeight; - continue; - } - int rectSize = qRound(charWidth); if (rectSize % 2) { rectSize++; @@ -608,8 +589,8 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) Q_UNUSED(bpRect); RichTextPainter::paintRichText(&p, x + charWidth, y, - block.width - charWidth, charHeight, 0, line, - mFontMetrics.get()); + block.width - charWidth, charHeight, 0, line, + mFontMetrics.get()); y += charHeight; } @@ -617,7 +598,8 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) } GraphView::EdgeConfiguration DisassemblerGraphView::edgeConfiguration(GraphView::GraphBlock &from, - GraphView::GraphBlock *to) + GraphView::GraphBlock *to, + bool interactive) { EdgeConfiguration ec; DisassemblyBlock &db = disassembly_blocks[from.entry]; @@ -630,10 +612,12 @@ GraphView::EdgeConfiguration DisassemblerGraphView::edgeConfiguration(GraphView: } ec.start_arrow = false; ec.end_arrow = true; - if (from.entry == currentBlockAddress) { - ec.width_scale = 2.0; - } else if (to->entry == currentBlockAddress) { - ec.width_scale = 2.0; + if (interactive) { + if (from.entry == currentBlockAddress) { + ec.width_scale = 2.0; + } else if (to->entry == currentBlockAddress) { + ec.width_scale = 2.0; + } } return ec; } @@ -1028,45 +1012,107 @@ void DisassemblerGraphView::blockTransitionedTo(GraphView::GraphBlock *to) } +Q_DECLARE_METATYPE(DisassemblerGraphView::GraphExportType); + void DisassemblerGraphView::on_actionExportGraph_triggered() { - QStringList filters; - filters.append(tr("Graphiz dot (*.dot)")); - if (!QStandardPaths::findExecutable("dot").isEmpty() - || !QStandardPaths::findExecutable("xdot").isEmpty()) { - filters.append(tr("GIF (*.gif)")); - filters.append(tr("PNG (*.png)")); - filters.append(tr("JPEG (*.jpg)")); - filters.append(tr("PostScript (*.ps)")); - filters.append(tr("SVG (*.svg)")); - filters.append(tr("JSON (*.json)")); + QVector types = { + {tr("PNG (*.png)"), "png", QVariant::fromValue(GraphExportType::Png)}, + {tr("JPEG (*.jpg)"), "jpg", QVariant::fromValue(GraphExportType::Jpeg)}, + {tr("SVG (*.svg)"), "svg", QVariant::fromValue(GraphExportType::Svg)} + }; + bool hasGraphviz = !QStandardPaths::findExecutable("dot").isEmpty() + || !QStandardPaths::findExecutable("xdot").isEmpty(); + if (hasGraphviz) { + types.append({ + {tr("Graphviz dot (*.dot)"), "dot", QVariant::fromValue(GraphExportType::GVDot)}, + {tr("Graphviz json (*.json)"), "json", QVariant::fromValue(GraphExportType::GVJson)}, + {tr("Graphviz gif (*.gif)"), "gif", QVariant::fromValue(GraphExportType::GVGif)}, + {tr("Graphviz png (*.png)"), "png", QVariant::fromValue(GraphExportType::GVPng)}, + {tr("Graphviz jpg (*.jpg)"), "jpg", QVariant::fromValue(GraphExportType::GVJpeg)}, + {tr("Graphviz PostScript (*.ps)"), "ps", QVariant::fromValue(GraphExportType::GVPostScript)}, + {tr("Graphviz svg (*.svg)"), "svg", QVariant::fromValue(GraphExportType::GVSvg)} + }); } - QFileDialog dialog(this, tr("Export Graph")); - dialog.setAcceptMode(QFileDialog::AcceptSave); - dialog.setFileMode(QFileDialog::AnyFile); - dialog.setNameFilters(filters); - dialog.selectFile("graph"); - dialog.setDefaultSuffix("dot"); + QString defaultName = "graph"; + if (auto f = Core()->functionAt(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; - int startIdx = dialog.selectedNameFilter().lastIndexOf("*.") + 2; - int count = dialog.selectedNameFilter().length() - startIdx - 1; - QString format = dialog.selectedNameFilter().mid(startIdx, count); - QString fileName = dialog.selectedFiles()[0]; - if (format != "dot") { - TempConfig tempConfig; - tempConfig.set("graph.gv.format", format); - qWarning() << Core()->cmd(QString("agfw \"%1\" @ $FB").arg(fileName)); + + auto selectedType = dialog.selectedType(); + if (!selectedType.data.canConvert()) { + qWarning() << "Bad selected type, should not happen."; return; } - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Can't open file"; - return; + QString filePath = dialog.selectedFiles().first(); + exportGraph(filePath, selectedType.data.value()); + +} + +void DisassemblerGraphView::exportGraph(QString filePath, GraphExportType type) +{ + switch (type) { + case GraphExportType::Png: + this->saveAsBitmap(filePath, "png"); + break; + case GraphExportType::Jpeg: + this->saveAsBitmap(filePath, "jpg"); + 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()->cmd(QString("agfd 0x%1").arg(currentFcnAddr, 0, 16)); } - QTextStream fileOut(&file); - fileOut << Core()->cmd("agfd $FB"); + 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()->cmdRaw(QString("agfw \"%1\" @ 0x%2").arg(filePath).arg(currentFcnAddr, 0, 16)); } void DisassemblerGraphView::mousePressEvent(QMouseEvent *event) diff --git a/src/widgets/DisassemblerGraphView.h b/src/widgets/DisassemblerGraphView.h index 4d4522cc..5d44b7a6 100644 --- a/src/widgets/DisassemblerGraphView.h +++ b/src/widgets/DisassemblerGraphView.h @@ -87,17 +87,18 @@ class DisassemblerGraphView : public GraphView }; public: - DisassemblerGraphView(QWidget *parent, CutterSeekable* seekable, MainWindow* mainWindow); + DisassemblerGraphView(QWidget *parent, CutterSeekable *seekable, MainWindow *mainWindow); ~DisassemblerGraphView() override; std::unordered_map disassembly_blocks; - virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block) override; + virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) override; virtual void blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos) override; virtual void blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos) override; virtual bool helpEvent(QHelpEvent *event) override; virtual void blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos) override; virtual GraphView::EdgeConfiguration edgeConfiguration(GraphView::GraphBlock &from, - GraphView::GraphBlock *to) override; + GraphView::GraphBlock *to, + bool interactive) override; virtual void blockTransitionedTo(GraphView::GraphBlock *to) override; void loadCurrentGraph(); @@ -109,6 +110,19 @@ public: using EdgeConfigurationMapping = std::map, EdgeConfiguration>; EdgeConfigurationMapping getEdgeConfigurations(); + enum class GraphExportType { + Png, Jpeg, Svg, GVDot, GVJson, + GVGif, GVPng, GVJpeg, GVPostScript, GVSvg + }; + void exportGraph(QString filePath, GraphExportType type); + void exportR2GraphvizGraph(QString filePath, QString type); + + /** + * @brief keep the current addr of the fcn of Graph + * Everytime overview updates its contents, it compares this value with the one in Graph + * if they aren't same, then Overview needs to update the pixmap cache. + */ + ut64 currentFcnAddr = RVA_INVALID; // TODO: make this less public public slots: void refreshView(); void colorsUpdatedSlot(); @@ -210,7 +224,7 @@ signals: void viewZoomed(); void graphMoved(); void resized(); - void nameChanged(const QString& name); + void nameChanged(const QString &name); public: bool isGraphEmpty() { return emptyGraph; } diff --git a/src/widgets/GraphView.cpp b/src/widgets/GraphView.cpp index 76d64130..8f3805d7 100644 --- a/src/widgets/GraphView.cpp +++ b/src/widgets/GraphView.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #ifndef QT_NO_OPENGL #include @@ -39,14 +40,14 @@ GraphView::GraphView(QWidget *parent) GraphView::~GraphView() { - // TODO: Cleanups } // Callbacks -void GraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) +void GraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) { - Q_UNUSED(p); - Q_UNUSED(block); + Q_UNUSED(p) + Q_UNUSED(block) + Q_UNUSED(interactive) qWarning() << "Draw block not overriden!"; } @@ -99,10 +100,12 @@ void GraphView::blockTransitionedTo(GraphView::GraphBlock *to) } GraphView::EdgeConfiguration GraphView::edgeConfiguration(GraphView::GraphBlock &from, - GraphView::GraphBlock *to) + GraphView::GraphBlock *to, + bool interactive) { - Q_UNUSED(from); - Q_UNUSED(to); + Q_UNUSED(from) + Q_UNUSED(to) + Q_UNUSED(interactive) qWarning() << "Edge configuration not overridden!"; EdgeConfiguration ec; return ec; @@ -128,15 +131,6 @@ void GraphView::computeGraph(ut64 entry) viewport()->update(); } -QPolygonF GraphView::recalculatePolygon(QPolygonF polygon) -{ - QPolygonF ret; - for (int i = 0; i < polygon.size(); i++) { - ret << QPointF(polygon[i].x() - offset.x(), polygon[i].y() - offset.y()); - } - return ret; -} - void GraphView::beginMouseDrag(QMouseEvent *event) { scroll_base_x = event->x(); @@ -302,29 +296,37 @@ void GraphView::paintGraphCache() p.setRenderHint(QPainter::Antialiasing); } - int render_width = viewport()->width(); - int render_height = viewport()->height(); + paint(p, offset, this->viewport()->rect(), current_scale); + p.end(); +#ifndef QT_NO_OPENGL + delete paintDevice; +#endif +} + +void GraphView::paint(QPainter &p, QPoint offset, QRect viewport, qreal scale, bool interactive) +{ + QPointF offsetF(offset.x(), offset.y()); p.setBrush(backgroundColor); - p.drawRect(viewport()->rect()); + p.drawRect(viewport); p.setBrush(Qt::black); - p.scale(current_scale, current_scale); + int render_width = viewport.width(); + int render_height = viewport.height(); + + // window - rectangle in logical coordinates + QRect window = QRect(offset, QSize(qRound(render_width / scale), qRound(render_height / scale))); + p.setWindow(window); + QRect windowF(window.x(), window.y(), window.width(), window.height()); for (auto &blockIt : blocks) { GraphBlock &block = blockIt.second; - qreal blockX = block.x * current_scale; - qreal blockY = block.y * current_scale; - qreal blockWidth = block.width * current_scale; - qreal blockHeight = block.height * current_scale; + QRectF blockRect(block.x, block.y, block.width, block.height); // Check if block is visible by checking if block intersects with view area - if (offset.x() * current_scale < blockX + blockWidth - && blockX < offset.x() * current_scale + render_width - && offset.y() * current_scale < blockY + blockHeight - && blockY < offset.y() * current_scale + render_height) { - drawBlock(p, block); + if (blockRect.intersects(windowF)) { + drawBlock(p, block, interactive); } p.setBrush(Qt::gray); @@ -336,15 +338,15 @@ void GraphView::paintGraphCache() if (edge.polyline.empty()) { continue; } - QPolygonF polyline = recalculatePolygon(edge.polyline); + QPolygonF polyline = edge.polyline; EdgeConfiguration ec = edgeConfiguration(block, &blocks[edge.target]); QPen pen(ec.color); pen.setStyle(ec.lineStyle); pen.setWidthF(pen.width() * ec.width_scale); - if (scale_thickness_multiplier * ec.width_scale > 1.01 && pen.widthF() * current_scale < 2) { - pen.setWidthF(ec.width_scale / current_scale); + if (scale_thickness_multiplier * ec.width_scale > 1.01 && pen.widthF() * scale < 2) { + pen.setWidthF(ec.width_scale / scale); } - if (pen.widthF() * current_scale < 2) { + if (pen.widthF() * scale < 2) { pen.setWidth(0); } p.setPen(pen); @@ -360,7 +362,7 @@ void GraphView::paintGraphCache() QPointF base = tip - dir * 6; arrow << base + 3 * dy; arrow << base - 3 * dy; - p.drawConvexPolygon(recalculatePolygon(arrow)); + p.drawConvexPolygon(arrow); }; if (!polyline.empty()) { @@ -371,32 +373,52 @@ void GraphView::paintGraphCache() if (ec.end_arrow) { auto lastPt = edge.polyline.last(); QPointF dir(0, -1); - switch(edge.arrow) { - case GraphLayout::GraphEdge::Down: - dir = QPointF(0, 1); - break; - case GraphLayout::GraphEdge::Up: - dir = QPointF(0, -1); - break; - case GraphLayout::GraphEdge::Left: - dir = QPointF(-1, 0); - break; - case GraphLayout::GraphEdge::Right: - dir = QPointF(1, 0); - break; - default: - break; + switch (edge.arrow) { + case GraphLayout::GraphEdge::Down: + dir = QPointF(0, 1); + break; + case GraphLayout::GraphEdge::Up: + dir = QPointF(0, -1); + break; + case GraphLayout::GraphEdge::Left: + dir = QPointF(-1, 0); + break; + case GraphLayout::GraphEdge::Right: + dir = QPointF(1, 0); + break; + default: + break; } drawArrow(lastPt, dir); } } } } +} +void GraphView::saveAsBitmap(QString path, const char *format) +{ + QImage image(width, height, QImage::Format_RGB32); + QPainter p; + p.begin(&image); + paint(p, QPoint(0, 0), image.rect(), 1.0, false); + p.end(); + if (!image.save(path, format)) { + qWarning() << "Could not save image"; + } +} + +void GraphView::saveAsSvg(QString path) +{ + QSvgGenerator generator; + generator.setFileName(path); + generator.setSize(QSize(width, height)); + generator.setViewBox(QRect(0, 0, width, height)); + generator.setTitle("Cutter graph export"); + QPainter p; + p.begin(&generator); + paint(p, QPoint(0, 0), QRect(0, 0, width, height), 1.0, false); p.end(); -#ifndef QT_NO_OPENGL - delete paintDevice; -#endif } @@ -470,28 +492,30 @@ void GraphView::setGraphLayout(GraphView::Layout layout) { graphLayout = layout; switch (layout) { - case Layout::GridNarrow: - this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Narrow)); - break; - case Layout::GridMedium: - this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Medium)); - break; - case Layout::GridWide: - this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Wide)); - break; + case Layout::GridNarrow: + this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Narrow)); + break; + case Layout::GridMedium: + this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Medium)); + break; + case Layout::GridWide: + this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Wide)); + break; #ifdef CUTTER_ENABLE_GRAPHVIZ - case Layout::GraphvizOrtho: - this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho)); - break; - case Layout::GraphvizOrthoLR: - this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho, GraphvizLayout::Direction::LR)); - break; - case Layout::GraphvizPolyline: - this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline)); - break; - case Layout::GraphvizPolylineLR: - this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline, GraphvizLayout::Direction::LR)); - break; + case Layout::GraphvizOrtho: + this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho)); + break; + case Layout::GraphvizOrthoLR: + this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho, + GraphvizLayout::Direction::LR)); + break; + case Layout::GraphvizPolyline: + this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline)); + break; + case Layout::GraphvizPolylineLR: + this->graphLayoutSystem.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline, + GraphvizLayout::Direction::LR)); + break; #endif } } @@ -635,12 +659,6 @@ void GraphView::keyPressEvent(QKeyEvent *event) void GraphView::mouseReleaseEvent(QMouseEvent *event) { - // TODO -// if(event->button() == Qt::ForwardButton) -// gotoNextSlot(); -// else if(event->button() == Qt::BackButton) -// gotoPreviousSlot(); - if (scroll_mode && (event->buttons() & (Qt::LeftButton | Qt::MiddleButton)) == 0) { scroll_mode = false; setCursor(Qt::ArrowCursor); diff --git a/src/widgets/GraphView.h b/src/widgets/GraphView.h index b41a9d58..2984f368 100644 --- a/src/widgets/GraphView.h +++ b/src/widgets/GraphView.h @@ -35,13 +35,13 @@ public: enum class Layout { GridNarrow - ,GridMedium - ,GridWide + , GridMedium + , GridWide #ifdef CUTTER_ENABLE_GRAPHVIZ - ,GraphvizOrtho - ,GraphvizOrthoLR - ,GraphvizPolyline - ,GraphvizPolylineLR + , GraphvizOrtho + , GraphvizOrthoLR + , GraphvizPolyline + , GraphvizPolylineLR #endif }; @@ -65,16 +65,13 @@ public: */ void showRectangle(const QRect &rect, bool anywhere = false); - /** - * @brief keep the current addr of the fcn of Graph - * Everytime overview updates its contents, it compares this value with the one in Graph - * if they aren't same, then Overview needs to update the pixmap cache. - */ - ut64 currentFcnAddr = RVA_INVALID; // TODO: move application specific code out of graph view - void setGraphLayout(Layout layout); Layout getGraphLayout() const { return graphLayout; } + void paint(QPainter &p, QPoint offset, QRect area, qreal scale = 1.0, bool interactive = true); + + void saveAsBitmap(QString path, const char *format = nullptr); + void saveAsSvg(QString path); protected: std::unordered_map blocks; QColor backgroundColor = QColor(Qt::white); @@ -89,14 +86,21 @@ protected: void computeGraph(ut64 entry); // Callbacks that should be overridden - virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block); + /** + * @brief drawBlock + * @param p painter object, not necesarily current widget + * @param block + * @param interactive - can be used for disabling elemnts during export + */ + virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive = true); virtual void blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos); virtual void blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos); virtual void blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos); virtual bool helpEvent(QHelpEvent *event); virtual void blockTransitionedTo(GraphView::GraphBlock *to); 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 event(QEvent *event) override; @@ -166,9 +170,7 @@ private: QSize getRequiredCacheSize(); qreal getRequiredCacheDevicePixelRatioF(); - QPolygonF recalculatePolygon(QPolygonF polygon); void beginMouseDrag(QMouseEvent *event); - public: QPoint getViewOffset() const { return offset; } void setViewOffset(QPoint offset); diff --git a/src/widgets/OverviewView.cpp b/src/widgets/OverviewView.cpp index 252f6c7e..9ab8c0a2 100644 --- a/src/widgets/OverviewView.cpp +++ b/src/widgets/OverviewView.cpp @@ -52,17 +52,16 @@ void OverviewView::refreshView() viewport()->update(); } -void OverviewView::drawBlock(QPainter &p, GraphView::GraphBlock &block) +void OverviewView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) { - int blockX = block.x - getViewOffset().x(); - int blockY = block.y - getViewOffset().y(); + Q_UNUSED(interactive) + QRectF blockRect(block.x, block.y, block.width, block.height); p.setPen(Qt::black); p.setBrush(Qt::gray); - p.drawRect(blockX, blockY, block.width, block.height); + p.drawRect(blockRect); p.setBrush(QColor(0, 0, 0, 100)); - p.drawRect(blockX + 2, blockY + 2, - block.width, block.height); + p.drawRect(blockRect.translated(2, 2)); // Draw basic block highlighting/tracing auto bb = Core()->getBBHighlighter()->getBasicBlock(block.entry); @@ -74,8 +73,7 @@ void OverviewView::drawBlock(QPainter &p, GraphView::GraphBlock &block) p.setBrush(disassemblyBackgroundColor); } p.setPen(QPen(graphNodeColor, 1)); - p.drawRect(blockX, blockY, - block.width, block.height); + p.drawRect(blockRect); } void OverviewView::paintEvent(QPaintEvent *event) @@ -131,8 +129,10 @@ void OverviewView::wheelEvent(QWheelEvent *event) } GraphView::EdgeConfiguration OverviewView::edgeConfiguration(GraphView::GraphBlock &from, - GraphView::GraphBlock *to) + GraphView::GraphBlock *to, + bool interactive) { + Q_UNUSED(interactive) EdgeConfiguration ec; auto baseEcIt = edgeConfigurations.find({from.entry, to->entry}); if (baseEcIt != edgeConfigurations.end()) diff --git a/src/widgets/OverviewView.h b/src/widgets/OverviewView.h index 1761c17c..27053289 100644 --- a/src/widgets/OverviewView.h +++ b/src/widgets/OverviewView.h @@ -34,6 +34,12 @@ public: void centreRect(); + /** + * @brief keep the current addr of the fcn of Graph + * Everytime overview updates its contents, it compares this value with the one in Graph + * if they aren't same, then Overview needs to update the pixmap cache. + */ + ut64 currentFcnAddr = RVA_INVALID; // TODO: make this less public public slots: /** * @brief scale and center all nodes in, then run update @@ -97,7 +103,7 @@ private: /** * @brief draw the computed blocks passed by Graph */ - virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block) override; + virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive) override; /** * @brief override the edgeConfiguration so as to @@ -105,7 +111,8 @@ private: * @return EdgeConfiguration */ virtual GraphView::EdgeConfiguration edgeConfiguration(GraphView::GraphBlock &from, - GraphView::GraphBlock *to) override; + GraphView::GraphBlock *to, + bool interactive) override; /** * @brief base background color changing depending on the theme