class ModFileTreeModel_QUndo(ModFileTreeModel): """ An extension of the ModFileTreeModel that only handles undo events; everything else is delegated back to the base class """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._stack = QUndoStack(self) @property def undostack(self): return self._stack @property def has_unsaved_changes(self): return not self._stack.isClean() def setMod(self, mod_entry): if mod_entry is self.mod: return self.save() self._stack.clear() super().setMod(mod_entry) # todo: show dialog box asking if the user would like to save # unsaved changes; have checkbox to allow 'remembering' the answer def setData(self, index, value, role=Qt_CheckStateRole): """Simply a wrapper around the base setData() that only pushes a qundocommand if setData would have returned True""" # see if the setData() command should succeed if super().setData(index, value, role): # if it does, the model should have created and queued a # command; try to to grab it and push it to the undostack try: self._stack.push(self.dequeue_command()) except IndexError as e: # if there was no command in the queue...well, what happened? self.LOGGER.error("No command queued") self.LOGGER.exception(e) # pass return True return False def save(self): # if we have any changes to apply: if not self._stack.isClean(): super().save() self._stack.setClean() def revert_changes(self): # revert to 'clean' state self._stack.setIndex(self._stack.cleanIndex())
class IconEditorGrid(QWidget): """ Class implementing the icon editor grid. @signal canRedoChanged(bool) emitted after the redo status has changed @signal canUndoChanged(bool) emitted after the undo status has changed @signal clipboardImageAvailable(bool) emitted to signal the availability of an image to be pasted @signal colorChanged(QColor) emitted after the drawing color was changed @signal imageChanged(bool) emitted after the image was modified @signal positionChanged(int, int) emitted after the cursor poition was changed @signal previewChanged(QPixmap) emitted to signal a new preview pixmap @signal selectionAvailable(bool) emitted to signal a change of the selection @signal sizeChanged(int, int) emitted after the size has been changed @signal zoomChanged(int) emitted to signal a change of the zoom value """ canRedoChanged = pyqtSignal(bool) canUndoChanged = pyqtSignal(bool) clipboardImageAvailable = pyqtSignal(bool) colorChanged = pyqtSignal(QColor) imageChanged = pyqtSignal(bool) positionChanged = pyqtSignal(int, int) previewChanged = pyqtSignal(QPixmap) selectionAvailable = pyqtSignal(bool) sizeChanged = pyqtSignal(int, int) zoomChanged = pyqtSignal(int) Pencil = 1 Rubber = 2 Line = 3 Rectangle = 4 FilledRectangle = 5 Circle = 6 FilledCircle = 7 Ellipse = 8 FilledEllipse = 9 Fill = 10 ColorPicker = 11 RectangleSelection = 20 CircleSelection = 21 MarkColor = QColor(255, 255, 255, 255) NoMarkColor = QColor(0, 0, 0, 0) ZoomMinimum = 100 ZoomMaximum = 10000 ZoomStep = 100 ZoomDefault = 1200 ZoomPercent = True def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (QWidget) """ super(IconEditorGrid, self).__init__(parent) self.setAttribute(Qt.WA_StaticContents) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.__curColor = Qt.black self.__zoom = 12 self.__curTool = self.Pencil self.__startPos = QPoint() self.__endPos = QPoint() self.__dirty = False self.__selecting = False self.__selRect = QRect() self.__isPasting = False self.__clipboardSize = QSize() self.__pasteRect = QRect() self.__undoStack = QUndoStack(self) self.__currentUndoCmd = None self.__image = QImage(32, 32, QImage.Format_ARGB32) self.__image.fill(qRgba(0, 0, 0, 0)) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) self.__compositingMode = QPainter.CompositionMode_SourceOver self.__lastPos = (-1, -1) self.__gridEnabled = True self.__selectionAvailable = False self.__initCursors() self.__initUndoTexts() self.setMouseTracking(True) self.__undoStack.canRedoChanged.connect(self.canRedoChanged) self.__undoStack.canUndoChanged.connect(self.canUndoChanged) self.__undoStack.cleanChanged.connect(self.__cleanChanged) self.imageChanged.connect(self.__updatePreviewPixmap) QApplication.clipboard().dataChanged.connect(self.__checkClipboard) self.__checkClipboard() def __initCursors(self): """ Private method to initialize the various cursors. """ self.__normalCursor = QCursor(Qt.ArrowCursor) pix = QPixmap(":colorpicker-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__colorPickerCursor = QCursor(pix, 1, 21) pix = QPixmap(":paintbrush-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__paintCursor = QCursor(pix, 0, 19) pix = QPixmap(":fill-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__fillCursor = QCursor(pix, 3, 20) pix = QPixmap(":aim-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__aimCursor = QCursor(pix, 10, 10) pix = QPixmap(":eraser-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__rubberCursor = QCursor(pix, 1, 16) def __initUndoTexts(self): """ Private method to initialize texts to be associated with undo commands for the various drawing tools. """ self.__undoTexts = { self.Pencil: self.tr("Set Pixel"), self.Rubber: self.tr("Erase Pixel"), self.Line: self.tr("Draw Line"), self.Rectangle: self.tr("Draw Rectangle"), self.FilledRectangle: self.tr("Draw Filled Rectangle"), self.Circle: self.tr("Draw Circle"), self.FilledCircle: self.tr("Draw Filled Circle"), self.Ellipse: self.tr("Draw Ellipse"), self.FilledEllipse: self.tr("Draw Filled Ellipse"), self.Fill: self.tr("Fill Region"), } def isDirty(self): """ Public method to check the dirty status. @return flag indicating a modified status (boolean) """ return self.__dirty def setDirty(self, dirty, setCleanState=False): """ Public slot to set the dirty flag. @param dirty flag indicating the new modification status (boolean) @param setCleanState flag indicating to set the undo stack to clean (boolean) """ self.__dirty = dirty self.imageChanged.emit(dirty) if not dirty and setCleanState: self.__undoStack.setClean() def sizeHint(self): """ Public method to report the size hint. @return size hint (QSize) """ size = self.__zoom * self.__image.size() if self.__zoom >= 3 and self.__gridEnabled: size += QSize(1, 1) return size def setPenColor(self, newColor): """ Public method to set the drawing color. @param newColor reference to the new color (QColor) """ self.__curColor = QColor(newColor) self.colorChanged.emit(QColor(newColor)) def penColor(self): """ Public method to get the current drawing color. @return current drawing color (QColor) """ return QColor(self.__curColor) def setCompositingMode(self, mode): """ Public method to set the compositing mode. @param mode compositing mode to set (QPainter.CompositionMode) """ self.__compositingMode = mode def compositingMode(self): """ Public method to get the compositing mode. @return compositing mode (QPainter.CompositionMode) """ return self.__compositingMode def setTool(self, tool): """ Public method to set the current drawing tool. @param tool drawing tool to be used (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection) """ self.__curTool = tool self.__lastPos = (-1, -1) if self.__curTool in [self.RectangleSelection, self.CircleSelection]: self.__selecting = True else: self.__selecting = False if self.__curTool in [ self.RectangleSelection, self.CircleSelection, self.Line, self.Rectangle, self.FilledRectangle, self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse ]: self.setCursor(self.__aimCursor) elif self.__curTool == self.Fill: self.setCursor(self.__fillCursor) elif self.__curTool == self.ColorPicker: self.setCursor(self.__colorPickerCursor) elif self.__curTool == self.Pencil: self.setCursor(self.__paintCursor) elif self.__curTool == self.Rubber: self.setCursor(self.__rubberCursor) else: self.setCursor(self.__normalCursor) def tool(self): """ Public method to get the current drawing tool. @return current drawing tool (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection) """ return self.__curTool def setIconImage(self, newImage, undoRedo=False, clearUndo=False): """ Public method to set a new icon image. @param newImage reference to the new image (QImage) @keyparam undoRedo flag indicating an undo or redo operation (boolean) @keyparam clearUndo flag indicating to clear the undo stack (boolean) """ if newImage != self.__image: self.__image = newImage.convertToFormat(QImage.Format_ARGB32) self.update() self.updateGeometry() self.resize(self.sizeHint()) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) if undoRedo: self.setDirty(not self.__undoStack.isClean()) else: self.setDirty(False) if clearUndo: self.__undoStack.clear() self.sizeChanged.emit(*self.iconSize()) def iconImage(self): """ Public method to get a copy of the icon image. @return copy of the icon image (QImage) """ return QImage(self.__image) def iconSize(self): """ Public method to get the size of the icon. @return width and height of the image as a tuple (integer, integer) """ return self.__image.width(), self.__image.height() def setZoomFactor(self, newZoom): """ Public method to set the zoom factor in percent. @param newZoom zoom factor (integer >= 100) """ newZoom = max(100, newZoom) # must not be less than 100 if newZoom != self.__zoom: self.__zoom = newZoom // 100 self.update() self.updateGeometry() self.resize(self.sizeHint()) self.zoomChanged.emit(int(self.__zoom * 100)) def zoomFactor(self): """ Public method to get the current zoom factor in percent. @return zoom factor (integer) """ return self.__zoom * 100 def setGridEnabled(self, enable): """ Public method to enable the display of grid lines. @param enable enabled status of the grid lines (boolean) """ if enable != self.__gridEnabled: self.__gridEnabled = enable self.update() def isGridEnabled(self): """ Public method to get the grid lines status. @return enabled status of the grid lines (boolean) """ return self.__gridEnabled def paintEvent(self, evt): """ Protected method called to repaint some of the widget. @param evt reference to the paint event object (QPaintEvent) """ painter = QPainter(self) if self.__zoom >= 3 and self.__gridEnabled: painter.setPen(self.palette().windowText().color()) i = 0 while i <= self.__image.width(): painter.drawLine(self.__zoom * i, 0, self.__zoom * i, self.__zoom * self.__image.height()) i += 1 j = 0 while j <= self.__image.height(): painter.drawLine(0, self.__zoom * j, self.__zoom * self.__image.width(), self.__zoom * j) j += 1 col = QColor("#aaa") painter.setPen(Qt.DashLine) for i in range(0, self.__image.width()): for j in range(0, self.__image.height()): rect = self.__pixelRect(i, j) if evt.region().intersects(rect): color = QColor.fromRgba(self.__image.pixel(i, j)) painter.fillRect(rect, QBrush(Qt.white)) painter.fillRect(QRect(rect.topLeft(), rect.center()), col) painter.fillRect(QRect(rect.center(), rect.bottomRight()), col) painter.fillRect(rect, QBrush(color)) if self.__isMarked(i, j): painter.drawRect(rect.adjusted(0, 0, -1, -1)) painter.end() def __pixelRect(self, i, j): """ Private method to determine the rectangle for a given pixel coordinate. @param i x-coordinate of the pixel in the image (integer) @param j y-coordinate of the pixel in the image (integer) @return rectangle for the given pixel coordinates (QRect) """ if self.__zoom >= 3 and self.__gridEnabled: return QRect(self.__zoom * i + 1, self.__zoom * j + 1, self.__zoom - 1, self.__zoom - 1) else: return QRect(self.__zoom * i, self.__zoom * j, self.__zoom, self.__zoom) def mousePressEvent(self, evt): """ Protected method to handle mouse button press events. @param evt reference to the mouse event object (QMouseEvent) """ if evt.button() == Qt.LeftButton: if self.__isPasting: self.__isPasting = False self.editPaste(True) self.__markImage.fill(self.NoMarkColor.rgba()) self.update(self.__pasteRect) self.__pasteRect = QRect() return if self.__curTool == self.Pencil: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__setImagePixel(evt.pos(), True) self.setDirty(True) self.__undoStack.push(cmd) self.__currentUndoCmd = cmd elif self.__curTool == self.Rubber: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__setImagePixel(evt.pos(), False) self.setDirty(True) self.__undoStack.push(cmd) self.__currentUndoCmd = cmd elif self.__curTool == self.Fill: i, j = self.__imageCoordinates(evt.pos()) col = QColor() col.setRgba(self.__image.pixel(i, j)) cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__drawFlood(i, j, col) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) elif self.__curTool == self.ColorPicker: i, j = self.__imageCoordinates(evt.pos()) col = QColor() col.setRgba(self.__image.pixel(i, j)) self.setPenColor(col) else: self.__unMark() self.__startPos = evt.pos() self.__endPos = evt.pos() def mouseMoveEvent(self, evt): """ Protected method to handle mouse move events. @param evt reference to the mouse event object (QMouseEvent) """ self.positionChanged.emit(*self.__imageCoordinates(evt.pos())) if self.__isPasting and not (evt.buttons() & Qt.LeftButton): self.__drawPasteRect(evt.pos()) return if evt.buttons() & Qt.LeftButton: if self.__curTool == self.Pencil: self.__setImagePixel(evt.pos(), True) self.setDirty(True) elif self.__curTool == self.Rubber: self.__setImagePixel(evt.pos(), False) self.setDirty(True) elif self.__curTool in [self.Fill, self.ColorPicker]: pass # do nothing else: self.__drawTool(evt.pos(), True) def mouseReleaseEvent(self, evt): """ Protected method to handle mouse button release events. @param evt reference to the mouse event object (QMouseEvent) """ if evt.button() == Qt.LeftButton: if self.__curTool in [self.Pencil, self.Rubber]: if self.__currentUndoCmd: self.__currentUndoCmd.setAfterImage(self.__image) self.__currentUndoCmd = None if self.__curTool not in [ self.Pencil, self.Rubber, self.Fill, self.ColorPicker, self.RectangleSelection, self.CircleSelection ]: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) if self.__drawTool(evt.pos(), False): self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.setDirty(True) def __setImagePixel(self, pos, opaque): """ Private slot to set or erase a pixel. @param pos position of the pixel in the widget (QPoint) @param opaque flag indicating a set operation (boolean) """ i, j = self.__imageCoordinates(pos) if self.__image.rect().contains(i, j) and (i, j) != self.__lastPos: if opaque: painter = QPainter(self.__image) painter.setPen(self.penColor()) painter.setCompositionMode(self.__compositingMode) painter.drawPoint(i, j) else: self.__image.setPixel(i, j, qRgba(0, 0, 0, 0)) self.__lastPos = (i, j) self.update(self.__pixelRect(i, j)) def __imageCoordinates(self, pos): """ Private method to convert from widget to image coordinates. @param pos widget coordinate (QPoint) @return tuple with the image coordinates (tuple of two integers) """ i = pos.x() // self.__zoom j = pos.y() // self.__zoom return i, j def __drawPasteRect(self, pos): """ Private slot to draw a rectangle for signaling a paste operation. @param pos widget position of the paste rectangle (QPoint) """ self.__markImage.fill(self.NoMarkColor.rgba()) if self.__pasteRect.isValid(): self.__updateImageRect( self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) x, y = self.__imageCoordinates(pos) isize = self.__image.size() if x + self.__clipboardSize.width() <= isize.width(): sx = self.__clipboardSize.width() else: sx = isize.width() - x if y + self.__clipboardSize.height() <= isize.height(): sy = self.__clipboardSize.height() else: sy = isize.height() - y self.__pasteRect = QRect(QPoint(x, y), QSize(sx - 1, sy - 1)) painter = QPainter(self.__markImage) painter.setPen(self.MarkColor) painter.drawRect(self.__pasteRect) painter.end() self.__updateImageRect(self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) def __drawTool(self, pos, mark): """ Private method to perform a draw operation depending of the current tool. @param pos widget coordinate to perform the draw operation at (QPoint) @param mark flag indicating a mark operation (boolean) @return flag indicating a successful draw (boolean) """ self.__unMark() if mark: self.__endPos = QPoint(pos) drawColor = self.MarkColor img = self.__markImage else: drawColor = self.penColor() img = self.__image start = QPoint(*self.__imageCoordinates(self.__startPos)) end = QPoint(*self.__imageCoordinates(pos)) painter = QPainter(img) painter.setPen(drawColor) painter.setCompositionMode(self.__compositingMode) if self.__curTool == self.Line: painter.drawLine(start, end) elif self.__curTool in [ self.Rectangle, self.FilledRectangle, self.RectangleSelection ]: left = min(start.x(), end.x()) top = min(start.y(), end.y()) right = max(start.x(), end.x()) bottom = max(start.y(), end.y()) if self.__curTool == self.RectangleSelection: painter.setBrush(QBrush(drawColor)) if self.__curTool == self.FilledRectangle: for y in range(top, bottom + 1): painter.drawLine(left, y, right, y) else: painter.drawRect(left, top, right - left, bottom - top) if self.__selecting: self.__selRect = QRect(left, top, right - left + 1, bottom - top + 1) self.__selectionAvailable = True self.selectionAvailable.emit(True) elif self.__curTool in [ self.Circle, self.FilledCircle, self.CircleSelection ]: r = max(abs(start.x() - end.x()), abs(start.y() - end.y())) if self.__curTool in [self.FilledCircle, self.CircleSelection]: painter.setBrush(QBrush(drawColor)) painter.drawEllipse(start, r, r) if self.__selecting: self.__selRect = QRect(start.x() - r, start.y() - r, 2 * r + 1, 2 * r + 1) self.__selectionAvailable = True self.selectionAvailable.emit(True) elif self.__curTool in [self.Ellipse, self.FilledEllipse]: r1 = abs(start.x() - end.x()) r2 = abs(start.y() - end.y()) if r1 == 0 or r2 == 0: return False if self.__curTool == self.FilledEllipse: painter.setBrush(QBrush(drawColor)) painter.drawEllipse(start, r1, r2) painter.end() if self.__curTool in [ self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse ]: self.update() else: self.__updateRect(self.__startPos, pos) return True def __drawFlood(self, i, j, oldColor, doUpdate=True): """ Private method to perform a flood fill operation. @param i x-value in image coordinates (integer) @param j y-value in image coordinates (integer) @param oldColor reference to the color at position i, j (QColor) @param doUpdate flag indicating an update is requested (boolean) (used for speed optimizations) """ if not self.__image.rect().contains(i, j) or \ self.__image.pixel(i, j) != oldColor.rgba() or \ self.__image.pixel(i, j) == self.penColor().rgba(): return self.__image.setPixel(i, j, self.penColor().rgba()) self.__drawFlood(i, j - 1, oldColor, False) self.__drawFlood(i, j + 1, oldColor, False) self.__drawFlood(i - 1, j, oldColor, False) self.__drawFlood(i + 1, j, oldColor, False) if doUpdate: self.update() def __updateRect(self, pos1, pos2): """ Private slot to update parts of the widget. @param pos1 top, left position for the update in widget coordinates (QPoint) @param pos2 bottom, right position for the update in widget coordinates (QPoint) """ self.__updateImageRect(QPoint(*self.__imageCoordinates(pos1)), QPoint(*self.__imageCoordinates(pos2))) def __updateImageRect(self, ipos1, ipos2): """ Private slot to update parts of the widget. @param ipos1 top, left position for the update in image coordinates (QPoint) @param ipos2 bottom, right position for the update in image coordinates (QPoint) """ r1 = self.__pixelRect(ipos1.x(), ipos1.y()) r2 = self.__pixelRect(ipos2.x(), ipos2.y()) left = min(r1.x(), r2.x()) top = min(r1.y(), r2.y()) right = max(r1.x() + r1.width(), r2.x() + r2.width()) bottom = max(r1.y() + r1.height(), r2.y() + r2.height()) self.update(left, top, right - left + 1, bottom - top + 1) def __unMark(self): """ Private slot to remove the mark indicator. """ self.__markImage.fill(self.NoMarkColor.rgba()) if self.__curTool in [ self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse, self.CircleSelection ]: self.update() else: self.__updateRect(self.__startPos, self.__endPos) if self.__selecting: self.__selRect = QRect() self.__selectionAvailable = False self.selectionAvailable.emit(False) def __isMarked(self, i, j): """ Private method to check, if a pixel is marked. @param i x-value in image coordinates (integer) @param j y-value in image coordinates (integer) @return flag indicating a marked pixel (boolean) """ return self.__markImage.pixel(i, j) == self.MarkColor.rgba() def __updatePreviewPixmap(self): """ Private slot to generate and signal an updated preview pixmap. """ p = QPixmap.fromImage(self.__image) self.previewChanged.emit(p) def previewPixmap(self): """ Public method to generate a preview pixmap. @return preview pixmap (QPixmap) """ p = QPixmap.fromImage(self.__image) return p def __checkClipboard(self): """ Private slot to check, if the clipboard contains a valid image, and signal the result. """ ok = self.__clipboardImage()[1] self.__clipboardImageAvailable = ok self.clipboardImageAvailable.emit(ok) def canPaste(self): """ Public slot to check the availability of the paste operation. @return flag indicating availability of paste (boolean) """ return self.__clipboardImageAvailable def __clipboardImage(self): """ Private method to get an image from the clipboard. @return tuple with the image (QImage) and a flag indicating a valid image (boolean) """ img = QApplication.clipboard().image() ok = not img.isNull() if ok: img = img.convertToFormat(QImage.Format_ARGB32) return img, ok def __getSelectionImage(self, cut): """ Private method to get an image from the selection. @param cut flag indicating to cut the selection (boolean) @return image of the selection (QImage) """ if cut: cmd = IconEditCommand(self, self.tr("Cut Selection"), self.__image) img = QImage(self.__selRect.size(), QImage.Format_ARGB32) img.fill(qRgba(0, 0, 0, 0)) for i in range(0, self.__selRect.width()): for j in range(0, self.__selRect.height()): if self.__image.rect().contains(self.__selRect.x() + i, self.__selRect.y() + j): if self.__isMarked(self.__selRect.x() + i, self.__selRect.y() + j): img.setPixel( i, j, self.__image.pixel(self.__selRect.x() + i, self.__selRect.y() + j)) if cut: self.__image.setPixel(self.__selRect.x() + i, self.__selRect.y() + j, qRgba(0, 0, 0, 0)) if cut: self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.__unMark() if cut: self.update(self.__selRect) return img def editCopy(self): """ Public slot to copy the selection. """ if self.__selRect.isValid(): img = self.__getSelectionImage(False) QApplication.clipboard().setImage(img) def editCut(self): """ Public slot to cut the selection. """ if self.__selRect.isValid(): img = self.__getSelectionImage(True) QApplication.clipboard().setImage(img) @pyqtSlot() def editPaste(self, pasting=False): """ Public slot to paste an image from the clipboard. @param pasting flag indicating part two of the paste operation (boolean) """ img, ok = self.__clipboardImage() if ok: if img.width() > self.__image.width() or \ img.height() > self.__image.height(): res = E5MessageBox.yesNo( self, self.tr("Paste"), self.tr("""<p>The clipboard image is larger than the""" """ current image.<br/>Paste as new image?</p>""")) if res: self.editPasteAsNew() return elif not pasting: self.__isPasting = True self.__clipboardSize = img.size() else: cmd = IconEditCommand(self, self.tr("Paste Clipboard"), self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) painter = QPainter(self.__image) painter.setPen(self.penColor()) painter.setCompositionMode(self.__compositingMode) painter.drawImage(self.__pasteRect.x(), self.__pasteRect.y(), img, 0, 0, self.__pasteRect.width() + 1, self.__pasteRect.height() + 1) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.__updateImageRect( self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) else: E5MessageBox.warning( self, self.tr("Pasting Image"), self.tr("""Invalid image data in clipboard.""")) def editPasteAsNew(self): """ Public slot to paste the clipboard as a new image. """ img, ok = self.__clipboardImage() if ok: cmd = IconEditCommand(self, self.tr("Paste Clipboard as New Image"), self.__image) self.setIconImage(img) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editSelectAll(self): """ Public slot to select the complete image. """ self.__unMark() self.__startPos = QPoint(0, 0) self.__endPos = QPoint(self.rect().bottomRight()) self.__markImage.fill(self.MarkColor.rgba()) self.__selRect = self.__image.rect() self.__selectionAvailable = True self.selectionAvailable.emit(True) self.update() def editClear(self): """ Public slot to clear the image. """ self.__unMark() cmd = IconEditCommand(self, self.tr("Clear Image"), self.__image) self.__image.fill(qRgba(0, 0, 0, 0)) self.update() self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editResize(self): """ Public slot to resize the image. """ from .IconSizeDialog import IconSizeDialog dlg = IconSizeDialog(self.__image.width(), self.__image.height()) res = dlg.exec_() if res == QDialog.Accepted: newWidth, newHeight = dlg.getData() if newWidth != self.__image.width() or \ newHeight != self.__image.height(): cmd = IconEditCommand(self, self.tr("Resize Image"), self.__image) img = self.__image.scaled(newWidth, newHeight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) self.setIconImage(img) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editNew(self): """ Public slot to generate a new, empty image. """ from .IconSizeDialog import IconSizeDialog dlg = IconSizeDialog(self.__image.width(), self.__image.height()) res = dlg.exec_() if res == QDialog.Accepted: width, height = dlg.getData() img = QImage(width, height, QImage.Format_ARGB32) img.fill(qRgba(0, 0, 0, 0)) self.setIconImage(img) def grayScale(self): """ Public slot to convert the image to gray preserving transparency. """ cmd = IconEditCommand(self, self.tr("Convert to Grayscale"), self.__image) for x in range(self.__image.width()): for y in range(self.__image.height()): col = self.__image.pixel(x, y) if col != qRgba(0, 0, 0, 0): gray = qGray(col) self.__image.setPixel(x, y, qRgba(gray, gray, gray, qAlpha(col))) self.update() self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editUndo(self): """ Public slot to perform an undo operation. """ if self.__undoStack.canUndo(): self.__undoStack.undo() def editRedo(self): """ Public slot to perform a redo operation. """ if self.__undoStack.canRedo(): self.__undoStack.redo() def canUndo(self): """ Public method to return the undo status. @return flag indicating the availability of undo (boolean) """ return self.__undoStack.canUndo() def canRedo(self): """ Public method to return the redo status. @return flag indicating the availability of redo (boolean) """ return self.__undoStack.canRedo() def __cleanChanged(self, clean): """ Private slot to handle the undo stack clean state change. @param clean flag indicating the clean state (boolean) """ self.setDirty(not clean) def shutdown(self): """ Public slot to perform some shutdown actions. """ self.__undoStack.canRedoChanged.disconnect(self.canRedoChanged) self.__undoStack.canUndoChanged.disconnect(self.canUndoChanged) self.__undoStack.cleanChanged.disconnect(self.__cleanChanged) def isSelectionAvailable(self): """ Public method to check the availability of a selection. @return flag indicating the availability of a selection (boolean) """ return self.__selectionAvailable
class ContentList(QWidget): editContent = pyqtSignal(object) def __init__(self, site, type): QWidget.__init__(self) self.site = site self.addedContentName = "" self.type = type self.undoStack = QUndoStack() vbox = QVBoxLayout() layout = QGridLayout() titleLabel = QLabel() button = QPushButton() if self.type == ContentType.PAGE: button.setText("Add Page") else: button.setText("Add Post") button.setMaximumWidth(120) if self.type == ContentType.PAGE: titleLabel.setText("Pages") else: titleLabel.setText("Posts") fnt = titleLabel.font() fnt.setPointSize(20) fnt.setBold(True) titleLabel.setFont(fnt) self.undo = FlatButton(":/images/undo_normal.png", ":/images/undo_hover.png", "", ":/images/undo_disabled.png") self.redo = FlatButton(":/images/redo_normal.png", ":/images/redo_hover.png", "", ":/images/redo_disabled.png") self.undo.setToolTip("Undo") self.redo.setToolTip("Redo") self.undo.setEnabled(False) self.redo.setEnabled(False) hbox = QHBoxLayout() hbox.addStretch(0) hbox.addWidget(self.undo) hbox.addWidget(self.redo) self.list = QTableWidget(0, 6, self) self.list.verticalHeader().hide() self.list.setSelectionMode(QAbstractItemView.SingleSelection) self.list.setSelectionBehavior(QAbstractItemView.SelectRows) self.list.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) self.list.setToolTip("Double click to edit item") labels = ["", "Name", "Source", "Layout", "Author", "Date"] self.list.setHorizontalHeaderLabels(labels) self.reload() layout.addWidget(titleLabel, 0, 0) layout.addLayout(hbox, 0, 2) layout.addWidget(button, 1, 0) layout.addWidget(self.list, 2, 0, 1, 3) vbox.addLayout(layout) self.setLayout(vbox) button.clicked.connect(self.addPage) self.list.cellDoubleClicked.connect(self.tableDoubleClicked) self.redo.clicked.connect(self.doredo) self.undo.clicked.connect(self.doundo) self.undoStack.canUndoChanged.connect(self.canUndoChanged) self.undoStack.canRedoChanged.connect(self.canRedoChanged) self.undoStack.redoTextChanged.connect(self.redoTextChanged) def reload(self): self.list.setRowCount(0) row = -1 itemToEdit = None if self.type == ContentType.PAGE: self.site.loadPages() for i in range(len(self.site.pages)): content = self.site.pages[i] self.addListItem(content) if content.source == self.addedContentName: row = self.list.rowCount() - 1 itemToEdit = self.list.item(row, 1) else: self.site.loadPosts() for i in range(0, len(self.site.posts)): content = self.site.posts[i] self.addListItem(content) if content.source == self.addedContentName: row = self.list.rowCount() - 1 if itemToEdit: self.addedContentName = "" self.list.selectRow(row) self.editContent.emit(itemToEdit) def addListItem(self, content): rows = self.list.rowCount() self.list.setRowCount(rows + 1) tcb = TableCellButtons() tcb.setItem(content) tcb.deleteItem.connect(self.deleteContent) tcb.editItem.connect(self.edit) self.list.setCellWidget(rows, 0, tcb) self.list.setRowHeight(rows, tcb.sizeHint().height()) titleItem = QTableWidgetItem(content.title) titleItem.setFlags(titleItem.flags() ^ Qt.ItemIsEditable) titleItem.setData(Qt.UserRole, content) self.list.setItem(rows, 1, titleItem) sourceItem = QTableWidgetItem(content.source) sourceItem.setFlags(titleItem.flags() ^ Qt.ItemIsEditable) self.list.setItem(rows, 2, sourceItem) layoutItem = QTableWidgetItem(content.layout) layoutItem.setFlags(layoutItem.flags() ^ Qt.ItemIsEditable) self.list.setItem(rows, 3, layoutItem) authorItem = QTableWidgetItem(content.author) authorItem.setFlags(authorItem.flags() ^ Qt.ItemIsEditable) self.list.setItem(rows, 4, authorItem) dateItem = QTableWidgetItem(content.date.toString("dd.MM.yyyy")) dateItem.setFlags(dateItem.flags() ^ Qt.ItemIsEditable) self.list.setItem(rows, 5, dateItem) def canUndoChanged(self, can): self.undo.setEnabled(can) def canRedoChanged(self, can): self.redo.setEnabled(can) def undoTextChanged(self, text): self.undo.setToolTip("Undo " + text) def redoTextChanged(self, text): self.redo.setToolTip("Redo " + text) def doundo(self): self.undoStack.undo() def doredo(self): self.undoStack.redo() def addPage(self): self.addedContentName = self.site.createTemporaryContent(self.type) info = QFileInfo(self.addedContentName) self.addedContentName = info.fileName() self.reload() def tableDoubleClicked(self, r, i): item = self.list.item(r, 1) self.undoStack.clear() self.editContent.emit(item) def edit(self, content): for row in range(self.list.rowCount()): item = self.list.item(row, 1) m = item.data(Qt.UserRole) if m == content: self.list.selectRow(row) self.undoStack.clear() self.editContent.emit(item) break def deleteContent(self, content): for row in range(self.list.rowCount()): item = self.list.item(row, 1) m = item.data(Qt.UserRole) if m == content: if m.content_type == ContentType.PAGE: subdir = "pages" else: subdir = "posts" delCommand = DeleteContentCommand( self, os.path.join(self.site.source_path, subdir, m.source), "delete content " + m.title) self.undoStack.push(delCommand) break
class mylabel(QLabel): print("into mylabel") def __init__(self, parent): super(mylabel, self).__init__(parent) self.image = QImage() self.drawing = True self.lastPoint = QPoint() self.modified = False self.scribbling = False self.eraserSize = 5 #橡皮擦初始值 self.fontSize = 12 #字形初始值 self.setAcceptDrops(True) self.savetextedit = [] self.text = [] #紀錄文字 self.eraserPos = [] self.temp_img = 1 #紀錄圖片編號 self.numstack = [] self.i = 0 # 紀錄textedit self.eraserClicked = False self.textClicked = False self.clearClicked = False self.undoStack = QUndoStack() self.m_undoaction = self.undoStack.createUndoAction(self, self.tr("&Undo")) # self.m_undoaction.setShortcut('Ctrl+Z') self.addAction(self.m_undoaction) self.m_redoaction = self.undoStack.createRedoAction(self, self.tr("&Redo")) # self.m_redoaction.setShortcut('Ctrl+Y') self.addAction(self.m_redoaction) #開啟圖片 def openimage(self, filename): self.image = filename self.scaredPixmap = self.image.scaled(self.width(), self.height(), aspectRatioMode=Qt.KeepAspectRatio) self.image = self.scaredPixmap self.setAlignment(Qt.AlignLeft) self.update() #獲得橡皮擦大小 def getErasersize(self, esize): self.eraserSize = int(esize) print(self.eraserSize) #獲得字體大小 def getFontsize(self, fsize): self.fontSize = int(fsize) print(self.fontSize) def mousePressEvent(self, event): print("left press") super().mousePressEvent(event) if event.button() == Qt.LeftButton and self.eraserClicked: print("start erase action") self.drawing = True self.eraserPos.clear() self.lastPoint = event.pos() self.eraserPos.append(self.lastPoint) elif event.button() == Qt.LeftButton and self.textClicked: print("add textedit") self.item = self.childAt(event.x(), event.y()) if not self.childAt(event.x(), event.y()): # 如果不是點在文字框上的話 # 文字框 self.textedit = textedit1(self) self.textedit.setObjectName("textedit{}".format(self.i)) self.textedit.setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid black;\n"); # 設定邊框 self.textedit.move(event.pos().x(), event.pos().y()) self.savetextedit.append(self.textedit) print("record textedit", self.savetextedit) # 設定文字框格式 self.textedit.setFont(QFont("Roman times", self.fontSize)) self.textedit.setLineWrapMode(QTextEdit.FixedColumnWidth) # 自動斷行 self.textedit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 隱藏滾動軸 self.textedit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 隱藏滾動軸 self.textedit.show() print(self.textedit) self.item = self.childAt(event.x(), event.y()) print("choose item", self.item) command = storeCommand(self.textedit) self.undoStack.push(command) print("undostack count", self.undoStack.count()) print("into undostack", self.textedit) if self.textedit: # 將文字框設為不能寫 x = 0 for self.savetextedit[x] in self.savetextedit: self.savetextedit[x].setReadOnly(True) self.savetextedit[x].setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid black;\n"); # 設定邊框 x = x + 1 else: self.item.setReadOnly(False) self.doc = self.item.document() self.doc.contentsChanged.connect(self.textAreaChanged) # 隨著輸入的文字改變文字框的大小 elif event.button() == Qt.RightButton: self.item = self.childAt(event.x(), event.y()) print("choose textedit", self.item) # 有存到stack裡,可是沒辦法redo,沒辦法傳進去移動過後的位置 try: if self.item: self.item.setReadOnly(True) print("moveCmd", self.item, self.item.pos()) # self.undoStack.push(moveCommand(self.item, self.item.pos())) self.undoStack.push(moveCommand(self.item, self.item.pos())) print("moveundostack count", self.undoStack.count()) except Exception as e: print(e) def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: print("delete textedit", self.item) self.item.deleteLater() self.savetextedit.remove(self.item) # 隨著輸入的文字改變文字框大小 def textAreaChanged(self): self.doc.adjustSize() newWidth = self.doc.size().width() + 20 newHeight = self.doc.size().height() + 20 if newWidth != self.item.width(): self.item.setFixedWidth(newWidth) if newHeight != self.item.height(): self.item.setFixedHeight(newHeight) def dragEnterEvent(self, e): print("drag") e.accept() def dropEvent(self, e): print("drop item") mime = e.mimeData().text() x, y = map(int, mime.split(',')) print(x,y) position = e.pos() print(position) e.source().setReadOnly(True) self.undoStack.push(moveCommand(e.source(), e.source().pos())) e.source().move(position - QPoint(x,y)) print("move item to", self.item.pos()) e.setDropAction(Qt.MoveAction) e.accept() def mouseMoveEvent(self, event): if (event.buttons() and Qt.LeftButton) and self.drawing and self.eraserClicked: print("start painting") self.path = QPainterPath() self.path.moveTo(self.lastPoint) self.path.lineTo(event.pos()) painter = QPainter(self.image) x = self.width() - self.image.width() y = self.height() - self.image.height() painter.translate(x / 2 - x, y / 2 - y) #因為將圖片置中,所以要重設置畫筆原點 painter.setPen(QPen(Qt.white, self.eraserSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawPath(self.path) print(self.path) self.lastPoint = event.pos() self.eraserPos.append(self.lastPoint) self.update() def mouseReleaseEvent(self, event): print("release mouse") if event.button() == Qt.LeftButton: self.drawing = False #儲存圖片佔存,供undo redo使用 if self.eraserClicked == True: self.imagename = "img{}.PNG".format(self.temp_img) print(self.imagename) self.image.save("./saveload/{}".format(self.imagename)) self.temp_img = self.temp_img + 1 self.numstack.append(self.imagename) def paintEvent(self, event): canvasPainter = QPainter(self) self.x = (self.width() - self.image.width())/2 self.y = (self.height() - self.image.height())/2 canvasPainter.translate(self.x, self.y) canvasPainter.drawImage(event.rect(), self.image, event.rect()) if not self.textClicked: x = 0 self.undoStack.clear() for self.savetextedit[x] in self.savetextedit: self.savetextedit[x].setStyleSheet("background-color:rgb(255,255,255,0);\n" # 設定透明度 "border: 6px solid transparent;\n"); # 設定邊框 if self.savetextedit[x].toPlainText() == "": self.savetextedit[x].deleteLater() self.savetextedit.remove(self.savetextedit[x]) x = x + 1 self.update()
class DiagramScene(QGraphicsScene): """ This class implements the main Diagram Scene. """ GridPen = QPen(QColor(80, 80, 80), 0, Qt.SolidLine) GridSize = 20 MinSize = 2000 MaxSize = 1000000 RecentNum = 5 sgnInsertionEnded = pyqtSignal('QGraphicsItem', int) sgnItemAdded = pyqtSignal('QGraphicsItem') sgnModeChanged = pyqtSignal(DiagramMode) sgnItemRemoved = pyqtSignal('QGraphicsItem') sgnUpdated = pyqtSignal() #################################################################################################################### # # # DIAGRAM SCENE IMPLEMENTATION # # # #################################################################################################################### def __init__(self, mainwindow, parent=None): """ Initialize the diagram scene. :type mainwindow: MainWindow :type parent: QWidget """ super().__init__(parent) self.document = File(parent=self) self.guid = GUID(self) self.factory = ItemFactory(self) self.index = ItemIndex(self) self.meta = PredicateMetaIndex(self) self.undostack = QUndoStack(self) self.undostack.setUndoLimit(50) self.validator = OWL2Validator(self) self.mainwindow = mainwindow self.pasteOffsetX = Clipboard.PasteOffsetX self.pasteOffsetY = Clipboard.PasteOffsetY self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.mouseOverNode = None self.mousePressEdge = None self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = {} connect(self.sgnItemAdded, self.index.add) connect(self.sgnItemRemoved, self.index.remove) #################################################################################################################### # # # EVENTS # # # #################################################################################################################### def dragEnterEvent(self, dragEvent): """ Executed when a dragged element enters the scene area. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragEnterEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dragMoveEvent(self, dragEvent): """ Executed when an element is dragged over the scene. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragMoveEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dropEvent(self, dropEvent): """ Executed when a dragged element is dropped on the scene. :type dropEvent: QGraphicsSceneDragDropEvent """ super().dropEvent(dropEvent) if dropEvent.mimeData().hasFormat('text/plain'): item = Item.forValue(dropEvent.mimeData().text()) node = self.factory.create(item=item, scene=self) node.setPos(snap(dropEvent.scenePos(), DiagramScene.GridSize, self.mainwindow.snapToGrid)) self.undostack.push(CommandNodeAdd(scene=self, node=node)) self.sgnInsertionEnded.emit(node, dropEvent.modifiers()) dropEvent.setDropAction(Qt.CopyAction) dropEvent.accept() else: dropEvent.ignore() def mousePressEvent(self, mouseEvent): """ Executed when a mouse button is clicked on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & Qt.LeftButton: if self.mode is DiagramMode.InsertNode: ######################################################################################################## # # # NODE INSERTION # # # ######################################################################################################## item = Item.forValue(self.modeParam) node = self.factory.create(item, self) node.setPos(snap(mousePos, DiagramScene.GridSize, self.mainwindow.snapToGrid)) self.undostack.push(CommandNodeAdd(self, node)) self.sgnInsertionEnded.emit(node, mouseEvent.modifiers()) super().mousePressEvent(mouseEvent) elif self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## node = self.itemOnTopOf(mousePos, edges=False) if node: item = Item.forValue(self.modeParam) edge = self.factory.create(item, self, source=node) edge.updateEdge(mousePos) self.mousePressEdge = edge self.addItem(edge) super().mousePressEvent(mouseEvent) else: super().mousePressEvent(mouseEvent) if self.mode is DiagramMode.Idle: #################################################################################################### # # # ITEM SELECTION # # # #################################################################################################### # See if we have some nodes selected in the scene: this is needed because itemOnTopOf # will discard labels, so if we have a node whose label is overlapping the node shape, # clicking on the label will make itemOnTopOf return the node item instead of the label. selected = self.selectedNodes() if selected: # We have some nodes selected in the scene so we probably are going to do a # move operation, prepare data for mouse move event => select a node that will act # as mouse grabber to compute delta movements for each componenet in the selection. self.mousePressNode = self.itemOnTopOf(mousePos, edges=False) if self.mousePressNode: self.mousePressNodePos = self.mousePressNode.pos() self.mousePressPos = mousePos self.mousePressData = { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in selected}, 'edges': {} } # Figure out if the nodes we are moving are sharing edges: if so, move the edge # together with the nodes (which actually means moving the edge breakpoints). for node in self.mousePressData['nodes']: for edge in node.edges: if edge not in self.mousePressData['edges']: if edge.other(node).isSelected(): self.mousePressData['edges'][edge] = edge.breakpoints[:] def mouseMoveEvent(self, mouseEvent): """ Executed when then mouse is moved on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & Qt.LeftButton: if self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## if self.mousePressEdge: edge = self.mousePressEdge edge.updateEdge(mousePos) currentNode = self.itemOnTopOf(mousePos, edges=False, skip={edge.source}) previousNode = self.mouseOverNode statusBar = self.mainwindow.statusBar() if previousNode: previousNode.updateBrush(selected=False) if currentNode: self.mouseOverNode = currentNode res = self.validator.result(edge.source, edge, currentNode) currentNode.updateBrush(selected=False, valid=res.valid) if not res.valid: statusBar.showMessage(res.message) else: statusBar.clearMessage() else: statusBar.clearMessage() self.mouseOverNode = None self.validator.clear() else: if self.mode is DiagramMode.Idle: if self.mousePressNode: self.setMode(DiagramMode.MoveNode) if self.mode is DiagramMode.MoveNode: #################################################################################################### # # # ITEM MOVEMENT # # # #################################################################################################### point = self.mousePressNodePos + mousePos - self.mousePressPos point = snap(point, DiagramScene.GridSize, self.mainwindow.snapToGrid) delta = point - self.mousePressNodePos edges = set() # Update all the breakpoints positions. for edge, breakpoints in self.mousePressData['edges'].items(): for i in range(len(breakpoints)): edge.breakpoints[i] = breakpoints[i] + delta # Move all the selected nodes. for node, data in self.mousePressData['nodes'].items(): edges |= set(node.edges) node.setPos(data['pos'] + delta) for edge, pos in data['anchors'].items(): node.setAnchor(edge, pos + delta) # Update edges. for edge in edges: edge.updateEdge() super().mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): """ Executed when the mouse is released from the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButton = mouseEvent.button() mousePos = mouseEvent.scenePos() if mouseButton == Qt.LeftButton: if self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## if self.mousePressEdge: edge = self.mousePressEdge edge.source.updateBrush(selected=False) currentNode = self.itemOnTopOf(mousePos, edges=False, skip={edge.source}) insertEdge = False if currentNode: currentNode.updateBrush(selected=False) if self.validator.valid(edge.source, edge, currentNode): edge.target = currentNode insertEdge = True # We remove the item temporarily from the graphics scene and we perform the add using # the undo command that will also emit the sgnItemAdded signal hence all the widgets will # be notified of the edge insertion. We do this because while creating the edge we need # to display it so the users knows what is he connecting, but we don't want to truly insert # it till it's necessary (when the mouse is released and the validator allows the insertion) self.removeItem(edge) if insertEdge: self.undostack.push(CommandEdgeAdd(self, edge)) edge.updateEdge() self.mouseOverNode = None self.mousePressEdge = None self.clearSelection() self.validator.clear() statusBar = self.mainwindow.statusBar() statusBar.clearMessage() self.sgnInsertionEnded.emit(edge, mouseEvent.modifiers()) elif self.mode is DiagramMode.MoveNode: ######################################################################################################## # # # ITEM MOVEMENT # # # ######################################################################################################## data = { 'undo': self.mousePressData, 'redo': { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in self.mousePressData['nodes']}, 'edges': {x: x.breakpoints[:] for x in self.mousePressData['edges']} } } self.undostack.push(CommandNodeMove(self, data)) self.setMode(DiagramMode.Idle) elif mouseButton == Qt.RightButton: if self.mode is not DiagramMode.SceneDrag: ######################################################################################################## # # # CONTEXT MENU # # # ######################################################################################################## item = self.itemOnTopOf(mousePos) if item: self.clearSelection() item.setSelected(True) self.mousePressPos = mousePos menu = self.mainwindow.menuFactory.create(self.mainwindow, self, item, mousePos) menu.exec_(mouseEvent.screenPos()) super().mouseReleaseEvent(mouseEvent) self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = None #################################################################################################################### # # # AXIOMS COMPOSITION # # # #################################################################################################################### def propertyAxiomComposition(self, source, restriction): """ Returns a collection of items to be added to the given source node to compose a property axiom. :type source: AbstractNode :type restriction: class :rtype: set """ node = restriction(scene=self) edge = InputEdge(scene=self, source=source, target=node) size = DiagramScene.GridSize offsets = ( QPointF(snapF(+source.width() / 2 + 70, size), 0), QPointF(snapF(-source.width() / 2 - 70, size), 0), QPointF(0, snapF(-source.height() / 2 - 70, size)), QPointF(0, snapF(+source.height() / 2 + 70, size)), QPointF(snapF(+source.width() / 2 + 70, size), snapF(-source.height() / 2 - 70, size)), QPointF(snapF(-source.width() / 2 - 70, size), snapF(-source.height() / 2 - 70, size)), QPointF(snapF(+source.width() / 2 + 70, size), snapF(+source.height() / 2 + 70, size)), QPointF(snapF(-source.width() / 2 - 70, size), snapF(+source.height() / 2 + 70, size)), ) pos = None num = sys.maxsize rad = QPointF(node.width() / 2, node.height() / 2) for o in offsets: count = len(self.items(QRectF(source.pos() + o - rad, source.pos() + o + rad))) if count < num: num = count pos = source.pos() + o node.setPos(pos) return {node, edge} def propertyDomainAxiomComposition(self, source): """ Returns a collection of items to be added to the given source node to compose a property domain. :type source: AbstractNode :rtype: set """ return self.propertyAxiomComposition(source, DomainRestrictionNode) def propertyRangeAxiomComposition(self, source): """ Returns a collection of items to be added to the given source node to compose a property range. :type source: AbstractNode :rtype: set """ return self.propertyAxiomComposition(source, RangeRestrictionNode) #################################################################################################################### # # # SLOTS # # # #################################################################################################################### @pyqtSlot() def clear(self): """ Clear the diagram by removing all the elements. """ self.index.clear() self.undostack.clear() super().clear() #################################################################################################################### # # # INTERFACE # # # #################################################################################################################### def edge(self, eid): """ Returns the edge matching the given edge id. :type eid: str """ return self.index.edgeForId(eid) def edges(self): """ Returns a view on all the edges of the diagram. :rtype: view """ return self.index.edges() def itemOnTopOf(self, point, nodes=True, edges=True, skip=None): """ Returns the shape which is on top of the given point. :type point: QPointF :type nodes: bool :type edges: bool :type skip: iterable :rtype: Item """ skip = skip or {} data = [x for x in self.items(point) if (nodes and x.node or edges and x.edge) and x not in skip] if data: return max(data, key=lambda x: x.zValue()) return None def node(self, nid): """ Returns the node matching the given node id. :type nid: str """ return self.index.nodeForId(nid) def nodes(self): """ Returns a view on all the nodes in the diagram. :rtype: view """ return self.index.nodes() def selectedEdges(self): """ Returns the edges selected in the scene. :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.edge] def selectedItems(self): """ Returns the items selected in the scene (will filter out labels since we don't need them). :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.node or x.edge] def selectedNodes(self): """ Returns the nodes selected in the scene. :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.node] def setMode(self, mode, param=None): """ Set the operation mode. :type mode: DiagramMode :type param: int """ if self.mode != mode or self.modeParam != param: self.mode = mode self.modeParam = param self.sgnModeChanged.emit(mode) def visibleRect(self, margin=0): """ Returns a rectangle matching the area of visible items. :type margin: float :rtype: QRectF """ bound = self.itemsBoundingRect() topLeft = QPointF(bound.left() - margin, bound.top() - margin) bottomRight = QPointF(bound.right() + margin, bound.bottom() + margin) return QRectF(topLeft, bottomRight)
class MainEditor(QWidget): def __init__(self): super(MainEditor, self).__init__() self.setWindowTitle('Lex Talionis Palette Editor v5.9.0') self.setMinimumSize(640, 480) self.grid = QGridLayout() self.setLayout(self.grid) self.main_view = MainView(self) self.menu_bar = QMenuBar(self) self.palette_list = PaletteList.PaletteList(self) self.image_map_list = ImageMap.ImageMapList() self.scripts = [] self.undo_stack = QUndoStack(self) self.create_menu_bar() self.grid.setMenuBar(self.menu_bar) self.grid.addWidget(self.main_view, 0, 0) self.grid.addWidget(self.palette_list, 0, 1, 2, 1) self.info_form = QFormLayout() self.grid.addLayout(self.info_form, 1, 0) self.create_info_bars() self.clear_info() def create_menu_bar(self): load_class_anim = QAction("Load Class Animation...", self, triggered=self.load_class) load_single_anim = QAction("Load Single Animation...", self, triggered=self.load_single) load_image = QAction("Load Image...", self, triggered=self.load_image) save = QAction("&Save...", self, shortcut="Ctrl+S", triggered=self.save) exit = QAction("E&xit...", self, shortcut="Ctrl+Q", triggered=self.close) file_menu = QMenu("&File", self) file_menu.addAction(load_class_anim) file_menu.addAction(load_single_anim) file_menu.addAction(load_image) file_menu.addAction(save) file_menu.addAction(exit) undo_action = QAction("Undo", self, shortcut="Ctrl+Z", triggered=self.undo) redo_action = QAction("Redo", self, triggered=self.redo) redo_action.setShortcuts(["Ctrl+Y", "Ctrl+Shift+Z"]) edit_menu = QMenu("&Edit", self) edit_menu.addAction(undo_action) edit_menu.addAction(redo_action) self.menu_bar.addMenu(file_menu) self.menu_bar.addMenu(edit_menu) def create_info_bars(self): self.class_text = QLineEdit() self.class_text.textChanged.connect(self.class_text_change) self.weapon_box = QComboBox() self.weapon_box.uniformItemSizes = True self.weapon_box.activated.connect(self.weapon_changed) self.palette_text = QLineEdit() self.palette_text.textChanged.connect(self.palette_text_change) self.play_button = QPushButton("View Animation") self.play_button.clicked.connect(self.view_animation) self.play_button.setEnabled(False) self.info_form.addRow("Class", self.class_text) self.info_form.addRow("Weapon", self.weapon_box) self.info_form.addRow("Palette", self.palette_text) self.info_form.addRow(self.play_button) def undo(self): self.undo_stack.undo() def redo(self): self.undo_stack.redo() def change_current_palette(self, position, color): palette_frame = self.palette_list.get_current_palette() image_map = self.image_map_list.get_current_map() color_idx = image_map.get(position[0], position[1]) palette_frame.set_color(color_idx, color) def view_animation(self): image = self.main_view.image index = self.image_map_list.get_current_map().get_index() script = self.image_map_list.get_current_map().get_script() ok = Animation.Animator.get_dialog(image, index, script) def clear_info(self): self.class_text.setEnabled(True) self.class_text.setText('') self.weapon_box.clear() self.palette_text.setText('') self.play_button.setEnabled(False) self.image_map_list.clear() self.palette_list.clear() self.scripts = [] self.mode = 0 # 1 - Class, 2 - Animation, 3 - Basic Image, 0 - Not defined yet self.undo_stack.clear() def class_text_change(self): pass def palette_text_change(self): self.palette_list.get_current_palette().set_name( self.palette_text.text()) def weapon_changed(self, idx): self.image_map_list.set_current_idx(idx) self.update_view() def update_view(self): cur_image_map = self.image_map_list.get_current_map() cur_palette = self.palette_list.get_current_palette() self.main_view.set_image(cur_image_map, cur_palette) self.main_view.show_image() def get_script_from_index(self, fn): script = str(fn[:-10] + '-Script.txt') if os.path.exists(script): return script head, tail = os.path.split(script) s_l = tail.split('-') class_name = s_l[0] gender_num = int(class_name[-1]) new_gender_num = (gender_num // 5) * 5 new_class_name = class_name[:-1] + str(new_gender_num) new_script_name = '-'.join([new_class_name] + s_l[1:]) script = os.path.join(head, new_script_name) if os.path.exists(script): return script new_class_name = class_name[:-1] + "0" new_script_name = '-'.join([new_class_name] + s_l[1:]) script = os.path.join(head, new_script_name) if os.path.exists(script): return script return None def get_images_from_index(self, fn): image_header = fn[:-10] images = glob.glob(str(image_header + "-*.png")) return images def get_all_index_files(self, index_file): head, tail = os.path.split(index_file) class_name = tail.split('-')[0] index_files = glob.glob(head + '/' + class_name + "*-Index.txt") return index_files def handle_duplicates(self, palette, image_map): for existing_palette in self.palette_list.list[:-1]: # print('Image Map Weapon: %s' % image_map.weapon_name) # print('Existing Palette %s' % existing_palette.name) # print(existing_palette.get_colors()) # print('New Palette %s' % palette.name) # print(palette.get_colors()) if existing_palette.name == palette.name: if palette.get_colors() != existing_palette.get_colors(): image_map.reorder(palette, existing_palette) return True return False def auto_load_path(self): starting_path = QDir.currentPath() check_here = str(QDir.currentPath() + '/pe_config.txt') if os.path.exists(check_here): with open(check_here) as fp: directory = fp.readline().strip() if os.path.exists(directory): starting_path = directory return starting_path def auto_save_path(self, index_file): auto_load = str(QDir.currentPath() + '/pe_config.txt') with open(auto_load, 'w') as fp: print(os.path.relpath(str(index_file))) fp.write(os.path.relpath(str(index_file))) def load_class(self): if not self.maybe_save(): return starting_path = self.auto_load_path() # starting_path = QDir.currentPath() + '/../Data' index_file = QFileDialog.getOpenFileName( self, "Choose Class", starting_path, "Index Files (*-Index.txt);;All Files (*)") if index_file: self.auto_save_path(index_file) self.clear_info() weapon_index_files = self.get_all_index_files(str(index_file)) for index_file in weapon_index_files: # One image_map for each weapon script_file = self.get_script_from_index(index_file) image_files = [ str(i) for i in self.get_images_from_index(index_file) ] if image_files: image_map = self.image_map_list.add_map_from_images( image_files) image_map.load_script(script_file) image_map.set_index(index_file) self.play_button.setEnabled(True) for image_filename in image_files: self.palette_list.add_palette_from_image( image_filename, image_map) dup = self.handle_duplicates( self.palette_list.get_last_palette(), image_map) if dup: self.palette_list.remove_last_palette() for weapon in self.image_map_list.get_available_weapons(): self.weapon_box.addItem(weapon) klass_name = os.path.split( image_files[0][:-4])[-1].split('-')[0] # Klass self.class_text.setText(klass_name) self.palette_list.set_current_palette(0) self.mode = 1 def load_single(self): if not self.maybe_save(): return starting_path = self.auto_load_path() index_file = QFileDialog.getOpenFileName( self, "Choose Animation", starting_path, "Index Files (*-Index.txt);;All Files (*)") if index_file: self.auto_save_path(index_file) script_file = self.get_script_from_index(index_file) image_files = [ str(i) for i in self.get_images_from_index(index_file) ] if image_files: self.clear_info() image_map = self.image_map_list.add_map_from_images( image_files) image_map.load_script(script_file) image_map.set_index(index_file) self.play_button.setEnabled(True) for image_filename in image_files: self.palette_list.add_palette_from_image( image_filename, image_map) self.weapon_box.addItem( self.image_map_list.get_current_map().weapon_name) klass_name = os.path.split( image_files[0][:-4])[-1].split('-')[0] # Klass self.class_text.setText(klass_name) self.palette_list.set_current_palette(0) self.mode = 2 def load_image(self): if not self.maybe_save(): return starting_path = self.auto_load_path() image_filename = QFileDialog.getOpenFileName( self, "Choose Image PNG", starting_path, "PNG Files (*.png);;All Files (*)") if image_filename: self.auto_save_path(image_filename) self.clear_info() image_map = self.image_map_list.add_map_from_images( [str(image_filename)]) self.palette_list.add_palette_from_image(str(image_filename), image_map) self.class_text.setEnabled(False) self.palette_list.set_current_palette(0) self.mode = 3 def maybe_save(self): if self.mode: ret = QMessageBox.warning( self, "Palette Editor", "These images may have been modified.\n" "Do you want to save your changes?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.save() elif ret == QMessageBox.Cancel: return False return True def save(self): if self.mode == 3: # Save as... PNG (defaults to name with palette ) name = QFileDialog.getSaveFileName(self, 'Save Image', self.auto_load_path()) image_map = self.image_map_list.get_current_map() palette = self.palette_list.get_current_palette() image = self.main_view.create_image(image_map, palette) pixmap = QPixmap.fromImage(image) pixmap.save(name, 'png') elif self.mode == 2: # Save all palettes with their palette names image_map = self.image_map_list.get_current_map() for palette in self.palette_list.list: image = self.main_view.create_image(image_map, palette) pixmap = QPixmap.fromImage(image) head, tail = os.path.split(image_map.image_filename) tail = '-'.join([ str(self.class_text.text()), str(image_map.weapon_name), str(palette.name) ]) + '.png' new_filename = os.path.join(head, tail) pixmap.save(new_filename, 'png') msg = QMessageBox.information(self, "Save Successful", "Successfully Saved!") elif self.mode == 1: # Save all weapons * palettes with their palette names for image_map in self.image_map_list.list: for palette in self.palette_list.list: image = self.main_view.create_image(image_map, palette) # image = image.convertToFormat(QImage.Format_RGB32) pixmap = QPixmap.fromImage(image) head, tail = os.path.split(image_map.image_filename) tail = '-'.join([ str(self.class_text.text()), str(image_map.weapon_name), str(palette.name) ]) + '.png' new_filename = os.path.join(head, tail) pixmap.save(new_filename, 'png') msg = QMessageBox.information(self, "Save Successful", "Successfully Saved!") return True
class PangoGraphicsScene(QGraphicsScene): gfx_changed = pyqtSignal(PangoGraphic, QGraphicsItem.GraphicsItemChange) gfx_removed = pyqtSignal(PangoGraphic) clear_tool = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self.change_stacks = {} self.stack = QUndoStack() self.fpath = None self.px = QPixmap() self.active_label = PangoGraphic() self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) self.tool = None self.tool_size = 10 self.full_clear() def full_clear(self): self.stack.clear() self.clear() self.init_reticle() self.reset_com() self.clear_tool.emit() def init_reticle(self): self.reticle = QGraphicsEllipseItem(-5, -5, 10, 10) self.reticle.setVisible(False) self.reticle.setPen(QPen(Qt.NoPen)) self.addItem(self.reticle) def set_fpath(self, fpath): self.fpath = fpath self.px = QPixmap(self.fpath) self.setSceneRect(QRectF(self.px.rect())) def drawBackground(self, painter, rect): painter.drawPixmap(0, 0, self.px) def reset_com(self): if type(self.active_com.gfx) is PangoPolyGraphic: if not self.active_com.gfx.poly.isClosed(): self.unravel_shapes(self.active_com.gfx) self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) # Undo all commands for shapes (including creation) def unravel_shapes(self, *gfxs): for stack in self.change_stacks.values(): for i in range(stack.count() - 1, -1, -1): com = stack.command(i) if type(com) is QUndoCommand: for j in range(0, com.childCount()): sub_com = com.child(j) if sub_com.gfx in gfxs: com.setObsolete(True) else: if com.gfx in gfxs: com.setObsolete(True) if type(com) == CreateShape: break # Reached shape creation stack.setIndex(0) stack.setIndex(stack.count()) self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) def event(self, event): super().event(event) if self.tool == "Lasso": self.select_handler(event) elif self.tool == "Path": self.path_handler(event) elif self.tool == "Poly": self.poly_handler(event) elif self.tool == "Bbox": self.bbox_handler(event) return False def select_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress and event.buttons( ) & Qt.LeftButton: gfx = self.itemAt(event.scenePos(), QTransform()) if type(gfx) is PangoPolyGraphic: min_dx = float("inf") for i in range(0, gfx.poly.count()): dx = QLineF(gfx.poly.value(i), event.scenePos()).length() if dx < min_dx: min_dx = dx idx = i if min_dx < 20: self.active_com = MoveShape(event.scenePos(), gfx, idx=idx) self.stack.push(self.active_com) elif type(gfx) is PangoBboxGraphic: min_dx = float("inf") for corner in [ "topLeft", "topRight", "bottomLeft", "bottomRight" ]: dx = QLineF(getattr(gfx.rect, corner)(), event.scenePos()).length() if dx < min_dx: min_dx = dx min_corner = corner if min_dx < 20: self.active_com = MoveShape(event.scenePos(), gfx, corner=min_corner) self.stack.push(self.active_com) elif event.type( ) == QEvent.GraphicsSceneMouseMove and event.buttons() & Qt.LeftButton: if type(self.active_com) is MoveShape: if type(self.active_com.gfx) is PangoPolyGraphic: self.stack.undo() self.active_com.pos = event.scenePos() self.stack.redo() elif type(self.active_com.gfx) is PangoBboxGraphic: self.stack.undo() old_pos = self.active_com.pos self.active_com.pos = event.scenePos() self.stack.redo() tl = self.active_com.gfx.rect.topLeft() br = self.active_com.gfx.rect.bottomRight() if tl.x() > br.x() or tl.y() > br.y(): self.stack.undo() self.active_com.pos = old_pos self.stack.redo() elif event.type() == QEvent.GraphicsSceneMouseRelease: self.reset_com() def path_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress: if type(self.active_com.gfx) is not PangoPathGraphic: self.active_com = CreateShape(PangoPathGraphic, event.scenePos(), self.active_label) self.stack.push(self.active_com) self.stack.beginMacro("Extended " + self.active_com.gfx.name) self.active_com = ExtendShape(event.scenePos(), self.active_com.gfx, "moveTo") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseMove: self.reticle.setPos(event.scenePos()) if event.buttons() & Qt.LeftButton: if type(self.active_com.gfx) is PangoPathGraphic: self.active_com = ExtendShape(event.scenePos(), self.active_com.gfx, "lineTo") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseRelease: self.stack.endMacro() def poly_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress: if type(self.active_com.gfx) is not PangoPolyGraphic: self.active_com = CreateShape(PangoPolyGraphic, event.scenePos(), self.active_label) self.stack.push(self.active_com) gfx = self.active_com.gfx pos = event.scenePos() if gfx.poly.count() <= 1 or not gfx.poly.isClosed(): if QLineF(event.scenePos(), gfx.poly.first()).length() < gfx.dw() * 2: pos = QPointF() pos.setX(gfx.poly.first().x()) pos.setY(gfx.poly.first().y()) self.active_com = ExtendShape(pos, self.active_com.gfx) self.stack.push(self.active_com) if gfx.poly.count() > 1 and gfx.poly.isClosed(): self.reset_com() def bbox_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress: if type(self.active_com.gfx) is not PangoBboxGraphic: self.active_com = CreateShape(PangoBboxGraphic, event.scenePos(), self.active_label) self.stack.beginMacro(self.active_com.text()) self.stack.push(self.active_com) self.active_com = MoveShape(event.scenePos(), self.active_com.gfx, corner="topLeft") self.stack.push(self.active_com) self.active_com = MoveShape(event.scenePos(), self.active_com.gfx, corner="bottomRight") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseMove: if event.buttons() & Qt.LeftButton: if type(self.active_com.gfx) is PangoBboxGraphic: tl = self.active_com.gfx.rect.topLeft() br = event.scenePos() if tl.x() < br.x() and tl.y() < br.y(): self.active_com = MoveShape(event.scenePos(), self.active_com.gfx, corner="bottomRight") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseRelease: if type(self.active_com.gfx) is PangoBboxGraphic: self.stack.endMacro() tl = self.active_com.gfx.rect.topLeft() br = self.active_com.gfx.rect.bottomRight() if QLineF(tl, br).length() < self.active_com.gfx.dw() * 2: self.unravel_shapes(self.active_com.gfx) self.reset_com()
class SchmereoMainWindow(QtWidgets.QMainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ui = uic.loadUi( uifile=pkg_resources.resource_stream("schmereo", "schmereo.ui"), baseinstance=self, ) # Platform-specific semantic keyboard shortcuts cannot be set in Qt Designer self.ui.actionNew.setShortcut(QKeySequence.New) self.ui.actionOpen.setShortcut(QKeySequence.Open) self.ui.actionQuit.setShortcut( QKeySequence.Quit) # no effect on Windows self.ui.actionSave.setShortcut(QKeySequence.Save) self.ui.actionSave_Project_As.setShortcut(QKeySequence.SaveAs) self.ui.actionZoom_In.setShortcuts( [QKeySequence.ZoomIn, "Ctrl+="]) # '=' so I don't need to press SHIFT self.ui.actionZoom_Out.setShortcut(QKeySequence.ZoomOut) # self.recent_files = RecentFileList( open_file_slot=self.load_file, settings_key="recent_files", menu=self.ui.menuRecent_Files, ) # Link views self.shared_camera = Camera() self.ui.leftImageWidget.camera = self.shared_camera self.ui.rightImageWidget.camera = self.shared_camera # self.ui.leftImageWidget.file_dropped.connect(self.load_left_file) self.ui.rightImageWidget.file_dropped.connect(self.load_right_file) # self.ui.leftImageWidget.image.transform.center = FractionalImagePos( -0.5, 0) self.ui.rightImageWidget.image.transform.center = FractionalImagePos( +0.5, 0) # for w in (self.ui.leftImageWidget, self.ui.rightImageWidget): w.messageSent.connect(self.ui.statusbar.showMessage) # self.marker_set = list() self.zoom_increment = 1.10 self.image_saver = ImageSaver(self.ui.leftImageWidget, self.ui.rightImageWidget) # TODO: object for AddMarker tool button tb = self.ui.addMarkerToolButton tb.setDefaultAction(self.ui.actionAdd_Marker) sz = 32 tb.setFixedSize(sz, sz) tb.setIconSize(QtCore.QSize(sz, sz)) hb = self.ui.handModeToolButton hb.setDefaultAction(self.ui.actionHand_Mode) hb.setFixedSize(sz, sz) hb.setIconSize(QtCore.QSize(sz, sz)) _set_action_icon( self.ui.actionAdd_Marker, "schmereo.marker", "crosshair64.png", "crosshair64blue.png", ) _set_action_icon(self.ui.actionHand_Mode, "schmereo", "cursor-openhand20.png") # tb.setDragEnabled(True) # TODO: drag tool button to place marker self.marker_manager = MarkerManager(self) self.aligner = Aligner(self) self.project_file_name = None # self.undo_stack = QUndoStack(self) undo_action = self.undo_stack.createUndoAction(self, '&Undo') undo_action.setShortcuts(QKeySequence.Undo) redo_action = self.undo_stack.createRedoAction(self, '&Redo') redo_action.setShortcuts(QKeySequence.Redo) self.undo_stack.cleanChanged.connect(self.on_undoStack_cleanChanged) # self.ui.menuEdit.insertAction(self.ui.actionAlign_Now, undo_action) self.ui.menuEdit.insertAction(self.ui.actionAlign_Now, redo_action) self.ui.menuEdit.insertSeparator(self.ui.actionAlign_Now) self.clip_box = ClipBox(parent=self, camera=self.shared_camera, images=[i.image for i in self.eye_widgets()]) self.ui.actionResolve_Clip_Box.triggered.connect( self.recenter_clip_box) for w in self.eye_widgets(): w.undo_stack = self.undo_stack w.clip_box = self.clip_box self.clip_box.changed.connect(w.update) self.project_folder = None def check_save(self) -> bool: if self.undo_stack.isClean(): return True # OK to do whatever now result = QMessageBox.warning( self, "The project has been modified.", "The project has been modified.\n" "Do you want to save your changes?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save, ) if result == QMessageBox.Save: if self.project_file_name is None: return self.on_actionSave_Project_As_triggered() else: return self.save_project_file(self.project_file_name) elif result == QMessageBox.Discard: return True # OK to do whatever now elif result == QMessageBox.Cancel: return False else: # Unexpected to get here? return False # cancel / abort def closeEvent(self, event: QCloseEvent): if self.check_save(): event.accept() else: event.ignore() def eye_widgets(self): for w in (self.ui.leftImageWidget, self.ui.rightImageWidget): yield w def keyReleaseEvent(self, event: QtGui.QKeyEvent): if event.key() == Qt.Key_Escape: self.marker_manager.set_marker_mode(False) def load_left_file(self, file_name: str) -> None: self.load_file(file_name) def load_right_file(self, file_name: str) -> None: self.load_file(file_name) @QtCore.pyqtSlot(str) def load_file(self, file_name: str) -> bool: result = False self.log_message(f"Loading file {file_name}...") try: image = Image.open(file_name) except OSError: return self.load_project(file_name) result = self.ui.leftImageWidget.load_image(file_name) if result: result = self.ui.rightImageWidget.load_image(file_name) if result: self.ui.leftImageWidget.update() self.ui.rightImageWidget.update() self.recent_files.add_file(file_name) self.project_folder = os.path.dirname(file_name) else: self.log_message(f"ERROR: Image load failed.") return result def load_project(self, file_name): with open(file_name, "r") as fh: data = json.load(fh) self.from_dict(data) for w in self.eye_widgets(): w.update() self.recent_files.add_file(file_name) self.project_file_name = file_name self.project_folder = os.path.dirname(file_name) self.setWindowFilePath(self.project_file_name) self.undo_stack.clear() self.undo_stack.setClean() return True def log_message(self, message: str) -> None: self.ui.statusbar.showMessage(message) @QtCore.pyqtSlot() def on_actionAbout_Schmereo_triggered(self): QtWidgets.QMessageBox.about( self, "About Schmereo", inspect.cleandoc(f""" Schmereo stereograph restoration application Version: {__version__} Author: Christopher M. Bruns Code: https://github.com/cmbruns/schmereo """), ) @QtCore.pyqtSlot() def on_actionAlign_Now_triggered(self): self.clip_box.recenter() self.undo_stack.push(AlignNowCommand(self)) @QtCore.pyqtSlot() def on_actionClear_Markers_triggered(self): self.undo_stack.push(ClearMarkersCommand(*self.eye_widgets())) @QtCore.pyqtSlot() def on_actionNew_triggered(self): if not self.check_save(): return self.project_folder = None self.project_file_name = None self.setWindowFilePath("untitled") self.shared_camera.reset() for w in self.eye_widgets(): w.image.transform.reset() self.undo_stack.clear() self.undo_stack.setClean() @QtCore.pyqtSlot() def on_actionOpen_triggered(self): folder = None if folder is None: folder = self.project_folder if folder is None: folder = "" file_name, file_type = QtWidgets.QFileDialog.getOpenFileName( parent=self, caption="Load Image", directory=folder, filter="Projects and Images (*.json *.tif);;All Files (*)", ) if file_name is None: return if len(file_name) < 1: return self.load_file(file_name) @QtCore.pyqtSlot() def on_actionQuit_triggered(self): if self.check_save(): QtCore.QCoreApplication.quit() @QtCore.pyqtSlot() def on_actionReport_a_Problem_triggered(self): url = QtCore.QUrl("https://github.com/cmbruns/schmereo/issues") QtGui.QDesktopServices.openUrl(url) @QtCore.pyqtSlot() def on_actionSave_triggered(self): if self.project_file_name is None: return self.clip_box.recenter() self.save_project_file(self.project_file_name) @QtCore.pyqtSlot() def on_actionSave_Images_triggered(self): if not self.image_saver.can_save(): return self.clip_box.recenter() path = "" if self.project_file_name is not None: path = f"{os.path.splitext(self.project_file_name)[0]}.pns" elif self.project_folder is not None: path = self.project_folder file_name, file_type = QtWidgets.QFileDialog.getSaveFileName( parent=self, caption="Save File(s)", directory=path, filter="3D Images (*.pns *.jps)", ) if file_name is None: return if len(file_name) < 1: return bs = self.clip_box.size self.image_saver.eye_size = (int(bs[0]), int(bs[1])) self.image_saver.save_image(file_name, file_type) if self.project_folder is None: self.project_folder = os.path.dirname(file_name) @QtCore.pyqtSlot() def on_actionSave_Project_As_triggered(self) -> bool: path = "" if self.project_file_name is not None: path = os.path.dirname(self.project_file_name) elif self.project_folder is not None: path = self.project_folder file_name, file_type = QtWidgets.QFileDialog.getSaveFileName( parent=self, caption="Save Project", directory=path, filter="Schmereo Projects (*.json);;All Files (*)", ) if file_name is None: return False if len(file_name) < 1: return False return self.save_project_file(file_name) @QtCore.pyqtSlot() def on_actionZoom_In_triggered(self): self.zoom(amount=self.zoom_increment) @QtCore.pyqtSlot() def on_actionZoom_Out_triggered(self): self.zoom(amount=1.0 / self.zoom_increment) @QtCore.pyqtSlot(bool) def on_undoStack_cleanChanged(self, is_clean: bool): self.ui.actionSave.setEnabled(not is_clean) doc_title = "untitled" if self.project_file_name is not None: doc_title = self.project_file_name if not is_clean: doc_title = f"{doc_title}*" self.setWindowFilePath(doc_title) def recenter_clip_box(self): self.clip_box.recenter() self.clip_box.notify() self.camera.notify() def save_project_file(self, file_name) -> bool: with open(file_name, "w") as fh: self.clip_box.recenter() json.dump(self.to_dict(), fh, indent=2) self.recent_files.add_file(file_name) self.setWindowFilePath(file_name) self.project_file_name = file_name self.project_folder = os.path.dirname(file_name) self.undo_stack.setClean() return True return False def to_dict(self): self.clip_box.recenter() # Normalize values before serialization return { "app": { "name": "schmereo", "version": __version__ }, "clip_box": self.clip_box.to_dict(), "left": self.ui.leftImageWidget.to_dict(), "right": self.ui.rightImageWidget.to_dict(), } def from_dict(self, data): self.ui.leftImageWidget.from_dict(data["left"]) self.ui.rightImageWidget.from_dict(data["right"]) if "clip_box" in data: self.clip_box.from_dict(data["clip_box"]) @QtCore.pyqtSlot() def zoom(self, amount: float): # In case the zoom is not linked between the two image widgets... widgets = (self.ui.leftImageWidget, self.ui.rightImageWidget) # store zoom values in case the cameras are all the same zooms = [w.camera.zoom for w in widgets] for idx, w in enumerate(widgets): w.camera.zoom = zooms[idx] * amount for w in widgets: w.camera.notify() # repaint now
class Document(QWidget, ProjectDocument): mark_item_created = pyqtSignal(MarkItem) added_mark_item = pyqtSignal(Project, MarkItem) browser_result_signal = pyqtSignal(bool) selected_mark_item_changed = pyqtSignal(MarkItem) def __init__(self, gadget, toolbar_gadget, file_name=None, project_name=None, image_path=None, person_name=None, parent=None, eraser_size=3, eraser_option=SelectionOptionToolBar.Subtract): super(Document, self).__init__(parent) ProjectDocument.__init__(self, parent=parent) self._writer_format = ProjectFormat() self._reader_format = ProjectFormat() self._export_format = ProjectFormat() self._mark_item_to_outline_item = {} self._modifier = False self._project = Project(image_path, file_name, project_name, person_name) self._image_path = image_path if image_path else self._project.image_path self._current_tool = gadget self._selection_option = toolbar_gadget self._eraser_size = eraser_size self._eraser_option = eraser_option self.__current_index = -1 self.__mouse_press_index = -1 self.__mouse_press_offset = QPoint() self.__resize_handel_pressed = False self.__undo_stack = QUndoStack(self) self._selection_item = None self._history_widget = None self._history_project_manager = None self._mark_item_manager = MarkItemManager() self._mark_item_manager.selected_item_changed.connect( self.selected_mark_item_changed) # # 创建场景 self._workbench_scene.setObjectName("workbench_scene") self._is_big_img = False # if self._is_big_img: # self.workbench_view = LoadIMGraphicsView(self._mark_item_manager, gadget, toolbar_gadget, eraser_size, # image_path, self._workbench_scene, parent=self) # else: self.workbench_view = GraphicsViewTest(self._mark_item_manager, gadget, toolbar_gadget, eraser_size, parent=self) # 把场景添加到视图中 self.workbench_view.setScene(self._workbench_scene) self.workbench_view.setObjectName("workbench_view") self.workbench_view.setContentsMargins(0, 0, 0, 0) self.workbench_view.setBackgroundBrush(QColor(147, 147, 147)) # 布局 self.tab_vertical_layout = QVBoxLayout(self) self._splitter1 = QSplitter(self) self._splitter1.setStyleSheet("margin: 0px") self._splitter1.addWidget(self.workbench_view) self._splitter2 = QSplitter(self) self._splitter2.setOrientation(Qt.Vertical) self._splitter2.setStyleSheet("margin: 0px") self._splitter2.addWidget(self._splitter1) self.tab_vertical_layout.addWidget(self._splitter2) self.tab_vertical_layout.setContentsMargins(0, 0, 0, 0) # 当前选择小工具 self.change_gadget(gadget) # 信号接收 self.workbench_view.border_moved_signal.connect(self.border_moved) self.workbench_view.border_created.connect(self.created_border) self.workbench_view.about_to_create_border.connect( self.about_to_create_border) self.workbench_view.eraser_action_signal.connect(self.eraser_action) if all([image_path, project_name, file_name]) and not self._is_big_img: self.create_document() @property def is_big_img(self): return self._is_big_img def about_to_cmp(self, project_documents: ProjectDocument = None): if not self._history_widget: self._history_project_manager = HistoryProjectManager( project_documents) self._history_widget = Thumbnail(self._history_project_manager, self) self._history_project_manager.set_scene( self._history_widget.current_project()) self._splitter1.addWidget(self._history_project_manager.get_view()) self._splitter2.addWidget(self._history_widget) self.workbench_view.set_is_comparing(True) self._history_project_manager.get_view().set_is_comparing(True) self._history_widget.selected_project_changed.connect( self._selected_history_project_changed) self._history_widget.close_event_signal.connect( self._toggle_cmp_history) self._history_widget.synchronize_changed_signal.connect( self._toggle_synchronize_view) items = project_documents[0].project().get_mark_items() for item in items: self._project.add_mark_item(item) else: self._toggle_cmp_history(True) self._history_widget.setHidden(False) if True: self.connect_to_synchronize_view() def had_cmp(self): return bool(self._history_widget) def _toggle_synchronize_view(self, is_synchronize: bool): if is_synchronize: self.connect_to_synchronize_view() else: self.disconnect_to_asynchronous_view() def connect_to_synchronize_view(self): self._history_project_manager.synchronize_with_origin_view( self.workbench_view) self._history_project_manager.get_view().connect_to_synchronize_with( self.workbench_view) self.workbench_view.connect_to_synchronize_with( self._history_project_manager.get_view()) def disconnect_to_asynchronous_view(self): self._history_project_manager.get_view( ).disconnect_to_asynchronous_with(self.workbench_view) self.workbench_view.disconnect_to_asynchronous_with( self._history_project_manager.get_view()) def _toggle_cmp_history(self, is_on: bool): self._history_project_manager.hidden_view(not is_on) self.workbench_view.set_is_comparing(is_on) self._history_project_manager.get_view().set_is_comparing(is_on) def _selected_history_project_changed(self, project_doc: ProjectDocument): self._history_project_manager.set_scene(project_doc) self._history_project_manager.synchronize_with_origin_view( self.workbench_view) def modifier(self): return not self.__undo_stack.isClean() def set_project(self, project: Project): self._project = project if not self._image_path: self._image_path = project.image_path if not self._is_big_img: self.load_document(self._image_path) for mark_item in self._project.get_mark_items(): self.add_mark_item(mark_item) def set_current_mark_item(self, mark_item: MarkItem): """""" if not mark_item: return item = [ item for item in self._workbench_scene.items() if isinstance(item, OutlineItem) and item.mark_item() == mark_item ] if item: self._mark_item_manager.set_selected_item(item[0]) self.workbench_view.centerOn(item[0]) def delete_mark_item(self, mark_item: [MarkItem, OutlineItem]): if not mark_item: return if isinstance(mark_item, MarkItem): if self._mark_item_manager.selected_mark_item().mark_item( ) == mark_item: self._mark_item_manager.set_selected_item(None) self._project.remove_mark_item(mark_item) item = [ item for item in self._workbench_scene.items() if isinstance(item, OutlineItem) and item.mark_item() == mark_item ] if item: self._mark_item_manager.unregister_mark_item( mark_item.item_name) self._workbench_scene.removeItem(item[0]) del item[0] elif isinstance(mark_item, OutlineItem): if self._mark_item_manager.selected_mark_item() == mark_item: self._mark_item_manager.set_selected_item(None) self._project.remove_mark_item(mark_item.mark_item()) self._mark_item_manager.unregister_mark_item(mark_item.item_name) self._workbench_scene.removeItem(mark_item) del mark_item def project(self) -> Project: return self._project def project_name(self): return self._project.project_name def undo_stack(self): return self.__undo_stack def create_document(self): self.load_document(self._image_path) self.save_project() def save_project(self): self.writer_format.save_project(self._project) self.__undo_stack.clear() def export_result(self, path, progress): self.writer_format.export_result(path, self._project, self._image.size(), self) def get_file_name(self): return self._project.project_full_path() def get_project_name(self): return self._project.parent() def about_to_create_border(self): if self._selection_option == SelectionOptionToolBar.Replace: self._workbench_scene.removeItem(self._selection_item) self._selection_item = None def cancel_selection(self): self._workbench_scene.removeItem(self._selection_item) self._selection_item.disconnect() self._selection_item = None def selection_as_mark_item(self): """TODO""" def created_border(self, border: SelectionItem): if self._selection_option == SelectionOptionToolBar.Replace: self._workbench_scene.removeItem(self._selection_item) self._selection_item = border self.__undo_stack.push( AddSelectionItem(self._workbench_scene, self._selection_item)) elif self._selection_option == SelectionOptionToolBar.Subtract: if self._selection_item: self._selection_item -= border elif self._selection_option == SelectionOptionToolBar.Add: if not self._selection_item: self._selection_item = border else: self._selection_item += border elif self._selection_option == SelectionOptionToolBar.Intersect: if self._selection_item: self._selection_item &= border if self._selection_item: if self._selection_item.is_empty(): self._workbench_scene.removeItem(self._selection_item) self._selection_item = None return self.workbench_view.view_zoom_signal.connect( self._selection_item.set_pen_width_by_scale) self._selection_item.cancel_selection_signal.connect( self.cancel_selection) self._selection_item.as_mark_item_signal.connect( self.selection_as_mark_item) self._selection_item.reverse_select_signal.connect( self._select_reverser_path) def add_border_item(self, item: SelectionItem): self.__undo_stack.push(AddItemCommand(self._workbench_scene, item)) def border_moved(self, item: SelectionItem): self.__undo_stack.push(MoveItemCommand(item)) def change_toolbar_gadget(self, toolbar_gadget: QAction): self._selection_option = toolbar_gadget.data() def change_eraser_option(self, option_action: QAction): self._eraser_option = option_action.data() def change_gadget(self, tool: QAction): if isinstance(tool, QAction): tool = tool.data() self.workbench_view.set_gadget(tool) if tool == ToolsToolBar.BrowserImageTool: self.browser_result() self.browser_result_signal.emit(True) else: if self._current_tool == ToolsToolBar.BrowserImageTool: self.end_browser() self.browser_result_signal.emit(False) self._current_tool = tool def eraser_size_changed(self, eraser_size: int): self._eraser_size = eraser_size self.workbench_view.set_eraser_size(eraser_size) def browser_result(self): self.workbench_view.setBackgroundBrush(QColor(Qt.black)) self._pixmap_item.setVisible(False) if self.workbench_view.is_comparing(): self._history_project_manager.browser_result() if self._selection_item: self._selection_item.setVisible(False) def end_browser(self): self.workbench_view.setBackgroundBrush(QColor(147, 147, 147)) self._pixmap_item.setVisible(True) if self.workbench_view.is_comparing(): self._history_project_manager.end_browser() if self._selection_item: self._selection_item.setVisible(True) def get_sub_image_in(self, item: SelectionItem) -> [QImage, None]: rect = item.rectangle() if self.is_big_img: """""" # slide_helper = SlideHelper(self.project().image_path) # image_from_rect = ImgFromRect(rect, slide_helper) # image_from_rect = image_from_rect.area_img # return image_from_rect else: rect_sub_image = self._image.copy(rect) polygon_path = item.get_path() polygon_sub_image = rect_sub_image for row in range(0, rect.width()): for clo in range(0, rect.height()): point = QPoint(row, clo) if not polygon_path.contains(point): polygon_sub_image.setPixel(point, 0) return polygon_sub_image def ai_delete_outline(self, detect_policy): result_numpy_array = None width_num_array = None if not self._selection_item: image = self._image else: image = self._image.copy(self._selection_item.rectangle()) if detect_policy == 5: for h in range(0, image.height(), 256): for w in range(0, image.width(), 256): image_ = self._image.copy(QRect(w, h, 255, 255)) image_ = qimage2numpy(image_) result = detect_one(image_) numpy_array = mat_to_img(result) if width_num_array is not None: width_num_array = np.hstack( (width_num_array, numpy_array)) else: width_num_array = numpy_array if result_numpy_array is not None: result_numpy_array = np.vstack( (result_numpy_array, width_num_array)) else: result_numpy_array = width_num_array width_num_array = None print(result_numpy_array.shape) return result_numpy_array def _get_outlines(self, numpy_array, detect_policy): outline_path1 = QPainterPath() outline_path2 = QPainterPath() outline1, outline2 = detect_outline(detect_policy, numpy_array, drop_area=80) for array in outline1: sub_path = [] for point in array[0]: point = self._selection_item.mapToScene( point[0][0], point[0][1]) sub_path.append(point) polygon = QPolygonF(sub_path) path = QPainterPath() path.addPolygon(polygon) outline_path1 += path for array in outline2: sub_path = [] for point in array[0]: point = self._selection_item.mapToScene( point[0][0], point[0][1]) sub_path.append(point) polygon = QPolygonF(sub_path) path = QPainterPath() path.addPolygon(polygon) outline_path2 += path return outline_path1, outline_path2 def _get_outline_by_no_selection(self, numpy_array, detect_policy): outline_path1 = QPainterPath() outline_path2 = QPainterPath() outline1, outline2 = detect_outline(detect_policy, numpy_array, drop_area=80) for array in outline1: sub_path = [] for point in array[0]: point = QPoint(point[0][0], point[0][1]) sub_path.append(point) polygon = QPolygonF(sub_path) path = QPainterPath() path.addPolygon(polygon) outline_path1 += path for array in outline2: sub_path = [] for point in array[0]: point = QPoint(point[0][0], point[0][1]) sub_path.append(point) polygon = QPolygonF(sub_path) path = QPainterPath() path.addPolygon(polygon) outline_path2 += path return outline_path1, outline_path2 def _to_create_mark_item(self, outline_path1, outline_path2): use_outline1_flag = True if not outline_path1.isEmpty(): self.create_mark_item(outline_path1) elif not outline_path2.isEmpty(): self.create_mark_item(outline_path2) use_outline1_flag = False if self._selection_item: self._selection_item.setFlag(QGraphicsItem.ItemIsMovable, False) if use_outline1_flag and not outline_path2.isEmpty(): self._selection_item.set_reverser_path(outline_path2) def detect_outline(self, detect_policy): """ 将选中的选区对应的部分图片copy出来,然后转为ndarray类型 用来转为OpenCV识别轮廓的输入数据 :param detect_policy: 用哪种识别算法识别轮廓 :return: None """ if detect_policy >= 5: numpy_array = self.ai_delete_outline(detect_policy) outline_path1, outline_path2 = self._get_outline_by_no_selection( numpy_array, detect_policy) if not self._selection_item: self._selection_item = SelectionItem( QPoint(0, 0), self._workbench_scene, self.workbench_view.transform().m11()) path = QPainterPath() path.addRect( QRectF(0, 0, self._image.width(), self._image.height())) self._selection_item.set_item_path_by_path(path) self._selection_item.reverse_select_signal.connect( self._select_reverser_path) self._to_create_mark_item(outline_path1, outline_path2) return if not self._selection_item: QMessageBox.warning(self, "警告", "没有选择区域!") return if isinstance(self._selection_item, SelectionItem): outline_path1 = QPainterPath() outline_path2 = QPainterPath() if detect_policy == 4: self._workbench_scene.removeItem(self._selection_item) outline_path1 = self._selection_item.mapToScene( self._selection_item.get_path()) self._selection_item = None else: sub_img = self.get_sub_image_in(self._selection_item) if sub_img is None: return if isinstance(sub_img, QImage): sub_img = qimage2numpy(sub_img) outline_path1, outline_path2 = self._get_outlines( sub_img, detect_policy) self._to_create_mark_item(outline_path1, outline_path2) def correction_outline(self, option): """""" if not self._selection_item: return mark_items = [ item for item in self._workbench_scene.items( self._selection_item.get_scene_path()) if isinstance(item, OutlineItem) ] for mark_item in mark_items: if mark_item.locked(): continue elif option == 1: mark_item += self._selection_item self._mark_item_manager.set_selected_item(mark_item) break elif option == 2: mark_item -= self._selection_item self._mark_item_manager.set_selected_item(mark_item) break self._workbench_scene.removeItem(self._selection_item) self._selection_item = None def eraser_action(self, eraser_area: SelectionItem): if self._selection_item: eraser_area &= self._selection_item if eraser_area.is_empty(): return mark_items = [ item for item in self._workbench_scene.items( eraser_area.get_scene_path()) if isinstance(item, OutlineItem) ] # if self._eraser_option == SelectionOptionToolBar.Add: # selected_item = self._mark_item_manager.selected_mark_item() # if selected_item in mark_items: # selected_item += eraser_area # return for item in mark_items: if item.locked(): continue item -= eraser_area self._workbench_scene.removeItem(eraser_area) del eraser_area def create_mark_item(self, outline: QPainterPath): item_name = self._mark_item_manager.get_unique_mark_item_name() new_mark_item = MarkItem(list(self._project.persons), item_name=item_name, outline_path=outline) self._project.add_mark_item(new_mark_item) self.add_mark_item(new_mark_item, True) def add_mark_item(self, mark_item: MarkItem, new_item=False): item = OutlineItem(mark_item, self._workbench_scene, self.workbench_view.transform().m11()) flag = True if new_item: flag = self.detect_intersect_with_others(item) if flag: self._mark_item_to_outline_item[mark_item] = item self.browser_result_signal.connect(item.is_browser_result) self.workbench_view.view_zoom_signal.connect( item.set_pen_width_by_scale) self._mark_item_manager.register_mark_item(item, mark_item.item_name) self.added_mark_item.emit(self._project, mark_item) self._mark_item_manager.set_selected_item(item) def _select_reverser_path(self): if self._selection_item: item = self._project.get_mark_items()[-1] reverse_path = self._selection_item.get_reverse_path() self._selection_item.set_reverser_path(item.get_outline()) item.set_outline(reverse_path) def detect_intersect_with_others(self, new_item: OutlineItem): selection_path = new_item.get_scene_path() mark_items = [ item for item in self._workbench_scene.items(selection_path) if isinstance(item, OutlineItem) ] for mark_item in mark_items: if mark_item != new_item: new_item -= mark_item if new_item.get_path().isEmpty(): self._workbench_scene.removeItem(new_item) del new_item return False else: new_item.get_path().closeSubpath() return True def paint_make_item(self, mark_item: MarkItem): pen = QPen() pen.setWidth(1) pen.setColor(Qt.yellow) self._workbench_scene.addPath(mark_item.draw_path(), pen) @property def writer_format(self): return self._reader_format @writer_format.setter def writer_format(self, new_writer_format): self._writer_format = new_writer_format @property def reader_format(self): return self._reader_format @reader_format.setter def reader_format(self, new_reader_format): self._reader_format = new_reader_format @property def export_format(self): return self._export_format @export_format.setter def export_format(self, new_export_format): self._export_format = new_export_format
class IconEditorGrid(QWidget): """ Class implementing the icon editor grid. @signal canRedoChanged(bool) emitted after the redo status has changed @signal canUndoChanged(bool) emitted after the undo status has changed @signal clipboardImageAvailable(bool) emitted to signal the availability of an image to be pasted @signal colorChanged(QColor) emitted after the drawing color was changed @signal imageChanged(bool) emitted after the image was modified @signal positionChanged(int, int) emitted after the cursor poition was changed @signal previewChanged(QPixmap) emitted to signal a new preview pixmap @signal selectionAvailable(bool) emitted to signal a change of the selection @signal sizeChanged(int, int) emitted after the size has been changed @signal zoomChanged(int) emitted to signal a change of the zoom value """ canRedoChanged = pyqtSignal(bool) canUndoChanged = pyqtSignal(bool) clipboardImageAvailable = pyqtSignal(bool) colorChanged = pyqtSignal(QColor) imageChanged = pyqtSignal(bool) positionChanged = pyqtSignal(int, int) previewChanged = pyqtSignal(QPixmap) selectionAvailable = pyqtSignal(bool) sizeChanged = pyqtSignal(int, int) zoomChanged = pyqtSignal(int) Pencil = 1 Rubber = 2 Line = 3 Rectangle = 4 FilledRectangle = 5 Circle = 6 FilledCircle = 7 Ellipse = 8 FilledEllipse = 9 Fill = 10 ColorPicker = 11 RectangleSelection = 20 CircleSelection = 21 MarkColor = QColor(255, 255, 255, 255) NoMarkColor = QColor(0, 0, 0, 0) ZoomMinimum = 100 ZoomMaximum = 10000 ZoomStep = 100 ZoomDefault = 1200 ZoomPercent = True def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (QWidget) """ super(IconEditorGrid, self).__init__(parent) self.setAttribute(Qt.WA_StaticContents) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.__curColor = Qt.black self.__zoom = 12 self.__curTool = self.Pencil self.__startPos = QPoint() self.__endPos = QPoint() self.__dirty = False self.__selecting = False self.__selRect = QRect() self.__isPasting = False self.__clipboardSize = QSize() self.__pasteRect = QRect() self.__undoStack = QUndoStack(self) self.__currentUndoCmd = None self.__image = QImage(32, 32, QImage.Format_ARGB32) self.__image.fill(qRgba(0, 0, 0, 0)) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) self.__compositingMode = QPainter.CompositionMode_SourceOver self.__lastPos = (-1, -1) self.__gridEnabled = True self.__selectionAvailable = False self.__initCursors() self.__initUndoTexts() self.setMouseTracking(True) self.__undoStack.canRedoChanged.connect(self.canRedoChanged) self.__undoStack.canUndoChanged.connect(self.canUndoChanged) self.__undoStack.cleanChanged.connect(self.__cleanChanged) self.imageChanged.connect(self.__updatePreviewPixmap) QApplication.clipboard().dataChanged.connect(self.__checkClipboard) self.__checkClipboard() def __initCursors(self): """ Private method to initialize the various cursors. """ self.__normalCursor = QCursor(Qt.ArrowCursor) pix = QPixmap(":colorpicker-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__colorPickerCursor = QCursor(pix, 1, 21) pix = QPixmap(":paintbrush-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__paintCursor = QCursor(pix, 0, 19) pix = QPixmap(":fill-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__fillCursor = QCursor(pix, 3, 20) pix = QPixmap(":aim-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__aimCursor = QCursor(pix, 10, 10) pix = QPixmap(":eraser-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__rubberCursor = QCursor(pix, 1, 16) def __initUndoTexts(self): """ Private method to initialize texts to be associated with undo commands for the various drawing tools. """ self.__undoTexts = { self.Pencil: self.tr("Set Pixel"), self.Rubber: self.tr("Erase Pixel"), self.Line: self.tr("Draw Line"), self.Rectangle: self.tr("Draw Rectangle"), self.FilledRectangle: self.tr("Draw Filled Rectangle"), self.Circle: self.tr("Draw Circle"), self.FilledCircle: self.tr("Draw Filled Circle"), self.Ellipse: self.tr("Draw Ellipse"), self.FilledEllipse: self.tr("Draw Filled Ellipse"), self.Fill: self.tr("Fill Region"), } def isDirty(self): """ Public method to check the dirty status. @return flag indicating a modified status (boolean) """ return self.__dirty def setDirty(self, dirty, setCleanState=False): """ Public slot to set the dirty flag. @param dirty flag indicating the new modification status (boolean) @param setCleanState flag indicating to set the undo stack to clean (boolean) """ self.__dirty = dirty self.imageChanged.emit(dirty) if not dirty and setCleanState: self.__undoStack.setClean() def sizeHint(self): """ Public method to report the size hint. @return size hint (QSize) """ size = self.__zoom * self.__image.size() if self.__zoom >= 3 and self.__gridEnabled: size += QSize(1, 1) return size def setPenColor(self, newColor): """ Public method to set the drawing color. @param newColor reference to the new color (QColor) """ self.__curColor = QColor(newColor) self.colorChanged.emit(QColor(newColor)) def penColor(self): """ Public method to get the current drawing color. @return current drawing color (QColor) """ return QColor(self.__curColor) def setCompositingMode(self, mode): """ Public method to set the compositing mode. @param mode compositing mode to set (QPainter.CompositionMode) """ self.__compositingMode = mode def compositingMode(self): """ Public method to get the compositing mode. @return compositing mode (QPainter.CompositionMode) """ return self.__compositingMode def setTool(self, tool): """ Public method to set the current drawing tool. @param tool drawing tool to be used (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection) """ self.__curTool = tool self.__lastPos = (-1, -1) if self.__curTool in [self.RectangleSelection, self.CircleSelection]: self.__selecting = True else: self.__selecting = False if self.__curTool in [self.RectangleSelection, self.CircleSelection, self.Line, self.Rectangle, self.FilledRectangle, self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse]: self.setCursor(self.__aimCursor) elif self.__curTool == self.Fill: self.setCursor(self.__fillCursor) elif self.__curTool == self.ColorPicker: self.setCursor(self.__colorPickerCursor) elif self.__curTool == self.Pencil: self.setCursor(self.__paintCursor) elif self.__curTool == self.Rubber: self.setCursor(self.__rubberCursor) else: self.setCursor(self.__normalCursor) def tool(self): """ Public method to get the current drawing tool. @return current drawing tool (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection) """ return self.__curTool def setIconImage(self, newImage, undoRedo=False, clearUndo=False): """ Public method to set a new icon image. @param newImage reference to the new image (QImage) @keyparam undoRedo flag indicating an undo or redo operation (boolean) @keyparam clearUndo flag indicating to clear the undo stack (boolean) """ if newImage != self.__image: self.__image = newImage.convertToFormat(QImage.Format_ARGB32) self.update() self.updateGeometry() self.resize(self.sizeHint()) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) if undoRedo: self.setDirty(not self.__undoStack.isClean()) else: self.setDirty(False) if clearUndo: self.__undoStack.clear() self.sizeChanged.emit(*self.iconSize()) def iconImage(self): """ Public method to get a copy of the icon image. @return copy of the icon image (QImage) """ return QImage(self.__image) def iconSize(self): """ Public method to get the size of the icon. @return width and height of the image as a tuple (integer, integer) """ return self.__image.width(), self.__image.height() def setZoomFactor(self, newZoom): """ Public method to set the zoom factor in percent. @param newZoom zoom factor (integer >= 100) """ newZoom = max(100, newZoom) # must not be less than 100 if newZoom != self.__zoom: self.__zoom = newZoom // 100 self.update() self.updateGeometry() self.resize(self.sizeHint()) self.zoomChanged.emit(int(self.__zoom * 100)) def zoomFactor(self): """ Public method to get the current zoom factor in percent. @return zoom factor (integer) """ return self.__zoom * 100 def setGridEnabled(self, enable): """ Public method to enable the display of grid lines. @param enable enabled status of the grid lines (boolean) """ if enable != self.__gridEnabled: self.__gridEnabled = enable self.update() def isGridEnabled(self): """ Public method to get the grid lines status. @return enabled status of the grid lines (boolean) """ return self.__gridEnabled def paintEvent(self, evt): """ Protected method called to repaint some of the widget. @param evt reference to the paint event object (QPaintEvent) """ painter = QPainter(self) if self.__zoom >= 3 and self.__gridEnabled: painter.setPen(self.palette().windowText().color()) i = 0 while i <= self.__image.width(): painter.drawLine( self.__zoom * i, 0, self.__zoom * i, self.__zoom * self.__image.height()) i += 1 j = 0 while j <= self.__image.height(): painter.drawLine( 0, self.__zoom * j, self.__zoom * self.__image.width(), self.__zoom * j) j += 1 col = QColor("#aaa") painter.setPen(Qt.DashLine) for i in range(0, self.__image.width()): for j in range(0, self.__image.height()): rect = self.__pixelRect(i, j) if evt.region().intersects(rect): color = QColor.fromRgba(self.__image.pixel(i, j)) painter.fillRect(rect, QBrush(Qt.white)) painter.fillRect(QRect(rect.topLeft(), rect.center()), col) painter.fillRect(QRect(rect.center(), rect.bottomRight()), col) painter.fillRect(rect, QBrush(color)) if self.__isMarked(i, j): painter.drawRect(rect.adjusted(0, 0, -1, -1)) painter.end() def __pixelRect(self, i, j): """ Private method to determine the rectangle for a given pixel coordinate. @param i x-coordinate of the pixel in the image (integer) @param j y-coordinate of the pixel in the image (integer) @return rectangle for the given pixel coordinates (QRect) """ if self.__zoom >= 3 and self.__gridEnabled: return QRect(self.__zoom * i + 1, self.__zoom * j + 1, self.__zoom - 1, self.__zoom - 1) else: return QRect(self.__zoom * i, self.__zoom * j, self.__zoom, self.__zoom) def mousePressEvent(self, evt): """ Protected method to handle mouse button press events. @param evt reference to the mouse event object (QMouseEvent) """ if evt.button() == Qt.LeftButton: if self.__isPasting: self.__isPasting = False self.editPaste(True) self.__markImage.fill(self.NoMarkColor.rgba()) self.update(self.__pasteRect) self.__pasteRect = QRect() return if self.__curTool == self.Pencil: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__setImagePixel(evt.pos(), True) self.setDirty(True) self.__undoStack.push(cmd) self.__currentUndoCmd = cmd elif self.__curTool == self.Rubber: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__setImagePixel(evt.pos(), False) self.setDirty(True) self.__undoStack.push(cmd) self.__currentUndoCmd = cmd elif self.__curTool == self.Fill: i, j = self.__imageCoordinates(evt.pos()) col = QColor() col.setRgba(self.__image.pixel(i, j)) cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__drawFlood(i, j, col) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) elif self.__curTool == self.ColorPicker: i, j = self.__imageCoordinates(evt.pos()) col = QColor() col.setRgba(self.__image.pixel(i, j)) self.setPenColor(col) else: self.__unMark() self.__startPos = evt.pos() self.__endPos = evt.pos() def mouseMoveEvent(self, evt): """ Protected method to handle mouse move events. @param evt reference to the mouse event object (QMouseEvent) """ self.positionChanged.emit(*self.__imageCoordinates(evt.pos())) if self.__isPasting and not (evt.buttons() & Qt.LeftButton): self.__drawPasteRect(evt.pos()) return if evt.buttons() & Qt.LeftButton: if self.__curTool == self.Pencil: self.__setImagePixel(evt.pos(), True) self.setDirty(True) elif self.__curTool == self.Rubber: self.__setImagePixel(evt.pos(), False) self.setDirty(True) elif self.__curTool in [self.Fill, self.ColorPicker]: pass # do nothing else: self.__drawTool(evt.pos(), True) def mouseReleaseEvent(self, evt): """ Protected method to handle mouse button release events. @param evt reference to the mouse event object (QMouseEvent) """ if evt.button() == Qt.LeftButton: if self.__curTool in [self.Pencil, self.Rubber]: if self.__currentUndoCmd: self.__currentUndoCmd.setAfterImage(self.__image) self.__currentUndoCmd = None if self.__curTool not in [self.Pencil, self.Rubber, self.Fill, self.ColorPicker, self.RectangleSelection, self.CircleSelection]: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) if self.__drawTool(evt.pos(), False): self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.setDirty(True) def __setImagePixel(self, pos, opaque): """ Private slot to set or erase a pixel. @param pos position of the pixel in the widget (QPoint) @param opaque flag indicating a set operation (boolean) """ i, j = self.__imageCoordinates(pos) if self.__image.rect().contains(i, j) and (i, j) != self.__lastPos: if opaque: painter = QPainter(self.__image) painter.setPen(self.penColor()) painter.setCompositionMode(self.__compositingMode) painter.drawPoint(i, j) else: self.__image.setPixel(i, j, qRgba(0, 0, 0, 0)) self.__lastPos = (i, j) self.update(self.__pixelRect(i, j)) def __imageCoordinates(self, pos): """ Private method to convert from widget to image coordinates. @param pos widget coordinate (QPoint) @return tuple with the image coordinates (tuple of two integers) """ i = pos.x() // self.__zoom j = pos.y() // self.__zoom return i, j def __drawPasteRect(self, pos): """ Private slot to draw a rectangle for signaling a paste operation. @param pos widget position of the paste rectangle (QPoint) """ self.__markImage.fill(self.NoMarkColor.rgba()) if self.__pasteRect.isValid(): self.__updateImageRect( self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) x, y = self.__imageCoordinates(pos) isize = self.__image.size() if x + self.__clipboardSize.width() <= isize.width(): sx = self.__clipboardSize.width() else: sx = isize.width() - x if y + self.__clipboardSize.height() <= isize.height(): sy = self.__clipboardSize.height() else: sy = isize.height() - y self.__pasteRect = QRect(QPoint(x, y), QSize(sx - 1, sy - 1)) painter = QPainter(self.__markImage) painter.setPen(self.MarkColor) painter.drawRect(self.__pasteRect) painter.end() self.__updateImageRect(self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) def __drawTool(self, pos, mark): """ Private method to perform a draw operation depending of the current tool. @param pos widget coordinate to perform the draw operation at (QPoint) @param mark flag indicating a mark operation (boolean) @return flag indicating a successful draw (boolean) """ self.__unMark() if mark: self.__endPos = QPoint(pos) drawColor = self.MarkColor img = self.__markImage else: drawColor = self.penColor() img = self.__image start = QPoint(*self.__imageCoordinates(self.__startPos)) end = QPoint(*self.__imageCoordinates(pos)) painter = QPainter(img) painter.setPen(drawColor) painter.setCompositionMode(self.__compositingMode) if self.__curTool == self.Line: painter.drawLine(start, end) elif self.__curTool in [self.Rectangle, self.FilledRectangle, self.RectangleSelection]: left = min(start.x(), end.x()) top = min(start.y(), end.y()) right = max(start.x(), end.x()) bottom = max(start.y(), end.y()) if self.__curTool == self.RectangleSelection: painter.setBrush(QBrush(drawColor)) if self.__curTool == self.FilledRectangle: for y in range(top, bottom + 1): painter.drawLine(left, y, right, y) else: painter.drawRect(left, top, right - left, bottom - top) if self.__selecting: self.__selRect = QRect( left, top, right - left + 1, bottom - top + 1) self.__selectionAvailable = True self.selectionAvailable.emit(True) elif self.__curTool in [self.Circle, self.FilledCircle, self.CircleSelection]: r = max(abs(start.x() - end.x()), abs(start.y() - end.y())) if self.__curTool in [self.FilledCircle, self.CircleSelection]: painter.setBrush(QBrush(drawColor)) painter.drawEllipse(start, r, r) if self.__selecting: self.__selRect = QRect(start.x() - r, start.y() - r, 2 * r + 1, 2 * r + 1) self.__selectionAvailable = True self.selectionAvailable.emit(True) elif self.__curTool in [self.Ellipse, self.FilledEllipse]: r1 = abs(start.x() - end.x()) r2 = abs(start.y() - end.y()) if r1 == 0 or r2 == 0: return False if self.__curTool == self.FilledEllipse: painter.setBrush(QBrush(drawColor)) painter.drawEllipse(start, r1, r2) painter.end() if self.__curTool in [self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse]: self.update() else: self.__updateRect(self.__startPos, pos) return True def __drawFlood(self, i, j, oldColor, doUpdate=True): """ Private method to perform a flood fill operation. @param i x-value in image coordinates (integer) @param j y-value in image coordinates (integer) @param oldColor reference to the color at position i, j (QColor) @param doUpdate flag indicating an update is requested (boolean) (used for speed optimizations) """ if not self.__image.rect().contains(i, j) or \ self.__image.pixel(i, j) != oldColor.rgba() or \ self.__image.pixel(i, j) == self.penColor().rgba(): return self.__image.setPixel(i, j, self.penColor().rgba()) self.__drawFlood(i, j - 1, oldColor, False) self.__drawFlood(i, j + 1, oldColor, False) self.__drawFlood(i - 1, j, oldColor, False) self.__drawFlood(i + 1, j, oldColor, False) if doUpdate: self.update() def __updateRect(self, pos1, pos2): """ Private slot to update parts of the widget. @param pos1 top, left position for the update in widget coordinates (QPoint) @param pos2 bottom, right position for the update in widget coordinates (QPoint) """ self.__updateImageRect(QPoint(*self.__imageCoordinates(pos1)), QPoint(*self.__imageCoordinates(pos2))) def __updateImageRect(self, ipos1, ipos2): """ Private slot to update parts of the widget. @param ipos1 top, left position for the update in image coordinates (QPoint) @param ipos2 bottom, right position for the update in image coordinates (QPoint) """ r1 = self.__pixelRect(ipos1.x(), ipos1.y()) r2 = self.__pixelRect(ipos2.x(), ipos2.y()) left = min(r1.x(), r2.x()) top = min(r1.y(), r2.y()) right = max(r1.x() + r1.width(), r2.x() + r2.width()) bottom = max(r1.y() + r1.height(), r2.y() + r2.height()) self.update(left, top, right - left + 1, bottom - top + 1) def __unMark(self): """ Private slot to remove the mark indicator. """ self.__markImage.fill(self.NoMarkColor.rgba()) if self.__curTool in [self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse, self.CircleSelection]: self.update() else: self.__updateRect(self.__startPos, self.__endPos) if self.__selecting: self.__selRect = QRect() self.__selectionAvailable = False self.selectionAvailable.emit(False) def __isMarked(self, i, j): """ Private method to check, if a pixel is marked. @param i x-value in image coordinates (integer) @param j y-value in image coordinates (integer) @return flag indicating a marked pixel (boolean) """ return self.__markImage.pixel(i, j) == self.MarkColor.rgba() def __updatePreviewPixmap(self): """ Private slot to generate and signal an updated preview pixmap. """ p = QPixmap.fromImage(self.__image) self.previewChanged.emit(p) def previewPixmap(self): """ Public method to generate a preview pixmap. @return preview pixmap (QPixmap) """ p = QPixmap.fromImage(self.__image) return p def __checkClipboard(self): """ Private slot to check, if the clipboard contains a valid image, and signal the result. """ ok = self.__clipboardImage()[1] self.__clipboardImageAvailable = ok self.clipboardImageAvailable.emit(ok) def canPaste(self): """ Public slot to check the availability of the paste operation. @return flag indicating availability of paste (boolean) """ return self.__clipboardImageAvailable def __clipboardImage(self): """ Private method to get an image from the clipboard. @return tuple with the image (QImage) and a flag indicating a valid image (boolean) """ img = QApplication.clipboard().image() ok = not img.isNull() if ok: img = img.convertToFormat(QImage.Format_ARGB32) return img, ok def __getSelectionImage(self, cut): """ Private method to get an image from the selection. @param cut flag indicating to cut the selection (boolean) @return image of the selection (QImage) """ if cut: cmd = IconEditCommand(self, self.tr("Cut Selection"), self.__image) img = QImage(self.__selRect.size(), QImage.Format_ARGB32) img.fill(qRgba(0, 0, 0, 0)) for i in range(0, self.__selRect.width()): for j in range(0, self.__selRect.height()): if self.__image.rect().contains(self.__selRect.x() + i, self.__selRect.y() + j): if self.__isMarked( self.__selRect.x() + i, self.__selRect.y() + j): img.setPixel(i, j, self.__image.pixel( self.__selRect.x() + i, self.__selRect.y() + j)) if cut: self.__image.setPixel(self.__selRect.x() + i, self.__selRect.y() + j, qRgba(0, 0, 0, 0)) if cut: self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.__unMark() if cut: self.update(self.__selRect) return img def editCopy(self): """ Public slot to copy the selection. """ if self.__selRect.isValid(): img = self.__getSelectionImage(False) QApplication.clipboard().setImage(img) def editCut(self): """ Public slot to cut the selection. """ if self.__selRect.isValid(): img = self.__getSelectionImage(True) QApplication.clipboard().setImage(img) @pyqtSlot() def editPaste(self, pasting=False): """ Public slot to paste an image from the clipboard. @param pasting flag indicating part two of the paste operation (boolean) """ img, ok = self.__clipboardImage() if ok: if img.width() > self.__image.width() or \ img.height() > self.__image.height(): res = E5MessageBox.yesNo( self, self.tr("Paste"), self.tr( """<p>The clipboard image is larger than the""" """ current image.<br/>Paste as new image?</p>""")) if res: self.editPasteAsNew() return elif not pasting: self.__isPasting = True self.__clipboardSize = img.size() else: cmd = IconEditCommand(self, self.tr("Paste Clipboard"), self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) painter = QPainter(self.__image) painter.setPen(self.penColor()) painter.setCompositionMode(self.__compositingMode) painter.drawImage( self.__pasteRect.x(), self.__pasteRect.y(), img, 0, 0, self.__pasteRect.width() + 1, self.__pasteRect.height() + 1) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.__updateImageRect( self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) else: E5MessageBox.warning( self, self.tr("Pasting Image"), self.tr("""Invalid image data in clipboard.""")) def editPasteAsNew(self): """ Public slot to paste the clipboard as a new image. """ img, ok = self.__clipboardImage() if ok: cmd = IconEditCommand( self, self.tr("Paste Clipboard as New Image"), self.__image) self.setIconImage(img) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editSelectAll(self): """ Public slot to select the complete image. """ self.__unMark() self.__startPos = QPoint(0, 0) self.__endPos = QPoint(self.rect().bottomRight()) self.__markImage.fill(self.MarkColor.rgba()) self.__selRect = self.__image.rect() self.__selectionAvailable = True self.selectionAvailable.emit(True) self.update() def editClear(self): """ Public slot to clear the image. """ self.__unMark() cmd = IconEditCommand(self, self.tr("Clear Image"), self.__image) self.__image.fill(qRgba(0, 0, 0, 0)) self.update() self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editResize(self): """ Public slot to resize the image. """ from .IconSizeDialog import IconSizeDialog dlg = IconSizeDialog(self.__image.width(), self.__image.height()) res = dlg.exec_() if res == QDialog.Accepted: newWidth, newHeight = dlg.getData() if newWidth != self.__image.width() or \ newHeight != self.__image.height(): cmd = IconEditCommand(self, self.tr("Resize Image"), self.__image) img = self.__image.scaled( newWidth, newHeight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) self.setIconImage(img) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editNew(self): """ Public slot to generate a new, empty image. """ from .IconSizeDialog import IconSizeDialog dlg = IconSizeDialog(self.__image.width(), self.__image.height()) res = dlg.exec_() if res == QDialog.Accepted: width, height = dlg.getData() img = QImage(width, height, QImage.Format_ARGB32) img.fill(qRgba(0, 0, 0, 0)) self.setIconImage(img) def grayScale(self): """ Public slot to convert the image to gray preserving transparency. """ cmd = IconEditCommand(self, self.tr("Convert to Grayscale"), self.__image) for x in range(self.__image.width()): for y in range(self.__image.height()): col = self.__image.pixel(x, y) if col != qRgba(0, 0, 0, 0): gray = qGray(col) self.__image.setPixel( x, y, qRgba(gray, gray, gray, qAlpha(col))) self.update() self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editUndo(self): """ Public slot to perform an undo operation. """ if self.__undoStack.canUndo(): self.__undoStack.undo() def editRedo(self): """ Public slot to perform a redo operation. """ if self.__undoStack.canRedo(): self.__undoStack.redo() def canUndo(self): """ Public method to return the undo status. @return flag indicating the availability of undo (boolean) """ return self.__undoStack.canUndo() def canRedo(self): """ Public method to return the redo status. @return flag indicating the availability of redo (boolean) """ return self.__undoStack.canRedo() def __cleanChanged(self, clean): """ Private slot to handle the undo stack clean state change. @param clean flag indicating the clean state (boolean) """ self.setDirty(not clean) def shutdown(self): """ Public slot to perform some shutdown actions. """ self.__undoStack.canRedoChanged.disconnect(self.canRedoChanged) self.__undoStack.canUndoChanged.disconnect(self.canUndoChanged) self.__undoStack.cleanChanged.disconnect(self.__cleanChanged) def isSelectionAvailable(self): """ Public method to check the availability of a selection. @return flag indicating the availability of a selection (boolean) """ return self.__selectionAvailable
class App(QMainWindow): reload_address = pyqtSignal(dict) def __init__(self, filename=None): super(App, self).__init__() self.is_write = False self.backup_file = os.path.join(os.getcwd(), '.tmp.json') self.setWindowTitle(GUI_NAME) self.filedialog = FileDialog(self) # self.table = Table(self) self.address_space = {} self.info = InfoDialog(title=GUI_NAME, parent=self) self.threadpool = QThreadPool() self.data = {'project': '', 'top_module': '', 'blocks': []} self.undoStack = QUndoStack() self.treeUndoStack = QUndoStack() self.treeUndoStack.setUndoLimit(50) self.tree = BlockView(parent=self, cols=register_columns, blocks=self.data['blocks'], undoStack=self.treeUndoStack) self.table = FieldView(parent=self, cols=field_columns, items=[], undoStack=self.undoStack) self.tree.selectionChanged.connect(self.createTable) self.hbox = QHBoxLayout() splitter = QSplitter(Qt.Horizontal) splitter.addWidget(self.tree) splitter.addWidget(self.table) splitter.setStretchFactor(0, 2) splitter.setStretchFactor(1, 5) self.hbox.addWidget(splitter) self.setCentralWidget(QWidget(self)) self.tabs = TabLayout(self) self.tabs.setContentsMargins(0, 0, 0, 0) tab1 = QWidget(self) tab1.setLayout(self.hbox) self.analyzer = AnalyzerWapper(parent=self, cols=field_columns, address_space=self.address_space, reload=self.reload_address) self.tabs.setTab(tab1, title='RegisterProfile') self.tabs.setTab(self.analyzer, title='RegisterAnalyzer') self.setCentralWidget(self.tabs) self.menubar = self.menuBar() self.create_ui() if self.check_backup(): self.loadFiles([self.backup_file]) else: if filename: self.loadFiles([filename]) # self.table.tableSaved.connect(self.backUpFile) self.tree.addAnalyzerTrigger.connect(self.analyzer.add_analyzer) self.undoStack.indexChanged.connect(self.backUpFile) def create_ui(self): self.resize(1200, 600) self.center() self.createMenuBar(self.menubar) self.show() def center(self): rect = self.frameGeometry() rect.moveCenter(QDesktopWidget().availableGeometry().center()) self.move(rect.topLeft()) def createTable(self, block_index, row, block_name): success, msg = self.table.linting() self.tree.blockSignals(True) if success: # print(row) self.undoStack.clear() self.tree.blockSignals(False) register = self.data['blocks'][block_index].get_register(row) if register is None: MessageBox.showError( self, "Fields is missing. cannot create table\n", GUI_NAME) return self.table.create_rows(register.fields, caption=block_name) else: self.table.setFocus() MessageBox.showWarning( self, msg, GUI_NAME, ) self.tree.blockSignals(False) def createMenuBar(self, menubar: QMenuBar): menubar.addSeparator() for config in menubar_configs: for label, items in config.items(): menu = menubar.addMenu(label) for item in items: text = item.get('text') sc = item.get('shortcut', '') icon = item.get('icon', None) sub = item.get('sub', None) if sub: submenu = QMenu(text, self) for each_sub in sub: text = each_sub.get('text') icon = each_sub.get('icon', None) action = QAction(text, self) action.triggered.connect( getattr(self, each_sub.get('action'))) if icon: action.setIcon(qta.icon(icon, color='gray')) submenu.addAction(action) menu.addMenu(submenu) continue action = QAction(text, self) if isinstance(sc, list): action.setShortcuts(sc) else: action.setShortcut(sc) if icon: action.setIcon(qta.icon(icon, color='gray')) action.triggered.connect( getattr(self, item.get('action')) # self.actions.get() ) menu.addAction(action) self.more_actions() def undo(self): if isinstance(self.focusWidget(), QTreeView): self.treeUndoStack.undo() else: # table view self.undoStack.undo() # self.undoStack.undo def redo(self): if isinstance(self.focusWidget(), QTreeView): self.treeUndoStack.redo() else: # table view self.undoStack.redo() def selectFont(self): font, ok = QFontDialog.getFont( self.font(), self, ) if ok: self.setFont(font) def openFiles(self, ): filenames = self.filedialog.askopenfiles() self.loadFiles(filenames) def openDir(self): folder = self.filedialog.askopendir() if not folder: return filenames = [] for filename in os.listdir(folder): filename = os.path.join(folder, filename) if os.path.isfile(filename): filenames.append(filename) self.loadFiles(filenames) def loadFiles(self, filenames): if filenames: self.info.show() # self.tree.saveChanges() try: for filename in filenames: if filename.endswith('.xlsx') or filename.endswith('.xls'): parser = ExcelParser( filename=filename, blocks=self.data['blocks'], # callback=self.create_tree ) elif filename.endswith('json'): parser = JsonLoad( filename=filename, blocks=self.data['blocks'], ) else: self.info.upload_text( f'# [Error] This {filename} type of file not support') continue parser.signal.progress.connect(self.info.upload_text) parser.signal.done.connect(self.thread_done) self.info.thread_cnt += 1 self.threadpool.start(parser) except Exception: MessageBox.showError( self, "Oops! Parsing File failed...\n" + traceback.format_exc(), GUI_NAME) def thread_done(self): self.info.thread_cnt -= 1 if self.info.thread_cnt == 0: self.info.progress_done() if not self.is_write: self.get_address_space() self.create_tree() else: self.remove_backup() self.is_write = False # self.info.close() # self.info.upload_text( # '# [INFO] Program Ends.' # ) def newModule(self): new = InputDialog(title="New Module", parent=self, inputs=block_columns, label="Module Information", resize=[600, 400]) info, yes = new.get() if yes: self.data['blocks'].append(Block(info)) self.create_tree() def more_actions(self): focus_next = QAction('nextChild', self) focus_next.setShortcut('Ctrl+W') focus_next.triggered.connect(self.nextChild) self.addAction(focus_next) def nextChild(self): if isinstance(self.focusWidget(), QTreeView): self.table.setFocus() else: self.tree.setFocus() def create_tree(self): self.data['blocks'].sort(key=lambda x: x.base_address) self.tree.create_rows(self.data['blocks']) def closeEvent(self, event): yes = MessageBox.askyesno(self, GUI_NAME, "Are you sure want to leave?") if yes: self.info.destroy() self.remove_backup() event.accept() else: event.ignore() def saveAsOne(self): filename, ftype = self.filedialog.asksavefile( ftypes="Excel Files (*.xls);;JSON Files (*.json);;All Files (*);;", initial_ftype="Excel Files (*.xls)") self.save(filename=filename, ftype=ftype) def saveExcelSeparately(self): folder = self.filedialog.askopendir() self.save(filename=folder, separately=True, ftype='Excel Files (*.xls)') def saveJsonSeparately(self): folder = self.filedialog.askopendir() self.save(filename=folder, separately=True, ftype='JSON Files (*.json)') def save(self, separately=False, filename=None, ftype='xls'): if filename: self.tree.clearSelection() self.is_write = True self.info.show() # self.tree.saveChanges() if not self.data['blocks']: self.info.upload_text("# [Warning] There is no module exist.") return if 'json' in ftype: if not filename.endswith('.json') and not separately: filename += '.json' writer = JsonWriter(filename=filename, blocks=self.data['blocks'], separately=separately) elif 'xls' in ftype: if not filename.endswith('.xls') and not separately: filename += '.xls' if not separately: dialog = InputDialog( title=GUI_NAME, parent=self, inputs={ "HEADPAGE": {}, "PREFIX": {}, "DATE": { 'default': date.today().strftime("%Y/%m/%d") }, "AUTHOR": {}, "DESCRIPTION": {} }, label='Press Cancel to save without chip_index', resize=[600, 400]) index_info, yes = dialog.get() if not yes: index_info = {} else: index_info = {} writer = ExcelWriter(filename=filename, blocks=self.data['blocks'], separately=separately, index_info=index_info) else: self.info.upload_text( f"# [Error] This {filename} type of file not support") return writer.signal.progress.connect(self.info.upload_text) writer.signal.done.connect(self.thread_done) self.info.thread_cnt += 1 self.threadpool.start(writer) def get_address_space(self): self.address_space.clear() for block in self.data['blocks']: # self.info.upload_text( # f"# [INFO] loading {block} ..." # ) for address_space, fields in block.address_space(): if address_space in self.address_space: self.info.upload_text( f"# [Warning] The address {address_space} is duplicated." ) self.address_space[address_space] = fields self.reload_address.emit(self.address_space) def remove_backup(self): if os.path.exists(self.backup_file): os.remove(self.backup_file) def check_backup(self): yes = False if os.path.exists(self.backup_file): yes = MessageBox.askyesno( self, GUI_NAME, "Maybe crash before.\n" "Do you want to reload previous file?") return yes def backUpFile(self, index=None): thread = BackUpFile(blocks=self.data['blocks'], filename=self.backup_file) self.threadpool.start(thread) def saveAsHTML(self): filename, _ = self.filedialog.asksavefile( initial_ftype="HTML (*.html)") if not filename: return if not filename.endswith('.html'): filename = filename.split('.')[0] + '.html' dialog = InputDialog(title=GUI_NAME, parent=self, inputs={ "Project": {}, "Module": {} }) project, save = dialog.get() if not save: return self.is_write = True self.info.show() # self.tree.saveChanges() path = os.path.abspath(os.path.dirname(__file__)) template = os.path.join(path, 'templates/template.html') writer = HTMLWriter(filename=filename, blocks=self.data['blocks'], template=template, project=project) writer.signal.progress.connect(self.info.upload_text) writer.signal.done.connect(self.thread_done) self.info.thread_cnt += 1 self.threadpool.start(writer) def doNothing(self): # importlib.reload(darkmode) # self.setStyleSheet(darkmode.style) pass
class AnnotationsNetworkView(NetworkView): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._undo_stack = QUndoStack(self) self._undo_view = QUndoView(self._undo_stack) self._undo_view.setCleanIcon(QIcon(":/icons/images/document-save.svg")) self._undo_menu = QMenu(self) action = QWidgetAction(self._undo_menu) action.setDefaultWidget(self._undo_view) self._undo_menu.addAction(action) self._orig_point = None self._item_to_draw = None self._item_old_pos = None self._item_old_line_or_rect = None self._mode = None self._dialog = None self.setDragMode(QGraphicsView.RubberBandDrag) def setScene(self, scene: AnnotationsNetworkScene): scene.editAnnotationItemRequested.connect( self.on_edit_annotation_item_requested) self._undo_stack.clear() super().setScene(scene) def addAnnotationItem(self, item: QGraphicsItem, pos: QPointF): scene = self.scene() if scene is None: return if isinstance(item, (ArrowItem, RectItem, EllipseItem)): item.setPen( QPen(Qt.black, scene.getDefaultPenSizeFromRect(), Qt.SolidLine)) self._undo_stack.push(AddCommand(item, scene)) item.setPos(pos) scene.annotationAdded.emit(item) return item def addAnnotationLine(self, line: QLineF, pos: QPointF) -> Union[ArrowItem, None]: return self.addAnnotationArrow(line, pos, False, False) def addAnnotationArrow(self, line: QLineF, pos: QPointF, has_head=True, has_tail=False) -> Union[ArrowItem, None]: item = ArrowItem(line, has_head=has_head, has_tail=has_tail) return self.addAnnotationItem(item, pos) def addAnnotationRect(self, rect: QRectF, pos: QPointF) -> Union[RectItem, None]: return self.addAnnotationItem(RectItem(rect), pos) def addAnnotationEllipse( self, rect: QRectF, pos: QPointF) -> Union[QGraphicsEllipseItem, None]: return self.addAnnotationItem(EllipseItem(rect), pos) def addAnnotationText(self, text: str, font: QFont, pos: QPointF) -> Union[TextItem, None]: item = TextItem(text) item.setFont(font) return self.addAnnotationItem(item, pos) def setDrawMode(self, mode): self._mode = mode if mode is None: self.setDragMode(QGraphicsView.RubberBandDrag) self._orig_point = None else: self.setDragMode(QGraphicsView.NoDrag) def mode(self): return self._mode def undoView(self): return self._undo_view def undoStack(self): return self._undo_stack def undoMenu(self): return self._undo_menu def loadAnnotations(self, buffer: bytes) -> None: if not buffer: return scene = self.scene() if scene is None: return def read(len): nonlocal buffer, pos data = buffer[pos:pos + len] pos += len return data pos = 0 data = read(1) while data: if data in (b'L', b'A'): x1, y1, x2, y2 = struct.unpack("ffff", read(16)) line = QLineF(0., 0., x2, y2) if data == b'L': self.addAnnotationLine(line, QPointF(x1, y1)) elif data == b'A': data, = struct.unpack("B", read(1)) has_head = (data >> 0) & 1 has_tail = (data >> 1) & 1 self.addAnnotationArrow(line, QPointF(x1, y1), has_head, has_tail) elif data in (b'R', b'C'): x, y, w, h = struct.unpack("ffff", read(16)) rect = QRectF(0., 0., w, h) if data == b'R': self.addAnnotationRect(rect, QPointF(x, y)) elif data == b'C': self.addAnnotationEllipse(rect, QPointF(x, y)) elif data == b'T': x, y, len = struct.unpack("ffH", read(10)) text = read(len).decode() font_size, = struct.unpack("H", read(2)) font = QFont() font.setPointSize(font_size) self.addAnnotationText(text, font, QPointF(x, y)) data = read(1) self._undo_stack.setClean() def saveAnnotations(self) -> bytes: scene = self.scene() if scene is None: return b'' buffer = b'' for item in scene.items(): if isinstance(item, ArrowItem): char = b'A' if item.hasHead() or item.hasTail() else b'L' line = item.line() pos = item.pos() buffer += char buffer += struct.pack("ffff", pos.x(), pos.y(), line.x2(), line.y2()) if char == b'A': buffer += struct.pack("B", item.hasHead() + item.hasTail() * 2) elif isinstance(item, (RectItem, EllipseItem)): char = b'R' if isinstance(item, RectItem) else b'C' rect = item.rect() pos = item.pos() buffer += char buffer += struct.pack("ffff", pos.x(), pos.y(), rect.width(), rect.height()) elif isinstance(item, TextItem): pos = item.pos() text = item.text() font = item.font() buffer += b'T' buffer += struct.pack("ff", pos.x(), pos.y()) buffer += struct.pack("H", len(text)) buffer += str.encode(text) buffer += struct.pack("H", font.pointSize()) return buffer def deleteSelectedAnnotations(self): scene = self.scene() if scene is None: return for item in scene.annotationsLayer.childItems(): if item.isSelected(): self._undo_stack.push(DeleteCommand(item, self)) def on_edit_annotation_item_requested(self, item: QGraphicsItem): # Edit text item if isinstance(item, TextItem): def edit_text_item(result): if result == QDialog.Accepted: old_text = item.text() old_font = item.font() text, font_size = self._dialog.getValues() font = QFont() font.setPointSize(font_size) item.setText(text) item.setFont(font) self._undo_stack.push( EditTextCommand(item, old_text, old_font.pointSize(), self.scene())) self._dialog = TextItemInputDialog(self) self._dialog.setValues(item.text(), item.font().pointSize()) self._dialog.finished.connect(edit_text_item) self._dialog.open() def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: scene = self.scene() if scene: if self._mode is None or self._mode == MODE_TEXT: item = self.itemAt(event.pos()) if self._mode is None: self.on_edit_annotation_item_requested(item) # Add a text item at mouse position elif self._mode == MODE_TEXT: def add_text_item(result): if result == QDialog.Accepted: text, font_size = self._dialog.getValues() font = QFont() font.setPointSize(font_size) self.addAnnotationText(text, font, pos) pos = self.mapToScene(event.pos()) self._dialog = TextItemInputDialog(self) self._dialog.setValues("", scene.getDefaultFontSizeFromRect()) self._dialog.finished.connect(add_text_item) self._dialog.open() else: super().mouseDoubleClickEvent(event) def mousePressEvent(self, event: QMouseEvent) -> None: scene = self.scene() if scene: # Edit item (line, rect, ellipse, etc) if self._mode is None: self._item_to_draw = item = self.itemAt(event.pos()) if item: self._item_old_pos = item.pos() event_pos = self.mapToScene(event.pos()) - item.pos() # Edit Arrows head and tails if isinstance(item, ArrowItem) and event.modifiers( ) & Qt.ShiftModifier == Qt.ShiftModifier: tol = item.pen().width() * 2. if (event_pos - item.line().p1()).manhattanLength() < tol: has_tail = item.hasTail() item.setTail(not has_tail) self._undo_stack.push( EditArrowCommand(item, has_tail, item.hasHead(), self.scene())) scene.arrowEdited.emit(item) elif (event_pos - item.line().p2()).manhattanLength() < tol: has_head = item.hasHead() item.setHead(not has_head) self._undo_stack.push( EditArrowCommand(item, item.hasTail(), has_head, self.scene())) scene.arrowEdited.emit(item) # Resize Line and Arrow if isinstance(item, ArrowItem): tol = item.pen().width() * 2. self._item_old_line_or_rect = item.line() if (event_pos - item.line().p1()).manhattanLength() < tol: self._orig_point = item.line().p2() + item.pos() elif (event_pos - item.line().p2()).manhattanLength() < tol: self._orig_point = item.line().p1() + item.pos() # Resize Rect and Ellipse elif isinstance(item, (EllipseItem, RectItem)): self._item_old_line_or_rect = item.rect() tol = item.pen().width() if (event_pos - item.rect().topLeft()).manhattanLength() < tol: self._orig_point = item.rect().bottomRight( ) + item.pos() elif (event_pos - item.rect().bottomRight() ).manhattanLength() < tol: self._orig_point = item.rect().topLeft( ) + item.pos() elif (event_pos - item.rect().topRight()).manhattanLength() < tol: self._orig_point = item.rect().bottomLeft( ) + item.pos() elif (event_pos - item.rect().bottomLeft() ).manhattanLength() < tol: self._orig_point = item.rect().topRight( ) + item.pos() # Define starting point of item (line, rect, ellipse, etc) elif self._mode != MODE_TEXT: self._orig_point = self.mapToScene(event.pos()) super().mousePressEvent(event) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: scene = self.scene() if scene and self._orig_point is not None: # Edit line or arrow if self._mode in (MODE_LINE, MODE_ARROW) \ or (self._mode is None and isinstance(self._item_to_draw, QGraphicsLineItem)): pos = self.mapToScene(event.pos()) x = pos.x() - self._orig_point.x() y = pos.y() - self._orig_point.y() if self._item_to_draw and self._orig_point == self._item_to_draw.line( ).p2() + self._item_to_draw.pos(): # Moving line around head line = QLineF(0, 0, -x, -y) point = pos else: line = QLineF(0, 0, x, y) point = None if self._item_to_draw is None: if self._mode == MODE_LINE: self._item_to_draw = self.addAnnotationLine( line, self._orig_point) elif self._mode == MODE_ARROW: self._item_to_draw = self.addAnnotationArrow( line, self._orig_point) else: self._item_to_draw.setLine(line) if point is not None: self._item_to_draw.setPos(point) return # Edit rect or circle elif self._mode in (MODE_RECT, MODE_ELLIPSE) \ or (self._mode is None and isinstance(self._item_to_draw, (RectItem, EllipseItem))): pos = self.mapToScene(event.pos()) width = pos.x() - self._orig_point.x() height = pos.y() - self._orig_point.y() dx = 0 if width >= 0 else width dy = 0 if height >= 0 else height rect = QRectF(0, 0, abs(width), abs(height)) point = self._orig_point + QPointF(dx, dy) if self._item_to_draw is None: if self._mode == MODE_RECT: self._item_to_draw = self.addAnnotationRect( rect, point) elif self._mode == MODE_ELLIPSE: self._item_to_draw = self.addAnnotationEllipse( rect, point) else: self._item_to_draw.setRect(rect) self._item_to_draw.setPos(point) return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: if self._mode is None and self._item_to_draw is not None and event.button( ) == Qt.LeftButton: if isinstance(self._item_old_line_or_rect, QRectF): line_or_rect = self._item_to_draw.rect() elif isinstance(self._item_old_line_or_rect, QLineF): line_or_rect = self._item_to_draw.line() else: line_or_rect = None if line_or_rect is None or self._item_old_line_or_rect == line_or_rect: if self._item_old_pos != self._item_to_draw.pos(): self._undo_stack.push( MoveCommand(self._item_to_draw, self._item_old_pos, self.scene())) else: self._undo_stack.push( ResizeCommand(self._item_to_draw, self._item_old_pos, self._item_old_line_or_rect, self.scene())) self._item_old_pos = None self._item_old_line_or_rect = None self._item_to_draw = None self._orig_point = None super().mouseReleaseEvent(event)
class Table(QTableWidget): def __init__(self, r, c, set_title): super().__init__(r, c) self.set_title = set_title self.check_change = True self.header_bold = False self.undo_stack = QUndoStack(self) self.init_cells() self.init_ui() self.installEventFilter(self) self.init_undo_cell_edits() return None def init_cells(self): for row in range(self.rowCount()): for col in range(self.columnCount()): (self.setItem(row, col, QTableWidgetItem()) if (self.item(row, col) == None) else None) def init_ui(self): self.cellChanged.connect(self.update_preview) self.cellChanged.connect(self.set_changed) self.cellChanged.connect((lambda: (self.set_header_style(True) if globals['header'] else None))) self.cellChanged.connect(self.on_cell_changed) self.itemSelectionChanged.connect(self.set_selection) self.cellActivated.connect((lambda: log('CELLACTIVATED'))) return self.show() def init_undo_cell_edits(self): "undo/redo of edits\n The whole thing is very complicated and dirty, because we can't\n use the standard way of using the QUndoCommand.\n We use 2 state variables, one that carries the current cell content\n and one that determines, if the current cell was changed.\n Every time we enter a cell, the content is written to open-editor-content\n and open-editor-content-changed is set to False. This is done in\n reimplemented function self.edit. If the user changes the cell content\n on-cell-changed is called and sets self.open-editor-content-changed\n to true. When the user leaves the cell self.closeEditor is called.\n If self.open-editor-content-changed is True it creates a QUndoCommand." self.open_editor_content = { '\ufdd0:old': '', '\ufdd0:new': '', '\ufdd0:row': 0, '\ufdd0:col': 0, } self.open_editor_content_changed = False def edit(self, index, tmp1, tmp2): log('OPENEDITOR') item = self.currentItem() txt = (item.text() if item else '') self.open_editor_content = { '\ufdd0:old': txt, '\ufdd0:row': self.currentRow(), '\ufdd0:col': self.currentColumn(), } self.open_editor_content_changed = False return QTableWidget.edit(self, index, tmp1, tmp2) def on_cell_changed(self, row, col): log('OnCELLCHANGED') self.open_editor_content_changed = True self.open_editor_content['\ufdd0:new'] = self.item( self.open_editor_content['\ufdd0:row'], self.open_editor_content['\ufdd0:col']).text() return debug(self.open_editor_content) def closeEditor(self, editor, hint): log('CLOSEPERSISTANTEDITOR') if self.open_editor_content_changed: log('DO EDIT-COMMAND') command = Command_Cell_Edit(self, self.open_editor_content, 'Edit Cell') _hy_anon_var_1 = self.undo_stack.push(command) else: _hy_anon_var_1 = None return QTableWidget.closeEditor(self, editor, hint) def range_content(self, selection_range): rows = [] for row in range(selection_range.topRow(), inc(selection_range.bottomRow())): cols = [] for col in range(selection_range.leftColumn(), inc(selection_range.rightColumn())): item = self.item(row, col) cols.append(item) rows.append(cols) return rows def undo(self): 'Undo changes to table' log('UNDO') return self.undo_stack.undo() def redo(self): 'Redo changes to table' log('REDO') return self.undo_stack.redo() def set_selection(self): 'Void -> Void\n Inserts the selection to the primary clipboard. http://doc.qt.io/qt-5/qclipboard.html#Mode-enum' return (self.copy_selection(clipboard_mode=CLIPBOARD_MODE_SELECTION) if is_pos(self.selectionMode()) else None) def paste(self, clipboard_mode=CLIPBOARD_MODE_CLIPBOARD): 'Void (Enum(0-2)) -> Void\n Inserts the clipboard, at the upper left corner of the current selection' (log('WARNING: Paste only works on first selection') if (len(self.selectedRanges()) > 1) else None) r = first(self.selectedRanges()) paste_list = self.parse_for_paste( first(globals['clipboard'].text('plain', clipboard_mode))) if (r == None): start_row = self.currentRow() start_col = self.currentColumn() _hy_anon_var_2 = None else: start_col = r.leftColumn() start_row = r.topRow() _hy_anon_var_2 = None command = Command_Paste(self, start_row, start_col, paste_list, 'Paste') return self.undo_stack.push(command) def copy_selection(self, clipboard_mode=CLIPBOARD_MODE_CLIPBOARD): 'Int(0,2) -> Void\n Copies the current selection to the clipboard. Depending on the clipboard-mode to define which clipboard system is used\n QClipboard::Clipboard\t0\tindicates that data should be stored and retrieved from the global clipboard.\n QClipboard::Selection\t1\tindicates that data should be stored and retrieved from the global mouse selection. Support for Selection is provided only on systems with a global mouse selection (e.g. X11).\n QClipboard::FindBuffer\t2\tindicates that data should be stored and retrieved from the Find buffer. This mode is used for holding search strings on macOS.\n http://doc.qt.io/qt-5/qclipboard.html#Mode-enum' (log('WARNING: Copy only works on first selection') if (len(self.selectedRanges()) > 1) else None) r = first(self.selectedRanges()) copy_content = '' try: for row in range(r.topRow(), inc(r.bottomRow())): for col in range(r.leftColumn(), inc(r.rightColumn())): item = self.item(row, col) if (item != None): copy_content = (copy_content + item.text()) if (not (col == r.rightColumn())): copy_content = (copy_content + '\t') _hy_anon_var_3 = None else: _hy_anon_var_3 = None _hy_anon_var_4 = _hy_anon_var_3 else: _hy_anon_var_4 = None if (not (row == r.bottomRow())): copy_content = (copy_content + '\n') _hy_anon_var_5 = None else: _hy_anon_var_5 = None _hy_anon_var_6 = globals['clipboard'].setText( copy_content, clipboard_mode) except AttributeError as e: _hy_anon_var_6 = log('WARING: No selection available') return _hy_anon_var_6 def delete_selection(self): 'Void -> Void\n Deletes the current selection.' log('DELETE-SELECTION') command = Command_Delete(self, 'Delete') return self.undo_stack.push(command) def cut_selection(self): self.copy_selection() return self.delete_selection() def c_current(self): if self.check_change: row = self.currentRow() col = self.currentColumn() try: value = self.item(row, col).text() except AttributeError as e: value = '' log('The current cell is ', row, ' ', col) _hy_anon_var_8 = log('In this cell we have: ', value) else: _hy_anon_var_8 = None return _hy_anon_var_8 def update_preview(self): return (globals['webview'].setHtml( htmlExport.create_preview(self, globals['header'], PREVIEWHEADER, PREVIEWFOOTER)) if self.check_change else None) def new_sheet(self): (self.save_sheet_csv() if ((globals['filepath'] == UNTITLED_PATH) and is_pos(self.used_row_count())) else self.save_sheet_csv(globals['filepath'])) reset_bang(globals, 'filepath', UNTITLED_PATH) self.clear() self.save_sheet_csv(globals['filepath']) self.undo_stack.clear() return self.update_preview() def open_sheet(self, defpath=None): self.blockSignals(True) path = ([defpath] if defpath else QFileDialog.getOpenFileName( self, 'Open CSV', os.getenv('Home'), 'CSV(*.csv)')) reset_bang(globals, 'filepath', first(path)) self.check_change = False if (first(path) != ''): with open(first(path), 'r', newline='') as csv_file: self.setRowCount(0) my_file = csv.reader(csv_file, dialect='excel') for row_data in my_file: row = self.rowCount() self.insertRow(row) (self.setColumnCount(len(row_data)) if (len(row_data) > COLS) else None) for [column, stuff] in enumerate(row_data): item = QTableWidgetItem(stuff) self.setItem(row, column, item) _hy_anon_var_9 = self.setRowCount(ROWS) _hy_anon_var_10 = _hy_anon_var_9 else: _hy_anon_var_10 = None self.init_cells() (self.set_header_style(True) if globals['header'] else None) debug(self.used_row_count()) self.check_change = True reset_bang(globals, 'filechanged', False) self.set_title() self.update_preview() self.undo_stack.clear() return self.blockSignals(False) def save_sheet_csv(self, defpath=None): path = ([defpath] if defpath else QFileDialog.getSaveFileName( self, 'Save CSV', os.getenv('Home'), 'CSV(*.csv)')) if (first(path) != ''): with open(first(path), 'w', newline='') as csv_file: writer = csv.writer(csv_file, dialect='excel') for row in range(inc(self.used_row_count())): row_data = [] for col in range(inc(self.used_column_count())): item = self.item(row, col) (row_data.append(item.text()) if (item != None) else row_data.append('')) writer.writerow(row_data) _hy_anon_var_11 = None _hy_anon_var_12 = _hy_anon_var_11 else: _hy_anon_var_12 = None reset_bang(globals, 'filepath', first(path)) reset_bang(globals, 'filechanged', False) return self.set_title() def save_sheet_html(self): path = QFileDialog.getSaveFileName(self, 'Save HTML', os.getenv('Home'), 'HTML(*.html)') if (first(path) != ''): with open(first(path), 'w') as file: file.write(htmlExport.qtable_to_html(self, globals['header'])) _hy_anon_var_13 = file.close() _hy_anon_var_14 = _hy_anon_var_13 else: _hy_anon_var_14 = None return _hy_anon_var_14 def used_column_count(self): 'Returns the number of the last column with content, starts with 0 if none is used' ucc = 0 for r in range(self.rowCount()): for c in range(self.columnCount()): item = self.item(r, c) if ((item != None) and is_pos(len(item.text())) and (c >= ucc)): ucc = inc(c) _hy_anon_var_15 = None else: _hy_anon_var_15 = None return ucc def used_row_count(self): 'Returns the number of the last row with content, starts with 0 if none is used' urc = 0 for r in range(self.rowCount()): for c in range(self.columnCount()): item = self.item(r, c) if ((item != None) and is_pos(len(item.text())) and (r >= urc)): urc = inc(r) _hy_anon_var_16 = None else: _hy_anon_var_16 = None return urc def set_header_style(self, bold): 'Bool -> Bool\n Consumes the if style of header is bold or not\n returns global state' for col in range(self.columnCount()): item = self.item(0, col) if (item != None): font = item.font() font.setBold(bold) _hy_anon_var_17 = item.setFont(font) else: _hy_anon_var_17 = None return globals def eventFilter(self, object, ev): if ((ev.type() == QEvent.FocusOut) and (ev.reason() == ACTIVEWINDOWFOCUSREASON)): debug('QtCore.QEvent.FocusOut') debug(ev.lostFocus()) debug(ev.reason()) _hy_anon_var_18 = self.on_focus_lost(ev) else: _hy_anon_var_18 = None return False def on_focus_lost(self, ev): self.save_sheet_csv(globals['filepath']) return self.set_title() def set_changed(self): reset_bang(globals, 'filechanged', True) self.set_title() return log('set_changed') def clear(self): self.blockSignals(True) self.clearContents() self.init_cells() return self.blockSignals(False) def parse_for_paste(self, clipboard_text): "String -> List[][]\n Consumes a String clipboard-text and produces a 2-dimensional list with\n lines and columns. Example:\n 'Test\tTest\tTest\n1\t1\t2\t3' -> [['Test', 'Test', 'Test'], ['1', '2', '3']]" paste_list = [] row = [] lns = clipboard_text.strip().split('\n') debug(lns) for ln in lns: debug(ln) paste_list.append(ln.split('\t')) return paste_list def map_paste_list(self, lst, start_row, start_col, func): 'list function -> Void\n Cycles through a paste-list and uses function func on each cell' pl_rnr = 0 for row in range(start_row, (start_row + len(lst))): pl_cnr = 0 for col in range(start_col, (start_col + len(lst[pl_rnr]))): self.blockSignals(True) func(lst, pl_rnr, pl_cnr, row, col) self.blockSignals(False) pl_cnr = inc(pl_cnr) self.update_preview() self.set_changed() pl_rnr = inc(pl_rnr)
class ModFileTreeModel(QAbstractItemModel): """ A custom model that presents a view into the actual files saved within a mod's folder. It is vastly simplified compared to the QFileSystemModel, and only supports editing the state of the checkbox on each file or folder (though there is some neat trickery that propagates a check-action on a directory to all of its descendants) """ #TODO: calculate and inform the user of any file-conflicts that will occur in their mod-setup to help them decide what needs to be hidden. rootPathChanged = pyqtSignal(str) hasUnsavedChanges = pyqtSignal(bool) def __init__(self, parent, **kwargs): """ :param ModManager manager: :param kwargs: anything to pass on to base class :return: """ global Manager Manager = modmanager.Manager() # noinspection PyArgumentList super().__init__(parent=parent,**kwargs) self._parent = parent self.rootpath = None #type: str self.modname = None #type: str self.rootitem = None #type: QFSItem # the mod table has this stored on the custom view, # but we have no custom view for the file tree, so...here it is self.undostack = QUndoStack() @property def root_path(self): return self.rootpath @property def root_item(self): return self.rootitem @property def current_mod(self): return self.modname @property def has_unsaved_changes(self): return Manager.DB.in_transaction def setRootPath(self, path=None): """ Using this instead of a setter just for API-similarity with QFileSystemModel. That's the same reason rootPathChanged is emitted at the end of the method, as well. :param str path: the absolute filesystem path to the active mod's data folder. If passed as ``None``, the model is reset to empty """ if path == self.rootpath: return # commit any changes we've made so far self.save() # drop the undo stack self.undostack.clear() if path is None: # reset Model to show nothing self.beginResetModel() self.rootpath=None self.rootitem=None self.modname=None self.rootPathChanged.emit(path) self.endResetModel() elif check_path(path): # tells the view to get ready to redisplay its contents self.beginResetModel() self.rootpath = path self.modname = os.path.basename(path) self._setup_or_reload_tree() # tells the view it should get new # data from model & reset itself self.endResetModel() # emit notifier signal self.rootPathChanged.emit(path) def _setup_or_reload_tree(self): """ Loads thde data from the db and disk """ self._load_tree() # now mark hidden files self._mark_hidden_files() # this used to call resetModel() stuff, too, but I decided # this wasn't the place for that. It's a little barren now... def _load_tree(self): """ Build the tree from the rootitem :return: """ # name for this item is never actually seen self.rootitem = QFSItem(path="", name="data", parent=None) self.rootitem.load_children(self.rootpath, namefilter=lambda n: n.lower() == 'meta.ini') def _mark_hidden_files(self): hfiles = list(r['filepath'] for r in Manager.DB.select( "hiddenfiles", "filepath", where="directory = ?", params=(self.modname,) )) # only files (with their full paths relative to the root of # the mod directory) are in the hidden files list; thus we # need only compare files and not dirs to the list. As usual, # a directory's checkstate will be derived from its children for c in self.rootitem.iterchildren(True): if c.lpath in hfiles: c.checkState = Qt_Unchecked def getitem(self, index) -> QFSItem: """Extracts actual item from given index :param QModelIndex index: """ if index.isValid(): item = index.internalPointer() if item: return item return self.rootitem def item_from_path(self, path_parts): """ :param path_parts: a tuple where each element is an element in the filesystem path leading from the root item to the item :return: the item """ item = self.rootitem for p in path_parts: item = item[p] return item def columnCount(self, *args, **kwargs) -> int: """Dir/File Name(+checkbox), path to file, file conflicts """ # return 2 return len(COLUMNS) def rowCount(self, index=QModelIndex(), *args, **kwargs) -> int: """Number of children contained by the item referenced by `index` :param QModelIndex index: """ # return 0 until we actually have something to show return self.getitem(index).child_count if self.rootitem else 0 def headerData(self, section, orient, role=None): """Just one column, 'Name'. super() call should take care of the size hints &c. :param int section: :param orient: :param role: """ if orient == Qt.Horizontal and role==Qt.DisplayRole: return ColHeaders[section] # return "Name" return super().headerData(section, orient, role) def index(self, row, col, parent=QModelIndex(), *args, **kwargs): """ :param int row: :param int col: :param QModelIndex parent: :return: the QModelIndex that represents the item at (row, col) with respect to the given parent index. (or the root index if parent is invalid) """ parent_item = self.rootitem if parent.isValid(): parent_item = parent.internalPointer() child = parent_item[row] if child: return self.createIndex(row, col, child) return QModelIndex() def getIndexFromItem(self, item) -> QModelIndex: return self.createIndex(item.row, 0, item) # noinspection PyArgumentList @pyqtSlot('QModelIndex',name="parent", result = 'QModelIndex') def parent(self, child_index=QModelIndex()): if not child_index.isValid(): return QModelIndex() # get the parent FSItem from the reference stored in each FSItem parent = child_index.internalPointer().parent if not parent or parent is self.rootitem: return QModelIndex() # Every FSItem has a row attribute # which we use to create the index return self.createIndex(parent.row, 0, parent) # noinspection PyArgumentList @pyqtSlot(name='parent', result='QObject') def parent_of_self(self): return self._parent def flags(self, index): """ Flags are held at the item level; lookup and return them from the item referred to by the index :param QModelIndex index: """ # item = self.getitem(index) return self.getitem(index).itemflags def data(self, index, role=Qt.DisplayRole): """ We handle DisplayRole to return the filename, CheckStateRole to indicate whether the file has been hidden, and Decoration Role to return different icons for folders and files. :param QModelIndex index: :param role: """ item = self.getitem(index) col = index.column() if col == COL_PATH: if role == Qt.DisplayRole: #second column is path return item.parent.path + "/" elif col == COL_CONFLICTS: # third column is conflicts if role == Qt.DisplayRole and \ self.modname in Manager.mods_with_conflicting_files \ and item.lpath in Manager.file_conflicts: return "Yes" else: # column must be Name if role == Qt.DisplayRole: return item.name elif role == Qt_CheckStateRole: # hides the complexity of the tristate workings return item.checkState elif role == Qt.DecorationRole: return item.icon # noinspection PyTypeChecker def setData(self, index, value, role=Qt_CheckStateRole): """Only the checkStateRole can be edited in this model. Most of the machinery for that is in the QFSItem class :param QModelIndex index: :param value: :param role: """ if not index.isValid(): return False item = self.getitem(index) if role==Qt_CheckStateRole: item.checkState = value #triggers cascade if this a dir # if this item is the last checked/unchecked item in a dir, # make sure the change is propagated up through the parent # hierarchy, to make sure that no folders remain checked # when none of their descendants are. ancestor = self._get_highest_affected_ancestor(item, value) if ancestor is not item: index1 = self.getIndexFromItem(ancestor) else: index1 = index # using the "last_child_seen" value--which SHOULD be the most # "bottom-right" child that was just changed--to feed to # datachanged saves a lot of individual calls. Hopefully there # won't be any concurrency issues to worry about later on. # update the db with which files are now hidden self.update_db(index1, self.getIndexFromItem(QFSItem.last_child_seen)) return True return super().setData(index, value, role) def _get_highest_affected_ancestor(self, item, value): """worst name for a function ever but i can't think of better""" if item.parent and item.parent.children_checkState() == value: return self._get_highest_affected_ancestor(item.parent, value) else: return item # noinspection PyUnresolvedReferences def _send_data_through_proxy(self, index1, index2, *args): proxy = self._parent.model() #QSortFilterProxyModel proxy.dataChanged.emit(proxy.mapFromSource(index1), proxy.mapFromSource(index2), *args) def save(self): """ Commit any unsaved changes (currenlty just to hidden files) to the db and save the updated db state to disk """ if Manager.DB.in_transaction: Manager.DB.commit() Manager.save_hidden_files() self.hasUnsavedChanges.emit(False) def revert(self): """ Undo all changes made to the tree since the last save. """ self.beginResetModel() #SOOOO... # will a rollback/drop-the-undostack work here? # or is individually undoing everything (a bunch of savepoint- # rollbacks) better? I guess it depends on whether we want to # be able to define a "clean" point in the middle of a # transaction... Manager.DB.rollback() self.undostack.clear() # while self.undostack.canUndo() and not self.undostack.isClean(): # self.undostack.undo() self._setup_or_reload_tree() self.endResetModel() self.hasUnsavedChanges.emit(False) def update_db(self, start_index, final_index): """Make changes to database. NOTE: this does not commit them! That must be done separately :param start_index: index of the "top-left" affected item :param final_index: index of the "bottom-right" affected item """ cb = partial(self._send_data_through_proxy, start_index, final_index) self.undostack.push( ChangeHiddenFilesCommand(self.rootitem, os.path.basename(self.rootpath), post_redo_callback=cb, post_undo_callback=cb )) self.hasUnsavedChanges.emit(True)