1 /*
2  * Copyright (C) 2019-2021 Alexandros Theodotou <alex at zrythm dot org>
3  *
4  * This file is part of Zrythm
5  *
6  * Zrythm is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Zrythm is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with Zrythm.  If not, see <https://www.gnu.org/licenses/>.
18  */
19 
20 #include "zrythm-config.h"
21 
22 #include <locale.h>
23 
24 #include "audio/engine.h"
25 #include "gui/widgets/main_window.h"
26 #include "gui/widgets/active_hardware_mb.h"
27 #include "gui/widgets/preferences.h"
28 #include "plugins/plugin_gtk.h"
29 #include "project.h"
30 #include "settings/settings.h"
31 #include "utils/flags.h"
32 #include "utils/localization.h"
33 #include "utils/gtk.h"
34 #include "utils/objects.h"
35 #include "utils/resources.h"
36 #include "utils/string.h"
37 #include "utils/ui.h"
38 #include "zrythm.h"
39 #include "zrythm_app.h"
40 
41 #include <gtk/gtk.h>
42 
43 #include <glib/gi18n.h>
44 
45 G_DEFINE_TYPE (
46   PreferencesWidget,
47   preferences_widget,
48   GTK_TYPE_DIALOG)
49 
50 typedef struct CallbackData
51 {
52   PreferencesWidget * preferences_widget;
53   SubgroupInfo *      info;
54   char *              key;
55 } CallbackData;
56 
57 #define KEY_IS(a,b,c) \
58   (string_is_equal (group, _(a)) && \
59   string_is_equal (subgroup, _(b)) && \
60   string_is_equal (key, c))
61 
62 static void
on_simple_string_entry_changed(GtkEditable * editable,CallbackData * data)63 on_simple_string_entry_changed (
64   GtkEditable *  editable,
65   CallbackData * data)
66 {
67   char * str =
68     gtk_editable_get_chars (editable, 0, -1);
69   g_return_if_fail (str);
70   g_settings_set_string (
71     data->info->settings, data->key, str);
72   g_free (str);
73 }
74 
75 static void
on_path_entry_changed(GtkEditable * editable,CallbackData * data)76 on_path_entry_changed (
77   GtkEditable *  editable,
78   CallbackData * data)
79 {
80   char * str =
81     gtk_editable_get_chars (editable, 0, -1);
82   g_return_if_fail (str);
83   char ** split_str =
84     g_strsplit (str, G_SEARCHPATH_SEPARATOR_S, 0);
85   g_settings_set_strv (
86     data->info->settings, data->key,
87     (const char * const *) split_str);
88   g_strfreev (split_str);
89   g_free (str);
90 }
91 
92 static void
on_enum_combo_box_active_changed(GtkComboBox * combo,CallbackData * data)93 on_enum_combo_box_active_changed (
94   GtkComboBox *  combo,
95   CallbackData * data)
96 {
97   g_settings_set_enum (
98     data->info->settings, data->key,
99     gtk_combo_box_get_active (combo));
100 }
101 
102 static void
on_string_combo_box_active_changed(GtkComboBoxText * combo,CallbackData * data)103 on_string_combo_box_active_changed (
104   GtkComboBoxText *  combo,
105   CallbackData * data)
106 {
107   g_settings_set_string (
108     data->info->settings, data->key,
109     gtk_combo_box_text_get_active_text (combo));
110 }
111 
112 static void
on_backends_combo_box_active_changed(GtkComboBox * combo,CallbackData * data)113 on_backends_combo_box_active_changed (
114   GtkComboBox *  combo,
115   CallbackData * data)
116 {
117   g_settings_set_enum (
118     data->info->settings, data->key,
119     atoi (gtk_combo_box_get_active_id (combo)));
120 }
121 
122 static void
on_file_set(GtkFileChooserButton * widget,CallbackData * data)123 on_file_set (
124   GtkFileChooserButton * widget,
125   CallbackData *         data)
126 {
127   GFile * file =
128     gtk_file_chooser_get_file (
129       GTK_FILE_CHOOSER (widget));
130   char * str =
131     g_file_get_path (file);
132   g_settings_set_string (
133     data->info->settings, data->key, str);
134   g_free (str);
135   g_object_unref (file);
136 }
137 
138 static void
font_scale_adjustment_changed(GtkAdjustment * adjustment,void * data)139 font_scale_adjustment_changed (
140   GtkAdjustment * adjustment,
141   void *          data)
142 {
143   double factor =
144     gtk_adjustment_get_value (adjustment);
145   zrythm_app_set_font_scale (zrythm_app, factor);
146 }
147 
148 static void
on_closure_notify_delete_data(CallbackData * data,GClosure * closure)149 on_closure_notify_delete_data (
150   CallbackData * data,
151   GClosure *     closure)
152 {
153   free (data);
154 }
155 
156 /** Path type. */
157 typedef enum PathType
158 {
159   /** Not a path. */
160   PATH_TYPE_NONE,
161 
162   /** Single entry separated by G_SEARCHPATH_SEPARATOR_S. */
163   PATH_TYPE_ENTRY,
164 
165   /** File chooser button. */
166   PATH_TYPE_FILE,
167 
168   /** File chooser button for directories. */
169   PATH_TYPE_DIRECTORY,
170 } PathType;
171 
172 /**
173  * Returns if the key is a path or not.
174  */
175 static PathType
get_path_type(const char * group,const char * subgroup,const char * key)176 get_path_type (
177   const char * group,
178   const char * subgroup,
179   const char * key)
180 {
181   if (KEY_IS ("General", "Paths", "zrythm-dir"))
182     {
183       return PATH_TYPE_DIRECTORY;
184     }
185   else if (
186     KEY_IS (
187       "Plugins", "Paths",
188       "vst-search-paths-windows") ||
189     KEY_IS (
190       "Plugins", "Paths", "sfz-search-paths") ||
191     KEY_IS (
192       "Plugins", "Paths", "sf2-search-paths"))
193     {
194       return PATH_TYPE_ENTRY;
195     }
196 
197   return PATH_TYPE_NONE;
198 }
199 
200 static bool
should_be_hidden(const char * group,const char * subgroup,const char * key)201 should_be_hidden (
202   const char * group,
203   const char * subgroup,
204   const char * key)
205 {
206   return
207 #ifndef _WOE32
208     KEY_IS (
209       "Plugins", "Paths",
210       "vst-search-paths-windows") ||
211 #endif
212 #ifndef HAVE_CARLA
213     KEY_IS (
214       "Plugins", "Paths", "sfz-search-paths") ||
215     KEY_IS (
216       "Plugins", "Paths", "sf2-search-paths") ||
217 #endif
218     (AUDIO_ENGINE->audio_backend !=
219        AUDIO_BACKEND_SDL &&
220      KEY_IS (
221        "General", "Engine",
222        "sdl-audio-device-name")) ||
223     (!audio_backend_is_rtaudio (
224        AUDIO_ENGINE->audio_backend) &&
225      KEY_IS (
226        "General", "Engine",
227        "rtaudio-audio-device-name")) ||
228     (AUDIO_ENGINE->audio_backend ==
229        AUDIO_BACKEND_JACK &&
230      KEY_IS (
231        "General", "Engine", "sample-rate")) ||
232     (AUDIO_ENGINE->audio_backend ==
233        AUDIO_BACKEND_JACK &&
234      KEY_IS (
235        "General", "Engine", "buffer-size"));
236 }
237 
238 static void
get_range_vals(GVariant * range,GVariant * current_var,const GVariantType * type,double * lower,double * upper,double * current)239 get_range_vals (
240   GVariant * range,
241   GVariant * current_var,
242   const GVariantType * type,
243   double *   lower,
244   double *   upper,
245   double *   current)
246 {
247   GVariant * range_vals =
248     g_variant_get_child_value (range, 1);
249   range_vals =
250     g_variant_get_child_value (range_vals, 0);
251   GVariant * lower_var =
252     g_variant_get_child_value (range_vals, 0);
253   GVariant * upper_var =
254     g_variant_get_child_value (range_vals, 1);
255 
256 #define TYPE_EQUALS(type2) \
257   string_is_equal ( \
258     (const char *) type,  \
259     (const char *) G_VARIANT_TYPE_##type2)
260 
261   if (TYPE_EQUALS (INT32))
262     {
263       *lower =
264         (double) g_variant_get_int32 (lower_var);
265       *upper =
266         (double) g_variant_get_int32 (upper_var);
267       *current =
268         (double)
269         g_variant_get_int32 (current_var);
270     }
271   else if (TYPE_EQUALS (UINT32))
272     {
273       *lower =
274         (double)
275         g_variant_get_uint32 (lower_var);
276       *upper =
277         (double)
278         g_variant_get_uint32 (upper_var);
279       *current =
280         (double)
281         g_variant_get_uint32 (current_var);
282     }
283   else if (TYPE_EQUALS (DOUBLE))
284     {
285       *lower =
286         g_variant_get_double (lower_var);
287       *upper =
288         (double)
289         g_variant_get_double (upper_var);
290       *current =
291         (double)
292         g_variant_get_double (current_var);
293     }
294 #undef TYPE_EQUALS
295 
296   g_variant_unref (range_vals);
297   g_variant_unref (lower_var);
298   g_variant_unref (upper_var);
299 }
300 
301 static GtkWidget *
make_control(PreferencesWidget * self,int group_idx,int subgroup_idx,const char * key)302 make_control (
303   PreferencesWidget * self,
304   int                 group_idx,
305   int                 subgroup_idx,
306   const char *        key)
307 {
308   SubgroupInfo * info =
309     &self->subgroup_infos[group_idx][subgroup_idx];
310   const char * group = info->group_name;
311   const char * subgroup = info->name;
312   GSettingsSchemaKey * schema_key =
313     g_settings_schema_get_key (
314       info->schema, key);
315   const GVariantType * type =
316     g_settings_schema_key_get_value_type (
317       schema_key);
318   GVariant * current_var =
319     g_settings_get_value (info->settings, key);
320   GVariant * range =
321     g_settings_schema_key_get_range (
322       schema_key);
323 
324 #if 0
325   g_message ("%s",
326     g_variant_get_type_string (current_var));
327 #endif
328 
329 #define TYPE_EQUALS(type2) \
330   string_is_equal ( \
331     (const char *) type,  \
332     (const char *) G_VARIANT_TYPE_##type2)
333 
334   GtkWidget * widget = NULL;
335   if (KEY_IS (
336         "General", "Engine",
337         "rtaudio-audio-device-name") ||
338       KEY_IS (
339         "General", "Engine",
340         "sdl-audio-device-name"))
341     {
342       widget = gtk_combo_box_text_new ();
343       ui_setup_device_name_combo_box (
344         GTK_COMBO_BOX_TEXT (widget));
345       CallbackData * data =
346         object_new (CallbackData);
347       data->info = info;
348       data->preferences_widget = self;
349       data->key = g_strdup (key);
350       g_signal_connect_data (
351         G_OBJECT (widget), "changed",
352         G_CALLBACK (
353           on_string_combo_box_active_changed),
354         data,
355         (GClosureNotify)
356           on_closure_notify_delete_data,
357         G_CONNECT_AFTER);
358     }
359   else if (KEY_IS (
360              "General", "Engine", "midi-backend"))
361     {
362       widget = gtk_combo_box_new ();
363       ui_setup_midi_backends_combo_box (
364         GTK_COMBO_BOX (widget));
365       CallbackData * data =
366         object_new (CallbackData);
367       data->info = info;
368       data->preferences_widget = self;
369       data->key = g_strdup (key);
370       g_signal_connect_data (
371         G_OBJECT (widget), "changed",
372         G_CALLBACK (
373           on_backends_combo_box_active_changed),
374         data,
375         (GClosureNotify)
376           on_closure_notify_delete_data,
377         G_CONNECT_AFTER);
378     }
379   else if (KEY_IS (
380              "General", "Engine", "audio-backend"))
381     {
382       widget = gtk_combo_box_new ();
383       ui_setup_audio_backends_combo_box (
384         GTK_COMBO_BOX (widget));
385       CallbackData * data =
386         object_new (CallbackData);
387       data->info = info;
388       data->preferences_widget = self;
389       data->key = g_strdup (key);
390       g_signal_connect_data (
391         G_OBJECT (widget), "changed",
392         G_CALLBACK (
393           on_backends_combo_box_active_changed),
394         data,
395         (GClosureNotify)
396           on_closure_notify_delete_data,
397         G_CONNECT_AFTER);
398     }
399   else if (KEY_IS (
400         "General", "Engine",
401         "audio-inputs"))
402     {
403       widget =
404         g_object_new (
405           ACTIVE_HARDWARE_MB_WIDGET_TYPE, NULL);
406       active_hardware_mb_widget_setup (
407         Z_ACTIVE_HARDWARE_MB_WIDGET (widget),
408         F_INPUT, F_NOT_MIDI, S_P_GENERAL_ENGINE,
409         "audio-inputs");
410     }
411   else if (KEY_IS (
412         "General", "Engine",
413         "midi-controllers"))
414     {
415       widget =
416         g_object_new (
417           ACTIVE_HARDWARE_MB_WIDGET_TYPE, NULL);
418       active_hardware_mb_widget_setup (
419         Z_ACTIVE_HARDWARE_MB_WIDGET (widget),
420         F_INPUT, F_MIDI, S_P_GENERAL_ENGINE,
421         "midi-controllers");
422     }
423   else if (KEY_IS ("UI", "General", "font-scale"))
424     {
425       double lower = 0.f, upper = 1.f, current = 0.f;
426       get_range_vals (
427         range, current_var, type, &lower, &upper, &current);
428       widget =
429         gtk_box_new (
430           GTK_ORIENTATION_HORIZONTAL, 2);
431       gtk_widget_set_visible (widget, true);
432       GtkWidget * scale =
433         gtk_scale_new_with_range (
434           GTK_ORIENTATION_HORIZONTAL, lower, upper,
435           0.1);
436       gtk_widget_set_visible (scale, true);
437       gtk_widget_set_hexpand (scale, true);
438       gtk_scale_add_mark (
439         GTK_SCALE (scale), 1.0, GTK_POS_TOP, NULL);
440       gtk_container_add (
441         GTK_CONTAINER (widget), scale);
442       GtkAdjustment * adj =
443         gtk_range_get_adjustment (
444           GTK_RANGE (scale));
445       gtk_adjustment_set_value (adj, current);
446       g_settings_bind (
447         info->settings, key, adj, "value",
448         G_SETTINGS_BIND_DEFAULT);
449       g_signal_connect (
450         adj, "value-changed",
451         G_CALLBACK (font_scale_adjustment_changed),
452         NULL);
453     }
454   else if (TYPE_EQUALS (BOOLEAN))
455     {
456       widget = gtk_switch_new ();
457       g_settings_bind (
458         info->settings, key, widget, "state",
459         G_SETTINGS_BIND_DEFAULT);
460     }
461   else if (TYPE_EQUALS (INT32) ||
462            TYPE_EQUALS (UINT32) ||
463            TYPE_EQUALS (DOUBLE))
464     {
465       double lower = 0.f, upper = 1.f, current = 0.f;
466       get_range_vals (
467         range, current_var, type, &lower, &upper, &current);
468       GtkAdjustment * adj =
469         gtk_adjustment_new (
470           current, lower, upper, 1.0, 1.0, 1.0);
471       widget =
472         gtk_spin_button_new (
473           adj, 1, TYPE_EQUALS (DOUBLE) ? 3 : 0);
474       g_settings_bind (
475         info->settings, key, widget, "value",
476         G_SETTINGS_BIND_DEFAULT);
477     }
478   else if (TYPE_EQUALS (STRING))
479     {
480       PathType path_type =
481         get_path_type (
482           info->group_name, info->name, key);
483       if (path_type == PATH_TYPE_DIRECTORY ||
484           path_type == PATH_TYPE_FILE)
485         {
486           widget =
487             gtk_file_chooser_button_new (
488               path_type == PATH_TYPE_DIRECTORY ?
489               _("Select a folder") :
490               _("Select a file"),
491               path_type == PATH_TYPE_DIRECTORY ?
492               GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER :
493               GTK_FILE_CHOOSER_ACTION_OPEN);
494           char * path =
495             g_settings_get_string (
496               info->settings, key);
497           gtk_file_chooser_set_current_folder (
498             GTK_FILE_CHOOSER (widget),
499             path);
500           g_free (path);
501           CallbackData * data =
502             object_new (CallbackData);
503           data->info = info;
504           data->preferences_widget = self;
505           data->key = g_strdup (key);
506           g_signal_connect_data (
507             G_OBJECT (widget), "file-set",
508             G_CALLBACK (on_file_set),
509             data,
510             (GClosureNotify)
511               on_closure_notify_delete_data,
512             G_CONNECT_AFTER);
513         }
514       else if (path_type == PATH_TYPE_NONE)
515         {
516           /* map enums */
517           const char ** strv = NULL;
518           const cyaml_strval_t * cyaml_strv = NULL;
519           size_t size = 0;
520 
521 #define SET_STRV_IF_MATCH(a,b,c,arr_name) \
522   if (KEY_IS (a,b,c)) \
523     { \
524       strv = arr_name; \
525       size = G_N_ELEMENTS (arr_name); \
526     }
527 
528 #define SET_STRV_IF_MATCH_W_COUNT(a,b,c,arr_name, \
529   count) \
530   if (KEY_IS (a,b,c)) \
531     { \
532       strv = arr_name; \
533       size = count; \
534     }
535 
536 #define SET_STRV_FROM_CYAML_IF_MATCH( \
537   a,b,c,arr_name) \
538   if (KEY_IS (a,b,c)) \
539     { \
540       cyaml_strv = arr_name; \
541       size = G_N_ELEMENTS (arr_name); \
542     }
543 
544           SET_STRV_IF_MATCH (
545             "General", "Engine", "audio-backend",
546             audio_backend_str);
547           SET_STRV_IF_MATCH (
548             "General", "Engine", "midi-backend",
549             midi_backend_str);
550           SET_STRV_IF_MATCH (
551             "General", "Engine", "sample-rate",
552             sample_rate_str);
553           SET_STRV_IF_MATCH (
554             "General", "Engine", "buffer-size",
555             buffer_size_str);
556           SET_STRV_FROM_CYAML_IF_MATCH (
557             "Editing", "Audio", "fade-algorithm",
558             curve_algorithm_strings);
559           SET_STRV_FROM_CYAML_IF_MATCH (
560             "Editing", "Automation",
561             "curve-algorithm",
562             curve_algorithm_strings);
563           SET_STRV_IF_MATCH_W_COUNT (
564             "UI", "General", "language",
565             localization_get_language_strings_w_codes (),
566             NUM_LL_LANGUAGES);
567           SET_STRV_IF_MATCH (
568             "UI", "General", "graphic-detail",
569             ui_detail_str);
570           SET_STRV_IF_MATCH (
571             "DSP", "Pan", "pan-algorithm",
572             pan_algorithm_str);
573           SET_STRV_IF_MATCH (
574             "DSP", "Pan", "pan-law",
575             pan_law_str);
576 
577 #undef SET_STRV_IF_MATCH
578 
579           if (strv || cyaml_strv)
580             {
581               widget =
582                 gtk_combo_box_text_new ();
583               for (size_t i = 0; i < size; i++)
584                 {
585                   if (cyaml_strv)
586                     {
587                       gtk_combo_box_text_append (
588                         GTK_COMBO_BOX_TEXT (widget),
589                         cyaml_strv[i].str,
590                         _(cyaml_strv[i].str));
591                     }
592                   else if (strv)
593                     {
594                       gtk_combo_box_text_append (
595                         GTK_COMBO_BOX_TEXT (widget),
596                         strv[i], _(strv[i]));
597                     }
598                 }
599               gtk_combo_box_set_active (
600                 GTK_COMBO_BOX (widget),
601                 g_settings_get_enum (
602                   info->settings, key));
603               CallbackData * data =
604                 object_new (CallbackData);
605               data->info = info;
606               data->preferences_widget = self;
607               data->key = g_strdup (key);
608               g_signal_connect_data (
609                 G_OBJECT (widget), "changed",
610                 G_CALLBACK (
611                   on_enum_combo_box_active_changed),
612                 data,
613                 (GClosureNotify)
614                   on_closure_notify_delete_data,
615                 G_CONNECT_AFTER);
616             }
617           /* else if not a string array */
618           else
619             {
620               /* create basic string entry control */
621               widget = gtk_entry_new ();
622               char * current_val =
623                 g_settings_get_string (
624                   info->settings, key);
625               gtk_entry_set_text (
626                 GTK_ENTRY (widget), current_val);
627               g_free (current_val);
628               CallbackData * data =
629                 object_new (CallbackData);
630               data->info = info;
631               data->preferences_widget = self;
632               data->key = g_strdup (key);
633               g_signal_connect_data (
634                 G_OBJECT (widget), "changed",
635                 G_CALLBACK (
636                   on_simple_string_entry_changed),
637                 data,
638                 (GClosureNotify)
639                   on_closure_notify_delete_data,
640                 G_CONNECT_AFTER);
641             }
642         }
643     }
644   else if (TYPE_EQUALS (STRING_ARRAY))
645     {
646       if (get_path_type (
647             info->group_name, info->name, key) ==
648             PATH_TYPE_ENTRY)
649         {
650           widget = gtk_entry_new ();
651           char ** paths =
652             g_settings_get_strv (
653               info->settings, key);
654           char * joined_str =
655             g_strjoinv (G_SEARCHPATH_SEPARATOR_S, paths);
656           gtk_entry_set_text (
657             GTK_ENTRY (widget), joined_str);
658           g_free (joined_str);
659           g_strfreev (paths);
660           CallbackData * data =
661             object_new (CallbackData);
662           data->info = info;
663           data->preferences_widget = self;
664           data->key = g_strdup (key);
665           g_signal_connect_data (
666             G_OBJECT (widget), "changed",
667             G_CALLBACK (on_path_entry_changed),
668             data,
669             (GClosureNotify)
670               on_closure_notify_delete_data,
671             G_CONNECT_AFTER);
672         }
673     }
674 #if 0
675   else if (string_is_equal (
676              g_variant_get_type_string (
677                current_var), "ai"))
678     {
679     }
680 #endif
681 
682 #undef TYPE_EQUALS
683 
684   g_warn_if_fail (widget);
685 
686   return widget;
687 }
688 
689 static void
add_subgroup(PreferencesWidget * self,int group_idx,int subgroup_idx,GtkSizeGroup * size_group)690 add_subgroup (
691   PreferencesWidget * self,
692   int                 group_idx,
693   int                 subgroup_idx,
694   GtkSizeGroup *      size_group)
695 {
696   SubgroupInfo * info =
697     &self->subgroup_infos[group_idx][subgroup_idx];
698 
699   const char * localized_subgroup_name =
700     info->name;
701   g_message (
702     "adding subgroup %s (%s)",
703     info->name, localized_subgroup_name);
704 
705   /* create a section for the subgroup */
706   GtkWidget * page_box =
707     gtk_notebook_get_nth_page (
708       self->group_notebook, info->group_idx);
709   GtkWidget * label =
710     plugin_gtk_new_label (
711       localized_subgroup_name, true, false,
712       0.f, 0.5f);
713   gtk_widget_set_visible (label, true);
714   gtk_container_add (
715     GTK_CONTAINER (page_box), label);
716 
717   char ** keys =
718     g_settings_schema_list_keys (info->schema);
719   int i = 0;
720   int num_controls = 0;
721   char * key;
722   while ((key = keys[i++]))
723     {
724       GSettingsSchemaKey * schema_key =
725         g_settings_schema_get_key (
726           info->schema, key);
727       /* note: this is already translated */
728       const char * summary =
729         g_settings_schema_key_get_summary (
730           schema_key);
731       /* note: this is already translated */
732       const char * description =
733         g_settings_schema_key_get_description (
734           schema_key);
735 
736       if (string_is_equal (key, "info") ||
737           should_be_hidden (
738             info->group_name, info->name, key))
739         continue;
740 
741       g_message ("adding control for %s", key);
742 
743       /* create a box to add controls */
744       GtkWidget * box =
745         gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
746       gtk_widget_set_visible (box, true);
747       gtk_container_add (
748         GTK_CONTAINER (page_box), box);
749 
750       /* add label */
751       GtkWidget * lbl =
752         plugin_gtk_new_label (
753           summary, false, false, 1.f, 0.5f);
754       gtk_widget_set_visible (lbl, true);
755       gtk_container_add (
756         GTK_CONTAINER (box), lbl);
757       gtk_size_group_add_widget (
758         size_group, lbl);
759 
760       /* add control */
761       GtkWidget * widget =
762         make_control (
763           self, group_idx, subgroup_idx, key);
764       if (widget)
765         {
766           gtk_widget_set_visible (widget, true);
767           if (GTK_IS_SWITCH (widget))
768             {
769               gtk_widget_set_halign (
770                 widget, GTK_ALIGN_START);
771             }
772           else
773             {
774               gtk_widget_set_hexpand (widget, true);
775             }
776           gtk_widget_set_tooltip_text (
777             widget, description);
778           gtk_container_add (
779             GTK_CONTAINER (box), widget);
780           num_controls++;
781         }
782       else
783         {
784           g_warning ("no widget for %s", key);
785         }
786     }
787 
788   /* Remove label if no controls added */
789   if (num_controls == 0)
790     {
791       gtk_container_remove (
792         GTK_CONTAINER (page_box), label);
793     }
794 }
795 
796 static void
add_group(PreferencesWidget * self,int group_idx)797 add_group (
798   PreferencesWidget * self,
799   int                 group_idx)
800 {
801   GSettingsSchemaSource * source =
802     g_settings_schema_source_get_default ();
803   char ** non_relocatable;
804   g_settings_schema_source_list_schemas (
805     source, 1, &non_relocatable, NULL);
806 
807   /* loop once to get the max subgroup index and
808    * group name */
809   char * schema_str;
810   const char * group_name = NULL;
811   const char * subgroup_name = NULL;
812   int i = 0;
813   int max_subgroup_idx = 0;
814   while ((schema_str = non_relocatable[i++]))
815     {
816       if (!string_contains_substr (
817             schema_str,
818             GSETTINGS_ZRYTHM_PREFIX ".preferences"))
819         continue;
820 
821       /* get the preferences.x.y schema */
822       GSettingsSchema * schema =
823         g_settings_schema_source_lookup (
824           source, schema_str, 1);
825       g_return_if_fail (schema);
826 
827       GSettings * settings =
828         g_settings_new (schema_str);
829       GVariant * info_val =
830         g_settings_get_value (
831           settings, "info");
832       int this_group_idx =
833         (int)
834         g_variant_get_int32 (
835           g_variant_get_child_value (
836             info_val, 0));
837 
838       if (this_group_idx != group_idx)
839         continue;
840 
841       GSettingsSchemaKey * info_key =
842         g_settings_schema_get_key (
843           schema, "info");
844       /* note: this is already translated */
845       group_name =
846         g_settings_schema_key_get_summary (info_key);
847       /* note: this is already translated */
848       subgroup_name =
849         g_settings_schema_key_get_description (
850           info_key);
851 
852       /* get max subgroup index */
853       int subgroup_idx =
854         (int)
855         g_variant_get_int32 (
856           g_variant_get_child_value (
857             info_val, 1));
858       SubgroupInfo * nfo =
859         &self->subgroup_infos[
860           group_idx][subgroup_idx];
861       nfo->schema = schema;
862       nfo->settings = settings;
863       nfo->group_name = group_name;
864       nfo->name = subgroup_name;
865       nfo->group_idx = group_idx;
866       nfo->subgroup_idx = subgroup_idx;
867       if (subgroup_idx > max_subgroup_idx)
868         max_subgroup_idx = subgroup_idx;
869     }
870 
871   const char * localized_group_name = group_name;
872   g_message (
873     "adding group %s (%s)",
874     group_name, localized_group_name);
875 
876   /* create a page for the group */
877   GtkWidget * box =
878     gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
879   gtk_widget_set_visible (box, true);
880   z_gtk_widget_set_margin (box, 4);
881   gtk_notebook_append_page (
882     self->group_notebook, box,
883     plugin_gtk_new_label (
884       localized_group_name, true, false, 0.f, 0.5f));
885 
886   /* create a sizegroup for the labels */
887   GtkSizeGroup * size_group =
888     gtk_size_group_new (
889       GTK_SIZE_GROUP_HORIZONTAL);
890 
891   /* add each subgroup */
892   for (int j = 0; j <= max_subgroup_idx; j++)
893     {
894       add_subgroup (self, group_idx, j, size_group);
895     }
896 }
897 
898 static void
on_window_closed(GtkWidget * object,PreferencesWidget * self)899 on_window_closed (
900   GtkWidget *object,
901   PreferencesWidget * self)
902 {
903   GtkWidget * dialog =
904     gtk_message_dialog_new_with_markup (
905       GTK_WINDOW (MAIN_WINDOW),
906       GTK_DIALOG_MODAL |
907         GTK_DIALOG_DESTROY_WITH_PARENT,
908       GTK_MESSAGE_INFO,
909       GTK_BUTTONS_OK,
910       _("Some changes will only take "
911       "effect after you restart %s"),
912       PROGRAM_NAME);
913   gtk_window_set_transient_for (
914     GTK_WINDOW (dialog),
915     GTK_WINDOW (self));
916   gtk_dialog_run (GTK_DIALOG (dialog));
917   gtk_widget_destroy (GTK_WIDGET (dialog));
918 
919   MAIN_WINDOW->preferences_opened = false;
920 }
921 
922 /**
923  * Sets up the preferences widget.
924  */
925 PreferencesWidget *
preferences_widget_new()926 preferences_widget_new ()
927 {
928   PreferencesWidget * self =
929     g_object_new (
930       PREFERENCES_WIDGET_TYPE,
931       "title", _("Preferences"),
932       NULL);
933 
934   for (int i = 0; i <= 6; i++)
935     {
936       add_group (self, i);
937     }
938 
939   g_signal_connect (
940     G_OBJECT (self), "destroy",
941     G_CALLBACK (on_window_closed), self);
942 
943   return self;
944 }
945 
946 static void
preferences_widget_class_init(PreferencesWidgetClass * _klass)947 preferences_widget_class_init (
948   PreferencesWidgetClass * _klass)
949 {
950 }
951 
952 static void
preferences_widget_init(PreferencesWidget * self)953 preferences_widget_init (
954   PreferencesWidget * self)
955 {
956   self->group_notebook =
957     GTK_NOTEBOOK (gtk_notebook_new ());
958   gtk_widget_set_visible (
959     GTK_WIDGET (self->group_notebook), true);
960   gtk_container_add (
961     GTK_CONTAINER (
962       gtk_dialog_get_content_area (
963         GTK_DIALOG (self))),
964     GTK_WIDGET (self->group_notebook));
965 }
966