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'))