1 /* dzl-search-bar.c
2  *
3  * Copyright (C) 2015 Christian Hergert <christian@hergert.me>
4  *
5  * This file is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as
7  * published by the Free Software Foundation; either version 3 of the
8  * License, or (at your option) any later version.
9  *
10  * This file is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #define G_LOG_DOMAIN "dzl-search-bar"
20 
21 #include "config.h"
22 
23 #include <glib/gi18n.h>
24 
25 #include "bindings/dzl-signal-group.h"
26 #include "widgets/dzl-search-bar.h"
27 
28 typedef struct
29 {
30   GtkRevealer    *revealer;
31   GtkBox         *box;
32   GtkSearchEntry *entry;
33   GtkButton      *close_button;
34 
35   DzlSignalGroup *window_signals;
36 
37   guint           search_mode_enabled : 1;
38 } DzlSearchBarPrivate;
39 
40 static void dzl_search_bar_init_buildable (GtkBuildableIface *iface);
41 
42 G_DEFINE_TYPE_WITH_CODE (DzlSearchBar, dzl_search_bar, GTK_TYPE_BIN,
43                          G_ADD_PRIVATE (DzlSearchBar)
44                          G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
45                                                 dzl_search_bar_init_buildable))
46 
47 enum {
48   PROP_0,
49   PROP_SHOW_CLOSE_BUTTON,
50   PROP_SEARCH_MODE_ENABLED,
51   LAST_PROP
52 };
53 
54 enum {
55   ACTIVATE,
56   REVEAL,
57   LAST_SIGNAL
58 };
59 
60 static GParamSpec *properties [LAST_PROP];
61 static guint       signals [LAST_SIGNAL];
62 
63 static void
dzl_search_bar__entry_activate(DzlSearchBar * self,GtkSearchEntry * entry)64 dzl_search_bar__entry_activate (DzlSearchBar   *self,
65                                 GtkSearchEntry *entry)
66 {
67   g_assert (DZL_IS_SEARCH_BAR (self));
68   g_assert (GTK_IS_SEARCH_ENTRY (entry));
69 
70   g_signal_emit (self, signals [ACTIVATE], 0);
71 }
72 
73 static gboolean
is_modifier_key(const GdkEventKey * event)74 is_modifier_key (const GdkEventKey *event)
75 {
76   static const guint modifier_keyvals[] = {
77     GDK_KEY_Shift_L, GDK_KEY_Shift_R, GDK_KEY_Shift_Lock,
78     GDK_KEY_Caps_Lock, GDK_KEY_ISO_Lock, GDK_KEY_Control_L,
79     GDK_KEY_Control_R, GDK_KEY_Meta_L, GDK_KEY_Meta_R,
80     GDK_KEY_Alt_L, GDK_KEY_Alt_R, GDK_KEY_Super_L, GDK_KEY_Super_R,
81     GDK_KEY_Hyper_L, GDK_KEY_Hyper_R, GDK_KEY_ISO_Level3_Shift,
82     GDK_KEY_ISO_Next_Group, GDK_KEY_ISO_Prev_Group,
83     GDK_KEY_ISO_First_Group, GDK_KEY_ISO_Last_Group,
84     GDK_KEY_Mode_switch, GDK_KEY_Num_Lock, GDK_KEY_Multi_key,
85     GDK_KEY_Scroll_Lock,
86     0
87   };
88   const guint *ac_val;
89 
90   g_return_val_if_fail (event != NULL, FALSE);
91 
92   ac_val = modifier_keyvals;
93 
94   while (*ac_val)
95     {
96       if (event->keyval == *ac_val++)
97         return TRUE;
98     }
99 
100   return FALSE;
101 }
102 
103 static gboolean
toplevel_key_press_event_before(DzlSearchBar * self,GdkEventKey * event,GtkWindow * toplevel)104 toplevel_key_press_event_before (DzlSearchBar *self,
105                                  GdkEventKey  *event,
106                                  GtkWindow    *toplevel)
107 {
108   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
109 
110   g_assert (DZL_IS_SEARCH_BAR (self));
111   g_assert (event != NULL);
112   g_assert (GTK_IS_WINDOW (toplevel));
113 
114   switch (event->keyval)
115     {
116     case GDK_KEY_Escape:
117       if (priv->search_mode_enabled && gtk_widget_has_focus (GTK_WIDGET (priv->entry)))
118         {
119           dzl_search_bar_set_search_mode_enabled (self, FALSE);
120           return GDK_EVENT_STOP;
121         }
122       break;
123 
124     default:
125       break;
126     }
127 
128   return GDK_EVENT_PROPAGATE;
129 }
130 
131 static gboolean
toplevel_key_press_event_after(DzlSearchBar * self,GdkEventKey * event,GtkWindow * toplevel)132 toplevel_key_press_event_after (DzlSearchBar *self,
133                                 GdkEventKey  *event,
134                                 GtkWindow    *toplevel)
135 {
136   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
137   GtkWidget *entry;
138 
139   g_assert (DZL_IS_SEARCH_BAR (self));
140   g_assert (event != NULL);
141   g_assert (GTK_IS_WINDOW (toplevel));
142 
143   entry = GTK_WIDGET (priv->entry);
144 
145   switch (event->keyval)
146     {
147     case GDK_KEY_Escape:
148     case GDK_KEY_Up:
149     case GDK_KEY_KP_Up:
150     case GDK_KEY_Down:
151     case GDK_KEY_KP_Down:
152     case GDK_KEY_Left:
153     case GDK_KEY_KP_Left:
154     case GDK_KEY_Right:
155     case GDK_KEY_KP_Right:
156     case GDK_KEY_Home:
157     case GDK_KEY_KP_Home:
158     case GDK_KEY_End:
159     case GDK_KEY_KP_End:
160     case GDK_KEY_Page_Up:
161     case GDK_KEY_KP_Page_Up:
162     case GDK_KEY_Page_Down:
163     case GDK_KEY_KP_Page_Down:
164     case GDK_KEY_KP_Tab:
165     case GDK_KEY_Tab:
166       /* ignore keynav */
167       break;
168 
169     default:
170       if (((event->state & GDK_MOD1_MASK) != 0) ||
171           ((event->state & GDK_CONTROL_MASK) != 0) ||
172           priv->search_mode_enabled ||
173           is_modifier_key (event))
174         break;
175 
176       dzl_search_bar_set_search_mode_enabled (self, TRUE);
177 
178       return GTK_WIDGET_GET_CLASS (entry)->key_press_event (entry, event);
179     }
180 
181   return GDK_EVENT_PROPAGATE;
182 }
183 
184 static void
dzl_search_bar_hierarchy_changed(GtkWidget * widget,GtkWidget * old_toplevel)185 dzl_search_bar_hierarchy_changed (GtkWidget *widget,
186                                   GtkWidget *old_toplevel)
187 {
188   DzlSearchBar *self = (DzlSearchBar *)widget;
189   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
190   GtkWidget *toplevel;
191 
192   g_assert (DZL_IS_SEARCH_BAR (self));
193 
194   toplevel = gtk_widget_get_toplevel (widget);
195 
196   if (GTK_IS_WINDOW (toplevel))
197     dzl_signal_group_set_target (priv->window_signals, toplevel);
198   else
199     dzl_signal_group_set_target (priv->window_signals, NULL);
200 }
201 
202 static void
dzl_search_bar_reveal(DzlSearchBar * self)203 dzl_search_bar_reveal (DzlSearchBar *self)
204 {
205   g_assert (DZL_IS_SEARCH_BAR (self));
206 
207   dzl_search_bar_set_search_mode_enabled (self, TRUE);
208 }
209 
210 static GObject *
dzl_search_bar_get_internal_child(GtkBuildable * buildable,GtkBuilder * builder,const gchar * childname)211 dzl_search_bar_get_internal_child (GtkBuildable *buildable,
212                                    GtkBuilder   *builder,
213                                    const gchar  *childname)
214 {
215   DzlSearchBar *self = (DzlSearchBar *)buildable;
216   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
217 
218   g_assert (GTK_IS_BUILDABLE (buildable));
219   g_assert (DZL_IS_SEARCH_BAR (self));
220   g_assert (GTK_IS_BUILDER (builder));
221   g_assert (childname != NULL);
222 
223   if (g_strcmp0 (childname, "entry") == 0)
224     return G_OBJECT (priv->entry);
225   else if (g_strcmp0 (childname, "revealer") == 0)
226     return G_OBJECT (priv->revealer);
227 
228   return NULL;
229 }
230 
231 static void
dzl_search_bar_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)232 dzl_search_bar_get_property (GObject    *object,
233                              guint       prop_id,
234                              GValue     *value,
235                              GParamSpec *pspec)
236 {
237   DzlSearchBar *self = DZL_SEARCH_BAR (object);
238 
239   switch (prop_id)
240     {
241     case PROP_SEARCH_MODE_ENABLED:
242       g_value_set_boolean (value, dzl_search_bar_get_search_mode_enabled (self));
243       break;
244 
245     case PROP_SHOW_CLOSE_BUTTON:
246       g_value_set_boolean (value, dzl_search_bar_get_show_close_button (self));
247       break;
248 
249     default:
250       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
251     }
252 }
253 
254 static void
dzl_search_bar_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)255 dzl_search_bar_set_property (GObject      *object,
256                              guint         prop_id,
257                              const GValue *value,
258                              GParamSpec   *pspec)
259 {
260   DzlSearchBar *self = DZL_SEARCH_BAR (object);
261 
262   switch (prop_id)
263     {
264     case PROP_SEARCH_MODE_ENABLED:
265       dzl_search_bar_set_search_mode_enabled (self, g_value_get_boolean (value));
266       break;
267 
268     case PROP_SHOW_CLOSE_BUTTON:
269       dzl_search_bar_set_show_close_button (self, g_value_get_boolean (value));
270       break;
271 
272     default:
273       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
274     }
275 }
276 
277 static void
dzl_search_bar_finalize(GObject * object)278 dzl_search_bar_finalize (GObject *object)
279 {
280   DzlSearchBar *self = (DzlSearchBar *)object;
281   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
282 
283   g_clear_object (&priv->window_signals);
284 
285   G_OBJECT_CLASS (dzl_search_bar_parent_class)->finalize (object);
286 }
287 
288 static void
dzl_search_bar_class_init(DzlSearchBarClass * klass)289 dzl_search_bar_class_init (DzlSearchBarClass *klass)
290 {
291   GObjectClass *object_class = G_OBJECT_CLASS (klass);
292   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
293 
294   object_class->finalize = dzl_search_bar_finalize;
295   object_class->get_property = dzl_search_bar_get_property;
296   object_class->set_property = dzl_search_bar_set_property;
297 
298   widget_class->hierarchy_changed = dzl_search_bar_hierarchy_changed;
299 
300   properties [PROP_SEARCH_MODE_ENABLED] =
301     g_param_spec_boolean ("search-mode-enabled",
302                           "Search Mode Enabled",
303                           "Search Mode Enabled",
304                           FALSE,
305                           (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
306 
307   properties [PROP_SHOW_CLOSE_BUTTON] =
308     g_param_spec_boolean ("show-close-button",
309                           "Show Close Button",
310                           "Show Close Button",
311                           FALSE,
312                           (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
313 
314   g_object_class_install_properties (object_class, LAST_PROP, properties);
315 
316   signals [ACTIVATE] =
317     g_signal_new ("activate",
318                   G_TYPE_FROM_CLASS (klass),
319                   G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
320                   0, NULL, NULL, NULL, G_TYPE_NONE, 0);
321 
322   signals [REVEAL] =
323     g_signal_new_class_handler ("reveal",
324                                 G_TYPE_FROM_CLASS (klass),
325                                 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
326                                 G_CALLBACK (dzl_search_bar_reveal),
327                                 NULL, NULL, NULL, G_TYPE_NONE, 0);
328 
329   gtk_widget_class_set_css_name (widget_class, "dzlsearchbar");
330 }
331 
332 static void
dzl_search_bar_init_buildable(GtkBuildableIface * iface)333 dzl_search_bar_init_buildable (GtkBuildableIface *iface)
334 {
335   iface->get_internal_child = dzl_search_bar_get_internal_child;
336 }
337 
338 static void
dzl_search_bar_init(DzlSearchBar * self)339 dzl_search_bar_init (DzlSearchBar *self)
340 {
341   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
342   GtkStyleContext *style_context;
343   GtkBox *box;
344 
345   priv->window_signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
346   dzl_signal_group_connect_object (priv->window_signals,
347                                    "key-press-event",
348                                    G_CALLBACK (toplevel_key_press_event_before),
349                                    self,
350                                    G_CONNECT_SWAPPED);
351   dzl_signal_group_connect_object (priv->window_signals,
352                                    "key-press-event",
353                                    G_CALLBACK (toplevel_key_press_event_after),
354                                    self,
355                                    G_CONNECT_SWAPPED | G_CONNECT_AFTER);
356 
357   priv->revealer =
358     g_object_new (GTK_TYPE_REVEALER,
359                   "transition-type", GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN,
360                   "visible", TRUE,
361                   NULL);
362   /* outer box used for styling */
363   box =
364     g_object_new (GTK_TYPE_BOX,
365                   "orientation", GTK_ORIENTATION_HORIZONTAL,
366                   "visible", TRUE,
367                   NULL);
368   priv->box =
369     g_object_new (GTK_TYPE_BOX,
370                   "hexpand", TRUE,
371                   "margin-bottom", 3,
372                   "margin-end", 6,
373                   "margin-start", 6,
374                   "margin-top", 3,
375                   "orientation", GTK_ORIENTATION_HORIZONTAL,
376                   "visible", TRUE,
377                   NULL);
378   priv->entry =
379     g_object_connect (g_object_new (GTK_TYPE_SEARCH_ENTRY,
380                                     "placeholder-text", _("Search"),
381                                     "visible", TRUE,
382                                     NULL),
383                       "swapped_object_signal::activate", dzl_search_bar__entry_activate, self,
384                       NULL);
385   priv->close_button =
386     g_object_new (GTK_TYPE_BUTTON,
387                   "child", g_object_new (GTK_TYPE_IMAGE,
388                                          "icon-name", "window-close-symbolic",
389                                          "visible", TRUE,
390                                          NULL),
391                   "visible", FALSE,
392                   NULL);
393 
394   style_context = gtk_widget_get_style_context (GTK_WIDGET (box));
395   gtk_style_context_add_class (style_context, "search-bar");
396 
397   gtk_container_add (GTK_CONTAINER (priv->revealer), GTK_WIDGET (box));
398   gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->box));
399   gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->revealer));
400   gtk_container_add_with_properties (GTK_CONTAINER (priv->box),
401                                      GTK_WIDGET (priv->close_button),
402                                      "fill", FALSE,
403                                      "pack-type", GTK_PACK_END,
404                                      NULL);
405   gtk_box_set_center_widget (priv->box, GTK_WIDGET (priv->entry));
406 }
407 
408 GtkWidget *
dzl_search_bar_new(void)409 dzl_search_bar_new (void)
410 {
411   return g_object_new (DZL_TYPE_SEARCH_BAR, NULL);
412 }
413 
414 gboolean
dzl_search_bar_get_search_mode_enabled(DzlSearchBar * self)415 dzl_search_bar_get_search_mode_enabled (DzlSearchBar *self)
416 {
417   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
418 
419   g_return_val_if_fail (DZL_IS_SEARCH_BAR (self), FALSE);
420 
421   return priv->search_mode_enabled;
422 }
423 
424 void
dzl_search_bar_set_search_mode_enabled(DzlSearchBar * self,gboolean search_mode_enabled)425 dzl_search_bar_set_search_mode_enabled (DzlSearchBar *self,
426                                         gboolean      search_mode_enabled)
427 {
428   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
429 
430   g_return_if_fail (DZL_IS_SEARCH_BAR (self));
431 
432   search_mode_enabled = !!search_mode_enabled;
433 
434   if (search_mode_enabled != priv->search_mode_enabled)
435     {
436       priv->search_mode_enabled = search_mode_enabled;
437       gtk_revealer_set_reveal_child (priv->revealer, search_mode_enabled);
438       gtk_entry_set_text (GTK_ENTRY (priv->entry), "");
439       if (search_mode_enabled)
440         gtk_widget_grab_focus (GTK_WIDGET (priv->entry));
441 
442       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SEARCH_MODE_ENABLED]);
443     }
444 }
445 
446 gboolean
dzl_search_bar_get_show_close_button(DzlSearchBar * self)447 dzl_search_bar_get_show_close_button (DzlSearchBar *self)
448 {
449   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
450 
451   g_return_val_if_fail (DZL_IS_SEARCH_BAR (self), FALSE);
452 
453   return gtk_widget_get_visible (GTK_WIDGET (priv->close_button));
454 }
455 
456 void
dzl_search_bar_set_show_close_button(DzlSearchBar * self,gboolean show_close_button)457 dzl_search_bar_set_show_close_button (DzlSearchBar *self,
458                                       gboolean      show_close_button)
459 {
460   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
461 
462   g_return_if_fail (DZL_IS_SEARCH_BAR (self));
463 
464   gtk_widget_set_visible (GTK_WIDGET (priv->close_button), show_close_button);
465   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_CLOSE_BUTTON]);
466 }
467 
468 /**
469  * dzl_search_bar_get_entry:
470  *
471  * Returns: (transfer none) (type Gtk.SearchEntry): A #GtkSearchEntry.
472  */
473 GtkWidget *
dzl_search_bar_get_entry(DzlSearchBar * self)474 dzl_search_bar_get_entry (DzlSearchBar *self)
475 {
476   DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self);
477 
478   g_return_val_if_fail (DZL_IS_SEARCH_BAR (self), NULL);
479 
480   return GTK_WIDGET (priv->entry);
481 }
482