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