Esempio n. 1
0
class ObjectSelectionTool(AbstractObjectTool):
    def __init__(self, parent=None):
        super().__init__(self.tr("Select Objects"),
                         QIcon(":images/22x22/tool-select-objects.png"),
                         QKeySequence(self.tr("S")), parent)
        self.mSelectionRectangle = SelectionRectangle()
        self.mOriginIndicator = OriginIndicator()
        self.mMousePressed = False
        self.mHoveredObjectItem = None
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.mResizingLimitHorizontal = False
        self.mResizingLimitVertical = False
        self.mMode = Mode.Resize
        self.mAction = Action.NoAction
        self.mRotateHandles = [0, 0, 0, 0]
        self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0]
        self.mAlignPosition = QPointF()
        self.mMovingObjects = QVector()
        self.mScreenStart = QPoint()
        self.mStart = QPointF()
        self.mModifiers = 0
        self.mOrigin = QPointF()

        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i] = RotateHandle(i)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i] = ResizeHandle(i)

    def __del__(self):
        if self.mSelectionRectangle.scene():
            self.mSelectionRectangle.scene().removeItem(
                self.mSelectionRectangle)
        if self.mOriginIndicator.scene():
            self.mOriginIndicator.scene().removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mRotateHandles.clear()
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mResizeHandles.clear()

    def tr(self, sourceText, disambiguation='', n=-1):
        return QCoreApplication.translate('ObjectSelectionTool', sourceText,
                                          disambiguation, n)

    def activate(self, scene):
        super().activate(scene)
        self.updateHandles()
        self.mapDocument().objectsChanged.connect(self.updateHandles)
        self.mapDocument().mapChanged.connect(self.updateHandles)
        scene.selectedObjectItemsChanged.connect(self.updateHandles)
        self.mapDocument().objectsRemoved.connect(self.objectsRemoved)
        if self.mOriginIndicator.scene() != scene:
            scene.addItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)

    def deactivate(self, scene):
        if self.mOriginIndicator.scene() == scene:
            scene.removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        self.mapDocument().objectsChanged.disconnect(self.updateHandles)
        self.mapDocument().mapChanged.disconnect(self.updateHandles)
        scene.selectedObjectItemsChanged.disconnect(self.updateHandles)
        super().deactivate(scene)

    def keyPressed(self, event):
        if (self.mAction != Action.NoAction):
            event.ignore()
            return

        moveBy = QPointF()
        x = event.key()
        if x == Qt.Key_Up:
            moveBy = QPointF(0, -1)
        elif x == Qt.Key_Down:
            moveBy = QPointF(0, 1)
        elif x == Qt.Key_Left:
            moveBy = QPointF(-1, 0)
        elif x == Qt.Key_Right:
            moveBy = QPointF(1, 0)
        else:
            super().keyPressed(event)
            return

        items = self.mapScene().selectedObjectItems()
        modifiers = event.modifiers()
        if (moveBy.isNull() or items.isEmpty()
                or (modifiers & Qt.ControlModifier)):
            event.ignore()
            return

        moveFast = modifiers & Qt.ShiftModifier
        snapToFineGrid = preferences.Preferences.instance().snapToFineGrid()
        if (moveFast):
            # TODO: This only makes sense for orthogonal maps
            moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth())
            moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight())
            if (snapToFineGrid):
                moveBy /= preferences.Preferences.instance().gridFine()

        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size()))
        i = 0
        for objectItem in items:
            object = objectItem.mapObject()
            oldPos = object.position()
            newPos = oldPos + moveBy
            undoStack.push(
                MoveMapObject(self.mapDocument(), object, newPos, oldPos))
            i += 1

        undoStack.endMacro()

    def mouseEntered(self):
        pass

    def mouseMoved(self, pos, modifiers):
        super().mouseMoved(pos, modifiers)

        # Update the hovered item (for mouse cursor)
        hoveredRotateHandle = None
        hoveredResizeHandle = None
        hoveredObjectItem = None

        view = self.mapScene().views()[0]
        if view:
            hoveredItem = self.mapScene().itemAt(pos, view.transform())
            hoveredRotateHandle = None
            hoveredResizeHandle = None
            tp = type(hoveredItem)
            if tp == RotateHandle:
                hoveredRotateHandle = hoveredItem
            elif tp == ResizeHandle:
                hoveredResizeHandle = hoveredItem

        if (not hoveredRotateHandle and not hoveredResizeHandle):
            hoveredObjectItem = self.topMostObjectItemAt(pos)

        self.mHoveredObjectItem = hoveredObjectItem

        if (self.mAction == Action.NoAction and self.mMousePressed):
            screenPos = QCursor.pos()
            dragDistance = (self.mScreenStart - screenPos).manhattanLength()
            if (dragDistance >= QApplication.startDragDistance()):
                hasSelection = not self.mapScene().selectedObjectItems(
                ).isEmpty()
                # Holding Alt forces moving current selection
                # Holding Shift forces selection rectangle
                if ((self.mClickedObjectItem or
                     (modifiers & Qt.AltModifier) and hasSelection)
                        and not (modifiers & Qt.ShiftModifier)):
                    self.startMoving(modifiers)
                elif (self.mClickedRotateHandle):
                    self.startRotating()
                elif (self.mClickedResizeHandle):
                    self.startResizing()
                else:
                    self.startSelecting()

        x = self.mAction
        if x == Action.Selecting:
            self.mSelectionRectangle.setRectangle(
                QRectF(self.mStart, pos).normalized())
        elif x == Action.Moving:
            self.updateMovingItems(pos, modifiers)
        elif x == Action.Rotating:
            self.updateRotatingItems(pos, modifiers)
        elif x == Action.Resizing:
            self.updateResizingItems(pos, modifiers)
        elif x == Action.NoAction:
            pass
        self.refreshCursor()

    def mousePressed(self, event):
        if (self.mAction != Action.NoAction
            ):  # Ignore additional presses during select/move
            return
        x = event.button()
        if x == Qt.LeftButton:
            self.mMousePressed = True
            self.mStart = event.scenePos()
            self.mScreenStart = event.screenPos()
            clickedRotateHandle = 0
            clickedResizeHandle = 0
            view = findView(event)
            if view:
                clickedItem = self.mapScene().itemAt(event.scenePos(),
                                                     view.transform())
                clickedRotateHandle = None
                clickedResizeHandle = None
                tp = type(clickedItem)
                if tp == RotateHandle:
                    clickedRotateHandle = clickedItem
                elif tp == ResizeHandle:
                    clickedResizeHandle = clickedItem
            self.mClickedRotateHandle = clickedRotateHandle
            self.mClickedResizeHandle = clickedResizeHandle
            if (not clickedRotateHandle and not clickedResizeHandle):
                self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart)
        else:
            super().mousePressed(event)

    def mouseReleased(self, event):
        if (event.button() != Qt.LeftButton):
            return
        x = self.mAction
        if x == Action.NoAction:
            if (not self.mClickedRotateHandle
                    and not self.mClickedResizeHandle):
                # Don't change selection as a result of clicking on a handle
                modifiers = event.modifiers()
                if (self.mClickedObjectItem):
                    selection = self.mapScene().selectedObjectItems()
                    if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)):
                        if (selection.contains(self.mClickedObjectItem)):
                            selection.remove(self.mClickedObjectItem)
                        else:
                            selection.insert(self.mClickedObjectItem)
                    elif (selection.contains(self.mClickedObjectItem)):
                        # Clicking one of the selected items changes the edit mode
                        if self.mMode == Mode.Resize:
                            _x = Mode.Rotate
                        else:
                            _x = Mode.Resize
                        self.setMode(_x)
                    else:
                        selection.clear()
                        selection.insert(self.mClickedObjectItem)
                        self.setMode(Mode.Resize)
                    self.mapScene().setSelectedObjectItems(selection)
                elif (not (modifiers & Qt.ShiftModifier)):
                    self.mapScene().setSelectedObjectItems(QSet())
        elif x == Action.Selecting:
            self.updateSelection(event.scenePos(), event.modifiers())
            self.mapScene().removeItem(self.mSelectionRectangle)
            self.mAction = Action.NoAction
        elif x == Action.Moving:
            self.finishMoving(event.scenePos())
        elif x == Action.Rotating:
            self.finishRotating(event.scenePos())
        elif x == Action.Resizing:
            self.finishResizing(event.scenePos())

        self.mMousePressed = False
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.refreshCursor()

    def modifiersChanged(self, modifiers):
        self.mModifiers = modifiers
        self.refreshCursor()

    def languageChanged(self):
        self.setName(self.tr("Select Objects"))
        self.setShortcut(QKeySequence(self.tr("S")))

    def updateHandles(self):
        if (self.mAction == Action.Moving or self.mAction == Action.Rotating
                or self.mAction == Action.Resizing):
            return
        objects = self.mapDocument().selectedObjects()
        showHandles = objects.size() > 0
        if (showHandles):
            renderer = self.mapDocument().renderer()
            boundingRect = objectBounds(
                objects.first(), renderer,
                objectTransform(objects.first(), renderer))
            for i in range(1, objects.size()):
                object = objects.at(i)
                boundingRect |= objectBounds(object, renderer,
                                             objectTransform(object, renderer))

            topLeft = boundingRect.topLeft()
            topRight = boundingRect.topRight()
            bottomLeft = boundingRect.bottomLeft()
            bottomRight = boundingRect.bottomRight()
            center = boundingRect.center()
            handleRotation = 0
            # If there is only one object selected, align to its orientation.
            if (objects.size() == 1):
                object = objects.first()
                handleRotation = object.rotation()
                if (resizeInPixelSpace(object)):
                    bounds = pixelBounds(object)
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(
                        renderer.pixelToScreenCoords_(bounds.topLeft()))
                    topRight = transform.map(
                        renderer.pixelToScreenCoords_(bounds.topRight()))
                    bottomLeft = transform.map(
                        renderer.pixelToScreenCoords_(bounds.bottomLeft()))
                    bottomRight = transform.map(
                        renderer.pixelToScreenCoords_(bounds.bottomRight()))
                    center = transform.map(
                        renderer.pixelToScreenCoords_(bounds.center()))
                    # Ugly hack to make handles appear nicer in this case
                    if (self.mapDocument().map().orientation() ==
                            Map.Orientation.Isometric):
                        handleRotation += 45
                else:
                    bounds = objectBounds(object, renderer, QTransform())
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(bounds.topLeft())
                    topRight = transform.map(bounds.topRight())
                    bottomLeft = transform.map(bounds.bottomLeft())
                    bottomRight = transform.map(bounds.bottomRight())
                    center = transform.map(bounds.center())

            self.mOriginIndicator.setPos(center)
            self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos(
                bottomLeft)
            self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos(
                bottomRight)
            top = (topLeft + topRight) / 2
            left = (topLeft + bottomLeft) / 2
            right = (topRight + bottomRight) / 2
            bottom = (bottomLeft + bottomRight) / 2
            self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top)
            self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin(
                bottom)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin(
                right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin(
                left)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin(
                top)
            self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mResizeHandles[
                AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight)
            self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mResizeHandles[
                AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft)
            self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos(
                bottomLeft)
            self.mResizeHandles[
                AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight)
            self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos(
                bottomRight)
            self.mResizeHandles[
                AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft)
            for i in range(AnchorPosition.CornerAnchorCount):
                self.mRotateHandles[i].setRotation(handleRotation)
            for i in range(AnchorPosition.AnchorCount):
                self.mResizeHandles[i].setRotation(handleRotation)

        self.updateHandleVisibility()

    def updateHandleVisibility(self):
        hasSelection = not self.mapDocument().selectedObjects().isEmpty()
        showHandles = hasSelection and (self.mAction == Action.NoAction
                                        or self.mAction == Action.Selecting)
        showOrigin = hasSelection and self.mAction != Action.Moving and (
            self.mMode == Mode.Rotate or self.mAction == Action.Resizing)
        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i].setVisible(showHandles
                                              and self.mMode == Mode.Rotate)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i].setVisible(showHandles
                                              and self.mMode == Mode.Resize)
        self.mOriginIndicator.setVisible(showOrigin)

    def objectsRemoved(self, objects):
        if (self.mAction != Action.Moving and self.mAction != Action.Rotating
                and self.mAction != Action.Resizing):
            return
        # Abort move/rotate/resize to avoid crashing...
        # TODO: This should really not be allowed to happen in the first place.
        # since it breaks the undo history, for example.
        for i in range(self.mMovingObjects.size() - 1, -1, -1):
            object = self.mMovingObjects[i]
            mapObject = object.item.mapObject()
            if objects.contains(mapObject):
                # Avoid referencing the removed object
                self.mMovingObjects.remove(i)
            else:
                mapObject.setPosition(object.oldPosition)
                mapObject.setSize(object.oldSize)
                mapObject.setPolygon(object.oldPolygon)
                mapObject.setRotation(object.oldRotation)

        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects)
        self.mMovingObjects.clear()

    def updateSelection(self, pos, modifiers):
        rect = QRectF(self.mStart, pos).normalized()
        # Make sure the rect has some contents, otherwise intersects returns False
        rect.setWidth(max(1.0, rect.width()))
        rect.setHeight(max(1.0, rect.height()))
        selectedItems = QSet()
        for item in self.mapScene().items(rect):
            if type(item) == MapObjectItem:
                selectedItems.insert(item)

        if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)):
            selectedItems |= self.mapScene().selectedObjectItems()
        else:
            self.setMode(Mode.Resize)
        self.mapScene().setSelectedObjectItems(selectedItems)

    def startSelecting(self):
        self.mAction = Action.Selecting
        self.mapScene().addItem(self.mSelectionRectangle)

    def startMoving(self, modifiers):
        # Move only the clicked item, if it was not part of the selection
        if (self.mClickedObjectItem and not (modifiers & Qt.AltModifier)):
            if (not self.mapScene().selectedObjectItems().contains(
                    self.mClickedObjectItem)):
                self.mapScene().setSelectedObjectItems(
                    QSet([self.mClickedObjectItem]))

        self.saveSelectionState()
        self.mAction = Action.Moving
        self.mAlignPosition = self.mMovingObjects[0].oldPosition
        for object in self.mMovingObjects:
            pos = object.oldPosition
            if (pos.x() < self.mAlignPosition.x()):
                self.mAlignPosition.setX(pos.x())
            if (pos.y() < self.mAlignPosition.y()):
                self.mAlignPosition.setY(pos.y())

        self.updateHandleVisibility()

    def updateMovingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()

        diff = self.snapToGrid(pos - self.mStart, modifiers)
        for object in self.mMovingObjects:
            newPixelPos = object.oldItemPosition + diff
            newPos = renderer.screenToPixelCoords_(newPixelPos)

            mapObject = object.item.mapObject()
            mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def finishMoving(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos):  # Move is a no-op
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(
            self.tr("Move %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            undoStack.push(
                MoveMapObject(self.mapDocument(), object.item.mapObject(),
                              object.oldPosition))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startRotating(self):
        self.mAction = Action.Rotating
        self.mOrigin = self.mOriginIndicator.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateRotatingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        startDiff = self.mOrigin - self.mStart
        currentDiff = self.mOrigin - pos
        startAngle = math.atan2(startDiff.y(), startDiff.x())
        currentAngle = math.atan2(currentDiff.y(), currentDiff.x())
        angleDiff = currentAngle - startAngle
        snap = 15 * M_PI / 180  # 15 degrees in radians
        if (modifiers & Qt.ControlModifier):
            angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()

            oldRelPos = object.oldItemPosition + offset - self.mOrigin
            sn = math.sin(angleDiff)
            cs = math.cos(angleDiff)
            newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn,
                                oldRelPos.x() * sn + oldRelPos.y() * cs)
            newPixelPos = self.mOrigin + newRelPos - offset
            newPos = renderer.screenToPixelCoords_(newPixelPos)
            newRotation = object.oldRotation + angleDiff * 180 / M_PI
            mapObject.setPosition(newPos)
            mapObject.setRotation(newRotation)

        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def finishRotating(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos):  # No rotation at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(
            self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(
                MoveMapObject(self.mapDocument(), mapObject,
                              object.oldPosition))
            undoStack.push(
                RotateMapObject(self.mapDocument(), mapObject,
                                object.oldRotation))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startResizing(self):
        self.mAction = Action.Resizing
        self.mOrigin = self.mOriginIndicator.pos()
        self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal(
        )
        self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical(
        )
        self.mStart = self.mClickedResizeHandle.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateResizingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        resizingOrigin = self.mClickedResizeHandle.resizingOrigin()
        if (modifiers & Qt.ShiftModifier):
            resizingOrigin = self.mOrigin
        self.mOriginIndicator.setPos(resizingOrigin)
        ## Alternative toggle snap modifier, since Control is taken by the preserve
        # aspect ratio option.
        ##
        snapHelper = SnapHelper(renderer)
        if (modifiers & Qt.AltModifier):
            snapHelper.toggleSnap()
        pixelPos = renderer.screenToPixelCoords_(pos)
        snapHelper.snap(pixelPos)
        snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos)
        diff = snappedScreenPos - resizingOrigin
        startDiff = self.mStart - resizingOrigin
        if (self.mMovingObjects.size() == 1):
            ## For single items the resizing is performed in object space in order
            # to handle different scaling on X and Y axis as well as to improve
            # handling of 0-sized objects.
            ##
            self.updateResizingSingleItem(resizingOrigin, snappedScreenPos,
                                          modifiers)
            return

        ## Calculate the scaling factor. Minimum is 1% to protect against making
        # everything 0-sized and non-recoverable (it's still possibly to run into
        # problems by repeatedly scaling down to 1%, but that's asking for it)
        ##
        scale = 0.0
        if (self.mResizingLimitHorizontal):
            scale = max(0.01, diff.y() / startDiff.y())
        elif (self.mResizingLimitVertical):
            scale = max(0.01, diff.x() / startDiff.x())
        else:
            scale = min(max(0.01,
                            diff.x() / startDiff.x()),
                        max(0.01,
                            diff.y() / startDiff.y()))

        if not math.isfinite(scale):
            scale = 1

        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()

            oldRelPos = object.oldItemPosition + offset - resizingOrigin
            scaledRelPos = QPointF(oldRelPos.x() * scale,
                                   oldRelPos.y() * scale)
            newScreenPos = resizingOrigin + scaledRelPos - offset
            newPos = renderer.screenToPixelCoords_(newScreenPos)
            origSize = object.oldSize
            newSize = QSizeF(origSize.width() * scale,
                             origSize.height() * scale)
            if (mapObject.polygon().isEmpty() == False):
                # For polygons, we have to scale in object space.
                rotation = object.item.rotation() * M_PI / -180
                sn = math.sin(rotation)
                cs = math.cos(rotation)
                oldPolygon = object.oldPolygon
                newPolygon = QPolygonF(oldPolygon.size())
                for n in range(oldPolygon.size()):
                    oldPoint = QPointF(oldPolygon[n])
                    rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn,
                                       oldPoint.y() * cs - oldPoint.x() * sn)
                    scaledPoint = QPointF(rotPoint.x() * scale,
                                          rotPoint.y() * scale)
                    newPoint = QPointF(
                        scaledPoint.x() * cs - scaledPoint.y() * sn,
                        scaledPoint.y() * cs + scaledPoint.x() * sn)
                    newPolygon[n] = newPoint

                mapObject.setPolygon(newPolygon)

            mapObject.setSize(newSize)
            mapObject.setPosition(newPos)

        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers):
        renderer = self.mapDocument().renderer()
        object = self.mMovingObjects.first()
        mapObject = object.item.mapObject()

        ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup
        # offset. We will un-apply it to these variables since the resize for
        # single items happens in local coordinate space.
        ##
        offset = mapObject.objectGroup().offset()

        ## These transformations undo and redo the object rotation, which is always
        # applied in screen space.
        ##
        unrotate = rotateAt(object.oldItemPosition, -object.oldRotation)
        rotate = rotateAt(object.oldItemPosition, object.oldRotation)
        origin = (resizingOrigin - offset) * unrotate
        pos = (screenPos - offset) * unrotate
        start = (self.mStart - offset) * unrotate
        oldPos = object.oldItemPosition
        ## In order for the resizing to work somewhat sanely in isometric mode,
        # the resizing is performed in pixel space except for tile objects, which
        # are not affected by isometric projection apart from their position.
        ##
        pixelSpace = resizeInPixelSpace(mapObject)
        preserveAspect = modifiers & Qt.ControlModifier
        if (pixelSpace):
            origin = renderer.screenToPixelCoords_(origin)
            pos = renderer.screenToPixelCoords_(pos)
            start = renderer.screenToPixelCoords_(start)
            oldPos = object.oldPosition

        newPos = oldPos
        newSize = object.oldSize
        ## In case one of the anchors was used as-is, the desired size can be
        # derived directly from the distance from the origin for rectangle
        # and ellipse objects. This allows scaling up a 0-sized object without
        # dealing with infinite scaling factor issues.
        #
        # For obvious reasons this can't work on polygons or polylines, nor when
        # preserving the aspect ratio.
        ##
        if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin
                and (mapObject.shape() == MapObject.Rectangle
                     or mapObject.shape() == MapObject.Ellipse)
                and not preserveAspect):
            newBounds = QRectF(newPos, newSize)
            newBounds = align(newBounds, mapObject.alignment())
            x = self.mClickedResizeHandle.anchorPosition()
            if x == AnchorPosition.LeftAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.BottomLeftAnchor:
                newBounds.setLeft(min(pos.x(), origin.x()))
            elif x == AnchorPosition.RightAnchor or x == AnchorPosition.TopRightAnchor or x == AnchorPosition.BottomRightAnchor:
                newBounds.setRight(max(pos.x(), origin.x()))
            else:
                # nothing to do on this axis
                pass

            x = self.mClickedResizeHandle.anchorPosition()
            if x == AnchorPosition.TopAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.TopRightAnchor:
                newBounds.setTop(min(pos.y(), origin.y()))
            elif x == AnchorPosition.BottomAnchor or x == AnchorPosition.BottomLeftAnchor or x == AnchorPosition.BottomRightAnchor:
                newBounds.setBottom(max(pos.y(), origin.y()))
            else:
                # nothing to do on this axis
                pass

            newBounds = unalign(newBounds, mapObject.alignment())
            newSize = newBounds.size()
            newPos = newBounds.topLeft()
        else:
            relPos = pos - origin
            startDiff = start - origin
            try:
                newx = relPos.x() / startDiff.x()
            except:
                newx = 0
            try:
                newy = relPos.y() / startDiff.y()
            except:
                newy = 0
            scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy))
            if not math.isfinite(scalingFactor.width()):
                scalingFactor.setWidth(1)
            if not math.isfinite(scalingFactor.height()):
                scalingFactor.setHeight(1)

            if (self.mResizingLimitHorizontal):
                if preserveAspect:
                    scalingFactor.setWidth(scalingFactor.height())
                else:
                    scalingFactor.setWidth(1)
            elif (self.mResizingLimitVertical):
                if preserveAspect:
                    scalingFactor.setHeight(scalingFactor.width())
                else:
                    scalingFactor.setHeight(1)
            elif (preserveAspect):
                scale = min(scalingFactor.width(), scalingFactor.height())
                scalingFactor.setWidth(scale)
                scalingFactor.setHeight(scale)

            oldRelPos = oldPos - origin
            newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(),
                                      oldRelPos.y() * scalingFactor.height())
            newSize.setWidth(newSize.width() * scalingFactor.width())
            newSize.setHeight(newSize.height() * scalingFactor.height())
            if (not object.oldPolygon.isEmpty()):
                newPolygon = QPolygonF(object.oldPolygon.size())
                for n in range(object.oldPolygon.size()):
                    point = object.oldPolygon[n]
                    newPolygon[n] = QPointF(point.x() * scalingFactor.width(),
                                            point.y() * scalingFactor.height())

                mapObject.setPolygon(newPolygon)

        if (pixelSpace):
            newPos = renderer.pixelToScreenCoords_(newPos)
        newPos = renderer.screenToPixelCoords_(newPos * rotate)
        mapObject.setSize(newSize)
        mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def finishResizing(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos):  # No scaling at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(
            self.tr("Resize %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(
                MoveMapObject(self.mapDocument(), mapObject,
                              object.oldPosition))
            undoStack.push(
                ResizeMapObject(self.mapDocument(), mapObject, object.oldSize))
            if (not object.oldPolygon.isEmpty()):
                undoStack.push(
                    ChangePolygon(self.mapDocument(), mapObject,
                                  object.oldPolygon))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def setMode(self, mode):
        if (self.mMode != mode):
            self.mMode = mode
            self.updateHandles()

    def saveSelectionState(self):
        self.mMovingObjects.clear()
        # Remember the initial state before moving, resizing or rotating
        for item in self.mapScene().selectedObjectItems():
            mapObject = item.mapObject()
            object = MovingObject()
            object.item = item
            object.oldItemPosition = item.pos()
            object.oldPosition = mapObject.position()
            object.oldSize = mapObject.size()
            object.oldPolygon = mapObject.polygon()
            object.oldRotation = mapObject.rotation()

            self.mMovingObjects.append(object)

    def refreshCursor(self):
        cursorShape = Qt.ArrowCursor

        if self.mAction == Action.NoAction:
            hasSelection = not self.mapScene().selectedObjectItems().isEmpty()

            if ((self.mHoveredObjectItem or
                 ((self.mModifiers & Qt.AltModifier) and hasSelection))
                    and not (self.mModifiers & Qt.ShiftModifier)):
                cursorShape = Qt.SizeAllCursor
        elif self.mAction == Action.Moving:
            cursorShape = Qt.SizeAllCursor

        if self.cursor.shape != cursorShape:
            self.setCursor(cursorShape)

    def snapToGrid(self, diff, modifiers):
        renderer = self.mapDocument().renderer()
        snapHelper = SnapHelper(renderer, modifiers)
        if (snapHelper.snaps()):
            alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition)
            newAlignScreenPos = alignScreenPos + diff
            newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos)
            snapHelper.snap(newAlignPixelPos)
            return renderer.pixelToScreenCoords_(
                newAlignPixelPos) - alignScreenPos

        return diff

    def changingObjects(self):
        changingObjects = QList()

        for movingObject in self.mMovingObjects:
            changingObjects.append(movingObject.item.mapObject())

        return changingObjects
Esempio n. 2
0
class ObjectTypesModel(QAbstractTableModel):
    ColorRole = Qt.UserRole

    def __init__(self, parent):
        super().__init__(parent)
        
        self.mObjectTypes = QVector()
        
    def setObjectTypes(self, objectTypes):
        self.beginResetModel()
        self.mObjectTypes = objectTypes
        self.endResetModel()

    def objectTypes(self):
        return self.mObjectTypes
        
    def rowCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = self.mObjectTypes.size()
        return _x

    def columnCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = 2
        return _x

    def headerData(self, section, orientation, role):
        if (orientation == Qt.Horizontal):
            if (role == Qt.DisplayRole):
                x = section
                if x==0:
                    return self.tr("Type")
                elif x==1:
                    return self.tr("Color")
            elif (role == Qt.TextAlignmentRole):
                return Qt.AlignLeft

        return QVariant()

    def data(self, index, role):
        # QComboBox requests data for an invalid index when the model is empty
        if (not index.isValid()):
            return QVariant()
        objectType = self.mObjectTypes.at(index.row())
        if (role == Qt.DisplayRole or role == Qt.EditRole):
            if (index.column() == 0):
                return objectType.name
        if (role == ObjectTypesModel.ColorRole and index.column() == 1):
            return objectType.color
        return QVariant()

    def setData(self, index, value, role):
        if (role == Qt.EditRole and index.column() == 0):
            self.mObjectTypes[index.row()].name = value.strip()
            self.dataChanged.emit(index, index)
            return True

        return False

    def flags(self, index):
        f = super().flags(index)
        if (index.column() == 0):
            f |= Qt.ItemIsEditable
        return f

    def setObjectTypeColor(self, objectIndex, color):
        self.mObjectTypes[objectIndex].color = color
        mi = self.index(objectIndex, 1)
        self.dataChanged.emit(mi, mi)

    def removeObjectTypes(self, indexes):
        rows = QVector()
        for index in indexes:
            rows.append(index.row())
        rows = sorted(rows)
        for i in range(len(rows) - 1, -1, -1):
            row = rows[i]
            self.beginRemoveRows(QModelIndex(), row, row)
            self.mObjectTypes.remove(row)
            self.endRemoveRows()

    def appendNewObjectType(self):
        self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(), self.mObjectTypes.size())
        self.mObjectTypes.append(ObjectType())
        self.endInsertRows()
Esempio n. 3
0
class MapScene(QGraphicsScene):
    selectedObjectItemsChanged = pyqtSignal()

    ##
    # Constructor.
    ##
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

        self.mLayerItems = QVector()
        self.mObjectItems = QMap()
        self.mObjectLineWidth = 0.0
        self.mSelectedObjectItems = QSet()
        self.mLastMousePos = QPointF()
        self.mShowTileObjectOutlines = False
        self.mHighlightCurrentLayer = False
        self.mGridVisible = False

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)

    ##
    # Destructor.
    ##
    def __del__(self):
        if QCoreApplication.instance():
            QCoreApplication.instance().removeEventFilter(self)

    ##
    # Returns the map document this scene is displaying.
    ##
    def mapDocument(self):
        return self.mMapDocument

    ##
    # Sets the map this scene displays.
    ##
    def setMapDocument(self, mapDocument):
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
            if (not self.mSelectedObjectItems.isEmpty()):
                self.mSelectedObjectItems.clear()
                self.selectedObjectItemsChanged.emit()

        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            renderer = self.mMapDocument.renderer()
            renderer.setObjectLineWidth(self.mObjectLineWidth)
            renderer.setFlag(RenderFlag.ShowTileObjectOutlines, self.mShowTileObjectOutlines)
            self.mMapDocument.mapChanged.connect(self.mapChanged)
            self.mMapDocument.regionChanged.connect(self.repaintRegion)
            self.mMapDocument.tileLayerDrawMarginsChanged.connect(self.tileLayerDrawMarginsChanged)
            self.mMapDocument.layerAdded.connect(self.layerAdded)
            self.mMapDocument.layerRemoved.connect(self.layerRemoved)
            self.mMapDocument.layerChanged.connect(self.layerChanged)
            self.mMapDocument.objectGroupChanged.connect(self.objectGroupChanged)
            self.mMapDocument.imageLayerChanged.connect(self.imageLayerChanged)
            self.mMapDocument.currentLayerIndexChanged.connect(self.currentLayerIndexChanged)
            self.mMapDocument.tilesetTileOffsetChanged.connect(self.tilesetTileOffsetChanged)
            self.mMapDocument.objectsInserted.connect(self.objectsInserted)
            self.mMapDocument.objectsRemoved.connect(self.objectsRemoved)
            self.mMapDocument.objectsChanged.connect(self.objectsChanged)
            self.mMapDocument.objectsIndexChanged.connect(self.objectsIndexChanged)
            self.mMapDocument.selectedObjectsChanged.connect(self.updateSelectedObjectItems)

        self.refreshScene()

    ##
    # Returns whether the tile grid is visible.
    ##
    def isGridVisible(self):
        return self.mGridVisible

    ##
    # Returns the set of selected map object items.
    ##
    def selectedObjectItems(self):
        return QSet(self.mSelectedObjectItems)

    ##
    # Sets the set of selected map object items. This translates to a call to
    # MapDocument.setSelectedObjects.
    ##
    def setSelectedObjectItems(self, items):
        # Inform the map document about the newly selected objects
        selectedObjects = QList()
        #selectedObjects.reserve(items.size())
        for item in items:
            selectedObjects.append(item.mapObject())
        self.mMapDocument.setSelectedObjects(selectedObjects)

    ##
    # Returns the MapObjectItem associated with the given \a mapObject.
    ##
    def itemForObject(self, object):
        return self.mObjectItems[object]

    ##
    # Enables the selected tool at this map scene.
    # Therefore it tells that tool, that this is the active map scene.
    ##
    def enableSelectedTool(self):
        if (not self.mSelectedTool or not self.mMapDocument):
            return
        self.mActiveTool = self.mSelectedTool
        self.mActiveTool.activate(self)
        self.mCurrentModifiers = QApplication.keyboardModifiers()
        if (self.mCurrentModifiers != Qt.NoModifier):
            self.mActiveTool.modifiersChanged(self.mCurrentModifiers)
        if (self.mUnderMouse):
            self.mActiveTool.mouseEntered()
            self.mActiveTool.mouseMoved(self.mLastMousePos, Qt.KeyboardModifiers())

    def disableSelectedTool(self):
        if (not self.mActiveTool):
            return
        if (self.mUnderMouse):
            self.mActiveTool.mouseLeft()
        self.mActiveTool.deactivate(self)
        self.mActiveTool = None

    ##
    # Sets the currently selected tool.
    ##
    def setSelectedTool(self, tool):
        self.mSelectedTool = tool

    ##
    # QGraphicsScene.drawForeground override that draws the tile grid.
    ##
    def drawForeground(self, painter, rect):
        if (not self.mMapDocument or not self.mGridVisible):
            return
            
        offset = QPointF()

        # Take into account the offset of the current layer
        layer = self.mMapDocument.currentLayer()
        if layer:
            offset = layer.offset()
            painter.translate(offset)
        
        prefs = preferences.Preferences.instance()
        self.mMapDocument.renderer().drawGrid(painter, rect.translated(-offset), prefs.gridColor())

    ##
    # Override for handling enter and leave events.
    ##
    def event(self, event):
        x = event.type()
        if x==QEvent.Enter:
            self.mUnderMouse = True
            if (self.mActiveTool):
                self.mActiveTool.mouseEntered()
        elif x==QEvent.Leave:
            self.mUnderMouse = False
            if (self.mActiveTool):
                self.mActiveTool.mouseLeft()
        else:
            pass

        return super().event(event)

    def keyPressEvent(self, event):
        if (self.mActiveTool):
            self.mActiveTool.keyPressed(event)
        if (not (self.mActiveTool and event.isAccepted())):
            super().keyPressEvent(event)

    def mouseMoveEvent(self, mouseEvent):
        self.mLastMousePos = mouseEvent.scenePos()
        if (not self.mMapDocument):
            return
        super().mouseMoveEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            self.mActiveTool.mouseMoved(mouseEvent.scenePos(), mouseEvent.modifiers())
            mouseEvent.accept()

    def mousePressEvent(self, mouseEvent):
        super().mousePressEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mousePressed(mouseEvent)

    def mouseReleaseEvent(self, mouseEvent):
        super().mouseReleaseEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mouseReleased(mouseEvent)

    ##
    # Override to ignore drag enter events.
    ##
    def dragEnterEvent(self, event):
        event.ignore()

    ##
    # Sets whether the tile grid is visible.
    ##
    def setGridVisible(self, visible):
        if (self.mGridVisible == visible):
            return
        self.mGridVisible = visible
        self.update()

    def setObjectLineWidth(self, lineWidth):
        if (self.mObjectLineWidth == lineWidth):
            return
        self.mObjectLineWidth = lineWidth
        if (self.mMapDocument):
            self.mMapDocument.renderer().setObjectLineWidth(lineWidth)
            # Changing the line width can change the size of the object items
            if (not self.mObjectItems.isEmpty()):
                for item in self.mObjectItems:
                    item[1].syncWithMapObject()
                self.update()

    def setShowTileObjectOutlines(self, enabled):
        if (self.mShowTileObjectOutlines == enabled):
            return
        self.mShowTileObjectOutlines = enabled
        if (self.mMapDocument):
            self.mMapDocument.renderer().setFlag(RenderFlag.ShowTileObjectOutlines, enabled)
            if (not self.mObjectItems.isEmpty()):
                self.update()

    ##
    # Sets whether the current layer should be highlighted.
    ##
    def setHighlightCurrentLayer(self, highlightCurrentLayer):
        if (self.mHighlightCurrentLayer == highlightCurrentLayer):
            return
        self.mHighlightCurrentLayer = highlightCurrentLayer
        self.updateCurrentLayerHighlight()

    ##
    # Refreshes the map scene.
    ##
    def refreshScene(self):
        self.mLayerItems.clear()
        self.mObjectItems.clear()
        self.removeItem(self.mDarkRectangle)
        self.clear()
        self.addItem(self.mDarkRectangle)
        if (not self.mMapDocument):
            self.setSceneRect(QRectF())
            return

        self.updateSceneRect()
        
        map = self.mMapDocument.map()
        self.mLayerItems.resize(map.layerCount())
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)
        layerIndex = 0
        for layer in map.layers():
            layerItem = self.createLayerItem(layer)
            layerItem.setZValue(layerIndex)
            self.addItem(layerItem)
            self.mLayerItems[layerIndex] = layerItem
            layerIndex += 1

        tileSelectionItem = TileSelectionItem(self.mMapDocument)
        tileSelectionItem.setZValue(10000 - 2)
        self.addItem(tileSelectionItem)
        self.mObjectSelectionItem = ObjectSelectionItem(self.mMapDocument)
        self.mObjectSelectionItem.setZValue(10000 - 1)
        self.addItem(self.mObjectSelectionItem)
        self.updateCurrentLayerHighlight()

    ##
    # Repaints the specified region. The region is in tile coordinates.
    ##
    def repaintRegion(self, region, layer):
        renderer = self.mMapDocument.renderer()
        margins = self.mMapDocument.map().drawMargins()
        for r in region.rects():
            boundingRect = QRectF(renderer.boundingRect(r))
            self.update(QRectF(renderer.boundingRect(r).adjusted(-margins.left(),
                                                      -margins.top(),
                                                      margins.right(),
                                                      margins.bottom())))
            boundingRect.translate(layer.offset())
            self.update(boundingRect)

    def currentLayerIndexChanged(self):
        self.updateCurrentLayerHighlight()
        # New layer may have a different offset, affecting the grid
        if self.mGridVisible:
            self.update()
        
    ##
    # Adapts the scene, layers and objects to new map size, orientation or
    # background color.
    ##
    def mapChanged(self):
        self.updateSceneRect()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()

        for item in self.mObjectItems.values():
            item.syncWithMapObject()
        map = self.mMapDocument.map()
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)

    def tilesetChanged(self, tileset):
        if (not self.mMapDocument):
            return
        if (contains(self.mMapDocument.map().tilesets(), tileset)):
            self.update()

    def tileLayerDrawMarginsChanged(self, tileLayer):
        index = self.mMapDocument.map().layers().indexOf(tileLayer)
        item = self.mLayerItems.at(index)
        item.syncWithTileLayer()

    def layerAdded(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.createLayerItem(layer)
        self.addItem(layerItem)
        self.mLayerItems.insert(index, layerItem)
        z = 0
        for item in self.mLayerItems:
            item.setZValue(z)
            z += 1

    def layerRemoved(self, index):
        self.mLayerItems.remove(index)

    ##
    # A layer has changed. This can mean that the layer visibility, opacity or
    # offset changed.
    ##
    def layerChanged(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.mLayerItems.at(index)
        layerItem.setVisible(layer.isVisible())
        multiplier = 1
        if (self.mHighlightCurrentLayer and self.mMapDocument.currentLayerIndex() < index):
            multiplier = opacityFactor
        layerItem.setOpacity(layer.opacity() * multiplier)
        layerItem.setPos(layer.offset())

        # Layer offset may have changed, affecting the scene rect and grid
        self.updateSceneRect()
        if self.mGridVisible:
            self.update()

    ##
    # When an object group has changed it may mean its color or drawing order
    # changed, which affects all its objects.
    ##
    def objectGroupChanged(self, objectGroup):
        self.objectsChanged(objectGroup.objects())
        self.objectsIndexChanged(objectGroup, 0, objectGroup.objectCount() - 1)

    ##
    # When an image layer has changed, it may change size and it may look
    # differently.
    ##
    def imageLayerChanged(self, imageLayer):
        index = self.mMapDocument.map().layers().indexOf(imageLayer)
        item = self.mLayerItems.at(index)
        item.syncWithImageLayer()
        item.update()

    ##
    # When the tile offset of a tileset has changed, it can affect the bounding
    # rect of all tile layers and tile objects. It also requires a full repaint.
    ##
    def tilesetTileOffsetChanged(self, tileset):
        self.update()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()
        for item in self.mObjectItems:
            cell = item.mapObject().cell()
            if (not cell.isEmpty() and cell.tile.tileset() == tileset):
                item.syncWithMapObject()

    ##
    # Inserts map object items for the given objects.
    ##
    def objectsInserted(self, objectGroup, first, last):
        ogItem = None
        # Find the object group item for the object group
        for item in self.mLayerItems:
            ogi = item
            if type(ogi)==ObjectGroupItem:
                if (ogi.objectGroup() == objectGroup):
                    ogItem = ogi
                    break

        drawOrder = objectGroup.drawOrder()
        for i in range(first, last+1):
            object = objectGroup.objectAt(i)
            item = MapObjectItem(object, self.mMapDocument, ogItem)
            if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                item.setZValue(item.y())
            else:
                item.setZValue(i)
            self.mObjectItems.insert(object, item)

    ##
    # Removes the map object items related to the given objects.
    ##
    def objectsRemoved(self, objects):
        for o in objects:
            i = self.mObjectItems.find(o)
            self.mSelectedObjectItems.remove(i)
            # python would not force delete QGraphicsItem
            self.removeItem(i)
            self.mObjectItems.erase(o)

    ##
    # Updates the map object items related to the given objects.
    ##
    def objectsChanged(self, objects):
        for object in objects:
            item = self.itemForObject(object)
            item.syncWithMapObject()

    ##
    # Updates the Z value of the objects when appropriate.
    ##
    def objectsIndexChanged(self, objectGroup, first, last):
        if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder):
            return
        for i in range(first, last+1):
            item = self.itemForObject(objectGroup.objectAt(i))
            item.setZValue(i)

    def updateSelectedObjectItems(self):
        objects = self.mMapDocument.selectedObjects()
        items = QSet()
        for object in objects:
            item = self.itemForObject(object)
            if item:
                items.insert(item)

        self.mSelectedObjectItems = items
        self.selectedObjectItemsChanged.emit()

    def syncAllObjectItems(self):
        for item in self.mObjectItems:
            item.syncWithMapObject()

    def createLayerItem(self, layer):
        layerItem = None
        tl = layer.asTileLayer()
        if tl:
            layerItem = TileLayerItem(tl, self.mMapDocument)
        else:
            og = layer.asObjectGroup()
            if og:
                drawOrder = og.drawOrder()
                ogItem = ObjectGroupItem(og)
                objectIndex = 0
                for object in og.objects():
                    item = MapObjectItem(object, self.mMapDocument, ogItem)
                    if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                        item.setZValue(item.y())
                    else:
                        item.setZValue(objectIndex)
                    self.mObjectItems.insert(object, item)
                    objectIndex += 1

                layerItem = ogItem
            else:
                il = layer.asImageLayer()
                if il:
                    layerItem = ImageLayerItem(il, self.mMapDocument)

        layerItem.setVisible(layer.isVisible())
        return layerItem

    def updateSceneRect(self):
        mapSize = self.mMapDocument.renderer().mapSize()
        sceneRect = QRectF(0, 0, mapSize.width(), mapSize.height())

        margins = self.mMapDocument.map().computeLayerOffsetMargins()
        sceneRect.adjust(-margins.left(),
                         -margins.top(),
                         margins.right(),
                         margins.bottom())

        self.setSceneRect(sceneRect)
        self.mDarkRectangle.setRect(sceneRect)
        
    def updateCurrentLayerHighlight(self):
        if (not self.mMapDocument):
            return
        currentLayerIndex = self.mMapDocument.currentLayerIndex()
        if (not self.mHighlightCurrentLayer or currentLayerIndex == -1):
            self.mDarkRectangle.setVisible(False)
            # Restore opacity for all layers
            for i in range(self.mLayerItems.size()):
                layer = self.mMapDocument.map().layerAt(i)
                self.mLayerItems.at(i).setOpacity(layer.opacity())

            return

        # Darken layers below the current layer
        self.mDarkRectangle.setZValue(currentLayerIndex - 0.5)
        self.mDarkRectangle.setVisible(True)
        # Set layers above the current layer to half opacity
        for i in range(1, self.mLayerItems.size()):
            layer = self.mMapDocument.map().layerAt(i)
            if currentLayerIndex < i:
                _x = opacityFactor
            else:
                _x = 1
            multiplier = _x
            self.mLayerItems.at(i).setOpacity(layer.opacity() * multiplier)

    def eventFilter(self, object, event):
        x = event.type()
        if x==QEvent.KeyPress or x==QEvent.KeyRelease:
                keyEvent = event
                newModifiers = keyEvent.modifiers()
                if (self.mActiveTool and newModifiers != self.mCurrentModifiers):
                    self.mActiveTool.modifiersChanged(newModifiers)
                    self.mCurrentModifiers = newModifiers
        else:
            pass

        return False
Esempio n. 4
0
class MapScene(QGraphicsScene):
    selectedObjectItemsChanged = pyqtSignal()

    ##
    # Constructor.
    ##
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

        self.mLayerItems = QVector()
        self.mObjectItems = QMap()
        self.mObjectLineWidth = 0.0
        self.mSelectedObjectItems = QSet()
        self.mLastMousePos = QPointF()
        self.mShowTileObjectOutlines = False
        self.mHighlightCurrentLayer = False
        self.mGridVisible = False

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(
            self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(
            self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)

    ##
    # Destructor.
    ##
    def __del__(self):
        if QCoreApplication.instance():
            QCoreApplication.instance().removeEventFilter(self)

    ##
    # Returns the map document this scene is displaying.
    ##
    def mapDocument(self):
        return self.mMapDocument

    ##
    # Sets the map this scene displays.
    ##
    def setMapDocument(self, mapDocument):
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
            if (not self.mSelectedObjectItems.isEmpty()):
                self.mSelectedObjectItems.clear()
                self.selectedObjectItemsChanged.emit()

        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            renderer = self.mMapDocument.renderer()
            renderer.setObjectLineWidth(self.mObjectLineWidth)
            renderer.setFlag(RenderFlag.ShowTileObjectOutlines,
                             self.mShowTileObjectOutlines)
            self.mMapDocument.mapChanged.connect(self.mapChanged)
            self.mMapDocument.regionChanged.connect(self.repaintRegion)
            self.mMapDocument.tileLayerDrawMarginsChanged.connect(
                self.tileLayerDrawMarginsChanged)
            self.mMapDocument.layerAdded.connect(self.layerAdded)
            self.mMapDocument.layerRemoved.connect(self.layerRemoved)
            self.mMapDocument.layerChanged.connect(self.layerChanged)
            self.mMapDocument.objectGroupChanged.connect(
                self.objectGroupChanged)
            self.mMapDocument.imageLayerChanged.connect(self.imageLayerChanged)
            self.mMapDocument.currentLayerIndexChanged.connect(
                self.currentLayerIndexChanged)
            self.mMapDocument.tilesetTileOffsetChanged.connect(
                self.tilesetTileOffsetChanged)
            self.mMapDocument.objectsInserted.connect(self.objectsInserted)
            self.mMapDocument.objectsRemoved.connect(self.objectsRemoved)
            self.mMapDocument.objectsChanged.connect(self.objectsChanged)
            self.mMapDocument.objectsIndexChanged.connect(
                self.objectsIndexChanged)
            self.mMapDocument.selectedObjectsChanged.connect(
                self.updateSelectedObjectItems)

        self.refreshScene()

    ##
    # Returns whether the tile grid is visible.
    ##
    def isGridVisible(self):
        return self.mGridVisible

    ##
    # Returns the set of selected map object items.
    ##
    def selectedObjectItems(self):
        return QSet(self.mSelectedObjectItems)

    ##
    # Sets the set of selected map object items. This translates to a call to
    # MapDocument.setSelectedObjects.
    ##
    def setSelectedObjectItems(self, items):
        # Inform the map document about the newly selected objects
        selectedObjects = QList()
        #selectedObjects.reserve(items.size())
        for item in items:
            selectedObjects.append(item.mapObject())
        self.mMapDocument.setSelectedObjects(selectedObjects)

    ##
    # Returns the MapObjectItem associated with the given \a mapObject.
    ##
    def itemForObject(self, object):
        return self.mObjectItems[object]

    ##
    # Enables the selected tool at this map scene.
    # Therefore it tells that tool, that this is the active map scene.
    ##
    def enableSelectedTool(self):
        if (not self.mSelectedTool or not self.mMapDocument):
            return
        self.mActiveTool = self.mSelectedTool
        self.mActiveTool.activate(self)
        self.mCurrentModifiers = QApplication.keyboardModifiers()
        if (self.mCurrentModifiers != Qt.NoModifier):
            self.mActiveTool.modifiersChanged(self.mCurrentModifiers)
        if (self.mUnderMouse):
            self.mActiveTool.mouseEntered()
            self.mActiveTool.mouseMoved(self.mLastMousePos,
                                        Qt.KeyboardModifiers())

    def disableSelectedTool(self):
        if (not self.mActiveTool):
            return
        if (self.mUnderMouse):
            self.mActiveTool.mouseLeft()
        self.mActiveTool.deactivate(self)
        self.mActiveTool = None

    ##
    # Sets the currently selected tool.
    ##
    def setSelectedTool(self, tool):
        self.mSelectedTool = tool

    ##
    # QGraphicsScene.drawForeground override that draws the tile grid.
    ##
    def drawForeground(self, painter, rect):
        if (not self.mMapDocument or not self.mGridVisible):
            return

        offset = QPointF()

        # Take into account the offset of the current layer
        layer = self.mMapDocument.currentLayer()
        if layer:
            offset = layer.offset()
            painter.translate(offset)

        prefs = preferences.Preferences.instance()
        self.mMapDocument.renderer().drawGrid(painter,
                                              rect.translated(-offset),
                                              prefs.gridColor())

    ##
    # Override for handling enter and leave events.
    ##
    def event(self, event):
        x = event.type()
        if x == QEvent.Enter:
            self.mUnderMouse = True
            if (self.mActiveTool):
                self.mActiveTool.mouseEntered()
        elif x == QEvent.Leave:
            self.mUnderMouse = False
            if (self.mActiveTool):
                self.mActiveTool.mouseLeft()
        else:
            pass

        return super().event(event)

    def keyPressEvent(self, event):
        if (self.mActiveTool):
            self.mActiveTool.keyPressed(event)
        if (not (self.mActiveTool and event.isAccepted())):
            super().keyPressEvent(event)

    def mouseMoveEvent(self, mouseEvent):
        self.mLastMousePos = mouseEvent.scenePos()
        if (not self.mMapDocument):
            return
        super().mouseMoveEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            self.mActiveTool.mouseMoved(mouseEvent.scenePos(),
                                        mouseEvent.modifiers())
            mouseEvent.accept()

    def mousePressEvent(self, mouseEvent):
        super().mousePressEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mousePressed(mouseEvent)

    def mouseReleaseEvent(self, mouseEvent):
        super().mouseReleaseEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mouseReleased(mouseEvent)

    ##
    # Override to ignore drag enter events.
    ##
    def dragEnterEvent(self, event):
        event.ignore()

    ##
    # Sets whether the tile grid is visible.
    ##
    def setGridVisible(self, visible):
        if (self.mGridVisible == visible):
            return
        self.mGridVisible = visible
        self.update()

    def setObjectLineWidth(self, lineWidth):
        if (self.mObjectLineWidth == lineWidth):
            return
        self.mObjectLineWidth = lineWidth
        if (self.mMapDocument):
            self.mMapDocument.renderer().setObjectLineWidth(lineWidth)
            # Changing the line width can change the size of the object items
            if (not self.mObjectItems.isEmpty()):
                for item in self.mObjectItems:
                    item[1].syncWithMapObject()
                self.update()

    def setShowTileObjectOutlines(self, enabled):
        if (self.mShowTileObjectOutlines == enabled):
            return
        self.mShowTileObjectOutlines = enabled
        if (self.mMapDocument):
            self.mMapDocument.renderer().setFlag(
                RenderFlag.ShowTileObjectOutlines, enabled)
            if (not self.mObjectItems.isEmpty()):
                self.update()

    ##
    # Sets whether the current layer should be highlighted.
    ##
    def setHighlightCurrentLayer(self, highlightCurrentLayer):
        if (self.mHighlightCurrentLayer == highlightCurrentLayer):
            return
        self.mHighlightCurrentLayer = highlightCurrentLayer
        self.updateCurrentLayerHighlight()

    ##
    # Refreshes the map scene.
    ##
    def refreshScene(self):
        self.mLayerItems.clear()
        self.mObjectItems.clear()
        self.removeItem(self.mDarkRectangle)
        self.clear()
        self.addItem(self.mDarkRectangle)
        if (not self.mMapDocument):
            self.setSceneRect(QRectF())
            return

        self.updateSceneRect()

        map = self.mMapDocument.map()
        self.mLayerItems.resize(map.layerCount())
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)
        layerIndex = 0
        for layer in map.layers():
            layerItem = self.createLayerItem(layer)
            layerItem.setZValue(layerIndex)
            self.addItem(layerItem)
            self.mLayerItems[layerIndex] = layerItem
            layerIndex += 1

        tileSelectionItem = TileSelectionItem(self.mMapDocument)
        tileSelectionItem.setZValue(10000 - 2)
        self.addItem(tileSelectionItem)
        self.mObjectSelectionItem = ObjectSelectionItem(self.mMapDocument)
        self.mObjectSelectionItem.setZValue(10000 - 1)
        self.addItem(self.mObjectSelectionItem)
        self.updateCurrentLayerHighlight()

    ##
    # Repaints the specified region. The region is in tile coordinates.
    ##
    def repaintRegion(self, region, layer):
        renderer = self.mMapDocument.renderer()
        margins = self.mMapDocument.map().drawMargins()
        for r in region.rects():
            boundingRect = QRectF(renderer.boundingRect(r))
            self.update(
                QRectF(
                    renderer.boundingRect(r).adjusted(-margins.left(),
                                                      -margins.top(),
                                                      margins.right(),
                                                      margins.bottom())))
            boundingRect.translate(layer.offset())
            self.update(boundingRect)

    def currentLayerIndexChanged(self):
        self.updateCurrentLayerHighlight()
        # New layer may have a different offset, affecting the grid
        if self.mGridVisible:
            self.update()

    ##
    # Adapts the scene, layers and objects to new map size, orientation or
    # background color.
    ##
    def mapChanged(self):
        self.updateSceneRect()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()

        for item in self.mObjectItems.values():
            item.syncWithMapObject()
        map = self.mMapDocument.map()
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)

    def tilesetChanged(self, tileset):
        if (not self.mMapDocument):
            return
        if (contains(self.mMapDocument.map().tilesets(), tileset)):
            self.update()

    def tileLayerDrawMarginsChanged(self, tileLayer):
        index = self.mMapDocument.map().layers().indexOf(tileLayer)
        item = self.mLayerItems.at(index)
        item.syncWithTileLayer()

    def layerAdded(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.createLayerItem(layer)
        self.addItem(layerItem)
        self.mLayerItems.insert(index, layerItem)
        z = 0
        for item in self.mLayerItems:
            item.setZValue(z)
            z += 1

    def layerRemoved(self, index):
        self.mLayerItems.remove(index)

    ##
    # A layer has changed. This can mean that the layer visibility, opacity or
    # offset changed.
    ##
    def layerChanged(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.mLayerItems.at(index)
        layerItem.setVisible(layer.isVisible())
        multiplier = 1
        if (self.mHighlightCurrentLayer
                and self.mMapDocument.currentLayerIndex() < index):
            multiplier = opacityFactor
        layerItem.setOpacity(layer.opacity() * multiplier)
        layerItem.setPos(layer.offset())

        # Layer offset may have changed, affecting the scene rect and grid
        self.updateSceneRect()
        if self.mGridVisible:
            self.update()

    ##
    # When an object group has changed it may mean its color or drawing order
    # changed, which affects all its objects.
    ##
    def objectGroupChanged(self, objectGroup):
        self.objectsChanged(objectGroup.objects())
        self.objectsIndexChanged(objectGroup, 0, objectGroup.objectCount() - 1)

    ##
    # When an image layer has changed, it may change size and it may look
    # differently.
    ##
    def imageLayerChanged(self, imageLayer):
        index = self.mMapDocument.map().layers().indexOf(imageLayer)
        item = self.mLayerItems.at(index)
        item.syncWithImageLayer()
        item.update()

    ##
    # When the tile offset of a tileset has changed, it can affect the bounding
    # rect of all tile layers and tile objects. It also requires a full repaint.
    ##
    def tilesetTileOffsetChanged(self, tileset):
        self.update()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()
        for item in self.mObjectItems:
            cell = item.mapObject().cell()
            if (not cell.isEmpty() and cell.tile.tileset() == tileset):
                item.syncWithMapObject()

    ##
    # Inserts map object items for the given objects.
    ##
    def objectsInserted(self, objectGroup, first, last):
        ogItem = None
        # Find the object group item for the object group
        for item in self.mLayerItems:
            ogi = item
            if type(ogi) == ObjectGroupItem:
                if (ogi.objectGroup() == objectGroup):
                    ogItem = ogi
                    break

        drawOrder = objectGroup.drawOrder()
        for i in range(first, last + 1):
            object = objectGroup.objectAt(i)
            item = MapObjectItem(object, self.mMapDocument, ogItem)
            if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                item.setZValue(item.y())
            else:
                item.setZValue(i)
            self.mObjectItems.insert(object, item)

    ##
    # Removes the map object items related to the given objects.
    ##
    def objectsRemoved(self, objects):
        for o in objects:
            i = self.mObjectItems.find(o)
            self.mSelectedObjectItems.remove(i)
            # python would not force delete QGraphicsItem
            self.removeItem(i)
            self.mObjectItems.erase(o)

    ##
    # Updates the map object items related to the given objects.
    ##
    def objectsChanged(self, objects):
        for object in objects:
            item = self.itemForObject(object)
            item.syncWithMapObject()

    ##
    # Updates the Z value of the objects when appropriate.
    ##
    def objectsIndexChanged(self, objectGroup, first, last):
        if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder):
            return
        for i in range(first, last + 1):
            item = self.itemForObject(objectGroup.objectAt(i))
            item.setZValue(i)

    def updateSelectedObjectItems(self):
        objects = self.mMapDocument.selectedObjects()
        items = QSet()
        for object in objects:
            item = self.itemForObject(object)
            if item:
                items.insert(item)

        self.mSelectedObjectItems = items
        self.selectedObjectItemsChanged.emit()

    def syncAllObjectItems(self):
        for item in self.mObjectItems:
            item.syncWithMapObject()

    def createLayerItem(self, layer):
        layerItem = None
        tl = layer.asTileLayer()
        if tl:
            layerItem = TileLayerItem(tl, self.mMapDocument)
        else:
            og = layer.asObjectGroup()
            if og:
                drawOrder = og.drawOrder()
                ogItem = ObjectGroupItem(og)
                objectIndex = 0
                for object in og.objects():
                    item = MapObjectItem(object, self.mMapDocument, ogItem)
                    if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                        item.setZValue(item.y())
                    else:
                        item.setZValue(objectIndex)
                    self.mObjectItems.insert(object, item)
                    objectIndex += 1

                layerItem = ogItem
            else:
                il = layer.asImageLayer()
                if il:
                    layerItem = ImageLayerItem(il, self.mMapDocument)

        layerItem.setVisible(layer.isVisible())
        return layerItem

    def updateSceneRect(self):
        mapSize = self.mMapDocument.renderer().mapSize()
        sceneRect = QRectF(0, 0, mapSize.width(), mapSize.height())

        margins = self.mMapDocument.map().computeLayerOffsetMargins()
        sceneRect.adjust(-margins.left(), -margins.top(), margins.right(),
                         margins.bottom())

        self.setSceneRect(sceneRect)
        self.mDarkRectangle.setRect(sceneRect)

    def updateCurrentLayerHighlight(self):
        if (not self.mMapDocument):
            return
        currentLayerIndex = self.mMapDocument.currentLayerIndex()
        if (not self.mHighlightCurrentLayer or currentLayerIndex == -1):
            self.mDarkRectangle.setVisible(False)
            # Restore opacity for all layers
            for i in range(self.mLayerItems.size()):
                layer = self.mMapDocument.map().layerAt(i)
                self.mLayerItems.at(i).setOpacity(layer.opacity())

            return

        # Darken layers below the current layer
        self.mDarkRectangle.setZValue(currentLayerIndex - 0.5)
        self.mDarkRectangle.setVisible(True)
        # Set layers above the current layer to half opacity
        for i in range(1, self.mLayerItems.size()):
            layer = self.mMapDocument.map().layerAt(i)
            if currentLayerIndex < i:
                _x = opacityFactor
            else:
                _x = 1
            multiplier = _x
            self.mLayerItems.at(i).setOpacity(layer.opacity() * multiplier)

    def eventFilter(self, object, event):
        x = event.type()
        if x == QEvent.KeyPress or x == QEvent.KeyRelease:
            keyEvent = event
            newModifiers = keyEvent.modifiers()
            if (self.mActiveTool and newModifiers != self.mCurrentModifiers):
                self.mActiveTool.modifiersChanged(newModifiers)
                self.mCurrentModifiers = newModifiers
        else:
            pass

        return False
Esempio n. 5
0
class ObjectSelectionTool(AbstractObjectTool):
    def __init__(self, parent = None):
        super().__init__(self.tr("Select Objects"),
              QIcon(":images/22x22/tool-select-objects.png"),
              QKeySequence(self.tr("S")),
              parent)
        self.mSelectionRectangle = SelectionRectangle()
        self.mOriginIndicator = OriginIndicator()
        self.mMousePressed = False
        self.mHoveredObjectItem = None
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.mResizingLimitHorizontal = False
        self.mResizingLimitVertical = False
        self.mMode = Mode.Resize
        self.mAction = Action.NoAction
        self.mRotateHandles = [0, 0, 0, 0]
        self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0]
        self.mAlignPosition = QPointF()
        self.mMovingObjects = QVector()
        self.mScreenStart = QPoint()
        self.mStart = QPointF()
        self.mModifiers = 0
        self.mOrigin = QPointF()

        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i] = RotateHandle(i)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i] = ResizeHandle(i)

    def __del__(self):
        if self.mSelectionRectangle.scene():
            self.mSelectionRectangle.scene().removeItem(self.mSelectionRectangle)
        if self.mOriginIndicator.scene():
            self.mOriginIndicator.scene().removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mRotateHandles.clear()
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mResizeHandles.clear()

    def tr(self, sourceText, disambiguation = '', n = -1):
        return QCoreApplication.translate('ObjectSelectionTool', sourceText, disambiguation, n)

    def activate(self, scene):
        super().activate(scene)
        self.updateHandles()
        self.mapDocument().objectsChanged.connect(self.updateHandles)
        self.mapDocument().mapChanged.connect(self.updateHandles)
        scene.selectedObjectItemsChanged.connect(self.updateHandles)
        self.mapDocument().objectsRemoved.connect(self.objectsRemoved)
        if self.mOriginIndicator.scene() != scene:
            scene.addItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)

    def deactivate(self, scene):
        if self.mOriginIndicator.scene() == scene:
            scene.removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        self.mapDocument().objectsChanged.disconnect(self.updateHandles)
        self.mapDocument().mapChanged.disconnect(self.updateHandles)
        scene.selectedObjectItemsChanged.disconnect(self.updateHandles)
        super().deactivate(scene)

    def keyPressed(self, event):
        if (self.mAction != Action.NoAction):
            event.ignore()
            return

        moveBy = QPointF()
        x = event.key()
        if x==Qt.Key_Up:
            moveBy = QPointF(0, -1)
        elif x==Qt.Key_Down:
            moveBy = QPointF(0, 1)
        elif x==Qt.Key_Left:
            moveBy = QPointF(-1, 0)
        elif x==Qt.Key_Right:
            moveBy = QPointF(1, 0)
        else:
            super().keyPressed(event)
            return

        items = self.mapScene().selectedObjectItems()
        modifiers = event.modifiers()
        if (moveBy.isNull() or items.isEmpty() or (modifiers & Qt.ControlModifier)):
            event.ignore()
            return

        moveFast = modifiers & Qt.ShiftModifier
        snapToFineGrid = preferences.Preferences.instance().snapToFineGrid()
        if (moveFast):
            # TODO: This only makes sense for orthogonal maps
            moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth())
            moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight())
            if (snapToFineGrid):
                moveBy /= preferences.Preferences.instance().gridFine()

        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size()))
        i = 0
        for objectItem in items:
            object = objectItem.mapObject()
            oldPos = object.position()
            newPos = oldPos + moveBy
            undoStack.push(MoveMapObject(self.mapDocument(), object, newPos, oldPos))
            i += 1

        undoStack.endMacro()

    def mouseEntered(self):
        pass
        
    def mouseMoved(self, pos, modifiers):
        super().mouseMoved(pos, modifiers)
        
        # Update the hovered item (for mouse cursor)
        hoveredRotateHandle = None
        hoveredResizeHandle = None
        hoveredObjectItem = None
        
        view = self.mapScene().views()[0]
        if view:
            hoveredItem = self.mapScene().itemAt(pos,view.transform())
            hoveredRotateHandle = None
            hoveredResizeHandle = None
            tp = type(hoveredItem)
            if tp==RotateHandle:
                hoveredRotateHandle = hoveredItem
            elif tp==ResizeHandle:
                hoveredResizeHandle = hoveredItem

        if (not hoveredRotateHandle and not hoveredResizeHandle):
            hoveredObjectItem = self.topMostObjectItemAt(pos)

        self.mHoveredObjectItem = hoveredObjectItem
        
        if (self.mAction == Action.NoAction and self.mMousePressed):
            screenPos = QCursor.pos()
            dragDistance = (self.mScreenStart - screenPos).manhattanLength()
            if (dragDistance >= QApplication.startDragDistance()):
                hasSelection = not self.mapScene().selectedObjectItems().isEmpty()
                # Holding Alt forces moving current selection
                # Holding Shift forces selection rectangle
                if ((self.mClickedObjectItem or (modifiers & Qt.AltModifier) and hasSelection) and not (modifiers & Qt.ShiftModifier)):
                    self.startMoving(modifiers)
                elif (self.mClickedRotateHandle):
                    self.startRotating()
                elif (self.mClickedResizeHandle):
                    self.startResizing()
                else:
                    self.startSelecting()

        x = self.mAction
        if x==Action.Selecting:
            self.mSelectionRectangle.setRectangle(QRectF(self.mStart, pos).normalized())
        elif x==Action.Moving:
            self.updateMovingItems(pos, modifiers)
        elif x==Action.Rotating:
            self.updateRotatingItems(pos, modifiers)
        elif x==Action.Resizing:
            self.updateResizingItems(pos, modifiers)
        elif x==Action.NoAction:
            pass
        self.refreshCursor()

    def mousePressed(self, event):
        if (self.mAction != Action.NoAction): # Ignore additional presses during select/move
            return
        x = event.button()
        if x==Qt.LeftButton:
            self.mMousePressed = True
            self.mStart = event.scenePos()
            self.mScreenStart = event.screenPos()
            clickedRotateHandle = 0
            clickedResizeHandle = 0
            view = findView(event)
            if view:
                clickedItem = self.mapScene().itemAt(event.scenePos(), view.transform())
                clickedRotateHandle = None
                clickedResizeHandle = None
                tp = type(clickedItem)
                if tp==RotateHandle:
                    clickedRotateHandle = clickedItem
                elif tp==ResizeHandle:
                    clickedResizeHandle = clickedItem
            self.mClickedRotateHandle = clickedRotateHandle
            self.mClickedResizeHandle = clickedResizeHandle
            if (not clickedRotateHandle and not clickedResizeHandle):
                self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart)
        else:
            super().mousePressed(event)

    def mouseReleased(self, event):
        if (event.button() != Qt.LeftButton):
            return
        x = self.mAction
        if x==Action.NoAction:
            if (not self.mClickedRotateHandle and not self.mClickedResizeHandle):
                # Don't change selection as a result of clicking on a handle
                modifiers = event.modifiers()
                if (self.mClickedObjectItem):
                    selection = self.mapScene().selectedObjectItems()
                    if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)):
                        if (selection.contains(self.mClickedObjectItem)):
                            selection.remove(self.mClickedObjectItem)
                        else:
                            selection.insert(self.mClickedObjectItem)
                    elif (selection.contains(self.mClickedObjectItem)):
                        # Clicking one of the selected items changes the edit mode
                        if self.mMode == Mode.Resize:
                            _x = Mode.Rotate
                        else:
                            _x = Mode.Resize
                        self.setMode(_x)
                    else:
                        selection.clear()
                        selection.insert(self.mClickedObjectItem)
                        self.setMode(Mode.Resize)
                    self.mapScene().setSelectedObjectItems(selection)
                elif (not (modifiers & Qt.ShiftModifier)):
                    self.mapScene().setSelectedObjectItems(QSet())
        elif x==Action.Selecting:
            self.updateSelection(event.scenePos(), event.modifiers())
            self.mapScene().removeItem(self.mSelectionRectangle)
            self.mAction = Action.NoAction
        elif x==Action.Moving:
            self.finishMoving(event.scenePos())
        elif x==Action.Rotating:
            self.finishRotating(event.scenePos())
        elif x==Action.Resizing:
            self.finishResizing(event.scenePos())

        self.mMousePressed = False
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.refreshCursor()
        
    def modifiersChanged(self, modifiers):
        self.mModifiers = modifiers
        self.refreshCursor()

    def languageChanged(self):
        self.setName(self.tr("Select Objects"))
        self.setShortcut(QKeySequence(self.tr("S")))

    def updateHandles(self):
        if (self.mAction == Action.Moving or self.mAction == Action.Rotating or self.mAction == Action.Resizing):
            return
        objects = self.mapDocument().selectedObjects()
        showHandles = objects.size() > 0
        if (showHandles):
            renderer = self.mapDocument().renderer()
            boundingRect = objectBounds(objects.first(), renderer, objectTransform(objects.first(), renderer))
            for i in range(1, objects.size()):
                object = objects.at(i)
                boundingRect |= objectBounds(object, renderer, objectTransform(object, renderer))

            topLeft = boundingRect.topLeft()
            topRight = boundingRect.topRight()
            bottomLeft = boundingRect.bottomLeft()
            bottomRight = boundingRect.bottomRight()
            center = boundingRect.center()
            handleRotation = 0
            # If there is only one object selected, align to its orientation.
            if (objects.size() == 1):
                object = objects.first()
                handleRotation = object.rotation()
                if (resizeInPixelSpace(object)):
                    bounds = pixelBounds(object)
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(renderer.pixelToScreenCoords_(bounds.topLeft()))
                    topRight = transform.map(renderer.pixelToScreenCoords_(bounds.topRight()))
                    bottomLeft = transform.map(renderer.pixelToScreenCoords_(bounds.bottomLeft()))
                    bottomRight = transform.map(renderer.pixelToScreenCoords_(bounds.bottomRight()))
                    center = transform.map(renderer.pixelToScreenCoords_(bounds.center()))
                    # Ugly hack to make handles appear nicer in this case
                    if (self.mapDocument().map().orientation() == Map.Orientation.Isometric):
                        handleRotation += 45
                else:
                    bounds = objectBounds(object, renderer, QTransform())
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(bounds.topLeft())
                    topRight = transform.map(bounds.topRight())
                    bottomLeft = transform.map(bounds.bottomLeft())
                    bottomRight = transform.map(bounds.bottomRight())
                    center = transform.map(bounds.center())

            self.mOriginIndicator.setPos(center)
            self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos(bottomLeft)
            self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos(bottomRight)
            top = (topLeft + topRight) / 2
            left = (topLeft + bottomLeft) / 2
            right = (topRight + bottomRight) / 2
            bottom = (bottomLeft + bottomRight) / 2
            self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top)
            self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin(bottom)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin(right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin(left)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin(top)
            self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mResizeHandles[AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight)
            self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mResizeHandles[AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft)
            self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos(bottomLeft)
            self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight)
            self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos(bottomRight)
            self.mResizeHandles[AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft)
            for i in range(AnchorPosition.CornerAnchorCount):
                self.mRotateHandles[i].setRotation(handleRotation)
            for i in range(AnchorPosition.AnchorCount):
                self.mResizeHandles[i].setRotation(handleRotation)

        self.updateHandleVisibility()

    def updateHandleVisibility(self):
        hasSelection = not self.mapDocument().selectedObjects().isEmpty()
        showHandles = hasSelection and (self.mAction == Action.NoAction or self.mAction == Action.Selecting)
        showOrigin = hasSelection and self.mAction != Action.Moving and (self.mMode == Mode.Rotate or self.mAction == Action.Resizing)
        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i].setVisible(showHandles and self.mMode == Mode.Rotate)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i].setVisible(showHandles and self.mMode == Mode.Resize)
        self.mOriginIndicator.setVisible(showOrigin)

    def objectsRemoved(self, objects):
        if (self.mAction != Action.Moving and self.mAction != Action.Rotating and self.mAction != Action.Resizing):
            return
        # Abort move/rotate/resize to avoid crashing...
        # TODO: This should really not be allowed to happen in the first place.
        # since it breaks the undo history, for example.
        for i in range(self.mMovingObjects.size() - 1, -1, -1):
            object = self.mMovingObjects[i]
            mapObject = object.item.mapObject()
            if objects.contains(mapObject):
                # Avoid referencing the removed object
                self.mMovingObjects.remove(i)
            else:
                mapObject.setPosition(object.oldPosition)
                mapObject.setSize(object.oldSize)
                mapObject.setPolygon(object.oldPolygon)
                mapObject.setRotation(object.oldRotation)
        
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects)
        self.mMovingObjects.clear()

    def updateSelection(self, pos, modifiers):
        rect = QRectF(self.mStart, pos).normalized()
        # Make sure the rect has some contents, otherwise intersects returns False
        rect.setWidth(max(1.0, rect.width()))
        rect.setHeight(max(1.0, rect.height()))
        selectedItems = QSet()
        for item in self.mapScene().items(rect):
            if type(item) == MapObjectItem:
                selectedItems.insert(item)

        if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)):
            selectedItems |= self.mapScene().selectedObjectItems()
        else:
            self.setMode(Mode.Resize)
        self.mapScene().setSelectedObjectItems(selectedItems)

    def startSelecting(self):
        self.mAction = Action.Selecting
        self.mapScene().addItem(self.mSelectionRectangle)

    def startMoving(self, modifiers):
        # Move only the clicked item, if it was not part of the selection
        if (self.mClickedObjectItem and not (modifiers & Qt.AltModifier)):
            if (not self.mapScene().selectedObjectItems().contains(self.mClickedObjectItem)):
                self.mapScene().setSelectedObjectItems(QSet([self.mClickedObjectItem]))

        self.saveSelectionState()
        self.mAction = Action.Moving
        self.mAlignPosition = self.mMovingObjects[0].oldPosition
        for object in self.mMovingObjects:
            pos = object.oldPosition
            if (pos.x() < self.mAlignPosition.x()):
                self.mAlignPosition.setX(pos.x())
            if (pos.y() < self.mAlignPosition.y()):
                self.mAlignPosition.setY(pos.y())

        self.updateHandleVisibility()

    def updateMovingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()

        diff = self.snapToGrid(pos-self.mStart, modifiers)
        for object in self.mMovingObjects:
            newPixelPos = object.oldItemPosition + diff
            newPos = renderer.screenToPixelCoords_(newPixelPos)

            mapObject = object.item.mapObject()
            mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def finishMoving(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos): # Move is a no-op
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            undoStack.push(MoveMapObject(self.mapDocument(), object.item.mapObject(), object.oldPosition))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startRotating(self):
        self.mAction = Action.Rotating
        self.mOrigin = self.mOriginIndicator.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateRotatingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        startDiff = self.mOrigin - self.mStart
        currentDiff = self.mOrigin - pos
        startAngle = math.atan2(startDiff.y(), startDiff.x())
        currentAngle = math.atan2(currentDiff.y(), currentDiff.x())
        angleDiff = currentAngle - startAngle
        snap = 15 * M_PI / 180 # 15 degrees in radians
        if (modifiers & Qt.ControlModifier):
            angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()
        
            oldRelPos = object.oldItemPosition + offset - self.mOrigin
            sn = math.sin(angleDiff)
            cs = math.cos(angleDiff)
            newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn, oldRelPos.x() * sn + oldRelPos.y() * cs)
            newPixelPos = self.mOrigin + newRelPos - offset
            newPos = renderer.screenToPixelCoords_(newPixelPos)
            newRotation = object.oldRotation + angleDiff * 180 / M_PI
            mapObject.setPosition(newPos)
            mapObject.setRotation(newRotation)
        
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def finishRotating(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos): # No rotation at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(MoveMapObject(self.mapDocument(), mapObject, object.oldPosition))
            undoStack.push(RotateMapObject(self.mapDocument(), mapObject, object.oldRotation))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startResizing(self):
        self.mAction = Action.Resizing
        self.mOrigin = self.mOriginIndicator.pos()
        self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal()
        self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical()
        self.mStart = self.mClickedResizeHandle.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateResizingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        resizingOrigin = self.mClickedResizeHandle.resizingOrigin()
        if (modifiers & Qt.ShiftModifier):
            resizingOrigin = self.mOrigin
        self.mOriginIndicator.setPos(resizingOrigin)
        ## Alternative toggle snap modifier, since Control is taken by the preserve
        # aspect ratio option.
        ##
        snapHelper = SnapHelper(renderer)
        if (modifiers & Qt.AltModifier):
            snapHelper.toggleSnap()
        pixelPos = renderer.screenToPixelCoords_(pos)
        snapHelper.snap(pixelPos)
        snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos)
        diff = snappedScreenPos - resizingOrigin
        startDiff = self.mStart - resizingOrigin
        if (self.mMovingObjects.size() == 1):
            ## For single items the resizing is performed in object space in order
            # to handle different scaling on X and Y axis as well as to improve
            # handling of 0-sized objects.
            ##
            self.updateResizingSingleItem(resizingOrigin, snappedScreenPos, modifiers)
            return

        ## Calculate the scaling factor. Minimum is 1% to protect against making
        # everything 0-sized and non-recoverable (it's still possibly to run into
        # problems by repeatedly scaling down to 1%, but that's asking for it)
        ##
        scale = 0.0
        if (self.mResizingLimitHorizontal):
            scale = max(0.01, diff.y() / startDiff.y())
        elif (self.mResizingLimitVertical):
            scale = max(0.01, diff.x() / startDiff.x())
        else:
            scale = min(max(0.01, diff.x() / startDiff.x()),
                         max(0.01, diff.y() / startDiff.y()))

        if not math.isfinite(scale):
            scale = 1
        
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()
        
            oldRelPos = object.oldItemPosition + offset - resizingOrigin
            scaledRelPos = QPointF(oldRelPos.x() * scale, oldRelPos.y() * scale)
            newScreenPos = resizingOrigin + scaledRelPos - offset
            newPos = renderer.screenToPixelCoords_(newScreenPos)
            origSize = object.oldSize
            newSize = QSizeF(origSize.width() * scale, origSize.height() * scale)
            if (mapObject.polygon().isEmpty() == False):
                # For polygons, we have to scale in object space.
                rotation = object.item.rotation() * M_PI / -180
                sn = math.sin(rotation)
                cs = math.cos(rotation)
                oldPolygon = object.oldPolygon
                newPolygon = QPolygonF(oldPolygon.size())
                for n in range(oldPolygon.size()):
                    oldPoint = QPointF(oldPolygon[n])
                    rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn, oldPoint.y() * cs - oldPoint.x() * sn)
                    scaledPoint = QPointF(rotPoint.x() * scale, rotPoint.y() * scale)
                    newPoint = QPointF(scaledPoint.x() * cs - scaledPoint.y() * sn, scaledPoint.y() * cs + scaledPoint.x() * sn)
                    newPolygon[n] = newPoint

                mapObject.setPolygon(newPolygon)

            mapObject.setSize(newSize)
            mapObject.setPosition(newPos)
        
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers):
        renderer = self.mapDocument().renderer()
        object = self.mMovingObjects.first()
        mapObject = object.item.mapObject()
        
        ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup
        # offset. We will un-apply it to these variables since the resize for
        # single items happens in local coordinate space.
        ##
        offset = mapObject.objectGroup().offset()
    
        ## These transformations undo and redo the object rotation, which is always
        # applied in screen space.
        ##
        unrotate = rotateAt(object.oldItemPosition, -object.oldRotation)
        rotate = rotateAt(object.oldItemPosition, object.oldRotation)
        origin = (resizingOrigin - offset) * unrotate
        pos = (screenPos - offset) * unrotate
        start = (self.mStart - offset) * unrotate
        oldPos = object.oldItemPosition
        ## In order for the resizing to work somewhat sanely in isometric mode,
        # the resizing is performed in pixel space except for tile objects, which
        # are not affected by isometric projection apart from their position.
        ##
        pixelSpace = resizeInPixelSpace(mapObject)
        preserveAspect = modifiers & Qt.ControlModifier
        if (pixelSpace):
            origin = renderer.screenToPixelCoords_(origin)
            pos = renderer.screenToPixelCoords_(pos)
            start = renderer.screenToPixelCoords_(start)
            oldPos = object.oldPosition

        newPos = oldPos
        newSize = object.oldSize
        ## In case one of the anchors was used as-is, the desired size can be
        # derived directly from the distance from the origin for rectangle
        # and ellipse objects. This allows scaling up a 0-sized object without
        # dealing with infinite scaling factor issues.
        #
        # For obvious reasons this can't work on polygons or polylines, nor when
        # preserving the aspect ratio.
        ##
        if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin and (mapObject.shape() == MapObject.Rectangle or
                 mapObject.shape() == MapObject.Ellipse) and not preserveAspect):
            newBounds = QRectF(newPos, newSize)
            newBounds = align(newBounds, mapObject.alignment())
            x = self.mClickedResizeHandle.anchorPosition()
            if x==AnchorPosition.LeftAnchor or x==AnchorPosition.TopLeftAnchor or x==AnchorPosition.BottomLeftAnchor:
                newBounds.setLeft(min(pos.x(), origin.x()))
            elif x==AnchorPosition.RightAnchor or x==AnchorPosition.TopRightAnchor or x==AnchorPosition.BottomRightAnchor:
                newBounds.setRight(max(pos.x(), origin.x()))
            else:
                # nothing to do on this axis
                pass

            x = self.mClickedResizeHandle.anchorPosition()
            if x==AnchorPosition.TopAnchor or x==AnchorPosition.TopLeftAnchor or x==AnchorPosition.TopRightAnchor:
                newBounds.setTop(min(pos.y(), origin.y()))
            elif x==AnchorPosition.BottomAnchor or x==AnchorPosition.BottomLeftAnchor or x==AnchorPosition.BottomRightAnchor:
                newBounds.setBottom(max(pos.y(), origin.y()))
            else:
                # nothing to do on this axis
                pass

            newBounds = unalign(newBounds, mapObject.alignment())
            newSize = newBounds.size()
            newPos = newBounds.topLeft()
        else:
            relPos = pos - origin
            startDiff = start - origin
            try:
                newx = relPos.x() / startDiff.x()
            except:
                newx = 0
            try:
                newy = relPos.y() / startDiff.y()
            except:
                newy = 0
            scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy))
            if not math.isfinite(scalingFactor.width()):
                scalingFactor.setWidth(1)
            if not math.isfinite(scalingFactor.height()):
                scalingFactor.setHeight(1)
            
            if (self.mResizingLimitHorizontal):
                if preserveAspect:
                    scalingFactor.setWidth(scalingFactor.height())
                else:
                    scalingFactor.setWidth(1)
            elif (self.mResizingLimitVertical):
                if preserveAspect:
                    scalingFactor.setHeight(scalingFactor.width())
                else:
                    scalingFactor.setHeight(1)
            elif (preserveAspect):
                scale = min(scalingFactor.width(), scalingFactor.height())
                scalingFactor.setWidth(scale)
                scalingFactor.setHeight(scale)

            oldRelPos = oldPos - origin
            newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(), oldRelPos.y() * scalingFactor.height())
            newSize.setWidth(newSize.width() * scalingFactor.width())
            newSize.setHeight(newSize.height() * scalingFactor.height())
            if (not object.oldPolygon.isEmpty()):
                newPolygon = QPolygonF(object.oldPolygon.size())
                for n in range(object.oldPolygon.size()):
                    point = object.oldPolygon[n]
                    newPolygon[n] = QPointF(point.x() * scalingFactor.width(), point.y() * scalingFactor.height())

                mapObject.setPolygon(newPolygon)

        if (pixelSpace):
            newPos = renderer.pixelToScreenCoords_(newPos)
        newPos = renderer.screenToPixelCoords_(newPos * rotate)
        mapObject.setSize(newSize)
        mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def finishResizing(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos): # No scaling at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Resize %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(MoveMapObject(self.mapDocument(), mapObject, object.oldPosition))
            undoStack.push(ResizeMapObject(self.mapDocument(), mapObject, object.oldSize))
            if (not object.oldPolygon.isEmpty()):
                undoStack.push(ChangePolygon(self.mapDocument(), mapObject, object.oldPolygon))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def setMode(self, mode):
        if (self.mMode != mode):
            self.mMode = mode
            self.updateHandles()

    def saveSelectionState(self):
        self.mMovingObjects.clear()
        # Remember the initial state before moving, resizing or rotating
        for item in self.mapScene().selectedObjectItems():
            mapObject = item.mapObject()
            object = MovingObject()
            object.item = item
            object.oldItemPosition = item.pos()
            object.oldPosition = mapObject.position()
            object.oldSize = mapObject.size()
            object.oldPolygon = mapObject.polygon()
            object.oldRotation = mapObject.rotation()

            self.mMovingObjects.append(object)

    def refreshCursor(self):
        cursorShape = Qt.ArrowCursor

        if self.mAction == Action.NoAction:
            hasSelection = not self.mapScene().selectedObjectItems().isEmpty()

            if ((self.mHoveredObjectItem or ((self.mModifiers & Qt.AltModifier) and hasSelection)) and not (self.mModifiers & Qt.ShiftModifier)):
                cursorShape = Qt.SizeAllCursor
        elif self.mAction == Action.Moving:
            cursorShape = Qt.SizeAllCursor

        if self.cursor.shape != cursorShape:
            self.setCursor(cursorShape)

    def snapToGrid(self, diff, modifiers):
        renderer = self.mapDocument().renderer()
        snapHelper = SnapHelper(renderer, modifiers)
        if (snapHelper.snaps()):
            alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition)
            newAlignScreenPos = alignScreenPos + diff
            newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos)
            snapHelper.snap(newAlignPixelPos)
            return renderer.pixelToScreenCoords_(newAlignPixelPos) - alignScreenPos

        return diff

    def changingObjects(self):
        changingObjects = QList()
        
        for movingObject in self.mMovingObjects:
            changingObjects.append(movingObject.item.mapObject())

        return changingObjects
Esempio n. 6
0
class ObjectTypesModel(QAbstractTableModel):
    ColorRole = Qt.UserRole

    def __init__(self, parent):
        super().__init__(parent)

        self.mObjectTypes = QVector()

    def setObjectTypes(self, objectTypes):
        self.beginResetModel()
        self.mObjectTypes = objectTypes
        self.endResetModel()

    def objectTypes(self):
        return self.mObjectTypes

    def rowCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = self.mObjectTypes.size()
        return _x

    def columnCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = 2
        return _x

    def headerData(self, section, orientation, role):
        if (orientation == Qt.Horizontal):
            if (role == Qt.DisplayRole):
                x = section
                if x == 0:
                    return self.tr("Type")
                elif x == 1:
                    return self.tr("Color")
            elif (role == Qt.TextAlignmentRole):
                return Qt.AlignLeft

        return QVariant()

    def data(self, index, role):
        # QComboBox requests data for an invalid index when the model is empty
        if (not index.isValid()):
            return QVariant()
        objectType = self.mObjectTypes.at(index.row())
        if (role == Qt.DisplayRole or role == Qt.EditRole):
            if (index.column() == 0):
                return objectType.name
        if (role == ObjectTypesModel.ColorRole and index.column() == 1):
            return objectType.color
        return QVariant()

    def setData(self, index, value, role):
        if (role == Qt.EditRole and index.column() == 0):
            self.mObjectTypes[index.row()].name = value.strip()
            self.dataChanged.emit(index, index)
            return True

        return False

    def flags(self, index):
        f = super().flags(index)
        if (index.column() == 0):
            f |= Qt.ItemIsEditable
        return f

    def setObjectTypeColor(self, objectIndex, color):
        self.mObjectTypes[objectIndex].color = color
        mi = self.index(objectIndex, 1)
        self.dataChanged.emit(mi, mi)

    def removeObjectTypes(self, indexes):
        rows = QVector()
        for index in indexes:
            rows.append(index.row())
        rows = sorted(rows)
        for i in range(len(rows) - 1, -1, -1):
            row = rows[i]
            self.beginRemoveRows(QModelIndex(), row, row)
            self.mObjectTypes.remove(row)
            self.endRemoveRows()

    def appendNewObjectType(self):
        self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(),
                             self.mObjectTypes.size())
        self.mObjectTypes.append(ObjectType())
        self.endInsertRows()
Esempio n. 7
0
class FrameListModel(QAbstractListModel):
    DEFAULT_DURATION = 100

    def __init__(self, parent):
        super().__init__(parent)

        self.mFrames = QVector()
        self.mTileset = None

    def rowCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = self.mFrames.size()
        return _x

    def data(self, index, role):
        x = role
        if x == Qt.EditRole or x == Qt.DisplayRole:
            return self.mFrames.at(index.row()).duration
        elif x == Qt.DecorationRole:
            tileId = self.mFrames.at(index.row()).tileId
            tile = self.mTileset.tileAt(tileId)
            if tile:
                return tile.image()

        return QVariant()

    def setData(self, index, value, role):
        if (role == Qt.EditRole):
            duration = value
            if (duration >= 0):
                self.mFrames[index.row()].duration = duration
                self.dataChanged.emit(index, index)
                return True

        return False

    def flags(self, index):
        defaultFlags = super().flags(index)
        if (index.isValid()):
            return Qt.ItemIsDragEnabled | Qt.ItemIsEditable | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags

    def removeRows(self, row, count, parent):
        if (not parent.isValid()):
            if (count > 0):
                self.beginRemoveRows(parent, row, row + count - 1)
                self.mFrames.remove(row, count)
                self.endRemoveRows()

            return True

        return False

    def mimeTypes(self):
        types = QStringList()
        types.append(TILES_MIMETYPE)
        types.append(FRAMES_MIMETYPE)
        return types

    def mimeData(self, indexes):
        mimeData = QMimeData()
        encodedData = QByteArray()
        stream = QDataStream(encodedData, QIODevice.WriteOnly)
        for index in indexes:
            if (index.isValid()):
                frame = self.mFrames.at(index.row())
                stream.writeInt(frame.tileId)
                stream.writeInt(frame.duration)

        mimeData.setData(FRAMES_MIMETYPE, encodedData)
        return mimeData

    def dropMimeData(self, data, action, row, column, parent):
        if (action == Qt.IgnoreAction):
            return True
        if (column > 0):
            return False
        beginRow = 0
        if (row != -1):
            beginRow = row
        elif parent.isValid():
            beginRow = parent.row()
        else:
            beginRow = self.mFrames.size()
        newFrames = QVector()
        if (data.hasFormat(FRAMES_MIMETYPE)):
            encodedData = data.data(FRAMES_MIMETYPE)
            stream = QDataStream(encodedData, QIODevice.ReadOnly)
            while (not stream.atEnd()):
                frame = Frame()
                frame.tileId = stream.readInt()
                frame.duration = stream.readInt()
                newFrames.append(frame)
        elif (data.hasFormat(TILES_MIMETYPE)):
            encodedData = data.data(TILES_MIMETYPE)
            stream = QDataStream(encodedData, QIODevice.ReadOnly)
            while (not stream.atEnd()):
                frame = Frame()
                frame.tileId = stream.readInt()
                frame.duration = FrameListModel.DEFAULT_DURATION
                newFrames.append(frame)

        if (newFrames.isEmpty()):
            return False
        self.beginInsertRows(QModelIndex(), beginRow,
                             beginRow + newFrames.size() - 1)
        self.mFrames.insert(beginRow, newFrames.size(), Frame())
        for i in range(newFrames.size()):
            self.mFrames[i + beginRow] = newFrames[i]
        self.endInsertRows()
        return True

    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction

    def setFrames(self, tileset, frames):
        self.beginResetModel()
        self.mTileset = tileset
        self.mFrames = frames
        self.endResetModel()

    def addTileIdAsFrame(self, id):
        frame = Frame()
        frame.tileId = id
        frame.duration = FrameListModel.DEFAULT_DURATION
        self.addFrame(frame)

    def frames(self):
        return self.mFrames

    def addFrame(self, frame):
        self.beginInsertRows(QModelIndex(), self.mFrames.size(),
                             self.mFrames.size())
        self.mFrames.append(frame)
        self.endInsertRows()