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

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 # pylint: disable=wrong-import-order, wrong-import-position
0011 
0012 import sys
0013 import codecs
0014 from itertools import chain
0015 
0016 import cgitb
0017 import tempfile
0018 import webbrowser
0019 import logging
0020 
0021 from log import logError, logDebug
0022 from common import Options, Internal, isAlive, Debug, handleSignals
0023 
0024 
0025 class MyHook(cgitb.Hook):
0026 
0027     """override the standard cgitb hook: invoke the browser"""
0028 
0029     def __init__(self):
0030         self.tmpFileName = tempfile.mkstemp(
0031             suffix='.html',
0032             prefix='bt_',
0033             text=True)[1]
0034         # cgitb can only handle ascii, work around that.
0035         # See https://bugs.python.org/issue22746
0036         cgitb.Hook.__init__(self, file=codecs.open(self.tmpFileName, 'w',
0037                                                    encoding='latin-1', errors='xmlcharrefreplace'))
0038 
0039     def handle(self, info=None):
0040         """handling the exception: show backtrace in browser"""
0041         if getattr(cgitb, 'Hook', None):
0042             # if we cannot import twisted (syntax error), Hook is not yet known
0043             cgitb.Hook.handle(self, info)
0044             webbrowser.open(self.tmpFileName)
0045 
0046 # sys.excepthook = MyHook()
0047 
0048 
0049 NOTFOUND = []
0050 
0051 try:
0052     from qt import Qt, QEvent, QMetaObject, QTimer, QKeySequence
0053     from qt import QWidget, QGridLayout, QAction
0054 except ImportError as importError:
0055     NOTFOUND.append('Please install PyQt5: %s' % importError)
0056 
0057 try:
0058     from twisted.spread import pb # pylint: disable=unused-import
0059     from twisted.internet.error import ReactorNotRunning
0060 except ImportError as importError:
0061     NOTFOUND.append('Package python3-twisted is missing or too old (I need 16.6.0): %s' % importError)
0062 
0063 
0064 try:
0065     from mi18n import i18n, i18nc
0066     from kde import KXmlGuiWindow, KStandardAction
0067 
0068     from board import FittingView
0069     from playerlist import PlayerList
0070     from tileset import Tileset
0071     from background import Background
0072     from scoring import scoreGame
0073     from scoringdialog import ScoreTable, ExplainView
0074     from humanclient import HumanClient
0075     from rulesetselector import RulesetSelector
0076     from animation import afterQueuedAnimations, AnimationSpeed
0077     from chat import ChatWindow
0078     from scene import PlayingScene, ScoringScene
0079     from configdialog import ConfigDialog
0080     from statesaver import StateSaver
0081     from util import checkMemory
0082     from kdestub import Action, KApplication
0083 
0084 except ImportError as importError:
0085     NOTFOUND.append('Kajongg is not correctly installed: modules: %s' %
0086                     importError)
0087 
0088 if NOTFOUND:
0089     logError("\n".join(" * %s" % s for s in NOTFOUND), showStack=False)
0090     sys.exit(3)
0091 
0092 
0093 def cleanExit(*unusedArgs): # pylint: disable=unused-argument
0094     """close sqlite3 files before quitting"""
0095     if isAlive(Internal.mainWindow):
0096         if Debug.quit:
0097             logDebug('cleanExit calling mainWindow.close')
0098         Internal.mainWindow.close()
0099     else:
0100         # this must be very early or very late
0101         if Debug.quit:
0102             logDebug('cleanExit calling sys.exit(0)')
0103         # sys.exit(0)
0104         MainWindow.aboutToQuit()
0105 
0106 handleSignals(cleanExit)
0107 
0108 
0109 class MainWindow(KXmlGuiWindow):
0110 
0111     """the main window"""
0112     # pylint: disable=too-many-instance-attributes
0113 
0114     def __init__(self):
0115         # see https://marc.info/?l=kde-games-devel&m=120071267328984&w=2
0116         super().__init__()
0117         Internal.app.aboutToQuit.connect(self.aboutToQuit)
0118         self.exitConfirmed = None
0119         self.exitReady = None
0120         self.exitWaitTime = None
0121         Internal.mainWindow = self
0122         self._scene = None
0123         self.centralView = None
0124         self.background = None
0125         self.playerWindow = None
0126         self.rulesetWindow = None
0127         self.confDialog = None
0128         self.__installReactor()
0129         if Options.gui:
0130             KStandardAction.preferences(
0131                 self.showSettings,
0132                 self.actionCollection())
0133             self.setupUi()
0134             self.setupGUI()
0135             Internal.Preferences.addWatch(
0136                 'tilesetName',
0137                 self.tilesetNameChanged)
0138             Internal.Preferences.addWatch(
0139                 'backgroundName',
0140                 self.backgroundChanged)
0141             self.retranslateUi()
0142             for action in self.toolBar().actions():
0143                 if 'onfigure' in action.text():
0144                     action.setPriority(QAction.LowPriority)
0145             if Options.host and not Options.demo:
0146                 self.scene = PlayingScene(self)
0147                 HumanClient()
0148             StateSaver(self)
0149             self.show()
0150         else:
0151             HumanClient()
0152 
0153     @staticmethod
0154     def __installReactor():
0155         """install the twisted reactor"""
0156         if Internal.reactor is None:
0157             import qtreactor
0158             qtreactor.install()
0159             from twisted.internet import reactor
0160             reactor.runReturn(installSignalHandlers=False)
0161             Internal.reactor = reactor
0162             if Debug.quit:
0163                 logDebug('Installed qtreactor')
0164 
0165     @property
0166     def scene(self):
0167         """a proxy"""
0168         return self._scene
0169 
0170     @scene.setter
0171     def scene(self, value):
0172         """if changing, updateGUI"""
0173         if not isAlive(self):
0174             return
0175         if self._scene == value:
0176             return
0177         if not value:
0178             self.actionChat.setChecked(False)
0179             self.actionExplain.setChecked(False)
0180             self.actionScoreTable.setChecked(False)
0181             self.actionExplain.setData(ExplainView)
0182             self.actionScoreTable.setData(ScoreTable)
0183         self._scene = value
0184         self.centralView.setScene(value)
0185         self.adjustMainView()
0186         self.updateGUI()
0187         canDemo = not value or isinstance(value, PlayingScene)
0188         self.actionChat.setEnabled(canDemo)
0189         self.actionAutoPlay.setEnabled(canDemo)
0190         self.actionExplain.setEnabled(value is not None)
0191         self.actionScoreTable.setEnabled(value is not None)
0192 
0193     def sizeHint(self):
0194         """give the main window a sensible default size"""
0195         result = KXmlGuiWindow.sizeHint(self)
0196         result.setWidth(result.height() * 3 // 2)
0197                         # we want space to the right for the buttons
0198         # the default is too small. Use at least 2/3 of screen height and 1/2
0199         # of screen width:
0200         available = KApplication.desktopSize()
0201         height = max(result.height(), available.height() * 2 // 3)
0202         width = max(result.width(), available.width() // 2)
0203         result.setHeight(height)
0204         result.setWidth(width)
0205         return result
0206 
0207     def showEvent(self, event):
0208         """force a resize which calculates the correct background image size"""
0209         self.centralView.resizeEvent(True)
0210         KXmlGuiWindow.showEvent(self, event)
0211 
0212     def _kajonggToggleAction(self, name, icon, shortcut=None, actionData=None):
0213         """a checkable action"""
0214         res = Action(self,
0215             name,
0216             icon,
0217             shortcut=shortcut,
0218             actionData=actionData)
0219         res.setCheckable(True)
0220         if actionData is not None:
0221             res.toggled.connect(self._toggleWidget)
0222         return res
0223 
0224     def setupUi(self):
0225         """create all other widgets
0226         we could make the scene view the central widget but I did
0227         not figure out how to correctly draw the background with
0228         QGraphicsView/QGraphicsScene.
0229         QGraphicsView.drawBackground always wants a pixmap
0230         for a huge rect like 4000x3000 where my screen only has
0231         1920x1200"""
0232         # pylint: disable=too-many-statements
0233         self.setObjectName("MainWindow")
0234         centralWidget = QWidget()
0235         self.centralView = FittingView()
0236         layout = QGridLayout(centralWidget)
0237         layout.setContentsMargins(0, 0, 0, 0)
0238         layout.addWidget(self.centralView)
0239         self.setCentralWidget(centralWidget)
0240         self.centralView.setFocusPolicy(Qt.StrongFocus)
0241         self.background = None  # just for pylint
0242         self.windTileset = Tileset(Internal.Preferences.windTilesetName)
0243         self.adjustMainView()
0244         self.actionScoreGame = Action(
0245             self,
0246             "scoreGame",
0247             "draw-freehand",
0248             self.scoringScene,
0249             Qt.Key_C)
0250         self.actionPlayGame = Action(
0251             self,
0252             "play",
0253             "arrow-right",
0254             self.playGame,
0255             Qt.Key_N)
0256         self.actionAbortGame = Action(
0257             self,
0258             "abort",
0259             "dialog-close",
0260             self.abortAction,
0261             Qt.Key_W)
0262         self.actionAbortGame.setEnabled(False)
0263         self.actionQuit = Action(
0264             self,
0265             "quit",
0266             "application-exit",
0267             self.close,
0268             Qt.Key_Q)
0269         self.actionPlayers = Action(
0270             self,
0271             "players", "im-user", self.slotPlayers)
0272         self.actionRulesets = Action(
0273             self,
0274             "rulesets",
0275             "games-kajongg-law",
0276             self.slotRulesets)
0277         self.actionChat = self._kajonggToggleAction("chat", "call-start",
0278                                                     shortcut=Qt.Key_H, actionData=ChatWindow)
0279         self.actionChat.setEnabled(False)
0280         self.actionAngle = Action(
0281             self,
0282             "angle",
0283             "object-rotate-left",
0284             self.changeAngle,
0285             Qt.Key_G)
0286         self.actionAngle.setEnabled(False)
0287         self.actionScoreTable = self._kajonggToggleAction(
0288             "scoreTable", "format-list-ordered",
0289             Qt.Key_T, actionData=ScoreTable)
0290         self.actionScoreTable.setEnabled(False)
0291         self.actionExplain = self._kajonggToggleAction(
0292             "explain", "applications-education",
0293             Qt.Key_E, actionData=ExplainView)
0294         self.actionExplain.setEnabled(False)
0295         self.actionFullscreen = self._kajonggToggleAction(
0296             "fullscreen", "view-fullscreen", shortcut=Qt.Key_F | Qt.ShiftModifier)
0297         self.actionFullscreen.toggled.connect(self.fullScreen)
0298         self.actionAutoPlay = Action(
0299             self,
0300             "demoMode",
0301             "arrow-right-double",
0302             None,
0303             Qt.Key_D)
0304         self.actionAutoPlay.setCheckable(True)
0305         self.actionAutoPlay.setEnabled(True)
0306         self.actionAutoPlay.toggled.connect(self._toggleDemoMode)
0307         self.actionAutoPlay.setChecked(Internal.autoPlay)
0308         QMetaObject.connectSlotsByName(self)
0309 
0310     def playGame(self):
0311         """manual wish for a new game"""
0312         if not Internal.autoPlay:
0313             # only if no demo game is running
0314             self.playingScene()
0315 
0316     def playingScene(self):
0317         """play a computer game: log into a server and show its tables"""
0318         self.scene = PlayingScene(self)
0319         HumanClient()
0320 
0321     def scoringScene(self):
0322         """start a scoring scene"""
0323         scene = ScoringScene(self)
0324         game = scoreGame()
0325         if game:
0326             self.scene = scene
0327             scene.game = game
0328             game.throwDices()
0329             self.updateGUI()
0330 
0331     def fullScreen(self, toggle):
0332         """toggle between full screen and normal view"""
0333         if toggle:
0334             self.setWindowState(self.windowState() | Qt.WindowFullScreen)
0335         else:
0336             self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)
0337 
0338     def close(self, unusedResult=None):
0339         """wrap close() because we call it with a QTimer"""
0340         if isAlive(self):
0341             return KXmlGuiWindow.close(self)
0342         return True  # is closed
0343 
0344     def closeEvent(self, event):
0345         KXmlGuiWindow.closeEvent(self, event)
0346         if event.isAccepted() and self.exitReady:
0347             QTimer.singleShot(5000, self.aboutToQuit)
0348 
0349     def queryClose(self):
0350         """queryClose, queryExit and aboutToQuit are no
0351         ideal match for the async Deferred approach.
0352 
0353         At app start, self.exitConfirmed and exitReady are None.
0354 
0355         queryClose will show a confirmation prompt if needed, but
0356         it will not wait for the answer. queryClose always returns True.
0357 
0358         Later, when the user confirms exit, self.exitConfirmed will be set.
0359         If the user cancels exit, self.exitConfirmed = False, otherwise
0360         self.close() is called. This time, no prompt will appear because the
0361         game has already been aborted.
0362 
0363         queryExit will return False if exitConfirmed or exitReady are not True.
0364         Otherwise, queryExit will set exitReady to False and asynchronously start
0365         shutdown. After the reactor stops running, exitReady is set to True,
0366         and self.close() is called. This time it should fall through everywhere,
0367         having queryClose() and queryExit() return True.
0368 
0369         and it will reset exitConfirmed to None.
0370 
0371         Or in other words: If queryClose or queryExit find something that they
0372         should do async like asking the user for confirmation or terminating
0373         the client/server connection, they start async operation and append
0374         a callback which will call self.close() when the async operation is
0375         done. This repeats until queryClose() and queryExit() find nothing
0376         more to do async. At that point queryExit says True
0377         and we really end the program.
0378         """
0379 
0380         # pylint: disable=too-many-branches
0381         def confirmed(result):
0382             """quit if the active game has been aborted"""
0383             self.exitConfirmed = bool(result)
0384             if Debug.quit:
0385                 if self.exitConfirmed:
0386                     logDebug('mainWindow.queryClose confirmed')
0387                 else:
0388                     logDebug('mainWindow.queryClose not confirmed')
0389             # start closing again. This time no question will appear, the game
0390             # is already aborted
0391             if self.exitConfirmed:
0392                 assert isAlive(self)
0393                 self.close()
0394             else:
0395                 self.exitConfirmed = None
0396 
0397         def cancelled(result):
0398             """just do nothing"""
0399             if Debug.quit:
0400                 logDebug('mainWindow.queryClose.cancelled: {}'.format(result))
0401             self.exitConfirmed = None
0402         if self.exitConfirmed is False:
0403             # user is currently being asked
0404             return False
0405         if self.exitConfirmed is None:
0406             if self.scene:
0407                 self.exitConfirmed = False
0408                 self.abortAction().addCallbacks(confirmed, cancelled)
0409             else:
0410                 self.exitConfirmed = True
0411                 if Debug.quit:
0412                     logDebug(
0413                         'MainWindow.queryClose not asking, exitConfirmed=True')
0414         return True
0415 
0416     def queryExit(self):
0417         """see queryClose"""
0418         def quitDebug(*args, **kwargs):
0419             """reducing branches in queryExit"""
0420             if Debug.quit:
0421                 logDebug(*args, **kwargs)
0422 
0423         if self.exitReady:
0424             quitDebug('MainWindow.queryExit returns True because exitReady is set')
0425             return True
0426         if self.exitConfirmed:
0427             # now we can get serious
0428             self.exitReady = False
0429             for widget in chain(
0430                     (x.tableList for x in HumanClient.humanClients), [
0431                         self.confDialog,
0432                         self.rulesetWindow, self.playerWindow]):
0433                 if isAlive(widget):
0434                     widget.hide()
0435             if self.exitWaitTime is None:
0436                 self.exitWaitTime = 0
0437             if Internal.reactor and Internal.reactor.running:
0438                 self.exitWaitTime += 10
0439                 if self.exitWaitTime % 1000 == 0:
0440                     logDebug(
0441                         'waiting since %d seconds for reactor to stop' %
0442                         (self.exitWaitTime // 1000))
0443                 try:
0444                     quitDebug('now stopping reactor')
0445                     Internal.reactor.stop()
0446                     assert isAlive(self)
0447                     QTimer.singleShot(10, self.close)
0448                 except ReactorNotRunning:
0449                     self.exitReady = True
0450                     quitDebug(
0451                         'MainWindow.queryExit returns True: It got exception ReactorNotRunning')
0452             else:
0453                 self.exitReady = True
0454                 quitDebug('MainWindow.queryExit returns True: Reactor is not running')
0455         return bool(self.exitReady)
0456 
0457     @staticmethod
0458     def aboutToQuit():
0459         """now all connections to servers are cleanly closed"""
0460         mainWindow = Internal.mainWindow
0461         Internal.mainWindow = None
0462         if mainWindow:
0463             if Debug.quit:
0464                 logDebug('aboutToQuit starting')
0465             if mainWindow.exitWaitTime is not None and mainWindow.exitWaitTime > 1000.0 or Debug.quit:
0466                 logDebug(
0467                     'reactor stopped after %d ms' %
0468                     (mainWindow.exitWaitTime))
0469         else:
0470             if Debug.quit:
0471                 logDebug('aboutToQuit: mainWindow is already None')
0472             # this does not happen with PyQt5/6 or PySide2, only with PySide6
0473             # return here to avoid recursion in StateSaver
0474             return
0475         StateSaver.saveAll()
0476         Internal.app.quit()
0477         try:
0478             # if we are killed while loading, Internal.db may not yet be
0479             # defined
0480             if Internal.db:
0481                 Internal.db.close()
0482         except NameError:
0483             pass
0484         checkMemory()
0485         logging.shutdown()
0486         if Debug.quit:
0487             logDebug('aboutToQuit ending')
0488 
0489     def abortAction(self):
0490         """abort current game"""
0491         if Debug.quit:
0492             logDebug('mainWindow.abortAction invoked')
0493         return self.scene.abort()
0494 
0495     def retranslateUi(self):
0496         """retranslate"""
0497         self.actionScoreGame.setText(
0498             i18nc('@action:inmenu', "&Score Manual Game"))
0499         self.actionScoreGame.setIconText(
0500             i18nc('@action:intoolbar', 'Manual Game'))
0501         self.actionScoreGame.setWhatsThis(
0502             i18nc('kajongg @info:tooltip',
0503                   '&Score a manual game.'))
0504 
0505         self.actionPlayGame.setText(i18nc('@action:intoolbar', "&Play"))
0506         self.actionPlayGame.setPriority(QAction.LowPriority)
0507         self.actionPlayGame.setWhatsThis(
0508             i18nc('kajongg @info:tooltip', 'Start a new game.'))
0509 
0510         self.actionAbortGame.setText(i18nc('@action:inmenu', "&Abort Game"))
0511         self.actionAbortGame.setPriority(QAction.LowPriority)
0512         self.actionAbortGame.setWhatsThis(
0513             i18nc('kajongg @info:tooltip',
0514                   'Abort the current game.'))
0515 
0516         self.actionQuit.setText(i18nc('@action:inmenu', "&Quit Kajongg"))
0517         self.actionQuit.setPriority(QAction.LowPriority)
0518 
0519         self.actionPlayers.setText(i18nc('@action:intoolbar', "&Players"))
0520         self.actionPlayers.setWhatsThis(
0521             i18nc('kajongg @info:tooltip',
0522                   'define your players.'))
0523 
0524         self.actionRulesets.setText(i18nc('@action:intoolbar', "&Rulesets"))
0525         self.actionRulesets.setWhatsThis(
0526             i18nc('kajongg @info:tooltip',
0527                   'customize rulesets.'))
0528 
0529         self.actionAngle.setText(
0530             i18nc('@action:inmenu',
0531                   "&Change Visual Angle"))
0532         self.actionAngle.setIconText(i18nc('@action:intoolbar', "Angle"))
0533         self.actionAngle.setWhatsThis(
0534             i18nc('kajongg @info:tooltip',
0535                   "Change the visual appearance of the tiles."))
0536 
0537         self.actionFullscreen.setText(
0538             i18nc('@action:inmenu',
0539                   "F&ull Screen Mode"))
0540 
0541         self.actionScoreTable.setText(
0542             i18nc('kajongg @action:inmenu', "&Score Table"))
0543         self.actionScoreTable.setIconText(
0544             i18nc('kajongg @action:intoolbar', "&Scores"))
0545         self.actionScoreTable.setWhatsThis(i18nc('kajongg @info:tooltip',
0546                                                  "Show or hide the score table for the current game."))
0547 
0548         self.actionExplain.setText(i18nc('@action:inmenu', "&Explain Scores"))
0549         self.actionExplain.setIconText(i18nc('@action:intoolbar', "&Explain"))
0550         self.actionExplain.setWhatsThis(i18nc('kajongg @info:tooltip',
0551                                               'Explain the scoring for all players in the current game.'))
0552 
0553         self.actionAutoPlay.setText(i18nc('@action:inmenu', "&Demo Mode"))
0554         self.actionAutoPlay.setPriority(QAction.LowPriority)
0555         self.actionAutoPlay.setWhatsThis(i18nc('kajongg @info:tooltip',
0556                                                'Let the computer take over for you. Start a new local game if needed.'))
0557 
0558         self.actionChat.setText(i18n("C&hat"))
0559         self.actionChat.setWhatsThis(
0560             i18nc('kajongg @info:tooltip',
0561                   'Chat with the other players.'))
0562 
0563     def changeEvent(self, event):
0564         """when the applicationwide language changes, recreate GUI"""
0565         if event.type() == QEvent.LanguageChange:
0566             self.setupGUI()
0567             self.retranslateUi()
0568 
0569     def slotPlayers(self):
0570         """show the player list"""
0571         if not self.playerWindow:
0572             self.playerWindow = PlayerList(self)
0573         self.playerWindow.show()
0574 
0575     def slotRulesets(self):
0576         """show the player list"""
0577         if not self.rulesetWindow:
0578             self.rulesetWindow = RulesetSelector()
0579         self.rulesetWindow.show()
0580 
0581     def adjustMainView(self):
0582         """adjust the view such that exactly the wanted things are displayed
0583         without having to scroll"""
0584         if not Internal.scaleScene or not isAlive(self):
0585             return
0586         view, scene = self.centralView, self.scene
0587         if scene:
0588             scene.adjustSceneView()
0589             view.fitInView(scene.itemsBoundingRect(), Qt.KeepAspectRatio)
0590 
0591     @afterQueuedAnimations
0592     def backgroundChanged(self, unusedDeferredResult, unusedOldName, newName):
0593         """if the wanted background changed, apply the change now"""
0594         centralWidget = self.centralWidget()
0595         if centralWidget:
0596             self.background = Background(newName)
0597             self.background.setPalette(centralWidget)
0598             centralWidget.setAutoFillBackground(True)
0599 
0600     @afterQueuedAnimations
0601     def tilesetNameChanged(
0602             self, unusedDeferredResult, unusedOldValue=None,
0603             unusedNewValue=None):
0604         """if the wanted tileset changed, apply the change now"""
0605         if self.centralView:
0606             with AnimationSpeed():
0607                 if self.scene:
0608                     self.scene.applySettings()
0609             self.adjustMainView()
0610 
0611     @afterQueuedAnimations
0612     def showSettings(self, unusedDeferredResult, unusedChecked=None):
0613         """show preferences dialog. If it already is visible, do nothing"""
0614         # This is called by the triggered() signal. So why does KDE
0615         # not return the bool checked?
0616         if ConfigDialog.showDialog("settings"):
0617             return
0618         # if an animation is running, Qt segfaults somewhere deep
0619         # in the SVG renderer rendering the wind tiles for the tile
0620         # preview
0621         self.confDialog = ConfigDialog(self, "settings")
0622         self.confDialog.show()
0623 
0624     def _toggleWidget(self, checked):
0625         """user has toggled widget visibility with an action"""
0626         assert self.scene
0627         action = self.sender()
0628         actionData = action.data()
0629         if checked:
0630             if isinstance(actionData, type):
0631                 clsName = actionData.__name__
0632                 actionData = actionData(scene=self.scene)
0633                 action.setData(actionData)
0634                 setattr(
0635                     self.scene,
0636                     clsName[0].lower() + clsName[1:],
0637                     actionData)
0638             actionData.show()
0639             actionData.raise_()
0640         else:
0641             assert actionData
0642             actionData.hide()
0643 
0644     def _toggleDemoMode(self, checked):
0645         """switch on / off for autoPlay"""
0646         if self.scene:
0647             self.scene.toggleDemoMode(checked)
0648         else:
0649             Internal.autoPlay = checked
0650             if checked and Internal.db:
0651                 self.playingScene()
0652 
0653     def updateGUI(self):
0654         """update some actions, all auxiliary windows and the statusbar"""
0655         if not isAlive(self):
0656             return
0657         self.setCaption('')
0658         for action in [self.actionScoreGame, self.actionPlayGame]:
0659             action.setEnabled(not bool(self.scene))
0660         self.actionAbortGame.setEnabled(bool(self.scene))
0661         scene = self.scene
0662         if isAlive(scene):
0663             scene.updateSceneGUI()
0664 
0665     @afterQueuedAnimations
0666     def changeAngle(self, deferredResult, unusedButtons=None, unusedModifiers=None): # pylint: disable=unused-argument
0667         """change the lightSource"""
0668         if self.scene:
0669             with AnimationSpeed():
0670                 self.scene.changeAngle()