1 /*
2  *  This file is part of GNOME Twitch - 'Enjoy Twitch on your GNU/Linux desktop'
3  *  Copyright © 2017 Vincent Szolnoky <vinszent@vinszent.com>
4  *
5  *  GNOME Twitch is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  GNOME Twitch is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with GNOME Twitch. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "gt-item-container.h"
20 #include "utils.h"
21 #include "gt-win.h"
22 #include <glib/gi18n.h>
23 
24 #define TAG "GtItemContainer"
25 #include "gnome-twitch/gt-log.h"
26 
27 typedef struct
28 {
29     GtkWidget* item_scroll;
30     GtkWidget* item_flow;
31     GtkWidget* fetching_label;
32     GtkWidget* empty_label;
33     GtkWidget* empty_sub_label;
34     GtkWidget* empty_image;
35     GtkWidget* empty_box;
36     GtkWidget* error_box;
37     GtkWidget* error_label;
38     GtkWidget* reload_button;
39 
40     gint child_width;
41     gint child_height;
42     gboolean append_extra;
43     gchar* empty_label_text;
44     gchar* empty_sub_label_text;
45     gchar* empty_image_name;
46     gchar* fetching_label_text;
47     gchar* error_label_text;
48 
49     GtItemContainerProperties props;
50 
51     GHashTable* items;
52     gboolean fetching_items;
53 
54     GdkRectangle* alloc;
55 } GtItemContainerPrivate;
56 
57 G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(GtItemContainer, gt_item_container, GTK_TYPE_STACK);
58 
59 enum
60 {
61     PROP_0,
62     PROP_FETCHING_ITEMS,
63     NUM_PROPS
64 };
65 
66 static GParamSpec* props[NUM_PROPS];
67 
68 static void
fetch_items(GtItemContainer * self)69 fetch_items(GtItemContainer* self)
70 {
71     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
72 
73     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
74 
75     GtkAdjustment* vadj = gtk_scrolled_window_get_vadjustment(
76         GTK_SCROLLED_WINDOW(priv->item_scroll));
77     gint num_items = g_hash_table_size(priv->items);
78 
79     guint height = num_items == 0 ?
80         gtk_widget_get_allocated_height(GTK_WIDGET(self)) :
81         ROUND(gtk_adjustment_get_upper(vadj));
82 
83     guint width = gtk_widget_get_allocated_width(GTK_WIDGET(self));
84 
85     /* Haven't been allocated size yet, wait for first size allocate */
86     if (height <= 1 && width <= 1)
87     {
88         if (!priv->append_extra)
89             utils_signal_connect_oneshot(self, "size-allocate", G_CALLBACK(fetch_items), NULL);
90         return;
91     }
92 
93     priv->fetching_items = TRUE;
94     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_FETCHING_ITEMS]);
95 
96     gtk_stack_set_visible_child(GTK_STACK(self), priv->item_scroll);
97 
98     guint columns = width / (GT_ITEM_CONTAINER_GET_CLASS(self)->get_container_properties ? priv->props.child_width : priv->child_width);
99     guint rows = height / (GT_ITEM_CONTAINER_GET_CLASS(self)->get_container_properties ? priv->props.child_height : priv->child_height);
100 
101     guint leftover = MAX(columns*rows - num_items , 0);
102 
103     guint amount = leftover + columns*3;
104 
105     amount = CLAMP(amount, 1, 100);
106 
107     if (GT_ITEM_CONTAINER_GET_CLASS(self)->request_extra_items)
108         GT_ITEM_CONTAINER_GET_CLASS(self)->request_extra_items(self, amount, num_items);
109 }
110 
111 static void
append_extra(GtkWidget * widget,GdkRectangle * alloc,gpointer udata)112 append_extra(GtkWidget* widget,
113     GdkRectangle* alloc,
114     gpointer udata)
115 {
116     GtItemContainer* self = GT_ITEM_CONTAINER(udata);
117     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
118 
119     if (priv->fetching_items)
120         return;
121 
122     if (alloc->width < 10 || alloc->height < 10)
123         return;
124 
125     gboolean bigger =
126         alloc->width > priv->alloc->width ||
127         alloc->height > priv->alloc->height;
128 
129     g_free(priv->alloc);
130     priv->alloc = g_memdup(alloc, sizeof(GdkRectangle));
131 
132     if (!bigger)
133         return;
134 
135     GtkAdjustment* vadj = gtk_scrolled_window_get_vadjustment(
136         GTK_SCROLLED_WINDOW(priv->item_scroll));
137 
138     gboolean scroll_fills_alloc =
139         gtk_adjustment_get_upper(vadj) - priv->child_height <
140         alloc->height;
141 
142     gboolean near_upper =
143         gtk_adjustment_get_value(vadj) + gtk_adjustment_get_page_size(vadj) +
144         priv->child_height*2 > gtk_adjustment_get_upper(vadj);
145 
146     if (!scroll_fills_alloc && !near_upper)
147         return;
148 
149     fetch_items(self);
150 }
151 
152 static void
edge_reached_cb(GtkScrolledWindow * scroll,GtkPositionType pos,gpointer udata)153 edge_reached_cb(GtkScrolledWindow* scroll,
154     GtkPositionType pos, gpointer udata)
155 {
156     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(udata));
157     RETURN_IF_FAIL(GTK_IS_SCROLLED_WINDOW(scroll));
158 
159     GtItemContainer* self = GT_ITEM_CONTAINER(udata);
160 
161     if (pos == GTK_POS_BOTTOM)
162         fetch_items(self);
163 }
164 
165 static void
child_activated_cb(GtkFlowBox * box,GtkFlowBoxChild * child,gpointer udata)166 child_activated_cb(GtkFlowBox* box,
167     GtkFlowBoxChild* child, gpointer udata)
168 {
169     GtItemContainer* self = GT_ITEM_CONTAINER(udata);
170 
171     GT_ITEM_CONTAINER_GET_CLASS(self)->activate_child(self, child);
172 }
173 
174 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)175 get_property (GObject*    obj,
176               guint       prop,
177               GValue*     val,
178               GParamSpec* pspec)
179 {
180     GtItemContainer* self = GT_ITEM_CONTAINER(obj);
181     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
182 
183     switch (prop)
184     {
185         case PROP_FETCHING_ITEMS:
186             g_value_set_boolean(val, priv->fetching_items);
187             break;
188         default:
189             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
190     }
191 }
192 
193 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)194 set_property(GObject*      obj,
195              guint         prop,
196              const GValue* val,
197              GParamSpec*   pspec)
198 {
199     GtItemContainer* self = GT_ITEM_CONTAINER(obj);
200     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
201 
202     switch (prop)
203     {
204         default:
205             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
206     }
207 }
208 
209 static void
constructed(GObject * obj)210 constructed(GObject* obj)
211 {
212     GtItemContainer* self = GT_ITEM_CONTAINER(obj);
213     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
214 
215     //NOTE: This causes a segfault
216     /* G_OBJECT_GET_CLASS(gt_item_container_parent_class)->constructed(obj); */
217 
218     /* TODO: Migrate all containers to new get_container_properties method */
219     if (GT_ITEM_CONTAINER_GET_CLASS(self)->get_container_properties)
220     {
221         GT_ITEM_CONTAINER_GET_CLASS(self)->get_container_properties(self, &priv->props);
222 
223         gtk_label_set_text(GTK_LABEL(priv->empty_label), priv->props.empty_label_text);
224         gtk_label_set_text(GTK_LABEL(priv->empty_sub_label), priv->props.empty_sub_label_text);
225         gtk_label_set_text(GTK_LABEL(priv->fetching_label), priv->props.fetching_label_text);
226         gtk_label_set_text(GTK_LABEL(priv->error_label), priv->props.error_label_text);
227         gtk_image_set_from_icon_name(GTK_IMAGE(priv->empty_image), priv->props.empty_image_name, GTK_ICON_SIZE_DIALOG);
228 
229         //TODO: This should probably be connected to the window's size-allocate
230         if (priv->props.append_extra)
231         {
232             g_signal_connect(GT_WIN_ACTIVE, "size-allocate", G_CALLBACK(append_extra), self);
233             g_signal_connect(priv->item_scroll, "edge-reached", G_CALLBACK(edge_reached_cb), self);
234         }
235 
236         g_signal_connect(priv->item_flow, "child-activated", G_CALLBACK(child_activated_cb), self);
237     }
238     else
239     {
240         GT_ITEM_CONTAINER_GET_CLASS(self)->get_properties(self,
241             &priv->child_width, &priv->child_height, &priv->append_extra,
242             &priv->empty_label_text, &priv->empty_sub_label_text, &priv->empty_image_name,
243             &priv->error_label_text, &priv->fetching_label_text);
244 
245         gtk_label_set_text(GTK_LABEL(priv->empty_label), priv->empty_label_text);
246         gtk_label_set_text(GTK_LABEL(priv->empty_sub_label), priv->empty_sub_label_text);
247         gtk_label_set_text(GTK_LABEL(priv->fetching_label), priv->fetching_label_text);
248         gtk_label_set_text(GTK_LABEL(priv->error_label), priv->error_label_text);
249         gtk_image_set_from_icon_name(GTK_IMAGE(priv->empty_image), priv->empty_image_name, GTK_ICON_SIZE_DIALOG);
250 
251         //TODO: This should probably be connected to the window's size-allocate
252         if (priv->append_extra)
253         {
254             g_signal_connect(self, "size-allocate", G_CALLBACK(append_extra), self);
255             g_signal_connect(priv->item_scroll, "edge-reached", G_CALLBACK(edge_reached_cb), self);
256         }
257 
258         g_signal_connect(priv->item_flow, "child-activated", G_CALLBACK(child_activated_cb), self);
259     }
260 
261 }
262 
263 static void
gt_item_container_class_init(GtItemContainerClass * klass)264 gt_item_container_class_init(GtItemContainerClass* klass)
265 {
266     G_OBJECT_CLASS(klass)->set_property = set_property;
267     G_OBJECT_CLASS(klass)->get_property = get_property;
268     G_OBJECT_CLASS(klass)->constructed = constructed;
269 
270     props[PROP_FETCHING_ITEMS] = g_param_spec_boolean(
271         "fetching-items", "Fetching items", "Whether fetching items",
272         FALSE, G_PARAM_READABLE);
273 
274     g_object_class_install_properties(G_OBJECT_CLASS(klass), NUM_PROPS, props);
275 
276     gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(klass),
277         "/com/vinszent/GnomeTwitch/ui/gt-item-container.ui");
278 
279     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, item_flow);
280     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, item_scroll);
281     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, empty_label);
282     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, empty_sub_label);
283     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, empty_image);
284     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, fetching_label);
285     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, empty_box);
286     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, error_box);
287     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, error_label);
288     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtItemContainer, reload_button);
289 }
290 
291 static void
gt_item_container_init(GtItemContainer * self)292 gt_item_container_init(GtItemContainer* self)
293 {
294     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
295 
296     priv->items = g_hash_table_new(g_direct_hash, g_direct_equal);
297     priv->alloc = g_new(GdkRectangle, 1);
298     priv->alloc->width = 0;
299     priv->alloc->height = 0;
300 
301     gtk_widget_init_template(GTK_WIDGET(self));
302 
303     g_signal_connect_swapped(priv->reload_button, "clicked",
304         G_CALLBACK(gt_item_container_refresh), self);
305 }
306 
307 GtkWidget*
gt_item_container_get_flow_box(GtItemContainer * self)308 gt_item_container_get_flow_box(GtItemContainer* self)
309 {
310     RETURN_VAL_IF_FAIL(GT_IS_ITEM_CONTAINER(self), NULL);
311 
312     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
313 
314     return priv->item_flow;
315 }
316 
317 void
gt_item_container_append_item(GtItemContainer * self,gpointer item)318 gt_item_container_append_item(GtItemContainer* self, gpointer item)
319 {
320     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
321     RETURN_IF_FAIL(item != NULL);
322 
323     DEBUG("Appending item");
324 
325     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
326     GtkWidget* child = GT_ITEM_CONTAINER_GET_CLASS(self)->create_child(self, item);
327 
328     g_hash_table_insert(priv->items, item, child);
329     gtk_container_add(GTK_CONTAINER(priv->item_flow), child);
330 
331     gtk_stack_set_visible_child(GTK_STACK(self), priv->item_scroll);
332 }
333 
334 void
gt_item_container_append_items(GtItemContainer * self,GList * items)335 gt_item_container_append_items(GtItemContainer* self, GList* items)
336 {
337     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
338 
339     DEBUG("Appending items with list length '%d'", g_list_length(items));
340 
341     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
342 
343     for (GList* l = items; l != NULL; l = l->next)
344     {
345         GtkWidget* child = GT_ITEM_CONTAINER_GET_CLASS(self)->create_child(self, l->data);
346 
347         g_hash_table_insert(priv->items, l->data, child);
348         gtk_container_add(GTK_CONTAINER(priv->item_flow), child);
349     }
350 }
351 
352 void
gt_item_container_set_items(GtItemContainer * self,GList * items)353 gt_item_container_set_items(GtItemContainer* self, GList* items)
354 {
355     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
356 
357     DEBUG("Settings items with list length '%d'", g_list_length(items));
358 
359     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
360 
361     utils_container_clear(GTK_CONTAINER(priv->item_flow));
362 
363     /* NOTE: We don't call any free funcs here because all data is
364      * owned by the children */
365     g_hash_table_steal_all(priv->items);
366 
367     for (GList* l = items; l != NULL; l = l->next)
368     {
369         GtkWidget* child = GT_ITEM_CONTAINER_GET_CLASS(self)->create_child(self, l->data);
370 
371         g_hash_table_insert(priv->items, l->data, child);
372         gtk_container_add(GTK_CONTAINER(priv->item_flow), child);
373     }
374 }
375 
376 void
gt_item_container_remove_item(GtItemContainer * self,gpointer item)377 gt_item_container_remove_item(GtItemContainer* self, gpointer item)
378 {
379     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
380     RETURN_IF_FAIL(item != NULL);
381 
382     DEBUG("Removing item");
383 
384     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
385 
386     GtkWidget* child = g_hash_table_lookup(priv->items, item);
387 
388     RETURN_IF_FAIL(GTK_IS_WIDGET(child));
389 
390     g_hash_table_steal(priv->items, item);
391 
392     gtk_container_remove(GTK_CONTAINER(priv->item_flow), child);
393 
394     if (g_hash_table_size(priv->items) == 0)
395         gtk_stack_set_visible_child(GTK_STACK(self), priv->empty_box);
396 }
397 
398 void
gt_item_container_set_fetching_items(GtItemContainer * self,gboolean fetching_items)399 gt_item_container_set_fetching_items(GtItemContainer* self, gboolean fetching_items)
400 {
401     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
402 
403     DEBUG("Setting fetching items to '%s'", PRINT_BOOL(fetching_items));
404 
405     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
406 
407     if (!fetching_items)
408     {
409         if (g_hash_table_size(priv->items) == 0)
410             gtk_stack_set_visible_child(GTK_STACK(self), priv->empty_box);
411         else
412             gtk_stack_set_visible_child(GTK_STACK(self), priv->item_scroll);
413     }
414 
415     priv->fetching_items = fetching_items;
416     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_FETCHING_ITEMS]);
417 }
418 
419 void
gt_item_container_refresh(GtItemContainer * self)420 gt_item_container_refresh(GtItemContainer* self)
421 {
422     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
423 
424     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
425 
426     DEBUG("Refreshing");
427 
428     utils_container_clear(GTK_CONTAINER(priv->item_flow));
429 
430     /* NOTE: No need to free items as they are owned by the children */
431     g_hash_table_steal_all(priv->items);
432 
433     fetch_items(self);
434 }
435 
436 void
gt_item_container_show_error(GtItemContainer * self,const GError * error)437 gt_item_container_show_error(GtItemContainer* self, const GError* error)
438 {
439     RETURN_IF_FAIL(GT_IS_ITEM_CONTAINER(self));
440     RETURN_IF_FAIL(error != NULL);
441 
442     GtItemContainerPrivate* priv = gt_item_container_get_instance_private(self);
443     GtWin* win = GT_WIN_TOPLEVEL(self);
444 
445     RETURN_IF_FAIL(GT_IS_WIN(win));
446 
447     gt_win_show_error_message(win, _("Unable to fetch items"),
448         "Unable to fetch items because: %s", error->message);
449 
450     gtk_stack_set_visible_child(GTK_STACK(self), priv->error_box);
451 }
452