diff --git a/data/io.elementary.settings-daemon.gschema.xml b/data/io.elementary.settings-daemon.gschema.xml index 506e381b..723d97de 100644 --- a/data/io.elementary.settings-daemon.gschema.xml +++ b/data/io.elementary.settings-daemon.gschema.xml @@ -71,6 +71,14 @@ + + + [] + The current schedules. + a(a{sv}a{sv}) + + + false diff --git a/src/Application.vala b/src/Application.vala index 0014d548..33b95d33 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -21,6 +21,8 @@ public sealed class SettingsDaemon.Application : Gtk.Application { private Backends.Housekeeping housekeeping; + private Backends.ScheduleManager schedule_manager; + private const string FDO_ACCOUNTS_NAME = "org.freedesktop.Accounts"; private const string FDO_ACCOUNTS_PATH = "/org/freedesktop/Accounts"; @@ -39,6 +41,8 @@ public sealed class SettingsDaemon.Application : Gtk.Application { GLib.Intl.textdomain (Build.GETTEXT_PACKAGE); add_main_option ("version", 'v', NONE, NONE, "Display the version", null); + + schedule_manager = new Backends.ScheduleManager (); } protected override int handle_local_options (VariantDict options) { @@ -77,6 +81,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application { protected override bool dbus_register (DBusConnection connection, string object_path) throws Error { base.dbus_register (connection, object_path); + connection.register_object (object_path, schedule_manager); connection.register_object (object_path, new Backends.SystemUpdate ()); return true; @@ -117,7 +122,10 @@ public sealed class SettingsDaemon.Application : Gtk.Application { try { pantheon_service = yield connection.get_proxy (FDO_ACCOUNTS_NAME, path, GET_INVALIDATED_PROPERTIES); - prefers_color_scheme_settings = new Backends.PrefersColorSchemeSettings (pantheon_service); + + // prefers_color_scheme_settings = new Backends.PrefersColorSchemeSettings (pantheon_service); + schedule_manager.pantheon_service = pantheon_service; + accent_color_manager = new Backends.AccentColorManager (pantheon_service); } catch { warning ("Unable to get pantheon's AccountsService proxy, color scheme preference may be incorrect"); diff --git a/src/Backends/ScheduleManager/DaylightSchedule.vala b/src/Backends/ScheduleManager/DaylightSchedule.vala new file mode 100644 index 00000000..406a5ad6 --- /dev/null +++ b/src/Backends/ScheduleManager/DaylightSchedule.vala @@ -0,0 +1,63 @@ +public class SettingsDaemon.Backends.DaylightSchedule : Schedule { + private double sunrise = 6.0; + private double sunset = 20.0; + + public DaylightSchedule.from_parsed (Parsed parsed) { + base.from_parsed (parsed); + } + + construct { + schedule_type = DAYLIGHT; + get_location.begin (); + Timeout.add (1000, time_callback); + } + + private bool time_callback () { + var is_in = is_in_time_window (); + + if (active != is_in) { + active = is_in; + } + + return Source.CONTINUE; + } + + private async void get_location () { + try { + var simple = yield new GClue.Simple (Build.PROJECT_NAME, GClue.AccuracyLevel.CITY, null); + + simple.notify["location"].connect (() => { + on_location_updated (simple.location.latitude, simple.location.longitude); + }); + + on_location_updated (simple.location.latitude, simple.location.longitude); + } catch (Error e) { + warning ("Failed to connect to GeoClue2 service: %s", e.message); + return; + } + } + + private void on_location_updated (double latitude, double longitude) { + var now = new DateTime.now_local (); + double _sunrise, _sunset; + if (SettingsDaemon.Utils.SunriseSunsetCalculator.get_sunrise_and_sunset (now, latitude, longitude, out _sunrise, out _sunset)) { + sunrise = _sunrise; + sunset = _sunset; + } + } + + private bool is_in_time_window () { + var date_time = new DateTime.now_local (); + double time_double = 0; + time_double += date_time.get_hour (); + time_double += (double) date_time.get_minute () / 60; + + // PM to AM + if (sunset > sunrise) { + return time_double < sunrise ? time_double <= sunset : time_double >= sunset; + } + + // AM to AM, PM to PM, AM to PM + return (time_double >= sunset && time_double <= sunrise); + } +} diff --git a/src/Backends/ScheduleManager/ManualSchedule.vala b/src/Backends/ScheduleManager/ManualSchedule.vala new file mode 100644 index 00000000..e611b706 --- /dev/null +++ b/src/Backends/ScheduleManager/ManualSchedule.vala @@ -0,0 +1,60 @@ +public class SettingsDaemon.Backends.ManualSchedule : Schedule { + public double from { get; construct set; } + public double to { get; construct set; } + + public ManualSchedule (string name, double from, double to) { + base (); + this.name = name; + this.from = from; + this.to = to; + } + + public ManualSchedule.from_parsed (Parsed parsed) { + base.from_parsed (parsed); + schedule_type = MANUAL; + from = (double) parsed.args["from"]; + to = (double) parsed.args["to"]; + } + + construct { + Timeout.add (1000, time_callback); + } + + private bool time_callback () { + var is_in = is_in_time_window (); + + if (active != is_in) { + active = is_in; + } + + return Source.CONTINUE; + } + + private bool is_in_time_window () { + var date_time = new DateTime.now_local (); + double time_double = 0; + time_double += date_time.get_hour (); + time_double += (double) date_time.get_minute () / 60; + + // PM to AM + if (from > to) { + return time_double < to ? time_double <= from : time_double >= from; + } + + // AM to AM, PM to PM, AM to PM + return (time_double >= from && time_double <= to); + } + + protected override void update (Schedule.Parsed parsed) { + base.update (parsed); + from = (double) parsed.args["from"]; + to = (double) parsed.args["to"]; + } + + protected override HashTable get_private_args () { + var result = new HashTable (str_hash, str_equal); + result["from"] = from; + result["to"] = to; + return result; + } +} diff --git a/src/Backends/ScheduleManager/Schedule.vala b/src/Backends/ScheduleManager/Schedule.vala new file mode 100644 index 00000000..acd642ca --- /dev/null +++ b/src/Backends/ScheduleManager/Schedule.vala @@ -0,0 +1,67 @@ +public class SettingsDaemon.Backends.Schedule : Object { + public enum Type { + MANUAL, + DAYLIGHT + } + + public struct Parsed { + string id; + Type type; + string name; + bool enabled; + HashTable args; + HashTable active_settings; + HashTable inactive_settings; + } + + public string id { get; protected set; } + public Type schedule_type { get; construct set; } + public string name { get; protected set; } + public bool enabled { get; protected set; default = true; } + public bool active { get; protected set; default = false; } + public HashTable active_settings { get; private set; } + public HashTable inactive_settings { get; private set; } //Inactive settings should usually be !active_settings but can also be e.g. a default wallpaper path + + public Schedule () { + id = Uuid.string_random (); + active_settings = new HashTable (str_hash, str_equal); + inactive_settings = new HashTable (str_hash, str_equal); + } + + public Schedule.from_parsed (Parsed parsed) { + id = parsed.id; + name = parsed.name; + enabled = parsed.enabled; + active_settings = parsed.active_settings; + inactive_settings = parsed.inactive_settings; + } + + /* Convenience method to add the same boolean inverted to inactive settings */ + public void add_boolean (string key, bool val) { + active_settings[key] = val; + inactive_settings[key] = !val; + } + + public Parsed get_parsed () { + Parsed result = { + id, + schedule_type, + name, + enabled, + get_private_args (), + active_settings, + inactive_settings + }; + return result; + } + + public virtual void update (Schedule.Parsed parsed) { + enabled = parsed.enabled; + active_settings = parsed.active_settings; + inactive_settings = parsed.inactive_settings; + } + + protected virtual HashTable get_private_args () { + return new HashTable (str_hash, str_equal); + } +} diff --git a/src/Backends/ScheduleManager/ScheduleManager.vala b/src/Backends/ScheduleManager/ScheduleManager.vala new file mode 100644 index 00000000..0af37d32 --- /dev/null +++ b/src/Backends/ScheduleManager/ScheduleManager.vala @@ -0,0 +1,116 @@ +[DBus (name="io.elementary.settings_daemon.ScheduleManager")] +public class SettingsDaemon.Backends.ScheduleManager : GLib.Object { + private const string NIGHT_LIGHT = "night-light"; + private const string DARK_MODE = "dark-mode"; + private const string DND = "dnd"; + private const string MONOCHROME = "monochrome"; + + private static Settings settings = new Settings ("io.elementary.settings-daemon.schedules"); + private static Settings dnd_settings = new Settings ("io.elementary.notifications"); + private static Settings monochrome_settings = new Settings ("io.elementary.desktop.wm.accessibility"); + + [DBus (visible=false)] + public unowned Pantheon.AccountsService? pantheon_service { get; set; } + + private HashTable schedules = new HashTable (str_hash, str_equal); + + construct { + foreach (var parsed_schedule in (Schedule.Parsed[]) settings.get_value ("schedules")) { + create_schedule_internal (parsed_schedule); + } + } + + private void create_schedule_internal (Schedule.Parsed parsed) { + if (parsed.name in schedules) { + warning ("Schedule with the same name already exists"); + return; + } + + switch (parsed.type) { + case MANUAL: + add_schedule (new ManualSchedule.from_parsed (parsed)); + break; + case DAYLIGHT: + add_schedule (new DaylightSchedule.from_parsed (parsed)); + break; + default: + break; + } + + save_schedules (); + } + + public Schedule.Parsed[] list_schedules () throws DBusError, IOError { + Schedule.Parsed[] parsed_schedules = {}; + foreach (var schedule in schedules.get_values ()) { + parsed_schedules += schedule.get_parsed (); + } + + return parsed_schedules; + } + + public void update_schedule (Schedule.Parsed parsed) throws DBusError, IOError { + if (parsed.id in schedules) { + schedules.remove (parsed.id); + } + + create_schedule_internal (parsed); + + save_schedules (); + } + + public void delete_schedule (string id) throws DBusError, IOError { + if (!(id in schedules)) { + throw new IOError.NOT_FOUND ("Schedule with the same name not found"); + } + + schedules.remove (id); + + save_schedules (); + } + + private void add_schedule (Schedule schedule) { + schedule.notify["active"].connect (() => schedule_active_changed (schedule)); + schedule_active_changed (schedule); + + schedules[schedule.id] = schedule; + } + + private void schedule_active_changed (Schedule schedule) { + if (schedule.enabled) { + activate_settings (schedule.active ? schedule.active_settings : schedule.inactive_settings); + } + } + + private void activate_settings (HashTable settings) { + foreach (var key in settings.get_keys ()) { + switch (key) { + case NIGHT_LIGHT: + //TODO + break; + case DARK_MODE: + if (pantheon_service != null) { + pantheon_service.prefers_color_scheme = ((bool) settings[DARK_MODE]) ? Granite.Settings.ColorScheme.DARK : Granite.Settings.ColorScheme.LIGHT; + } + break; + case DND: + dnd_settings.set_boolean ("do-not-disturb", (bool) settings[DND]); + break; + case MONOCHROME: + monochrome_settings.set_boolean ("enable-monochrome-filter", (bool) settings[MONOCHROME]); + break; + default: + break; + } + } + } + + private void save_schedules () { + Schedule.Parsed[] parsed_schedules = {}; + foreach (var schedule in schedules.get_values ()) { + parsed_schedules += schedule.get_parsed (); + } + + settings.set_value ("schedules", parsed_schedules); + } +} diff --git a/src/meson.build b/src/meson.build index 05f2250f..243c7531 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,6 +8,10 @@ sources = files( 'Backends/MouseSettings.vala', 'Backends/NightLightSettings.vala', 'Backends/PrefersColorSchemeSettings.vala', + 'Backends' / 'ScheduleManager' / 'DaylightSchedule.vala', + 'Backends' / 'ScheduleManager' / 'ManualSchedule.vala', + 'Backends' / 'ScheduleManager' / 'Schedule.vala', + 'Backends' / 'ScheduleManager' / 'ScheduleManager.vala', 'Backends/SystemUpdate.vala', 'Utils/SunriseSunsetCalculator.vala', )