1/* Copyright 2014-2020 GoForIt! developers
2*
3* This file is part of GoForIt!.
4*
5* GoForIt! is free software: you can redistribute it
6* and/or modify it under the terms of version 3 of the
7* GNU General Public License as published by the Free Software Foundation.
8*
9* GoForIt! is distributed in the hope that it will be
10* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12* Public License for more details.
13*
14* You should have received a copy of the GNU General Public License along
15* with GoForIt!. If not, see http://www.gnu.org/licenses/.
16*/
17
18/**
19 * A class that handles access to settings in a transparent manner.
20 * Its main motivation is the option of easily replacing Glib.KeyFile with
21 * another settings storage mechanism in the future.
22 */
23private class GOFI.SettingsManager : Object {
24
25    private GLib.Settings _settings;
26    private GLib.Settings timer_settings;
27    private GLib.Settings saved_state;
28
29    /*
30     * A list of settings values with their corresponding access methods.
31     * The "heart" of the SettingsManager class.
32     */
33
34    const string ID_TIMER = GOFI.APP_ID + ".timer";
35    const string KEY_TASK_DURATION = "task-duration";
36    const string KEY_BREAK_DURATION = "break-duration";
37    const string KEY_LBREAK_DURATION = "long-break-duration";
38    const string KEY_POMODORO_PERIOD = "pomodoro-period";
39    const string KEY_REMINDER_TIME = "reminder-time";
40    const string KEY_TIMER_MODE = "timer-mode";
41    const string KEY_SCHEDULE = "schedule";
42    const string KEY_RESUME_TASKS_AFTER_BREAK = "resume-tasks-after-break";
43    const string KEY_RESET_TIMER_ON_TASK_SWITCH = "reset-timer-on-task-switch";
44
45    const string ID_GENERAL = GOFI.APP_ID + ".settings";
46    const string KEY_TASKS_ON_TOP = "new-tasks-on-top";
47    const string KEY_ADD_DEFAULT_TODOS = "add-default-todos";
48    const string KEY_LISTS = "lists";
49    const string KEY_USE_HEADER_BAR = "use-header-bar";
50    const string KEY_COLOR_SCHEME = "color-scheme";
51    const string KEY_SMALL_ICONS = "small-toolbar-icons";
52    const string KEY_SWITCHER_USE_ICONS = "switcher-use-icons";
53
54    const string ID_SAVED_STATE = GOFI.APP_ID + ".saved-state";
55    const string KEY_WINDOW_POS = "window-position";
56    const string KEY_WINDOW_SIZE = "window-size";
57    const string KEY_LAST_LIST = "last-loaded-list";
58
59    // Whether or not GoForIt! has been started for the first time
60    public bool first_start = false;
61    public bool performed_migration = false;
62
63    /*---GROUP:Todo.txt-------------------------------------------------------*/
64    public string todo_txt_location {
65        owned get { return ""; }
66    }
67    /*---GROUP:Behavior-------------------------------------------------------*/
68    public bool new_tasks_on_top {
69        public get;
70        public set;
71    }
72    public bool add_default_todos {
73        public get;
74        public set;
75    }
76    /*---GROUP:Timer----------------------------------------------------------*/
77
78
79    public int task_duration {
80        get;
81        set;
82    }
83    public int break_duration {
84        get;
85        set;
86    }
87    public int long_break_duration {
88        get;
89        set;
90    }
91    public int pomodoro_period {
92        get;
93        set;
94    }
95
96    public int reminder_time {
97        get;
98        set;
99    }
100    public bool reminder_active {
101        get {
102            return (reminder_time > 0);
103        }
104    }
105
106    public TimerMode timer_mode {
107        public get;
108        public set;
109    }
110
111    public Schedule schedule {
112        get {
113            return _schedule;
114        }
115        set {
116            _schedule.import_raw (value.export_raw ());
117            save_schedule ();
118            timer_duration_changed ();
119        }
120    }
121    Schedule _schedule;
122
123    public bool resume_tasks_after_break {
124        get;
125        set;
126    }
127
128    public bool reset_timer_on_task_switch {
129        get;
130        set;
131    }
132
133    /*---GROUP:UI-------------------------------------------------------------*/
134    public bool use_header_bar {
135        get {
136            switch (prefers_header_bar) {
137                case OverrideBool.TRUE:
138                    return true;
139                case OverrideBool.FALSE:
140                    return false;
141                default:
142                    return GOFI.Utils.desktop_hb_status.use_feature (true);
143            }
144        }
145        set {
146            if (value) {
147                prefers_header_bar = OverrideBool.TRUE;
148            } else {
149                prefers_header_bar = OverrideBool.FALSE;
150            }
151        }
152    }
153    public OverrideBool prefers_header_bar {
154        get;
155        set;
156    }
157    public ColorScheme color_scheme {
158        get;
159        set;
160    }
161    public bool use_dark_theme {
162        get {
163            switch (color_scheme) {
164                case ColorScheme.LIGHT:
165                    return false;
166                case ColorScheme.DARK:
167                    return true;
168                default:
169                    return system_theme_is_dark;
170            }
171        }
172    }
173    public bool system_theme_is_dark {
174        get;
175        set;
176    }
177    public Gtk.IconSize toolbar_icon_size {
178        get {
179            if (use_small_toolbar_icons) {
180                return Gtk.IconSize.SMALL_TOOLBAR;
181            }
182            return Gtk.IconSize.LARGE_TOOLBAR;
183        }
184    }
185    public bool use_small_toolbar_icons {
186        get;
187        set;
188    }
189    public bool switcher_use_icons {
190        get;
191        set;
192    }
193    /*---GROUP:LISTS----------------------------------------------------------*/
194    public List<ListIdentifier?> lists {
195        owned get {
196            List<ListIdentifier?> identifiers = new List<ListIdentifier?> ();
197
198            var lists_value = _settings.get_value (KEY_LISTS);
199            var n_lists = lists_value.n_children ();
200
201            for (size_t i = 0; i < n_lists; i++) {
202                string provider, id;
203                lists_value.get_child (i, "(ss)", out provider, out id);
204                if (provider != "" && id != "") {
205                    identifiers.prepend (new ListIdentifier (provider, id));
206                }
207            }
208            return identifiers;
209        }
210        set {
211            Variant[] _lists = {};
212            foreach (unowned ListIdentifier identifier in value) {
213                _lists += new Variant.tuple ({
214                    new Variant.string (identifier.provider),
215                    new Variant.string (identifier.id)
216                });
217            }
218            _settings.set_value (KEY_LISTS, new Variant.array (new VariantType ("(ss)"), _lists));
219        }
220    }
221
222    /*---Saved state----------------------------------------------------------*/
223    public void set_window_position (int x, int y) {
224        saved_state.set (KEY_WINDOW_POS, "(ii)", x, y);
225    }
226    public void get_window_position (out int x, out int y) {
227        saved_state.get (KEY_WINDOW_POS, "(ii)", out x, out y);
228    }
229    public void set_window_size (int width, int height) {
230        saved_state.set (KEY_WINDOW_SIZE, "(ii)", width, height);
231    }
232    public void get_window_size (out int width, out int height) {
233        saved_state.get (KEY_WINDOW_SIZE, "(ii)", out width, out height);
234    }
235    public ListIdentifier? list_last_loaded {
236        owned get {
237            string provider, id;
238            saved_state.get (KEY_LAST_LIST, "(ss)", out provider, out id);
239            if (provider != "" && id != "") {
240                return new ListIdentifier (provider, id);
241            }
242            return null;
243        }
244        set {
245            if (value == null) {
246                saved_state.set (KEY_LAST_LIST, "(ss)", "", "");
247            } else {
248                saved_state.set (KEY_LAST_LIST, "(ss)", value.provider, value.id);
249            }
250        }
251    }
252
253    /* Signals */
254    public signal void todo_txt_location_changed ();
255    public signal void timer_duration_changed ();
256    public signal void use_dark_theme_changed (bool use_dark);
257    public signal void toolbar_icon_size_changed (Gtk.IconSize size);
258    public signal void switcher_use_icons_changed (bool use_icons);
259
260    public SettingsManager () {
261        init_with_backend (null);
262    }
263
264    private void init_with_backend (GLib.SettingsBackend? backend) {
265        _schedule = new Schedule ();
266        if (backend != null) {
267            _settings = new GLib.Settings.with_backend (ID_GENERAL, backend);
268            timer_settings = new GLib.Settings.with_backend (ID_TIMER, backend);
269            saved_state = new GLib.Settings.with_backend (ID_SAVED_STATE, backend);
270        } else {
271            _settings = new GLib.Settings (ID_GENERAL);
272            timer_settings = new GLib.Settings (ID_TIMER);
273            saved_state = new GLib.Settings (ID_SAVED_STATE);
274        }
275
276        bind_settings ();
277        perform_migration ();
278    }
279
280// Broken due to bad gio vapi bindings (vala version < 0.50)
281/*
282    public SettingsManager.key_file_backend (string path) {
283        var key_file_backend = GLib.SettingsBackend.keyfile_settings_backend_new (path, "/", null);
284        init_with_backend (key_file_backend);
285    }
286*/
287
288    private void bind_settings () {
289        var sbf = GLib.SettingsBindFlags.DEFAULT;
290        _settings.bind (KEY_TASKS_ON_TOP, this, "new_tasks_on_top", sbf);
291        _settings.bind (KEY_ADD_DEFAULT_TODOS, this, "add_default_todos", sbf);
292        _settings.bind (KEY_SWITCHER_USE_ICONS, this, "switcher_use_icons", sbf);
293        _settings.bind (KEY_SMALL_ICONS, this, "use_small_toolbar_icons", sbf);
294        _settings.bind (KEY_COLOR_SCHEME, this, "color_scheme", sbf);
295        _settings.bind (KEY_USE_HEADER_BAR, this, "prefers_header_bar", sbf);
296
297        timer_settings.bind (KEY_REMINDER_TIME, this, "reminder_time", sbf);
298        timer_settings.bind (KEY_TIMER_MODE, this, "timer_mode", sbf);
299        timer_settings.bind (KEY_RESUME_TASKS_AFTER_BREAK, this, "resume_tasks_after_break", sbf);
300        timer_settings.bind (KEY_RESET_TIMER_ON_TASK_SWITCH, this, "reset_timer_on_task_switch", sbf);
301        timer_settings.bind (KEY_TASK_DURATION, this, "task_duration", sbf);
302        timer_settings.bind (KEY_BREAK_DURATION, this, "break_duration", sbf);
303        timer_settings.bind (KEY_LBREAK_DURATION, this, "long_break_duration", sbf);
304        timer_settings.bind (KEY_POMODORO_PERIOD, this, "pomodoro_period", sbf);
305
306        if (timer_mode == TimerMode.CUSTOM) {
307            restore_saved_schedule ();
308        } else {
309            build_schedule ();
310        }
311
312        this.notify.connect (on_property_changed);
313
314#if USE_GRANITE
315        read_granite_prefers_color_scheme ();
316        Granite.Settings.get_default ().notify["prefers-color-scheme"]
317            .connect (read_granite_prefers_color_scheme);
318#else
319        var gtk_settings = Gtk.Settings.get_default ();
320        system_theme_is_dark = gtk_settings.gtk_application_prefer_dark_theme;
321#endif
322    }
323
324#if USE_GRANITE
325    private void read_granite_prefers_color_scheme () {
326        switch (Granite.Settings.get_default ().prefers_color_scheme) {
327            case Granite.Settings.ColorScheme.DARK:
328                system_theme_is_dark = true;
329                break;
330            default:
331                system_theme_is_dark = false;
332                break;
333        }
334    }
335#endif
336
337    private void on_property_changed (GLib.ParamSpec pspec) {
338        switch (pspec.name) {
339            case "task-duration":
340            case "break-duration":
341            case "long-break-duration":
342            case "pomodoro-period":
343                build_schedule ();
344                timer_duration_changed ();
345                break;
346            case "timer-mode":
347                if (timer_mode == TimerMode.CUSTOM) {
348                    save_schedule ();
349                }
350                break;
351            case "color-scheme":
352                use_dark_theme_changed (use_dark_theme);
353                break;
354            case "switcher-use-icons":
355                switcher_use_icons_changed (switcher_use_icons);
356                break;
357            case "use-small-toolbar-icons":
358                toolbar_icon_size_changed (toolbar_icon_size);
359                break;
360            case "system-theme-is-dark":
361                if (color_scheme == ColorScheme.DEFAULT) {
362                    use_dark_theme_changed (system_theme_is_dark);
363                }
364                break;
365            default:
366                break;
367        }
368    }
369
370    private void restore_saved_schedule () {
371        _schedule.load_variant (timer_settings.get_value (KEY_SCHEDULE));
372        if (!_schedule.valid) {
373            warning (
374                "Timer-mode is set to custom, but no schedule has been configured!" +
375                "populating schedule with a pomodoro schedule!"
376            );
377            build_pomodoro_schedule ();
378        }
379    }
380
381    private void save_schedule () {
382        timer_settings.set_value (KEY_SCHEDULE, _schedule.to_variant ());
383    }
384
385    private void build_schedule () {
386        switch (timer_mode) {
387            case TimerMode.SIMPLE:
388                _schedule.import_raw ({task_duration, break_duration});
389                return;
390            case TimerMode.POMODORO:
391                build_pomodoro_schedule ();
392                return;
393            default:
394                return;
395        }
396    }
397
398    private void build_pomodoro_schedule () {
399        var arr_size = pomodoro_period * 2;
400        var durations = new int[arr_size];
401        for (int i = 0; i < arr_size - 2; i += 2) {
402            durations[i] = task_duration;
403            durations[i + 1] = break_duration;
404        }
405        durations[arr_size - 2] = task_duration;
406        durations[arr_size - 1] = long_break_duration;
407        _schedule.import_raw (durations);
408    }
409
410    private void perform_migration () {
411        if (_settings.get_int ("settings-version") <= 0) {
412            var settings_importer = new KeyFileSettingsImport (this);
413            first_start = !settings_importer.import_settings ();
414            performed_migration = true;
415            add_default_todos = first_start;
416        }
417
418        _settings.set_int ("settings-version", 1) ;
419    }
420}
421
422class GOFI.KeyFileSettingsImport {
423
424    /*
425     * A list of constants that define settings group names
426     */
427    private const string GROUP_TODO_TXT = "Todo.txt";
428    private const string GROUP_BEHAVIOR = "Behavior";
429    private const string GROUP_TIMER = "Timer";
430    private const string GROUP_UI = "Interface";
431    private const string GROUP_LISTS = "Lists";
432
433    private KeyFile key_file;
434    private SettingsManager settings;
435
436    public KeyFileSettingsImport (SettingsManager settings) {
437        this.settings = settings;
438    }
439
440    private void import_timer_settings () throws GLib.KeyFileError {
441        if (!key_file.has_group (GROUP_TIMER)) {
442            return;
443        }
444
445        if (key_file.has_key (GROUP_TIMER, "task_duration")) {
446            settings.task_duration =
447                key_file.get_integer (GROUP_TIMER, "task_duration");
448        }
449        if (key_file.has_key (GROUP_TIMER, "break_duration")) {
450            settings.break_duration =
451                key_file.get_integer (GROUP_TIMER, "break_duration");
452        }
453        if (key_file.has_key (GROUP_TIMER, "long_break_duration")) {
454            settings.long_break_duration =
455                key_file.get_integer (GROUP_TIMER, "long_break_duration");
456        }
457        if (key_file.has_key (GROUP_TIMER, "reminder_time")) {
458            settings.reminder_time =
459                key_file.get_integer (GROUP_TIMER, "reminder_time");
460        }
461        if (key_file.has_key (GROUP_TIMER, "pomodoro_period")) {
462            settings.pomodoro_period =
463                key_file.get_integer (GROUP_TIMER, "pomodoro_period");
464        }
465        if (key_file.has_key (GROUP_TIMER, "resume_tasks_after_break")) {
466            settings.resume_tasks_after_break =
467                key_file.get_boolean (GROUP_TIMER, "resume_tasks_after_break");
468        }
469        if (key_file.has_key (GROUP_TIMER, "reset_timer_on_task_switch")) {
470            settings.reset_timer_on_task_switch =
471                key_file.get_boolean (GROUP_TIMER, "reset_timer_on_task_switch");
472        }
473        if (key_file.has_key (GROUP_TIMER, "timer_mode")) {
474            var timer_mode = TimerMode.from_string (key_file.get_value (GROUP_TIMER, "timer_mode"));
475            if (timer_mode == TimerMode.CUSTOM && key_file.has_key (GROUP_TIMER, "schedule")) {
476                var schedule = new Schedule ();
477                var durations = key_file.get_integer_list (GROUP_TIMER, "schedule");
478                if (durations.length >= 2) {
479                    schedule.import_raw (durations);
480                    return;
481                }
482            }
483            settings.timer_mode = timer_mode;
484        } else {
485            settings.timer_mode = TimerMode.SIMPLE;
486        }
487    }
488
489    private void import_ui_settings () throws GLib.KeyFileError {
490        if (!key_file.has_group (GROUP_UI)) {
491            return;
492        }
493
494        if (key_file.has_key (GROUP_UI, "use_header_bar")) {
495            if (key_file.get_boolean (GROUP_UI, "use_header_bar")) {
496                settings.prefers_header_bar = OverrideBool.TRUE;
497            } else {
498                settings.prefers_header_bar = OverrideBool.FALSE;
499            }
500        }
501        if (key_file.has_key (GROUP_UI, "use_dark_theme")) {
502            if (key_file.get_boolean (GROUP_UI, "use_dark_theme")) {
503                settings.color_scheme = ColorScheme.DARK;
504            } else {
505                settings.color_scheme = ColorScheme.LIGHT;
506            }
507        }
508        if (key_file.has_key (GROUP_UI, "switcher_label_type")) {
509            settings.switcher_use_icons =
510                key_file.get_value (GROUP_UI, "switcher_label_type") != "text";
511        }
512        if (key_file.has_key (GROUP_UI, "toolbar_icon_size")) {
513            settings.use_small_toolbar_icons =
514                key_file.get_value (GROUP_UI, "toolbar_icon_size") == "small";
515        }
516
517        int x, y, width, height;
518        width = key_file.get_integer (GROUP_UI, "win_width");
519        height = key_file.get_integer (GROUP_UI, "win_height");
520
521        settings.set_window_size (width, height);
522
523        x = key_file.get_integer (GROUP_UI, "win_x");
524        y = key_file.get_integer (GROUP_UI, "win_y");
525
526        settings.set_window_position (x, y);
527    }
528
529    private void import_list_settings () throws GLib.KeyFileError {
530        if (!key_file.has_group (GROUP_LISTS)) {
531            return;
532        }
533
534        if (key_file.has_key (GROUP_LISTS, "lists")) {
535            List<ListIdentifier?> identifiers = new List<ListIdentifier?> ();
536            var strs = key_file.get_string_list (GROUP_LISTS, "lists");
537
538            foreach (string id_str in strs) {
539                var identifier = ListIdentifier.from_string (id_str);
540                if (identifier != null) {
541                    identifiers.prepend ((owned) identifier);
542                } else {
543                    warning ("Can't decode list information! (%s)", id_str);
544                }
545            }
546            settings.lists = identifiers;
547        }
548
549        if (key_file.has_key (GROUP_LISTS, "last")) {
550            var encoded_id = key_file.get_value (GROUP_LISTS, "last");
551            ListIdentifier list_identifier = null;
552            if (encoded_id != "" && (list_identifier = ListIdentifier.from_string (encoded_id)) != null) {
553                settings.list_last_loaded = list_identifier;
554            }
555        }
556    }
557
558    private void import_behavior_settings () throws GLib.KeyFileError {
559        if (!key_file.has_group (GROUP_BEHAVIOR)) {
560            return;
561        }
562
563        if (key_file.has_key (GROUP_BEHAVIOR, "new_tasks_on_top")) {
564            settings.new_tasks_on_top = key_file.get_boolean (GROUP_BEHAVIOR, "new_tasks_on_top");
565        }
566    }
567
568    public bool import_settings () {
569        // Instantiate the key_file object
570        key_file = new KeyFile ();
571
572        if (FileUtils.test (GOFI.Utils.config_file, FileTest.EXISTS)) {
573            // If it does exist, read existing values
574            try {
575                key_file.load_from_file (GOFI.Utils.config_file,
576                   KeyFileFlags.KEEP_COMMENTS | KeyFileFlags.KEEP_TRANSLATIONS);
577            } catch (Error e) {
578                stderr.printf ("Reading %s failed", GOFI.Utils.config_file);
579                warning ("%s", e.message);
580                return false;
581            }
582        } else {
583            return false;
584        }
585
586        try {
587            import_list_settings ();
588            import_timer_settings ();
589            import_ui_settings ();
590            import_behavior_settings ();
591        } catch (Error e) {
592            warning ("An error occured while importing the settings from"
593                + " %s: %s", GOFI.Utils.config_file, e.message);
594        }
595        return true;
596    }
597}
598
599private enum GOFI.OverrideBool {
600    DEFAULT = 0,
601    FALSE = 1,
602    TRUE = 2;
603}
604
605private enum GOFI.ColorScheme {
606    DEFAULT = 0,
607    LIGHT = 1,
608    DARK = 2;
609
610    public const string STR_DEFAULT = "default";
611    public const string STR_DARK = "dark";
612    public const string STR_LIGHT = "light";
613
614    public string get_description () {
615        switch (this) {
616            case LIGHT:
617                return _("Light");
618            case DARK:
619                return _("Dark");
620            default:
621                return _("Default");
622        }
623    }
624
625    public static ColorScheme from_string (string str) {
626        switch (str) {
627            case STR_LIGHT: return LIGHT;
628            case STR_DARK: return DARK;
629            default: return DEFAULT;
630        }
631    }
632
633    public string to_string () {
634        switch (this) {
635            case LIGHT:
636                return STR_LIGHT;
637            case DARK:
638                return STR_DARK;
639            default:
640                return STR_DEFAULT;
641        }
642    }
643
644    public static ColorScheme[] all () {
645        return {DEFAULT, LIGHT, DARK};
646    }
647}
648
649private enum GOFI.TimerMode {
650    SIMPLE = 0,
651    POMODORO = 1,
652    CUSTOM = 2;
653
654    public const string STR_SIMPLE = "simple";
655    public const string STR_POMODORO = "pomodoro";
656    public const string STR_CUSTOM = "custom";
657
658    public const TimerMode DEFAULT_TIMER_MODE = TimerMode.SIMPLE;
659
660    public static TimerMode from_string (string str) {
661        switch (str) {
662            case STR_SIMPLE: return SIMPLE;
663            case STR_POMODORO: return POMODORO;
664            case STR_CUSTOM: return CUSTOM;
665            default: return DEFAULT_TIMER_MODE;
666        }
667    }
668
669    public string to_string () {
670        switch (this) {
671            case SIMPLE:
672                return STR_SIMPLE;
673            case POMODORO:
674                return STR_POMODORO;
675            case CUSTOM:
676                return STR_CUSTOM;
677            default:
678                assert_not_reached ();
679        }
680    }
681}
682