1 /* cc-xkb-modifier-dialog.c
2  *
3  * Copyright 2019 Bastien Nocera <hadess@hadess.net>
4  *
5  * This program 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 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-2.0-or-later
19  */
20 
21 #include <glib/gi18n.h>
22 #define HANDY_USE_UNSTABLE_API
23 #include <handy.h>
24 
25 #include "cc-xkb-modifier-dialog.h"
26 #include "list-box-helper.h"
27 
28 struct _CcXkbModifierDialog
29 {
30   GtkDialog       parent_instance;
31 
32   GtkLabel       *description_label;
33   GtkSwitch      *enabled_switch;
34   GtkListBox     *listbox;
35   GtkListBox     *switch_listbox;
36   HdyActionRow   *switch_row;
37 
38   GSettings      *input_source_settings;
39   const CcXkbModifier *modifier;
40   GSList         *radio_group;
41 };
42 
43 G_DEFINE_TYPE (CcXkbModifierDialog, cc_xkb_modifier_dialog, GTK_TYPE_DIALOG)
44 
45 static const gchar *custom_css =
46 ".xkb-option-button {"
47 "    padding: 12px"
48 "}";
49 
50 static const CcXkbOption*
get_xkb_option_from_name(const CcXkbModifier * modifier,const gchar * name)51 get_xkb_option_from_name (const CcXkbModifier *modifier, const gchar* name)
52 {
53   const CcXkbOption *options = modifier->options;
54   int i;
55 
56   for (i = 0; options[i].label && options[i].xkb_option; i++)
57     {
58       if (g_str_equal (name, options[i].xkb_option))
59         return &options[i];
60     }
61 
62   return NULL;
63 }
64 
65 static GtkRadioButton *
get_radio_button_from_xkb_option_name(CcXkbModifierDialog * self,const gchar * name)66 get_radio_button_from_xkb_option_name (CcXkbModifierDialog *self,
67                                        const gchar         *name)
68 {
69   gchar *xkb_option;
70   GSList *l;
71 
72   for (l = self->radio_group; l != NULL; l = l->next)
73     {
74       xkb_option = g_object_get_data (l->data, "xkb-option");
75       if (g_strcmp0 (xkb_option, name) == 0)
76         return l->data;
77     }
78 
79   return NULL;
80 }
81 
82 static void
update_active_radio(CcXkbModifierDialog * self)83 update_active_radio (CcXkbModifierDialog *self)
84 {
85   g_auto(GStrv) options = NULL;
86   GtkRadioButton *rightalt_radio;
87   const CcXkbOption *default_option;
88   guint i;
89 
90   options = g_settings_get_strv (self->input_source_settings, "xkb-options");
91 
92   for (i = 0; options != NULL && options[i] != NULL; i++)
93     {
94       GtkRadioButton *radio;
95 
96       if (!g_str_has_prefix (options[i], self->modifier->prefix))
97         continue;
98 
99       radio = get_radio_button_from_xkb_option_name (self, options[i]);
100 
101       if (!radio)
102         continue;
103 
104       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (radio), TRUE);
105       gtk_switch_set_active (self->enabled_switch, TRUE);
106       return;
107     }
108 
109   if (self->modifier->default_option != NULL)
110     {
111       default_option = get_xkb_option_from_name(self->modifier, self->modifier->default_option);
112       rightalt_radio = get_radio_button_from_xkb_option_name (self, default_option->xkb_option);
113       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (rightalt_radio), TRUE);
114       gtk_switch_set_active (self->enabled_switch, TRUE);
115     }
116   else
117     {
118       gtk_switch_set_active (self->enabled_switch, FALSE);
119     }
120 }
121 
122 static void
set_xkb_option(CcXkbModifierDialog * self,gchar * xkb_option)123 set_xkb_option (CcXkbModifierDialog *self,
124                 gchar               *xkb_option)
125 {
126   g_autoptr(GPtrArray) array = NULL;
127   g_auto(GStrv) options = NULL;
128   gboolean found;
129   guint i;
130 
131   /* Either replace the existing "<modifier>:" option in the string
132    * array, or add the option at the end
133    */
134   array = g_ptr_array_new ();
135   options = g_settings_get_strv (self->input_source_settings, "xkb-options");
136   found = FALSE;
137 
138   for (i = 0; options != NULL && options[i] != NULL; i++)
139     {
140       if (g_str_has_prefix (options[i], self->modifier->prefix))
141         {
142           if (!found && xkb_option != NULL)
143             g_ptr_array_add (array, xkb_option);
144           found = TRUE;
145         }
146       else
147         {
148           g_ptr_array_add (array, options[i]);
149         }
150     }
151 
152   if (!found && xkb_option != NULL)
153     g_ptr_array_add (array, xkb_option);
154 
155   g_ptr_array_add (array, NULL);
156 
157   g_settings_set_strv (self->input_source_settings,
158                        "xkb-options",
159                        (const gchar * const *) array->pdata);
160 }
161 
162 static void
on_active_radio_changed_cb(CcXkbModifierDialog * self,GtkRadioButton * radio)163 on_active_radio_changed_cb (CcXkbModifierDialog *self,
164                             GtkRadioButton      *radio)
165 {
166   gchar *xkb_option;
167 
168   if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (radio)))
169     return;
170 
171   if (!gtk_switch_get_state (self->enabled_switch))
172     return;
173 
174   xkb_option = (gchar *)g_object_get_data (G_OBJECT (radio), "xkb-option");
175   set_xkb_option (self, xkb_option);
176 }
177 
178 static void
on_xkb_options_changed_cb(CcXkbModifierDialog * self)179 on_xkb_options_changed_cb (CcXkbModifierDialog *self)
180 {
181   if (self->modifier == NULL)
182     update_active_radio (self);
183 }
184 
185 static gboolean
enable_switch_changed_cb(GtkSwitch * widget,gboolean state,gpointer user_data)186 enable_switch_changed_cb (GtkSwitch *widget,
187                           gboolean   state,
188                           gpointer   user_data)
189 {
190   CcXkbModifierDialog *self = user_data;
191   gchar *xkb_option;
192   GSList *l;
193 
194   gtk_widget_set_sensitive (GTK_WIDGET (self->listbox), state);
195 
196   if (state)
197     {
198       for (l = self->radio_group; l != NULL; l = l->next)
199         {
200           if (gtk_toggle_button_get_active (l->data))
201             {
202               xkb_option = (gchar *)g_object_get_data (l->data, "xkb-option");
203               set_xkb_option (self, xkb_option);
204               break;
205             }
206         }
207     }
208   else
209     {
210       set_xkb_option (self, NULL);
211     }
212 
213   return FALSE;
214 }
215 
216 static void
cc_xkb_modifier_dialog_finalize(GObject * object)217 cc_xkb_modifier_dialog_finalize (GObject *object)
218 {
219   CcXkbModifierDialog *self = (CcXkbModifierDialog *)object;
220 
221   g_clear_object (&self->input_source_settings);
222 
223   G_OBJECT_CLASS (cc_xkb_modifier_dialog_parent_class)->finalize (object);
224 }
225 
226 static void
cc_xkb_modifier_dialog_class_init(CcXkbModifierDialogClass * klass)227 cc_xkb_modifier_dialog_class_init (CcXkbModifierDialogClass *klass)
228 {
229   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
230   GObjectClass *object_class = G_OBJECT_CLASS (klass);
231 
232   object_class->finalize = cc_xkb_modifier_dialog_finalize;
233 
234   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-xkb-modifier-dialog.ui");
235 
236   gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, description_label);
237   gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, enabled_switch);
238   gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, listbox);
239   gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, switch_listbox);
240   gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, switch_row);
241 
242   gtk_widget_class_bind_template_callback (widget_class, enable_switch_changed_cb);
243 }
244 
245 static void
add_radio_buttons(CcXkbModifierDialog * self)246 add_radio_buttons (CcXkbModifierDialog *self)
247 {
248   GtkWidget *row, *radio_button, *label, *last_button = NULL;
249   CcXkbOption *options = self->modifier->options;
250   int i;
251 
252   for (i = 0; options[i].label && options[i].xkb_option; i++)
253     {
254       row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
255                           "visible", TRUE,
256                           "selectable", FALSE,
257                           NULL);
258       gtk_container_add (GTK_CONTAINER (self->listbox), row);
259 
260       radio_button = g_object_new (GTK_TYPE_RADIO_BUTTON,
261                                    "visible", TRUE,
262                                    "can_focus", TRUE,
263                                    "receives_default", FALSE,
264                                    "draw_indicator", TRUE,
265                                    NULL);
266       label = g_object_new (GTK_TYPE_LABEL,
267                             "visible", TRUE,
268                             "margin_left", 6,
269                             "label", g_dpgettext2 (NULL, "keyboard key", options[i].label),
270                             NULL);
271       gtk_container_add (GTK_CONTAINER (radio_button), label);
272       gtk_style_context_add_class (gtk_widget_get_style_context (radio_button), "xkb-option-button");
273       gtk_radio_button_join_group (GTK_RADIO_BUTTON (radio_button), GTK_RADIO_BUTTON (last_button));
274       g_object_set_data (G_OBJECT (radio_button), "xkb-option", options[i].xkb_option);
275       g_signal_connect_object (radio_button, "toggled", (GCallback)on_active_radio_changed_cb, self, G_CONNECT_SWAPPED);
276       gtk_container_add (GTK_CONTAINER (row), radio_button);
277 
278       last_button = radio_button;
279     }
280 
281   self->radio_group = NULL;
282   if (last_button != NULL)
283     self->radio_group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (last_button));
284 }
285 
286 static void
cc_xkb_modifier_dialog_init(CcXkbModifierDialog * self)287 cc_xkb_modifier_dialog_init (CcXkbModifierDialog *self)
288 {
289   g_autoptr(GtkCssProvider) provider = NULL;
290 
291   gtk_widget_init_template (GTK_WIDGET (self));
292 
293   provider = gtk_css_provider_new ();
294   gtk_css_provider_load_from_data (provider, custom_css, -1, NULL);
295 
296   gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
297                                              GTK_STYLE_PROVIDER (provider),
298                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1);
299 
300   self->modifier = NULL;
301 
302   self->input_source_settings = g_settings_new ("org.gnome.desktop.input-sources");
303   g_signal_connect_object (self->input_source_settings,
304                            "changed::xkb-options",
305                            G_CALLBACK (on_xkb_options_changed_cb),
306                            self, G_CONNECT_SWAPPED);
307 }
308 
309 CcXkbModifierDialog *
cc_xkb_modifier_dialog_new(GSettings * input_settings,const CcXkbModifier * modifier)310 cc_xkb_modifier_dialog_new (GSettings *input_settings,
311                             const CcXkbModifier *modifier)
312 {
313   CcXkbModifierDialog *self;
314 
315   self = g_object_new (CC_TYPE_XKB_MODIFIER_DIALOG,
316                        "use-header-bar", TRUE,
317                        NULL);
318   self->input_source_settings = g_object_ref (input_settings);
319 
320   self->modifier = modifier;
321   gtk_window_set_title (GTK_WINDOW (self), gettext (modifier->title));
322   hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self->switch_row), gettext (modifier->title));
323   gtk_label_set_markup (self->description_label, gettext (modifier->description));
324   gtk_widget_set_visible (GTK_WIDGET (self->switch_listbox), modifier->default_option == NULL);
325   add_radio_buttons (self);
326   update_active_radio (self);
327   gtk_widget_set_sensitive (GTK_WIDGET (self->listbox), gtk_switch_get_state (self->enabled_switch));
328 
329   return self;
330 }
331 
332 gboolean
xcb_modifier_transform_binding_to_label(GValue * value,GVariant * variant,gpointer user_data)333 xcb_modifier_transform_binding_to_label (GValue   *value,
334                                          GVariant *variant,
335                                          gpointer  user_data)
336 {
337   const CcXkbModifier *modifier = user_data;
338   const CcXkbOption *entry = NULL;
339   const char **items;
340   guint i;
341 
342   items = g_variant_get_strv (variant, NULL);
343 
344   for (i = 0; items != NULL && items[i] != NULL; i++)
345     {
346       entry = get_xkb_option_from_name (modifier, items[i]);
347       if (entry != NULL)
348         break;
349     }
350 
351   if (entry == NULL && modifier->default_option == NULL)
352     {
353       g_value_set_string (value, _("Disabled"));
354       return TRUE;
355     }
356   else if (entry == NULL)
357     {
358       entry = get_xkb_option_from_name(modifier, modifier->default_option);
359     }
360 
361   g_value_set_string (value,
362                       g_dpgettext2 (NULL, "keyboard key", entry->label));
363   return TRUE;
364 }
365