1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 
21 #include <cppuhelper/exc_hlp.hxx>
22 #include <comphelper/anytostring.hxx>
23 #include <sal/log.hxx>
24 #include <com/sun/star/presentation/ParagraphTarget.hpp>
25 #include <com/sun/star/animations/Timing.hpp>
26 #include <com/sun/star/animations/AnimationAdditiveMode.hpp>
27 #include <com/sun/star/animations/AnimationFill.hpp>
28 #include <com/sun/star/presentation/ShapeAnimationSubType.hpp>
29 
30 #include "nodetools.hxx"
31 #include <doctreenode.hxx>
32 #include "animationbasenode.hxx"
33 #include <delayevent.hxx>
34 #include <framerate.hxx>
35 
36 #include <boost/optional.hpp>
37 #include <algorithm>
38 
39 using namespace com::sun::star;
40 
41 namespace slideshow {
42 namespace internal {
43 
AnimationBaseNode(const uno::Reference<animations::XAnimationNode> & xNode,const BaseContainerNodeSharedPtr & rParent,const NodeContext & rContext)44 AnimationBaseNode::AnimationBaseNode(
45     const uno::Reference< animations::XAnimationNode >&   xNode,
46     const BaseContainerNodeSharedPtr&                     rParent,
47     const NodeContext&                                    rContext )
48     : BaseNode( xNode, rParent, rContext ),
49       mxAnimateNode( xNode, uno::UNO_QUERY_THROW ),
50       maAttributeLayerHolder(),
51       maSlideSize( rContext.maSlideSize ),
52       mpActivity(),
53       mpShape(),
54       mpShapeSubset(),
55       mpSubsetManager(rContext.maContext.mpSubsettableShapeManager),
56       mbPreservedVisibility(true),
57       mbIsIndependentSubset( rContext.mbIsIndependentSubset )
58 {
59     // extract native node targets
60     // ===========================
61 
62     // plain shape target
63     uno::Reference< drawing::XShape > xShape( mxAnimateNode->getTarget(),
64                                               uno::UNO_QUERY );
65 
66     // distinguish 5 cases:
67 
68     //  - plain shape target
69     //  (NodeContext.mpMasterShapeSubset full set)
70 
71     //  - parent-generated subset (generate an
72     //  independent subset)
73 
74     //  - parent-generated subset from iteration
75     //  (generate a dependent subset)
76 
77     //  - XShape target at the XAnimatioNode (generate
78     //  a plain shape target)
79 
80     //  - ParagraphTarget target at the XAnimationNode
81     //  (generate an independent shape subset)
82     if( rContext.mpMasterShapeSubset )
83     {
84         if( rContext.mpMasterShapeSubset->isFullSet() )
85         {
86             // case 1: plain shape target from parent
87             mpShape = rContext.mpMasterShapeSubset->getSubsetShape();
88         }
89         else
90         {
91             // cases 2 & 3: subset shape
92             mpShapeSubset = rContext.mpMasterShapeSubset;
93         }
94     }
95     else
96     {
97         // no parent-provided shape, try to extract
98         // from XAnimationNode - cases 4 and 5
99 
100         if( xShape.is() )
101         {
102             mpShape = lookupAttributableShape( getContext().mpSubsettableShapeManager,
103                                                xShape );
104         }
105         else
106         {
107             // no shape provided. Maybe a ParagraphTarget?
108             presentation::ParagraphTarget aTarget;
109 
110             if( !(mxAnimateNode->getTarget() >>= aTarget) )
111                 ENSURE_OR_THROW(
112                     false, "could not extract any target information" );
113 
114             xShape = aTarget.Shape;
115 
116             ENSURE_OR_THROW( xShape.is(), "invalid shape in ParagraphTarget" );
117 
118             mpShape = lookupAttributableShape( getContext().mpSubsettableShapeManager,
119                                                xShape );
120 
121             // NOTE: For shapes with ParagraphTarget, we ignore
122             // the SubItem property. We implicitly assume that it
123             // is set to ONLY_TEXT.
124             OSL_ENSURE(
125                 mxAnimateNode->getSubItem() ==
126                 presentation::ShapeAnimationSubType::ONLY_TEXT ||
127                 mxAnimateNode->getSubItem() ==
128                 presentation::ShapeAnimationSubType::AS_WHOLE,
129                 "ParagraphTarget given, but subitem not AS_TEXT or AS_WHOLE? "
130                 "Make up your mind, I'll ignore the subitem." );
131 
132             // okay, found a ParagraphTarget with a valid XShape. Does the shape
133             // provide the given paragraph?
134             if( aTarget.Paragraph >= 0 &&
135                 mpShape->getTreeNodeSupplier().getNumberOfTreeNodes(
136                     DocTreeNode::NodeType::LogicalParagraph) > aTarget.Paragraph )
137             {
138                 const DocTreeNode& rTreeNode(
139                     mpShape->getTreeNodeSupplier().getTreeNode(
140                         aTarget.Paragraph,
141                         DocTreeNode::NodeType::LogicalParagraph ) );
142 
143                 // CAUTION: the creation of the subset shape
144                 // _must_ stay in the node constructor, since
145                 // Slide::prefetchShow() initializes shape
146                 // attributes right after animation import (or
147                 // the Slide class must be changed).
148                 mpShapeSubset.reset(
149                     new ShapeSubset( mpShape,
150                                      rTreeNode,
151                                      mpSubsetManager ));
152 
153                 // Override NodeContext, and flag this node as
154                 // a special independent subset one. This is
155                 // important when applying initial attributes:
156                 // independent shape subsets must be setup
157                 // when the slide starts, since they, as their
158                 // name suggest, can have state independent to
159                 // the master shape. The following example
160                 // might illustrate that: a master shape has
161                 // no effect, one of the text paragraphs
162                 // within it has an appear effect. Now, the
163                 // respective paragraph must be invisible when
164                 // the slide is initially shown, and become
165                 // visible only when the effect starts.
166                 mbIsIndependentSubset = true;
167 
168                 // already enable subset right here, the
169                 // setup of initial shape attributes of
170                 // course needs the subset shape
171                 // generated, to apply e.g. visibility
172                 // changes.
173                 mpShapeSubset->enableSubsetShape();
174             }
175         }
176     }
177 }
178 
dispose()179 void AnimationBaseNode::dispose()
180 {
181     if (mpActivity) {
182         mpActivity->dispose();
183         mpActivity.reset();
184     }
185 
186     maAttributeLayerHolder.reset();
187     mxAnimateNode.clear();
188     mpShape.reset();
189     mpShapeSubset.reset();
190 
191     BaseNode::dispose();
192 }
193 
init_st()194 bool AnimationBaseNode::init_st()
195 {
196     // if we've still got an old activity lying around, dispose it:
197     if (mpActivity) {
198         mpActivity->dispose();
199         mpActivity.reset();
200     }
201 
202     // note: actually disposing the activity too early might cause problems,
203     // because on dequeued() it calls endAnimation(pAnim->end()), thus ending
204     // animation _after_ last screen update.
205     // review that end() is properly called (which calls endAnimation(), too).
206 
207     try {
208         // TODO(F2): For restart functionality, we must regenerate activities,
209         // since they are not able to reset their state (or implement _that_)
210         mpActivity = createActivity();
211     }
212     catch (uno::Exception const&) {
213         TOOLS_WARN_EXCEPTION( "slideshow", "" );
214         // catch and ignore. We later handle empty activities, but for
215         // other nodes to function properly, the core functionality of
216         // this node must remain up and running.
217     }
218     return true;
219 }
220 
resolve_st()221 bool AnimationBaseNode::resolve_st()
222 {
223     // enable shape subset for automatically generated
224     // subsets. Independent subsets are already setup
225     // during construction time. Doing it only here
226     // saves us a lot of sprites and shapes lying
227     // around. This is especially important for
228     // character-wise iterations, since the shape
229     // content (e.g. thousands of characters) would
230     // otherwise be painted character-by-character.
231     if (isDependentSubsettedShape() && mpShapeSubset) {
232         mpShapeSubset->enableSubsetShape();
233     }
234     return true;
235 }
236 
activate_st()237 void AnimationBaseNode::activate_st()
238 {
239     AttributableShapeSharedPtr const pShape(getShape());
240     mbPreservedVisibility = pShape->isVisible();
241 
242     // create new attribute layer
243     maAttributeLayerHolder.createAttributeLayer(pShape);
244 
245     ENSURE_OR_THROW( maAttributeLayerHolder.get(),
246                       "Could not generate shape attribute layer" );
247 
248     // TODO(Q2): This affects the way mpActivity
249     // works, but is performed here because of
250     // locality (we're fiddling with the additive mode
251     // here, anyway, and it's the only place where we
252     // do). OTOH, maybe the complete additive mode
253     // setup should be moved to the activities.
254 
255     // for simple by-animations, the SMIL spec
256     // requires us to emulate "0,by-value" value list
257     // behaviour, with additive mode forced to "sum",
258     // no matter what the input is
259     // (http://www.w3.org/TR/smil20/animation.html#adef-by).
260     if( mxAnimateNode->getBy().hasValue() &&
261         !mxAnimateNode->getTo().hasValue() &&
262         !mxAnimateNode->getFrom().hasValue() )
263     {
264         // force attribute mode to REPLACE (note the
265         // subtle discrepancy to the paragraph above,
266         // where SMIL requires SUM. This is internally
267         // handled by the FromToByActivity, and is
268         // because otherwise DOM values would not be
269         // handled correctly: the activity cannot
270         // determine whether an
271         // Activity::getUnderlyingValue() yields the
272         // DOM value, or already a summed-up conglomerate)
273 
274         // Note that this poses problems with our
275         // hybrid activity duration (time or min number of frames),
276         // since if activities
277         // exceed their duration, wrong 'by' start
278         // values might arise ('Laser effect')
279         maAttributeLayerHolder.get()->setAdditiveMode(
280             animations::AnimationAdditiveMode::REPLACE );
281     }
282     else
283     {
284         // apply additive mode to newly created Attribute layer
285         maAttributeLayerHolder.get()->setAdditiveMode(
286             mxAnimateNode->getAdditive() );
287     }
288 
289     // fake normal animation behaviour, even if we
290     // show nothing.  This is the appropriate way to
291     // handle errors on Activity generation, because
292     // maybe all other effects on the slide are
293     // correctly initialized (but won't run, if we
294     // signal an error here)
295     if (mpActivity) {
296         // supply Activity (and the underlying Animation) with
297         // it's AttributeLayer, to perform the animation on
298         mpActivity->setTargets( getShape(), maAttributeLayerHolder.get() );
299 
300         // add to activities queue
301         getContext().mrActivitiesQueue.addActivity( mpActivity );
302     }
303     else {
304         // Actually, DO generate the event for empty activity,
305         // to keep the chain of animations running
306         BaseNode::scheduleDeactivationEvent();
307     }
308 }
309 
deactivate_st(NodeState eDestState)310 void AnimationBaseNode::deactivate_st( NodeState eDestState )
311 {
312     if (eDestState == FROZEN && mpActivity)
313         mpActivity->end();
314 
315     if (isDependentSubsettedShape()) {
316         // for dependent subsets, remove subset shape
317         // from layer, re-integrate subsetted part
318         // back into original shape. For independent
319         // subsets, we cannot make any assumptions
320         // about subset attribute state relative to
321         // master shape, thus, have to keep it. This
322         // will effectively re-integrate the subsetted
323         // part into the original shape (whose
324         // animation will hopefully have ended, too)
325 
326         // this statement will save a whole lot of
327         // sprites for iterated text effects, since
328         // those sprites will only exist during the
329         // actual lifetime of the effects
330         if (mpShapeSubset) {
331             mpShapeSubset->disableSubsetShape();
332         }
333     }
334 
335     if (eDestState != ENDED)
336         return;
337 
338     // no shape anymore, no layer needed:
339     maAttributeLayerHolder.reset();
340 
341     if (! isDependentSubsettedShape()) {
342 
343         // for all other shapes, removing the
344         // attribute layer quite possibly changes
345         // shape display. Thus, force update
346         AttributableShapeSharedPtr const pShape( getShape() );
347 
348         // don't anybody dare to check against
349         // pShape->isVisible() here, removing the
350         // attribute layer might actually make the
351         // shape invisible!
352         getContext().mpSubsettableShapeManager->notifyShapeUpdate( pShape );
353     }
354 
355     if (mpActivity) {
356         // kill activity, if still running
357         mpActivity->dispose();
358         mpActivity.reset();
359     }
360 }
361 
removeEffect()362 void AnimationBaseNode::removeEffect()
363 {
364     if (!isDependentSubsettedShape()) {
365         AttributableShapeSharedPtr const pShape(getShape());
366         pShape->setVisibility(!mbPreservedVisibility);
367         getContext().mpSubsettableShapeManager->notifyShapeUpdate( pShape );
368         pShape->setVisibility(mbPreservedVisibility);
369     }
370 }
371 
hasPendingAnimation() const372 bool AnimationBaseNode::hasPendingAnimation() const
373 {
374     // TODO(F1): This might not always be true. Are there 'inactive'
375     // animation nodes?
376     return true;
377 }
378 
379 #if defined(DBG_UTIL)
showState() const380 void AnimationBaseNode::showState() const
381 {
382     BaseNode::showState();
383 
384     SAL_INFO( "slideshow.verbose", "AnimationBaseNode info: independent subset=" <<
385               (mbIsIndependentSubset ? "y" : "n") );
386 }
387 #endif
388 
389 ActivitiesFactory::CommonParameters
fillCommonParameters() const390 AnimationBaseNode::fillCommonParameters() const
391 {
392     double nDuration = 0.0;
393 
394     // TODO(F3): Duration/End handling is barely there
395     if( !(mxAnimateNode->getDuration() >>= nDuration) ) {
396         mxAnimateNode->getEnd() >>= nDuration; // Wah.
397     }
398 
399     // minimal duration we fallback to (avoid 0 here!)
400     nDuration = ::std::max( 0.001, nDuration );
401 
402     const bool bAutoReverse( mxAnimateNode->getAutoReverse() );
403 
404     boost::optional<double> aRepeats;
405     double nRepeats = 0;
406     if( mxAnimateNode->getRepeatCount() >>= nRepeats ) {
407         aRepeats = nRepeats;
408     }
409     else {
410         if( mxAnimateNode->getRepeatDuration() >>= nRepeats ) {
411             // when repeatDuration is given,
412             // autoreverse does _not_ modify the
413             // active duration. Thus, calc repeat
414             // count with already adapted simple
415             // duration (twice the specified duration)
416 
417             // convert duration back to repeat counts
418             if( bAutoReverse )
419                 aRepeats = nRepeats / (2.0 * nDuration);
420             else
421                 aRepeats = nRepeats / nDuration;
422         }
423         else
424         {
425             // no double value for both values - Timing::INDEFINITE?
426             animations::Timing eTiming;
427 
428             if( !(mxAnimateNode->getRepeatDuration() >>= eTiming) ||
429                 eTiming != animations::Timing_INDEFINITE )
430             {
431                 if( !(mxAnimateNode->getRepeatCount() >>= eTiming) ||
432                     eTiming != animations::Timing_INDEFINITE )
433                 {
434                     // no indefinite timing, no other values given -
435                     // use simple run, i.e. repeat of 1.0
436                     aRepeats = 1.0;
437                 }
438             }
439         }
440     }
441 
442     // calc accel/decel:
443     double nAcceleration = 0.0;
444     double nDeceleration = 0.0;
445     BaseNodeSharedPtr const pSelf( getSelf() );
446     for ( std::shared_ptr<BaseNode> pNode( pSelf );
447           pNode; pNode = pNode->getParentNode() )
448     {
449         uno::Reference<animations::XAnimationNode> const xAnimationNode(
450             pNode->getXAnimationNode() );
451         nAcceleration = std::max( nAcceleration,
452                                   xAnimationNode->getAcceleration() );
453         nDeceleration = std::max( nDeceleration,
454                                   xAnimationNode->getDecelerate() );
455     }
456 
457     EventSharedPtr pEndEvent;
458     if (pSelf) {
459         pEndEvent = makeEvent( [pSelf] () {pSelf->deactivate(); },
460             "AnimationBaseNode::deactivate");
461     }
462 
463     // Calculate the minimum frame count that depends on the duration and
464     // the minimum frame count.
465     const sal_Int32 nMinFrameCount (std::clamp<sal_Int32>(
466         basegfx::fround(nDuration * FrameRate::MinimumFramesPerSecond), 1, 10));
467 
468     return ActivitiesFactory::CommonParameters(
469         pEndEvent,
470         getContext().mrEventQueue,
471         getContext().mrActivitiesQueue,
472         nDuration,
473         nMinFrameCount,
474         bAutoReverse,
475         aRepeats,
476         nAcceleration,
477         nDeceleration,
478         getShape(),
479         getSlideSize());
480 }
481 
getShape() const482 AttributableShapeSharedPtr const & AnimationBaseNode::getShape() const
483 {
484     // any subsetting at all?
485     if (mpShapeSubset)
486         return mpShapeSubset->getSubsetShape();
487     else
488         return mpShape; // nope, plain shape always
489 }
490 
491 } // namespace internal
492 } // namespace slideshow
493 
494 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
495