File indexing completed on 2024-04-14 03:59:08
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2010-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 import functools 0011 import types 0012 0013 from twisted.internet.defer import Deferred, succeed, fail 0014 0015 from qt import QPropertyAnimation, QParallelAnimationGroup, \ 0016 QAbstractAnimation, QEasingCurve 0017 from qt import Property, QGraphicsObject, QGraphicsItem 0018 0019 from common import Internal, Debug, isAlive, StrMixin 0020 from log import logDebug, logException, id4 0021 0022 0023 class Animation(QPropertyAnimation, StrMixin): 0024 0025 """a Qt animation with helper methods""" 0026 0027 nextAnimations = [] 0028 clsUid = 0 0029 0030 def __init__(self, graphicsObject, propName, endValue, parent=None): 0031 Animation.clsUid += 1 0032 self.uid = Animation.clsUid 0033 pName = propName 0034 QPropertyAnimation.__init__(self, graphicsObject, pName.encode(), parent) 0035 QPropertyAnimation.setEndValue(self, endValue) 0036 duration = Internal.Preferences.animationDuration() 0037 self.setDuration(duration) 0038 self.setEasingCurve(QEasingCurve.InOutQuad) 0039 graphicsObject.queuedAnimations.append(self) 0040 Animation.nextAnimations.append(self) 0041 self.debug = graphicsObject.name() in Debug.animation or Debug.animation == 'all' 0042 self.debug |= 'T{}t'.format(id4(graphicsObject)) in Debug.animation 0043 if self.debug: 0044 oldAnimation = graphicsObject.activeAnimation.get(propName, None) 0045 if isAlive(oldAnimation): 0046 logDebug( 0047 'new Animation(%s) (after %s is done)' % 0048 (self, oldAnimation.ident())) 0049 else: 0050 logDebug('Animation(%s)' % self) 0051 0052 def setEndValue(self, endValue): 0053 """wrapper with debugging code""" 0054 graphicsObject = self.targetObject() 0055 if not isAlive(graphicsObject): 0056 # may happen when aborting a game because animations are cancelled first, 0057 # before the last move from server is executed 0058 return 0059 if graphicsObject.name() in Debug.animation or Debug.animation == 'all': 0060 pName = self.pName().decode() 0061 logDebug( 0062 '%s: change endValue for %s: %s->%s %s' % ( 0063 self.ident(), pName, 0064 self.formatValue(self.endValue()), 0065 self.formatValue(endValue), graphicsObject)) 0066 QPropertyAnimation.setEndValue(self, endValue) 0067 0068 def ident(self): 0069 """the identifier to be used in debug messages""" 0070 pGroup = self.group() if isAlive(self) else 'notAlive' 0071 if pGroup or not isAlive(self): 0072 return '%s/A%s' % (pGroup, id4(self)) 0073 return 'A%s-%s' % (id4(self), self.targetObject().name()) 0074 0075 def pName(self): 0076 """ 0077 Return self.propertyName() as a python string. 0078 0079 @return: C{str} 0080 """ 0081 if not isAlive(self): 0082 return 'notAlive' 0083 return bytes(self.propertyName()).decode() 0084 0085 def formatValue(self, value): 0086 """string format the wanted value from qvariant""" 0087 pName = self.pName() 0088 if pName == 'pos': 0089 return '%.0f/%.0f' % (value.x(), value.y()) 0090 if pName == 'rotation': 0091 return '%d' % value 0092 if pName == 'scale': 0093 return '%.2f' % value 0094 return 'formatValue: unexpected {}={}'.format(pName, value) 0095 0096 def __str__(self): 0097 """for debug messages""" 0098 if isAlive(self) and isAlive(self.targetObject()): 0099 currentValue = getattr(self.targetObject(), self.pName()) 0100 endValue = self.endValue() 0101 targetObject = self.targetObject() 0102 else: 0103 currentValue = 'notAlive' 0104 endValue = 'notAlive' 0105 targetObject = 'notAlive' 0106 return '%s %s: %s->%s for %s duration=%dms' % ( 0107 self.ident(), self.pName(), 0108 self.formatValue(currentValue), 0109 self.formatValue(endValue), 0110 targetObject, 0111 self.duration()) 0112 0113 @staticmethod 0114 def removeImmediateAnimations(): 0115 """execute and remove immediate moves from the list 0116 We do not animate objects if 0117 - we are in a graphics object drag/drop operation 0118 - the user disabled animation 0119 - there are too many animations in the group so it would be too slow 0120 - the object has duration 0 0121 """ 0122 if Animation.nextAnimations: 0123 needRefresh = False 0124 shortcutAll = (Internal.scene is None 0125 or Internal.mainWindow.centralView.dragObject 0126 or Internal.Preferences.animationSpeed == 99 0127 or len(Animation.nextAnimations) > 1000) 0128 # change 1000 to 100 if we do not want to animate shuffling and 0129 # initial deal 0130 for animation in Animation.nextAnimations[:]: 0131 if shortcutAll or animation.duration() == 0: 0132 animation.targetObject().shortcutAnimation(animation) 0133 Animation.nextAnimations.remove(animation) 0134 needRefresh = True 0135 if needRefresh and Internal.scene: 0136 Internal.scene.focusRect.refresh() 0137 0138 0139 class ParallelAnimationGroup(QParallelAnimationGroup, StrMixin): 0140 0141 """ 0142 current is the currently executed group 0143 doAfter is a list of Deferred to be called when this group 0144 is done. If another group is chained to this one, transfer 0145 doAfter to that other group. 0146 """ 0147 0148 running = [] # we need a reference to active animation groups 0149 current = None 0150 clsUid = 0 0151 def __init__(self, animations, parent=None): 0152 QParallelAnimationGroup.__init__(self, parent) 0153 self.animations = animations 0154 self.uid = ParallelAnimationGroup.clsUid 0155 ParallelAnimationGroup.clsUid += 1 0156 self.deferred = Deferred() 0157 self.deferred.addErrback(logException) 0158 self.steps = 0 0159 self.debug = any(x.debug for x in self.animations) 0160 self.debug |= 'G{}g'.format(id4(self)) in Debug.animation 0161 self.doAfter = list() 0162 if ParallelAnimationGroup.current: 0163 if self.debug or ParallelAnimationGroup.current.debug: 0164 logDebug('Chaining Animation group G%s to G%s' % 0165 (id4(self), ParallelAnimationGroup.current)) 0166 self.doAfter = ParallelAnimationGroup.current.doAfter 0167 ParallelAnimationGroup.current.doAfter = list() 0168 ParallelAnimationGroup.current.deferred.addCallback(self.start).addErrback(logException) 0169 else: 0170 self.start() 0171 ParallelAnimationGroup.running.append(self) 0172 ParallelAnimationGroup.current = self 0173 self.stateChanged.connect(self.showState) 0174 0175 @staticmethod 0176 def cancelAll(): 0177 """cancel all animations""" 0178 if Debug.quit: 0179 logDebug('Cancelling all animations') 0180 for group in ParallelAnimationGroup.running: 0181 if isAlive(group): 0182 group.clear() 0183 0184 def showState(self, newState, oldState): 0185 """override Qt method""" 0186 if self.debug: 0187 logDebug('G{}: {} -> {} isAlive:{}'.format( 0188 self.uid, self.stateName(oldState), self.stateName(newState), isAlive(self))) 0189 0190 def updateCurrentTime(self, value): 0191 """count how many steps an animation does.""" 0192 self.steps += 1 0193 if self.steps % 50 == 0: 0194 # periodically check if the board still exists. 0195 # if not (game end), we do not want to go on 0196 for animation in self.animations: 0197 graphicsObject = animation.targetObject() 0198 if hasattr(graphicsObject, 'board') and not isAlive(graphicsObject.board): 0199 graphicsObject.clearActiveAnimation(animation) 0200 self.removeAnimation(animation) 0201 QParallelAnimationGroup.updateCurrentTime(self, value) 0202 0203 def start(self, unusedResults='DIREKT'): 0204 """start the animation, returning its deferred""" 0205 if not isAlive(self): 0206 return fail() 0207 assert self.state() != QAbstractAnimation.Running 0208 for animation in self.animations: 0209 graphicsObject = animation.targetObject() 0210 if not isAlive(animation) or not isAlive(graphicsObject): 0211 return fail() 0212 graphicsObject.setActiveAnimation(animation) 0213 self.addAnimation(animation) 0214 propName = animation.pName() 0215 animation.setStartValue(graphicsObject.getValue(propName)) 0216 if propName == 'rotation': 0217 # change direction if that makes the difference smaller 0218 endValue = animation.endValue() 0219 currValue = graphicsObject.rotation 0220 if endValue - currValue > 180: 0221 animation.setStartValue(currValue + 360) 0222 if currValue - endValue > 180: 0223 animation.setStartValue(currValue - 360) 0224 for animation in self.animations: 0225 animation.targetObject().setDrawingOrder() 0226 self.finished.connect(self.allFinished) 0227 scene = Internal.scene 0228 scene.focusRect.hide() 0229 QParallelAnimationGroup.start( 0230 self, 0231 QAbstractAnimation.DeleteWhenStopped) 0232 if self.debug: 0233 logDebug('%s started with speed %d (%s)' % ( 0234 self, Internal.Preferences.animationSpeed, 0235 ','.join('A%s' % id4(x) for x in self.animations))) 0236 return succeed(None).addErrback(logException) 0237 0238 def allFinished(self): 0239 """all animations have finished. Cleanup and callback""" 0240 self.fixAllBoards() 0241 if self == ParallelAnimationGroup.current: 0242 ParallelAnimationGroup.current = None 0243 ParallelAnimationGroup.running = [] 0244 if Debug.animationSpeed and self.duration(): 0245 perSecond = self.steps * 1000.0 / self.duration() 0246 if perSecond < 50: 0247 logDebug('%d steps for %d animations, %.1f/sec' % 0248 (self.steps, len(self.children()), perSecond)) 0249 # if we have a deferred, callback now 0250 assert self.deferred 0251 if self.debug: 0252 logDebug('Done: {}'.format(self)) 0253 if self.deferred: 0254 self.deferred.callback(None) 0255 for after in self.doAfter: 0256 after.callback(None) 0257 0258 def fixAllBoards(self): 0259 """set correct drawing order for all moved graphics objects""" 0260 for animation in self.children(): 0261 graphicsObject = animation.targetObject() 0262 if graphicsObject: 0263 graphicsObject.clearActiveAnimation(animation) 0264 if Internal.scene: 0265 Internal.scene.focusRect.refresh() 0266 0267 def stateName(self, state=None): 0268 """for debug output""" 0269 if not isAlive(self): 0270 return 'not alive' 0271 if state is None: 0272 state = self.state() 0273 if state == QAbstractAnimation.Stopped: 0274 return 'stopped' 0275 if state == QAbstractAnimation.Running: 0276 return 'running' 0277 assert False 0278 return None 0279 0280 def __str__(self): 0281 """for debugging""" 0282 return 'G{}({}:{})'.format(self.uid, len(self.animations), self.stateName()) 0283 0284 0285 class AnimatedMixin: 0286 """for UITile and PlayerWind""" 0287 0288 def __init__(self): 0289 super().__init__() 0290 self.activeAnimation = dict() # key is the property name 0291 self.queuedAnimations = [] 0292 0293 def _get_pos(self): 0294 """getter for property pos""" 0295 return QGraphicsObject.pos(self) 0296 0297 def _set_pos(self, pos): 0298 """setter for property pos""" 0299 QGraphicsObject.setPos(self, pos) 0300 0301 pos = Property('QPointF', fget=_get_pos, fset=_set_pos) 0302 0303 def _get_scale(self): 0304 """getter for property scale""" 0305 return QGraphicsObject.scale(self) 0306 0307 def _set_scale(self, scale): 0308 """setter for property scale""" 0309 QGraphicsObject.setScale(self, scale) 0310 0311 scale = Property(float, fget=_get_scale, fset=_set_scale) 0312 0313 def _get_rotation(self): 0314 """getter for property rotation""" 0315 return QGraphicsObject.rotation(self) 0316 0317 def _set_rotation(self, rotation): 0318 """setter for property rotation""" 0319 QGraphicsObject.setRotation(self, rotation) 0320 0321 rotation = Property(float, fget=_get_rotation, fset=_set_rotation) 0322 0323 def queuedAnimation(self, propertyName): 0324 """return the last queued animation for this graphics object and propertyName""" 0325 for item in reversed(self.queuedAnimations): 0326 if item.pName() == propertyName: 0327 return item 0328 return None 0329 0330 def shortcutAnimation(self, animation): 0331 """directly set the end value of the animation""" 0332 if animation.debug: 0333 logDebug('shortcut {}: UTile {}: clear queuedAnimations'.format(animation, self.name())) 0334 setattr(self, animation.pName(), animation.endValue()) 0335 self.queuedAnimations = [] 0336 self.setDrawingOrder() 0337 0338 def getValue(self, pName): 0339 """get a current property value""" 0340 return {'pos': self.pos, 'rotation': self.rotation, 0341 'scale': self.scale}[pName] 0342 0343 def setActiveAnimation(self, animation): 0344 """the graphics object knows which of its properties are currently animated""" 0345 self.queuedAnimations = [] 0346 propName = animation.pName() 0347 if self.name() in Debug.animation: 0348 oldAnimation = self.activeAnimation.get(propName, None) 0349 if not isAlive(oldAnimation): 0350 oldAnimation = None 0351 if oldAnimation: 0352 logDebug('**** setActiveAnimation {} {}: {} OVERRIDES {}'.format( 0353 self.name(), propName, animation, oldAnimation)) 0354 else: 0355 logDebug('setActiveAnimation {} {}: set {}'.format(self.name(), propName, animation)) 0356 self.activeAnimation[propName] = animation 0357 self.setCacheMode(QGraphicsItem.ItemCoordinateCache) 0358 0359 def clearActiveAnimation(self, animation): 0360 """an animation for this graphics object has ended. 0361 Finalize graphics object in its new position""" 0362 del self.activeAnimation[animation.pName()] 0363 if self.name() in Debug.animation: 0364 logDebug('UITile {}: clear activeAnimation[{}]'.format(self.name(), animation.pName())) 0365 self.setDrawingOrder() 0366 if not self.activeAnimation: 0367 self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) 0368 self.update() 0369 0370 def setupAnimations(self): 0371 """move the item to its new place. This puts new Animation 0372 objects into the queue to be animated by calling animate()""" 0373 for pName, newValue in self.moveDict().items(): 0374 if self.scene() != Internal.scene: 0375 # not part of the playing scene, like tiles in tilesetselector 0376 setattr(self, pName, newValue) 0377 continue 0378 animation = self.queuedAnimation(pName) 0379 if animation: 0380 curValue = animation.endValue() 0381 if curValue != newValue: 0382 # change a queued animation 0383 if self.name() in Debug.animation: 0384 logDebug('setEndValue for {}: {}: {}->{}'.format( 0385 animation, pName, animation.formatValue(curValue), animation.formatValue(newValue))) 0386 animation.setEndValue(newValue) 0387 else: 0388 animation = self.activeAnimation.get(pName, None) 0389 if isAlive(animation): 0390 curValue = animation.endValue() 0391 else: 0392 curValue = self.getValue(pName) 0393 if pName != 'scale' or abs(curValue - newValue) > 0.00001: 0394 # ignore rounding differences for scale 0395 if curValue != newValue: 0396 Animation(self, pName, newValue) 0397 0398 0399 class AnimationSpeed: 0400 0401 """a helper class for moving graphics with a given speed. 99=immediate.""" 0402 0403 def __init__(self, speed=None): 0404 if speed is None: 0405 speed = 99 0406 if Internal.Preferences: 0407 self.__speed = speed 0408 self.prevAnimationSpeed = Internal.Preferences.animationSpeed 0409 if Internal.Preferences.animationSpeed != speed: 0410 Internal.Preferences.animationSpeed = speed 0411 if Debug.animationSpeed: 0412 logDebug('AnimationSpeed sets speed %d' % speed) 0413 0414 def __enter__(self): 0415 return self 0416 0417 def __exit__(self, exc_type, exc_value, trback): 0418 """reset previous animation speed""" 0419 if Internal.Preferences: 0420 if self.__speed < 99: 0421 animate() 0422 if Internal.Preferences.animationSpeed != self.prevAnimationSpeed: 0423 if Debug.animationSpeed: 0424 logDebug('AnimationSpeed restores speed %d to %d' % ( 0425 Internal.Preferences.animationSpeed, self.prevAnimationSpeed)) 0426 Internal.Preferences.animationSpeed = self.prevAnimationSpeed 0427 0428 0429 def afterQueuedAnimations(doAfter): 0430 """A decorator""" 0431 0432 @functools.wraps(doAfter) 0433 def doAfterQueuedAnimations(*args, **kwargs): 0434 """do this after all queued animations have finished""" 0435 method = types.MethodType(doAfter, args[0]) 0436 args = args[1:] 0437 varnames = doAfter.__code__.co_varnames 0438 assert varnames[1] in ('deferredResult', 'unusedDeferredResult'), \ 0439 '{} passed {} instead of deferredResult'.format( 0440 doAfter.__qualname__, varnames[1]) 0441 animateAndDo(method, *args, **kwargs) 0442 0443 return doAfterQueuedAnimations 0444 0445 0446 def animate(): 0447 """now run all prepared animations. Returns a Deferred 0448 so callers can attach callbacks to be executed when 0449 animation is over. 0450 """ 0451 if Animation.nextAnimations: 0452 Animation.removeImmediateAnimations() 0453 animations = Animation.nextAnimations 0454 if animations: 0455 Animation.nextAnimations = [] 0456 return ParallelAnimationGroup(animations).deferred 0457 elif ParallelAnimationGroup.current: 0458 return ParallelAnimationGroup.current.deferred 0459 return succeed(None).addErrback(logException) 0460 0461 0462 def doCallbackWithSpeed(result, speed, callback, *args, **kwargs): 0463 """as the name says""" 0464 with AnimationSpeed(speed): 0465 callback(result, *args, **kwargs) 0466 0467 0468 def animateAndDo(callback, *args, **kwargs): 0469 """if we want the next animations to have the same speed as the current group, 0470 do not use animate().addCallback() because speed would not be kept""" 0471 result = animate() 0472 if Internal.Preferences: 0473 # we might be called very early 0474 result.addCallback( 0475 doCallbackWithSpeed, Internal.Preferences.animationSpeed, 0476 callback, *args, **kwargs).addErrback(logException) 0477 return result