1 /*
2  * Copyright © 2009-2018 Siyan Panayotov <contact@siyanpanayotov.com>
3  *
4  *
5  * Based on code by (see README for details):
6  * - Björn Lindqvist <bjourne@gmail.com>
7  *
8  * This file is part of Viewnior.
9  *
10  * Viewnior is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * Viewnior is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with Viewnior.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include <glib.h>
25 #include <gdk/gdkkeysyms.h>
26 #include "uni-anim-view.h"
27 
28 /*************************************************************/
29 /***** Private data ******************************************/
30 /*************************************************************/
31 enum {
32     TOGGLE_RUNNING,
33     STEP,
34     LAST_SIGNAL
35 };
36 
37 static guint uni_anim_view_signals[LAST_SIGNAL] = { 0 };
38 
39 G_DEFINE_TYPE (UniAnimView, uni_anim_view, UNI_TYPE_IMAGE_VIEW);
40 
41 /*************************************************************/
42 /***** Static stuff ******************************************/
43 /*************************************************************/
44 
45 static gboolean
uni_anim_view_updator(gpointer data)46 uni_anim_view_updator (gpointer data)
47 {
48     UniAnimView *aview = (UniAnimView *) data;
49 
50     // Workaround for #437791.
51     glong delay_us = aview->delay * 1000;
52     if (aview->delay == 20)
53     {
54         // If the delay time is 20 ms, the GIF is a "fast player." and
55         // we increase it to a more reasonable 100 ms so that the
56         // frame is only updated 1/5 of the times
57         // uni_anim_view_updator() is run.
58         delay_us = 200;
59     }
60     g_time_val_add (&aview->time, delay_us);
61 
62     gboolean next = gdk_pixbuf_animation_iter_advance (aview->iter,
63                                                        &aview->time);
64     uni_anim_view_set_is_playing (aview, FALSE);
65 
66     aview->delay = gdk_pixbuf_animation_iter_get_delay_time (aview->iter);
67     aview->timer_id = g_timeout_add (aview->delay,
68                                      uni_anim_view_updator, aview);
69 
70     if (!next)
71         return FALSE;
72 
73     GdkPixbuf *pixbuf = gdk_pixbuf_animation_iter_get_pixbuf (aview->iter);
74     uni_image_view_set_pixbuf (UNI_IMAGE_VIEW (aview), pixbuf, FALSE);
75 
76     return FALSE;
77 }
78 
79 /*************************************************************/
80 /***** Private signal handlers *******************************/
81 /*************************************************************/
82 static void
uni_anim_view_toggle_running(UniAnimView * aview)83 uni_anim_view_toggle_running (UniAnimView * aview)
84 {
85     uni_anim_view_set_is_playing (aview, !aview->timer_id);
86 }
87 
88 /* Steps the animation one frame forward. If the animation is playing
89  * it will be stopped. Will it wrap around if the animation is at its
90  * last frame?
91  **/
92 static void
uni_anim_view_step(UniAnimView * aview)93 uni_anim_view_step (UniAnimView * aview)
94 {
95     if (aview->anim)
96     {
97         /* Part of workaround for #437791. uni_anim_view_updator()
98          * might not always immidiately step to the next frame, so we
99          * loop until the frame is changed.
100          *
101          * If we are on the last frame, it will not wrap around so the
102          * frame will never change. So we risk an infinite loop.
103          * Unfortunately but expectedly, GdkPixbufAnimationIter
104          * doesn't provide a way to check if we
105          * are on the last frame because the API is totally brain
106          * damaged. The work-around is to give uni_anim_view_updator
107          * exactly 10 chances to advance the frame before bailing out.
108          * */
109         int n = 0;
110         GdkPixbuf *old = gdk_pixbuf_animation_iter_get_pixbuf (aview->iter);
111         while ((gdk_pixbuf_animation_iter_get_pixbuf (aview->iter) == old)
112                && (n < 10))
113         {
114             uni_anim_view_updator (aview);
115             n++;
116         }
117     }
118     uni_anim_view_set_is_playing (aview, FALSE);
119 }
120 
121 /*************************************************************/
122 /***** Stuff that deals with the type ************************/
123 /*************************************************************/
124 static void
uni_anim_view_init(UniAnimView * aview)125 uni_anim_view_init (UniAnimView * aview)
126 {
127     aview->anim = NULL;
128     aview->iter = NULL;
129     aview->timer_id = 0;
130 }
131 
132 static void
uni_anim_view_finalize(GObject * object)133 uni_anim_view_finalize (GObject * object)
134 {
135     uni_anim_view_set_is_playing (UNI_ANIM_VIEW (object), FALSE);
136 
137     /* Chain up. */
138     G_OBJECT_CLASS (uni_anim_view_parent_class)->finalize (object);
139 }
140 
141 static void
uni_anim_view_init_signals(UniAnimViewClass * klass)142 uni_anim_view_init_signals (UniAnimViewClass * klass)
143 {
144     /**
145      * UniAnimView::toggle-running:
146      * @aview: a #UniAnimView
147      *
148      * Stops the animation if it was playing or resumes it, if it was
149      * playing. ::toggle-running is a keybinding signal emitted when
150      * %GDK_KEY_p is pressed on the widget and should not be used by
151      * clients of this library.
152      **/
153     uni_anim_view_signals[TOGGLE_RUNNING] =
154         g_signal_new ("toggle_running",
155                       G_TYPE_FROM_CLASS (klass),
156                       G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
157                       G_STRUCT_OFFSET (UniAnimViewClass, toggle_running),
158                       NULL, NULL,
159                       g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
160     /**
161      * UniAnimView::step:
162      * @aview: a #UniAnimView
163      *
164      * Steps the animation one frame forward. If the animation is
165      * playing it will first be stopped. ::step is a keybinding signal
166      * emitted when %GDK_KEY_j is pressed on the widget and should not be
167      * used by clients of this library.
168      **/
169     uni_anim_view_signals[STEP] =
170         g_signal_new ("step",
171                       G_TYPE_FROM_CLASS (klass),
172                       G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
173                       G_STRUCT_OFFSET (UniAnimViewClass, step),
174                       NULL, NULL,
175                       g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
176 }
177 
178 static void
uni_anim_view_class_init(UniAnimViewClass * klass)179 uni_anim_view_class_init (UniAnimViewClass * klass)
180 {
181     uni_anim_view_init_signals (klass);
182 
183     GObjectClass *object_class = G_OBJECT_CLASS (klass);
184     object_class->finalize = uni_anim_view_finalize;
185 
186     klass->toggle_running = uni_anim_view_toggle_running;
187     klass->step = uni_anim_view_step;
188 
189     /* Add keybindings. */
190     GtkBindingSet *binding_set = gtk_binding_set_by_class (klass);
191 
192     /* Stop */
193     gtk_binding_entry_add_signal (binding_set, GDK_KEY_p, 0, "toggle_running", 0);
194 
195     /* Step */
196     gtk_binding_entry_add_signal (binding_set, GDK_KEY_j, 0, "step", 0);
197 }
198 
199 /**
200  * uni_anim_view_new:
201  * @returns: A new #UniAnimView.
202  *
203  * Creates a new #UniAnimView with default values.
204  **/
205 GtkWidget *
uni_anim_view_new(void)206 uni_anim_view_new (void)
207 {
208     GtkWidget *aview = g_object_new (UNI_TYPE_ANIM_VIEW, NULL);
209     return aview;
210 }
211 
212 
213 /*************************************************************/
214 /***** Read-write properties *********************************/
215 /*************************************************************/
216 /**
217  * uni_anim_view_set_anim:
218  * @aview: A #UniAnimView.
219  * @anim: A pixbuf animation to play.
220  *
221  * Sets the pixbuf animation to play, or %NULL to not play any
222  * animation.
223  *
224  * If the animation is a static image or only has one frame, then the
225  * static image will be displayed instead. If more frames are loaded
226  * into the animation, then #UniAnimView will automatically animate to
227  * those frames.
228  *
229  * The effect of this method is analoguous to
230  * uni_image_view_set_pixbuf(). Fit mode is reset to
231  * %GTK_FIT_SIZE_IF_LARGER so that the whole area of the animation
232  * fits in the view. Three signals are emitted, first the
233  * #UniImageView will emit ::zoom-changed and then ::pixbuf-changed,
234  * second, #UniAnimView itself will emit ::anim-changed.
235  *
236  * The default pixbuf animation is %NULL.
237  **/
238 
239 /* Return TRUE if anim is a static image */
240 gboolean
uni_anim_view_set_anim(UniAnimView * aview,GdkPixbufAnimation * anim)241 uni_anim_view_set_anim (UniAnimView * aview, GdkPixbufAnimation * anim)
242 {
243     gboolean is_static;
244 
245     if (aview->anim)
246         g_object_unref (aview->anim);
247     aview->anim = anim;
248 
249     if (!anim)
250     {
251         uni_anim_view_set_is_playing (aview, FALSE);
252         uni_image_view_set_pixbuf (UNI_IMAGE_VIEW (aview), NULL, TRUE);
253         return TRUE;
254     }
255 
256     g_object_ref (aview->anim);
257     if (aview->iter)
258         g_object_unref (aview->iter);
259 
260     g_get_current_time (&aview->time);
261     aview->iter = gdk_pixbuf_animation_get_iter (aview->anim, &aview->time);
262 
263     GdkPixbuf *pixbuf;
264 
265     is_static = gdk_pixbuf_animation_is_static_image (anim);
266 
267     if (is_static)
268     {
269         pixbuf = gdk_pixbuf_animation_get_static_image (anim);
270     }
271     else
272     {
273         pixbuf = gdk_pixbuf_animation_iter_get_pixbuf (aview->iter);
274     }
275 
276     uni_image_view_set_pixbuf (UNI_IMAGE_VIEW (aview), pixbuf, TRUE);
277 
278     uni_anim_view_set_is_playing (aview, FALSE);
279     aview->delay = gdk_pixbuf_animation_iter_get_delay_time (aview->iter);
280 
281     if(!is_static)
282         aview->timer_id = g_timeout_add (aview->delay,
283                                          uni_anim_view_updator, aview);
284     return is_static;
285 }
286 
287 
288 /* No conversion from GdkPixbuf to GdkPixbufAnim can be made
289  * directly using the current API, so this makes a static
290  * GdkPixbufAnimation and updates the UniAnimView */
291 void
uni_anim_view_set_static(UniAnimView * aview,GdkPixbuf * pixbuf)292 uni_anim_view_set_static (UniAnimView * aview, GdkPixbuf * pixbuf)
293 {
294     GdkPixbufSimpleAnim *s_anim;
295 
296     s_anim = gdk_pixbuf_simple_anim_new (gdk_pixbuf_get_width(pixbuf),
297                                          gdk_pixbuf_get_height(pixbuf),
298                                          -1);
299     gdk_pixbuf_simple_anim_add_frame(s_anim, pixbuf);
300 
301     /* Simple version of uni_anim_view_set_anim */
302     if (aview->anim)
303         g_object_unref (aview->anim);
304 
305     aview->anim = (GdkPixbufAnimation*)s_anim;
306 
307     g_object_ref (aview->anim);
308     if (aview->iter)
309         g_object_unref (aview->iter);
310 
311     uni_image_view_set_pixbuf (UNI_IMAGE_VIEW (aview), pixbuf, TRUE);
312     uni_anim_view_set_is_playing (aview, FALSE);
313     aview->delay = -1;
314     aview->iter = NULL;
315 
316     g_object_unref(pixbuf);
317 }
318 
319 
320 /**
321  * uni_anim_view_set_is_playing:
322  * @aview: a #UniImageView
323  * @playing: %TRUE to play the animation, %FALSE otherwise
324  *
325  * Sets whether the animation should play or not. If there is no
326  * current animation this method does not have any effect.
327  **/
328 void
uni_anim_view_set_is_playing(UniAnimView * aview,gboolean playing)329 uni_anim_view_set_is_playing (UniAnimView * aview, gboolean playing)
330 {
331     if (!playing && aview->timer_id)
332     {
333         /* Commanded to stop AND the animation is playing. */
334         g_source_remove (aview->timer_id);
335         aview->timer_id = 0;
336     }
337     else if (playing && aview->anim)
338         uni_anim_view_updator (aview);
339 }
340 
341 /**
342  * uni_anim_view_get_is_playing:
343  * @aview: A #UniImageView.
344  * @returns: %TRUE if an animation is playing, %FALSE otherwise.
345  *
346  * Returns whether the animation is playing or not. If there is no
347  * current animation, this method will always returns %FALSE.
348  **/
349 gboolean
uni_anim_view_get_is_playing(UniAnimView * aview)350 uni_anim_view_get_is_playing (UniAnimView * aview)
351 {
352     return aview->timer_id && aview->anim;
353 }
354