summaryrefslogtreecommitdiff
path: root/src/slideshow
diff options
context:
space:
mode:
Diffstat (limited to 'src/slideshow')
-rw-r--r--src/slideshow/Slideshow.vala32
-rw-r--r--src/slideshow/TransitionEffects.vala351
-rw-r--r--src/slideshow/mk/slideshow.mk29
3 files changed, 412 insertions, 0 deletions
diff --git a/src/slideshow/Slideshow.vala b/src/slideshow/Slideshow.vala
new file mode 100644
index 0000000..d55e3d6
--- /dev/null
+++ b/src/slideshow/Slideshow.vala
@@ -0,0 +1,32 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace Slideshow {
+
+public void init() throws Error {
+ string[] core_ids = new string[0];
+ core_ids += "org.yorba.shotwell.transitions.crumble";
+ core_ids += "org.yorba.shotwell.transitions.fade";
+ core_ids += "org.yorba.shotwell.transitions.slide";
+ core_ids += "org.yorba.shotwell.transitions.blinds";
+ core_ids += "org.yorba.shotwell.transitions.circle";
+ core_ids += "org.yorba.shotwell.transitions.circles";
+ core_ids += "org.yorba.shotwell.transitions.clock";
+ core_ids += "org.yorba.shotwell.transitions.stripes";
+ core_ids += "org.yorba.shotwell.transitions.squares";
+ core_ids += "org.yorba.shotwell.transitions.chess";
+
+ Plugins.register_extension_point(typeof(Spit.Transitions.Descriptor), _("Slideshow Transitions"),
+ Resources.ICON_SLIDESHOW_EXTENSION_POINT, core_ids);
+ TransitionEffectsManager.init();
+}
+
+public void terminate() {
+ TransitionEffectsManager.terminate();
+}
+
+}
+
diff --git a/src/slideshow/TransitionEffects.vala b/src/slideshow/TransitionEffects.vala
new file mode 100644
index 0000000..7b10543
--- /dev/null
+++ b/src/slideshow/TransitionEffects.vala
@@ -0,0 +1,351 @@
+/* Copyright 2010 Maxim Kartashev
+ * Copyright 2011-2014 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();
+ }
+}
diff --git a/src/slideshow/mk/slideshow.mk b/src/slideshow/mk/slideshow.mk
new file mode 100644
index 0000000..0a62e8d
--- /dev/null
+++ b/src/slideshow/mk/slideshow.mk
@@ -0,0 +1,29 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Slideshow
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := slideshow
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. Slideshow.vala.
+UNIT_FILES := \
+ TransitionEffects.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES := \
+ Plugins
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+