|
15 | 15 | from .factory import NodeFactory |
16 | 16 | from .menu import NodeGraphMenu, NodesMenu |
17 | 17 | from .model import NodeGraphModel |
18 | | -from .node import NodeObject, BaseNode |
| 18 | +from .node import NodeObject, BaseNode, BackdropNode |
19 | 19 | from .port import Port |
20 | | -from ..constants import (DRAG_DROP_ID, |
21 | | - PIPE_LAYOUT_CURVED, |
22 | | - PIPE_LAYOUT_STRAIGHT, |
23 | | - PIPE_LAYOUT_ANGLE, |
24 | | - IN_PORT, OUT_PORT, |
25 | | - VIEWER_GRID_LINES) |
| 20 | +from ..constants import ( |
| 21 | + DRAG_DROP_ID, |
| 22 | + NODE_LAYOUT_DIRECTION, NODE_LAYOUT_VERTICAL, NODE_LAYOUT_HORIZONTAL, |
| 23 | + PIPE_LAYOUT_CURVED, PIPE_LAYOUT_STRAIGHT, PIPE_LAYOUT_ANGLE, |
| 24 | + IN_PORT, OUT_PORT, |
| 25 | + VIEWER_GRID_LINES |
| 26 | +) |
26 | 27 | from ..widgets.node_space_bar import node_space_bar |
27 | 28 | from ..widgets.viewer import NodeViewer |
28 | 29 |
|
@@ -182,15 +183,17 @@ def _wire_signals(self): |
182 | 183 | self._viewer.moved_nodes.connect(self._on_nodes_moved) |
183 | 184 | self._viewer.node_double_clicked.connect(self._on_node_double_clicked) |
184 | 185 | self._viewer.node_name_changed.connect(self._on_node_name_changed) |
185 | | - self._viewer.insert_node.connect(self._insert_node) |
| 186 | + self._viewer.node_backdrop_updated.connect( |
| 187 | + self._on_node_backdrop_updated) |
| 188 | + self._viewer.insert_node.connect(self._on_insert_node) |
186 | 189 |
|
187 | 190 | # pass through translated signals. |
188 | 191 | self._viewer.node_selected.connect(self._on_node_selected) |
189 | 192 | self._viewer.node_selection_changed.connect( |
190 | 193 | self._on_node_selection_changed) |
191 | 194 | self._viewer.data_dropped.connect(self._on_node_data_dropped) |
192 | 195 |
|
193 | | - def _insert_node(self, pipe, node_id, prev_node_pos): |
| 196 | + def _on_insert_node(self, pipe, node_id, prev_node_pos): |
194 | 197 | """ |
195 | 198 | Slot function triggered when a selected node has collided with a pipe. |
196 | 199 |
|
@@ -344,6 +347,18 @@ def _on_nodes_moved(self, node_data): |
344 | 347 | self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos)) |
345 | 348 | self._undo_stack.endMacro() |
346 | 349 |
|
| 350 | + def _on_node_backdrop_updated(self, node_id, update_property, value): |
| 351 | + """ |
| 352 | + called when a BackdropNode is updated. |
| 353 | +
|
| 354 | + Args: |
| 355 | + node_id (str): backdrop node id. |
| 356 | + value (str): update type. |
| 357 | + """ |
| 358 | + backdrop = self.get_node_by_id(node_id) |
| 359 | + if backdrop and isinstance(backdrop, BackdropNode): |
| 360 | + backdrop.on_backdrop_updated(update_property, value) |
| 361 | + |
347 | 362 | def _on_search_triggered(self, node_type, pos): |
348 | 363 | """ |
349 | 364 | called when the tab search widget is triggered in the viewer. |
@@ -859,7 +874,7 @@ def create_node(self, node_type, name=None, selected=True, color=None, |
859 | 874 | pos (list[int, int]): initial x, y position for the node (default: ``(0, 0)``). |
860 | 875 |
|
861 | 876 | Returns: |
862 | | - NodeGraphQt.BaseNode: the created instance of the node. |
| 877 | + NodeGraphQt.NodeObject: the created instance of the node. |
863 | 878 | """ |
864 | 879 | if not self._editable: |
865 | 880 | return |
@@ -903,6 +918,7 @@ def format_color(clr): |
903 | 918 | else: |
904 | 919 | node.set_parent(None) |
905 | 920 |
|
| 921 | + # update the node view from model. |
906 | 922 | node.update() |
907 | 923 |
|
908 | 924 | undo_cmd = NodeAddedCmd(self, node, node.model.pos) |
@@ -1552,6 +1568,143 @@ def disable_nodes(self, nodes, mode=None): |
1552 | 1568 | return |
1553 | 1569 | nodes[0].set_disabled(mode) |
1554 | 1570 |
|
| 1571 | + # auto layout node functions. |
| 1572 | + |
| 1573 | + @staticmethod |
| 1574 | + def _update_node_rank(node, nodes_rank, down_stream=True): |
| 1575 | + """ |
| 1576 | + Recursive function for updating the node ranking. |
| 1577 | +
|
| 1578 | + Args: |
| 1579 | + node (NodeGraphQt.BaseNode): node to start from. |
| 1580 | + nodes_rank (dict): node ranking object to be updated. |
| 1581 | + down_stream (bool): true to rank down stram. |
| 1582 | + """ |
| 1583 | + if down_stream: |
| 1584 | + node_values = node.connected_output_nodes().values() |
| 1585 | + else: |
| 1586 | + node_values = node.connected_input_nodes().values() |
| 1587 | + |
| 1588 | + connected_nodes = set() |
| 1589 | + for nodes in node_values: |
| 1590 | + connected_nodes.update(nodes) |
| 1591 | + |
| 1592 | + rank = nodes_rank[node] + 1 |
| 1593 | + for n in connected_nodes: |
| 1594 | + if n in nodes_rank: |
| 1595 | + nodes_rank[n] = max(nodes_rank[n], rank) |
| 1596 | + else: |
| 1597 | + nodes_rank[n] = rank |
| 1598 | + NodeGraph._update_node_rank(n, nodes_rank, down_stream) |
| 1599 | + |
| 1600 | + @staticmethod |
| 1601 | + def _compute_node_rank(nodes, down_stream=True): |
| 1602 | + """ |
| 1603 | + Compute the ranking of nodes. |
| 1604 | +
|
| 1605 | + Args: |
| 1606 | + nodes (list[NodeGraphQt.BaseNode]): nodes to start ranking from. |
| 1607 | + down_stream (bool): true to compute down stream. |
| 1608 | +
|
| 1609 | + Returns: |
| 1610 | + dict: {NodeGraphQt.BaseNode: node_rank, ...} |
| 1611 | + """ |
| 1612 | + nodes_rank = {} |
| 1613 | + for node in nodes: |
| 1614 | + nodes_rank[node] = 0 |
| 1615 | + NodeGraph._update_node_rank(node, nodes_rank, down_stream) |
| 1616 | + return nodes_rank |
| 1617 | + |
| 1618 | + def auto_layout_nodes(self, nodes=None, down_stream=True): |
| 1619 | + """ |
| 1620 | + Auto layout the nodes in the node graph. |
| 1621 | +
|
| 1622 | + Args: |
| 1623 | + nodes (list[NodeGraphQt.BaseNode]): list of nodes to auto layout |
| 1624 | + if nodes is None then all nodes is layed out. |
| 1625 | + down_stream (bool): false to layout up stream. |
| 1626 | + """ |
| 1627 | + self.begin_undo('Auto Layout Nodes') |
| 1628 | + |
| 1629 | + nodes = nodes or self.all_nodes() |
| 1630 | + |
| 1631 | + # filter out the backdrops. |
| 1632 | + backdrops = { |
| 1633 | + n: n.nodes() for n in nodes if isinstance(n, BackdropNode) |
| 1634 | + } |
| 1635 | + filtered_nodes = [n for n in nodes if not isinstance(n, BackdropNode)] |
| 1636 | + |
| 1637 | + start_nodes = [] |
| 1638 | + if down_stream: |
| 1639 | + start_nodes += [ |
| 1640 | + n for n in filtered_nodes |
| 1641 | + if not any(n.connected_input_nodes().values()) |
| 1642 | + ] |
| 1643 | + else: |
| 1644 | + start_nodes += [ |
| 1645 | + n for n in filtered_nodes |
| 1646 | + if not any(n.connected_output_nodes().values()) |
| 1647 | + ] |
| 1648 | + |
| 1649 | + if not start_nodes: |
| 1650 | + return |
| 1651 | + |
| 1652 | + node_views = [n.view for n in nodes] |
| 1653 | + nodes_center_0 = self.viewer().nodes_rect_center(node_views) |
| 1654 | + |
| 1655 | + nodes_rank = NodeGraph._compute_node_rank(start_nodes, down_stream) |
| 1656 | + |
| 1657 | + rank_map = {} |
| 1658 | + for node, rank in nodes_rank.items(): |
| 1659 | + if rank in rank_map: |
| 1660 | + rank_map[rank].append(node) |
| 1661 | + else: |
| 1662 | + rank_map[rank] = [node] |
| 1663 | + |
| 1664 | + if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: |
| 1665 | + current_x = 0 |
| 1666 | + node_height = 120 |
| 1667 | + for rank in sorted(range(len(rank_map)), reverse=not down_stream): |
| 1668 | + ranked_nodes = rank_map[rank] |
| 1669 | + max_width = max([node.view.width for node in ranked_nodes]) |
| 1670 | + current_x += max_width |
| 1671 | + current_y = 0 |
| 1672 | + for idx, node in enumerate(ranked_nodes): |
| 1673 | + dy = max(node_height, node.view.height) |
| 1674 | + current_y += 0 if idx == 0 else dy |
| 1675 | + node.set_pos(current_x, current_y) |
| 1676 | + current_y += dy * 0.5 + 10 |
| 1677 | + |
| 1678 | + current_x += max_width * 0.5 + 100 |
| 1679 | + elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: |
| 1680 | + current_y = 0 |
| 1681 | + node_width = 250 |
| 1682 | + for rank in sorted(range(len(rank_map)), reverse=not down_stream): |
| 1683 | + ranked_nodes = rank_map[rank] |
| 1684 | + max_height = max([node.view.height for node in ranked_nodes]) |
| 1685 | + current_y += max_height |
| 1686 | + current_x = 0 |
| 1687 | + for idx, node in enumerate(ranked_nodes): |
| 1688 | + dx = max(node_width, node.view.width) |
| 1689 | + current_x += 0 if idx == 0 else dx |
| 1690 | + node.set_pos(current_x, current_y) |
| 1691 | + current_x += dx * 0.5 + 10 |
| 1692 | + |
| 1693 | + current_y += max_height * 0.5 + 100 |
| 1694 | + |
| 1695 | + nodes_center_1 = self.viewer().nodes_rect_center(node_views) |
| 1696 | + dx = nodes_center_0[0] - nodes_center_1[0] |
| 1697 | + dy = nodes_center_0[1] - nodes_center_1[1] |
| 1698 | + [n.set_pos(n.x_pos() + dx, n.y_pos() + dy) for n in nodes] |
| 1699 | + |
| 1700 | + # wrap the backdrop nodes. |
| 1701 | + for backdrop, contained_nodes in backdrops.items(): |
| 1702 | + backdrop.wrap_nodes(contained_nodes) |
| 1703 | + |
| 1704 | + self.end_undo() |
| 1705 | + |
| 1706 | + # prompt dialog functions. |
| 1707 | + |
1555 | 1708 | def question_dialog(self, text, title='Node Graph'): |
1556 | 1709 | """ |
1557 | 1710 | Prompts a question open dialog with ``"Yes"`` and ``"No"`` buttons in |
@@ -1618,6 +1771,8 @@ def save_dialog(self, current_dir=None, ext=None): |
1618 | 1771 | """ |
1619 | 1772 | return self._viewer.save_dialog(current_dir, ext) |
1620 | 1773 |
|
| 1774 | + # hmmm... refactor functions below for "GroupNode" not "SubGraph". |
| 1775 | + |
1621 | 1776 | def use_OpenGL(self): |
1622 | 1777 | """ |
1623 | 1778 | Use OpenGL to draw the graph. |
|
0 commit comments