1 /* Nautilus - Floating status bar.
2  *
3  * Copyright (C) 2011 Red Hat Inc.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library 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 GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public
16  * License along with this library; if not, see <http://www.gnu.org/licenses/>.
17  *
18  * Authors: Cosimo Cecchi <cosimoc@redhat.com>
19  *
20  */
21 
22 #include <config.h>
23 
24 #include <string.h>
25 
26 #include "nautilus-floating-bar.h"
27 
28 #define HOVER_HIDE_TIMEOUT_INTERVAL 100
29 
30 struct _NautilusFloatingBar
31 {
32     GtkBox parent;
33 
34     gchar *primary_label;
35     gchar *details_label;
36 
37     GtkWidget *primary_label_widget;
38     GtkWidget *details_label_widget;
39     GtkWidget *spinner;
40     gboolean show_spinner;
41     gboolean is_interactive;
42     guint hover_timeout_id;
43 };
44 
45 enum
46 {
47     PROP_PRIMARY_LABEL = 1,
48     PROP_DETAILS_LABEL,
49     PROP_SHOW_SPINNER,
50     NUM_PROPERTIES
51 };
52 
53 enum
54 {
55     ACTION,
56     NUM_SIGNALS
57 };
58 
59 static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
60 static guint signals[NUM_SIGNALS] = { 0, };
61 
62 G_DEFINE_TYPE (NautilusFloatingBar, nautilus_floating_bar,
63                GTK_TYPE_BOX);
64 
65 static void
66 action_button_clicked_cb (GtkButton           *button,
67                           NautilusFloatingBar *self)
68 {
69     gint action_id;
70 
71     action_id = GPOINTER_TO_INT
72                     (g_object_get_data (G_OBJECT (button), "action-id"));
73 
74     g_signal_emit (self, signals[ACTION], 0, action_id);
75 }
76 
77 static void
78 nautilus_floating_bar_finalize (GObject *obj)
79 {
80     NautilusFloatingBar *self = NAUTILUS_FLOATING_BAR (obj);
81 
82     nautilus_floating_bar_remove_hover_timeout (self);
83     g_free (self->primary_label);
84     g_free (self->details_label);
85 
86     G_OBJECT_CLASS (nautilus_floating_bar_parent_class)->finalize (obj);
87 }
88 
89 static void
90 nautilus_floating_bar_get_property (GObject    *object,
91                                     guint       property_id,
92                                     GValue     *value,
93                                     GParamSpec *pspec)
94 {
95     NautilusFloatingBar *self = NAUTILUS_FLOATING_BAR (object);
96 
97     switch (property_id)
98     {
99         case PROP_PRIMARY_LABEL:
100         {
101             g_value_set_string (value, self->primary_label);
102         }
103         break;
104 
105         case PROP_DETAILS_LABEL:
106         {
107             g_value_set_string (value, self->details_label);
108         }
109         break;
110 
111         case PROP_SHOW_SPINNER:
112         {
113             g_value_set_boolean (value, self->show_spinner);
114         }
115         break;
116 
117         default:
118         {
119             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
120         }
121         break;
122     }
123 }
124 
125 static void
126 nautilus_floating_bar_set_property (GObject      *object,
127                                     guint         property_id,
128                                     const GValue *value,
129                                     GParamSpec   *pspec)
130 {
131     NautilusFloatingBar *self = NAUTILUS_FLOATING_BAR (object);
132 
133     switch (property_id)
134     {
135         case PROP_PRIMARY_LABEL:
136         {
137             nautilus_floating_bar_set_primary_label (self, g_value_get_string (value));
138         }
139         break;
140 
141         case PROP_DETAILS_LABEL:
142         {
143             nautilus_floating_bar_set_details_label (self, g_value_get_string (value));
144         }
145         break;
146 
147         case PROP_SHOW_SPINNER:
148         {
149             nautilus_floating_bar_set_show_spinner (self, g_value_get_boolean (value));
150         }
151         break;
152 
153         default:
154         {
155             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
156         }
157         break;
158     }
159 }
160 
161 static void
162 update_labels (NautilusFloatingBar *self)
163 {
164     gboolean primary_visible, details_visible;
165 
166     primary_visible = (self->primary_label != NULL) &&
167                       (strlen (self->primary_label) > 0);
168     details_visible = (self->details_label != NULL) &&
169                       (strlen (self->details_label) > 0);
170 
171     gtk_label_set_text (GTK_LABEL (self->primary_label_widget),
172                         self->primary_label);
173     gtk_widget_set_visible (self->primary_label_widget, primary_visible);
174 
175     gtk_label_set_text (GTK_LABEL (self->details_label_widget),
176                         self->details_label);
177     gtk_widget_set_visible (self->details_label_widget, details_visible);
178 }
179 
180 void
181 nautilus_floating_bar_remove_hover_timeout (NautilusFloatingBar *self)
182 {
183     if (self->hover_timeout_id != 0)
184     {
185         g_source_remove (self->hover_timeout_id);
186         self->hover_timeout_id = 0;
187     }
188 }
189 
190 typedef struct
191 {
192     GtkWidget *overlay;
193     GtkWidget *floating_bar;
194     GdkDevice *device;
195     gint y_down_limit;
196     gint y_upper_limit;
197 } CheckPointerData;
198 
199 static void
200 check_pointer_data_free (gpointer data)
201 {
202     g_slice_free (CheckPointerData, data);
203 }
204 
205 static gboolean
206 check_pointer_timeout (gpointer user_data)
207 {
208     CheckPointerData *data = user_data;
209     gint pointer_y = -1;
210 
211     gdk_window_get_device_position (gtk_widget_get_window (data->overlay), data->device,
212                                     NULL, &pointer_y, NULL);
213 
214     if (pointer_y == -1 || pointer_y < data->y_down_limit || pointer_y > data->y_upper_limit)
215     {
216         gtk_widget_show (data->floating_bar);
217         NAUTILUS_FLOATING_BAR (data->floating_bar)->hover_timeout_id = 0;
218 
219         return G_SOURCE_REMOVE;
220     }
221     else
222     {
223         gtk_widget_hide (data->floating_bar);
224     }
225 
226     return G_SOURCE_CONTINUE;
227 }
228 
229 static gboolean
230 overlay_enter_notify_cb (GtkWidget        *parent,
231                          GdkEventCrossing *event,
232                          gpointer          user_data)
233 {
234     GtkWidget *widget = user_data;
235     CheckPointerData *data;
236     gint y_pos;
237 
238     NautilusFloatingBar *self = NAUTILUS_FLOATING_BAR (widget);
239 
240     if (self->hover_timeout_id != 0)
241     {
242         g_source_remove (self->hover_timeout_id);
243     }
244 
245     if (event->window != gtk_widget_get_window (widget))
246     {
247         return GDK_EVENT_PROPAGATE;
248     }
249 
250     if (NAUTILUS_FLOATING_BAR (widget)->is_interactive)
251     {
252         return GDK_EVENT_PROPAGATE;
253     }
254 
255     gdk_window_get_position (gtk_widget_get_window (widget), NULL, &y_pos);
256 
257     data = g_slice_new (CheckPointerData);
258     data->overlay = parent;
259     data->floating_bar = widget;
260     data->device = gdk_event_get_device ((GdkEvent *) event);
261     data->y_down_limit = y_pos;
262     data->y_upper_limit = y_pos + gtk_widget_get_allocated_height (widget);
263 
264     self->hover_timeout_id = g_timeout_add_full (G_PRIORITY_DEFAULT, HOVER_HIDE_TIMEOUT_INTERVAL,
265                                                  check_pointer_timeout, data,
266                                                  check_pointer_data_free);
267 
268     g_source_set_name_by_id (self->hover_timeout_id, "[nautilus-floating-bar] overlay_enter_notify_cb");
269 
270     return GDK_EVENT_STOP;
271 }
272 
273 static void
274 nautilus_floating_bar_parent_set (GtkWidget *widget,
275                                   GtkWidget *old_parent)
276 {
277     GtkWidget *parent;
278 
279     parent = gtk_widget_get_parent (widget);
280 
281     if (old_parent != NULL)
282     {
283         g_signal_handlers_disconnect_by_func (old_parent,
284                                               overlay_enter_notify_cb, widget);
285     }
286 
287     if (parent != NULL)
288     {
289         g_signal_connect (parent, "enter-notify-event",
290                           G_CALLBACK (overlay_enter_notify_cb), widget);
291     }
292 }
293 
294 static void
295 get_padding_and_border (GtkWidget *widget,
296                         GtkBorder *border)
297 {
298     GtkStyleContext *context;
299     GtkStateFlags state;
300     GtkBorder tmp;
301 
302     context = gtk_widget_get_style_context (widget);
303     state = gtk_widget_get_state_flags (widget);
304 
305     gtk_style_context_get_padding (context, state, border);
306     gtk_style_context_get_border (context, state, &tmp);
307     border->top += tmp.top;
308     border->right += tmp.right;
309     border->bottom += tmp.bottom;
310     border->left += tmp.left;
311 }
312 
313 static void
314 nautilus_floating_bar_get_preferred_width (GtkWidget *widget,
315                                            gint      *minimum_size,
316                                            gint      *natural_size)
317 {
318     GtkBorder border;
319 
320     get_padding_and_border (widget, &border);
321 
322     GTK_WIDGET_CLASS (nautilus_floating_bar_parent_class)->get_preferred_width (widget,
323                                                                                 minimum_size,
324                                                                                 natural_size);
325 
326     *minimum_size += border.left + border.right;
327     *natural_size += border.left + border.right;
328 }
329 
330 static void
331 nautilus_floating_bar_get_preferred_width_for_height (GtkWidget *widget,
332                                                       gint       height,
333                                                       gint      *minimum_size,
334                                                       gint      *natural_size)
335 {
336     GtkBorder border;
337 
338     get_padding_and_border (widget, &border);
339 
340     GTK_WIDGET_CLASS (nautilus_floating_bar_parent_class)->get_preferred_width_for_height (widget,
341                                                                                            height,
342                                                                                            minimum_size,
343                                                                                            natural_size);
344 
345     *minimum_size += border.left + border.right;
346     *natural_size += border.left + border.right;
347 }
348 
349 static void
350 nautilus_floating_bar_get_preferred_height (GtkWidget *widget,
351                                             gint      *minimum_size,
352                                             gint      *natural_size)
353 {
354     GtkBorder border;
355 
356     get_padding_and_border (widget, &border);
357 
358     GTK_WIDGET_CLASS (nautilus_floating_bar_parent_class)->get_preferred_height (widget,
359                                                                                  minimum_size,
360                                                                                  natural_size);
361 
362     *minimum_size += border.top + border.bottom;
363     *natural_size += border.top + border.bottom;
364 }
365 
366 static void
367 nautilus_floating_bar_get_preferred_height_for_width (GtkWidget *widget,
368                                                       gint       width,
369                                                       gint      *minimum_size,
370                                                       gint      *natural_size)
371 {
372     GtkBorder border;
373 
374     get_padding_and_border (widget, &border);
375 
376     GTK_WIDGET_CLASS (nautilus_floating_bar_parent_class)->get_preferred_height_for_width (widget,
377                                                                                            width,
378                                                                                            minimum_size,
379                                                                                            natural_size);
380 
381     *minimum_size += border.top + border.bottom;
382     *natural_size += border.top + border.bottom;
383 }
384 
385 static void
386 nautilus_floating_bar_constructed (GObject *obj)
387 {
388     NautilusFloatingBar *self = NAUTILUS_FLOATING_BAR (obj);
389     GtkWidget *w, *box, *labels_box;
390 
391     G_OBJECT_CLASS (nautilus_floating_bar_parent_class)->constructed (obj);
392 
393     box = GTK_WIDGET (obj);
394 
395     w = gtk_spinner_new ();
396     gtk_box_pack_start (GTK_BOX (box), w, FALSE, FALSE, 0);
397     gtk_widget_set_visible (w, self->show_spinner);
398     gtk_spinner_start (GTK_SPINNER (w));
399     self->spinner = w;
400 
401     gtk_widget_set_size_request (w, 16, 16);
402     gtk_widget_set_margin_start (w, 8);
403 
404     labels_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
405     gtk_box_pack_start (GTK_BOX (box), labels_box, TRUE, TRUE, 0);
406     g_object_set (labels_box,
407                   "margin-top", 2,
408                   "margin-bottom", 2,
409                   "margin-start", 12,
410                   "margin-end", 12,
411                   NULL);
412     gtk_widget_show (labels_box);
413 
414     w = gtk_label_new (NULL);
415     gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_MIDDLE);
416     gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE);
417     gtk_container_add (GTK_CONTAINER (labels_box), w);
418     self->primary_label_widget = w;
419     gtk_widget_show (w);
420 
421     w = gtk_label_new (NULL);
422     gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE);
423     gtk_container_add (GTK_CONTAINER (labels_box), w);
424     self->details_label_widget = w;
425     gtk_widget_show (w);
426 }
427 
428 static void
429 nautilus_floating_bar_init (NautilusFloatingBar *self)
430 {
431     GtkStyleContext *context;
432 
433     context = gtk_widget_get_style_context (GTK_WIDGET (self));
434     gtk_style_context_add_class (context, "floating-bar");
435 }
436 
437 static void
438 nautilus_floating_bar_class_init (NautilusFloatingBarClass *klass)
439 {
440     GObjectClass *oclass = G_OBJECT_CLASS (klass);
441     GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
442 
443     oclass->constructed = nautilus_floating_bar_constructed;
444     oclass->set_property = nautilus_floating_bar_set_property;
445     oclass->get_property = nautilus_floating_bar_get_property;
446     oclass->finalize = nautilus_floating_bar_finalize;
447 
448     wclass->get_preferred_width = nautilus_floating_bar_get_preferred_width;
449     wclass->get_preferred_width_for_height = nautilus_floating_bar_get_preferred_width_for_height;
450     wclass->get_preferred_height = nautilus_floating_bar_get_preferred_height;
451     wclass->get_preferred_height_for_width = nautilus_floating_bar_get_preferred_height_for_width;
452     wclass->parent_set = nautilus_floating_bar_parent_set;
453 
454     properties[PROP_PRIMARY_LABEL] =
455         g_param_spec_string ("primary-label",
456                              "Bar's primary label",
457                              "Primary label displayed by the bar",
458                              NULL,
459                              G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
460     properties[PROP_DETAILS_LABEL] =
461         g_param_spec_string ("details-label",
462                              "Bar's details label",
463                              "Details label displayed by the bar",
464                              NULL,
465                              G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
466     properties[PROP_SHOW_SPINNER] =
467         g_param_spec_boolean ("show-spinner",
468                               "Show spinner",
469                               "Whether a spinner should be shown in the floating bar",
470                               FALSE,
471                               G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
472 
473     signals[ACTION] =
474         g_signal_new ("action",
475                       G_TYPE_FROM_CLASS (klass),
476                       G_SIGNAL_RUN_LAST,
477                       0, NULL, NULL,
478                       g_cclosure_marshal_VOID__INT,
479                       G_TYPE_NONE, 1,
480                       G_TYPE_INT);
481 
482     g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
483 }
484 
485 void
486 nautilus_floating_bar_set_primary_label (NautilusFloatingBar *self,
487                                          const gchar         *label)
488 {
489     if (g_strcmp0 (self->primary_label, label) != 0)
490     {
491         g_free (self->primary_label);
492         self->primary_label = g_strdup (label);
493 
494         g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PRIMARY_LABEL]);
495 
496         update_labels (self);
497     }
498 }
499 
500 void
501 nautilus_floating_bar_set_details_label (NautilusFloatingBar *self,
502                                          const gchar         *label)
503 {
504     if (g_strcmp0 (self->details_label, label) != 0)
505     {
506         g_free (self->details_label);
507         self->details_label = g_strdup (label);
508 
509         g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DETAILS_LABEL]);
510 
511         update_labels (self);
512     }
513 }
514 
515 void
516 nautilus_floating_bar_set_labels (NautilusFloatingBar *self,
517                                   const gchar         *primary_label,
518                                   const gchar         *details_label)
519 {
520     nautilus_floating_bar_set_primary_label (self, primary_label);
521     nautilus_floating_bar_set_details_label (self, details_label);
522 }
523 
524 void
525 nautilus_floating_bar_set_show_spinner (NautilusFloatingBar *self,
526                                         gboolean             show_spinner)
527 {
528     if (self->show_spinner != show_spinner)
529     {
530         self->show_spinner = show_spinner;
531         gtk_widget_set_visible (self->spinner,
532                                 show_spinner);
533 
534         g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_SPINNER]);
535     }
536 }
537 
538 GtkWidget *
539 nautilus_floating_bar_new (const gchar *primary_label,
540                            const gchar *details_label,
541                            gboolean     show_spinner)
542 {
543     return g_object_new (NAUTILUS_TYPE_FLOATING_BAR,
544                          "primary-label", primary_label,
545                          "details-label", details_label,
546                          "show-spinner", show_spinner,
547                          "orientation", GTK_ORIENTATION_HORIZONTAL,
548                          "spacing", 8,
549                          NULL);
550 }
551 
552 void
553 nautilus_floating_bar_add_action (NautilusFloatingBar *self,
554                                   const gchar         *icon_name,
555                                   gint                 action_id)
556 {
557     GtkWidget *button;
558     GtkStyleContext *context;
559 
560     button = gtk_button_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
561     context = gtk_widget_get_style_context (button);
562     gtk_style_context_add_class (context, "circular");
563     gtk_style_context_add_class (context, "flat");
564     gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
565     gtk_box_pack_end (GTK_BOX (self), button, FALSE, FALSE, 0);
566     gtk_widget_show (button);
567 
568     g_object_set_data (G_OBJECT (button), "action-id",
569                        GINT_TO_POINTER (action_id));
570 
571     g_signal_connect (button, "clicked",
572                       G_CALLBACK (action_button_clicked_cb), self);
573 
574     self->is_interactive = TRUE;
575 }
576 
577 void
578 nautilus_floating_bar_cleanup_actions (NautilusFloatingBar *self)
579 {
580     GtkWidget *widget;
581     GList *children, *l;
582     gpointer data;
583 
584     children = gtk_container_get_children (GTK_CONTAINER (self));
585     l = children;
586 
587     while (l != NULL)
588     {
589         widget = l->data;
590         data = g_object_get_data (G_OBJECT (widget), "action-id");
591         l = l->next;
592 
593         if (data != NULL)
594         {
595             /* destroy this */
596             gtk_widget_destroy (widget);
597         }
598     }
599 
600     g_list_free (children);
601 
602     self->is_interactive = FALSE;
603 }
604