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