File indexing completed on 2024-03-24 04:04:37
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 import datetime 0011 0012 from qt import Qt, QAbstractTableModel 0013 from qt import QDialog, QDialogButtonBox, QWidget 0014 from qt import QHBoxLayout, QVBoxLayout, QAbstractItemView 0015 from qt import QItemSelectionModel, QGridLayout, QColor, QPalette 0016 0017 from mi18n import i18n, i18nc, i18nE 0018 from kde import KApplication, KIcon, KDialogButtonBox 0019 0020 from genericdelegates import RichTextColumnDelegate 0021 0022 from log import logDebug 0023 from statesaver import StateSaver 0024 from rule import Ruleset 0025 from guiutil import ListComboBox, MJTableView, decorateWindow 0026 from differ import RulesetDiffer 0027 from common import Internal, Debug 0028 from modeltest import ModelTest 0029 from chat import ChatMessage, ChatWindow 0030 0031 0032 class TablesModel(QAbstractTableModel): 0033 0034 """a model for our tables""" 0035 0036 def __init__(self, tables, parent=None): 0037 super().__init__(parent) 0038 self.tables = tables 0039 assert isinstance(tables, list) 0040 0041 def headerData( # pylint: disable=no-self-use 0042 self, section, 0043 orientation, role=Qt.DisplayRole): 0044 """show header""" 0045 if role == Qt.TextAlignmentRole: 0046 if orientation == Qt.Horizontal: 0047 if section in [3, 4]: 0048 return int(Qt.AlignLeft) 0049 return int(Qt.AlignHCenter | Qt.AlignVCenter) 0050 if role != Qt.DisplayRole: 0051 return None 0052 if orientation != Qt.Horizontal: 0053 return int(section + 1) 0054 result = '' 0055 if section < 5: 0056 result = [i18n('Table'), 0057 '', 0058 i18n('Players'), 0059 i18nc('table status', 0060 'Status'), 0061 i18n('Ruleset')][section] 0062 return result 0063 0064 def rowCount(self, parent=None): 0065 """how many tables are in the model?""" 0066 if parent and parent.isValid(): 0067 # we have only top level items 0068 return 0 0069 return len(self.tables) 0070 0071 def columnCount(self, unusedParent=None): # pylint: disable=no-self-use 0072 """for now we only have id (invisible), id (visible), players, 0073 status, ruleset.name. 0074 id(invisible) always holds the real id, also 1000 for suspended tables. 0075 id(visible) is what should be displayed.""" 0076 return 5 0077 0078 def data(self, index, role=Qt.DisplayRole): 0079 """score table""" 0080 # pylint: disable=too-many-branches,too-many-locals 0081 result = None 0082 if role == Qt.TextAlignmentRole: 0083 if index.column() == 0: 0084 result = int(Qt.AlignHCenter | Qt.AlignVCenter) 0085 else: 0086 result = int(Qt.AlignLeft | Qt.AlignVCenter) 0087 if index.isValid() and (0 <= index.row() < len(self.tables)): 0088 table = self.tables[index.row()] 0089 if role == Qt.DisplayRole and index.column() in (0, 1): 0090 result = table.tableid 0091 elif role == Qt.DisplayRole and index.column() == 2: 0092 players = [] 0093 zipped = list(zip(table.playerNames, table.playersOnline)) 0094 for idx, pair in enumerate(zipped): 0095 name, online = pair[0], pair[1] 0096 if idx < len(zipped) - 1: 0097 name += ', ' 0098 palette = KApplication.palette() 0099 if online: 0100 color = palette.color( 0101 QPalette.Active, 0102 QPalette.WindowText).name() 0103 style = ('font-weight:normal;' 0104 'font-style:normal;color:%s' 0105 % color) 0106 else: 0107 color = palette.color( 0108 QPalette.Disabled, 0109 QPalette.WindowText).name() 0110 style = ('font-weight:100;font-style:italic;color:%s' 0111 % color) 0112 players.append( 0113 '<nobr style="%s">' % 0114 style + 0115 name + 0116 '</nobr>') 0117 names = ''.join(players) 0118 result = names 0119 elif role == Qt.DisplayRole and index.column() == 3: 0120 status = table.status() 0121 if table.suspendedAt: 0122 dateVal = ' ' + datetime.datetime.strptime( 0123 table.suspendedAt, 0124 '%Y-%m-%dT%H:%M:%S').strftime('%c') 0125 status = 'Suspended' 0126 else: 0127 dateVal = '' 0128 result = i18nc('table status', status) + dateVal 0129 elif index.column() == 4: 0130 if role == Qt.DisplayRole: 0131 result = i18n((table.myRuleset if table.myRuleset else table.ruleset).name) 0132 elif role == Qt.ForegroundRole: 0133 palette = KApplication.palette() 0134 color = palette.windowText().color() if table.myRuleset else 'red' 0135 result = QColor(color) 0136 return result 0137 0138 0139 class SelectRuleset(QDialog): 0140 0141 """a dialog for selecting a ruleset""" 0142 0143 def __init__(self, server=None): 0144 QDialog.__init__(self, None) 0145 decorateWindow(self, i18n('Select a ruleset')) 0146 self.buttonBox = KDialogButtonBox(self) 0147 self.buttonBox.setStandardButtons( 0148 QDialogButtonBox.Cancel | QDialogButtonBox.Ok) 0149 self.buttonBox.accepted.connect(self.accept) 0150 self.buttonBox.rejected.connect(self.reject) 0151 self.cbRuleset = ListComboBox(Ruleset.selectableRulesets(server)) 0152 self.grid = QGridLayout() # our child SelectPlayers needs this 0153 self.grid.setColumnStretch(0, 1) 0154 self.grid.setColumnStretch(1, 6) 0155 vbox = QVBoxLayout(self) 0156 vbox.addLayout(self.grid) 0157 vbox.addWidget(self.cbRuleset) 0158 vbox.addWidget(self.buttonBox) 0159 0160 0161 class TableList(QWidget): 0162 0163 """a widget for viewing, joining, leaving tables""" 0164 # pylint: disable=too-many-instance-attributes 0165 0166 def __init__(self, client): 0167 super().__init__(None) 0168 self.autoStarted = False 0169 self.client = client 0170 self.setObjectName('TableList') 0171 self.resize(700, 400) 0172 self.view = MJTableView(self) 0173 self.differ = None 0174 self.debugModelTest = None 0175 self.requestedNewTable = False 0176 self.view.setItemDelegateForColumn( 0177 2, 0178 RichTextColumnDelegate(self.view)) 0179 0180 buttonBox = QDialogButtonBox(self) 0181 self.newButton = buttonBox.addButton( 0182 i18nc('allocate a new table', 0183 "&New"), 0184 QDialogButtonBox.ActionRole) 0185 self.newButton.setIcon(KIcon("document-new")) 0186 self.newButton.setToolTip(i18n("Allocate a new table")) 0187 self.newButton.clicked.connect(self.client.newTable) 0188 self.joinButton = buttonBox.addButton( 0189 i18n("&Join"), 0190 QDialogButtonBox.AcceptRole) 0191 self.joinButton.clicked.connect(client.joinTable) 0192 self.joinButton.setIcon(KIcon("list-add-user")) 0193 self.joinButton.setToolTip(i18n("Join a table")) 0194 self.leaveButton = buttonBox.addButton( 0195 i18n("&Leave"), 0196 QDialogButtonBox.AcceptRole) 0197 self.leaveButton.clicked.connect(self.leaveTable) 0198 self.leaveButton.setIcon(KIcon("list-remove-user")) 0199 self.leaveButton.setToolTip(i18n("Leave a table")) 0200 self.compareButton = buttonBox.addButton( 0201 i18nc('Kajongg-Ruleset', 0202 'Compare'), 0203 QDialogButtonBox.AcceptRole) 0204 self.compareButton.clicked.connect(self.compareRuleset) 0205 self.compareButton.setIcon(KIcon("preferences-plugin-script")) 0206 self.compareButton.setToolTip( 0207 i18n('Compare the rules of this table with my own rulesets')) 0208 self.chatButton = buttonBox.addButton( 0209 i18n('&Chat'), 0210 QDialogButtonBox.AcceptRole) 0211 self.chatButton.setIcon(KIcon("call-start")) 0212 self.chatButton.clicked.connect(self.chat) 0213 self.startButton = buttonBox.addButton( 0214 i18n('&Start'), 0215 QDialogButtonBox.AcceptRole) 0216 self.startButton.clicked.connect(self.startGame) 0217 self.startButton.setIcon(KIcon("arrow-right")) 0218 self.startButton.setToolTip( 0219 i18n("Start playing on a table. " 0220 "Empty seats will be taken by robot players.")) 0221 0222 cmdLayout = QHBoxLayout() 0223 cmdLayout.addWidget(buttonBox) 0224 0225 layout = QVBoxLayout() 0226 layout.addWidget(self.view) 0227 layout.addLayout(cmdLayout) 0228 self.setLayout(layout) 0229 0230 self.view.doubleClicked.connect(client.joinTable) 0231 StateSaver(self, self.view.horizontalHeader()) 0232 self.updateButtonsForTable(None) 0233 0234 def hideEvent(self, unusedEvent): # pylint: disable=no-self-use 0235 """table window hides""" 0236 scene = Internal.scene 0237 if scene: 0238 scene.startingGame = False 0239 model = self.view.model() 0240 if model: 0241 for table in model.tables: 0242 if table.chatWindow: 0243 table.chatWindow.hide() 0244 table.chatWindow = None 0245 if scene: 0246 if not scene.game or scene.game.client != self.client: 0247 # do we still need this connection? 0248 self.client.logout() 0249 0250 def chat(self): 0251 """chat. Only generate ChatWindow after the 0252 message has successfully been sent to the server. 0253 Because the server might have gone away.""" 0254 def initChat(_): 0255 """now that we were able to send the message to the server 0256 instantiate the chat window""" 0257 table.chatWindow = ChatWindow(table=table) 0258 table.chatWindow.receiveLine(msg) 0259 table = self.selectedTable() 0260 if not table.chatWindow: 0261 line = i18nE('opens a chat window') 0262 msg = ChatMessage( 0263 table.tableid, 0264 table.client.name, 0265 line, 0266 isStatusMessage=True) 0267 table.client.sendChat( 0268 msg).addCallback( 0269 initChat).addErrback( 0270 self.client.tableError) 0271 elif table.chatWindow.isVisible(): 0272 table.chatWindow.hide() 0273 else: 0274 table.chatWindow.show() 0275 0276 def show(self): 0277 """prepare the view and show it""" 0278 if self.client.hasLocalServer(): 0279 title = i18n( 0280 'Local Games with Ruleset %1', 0281 self.client.ruleset.name) 0282 else: 0283 title = i18n('Tables at %1', self.client.connection.url) 0284 decorateWindow(self, ' - '.join([self.client.name, title])) 0285 self.view.hideColumn(1) 0286 tableCount = self.view.model().rowCount( 0287 None) if self.view.model() else 0 0288 self.view.showColumn(0) 0289 self.view.showColumn(2) 0290 self.view.showColumn(4) 0291 if tableCount or not self.client.hasLocalServer(): 0292 QWidget.show(self) 0293 if self.client.hasLocalServer(): 0294 self.view.hideColumn(0) 0295 self.view.hideColumn(2) 0296 self.view.hideColumn(4) 0297 0298 def selectTable(self, idx): 0299 """select table by idx""" 0300 self.view.selectRow(idx) 0301 self.updateButtonsForTable(self.selectedTable()) 0302 0303 def updateButtonsForTable(self, table): 0304 """update button status for the currently selected table""" 0305 hasTable = bool(table) 0306 suspended = hasTable and bool(table.suspendedAt) 0307 running = hasTable and table.running 0308 suspendedLocalGame = ( 0309 suspended and table.gameid 0310 and self.client.hasLocalServer()) 0311 self.joinButton.setEnabled( 0312 hasTable and 0313 not running and 0314 not table.isOnline(self.client.name) and 0315 (self.client.name in table.playerNames) == suspended) 0316 self.leaveButton.setVisible(not suspendedLocalGame) 0317 self.compareButton.setVisible(not suspendedLocalGame) 0318 self.startButton.setVisible(not suspended) 0319 if suspendedLocalGame: 0320 self.newButton.setToolTip(i18n("Start a new game")) 0321 self.joinButton.setText( 0322 i18nc('resuming a local suspended game', '&Resume')) 0323 self.joinButton.setToolTip( 0324 i18n("Resume the selected suspended game")) 0325 else: 0326 self.newButton.setToolTip(i18n("Allocate a new table")) 0327 self.joinButton.setText(i18n('&Join')) 0328 self.joinButton.setToolTip(i18n("Join a table")) 0329 self.leaveButton.setEnabled( 0330 hasTable and not running and not self.joinButton.isEnabled()) 0331 self.startButton.setEnabled( 0332 not running and not suspendedLocalGame and hasTable 0333 and self.client.name == table.playerNames[0]) 0334 self.compareButton.setEnabled(hasTable and table.myRuleset is None) 0335 self.chatButton.setVisible(not self.client.hasLocalServer()) 0336 self.chatButton.setEnabled( 0337 not running and hasTable 0338 and self.client.name in table.playerNames 0339 and sum(x.startswith('Robot ') for x in table.playerNames) < 3) 0340 if self.chatButton.isEnabled(): 0341 self.chatButton.setToolTip(i18n( 0342 "Chat with others on this table")) 0343 else: 0344 self.chatButton.setToolTip(i18n( 0345 "For chatting with others on this table, " 0346 "please first take a seat")) 0347 0348 def selectionChanged(self, selected, unusedDeselected): 0349 """update button states according to selection""" 0350 if selected.indexes(): 0351 self.selectTable(selected.indexes()[0].row()) 0352 0353 def selectedTable(self): 0354 """return the selected table""" 0355 if self.view.selectionModel(): 0356 index = self.view.selectionModel().currentIndex() 0357 if index.isValid() and self.view.model(): 0358 return self.view.model().tables[index.row()] 0359 return None 0360 0361 def compareRuleset(self): 0362 """compare the ruleset of this table against ours""" 0363 table = self.selectedTable() 0364 self.differ = RulesetDiffer(table.ruleset, Ruleset.availableRulesets()) 0365 self.differ.show() 0366 0367 def startGame(self): 0368 """start playing at the selected table""" 0369 table = self.selectedTable() 0370 self.startButton.setEnabled(False) 0371 self.client.callServer( 0372 'startGame', 0373 table.tableid).addErrback( 0374 self.client.tableError) 0375 0376 def leaveTable(self): 0377 """leave a table""" 0378 table = self.selectedTable() 0379 self.client.callServer( 0380 'leaveTable', 0381 table.tableid).addErrback( 0382 self.client.tableError) 0383 0384 def __keepChatWindows(self, tables): 0385 """copy chatWindows from the old table list which will be 0386 thrown away""" 0387 if self.view.model(): 0388 chatWindows = {x.tableid: x.chatWindow for x in self.view.model().tables} 0389 unusedWindows = {x.chatWindow for x in self.view.model().tables} 0390 for table in tables: 0391 table.chatWindow = chatWindows.get(table.tableid, None) 0392 unusedWindows -= {table.chatWindow} 0393 for unusedWindow in unusedWindows: 0394 if unusedWindow: 0395 unusedWindow.hide() 0396 0397 def __preselectTableId(self, tables): 0398 """which table should be preselected? 0399 If we just requested a new table: 0400 select first new table. 0401 Only in the rare case that two clients request a new table at 0402 the same moment, this could put the focus on the wrong table. 0403 Ignore that for now. 0404 else if we had one selected: 0405 select that again 0406 else: 0407 select first table""" 0408 if self.requestedNewTable: 0409 self.requestedNewTable = False 0410 model = self.view.model() 0411 if model: 0412 oldIds = {x.tableid for x in model.tables} 0413 newIds = sorted({x.tableid for x in tables} - oldIds) 0414 if newIds: 0415 return newIds[0] 0416 if self.selectedTable(): 0417 return self.selectedTable().tableid 0418 return 0 0419 0420 def loadTables(self, tables): 0421 """build and use a model around the tables. 0422 Show all new tables (no gameid given yet) and all suspended 0423 tables that also exist locally. In theory all suspended games should 0424 exist locally but there might have been bugs or somebody might 0425 have removed the local database like when reinstalling linux""" 0426 if not Internal.scene: 0427 return 0428 if Debug.table: 0429 for table in tables: 0430 if table.gameid and not table.gameExistsLocally(): 0431 logDebug('Table %s does not exist locally' % table) 0432 tables = [x for x in tables if not x.gameid or x.gameExistsLocally()] 0433 tables.sort(key=lambda x: x.tableid) 0434 preselectTableId = self.__preselectTableId(tables) 0435 self.__keepChatWindows(tables) 0436 model = TablesModel(tables) 0437 self.view.setModel(model) 0438 if Debug.modelTest: 0439 self.debugModelTest = ModelTest(model, self.view) 0440 selection = QItemSelectionModel(model, self.view) 0441 self.view.initView() 0442 self.view.setSelectionModel(selection) 0443 self.view.setSelectionBehavior(QAbstractItemView.SelectRows) 0444 self.view.setSelectionMode(QAbstractItemView.SingleSelection) 0445 selection.selectionChanged.connect(self.selectionChanged) 0446 if len(tables) == 1: 0447 self.selectTable(0) 0448 self.startButton.setFocus() 0449 elif not tables: 0450 self.newButton.setFocus() 0451 else: 0452 _ = [x for x in tables if x.tableid >= preselectTableId] 0453 self.selectTable(tables.index(_[0]) if _ else 0) 0454 self.updateButtonsForTable(self.selectedTable()) 0455 self.view.setFocus()