From abe7046d4f04fc191a18172c969b752286df1ad5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 30 Aug 2024 16:18:03 +0200 Subject: [PATCH] Scenario: Continue adapt network window to scenario. --- src/View/Scenarios/ContextMenu.py | 22 - src/View/Scenarios/GraphWidget.py | 779 ++++++------------------------ src/View/Scenarios/Window.py | 14 +- 3 files changed, 147 insertions(+), 668 deletions(-) diff --git a/src/View/Scenarios/ContextMenu.py b/src/View/Scenarios/ContextMenu.py index bcee3f0f..e4462fd2 100644 --- a/src/View/Scenarios/ContextMenu.py +++ b/src/View/Scenarios/ContextMenu.py @@ -88,25 +88,3 @@ class NodeMenu(AbstractMenu): else: if action == add_res: self._parent.add_node_reservoir(node) - - -class EdgeMenu(AbstractMenu): - def run(self): - it = self._items[0] - - reverse = self._menu.addAction(self._trad["menu_rev_edge"]) - delete = self._menu.addAction(self._trad["menu_del_edge"]) - if self._parent.graph.is_enable_edge(it.edge): - enable = self._menu.addAction(self._trad["menu_dis_edge"]) - is_enable = True - else: - enable = self._menu.addAction(self._trad["menu_ena_edge"]) - is_enable = False - - action = self._exec() - if action == delete: - self._parent.del_edge(it) - elif action == enable: - self._parent.enable_edge(it, is_enable) - elif action == reverse: - self._parent.reverse_edge(it) diff --git a/src/View/Scenarios/GraphWidget.py b/src/View/Scenarios/GraphWidget.py index 6638362a..5c982365 100644 --- a/src/View/Scenarios/GraphWidget.py +++ b/src/View/Scenarios/GraphWidget.py @@ -36,7 +36,7 @@ from PyQt5.QtWidgets import ( from View.Scenarios.UndoCommand import * from View.Scenarios.ContextMenu import ( - DefaultMenu, NodeMenu, EdgeMenu, + DefaultMenu, NodeMenu, ) logger = logging.getLogger() @@ -44,16 +44,17 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class ScenarioItem(QGraphicsItem): - Type = QGraphicsItem.UserType + 1 +class ScenarioItem(QGraphicsTextItem): + Type = QGraphicsItem.UserType + 10 - def __init__(self, node, graph_widget): + def __init__(self, scenario, graph_widget): super(ScenarioItem, self).__init__() - self.node = node - self.setPos(QPointF(self.node.pos.x, self.node.pos.y)) - self.graph = graph_widget + self.scenario = scenario + + self.setPlainText(self.scenario.name) + self.setDefaultTextColor(Qt.black) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) @@ -61,339 +62,114 @@ class ScenarioItem(QGraphicsItem): self.setZValue(1) def type(self): - return NodeItem.Type - - def boundingRect(self): - adjust = 2.0 - return QRectF(-10 - adjust, -10 - adjust, 20 + adjust, 20 + adjust) - - def shape(self): - path = QPainterPath() - if self.graph.graph._reservoir.get_assoc_to_node(self.node) is None: - path.addEllipse(-10, -10, 20, 20) - else: - path.addRect(-10, -10, 20, 20) - return path - - @timer - def paint(self, painter, option, widget): - painter.setPen(Qt.NoPen) - - # Select color in function of node position in graph and - # status - color = Qt.darkBlue - if self.graph.selected_new_edge_src_node() == self: - color = Qt.darkRed - elif self.graph.selected_item() == self: - color = Qt.red - elif not self.graph.graph.is_enable_node(self.node): - color = Qt.darkGray - elif self.graph.graph.is_upstream_node(self.node): - color = Qt.yellow - elif self.graph.graph.is_downstream_node(self.node): - color = Qt.green - - painter.setBrush(QBrush(color)) - if self.graph.graph._reservoir.get_assoc_to_node(self.node) is None: - painter.drawEllipse(-10, -10, 20, 20) - else: - painter.drawRect(-10, -10, 20, 20) - - def itemChange(self, change, value): - if change == QGraphicsItem.ItemPositionHasChanged: - self.graph.node_change_position(value, self) - - self.graph.update_edges(self) - return super(NodeItem, self).itemChange(change, value) - - def mousePressEvent(self, event): - self.update() - if not self.graph._only_display: - super(NodeItem, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - self.update() - super(NodeItem, self).mouseReleaseEvent(event) - - def mouseMoveEvent(self, event): - self.update() - if not self.graph._only_display: - super(NodeItem, self).mouseMoveEvent(event) - - -class EdgeItem(QGraphicsItem): - _arc_dist = 0.1 - - Type = QGraphicsItem.UserType + 2 - - def __init__(self, src_node_item, dest_node_item, - edge, graph_widget): - super(EdgeItem, self).__init__() - - self.src_node = src_node_item - self.dest_node = dest_node_item - self.edge = edge - - self.graph = graph_widget - self._ind = 1 - - geometry = self.compute_arc_two_point( - QPointF( - self.src_node.pos().x(), - self.src_node.pos().y() - ), - QPointF( - self.dest_node.pos().x(), - self.dest_node.pos().y() - ), - ) - - self._bound_rect = geometry['bound_rect'] - self._arc = geometry['arc'] - - self.setAcceptedMouseButtons(Qt.NoButton) - - def type(self): - return Edge.Type - - def boundingRect(self): - # Rectangle of edge for display update - pen_width = 2.0 - extra = 0 # (pen_width + 5) / 2.0 - - return self._bound_rect.normalized().adjusted( - -extra, -extra, extra, extra - ) - - def shape(self): - return self._arc - - def paint(self, painter, option, widget): - # self.paint_line(painter, option, widget) - self.paint_arc(painter, option, widget) - - @timer - def paint_line(self, painter, option, widget): - # Draw shape of the edge - # color = QColor(Qt.white) - # color.setAlpha(128) - # painter.setBrush(color) - # painter.drawPath(self.shape()) - - line = QLineF(self.src_node.pos(), self.dest_node.pos()) - if line.length() == 0.0: - return - - # Select color - # color = Qt.darkBlue - color = Qt.black - if self.graph.selected_item() == self: - color = Qt.red - elif self.graph.current_edge() == self: - color = Qt.blue - elif not self.graph.graph.is_enable_edge(self.edge): - color = Qt.darkGray - - painter.setPen( - QPen( - color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin - ) - ) - - painter.drawLine(line) - - # Draw arrow - center = QPointF( - (self.src_node.pos().x() + self.dest_node.pos().x()) / 2, - (self.src_node.pos().y() + self.dest_node.pos().y()) / 2, - ) - angle = math.acos(line.dx() / line.length()) - if line.dy() >= 0: - angle = (math.pi * 2.0) - angle - - self.paint_arrow( - painter, option, widget, - color, center, angle - ) - - @timer - def paint_arc(self, painter, option, widget): - line = QLineF(self.src_node.pos(), self.dest_node.pos()) - if line.length() == 0.0: - return - - # Select color - # color = Qt.darkBlue - color = Qt.black - if self.graph.selected_item() == self: - color = Qt.red - elif self.graph.current_edge() == self: - color = Qt.blue - elif not self.graph.graph.is_enable_edge(self.edge): - color = Qt.darkGray - - painter.setPen( - QPen( - color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin - ) - ) - - geometry = self.compute_arc_two_point( - QPointF( - self.src_node.pos().x(), - self.src_node.pos().y() - ), - QPointF( - self.dest_node.pos().x(), - self.dest_node.pos().y() - ), - ) - - self._bound_rect = geometry['bound_rect'] - self._arc = geometry['arc'] - - painter.drawPath(self._arc) - - line = geometry['line'] - angle = math.acos(line.dx() / line.length()) - if line.dy() >= 0: - angle = (math.pi * 2.0) - angle - - self.paint_arrow( - painter, option, widget, - color, geometry['c'], angle, - ) - - def compute_arc_two_point(self, a, b): - # This function is a black magic spell for invoking an arc - # path between two points in Qt! (In fact, it's just cryptic - # trigonometry functions from stackoverflow: - # https://stackoverflow.com/questions/26901540/arc-in-qgraphicsscene) - c = self.compute_arc_p3(a, b) - - line_ac = QLineF(a, c) - line_bc = QLineF(b, c) - line_ab = QLineF(a, b) - - rad = abs( - line_bc.length() / ( - 2 * math.sin( - math.radians( - line_ac.angleTo(line_ab) - ) - ) - ) - ) - - bisector_bc = QLineF(line_bc.pointAt(0.5), line_bc.p2()) - bisector_bc.setAngle(line_bc.normalVector().angle()) - - bisector_ab = QLineF(line_ab.pointAt(0.5), line_ab.p2()) - bisector_ab.setAngle(line_ab.normalVector().angle()) - - center = QPointF(0, 0) - bisector_ab.intersect(bisector_bc, center) - - circle = QRectF( - center.x() - rad, - center.y() - rad, - rad * 2, rad * 2 - ) - - line_oa = QLineF(center, a) - line_ob = QLineF(center, b) - line_oc = QLineF(center, c) - - start = line_ob - end = line_oa - - w = 1 - bounding_rect = circle.adjusted( - -w, -w, w, w - ) - - path = QPainterPath() - path.arcMoveTo(circle, start.angle()) - path.arcTo(circle, start.angle(), start.angleTo(end)) - - return { - 'bound_rect': bounding_rect, - 'arc': path, - 'c': c, - 'line': line_ab, - } - - def compute_arc_p3(self, p1, p2): - dist_p3 = self._ind * self._arc_dist - - center_p1_p2 = QPointF( - (p1.x() + p2.x()) / 2, - (p1.y() + p2.y()) / 2 - ) - - # u = (p2.x() - p1.x(), p2.y() - p1.y()) - v = (p2.y() - p1.y(), p1.x() - p2.x()) - p3 = QPointF( - center_p1_p2.x() + dist_p3 * v[0], - center_p1_p2.y() + dist_p3 * v[1] - ) - - return p3 - - def paint_arrow(self, painter, option, widget, - color, center, angle): - brush = QBrush() - brush.setColor(color) - brush.setStyle(Qt.SolidPattern) - - size = 10.0 - arrow_p1 = center + QPointF( - math.sin(angle - math.pi / 3) * size, - math.cos(angle - math.pi / 3) * size - ) - arrow_p2 = center + QPointF( - math.sin(angle - math.pi + math.pi / 3) * size, - math.cos(angle - math.pi + math.pi / 3) * size - ) - poly = QPolygonF([center, arrow_p1, arrow_p2]) - path = QPainterPath() - path.addPolygon(poly) - - painter.drawPolygon(poly) - painter.fillPath(path, brush) - - -class NodeText(QGraphicsTextItem): - def __init__(self, node_item): - super(NodeText, self).__init__() - - self.item = node_item - self.setPlainText(self.item.node.name) - self.setDefaultTextColor(Qt.black) - self.set_custom_pos(self.item.pos()) - self.setZValue(2) + return ScenarioItem.Type def set_custom_pos(self, pos): x = pos.x() - y = pos.y() - 42 # Dont panic! The answer is 42 + y = pos.y() self.setPos(QPointF(x, y)) @timer def paint(self, painter, option, widget): color = QColor(Qt.white) - color.setAlpha(128) - # Draw text background - painter.setBrush(color) - painter.drawRect(self.boundingRect()) + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(color)) + painter.drawEllipse(self.boundingRect()) - # Draw text - super(NodeText, self).paint(painter, option, widget) + super(ScenarioItem, self).paint(painter, option, widget) - def rename(self): - # Update the node text - self.setPlainText(self.item.node.name) + +class EdgeItem(QGraphicsItem): + Type = QGraphicsItem.UserType + 11 + + def __init__(self, scenario_item, parent_item, graph_widget): + super(EdgeItem, self).__init__() + + self.item = scenario_item + self.parent_item = parent_item + self.graph = graph_widget + + self.setAcceptedMouseButtons(Qt.NoButton) + + def type(self): + return EdgeItem.Type + + def boundingRect(self): + pen_width = 2.0 + extra = (pen_width + 5) / 2.0 + + return QRectF( + self.parent_item.pos(), + QSizeF( + self.item.pos().x() - self.parent_item.pos().x(), + self.item.pos().y() - self.parent_item.pos().y() + ) + ).normalized().adjusted(-extra, -extra, extra, extra) + + def shape(self): + vec = self.item.pos() - self.parent_item.pos() + vec = vec * (5 / math.sqrt(QPointF.dotProduct(vec, vec))) + extra = QPointF(vec.y(), -vec.x()) + + result = QPainterPath(self.parent_item.pos() - vec + extra) + result.lineTo(self.parent_item.pos() - vec - extra) + result.lineTo(self.item.pos() + vec - extra) + result.lineTo(self.item.pos() + vec + extra) + result.closeSubpath() + + return result + + @timer + def paint(self, painter, option, widget): + line = QLineF(self.parent_item.pos(), self.item.pos()) + if line.length() == 0.0: + return + + color = Qt.black + painter.setPen( + QPen( + color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin + ) + ) + # Draw the line + painter.drawLine(line) + + arrow = self.get_arrow_path(line, color) + + brush = QBrush() + brush.setColor(color) + brush.setStyle(Qt.SolidPattern) + + path = QPainterPath() + path.addPolygon(arrow) + + painter.drawPolygon(arrow) + painter.fillPath(path, brush) + + def get_arrow_path(self, line, color): + line_center = QPointF( + (self.parent_item.pos().x() + self.item.pos().x()) / 2, + (self.parent_item.pos().y() + self.item.pos().y()) / 2, + ) + + angle = math.acos(line.dx() / line.length()) + if line.dy() >= 0: + angle = (math.pi * 2.0) - angle + + size = 10.0 + arrow_p1 = line_center + QPointF( + math.sin(angle - math.pi / 3) * size, + math.cos(angle - math.pi / 3) * size + ) + arrow_p2 = line_center + QPointF( + math.sin(angle - math.pi + math.pi / 3) * size, + math.cos(angle - math.pi + math.pi / 3) * size + ) + poly = QPolygonF([line_center, arrow_p1, arrow_p2]) + + return poly class GraphWidget(QGraphicsView): @@ -407,20 +183,15 @@ class GraphWidget(QGraphicsView): super(GraphWidget, self).__init__(parent=parent) self.parent = parent - self._state = "move" - self._only_display = only_display self._undo = undo self._trad = trad self.graph = graph self._selected_item = None - self._selected_new_edge_src_node = None - self._current_edge = None self._current_moved_node = None - self.tmp_line = None - self.node_items = [] + self.scenarios_items = {} self.edge_items = [] self.texts = {} @@ -461,155 +232,22 @@ class GraphWidget(QGraphicsView): Returns: Nothing """ - self.node_items = [] + self.scenarios_items = {} self.edge_items = [] - self.texts = {} - for node in self.graph.nodes(): - inode = NodeItem(node, self) - self.texts[inode] = NodeText(inode) - self.scene().addItem(self.texts[inode]) - self.scene().addItem(inode) - self.node_items.append(inode) + for scenar in self.graph.scenarios.lst: + iscenar = ScenarioItem(scenar, self) + self.scene().addItem(iscenar) + self.scenarios_items[scenar] = iscenar - curr_edge = self.graph.current_reach() - - iedges = [] - multiple_edges = {} - for edge in self.graph.edges(): - n1 = next( - filter( - lambda n: n.node.name == edge.node1.name, - self.node_items + if scenar.parent is not None: + iedge = EdgeItem( + self.scenarios_items[scenar], + self.scenarios_items[scenar.parent], + self ) - ) - - n2 = next( - filter( - lambda n: n.node.name == edge.node2.name, - self.node_items - ) - ) - - # Multiple edges counter - if (n1, n2) not in multiple_edges: - ind = 1 - else: - ind = multiple_edges[(n1, n2)] + 1 - - multiple_edges[(n1, n2)] = ind - - iedge = EdgeItem(n1, n2, edge, self) - iedge._ind = ind - - if edge == curr_edge: - self._current_edge = iedge - - iedges.append(iedge) - - for iedge in reversed(iedges): - self.scene().addItem(iedge) - self.edge_items.append(iedge) - - def state(self, status): - """Set the current status of draw widget - - Set the current status of draw widget, like "move", "add" or - "del" - - Args: - status: String of current status - - Returns: - Nothing - """ - self._state = status - - def add_node(self, pos): - """Create a new node in graph and draw it - - Args: - pos: The position of new node - - Returns: - Nothing - """ - node = self.graph.create_node(pos.x(), pos.y()) - self._undo.push( - AddNodeCommand( - self.graph, node - ) - ) - inode = NodeItem(node, self) - self.scene().addItem(inode) - self.node_items.append(inode) - self.texts[inode] = NodeText(inode) - self.scene().addItem(self.texts[inode]) - - self.changeNode.emit(self.sender()) - - def del_node(self, item): - """Delete a node and update display - - Args: - item: The node item to delete - - Returns: - Nothing - """ - node = item.node - self._undo.push( - DelNodeCommand( - self.graph, node - ) - ) - - self.scene().clear() - self.create_items() - self.changeNode.emit(self.sender()) - self.changeEdge.emit(self.sender()) - - def rename_nodes(self): - """Update all nodes item name in scene - - Returns: - Nothing - """ - for i in self.texts: - if type(i) is NodeItem: - self.texts[i].rename() - - def add_node_reservoir(self, node): - self.parent.add_node_reservoir(node) - self.display_update() - - def del_node_reservoir(self, node): - self.parent.del_node_reservoir(node) - self.changeNode.emit(self.sender()) - self.display_update() - - def edit_node_reservoir(self, node): - self.changeNode.emit(self.sender()) - self.parent.edit_node_reservoir(node) - - def enable_edge(self, edge, prev): - self._undo.push( - EnableEdgeCommand( - edge.edge, not prev - ) - ) - self.changeEdge.emit(self.sender()) - self.changeNode.emit(self.sender()) - self.display_update() - - def reverse_edge(self, edge): - self._undo.push( - ReverseEdgeCommand( - edge.edge - ) - ) - self.changeEdge.emit(self.sender()) - self.display_update() + self.scene().addItem(iedge) + self.edge_items.append(iedge) def display_update(self): """Clear the scene and redraw it @@ -620,18 +258,6 @@ class GraphWidget(QGraphicsView): self.scene().clear() self.create_items() - def keyPressEvent(self, event): - key = event.key() - - if key == Qt.Key_Plus: - self.scaleView(1.2) - elif key == Qt.Key_Minus: - self.scaleView(1 / 1.2) - elif key == Qt.Key_Escape: - self.reset_selection() - else: - super(GraphWidget, self).keyPressEvent(event) - def drawBackground(self, painter, rect): sceneRect = self.sceneRect() painter.fillRect( @@ -654,160 +280,35 @@ class GraphWidget(QGraphicsView): self.scale(scaleFactor, scaleFactor) - def mousePressEvent(self, event): - pos = self.mapToScene(event.pos()) - self.clicked = True - - if event.buttons() & Qt.RightButton: - self.update() - super(GraphWidget, self).mousePressEvent(event) - return - - # Move item and select edge item - if self._state == "move": - self._selected_new_edge_src_node = None - - items = self.items(event.pos()) - if items and type(items[0]) is EdgeItem: - edge = items[0] - if edge: - self.set_current_edge(edge) - elif items and type(items[0]) is NodeItem: - self._mouse_origin_x = pos.x() - self._mouse_origin_y = pos.y() - self._current_moved_node = items[0] - - # Add nodes and edges - elif self._state == "add": - items = self.items(event.pos()) - nodes = list(filter(lambda i: type(i) is NodeItem, items)) - if not nodes: - self.add_node(pos) - self.set_selected_new_edge_src_node(None) - else: - if self.selected_new_edge_src_node() is None: - self.set_selected_new_edge_src_node(nodes[0]) - else: - self.add_edge(self.selected_new_edge_src_node(), nodes[0]) - - # Delete nodes and edges - elif self._state == "del": - self._selected_new_edge_src_node = None - items = list( - filter( - lambda i: type(i) is NodeItem or type(i) is EdgeItem, - self.items(event.pos()) - ) - ) - if len(items) > 0: - item = items[0] - if type(item) is NodeItem: - self.del_node(item) - elif type(item) is EdgeItem: - self.del_edge(item) - - self.update() - super(GraphWidget, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - self.clicked = False - - if self._only_display: - return - - if self._state == "move": - if self._current_moved_node is not None: - pos = self.mapToScene(event.pos()) - self._undo.push( - SetNodePosCommand( - self._current_moved_node, - (pos.x(), pos.y()), - (self._mouse_origin_x, - self._mouse_origin_y) - ) - ) - - self.update() - super(GraphWidget, self).mouseReleaseEvent(event) - - def mouseMoveEvent(self, event): - pos = self.mapToScene(event.pos()) - - # Selecte item on the fly - items = self.items(event.pos()) - selectable_items = list( - filter( - lambda i: (type(i) is NodeItem or type(i) is EdgeItem), - items - ) - ) - - if selectable_items: - self.set_selected_item(selectable_items[0]) - elif not self.clicked: - self.set_selected_item(None) - - # Update temporary line - if self.selected_new_edge_src_node() is not None: - if self.tmp_line is None: - self.tmp_line = NewEdgeLine( - self.selected_new_edge_src_node().pos(), - pos - ) - self.scene().addItem(self.tmp_line) - else: - self.tmp_line.dest = pos - self.tmp_line.update() - - # If state is "move" - if self._state == "move": - # Move on scene - if not self.selected_item(): - if event.buttons() & Qt.LeftButton: - old_p = self.mapToScene( - int(self.m_origin_x), int(self.m_origin_y) - ) - new_p = self.mapToScene(event.pos()) - translation = new_p - old_p - - self.translate(translation.x(), translation.y()) - - self.m_origin_x = event.x() - self.m_origin_y = event.y() - - # Propagate event - self.update() - super(GraphWidget, self).mouseMoveEvent(event) - # Contextual menu def contextMenuEvent(self, event): if self._only_display: return - pos = self.mapToScene(event.pos()) - items = self.items(event.pos()) + # pos = self.mapToScene(event.pos()) + # items = self.items(event.pos()) - # Select current menu - while len(items) > 0: - if type(items[0]) in [NodeItem, EdgeItem]: - break - else: - items = items[1:] + # # Select current menu + # while len(items) > 0: + # if type(items[0]) in [NodeItem, EdgeItem]: + # break + # else: + # items = items[1:] - if len(items) == 0: - m_type = DefaultMenu - elif type(items[0]) is NodeItem: - m_type = NodeMenu - elif type(items[0]) is EdgeItem: - m_type = EdgeMenu - else: - m_type = DefaultMenu + # if len(items) == 0: + # m_type = DefaultMenu + # elif type(items[0]) is NodeItem: + # m_type = NodeMenu + # elif type(items[0]) is EdgeItem: + # m_type = EdgeMenu + # else: + # m_type = DefaultMenu - # Create and exec menu - m = m_type( - event=event, pos=pos, items=items, - graph=self.graph, trad=self._trad, parent=self - ) - m.run() - self.clicked = False + # # Create and exec menu + # m = m_type( + # event=event, pos=pos, items=items, + # graph=self.graph, trad=self._trad, parent=self + # ) + # m.run() + # self.clicked = False diff --git a/src/View/Scenarios/Window.py b/src/View/Scenarios/Window.py index 0afe5ea7..af0c7339 100644 --- a/src/View/Scenarios/Window.py +++ b/src/View/Scenarios/Window.py @@ -81,18 +81,18 @@ class ScenariosWindow(PamhyrWindow): self.setup_connections() def setup_graph(self): - # self._graph_widget = GraphWidget( - # self._study, - # undo=self._undo_stack, - # trad=self._trad, - # parent=self, - # ) + self._graph_widget = GraphWidget( + self._study, + undo=self._undo_stack, + trad=self._trad, + parent=self, + ) self._graph_layout = self.find( QHBoxLayout, "horizontalLayout_graph" ) - # self._graph_layout.addWidget(self._graph_widget) + self._graph_layout.addWidget(self._graph_widget) def setup_table(self): table = self.find(QTableView, "tableView_scenarios")