Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright © 2016 Red Hat, Inc.
4 : : * Copyright © 2019 Endless Mobile, Inc.
5 : : *
6 : : * This program is free software; you can redistribute it and/or modify
7 : : * it under the terms of the GNU General Public License as published by
8 : : * the Free Software Foundation; either version 2 of the License, or
9 : : * (at your option) any later version.
10 : : *
11 : : * This program is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : : * GNU General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU General Public License
17 : : * along with this program; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Authors:
20 : : * - Felipe Borges <felipeborges@gnome.org>
21 : : * - Philip Withnall <withnall@endlessm.com>
22 : : */
23 : :
24 : : #include <glib-object.h>
25 : : #include <gtk/gtk.h>
26 : :
27 : : #include "carousel.h"
28 : :
29 : :
30 : : #define ARROW_SIZE 20
31 : :
32 : : struct _MctCarouselItem {
33 : : GtkRadioButton parent;
34 : :
35 : : gint page;
36 : : };
37 : :
38 [ # # # # : 0 : G_DEFINE_TYPE (MctCarouselItem, mct_carousel_item, GTK_TYPE_RADIO_BUTTON)
# # ]
39 : :
40 : : GtkWidget *
41 : 0 : mct_carousel_item_new (void)
42 : : {
43 : 0 : return g_object_new (MCT_TYPE_CAROUSEL_ITEM, NULL);
44 : : }
45 : :
46 : : static void
47 : 0 : mct_carousel_item_class_init (MctCarouselItemClass *klass)
48 : : {
49 : 0 : }
50 : :
51 : : static void
52 : 0 : mct_carousel_item_init (MctCarouselItem *self)
53 : : {
54 : 0 : gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE);
55 : 0 : gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
56 : : "carousel-item");
57 : 0 : }
58 : :
59 : : struct _MctCarousel {
60 : : GtkRevealer parent;
61 : :
62 : : GList *children;
63 : : gint visible_page;
64 : : MctCarouselItem *selected_item;
65 : : GtkWidget *last_box;
66 : : GtkWidget *arrow;
67 : : gint arrow_start_x;
68 : :
69 : : /* Widgets */
70 : : GtkStack *stack;
71 : : GtkWidget *go_back_button;
72 : : GtkWidget *go_next_button;
73 : :
74 : : GtkStyleProvider *provider;
75 : : };
76 : :
77 [ # # # # : 0 : G_DEFINE_TYPE (MctCarousel, mct_carousel, GTK_TYPE_REVEALER)
# # ]
78 : :
79 : : enum {
80 : : ITEM_ACTIVATED,
81 : : NUM_SIGNALS
82 : : };
83 : :
84 : : static guint signals[NUM_SIGNALS] = { 0, };
85 : :
86 : : #define ITEMS_PER_PAGE 3
87 : :
88 : : static gint
89 : 0 : mct_carousel_item_get_x (MctCarouselItem *item,
90 : : MctCarousel *carousel)
91 : : {
92 : : GtkWidget *widget, *parent;
93 : : gint width;
94 : : gint dest_x;
95 : :
96 : 0 : parent = GTK_WIDGET (carousel->stack);
97 : 0 : widget = GTK_WIDGET (item);
98 : :
99 : 0 : width = gtk_widget_get_allocated_width (widget);
100 [ # # ]: 0 : if (!gtk_widget_translate_coordinates (widget,
101 : : parent,
102 : : width / 2,
103 : : 0,
104 : : &dest_x,
105 : : NULL))
106 : 0 : return 0;
107 : :
108 [ # # ]: 0 : return CLAMP (dest_x - ARROW_SIZE,
109 : : 0,
110 : : gtk_widget_get_allocated_width (parent));
111 : : }
112 : :
113 : : static void
114 : 0 : mct_carousel_move_arrow (MctCarousel *self)
115 : : {
116 : : GtkStyleContext *context;
117 : : gchar *css;
118 : : gint end_x;
119 : : GtkSettings *settings;
120 : : gboolean animations;
121 : :
122 [ # # ]: 0 : if (!self->selected_item)
123 : 0 : return;
124 : :
125 : 0 : end_x = mct_carousel_item_get_x (self->selected_item, self);
126 : :
127 : 0 : context = gtk_widget_get_style_context (self->arrow);
128 [ # # ]: 0 : if (self->provider)
129 : 0 : gtk_style_context_remove_provider (context, self->provider);
130 [ # # ]: 0 : g_clear_object (&self->provider);
131 : :
132 : 0 : settings = gtk_widget_get_settings (GTK_WIDGET (self));
133 : 0 : g_object_get (settings, "gtk-enable-animations", &animations, NULL);
134 : :
135 : : /* Animate the arrow movement if animations are enabled. Otherwise,
136 : : * jump the arrow to the right location instantly. */
137 [ # # ]: 0 : if (animations)
138 : : {
139 : 0 : css = g_strdup_printf ("@keyframes arrow_keyframes-%d-%d {\n"
140 : : " from { margin-left: %dpx; }\n"
141 : : " to { margin-left: %dpx; }\n"
142 : : "}\n"
143 : : "* {\n"
144 : : " animation-name: arrow_keyframes-%d-%d;\n"
145 : : "}\n",
146 : : self->arrow_start_x, end_x,
147 : : self->arrow_start_x, end_x,
148 : : self->arrow_start_x, end_x);
149 : : }
150 : : else
151 : : {
152 : 0 : css = g_strdup_printf ("* { margin-left: %dpx }", end_x);
153 : : }
154 : :
155 : 0 : self->provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
156 : 0 : gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (self->provider), css, -1, NULL);
157 : 0 : gtk_style_context_add_provider (context, self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
158 : :
159 : 0 : g_free (css);
160 : : }
161 : :
162 : : static gint
163 : 0 : get_last_page_number (MctCarousel *self)
164 : : {
165 [ # # ]: 0 : if (g_list_length (self->children) == 0)
166 : 0 : return 0;
167 : :
168 : 0 : return ((g_list_length (self->children) - 1) / ITEMS_PER_PAGE);
169 : : }
170 : :
171 : : static void
172 : 0 : update_buttons_visibility (MctCarousel *self)
173 : : {
174 : 0 : gtk_widget_set_visible (self->go_back_button, (self->visible_page > 0));
175 : 0 : gtk_widget_set_visible (self->go_next_button, (self->visible_page < get_last_page_number (self)));
176 : 0 : }
177 : :
178 : : /**
179 : : * mct_carousel_find_item:
180 : : * @carousel: an MctCarousel instance
181 : : * @data: user data passed to the comparison function
182 : : * @func: the function to call for each element.
183 : : * It should return 0 when the desired element is found
184 : : *
185 : : * Finds an MctCarousel item using the supplied function to find the
186 : : * desired element.
187 : : * Ideally useful for matching a model object and its correspondent
188 : : * widget.
189 : : *
190 : : * Returns: the found MctCarouselItem, or %NULL if it is not found
191 : : */
192 : : MctCarouselItem *
193 : 0 : mct_carousel_find_item (MctCarousel *self,
194 : : gconstpointer data,
195 : : GCompareFunc func)
196 : : {
197 : : GList *list;
198 : :
199 : 0 : list = self->children;
200 [ # # ]: 0 : while (list != NULL)
201 : : {
202 [ # # ]: 0 : if (!func (list->data, data))
203 : 0 : return list->data;
204 : 0 : list = list->next;
205 : : }
206 : :
207 : 0 : return NULL;
208 : : }
209 : :
210 : : static void
211 : 0 : on_item_toggled (MctCarouselItem *item,
212 : : GdkEvent *event,
213 : : gpointer user_data)
214 : : {
215 : 0 : MctCarousel *self = MCT_CAROUSEL (user_data);
216 : :
217 : 0 : mct_carousel_select_item (self, item);
218 : 0 : }
219 : :
220 : : void
221 : 0 : mct_carousel_select_item (MctCarousel *self,
222 : : MctCarouselItem *item)
223 : : {
224 : : gchar *page_name;
225 : 0 : gboolean page_changed = TRUE;
226 : :
227 : : /* Select first user if none is specified */
228 [ # # ]: 0 : if (item == NULL)
229 : : {
230 [ # # ]: 0 : if (self->children != NULL)
231 : 0 : item = self->children->data;
232 : : else
233 : 0 : return;
234 : : }
235 : :
236 [ # # ]: 0 : if (self->selected_item != NULL)
237 : : {
238 : 0 : page_changed = (self->selected_item->page != item->page);
239 : 0 : self->arrow_start_x = mct_carousel_item_get_x (self->selected_item, self);
240 : : }
241 : :
242 : 0 : self->selected_item = item;
243 : 0 : self->visible_page = item->page;
244 : 0 : g_signal_emit (self, signals[ITEM_ACTIVATED], 0, item);
245 : :
246 [ # # ]: 0 : if (!page_changed)
247 : : {
248 : 0 : mct_carousel_move_arrow (self);
249 : 0 : return;
250 : : }
251 : :
252 : 0 : page_name = g_strdup_printf ("%d", self->visible_page);
253 : 0 : gtk_stack_set_visible_child_name (self->stack, page_name);
254 : :
255 : 0 : g_free (page_name);
256 : :
257 : 0 : update_buttons_visibility (self);
258 : :
259 : : /* mct_carousel_move_arrow is called from on_transition_running */
260 : : }
261 : :
262 : : static void
263 : 0 : mct_carousel_select_item_at_index (MctCarousel *self,
264 : : gint index)
265 : : {
266 : 0 : GList *l = NULL;
267 : :
268 : 0 : l = g_list_nth (self->children, index);
269 : 0 : mct_carousel_select_item (self, l->data);
270 : 0 : }
271 : :
272 : : static void
273 : 0 : mct_carousel_goto_previous_page (GtkWidget *button,
274 : : gpointer user_data)
275 : : {
276 : 0 : MctCarousel *self = MCT_CAROUSEL (user_data);
277 : :
278 : 0 : self->visible_page--;
279 [ # # ]: 0 : if (self->visible_page < 0)
280 : 0 : self->visible_page = 0;
281 : :
282 : : /* Select first item of the page */
283 : 0 : mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE);
284 : 0 : }
285 : :
286 : : static void
287 : 0 : mct_carousel_goto_next_page (GtkWidget *button,
288 : : gpointer user_data)
289 : : {
290 : 0 : MctCarousel *self = MCT_CAROUSEL (user_data);
291 : : gint last_page;
292 : :
293 : 0 : last_page = get_last_page_number (self);
294 : :
295 : 0 : self->visible_page++;
296 [ # # ]: 0 : if (self->visible_page > last_page)
297 : 0 : self->visible_page = last_page;
298 : :
299 : : /* Select first item of the page */
300 : 0 : mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE);
301 : 0 : }
302 : :
303 : : static void
304 : 0 : mct_carousel_add (GtkContainer *container,
305 : : GtkWidget *widget)
306 : : {
307 : 0 : MctCarousel *self = MCT_CAROUSEL (container);
308 : : gboolean last_box_is_full;
309 : :
310 [ # # ]: 0 : if (!MCT_IS_CAROUSEL_ITEM (widget))
311 : : {
312 : 0 : GTK_CONTAINER_CLASS (mct_carousel_parent_class)->add (container, widget);
313 : 0 : return;
314 : : }
315 : :
316 : 0 : gtk_style_context_add_class (gtk_widget_get_style_context (widget), "menu");
317 : 0 : gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE);
318 : :
319 : 0 : self->children = g_list_append (self->children, widget);
320 : 0 : MCT_CAROUSEL_ITEM (widget)->page = get_last_page_number (self);
321 [ # # ]: 0 : if (self->selected_item != NULL)
322 : 0 : gtk_radio_button_join_group (GTK_RADIO_BUTTON (widget), GTK_RADIO_BUTTON (self->selected_item));
323 : 0 : g_signal_connect (widget, "button-press-event", G_CALLBACK (on_item_toggled), self);
324 : :
325 : 0 : last_box_is_full = ((g_list_length (self->children) - 1) % ITEMS_PER_PAGE == 0);
326 [ # # ]: 0 : if (last_box_is_full)
327 : : {
328 : 0 : g_autofree gchar *page = NULL;
329 : :
330 : 0 : page = g_strdup_printf ("%d", MCT_CAROUSEL_ITEM (widget)->page);
331 : 0 : self->last_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
332 : 0 : gtk_widget_show (self->last_box);
333 : 0 : gtk_widget_set_valign (self->last_box, GTK_ALIGN_CENTER);
334 : 0 : gtk_stack_add_named (self->stack, self->last_box, page);
335 : : }
336 : :
337 : 0 : gtk_widget_show_all (widget);
338 : 0 : gtk_box_pack_start (GTK_BOX (self->last_box), widget, TRUE, FALSE, 10);
339 : :
340 : 0 : update_buttons_visibility (self);
341 : : }
342 : :
343 : : static void
344 : 0 : destroy_widget_cb (GtkWidget *widget,
345 : : gpointer user_data)
346 : : {
347 : 0 : gtk_widget_destroy (widget);
348 : 0 : }
349 : :
350 : : void
351 : 0 : mct_carousel_purge_items (MctCarousel *self)
352 : : {
353 : 0 : gtk_container_forall (GTK_CONTAINER (self->stack),
354 : : destroy_widget_cb,
355 : : NULL);
356 : :
357 : 0 : g_list_free (self->children);
358 : 0 : self->children = NULL;
359 : 0 : self->visible_page = 0;
360 : 0 : self->selected_item = NULL;
361 : 0 : }
362 : :
363 : : MctCarousel *
364 : 0 : mct_carousel_new (void)
365 : : {
366 : 0 : return g_object_new (MCT_TYPE_CAROUSEL, NULL);
367 : : }
368 : :
369 : : static void
370 : 0 : mct_carousel_dispose (GObject *object)
371 : : {
372 : 0 : MctCarousel *self = MCT_CAROUSEL (object);
373 : :
374 [ # # ]: 0 : g_clear_object (&self->provider);
375 [ # # ]: 0 : if (self->children != NULL)
376 : : {
377 : 0 : g_list_free (self->children);
378 : 0 : self->children = NULL;
379 : : }
380 : :
381 : 0 : G_OBJECT_CLASS (mct_carousel_parent_class)->dispose (object);
382 : 0 : }
383 : :
384 : : static void
385 : 0 : mct_carousel_class_init (MctCarouselClass *klass)
386 : : {
387 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
388 : 0 : GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
389 : 0 : GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
390 : :
391 : 0 : gtk_widget_class_set_template_from_resource (wclass,
392 : : "/org/freedesktop/MalcontentControl/ui/carousel.ui");
393 : :
394 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, stack);
395 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, go_back_button);
396 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, go_next_button);
397 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, arrow);
398 : :
399 : 0 : gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_previous_page);
400 : 0 : gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_next_page);
401 : :
402 : 0 : object_class->dispose = mct_carousel_dispose;
403 : :
404 : 0 : container_class->add = mct_carousel_add;
405 : :
406 : 0 : signals[ITEM_ACTIVATED] =
407 : 0 : g_signal_new ("item-activated",
408 : : MCT_TYPE_CAROUSEL,
409 : : G_SIGNAL_RUN_LAST,
410 : : 0,
411 : : NULL, NULL,
412 : : g_cclosure_marshal_VOID__OBJECT,
413 : : G_TYPE_NONE, 1,
414 : : MCT_TYPE_CAROUSEL_ITEM);
415 : 0 : }
416 : :
417 : : static void
418 : 0 : on_size_allocate (MctCarousel *self)
419 : : {
420 [ # # ]: 0 : if (self->selected_item == NULL)
421 : 0 : return;
422 : :
423 [ # # ]: 0 : if (gtk_stack_get_transition_running (self->stack))
424 : 0 : return;
425 : :
426 : 0 : self->arrow_start_x = mct_carousel_item_get_x (self->selected_item, self);
427 : 0 : mct_carousel_move_arrow (self);
428 : : }
429 : :
430 : : static void
431 : 0 : on_transition_running (MctCarousel *self)
432 : : {
433 [ # # ]: 0 : if (!gtk_stack_get_transition_running (self->stack))
434 : 0 : mct_carousel_move_arrow (self);
435 : 0 : }
436 : :
437 : : static void
438 : 0 : mct_carousel_init (MctCarousel *self)
439 : : {
440 : : GtkStyleProvider *provider;
441 : :
442 : 0 : gtk_widget_init_template (GTK_WIDGET (self));
443 : :
444 : 0 : provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
445 : 0 : gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (provider),
446 : : "/org/freedesktop/MalcontentControl/ui/carousel.css");
447 : :
448 : 0 : gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
449 : : provider,
450 : : GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
451 : :
452 : 0 : g_object_unref (provider);
453 : :
454 : 0 : g_signal_connect_swapped (self->stack, "size-allocate", G_CALLBACK (on_size_allocate), self);
455 : 0 : g_signal_connect_swapped (self->stack, "notify::transition-running", G_CALLBACK (on_transition_running), self);
456 : 0 : }
457 : :
458 : : guint
459 : 0 : mct_carousel_get_item_count (MctCarousel *self)
460 : : {
461 : 0 : return g_list_length (self->children);
462 : : }
|