diff --git a/src/Config.vala.in b/src/Config.vala.in index 184e5218..550688d7 100644 --- a/src/Config.vala.in +++ b/src/Config.vala.in @@ -1,7 +1,7 @@ namespace Build { - public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@"; - public const string LANG_LIST = "@LANG_LIST@"; - public const string PREFERRED_LANG_LIST = "@PREFERRED_LANG_LIST@"; - public const string XKB_BASE = "@XKB_BASE@"; - public const string ISO_CODES_LOCATION = "@ISO_CODES_LOCATION@"; + public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@"; + public const string LANG_LIST = "@LANG_LIST@"; + public const string PREFERRED_LANG_LIST = "@PREFERRED_LANG_LIST@"; + public const string XKB_BASE = "@XKB_BASE@"; + public const string ISO_CODES_LOCATION = "@ISO_CODES_LOCATION@"; } diff --git a/src/Helpers/AccountsServiceInterface.vala b/src/Helpers/AccountsServiceInterface.vala index 0178288e..075d73be 100644 --- a/src/Helpers/AccountsServiceInterface.vala +++ b/src/Helpers/AccountsServiceInterface.vala @@ -7,6 +7,7 @@ public interface Installer.AccountsService : Object { public abstract KeyboardLayout[] keyboard_layouts { owned get; set; } public abstract uint active_keyboard_layout { get; set; } + public abstract string clock_format { owned get; set; } public abstract bool left_handed { get; set; } } diff --git a/src/Helpers/LocationHelper.vala b/src/Helpers/LocationHelper.vala new file mode 100644 index 00000000..155c9c76 --- /dev/null +++ b/src/Helpers/LocationHelper.vala @@ -0,0 +1,164 @@ +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*- + * Copyright (c) 2014 Pantheon Developers (http://launchpad.net/switchboard-plug-datetime) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Corentin Noël + */ + +public class LocationHelper : GLib.Object { + [DBus (name = "org.freedesktop.timedate1")] + public interface DateTime1 : Object { + public abstract string Timezone {public owned get;} + public abstract bool LocalRTC {public get;} + public abstract bool CanNTP {public get;} + public abstract bool NTP {public get;} + + //usec_utc expects number of microseconds since 1 Jan 1970 UTC + public abstract void set_time (int64 usec_utc, bool relative, bool user_interaction) throws GLib.Error; + public abstract void set_timezone (string timezone, bool user_interaction) throws GLib.Error; + public abstract void SetLocalRTC (bool local_rtc, bool fix_system, bool user_interaction) throws GLib.Error; //vala-lint=naming-convention + public abstract void SetNTP (bool use_ntp, bool user_interaction) throws GLib.Error; //vala-lint=naming-convention + } + + private static List lines; + private static LocationHelper? helper = null; + + public static LocationHelper get_default () { + if (helper == null) + helper = new LocationHelper (); + return helper; + } + private LocationHelper () { + var file = File.new_for_path ("/usr/share/zoneinfo/zone.tab"); + if (!file.query_exists ()) { + critical ("/usr/share/zoneinfo/zone.tab doesn't exist !"); + return; + } + + lines = new List (); + try { + var dis = new DataInputStream (file.read ()); + string line; + while ((line = dis.read_line (null)) != null) { + if (line.has_prefix ("#")) { + continue; + } + + lines.append (line); + } + } catch (Error e) { + critical (e.message); + } +#if GENERATE + generate_translation_template (); +#endif + } + + public HashTable get_timezones_from_continent (string continent) { + var timezones = new HashTable (str_hash, str_equal); + foreach (var line in lines) { + var items = line.split ("\t", 4); + string value = items[2]; + if (value.has_prefix (continent) == false) + continue; + + string tz_name_field; + // Take the original English string if there is something wrong with the translation + if (_(items[2]) == null || _(items[2]) == "") { + tz_name_field = items[2]; + } else { + tz_name_field = _(items[2]); + } + + string city = tz_name_field.split ("/", 2)[1]; + if (city != null && city != "") { + string key = format_city (city); + if (items[3] != null && items[3] != "") { + if (items[3] != "mainland" && items[3] != "most locations" && _(items[3]) != key) { + key = "%s - %s".printf (key, format_city (_(items[3]))); + } + } + + timezones.set (key, value); + } + } + + return timezones; + } + + public string? get_countrycode_from_timezone (string timezone) { + foreach (var line in lines) { + var items = line.split ("\t", 4); + string value = items[2]; + + if (value == timezone) { + return items[0]; + } + } + + return null; + } + + public HashTable get_locations () { + var locations = new HashTable (str_hash, str_equal); + foreach (var line in lines) { + var items = line.split ("\t", 4); + string key = items[1]; + string value = items[2]; + locations.set (key, value); + } + + return locations; + } + + public static string format_city (string city) { + return city.replace ("_", " ").replace ("/", ", "); + } + + public static string get_clock_format () { + var t_fmt = Posix.nl_langinfo (Posix.NLItem.T_FMT); + + if (t_fmt.contains ("%r") || t_fmt.contains ("%l") || t_fmt.contains ("%I")) { + return "12h"; + } + + return "24h"; + } + +#if GENERATE + public void generate_translation_template () { + var file = GLib.File.new_for_path (GLib.Environment.get_home_dir () + "/Translations.vala"); + try { + var dos = new GLib.DataOutputStream (file.create (GLib.FileCreateFlags.REPLACE_DESTINATION)); + dos.put_string ("#if 0\n"); + foreach (var line in lines) { + var items = line.split ("\t", 4); + string key = items[2]; + string comment = items[3]; + dos.put_string ("///Translators: Secondary \"/\" and all \"_\" will be replaced by \", \" and \" \".\n"); + dos.put_string ("_(\""+ key + "\");\n"); + if (comment != null && comment != "") { + dos.put_string ("///Translators: Comment for Timezone %s\n".printf (key)); + dos.put_string ("_(\""+ comment + "\");\n"); + } + } + dos.put_string ("#endif\n"); + } catch (Error e) { + critical (e.message); + } + } +#endif +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 0123fb1d..8ce20a12 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -22,6 +22,7 @@ public class Installer.MainWindow : Hdy.Window { private AccountView account_view; private LanguageView language_view; + private LocationView location_view; private KeyboardLayoutView keyboard_layout_view; private NetworkView network_view; @@ -82,19 +83,30 @@ public class Installer.MainWindow : Hdy.Window { deck.add (network_view); deck.visible_child = network_view; - network_view.next_step.connect (load_account_view); + network_view.next_step.connect (load_location_view); } else { - load_account_view (); + load_location_view (); } } + private void load_location_view () { + if (location_view != null) { + location_view.destroy (); + } + + location_view = new LocationView (); + deck.add (location_view); + deck.visible_child = location_view; + + location_view.next_step.connect (() => load_account_view ()); + } + private void load_account_view () { if (account_view != null) { account_view.destroy (); } account_view = new AccountView (); - deck.add (account_view); deck.visible_child = account_view; } diff --git a/src/Objects/Configuration.vala b/src/Objects/Configuration.vala index dd8c74a9..d73d9ec3 100644 --- a/src/Objects/Configuration.vala +++ b/src/Objects/Configuration.vala @@ -32,5 +32,7 @@ public class Configuration : GLib.Object { public string? country { get; set; default = null; } public InitialSetup.KeyboardLayout keyboard_layout { get; set; } public InitialSetup.KeyboardVariant? keyboard_variant { get; set; default = null; } + public string timezone { get; set; } + public string clock_format { get; set; default = "24h"; } public bool left_handed { get; set; } } diff --git a/src/Objects/LocationLayout.vala b/src/Objects/LocationLayout.vala new file mode 100644 index 00000000..c36bf046 --- /dev/null +++ b/src/Objects/LocationLayout.vala @@ -0,0 +1,87 @@ +/*- + * Copyright 2019 elementary, Inc (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Corentin Noël + */ + +public class InitialSetup.LocationLayout : GLib.Object { + public string name { get; construct; } + public string original_name { get; construct; } + public string display_name { + get { + return name; + } + } + + private GLib.ListStore variants_store; + + public LocationLayout (string name, string original_name) { + Object (name: name, original_name: original_name); + } + + construct { + variants_store = new GLib.ListStore (typeof (LocationVariant)); + variants_store.append (new LocationVariant (this, null, null)); + } + + public void add_variant (string name, string original_name) { + var variant = new LocationVariant (this, name, original_name); + variants_store.insert_sorted (variant, (GLib.CompareDataFunc) LocationVariant.compare); + } + + public bool has_variants () { + return variants_store.get_n_items () > 1; + } + + public unowned GLib.ListStore get_variants () { + return variants_store; + } + + public GLib.Variant to_gsd_variant () { + return new GLib.Variant ("(ss)", "xkb", name); + } + + public static int compare (LocationLayout a, LocationLayout b) { + return a.display_name.collate (b.display_name); + } + + public static GLib.ListStore get_all () { + var layout_store = new GLib.ListStore (typeof (LocationLayout)); + + var continents = new List (); + continents.append ("Africa"); + continents.append ("America"); + continents.append ("Antarctica"); + continents.append ("Asia"); + continents.append ("Atlantic"); + continents.append ("Australia"); + continents.append ("Europe"); + continents.append ("Indian"); + continents.append ("Pacific"); + + continents.foreach ((continent) => { + var layout = new LocationLayout (_(continent), continent); + layout_store.insert_sorted (layout, (GLib.CompareDataFunc) LocationLayout.compare); + + var timezones = LocationHelper.get_default ().get_timezones_from_continent (continent); + timezones.foreach ((city, timezone) => { + layout.add_variant (_(city), timezone); + }); + }); + + return layout_store; + } +} diff --git a/src/Objects/LocationVariant.vala b/src/Objects/LocationVariant.vala new file mode 100644 index 00000000..4672453c --- /dev/null +++ b/src/Objects/LocationVariant.vala @@ -0,0 +1,53 @@ +/*- + * Copyright 2019 elementary, Inc (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Corentin Noël + */ + +public class InitialSetup.LocationVariant : GLib.Object { + public unowned LocationLayout layout { get; construct; } + public string? name { get; construct; } + public string? original_name { get; construct; } + public string display_name { + get { + return name; + } + } + + public LocationVariant (LocationLayout layout, string? name, string? original_name) { + Object (layout: layout, name: name, original_name: original_name); + } + + public GLib.Variant to_gsd_variant () { + if (name == null) { + return layout.to_gsd_variant (); + } else { + return new GLib.Variant ("(ss)", "xkb", "%s+%s".printf (layout.name, name)); + } + } + + public static int compare (LocationVariant a, LocationVariant b) { + if (a.name == null) { + return -1; + } + + if (b.name == null) { + return 1; + } + + return a.display_name.collate (b.display_name); + } +} diff --git a/src/Views/AccountView.vala b/src/Views/AccountView.vala index 93fbfdaf..b418dbb3 100644 --- a/src/Views/AccountView.vala +++ b/src/Views/AccountView.vala @@ -334,9 +334,43 @@ public class Installer.AccountView : AbstractInstallerView { created_user.set_password (pw_entry.text, ""); yield set_accounts_service_settings (); yield set_locale (); + yield set_timezone (); + yield set_clock_format (); Utils.set_hostname (hostname_entry.text); } + private async void set_timezone () { + try { + LocationHelper.DateTime1 datetime1 = yield Bus.get_proxy (BusType.SYSTEM, "org.freedesktop.timedate1", "/org/freedesktop/timedate1"); + unowned Configuration configuration = Configuration.get_default (); + datetime1.set_timezone (configuration.timezone, true); + } catch (Error e) { + warning (e.message); + } + } + + private async void set_clock_format () { + AccountsService accounts_service = null; + + try { + var act_service = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, + "org.freedesktop.Accounts", + "/org/freedesktop/Accounts"); + var user_path = act_service.find_user_by_name (created_user.user_name); + + accounts_service = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, + "org.freedesktop.Accounts", + user_path, + GLib.DBusProxyFlags.GET_INVALIDATED_PROPERTIES); + } catch (Error e) { + warning ("Unable to get AccountsService proxy, clock format on new user may be incorrect: %s", e.message); + } + + if (accounts_service != null) { + accounts_service.clock_format = Configuration.get_default ().clock_format; + } + } + private async void set_locale () { string lang = Configuration.get_default ().lang; string? locale = null; diff --git a/src/Views/LocationView.vala b/src/Views/LocationView.vala new file mode 100644 index 00000000..29933532 --- /dev/null +++ b/src/Views/LocationView.vala @@ -0,0 +1,146 @@ +/*- + * Copyright 2022 elementary. Inc. (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +public class LocationView : AbstractInstallerView { + private VariantWidget location_variant_widget; + + construct { + var image = new Gtk.Image.from_icon_name ("preferences-system-time", Gtk.IconSize.DIALOG) { + valign = Gtk.Align.END + }; + + var title_label = new Gtk.Label (_("Select Time Zone")) { + valign = Gtk.Align.START + }; + title_label.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); + + location_variant_widget = new VariantWidget (); + + var helper = LocationHelper.get_default (); + + content_area.attach (image, 0, 0); + content_area.attach (title_label, 0, 1); + content_area.attach (location_variant_widget, 1, 0, 1, 2); + + var back_button = new Gtk.Button.with_label (_("Back")); + + var next_button = new Gtk.Button.with_label (_("Select")); + next_button.sensitive = false; + next_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + action_area.add (back_button); + action_area.add (next_button); + + location_variant_widget.variant_listbox.row_activated.connect (() => { + next_button.activate (); + }); + + location_variant_widget.variant_listbox.row_selected.connect (() => { + unowned Gtk.ListBoxRow row = location_variant_widget.main_listbox.get_selected_row (); + if (row != null) { + var layout = ((LayoutRow) row).layout; + unowned Configuration configuration = Configuration.get_default (); + GLib.Variant? layout_variant = null; + string second_variant = layout.name; + + unowned Gtk.ListBoxRow vrow = location_variant_widget.variant_listbox.get_selected_row (); + if (vrow != null) { + unowned InitialSetup.LocationVariant variant = ((VariantRow) vrow).variant; + configuration.timezone = variant.original_name; + if (variant != null) { + layout_variant = variant.to_gsd_variant (); + } + } + + if (layout_variant == null) { + layout_variant = layout.to_gsd_variant (); + } + } + }); + + back_button.clicked.connect (() => ((Hdy.Deck) get_parent ()).navigate (Hdy.NavigationDirection.BACK)); + + next_button.clicked.connect (() => { + next_step (); + }); + + location_variant_widget.main_listbox.row_activated.connect ((row) => { + unowned InitialSetup.LocationLayout layout = ((LayoutRow) row).layout; + if (!layout.has_variants ()) { + return; + } + + location_variant_widget.variant_listbox.bind_model (layout.get_variants (), (variant) => { + return new VariantRow (variant as InitialSetup.LocationVariant); + }); + location_variant_widget.variant_listbox.select_row (location_variant_widget.variant_listbox.get_row_at_index (0)); + + location_variant_widget.show_variants (_("Continent"), "%s".printf (layout.display_name)); + }); + + location_variant_widget.main_listbox.row_selected.connect ((row) => { + next_button.sensitive = true; + }); + + location_variant_widget.main_listbox.bind_model (InitialSetup.LocationLayout.get_all (), (layout) => { + return new LayoutRow (layout as InitialSetup.LocationLayout); + }); + + show_all (); + } + + private class LayoutRow : Gtk.ListBoxRow { + public unowned InitialSetup.LocationLayout layout; + + public LayoutRow (InitialSetup.LocationLayout layout) { + this.layout = layout; + + string layout_description = layout.display_name; + if (layout.has_variants ()) { + layout_description = _("%s…").printf (layout_description); + } + + var label = new Gtk.Label (layout_description) { + ellipsize = Pango.EllipsizeMode.END, + margin = 6, + xalign = 0 + }; + label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + add (label); + show_all (); + } + } + + private class VariantRow : Gtk.ListBoxRow { + public unowned InitialSetup.LocationVariant variant; + + public VariantRow (InitialSetup.LocationVariant variant) { + this.variant = variant; + + var label = new Gtk.Label (variant.display_name) { + ellipsize = Pango.EllipsizeMode.END, + margin = 6, + xalign = 0 + }; + label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + add (label); + show_all (); + } + } +} diff --git a/src/meson.build b/src/meson.build index 7c80dfe2..454287d2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,14 +4,18 @@ vala_files = [ 'MainWindow.vala', 'Helpers/AccountsServiceInterface.vala', 'Helpers/LocaleHelper.vala', + 'Helpers/LocationHelper.vala', 'Objects/Configuration.vala', 'Objects/KeyboardLayout.vala', 'Objects/KeyboardVariant.vala', + 'Objects/LocationLayout.vala', + 'Objects/LocationVariant.vala', 'Utils.vala', 'Views/AbstractInstallerView.vala', 'Views/AccountView.vala', 'Views/KeyboardLayoutView.vala', 'Views/LanguageView.vala', + 'Views/LocationView.vala', 'Views/NetworkView.vala', 'Widgets/LayoutWidget.vala', 'Widgets/VariantWidget.vala'