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 : : * - Georges Basile Stavracas Neto <georges@endlessos.org>
22 : : * - Philip Withnall <withnall@endlessm.com>
23 : : */
24 : :
25 : : #include <glib-object.h>
26 : : #include <gtk/gtk.h>
27 : :
28 : : #include "carousel.h"
29 : :
30 : :
31 : : #define ARROW_SIZE 20
32 : :
33 : : #define MCT_TYPE_CAROUSEL_LAYOUT (mct_carousel_layout_get_type ())
34 : : G_DECLARE_FINAL_TYPE (MctCarouselLayout, mct_carousel_layout, MCT, CAROUSEL_LAYOUT, GtkLayoutManager)
35 : :
36 : : struct _MctCarouselItem {
37 : : GtkButton parent;
38 : :
39 : : gint page;
40 : : };
41 : :
42 [ # # # # : 0 : G_DEFINE_TYPE (MctCarouselItem, mct_carousel_item, GTK_TYPE_BUTTON)
# # ]
43 : :
44 : : GtkWidget *
45 : 0 : mct_carousel_item_new (void)
46 : : {
47 : 0 : return g_object_new (MCT_TYPE_CAROUSEL_ITEM, NULL);
48 : : }
49 : :
50 : : void
51 : 0 : mct_carousel_item_set_child (MctCarouselItem *self,
52 : : GtkWidget *child)
53 : : {
54 : 0 : g_return_if_fail (MCT_IS_CAROUSEL_ITEM (self));
55 : :
56 : 0 : gtk_button_set_child (GTK_BUTTON (self), child);
57 : : }
58 : :
59 : : static void
60 : 0 : mct_carousel_item_class_init (MctCarouselItemClass *klass)
61 : : {
62 : 0 : gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "carousel-item");
63 : 0 : }
64 : :
65 : : static void
66 : 0 : mct_carousel_item_init (MctCarouselItem *self)
67 : : {
68 : 0 : }
69 : :
70 : : struct _MctCarousel {
71 : : AdwBin parent;
72 : :
73 : : GtkRevealer *revealer;
74 : :
75 : : GList *children;
76 : : gint visible_page;
77 : : MctCarouselItem *selected_item;
78 : : GtkWidget *last_box;
79 : : GtkWidget *arrow;
80 : :
81 : : /* Widgets */
82 : : GtkStack *stack;
83 : : GtkWidget *go_back_button;
84 : : GtkWidget *go_next_button;
85 : :
86 : : GtkStyleProvider *provider;
87 : : };
88 : :
89 [ # # # # : 0 : G_DEFINE_TYPE (MctCarousel, mct_carousel, ADW_TYPE_BIN)
# # ]
90 : :
91 : : enum {
92 : : ITEM_ACTIVATED,
93 : : NUM_SIGNALS
94 : : };
95 : :
96 : : static guint signals[NUM_SIGNALS] = { 0, };
97 : :
98 : : #define ITEMS_PER_PAGE 3
99 : :
100 : : static gint
101 : 0 : mct_carousel_item_get_x (MctCarouselItem *item,
102 : : MctCarousel *carousel)
103 : : {
104 : : GtkWidget *widget, *parent;
105 : : gint width;
106 : : gdouble dest_x;
107 : : graphene_point_t p;
108 : :
109 : 0 : parent = GTK_WIDGET (carousel->revealer);
110 : 0 : widget = GTK_WIDGET (item);
111 : :
112 : 0 : width = gtk_widget_get_width (widget);
113 [ # # ]: 0 : if (!gtk_widget_compute_point (widget, parent,
114 : 0 : &GRAPHENE_POINT_INIT (width / 2, 0),
115 : : &p))
116 : 0 : return 0;
117 : :
118 : 0 : dest_x = p.x;
119 : :
120 [ # # # # ]: 0 : return CLAMP (dest_x - ARROW_SIZE,
121 : : 0,
122 : : gtk_widget_get_width (parent));
123 : : }
124 : :
125 : : static void
126 : 0 : mct_carousel_move_arrow (MctCarousel *self)
127 : : {
128 : : gchar *css;
129 : : gint end_x;
130 : :
131 [ # # ]: 0 : if (!self->selected_item)
132 : 0 : return;
133 : :
134 : 0 : end_x = mct_carousel_item_get_x (self->selected_item, self);
135 : :
136 [ # # ]: 0 : if (self->provider)
137 : 0 : gtk_style_context_remove_provider_for_display (gtk_widget_get_display (self->arrow), self->provider);
138 [ # # ]: 0 : g_clear_object (&self->provider);
139 : :
140 : 0 : css = g_strdup_printf (".carousel-arrow { margin-left: %dpx; }", end_x);
141 : 0 : self->provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
142 : 0 : gtk_css_provider_load_from_string (GTK_CSS_PROVIDER (self->provider), css);
143 : 0 : gtk_style_context_add_provider_for_display (gtk_widget_get_display (self->arrow),
144 : : self->provider,
145 : : GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
146 : :
147 : 0 : g_free (css);
148 : : }
149 : :
150 : : static gint
151 : 0 : get_last_page_number (MctCarousel *self)
152 : : {
153 [ # # ]: 0 : if (g_list_length (self->children) == 0)
154 : 0 : return 0;
155 : :
156 : 0 : return ((g_list_length (self->children) - 1) / ITEMS_PER_PAGE);
157 : : }
158 : :
159 : : static void
160 : 0 : update_buttons_visibility (MctCarousel *self)
161 : : {
162 : 0 : gtk_widget_set_visible (self->go_back_button, (self->visible_page > 0));
163 : 0 : gtk_widget_set_visible (self->go_next_button, (self->visible_page < get_last_page_number (self)));
164 : 0 : }
165 : :
166 : : /**
167 : : * mct_carousel_find_item:
168 : : * @carousel: an MctCarousel instance
169 : : * @data: user data passed to the comparison function
170 : : * @func: the function to call for each element.
171 : : * It should return 0 when the desired element is found
172 : : *
173 : : * Finds an MctCarousel item using the supplied function to find the
174 : : * desired element.
175 : : * Ideally useful for matching a model object and its correspondent
176 : : * widget.
177 : : *
178 : : * Returns: the found MctCarouselItem, or %NULL if it is not found
179 : : */
180 : : MctCarouselItem *
181 : 0 : mct_carousel_find_item (MctCarousel *self,
182 : : gconstpointer data,
183 : : GCompareFunc func)
184 : : {
185 : : GList *list;
186 : :
187 : 0 : list = self->children;
188 [ # # ]: 0 : while (list != NULL)
189 : : {
190 [ # # ]: 0 : if (!func (list->data, data))
191 : 0 : return list->data;
192 : 0 : list = list->next;
193 : : }
194 : :
195 : 0 : return NULL;
196 : : }
197 : :
198 : : static void
199 : 0 : on_item_toggled (MctCarouselItem *item,
200 : : GdkEvent *event,
201 : : gpointer user_data)
202 : : {
203 : 0 : MctCarousel *self = MCT_CAROUSEL (user_data);
204 : :
205 : 0 : mct_carousel_select_item (self, item);
206 : 0 : }
207 : :
208 : : void
209 : 0 : mct_carousel_select_item (MctCarousel *self,
210 : : MctCarouselItem *item)
211 : : {
212 : : gchar *page_name;
213 : 0 : gboolean page_changed = TRUE;
214 : :
215 : : /* Select first user if none is specified */
216 [ # # ]: 0 : if (item == NULL)
217 : : {
218 [ # # ]: 0 : if (self->children != NULL)
219 : 0 : item = self->children->data;
220 : : else
221 : 0 : return;
222 : : }
223 : :
224 [ # # ]: 0 : if (self->selected_item != NULL)
225 : 0 : page_changed = (self->selected_item->page != item->page);
226 : :
227 : 0 : self->selected_item = item;
228 : 0 : self->visible_page = item->page;
229 : 0 : g_signal_emit (self, signals[ITEM_ACTIVATED], 0, item);
230 : :
231 [ # # ]: 0 : if (!page_changed)
232 : : {
233 : 0 : mct_carousel_move_arrow (self);
234 : 0 : return;
235 : : }
236 : :
237 : 0 : page_name = g_strdup_printf ("%d", self->visible_page);
238 : 0 : gtk_stack_set_visible_child_name (self->stack, page_name);
239 : :
240 : 0 : g_free (page_name);
241 : :
242 : 0 : update_buttons_visibility (self);
243 : :
244 : : /* mct_carousel_move_arrow is called from on_transition_running */
245 : : }
246 : :
247 : : static void
248 : 0 : mct_carousel_select_item_at_index (MctCarousel *self,
249 : : gint index)
250 : : {
251 : 0 : GList *l = NULL;
252 : :
253 : 0 : l = g_list_nth (self->children, index);
254 : 0 : mct_carousel_select_item (self, l->data);
255 : 0 : }
256 : :
257 : : static void
258 : 0 : mct_carousel_goto_previous_page (GtkWidget *button,
259 : : gpointer user_data)
260 : : {
261 : 0 : MctCarousel *self = MCT_CAROUSEL (user_data);
262 : :
263 : 0 : self->visible_page--;
264 [ # # ]: 0 : if (self->visible_page < 0)
265 : 0 : self->visible_page = 0;
266 : :
267 : : /* Select first item of the page */
268 : 0 : mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE);
269 : 0 : }
270 : :
271 : : static void
272 : 0 : mct_carousel_goto_next_page (GtkWidget *button,
273 : : gpointer user_data)
274 : : {
275 : 0 : MctCarousel *self = MCT_CAROUSEL (user_data);
276 : : gint last_page;
277 : :
278 : 0 : last_page = get_last_page_number (self);
279 : :
280 : 0 : self->visible_page++;
281 [ # # ]: 0 : if (self->visible_page > last_page)
282 : 0 : self->visible_page = last_page;
283 : :
284 : : /* Select first item of the page */
285 : 0 : mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE);
286 : 0 : }
287 : :
288 : : void
289 : 0 : mct_carousel_add (MctCarousel *self,
290 : : MctCarouselItem *item)
291 : : {
292 : : gboolean last_box_is_full;
293 : :
294 : 0 : g_return_if_fail (MCT_IS_CAROUSEL (self));
295 : 0 : g_return_if_fail (MCT_IS_CAROUSEL_ITEM (item));
296 : :
297 : 0 : self->children = g_list_append (self->children, item);
298 : 0 : item->page = get_last_page_number (self);
299 : 0 : g_signal_connect (item, "clicked", G_CALLBACK (on_item_toggled), self);
300 : :
301 : 0 : last_box_is_full = ((g_list_length (self->children) - 1) % ITEMS_PER_PAGE == 0);
302 [ # # ]: 0 : if (last_box_is_full)
303 : : {
304 : 0 : g_autofree gchar *page = NULL;
305 : :
306 : 0 : page = g_strdup_printf ("%d", item->page);
307 : 0 : self->last_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 18);
308 : 0 : gtk_widget_set_hexpand (self->last_box, TRUE);
309 : 0 : gtk_widget_set_valign (self->last_box, GTK_ALIGN_CENTER);
310 : 0 : gtk_box_set_homogeneous (GTK_BOX (self->last_box), TRUE);
311 : 0 : gtk_stack_add_named (self->stack, self->last_box, page);
312 : : }
313 : :
314 : 0 : gtk_box_append (GTK_BOX (self->last_box), GTK_WIDGET (item));
315 : :
316 : 0 : update_buttons_visibility (self);
317 : : }
318 : :
319 : : void
320 : 0 : mct_carousel_purge_items (MctCarousel *self)
321 : : {
322 : : GtkWidget *child;
323 : :
324 [ # # ]: 0 : while ((child = gtk_widget_get_first_child (GTK_WIDGET (self->stack))) != NULL)
325 : 0 : gtk_stack_remove (self->stack, child);
326 : :
327 : 0 : g_list_free (self->children);
328 : 0 : self->children = NULL;
329 : 0 : self->visible_page = 0;
330 : 0 : self->selected_item = NULL;
331 : 0 : }
332 : :
333 : : MctCarousel *
334 : 0 : mct_carousel_new (void)
335 : : {
336 : 0 : return g_object_new (MCT_TYPE_CAROUSEL, NULL);
337 : : }
338 : :
339 : : static void
340 : 0 : mct_carousel_dispose (GObject *object)
341 : : {
342 : 0 : MctCarousel *self = MCT_CAROUSEL (object);
343 : :
344 [ # # ]: 0 : g_clear_object (&self->provider);
345 [ # # ]: 0 : if (self->children != NULL)
346 : : {
347 : 0 : g_list_free (self->children);
348 : 0 : self->children = NULL;
349 : : }
350 : :
351 : 0 : G_OBJECT_CLASS (mct_carousel_parent_class)->dispose (object);
352 : 0 : }
353 : :
354 : : static void
355 : 0 : mct_carousel_class_init (MctCarouselClass *klass)
356 : : {
357 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
358 : 0 : GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
359 : :
360 : 0 : gtk_widget_class_set_template_from_resource (wclass,
361 : : "/org/freedesktop/MalcontentControl/ui/carousel.ui");
362 : :
363 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, stack);
364 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, go_back_button);
365 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, go_next_button);
366 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, arrow);
367 : 0 : gtk_widget_class_bind_template_child (wclass, MctCarousel, revealer);
368 : :
369 : 0 : gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_previous_page);
370 : 0 : gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_next_page);
371 : :
372 : 0 : gtk_widget_class_set_layout_manager_type (wclass, MCT_TYPE_CAROUSEL_LAYOUT);
373 : :
374 : 0 : object_class->dispose = mct_carousel_dispose;
375 : :
376 : 0 : signals[ITEM_ACTIVATED] =
377 : 0 : g_signal_new ("item-activated",
378 : : MCT_TYPE_CAROUSEL,
379 : : G_SIGNAL_RUN_LAST,
380 : : 0,
381 : : NULL, NULL,
382 : : g_cclosure_marshal_VOID__OBJECT,
383 : : G_TYPE_NONE, 1,
384 : : MCT_TYPE_CAROUSEL_ITEM);
385 : 0 : }
386 : :
387 : : static void
388 : 0 : on_transition_running (MctCarousel *self)
389 : : {
390 [ # # ]: 0 : if (!gtk_stack_get_transition_running (self->stack))
391 : 0 : mct_carousel_move_arrow (self);
392 : 0 : }
393 : :
394 : : static void
395 : 0 : mct_carousel_init (MctCarousel *self)
396 : : {
397 : : GtkStyleProvider *provider;
398 : :
399 : 0 : gtk_widget_init_template (GTK_WIDGET (self));
400 : :
401 : 0 : provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
402 : 0 : gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (provider),
403 : : "/org/freedesktop/MalcontentControl/ui/carousel.css");
404 : :
405 : 0 : gtk_style_context_add_provider_for_display (gdk_display_get_default (),
406 : : provider,
407 : : GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
408 : :
409 : 0 : g_object_unref (provider);
410 : :
411 : 0 : g_signal_connect_swapped (self->stack, "notify::transition-running", G_CALLBACK (on_transition_running), self);
412 : 0 : }
413 : :
414 : : guint
415 : 0 : mct_carousel_get_item_count (MctCarousel *self)
416 : : {
417 : 0 : return g_list_length (self->children);
418 : : }
419 : :
420 : : void
421 : 0 : mct_carousel_set_revealed (MctCarousel *self,
422 : : gboolean revealed)
423 : : {
424 : 0 : g_return_if_fail (MCT_IS_CAROUSEL (self));
425 : :
426 : 0 : gtk_revealer_set_reveal_child (self->revealer, revealed);
427 : : }
428 : :
429 : : struct _MctCarouselLayout {
430 : : GtkLayoutManager parent;
431 : : };
432 : :
433 [ # # # # : 0 : G_DEFINE_FINAL_TYPE (MctCarouselLayout, mct_carousel_layout, GTK_TYPE_LAYOUT_MANAGER)
# # ]
434 : :
435 : : static void
436 : 0 : mct_carousel_layout_measure (GtkLayoutManager *layout_manager,
437 : : GtkWidget *widget,
438 : : GtkOrientation orientation,
439 : : int for_size,
440 : : int *minimum,
441 : : int *natural,
442 : : int *minimum_baseline,
443 : : int *natural_baseline)
444 : : {
445 : : MctCarousel *carousel;
446 : :
447 : 0 : g_assert (MCT_IS_CAROUSEL (widget));
448 : :
449 : 0 : carousel = MCT_CAROUSEL (widget);
450 : :
451 : 0 : gtk_widget_measure (GTK_WIDGET (carousel->revealer),
452 : : orientation, for_size,
453 : : minimum, natural,
454 : : minimum_baseline, natural_baseline);
455 : 0 : }
456 : :
457 : : static void
458 : 0 : mct_carousel_layout_allocate (GtkLayoutManager *layout_manager,
459 : : GtkWidget *widget,
460 : : int width,
461 : : int height,
462 : : int baseline)
463 : : {
464 : : MctCarousel *carousel;
465 : :
466 : 0 : g_assert (MCT_IS_CAROUSEL (widget));
467 : :
468 : 0 : carousel = MCT_CAROUSEL (widget);
469 : 0 : gtk_widget_allocate (GTK_WIDGET (carousel->revealer), width, height, baseline, NULL);
470 : :
471 [ # # ]: 0 : if (carousel->selected_item == NULL)
472 : 0 : return;
473 : :
474 [ # # ]: 0 : if (gtk_stack_get_transition_running (carousel->stack))
475 : 0 : return;
476 : :
477 : 0 : mct_carousel_move_arrow (carousel);
478 : : }
479 : :
480 : : static void
481 : 0 : mct_carousel_layout_class_init (MctCarouselLayoutClass *klass)
482 : : {
483 : 0 : GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
484 : :
485 : 0 : layout_manager_class->measure = mct_carousel_layout_measure;
486 : 0 : layout_manager_class->allocate = mct_carousel_layout_allocate;
487 : 0 : }
488 : :
489 : : static void
490 : 0 : mct_carousel_layout_init (MctCarouselLayout *self)
491 : : {
492 : 0 : }
|