diff --git a/docs/source/user-docs/menus/graph-widget-context-menu.rst b/docs/source/user-docs/menus/graph-widget-context-menu.rst index 185bcd6f..c58f471f 100644 --- a/docs/source/user-docs/menus/graph-widget-context-menu.rst +++ b/docs/source/user-docs/menus/graph-widget-context-menu.rst @@ -41,11 +41,17 @@ When Graphviz is installed, the following options are also available: - Graphviz PNG Image - Graphviz JPG Image - Graphviz SVG Image - - **Steps:** Right click anywhere on the Graph view and choose ``Export Graph``. +Graph Layout Direction +---------------------------------------- +**Description:** Graph layout direction can be either vertical top to bottom or horizontal left to right. + + +**Steps:** Right click anywhere on the Graph view ``Layout -> Horizontal``. + Choose Graph Layout ---------------------------------------- **Description:** Choose the layout to be used by Cutter to display the Graph. Cutter supports the following Graph layout algorithms: @@ -56,10 +62,7 @@ Choose Graph Layout When Graphviz is installed, the following options are also available: - - Graphviz polyline - - Graphviz polyline LR - - Graphviz ortho - - Graphviz ortho LR - + - Graphviz polyline + - Graphviz ortho **Steps:** Right click anywhere on the Graph view and choose a layout from the ``Layout`` sub-menu. \ No newline at end of file diff --git a/src/Cutter.pro b/src/Cutter.pro index 8512c803..1271e620 100644 --- a/src/Cutter.pro +++ b/src/Cutter.pro @@ -424,7 +424,8 @@ SOURCES += \ common/IOModesController.cpp \ common/SettingsUpgrade.cpp \ dialogs/LayoutManager.cpp \ - common/CutterLayout.cpp + common/CutterLayout.cpp \ + widgets/GraphHorizontalAdapter.cpp GRAPHVIZ_SOURCES = \ widgets/GraphvizLayout.cpp @@ -577,7 +578,8 @@ HEADERS += \ dialogs/LayoutManager.h \ common/CutterLayout.h \ common/BinaryTrees.h \ - common/LinkedListPool.h + common/LinkedListPool.h \ + widgets/GraphHorizontalAdapter.h GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h diff --git a/src/widgets/DisassemblerGraphView.cpp b/src/widgets/DisassemblerGraphView.cpp index 1cbb5a5d..fce620f4 100644 --- a/src/widgets/DisassemblerGraphView.cpp +++ b/src/widgets/DisassemblerGraphView.cpp @@ -42,6 +42,7 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se MainWindow *mainWindow, QList additionalMenuActions) : GraphView(parent), mFontMetrics(nullptr), + graphLayout(GraphView::Layout::GridMedium), blockMenu(new DisassemblyContextMenu(this, mainWindow)), contextMenu(new QMenu(this)), seekable(seekable), @@ -104,27 +105,29 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se , {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} #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]() { - setGraphLayout(layout); - refreshView(); - onSeekChanged(this->seekable->getOffset()); // try to keep the view on current block + this->graphLayout = layout; + updateLayout(); }); - if (layout == getGraphLayout()) { + if (layout == this->graphLayout) { action->setChecked(true); } } layoutMenu->addActions(layoutGroup->actions()); + contextMenu->addSeparator(); contextMenu->addActions(additionalMenuActions); @@ -1155,6 +1158,13 @@ void DisassemblerGraphView::onActionUnhighlightBITriggered() Config()->colorsUpdated(); } +void DisassemblerGraphView::updateLayout() +{ + setGraphLayout(GraphView::makeGraphLayout(graphLayout, horizontalLayoutAction->isChecked())); + refreshView(); + onSeekChanged(this->seekable->getOffset()); // try to keep the view on current block +} + void DisassemblerGraphView::exportGraph(QString filePath, GraphExportType type) { bool graphTransparent = Config()->getBitmapTransparentState(); diff --git a/src/widgets/DisassemblerGraphView.h b/src/widgets/DisassemblerGraphView.h index 8735511e..fd8de4fe 100644 --- a/src/widgets/DisassemblerGraphView.h +++ b/src/widgets/DisassemblerGraphView.h @@ -159,6 +159,7 @@ private slots: void on_actionExportGraph_triggered(); void onActionHighlightBITriggered(); void onActionUnhighlightBITriggered(); + void updateLayout(); private: bool transition_dont_seek = false; @@ -173,8 +174,11 @@ private: bool emptyGraph; ut64 currentBlockAddress = RVA_INVALID; + GraphView::Layout graphLayout; + DisassemblyContextMenu *blockMenu; QMenu *contextMenu; + QAction* horizontalLayoutAction; void connectSeekChanged(bool disconnect); diff --git a/src/widgets/GraphGridLayout.cpp b/src/widgets/GraphGridLayout.cpp index 0e57c83f..c48f0cc6 100644 --- a/src/widgets/GraphGridLayout.cpp +++ b/src/widgets/GraphGridLayout.cpp @@ -248,7 +248,7 @@ void GraphGridLayout::CalculateLayout(std::unordered_map &bloc } for (const auto &edgeList : layoutState.edge) { auto &startBlock = layoutState.grid_blocks[edgeList.first]; - startBlock.outputCount++; + startBlock.outputCount = edgeList.second.size(); for (auto &edge : edgeList.second) { auto &targetBlock = layoutState.grid_blocks[edge.dest]; targetBlock.inputCount++; @@ -573,6 +573,9 @@ void GraphGridLayout::calculateEdgeMainColumn(GraphGridLayout::LayoutState &stat void GraphGridLayout::roughRouting(GraphGridLayout::LayoutState &state) const { auto getSpacingOverride = [this](int blockWidth, int edgeCount) { + if (edgeCount == 0) { + return 0; + } int maxSpacing = blockWidth / edgeCount; if (maxSpacing < layoutConfig.edgeHorizontalSpacing) { return std::max(maxSpacing, 1); @@ -614,6 +617,13 @@ void GraphGridLayout::roughRouting(GraphGridLayout::LayoutState &state) const target.inputCount); edge.points.front().spacingOverride = startSpacingOverride; edge.points.back().spacingOverride = targetSpacingOverride; + if (edge.points.size() <= 2) { + if (startSpacingOverride && startSpacingOverride < targetSpacingOverride) { + edge.points.back().spacingOverride = startSpacingOverride; + } + } else { + edge.points[1].spacingOverride = startSpacingOverride; + } int length = 0; diff --git a/src/widgets/GraphHorizontalAdapter.cpp b/src/widgets/GraphHorizontalAdapter.cpp new file mode 100644 index 00000000..343eb5a3 --- /dev/null +++ b/src/widgets/GraphHorizontalAdapter.cpp @@ -0,0 +1,58 @@ +#include "GraphHorizontalAdapter.h" + +GraphHorizontalAdapter::GraphHorizontalAdapter(std::unique_ptr layout) + : GraphLayout({}) + , layout(std::move(layout)) +{ + swapLayoutConfigDirection(); +} + +void GraphHorizontalAdapter::CalculateLayout( + GraphLayout::Graph &blocks, + unsigned long long entry, + int &width, + int &height) const +{ + for (auto &block : blocks) { + std::swap(block.second.width, block.second.height); + } + layout->CalculateLayout(blocks, entry, height, width); // intentionally swapping height and width + for (auto &block : blocks) { + std::swap(block.second.width, block.second.height); + std::swap(block.second.x, block.second.y); + for (auto &edge : block.second.edges) { + for (auto &point : edge.polyline) { + std::swap(point.rx(), point.ry()); + } + switch (edge.arrow) { + case GraphEdge::Down: + edge.arrow = GraphEdge::Right; + break; + case GraphEdge::Left: + edge.arrow = GraphEdge::Up; + break; + case GraphEdge::Up: + edge.arrow = GraphEdge::Left; + break; + case GraphEdge::Right: + edge.arrow = GraphEdge::Down; + break; + case GraphEdge::None: + edge.arrow = GraphEdge::None; + break; + } + } + } +} + +void GraphHorizontalAdapter::setLayoutConfig(const GraphLayout::LayoutConfig &config) +{ + GraphLayout::setLayoutConfig(config); + swapLayoutConfigDirection(); +} + +void GraphHorizontalAdapter::swapLayoutConfigDirection() +{ + std::swap(layoutConfig.edgeVerticalSpacing, layoutConfig.edgeHorizontalSpacing); + std::swap(layoutConfig.blockVerticalSpacing, layoutConfig.blockHorizontalSpacing); +} diff --git a/src/widgets/GraphHorizontalAdapter.h b/src/widgets/GraphHorizontalAdapter.h new file mode 100644 index 00000000..f17b181b --- /dev/null +++ b/src/widgets/GraphHorizontalAdapter.h @@ -0,0 +1,24 @@ +#ifndef GRAPH_HORIZONTAL_ADAPTER_H +#define GRAPH_HORIZONTAL_ADAPTER_H + +#include "core/Cutter.h" +#include "GraphLayout.h" + +/** + * @brief Adapter for converting vertical graph layout into horizontal one. + */ +class GraphHorizontalAdapter : public GraphLayout +{ +public: + GraphHorizontalAdapter(std::unique_ptr layout); + virtual void CalculateLayout(GraphLayout::Graph &blocks, + ut64 entry, + int &width, + int &height) const override; + void setLayoutConfig(const LayoutConfig &config) override; +private: + std::unique_ptr layout; + void swapLayoutConfigDirection(); +}; + +#endif // GRAPH_HORIZONTAL_ADAPTER_H diff --git a/src/widgets/GraphLayout.h b/src/widgets/GraphLayout.h index c5229391..dc973b60 100644 --- a/src/widgets/GraphLayout.h +++ b/src/widgets/GraphLayout.h @@ -42,6 +42,10 @@ public: virtual ~GraphLayout() {} virtual void CalculateLayout(Graph &blocks, ut64 entry, int &width, int &height) const = 0; + virtual void setLayoutConfig(const LayoutConfig &config) + { + this->layoutConfig = config; + }; protected: LayoutConfig layoutConfig; }; diff --git a/src/widgets/GraphView.cpp b/src/widgets/GraphView.cpp index 44f6f421..47df8520 100644 --- a/src/widgets/GraphView.cpp +++ b/src/widgets/GraphView.cpp @@ -4,6 +4,7 @@ #ifdef CUTTER_ENABLE_GRAPHVIZ #include "GraphvizLayout.h" #endif +#include "GraphHorizontalAdapter.h" #include "Helpers.h" #include @@ -36,7 +37,7 @@ GraphView::GraphView(QWidget *parent) glWidget = nullptr; } #endif - setGraphLayout(Layout::GridMedium); + setGraphLayout(makeGraphLayout(Layout::GridMedium)); } GraphView::~GraphView() @@ -513,36 +514,45 @@ QPoint GraphView::viewToLogicalCoordinates(QPoint p) return p / current_scale + offset; } -void GraphView::setGraphLayout(GraphView::Layout layout) +void GraphView::setGraphLayout(std::unique_ptr layout) { - graphLayout = layout; + graphLayoutSystem = std::move(layout); + if (!graphLayoutSystem) { + graphLayoutSystem = makeGraphLayout(Layout::GridMedium); + } +} + +std::unique_ptr GraphView::makeGraphLayout(GraphView::Layout layout, bool horizontal) +{ + std::unique_ptr result; + bool needAdapter = true; switch (layout) { case Layout::GridNarrow: - this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Narrow)); + result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Narrow)); break; case Layout::GridMedium: - this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Medium)); + result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Medium)); break; case Layout::GridWide: - this->graphLayoutSystem.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Wide)); + result.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)); + result.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho, + horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB)); + needAdapter = false; 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)); + result.reset(new GraphvizLayout(GraphvizLayout::LineType::Polyline, + horizontal ? GraphvizLayout::Direction::LR : GraphvizLayout::Direction::TB)); + needAdapter = false; break; #endif } + if (needAdapter && horizontal) { + result.reset(new GraphHorizontalAdapter(std::move(result))); + } + return result; } void GraphView::addBlock(GraphView::GraphBlock block) diff --git a/src/widgets/GraphView.h b/src/widgets/GraphView.h index 3c41f2f5..a7796c8a 100644 --- a/src/widgets/GraphView.h +++ b/src/widgets/GraphView.h @@ -44,11 +44,10 @@ public: , GridWide #ifdef CUTTER_ENABLE_GRAPHVIZ , GraphvizOrtho - , GraphvizOrthoLR , GraphvizPolyline - , GraphvizPolylineLR #endif }; + static std::unique_ptr makeGraphLayout(Layout layout, bool horizontal = false); struct EdgeConfiguration { QColor color = QColor(128, 128, 128); @@ -77,8 +76,8 @@ public: GraphView::GraphBlock *getBlockContaining(QPoint p); QPoint viewToLogicalCoordinates(QPoint p); - void setGraphLayout(Layout layout); - Layout getGraphLayout() const { return graphLayout; } + void setGraphLayout(std::unique_ptr layout); + GraphLayout& getGraphLayout() const { return *graphLayoutSystem; } void paint(QPainter &p, QPoint offset, QRect area, qreal scale = 1.0, bool interactive = true); @@ -174,7 +173,6 @@ private: QSize cacheSize; QOpenGLWidget *glWidget; #endif - Layout graphLayout; /** * @brief flag to control if the cache is invalid and should be re-created in the next draw