from enum import Enum from PyQt5.QtWidgets import QGraphicsScene, QGraphicsItem, QGraphicsLineItem, QGraphicsRectItem, \ QGraphicsEllipseItem, QInputDialog, QTableWidgetItem, QMessageBox from PyQt5.Qt import Qt, QPointF, QPen, QTransform, QApplication from sketchUI.graphics_edge_item import GraphicsEdgeItem from sketchUI.graphics_node_item import GraphicsNodeItem, IconType from sketchUI.graphics_sketch_group import SketchGroup from sketchUI.graphics_sketch_line import SketchedLineItem from sketchUI import mvops from evolution.node_ops import NodeAdd, NodeDelete, NodeRetype from evolution.edge_ops import EdgeDel, EdgeAdd from evolution.attribute_ops import AttributeAdd import commons class Mode(Enum): SELECT = 0 CONNECT = 1 LINE = 2 RECT = 3 CIRCLE = 4 FREE = 5 class SketchScene(QGraphicsScene): def __init__(self, model, parent): QGraphicsScene.__init__(self) self._cur_drawing = False self._mode = None # set from mainwindow on start self._connect_from_item = None # mouse pressed on this item if in connect mode self._cur_model = model self._orig_point = QPointF() self._free_draw_lines = [] self._parent = parent def set_mode(self, mode): self._mode = mode def _in_draw_mode(self): # are we in one of the draw modes? (rect, line, free, ...) if self._mode == Mode.CONNECT or self._mode == Mode.SELECT: return False return True def draw_edge(self, from_item, to_item, edge_id=None): # type: (GraphicsNodeItem, GraphicsNodeItem, str) -> GraphicsEdgeItem # draw an edge between two items. line = GraphicsEdgeItem(from_item, to_item, edge_id) line.setFlag(QGraphicsItem.ItemIsMovable, False) line.setFlag(QGraphicsItem.ItemIsSelectable, False) self.addItem(line) line.redraw() return line def mousePressEvent(self, event): if event.button() == Qt.LeftButton and self._in_draw_mode(): # start drawing, save click point self._orig_point = event.scenePos() self._cur_drawing = True elif event.button() == Qt.LeftButton and self._mode == Mode.CONNECT: # store clicked item to connect it later item = self.itemAt(event.scenePos(), QTransform()) if not item or not isinstance(item, GraphicsNodeItem): return else: self._connect_from_item = item elif event.button() == Qt.LeftButton and self._mode == Mode.SELECT: item = self.itemAt(event.scenePos(), QTransform()) if not item: return elif isinstance(item, GraphicsNodeItem): self._parent.plainTextEdit.appendPlainText("Selected node {}:{}".format(item.node_id, item.get_type())) self.highlight_node(item.node_id, Qt.blue) # load attributes for selected node self._parent.tableWidget.setRowCount(0) self._parent.tableWidget.blockSignals(True) attrs = commons.get_attributes_of_node(self._cur_model, item.node_id) for attr in attrs: table_item_key = QTableWidgetItem(attr.key) table_item_key.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) table_item_val = QTableWidgetItem(attr.val) cur_row_cnt = self._parent.tableWidget.rowCount() self._parent.tableWidget.insertRow(cur_row_cnt) self._parent.tableWidget.setItem(cur_row_cnt, 0, table_item_key) self._parent.tableWidget.setItem(cur_row_cnt, 1, table_item_val) self._parent.tableWidget.blockSignals(False) elif isinstance(item, GraphicsEdgeItem): self._parent.plainTextEdit.appendPlainText("Selected edge ({},{}),id={}".format(item.from_item.get_type(), item.to_item.get_type(), item.edge_id)) else: pass QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event): # if in freehand mode, draw lines from move movement if self._mode == Mode.FREE and self._cur_drawing: pt = event.scenePos() line = QGraphicsLineItem(self._orig_point.x(), self._orig_point.y(), pt.x(), pt.y()) line.setPen(QPen(Qt.black, 2, Qt.SolidLine)) self.addItem(line) self._orig_point = pt self._free_draw_lines.append(line) QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self._cur_drawing: end_point = event.scenePos() if self._mode == Mode.LINE: line = SketchedLineItem(self._orig_point.x(), self._orig_point.y(), end_point.x(), end_point.y()) line.setPen(QPen(Qt.black, 2, Qt.SolidLine)) self.addItem(line) elif self._mode == Mode.RECT: width = abs(end_point.x() - self._orig_point.x()) height = abs(end_point.y() - self._orig_point.y()) rect = QGraphicsRectItem(self._orig_point.x(), self._orig_point.y(), width, height) rect.setPen(QPen(Qt.black, 2, Qt.SolidLine)) self.addItem(rect) elif self._mode == Mode.CIRCLE: width = abs(end_point.x() - self._orig_point.x()) height = abs(end_point.y() - self._orig_point.y()) ellipse = QGraphicsEllipseItem(self._orig_point.x(), self._orig_point.y(), width, height) ellipse.setPen(QPen(Qt.black, 2, Qt.SolidLine)) self.addItem(ellipse) elif self._mode == Mode.FREE: line = QGraphicsLineItem(self._orig_point.x(), self._orig_point.y(), end_point.x(), end_point.y()) line.setPen(QPen(Qt.black, 2, Qt.SolidLine)) self.addItem(line) # group lines together self._free_draw_lines.append(line) group = self.createItemGroup(self._free_draw_lines) group.setFlag(QGraphicsItem.ItemIsSelectable, False) group.setFlag(QGraphicsItem.ItemIsMovable, False) del self._free_draw_lines[:] else: pass self._cur_drawing = False else: if self._mode == Mode.SELECT: item = self.itemAt(event.scenePos(), QTransform()) if not item: return for item in self.items(): if isinstance(item, GraphicsEdgeItem): item.redraw() elif self._mode == Mode.CONNECT: item = self.itemAt(event.scenePos(), QTransform()) if not item or not isinstance(item, GraphicsNodeItem): return from_item = self._connect_from_item to_item = item # global or local edge add? scope, ok = QInputDialog.getItem(self._parent, "Select scope", "Scope", ["Local", "Global"]) if not ok: return QApplication.setOverrideCursor(Qt.WaitCursor) add_handler = EdgeAdd() if scope == "Global": add_handler.execute(self._cur_model, from_item.node_id, to_item.node_id, local=False) else: add_handler.execute(self._cur_model, from_item.node_id, to_item.node_id, local=True) self.draw_edge(self._connect_from_item, item) # reload model because only then the ids of the edges get set anew self._parent._load_model() QApplication.restoreOverrideCursor() else: pass QGraphicsScene.mouseReleaseEvent(self, event) def keyPressEvent(self, event): if not self._mode == Mode.SELECT: return # "del" deletes all selected items if event.key() == Qt.Key_Delete: self._handle_keypress_delete(self.selectedItems()) # "G" groups selected items elif event.key() == Qt.Key_G: selected = self.selectedItems() self.clearSelection() group = SketchGroup() for item in selected: group.addToGroup(item) bb_rect = QGraphicsRectItem(group.boundingRect()) bb_rect.setData(0, "groupBBox") # identifier for "does not belong to the actual sketch" bb_rect.setPen(QPen(Qt.gray, 1, Qt.DashLine)) group.addToGroup(bb_rect) self.addItem(group) group.setFlag(QGraphicsItem.ItemIsSelectable, True) group.setFlag(QGraphicsItem.ItemIsMovable, True) # "T" lets user type selected element elif event.key() == Qt.Key_T: # exactly one element that is a group must be selected selected = self.selectedItems() if len(selected) != 1: return item = selected[0] if isinstance(item, SketchGroup): self._handle_keypress_type_on_group(item) elif isinstance(item, GraphicsNodeItem): self._handle_keypress_type_on_node(item) else: self._parent.plainTextEdit.appendPlainText("Error: Cannot type element {}".format(item)) # "A" attributes a node elif event.key() == Qt.Key_A: self._handle_keypress_attribute(self.selectedItems()) else: QGraphicsScene.keyPressEvent(self, event) def _handle_keypress_type_on_node(self, item): # type: (GraphicsNodeItem) -> None """ type an already typed node = retype it """ old_type = item.get_type() node_type, ok = QInputDialog.getText(self._parent, "Retype node", "New type", text=item.get_type()) if not ok or not node_type: # user canceled or node type empty return if node_type in commons.get_available_types(): self._parent.plainTextEdit.appendPlainText("Error: Already such a type: {}".format(node_type)) return self._parent.plainTextEdit.appendPlainText("Performing retype of node {}".format(node_type)) QApplication.setOverrideCursor(Qt.WaitCursor) retype_handler = NodeRetype() retype_handler.execute(self._cur_model, item.node_id, node_type, local=False) # rename on screen for node_item in self.items(): if not isinstance(node_item, GraphicsNodeItem): continue if node_item.get_type() == old_type: node_item.set_type(node_type) # update list widget self._parent.populate_types() QApplication.restoreOverrideCursor() def _handle_keypress_type_on_group(self, group): # type: (SketchGroup) -> None """ type the selected group = make a real node out of it and store it in the model also capture its concrete syntax and store it in the modelverse """ # get the type from the user node_type, ok = QInputDialog.getText(self._parent, "Type node", "Enter type") if not ok or not node_type: # user canceled or empty type string return reload = False if node_type in commons.get_available_types(): # There is already such a type registered. Overwrite its conrete syntax? box = QMessageBox() box.setText("Type {} already exists".format(node_type)) box.setInformativeText("Do you want to overwrite it?") box.setStandardButtons(QMessageBox.No | QMessageBox.Yes) box.setDefaultButton(QMessageBox.No) ret = box.exec_() if ret == QMessageBox.Yes: reload = True pass else: return # perform add local or global? scope, ok = QInputDialog.getItem(self._parent, "Select scope", "Scope", ["Local", "Global"]) if not ok: return self._parent.plainTextEdit.appendPlainText("Typing group to type {}".format(node_type)) QApplication.setOverrideCursor(Qt.WaitCursor) # add the node to the model add_handler = NodeAdd() if scope == "Global": add_handler.execute(self._cur_model, node_type, local=False, check_if_last=False) else: add_handler.execute(self._cur_model, node_type, local=True, check_if_last=True) # Get node id of newly added node in current model nodeid = commons.all_nodes_with_type(self._cur_model, node_type)[0] self._parent.plainTextEdit.appendPlainText("Capturing concrete syntax of group ...") self._parent.plainTextEdit.repaint() # create concrete syntax model for the sketched elements csm = mvops.new_concrete_syntax_model(node_type, IconType.PRIMITIVE, overwrite=True) if not csm: self._parent.plainTextEdit.appendPlainText("Error: Concrete syntax for type {} already exists".format(node_type)) return # check if we need to scale the group items down to 100x100 first group_brect = group.boundingRect() need_scale = False scale_factor = 1.0 if group_brect.width() > 100 or group_brect.height() > 100: need_scale = True scale_factor = 100.0 / max(group_brect.width(), group_brect.height()) # populate CSM with sketched elements for item in group.childItems(): if item.data(0) == "groupBBox": # just the bounding box from the group, ignore continue if isinstance(item, QGraphicsRectItem): rect = group.get_item_coord_relative(item) if need_scale: new_top_left = rect.topLeft() * scale_factor new_width = rect.width() * scale_factor new_height = rect.height() * scale_factor rect.setTopLeft(new_top_left) rect.setWidth(new_width) rect.setHeight(new_height) mvops.add_rect_to_cs(csm, rect) elif isinstance(item, QGraphicsEllipseItem): rect = group.get_item_coord_relative(item) if need_scale: new_top_left = rect.topLeft() * scale_factor new_width = rect.width() * scale_factor new_height = rect.height() * scale_factor rect.setTopLeft(new_top_left) rect.setWidth(new_width) rect.setHeight(new_height) mvops.add_ellipse_to_cs(csm, rect) elif isinstance(item, SketchedLineItem): p1, p2 = group.get_item_coord_relative(item) if need_scale: p1 *= scale_factor p2 *= scale_factor mvops.add_line_to_cs(csm, p1, p2) else: print("Dont know how to capture CS of item {}".format(item)) if reload: # reload whole scene because concrete syntax of a type has changed self._parent._load_model() QApplication.restoreOverrideCursor() else: # update view: replace group by actual node item with newly populated CS csm_content = mvops.get_consyn_of(node_type) nodeitem = GraphicsNodeItem(nodeid, node_type, csm_content) nodeitem.setPos(group.scenePos()) nodeitem.setFlag(QGraphicsItem.ItemIsSelectable, True) nodeitem.setFlag(QGraphicsItem.ItemIsMovable, True) self.removeItem(group) self.addItem(nodeitem) self._parent.populate_types() QApplication.restoreOverrideCursor() self._parent.plainTextEdit.appendPlainText("OK") def _handle_keypress_delete(self, selected): if len(selected) == 1 and isinstance(selected[0], GraphicsEdgeItem): # an edge is to be deleted edge = selected[0] edge_from_type = edge.from_item.get_type() edge_to_type = edge.to_item.get_type() self._parent.plainTextEdit.appendPlainText("Deleting edge ({},{}),id={}".format(edge_from_type, edge_to_type, edge.edge_id)) # local or global? scope, ok = QInputDialog.getItem(self._parent, "Select scope", "Scope", ["Local", "Global"]) if not ok: return del_handler = EdgeDel() QApplication.setOverrideCursor(Qt.WaitCursor) if scope == "Global": del_handler.execute(self._cur_model, edge.edge_id, local=False, check_if_last=False) # delete all edges with same typing in view for item in self.items(): if not isinstance(item, GraphicsEdgeItem): continue if item.from_item.get_type() in [edge_from_type, edge_to_type] and item.to_item.get_type() in [edge_from_type, edge_to_type]: self.removeItem(item) else: del_handler.execute(self._cur_model, edge.edge_id, local=True, check_if_last=True) # delete from view self.removeItem(edge) QApplication.restoreOverrideCursor() elif len(selected) == 1 and isinstance(selected[0], GraphicsNodeItem): del_hander = NodeDelete() node = selected[0] # a node is to be deleted self._parent.plainTextEdit.appendPlainText("Deleting node of type {}".format(node.get_type())) # when deleting a node, local or global? scope, ok = QInputDialog.getItem(self._parent, "Select scope", "Scope", ["Local", "Global"]) if not ok: return QApplication.setOverrideCursor(Qt.WaitCursor) if scope == "Global": # global language evolution, so delete node with same type everywhere del_hander.execute(self._cur_model, node.node_id, local=False, check_if_last=False) # also delete its associated CS model mvops.del_concrete_syntax_model(node.get_type()) # delete all nodes of type from view for item in self.items(): if not isinstance(item, GraphicsNodeItem): continue if item.get_type() == node.get_type(): self.removeItem(item) else: # just local, delete from this model only del_hander.execute(self._cur_model, node.node_id, local=True, check_if_last=True) if del_hander.was_last(): # it was the last node in the language, so delete its CS model as well mvops.del_concrete_syntax_model(node.get_type()) # delete this node from view self.removeItem(node) # in view, delete edges that were connected to this node as well # modelverse does this on its own so do not delete edges explicitly here if scope == "Local": for edge in self.items(): if not isinstance(edge, GraphicsEdgeItem): continue if edge.from_item.node_id == node.node_id or edge.to_item.node_id == node.node_id: self.removeItem(edge) else: # have to remove all edges connected to this type in model for edge in self.items(): if not isinstance(edge, GraphicsEdgeItem): continue if edge.from_item.get_type() == node.get_type() or edge.to_item.get_type() == node.get_type(): self.removeItem(edge) # repopulate available types just in case self._parent.populate_types() else: if not any(isinstance(x, GraphicsNodeItem) for x in selected) and not any(isinstance(x, GraphicsEdgeItem) for x in selected): # neither NodeItem nor EdgeItem in selected -> untyped sketch item, simply remove for obj in selected: self.removeItem(obj) QApplication.restoreOverrideCursor() def _handle_keypress_attribute(self, selected): if not len(selected) == 1: return item = selected[0] if not isinstance(item, GraphicsNodeItem): return # ask user for key value key, ok = QInputDialog.getText(self._parent, "New attribute", "Key value") if not ok or not key: return # check if key value already used for this node attrs = commons.get_attributes_of_node(self._cur_model, item.node_id) for attr in attrs: if attr.key == key: self._parent.plainTextEdit.appendPlainText("Error: Already such a key for the node: {}".format(key)) return # ask of global or local add attribute scope, ok = QInputDialog.getItem(self._parent, "Select scope", "Scope", ["Local", "Global"]) if not ok: return add_handler = AttributeAdd() if scope == "Global": add_handler.execute(self._cur_model, item.node_id, key, "unknown", local=False) else: add_handler.execute(self._cur_model, item.node_id, key, "unknown", local=True) # add to view self._parent.add_new_attribute(key, "unknown") def highlight_node(self, node_id, color): for item in self.items(): if not isinstance(item, GraphicsNodeItem): continue if item.node_id == node_id: item.set_highlighted(True, color) else: item.set_highlighted(False, color) item.update(item.boundingRect())