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