1#!/usr/local/bin/python3.8
2
3from gi.repository import Gio, GLib
4from xapp.SettingsWidgets import *
5
6CAN_BACKEND = ["Switch", "SpinButton", "Entry", "TextView", "FontButton", "Range", "ComboBox",
7               "ColorChooser", "FileChooser", "IconChooser"]
8
9# Monkey patch Gio.Settings object
10def __setitem__(self, key, value):
11    # set_value() aborts the program on an unknown key
12    if key not in self:
13        raise KeyError('unknown key: %r' % (key,))
14
15    # determine type string of this key
16    range = self.get_range(key)
17    type_ = range.get_child_value(0).get_string()
18    v = range.get_child_value(1)
19    if type_ == 'type':
20        # v is boxed empty array, type of its elements is the allowed value type
21        assert v.get_child_value(0).get_type_string().startswith('a')
22        type_str = v.get_child_value(0).get_type_string()[1:]
23    elif type_ == 'enum':
24        # v is an array with the allowed values
25        assert v.get_child_value(0).get_type_string().startswith('a')
26        type_str = v.get_child_value(0).get_child_value(0).get_type_string()
27    elif type_ == 'flags':
28        # v is an array with the allowed values
29        assert v.get_child_value(0).get_type_string().startswith('a')
30        type_str = v.get_child_value(0).get_type_string()
31    elif type_ == 'range':
32        # type_str is a tuple giving the range
33        assert v.get_child_value(0).get_type_string().startswith('(')
34        type_str = v.get_child_value(0).get_type_string()[1]
35
36    if not self.set_value(key, GLib.Variant(type_str, value)):
37        raise ValueError("value '%s' for key '%s' is outside of valid range" % (value, key))
38
39def bind_with_mapping(self, key, widget, prop, flags, key_to_prop, prop_to_key):
40    self._ignore_key_changed = False
41
42    def key_changed(settings, key):
43        if self._ignore_key_changed:
44            return
45        self._ignore_prop_changed = True
46        widget.set_property(prop, key_to_prop(self[key]))
47        self._ignore_prop_changed = False
48
49    def prop_changed(widget, param):
50        if self._ignore_prop_changed:
51            return
52        self._ignore_key_changed = True
53        self[key] = prop_to_key(widget.get_property(prop))
54        self._ignore_key_changed = False
55
56    if not (flags & (Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET)): # ie Gio.SettingsBindFlags.DEFAULT
57        flags |= Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET
58    if flags & Gio.SettingsBindFlags.GET:
59        key_changed(self, key)
60        if not (flags & Gio.SettingsBindFlags.GET_NO_CHANGES):
61            self.connect('changed::' + key, key_changed)
62    if flags & Gio.SettingsBindFlags.SET:
63        widget.connect('notify::' + prop, prop_changed)
64    if not (flags & Gio.SettingsBindFlags.NO_SENSITIVITY):
65        self.bind_writable(key, widget, "sensitive", False)
66
67Gio.Settings.bind_with_mapping = bind_with_mapping
68Gio.Settings.__setitem__ = __setitem__
69
70class BinFileMonitor(GObject.GObject):
71    __gsignals__ = {
72        'changed': (GObject.SignalFlags.RUN_LAST, None, ()),
73    }
74    def __init__(self):
75        super(BinFileMonitor, self).__init__()
76
77        self.changed_id = 0
78
79        env = GLib.getenv("PATH")
80
81        if env == None:
82            env = "/bin:/usr/bin:."
83
84        self.paths = env.split(":")
85
86        self.monitors = []
87
88        for path in self.paths:
89            file = Gio.File.new_for_path(path)
90            mon = file.monitor_directory(Gio.FileMonitorFlags.SEND_MOVED, None)
91            mon.connect("changed", self.queue_emit_changed)
92            self.monitors.append(mon)
93
94    def _emit_changed(self):
95        self.emit("changed")
96        self.changed_id = 0
97        return False
98
99    def queue_emit_changed(self, file, other, event_type, data=None):
100        if self.changed_id > 0:
101            GLib.source_remove(self.changed_id)
102            self.changed_id = 0
103
104        self.changed_id = GLib.idle_add(self._emit_changed)
105
106file_monitor = None
107
108def get_file_monitor():
109    global file_monitor
110
111    if file_monitor == None:
112        file_monitor = BinFileMonitor()
113
114    return file_monitor
115
116# This class is not meant to be used directly - it is only a backend for the
117# settings widgets to enable them to bind attributes to gsettings keys. To use
118# the gesttings backend, simply add the "GSettings" prefix to the beginning
119# of the widget class name. The arguments of the backended class will be
120# (label, schema, key, any additional widget-specific args and keyword args).
121# (Note: this only works for classes that are gsettings compatible.)
122#
123# If you wish to make a new widget available to be backended, place it in the
124# CAN_BACKEND list. In addition, you will need to add the following attributes
125# to the widget class:
126#
127# bind_dir - (Gio.SettingsBindFlags) flags to define the binding direction or
128#            None if you don't want the setting bound (for example if the
129#            setting effects multiple attributes)
130# bind_prop - (string) the attribute in the widget that will be bound to the
131#             setting. This property may be omitted if bind_dir is None
132# bind_object - (optional) the object to which to bind to (only needed if the
133#               attribute to be bound is not a property of self.content_widget)
134# map_get, map_set - (function, optional) a function to map between setting and
135#                    bound attribute. May also be passed as a keyword arg during
136#                    instantiation. These will be ignored if bind_dir=None
137# set_rounding - (function, optional) To be used to set the digits to round to
138#                if the setting is an integer
139class PXGSettingsBackend(object):
140    def bind_settings(self):
141        if hasattr(self, "set_rounding"):
142            vtype = self.settings.get_value(self.key).get_type_string()
143            if vtype in ["i", "u"]:
144                self.set_rounding(0)
145        if hasattr(self, "bind_object"):
146            bind_object = self.bind_object
147        else:
148            bind_object = self.content_widget
149        if hasattr(self, "map_get") or hasattr(self, "map_set"):
150            self.settings.bind_with_mapping(self.key, bind_object, self.bind_prop, self.bind_dir, self.map_get, self.map_set)
151        elif self.bind_dir != None:
152            self.settings.bind(self.key, bind_object, self.bind_prop, self.bind_dir)
153        else:
154            self.settings.connect("changed::"+self.key, self.on_setting_changed)
155            self.settings.bind_writable(self.key, bind_object, "sensitive", False)
156            self.on_setting_changed()
157            self.connect_widget_handlers()
158
159    def set_value(self, value):
160        self.settings[self.key] = value
161
162    def get_value(self):
163        return self.settings[self.key]
164
165    def get_range(self):
166        range = self.settings.get_range(self.key)
167        if range[0] == "range":
168            return [range[1][0], range[1][1]]
169        else:
170            return None
171
172    def on_setting_changed(self, *args):
173        raise NotImplementedError("SettingsWidget class must implement on_setting_changed().")
174
175    def connect_widget_handlers(self, *args):
176        if self.bind_dir == None:
177            raise NotImplementedError("SettingsWidget classes with no .bind_dir must implement connect_widget_handlers().")
178
179def g_settings_factory(subclass):
180    class NewClass(globals()[subclass], PXGSettingsBackend):
181        def __init__(self, label, schema, key, *args, **kwargs):
182            self.key = key
183            if schema not in settings_objects:
184                settings_objects[schema] = Gio.Settings.new(schema)
185            self.settings = settings_objects[schema]
186
187            if "map_get" in kwargs:
188                self.map_get = kwargs["map_get"]
189                del kwargs["map_get"]
190            if "map_set" in kwargs:
191                self.map_set = kwargs["map_set"]
192                del kwargs["map_set"]
193
194            super(NewClass, self).__init__(label, *args, **kwargs)
195            self.bind_settings()
196    return NewClass
197
198for widget in CAN_BACKEND:
199    globals()["GSettings"+widget] = g_settings_factory(widget)
200