File indexing completed on 2024-03-24 04:04:31
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()