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
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()
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
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
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
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()
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()