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