Ejemplo n.º 1
0
 def slotRulesets(self):
     """show the player list"""
     if not self.rulesetWindow:
         self.rulesetWindow = RulesetSelector()
     self.rulesetWindow.show()
Ejemplo n.º 2
0
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
        self.__installReactor()
        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()

    @staticmethod
    def __installReactor():
        """install the twisted reactor"""
        if Internal.reactor is None:
            import qtreactor
            qtreactor.install()
            from twisted.internet import reactor
            reactor.runReturn(installSignalHandlers=False)
            Internal.reactor = reactor
            if Debug.quit:
                logDebug('Installed qtreactor')

    @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(ExplainView)
            self.actionScoreTable.setData(ScoreTable)
        self._scene = value
        self.centralView.setScene(value)
        self.adjustMainView()
        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 = Internal.app.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 = QAction(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 actionData is not None:
            res.setData(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)
        if actionData is not None:
            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.adjustMainView()
        self.actionScoreGame = self.kajonggAction("scoreGame", "draw-freehand",
                                                  self.scoringScene, 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)
        self.actionChat.setEnabled(False)
        self.actionAngle = self.kajonggAction("angle", "object-rotate-left",
                                              self.changeAngle, Qt.Key_G)
        self.actionAngle.setEnabled(False)
        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.actionFullscreen = self._kajonggToggleAction("fullscreen",
                                                          "view-fullscreen",
                                                          shortcut=Qt.Key_F +
                                                          Qt.ShiftModifier)
        self.actionFullscreen.toggled.connect(self.fullScreen)
        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 playGame(self):
        """manual wish for a new game"""
        if not Internal.autoPlay:
            # only if no demo game is running
            self.playingScene()

    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"""
        if toggle:
            self.setWindowState(self.windowState() | Qt.WindowFullScreen)
        else:
            self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)

    def close(self, dummyResult=None):
        """wrap close() because we call it with a QTimer"""
        if isAlive(self):
            ParallelAnimationGroup.cancelAll()
            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('mainWindow.queryClose confirmed')
                else:
                    logDebug('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('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(
                        '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(
                '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('waiting since %d seconds for reactor to stop' %
                             (self.exitWaitTime // 1000))
                try:
                    quitDebug('now stopping reactor')
                    Internal.reactor.stop()
                    assert isAlive(self)
                    QTimer.singleShot(10, self.close)
                except ReactorNotRunning:
                    self.exitReady = True
                    quitDebug(
                        'MainWindow.queryExit returns True: It got exception ReactorNotRunning'
                    )
            else:
                self.exitReady = True
                quitDebug(
                    '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('aboutToQuit starting')
            if mainWindow.exitWaitTime > 1000.0 or Debug.quit:
                logDebug('reactor stopped after %d ms' %
                         (mainWindow.exitWaitTime))
        else:
            if Debug.quit:
                logDebug('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('aboutToQuit ending')

    def abortAction(self):
        """abort current game"""
        if Debug.quit:
            logDebug('mainWindow.abortAction invoked')
        return self.scene.abort()

    def retranslateUi(self):
        """retranslate"""
        self.actionScoreGame.setText(
            i18nc('@action:inmenu', "&Score Manual Game"))
        self.actionScoreGame.setIconText(
            i18nc('@action:intoolbar', 'Manual Game'))
        self.actionScoreGame.setWhatsThis(
            i18nc('kajongg @info:tooltip', '&Score a manual game.'))

        self.actionPlayGame.setText(i18nc('@action:intoolbar', "&Play"))
        self.actionPlayGame.setPriority(QAction.LowPriority)
        self.actionPlayGame.setWhatsThis(
            i18nc('kajongg @info:tooltip', 'Start a new game.'))

        self.actionAbortGame.setText(i18nc('@action:inmenu', "&Abort Game"))
        self.actionAbortGame.setPriority(QAction.LowPriority)
        self.actionAbortGame.setWhatsThis(
            i18nc('kajongg @info:tooltip', 'Abort the current game.'))

        self.actionQuit.setText(i18nc('@action:inmenu', "&Quit Kajongg"))
        self.actionQuit.setPriority(QAction.LowPriority)

        self.actionPlayers.setText(i18nc('@action:intoolbar', "&Players"))
        self.actionPlayers.setWhatsThis(
            i18nc('kajongg @info:tooltip', 'define your players.'))

        self.actionRulesets.setText(i18nc('@action:intoolbar', "&Rulesets"))
        self.actionRulesets.setWhatsThis(
            i18nc('kajongg @info:tooltip', 'customize rulesets.'))

        self.actionAngle.setText(
            i18nc('@action:inmenu', "&Change Visual Angle"))
        self.actionAngle.setIconText(i18nc('@action:intoolbar', "Angle"))
        self.actionAngle.setWhatsThis(
            i18nc('kajongg @info:tooltip',
                  "Change the visual appearance of the tiles."))

        self.actionFullscreen.setText(
            i18nc('@action:inmenu', "F&ull Screen Mode"))

        self.actionScoreTable.setText(
            i18nc('kajongg @action:inmenu', "&Score Table"))
        self.actionScoreTable.setIconText(
            i18nc('kajongg @action:intoolbar', "&Scores"))
        self.actionScoreTable.setWhatsThis(
            i18nc('kajongg @info:tooltip',
                  "Show or hide the score table for the current game."))

        self.actionExplain.setText(i18nc('@action:inmenu', "&Explain Scores"))
        self.actionExplain.setIconText(i18nc('@action:intoolbar', "&Explain"))
        self.actionExplain.setWhatsThis(
            i18nc('kajongg @info:tooltip',
                  'Explain the scoring for all players in the current game.'))

        self.actionAutoPlay.setText(i18nc('@action:inmenu', "&Demo Mode"))
        self.actionAutoPlay.setPriority(QAction.LowPriority)
        self.actionAutoPlay.setWhatsThis(
            i18nc(
                'kajongg @info:tooltip',
                'Let the computer take over for you. Start a new local game if needed.'
            ))

        self.actionChat.setText(i18n("C&hat"))
        self.actionChat.setWhatsThis(
            i18nc('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 adjustMainView(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.adjustSceneView()
            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 AnimationSpeed():
                if self.scene:
                    self.scene.applySettings()
            self.adjustMainView()

    @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 = action.data()
        if checked:
            if isinstance(actionData, type):
                clsName = actionData.__name__
                actionData = actionData(scene=self.scene)
                action.setData(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 AnimationSpeed():
                self.scene.changeAngle()
Ejemplo n.º 3
0
 def slotRulesets(self):
     """show the player list"""
     if not self.rulesetWindow:
         self.rulesetWindow = RulesetSelector()
     self.rulesetWindow.show()
Ejemplo n.º 4
0
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_()
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
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_()