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)