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