File indexing completed on 2024-04-28 07:51:05

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2008-2014 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 from qt import Qt, QPointF, QPoint, QRectF, QMimeData, QSize
0011 from qt import QGraphicsRectItem, QSizePolicy, QFrame, QFont
0012 from qt import QGraphicsView, QLabel
0013 from qt import QColor, QPainter, QDrag, QPixmap, QStyleOptionGraphicsItem, QPen, QBrush
0014 from qt import QFontMetrics, QGraphicsObject
0015 from qt import QMenu, QCursor
0016 from qt import QGraphicsSvgItem
0017 from tileset import Tileset
0018 from tile import Tile, elements
0019 from uitile import UITile, UIMeld
0020 from guiutil import Painter, rotateCenter, sceneRotation
0021 from meld import Meld
0022 from animation import AnimationSpeed, animate, AnimatedMixin
0023 from message import Message
0024 
0025 from util import stack, uniqueList
0026 from log import logDebug, logException
0027 from mi18n import i18n, i18nc
0028 from common import LIGHTSOURCES, Internal, Debug, isAlive, StrMixin
0029 from common import DrawOnTopMixin
0030 from wind import Wind, East
0031 
0032 
0033 class PlayerWind(AnimatedMixin, QGraphicsObject, StrMixin, DrawOnTopMixin):
0034 
0035     """a round wind tile"""
0036 
0037     roundWindColor = QColor(235, 235, 173)
0038     whiteColor = QColor('white')
0039 
0040     def __init__(self, wind, parent=None):
0041         """generate new wind marker"""
0042         super().__init__()
0043         assert not parent
0044         assert isinstance(wind, Wind), 'wind {}  must be a real Wind but is {}'.format(
0045             wind, type(wind))
0046         self.__wind = wind
0047         self.__brush = self.whiteColor
0048         self.board = None
0049 
0050     def name(self):
0051         """for identification in animations"""
0052         return self.__wind.tile
0053 
0054     def moveDict(self):
0055         """a dict with attributes for the new position,
0056         normally pos, rotation and scale"""
0057         sideCenter = self.board.center()
0058         boardPos = QPointF(
0059             sideCenter.x() * 1.63,
0060             sideCenter.y() - self.boundingRect().height() / 2.0)
0061         scenePos = self.board.mapToScene(boardPos)
0062         return {'pos': scenePos, 'rotation': sceneRotation(self.board)}
0063 
0064     @property
0065     def wind(self):
0066         """our wind"""
0067         return self.__wind
0068 
0069     @property
0070     def prevailing(self):
0071         """is this the prevailing wind?"""
0072         return self.__brush is self.roundWindColor
0073 
0074     @prevailing.setter
0075     def prevailing(self, value):
0076         if isinstance(value, bool):
0077             newPrevailing = value
0078         else:
0079             newPrevailing = self.wind == Wind.all4[value % 4]
0080         self.__brush = self.roundWindColor if newPrevailing else self.whiteColor
0081 
0082     def paint(self, painter, unusedOption, unusedWidget=None):
0083         """paint the marker"""
0084         with Painter(painter):
0085             painter.setBrush(self.__brush)
0086             size = int(Internal.scene.windTileset.faceSize.height())
0087             ellRect = QRectF(QPointF(), QPointF(size, size))
0088             painter.drawEllipse(ellRect)
0089             renderer = Internal.scene.windTileset.renderer()
0090             painter.translate(12, 12)
0091             painter.scale(0.60, 0.60)
0092             renderer.render(painter, self.wind.markerSvgName, self.boundingRect())
0093 
0094     def boundingRect(self):  # pylint: disable=no-self-use
0095         """define the part of the tile we want to see"""
0096         size = int(Internal.scene.windTileset.faceSize.height() * 1.1)
0097         return QRectF(QPointF(), QPointF(size, size))
0098 
0099     def __str__(self):
0100         """for debugging"""
0101         return 'WindMarker(%s x/y= %.1f/%1f)' % (
0102             self.name(), self.x(), self.y())
0103 
0104 
0105 class WindLabel(QLabel):
0106 
0107     """QLabel holding the wind tile"""
0108 
0109     @property
0110     def wind(self):
0111         """the current wind on this label"""
0112         return self.__wind
0113 
0114     @wind.setter
0115     def wind(self, wind):
0116         """setting the wind also changes the pixmap"""
0117         if self.__wind != wind:
0118             self.__wind = wind
0119             self._refresh()
0120 
0121     def __init__(self, wind=None, roundsFinished=0, parent=None):
0122         QLabel.__init__(self, parent)
0123         self.__wind = None
0124         if wind is None:
0125             wind = East
0126         self.__roundsFinished = roundsFinished
0127         self.wind = wind
0128 
0129     @property
0130     def roundsFinished(self):
0131         """setting roundsFinished also changes graphics if needed"""
0132         return self.__roundsFinished
0133 
0134     @roundsFinished.setter
0135     def roundsFinished(self, roundsFinished):
0136         """setting roundsFinished also changes graphics if needed"""
0137         if self.__roundsFinished != roundsFinished:
0138             self.__roundsFinished = roundsFinished
0139             self._refresh()
0140 
0141     def _refresh(self):
0142         """update graphics"""
0143         self.setPixmap(self.genWindPixmap())
0144 
0145     def genWindPixmap(self):
0146         """prepare wind tiles"""
0147         pwind = PlayerWind(self.__wind)
0148         pwind.prevailing = self.__wind == Wind.all4[min(self.__roundsFinished, 3)]
0149         pMap = QPixmap(40, 40)
0150         pMap.fill(Qt.transparent)
0151         painter = QPainter(pMap)
0152         painter.setRenderHint(QPainter.Antialiasing)
0153         painter.scale(0.40, 0.40)
0154         pwind.paint(painter, QStyleOptionGraphicsItem())
0155         for child in pwind.childItems():
0156             if isinstance(child, QGraphicsSvgItem):
0157                 with Painter(painter):
0158                     painter.translate(child.mapToParent(0.0, 0.0))
0159                     child.paint(painter, QStyleOptionGraphicsItem())
0160         return pMap
0161 
0162 
0163 class Board(QGraphicsRectItem, StrMixin):
0164 
0165     """ a board with any number of positioned tiles"""
0166     # pylint: disable=too-many-instance-attributes
0167 
0168     penColor = 'black'
0169 
0170     arrows = [Qt.Key_Left, Qt.Key_Down, Qt.Key_Up, Qt.Key_Right]
0171 
0172     def __init__(self, width, height, tileset, boardRotation=0):
0173         QGraphicsRectItem.__init__(self)
0174         self.uiTiles = []
0175         self.isHandBoard = False
0176         self._focusTile = None
0177         self.__prevPos = 0
0178         self._noPen()
0179         self.tileDragEnabled = False
0180         self.setRotation(boardRotation)
0181         self._lightSource = 'NW'
0182         self.__xWidth = 0
0183         self.__xHeight = 0
0184         self.__yWidth = 0
0185         self.__yHeight = 0
0186         self.__fixedWidth = width
0187         self.__fixedHeight = height
0188         self._tileset = None
0189         self.tileset = tileset
0190         self.level = 0
0191         Internal.Preferences.addWatch('showShadows', self.showShadowsChanged)
0192 
0193     @property
0194     def name(self):  # pylint: disable=no-self-use
0195         """default board name, used for debugging messages"""
0196         return 'board'
0197 
0198     def __str__(self):
0199         """for debugging"""
0200         return self.name
0201 
0202     def setVisible(self, value):
0203         """also update focusRect if it belongs to this board"""
0204         if self.scene() and isAlive(self):
0205             QGraphicsRectItem.setVisible(self, value)
0206             self.scene().focusRect.refresh()
0207 
0208     def hide(self):
0209         """remove all uiTile references so they can be garbage collected"""
0210         for uiTile in self.uiTiles:
0211             uiTile.hide()
0212         self.uiTiles = []
0213         self._focusTile = None
0214         if isAlive(self):
0215             self.setVisible(False)
0216 
0217     def autoSelectTile(self):
0218         """call this when Kajongg should automatically focus
0219         on an appropriate uiTile"""
0220         focusCandidates = self._focusableTiles()
0221         if focusCandidates:
0222             firstCandidate = focusCandidates[0]
0223             if self._focusTile not in focusCandidates:
0224                 focusCandidates = [x for x in focusCandidates if x.sortKey() >= self.__prevPos]
0225                 focusCandidates.append(firstCandidate)
0226                 self.focusTile = focusCandidates[0]
0227 
0228     @property
0229     def currentFocusTile(self):
0230         """get focusTile without selecting one"""
0231         return self._focusTile
0232 
0233     @property
0234     def focusTile(self):
0235         """the uiTile of this board with focus. This is per Board!"""
0236         if self._focusTile is None:
0237             self.autoSelectTile()
0238         return self._focusTile
0239 
0240     @focusTile.setter
0241     def focusTile(self, uiTile):
0242         """the uiTile of this board with focus. This is per Board!"""
0243         if uiTile is self._focusTile:
0244             return
0245         if uiTile:
0246             assert uiTile.tile.isKnown, uiTile
0247             if not isinstance(uiTile.board, DiscardBoard):
0248                 assert uiTile.focusable, uiTile
0249             self.__prevPos = uiTile.sortKey()
0250         self._focusTile = uiTile
0251         if self._focusTile and self._focusTile.tile in Debug.focusable:
0252             logDebug('%s: new focus uiTile %s from %s' % (
0253                 self.name, self._focusTile.tile if self._focusTile else 'None', stack('')[-1]))
0254         if self.hasLogicalFocus:
0255             self.scene().focusBoard = self
0256 
0257     def setEnabled(self, enabled):
0258         """enable/disable this board"""
0259         self.tileDragEnabled = enabled
0260         QGraphicsRectItem.setEnabled(self, enabled)
0261 
0262     def _focusableTiles(self, sortDir=Qt.Key_Right):
0263         """return a list of all tiles in this board sorted such that
0264         moving in the sortDir direction corresponds to going to
0265         the next list element.
0266         respect board orientation: Right Arrow should always move right
0267         relative to the screen, not relative to the board"""
0268         return sorted((x for x in self.uiTiles if x.focusable), key=lambda x: x.sortKey(sortDir))
0269 
0270     @property
0271     def hasLogicalFocus(self):
0272         """defines if this board should show a focusRect
0273         if another board has focus, setting this to False does
0274         not change scene.focusBoard
0275 
0276         Up to May 2021, this was called hasFocus, overriding QGraphicsItem.hasFocus
0277         but pylint did not like that."""
0278         return self.scene() and self.scene().focusBoard == self and self._focusTile
0279 
0280     @hasLogicalFocus.setter
0281     def hasLogicalFocus(self, value):
0282         """set focus on this board"""
0283         if isAlive(self):
0284             scene = self.scene()
0285             if isAlive(scene):
0286                 if scene.focusBoard == self or value:
0287                     if self.focusTile:
0288                         assert self.focusTile.board == self, '%s not in self %s' % (
0289                             self.focusTile, self)
0290                 if value:
0291                     scene.focusBoard = self
0292 
0293     @staticmethod
0294     def mapChar2Arrow(event):
0295         """map the keys hjkl to arrows like in vi and konqueror"""
0296         key = event.key()
0297         if key in Board.arrows:
0298             return key
0299         charArrows = i18nc(
0300             'kajongg:arrow keys hjkl like in konqueror',
0301             'hjklHJKL')
0302         key = event.text()
0303         if key and key in charArrows:
0304             key = Board.arrows[charArrows.index(key) % 4]
0305         return key
0306 
0307     def keyPressEvent(self, event):
0308         """navigate in the board"""
0309         key = Board.mapChar2Arrow(event)
0310         if key in Board.arrows:
0311             self.__moveCursor(key)
0312         else:
0313             QGraphicsRectItem.keyPressEvent(self, event)
0314 
0315     def __moveCursor(self, key):
0316         """move focus"""
0317         tiles = self._focusableTiles(key)
0318         if tiles:
0319             # sometimes the handBoard still has focus but
0320             # has no focusable tiles. Like after declaring
0321             # Original Call.
0322             oldPos = self.focusTile.xoffset, self.focusTile.yoffset
0323             tiles = [
0324                 x for x in tiles if (x.xoffset,
0325                                      x.yoffset) != oldPos or x == self.focusTile]
0326             assert tiles, [str(x) for x in self.uiTiles]
0327             tiles.append(tiles[0])
0328             self.focusTile = tiles[tiles.index(self.focusTile) + 1]
0329 
0330     def mapMouseTile(self, uiTile):  # pylint: disable=no-self-use
0331         """map the pressed tile to the wanted tile. For melds, this would
0332         be the first tile no matter which one is pressed"""
0333         return uiTile
0334 
0335     def uiMeldWithTile(self, uiTile):  # pylint: disable=no-self-use
0336         """return the UI Meld with uiTile. A Board does not know about melds,
0337         so default is to return a Meld with only uiTile"""
0338         return UIMeld(uiTile)
0339 
0340     def meldVariants(self, tile, lowerHalf):  # pylint: disable=no-self-use,unused-argument
0341         """all possible melds that could be meant by dragging/dropping uiTile"""
0342         return [Meld(tile)]
0343 
0344     def chooseVariant(self, uiTile, lowerHalf=False):
0345         """make the user choose from a list of possible melds for the target.
0346         The melds do not contain real Tiles, just the scoring strings."""
0347         variants = self.meldVariants(uiTile, lowerHalf)
0348         idx = 0
0349         if len(variants) > 1:
0350             menu = QMenu(i18n('Choose from'))
0351             for idx, variant in enumerate(variants):
0352                 action = menu.addAction(variant.typeName())
0353                 action.setData(idx)
0354             if Internal.scene.mainWindow.centralView.dragObject:
0355                 menuPoint = QCursor.pos()
0356             else:
0357                 menuPoint = uiTile.board.tileFaceRect().bottomRight()
0358                 view = Internal.scene.mainWindow.centralView
0359                 menuPoint = view.mapToGlobal(
0360                     view.mapFromScene(uiTile.mapToScene(menuPoint)))
0361             action = menu.exec_(menuPoint)
0362             if not action:
0363                 return None
0364             idx = action.data()
0365         return variants[idx]
0366 
0367     def dragEnterEvent(self, unusedEvent):
0368         """drag enters the HandBoard: highlight it"""
0369         self.setPen(QPen(QColor(self.penColor)))
0370 
0371     def dragLeaveEvent(self, unusedEvent):
0372         """drag leaves the HandBoard"""
0373         self._noPen()
0374 
0375     def _noPen(self):
0376         """remove pen for this board. The pen defines the border"""
0377         if Debug.graphics:
0378             self.setPen(QPen(QColor(self.penColor)))
0379         else:
0380             self.setPen(QPen(Qt.NoPen))
0381 
0382     def tileAt(self, xoffset, yoffset, level=0):
0383         """if there is a uiTile at this place, return it"""
0384         for uiTile in self.uiTiles:
0385             if (uiTile.xoffset, uiTile.yoffset, uiTile.level) == (xoffset, yoffset, level):
0386                 return uiTile
0387         return None
0388 
0389     def tilesByElement(self, element):
0390         """return all child items holding a uiTile for element"""
0391         return [x for x in self.uiTiles if x.tile is element]
0392 
0393     def rotatedLightSource(self):
0394         """the light source we need for the original uiTile before it is rotated"""
0395         lightSourceIndex = LIGHTSOURCES.index(self.lightSource)
0396         lightSourceIndex = (lightSourceIndex + sceneRotation(self) // 90) % 4
0397         return LIGHTSOURCES[lightSourceIndex]
0398 
0399     def tileFacePos(self, showShadows=None):
0400         """the face pos of a uiTile relative to its origin"""
0401         if showShadows is None:
0402             showShadows = Internal.Preferences.showShadows
0403         if not showShadows:
0404             return QPointF()
0405         lightSource = self.rotatedLightSource()
0406         xoffset = self.tileset.shadowWidth() - 1 if 'E' in lightSource else 0
0407         yoffset = self.tileset.shadowHeight() - 1 if 'S' in lightSource else 0
0408         return QPointF(xoffset, yoffset)
0409 
0410     def tileFaceRect(self):
0411         """the face rect of a uiTile relative its origin"""
0412         return QRectF(self.tileFacePos(), self.tileset.faceSize)
0413 
0414     def setPos(self, xWidth=0, xHeight=0, yWidth=0, yHeight=0):
0415         """set the position in the parent item expressing the position in tile face units.
0416         The X position is xWidth*facewidth + xHeight*faceheight, analog for Y"""
0417         self.__xWidth = xWidth
0418         self.__xHeight = xHeight
0419         self.__yWidth = yWidth
0420         self.__yHeight = yHeight
0421         self.setGeometry()
0422 
0423     def setRect(self, width, height):
0424         """gives the board a fixed size in uiTile coordinates"""
0425         self.__fixedWidth = width
0426         self.__fixedHeight = height
0427         self.computeRect()
0428 
0429     def computeRect(self):
0430         """translate from our rect coordinates to scene coord"""
0431         sizeX = self.tileset.faceSize.width() * self.__fixedWidth
0432         sizeY = self.tileset.faceSize.height() * self.__fixedHeight
0433         if Internal.Preferences.showShadows:
0434             sizeX += self.tileset.shadowWidth() + \
0435                 2 * self.tileset.shadowHeight()
0436             sizeY += self.tileset.shadowHeight()
0437         rect = self.rect()
0438         rect.setWidth(sizeX)
0439         rect.setHeight(sizeY)
0440         self.prepareGeometryChange()
0441         QGraphicsRectItem.setRect(self, rect)
0442 
0443     @property
0444     def width(self):
0445         """getter for width"""
0446         return self.__fixedWidth
0447 
0448     @property
0449     def height(self):
0450         """getter for width"""
0451         return self.__fixedHeight
0452 
0453     def setGeometry(self):
0454         """move the board to the correct position and set its rect surrounding all its
0455         items. This is needed for enabling drops into the board.
0456         This is also called when the tileset or the light source for this board changes"""
0457         width = self.tileset.faceSize.width()
0458         height = self.tileset.faceSize.height()
0459         if not Internal.Preferences.showShadows:
0460             offsets = (0, 0)
0461         elif self.isHandBoard:
0462             offsets = (-self.tileset.shadowHeight() * 2, 0)
0463         else:
0464             offsets = self.tileset.shadowOffsets(
0465                 self._lightSource,
0466                 sceneRotation(self))
0467         newX = self.__xWidth * width + self.__xHeight * height + offsets[0]
0468         newY = self.__yWidth * width + self.__yHeight * height + offsets[1]
0469         QGraphicsRectItem.setPos(self, newX, newY)
0470 
0471     def showShadowsChanged(self, unusedOldValue, newValue):
0472         """set active lightSource"""
0473         for uiTile in self.uiTiles:
0474             uiTile.setClippingFlags()
0475         self._reload(self.tileset, showShadows=newValue)
0476 
0477     @property
0478     def lightSource(self):
0479         """the active lightSource"""
0480         return self._lightSource
0481 
0482     @lightSource.setter
0483     def lightSource(self, lightSource):
0484         """set active lightSource"""
0485         if self._lightSource != lightSource:
0486             if lightSource not in LIGHTSOURCES:
0487                 logException('lightSource %s illegal' % lightSource)
0488             self._reload(self.tileset, lightSource)
0489 
0490     @property
0491     def tileset(self):
0492         """get/set the active tileset and resize accordingly"""
0493         return self._tileset
0494 
0495     @tileset.setter
0496     def tileset(self, tileset):
0497         """get/set the active tileset and resize accordingly"""
0498         self._reload(tileset, self._lightSource)
0499 
0500     def _reload(self, tileset=None, lightSource=None, showShadows=None):
0501         """call this if tileset or lightsource change: recomputes the entire board"""
0502         if tileset is None:
0503             tileset = self.tileset
0504         if lightSource is None:
0505             lightSource = self._lightSource
0506         if showShadows is None:
0507             showShadows = Internal.Preferences.showShadows
0508         if (self._tileset != tileset
0509                 or self._lightSource != lightSource
0510                 or Internal.Preferences.showShadows != showShadows):
0511             self.prepareGeometryChange()
0512             self._tileset = tileset
0513             self._lightSource = lightSource
0514             self.setGeometry()
0515             for child in self.childItems():
0516                 if isinstance(child, Board):
0517                     child.lightSource = lightSource
0518                     child.showShadows = showShadows
0519             for uiTile in self.uiTiles:
0520                 self.placeTile(uiTile)
0521                 uiTile.update()
0522             self.computeRect()
0523             if self.hasLogicalFocus:
0524                 self.scene().focusBoard = self
0525 
0526     def focusRectWidth(self):  # pylint: disable=no-self-use
0527         """how many tiles are in focus rect?"""
0528         return 1
0529 
0530     def shiftZ(self, level):
0531         """used for 3D: compute the needed shift for the uiTile.
0532         level is the vertical position. 0 is the face position on
0533         ground level, -1 is the imprint a uiTile makes on the
0534         surface it stands on"""
0535         if not Internal.Preferences.showShadows:
0536             return QPointF()
0537         shiftX = 0
0538         shiftY = 0
0539         if level != 0:
0540             lightSource = self.rotatedLightSource()
0541             stepX = level * self.tileset.shadowWidth() / 2
0542             stepY = level * self.tileset.shadowHeight() / 2
0543             if 'E' in lightSource:
0544                 shiftX = stepX
0545             if 'W' in lightSource:
0546                 shiftX = -stepX
0547             if 'N' in lightSource:
0548                 shiftY = -stepY
0549             if 'S' in lightSource:
0550                 shiftY = stepY
0551         return QPointF(shiftX, shiftY)
0552 
0553     def tileSize(self):
0554         """the current uiTile size"""
0555         return self._tileset.tileSize
0556 
0557     def faceSize(self):
0558         """the current face size"""
0559         return self._tileset.faceSize
0560 
0561     def placeTile(self, uiTile):
0562         """places the uiTile in the scene"""
0563         assert isinstance(uiTile, UITile)
0564         assert uiTile.board == self
0565         uiTile.setupAnimations()
0566 
0567     def addUITile(self, uiTile):
0568         """add uiTile to this board"""
0569         self.uiTiles.append(uiTile)
0570 
0571     def removeUITile(self, uiTile):
0572         """remove uiTile from this board"""
0573         self.uiTiles.remove(uiTile)
0574         if self.currentFocusTile == uiTile:
0575             self.focusTile = None
0576 
0577 
0578 class CourtBoard(Board):
0579 
0580     """A Board that is displayed within the wall"""
0581     penColor = 'green'
0582 
0583     def __init__(self, width, height):
0584         Board.__init__(self, width, height, Tileset.current())
0585         self.setAcceptDrops(True)
0586 
0587     def maximize(self):
0588         """make it as big as possible within the wall"""
0589         cWall = Internal.scene.game.wall
0590         newSceneX = cWall[3].sceneBoundingRect().right()
0591         newSceneY = cWall[2].sceneBoundingRect().bottom()
0592         tileset = self.tileset
0593         xAvail = cWall[1].sceneBoundingRect().left() - newSceneX
0594         yAvail = cWall[0].sceneBoundingRect().top() - newSceneY
0595         shadowHeight = tileset.shadowHeight()
0596         shadowWidth = tileset.shadowWidth()
0597         if Internal.Preferences.showShadows:
0598             # this should use the real shadow values from the wall because the wall
0599             # tiles are smaller than those in the CourtBoard but this should be
0600             # good enough
0601             newSceneX -= shadowHeight / 2 if 'W' in self.lightSource else 0
0602             newSceneY -= shadowWidth / 2 if 'N' in self.lightSource else 0
0603             xAvail -= shadowHeight if 'E' in self.lightSource else 0
0604             yAvail -= shadowWidth if 'S' in self.lightSource else 0
0605         xNeeded = self.width * tileset.faceSize.width()
0606         yNeeded = self.height * tileset.faceSize.height()
0607         xScaleFactor = xAvail / xNeeded
0608         yScaleFactor = yAvail / yNeeded
0609         QGraphicsRectItem.setPos(self, newSceneX, newSceneY)
0610         self.setScale(min(xScaleFactor, yScaleFactor))
0611         for uiTile in self.uiTiles:
0612             uiTile.board.placeTile(uiTile)
0613 
0614 
0615 class SelectorBoard(CourtBoard):
0616 
0617     """a board containing all possible tiles for selection"""
0618     # pylint: disable=too-many-public-methods
0619 
0620     def __init__(self):
0621         CourtBoard.__init__(self, 9, 5)
0622         self.allSelectorTiles = []
0623 
0624     def checkTiles(self):
0625         """does not apply"""
0626 
0627     def load(self, game):
0628         """load the tiles according to game.ruleset"""
0629         for uiTile in self.uiTiles:
0630             uiTile.setBoard(None)
0631         self.uiTiles = []
0632         self.allSelectorTiles = [UITile(x) for x in elements.all(game.ruleset)]
0633         self.refill()
0634 
0635     def refill(self):
0636         """move all tiles back into the selector"""
0637         with AnimationSpeed():
0638             for uiTile in self.allSelectorTiles:
0639                 uiTile.tile = uiTile.tile.exposed
0640                 self.__placeAvailable(uiTile)
0641                 uiTile.dark = False
0642                 uiTile.focusable = True
0643             self.focusTile = self.tilesByElement(Tile('c1'))[0]
0644 
0645     @property
0646     def name(self):  # pylint: disable=no-self-use
0647         """for debugging messages"""
0648         return 'selector'
0649 
0650     def dragMoveEvent(self, event):
0651         """allow dropping only from handboards, not from self"""
0652         uiTile = event.mimeData().uiTile
0653         event.setAccepted(uiTile.board != self)
0654 
0655     def dropEvent(self, event):
0656         """drop a uiTile into the selector"""
0657         uiTile = event.mimeData().uiTile
0658         self.dropTile(uiTile)
0659         event.accept()
0660 
0661     def dropTile(self, uiTile, lowerHalf=False):  # pylint: disable=unused-argument
0662         """drop uiTile into selector board"""
0663         uiMeld = uiTile.board.uiMeldWithTile(uiTile)
0664         self.dropMeld(uiMeld)
0665 
0666     def dropMeld(self, uiMeld):
0667         """drop uiMeld into selector board"""
0668         senderHand = uiMeld[0].board
0669         if senderHand == self:
0670             return
0671         for myTile in uiMeld:
0672             self.__placeAvailable(myTile)
0673             myTile.focusable = True
0674         senderHand.deselect(uiMeld)
0675         (senderHand if senderHand.uiTiles else self).hasLogicalFocus = True
0676         self._noPen()
0677         animate()
0678 
0679     def assignUITiles(self, uiTile, meld):
0680         """generate a UIMeld. First uiTile is given, the rest should be as defined by meld"""
0681         assert isinstance(uiTile, UITile), uiTile
0682         result = UIMeld(uiTile)
0683         for tile in meld[1:]:
0684             baseTiles = [
0685                 x for x in self.tilesByElement(
0686                     tile.exposed) if x not in result]
0687             result.append(baseTiles[0])
0688         return result
0689 
0690     def deselect(self, meld):
0691         """we are going to lose those tiles or melds"""
0692 
0693     def __placeAvailable(self, uiTile):
0694         """place the uiTile in the selector at its place"""
0695         # define coordinates and order for tiles:
0696         offsets = {
0697             Tile.dragon: (3, 6, Tile.dragons),
0698             Tile.flower: (4, 5, Tile.winds),
0699             Tile.season: (4, 0, Tile.winds),
0700             Tile.wind: (3, 0, Tile.winds),
0701             Tile.bamboo: (1, 0, Tile.numbers),
0702             Tile.stone: (2, 0, Tile.numbers),
0703             Tile.character: (0, 0, Tile.numbers)}
0704         row, baseColumn, order = offsets[uiTile.tile.lowerGroup]
0705         column = baseColumn + order.index(uiTile.tile.char)
0706         uiTile.dark = False
0707         uiTile.setBoard(self, column, row)
0708 
0709     def meldVariants(self, tile, lowerHalf):
0710         """return a list of possible variants based on meld. Those are logical melds."""
0711         # pylint: disable=too-many-locals
0712         assert isinstance(tile, UITile)
0713         wantedTile = tile.tile
0714         for selectorTile in self.uiTiles:
0715             selectorTile.tile = selectorTile.tile.exposed
0716         lowerName = wantedTile.exposed
0717         upperName = wantedTile.concealed
0718         if lowerHalf:
0719             scName = upperName
0720         else:
0721             scName = lowerName
0722         result = [scName.single]
0723         baseTiles = len(self.tilesByElement(lowerName))
0724         if baseTiles >= 2:
0725             result.append(scName.pair)
0726         if baseTiles >= 3:
0727             result.append(scName.pung)
0728         if baseTiles == 4:
0729             if lowerHalf:
0730                 result.append(lowerName.kong.declared)
0731             else:
0732                 result.append(lowerName.kong)
0733                 result.append(lowerName.kong.exposedClaimed)
0734         if wantedTile.isNumber and wantedTile.value < 8:
0735             chow2 = scName.nextForChow
0736             chow3 = chow2.nextForChow
0737             if self.tilesByElement(chow2.exposed) and self.tilesByElement(chow3.exposed):
0738                 result.append(scName.chow)
0739         # result now holds a list of melds
0740         return result
0741 
0742 
0743 class MimeData(QMimeData):
0744 
0745     """we also want to pass a reference to the moved meld"""
0746 
0747     def __init__(self, uiTile):
0748         QMimeData.__init__(self)
0749         self.uiTile = uiTile
0750         self.setText(str(uiTile))
0751 
0752 
0753 class FittingView(QGraphicsView):
0754 
0755     """a graphics view that always makes sure the whole scene is visible"""
0756 
0757     def __init__(self, parent=None):
0758         """generate a fitting view with our favourite properties"""
0759         QGraphicsView.__init__(self, parent)
0760         self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
0761         self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
0762         vpol = QSizePolicy()
0763         vpol.setHorizontalPolicy(QSizePolicy.Expanding)
0764         vpol.setVerticalPolicy(QSizePolicy.Expanding)
0765         self.setSizePolicy(vpol)
0766         self.setRenderHint(QPainter.Antialiasing)
0767         self.setRenderHint(QPainter.SmoothPixmapTransform)
0768         self.setStyleSheet('background: transparent')
0769         self.setFrameStyle(QFrame.NoFrame)
0770         self.tilePressed = None
0771         self.dragObject = None
0772         self.setFocus()
0773 
0774     def wheelEvent(self, event):  # pylint: disable=no-self-use
0775         """we do not want scrolling for the scene view.
0776         Instead scrolling down changes perspective like in kmahjongg"""
0777         angleX = event.angleDelta().x()
0778         angleY = event.angleDelta().y()
0779         if angleX > 15 or angleY > -50:
0780             return
0781         Internal.mainWindow.changeAngle()
0782 
0783     def resizeEvent(self, unusedEvent):
0784         """scale the scene and its background for new view size"""
0785         Internal.Preferences.callTrigger(
0786             'tilesetName')  # this redraws and resizes
0787         Internal.Preferences.callTrigger('backgroundName')  # redraw background
0788         if Internal.scaleScene and self.scene():
0789             self.fitInView(
0790                 self.scene().itemsBoundingRect(),
0791                 Qt.KeepAspectRatio)
0792         self.setFocus()
0793 
0794     def __matchingTile(self, position, uiTile):
0795         """is position in the clickableRect of this uiTile?"""
0796         if not isinstance(uiTile, UITile):
0797             return False
0798         itemPos = uiTile.mapFromScene(self.mapToScene(position))
0799         return uiTile.board.tileFaceRect().contains(itemPos)
0800 
0801     def tileAt(self, position):
0802         """find out which uiTile is clickable at this position. Always
0803         returns a list. If there are several tiles above each other,
0804         return all of them, highest first"""
0805         allTiles = [x for x in self.items(position) if isinstance(x, UITile)]
0806         items = [x for x in allTiles if self.__matchingTile(position, x)]
0807         if not items:
0808             return None
0809         for item in items[:]:
0810             for other in allTiles:
0811                 if (other.xoffset, other.yoffset) == (item.xoffset, item.yoffset):
0812                     if other.level > item.level:
0813                         items.append(other)
0814         return uniqueList(sorted(items, key=lambda x: -x.level))
0815 
0816     def mousePressEvent(self, event):
0817         """set blue focus frame"""
0818         tiles = self.tileAt(event.pos())
0819         if tiles:
0820             if event.modifiers() & Qt.ShiftModifier:
0821                 for uiTile in tiles:
0822                     print(
0823                         '%s: board.level:%s' %
0824                         (str(uiTile), uiTile.board.level))
0825             board = tiles[0].board
0826             uiTile = board.mapMouseTile(tiles[0])
0827             if uiTile.focusable:
0828                 board.focusTile = uiTile
0829                 board.hasLogicalFocus = True
0830                 if hasattr(Internal.scene, 'clientDialog'):
0831                     if Internal.scene.clientDialog:
0832                         Internal.scene.clientDialog.buttons[0].setFocus()
0833                 self.tilePressed = uiTile
0834             else:
0835                 event.ignore()
0836         else:
0837             self.tilePressed = None
0838             event.ignore()
0839 
0840     def mouseReleaseEvent(self, event):
0841         """release self.tilePressed"""
0842         self.tilePressed = None
0843         return QGraphicsView.mouseReleaseEvent(self, event)
0844 
0845     def mouseMoveEvent(self, event):
0846         """selects the correct uiTile"""
0847         tilePressed = self.tilePressed
0848         self.tilePressed = None
0849         if tilePressed:
0850             board = tilePressed.board
0851             if board and board.tileDragEnabled:
0852                 self.dragObject = self.drag(tilePressed)
0853                 self.dragObject.exec_(Qt.MoveAction)
0854                 self.dragObject = None
0855                 return None
0856         return QGraphicsView.mouseMoveEvent(self, event)
0857 
0858     def drag(self, uiTile):
0859         """return a drag object"""
0860         drag = QDrag(self)
0861         mimeData = MimeData(uiTile)
0862         drag.setMimeData(mimeData)
0863         tRect = uiTile.boundingRect()
0864         tRect = self.viewportTransform().mapRect(tRect)
0865         pmapSize = QSize(
0866             int(tRect.width() * uiTile.scale),
0867             int(tRect.height() * uiTile.scale))
0868         pMap = uiTile.pixmapFromSvg(pmapSize)
0869         drag.setPixmap(pMap)
0870         drag.setHotSpot(QPoint(pMap.width() // 2, pMap.height() // 2))
0871         return drag
0872 
0873 
0874 class YellowText(QGraphicsRectItem):
0875 
0876     """a yellow rect with a message, used for claims"""
0877 
0878     def __init__(self, side):
0879         QGraphicsRectItem.__init__(self, side)
0880         self.side = side
0881         self.font = QFont()
0882         self.font.setPointSize(48)
0883         self.height = 62
0884         self.width = 200
0885         self.msg = None
0886         self.setText('')
0887 
0888     def setText(self, msg):
0889         """set the text of self"""
0890         self.msg = '%s  ' % msg
0891         metrics = QFontMetrics(self.font)
0892         self.width = metrics.width(self.msg)
0893         self.height = metrics.lineSpacing() * 1.1
0894         self.setRect(0, 0, self.width, self.height)
0895         self.resetTransform()
0896         self.setPos(self.side.center())
0897         rotation = self.side.rotation()
0898         rotateCenter(self, -rotation)
0899         xOffset = -self.rect().width() / 2
0900         yOffset = -self.rect().height() / 2
0901         if rotation % 180 == 0:
0902             self.moveBy(xOffset, yOffset * 4)
0903         else:
0904             self.moveBy(xOffset, yOffset)
0905 
0906     def paint(self, painter, unusedOption, unusedWidget):
0907         """override predefined paint"""
0908         painter.setFont(self.font)
0909         painter.fillRect(self.rect(), QBrush(QColor('yellow')))
0910         painter.drawText(self.rect(), self.msg)
0911 
0912 
0913 class DiscardBoard(CourtBoard):
0914 
0915     """A special board for discarded tiles"""
0916     # pylint: disable=too-many-public-methods
0917     penColor = 'orange'
0918 
0919     def __init__(self):
0920         CourtBoard.__init__(self, 11, 9)
0921         self.__places = None
0922         self.lastDiscarded = None
0923 
0924     @property
0925     def name(self):  # pylint: disable=no-self-use
0926         """to be used in debug output"""
0927         return "discardBoard"
0928 
0929     def hide(self):
0930         """remove all uiTile references so they can be garbage collected"""
0931         self.lastDiscarded = None
0932         CourtBoard.hide(self)
0933 
0934     def setRandomPlaces(self, randomGenerator):
0935         """precompute random positions"""
0936         self.__places = [(x, y) for x in range(self.width)
0937                          for y in range(self.height)]
0938         randomGenerator.shuffle(self.__places)
0939 
0940     def discardTile(self, uiTile):
0941         """add uiTile to a random position"""
0942         assert isinstance(uiTile, UITile)
0943         uiTile.setBoard(self, *self.__places.pop(0))
0944         uiTile.dark = False
0945         uiTile.focusable = False
0946         self.focusTile = uiTile
0947         self.hasLogicalFocus = True
0948         self.lastDiscarded = uiTile
0949 
0950     def dropEvent(self, event):
0951         """drop a uiTile into the discard board"""
0952         uiTile = event.mimeData().uiTile
0953         assert isinstance(uiTile, UITile), uiTile
0954         uiTile.setPos(event.scenePos() - uiTile.boundingRect().center())
0955         Internal.scene.clientDialog.selectButton(Message.Discard)
0956         event.accept()
0957         self._noPen()