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