File indexing completed on 2024-04-21 04:01:53

0001 # -*- coding: utf-8 -*-
0002 """
0003 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0004 
0005 partially based on C++ code from:
0006 Copyright (C) 2006 Mauricio Piacentini <mauricio@tabuleiro.com>
0007 
0008 SPDX-License-Identifier: GPL-2.0
0009 
0010 """
0011 
0012 from qt import Qt, QSize
0013 from qt import QWidget, QHBoxLayout, QVBoxLayout, \
0014     QPushButton, QSpacerItem, QSizePolicy, \
0015     QTreeView, QFont, QAbstractItemView, QHeaderView
0016 from qt import QModelIndex
0017 from kdestub import KApplication
0018 from rule import Ruleset, PredefinedRuleset, RuleBase, ParameterRule, BoolRule
0019 from util import uniqueList
0020 from mi18n import i18n, i18nc, i18ncE, english
0021 from differ import RulesetDiffer
0022 from common import Debug, Internal
0023 from tree import TreeItem, RootItem, TreeModel
0024 from dialogs import Sorry
0025 from modeltest import ModelTest
0026 from genericdelegates import RightAlignedCheckboxDelegate, ZeroEmptyColumnDelegate
0027 from statesaver import StateSaver
0028 from guiutil import decorateWindow
0029 
0030 
0031 class RuleRootItem(RootItem):
0032 
0033     """the root item for the ruleset tree"""
0034 
0035     def columnCount(self):
0036         return len(self.rawContent)
0037 
0038 
0039 class RuleTreeItem(TreeItem):
0040 
0041     """generic class for items in our rule tree"""
0042     # pylint: disable=abstract-method
0043     # we know content() is abstract, this class is too
0044 
0045     def columnCount(self):
0046         """can be different for every rule"""
0047         if hasattr(self, 'colCount'):
0048             return self.colCount  # pylint: disable=no-member
0049         return len(self.rawContent)
0050 
0051     def ruleset(self):
0052         """return the ruleset containing this item"""
0053         item = self
0054         while not isinstance(item.rawContent, Ruleset):
0055             item = item.parent
0056         return item.rawContent
0057 
0058 
0059 class RulesetItem(RuleTreeItem):
0060 
0061     """represents a ruleset in the tree"""
0062 
0063     def __init__(self, content):
0064         RuleTreeItem.__init__(self, content)
0065 
0066     def content(self, column):
0067         """return content stored in this item"""
0068         if column == 0:
0069             return self.rawContent.name
0070         return ''
0071 
0072     def columnCount(self):
0073         return 1
0074 
0075     def remove(self):
0076         """remove this ruleset from the model and the database"""
0077         self.rawContent.remove()
0078 
0079     def tooltip(self):
0080         """the tooltip for a ruleset"""
0081         return self.rawContent.description
0082 
0083 
0084 class RuleListItem(RuleTreeItem):
0085 
0086     """represents a list of rules in the tree"""
0087 
0088     def __init__(self, content):
0089         RuleTreeItem.__init__(self, content)
0090 
0091     def content(self, column):
0092         """return content stored in this item"""
0093         if column == 0:
0094             return self.rawContent.name
0095         return ''
0096 
0097     def tooltip(self):
0098         """tooltip for a list item explaining the usage of this list"""
0099         ruleset = self.ruleset()
0100         return '<b>' + i18n(ruleset.name) + '</b><br><br>' + \
0101             i18n(self.rawContent.description)
0102 
0103 
0104 class RuleItem(RuleTreeItem):
0105 
0106     """represents a rule in the tree"""
0107 
0108     def __init__(self, content):
0109         RuleTreeItem.__init__(self, content)
0110 
0111     def content(self, column):
0112         """return the content stored in this node"""
0113         colNames = self.parent.parent.parent.rawContent
0114         content = self.rawContent
0115         if column == 0:
0116             return content.name
0117         if isinstance(content, ParameterRule):
0118             if column == 1:
0119                 return content.parameter
0120         else:
0121             if not hasattr(content.score, str(column)):
0122                 column = colNames[column]
0123             return getattr(content.score, column)
0124         return ''
0125 
0126     def tooltip(self):
0127         """tooltip for rule: just the name of the ruleset"""
0128         ruleset = self.ruleset()
0129         if self.rawContent.description:
0130             return '<b>' + i18n(ruleset.name) + '</b><br><br>' + \
0131                 i18n(self.rawContent.description)
0132         return i18n(ruleset.name)
0133 
0134 
0135 class RuleModel(TreeModel):
0136 
0137     """a model for our rule table"""
0138 
0139     def __init__(self, rulesets, title, parent=None):
0140         super().__init__(parent)
0141         self.rulesets = rulesets
0142         self.loaded = False
0143         unitNames = list()
0144         for ruleset in rulesets:
0145             ruleset.load()
0146             for rule in ruleset.allRules:
0147                 unitNames.extend(rule.score.unitNames.items())
0148         unitNames = sorted(unitNames, key=lambda x: x[1])
0149         unitNames = uniqueList(x[0] for x in unitNames)
0150         rootData = [title]
0151         rootData.extend(unitNames)
0152         self.rootItem = RuleRootItem(rootData)
0153 
0154     def canFetchMore(self, unusedParent=None):
0155         """did we already load the rules? We only want to do that
0156         when the config tab with rulesets is actually shown"""
0157         return not self.loaded
0158 
0159     def fetchMore(self, unusedParent=None):
0160         """load the rules"""
0161         for ruleset in self.rulesets:
0162             self.appendRuleset(ruleset)
0163         self.loaded = True
0164 
0165     def data(self, index, role):  # pylint: disable=no-self-use
0166         """get data fom model"""
0167         # pylint: disable=too-many-branches
0168         # too many branches
0169         result = None
0170         if index.isValid():
0171             item = index.internalPointer()
0172             if role in (Qt.DisplayRole, Qt.EditRole):
0173                 if index.column() == 1:
0174                     if isinstance(item, RuleItem) and isinstance(item.rawContent, BoolRule):
0175                         return ''
0176                 showValue = item.content(index.column())
0177                 if isinstance(showValue, str) and showValue.endswith('.0'):
0178                     try:
0179                         showValue = str(int(float(showValue)))
0180                     except ValueError:
0181                         pass
0182                 if showValue == '0':
0183                     showValue = ''
0184                 result = showValue
0185             elif role == Qt.CheckStateRole:
0186                 if self.isCheckboxCell(index):
0187                     bData = item.content(index.column())
0188                     result = Qt.Checked if bData else Qt.Unchecked
0189             elif role == Qt.TextAlignmentRole:
0190                 result = int(Qt.AlignLeft | Qt.AlignVCenter)
0191                 if index.column() > 0:
0192                     result = int(Qt.AlignRight | Qt.AlignVCenter)
0193             elif role == Qt.FontRole and index.column() == 0:
0194                 ruleset = item.ruleset()
0195                 if isinstance(ruleset, PredefinedRuleset):
0196                     font = QFont()
0197                     font.setItalic(True)
0198                     result = font
0199             elif role == Qt.ToolTipRole:
0200                 tip = '<b></b>%s<b></b>' % i18n(
0201                     item.tooltip()) if item else ''
0202                 result = tip
0203         return result
0204 
0205     @staticmethod
0206     def isCheckboxCell(index):
0207         """are we dealing with a checkbox?"""
0208         if index.column() != 1:
0209             return False
0210         item = index.internalPointer()
0211         return isinstance(item, RuleItem) and isinstance(item.rawContent, BoolRule)
0212 
0213     def headerData(self, section, orientation, role):
0214         """tell the view about the wanted headers"""
0215         if Qt is None:
0216             # happens when kajongg exits unexpectedly
0217             return None
0218         if role == Qt.DisplayRole and orientation == Qt.Horizontal:
0219             if section >= self.rootItem.columnCount():
0220                 return None
0221             result = self.rootItem.content(section)
0222             if result == 'doubles':
0223                 return 'x2'
0224             return i18nc('kajongg', result)
0225         if role == Qt.TextAlignmentRole:
0226             leftRight = Qt.AlignLeft if section == 0 else Qt.AlignRight
0227             return int(leftRight | Qt.AlignVCenter)
0228         return None
0229 
0230     def appendRuleset(self, ruleset):
0231         """append ruleset to the model"""
0232         if not ruleset:
0233             return
0234         ruleset.load()
0235         parent = QModelIndex()
0236         row = self.rootItem.childCount()
0237         rulesetItems = list([RulesetItem(ruleset)])
0238         self.insertRows(row, rulesetItems, parent)
0239         rulesetIndex = self.index(row, 0, parent)
0240         ruleLists = [x for x in ruleset.ruleLists if len(x)]
0241         ruleListItems = [RuleListItem(x) for x in ruleLists]
0242         for item in ruleListItems:
0243             item.colCount = self.rootItem.columnCount()
0244         self.insertRows(0, ruleListItems, rulesetIndex)
0245         for ridx, ruleList in enumerate(ruleLists):
0246             listIndex = self.index(ridx, 0, rulesetIndex)
0247             ruleItems = [RuleItem(x) for x in ruleList if 'internal' not in x.options]
0248             self.insertRows(0, ruleItems, listIndex)
0249 
0250 
0251 class EditableRuleModel(RuleModel):
0252 
0253     """add methods needed for editing"""
0254 
0255     def __init__(self, rulesets, title, parent=None):
0256         RuleModel.__init__(self, rulesets, title, parent)
0257 
0258     def __setRuleData(self, column, content, value):
0259         """change rule data in the model"""
0260         dirty, message = False, None
0261         if column == 0:
0262             if content.name != english(value):
0263                 dirty = True
0264                 content.name = english(value)
0265         elif column == 1 and isinstance(content, ParameterRule):
0266             oldParameter = content.parameter
0267             if isinstance(content, BoolRule):
0268                 return False, ''
0269             if content.parameter != value:
0270                 dirty = True
0271                 content.parameter = value
0272             message = content.validate()
0273             if message:
0274                 content.parameter = oldParameter
0275                 dirty = False
0276         else:
0277             unitName = self.rootItem.content(column)
0278             dirty, message = content.score.change(unitName, value)
0279         return dirty, message
0280 
0281     def setData(self, index, value, role=Qt.EditRole):
0282         """change data in the model"""
0283         # pylint:  disable=too-many-branches
0284         if not index.isValid():
0285             return False
0286         dirty = False
0287         column = index.column()
0288         item = index.internalPointer()
0289         ruleset = item.ruleset()
0290         content = item.rawContent
0291         if role == Qt.EditRole:
0292             if isinstance(content, Ruleset) and column == 0:
0293                 oldName = content.name
0294                 content.rename(english(value))
0295                 dirty = oldName != content.name
0296             elif isinstance(content, RuleBase):
0297                 dirty, message = self.__setRuleData(column, content, value)
0298                 if message:
0299                     Sorry(message)
0300                     return False
0301             else:
0302                 return False
0303         elif role == Qt.CheckStateRole:
0304             if isinstance(content, BoolRule) and column == 1:
0305                 if not isinstance(ruleset, PredefinedRuleset):
0306                     newValue = value == Qt.Checked
0307                     if content.parameter != newValue:
0308                         dirty = True
0309                         content.parameter = newValue
0310             else:
0311                 return False
0312         if dirty:
0313             if isinstance(content, RuleBase):
0314                 ruleset.updateRule(content)
0315             self.dataChanged.emit(index, index)
0316         return True
0317 
0318     def flags(self, index):  # pylint: disable=no-self-use
0319         """tell the view what it can do with this item"""
0320         if not index.isValid():
0321             return Qt.ItemIsEnabled
0322         column = index.column()
0323         item = index.internalPointer()
0324         content = item.rawContent
0325         checkable = False
0326         if isinstance(content, Ruleset) and column == 0:
0327             mayEdit = True
0328         elif isinstance(content, RuleBase):
0329             checkable = column == 1 and isinstance(content, BoolRule)
0330             mayEdit = bool(column)
0331         else:
0332             mayEdit = False
0333         mayEdit = mayEdit and not isinstance(item.ruleset(), PredefinedRuleset)
0334         result = Qt.ItemIsEnabled | Qt.ItemIsSelectable
0335         if mayEdit:
0336             result |= Qt.ItemIsEditable
0337         if checkable:
0338             result |= Qt.ItemIsUserCheckable
0339         return result
0340 
0341 
0342 class RuleTreeView(QTreeView):
0343 
0344     """Tree view for our rulesets"""
0345 
0346     def __init__(self, name, btnCopy=None,
0347                  btnRemove=None, btnCompare=None, parent=None):
0348         QTreeView.__init__(self, parent)
0349         self.name = name
0350         self.setObjectName('RuleTreeView')
0351         self.btnCopy = btnCopy
0352         self.btnRemove = btnRemove
0353         self.btnCompare = btnCompare
0354         for button in [self.btnCopy, self.btnRemove, self.btnCompare]:
0355             if button:
0356                 button.setEnabled(False)
0357         self.header().setObjectName('RuleTreeViewHeader')
0358         self.setSelectionMode(QAbstractItemView.SingleSelection)
0359         self.ruleModel = None
0360         self.ruleModelTest = None
0361         self.rulesets = []  # nasty: this generates self.ruleModel
0362         self.differs = []
0363 
0364     def dataChanged(self, unusedIndex1, unusedIndex2, unusedRoles=None):
0365         """get called if the model has changed: Update all differs"""
0366         for differ in self.differs:
0367             differ.rulesetChanged()
0368 
0369     @property
0370     def rulesets(self):
0371         """a list of rulesets made available by this model"""
0372         return self.ruleModel.rulesets
0373 
0374     @rulesets.setter
0375     def rulesets(self, rulesets):
0376         """a new list: update display"""
0377         if not self.ruleModel or self.ruleModel.rulesets != rulesets:
0378             if self.btnRemove and self.btnCopy:
0379                 self.ruleModel = EditableRuleModel(rulesets, self.name)
0380             else:
0381                 self.ruleModel = RuleModel(rulesets, self.name)
0382             self.setItemDelegateForColumn(
0383                 1,
0384                 RightAlignedCheckboxDelegate(
0385                     self,
0386                     self.ruleModel.isCheckboxCell))
0387             for  col in (1, 2, 3):
0388                 self.setItemDelegateForColumn(col, ZeroEmptyColumnDelegate(self))
0389             self.setModel(self.ruleModel)
0390             if Debug.modelTest:
0391                 self.ruleModelTest = ModelTest(self.ruleModel, self)
0392             self.show()
0393 
0394     def selectionChanged(self, selected, unusedDeselected=None):
0395         """update editing buttons"""
0396         enableCopy = enableRemove = enableCompare = False
0397         if selected.indexes():
0398             item = selected.indexes()[0].internalPointer()
0399             isPredefined = isinstance(item.ruleset(), PredefinedRuleset)
0400             if isinstance(item, RulesetItem):
0401                 enableCompare = True
0402                 enableCopy = sum(
0403                     x.hash == item.ruleset(
0404                     ).hash for x in self.ruleModel.rulesets) == 1
0405                 enableRemove = not isPredefined
0406         if self.btnCopy:
0407             self.btnCopy.setEnabled(enableCopy)
0408         if self.btnRemove:
0409             self.btnRemove.setEnabled(enableRemove)
0410         if self.btnCompare:
0411             self.btnCompare.setEnabled(enableCompare)
0412 
0413     def showEvent(self, unusedEvent):
0414         """reload the models when the view comes into sight"""
0415         # default: make sure the name column is wide enough
0416         if self.ruleModel.canFetchMore():
0417             # we want to load all before adjusting column width
0418             self.ruleModel.fetchMore()
0419         header = self.header()
0420         header.setStretchLastSection(False)
0421         header.setMinimumSectionSize(-1)
0422         for col in range(1, header.count()):
0423             header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
0424         header.setSectionResizeMode(0, QHeaderView.Stretch)
0425         for col in range(header.count()):
0426             self.resizeColumnToContents(col)
0427 
0428     def selectedRow(self):
0429         """return the currently selected row index (with column 0)"""
0430         rows = self.selectionModel().selectedRows()
0431         return rows[0] if len(rows) == 1 else None
0432 
0433     def copyRow(self):
0434         """copy a ruleset"""
0435         row = self.selectedRow()
0436         if row:
0437             item = row.internalPointer()
0438             assert isinstance(item, RulesetItem)
0439             ruleset = item.rawContent.copyTemplate()
0440             self.model().appendRuleset(ruleset)
0441             self.rulesets.append(ruleset)
0442             self.selectionChanged(self.selectionModel().selection())
0443 
0444     def removeRow(self):
0445         """removes a ruleset or a rule"""
0446         row = self.selectedRow()
0447         if row:
0448             item = row.internalPointer()
0449             assert not isinstance(item.ruleset(), PredefinedRuleset)
0450             assert isinstance(item, RulesetItem)
0451             ruleset = item.ruleset()
0452             self.model().removeRows(row.row(), parent=row.parent())
0453             self.rulesets.remove(ruleset)
0454             self.selectionChanged(self.selectionModel().selection())
0455 
0456     def compareRow(self):
0457         """shows the difference between two rulesets"""
0458         rows = self.selectionModel().selectedRows()
0459         ruleset = rows[0].internalPointer().rawContent
0460         assert isinstance(ruleset, Ruleset)
0461         differ = RulesetDiffer(ruleset, self.rulesets)
0462         differ.show()
0463         self.differs.append(differ)
0464 
0465 
0466 class RulesetSelector(QWidget):
0467 
0468     """presents all available rulesets with previews"""
0469 
0470     def __init__(self, parent=None):
0471         super().__init__(parent)
0472         self.setContentsMargins(0, 0, 0, 0)
0473         self.setupUi()
0474 
0475     def setupUi(self):
0476         """layout the window"""
0477         decorateWindow(self, i18nc("@title:window", "Customize rulesets"))
0478         self.setObjectName('Rulesets')
0479         hlayout = QHBoxLayout(self)
0480         v1layout = QVBoxLayout()
0481         self.v1widget = QWidget()
0482         v1layout = QVBoxLayout(self.v1widget)
0483         v2layout = QVBoxLayout()
0484         hlayout.addWidget(self.v1widget)
0485         hlayout.addLayout(v2layout)
0486         for widget in [self.v1widget, hlayout, v1layout, v2layout]:
0487             widget.setContentsMargins(0, 0, 0, 0)
0488         hlayout.setStretchFactor(self.v1widget, 10)
0489         self.btnCopy = QPushButton()
0490         self.btnRemove = QPushButton()
0491         self.btnCompare = QPushButton()
0492         self.btnClose = QPushButton()
0493         self.rulesetView = RuleTreeView(
0494             i18ncE('kajongg',
0495                    'Rule'),
0496             self.btnCopy,
0497             self.btnRemove,
0498             self.btnCompare)
0499         v1layout.addWidget(self.rulesetView)
0500         self.rulesetView.setWordWrap(True)
0501         self.rulesetView.setMouseTracking(True)
0502         spacerItem = QSpacerItem(
0503             20,
0504             20,
0505             QSizePolicy.Minimum,
0506             QSizePolicy.Expanding)
0507         v2layout.addWidget(self.btnCopy)
0508         v2layout.addWidget(self.btnRemove)
0509         v2layout.addWidget(self.btnCompare)
0510         self.btnCopy.clicked.connect(self.rulesetView.copyRow)
0511         self.btnRemove.clicked.connect(self.rulesetView.removeRow)
0512         self.btnCompare.clicked.connect(self.rulesetView.compareRow)
0513         self.btnClose.clicked.connect(self.hide)
0514         v2layout.addItem(spacerItem)
0515         v2layout.addWidget(self.btnClose)
0516         self.retranslateUi()
0517         StateSaver(self)
0518         self.show()
0519 
0520     def sizeHint(self):
0521         """we never want a horizontal scrollbar for player names,
0522         we always want to see them in full"""
0523         result = QWidget.sizeHint(self)
0524         available = KApplication.desktopSize()
0525         height = max(result.height(), available.height() * 2 // 3)
0526         width = max(result.width(), available.width() // 2)
0527         return QSize(width, height)
0528 
0529     def minimumSizeHint(self):
0530         """we never want a horizontal scrollbar for player names,
0531         we always want to see them in full"""
0532         return self.sizeHint()
0533 
0534     def showEvent(self, unusedEvent):
0535         """reload the rulesets"""
0536         self.refresh()
0537 
0538     def refresh(self):
0539         """retranslate and reload rulesets"""
0540         self.retranslateUi()
0541         self.rulesetView.rulesets = Ruleset.availableRulesets()
0542 
0543     def hideEvent(self, event):
0544         """close all differ dialogs"""
0545         for differ in self.rulesetView.differs:
0546             differ.hide()
0547             del differ
0548         QWidget.hideEvent(self, event)
0549 
0550     def retranslateUi(self):
0551         """translate to current language"""
0552         self.btnCopy.setText(i18n("C&opy"))
0553         self.btnCompare.setText(i18nc('Kajongg ruleset comparer', 'Co&mpare'))
0554         self.btnRemove.setText(i18n('&Remove'))
0555         self.btnClose.setText(i18n('&Close'))