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