Ejemplo n.º 1
0
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())
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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()
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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()
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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)