1#!/usr/local/bin/python3.8 2 3from ChooserButtonWidgets import DateChooserButton, TimeChooserButton 4from SettingsWidgets import SidePage 5from xapp.GSettingsWidgets import * 6import pytz 7import gi 8import datetime 9import os 10gi.require_version('TimezoneMap', '1.0') 11from gi.repository import TimezoneMap 12 13class Module: 14 name = "calendar" 15 comment = _("Manage date and time settings") 16 category = "prefs" 17 18 def __init__(self, content_box): 19 keywords = _("time, date, calendar, format, network, sync") 20 self.sidePage = SidePage(_("Date & Time"), "cs-date-time", keywords, content_box, 560, module=self) 21 22 def on_module_selected(self): 23 if not self.loaded: 24 print("Loading Calendar module") 25 26 page = SettingsPage() 27 self.sidePage.add_widget(page) 28 29 settings = page.add_section(_("Date and Time")) 30 widget = SettingsWidget() 31 self.tz_map = TimezoneMap.TimezoneMap.new() 32 self.tz_map.set_size_request(-1, 205) 33 widget.pack_start(self.tz_map, True, True, 0) 34 settings.add_row(widget) 35 36 self.tz_selector = TimeZoneSelector() 37 settings.add_row(self.tz_selector) 38 39 self.ntp_switch = Switch(_("Network time")) 40 settings.add_row(self.ntp_switch) 41 42 self.set_time_row = SettingsWidget() 43 self.revealer = SettingsRevealer() 44 settings.add_reveal_row(self.set_time_row, revealer=self.revealer) 45 self.set_time_row.pack_start(Gtk.Label(_("Manually set date and time")), False, False, 0) 46 self.date_chooser = DateChooserButton(True) 47 self.time_chooser = TimeChooserButton(True) 48 self.set_time_row.pack_end(self.time_chooser, False, False, 0) 49 self.set_time_row.pack_end(self.date_chooser, False, False, 0) 50 self.date_chooser.connect('date-changed', self.set_date_and_time) 51 self.time_chooser.connect('time-changed', self.set_date_and_time) 52 53 settings = page.add_section(_("Format")) 54 settings.add_row(GSettingsSwitch(_("Use 24h clock"), "org.cinnamon.desktop.interface", "clock-use-24h")) 55 settings.add_row(GSettingsSwitch(_("Display the date"), "org.cinnamon.desktop.interface", "clock-show-date")) 56 settings.add_row(GSettingsSwitch(_("Display seconds"), "org.cinnamon.desktop.interface", "clock-show-seconds")) 57 days = [[7, _("Use locale default")], [0, _("Sunday")], [1, _("Monday")]] 58 settings.add_row(GSettingsComboBox(_("First day of week"), "org.cinnamon.desktop.interface", "first-day-of-week", days, valtype=int)) 59 60 if os.path.exists('/usr/sbin/ntpd'): 61 print('using csd backend') 62 self.proxy_handler = CsdDBusProxyHandler(self._on_proxy_ready) 63 else: 64 print('using systemd backend') 65 self.proxy_handler = SytemdDBusProxyHandler(self._on_proxy_ready) 66 67 def _on_proxy_ready(self): 68 self.zone = self.proxy_handler.get_timezone() 69 if self.zone is None: 70 self.tz_map.set_sensitive(False) 71 self.tz_selector.set_sensitive(False) 72 else: 73 self.tz_map.set_timezone(self.zone) 74 self.tz_map.connect('location-changed', self.on_map_location_changed) 75 self.tz_selector.set_timezone(self.zone) 76 self.tz_selector.connect('timezone-changed', self.on_selector_location_changed) 77 can_use_ntp, is_using_ntp = self.proxy_handler.get_ntp() 78 self.ntp_switch.set_sensitive(can_use_ntp) 79 self.ntp_switch.content_widget.set_active(is_using_ntp) 80 self.ntp_switch.content_widget.connect('notify::active', self.on_ntp_changed) 81 self.revealer.set_reveal_child(not is_using_ntp) 82 83 def on_map_location_changed(self, *args): 84 zone = self.tz_map.get_location().props.zone 85 if zone == self.zone: 86 return 87 88 self.tz_selector.set_timezone(zone) 89 self.set_timezone(zone) 90 91 def on_selector_location_changed(self, *args): 92 zone = self.tz_selector.get_timezone() 93 if zone == self.zone: 94 return 95 96 self.set_timezone(zone) 97 self.tz_map.set_timezone(zone) 98 99 def set_timezone(self, zone): 100 self.zone = zone 101 self.proxy_handler.set_timezone(zone) 102 103 def on_ntp_changed(self, *args): 104 active = self.ntp_switch.content_widget.get_active() 105 self.revealer.set_reveal_child(not active) 106 self.proxy_handler.set_ntp(active) 107 108 def set_date_and_time(self, *args): 109 unaware = datetime.datetime.combine(self.date_chooser.get_date(), self.time_chooser.get_time()) 110 tz = pytz.timezone(self.zone) 111 self.datetime = tz.localize(unaware) 112 113 seconds = int((self.datetime - datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)).total_seconds()) 114 self.proxy_handler.set_time(seconds) 115 116class SytemdDBusProxyHandler(object): 117 def __init__(self, proxy_ready_callback): 118 self.proxy_ready_callback = proxy_ready_callback 119 try: 120 Gio.DBusProxy.new_for_bus(Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None, 121 'org.freedesktop.timedate1', 122 '/org/freedesktop/timedate1', 123 'org.freedesktop.timedate1', 124 None, self._on_proxy_ready, None) 125 except dbus.exceptions.DBusException as e: 126 print(e) 127 self._proxy = None 128 129 def _on_proxy_ready(self, object, result, data=None): 130 self._proxy = Gio.DBusProxy.new_for_bus_finish(result) 131 self.proxy_ready_callback() 132 133 def get_timezone(self): 134 if not self._proxy: 135 return None 136 return str(self._proxy.get_cached_property('Timezone')).lstrip('\'').rstrip('\'') 137 138 def get_ntp(self): 139 if not self._proxy: 140 return False, False 141 can_use_ntp = self._proxy.get_cached_property('CanNTP') 142 using_ntp = self._proxy.get_cached_property('NTP') 143 return can_use_ntp, using_ntp 144 145 def set_timezone(self, zone): 146 if self._proxy: 147 self._proxy.SetTimezone('(sb)', zone, True) 148 149 def set_ntp(self, active): 150 if self._proxy: 151 # not passing a callback to the dbus function will cause it to run synchronously and freeze the ui 152 def async_empty_callback(*args, **kwargs): 153 pass 154 self._proxy.SetNTP('(bb)', active, True, result_handler=async_empty_callback) 155 156 def set_time(self, seconds): 157 if self._proxy: 158 self._proxy.SetTime('(xbb)', seconds * 1000000, False, True) 159 160 161class CsdDBusProxyHandler(object): 162 def __init__(self, proxy_ready_callback): 163 self.proxy_ready_callback = proxy_ready_callback 164 try: 165 Gio.DBusProxy.new_for_bus(Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None, 166 'org.cinnamon.SettingsDaemon.DateTimeMechanism', 167 '/', 168 'org.cinnamon.SettingsDaemon.DateTimeMechanism', 169 None, self._on_proxy_ready, None) 170 except dbus.exceptions.DBusException as e: 171 print(e) 172 self._proxy = None 173 174 def _on_proxy_ready(self, object, result, data=None): 175 self._proxy = Gio.DBusProxy.new_for_bus_finish(result) 176 self.proxy_ready_callback() 177 178 def get_timezone(self): 179 return self._proxy.GetTimezone() 180 181 def get_ntp(self): 182 return self._proxy.GetUsingNtp() 183 184 def set_timezone(self, zone): 185 self._proxy.SetTimezone('(s)', zone) 186 187 def set_ntp(self, active): 188 # not passing a callback to the dbus function will cause it to run synchronously and freeze the ui 189 def async_empty_callback(*args, **kwargs): 190 pass 191 self._proxy.SetUsingNtp('(b)', active, result_handler=async_empty_callback) 192 193 def set_time(self, seconds): 194 self._proxy.SetTime('(x)', seconds) 195 196 197class TimeZoneSelector(SettingsWidget): 198 __gsignals__ = { 199 'timezone-changed': (GObject.SignalFlags.RUN_FIRST, None, (str,)) 200 } 201 202 def __init__(self): 203 super(TimeZoneSelector, self).__init__() 204 205 self.pack_start(Gtk.Label(_("Region")), False, False, 0) 206 self.region_combo = Gtk.ComboBox() 207 self.pack_start(self.region_combo, False, False, 0) 208 self.pack_start(Gtk.Label(_("City")), False, False, 0) 209 self.city_combo = Gtk.ComboBox() 210 self.pack_start(self.city_combo, False, False, 0) 211 self.region_combo.connect('changed', self.on_region_changed) 212 self.city_combo.connect('changed', self.on_city_changed) 213 214 self.region_list = Gtk.ListStore(str, str) 215 self.region_combo.set_model(self.region_list) 216 renderer_text = Gtk.CellRendererText() 217 self.region_combo.pack_start(renderer_text, True) 218 self.region_combo.add_attribute(renderer_text, "text", 1) 219 self.region_combo.set_id_column(0) 220 221 renderer_text = Gtk.CellRendererText() 222 self.city_combo.pack_start(renderer_text, True) 223 self.city_combo.add_attribute(renderer_text, "text", 1) 224 self.city_combo.set_id_column(0) 225 226 self.region_map = {} 227 for tz in pytz.common_timezones: 228 try: 229 region, city = tz.split('/', maxsplit=1) 230 except: 231 continue 232 233 if region not in self.region_map: 234 self.region_map[region] = Gtk.ListStore(str, str) 235 self.region_list.append([region, _(region)]) 236 self.region_map[region].append([city, _(city)]) 237 238 def set_timezone(self, timezone): 239 if timezone == "Etc/UTC": 240 return 241 242 self.timezone = timezone 243 region, city = timezone.split('/', maxsplit=1) 244 self.region_combo.set_active_id(region) 245 self.city_combo.set_model(self.region_map[region]) 246 self.city_combo.set_active_id(city) 247 248 def on_region_changed(self, *args): 249 region = self.region_combo.get_active_id() 250 self.city_combo.set_model(self.region_map[region]) 251 252 def on_city_changed(self, *args): 253 self.timezone = '/'.join([self.region_combo.get_active_id(), self.city_combo.get_active_id()]) 254 self.emit('timezone-changed', self.timezone) 255 256 def get_timezone(self): 257 return self.timezone 258