1#!/usr/local/bin/python3.8
2
3from gi.repository import Gio
4from xapp.SettingsWidgets import *
5from SettingsWidgets import SoundFileChooser, TweenChooser, EffectChooser, DateChooser, TimeChooser, Keybinding
6from xapp.GSettingsWidgets import CAN_BACKEND as px_can_backend
7from SettingsWidgets import CAN_BACKEND as c_can_backend
8from TreeListWidgets import List
9import os
10import collections
11import json
12import operator
13
14can_backend = px_can_backend + c_can_backend
15can_backend.append('List')
16
17JSON_SETTINGS_PROPERTIES_MAP = {
18    "description"      : "label",
19    "min"              : "mini",
20    "max"              : "maxi",
21    "step"             : "step",
22    "units"            : "units",
23    "show-value"       : "show_value",
24    "select-dir"       : "dir_select",
25    "height"           : "height",
26    "tooltip"          : "tooltip",
27    "possible"         : "possible",
28    "expand-width"     : "expand_width",
29    "columns"          : "columns",
30    "event-sounds"     : "event_sounds",
31    "default_icon"     : "default_icon",
32    "icon_categories"  : "icon_categories",
33    "default_category" : "default_category",
34    "show-seconds"     : "show_seconds",
35    "show-buttons"     : "show_buttons"
36}
37
38OPERATIONS = ['<=', '>=', '<', '>', '!=', '=']
39
40OPERATIONS_MAP = {'<': operator.lt, '<=': operator.le, '>': operator.gt, '>=': operator.ge, '!=': operator.ne, '=': operator.eq}
41
42class JSONSettingsHandler(object):
43    def __init__(self, filepath, notify_callback=None):
44        super(JSONSettingsHandler, self).__init__()
45
46        self.resume_timeout = None
47        self.notify_callback = notify_callback
48
49        self.filepath = filepath
50        self.file_obj = Gio.File.new_for_path(self.filepath)
51        self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None)
52        self.file_monitor.connect("changed", self.check_settings)
53
54        self.bindings = {}
55        self.listeners = {}
56        self.deps = {}
57
58        self.settings = self.get_settings()
59
60    def bind(self, key, obj, prop, direction, map_get=None, map_set=None):
61        if direction & (Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET) == 0:
62            direction |= Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET
63
64        binding_info = {"obj": obj, "prop": prop, "dir": direction, "map_get": map_get, "map_set": map_set}
65        if key not in self.bindings:
66            self.bindings[key] = []
67        self.bindings[key].append(binding_info)
68
69        if direction & Gio.SettingsBindFlags.GET != 0:
70            self.set_object_value(binding_info, self.get_value(key))
71        if direction & Gio.SettingsBindFlags.SET != 0:
72            binding_info["oid"] = obj.connect("notify::"+prop, self.object_value_changed, key)
73
74    def listen(self, key, callback):
75        if key not in self.listeners:
76            self.listeners[key] = []
77        self.listeners[key].append(callback)
78
79    def get_value(self, key):
80        return self.get_property(key, "value")
81
82    def set_value(self, key, value):
83        if value != self.settings[key]["value"]:
84            self.settings[key]["value"] = value
85            self.save_settings()
86            if self.notify_callback:
87                self.notify_callback(self, key, value)
88
89            if key in self.bindings:
90                for info in self.bindings[key]:
91                    self.set_object_value(info, value)
92
93            if key in self.listeners:
94                for callback in self.listeners[key]:
95                    callback(key, value)
96
97    def get_property(self, key, prop):
98        props = self.settings[key]
99        return props[prop]
100
101    def has_property(self, key, prop):
102        return prop in self.settings[key]
103
104    def has_key(self, key):
105        return key in self.settings
106
107    def object_value_changed(self, obj, value, key):
108        for info in self.bindings[key]:
109            if obj == info["obj"]:
110                value = info["obj"].get_property(info["prop"])
111                if "map_set" in info and info["map_set"] != None:
112                    value = info["map_set"](value)
113
114        for info in self.bindings[key]:
115            if obj != info["obj"]:
116                self.set_object_value(info, value)
117        self.set_value(key, value)
118
119        if key in self.listeners:
120            for callback in self.listeners[key]:
121                callback(key, value)
122
123    def set_object_value(self, info, value):
124        if info["dir"] & Gio.SettingsBindFlags.GET == 0:
125            return
126
127        with info["obj"].freeze_notify():
128            if "map_get" in info and info["map_get"] != None:
129                value = info["map_get"](value)
130            if value != info["obj"].get_property(info["prop"]) and value is not None:
131                info["obj"].set_property(info["prop"], value)
132
133    def check_settings(self, *args):
134        old_settings = self.settings
135        self.settings = self.get_settings()
136
137        for key in self.bindings:
138            new_value = self.settings[key]["value"]
139            if new_value != old_settings[key]["value"]:
140                for info in self.bindings[key]:
141                    self.set_object_value(info, new_value)
142
143        for key, callback_list in self.listeners.items():
144            new_value = self.settings[key]["value"]
145            if new_value != old_settings[key]["value"]:
146                for callback in callback_list:
147                    callback(key, new_value)
148
149    def get_settings(self):
150        file = open(self.filepath)
151        raw_data = file.read()
152        file.close()
153        try:
154            settings = json.loads(raw_data, object_pairs_hook=collections.OrderedDict)
155        except:
156            raise Exception("Failed to parse settings JSON data for file %s" % (self.filepath))
157        return settings
158
159    def save_settings(self):
160        self.pause_monitor()
161        if os.path.exists(self.filepath):
162            os.remove(self.filepath)
163        raw_data = json.dumps(self.settings, indent=4)
164        new_file = open(self.filepath, 'w+')
165        new_file.write(raw_data)
166        new_file.close()
167        self.resume_monitor()
168
169    def pause_monitor(self):
170        self.file_monitor.cancel()
171        self.handler = None
172
173    def resume_monitor(self):
174        if self.resume_timeout:
175            GLib.source_remove(self.resume_timeout)
176        self.resume_timeout = GLib.timeout_add(2000, self.do_resume)
177
178    def do_resume(self):
179        self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None)
180        self.handler = self.file_monitor.connect("changed", self.check_settings)
181        self.resume_timeout = None
182        return False
183
184    def reset_to_defaults(self):
185        for key in self.settings:
186            if "value" in self.settings[key]:
187                self.settings[key]["value"] = self.settings[key]["default"]
188                self.do_key_update(key)
189
190        self.save_settings()
191
192    def do_key_update(self, key):
193        if key in self.bindings:
194            for info in self.bindings[key]:
195                self.set_object_value(info, self.settings[key]["value"])
196
197        if key in self.listeners:
198            for callback in self.listeners[key]:
199                callback(key, self.settings[key]["value"])
200
201    def load_from_file(self, filepath):
202        file = open(filepath)
203        raw_data = file.read()
204        file.close()
205        try:
206            settings = json.loads(raw_data, encoding=None, object_pairs_hook=collections.OrderedDict)
207        except:
208            raise Exception("Failed to parse settings JSON data for file %s" % (self.filepath))
209
210        for key in self.settings:
211            if "value" not in self.settings[key]:
212                continue
213            if key in settings and "value" in self.settings[key]:
214                self.settings[key]["value"] = settings[key]["value"]
215                self.do_key_update(key)
216            else:
217                print("Skipping key %s: the key does not exist in %s or has no value" % (key, filepath))
218        self.save_settings()
219
220    def save_to_file(self, filepath):
221        if os.path.exists(filepath):
222            os.remove(filepath)
223        raw_data = json.dumps(self.settings, indent=4)
224        new_file = open(filepath, 'w+')
225        new_file.write(raw_data)
226        new_file.close()
227
228class JSONSettingsRevealer(Gtk.Revealer):
229    def __init__(self, settings, key):
230        super(JSONSettingsRevealer, self).__init__()
231        self.settings = settings
232
233        self.key = None
234        self.op = None
235        self.value = None
236        for op in OPERATIONS:
237            if op in key:
238                self.op = op
239                self.key, self.value = key.split(op)
240                break
241
242        if self.key is None:
243            if key[:1] == '!':
244                self.invert = True
245                self.key = key[1:]
246            else:
247                self.invert = False
248                self.key = key
249
250        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15)
251        Gtk.Revealer.add(self, self.box)
252
253        self.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN)
254        self.set_transition_duration(150)
255
256        self.settings.listen(self.key, self.key_changed)
257        self.key_changed(self.key, self.settings.get_value(self.key))
258
259    def add(self, widget):
260        self.box.pack_start(widget, False, True, 0)
261
262    def key_changed(self, key, value):
263        if self.op is not None:
264            val_type = type(value)
265            self.set_reveal_child(OPERATIONS_MAP[self.op](value, val_type(self.value)))
266        elif value != self.invert:
267            self.set_reveal_child(True)
268        else:
269            self.set_reveal_child(False)
270
271class JSONSettingsBackend(object):
272    def attach(self):
273        self._saving = False
274
275        if hasattr(self, "set_rounding") and self.settings.has_property(self.key, "round"):
276            self.set_rounding(self.settings.get_property(self.key, "round"))
277        if hasattr(self, "bind_object"):
278            bind_object = self.bind_object
279        else:
280            bind_object = self.content_widget
281        if self.bind_dir != None:
282            self.settings.bind(self.key, bind_object, self.bind_prop, self.bind_dir,
283                               self.map_get if hasattr(self, "map_get") else None,
284                               self.map_set if hasattr(self, "map_set") else None)
285        else:
286            self.settings.listen(self.key, self._settings_changed_callback)
287            self.on_setting_changed()
288            self.connect_widget_handlers()
289
290    def set_value(self, value):
291        self._saving = True
292        self.settings.set_value(self.key, value)
293        self._saving = False
294
295    def get_value(self):
296        return self.settings.get_value(self.key)
297
298    def get_range(self):
299        min = self.settings.get_property(self.key, "min")
300        max = self.settings.get_property(self.key, "max")
301        return [min, max]
302
303    def _settings_changed_callback(self, *args):
304        if not self._saving:
305            self.on_setting_changed(*args)
306
307    def on_setting_changed(self, *args):
308        raise NotImplementedError("SettingsWidget class must implement on_setting_changed().")
309
310    def connect_widget_handlers(self, *args):
311        if self.bind_dir == None:
312            raise NotImplementedError("SettingsWidget classes with no .bind_dir must implement connect_widget_handlers().")
313
314def json_settings_factory(subclass):
315    class NewClass(globals()[subclass], JSONSettingsBackend):
316        def __init__(self, key, settings, properties):
317            self.key = key
318            self.settings = settings
319
320            kwargs = {}
321            for prop in properties:
322                if prop in JSON_SETTINGS_PROPERTIES_MAP:
323                    kwargs[JSON_SETTINGS_PROPERTIES_MAP[prop]] = properties[prop]
324                elif prop == "options":
325                    kwargs["options"] = []
326                    for value, label in properties[prop].items():
327                        kwargs["options"].append((label, value))
328            super(NewClass, self).__init__(**kwargs)
329            self.attach()
330
331    return NewClass
332
333for widget in can_backend:
334    globals()["JSONSettings"+widget] = json_settings_factory(widget)
335