File indexing completed on 2024-04-21 07:49:06

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)