from PyQt5.QtCore import Qt, QPointF, pyqtSignal from PyQt5.QtGui import QPen, QKeySequence, QTransform, QCursor from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsProxyWidget, QGraphicsItem, QUndoStack, QAction, QUndoView from .undo import * from .dialogs import showUndoDialog import shapes class CustomView(QGraphicsView): """ Defines custom QGraphicsView with zoom features and drag-drop accept event, overriding wheel event """ def __init__(self, scene = None, parent=None): if scene is not None: #overloaded constructor super(CustomView, self).__init__(scene, parent) else: super(CustomView, self).__init__(parent) self._zoom = 1 self.setDragMode(True) #sets pannable using mouse self.setAcceptDrops(True) #sets ability to accept drops #following four functions are required to be overridden for drag-drop functionality def dragEnterEvent(self, QDragEnterEvent): #defines acceptable drop items if QDragEnterEvent.mimeData().hasText(): QDragEnterEvent.acceptProposedAction() def dragMoveEvent(self, QDragMoveEvent): #defines acceptable drop items if QDragMoveEvent.mimeData().hasText(): QDragMoveEvent.acceptProposedAction() def dragLeaveEvent(self, QDragLeaveEvent): #accept any drag leave event, avoid unnecessary logging QDragLeaveEvent.accept() def dropEvent(self, QDropEvent): #defines item drop, fetches text, creates corresponding QGraphicItem and adds it to scene if QDropEvent.mimeData().hasText(): #QDropEvent.mimeData().text() defines intended drop item, the pos values define position obj = QDropEvent.mimeData().text().replace(',', '').split('/') graphic = getattr(shapes, obj[0])(*map(lambda x: int(x) if x.isdigit() else x, obj[1:])) graphic.setPos(QDropEvent.pos().x(), QDropEvent.pos().y()) self.scene().addItemPlus(graphic) graphic.setParent(self) QDropEvent.acceptProposedAction() def wheelEvent(self, QWheelEvent): #overload wheelevent, to zoom if control is pressed, else scroll normally if QWheelEvent.modifiers() & Qt.ControlModifier: #check if control is pressed if QWheelEvent.source() == Qt.MouseEventNotSynthesized: #check if precision mouse(mac) # angle delta is 1/8th of a degree per scroll unit if self.zoom + QWheelEvent.angleDelta().y()/2880 > 0.1: # hit and trial value (2880) self.zoom += QWheelEvent.angleDelta().y()/2880 else: # precision delta is exactly equal to amount to scroll if self.zoom + QWheelEvent.pixelDelta().y() > 0.1: self.zoom += QWheelEvent.angleDelta().y() QWheelEvent.accept() # accept event so that scrolling doesnt happen simultaneously else: return super(CustomView, self).wheelEvent(QWheelEvent) # scroll if ctrl not pressed @property def zoom(self): # property for zoom return self._zoom @zoom.setter def zoom(self, value): # set scale according to zoom value being set temp = self.zoom self._zoom = value self.scale(self.zoom / temp, self.zoom / temp) class CustomScene(QGraphicsScene): """ Extends QGraphicsScene with undo-redo functionality """ labelAdded = pyqtSignal(shapes.QGraphicsItem) def __init__(self, *args, parent=None): super(CustomScene, self).__init__(*args, parent=parent) self.undoStack = QUndoStack(self) #Used to store undo-redo moves self.createActions() #creates necessary actions that need to be called for undo-redo def createActions(self): # helper function to create delete, undo and redo shortcuts self.deleteAction = QAction("Delete Item", self) self.deleteAction.setShortcut(Qt.Key_Delete) self.deleteAction.triggered.connect(self.deleteItem) self.undoAction = self.undoStack.createUndoAction(self, "Undo") self.undoAction.setShortcut(QKeySequence.Undo) self.redoAction = self.undoStack.createRedoAction(self, "Redo") self.redoAction.setShortcut(QKeySequence.Redo) def createUndoView(self, parent): # creates an undo stack view for current QGraphicsScene undoView = QUndoView(self.undoStack, parent) showUndoDialog(undoView, parent) def deleteItem(self): # (slot) used to delete all selected items, and add undo action for each of them if self.selectedItems(): for item in self.selectedItems(): if issubclass(item.__class__,shapes.NodeItem) or isinstance(item,shapes.Line): itemToDelete = item self.count = 0 if(issubclass(itemToDelete.__class__,shapes.NodeItem)): for i in itemToDelete.lineGripItems: for j in i.lines: self.count+=1 self.undoStack.push(deleteCommand(j, self)) self.undoStack.push(deleteCommand(itemToDelete, self)) def itemMoved(self, movedItem, lastPos): #item move event, checks if item is moved self.undoStack.push(moveCommand(movedItem, lastPos)) self.advance() def addItemPlus(self, item): # extended add item method, so that a corresponding undo action is also pushed self.undoStack.push(addCommand(item, self)) def mousePressEvent(self, event): # overloaded mouse press event to check if an item was moved bdsp = event.buttonDownScenePos(Qt.LeftButton) #get click pos point = QPointF(bdsp.x(), bdsp.y()) #create a Qpoint from click pos itemList = self.items(point) #get items at said point self.movingItem = itemList[0] if itemList else None #set first item in list as moving item if self.movingItem and event.button() == Qt.LeftButton: self.oldPos = self.movingItem.pos() #if left click is held, then store old pos self.clearSelection() #clears selected items return super(CustomScene, self).mousePressEvent(event) def mouseReleaseEvent(self, event): # overloaded mouse release event to check if an item was moved if self.movingItem and event.button() == Qt.LeftButton: if self.oldPos != self.movingItem.pos(): #if item pos had changed, when mouse was realeased, emit itemMoved signal self.itemMoved(self.movingItem, self.oldPos) self.movingItem = None #clear movingitem reference return super(CustomScene, self).mouseReleaseEvent(event) def mouseMoveEvent(self, mouseEvent): item = self.itemAt(mouseEvent.scenePos().x(), mouseEvent.scenePos().y(), QTransform()) if isinstance(item,shapes.SizeGripItem): item.parentItem().showGripItem() super(CustomScene,self).mouseMoveEvent(mouseEvent) def reInsertLines(self): currentIndex = self.undoStack.index() i = 2 skipper = 0 while i != self.count+2+skipper: currentCommand = self.undoStack.command(currentIndex-i) if not self.undoStack.text(currentIndex-i).__contains__('Move'): currentLine = currentCommand.diagramItem startGrip = currentCommand.startGripItem endGrip = currentCommand.endGripItem index_LineGripStart = currentCommand.indexLGS index_LineGripEnd = currentCommand.indexLGE startGrip.lineGripItems[index_LineGripStart].lines.append(currentLine) endGrip.lineGripItems[index_LineGripEnd].lines.append(currentLine) else: skipper+=1 self.undoStack.setIndex(currentIndex-i) i+=1