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