File indexing completed on 2024-04-14 03:59:13
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 # pylint: disable=ungrouped-imports 0011 0012 from qt import Qt, QPointF, QSize, QModelIndex, QEvent, QTimer 0013 0014 from qt import QColor, QPushButton, QPixmapCache 0015 from qt import QWidget, QLabel, QTabWidget 0016 from qt import QGridLayout, QVBoxLayout, QHBoxLayout, QSpinBox 0017 from qt import QDialog, QStringListModel, QListView, QSplitter, QValidator 0018 from qt import QIcon, QPixmap, QPainter, QDialogButtonBox 0019 from qt import QSizePolicy, QComboBox, QCheckBox, QScrollBar 0020 from qt import QAbstractItemView, QHeaderView 0021 from qt import QTreeView, QFont, QFrame 0022 from qt import QStyledItemDelegate 0023 from qt import QBrush, QPalette 0024 from kde import KDialogButtonBox, KApplication 0025 0026 from modeltest import ModelTest 0027 0028 from rulesetselector import RuleTreeView 0029 from board import WindLabel 0030 from mi18n import i18n, i18nc 0031 from common import Internal, Debug 0032 from statesaver import StateSaver 0033 from query import Query 0034 from guiutil import ListComboBox, Painter, decorateWindow, BlockSignals 0035 from tree import TreeItem, RootItem, TreeModel 0036 from wind import Wind 0037 0038 0039 class ScoreTreeItem(TreeItem): 0040 0041 """generic class for items in our score tree""" 0042 # pylint: disable=abstract-method 0043 # we know content() is abstract, this class is too 0044 0045 def columnCount(self): 0046 """count the hands of the first player""" 0047 child1 = self 0048 while not isinstance(child1, ScorePlayerItem) and child1.children: 0049 child1 = child1.children[0] 0050 if isinstance(child1, ScorePlayerItem): 0051 return len(child1.rawContent[1]) + 1 0052 return 1 0053 0054 0055 class ScoreRootItem(RootItem): 0056 0057 """the root item for the score tree""" 0058 0059 def columnCount(self): 0060 child1 = self 0061 while not isinstance(child1, ScorePlayerItem) and child1.children: 0062 child1 = child1.children[0] 0063 if isinstance(child1, ScorePlayerItem): 0064 return len(child1.rawContent[1]) + 1 0065 return 1 0066 0067 0068 class ScoreGroupItem(ScoreTreeItem): 0069 0070 """represents a group in the tree like Points, Payments, Balance""" 0071 0072 def __init__(self, content): 0073 ScoreTreeItem.__init__(self, content) 0074 0075 def content(self, column): 0076 """return content stored in this item""" 0077 return i18n(self.rawContent) 0078 0079 0080 class ScorePlayerItem(ScoreTreeItem): 0081 0082 """represents a player in the tree""" 0083 0084 def __init__(self, content): 0085 ScoreTreeItem.__init__(self, content) 0086 0087 def content(self, column): 0088 """return the content stored in this node""" 0089 if column == 0: 0090 return i18n(self.rawContent[0]) 0091 try: 0092 return self.hands()[column - 1] 0093 except IndexError: 0094 # we have a penalty but no hand yet. Should 0095 # not happen in practical use 0096 return None 0097 0098 def hands(self): 0099 """a small helper""" 0100 return self.rawContent[1] 0101 0102 def chartPoints(self, column, steps): 0103 """the returned points spread over a height of four rows""" 0104 points = [x.balance for x in self.hands()] 0105 points.insert(0, 0) 0106 points.insert(0, 0) 0107 points.append(points[-1]) 0108 column -= 1 0109 points = points[column:column + 4] 0110 points = [float(x) for x in points] 0111 for idx in range(1, len(points) - 2): # skip the ends 0112 for step in range(steps): 0113 point_1, point0, point1, point2 = points[idx - 1:idx + 3] 0114 fstep = float(step) / steps 0115 # wikipedia Catmull-Rom -> Cubic_Hermite_spline 0116 # 0 -> point0, 1 -> point1, 1/2 -> (- point_1 + 9 point0 + 9 0117 # point1 - point2) / 16 0118 yield ( 0119 fstep * ((2 - fstep) * fstep - 1) * point_1 0120 + (fstep * fstep * ( 0121 3 * fstep - 5) + 2) * point0 0122 + fstep * 0123 ((4 - 3 * fstep) * fstep + 1) * point1 0124 + (fstep - 1) * fstep * fstep * point2) / 2 0125 yield points[-2] 0126 0127 0128 class ScoreItemDelegate(QStyledItemDelegate): 0129 0130 """since setting delegates for a row does not work as wanted with a 0131 tree view, we set the same delegate on ALL items.""" 0132 # try to use colors that look good with all color schemes. Bright 0133 # contrast colors are not optimal as long as our lines have a width of 0134 # only one pixel: antialiasing is not sufficient 0135 colors = [KApplication.palette().color(x) 0136 for x in [QPalette.Text, QPalette.Link, QPalette.LinkVisited]] 0137 colors.append(QColor('orange')) 0138 0139 def __init__(self, parent=None): 0140 QStyledItemDelegate.__init__(self, parent) 0141 0142 def paint(self, painter, option, index): 0143 """where the real work is done...""" 0144 item = index.internalPointer() 0145 if isinstance(item, ScorePlayerItem) and item.parent.row() == 3 and index.column() != 0: 0146 for idx, playerItem in enumerate(index.parent().internalPointer().children): 0147 chart = index.model().chart(option.rect, index, playerItem) 0148 if chart: 0149 with Painter(painter): 0150 painter.translate(option.rect.topLeft()) 0151 painter.setPen(self.colors[idx]) 0152 painter.setRenderHint(QPainter.Antialiasing) 0153 # if we want to use a pen width > 1, we can no longer directly drawPolyline 0154 # separately per cell beause the lines spread vertically over two rows: We would 0155 # have to draw the lines into one big pixmap and copy 0156 # from the into the cells 0157 painter.drawPolyline(*chart) 0158 return None 0159 return QStyledItemDelegate.paint(self, painter, option, index) 0160 0161 0162 class ScoreModel(TreeModel): 0163 0164 """a model for our score table""" 0165 steps = 30 # how fine do we want the stepping in the chart spline 0166 0167 def __init__(self, parent=None): 0168 super().__init__(parent) 0169 self.scoreTable = parent 0170 self.rootItem = ScoreRootItem(None) 0171 self.minY = self.maxY = None 0172 self.loadData() 0173 0174 def chart(self, rect, index, playerItem): 0175 """return list(QPointF) for a player in a specific tree cell""" 0176 chartHeight = float(rect.height()) * 4 0177 yScale = chartHeight / (self.minY - self.maxY) 0178 yOffset = rect.height() * index.row() 0179 _ = (playerItem.chartPoints(index.column(), self.steps)) 0180 yValues = [(y - self.maxY) * yScale - yOffset for y in _] 0181 stepX = float(rect.width()) / self.steps 0182 xValues = [x * stepX for x in range(self.steps + 1)] 0183 return [QPointF(x, y) for x, y in zip(xValues, yValues)] 0184 0185 def data(self, index, role=None): # pylint: disable=no-self-use,too-many-branches 0186 """score table""" 0187 # pylint: disable=too-many-return-statements 0188 if not index.isValid(): 0189 return None 0190 column = index.column() 0191 item = index.internalPointer() 0192 if role is None: 0193 role = Qt.DisplayRole 0194 if role == Qt.DisplayRole: 0195 if isinstance(item, ScorePlayerItem): 0196 content = item.content(column) 0197 if isinstance(content, HandResult): 0198 parentRow = item.parent.row() 0199 if parentRow == 0: 0200 if not content.penalty: 0201 content = '%d %s' % (content.points, content.wind) 0202 elif parentRow == 1: 0203 content = str(content.payments) 0204 else: 0205 content = str(content.balance) 0206 return content 0207 return '' if column > 0 else item.content(0) 0208 if role == Qt.TextAlignmentRole: 0209 return int(Qt.AlignLeft | Qt.AlignVCenter) if index.column() == 0 else int(Qt.AlignRight | Qt.AlignVCenter) 0210 if role == Qt.FontRole: 0211 return QFont('Monospaced') 0212 if role == Qt.ForegroundRole: 0213 if isinstance(item, ScorePlayerItem) and item.parent.row() == 3: 0214 content = item.content(column) 0215 if not isinstance(content, HandResult): 0216 return QBrush(ScoreItemDelegate.colors[index.row()]) 0217 if column > 0 and isinstance(item, ScorePlayerItem): 0218 content = item.content(column) 0219 # pylint: disable=maybe-no-member 0220 # pylint thinks content is a str 0221 if role == Qt.BackgroundRole: 0222 if content and content.won: 0223 return QColor(165, 255, 165) 0224 if role == Qt.ToolTipRole: 0225 englishHints = content.manualrules.split('||') 0226 tooltip = '<br />'.join(i18n(x) for x in englishHints) 0227 return tooltip 0228 return None 0229 0230 def headerData(self, section, orientation, role): 0231 """tell the view about the wanted headers""" 0232 if role == Qt.DisplayRole and orientation == Qt.Horizontal: 0233 if section == 0: 0234 return i18n('Round/Hand') 0235 child1 = self.rootItem.children[0] 0236 if child1 and child1.children: 0237 child1 = child1.children[0] 0238 hands = child1.hands() 0239 handResult = hands[section - 1] 0240 if not handResult.penalty: 0241 return handResult.handId() 0242 elif role == Qt.TextAlignmentRole: 0243 return int(Qt.AlignLeft | Qt.AlignVCenter) if section == 0 else int(Qt.AlignRight | Qt.AlignVCenter) 0244 return None 0245 0246 def loadData(self): 0247 """loads all data from the data base into a 2D matrix formatted like the wanted tree""" 0248 game = self.scoreTable.game 0249 data = [] 0250 records = Query( 0251 'select player,rotated,notrotated,penalty,won,prevailing,wind,points,payments,balance,manualrules' 0252 ' from score where game=? order by player,hand', (game.gameid,)).records 0253 humans = sorted( 0254 (x for x in game.players if not x.name.startswith('Robot'))) 0255 robots = sorted( 0256 (x for x in game.players if x.name.startswith('Robot'))) 0257 data = [tuple([player.localName, [HandResult(*x[1:]) for x in records 0258 if x[0] == player.nameid]]) for player in humans + robots] 0259 self.__findMinMaxChartPoints(data) 0260 parent = QModelIndex() 0261 groupIndex = self.index(self.rootItem.childCount(), 0, parent) 0262 groupNames = [i18nc('kajongg', 'Score'), i18nc('kajongg', 'Payments'), 0263 i18nc('kajongg', 'Balance'), i18nc('kajongg', 'Chart')] 0264 for idx, groupName in enumerate(groupNames): 0265 self.insertRows(idx, list([ScoreGroupItem(groupName)]), groupIndex) 0266 listIndex = self.index(idx, 0, groupIndex) 0267 for idx1, item in enumerate(data): 0268 self.insertRows(idx1, list([ScorePlayerItem(item)]), listIndex) 0269 0270 def __findMinMaxChartPoints(self, data): 0271 """find and save the extremes of the spline. They can be higher than 0272 the pure balance values""" 0273 self.minY = 9999999 0274 self.maxY = -9999999 0275 for item in data: 0276 playerItem = ScorePlayerItem(item) 0277 for col in range(len(playerItem.hands())): 0278 points = list(playerItem.chartPoints(col + 1, self.steps)) 0279 self.minY = min(self.minY, min(points)) 0280 self.maxY = max(self.maxY, max(points)) 0281 self.minY -= 2 # antialiasing might cross the cell border 0282 self.maxY += 2 0283 0284 0285 class HandResult: 0286 0287 """holds the results of a hand for the scoring table""" 0288 # pylint: disable=too-many-arguments 0289 # we have too many arguments 0290 0291 def __init__(self, rotated, notRotated, penalty, won, 0292 prevailing, wind, points, payments, balance, manualrules): 0293 self.rotated = rotated 0294 self.notRotated = notRotated 0295 self.penalty = bool(penalty) 0296 self.won = won 0297 self.prevailing = Wind(prevailing) 0298 self.wind = Wind(wind) 0299 self.points = points 0300 self.payments = payments 0301 self.balance = balance 0302 self.manualrules = manualrules 0303 0304 def __str__(self): 0305 return '%d %d %s %d %d %s' % ( 0306 self.penalty, self.points, self.wind, self.payments, self.balance, self.manualrules) 0307 0308 def handId(self): 0309 """identifies the hand for window title and scoring table""" 0310 character = chr( 0311 ord('a') - 1 + self.notRotated) if self.notRotated else '' 0312 return '%s%s%s' % (self.prevailing, self.rotated + 1, character) 0313 0314 def roundHand(self, allHands): 0315 """the nth hand in the current round, starting with 1""" 0316 idx = allHands.index(self) 0317 _ = reversed(allHands[:idx]) 0318 allHands = [x for x in _ if not x.penalty] 0319 if not allHands: 0320 return 1 0321 for idx, hand in enumerate(allHands): 0322 if hand.prevailing != self.prevailing: 0323 return idx + 1 0324 return idx + 2 0325 0326 0327 class ScoreViewLeft(QTreeView): 0328 0329 """subclass for defining sizeHint""" 0330 0331 def __init__(self, parent=None): 0332 QTreeView.__init__(self, parent) 0333 self.setItemDelegate(ScoreItemDelegate(self)) 0334 0335 def __col0Width(self): 0336 """the width we need for displaying column 0 0337 without scrollbar""" 0338 return self.columnWidth(0) + self.frameWidth() * 2 0339 0340 def sizeHint(self): 0341 """we never want a horizontal scrollbar for player names, 0342 we always want to see them in full""" 0343 return QSize(self.__col0Width(), QTreeView.sizeHint(self).height()) 0344 0345 def minimumSizeHint(self): 0346 """we never want a horizontal scrollbar for player names, 0347 we always want to see them in full""" 0348 return self.sizeHint() 0349 0350 0351 class ScoreViewRight(QTreeView): 0352 0353 """we need to subclass for catching events""" 0354 0355 def __init__(self, parent=None): 0356 QTreeView.__init__(self, parent) 0357 self.setItemDelegate(ScoreItemDelegate(self)) 0358 0359 def changeEvent(self, event): 0360 """recompute column width if font changes""" 0361 if event.type() == QEvent.FontChange: 0362 self.setColWidth() 0363 0364 def setColWidth(self): 0365 """we want a fixed column width sufficient for all values""" 0366 colRange = range(1, self.header().count()) 0367 if colRange: 0368 for col in colRange: 0369 self.resizeColumnToContents(col) 0370 width = max(self.columnWidth(x) for x in colRange) 0371 for col in colRange: 0372 self.setColumnWidth(col, width) 0373 0374 0375 class HorizontalScrollBar(QScrollBar): 0376 0377 """We subclass here because we want to react on show/hide""" 0378 0379 def __init__(self, scoreTable, parent=None): 0380 QScrollBar.__init__(self, parent) 0381 self.scoreTable = scoreTable 0382 0383 def showEvent(self, unusedEvent): 0384 """adjust the left view""" 0385 self.scoreTable.adaptLeftViewHeight() 0386 0387 def hideEvent(self, unusedEvent): 0388 """adjust the left view""" 0389 self.scoreTable.viewRight.header().setOffset( 0390 0) # we should not have to do this... 0391 # how to reproduce problem without setOffset: 0392 # show table with hor scroll, scroll to right, extend window 0393 # width very fast. The faster we do that, the wronger the 0394 # offset of the first column in the viewport. 0395 self.scoreTable.adaptLeftViewHeight() 0396 0397 0398 class ScoreTable(QWidget): 0399 0400 """show scores of current or last game, even if the last game is 0401 finished. To achieve this we keep our own reference to game.""" 0402 0403 def __init__(self, scene): 0404 super().__init__(None) 0405 self.setObjectName('ScoreTable') 0406 self.scene = scene 0407 self.scoreModel = None 0408 self.scoreModelTest = None 0409 decorateWindow(self, i18nc('kajongg', 'Scores')) 0410 self.setAttribute(Qt.WA_AlwaysShowToolTips) 0411 self.setMouseTracking(True) 0412 self.__tableFields = ['prevailing', 'won', 'wind', 0413 'points', 'payments', 'balance', 'hand', 'manualrules'] 0414 self.setupUi() 0415 self.refresh() 0416 StateSaver(self, self.splitter) 0417 0418 @property 0419 def game(self): 0420 """a proxy""" 0421 return self.scene.game 0422 0423 def setColWidth(self): 0424 """we want to accommodate for 5 digits plus minus sign 0425 and all column widths should be the same, making 0426 horizontal scrolling per item more pleasant""" 0427 self.viewRight.setColWidth() 0428 0429 def setupUi(self): 0430 """setup UI elements""" 0431 self.viewLeft = ScoreViewLeft(self) 0432 self.viewRight = ScoreViewRight(self) 0433 self.viewRight.setHorizontalScrollBar(HorizontalScrollBar(self)) 0434 self.viewRight.setHorizontalScrollMode(QAbstractItemView.ScrollPerItem) 0435 self.viewRight.setFocusPolicy(Qt.NoFocus) 0436 self.viewRight.header().setSectionsClickable(False) 0437 self.viewRight.header().setSectionsMovable(False) 0438 self.viewRight.setSelectionMode(QAbstractItemView.NoSelection) 0439 windowLayout = QVBoxLayout(self) 0440 self.splitter = QSplitter(Qt.Vertical) 0441 self.splitter.setObjectName('ScoreTableSplitter') 0442 windowLayout.addWidget(self.splitter) 0443 scoreWidget = QWidget() 0444 self.scoreLayout = QHBoxLayout(scoreWidget) 0445 leftLayout = QVBoxLayout() 0446 leftLayout.addWidget(self.viewLeft) 0447 self.leftLayout = leftLayout 0448 self.scoreLayout.addLayout(leftLayout) 0449 self.scoreLayout.addWidget(self.viewRight) 0450 self.splitter.addWidget(scoreWidget) 0451 self.ruleTree = RuleTreeView(i18nc('kajongg', 'Used Rules')) 0452 self.splitter.addWidget(self.ruleTree) 0453 # this shows just one line for the ruleTree - so we just see the 0454 # name of the ruleset: 0455 self.splitter.setSizes(list([1000, 1])) 0456 0457 def sizeHint(self): 0458 """give the scoring table window a sensible default size""" 0459 result = QWidget.sizeHint(self) 0460 result.setWidth(result.height() * 3 // 2) 0461 # the default is too small. Use at least 2/5 of screen height and 1/4 0462 # of screen width: 0463 available = KApplication.desktopSize() 0464 height = max(result.height(), available.height() * 2 // 5) 0465 width = max(result.width(), available.width() // 4) 0466 result.setHeight(height) 0467 result.setWidth(width) 0468 return result 0469 0470 def refresh(self): 0471 """load this game and this player. Keep parameter list identical with 0472 ExplainView""" 0473 if not self.game: 0474 # keep scores of previous game on display 0475 return 0476 if self.scoreModel: 0477 expandGroups = [ 0478 self.viewLeft.isExpanded( 0479 self.scoreModel.index(x, 0, QModelIndex())) 0480 for x in range(4)] 0481 else: 0482 expandGroups = [True, False, True, True] 0483 gameid = str(self.game.seed or self.game.gameid) 0484 if self.game.finished(): 0485 title = i18n('Final scores for game <numid>%1</numid>', gameid) 0486 else: 0487 title = i18n('Scores for game <numid>%1</numid>', gameid) 0488 decorateWindow(self, title) 0489 self.ruleTree.rulesets = list([self.game.ruleset]) 0490 self.scoreModel = ScoreModel(self) 0491 if Debug.modelTest: 0492 self.scoreModelTest = ModelTest(self.scoreModel, self) 0493 for view in [self.viewLeft, self.viewRight]: 0494 view.setModel(self.scoreModel) 0495 header = view.header() 0496 header.setStretchLastSection(False) 0497 view.setAlternatingRowColors(True) 0498 self.viewRight.header().setSectionResizeMode(QHeaderView.Fixed) 0499 for col in range(self.viewLeft.header().count()): 0500 self.viewLeft.header().setSectionHidden(col, col > 0) 0501 self.viewRight.header().setSectionHidden(col, col == 0) 0502 self.scoreLayout.setStretch(1, 100) 0503 self.scoreLayout.setSpacing(0) 0504 self.viewLeft.setFrameStyle(QFrame.NoFrame) 0505 self.viewRight.setFrameStyle(QFrame.NoFrame) 0506 self.viewLeft.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 0507 for master, slave in ((self.viewRight, self.viewLeft), (self.viewLeft, self.viewRight)): 0508 master.expanded.connect(slave.expand) 0509 master.collapsed.connect(slave.collapse) 0510 master.verticalScrollBar().valueChanged.connect( 0511 slave.verticalScrollBar().setValue) 0512 for row, expand in enumerate(expandGroups): 0513 self.viewLeft.setExpanded( 0514 self.scoreModel.index(row, 0515 0, 0516 QModelIndex()), 0517 expand) 0518 self.viewLeft.resizeColumnToContents(0) 0519 self.viewRight.setColWidth() 0520 # we need a timer since the scrollbar is not yet visible 0521 QTimer.singleShot(0, self.scrollRight) 0522 0523 def scrollRight(self): 0524 """make sure the latest hand is visible""" 0525 scrollBar = self.viewRight.horizontalScrollBar() 0526 scrollBar.setValue(scrollBar.maximum()) 0527 0528 def showEvent(self, unusedEvent): 0529 """Only now the views and scrollbars have useful sizes, so we can compute the spacer 0530 for the left view""" 0531 self.adaptLeftViewHeight() 0532 0533 def adaptLeftViewHeight(self): 0534 """if the right view has a horizontal scrollbar, make sure both 0535 view have the same vertical scroll area. Otherwise scrolling to 0536 bottom results in unsyncronized views.""" 0537 if self.viewRight.horizontalScrollBar().isVisible(): 0538 height = self.viewRight.horizontalScrollBar().height() 0539 else: 0540 height = 0 0541 if self.leftLayout.count() > 1: 0542 # remove previous spacer 0543 self.leftLayout.takeAt(1) 0544 if height: 0545 self.leftLayout.addSpacing(height) 0546 0547 def closeEvent(self, unusedEvent): # pylint: disable=no-self-use 0548 """update action button state""" 0549 Internal.mainWindow.actionScoreTable.setChecked(False) 0550 0551 0552 class ExplainView(QListView): 0553 0554 """show a list explaining all score computations""" 0555 0556 def __init__(self, scene): 0557 QListView.__init__(self) 0558 self.scene = scene 0559 decorateWindow(self, i18nc("@title:window", "Explain Scores").replace('&', '')) 0560 self.setGeometry(0, 0, 300, 400) 0561 self.model = QStringListModel() 0562 self.setModel(self.model) 0563 StateSaver(self) 0564 self.refresh() 0565 0566 @property 0567 def game(self): 0568 """a proxy""" 0569 return self.scene.game 0570 0571 def refresh(self): 0572 """refresh for new values""" 0573 lines = [] 0574 if self.game is None: 0575 lines.append(i18n('There is no active game')) 0576 else: 0577 i18nName = i18n(self.game.ruleset.name) 0578 lines.append(i18n('%1', i18nName)) 0579 lines.append('') 0580 for player in self.game.players: 0581 pLines = [] 0582 explainHand = player.explainHand() 0583 if explainHand.hasTiles(): 0584 total = explainHand.total() 0585 if total: 0586 pLines = ['%s - %s' % (player.localName, total)] 0587 for line in explainHand.explain(): 0588 pLines.append('- ' + line) 0589 elif player.handTotal: 0590 pLines.append( 0591 i18n( 0592 'Manual score for %1: %2 points', 0593 player.localName, 0594 player.handTotal)) 0595 if pLines: 0596 pLines.append('') 0597 lines.extend(pLines) 0598 if 'xxx'.join(lines) != 'xxx'.join(str(x) for x in self.model.stringList()): # TODO: ohne? 0599 # QStringListModel does not optimize identical lists away, so we do 0600 self.model.setStringList(lines) 0601 0602 def closeEvent(self, unusedEvent): # pylint: disable=no-self-use 0603 """update action button state""" 0604 Internal.mainWindow.actionExplain.setChecked(False) 0605 0606 0607 class PenaltyBox(QSpinBox): 0608 0609 """with its own validator, we only accept multiples of parties""" 0610 0611 def __init__(self, parties, parent=None): 0612 QSpinBox.__init__(self, parent) 0613 self.parties = parties 0614 self.prevValue = None 0615 0616 def validate(self, inputData, pos): 0617 """check if value is a multiple of parties""" 0618 result, inputData, newPos = QSpinBox.validate(self, inputData, pos) 0619 if result == QValidator.Acceptable: 0620 if int(inputData) % self.parties != 0: 0621 result = QValidator.Intermediate 0622 if result == QValidator.Acceptable: 0623 self.prevValue = str(inputData) 0624 return (result, inputData, newPos) 0625 0626 def fixup(self, data): 0627 """change input to a legal value""" 0628 value = int(str(data)) 0629 prevValue = int(str(self.prevValue)) 0630 assert value != prevValue 0631 common = int(self.parties * 10) 0632 small = value // common 0633 if value > prevValue: 0634 newV = str(int((small + 1) * common)) 0635 else: 0636 newV = str(int(small * common)) 0637 data.clear() 0638 data.append(newV) 0639 0640 0641 class RuleBox(QCheckBox): 0642 0643 """additional attribute: ruleId""" 0644 0645 def __init__(self, rule): 0646 QCheckBox.__init__(self, i18n(rule.name)) 0647 self.rule = rule 0648 0649 def setApplicable(self, applicable): 0650 """update box""" 0651 self.setVisible(applicable) 0652 if not applicable: 0653 self.setChecked(False) 0654 0655 0656 class PenaltyDialog(QDialog): 0657 0658 """enter penalties""" 0659 0660 def __init__(self, game): 0661 """selection for this player, tiles are the still available tiles""" 0662 QDialog.__init__(self, None) 0663 decorateWindow(self, i18n("Penalty")) 0664 self.game = game 0665 grid = QGridLayout(self) 0666 lblOffense = QLabel(i18n('Offense:')) 0667 crimes = list( 0668 x for x in game.ruleset.penaltyRules if not ('absolute' in x.options and game.winner)) 0669 self.cbCrime = ListComboBox(crimes) 0670 lblOffense.setBuddy(self.cbCrime) 0671 grid.addWidget(lblOffense, 0, 0) 0672 grid.addWidget(self.cbCrime, 0, 1, 1, 4) 0673 lblPenalty = QLabel(i18n('Total Penalty')) 0674 self.spPenalty = PenaltyBox(2) 0675 self.spPenalty.setRange(0, 9999) 0676 lblPenalty.setBuddy(self.spPenalty) 0677 self.lblUnits = QLabel(i18n('points')) 0678 grid.addWidget(lblPenalty, 1, 0) 0679 grid.addWidget(self.spPenalty, 1, 1) 0680 grid.addWidget(self.lblUnits, 1, 2) 0681 self.payers = [] 0682 self.payees = [] 0683 # a penalty can never involve the winner, neither as payer nor as payee 0684 for idx in range(3): 0685 self.payers.append(ListComboBox(game.losers())) 0686 self.payees.append(ListComboBox(game.losers())) 0687 for idx, payer in enumerate(self.payers): 0688 grid.addWidget(payer, 3 + idx, 0) 0689 payer.lblPayment = QLabel() 0690 grid.addWidget(payer.lblPayment, 3 + idx, 1) 0691 for idx, payee in enumerate(self.payees): 0692 grid.addWidget(payee, 3 + idx, 3) 0693 payee.lblPayment = QLabel() 0694 grid.addWidget(payee.lblPayment, 3 + idx, 4) 0695 grid.addWidget(QLabel(''), 6, 0) 0696 grid.setRowStretch(6, 10) 0697 for player in self.payers + self.payees: 0698 player.currentIndexChanged.connect(self.playerChanged) 0699 self.spPenalty.valueChanged.connect(self.penaltyChanged) 0700 self.cbCrime.currentIndexChanged.connect(self.crimeChanged) 0701 buttonBox = KDialogButtonBox(self) 0702 grid.addWidget(buttonBox, 7, 0, 1, 5) 0703 buttonBox.setStandardButtons(QDialogButtonBox.Cancel) 0704 buttonBox.rejected.connect(self.reject) 0705 self.btnExecute = buttonBox.addButton( 0706 i18n("&Execute"), 0707 QDialogButtonBox.AcceptRole) 0708 self.btnExecute.clicked.connect(self.accept) 0709 self.crimeChanged() 0710 StateSaver(self) 0711 0712 def accept(self): 0713 """execute the penalty""" 0714 offense = self.cbCrime.current 0715 payers = [x.current for x in self.payers if x.isVisible()] 0716 payees = [x.current for x in self.payees if x.isVisible()] 0717 for player in self.game.players: 0718 if player in payers: 0719 amount = -self.spPenalty.value() // len(payers) 0720 elif player in payees: 0721 amount = self.spPenalty.value() // len(payees) 0722 else: 0723 amount = 0 0724 player.getsPayment(amount) 0725 self.game.savePenalty(player, offense, amount) 0726 QDialog.accept(self) 0727 0728 def usedCombos(self, partyCombos): 0729 """return all used player combos for this offense""" 0730 return [x for x in partyCombos if x.isVisibleTo(self)] 0731 0732 def allParties(self): 0733 """return all parties involved in penalty payment""" 0734 return [x.current for x in self.usedCombos(self.payers + self.payees)] 0735 0736 def playerChanged(self): 0737 """shuffle players to ensure everybody only appears once. 0738 enable execution if all input is valid""" 0739 changedCombo = self.sender() 0740 if not isinstance(changedCombo, ListComboBox): 0741 changedCombo = self.payers[0] 0742 usedPlayers = set(self.allParties()) 0743 unusedPlayers = set(self.game.losers()) - usedPlayers 0744 foundPlayers = [changedCombo.current] 0745 for combo in self.usedCombos(self.payers + self.payees): 0746 if combo is not changedCombo: 0747 if combo.current in foundPlayers: 0748 combo.current = unusedPlayers.pop() 0749 foundPlayers.append(combo.current) 0750 0751 def crimeChanged(self): 0752 """another offense has been selected""" 0753 offense = self.cbCrime.current 0754 payers = int(offense.options.get('payers', 1)) 0755 payees = int(offense.options.get('payees', 1)) 0756 self.spPenalty.prevValue = str(-offense.score.points) 0757 self.spPenalty.setValue(-offense.score.points) 0758 self.spPenalty.parties = max(payers, payees) 0759 self.spPenalty.setSingleStep(10) 0760 self.lblUnits.setText(i18n('points')) 0761 self.playerChanged() 0762 self.penaltyChanged() 0763 0764 def penaltyChanged(self): 0765 """total has changed, update payments""" 0766 # normally value is only validated when leaving the field 0767 self.spPenalty.interpretText() 0768 offense = self.cbCrime.current 0769 penalty = self.spPenalty.value() 0770 payers = int(offense.options.get('payers', 1)) 0771 payees = int(offense.options.get('payees', 1)) 0772 payerAmount = -penalty // payers 0773 payeeAmount = penalty // payees 0774 for pList, amount, count in ((self.payers, payerAmount, payers), (self.payees, payeeAmount, payees)): 0775 for idx, player in enumerate(pList): 0776 player.setVisible(idx < count) 0777 player.lblPayment.setVisible(idx < count) 0778 if idx < count: 0779 if pList == self.payers: 0780 player.lblPayment.setText( 0781 i18nc( 0782 'penalty dialog, appears behind paying player combobox', 0783 'pays %1 points', -amount)) 0784 else: 0785 player.lblPayment.setText( 0786 i18nc( 0787 'penalty dialog, appears behind profiting player combobox', 0788 'gets %1 points', amount)) 0789 0790 0791 class ScoringDialog(QWidget): 0792 0793 """a dialog for entering the scores""" 0794 # pylint: disable=too-many-instance-attributes 0795 0796 def __init__(self, scene): 0797 QWidget.__init__(self) 0798 self.scene = scene 0799 decorateWindow(self, i18nc("@title:window", "Scoring for this Hand")) 0800 self.nameLabels = [None] * 4 0801 self.spValues = [None] * 4 0802 self.windLabels = [None] * 4 0803 self.wonBoxes = [None] * 4 0804 self.detailsLayout = [None] * 4 0805 self.details = [None] * 4 0806 self.__tilePixMaps = [] 0807 self.__meldPixMaps = [] 0808 grid = QGridLayout(self) 0809 pGrid = QGridLayout() 0810 grid.addLayout(pGrid, 0, 0, 2, 1) 0811 pGrid.addWidget(QLabel(i18nc('kajongg', "Player")), 0, 0) 0812 pGrid.addWidget(QLabel(i18nc('kajongg', "Wind")), 0, 1) 0813 pGrid.addWidget(QLabel(i18nc('kajongg', 'Score')), 0, 2) 0814 pGrid.addWidget(QLabel(i18n("Winner")), 0, 3) 0815 self.detailTabs = QTabWidget() 0816 self.detailTabs.setDocumentMode(True) 0817 pGrid.addWidget(self.detailTabs, 0, 4, 8, 1) 0818 for idx in range(4): 0819 self.setupUiForPlayer(pGrid, idx) 0820 self.draw = QCheckBox(i18nc('kajongg', 'Draw')) 0821 self.draw.clicked.connect(self.wonChanged) 0822 btnPenalties = QPushButton(i18n("&Penalties")) 0823 btnPenalties.clicked.connect(self.penalty) 0824 self.btnSave = QPushButton(i18n('&Save Hand')) 0825 self.btnSave.clicked.connect(self.game.nextScoringHand) 0826 self.btnSave.setEnabled(False) 0827 self.setupUILastTileMeld(pGrid) 0828 pGrid.setRowStretch(87, 10) 0829 pGrid.addWidget(self.draw, 7, 3) 0830 self.cbLastTile.currentIndexChanged.connect(self.slotLastTile) 0831 self.cbLastMeld.currentIndexChanged.connect(self.slotInputChanged) 0832 btnBox = QHBoxLayout() 0833 btnBox.addWidget(btnPenalties) 0834 btnBox.addWidget(self.btnSave) 0835 pGrid.addLayout(btnBox, 8, 4) 0836 StateSaver(self) 0837 self.refresh() 0838 0839 @property 0840 def game(self): 0841 """proxy""" 0842 return self.scene.game 0843 0844 def setupUILastTileMeld(self, pGrid): 0845 """setup UI elements for last tile and last meld""" 0846 self.lblLastTile = QLabel(i18n('&Last Tile:')) 0847 self.cbLastTile = QComboBox() 0848 self.cbLastTile.setMinimumContentsLength(1) 0849 vpol = QSizePolicy() 0850 vpol.setHorizontalPolicy(QSizePolicy.Fixed) 0851 self.cbLastTile.setSizePolicy(vpol) 0852 self.cbLastTile.setSizeAdjustPolicy( 0853 QComboBox.AdjustToMinimumContentsLengthWithIcon) 0854 self.lblLastTile.setBuddy(self.cbLastTile) 0855 self.lblLastMeld = QLabel(i18n('L&ast Meld:')) 0856 self.prevLastTile = None 0857 self.cbLastMeld = QComboBox() 0858 self.cbLastMeld.setMinimumContentsLength(1) 0859 self.cbLastMeld.setSizePolicy(vpol) 0860 self.cbLastMeld.setSizeAdjustPolicy( 0861 QComboBox.AdjustToMinimumContentsLengthWithIcon) 0862 self.lblLastMeld.setBuddy(self.cbLastMeld) 0863 self.comboTilePairs = set() 0864 pGrid.setRowStretch(6, 5) 0865 pGrid.addWidget(self.lblLastTile, 7, 0, 1, 2) 0866 pGrid.addWidget(self.cbLastTile, 7, 2, 1, 1) 0867 pGrid.addWidget(self.lblLastMeld, 8, 0, 1, 2) 0868 pGrid.addWidget(self.cbLastMeld, 8, 2, 1, 2) 0869 0870 self.lblLastTile.setVisible(False) 0871 self.cbLastTile.setVisible(False) 0872 self.cbLastMeld.setVisible(False) 0873 self.lblLastMeld.setVisible(False) 0874 0875 def setupUiForPlayer(self, pGrid, idx): 0876 """setup UI elements for a player""" 0877 self.spValues[idx] = QSpinBox() 0878 self.nameLabels[idx] = QLabel() 0879 self.nameLabels[idx].setBuddy(self.spValues[idx]) 0880 self.windLabels[idx] = WindLabel() 0881 pGrid.addWidget(self.nameLabels[idx], idx + 2, 0) 0882 pGrid.addWidget(self.windLabels[idx], idx + 2, 1) 0883 pGrid.addWidget(self.spValues[idx], idx + 2, 2) 0884 self.wonBoxes[idx] = QCheckBox("") 0885 pGrid.addWidget(self.wonBoxes[idx], idx + 2, 3) 0886 self.wonBoxes[idx].clicked.connect(self.wonChanged) 0887 self.spValues[idx].valueChanged.connect(self.slotInputChanged) 0888 detailTab = QWidget() 0889 self.detailTabs.addTab(detailTab, '') 0890 self.details[idx] = QWidget() 0891 detailTabLayout = QVBoxLayout(detailTab) 0892 detailTabLayout.addWidget(self.details[idx]) 0893 detailTabLayout.addStretch() 0894 self.detailsLayout[idx] = QVBoxLayout(self.details[idx]) 0895 0896 def refresh(self): 0897 """reload game""" 0898 self.clear() 0899 game = self.game 0900 self.setVisible(game is not None and not game.finished()) 0901 if game: 0902 for idx, player in enumerate(game.players): 0903 for child in self.details[idx].children(): 0904 if isinstance(child, RuleBox): 0905 child.hide() 0906 self.detailsLayout[idx].removeWidget(child) 0907 del child 0908 if game: 0909 self.spValues[idx].setRange(0, game.ruleset.limit or 99999) 0910 self.nameLabels[idx].setText(player.localName) 0911 self.refreshWindLabels() 0912 self.detailTabs.setTabText(idx, player.localName) 0913 player.manualRuleBoxes = [RuleBox(x) 0914 for x in game.ruleset.allRules if x.hasSelectable] 0915 for ruleBox in player.manualRuleBoxes: 0916 self.detailsLayout[idx].addWidget(ruleBox) 0917 ruleBox.clicked.connect(self.slotInputChanged) 0918 player.refreshManualRules() 0919 0920 def show(self): 0921 """only now compute content""" 0922 if self.game and not self.game.finished(): 0923 self.slotInputChanged() 0924 QWidget.show(self) 0925 0926 def penalty(self): 0927 """penalty button clicked""" 0928 dlg = PenaltyDialog(self.game) 0929 dlg.exec_() 0930 0931 def slotLastTile(self): 0932 """called when the last tile changes""" 0933 newLastTile = self.computeLastTile() 0934 if not newLastTile: 0935 return 0936 if self.prevLastTile and self.prevLastTile.isExposed != newLastTile.isExposed: 0937 # state of last tile (concealed/exposed) changed: 0938 # for all checked boxes check if they still are applicable 0939 winner = self.game.winner 0940 if winner: 0941 for box in winner.manualRuleBoxes: 0942 if box.isChecked(): 0943 box.setChecked(False) 0944 if winner.hand.manualRuleMayApply(box.rule): 0945 box.setChecked(True) 0946 self.prevLastTile = newLastTile 0947 self.fillLastMeldCombo() 0948 self.slotInputChanged() 0949 0950 def computeLastTile(self): 0951 """return the currently selected last tile""" 0952 idx = self.cbLastTile.currentIndex() 0953 if idx >= 0: 0954 return self.cbLastTile.itemData(idx) 0955 return None 0956 0957 def clickedPlayerIdx(self, checkbox): 0958 """the player whose box has been clicked""" 0959 for idx in range(4): 0960 if checkbox == self.wonBoxes[idx]: 0961 return idx 0962 assert False 0963 return None 0964 0965 def wonChanged(self): 0966 """if a new winner has been defined, uncheck any previous winner""" 0967 newWinner = None 0968 if self.sender() != self.draw: 0969 clicked = self.clickedPlayerIdx(self.sender()) 0970 if self.wonBoxes[clicked].isChecked(): 0971 newWinner = self.game.players[clicked] 0972 else: 0973 newWinner = None 0974 self.game.winner = newWinner 0975 for idx in range(4): 0976 if newWinner != self.game.players[idx]: 0977 self.wonBoxes[idx].setChecked(False) 0978 if newWinner: 0979 self.draw.setChecked(False) 0980 self.lblLastTile.setVisible(True) 0981 self.cbLastTile.setVisible(True) 0982 self.lblLastMeld.setVisible(False) 0983 self.cbLastMeld.setVisible(False) 0984 self.fillLastTileCombo() 0985 self.slotInputChanged() 0986 0987 def updateManualRules(self): 0988 """enable/disable them""" 0989 # if an exclusive rule has been activated, deactivate it for 0990 # all other players 0991 ruleBox = self.sender() 0992 if isinstance(ruleBox, RuleBox) and ruleBox.isChecked() and ruleBox.rule.exclusive(): 0993 for idx, player in enumerate(self.game.players): 0994 if ruleBox.parentWidget() != self.details[idx]: 0995 for pBox in player.manualRuleBoxes: 0996 if pBox.rule.name == ruleBox.rule.name: 0997 pBox.setChecked(False) 0998 try: 0999 newState = bool(self.game.winner.handBoard.uiTiles) 1000 except AttributeError: 1001 newState = False 1002 self.lblLastTile.setVisible(newState) 1003 self.cbLastTile.setVisible(newState) 1004 if self.game: 1005 for player in self.game.players: 1006 player.refreshManualRules(self.sender()) 1007 1008 def clear(self): 1009 """prepare for next hand""" 1010 if self.game: 1011 for idx, player in enumerate(self.game.players): 1012 self.spValues[idx].clear() 1013 self.spValues[idx].setValue(0) 1014 self.wonBoxes[idx].setChecked(False) 1015 player.payment = 0 1016 player.invalidateHand() 1017 for box in self.wonBoxes: 1018 box.setVisible(False) 1019 self.draw.setChecked(False) 1020 self.updateManualRules() 1021 1022 if self.game is None: 1023 self.hide() 1024 else: 1025 self.refreshWindLabels() 1026 self.computeScores() 1027 self.spValues[0].setFocus() 1028 self.spValues[0].selectAll() 1029 1030 def refreshWindLabels(self): 1031 """update their wind and prevailing""" 1032 for idx, player in enumerate(self.game.players): 1033 self.windLabels[idx].wind = player.wind 1034 self.windLabels[idx].roundsFinished = self.game.roundsFinished 1035 1036 def computeScores(self): 1037 """if tiles have been selected, compute their value""" 1038 # pylint: disable=too-many-branches 1039 # too many branches 1040 if not self.game: 1041 return 1042 if self.game.finished(): 1043 self.hide() 1044 return 1045 for nameLabel, wonBox, spValue, player in zip( 1046 self.nameLabels, self.wonBoxes, self.spValues, self.game.players): 1047 with BlockSignals([spValue, wonBox]): 1048 # we do not want that change to call computeScores again 1049 if player.handBoard and player.handBoard.uiTiles: 1050 spValue.setEnabled(False) 1051 nameLabel.setBuddy(wonBox) 1052 for _ in range(10): 1053 prevTotal = player.handTotal 1054 player.invalidateHand() 1055 wonBox.setVisible(player.hand.won) 1056 if not wonBox.isVisibleTo(self) and wonBox.isChecked(): 1057 wonBox.setChecked(False) 1058 self.game.winner = None 1059 elif prevTotal == player.handTotal: 1060 break 1061 player.refreshManualRules() 1062 spValue.setValue(player.handTotal) 1063 else: 1064 if not spValue.isEnabled(): 1065 spValue.clear() 1066 spValue.setValue(0) 1067 spValue.setEnabled(True) 1068 nameLabel.setBuddy(spValue) 1069 wonBox.setVisible( 1070 player.handTotal >= self.game.ruleset.minMJTotal()) 1071 if not wonBox.isVisibleTo(self) and wonBox.isChecked(): 1072 wonBox.setChecked(False) 1073 if not wonBox.isVisibleTo(self) and player is self.game.winner: 1074 self.game.winner = None 1075 if Internal.scene.explainView: 1076 Internal.scene.explainView.refresh() 1077 1078 def __lastMeldContent(self): 1079 """prepare content for lastmeld combo""" 1080 lastTiles = set() 1081 winnerTiles = [] 1082 if self.game.winner and self.game.winner.handBoard: 1083 winnerTiles = self.game.winner.handBoard.uiTiles 1084 pairs = [] 1085 for meld in self.game.winner.hand.melds: 1086 if len(meld) < 4: 1087 pairs.extend(meld) 1088 for tile in winnerTiles: 1089 if tile.tile in pairs and not tile.isBonus: 1090 lastTiles.add(tile.tile) 1091 return lastTiles, winnerTiles 1092 1093 def __fillLastTileComboWith(self, lastTiles, winnerTiles): 1094 """fill last meld combo with prepared content""" 1095 self.comboTilePairs = lastTiles 1096 idx = self.cbLastTile.currentIndex() 1097 if idx < 0: 1098 idx = 0 1099 indexedTile = self.cbLastTile.itemData(idx) 1100 restoredIdx = None 1101 self.cbLastTile.clear() 1102 if not winnerTiles: 1103 return 1104 pmSize = winnerTiles[0].board.tileset.faceSize 1105 pmSize = QSize(pmSize.width() * 0.5, pmSize.height() * 0.5) 1106 self.cbLastTile.setIconSize(pmSize) 1107 QPixmapCache.clear() 1108 self.__tilePixMaps = [] 1109 shownTiles = set() 1110 for tile in winnerTiles: 1111 if tile.tile in lastTiles and tile.tile not in shownTiles: 1112 shownTiles.add(tile.tile) 1113 self.cbLastTile.addItem( 1114 QIcon(tile.pixmapFromSvg(pmSize, withBorders=False)), 1115 '', tile.tile) 1116 if indexedTile is tile.tile: 1117 restoredIdx = self.cbLastTile.count() - 1 1118 if not restoredIdx and indexedTile: 1119 # try again, maybe the tile changed between concealed and exposed 1120 indexedTile = indexedTile.exposed 1121 for idx in range(self.cbLastTile.count()): 1122 if indexedTile is self.cbLastTile.itemData(idx).exposed: 1123 restoredIdx = idx 1124 break 1125 if not restoredIdx: 1126 restoredIdx = 0 1127 self.cbLastTile.setCurrentIndex(restoredIdx) 1128 self.prevLastTile = self.computeLastTile() 1129 1130 def clearLastTileCombo(self): 1131 """as the name says""" 1132 self.comboTilePairs = None 1133 self.cbLastTile.clear() 1134 1135 def fillLastTileCombo(self): 1136 """fill the drop down list with all possible tiles. 1137 If the drop down had content before try to preserve the 1138 current index. Even if the tile changed state meanwhile.""" 1139 if self.game is None: 1140 return 1141 lastTiles, winnerTiles = self.__lastMeldContent() 1142 if self.comboTilePairs == lastTiles: 1143 return 1144 with BlockSignals(self.cbLastTile): 1145 # we only want to emit the changed signal once 1146 self.__fillLastTileComboWith(lastTiles, winnerTiles) 1147 self.cbLastTile.currentIndexChanged.emit(0) 1148 1149 def __fillLastMeldComboWith(self, winnerMelds, indexedMeld, lastTile): 1150 """fill last meld combo with prepared content""" 1151 winner = self.game.winner 1152 faceWidth = winner.handBoard.tileset.faceSize.width() * 0.5 1153 faceHeight = winner.handBoard.tileset.faceSize.height() * 0.5 1154 restoredIdx = None 1155 for meld in winnerMelds: 1156 pixMap = QPixmap(faceWidth * len(meld), faceHeight) 1157 pixMap.fill(Qt.transparent) 1158 self.__meldPixMaps.append(pixMap) 1159 painter = QPainter(pixMap) 1160 for element in meld: 1161 painter.drawPixmap(0, 0, 1162 winner.handBoard.tilesByElement(element) 1163 [0].pixmapFromSvg(QSize(faceWidth, faceHeight), withBorders=False)) 1164 painter.translate(QPointF(faceWidth, 0.0)) 1165 self.cbLastMeld.addItem(QIcon(pixMap), '', str(meld)) 1166 if indexedMeld == str(meld): 1167 restoredIdx = self.cbLastMeld.count() - 1 1168 if not restoredIdx and indexedMeld: 1169 # try again, maybe the meld changed between concealed and exposed 1170 indexedMeld = indexedMeld.lower() 1171 for idx in range(self.cbLastMeld.count()): 1172 meldContent = str(self.cbLastMeld.itemData(idx)) 1173 if indexedMeld == meldContent.lower(): 1174 restoredIdx = idx 1175 if lastTile not in meldContent: 1176 lastTile = lastTile.swapped 1177 assert lastTile in meldContent 1178 with BlockSignals(self.cbLastTile): # we want to continue right here 1179 idx = self.cbLastTile.findData(lastTile) 1180 self.cbLastTile.setCurrentIndex(idx) 1181 break 1182 if not restoredIdx: 1183 restoredIdx = 0 1184 self.cbLastMeld.setCurrentIndex(restoredIdx) 1185 self.cbLastMeld.setIconSize(QSize(faceWidth * 3, faceHeight)) 1186 1187 def fillLastMeldCombo(self): 1188 """fill the drop down list with all possible melds. 1189 If the drop down had content before try to preserve the 1190 current index. Even if the meld changed state meanwhile.""" 1191 with BlockSignals(self.cbLastMeld): # we only want to emit the changed signal once 1192 showCombo = False 1193 idx = self.cbLastMeld.currentIndex() 1194 if idx < 0: 1195 idx = 0 1196 indexedMeld = str(self.cbLastMeld.itemData(idx)) 1197 self.cbLastMeld.clear() 1198 self.__meldPixMaps = [] 1199 if not self.game.winner: 1200 return 1201 if self.cbLastTile.count() == 0: 1202 return 1203 lastTile = Internal.scene.computeLastTile() 1204 winnerMelds = [m for m in self.game.winner.hand.melds if len(m) < 4 1205 and lastTile in m] 1206 assert winnerMelds, 'lastTile %s missing in %s' % ( 1207 lastTile, self.game.winner.hand.melds) 1208 if len(winnerMelds) == 1: 1209 self.cbLastMeld.addItem(QIcon(), '', str(winnerMelds[0])) 1210 self.cbLastMeld.setCurrentIndex(0) 1211 self.lblLastMeld.setVisible(False) 1212 self.cbLastMeld.setVisible(False) 1213 return 1214 showCombo = True 1215 self.__fillLastMeldComboWith(winnerMelds, indexedMeld, lastTile) 1216 self.lblLastMeld.setVisible(showCombo) 1217 self.cbLastMeld.setVisible(showCombo) 1218 self.cbLastMeld.currentIndexChanged.emit(0) 1219 1220 def slotInputChanged(self): 1221 """some input fields changed: update""" 1222 for player in self.game.players: 1223 player.invalidateHand() 1224 self.updateManualRules() 1225 self.computeScores() 1226 self.validate() 1227 for player in self.game.players: 1228 player.showInfo() 1229 Internal.mainWindow.updateGUI() 1230 1231 def validate(self): 1232 """update the status of the OK button""" 1233 game = self.game 1234 if game: 1235 valid = True 1236 if game.winner and game.winner.handTotal < game.ruleset.minMJTotal(): 1237 valid = False 1238 elif not game.winner and not self.draw.isChecked(): 1239 valid = False 1240 self.btnSave.setEnabled(valid)