def setupUi(self): """create all other widgets we could make the scene view the central widget but I did not figure out how to correctly draw the background with QGraphicsView/QGraphicsScene. QGraphicsView.drawBackground always wants a pixmap for a huge rect like 4000x3000 where my screen only has 1920x1200""" # pylint: disable=R0915 self.setObjectName("MainWindow") centralWidget = QWidget() scene = MJScene() self.centralScene = scene self.centralView = FittingView() layout = QGridLayout(centralWidget) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.centralView) self.tileset = None # just for pylint self.background = None # just for pylint self.tilesetName = Preferences.tilesetName self.windTileset = Tileset(Preferences.windTilesetName) self.discardBoard = DiscardBoard() self.discardBoard.setVisible(False) scene.addItem(self.discardBoard) self.selectorBoard = SelectorBoard() self.selectorBoard.setVisible(False) scene.addItem(self.selectorBoard) self.setCentralWidget(centralWidget) self.centralView.setScene(scene) self.centralView.setFocusPolicy(Qt.StrongFocus) self.adjustView() self.actionScoreGame = self.__kajonggAction("scoreGame", "draw-freehand", self.scoreGame, Qt.Key_C) self.actionPlayGame = self.__kajonggAction("play", "arrow-right", self.playGame, Qt.Key_N) self.actionAbortGame = self.__kajonggAction("abort", "dialog-close", self.abortAction, Qt.Key_W) self.actionAbortGame.setEnabled(False) self.actionQuit = self.__kajonggAction("quit", "application-exit", self.close, Qt.Key_Q) self.actionPlayers = self.__kajonggAction("players", "im-user", self.slotPlayers) self.actionRulesets = self.__kajonggAction("rulesets", "games-kajongg-law", self.slotRulesets) self.actionChat = self.__kajonggToggleAction("chat", "call-start", shortcut=Qt.Key_H, actionData=ChatWindow) game = self.game self.actionChat.setEnabled(bool(game) and bool(game.client) and not game.client.hasLocalServer()) self.actionChat.setChecked(bool(game) and bool(game.client) and bool(game.client.table.chatWindow)) self.actionScoring = self.__kajonggToggleAction("scoring", "draw-freehand", shortcut=Qt.Key_S, actionData=ScoringDialog) self.actionScoring.setEnabled(False) self.actionAngle = self.__kajonggAction("angle", "object-rotate-left", self.changeAngle, Qt.Key_G) self.actionAngle.setEnabled(False) self.actionFullscreen = KToggleFullScreenAction(self.actionCollection()) self.actionFullscreen.setShortcut(Qt.CTRL + Qt.Key_F) self.actionFullscreen.setShortcutContext(Qt.ApplicationShortcut) self.actionFullscreen.setWindow(self) self.actionCollection().addAction("fullscreen", self.actionFullscreen) self.actionFullscreen.toggled.connect(self.fullScreen) self.actionScoreTable = self.__kajonggToggleAction("scoreTable", "format-list-ordered", Qt.Key_T, actionData=ScoreTable) self.actionExplain = self.__kajonggToggleAction("explain", "applications-education", Qt.Key_E, actionData=ExplainView) self.actionAutoPlay = self.__kajonggAction("demoMode", "arrow-right-double", None, Qt.Key_D) self.actionAutoPlay.setCheckable(True) self.actionAutoPlay.toggled.connect(self.__toggleDemoMode) self.actionAutoPlay.setChecked(Internal.autoPlay) QMetaObject.connectSlotsByName(self)
class PlayField(KXmlGuiWindow): """the main window""" # pylint: disable=R0902 # pylint we need more than 10 instance attributes def __init__(self): # see http://lists.kde.org/?l=kde-games-devel&m=120071267328984&w=2 Internal.field = self self.game = None self.__startingGame = False self.ignoreResizing = 1 super(PlayField, self).__init__() self.background = None self.showShadows = None self._clientDialog = None self.playerWindow = None self.rulesetWindow = None self.scoreTable = None self.explainView = None self.scoringDialog = None self.confDialog = None self.setupUi() KStandardAction.preferences(self.showSettings, self.actionCollection()) self.applySettings() self.setupGUI() self.retranslateUi() for action in self.toolBar().actions(): if 'onfigure' in action.text(): action.setPriority(QAction.LowPriority) if Options.host: self.playGame() @property def clientDialog(self): """wrapper: hide dialog when it is set to None""" return self._clientDialog @clientDialog.setter def clientDialog(self, value): """wrapper: hide dialog when it is set to None""" if isAlive(self._clientDialog) and not value: self._clientDialog.timer.stop() self._clientDialog.hide() self._clientDialog = value def sizeHint(self): """give the main window a sensible default size""" result = KXmlGuiWindow.sizeHint(self) result.setWidth(result.height() * 3 // 2) # we want space to the right for the buttons # the default is too small. Use at least 2/3 of screen height and 1/2 of screen width: available = KApplication.kApplication().desktop().availableGeometry() height = max(result.height(), available.height() * 2 // 3) width = max(result.width(), available.width() // 2) result.setHeight(height) result.setWidth(width) return result def resizeEvent(self, event): """Use this hook to determine if we want to ignore one more resize event happening for maximized / almost maximized windows. this misses a few cases where the window is almost maximized because at this point the window has no border yet: event.size, self.geometry() and self.frameGeometry are all the same. So we cannot check if the bordered window would fit into availableGeometry. """ available = KApplication.kApplication().desktop().availableGeometry() if self.ignoreResizing == 1: # at startup if available.width() <= event.size().width() \ or available.height() <= event.size().height(): self.ignoreResizing += 1 KXmlGuiWindow.resizeEvent(self, event) if self.clientDialog: self.clientDialog.placeInField() def showEvent(self, event): """force a resize which calculates the correct background image size""" self.centralView.resizeEvent(True) KXmlGuiWindow.showEvent(self, event) def handSelectorChanged(self, handBoard): """update all relevant dialogs""" if self.scoringDialog: self.scoringDialog.slotInputChanged() if self.game and not self.game.finished(): self.game.wall.decoratePlayer(handBoard.player) # pylint: disable=E1101 # first decorate walls - that will compute player.handBoard for explainView if self.explainView: self.explainView.refresh(self.game) def __kajonggAction(self, name, icon, slot=None, shortcut=None, actionData=None): """simplify defining actions""" res = KAction(self) res.setIcon(KIcon(icon)) if slot: res.triggered.connect(slot) self.actionCollection().addAction(name, res) if shortcut: res.setShortcut( Qt.CTRL + shortcut) res.setShortcutContext(Qt.ApplicationShortcut) if PYQT_VERSION_STR != '4.5.2' or actionData is not None: res.setData(QVariant(actionData)) return res def __kajonggToggleAction(self, name, icon, shortcut=None, actionData=None): """a checkable action""" res = self.__kajonggAction(name, icon, shortcut=shortcut, actionData=actionData) res.setCheckable(True) res.toggled.connect(self.__toggleWidget) return res def setupUi(self): """create all other widgets we could make the scene view the central widget but I did not figure out how to correctly draw the background with QGraphicsView/QGraphicsScene. QGraphicsView.drawBackground always wants a pixmap for a huge rect like 4000x3000 where my screen only has 1920x1200""" # pylint: disable=R0915 self.setObjectName("MainWindow") centralWidget = QWidget() scene = MJScene() self.centralScene = scene self.centralView = FittingView() layout = QGridLayout(centralWidget) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.centralView) self.tileset = None # just for pylint self.background = None # just for pylint self.tilesetName = Preferences.tilesetName self.windTileset = Tileset(Preferences.windTilesetName) self.discardBoard = DiscardBoard() self.discardBoard.setVisible(False) scene.addItem(self.discardBoard) self.selectorBoard = SelectorBoard() self.selectorBoard.setVisible(False) scene.addItem(self.selectorBoard) self.setCentralWidget(centralWidget) self.centralView.setScene(scene) self.centralView.setFocusPolicy(Qt.StrongFocus) self.adjustView() self.actionScoreGame = self.__kajonggAction("scoreGame", "draw-freehand", self.scoreGame, Qt.Key_C) self.actionPlayGame = self.__kajonggAction("play", "arrow-right", self.playGame, Qt.Key_N) self.actionAbortGame = self.__kajonggAction("abort", "dialog-close", self.abortAction, Qt.Key_W) self.actionAbortGame.setEnabled(False) self.actionQuit = self.__kajonggAction("quit", "application-exit", self.close, Qt.Key_Q) self.actionPlayers = self.__kajonggAction("players", "im-user", self.slotPlayers) self.actionRulesets = self.__kajonggAction("rulesets", "games-kajongg-law", self.slotRulesets) self.actionChat = self.__kajonggToggleAction("chat", "call-start", shortcut=Qt.Key_H, actionData=ChatWindow) game = self.game self.actionChat.setEnabled(bool(game) and bool(game.client) and not game.client.hasLocalServer()) self.actionChat.setChecked(bool(game) and bool(game.client) and bool(game.client.table.chatWindow)) self.actionScoring = self.__kajonggToggleAction("scoring", "draw-freehand", shortcut=Qt.Key_S, actionData=ScoringDialog) self.actionScoring.setEnabled(False) self.actionAngle = self.__kajonggAction("angle", "object-rotate-left", self.changeAngle, Qt.Key_G) self.actionAngle.setEnabled(False) self.actionFullscreen = KToggleFullScreenAction(self.actionCollection()) self.actionFullscreen.setShortcut(Qt.CTRL + Qt.Key_F) self.actionFullscreen.setShortcutContext(Qt.ApplicationShortcut) self.actionFullscreen.setWindow(self) self.actionCollection().addAction("fullscreen", self.actionFullscreen) self.actionFullscreen.toggled.connect(self.fullScreen) self.actionScoreTable = self.__kajonggToggleAction("scoreTable", "format-list-ordered", Qt.Key_T, actionData=ScoreTable) self.actionExplain = self.__kajonggToggleAction("explain", "applications-education", Qt.Key_E, actionData=ExplainView) self.actionAutoPlay = self.__kajonggAction("demoMode", "arrow-right-double", None, Qt.Key_D) self.actionAutoPlay.setCheckable(True) self.actionAutoPlay.toggled.connect(self.__toggleDemoMode) self.actionAutoPlay.setChecked(Internal.autoPlay) QMetaObject.connectSlotsByName(self) def showWall(self): """shows the wall according to the game rules (lenght may vary)""" UIWall(self.game) # sets self.game.wall if self.discardBoard: # scale it such that it uses the place within the wall optimally. # we need to redo this because the wall length can vary between games. self.discardBoard.maximize() def genPlayers(self): """generate four default VisiblePlayers""" return Players([VisiblePlayer(self.game, idx) for idx in range(4)]) def fullScreen(self, toggle): """toggle between full screen and normal view""" self.actionFullscreen.setFullScreen(self, toggle) def abortAction(self): """abort current game""" def doNotQuit(dummy): """ignore failure to abort""" self.abort().addErrback(doNotQuit) def abort(self): """abort current game""" def gotAnswer(result, autoPlaying): """user answered""" if result: return self.abortGame() else: self.actionAutoPlay.setChecked(autoPlaying) return fail(Exception('no abort')) def gotError(result): """abortGame failed""" logDebug('abortGame error:%s/%s ' % (str(result), result.getErrorMessage())) if not self.game: self.startingGame = False return succeed(None) autoPlaying = self.actionAutoPlay.isChecked() self.actionAutoPlay.setChecked(False) if self.game.finished(): return self.abortGame() else: return QuestionYesNo(m18n("Do you really want to abort this game?"), always=True).addCallback( gotAnswer, autoPlaying).addErrback(gotError) def abortGame(self): """if a game is active, abort it""" if self.game is None: # meanwhile somebody else might have aborted return succeed(None) game = self.game self.game = None return game.close() def closeEvent(self, event): """somebody wants us to close, maybe ALT-F4 or so""" event.ignore() def doNotQuit(dummy): """ignore failure to abort""" self.abort().addCallback(HumanClient.shutdownHumanClients).addCallbacks(Client.quitProgram, doNotQuit) def __moveTile(self, tile, wind, lowerHalf): """the user pressed a wind letter or X for center, wanting to move a tile there""" # this tells the receiving board that this is keyboard, not mouse navigation> # needed for useful placement of the popup menu assert self.game.isScoringGame() assert isinstance(tile, Tile), (tile, str(tile)) currentBoard = tile.board dragTile, dragMeld = currentBoard.dragObject(tile) if wind == 'X': receiver = self.selectorBoard else: receiver = self.game.players[wind].handBoard if receiver != currentBoard or bool(lowerHalf) != bool(tile.yoffset): movingLastMeld = tile.element in self.computeLastMeld().pairs if movingLastMeld: self.scoringDialog.clearLastTileCombo() receiver.dropHere(dragTile, dragMeld, lowerHalf) if movingLastMeld and receiver == currentBoard: self.scoringDialog.fillLastTileCombo() def __navigateScoringGame(self, event): """keyboard navigation in a scoring game""" mod = event.modifiers() key = event.key() wind = chr(key%128) moveCommands = m18nc('kajongg:keyboard commands for moving tiles to the players ' \ 'with wind ESWN or to the central tile selector (X)', 'ESWNX') tile = self.centralScene.focusItem().tile if wind in moveCommands: # translate i18n wind key to ESWN: wind = 'ESWNX'[moveCommands.index(wind)] self.__moveTile(tile, wind, mod &Qt.ShiftModifier) return True if key == Qt.Key_Tab and self.game: tabItems = [self.selectorBoard] tabItems.extend(list(p.handBoard for p in self.game.players if p.handBoard.tiles)) tabItems.append(tabItems[0]) currentBoard = tile.board if isinstance(tile, Tile) else None currIdx = 0 while tabItems[currIdx] != currentBoard and currIdx < len(tabItems) -2: currIdx += 1 tabItems[currIdx+1].hasFocus = True return True def keyPressEvent(self, event): """navigate in the selectorboard""" mod = event.modifiers() if mod in (Qt.NoModifier, Qt.ShiftModifier): if self.game and self.game.isScoringGame(): if self.__navigateScoringGame(event): return if self.clientDialog: self.clientDialog.keyPressEvent(event) KXmlGuiWindow.keyPressEvent(self, event) def retranslateUi(self): """retranslate""" self.actionScoreGame.setText(m18nc('@action:inmenu', "&Score Manual Game")) self.actionScoreGame.setIconText(m18nc('@action:intoolbar', 'Manual Game')) self.actionScoreGame.setHelpText(m18nc('kajongg @info:tooltip', '&Score a manual game.')) self.actionPlayGame.setText(m18nc('@action:intoolbar', "&Play")) self.actionPlayGame.setPriority(QAction.LowPriority) self.actionPlayGame.setHelpText(m18nc('kajongg @info:tooltip', 'Start a new game.')) self.actionAbortGame.setText(m18nc('@action:inmenu', "&Abort Game")) self.actionAbortGame.setPriority(QAction.LowPriority) self.actionAbortGame.setHelpText(m18nc('kajongg @info:tooltip', 'Abort the current game.')) self.actionQuit.setText(m18nc('@action:inmenu', "&Quit Kajongg")) self.actionQuit.setPriority(QAction.LowPriority) self.actionPlayers.setText(m18nc('@action:intoolbar', "&Players")) self.actionPlayers.setHelpText(m18nc('kajongg @info:tooltip', 'define your players.')) self.actionRulesets.setText(m18nc('@action:intoolbar', "&Rulesets")) self.actionRulesets.setHelpText(m18nc('kajongg @info:tooltip', 'customize rulesets.')) self.actionAngle.setText(m18nc('@action:inmenu', "&Change Visual Angle")) self.actionAngle.setIconText(m18nc('@action:intoolbar', "Angle")) self.actionAngle.setHelpText(m18nc('kajongg @info:tooltip', "Change the visual appearance of the tiles.")) self.actionScoring.setText(m18nc('@action:inmenu', "&Show Scoring Editor")) self.actionScoring.setIconText(m18nc('@action:intoolbar', "&Scoring")) self.actionScoring.setHelpText(m18nc('kajongg @info:tooltip', "Show or hide the scoring editor for a manual game.")) self.actionScoreTable.setText(m18nc('kajongg @action:inmenu', "&Score Table")) self.actionScoreTable.setIconText(m18nc('kajongg @action:intoolbar', "&Scores")) self.actionScoreTable.setHelpText(m18nc('kajongg @info:tooltip', "Show or hide the score table for the current game.")) self.actionExplain.setText(m18nc('@action:inmenu', "&Explain Scores")) self.actionExplain.setIconText(m18nc('@action:intoolbar', "&Explain")) self.actionExplain.setHelpText(m18nc('kajongg @info:tooltip', 'Explain the scoring for all players in the current game.')) self.actionAutoPlay.setText(m18nc('@action:inmenu', "&Demo Mode")) self.actionAutoPlay.setPriority(QAction.LowPriority) self.actionAutoPlay.setHelpText(m18nc('kajongg @info:tooltip', 'Let the computer take over for you. Start a new local game if needed.')) self.actionChat.setText(m18n("C&hat")) self.actionChat.setHelpText(m18nc('kajongg @info:tooltip', 'Chat with the other players.')) def changeEvent(self, event): """when the applicationwide language changes, recreate GUI""" if event.type() == QEvent.LanguageChange: self.setupGUI() self.retranslateUi() def slotPlayers(self): """show the player list""" if not self.playerWindow: self.playerWindow = PlayerList(self) self.playerWindow.show() def slotRulesets(self): """show the player list""" if not self.rulesetWindow: self.rulesetWindow = RulesetSelector() self.rulesetWindow.show() def selectScoringGame(self): """show all games, select an existing game or create a new game""" Players.load() if len(Players.humanNames) < 4: logWarning(m18n('Please define four players in <interface>Settings|Players</interface>')) return False gameSelector = Games(self) if gameSelector.exec_(): selected = gameSelector.selectedGame if selected is not None: ScoringGame.loadFromDB(selected) else: self.newGame() if self.game: self.game.throwDices() gameSelector.close() self.updateGUI() return bool(self.game) def scoreGame(self): """score a local game""" if self.selectScoringGame(): self.actionScoring.setChecked(True) def playGame(self): """play a remote game: log into a server and show its tables""" self.startingGame = True HumanClient() def adjustView(self): """adjust the view such that exactly the wanted things are displayed without having to scroll""" if not Internal.scaleScene: return if self.game: with Animated(False): self.game.wall.decorate() if self.discardBoard: self.discardBoard.maximize() if self.selectorBoard: self.selectorBoard.maximize() for tile in self.game.wall.tiles: if tile.board: tile.board.placeTile(tile) view, scene = self.centralView, self.centralScene oldRect = view.sceneRect() view.setSceneRect(scene.itemsBoundingRect()) newRect = view.sceneRect() if oldRect != newRect: view.fitInView(scene.itemsBoundingRect(), Qt.KeepAspectRatio) @property def startingGame(self): """are we trying to start a game?""" return self.__startingGame @startingGame.setter def startingGame(self, value): """are we trying to start a game?""" if value != self.__startingGame: self.__startingGame = value self.updateGUI() @property def tilesetName(self): """the name of the current tileset""" return self.tileset.desktopFileName @tilesetName.setter def tilesetName(self, name): """the name of the current tileset""" self.tileset = Tileset(name) @property def backgroundName(self): """setting this also actually changes the background""" return self.background.desktopFileName if self.background else '' @backgroundName.setter def backgroundName(self, name): """setter for backgroundName""" self.background = Background(name) self.background.setPalette(self.centralWidget()) self.centralWidget().setAutoFillBackground(True) def applySettings(self): """apply preferences""" # pylint: disable=R0912 # too many branches self.actionAngle.setEnabled(bool(self.game) and Preferences.showShadows) animate() # drain the queue afterCurrentAnimationDo(self.__applySettings2) def __applySettings2(self, dummyResults): """now no animation is running""" with Animated(False): if self.tilesetName != Preferences.tilesetName: self.tilesetName = Preferences.tilesetName if self.game: self.game.wall.tileset = self.tileset for item in self.centralScene.nonTiles(): try: item.tileset = self.tileset except AttributeError: continue # change players last because we need the wall already to be repositioned self.adjustView() # the new tiles might be larger if self.game: for player in self.game.players: if player.handBoard: player.handBoard.rearrangeMelds = Preferences.rearrangeMelds if self.backgroundName != Preferences.backgroundName: self.backgroundName = Preferences.backgroundName if self.showShadows is None or self.showShadows != Preferences.showShadows: self.showShadows = Preferences.showShadows if self.game: wall = self.game.wall wall.showShadows = self.showShadows self.selectorBoard.showShadows = self.showShadows if self.discardBoard: self.discardBoard.showShadows = self.showShadows for tile in self.centralScene.graphicsTileItems(): tile.setClippingFlags() self.adjustView() Sound.enabled = Preferences.useSounds self.centralScene.placeFocusRect() def showSettings(self): """show preferences dialog. If it already is visible, do nothing""" if KConfigDialog.showDialog("settings"): return # if an animation is running, Qt segfaults somewhere deep # in the SVG renderer rendering the wind tiles for the tile # preview afterCurrentAnimationDo(self.__showSettings2) def __showSettings2(self, dummyResult): """now that no animation is running, show settings dialog""" self.confDialog = ConfigDialog(self, "settings") self.confDialog.settingsChanged.connect(self.applySettings) self.confDialog.show() def newGame(self): """asks user for players and ruleset for a new game and returns that new game""" Players.load() # we want to make sure we have the current definitions selectDialog = SelectPlayers(self.game) if not selectDialog.exec_(): return return ScoringGame(selectDialog.names, selectDialog.cbRuleset.current) def __toggleWidget(self, checked): """user has toggled widget visibility with an action""" action = self.sender() actionData = action.data().toPyObject() if checked: if isinstance(actionData, type): actionData = actionData(game=self.game) action.setData(QVariant(actionData)) if isinstance(actionData, ScoringDialog): self.scoringDialog = actionData actionData.btnSave.clicked.connect(self.nextScoringHand) actionData.scoringClosed.connect(self.__scoringClosed) elif isinstance(actionData, ExplainView): self.explainView = actionData elif isinstance(actionData, ScoreTable): self.scoreTable = actionData actionData.show() actionData.raise_() else: assert actionData actionData.hide() def __toggleDemoMode(self, checked): """switch on / off for autoPlay""" if self.game: self.centralScene.placeFocusRect() # show/hide it self.game.autoPlay = checked if checked and self.clientDialog: self.clientDialog.proposeAction() # an illegal action might have focus self.clientDialog.selectButton() # select default, abort timeout else: Internal.autoPlay = checked if checked: # TODO: use the last used ruleset. Right now it always takes the first of the list. self.playGame() def __scoringClosed(self): """the scoring window has been closed with ALT-F4 or similar""" self.actionScoring.setChecked(False) def nextScoringHand(self): """save hand to database, update score table and balance in status line, prepare next hand""" if self.game.winner: for player in self.game.players: player.usedDangerousFrom = None for ruleBox in player.manualRuleBoxes: rule = ruleBox.rule if rule.name == 'Dangerous Game' and ruleBox.isChecked(): self.game.winner.usedDangerousFrom = player self.game.saveHand() self.game.maybeRotateWinds() self.game.prepareHand() self.game.initHand() def prepareHand(self): """redecorate wall""" self.updateGUI() if self.game: self.game.wall.decorate() if self.scoringDialog: self.scoringDialog.clearLastTileCombo() def updateGUI(self): """update some actions, all auxiliary windows and the statusbar""" if not isAlive(self): return title = '' connections = list(x.connection for x in HumanClient.humanClients if x.connection) game = self.game if not game: title = ', '.join('{name}/{url}'.format(name=x.username, url=x.url) for x in connections) if title: self.setWindowTitle('%s - Kajongg' % title) for action in [self.actionScoreGame, self.actionPlayGame]: action.setEnabled(not bool(game)) self.actionAbortGame.setEnabled(bool(game)) self.actionAngle.setEnabled(bool(game) and self.showShadows) scoring = bool(game and game.isScoringGame()) self.selectorBoard.setVisible(scoring) self.selectorBoard.setEnabled(scoring) self.discardBoard.setVisible(bool(game) and not scoring) self.actionScoring.setEnabled(scoring and not game.finished()) self.actionAutoPlay.setEnabled(not self.startingGame and not scoring) self.actionChat.setEnabled(bool(game) and bool(game.client) and not game.client.hasLocalServer() and not self.startingGame) # chatting on tables before game started works with chat button per table self.actionChat.setChecked(self.actionChat.isEnabled() and bool(game.client.table.chatWindow)) if self.actionScoring.isChecked(): self.actionScoring.setChecked(scoring and not game.finished()) for view in [self.scoringDialog, self.explainView, self.scoreTable]: if view: view.refresh(game) self.__showBalance() def changeAngle(self): """change the lightSource""" if self.game: afterCurrentAnimationDo(self.__changeAngle2) def __changeAngle2(self, dummyResult): """now that no animation is running, really change""" if self.game: # might be finished meanwhile with Animated(False): wall = self.game.wall oldIdx = LIGHTSOURCES.index(wall.lightSource) # pylint: disable=E1101 newLightSource = LIGHTSOURCES[(oldIdx + 1) % 4] wall.lightSource = newLightSource self.selectorBoard.lightSource = newLightSource self.discardBoard.lightSource = newLightSource self.adjustView() scoringDialog = self.actionScoring.data().toPyObject() if isinstance(scoringDialog, ScoringDialog): scoringDialog.computeScores() self.centralScene.placeFocusRect() def __showBalance(self): """show the player balances in the status bar""" sBar = self.statusBar() if self.game: for idx, player in enumerate(self.game.players): sbMessage = player.localName + ': ' + str(player.balance) if sBar.hasItem(idx): sBar.changeItem(sbMessage, idx) else: sBar.insertItem(sbMessage, idx, 1) sBar.setItemAlignment(idx, Qt.AlignLeft) else: for idx in range(5): if sBar.hasItem(idx): sBar.removeItem(idx) def computeLastTile(self): """compile hand info into a string as needed by the scoring engine""" if self.scoringDialog: return self.scoringDialog.computeLastTile() def computeLastMeld(self): """compile hand info into a string as needed by the scoring engine""" if self.scoringDialog: cbLastMeld = self.scoringDialog.cbLastMeld idx = cbLastMeld.currentIndex() if idx >= 0: return Meld(str(cbLastMeld.itemData(idx).toString())) return Meld() @staticmethod def askSwap(swappers): """use this as a proxy such that module game does not have to import playfield. Game should also run on a server without KDE being installed""" return SwapDialog(swappers).exec_()
def setupUi(self): """create all other widgets we could make the scene view the central widget but I did not figure out how to correctly draw the background with QGraphicsView/QGraphicsScene. QGraphicsView.drawBackground always wants a pixmap for a huge rect like 4000x3000 where my screen only has 1920x1200""" # pylint: disable=R0915 self.setObjectName("MainWindow") centralWidget = QWidget() scene = MJScene() self.centralScene = scene self.centralView = FittingView() layout = QGridLayout(centralWidget) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.centralView) self.tileset = None # just for pylint self.background = None # just for pylint self.tilesetName = Preferences.tilesetName self.windTileset = Tileset(Preferences.windTilesetName) self.discardBoard = DiscardBoard() self.discardBoard.setVisible(False) scene.addItem(self.discardBoard) self.selectorBoard = SelectorBoard() self.selectorBoard.setVisible(False) scene.addItem(self.selectorBoard) self.setCentralWidget(centralWidget) self.centralView.setScene(scene) self.centralView.setFocusPolicy(Qt.StrongFocus) self.adjustView() self.actionScoreGame = self.__kajonggAction("scoreGame", "draw-freehand", self.scoreGame, Qt.Key_C) self.actionPlayGame = self.__kajonggAction("play", "arrow-right", self.playGame, Qt.Key_N) self.actionAbortGame = self.__kajonggAction("abort", "dialog-close", self.abortAction, Qt.Key_W) self.actionAbortGame.setEnabled(False) self.actionQuit = self.__kajonggAction("quit", "application-exit", self.close, Qt.Key_Q) self.actionPlayers = self.__kajonggAction("players", "im-user", self.slotPlayers) self.actionRulesets = self.__kajonggAction("rulesets", "games-kajongg-law", self.slotRulesets) self.actionChat = self.__kajonggToggleAction("chat", "call-start", shortcut=Qt.Key_H, actionData=ChatWindow) game = self.game self.actionChat.setEnabled( bool(game) and bool(game.client) and not game.client.hasLocalServer()) self.actionChat.setChecked( bool(game) and bool(game.client) and bool(game.client.table.chatWindow)) self.actionScoring = self.__kajonggToggleAction( "scoring", "draw-freehand", shortcut=Qt.Key_S, actionData=ScoringDialog) self.actionScoring.setEnabled(False) self.actionAngle = self.__kajonggAction("angle", "object-rotate-left", self.changeAngle, Qt.Key_G) self.actionAngle.setEnabled(False) self.actionFullscreen = KToggleFullScreenAction( self.actionCollection()) self.actionFullscreen.setShortcut(Qt.CTRL + Qt.Key_F) self.actionFullscreen.setShortcutContext(Qt.ApplicationShortcut) self.actionFullscreen.setWindow(self) self.actionCollection().addAction("fullscreen", self.actionFullscreen) self.actionFullscreen.toggled.connect(self.fullScreen) self.actionScoreTable = self.__kajonggToggleAction( "scoreTable", "format-list-ordered", Qt.Key_T, actionData=ScoreTable) self.actionExplain = self.__kajonggToggleAction( "explain", "applications-education", Qt.Key_E, actionData=ExplainView) self.actionAutoPlay = self.__kajonggAction("demoMode", "arrow-right-double", None, Qt.Key_D) self.actionAutoPlay.setCheckable(True) self.actionAutoPlay.toggled.connect(self.__toggleDemoMode) self.actionAutoPlay.setChecked(Internal.autoPlay) QMetaObject.connectSlotsByName(self)
class MainWindow(KXmlGuiWindow): """the main window""" # pylint: disable=too-many-instance-attributes def __init__(self): # see http://lists.kde.org/?l=kde-games-devel&m=120071267328984&w=2 super(MainWindow, self).__init__() Internal.app.aboutToQuit.connect(self.aboutToQuit) self.exitConfirmed = None self.exitReady = None self.exitWaitTime = None Internal.mainWindow = self self._scene = None self.centralView = None self.background = None self.playerWindow = None self.rulesetWindow = None self.confDialog = None if Options.gui: KStandardAction.preferences( self.showSettings, self.actionCollection()) self.setupUi() self.setupGUI() Internal.Preferences.addWatch( 'tilesetName', self.tilesetNameChanged) Internal.Preferences.addWatch( 'backgroundName', self.backgroundChanged) self.retranslateUi() for action in self.toolBar().actions(): if 'onfigure' in action.text(): action.setPriority(QAction.LowPriority) if Options.host and not Options.demo: self.scene = PlayingScene(self) HumanClient() StateSaver(self) self.show() else: HumanClient() @property def scene(self): """a proxy""" return self._scene @scene.setter def scene(self, value): """if changing, updateGUI""" if not isAlive(self): return if self._scene == value: return if not value: self.actionChat.setChecked(False) self.actionExplain.setChecked(False) self.actionScoreTable.setChecked(False) self.actionExplain.setData(toQVariant(ExplainView)) self.actionScoreTable.setData(toQVariant(ScoreTable)) self._scene = value self.centralView.setScene(value) self.adjustView() self.updateGUI() self.actionChat.setEnabled(isinstance(value, PlayingScene)) self.actionExplain.setEnabled(value is not None) self.actionScoreTable.setEnabled(value is not None) def sizeHint(self): """give the main window a sensible default size""" result = KXmlGuiWindow.sizeHint(self) result.setWidth(result.height() * 3 // 2) # we want space to the right for the buttons # the default is too small. Use at least 2/3 of screen height and 1/2 # of screen width: available = KApplication.kApplication().desktop().availableGeometry() height = max(result.height(), available.height() * 2 // 3) width = max(result.width(), available.width() // 2) result.setHeight(height) result.setWidth(width) return result def showEvent(self, event): """force a resize which calculates the correct background image size""" self.centralView.resizeEvent(True) KXmlGuiWindow.showEvent(self, event) def kajonggAction( self, name, icon, slot=None, shortcut=None, actionData=None): """simplify defining actions""" res = KAction(self) if icon: res.setIcon(KIcon(icon)) if slot: res.triggered.connect(slot) self.actionCollection().addAction(name, res) if shortcut: res.setShortcut(Qt.CTRL + shortcut) res.setShortcutContext(Qt.ApplicationShortcut) if PYQT_VERSION_STR != '4.5.2' or actionData is not None: res.setData(toQVariant(actionData)) return res def _kajonggToggleAction(self, name, icon, shortcut=None, actionData=None): """a checkable action""" res = self.kajonggAction( name, icon, shortcut=shortcut, actionData=actionData) res.setCheckable(True) res.toggled.connect(self._toggleWidget) return res def setupUi(self): """create all other widgets we could make the scene view the central widget but I did not figure out how to correctly draw the background with QGraphicsView/QGraphicsScene. QGraphicsView.drawBackground always wants a pixmap for a huge rect like 4000x3000 where my screen only has 1920x1200""" # pylint: disable=too-many-statements self.setObjectName("MainWindow") centralWidget = QWidget() self.centralView = FittingView() layout = QGridLayout(centralWidget) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.centralView) self.setCentralWidget(centralWidget) self.centralView.setFocusPolicy(Qt.StrongFocus) self.background = None # just for pylint self.windTileset = Tileset(Internal.Preferences.windTilesetName) self.adjustView() self.actionScoreGame = self.kajonggAction( "scoreGame", "draw-freehand", self.scoringScene, Qt.Key_C) self.actionPlayGame = self.kajonggAction( "play", "arrow-right", self.playingScene, Qt.Key_N) self.actionAbortGame = self.kajonggAction( "abort", "dialog-close", self.abortAction, Qt.Key_W) self.actionAbortGame.setEnabled(False) self.actionQuit = self.kajonggAction( "quit", "application-exit", self.close, Qt.Key_Q) self.actionPlayers = self.kajonggAction( "players", "im-user", self.slotPlayers) self.actionRulesets = self.kajonggAction( "rulesets", "games-kajongg-law", self.slotRulesets) self.actionChat = self._kajonggToggleAction("chat", "call-start", shortcut=Qt.Key_H, actionData=ChatWindow) self.actionChat.setEnabled(False) self.actionAngle = self.kajonggAction( "angle", "object-rotate-left", self.changeAngle, Qt.Key_G) self.actionAngle.setEnabled(False) self.actionFullscreen = KToggleFullScreenAction( self.actionCollection()) self.actionFullscreen.setShortcut(Qt.CTRL + Qt.Key_F) self.actionFullscreen.setShortcutContext(Qt.ApplicationShortcut) self.actionFullscreen.setWindow(self) self.actionCollection().addAction("fullscreen", self.actionFullscreen) self.actionFullscreen.toggled.connect(self.fullScreen) self.actionScoreTable = self._kajonggToggleAction( "scoreTable", "format-list-ordered", Qt.Key_T, actionData=ScoreTable) self.actionScoreTable.setEnabled(False) self.actionExplain = self._kajonggToggleAction( "explain", "applications-education", Qt.Key_E, actionData=ExplainView) self.actionExplain.setEnabled(False) self.actionAutoPlay = self.kajonggAction( "demoMode", "arrow-right-double", None, Qt.Key_D) self.actionAutoPlay.setCheckable(True) self.actionAutoPlay.setEnabled(True) self.actionAutoPlay.toggled.connect(self._toggleDemoMode) self.actionAutoPlay.setChecked(Internal.autoPlay) QMetaObject.connectSlotsByName(self) def playingScene(self): """play a computer game: log into a server and show its tables""" self.scene = PlayingScene(self) HumanClient() def scoringScene(self): """start a scoring scene""" scene = ScoringScene(self) game = scoreGame() if game: self.scene = scene scene.game = game game.throwDices() self.updateGUI() def fullScreen(self, toggle): """toggle between full screen and normal view""" self.actionFullscreen.setFullScreen(self, toggle) def close(self, dummyResult=None): """wrap close() because we call it with a QTimer""" if isAlive(self): return KXmlGuiWindow.close(self) def closeEvent(self, event): KXmlGuiWindow.closeEvent(self, event) if event.isAccepted() and self.exitReady: QTimer.singleShot(5000, self.aboutToQuit) def queryClose(self): """queryClose, queryExit and aboutToQuit are no ideal match for the async Deferred approach. At app start, self.exitConfirmed and exitReady are None. queryClose will show a confirmation prompt if needed, but it will not wait for the answer. queryClose always returns True. Later, when the user confirms exit, self.exitConfirmed will be set. If the user cancels exit, self.exitConfirmed = False, otherwise self.close() is called. This time, no prompt will appear because the game has already been aborted. queryExit will return False if exitConfirmed or exitReady are not True. Otherwise, queryExit will set exitReady to False and asynchronously start shutdown. After the reactor stops running, exitReady is set to True, and self.close() is called. This time it should fall through everywhere, having queryClose() and queryExit() return True. and it will reset exitConfirmed to None. Or in other words: If queryClose or queryExit find something that they should do async like asking the user for confirmation or terminating the client/server connection, they start async operation and append a callback which will call self.close() when the async operation is done. This repeats until queryClose() and queryExit() find nothing more to do async. At that point queryExit says True and we really end the program. """ # pylint: disable=too-many-branches def confirmed(result): """quit if the active game has been aborted""" self.exitConfirmed = bool(result) if Debug.quit: if self.exitConfirmed: logDebug(u'mainWindow.queryClose confirmed') else: logDebug(u'mainWindow.queryClose not confirmed') # start closing again. This time no question will appear, the game # is already aborted if self.exitConfirmed: assert isAlive(self) self.close() else: self.exitConfirmed = None def cancelled(result): """just do nothing""" if Debug.quit: logDebug(u'mainWindow.queryClose.cancelled: {}'.format(result)) self.exitConfirmed = None if self.exitConfirmed is False: # user is currently being asked return False if self.exitConfirmed is None: if self.scene: self.exitConfirmed = False self.abortAction().addCallbacks(confirmed, cancelled) else: self.exitConfirmed = True if Debug.quit: logDebug( u'MainWindow.queryClose not asking, exitConfirmed=True') return True def queryExit(self): """see queryClose""" def quitDebug(*args, **kwargs): """reducing branches in queryExit""" if Debug.quit: logDebug(*args, **kwargs) if self.exitReady: quitDebug(u'MainWindow.queryExit returns True because exitReady is set') return True if self.exitConfirmed: # now we can get serious self.exitReady = False for widget in chain( (x.tableList for x in HumanClient.humanClients), [ self.confDialog, self.rulesetWindow, self.playerWindow]): if isAlive(widget): widget.hide() if self.exitWaitTime is None: self.exitWaitTime = 0 if Internal.reactor and Internal.reactor.running: self.exitWaitTime += 10 if self.exitWaitTime % 1000 == 0: logDebug( u'waiting since %d seconds for reactor to stop' % (self.exitWaitTime // 1000)) try: quitDebug(u'now stopping reactor') Internal.reactor.stop() assert isAlive(self) QTimer.singleShot(10, self.close) except ReactorNotRunning: self.exitReady = True quitDebug( u'MainWindow.queryExit returns True: It got exception ReactorNotRunning') else: self.exitReady = True quitDebug(u'MainWindow.queryExit returns True: Reactor is not running') return bool(self.exitReady) @staticmethod def aboutToQuit(): """now all connections to servers are cleanly closed""" mainWindow = Internal.mainWindow Internal.mainWindow = None if mainWindow: if Debug.quit: logDebug(u'aboutToQuit starting') if mainWindow.exitWaitTime > 1000.0 or Debug.quit: logDebug( u'reactor stopped after %d ms' % (mainWindow.exitWaitTime)) else: if Debug.quit: logDebug(u'aboutToQuit: mainWindow is already None') StateSaver.saveAll() Internal.app.quit() try: # if we are killed while loading, Internal.db may not yet be # defined if Internal.db: Internal.db.close() except NameError: pass checkMemory() logging.shutdown() if Debug.quit: logDebug(u'aboutToQuit ending') def abortAction(self): """abort current game""" if Debug.quit: logDebug(u'mainWindow.abortAction invoked') return self.scene.abort() def retranslateUi(self): """retranslate""" self.actionScoreGame.setText( m18nc('@action:inmenu', "&Score Manual Game")) self.actionScoreGame.setIconText( m18nc('@action:intoolbar', 'Manual Game')) self.actionScoreGame.setHelpText( m18nc('kajongg @info:tooltip', '&Score a manual game.')) self.actionPlayGame.setText(m18nc('@action:intoolbar', "&Play")) self.actionPlayGame.setPriority(QAction.LowPriority) self.actionPlayGame.setHelpText( m18nc('kajongg @info:tooltip', 'Start a new game.')) self.actionAbortGame.setText(m18nc('@action:inmenu', "&Abort Game")) self.actionAbortGame.setPriority(QAction.LowPriority) self.actionAbortGame.setHelpText( m18nc('kajongg @info:tooltip', 'Abort the current game.')) self.actionQuit.setText(m18nc('@action:inmenu', "&Quit Kajongg")) self.actionQuit.setPriority(QAction.LowPriority) self.actionPlayers.setText(m18nc('@action:intoolbar', "&Players")) self.actionPlayers.setHelpText( m18nc('kajongg @info:tooltip', 'define your players.')) self.actionRulesets.setText(m18nc('@action:intoolbar', "&Rulesets")) self.actionRulesets.setHelpText( m18nc('kajongg @info:tooltip', 'customize rulesets.')) self.actionAngle.setText( m18nc('@action:inmenu', "&Change Visual Angle")) self.actionAngle.setIconText(m18nc('@action:intoolbar', "Angle")) self.actionAngle.setHelpText( m18nc('kajongg @info:tooltip', "Change the visual appearance of the tiles.")) self.actionScoreTable.setText( m18nc('kajongg @action:inmenu', "&Score Table")) self.actionScoreTable.setIconText( m18nc('kajongg @action:intoolbar', "&Scores")) self.actionScoreTable.setHelpText(m18nc('kajongg @info:tooltip', "Show or hide the score table for the current game.")) self.actionExplain.setText(m18nc('@action:inmenu', "&Explain Scores")) self.actionExplain.setIconText(m18nc('@action:intoolbar', "&Explain")) self.actionExplain.setHelpText(m18nc('kajongg @info:tooltip', 'Explain the scoring for all players in the current game.')) self.actionAutoPlay.setText(m18nc('@action:inmenu', "&Demo Mode")) self.actionAutoPlay.setPriority(QAction.LowPriority) self.actionAutoPlay.setHelpText(m18nc('kajongg @info:tooltip', 'Let the computer take over for you. Start a new local game if needed.')) self.actionChat.setText(m18n("C&hat")) self.actionChat.setHelpText( m18nc('kajongg @info:tooltip', 'Chat with the other players.')) def changeEvent(self, event): """when the applicationwide language changes, recreate GUI""" if event.type() == QEvent.LanguageChange: self.setupGUI() self.retranslateUi() def slotPlayers(self): """show the player list""" if not self.playerWindow: self.playerWindow = PlayerList(self) self.playerWindow.show() def slotRulesets(self): """show the player list""" if not self.rulesetWindow: self.rulesetWindow = RulesetSelector() self.rulesetWindow.show() def adjustView(self): """adjust the view such that exactly the wanted things are displayed without having to scroll""" if not Internal.scaleScene or not isAlive(self): return view, scene = self.centralView, self.scene if scene: scene.adjustView() oldRect = view.sceneRect() view.setSceneRect(scene.itemsBoundingRect()) newRect = view.sceneRect() if oldRect != newRect: view.fitInView(scene.itemsBoundingRect(), Qt.KeepAspectRatio) @afterQueuedAnimations def backgroundChanged(self, dummyDeferredResult, dummyOldName, newName): """if the wanted background changed, apply the change now""" centralWidget = self.centralWidget() if centralWidget: self.background = Background(newName) self.background.setPalette(centralWidget) centralWidget.setAutoFillBackground(True) @afterQueuedAnimations def tilesetNameChanged( self, dummyDeferredResult, dummyOldValue=None, dummyNewValue=None, *dummyArgs): """if the wanted tileset changed, apply the change now""" if self.centralView: with MoveImmediate(): if self.scene: self.scene.applySettings() self.adjustView() @afterQueuedAnimations def showSettings(self, dummyDeferredResult, dummyChecked=None): """show preferences dialog. If it already is visible, do nothing""" # This is called by the triggered() signal. So why does KDE # not return the bool checked? if ConfigDialog.showDialog("settings"): return # if an animation is running, Qt segfaults somewhere deep # in the SVG renderer rendering the wind tiles for the tile # preview self.confDialog = ConfigDialog(self, "settings") self.confDialog.show() def _toggleWidget(self, checked): """user has toggled widget visibility with an action""" assert self.scene action = self.sender() actionData = variantValue(action.data()) if checked: if isinstance(actionData, type): clsName = actionData.__name__ actionData = actionData(scene=self.scene) action.setData(toQVariant(actionData)) setattr( self.scene, clsName[0].lower() + clsName[1:], actionData) actionData.show() actionData.raise_() else: assert actionData actionData.hide() def _toggleDemoMode(self, checked): """switch on / off for autoPlay""" if self.scene: self.scene.toggleDemoMode(checked) else: Internal.autoPlay = checked if checked and Internal.db: self.playingScene() def updateGUI(self): """update some actions, all auxiliary windows and the statusbar""" if not isAlive(self): return self.setCaption('') for action in [self.actionScoreGame, self.actionPlayGame]: action.setEnabled(not bool(self.scene)) self.actionAbortGame.setEnabled(bool(self.scene)) scene = self.scene if isAlive(scene): scene.updateSceneGUI() @afterQueuedAnimations def changeAngle(self, deferredResult, dummyButtons=None, dummyModifiers=None): # pylint: disable=unused-argument """change the lightSource""" if self.scene: with MoveImmediate(): self.scene.changeAngle()
class PlayField(KXmlGuiWindow): """the main window""" # pylint: disable=R0902 # pylint we need more than 10 instance attributes def __init__(self): # see http://lists.kde.org/?l=kde-games-devel&m=120071267328984&w=2 Internal.field = self self.game = None self.__startingGame = False self.ignoreResizing = 1 super(PlayField, self).__init__() self.background = None self.showShadows = None self._clientDialog = None self.playerWindow = None self.rulesetWindow = None self.scoreTable = None self.explainView = None self.scoringDialog = None self.confDialog = None self.setupUi() KStandardAction.preferences(self.showSettings, self.actionCollection()) self.applySettings() self.setupGUI() self.retranslateUi() for action in self.toolBar().actions(): if 'onfigure' in action.text(): action.setPriority(QAction.LowPriority) if Options.host: self.playGame() @property def clientDialog(self): """wrapper: hide dialog when it is set to None""" return self._clientDialog @clientDialog.setter def clientDialog(self, value): """wrapper: hide dialog when it is set to None""" if isAlive(self._clientDialog) and not value: self._clientDialog.timer.stop() self._clientDialog.hide() self._clientDialog = value def sizeHint(self): """give the main window a sensible default size""" result = KXmlGuiWindow.sizeHint(self) result.setWidth(result.height() * 3 // 2) # we want space to the right for the buttons # the default is too small. Use at least 2/3 of screen height and 1/2 of screen width: available = KApplication.kApplication().desktop().availableGeometry() height = max(result.height(), available.height() * 2 // 3) width = max(result.width(), available.width() // 2) result.setHeight(height) result.setWidth(width) return result def resizeEvent(self, event): """Use this hook to determine if we want to ignore one more resize event happening for maximized / almost maximized windows. this misses a few cases where the window is almost maximized because at this point the window has no border yet: event.size, self.geometry() and self.frameGeometry are all the same. So we cannot check if the bordered window would fit into availableGeometry. """ available = KApplication.kApplication().desktop().availableGeometry() if self.ignoreResizing == 1: # at startup if available.width() <= event.size().width() \ or available.height() <= event.size().height(): self.ignoreResizing += 1 KXmlGuiWindow.resizeEvent(self, event) if self.clientDialog: self.clientDialog.placeInField() def showEvent(self, event): """force a resize which calculates the correct background image size""" self.centralView.resizeEvent(True) KXmlGuiWindow.showEvent(self, event) def handSelectorChanged(self, handBoard): """update all relevant dialogs""" if self.scoringDialog: self.scoringDialog.slotInputChanged() if self.game and not self.game.finished(): self.game.wall.decoratePlayer(handBoard.player) # pylint: disable=E1101 # first decorate walls - that will compute player.handBoard for explainView if self.explainView: self.explainView.refresh(self.game) def __kajonggAction(self, name, icon, slot=None, shortcut=None, actionData=None): """simplify defining actions""" res = KAction(self) res.setIcon(KIcon(icon)) if slot: res.triggered.connect(slot) self.actionCollection().addAction(name, res) if shortcut: res.setShortcut(Qt.CTRL + shortcut) res.setShortcutContext(Qt.ApplicationShortcut) if PYQT_VERSION_STR != '4.5.2' or actionData is not None: res.setData(QVariant(actionData)) return res def __kajonggToggleAction(self, name, icon, shortcut=None, actionData=None): """a checkable action""" res = self.__kajonggAction(name, icon, shortcut=shortcut, actionData=actionData) res.setCheckable(True) res.toggled.connect(self.__toggleWidget) return res def setupUi(self): """create all other widgets we could make the scene view the central widget but I did not figure out how to correctly draw the background with QGraphicsView/QGraphicsScene. QGraphicsView.drawBackground always wants a pixmap for a huge rect like 4000x3000 where my screen only has 1920x1200""" # pylint: disable=R0915 self.setObjectName("MainWindow") centralWidget = QWidget() scene = MJScene() self.centralScene = scene self.centralView = FittingView() layout = QGridLayout(centralWidget) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.centralView) self.tileset = None # just for pylint self.background = None # just for pylint self.tilesetName = Preferences.tilesetName self.windTileset = Tileset(Preferences.windTilesetName) self.discardBoard = DiscardBoard() self.discardBoard.setVisible(False) scene.addItem(self.discardBoard) self.selectorBoard = SelectorBoard() self.selectorBoard.setVisible(False) scene.addItem(self.selectorBoard) self.setCentralWidget(centralWidget) self.centralView.setScene(scene) self.centralView.setFocusPolicy(Qt.StrongFocus) self.adjustView() self.actionScoreGame = self.__kajonggAction("scoreGame", "draw-freehand", self.scoreGame, Qt.Key_C) self.actionPlayGame = self.__kajonggAction("play", "arrow-right", self.playGame, Qt.Key_N) self.actionAbortGame = self.__kajonggAction("abort", "dialog-close", self.abortAction, Qt.Key_W) self.actionAbortGame.setEnabled(False) self.actionQuit = self.__kajonggAction("quit", "application-exit", self.close, Qt.Key_Q) self.actionPlayers = self.__kajonggAction("players", "im-user", self.slotPlayers) self.actionRulesets = self.__kajonggAction("rulesets", "games-kajongg-law", self.slotRulesets) self.actionChat = self.__kajonggToggleAction("chat", "call-start", shortcut=Qt.Key_H, actionData=ChatWindow) game = self.game self.actionChat.setEnabled( bool(game) and bool(game.client) and not game.client.hasLocalServer()) self.actionChat.setChecked( bool(game) and bool(game.client) and bool(game.client.table.chatWindow)) self.actionScoring = self.__kajonggToggleAction( "scoring", "draw-freehand", shortcut=Qt.Key_S, actionData=ScoringDialog) self.actionScoring.setEnabled(False) self.actionAngle = self.__kajonggAction("angle", "object-rotate-left", self.changeAngle, Qt.Key_G) self.actionAngle.setEnabled(False) self.actionFullscreen = KToggleFullScreenAction( self.actionCollection()) self.actionFullscreen.setShortcut(Qt.CTRL + Qt.Key_F) self.actionFullscreen.setShortcutContext(Qt.ApplicationShortcut) self.actionFullscreen.setWindow(self) self.actionCollection().addAction("fullscreen", self.actionFullscreen) self.actionFullscreen.toggled.connect(self.fullScreen) self.actionScoreTable = self.__kajonggToggleAction( "scoreTable", "format-list-ordered", Qt.Key_T, actionData=ScoreTable) self.actionExplain = self.__kajonggToggleAction( "explain", "applications-education", Qt.Key_E, actionData=ExplainView) self.actionAutoPlay = self.__kajonggAction("demoMode", "arrow-right-double", None, Qt.Key_D) self.actionAutoPlay.setCheckable(True) self.actionAutoPlay.toggled.connect(self.__toggleDemoMode) self.actionAutoPlay.setChecked(Internal.autoPlay) QMetaObject.connectSlotsByName(self) def showWall(self): """shows the wall according to the game rules (lenght may vary)""" UIWall(self.game) # sets self.game.wall if self.discardBoard: # scale it such that it uses the place within the wall optimally. # we need to redo this because the wall length can vary between games. self.discardBoard.maximize() def genPlayers(self): """generate four default VisiblePlayers""" return Players([VisiblePlayer(self.game, idx) for idx in range(4)]) def fullScreen(self, toggle): """toggle between full screen and normal view""" self.actionFullscreen.setFullScreen(self, toggle) def abortAction(self): """abort current game""" def doNotQuit(dummy): """ignore failure to abort""" self.abort().addErrback(doNotQuit) def abort(self): """abort current game""" def gotAnswer(result, autoPlaying): """user answered""" if result: return self.abortGame() else: self.actionAutoPlay.setChecked(autoPlaying) return fail(Exception('no abort')) def gotError(result): """abortGame failed""" logDebug('abortGame error:%s/%s ' % (str(result), result.getErrorMessage())) if not self.game: self.startingGame = False return succeed(None) autoPlaying = self.actionAutoPlay.isChecked() self.actionAutoPlay.setChecked(False) if self.game.finished(): return self.abortGame() else: return QuestionYesNo( m18n("Do you really want to abort this game?"), always=True).addCallback(gotAnswer, autoPlaying).addErrback(gotError) def abortGame(self): """if a game is active, abort it""" if self.game is None: # meanwhile somebody else might have aborted return succeed(None) game = self.game self.game = None return game.close() def closeEvent(self, event): """somebody wants us to close, maybe ALT-F4 or so""" event.ignore() def doNotQuit(dummy): """ignore failure to abort""" self.abort().addCallback( HumanClient.shutdownHumanClients).addCallbacks( Client.quitProgram, doNotQuit) def __moveTile(self, tile, wind, lowerHalf): """the user pressed a wind letter or X for center, wanting to move a tile there""" # this tells the receiving board that this is keyboard, not mouse navigation> # needed for useful placement of the popup menu assert self.game.isScoringGame() assert isinstance(tile, Tile), (tile, str(tile)) currentBoard = tile.board dragTile, dragMeld = currentBoard.dragObject(tile) if wind == 'X': receiver = self.selectorBoard else: receiver = self.game.players[wind].handBoard if receiver != currentBoard or bool(lowerHalf) != bool(tile.yoffset): movingLastMeld = tile.element in self.computeLastMeld().pairs if movingLastMeld: self.scoringDialog.clearLastTileCombo() receiver.dropHere(dragTile, dragMeld, lowerHalf) if movingLastMeld and receiver == currentBoard: self.scoringDialog.fillLastTileCombo() def __navigateScoringGame(self, event): """keyboard navigation in a scoring game""" mod = event.modifiers() key = event.key() wind = chr(key % 128) moveCommands = m18nc('kajongg:keyboard commands for moving tiles to the players ' \ 'with wind ESWN or to the central tile selector (X)', 'ESWNX') tile = self.centralScene.focusItem().tile if wind in moveCommands: # translate i18n wind key to ESWN: wind = 'ESWNX'[moveCommands.index(wind)] self.__moveTile(tile, wind, mod & Qt.ShiftModifier) return True if key == Qt.Key_Tab and self.game: tabItems = [self.selectorBoard] tabItems.extend( list(p.handBoard for p in self.game.players if p.handBoard.tiles)) tabItems.append(tabItems[0]) currentBoard = tile.board if isinstance(tile, Tile) else None currIdx = 0 while tabItems[currIdx] != currentBoard and currIdx < len( tabItems) - 2: currIdx += 1 tabItems[currIdx + 1].hasFocus = True return True def keyPressEvent(self, event): """navigate in the selectorboard""" mod = event.modifiers() if mod in (Qt.NoModifier, Qt.ShiftModifier): if self.game and self.game.isScoringGame(): if self.__navigateScoringGame(event): return if self.clientDialog: self.clientDialog.keyPressEvent(event) KXmlGuiWindow.keyPressEvent(self, event) def retranslateUi(self): """retranslate""" self.actionScoreGame.setText( m18nc('@action:inmenu', "&Score Manual Game")) self.actionScoreGame.setIconText( m18nc('@action:intoolbar', 'Manual Game')) self.actionScoreGame.setHelpText( m18nc('kajongg @info:tooltip', '&Score a manual game.')) self.actionPlayGame.setText(m18nc('@action:intoolbar', "&Play")) self.actionPlayGame.setPriority(QAction.LowPriority) self.actionPlayGame.setHelpText( m18nc('kajongg @info:tooltip', 'Start a new game.')) self.actionAbortGame.setText(m18nc('@action:inmenu', "&Abort Game")) self.actionAbortGame.setPriority(QAction.LowPriority) self.actionAbortGame.setHelpText( m18nc('kajongg @info:tooltip', 'Abort the current game.')) self.actionQuit.setText(m18nc('@action:inmenu', "&Quit Kajongg")) self.actionQuit.setPriority(QAction.LowPriority) self.actionPlayers.setText(m18nc('@action:intoolbar', "&Players")) self.actionPlayers.setHelpText( m18nc('kajongg @info:tooltip', 'define your players.')) self.actionRulesets.setText(m18nc('@action:intoolbar', "&Rulesets")) self.actionRulesets.setHelpText( m18nc('kajongg @info:tooltip', 'customize rulesets.')) self.actionAngle.setText( m18nc('@action:inmenu', "&Change Visual Angle")) self.actionAngle.setIconText(m18nc('@action:intoolbar', "Angle")) self.actionAngle.setHelpText( m18nc('kajongg @info:tooltip', "Change the visual appearance of the tiles.")) self.actionScoring.setText( m18nc('@action:inmenu', "&Show Scoring Editor")) self.actionScoring.setIconText(m18nc('@action:intoolbar', "&Scoring")) self.actionScoring.setHelpText( m18nc('kajongg @info:tooltip', "Show or hide the scoring editor for a manual game.")) self.actionScoreTable.setText( m18nc('kajongg @action:inmenu', "&Score Table")) self.actionScoreTable.setIconText( m18nc('kajongg @action:intoolbar', "&Scores")) self.actionScoreTable.setHelpText( m18nc('kajongg @info:tooltip', "Show or hide the score table for the current game.")) self.actionExplain.setText(m18nc('@action:inmenu', "&Explain Scores")) self.actionExplain.setIconText(m18nc('@action:intoolbar', "&Explain")) self.actionExplain.setHelpText( m18nc('kajongg @info:tooltip', 'Explain the scoring for all players in the current game.')) self.actionAutoPlay.setText(m18nc('@action:inmenu', "&Demo Mode")) self.actionAutoPlay.setPriority(QAction.LowPriority) self.actionAutoPlay.setHelpText( m18nc( 'kajongg @info:tooltip', 'Let the computer take over for you. Start a new local game if needed.' )) self.actionChat.setText(m18n("C&hat")) self.actionChat.setHelpText( m18nc('kajongg @info:tooltip', 'Chat with the other players.')) def changeEvent(self, event): """when the applicationwide language changes, recreate GUI""" if event.type() == QEvent.LanguageChange: self.setupGUI() self.retranslateUi() def slotPlayers(self): """show the player list""" if not self.playerWindow: self.playerWindow = PlayerList(self) self.playerWindow.show() def slotRulesets(self): """show the player list""" if not self.rulesetWindow: self.rulesetWindow = RulesetSelector() self.rulesetWindow.show() def selectScoringGame(self): """show all games, select an existing game or create a new game""" Players.load() if len(Players.humanNames) < 4: logWarning( m18n( 'Please define four players in <interface>Settings|Players</interface>' )) return False gameSelector = Games(self) if gameSelector.exec_(): selected = gameSelector.selectedGame if selected is not None: ScoringGame.loadFromDB(selected) else: self.newGame() if self.game: self.game.throwDices() gameSelector.close() self.updateGUI() return bool(self.game) def scoreGame(self): """score a local game""" if self.selectScoringGame(): self.actionScoring.setChecked(True) def playGame(self): """play a remote game: log into a server and show its tables""" self.startingGame = True HumanClient() def adjustView(self): """adjust the view such that exactly the wanted things are displayed without having to scroll""" if not Internal.scaleScene: return if self.game: with Animated(False): self.game.wall.decorate() if self.discardBoard: self.discardBoard.maximize() if self.selectorBoard: self.selectorBoard.maximize() for tile in self.game.wall.tiles: if tile.board: tile.board.placeTile(tile) view, scene = self.centralView, self.centralScene oldRect = view.sceneRect() view.setSceneRect(scene.itemsBoundingRect()) newRect = view.sceneRect() if oldRect != newRect: view.fitInView(scene.itemsBoundingRect(), Qt.KeepAspectRatio) @property def startingGame(self): """are we trying to start a game?""" return self.__startingGame @startingGame.setter def startingGame(self, value): """are we trying to start a game?""" if value != self.__startingGame: self.__startingGame = value self.updateGUI() @property def tilesetName(self): """the name of the current tileset""" return self.tileset.desktopFileName @tilesetName.setter def tilesetName(self, name): """the name of the current tileset""" self.tileset = Tileset(name) @property def backgroundName(self): """setting this also actually changes the background""" return self.background.desktopFileName if self.background else '' @backgroundName.setter def backgroundName(self, name): """setter for backgroundName""" self.background = Background(name) self.background.setPalette(self.centralWidget()) self.centralWidget().setAutoFillBackground(True) def applySettings(self): """apply preferences""" # pylint: disable=R0912 # too many branches self.actionAngle.setEnabled( bool(self.game) and Preferences.showShadows) animate() # drain the queue afterCurrentAnimationDo(self.__applySettings2) def __applySettings2(self, dummyResults): """now no animation is running""" with Animated(False): if self.tilesetName != Preferences.tilesetName: self.tilesetName = Preferences.tilesetName if self.game: self.game.wall.tileset = self.tileset for item in self.centralScene.nonTiles(): try: item.tileset = self.tileset except AttributeError: continue # change players last because we need the wall already to be repositioned self.adjustView() # the new tiles might be larger if self.game: for player in self.game.players: if player.handBoard: player.handBoard.rearrangeMelds = Preferences.rearrangeMelds if self.backgroundName != Preferences.backgroundName: self.backgroundName = Preferences.backgroundName if self.showShadows is None or self.showShadows != Preferences.showShadows: self.showShadows = Preferences.showShadows if self.game: wall = self.game.wall wall.showShadows = self.showShadows self.selectorBoard.showShadows = self.showShadows if self.discardBoard: self.discardBoard.showShadows = self.showShadows for tile in self.centralScene.graphicsTileItems(): tile.setClippingFlags() self.adjustView() Sound.enabled = Preferences.useSounds self.centralScene.placeFocusRect() def showSettings(self): """show preferences dialog. If it already is visible, do nothing""" if KConfigDialog.showDialog("settings"): return # if an animation is running, Qt segfaults somewhere deep # in the SVG renderer rendering the wind tiles for the tile # preview afterCurrentAnimationDo(self.__showSettings2) def __showSettings2(self, dummyResult): """now that no animation is running, show settings dialog""" self.confDialog = ConfigDialog(self, "settings") self.confDialog.settingsChanged.connect(self.applySettings) self.confDialog.show() def newGame(self): """asks user for players and ruleset for a new game and returns that new game""" Players.load() # we want to make sure we have the current definitions selectDialog = SelectPlayers(self.game) if not selectDialog.exec_(): return return ScoringGame(selectDialog.names, selectDialog.cbRuleset.current) def __toggleWidget(self, checked): """user has toggled widget visibility with an action""" action = self.sender() actionData = action.data().toPyObject() if checked: if isinstance(actionData, type): actionData = actionData(game=self.game) action.setData(QVariant(actionData)) if isinstance(actionData, ScoringDialog): self.scoringDialog = actionData actionData.btnSave.clicked.connect(self.nextScoringHand) actionData.scoringClosed.connect(self.__scoringClosed) elif isinstance(actionData, ExplainView): self.explainView = actionData elif isinstance(actionData, ScoreTable): self.scoreTable = actionData actionData.show() actionData.raise_() else: assert actionData actionData.hide() def __toggleDemoMode(self, checked): """switch on / off for autoPlay""" if self.game: self.centralScene.placeFocusRect() # show/hide it self.game.autoPlay = checked if checked and self.clientDialog: self.clientDialog.proposeAction( ) # an illegal action might have focus self.clientDialog.selectButton( ) # select default, abort timeout else: Internal.autoPlay = checked if checked: # TODO: use the last used ruleset. Right now it always takes the first of the list. self.playGame() def __scoringClosed(self): """the scoring window has been closed with ALT-F4 or similar""" self.actionScoring.setChecked(False) def nextScoringHand(self): """save hand to database, update score table and balance in status line, prepare next hand""" if self.game.winner: for player in self.game.players: player.usedDangerousFrom = None for ruleBox in player.manualRuleBoxes: rule = ruleBox.rule if rule.name == 'Dangerous Game' and ruleBox.isChecked(): self.game.winner.usedDangerousFrom = player self.game.saveHand() self.game.maybeRotateWinds() self.game.prepareHand() self.game.initHand() def prepareHand(self): """redecorate wall""" self.updateGUI() if self.game: self.game.wall.decorate() if self.scoringDialog: self.scoringDialog.clearLastTileCombo() def updateGUI(self): """update some actions, all auxiliary windows and the statusbar""" if not isAlive(self): return title = '' connections = list(x.connection for x in HumanClient.humanClients if x.connection) game = self.game if not game: title = ', '.join('{name}/{url}'.format(name=x.username, url=x.url) for x in connections) if title: self.setWindowTitle('%s - Kajongg' % title) for action in [self.actionScoreGame, self.actionPlayGame]: action.setEnabled(not bool(game)) self.actionAbortGame.setEnabled(bool(game)) self.actionAngle.setEnabled(bool(game) and self.showShadows) scoring = bool(game and game.isScoringGame()) self.selectorBoard.setVisible(scoring) self.selectorBoard.setEnabled(scoring) self.discardBoard.setVisible(bool(game) and not scoring) self.actionScoring.setEnabled(scoring and not game.finished()) self.actionAutoPlay.setEnabled(not self.startingGame and not scoring) self.actionChat.setEnabled( bool(game) and bool(game.client) and not game.client.hasLocalServer() and not self.startingGame) # chatting on tables before game started works with chat button per table self.actionChat.setChecked(self.actionChat.isEnabled() and bool(game.client.table.chatWindow)) if self.actionScoring.isChecked(): self.actionScoring.setChecked(scoring and not game.finished()) for view in [self.scoringDialog, self.explainView, self.scoreTable]: if view: view.refresh(game) self.__showBalance() def changeAngle(self): """change the lightSource""" if self.game: afterCurrentAnimationDo(self.__changeAngle2) def __changeAngle2(self, dummyResult): """now that no animation is running, really change""" if self.game: # might be finished meanwhile with Animated(False): wall = self.game.wall oldIdx = LIGHTSOURCES.index(wall.lightSource) # pylint: disable=E1101 newLightSource = LIGHTSOURCES[(oldIdx + 1) % 4] wall.lightSource = newLightSource self.selectorBoard.lightSource = newLightSource self.discardBoard.lightSource = newLightSource self.adjustView() scoringDialog = self.actionScoring.data().toPyObject() if isinstance(scoringDialog, ScoringDialog): scoringDialog.computeScores() self.centralScene.placeFocusRect() def __showBalance(self): """show the player balances in the status bar""" sBar = self.statusBar() if self.game: for idx, player in enumerate(self.game.players): sbMessage = player.localName + ': ' + str(player.balance) if sBar.hasItem(idx): sBar.changeItem(sbMessage, idx) else: sBar.insertItem(sbMessage, idx, 1) sBar.setItemAlignment(idx, Qt.AlignLeft) else: for idx in range(5): if sBar.hasItem(idx): sBar.removeItem(idx) def computeLastTile(self): """compile hand info into a string as needed by the scoring engine""" if self.scoringDialog: return self.scoringDialog.computeLastTile() def computeLastMeld(self): """compile hand info into a string as needed by the scoring engine""" if self.scoringDialog: cbLastMeld = self.scoringDialog.cbLastMeld idx = cbLastMeld.currentIndex() if idx >= 0: return Meld(str(cbLastMeld.itemData(idx).toString())) return Meld() @staticmethod def askSwap(swappers): """use this as a proxy such that module game does not have to import playfield. Game should also run on a server without KDE being installed""" return SwapDialog(swappers).exec_()