from common import * from math import floor, ceil import weakref import math class KPEditorItem(QtGui.QGraphicsItem): def __init__(self): QtGui.QGraphicsItem.__init__(self) self.setFlags( self.ItemSendsGeometryChanges | self.ItemIsSelectable | self.ItemIsMovable ) self.overrideSnap = False def itemChange(self, change, value): if change == self.ItemPositionChange and not self.overrideSnap: currentX, currentY = self.x(), self.y() # snap the item snapX, snapY = self.SNAP_TO newpos = value.toPyObject() x, y = newpos.x(), newpos.y() x = int((x + (snapX/2)) / snapX) * snapX y = int((y + (snapY/2)) / snapY) * snapY if x < 0: x = 0 if x >= (12288+snapX): x = (12288+snapX-1) if y < 0: y = 0 if y >= (12288+snapY): y = (12288+snapY-1) if x != currentX or y != currentY: self._itemMoved(currentX, currentY, x, y) newpos.setX(x) newpos.setY(y) return newpos return QtGui.QGraphicsItem.itemChange(self, change, value) def boundingRect(self): return self._boundingRect RESIZER_TOP_LEFT = 1 RESIZER_TOP_RIGHT = 2 RESIZER_BOTTOM_LEFT = 3 RESIZER_BOTTOM_RIGHT = 4 RESIZER_TOP = 5 RESIZER_BOTTOM = 6 RESIZER_LEFT = 7 RESIZER_RIGHT = 8 def resizerPortionAt(self, x, y): try: rightBound, bottomBound = self._resizerEndXY except AttributeError: rect = self._boundingRect rightBound, bottomBound = rect.right() - 5, rect.bottom() - 5 if y < 5: if x < 5: return 1 # TOP_LEFT elif x >= rightBound: return 2 # TOP_RIGHT else: return 5 # TOP elif y >= bottomBound: if x < 5: return 3 # BOTTOM_LEFT elif x >= rightBound: return 4 # BOTTOM_RIGHT else: return 6 # BOTTOM else: if x < 5: return 7 # LEFT elif x >= rightBound: return 8 # RIGHT else: return None def _itemMoved(self, oldX, oldY, newX, newY): pass class KPEditorObject(KPEditorItem): SNAP_TO = (24,24) def __init__(self, obj, layer): KPEditorItem.__init__(self) obj.qtItem = self self._objRef = weakref.ref(obj) self._layerRef = weakref.ref(layer) self._updatePosition() self._updateSize() self.setAcceptHoverEvents(True) self.resizing = None # I don't bother setting the ZValue because it doesn't quite matter: # only one layer's objects are ever clickable, and drawBackground takes # care of the layered drawing def _updatePosition(self): x,y = self._objRef().position self.setPos(x*24, y*24) def _updateSize(self): self.prepareGeometryChange() obj = self._objRef() w,h = obj.size self._boundingRect = QtCore.QRectF(0, 0, w*24, h*24) self._selectionRect = QtCore.QRectF(0, 0, w*24-1, h*24-1) self._resizerEndXY = (w*24-5, h*24-5) def paint(self, painter, option, widget): if self.isSelected(): painter.setPen(QtGui.QPen(Qt.white, 1, Qt.DotLine)) painter.drawRect(self._selectionRect) def hoverMoveEvent(self, event): if self._layerRef() != KP.mapScene.currentLayer: self.setCursor(Qt.ArrowCursor) return pos = event.pos() bit = self.resizerPortionAt(pos.x(), pos.y()) if bit == 1 or bit == 4: self.setCursor(Qt.SizeFDiagCursor) elif bit == 2 or bit == 3: self.setCursor(Qt.SizeBDiagCursor) elif bit == 7 or bit == 8: self.setCursor(Qt.SizeHorCursor) elif bit == 5 or bit == 6: self.setCursor(Qt.SizeVerCursor) else: self.setCursor(Qt.ArrowCursor) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: pos = event.pos() bit = self.resizerPortionAt(pos.x(), pos.y()) if self._layerRef() == KP.mapScene.currentLayer and bit: event.accept() x, xSide, y, ySide = False, None, False, None if bit == 1 or bit == 7 or bit == 3: x, xSide = True, 1 elif bit == 2 or bit == 4 or bit == 8: x, xSide = True, 0 if bit == 1 or bit == 2 or bit == 5: y, ySide = True, 1 elif bit == 3 or bit == 4 or bit == 6: y, ySide = True, 0 self.resizing = (x, xSide, y, ySide) return KPEditorItem.mousePressEvent(self, event) def _tryAndResize(self, obj, axisIndex, mousePosition, stationarySide): objPosition = obj.position[axisIndex] objSize = obj.size[axisIndex] mousePosition = int(mousePosition / 12) * 12 if stationarySide == 0: # Resize the right/bottom side relativeMousePosition = mousePosition - objPosition newSize = relativeMousePosition + 1 if newSize == objSize or newSize < 1: return False if axisIndex == 1: obj.size = (obj.size[0], newSize) else: obj.size = (newSize, obj.size[1]) else: # Resize the left/top side rightSide = objPosition + objSize - 1 newLeftSide = mousePosition newPosition = newLeftSide newSize = rightSide - newLeftSide + 1 if newSize < 1: return False if newPosition == objPosition and newSize == objSize: return False if axisIndex == 1: obj.position = (obj.position[0], newPosition) obj.size = (obj.size[0], newSize) else: obj.position = (newPosition, obj.position[1]) obj.size = (newSize, obj.size[1]) return True def mouseMoveEvent(self, event): if self.resizing: obj = self._objRef() scenePos = event.scenePos() hasChanged = False resizeX, xSide, resizeY, ySide = self.resizing if resizeX: hasChanged |= self._tryAndResize(obj, 0, int(scenePos.x() / 24), xSide) if resizeY: hasChanged |= self._tryAndResize(obj, 1, int(scenePos.y() / 24), ySide) if hasChanged: # TODO: move overrides into updatePosition? # TODO: snapping for resizing self.overrideSnap = True obj.updateCache() self._layerRef().updateCache() self._updatePosition() self._updateSize() self.overrideSnap = False else: KPEditorItem.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self.resizing and event.button() == Qt.LeftButton: self.resizing = None else: KPEditorItem.mouseReleaseEvent(self, event) def _itemMoved(self, oldX, oldY, newX, newY): obj = self._objRef() obj.position = (newX/24, newY/24) self._layerRef().updateCache() def remove(self): obj = self._objRef() layer = self._layerRef() layer.objects.remove(obj) layer.updateCache() class KPEditorDoodad(KPEditorItem): SNAP_TO = (12,12) def __init__(self, doodad, layer): KPEditorItem.__init__(self) self.setFlags( self.ItemSendsGeometryChanges | self.ItemIsSelectable | self.ItemIsMovable ) doodad.qtItem = self self._doodadRef = weakref.ref(doodad) self._layerRef = weakref.ref(layer) # TODO: refactor this to store source doodad data under KP.map sourceItem = KP.mainWindow.doodadSelector.getDoodad(doodad.index) self._sourceRef = weakref.ref(sourceItem) self.resizing = None self.rotating = None self._updatePixmap() self._updatePosition() self._updateSize() self._updateTransform() self.setAcceptHoverEvents(True) def _updatePixmap(self): source = self._sourceRef() pixmap = source.icon().pixmap(source.icon().availableSizes()[0]) self.prepareGeometryChange() w, h = pixmap.width(), pixmap.height() self.pixmap = pixmap def _updatePosition(self): x,y = self._doodadRef().position self.setPos(x, y) def _updateSize(self): self.prepareGeometryChange() w,h = self._doodadRef().size self.setTransformOriginPoint(w/2, h/2) self._boundingRect = QtCore.QRectF(0, 0, w, h) self._selectionRect = QtCore.QRectF(0, 0, w-1, h-1) self._resizerEndXY = (w-5, h-5) def _updateTransform(self): doodad = self._doodadRef() pixmap = self.pixmap displaceX, displaceY = pixmap.width()/2, pixmap.height()/2 transform = QtGui.QTransform() transform.translate(displaceX, displaceY) transform.rotate(doodad.angle) transform.translate(-displaceX, -displaceY) self.setTransform(transform) def paint(self, painter, option, widget): if self.isSelected(): painter.setPen(QtGui.QPen(Qt.white, 1, Qt.DotLine)) painter.drawRect(self._selectionRect) def _itemMoved(self, oldX, oldY, newX, newY): doodad = self._doodadRef() doodad.position = (newX, newY) def hoverMoveEvent(self, event): if self._layerRef() != KP.mapScene.currentLayer: self.setCursor(Qt.ArrowCursor) return pos = event.pos() bit = self.resizerPortionAt(pos.x(), pos.y()) if (event.modifiers() == Qt.ShiftModifier): if bit: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.ArrowCursor) else: if bit == 1 or bit == 4: self.setCursor(Qt.SizeFDiagCursor) elif bit == 2 or bit == 3: self.setCursor(Qt.SizeBDiagCursor) elif bit == 7 or bit == 8: self.setCursor(Qt.SizeHorCursor) elif bit == 5 or bit == 6: self.setCursor(Qt.SizeVerCursor) else: self.setCursor(Qt.ArrowCursor) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: pos = event.pos() bit = self.resizerPortionAt(pos.x(), pos.y()) if self._layerRef() == KP.mapScene.currentLayer and bit: event.accept() if (event.modifiers() & Qt.ShiftModifier): self.rotating = self.mapToScene(pos), self._doodadRef().angle self.setCursor(Qt.ClosedHandCursor) return else: x, xSide, y, ySide = False, None, False, None if bit == 1 or bit == 7 or bit == 3: x, xSide = True, 1 elif bit == 2 or bit == 4 or bit == 8: x, xSide = True, 0 if bit == 1 or bit == 2 or bit == 5: y, ySide = True, 1 elif bit == 3 or bit == 4 or bit == 6: y, ySide = True, 0 self.resizing = (x, xSide, y, ySide) return KPEditorItem.mousePressEvent(self, event) def _tryAndResize(self, obj, axisIndex, mousePosition, stationarySide): objPosition = obj.position[axisIndex] objSize = obj.size[axisIndex] if stationarySide == 0: # Resize the right/bottom side relativeMousePosition = mousePosition - objPosition newSize = relativeMousePosition + 1 if newSize == objSize or newSize < 1: return False if axisIndex == 1: obj.size = (obj.size[0], newSize) else: obj.size = (newSize, obj.size[1]) else: # Resize the left/top side rightSide = objPosition + objSize - 1 newLeftSide = mousePosition newPosition = newLeftSide newSize = rightSide - newLeftSide + 1 if newSize < 1: return False if newPosition == objPosition and newSize == objSize: return False if axisIndex == 1: obj.position = (obj.position[0], newPosition) obj.size = (obj.size[0], newSize) else: obj.position = (newPosition, obj.position[1]) obj.size = (newSize, obj.size[1]) return True def _tryAndRotate(self, obj, mouseX, mouseY, originalPos, oldAngle, modifiers): center = self.mapToScene(self.boundingRect().center()) objX = center.x() objY = center.y() origX = originalPos.x() origY = originalPos.y() dy = origY - objY dx = origX - objX rads = math.atan2(dy, dx) origAngle = math.degrees(rads) dy = mouseY - objY dx = mouseX - objX rads = math.atan2(dy, dx) angle = math.degrees(rads) # Move this to ItemChange() or something at some point. finalAngle = angle - origAngle + oldAngle if (modifiers & Qt.ControlModifier): finalAngle = int(finalAngle / 45.0) * 45.0 return True, finalAngle def mouseMoveEvent(self, event): if self.resizing: obj = self._doodadRef() scenePos = event.scenePos() hasChanged = False resizeX, xSide, resizeY, ySide = self.resizing if resizeX: hasChanged |= self._tryAndResize(obj, 0, scenePos.x(), xSide) if resizeY: hasChanged |= self._tryAndResize(obj, 1, scenePos.y(), ySide) if hasChanged: self._updatePosition() self._updateSize() elif self.rotating: obj = self._doodadRef() scenePos = event.scenePos() hasChanged = False hasChanged, angle = self._tryAndRotate(obj, scenePos.x(), scenePos.y(), self.rotating[0], self.rotating[1], event.modifiers()) if hasChanged: obj.angle = angle self._updateTransform() else: KPEditorItem.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self.resizing and event.button() == Qt.LeftButton: self.resizing = None elif self.rotating and event.button() == Qt.LeftButton: self.rotating = None else: KPEditorItem.mouseReleaseEvent(self, event) def remove(self): doodad = self._doodadRef() layer = self._layerRef() layer.objects.remove(doodad) del doodad class KPEditorNode(QtGui.QGraphicsPixmapItem): def __init__(self, node, layer): QtGui.QGraphicsPixmapItem.__init__(self) self.setFlags( self.ItemSendsGeometryChanges | self.ItemIsSelectable | self.ItemIsMovable ) class KPEditorPathSegment(QtGui.QGraphicsPathItem): def __init__(self, node, layer): QtGui.QGraphicsPathItem.__init__(self) self.setFlags( self.ItemSendsGeometryChanges | self.ItemIsSelectable | self.ItemIsMovable ) self.path = QtGui.QPainterPath() self.brush = None self.pen = None self.rotation = 0 def makeStraight(self, start, end): self.path.moveTo(start) self.path.addPolygon(QtGui.QPolygon([QtCore.QPointF(start.x(), start.y()-5.0), QtCore.QPointF(end.x(), end.y()-5.0), QtCore.QPointF(end.x(), end.y()+5.0), QtCore.QPointF(start.x(), start.y()+5.0), QtCore.QPointF(start.x(), start.y()-5.0)])) center = QtCore.QPointF((end.x()/2)-(start.x()/2), (end.y()/2)-((start.y()/2) + 16)) graduate = QtGui.QRadialGradiant(center, float((end.x()/2)-(start.x()/2))) graduate.setColorA(QtGui.QColor(200, 210, 240)) graduate.setColorB(QtGui.QColor(150, 160, 190)) self.brush = QtGui.QBrush(graduate) self.pen = QtGui.QPen(QColor(120, 130, 160)) self.pen.setWidth(4) self.pen.setCapStyle(Qt.RoundCap) self.pen.setJoinStyle(Qt.RoundJoin) def makeArc(self, start, end): self.path.moveTo(start) self.path.addEllipse(start.x(), start.y(), end.x()-start.x(), end.y()-start.y()) r = QtGui.QPainterPath() r.addEllipse(start.x(), start.y()-4, end.x()-start.x(), end.y()-start.y()-16) self.path.subtracted(r) center = QtCore.QPointF((end.x()/2)-(start.x()/2), (end.y()/2)-((start.y()/2) + 16)) graduate = QtGui.QRadialGradiant(center, float((end.x()/2)-(start.x()/2))) graduate.setColorA(QtGui.QColor(200, 210, 240)) graduate.setColorB(QtGui.QColor(150, 160, 190)) self.brush = QtGui.QBrush(graduate) self.pen = QtGui.QPen(QColor(120, 130, 160)) self.pen.setWidth(4) self.pen.setCapStyle(Qt.RoundCap) self.pen.setJoinStyle(Qt.RoundJoin) sX = start.x() sY = start.y() eX = end.x() eY = end.y() dy = eY - sY dx = eX - sX rads = math.atan2(dy, dx) self.rotate = math.degrees(rads) class KPMapScene(QtGui.QGraphicsScene): def __init__(self): QtGui.QGraphicsScene.__init__(self, 0, 0, 512*24, 512*24) # todo: handle selectionChanged # todo: look up why I used setItemIndexMethod(self.NoIndex) in Reggie self.currentLayer = None KP.mapScene = self def drawBackground(self, painter, rect): painter.fillRect(rect, QtGui.QColor(209, 218, 236)) areaLeft, areaTop = rect.x(), rect.y() areaWidth, areaHeight = rect.width(), rect.height() areaRight, areaBottom = areaLeft+areaWidth, areaTop+areaHeight areaLeftT = floor(areaLeft / 24) areaTopT = floor(areaTop / 24) areaRightT = ceil(areaRight / 24) areaBottomT = ceil(areaBottom / 24) # compile a list of doodads visibleDoodadsByLayer = {} for obj in self.items(rect): if not isinstance(obj, KPEditorDoodad): continue layer = obj._layerRef() try: doodadList = visibleDoodadsByLayer[layer] except KeyError: doodadList = [] visibleDoodadsByLayer[layer] = doodadList doodadList.append(obj) # now draw everything! for layer in reversed(KP.map.layers): if not layer.visible: continue if isinstance(layer, KPDoodadLayer): try: toDraw = visibleDoodadsByLayer[layer] except KeyError: continue for item in reversed(toDraw): painter.save() painter.setWorldTransform(item.sceneTransform(), True) w, h = item._doodadRef().size painter.drawPixmap(0, 0, w, h, item.pixmap) painter.restore() elif isinstance(layer, KPTileLayer): left, top = layer.cacheBasePos width, height = layer.cacheSize right, bottom = left+width, top+height if width == 0 and height == 0: continue if right < areaLeftT: continue if left > areaRightT: continue if bottom < areaTopT: continue if top > areaBottomT: continue # decide how much of the layer we'll actually draw drawLeft = int(max(areaLeftT, left)) drawRight = int(min(areaRightT, right)) drawTop = int(max(areaTopT, top)) drawBottom = int(min(areaBottomT, bottom)) srcY = drawTop - top destY = drawTop * 24 baseSrcX = drawLeft - left baseDestX = drawLeft * 24 rows = layer.cache tileset = KP.map.loadedTilesets[layer.tileset] tileList = tileset.tiles for y in xrange(drawTop, drawBottom): srcX = baseSrcX destX = baseDestX row = rows[srcY] for x in xrange(drawLeft, drawRight): tile = row[srcX] if tile != -1: painter.drawPixmap(destX, destY, tileList[tile]) srcX += 1 destX += 24 srcY += 1 destY += 24 def setCurrentLayer(self, layer): if self.currentLayer is not None: self.setLayerObjectsFlag(self.currentLayer, QtGui.QGraphicsItem.ItemIsSelectable, False) self.setLayerObjectsFlag(self.currentLayer, QtGui.QGraphicsItem.ItemIsMovable, False) self.currentLayer = layer self.setLayerObjectsFlag(layer, QtGui.QGraphicsItem.ItemIsSelectable, True) self.setLayerObjectsFlag(layer, QtGui.QGraphicsItem.ItemIsMovable, True) def setLayerObjectsFlag(self, layer, flag, value): for obj in layer.objects: item = obj.qtItem if item: item.setFlag(flag, value) class KPEditorWidget(QtGui.QGraphicsView): def __init__(self, scene, parent=None): QtGui.QGraphicsView.__init__(self, scene, parent) self.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.setDragMode(self.RubberBandDrag) self.xScrollBar = QtGui.QScrollBar(Qt.Horizontal, parent) self.setHorizontalScrollBar(self.xScrollBar) self.yScrollBar = QtGui.QScrollBar(Qt.Vertical, parent) self.setVerticalScrollBar(self.yScrollBar) self.centerOn(0,0) # set up stuff for painting self.paintNext = None self.paintNextID = None self._resetPaintVars() def _resetPaintVars(self): self.painting = None self.paintingItem = None self.paintBeginPosition = None def _tryToPaint(self, event): '''Called when a paint attempt is initiated''' if self.paintNext is None: return paint = self.paintNext if isinstance(paint, KPTileObject): clicked = self.mapToScene(event.x(), event.y()) x, y = clicked.x(), clicked.y() if x < 0: x = 0 if y < 0: y = 0 x = int(x / 24) y = int(y / 24) layer = self.scene().currentLayer if not layer.visible: return if not isinstance(layer, KPTileLayer): return obj = KPObject() obj.position = (x,y) obj.size = (1,1) obj.tileset = layer.tileset obj.kind = paint obj.updateCache() layer.objects.append(obj) layer.updateCache() item = KPEditorObject(obj, layer) self.scene().addItem(item) self.painting = obj self.paintingItem = item self.paintBeginPosition = (x, y) if isinstance(paint, QtGui.QListWidgetItem): clicked = self.mapToScene(event.x(), event.y()) x, y = clicked.x(), clicked.y() if x < 0: x = 0 if y < 0: y = 0 layer = self.scene().currentLayer if not layer.visible: return if not isinstance(layer, KPDoodadLayer): return obj = KPDoodad() obj.position = (x,y) obj.index = self.paintNextID obj.setDefaultSize() layer.objects.append(obj) item = KPEditorDoodad(obj, layer) self.scene().addItem(item) self.painting = obj self.paintingItem = item self.paintBeginPosition = (x, y) def _movedWhilePainting(self, event): '''Called when the mouse is moved while painting something''' obj = self.painting item = self.paintingItem if isinstance(obj, KPObject): clicked = self.mapToScene(event.x(), event.y()) x, y = clicked.x(), clicked.y() if x < 0: x = 0 if y < 0: y = 0 x = int(x / 24) y = int(y / 24) beginX, beginY = self.paintBeginPosition if x >= beginX: objX = beginX width = x - beginX + 1 else: objX = x width = beginX - x + 1 if y >= beginY: objY = beginY height = y - beginY + 1 else: objY = y height = beginY - y + 1 currentX, currentY = obj.position currentWidth, currentHeight = obj.size # update everything if changed changed = False if currentX != objX or currentY != objY: obj.position = (objX, objY) item._updatePosition() changed = True if currentWidth != width or currentHeight != height: obj.size = (width, height) obj.updateCache() item._updateSize() changed = True if not changed: return item._layerRef().updateCache() def mousePressEvent(self, event): if event.button() == Qt.RightButton: self._tryToPaint(event) event.accept() else: QtGui.QGraphicsView.mousePressEvent(self, event) def mouseMoveEvent(self, event): if event.buttons() == Qt.RightButton and self.painting: self._movedWhilePainting(event) event.accept() else: QtGui.QGraphicsView.mouseMoveEvent(self, event) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_Backspace: scene = self.scene() selection = scene.selectedItems() if len(selection) > 0: for obj in selection: obj.remove() obj.setSelected(False) scene.removeItem(obj) scene.update() self.update() return else: QtGui.QGraphicsView.keyPressEvent(self, event)