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.ignoreMovement = False self.overrideSnap = False def itemChange(self, change, value): if change == self.ItemPositionChange and not self.ignoreMovement: currentX, currentY = self.x(), self.y() newpos = value.toPyObject() x, y = newpos.x(), newpos.y() if self.overrideSnap: snapX, snapY = 1, 1 else: # snap the item snapX, snapY = self.SNAP_TO 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 def resizerPortionAt(self, x, y, originX=0, originY=0): try: leftBound, topBound = originX+5, originY+5 rightBound, bottomBound = self._resizerEndXY except AttributeError: rect = self._boundingRect leftBound, topBound = rect.x() + 5, rect.y() + 5 rightBound, bottomBound = rect.right() - 5, rect.bottom() - 5 if y < topBound: if x < leftBound: return 1 # TOP_LEFT elif x >= rightBound: return 2 # TOP_RIGHT else: return 5 # TOP elif y >= bottomBound: if x < leftBound: return 3 # BOTTOM_LEFT elif x >= rightBound: return 4 # BOTTOM_RIGHT else: return 6 # BOTTOM else: if x < leftBound: 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 if not hasattr(KPEditorObject, 'SELECTION_PEN'): KPEditorObject.SELECTION_PEN = QtGui.QPen(Qt.white, 1, Qt.DotLine) # 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): self.ignoreMovement = True x,y = self._objRef().position self.setPos(x*24, y*24) self.ignoreMovement = False 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(self.SELECTION_PEN) 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] 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: obj.updateCache() self._layerRef().updateCache() self._updatePosition() self._updateSize() 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, withItem=False): obj = self._objRef() layer = self._layerRef() layer.objects.remove(obj) layer.updateCache() if withItem: self.scene().removeItem(self) class KPEditorDoodad(KPEditorItem): SNAP_TO = (12,12) def __init__(self, doodad, layer): KPEditorItem.__init__(self) 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.setAcceptHoverEvents(True) if not hasattr(KPEditorDoodad, 'SELECTION_PEN'): KPEditorDoodad.SELECTION_PEN = QtGui.QPen(Qt.red, 1, Qt.DotLine) 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): # NOTE: EditorDoodads originate at the centre, not the top left like the others doodad = self._doodadRef() x,y = doodad.position w,h = doodad.size self.setPos(x+floor(w/2.0), y+floor(h/2.0)) def _updateSize(self): self.prepareGeometryChange() w,h = self._doodadRef().size self._boundingRect = QtCore.QRectF(-w/2, -h/2, w, h) self._selectionRect = self._boundingRect.adjusted(0, 0, -1, -1) def _updateTransform(self): doodad = self._doodadRef() self.setRotation(doodad.angle) def paint(self, painter, option, widget): if self.isSelected(): painter.setPen(self.SELECTION_PEN) painter.drawRect(self._selectionRect) def _itemMoved(self, oldX, oldY, newX, newY): doodad = self._doodadRef() w,h = doodad.size doodad.position = [newX-floor(w/2.0), newY-floor(h/2.0)] 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: # left x, xSide = True, 1 elif bit == 2 or bit == 4 or bit == 8: # right x, xSide = True, 0 if bit == 1 or bit == 2 or bit == 5: # top y, ySide = True, 1 elif bit == 3 or bit == 4 or bit == 6: # bottom y, ySide = True, 0 self._updateSize() self.resizing = (x, xSide, y, ySide) return KPEditorItem.mousePressEvent(self, event) def _tryAndResize(self, obj, axisIndex, mousePosition, stationarySide): newSize = abs(mousePosition) * 2 if newSize < 10: return False obj.size[axisIndex] = newSize 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() hasChanged = False resizeX, xSide, resizeY, ySide = self.resizing if resizeX: hasChanged |= self._tryAndResize(obj, 0, event.pos().x(), xSide) if resizeY: hasChanged |= self._tryAndResize(obj, 1, event.pos().y(), ySide) if hasChanged: # Doodads aren't supposed to snap, they're all free flowing like the wind. self._updateSize() elif self.rotating: obj = self._doodadRef() scenePos = event.scenePos() self.setTransformOriginPoint(self.boundingRect().center()) 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 # self._doodadRef().position = [self.x(), self.y()] elif self.rotating and event.button() == Qt.LeftButton: self.rotating = None else: KPEditorItem.mouseReleaseEvent(self, event) def remove(self, withItem=False): doodad = self._doodadRef() layer = self._layerRef() layer.objects.remove(doodad) if withItem: self.scene().removeItem(self) class KPEditorNode(KPEditorItem): SNAP_TO = (12,12) def __init__(self, node): KPEditorItem.__init__(self) node.qtItem = self self._nodeRef = weakref.ref(node) self.setZValue(101) self._boundingRect = QtCore.QRectF(-12, -12, 24, 24) self._selectionRect = QtCore.QRectF(-12, -12, 23, 23) self._updatePosition() if not hasattr(KPEditorNode, 'SELECTION_PEN'): KPEditorNode.SELECTION_PEN = QtGui.QPen(Qt.blue, 1, Qt.DotLine) def _updatePosition(self): node = self._nodeRef() x, y = node.position self.setPos(x+12, y+12) def _itemMoved(self, oldX, oldY, newX, newY): node = self._nodeRef() node.position = (newX-12, newY-12) for exit in node.exits: exit.qtItem.updatePosition() def paint(self, painter, option, widget): painter.fillRect(self._boundingRect, Qt.white) if self.isSelected(): painter.setPen(self.SELECTION_PEN) painter.drawRect(self._selectionRect) def remove(self, withItem=False): node = self._nodeRef() layer = KP.map.pathLayer layer.nodes.remove(node) if len(node.exits) == 2: # let's try to join the two! pathOne, pathTwo = node.exits start1, end1 = pathOne._startNodeRef(), pathOne._endNodeRef() start2, end2 = pathTwo._startNodeRef(), pathTwo._endNodeRef() if start1 == node: start = end1 else: start = start1 if start2 == node: end = end2 else: end = start2 joinedPath = KPPath(start, end, pathOne) layer.paths.append(joinedPath) item = KPEditorPath(joinedPath) self.scene().addItem(item) # whatever happened, delete the old paths anyway for exit in node.exits: exit.qtItem.remove(True) if withItem: self.scene().removeItem(self) class KPEditorPath(QtGui.QGraphicsLineItem): def __init__(self, path): QtGui.QGraphicsLineItem.__init__(self) self.setFlag(self.ItemIsSelectable, True) self.setZValue(100) startNode = path._startNodeRef().qtItem endNode = path._endNodeRef().qtItem self._startNodeRef = weakref.ref(startNode) self._endNodeRef = weakref.ref(endNode) self._pathRef = weakref.ref(path) path.qtItem = self self.updatePosition() if not hasattr(KPEditorPath, 'PEN'): KPEditorPath.BRUSH = QtGui.QBrush(QtGui.QColor(255, 255, 255, 140)) KPEditorPath.PEN = QtGui.QPen(KPEditorPath.BRUSH, 8, Qt.SolidLine, Qt.RoundCap) self.setPen(KPEditorPath.PEN) def updatePosition(self): path = self._pathRef() x1, y1 = path._startNodeRef().position x2, y2 = path._endNodeRef().position self.setLine(QtCore.QLineF(x1+12, y1+12, x2+12, y2+12)) def remove(self, withItem=False): path = self._pathRef() layer = KP.map.pathLayer layer.paths.remove(path) for ref in (self._startNodeRef, self._endNodeRef): node = ref()._nodeRef() try: node.exits.remove(path) except ValueError: pass if withItem: self.scene().removeItem(self) 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 p = item._boundingRect painter.drawPixmap(p.x(), p.y(), p.width(), p.height(), 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.currentLayer.setActivated(False) self.currentLayer = layer self.currentLayer.setActivated(True) class KPEditorWidget(QtGui.QGraphicsView): def __init__(self, scene, parent=None): QtGui.QGraphicsView.__init__(self, scene, parent) self.setRenderHints(QtGui.QPainter.Antialiasing) 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''' paint = self.paintNext layer = self.scene().currentLayer if not layer.visible: return if isinstance(layer, KPTileLayer): if paint is None: return 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) 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) elif isinstance(layer, KPDoodadLayer): if paint is None: return clicked = self.mapToScene(event.x(), event.y()) x, y = clicked.x(), clicked.y() if x < 0: x = 0 if y < 0: y = 0 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) elif isinstance(layer, KPPathLayer): print "Going to paint something related to paths" # decide what's under the mouse clicked = self.mapToScene(event.x(), event.y()) x, y = clicked.x(), clicked.y() itemsUnder = self.scene().items(clicked) for item in itemsUnder: if isinstance(item, KPEditorNode): print "Trying a line" # Paint a path to this node (if one is selected) sourceItem, sourceNode = None, None selected = self.scene().selectedItems() for selItem in selected: if isinstance(item, KPEditorNode) and selItem != item: sourceItem = selItem sourceNode = selItem._nodeRef() print "Got source" break if sourceItem is None: return # Make sure that no path already exists between these nodes destNode = item._nodeRef() for pathToCheck in sourceNode.exits: if pathToCheck._startNodeRef() == destNode: return if pathToCheck._endNodeRef() == destNode: return path = KPPath(sourceNode, destNode) KP.map.pathLayer.paths.append(path) print "Done!" item = KPEditorPath(path) self.scene().addItem(item) return elif isinstance(item, KPEditorPath): # Split this path into two.. at this point origPath = item._pathRef() node = KPNode() node.position = (x - 12, y - 12) KP.map.pathLayer.nodes.append(node) # Start node => Original path => New node => New path => End node endNode = origPath._endNodeRef() origPath.setEnd(node) origPath.qtItem.updatePosition() nodeItem = KPEditorNode(node) self.scene().addItem(nodeItem) self.painting = node self.paintingItem = item self.paintBeginPosition = (x - 12, y - 12) newPath = KPPath(node, endNode, origPath) KP.map.pathLayer.paths.append(newPath) pathItem = KPEditorPath(newPath) self.scene().addItem(pathItem) return # Paint a new node node = KPNode() node.isStop = True node.position = (x - 12, y - 12) KP.map.pathLayer.nodes.append(node) print "Painting a node at %r" % (node.position,) item = KPEditorNode(node) self.scene().addItem(item) self.painting = node self.paintingItem = item self.paintBeginPosition = (x - 12, y - 12) 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.setSelected(False) obj.remove(True) scene.update() self.update() return else: QtGui.QGraphicsView.keyPressEvent(self, event)