summaryrefslogtreecommitdiff
path: root/src/slideshow/TransitionEffects.vala
blob: 5acc36d248b8ca532dba5a2ea227d9bd962b40da (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
/* Copyright 2010 Maxim Kartashev
 * Copyright 2011-2015 Yorba Foundation
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

public class TransitionEffectsManager {
    public const string NULL_EFFECT_ID = NullTransitionDescriptor.EFFECT_ID;
    public const string RANDOM_EFFECT_ID = RandomEffectDescriptor.EFFECT_ID;
    private static TransitionEffectsManager? instance = null;
    
    // effects are stored by effect ID
    private Gee.Map<string, Spit.Transitions.Descriptor> effects = new Gee.HashMap<
        string, Spit.Transitions.Descriptor>();
    private Spit.Transitions.Descriptor null_descriptor = new NullTransitionDescriptor();
    private Spit.Transitions.Descriptor random_descriptor = new RandomEffectDescriptor();
    
    private TransitionEffectsManager() {
        load_transitions();
        Plugins.Notifier.get_instance().pluggable_activation.connect(load_transitions);
    }
    
    ~TransitionEffectsManager() {
        Plugins.Notifier.get_instance().pluggable_activation.disconnect(load_transitions);
    }
    
    private void load_transitions() {
        effects.clear();
        
        // add null and random effect first
        effects.set(null_descriptor.get_id(), null_descriptor);
        effects.set(random_descriptor.get_id(),random_descriptor);

        // load effects from plug-ins
        Gee.Collection<Spit.Pluggable> pluggables = Plugins.get_pluggables_for_type(
            typeof(Spit.Transitions.Descriptor));
        foreach (Spit.Pluggable pluggable in pluggables) {
            int pluggable_interface = pluggable.get_pluggable_interface(Spit.Transitions.CURRENT_INTERFACE,
                Spit.Transitions.CURRENT_INTERFACE);
            if (pluggable_interface != Spit.Transitions.CURRENT_INTERFACE) {
                warning("Unable to load transitions plug-in %s: reported interface %d",
                    Plugins.get_pluggable_module_id(pluggable), pluggable_interface);
                
                continue;
            }
            
            Spit.Transitions.Descriptor desc = (Spit.Transitions.Descriptor) pluggable;
            if (effects.has_key(desc.get_id()))
                warning("Multiple transitions loaded with same effect ID %s", desc.get_id());
            else
                effects.set(desc.get_id(), desc);
        }
    }
    
    public static void init() {
        instance = new TransitionEffectsManager();
    }
    
    public static void terminate() {
        instance = null;
    }
    
    public static TransitionEffectsManager get_instance() {
        assert(instance != null);
        
        return instance;
    }
    
    public Gee.Collection<string> get_effect_ids() {
        return effects.keys;
    }
    
    public Gee.Collection<string> get_effect_names(owned CompareDataFunc? comparator = null) {
        Gee.Collection<string> effect_names = new Gee.TreeSet<string>((owned) comparator);
        foreach (Spit.Transitions.Descriptor desc in effects.values)
            effect_names.add(desc.get_pluggable_name());
        
        return effect_names;
    }
    
    public string? get_id_for_effect_name(string effect_name) {
        foreach (Spit.Transitions.Descriptor desc in effects.values) {
            if (desc.get_pluggable_name() == effect_name)
                return desc.get_id();
        }
        
        return null;
    }
    
    public Spit.Transitions.Descriptor? get_effect_descriptor(string effect_id) {
        return effects.get(effect_id);
    }
    
    public string get_effect_name(string effect_id) {
        Spit.Transitions.Descriptor? desc = get_effect_descriptor(effect_id);
        
        return (desc != null) ? desc.get_pluggable_name() : _("(None)");
    }
    
    public Spit.Transitions.Descriptor get_null_descriptor() {
        return null_descriptor;
    }
    
    public TransitionClock? create_transition_clock(string effect_id) {
        Spit.Transitions.Descriptor? desc = get_effect_descriptor(effect_id);
        
        return (desc != null) ? new TransitionClock(desc) : null;
    }
    
    public TransitionClock create_null_transition_clock() {
        return new TransitionClock(null_descriptor);
    }
}

public class TransitionClock {
    // This method is called by TransitionClock to indicate that it's time for the transition to be
    // repainted.  The callback should call TransitionClock.paint() with the appropriate Drawable
    // either immediately or quite soon (in an expose event).
    public delegate void RepaintCallback();
    
    private Spit.Transitions.Descriptor desc;
    private Spit.Transitions.Effect effect;
    private int desired_fps;
    private int min_fps;
    private int current_fps = 0;
    private OpTimer paint_timer;
    private Spit.Transitions.Visuals? visuals = null;
    private Spit.Transitions.Motion? motion = null;
    private unowned RepaintCallback? repaint = null;
    private uint timer_id = 0;
    private ulong time_started = 0;
    private int frame_number = 0;
    private bool cancelled = false;
    
    public TransitionClock(Spit.Transitions.Descriptor desc) {
        this.desc = desc;
        
        effect = desc.create(new Plugins.StandardHostInterface(desc, "transitions"));
        effect.get_fps(out desired_fps, out min_fps);
        
        paint_timer = new OpTimer(desc.get_pluggable_name());
    }
    
    ~TransitionClock() {
        cancel_timer();
        debug("%s tick_msec=%d min/desired/current fps=%d/%d/%d", paint_timer.to_string(),
            (motion != null) ? motion.tick_msec : 0, min_fps, desired_fps, current_fps);
    }
    
    public bool is_in_progress() {
        return (!cancelled && motion != null) ? frame_number < motion.total_frames : false;
    }
    
    public void start(Spit.Transitions.Visuals visuals, Spit.Transitions.Direction direction,
        int duration_msec, RepaintCallback repaint) {
        reset();
        
        // if no desired FPS, this is a no-op transition
        if (desired_fps == 0)
            return;
        
        this.visuals = visuals;
        this.repaint = repaint;
        motion = new Spit.Transitions.Motion(direction, desired_fps, duration_msec);
        
        effect.start(visuals, motion);
        
        // start the timer
        // TODO: It may be smarter to not use Timeout naively, as it does not attempt to catch up
        // when tick() is called late.
        time_started = now_ms();
        timer_id = Timeout.add_full(Priority.HIGH, motion.tick_msec, tick);
    }
    
    // This resets all state for the clock.  No check is done if the clock is running.
    private void reset() {
        visuals = null;
        motion = null;
        repaint = null;
        cancel_timer();
        time_started = 0;
        frame_number = 1;
        current_fps = 0;
        cancelled = false;
    }
    
    private void cancel_timer() {
        if (timer_id != 0) {
            Source.remove(timer_id);
            timer_id = 0;
        }
    }
    
    // Calculate current FPS rate and returns true if it's above minimum
    private bool is_fps_ok() {
        assert(time_started > 0);
        
        if (frame_number <= 3) 
            return true; // don't bother measuring if statistical data are too small
        
        double elapsed_msec = (double) (now_ms() - time_started);
        if (elapsed_msec <= 0.0)
            return true;
        
        current_fps = (int) ((frame_number * 1000.0) / elapsed_msec);
        if (current_fps < min_fps) {
            debug("Transition rate of %dfps below minimum of %dfps (elapsed=%lf frames=%d)",
                current_fps, min_fps, elapsed_msec, frame_number);
        }
        
        return (current_fps >= min_fps);
    }
    
    // Cancels current transition.
    public void cancel() {
        cancelled = true;
        cancel_timer();
        effect.cancel();
        
        // repaint to complete the transition
        repaint();
    }
    
    // Call this whenever using a TransitionClock in the expose event.  Returns false if the
    // transition has completed, in which case the caller should paint the final result.
    public bool paint(Cairo.Context ctx, int width, int height) {
        if (!is_in_progress())
            return false;
        
        paint_timer.start();
        
        ctx.save();
        
        if (effect.needs_clear_background()) {
            ctx.set_source_rgba(visuals.bg_color.red, visuals.bg_color.green, visuals.bg_color.blue,
                visuals.bg_color.alpha);
            ctx.rectangle(0, 0, width, height);
            ctx.fill();
        }
        
        effect.paint(visuals, motion, ctx, width, height, frame_number);
        
        ctx.restore();
        
        paint_timer.stop();
        
        return true;
    }
    
    private bool tick() {
        if (!is_fps_ok()) {
            debug("Cancelling transition: below minimum fps");
            cancel();
        }
        
        // repaint always; this timer tick will go away when the frames have exhausted (and
        // guarantees the first frame is painted before advancing the counter)
        repaint();
        
        if (!is_in_progress()) {
            cancel_timer();
            
            return false;
        }
        
        // advance to the next frame
        if (frame_number < motion.total_frames)
            effect.advance(visuals, motion, ++frame_number);
        
        return true;
    }
}

public class NullTransitionDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor {
    public const string EFFECT_ID = "org.yorba.shotwell.transitions.null";
    
    public int get_pluggable_interface(int min_host_version, int max_host_version) {
        return Spit.Transitions.CURRENT_INTERFACE;
    }
    
    public unowned string get_id() {
        return EFFECT_ID;
    }
    
    public unowned string get_pluggable_name() {
        return _("None");
    }
    
    public void get_info(ref Spit.PluggableInfo info) {
    }
    
    public void activation(bool enabled) {
    }
    
    public Spit.Transitions.Effect create(Spit.HostInterface host) {
        return new NullEffect();
    }
}

public class NullEffect : Object, Spit.Transitions.Effect {
    public NullEffect() {
    }
    
    public void get_fps(out int desired_fps, out int min_fps) {
        desired_fps = 0;
        min_fps = 0;
    }
    
    public void start(Spit.Transitions.Visuals visuals, Spit.Transitions.Motion motion) {
    }
    
    public bool needs_clear_background() {
        return false;
    }
    
    public void paint(Spit.Transitions.Visuals visuals, Spit.Transitions.Motion motion, Cairo.Context ctx,
        int width, int height, int frame_number) {
    }
    
    public void advance(Spit.Transitions.Visuals visuals, Spit.Transitions.Motion motion, int frame_number) {
    }
    
    public void cancel() {
    }
}
public class RandomEffectDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor {
    public const string EFFECT_ID = "org.yorba.shotwell.transitions.random";

    public int get_pluggable_interface(int min_host_version, int max_host_version) {
        return Spit.Transitions.CURRENT_INTERFACE;
    }

    public unowned string get_id() {
        return EFFECT_ID;
    }
    
    public unowned string get_pluggable_name() {
        return _("Random");
    }

    public void get_info(ref Spit.PluggableInfo info) {
    }
    
    public void activation(bool enabled) {
    }

    public Spit.Transitions.Effect create(Spit.HostInterface host) {
        return new NullEffect();
    }
}