File indexing completed on 2024-03-24 04:04:37
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 from qt import Qt, QRectF, QPointF, QSizeF, QSize 0011 from qt import QGraphicsObject, QGraphicsItem, QPixmap, QPainter, QColor 0012 0013 from util import stack 0014 from log import logException, logDebug, id4 0015 from guiutil import Painter, sceneRotation 0016 from common import LIGHTSOURCES, ZValues, Internal, Debug 0017 from common import StrMixin, isAlive 0018 from tile import Tile 0019 from meld import Meld 0020 from animation import AnimatedMixin 0021 0022 0023 class UITile(AnimatedMixin, QGraphicsObject, StrMixin): 0024 0025 """A tile visible on the screen. Every tile is only allocated once 0026 and then reshuffled and reused for every game. 0027 The unit of xoffset is the width of the tile, 0028 the unit of yoffset is the height of the tile. 0029 This is a QObject because we want to animate it.""" 0030 0031 # pylint: disable=too-many-instance-attributes 0032 0033 clsUid = 0 0034 0035 def __init__(self, tile, xoffset=0.0, yoffset=0.0, level=0): 0036 super().__init__() 0037 if not isinstance(tile, Tile): 0038 tile = Tile(tile) 0039 UITile.clsUid += 1 0040 self.uid = UITile.clsUid 0041 self._tile = tile 0042 self._boundingRect = None 0043 self._cross = False 0044 self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) 0045 # while moving the tile we use ItemCoordinateCache, see 0046 # Tile.setActiveAnimation 0047 self.__board = None 0048 self.setClippingFlags() 0049 self.__xoffset = xoffset 0050 self.__yoffset = yoffset 0051 self.__dark = False 0052 self.level = level 0053 0054 def name(self): 0055 """identification for animations""" 0056 return self._tile 0057 0058 def setClippingFlags(self): 0059 """if we do not show shadows, we need to clip""" 0060 showShadows = Internal.Preferences.showShadows 0061 self.setFlag( 0062 QGraphicsItem.ItemClipsChildrenToShape, 0063 enabled=not showShadows) 0064 self.setFlag(QGraphicsItem.ItemClipsToShape, enabled=not showShadows) 0065 0066 def keyPressEvent(self, event): 0067 """redirect to the board""" 0068 if self is not self.board.focusTile: 0069 logDebug('id4(self):%s, self:%s, focusTile:%s/%s' % \ 0070 (id4(self), self, id4(self.board.focusTile), self.board.focusTile)) 0071 return self.board.keyPressEvent(event) 0072 0073 def __lightDistance(self): 0074 """the distance of item from the light source""" 0075 board = self.board 0076 if not board: 0077 return 0 0078 rect = self.sceneBoundingRect() 0079 lightSource = board.lightSource 0080 result = 0 0081 if 'E' in lightSource: 0082 result -= rect.right() 0083 if 'W' in lightSource: 0084 result += rect.left() 0085 if 'S' in lightSource: 0086 result -= rect.bottom() 0087 if 'N' in lightSource: 0088 result += rect.top() 0089 return result 0090 0091 @property 0092 def tileset(self): 0093 """the active tileset""" 0094 return self.board.tileset if self.board else None 0095 0096 def moveDict(self): 0097 """a dict with attributes for the new position, 0098 normally pos, rotation and scale""" 0099 assert self.board 0100 width = self.tileset.faceSize.width() 0101 height = self.tileset.faceSize.height() 0102 shiftZ = self.board.shiftZ(self.level) 0103 boardPos = QPointF( 0104 self.xoffset * width, 0105 self.yoffset * height) + shiftZ 0106 scenePos = self.board.mapToScene(boardPos) 0107 return {'pos': scenePos, 'rotation': sceneRotation(self.board), 'scale': self.board.scale()} 0108 0109 def setDrawingOrder(self): 0110 """set drawing order for this tile""" 0111 if not isAlive(self): 0112 return 0113 if self.board: 0114 boardLevel = self.board.level 0115 else: 0116 boardLevel = ZValues.boardZFactor 0117 movingZ = 0 0118 # show moving tiles above non-moving tiles 0119 changePos = self.activeAnimation.get('pos') 0120 if changePos and not isAlive(changePos): 0121 return 0122 changeRotation = self.activeAnimation.get('rotation') 0123 if changeRotation and not isAlive(changeRotation): 0124 return 0125 changeScale = self.activeAnimation.get('scale') 0126 if changeScale and not isAlive(changeScale): 0127 return 0128 # show rotating and scaling tiles above all others 0129 if changeScale or changeRotation: 0130 movingZ += ZValues.movingZ 0131 movingZ += ZValues.boardZFactor 0132 elif changePos: 0133 if self.rotation % 180 == 0: 0134 currentY = self.y() 0135 newY = changePos.endValue().y() 0136 else: 0137 currentY = self.x() 0138 newY = changePos.endValue().x() 0139 if currentY != newY: 0140 movingZ += ZValues.movingZ 0141 self.setZValue(movingZ + 0142 boardLevel + 0143 (self.level + (2 if self.tile.isKnown else 1)) 0144 * ZValues.itemZFactor + 0145 self.__lightDistance()) 0146 0147 def boundingRect(self): 0148 """define the part of the tile we want to see. Do not return QRect() 0149 if tileset is not known because that makes QGraphicsscene crash""" 0150 if self.tileset: 0151 self._boundingRect = QRectF( 0152 QPointF(), 0153 self.tileset.tileSize if Internal.Preferences.showShadows 0154 else self.tileset.faceSize) 0155 return self._boundingRect 0156 0157 def facePos(self, showShadows=None): 0158 """return the face position relative to the tile 0159 depend on tileset, lightSource and shadow""" 0160 if showShadows is None: 0161 showShadows = Internal.Preferences.showShadows 0162 return self.board.tileFacePos(showShadows) 0163 0164 def showFace(self): 0165 """should we show face for this tile?""" 0166 return self.tile.isKnown 0167 0168 def __elementId(self, showShadows=None): 0169 """return the SVG element id of the tile""" 0170 if showShadows is None: 0171 showShadows = Internal.Preferences.showShadows 0172 if not showShadows: 0173 return "TILE_2" 0174 lightSourceIndex = LIGHTSOURCES.index(self.board.rotatedLightSource()) 0175 return "TILE_{}".format(lightSourceIndex % 4 + 1) 0176 0177 def paint(self, painter, unusedOption, unusedWidget=None): 0178 """paint the entire tile. 0179 I tried to cache a pixmap for the tile and darkener but without face, 0180 but that actually made it slower.""" 0181 with Painter(painter): 0182 renderer = self.tileset.renderer() 0183 withBorders = Internal.Preferences.showShadows 0184 if not withBorders: 0185 painter.scale(*self.tileset.tileFaceRelation()) 0186 renderer.render(painter, self.__elementId(), self.boundingRect()) 0187 self._drawDarkness(painter) 0188 with Painter(painter): 0189 if self.showFace(): 0190 if withBorders: 0191 faceSize = self.tileset.faceSize.toSize() 0192 renderer.render( 0193 painter, self.tileset.svgName[str(self.tile.exposed)], 0194 QRectF(self.facePos(), QSizeF(faceSize))) 0195 else: 0196 renderer.render( 0197 painter, self.tileset.svgName[str(self.tile.exposed)], 0198 self.boundingRect()) 0199 if self.cross: 0200 self.__paintCross(painter) 0201 0202 def __paintCross(self, painter): 0203 """paint a cross on the tile""" 0204 with Painter(painter): 0205 faceSize = self.tileset.faceSize 0206 width = faceSize.width() 0207 height = faceSize.height() 0208 painter.translate(self.facePos()) 0209 painter.drawLine(QPointF(0.0, 0.0), QPointF(width, height)) 0210 painter.drawLine(QPointF(width, 0.0), QPointF(0.0, height)) 0211 0212 def pixmapFromSvg(self, pmapSize, withBorders=None): 0213 """return a pixmap with default size as given in SVG 0214 and optional borders/shadows""" 0215 if withBorders is None: 0216 withBorders = Internal.Preferences.showShadows 0217 if withBorders: 0218 originalSize = self.tileset.tileSize.toSize() 0219 else: 0220 originalSize = self.tileset.faceSize.toSize() 0221 result = QPixmap(pmapSize) 0222 result.fill(Qt.transparent) 0223 painter = QPainter(result) 0224 if not painter.isActive(): 0225 logException( 0226 'painter is not active. Wanted size: %s' % 0227 str(pmapSize)) 0228 try: 0229 xScale = float(pmapSize.width()) / originalSize.width() 0230 yScale = float(pmapSize.height()) / originalSize.height() 0231 except ZeroDivisionError: 0232 xScale = 1 0233 yScale = 1 0234 # draw the tile too far to the left/upper side such that its shadow is outside of the print region 0235 if not withBorders: 0236 painter.scale(*self.tileset.tileFaceRelation()) 0237 renderer = self.tileset.renderer() 0238 renderer.render(painter, self.__elementId(showShadows=withBorders)) 0239 painter.resetTransform() 0240 self._drawDarkness(painter) 0241 if self.showFace(): 0242 faceSize = self.tileset.faceSize.toSize() 0243 faceSize = QSize( 0244 int(faceSize.width() * xScale), 0245 int(faceSize.height() * yScale)) 0246 painter.resetTransform() 0247 painter.translate(self.facePos(withBorders)) 0248 renderer.render(painter, self.tileset.svgName[self.tile.exposed], 0249 QRectF(QPointF(), QSizeF(faceSize))) 0250 return result 0251 0252 def _drawDarkness(self, painter): 0253 """if appropriate, make tiles darker. Mainly used for hidden tiles""" 0254 if self.dark: 0255 board = self.board 0256 rect = board.tileFaceRect().adjusted(-1, -1, -1, -1) 0257 color = QColor('black') 0258 color.setAlpha(self.tileset.darkenerAlpha) 0259 painter.fillRect(rect, color) 0260 0261 def sortKey(self, sortDir=Qt.Key_Right): 0262 """moving order for cursor""" 0263 dirs = [Qt.Key_Right, Qt.Key_Up, Qt.Key_Left, Qt.Key_Down] * 2 0264 sorter = dirs[dirs.index(sortDir) + sceneRotation(self.__board) // 90] 0265 if sorter == Qt.Key_Down: 0266 return self.xoffset * 100 + self.yoffset 0267 if sorter == Qt.Key_Up: 0268 return -(self.xoffset * 100 + self.yoffset) 0269 if sorter == Qt.Key_Left: 0270 return -(self.yoffset * 100 + self.xoffset) 0271 return self.yoffset * 100 + self.xoffset 0272 0273 def setBoard(self, board, xoffset=None, yoffset=None, level=0): 0274 """change Position of tile in board""" 0275 placeDirty = False 0276 if self.__board != board: 0277 oldBoard = self.__board 0278 self.__board = board 0279 if oldBoard: 0280 oldBoard.removeUITile(self) 0281 if board: 0282 board.addUITile(self) 0283 placeDirty = True 0284 if self.level != level: 0285 self.level = level 0286 placeDirty = True 0287 if xoffset is not None and xoffset != self.__xoffset: 0288 self.__xoffset = xoffset 0289 placeDirty = True 0290 if yoffset is not None and yoffset != self.__yoffset: 0291 self.__yoffset = yoffset 0292 placeDirty = True 0293 if board and placeDirty: 0294 if board.scene() and not self.scene(): 0295 board.scene().addItem(self) 0296 board.placeTile(self) 0297 0298 @property 0299 def tile(self): 0300 """tile""" 0301 return self._tile 0302 0303 @tile.setter 0304 def tile(self, value): # pylint: disable=arguments-differ 0305 """set tile name and update display""" 0306 if value is not self._tile: 0307 assert not self._tile.isKnown or (self._tile.exposed == value.exposed) 0308 self._tile = value 0309 self.setDrawingOrder() # because known tiles are above unknown tiles 0310 self.update() 0311 0312 @property 0313 def cross(self): 0314 """cross tiles in kongbox""" 0315 return self._cross 0316 0317 @cross.setter 0318 def cross(self, value): 0319 """cross tiles in kongbox""" 0320 if self._cross == value: 0321 return 0322 self._cross = value 0323 self.update() 0324 0325 @property 0326 def dark(self): 0327 """show face?""" 0328 return self.__dark 0329 0330 @dark.setter 0331 def dark(self, value): 0332 """toggle and update display""" 0333 if value != self.__dark: 0334 self.__dark = value 0335 self.update() 0336 0337 @property 0338 def focusable(self): 0339 """as the name says""" 0340 return bool(self.flags() & QGraphicsItem.ItemIsFocusable) 0341 0342 @focusable.setter 0343 def focusable(self, value): 0344 """redirect and generate Debug output""" 0345 if self.tile in Debug.focusable: 0346 newStr = 'focusable' if value else 'unfocusable' 0347 logDebug('%s: %s from %s' % (newStr, self.tile, stack('')[-2])) 0348 self.setFlag(QGraphicsItem.ItemIsFocusable, value) 0349 0350 @property 0351 def board(self): 0352 """get current board of this tile. Readonly.""" 0353 return self.__board 0354 0355 @property 0356 def xoffset(self): 0357 """in logical board coordinates""" 0358 return self.__xoffset 0359 0360 @xoffset.setter 0361 def xoffset(self, value): 0362 """in logical board coordinates""" 0363 if value != self.__xoffset: 0364 self.__xoffset = value 0365 if self.__board: 0366 self.__board.placeTile(self) 0367 0368 @property 0369 def yoffset(self): 0370 """in logical board coordinates""" 0371 return self.__yoffset 0372 0373 @yoffset.setter 0374 def yoffset(self, value): 0375 """in logical board coordinates. Update board display.""" 0376 if value != self.__yoffset: 0377 self.__yoffset = value 0378 if self.__board: 0379 self.__board.placeTile(self) 0380 0381 def __str__(self): 0382 """printable string with tile""" 0383 rotation = ' rot%d' % self.rotation if self.rotation else '' 0384 scale = ' scale=%.2f' % self.scale if self.scale != 1 else '' 0385 level = ' level=%d' % self.level if self.level else '' 0386 if self.boundingRect(): 0387 size = self.boundingRect() 0388 size = ' %.2dx%.2d' % (size.width(), size.height()) 0389 else: 0390 size = '' 0391 return '%s(%s) %d: x/y/z=%.1f(%.1f)/%.1f(%.1f)/%.2f%s%s%s%s' % \ 0392 (self.tile, 0393 self.board.name if self.board else 'None', id4(self), 0394 self.xoffset, self.x(), self.yoffset, 0395 self.y(), self.zValue(), size, rotation, scale, level) 0396 0397 @property 0398 def isBonus(self): 0399 """proxy for tile""" 0400 return self.tile.isBonus 0401 0402 0403 class UIMeld(list): 0404 0405 """represents a visible meld. Can be empty. Many Meld methods will 0406 raise exceptions if the meld is empty. But we do not care, 0407 those methods are not supposed to be called on empty melds. 0408 UIMeld is a list of UITile""" 0409 0410 __hash__ = None 0411 0412 def __init__(self, newContent): 0413 list.__init__(self) 0414 if ( 0415 isinstance(newContent, list) 0416 and newContent 0417 and isinstance(newContent[0], UITile)): 0418 self.extend(newContent) 0419 elif isinstance(newContent, UITile): 0420 self.append(newContent) 0421 assert self, newContent 0422 0423 @property 0424 def meld(self): 0425 """return a logical meld""" 0426 return Meld(x.tile for x in self)