1/* Copyright 2010 Maxim Kartashev
2 * Copyright 2016 Software Freedom Conservancy Inc.
3 *
4 * This software is licensed under the GNU LGPL (version 2.1 or later).
5 * See the COPYING file in this distribution.
6 */
7
8public class TransitionEffectsManager {
9    public const string NULL_EFFECT_ID = NullTransitionDescriptor.EFFECT_ID;
10    public const string RANDOM_EFFECT_ID = RandomEffectDescriptor.EFFECT_ID;
11    private static TransitionEffectsManager? instance = null;
12
13    // effects are stored by effect ID
14    private Gee.Map<string, Spit.Transitions.Descriptor> effects = new Gee.HashMap<
15        string, Spit.Transitions.Descriptor>();
16    private Spit.Transitions.Descriptor null_descriptor = new NullTransitionDescriptor();
17    private Spit.Transitions.Descriptor random_descriptor = new RandomEffectDescriptor();
18
19    private TransitionEffectsManager() {
20        load_transitions();
21        Plugins.Notifier.get_instance().pluggable_activation.connect(load_transitions);
22    }
23
24    ~TransitionEffectsManager() {
25        Plugins.Notifier.get_instance().pluggable_activation.disconnect(load_transitions);
26    }
27
28    private void load_transitions() {
29        effects.clear();
30
31        // add null and random effect first
32        effects.set(null_descriptor.get_id(), null_descriptor);
33        effects.set(random_descriptor.get_id(),random_descriptor);
34
35        // load effects from plug-ins
36        Gee.Collection<Spit.Pluggable> pluggables = Plugins.get_pluggables_for_type(
37            typeof(Spit.Transitions.Descriptor));
38        foreach (Spit.Pluggable pluggable in pluggables) {
39            int pluggable_interface = pluggable.get_pluggable_interface(Spit.Transitions.CURRENT_INTERFACE,
40                Spit.Transitions.CURRENT_INTERFACE);
41            if (pluggable_interface != Spit.Transitions.CURRENT_INTERFACE) {
42                warning("Unable to load transitions plug-in %s: reported interface %d",
43                    Plugins.get_pluggable_module_id(pluggable), pluggable_interface);
44
45                continue;
46            }
47
48            Spit.Transitions.Descriptor desc = (Spit.Transitions.Descriptor) pluggable;
49            if (effects.has_key(desc.get_id()))
50                warning("Multiple transitions loaded with same effect ID %s", desc.get_id());
51            else
52                effects.set(desc.get_id(), desc);
53        }
54    }
55
56    public static void init() {
57        instance = new TransitionEffectsManager();
58    }
59
60    public static void terminate() {
61        instance = null;
62    }
63
64    public static TransitionEffectsManager get_instance() {
65        assert(instance != null);
66
67        return instance;
68    }
69
70    public Gee.Collection<string> get_effect_ids() {
71        return effects.keys;
72    }
73
74    public Gee.Collection<string> get_effect_names(owned CompareDataFunc? comparator = null) {
75        Gee.Collection<string> effect_names = new Gee.TreeSet<string>((owned) comparator);
76        foreach (Spit.Transitions.Descriptor desc in effects.values)
77            effect_names.add(desc.get_pluggable_name());
78
79        return effect_names;
80    }
81
82    public string? get_id_for_effect_name(string effect_name) {
83        foreach (Spit.Transitions.Descriptor desc in effects.values) {
84            if (desc.get_pluggable_name() == effect_name)
85                return desc.get_id();
86        }
87
88        return null;
89    }
90
91    public Spit.Transitions.Descriptor? get_effect_descriptor(string effect_id) {
92        return effects.get(effect_id);
93    }
94
95    public string get_effect_name(string effect_id) {
96        Spit.Transitions.Descriptor? desc = get_effect_descriptor(effect_id);
97
98        return (desc != null) ? desc.get_pluggable_name() : _("(None)");
99    }
100
101    public Spit.Transitions.Descriptor get_null_descriptor() {
102        return null_descriptor;
103    }
104
105    public TransitionClock? create_transition_clock(string effect_id) {
106        Spit.Transitions.Descriptor? desc = get_effect_descriptor(effect_id);
107
108        return (desc != null) ? new TransitionClock(desc) : null;
109    }
110
111    public TransitionClock create_null_transition_clock() {
112        return new TransitionClock(null_descriptor);
113    }
114}
115
116public class TransitionClock {
117    // This method is called by TransitionClock to indicate that it's time for the transition to be
118    // repainted.  The callback should call TransitionClock.paint() with the appropriate Drawable
119    // either immediately or quite soon (in an expose event).
120    public delegate void RepaintCallback();
121
122    private Spit.Transitions.Descriptor desc;
123    private Spit.Transitions.Effect effect;
124    private int desired_fps;
125    private int min_fps;
126    private int current_fps = 0;
127    private OpTimer paint_timer;
128    private Spit.Transitions.Visuals? visuals = null;
129    private Spit.Transitions.Motion? motion = null;
130    private unowned RepaintCallback? repaint = null;
131    private uint timer_id = 0;
132    private ulong time_started = 0;
133    private int frame_number = 0;
134    private bool cancelled = false;
135
136    public TransitionClock(Spit.Transitions.Descriptor desc) {
137        this.desc = desc;
138
139        effect = desc.create(new Plugins.StandardHostInterface(desc, "transitions"));
140        effect.get_fps(out desired_fps, out min_fps);
141
142        paint_timer = new OpTimer(desc.get_pluggable_name());
143    }
144
145    ~TransitionClock() {
146        cancel_timer();
147        debug("%s tick_msec=%d min/desired/current fps=%d/%d/%d", paint_timer.to_string(),
148            (motion != null) ? motion.tick_msec : 0, min_fps, desired_fps, current_fps);
149    }
150
151    public bool is_in_progress() {
152        return (!cancelled && motion != null) ? frame_number < motion.total_frames : false;
153    }
154
155    public void start(Spit.Transitions.Visuals visuals, Spit.Transitions.Direction direction,
156        int duration_msec, RepaintCallback repaint) {
157        reset();
158
159        // if no desired FPS, this is a no-op transition
160        if (desired_fps == 0)
161            return;
162
163        this.visuals = visuals;
164        this.repaint = repaint;
165        motion = new Spit.Transitions.Motion(direction, desired_fps, duration_msec);
166
167        effect.start(visuals, motion);
168
169        // start the timer
170        // TODO: It may be smarter to not use Timeout naively, as it does not attempt to catch up
171        // when tick() is called late.
172        time_started = now_ms();
173        timer_id = Timeout.add_full(Priority.HIGH, motion.tick_msec, tick);
174    }
175
176    // This resets all state for the clock.  No check is done if the clock is running.
177    private void reset() {
178        visuals = null;
179        motion = null;
180        repaint = null;
181        cancel_timer();
182        time_started = 0;
183        frame_number = 1;
184        current_fps = 0;
185        cancelled = false;
186    }
187
188    private void cancel_timer() {
189        if (timer_id != 0) {
190            Source.remove(timer_id);
191            timer_id = 0;
192        }
193    }
194
195    // Calculate current FPS rate and returns true if it's above minimum
196    private bool is_fps_ok() {
197        assert(time_started > 0);
198
199        if (frame_number <= 3)
200            return true; // don't bother measuring if statistical data are too small
201
202        double elapsed_msec = (double) (now_ms() - time_started);
203        if (elapsed_msec <= 0.0)
204            return true;
205
206        current_fps = (int) ((frame_number * 1000.0) / elapsed_msec);
207        if (current_fps < min_fps) {
208            debug("Transition rate of %dfps below minimum of %dfps (elapsed=%lf frames=%d)",
209                current_fps, min_fps, elapsed_msec, frame_number);
210        }
211
212        return (current_fps >= min_fps);
213    }
214
215    // Cancels current transition.
216    public void cancel() {
217        cancelled = true;
218        cancel_timer();
219        effect.cancel();
220
221        // repaint to complete the transition
222        repaint();
223    }
224
225    // Call this whenever using a TransitionClock in the expose event.  Returns false if the
226    // transition has completed, in which case the caller should paint the final result.
227    public bool paint(Cairo.Context ctx, int width, int height) {
228        if (!is_in_progress())
229            return false;
230
231        paint_timer.start();
232
233        ctx.save();
234
235        if (effect.needs_clear_background()) {
236            ctx.set_source_rgba(visuals.bg_color.red, visuals.bg_color.green, visuals.bg_color.blue,
237                visuals.bg_color.alpha);
238            ctx.rectangle(0, 0, width, height);
239            ctx.fill();
240        }
241
242        effect.paint(visuals, motion, ctx, width, height, frame_number);
243
244        ctx.restore();
245
246        paint_timer.stop();
247
248        return true;
249    }
250
251    private bool tick() {
252        if (!is_fps_ok()) {
253            debug("Cancelling transition: below minimum fps");
254            cancel();
255        }
256
257        // repaint always; this timer tick will go away when the frames have exhausted (and
258        // guarantees the first frame is painted before advancing the counter)
259        repaint();
260
261        if (!is_in_progress()) {
262            cancel_timer();
263
264            return false;
265        }
266
267        // advance to the next frame
268        if (frame_number < motion.total_frames)
269            effect.advance(visuals, motion, ++frame_number);
270
271        return true;
272    }
273}
274
275public class NullTransitionDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor {
276    public const string EFFECT_ID = "org.yorba.shotwell.transitions.null";
277
278    public int get_pluggable_interface(int min_host_version, int max_host_version) {
279        return Spit.Transitions.CURRENT_INTERFACE;
280    }
281
282    public unowned string get_id() {
283        return EFFECT_ID;
284    }
285
286    public unowned string get_pluggable_name() {
287        return _("None");
288    }
289
290    public void get_info(ref Spit.PluggableInfo info) {
291    }
292
293    public void activation(bool enabled) {
294    }
295
296    public Spit.Transitions.Effect create(Spit.HostInterface host) {
297        return new NullEffect();
298    }
299}
300
301public class NullEffect : Object, Spit.Transitions.Effect {
302    public NullEffect() {
303    }
304
305    public void get_fps(out int desired_fps, out int min_fps) {
306        desired_fps = 0;
307        min_fps = 0;
308    }
309
310    public void start(Spit.Transitions.Visuals visuals, Spit.Transitions.Motion motion) {
311    }
312
313    public bool needs_clear_background() {
314        return false;
315    }
316
317    public void paint(Spit.Transitions.Visuals visuals, Spit.Transitions.Motion motion, Cairo.Context ctx,
318        int width, int height, int frame_number) {
319    }
320
321    public void advance(Spit.Transitions.Visuals visuals, Spit.Transitions.Motion motion, int frame_number) {
322    }
323
324    public void cancel() {
325    }
326}
327public class RandomEffectDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor {
328    public const string EFFECT_ID = "org.yorba.shotwell.transitions.random";
329
330    public int get_pluggable_interface(int min_host_version, int max_host_version) {
331        return Spit.Transitions.CURRENT_INTERFACE;
332    }
333
334    public unowned string get_id() {
335        return EFFECT_ID;
336    }
337
338    public unowned string get_pluggable_name() {
339        return _("Random");
340    }
341
342    public void get_info(ref Spit.PluggableInfo info) {
343    }
344
345    public void activation(bool enabled) {
346    }
347
348    public Spit.Transitions.Effect create(Spit.HostInterface host) {
349        return new NullEffect();
350    }
351}
352