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