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 ) def itemChange(self, change, value): if change == self.ItemPositionChange: currentX, currentY = self.x(), self.y() # snap the item to 24x24 newpos = value.toPyObject() x, y = newpos.x(), newpos.y() x = int((x + 12) / 24) * 24 y = int((y + 12) / 24) * 24 if x < 0: x = 0 if x > 12264: x = 12264 if y < 0: y = 0 if y > 12264: y = 12264 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 _itemMoved(self, oldX, oldY, newX, newY): pass class KPEditorObject(KPEditorItem): 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): obj = self._objRef() x,y = obj.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) 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): rightBound, bottomBound = self._resizerEndXY 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 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() class KPEditorDoodad(QtGui.QGraphicsPixmapItem): def __init__(self, doodad, layer): QtGui.QGraphicsPixmapItem.__init__(self) self.setFlags( self.ItemSendsGeometryChanges | self.ItemIsSelectable | self.ItemIsMovable ) doodad.qtItem = self self._doodadRef = weakref.ref(doodad) self._layerRef = weakref.ref(layer) self.resizing = None self.rotating = None self.setAcceptHoverEvents(True) self.setTransformationMode(1) def paint(self, painter, option, widget): try: QtGui.QGraphicsPixmapItem.paint(self, painter, option, widget) except: pass if self.isSelected(): painter.setPen(QtGui.QPen(Qt.white, 1, Qt.DotLine)) painter.drawRect(self.boundingRect().adjusted(-1,-1,1,1)) def _itemMoved(self, oldX, oldY, newX, newY): doodad = self._doodadRef() doodad.position = (newX, newY) def itemChange(self, change, value): return QtGui.QGraphicsItem.itemChange(self, change, value) def resizerPortionAt(self, x, y): rightBound = self.boundingRect().right() - 5 bottomBound = self.boundingRect().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 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.AltModifier): 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.AltModifier): self.rotating = self.mapToScene(pos), self._doodadRef().angle self.setCursor(Qt.ClosedHandCursor) return else: self.resizing = bit return KPEditorItem.mousePressEvent(self, event) def _tryAndResize(self, bit, mousePosition, modifiers): itemPos = self.mapFromScene(mousePosition) x2 = self.boundingRect().right() + 5.0 y2 = self.boundingRect().bottom() + 5.0 if bit == 1: relativeScale = min([(x2 - itemPos.x()) / x2, (y2 - itemPos.y()) / y2]) elif bit == 2: relativeScale = min([itemPos.x() / x2, (y2 - itemPos.y()) / y2]) elif bit == 3: relativeScale = min([(x2 - itemPos.x()) / x2, itemPos.y() / y2]) elif bit == 4: relativeScale = min([itemPos.x() / x2, itemPos.y() / y2]) elif bit == 5: relativeScale = (y2 - itemPos.y()) / y2 elif bit == 6: relativeScale = itemPos.y() / y2 elif bit == 7: relativeScale = (x2 - itemPos.x()) / x2 elif bit == 8: relativeScale = itemPos.x() / x2 else: return False, 0 if relativeScale > 1.0: relativeScale = 1.0 if relativeScale < 0.1: relativeScale = 0.1 self.setTransformOriginPoint(self.boundingRect().center()) return True, relativeScale 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. # # if (modifiers == Qt.ShiftModifier): # if int(angle / 45.0) == int(oldAngle / 45.0): # return False, 0 # else: # angle = int(angle / 45.0) * 45.0 return True, angle - origAngle + oldAngle def mouseMoveEvent(self, event): # Fucking Transform can't do independent transforms with doing matrix math directly. # And before anyone asks, using QTransform.scale() is no good. if self.resizing: obj = self._doodadRef() scenePos = event.scenePos() hasChanged = False bit = self.resizing hasChanged, scale = self._tryAndResize(bit, scenePos, event.modifiers()) if hasChanged: obj.scale = scale self.setScale(scale) 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.setRotation(angle) 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) 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 toDraw: item.paint(painter, QtGui.QStyleOptionGraphicsItem(), self) # painter.setWorldTransform(item.transform(), True) # painter.drawPixmap(0, 0, item.pixmap) 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 size = paint.icon().availableSizes()[0] obj = KPDoodad() obj.position = (x,y) obj.size = (size.width(), size.height()) obj.index = self.paintNextID layer.objects.append(obj) item = KPEditorDoodad(obj, layer) item.setPixmap(paint.icon().pixmap(size)) item.setTransformOriginPoint(item.boundingRect().center()) item.setPos(x, y) 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)