1#!/usr/local/bin/python3.8 2import getopt 3import gi 4gi.require_version('Gtk', '3.0') 5gi.require_version('XApp', '1.0') 6 7import os 8import sys 9from setproctitle import setproctitle 10import config 11sys.path.append(config.currentPath + "/bin") 12import gettext 13import json 14import importlib.util 15import traceback 16 17from JsonSettingsWidgets import * 18from ExtensionCore import find_extension_subdir 19from gi.repository import Gtk, Gio, XApp 20 21# i18n 22gettext.install("cinnamon", "/usr/local/share/locale") 23 24home = os.path.expanduser("~") 25 26translations = {} 27 28proxy = None 29 30XLET_SETTINGS_WIDGETS = { 31 "entry" : "JSONSettingsEntry", 32 "textview" : "JSONSettingsTextView", 33 "checkbox" : "JSONSettingsSwitch", # deprecated: please use switch instead 34 "switch" : "JSONSettingsSwitch", 35 "spinbutton" : "JSONSettingsSpinButton", 36 "filechooser" : "JSONSettingsFileChooser", 37 "scale" : "JSONSettingsRange", 38 "radiogroup" : "JSONSettingsComboBox", # deprecated: please use combobox instead 39 "combobox" : "JSONSettingsComboBox", 40 "colorchooser" : "JSONSettingsColorChooser", 41 "fontchooser" : "JSONSettingsFontButton", 42 "soundfilechooser" : "JSONSettingsSoundFileChooser", 43 "iconfilechooser" : "JSONSettingsIconChooser", 44 "tween" : "JSONSettingsTweenChooser", 45 "effect" : "JSONSettingsEffectChooser", 46 "datechooser" : "JSONSettingsDateChooser", 47 "timechooser" : "JSONSettingsTimeChooser", 48 "keybinding" : "JSONSettingsKeybinding", 49 "list" : "JSONSettingsList" 50} 51 52class XLETSettingsButton(Button): 53 def __init__(self, info, uuid, instance_id): 54 super(XLETSettingsButton, self).__init__(info["description"]) 55 self.uuid = uuid 56 self.instance_id = instance_id 57 self.xletCallback = str(info["callback"]) 58 59 def on_activated(self): 60 proxy.activateCallback('(sss)', self.xletCallback, self.uuid, self.instance_id) 61 62def translate(uuid, string): 63 #check for a translation for this xlet 64 if uuid not in translations: 65 try: 66 translations[uuid] = gettext.translation(uuid, home + "/.local/share/locale").gettext 67 except IOError: 68 try: 69 translations[uuid] = gettext.translation(uuid, "/usr/local/share/locale").gettext 70 except IOError: 71 translations[uuid] = None 72 73 #do not translate whitespaces 74 if not string.strip(): 75 return string 76 77 if translations[uuid]: 78 result = translations[uuid](string) 79 80 try: 81 result = result.decode("utf-8") 82 except (AttributeError, UnicodeDecodeError): 83 result = result 84 85 if result != string: 86 return result 87 return _(string) 88 89class MainWindow(object): 90 def __init__(self, xlet_type, uuid, *instance_id): 91 ## Respecting preview implementation, add the possibility to open a specific tab (if there 92 ## are multiple layouts) and/or a specific instance settings (if there are multiple 93 ## instances of a xlet). 94 ## To do this, two new arguments: 95 ## -t <n> or --tab=<n>, where <n> is the tab index (starting at 0). 96 ## -i <id> or --id=<id>, where <id> is the id of the instance. 97 ## Examples, supposing there are two instances of Cinnamenu@json applet, with ids '210' and 98 ## '235' (uncomment line 144 containing print("self.instance_info =", self.instance_info) 99 ## to know all instances ids): 100 ## (Please note that cinnamon-settings is the one offered in #8333) 101 ## cinnamon-settings applets Cinnamenu@json # opens first tab in first instance 102 ## cinnamon-settings applets Cinnamenu@json '235' # opens first tab in '235' instance 103 ## cinnamon-settings applets Cinnamenu@json 235 # idem 104 ## cinnamon-settings applets Cinnamenu@json -t 1 -i 235 # opens 2nd tab in '235' instance 105 ## cinnamon-settings applets Cinnamenu@json --tab=1 --id=235 # idem 106 ## cinnamon-settings applets Cinnamenu@json --tab=1 # opens 2nd tab in first instance 107 ## (Also works with 'xlet-settings applet' instead of 'cinnamon-settings applets'.) 108 109 #print("instance_id =", instance_id) 110 self.tab = 0 111 opts = [] 112 try: 113 instance_id = int(instance_id[0]) 114 except (TypeError, ValueError, IndexError): 115 instance_id = None 116 try: 117 if len(sys.argv) > 3: 118 opts = getopt.getopt(sys.argv[3:], "t:i:", ["tab=", "id="])[0] 119 except getopt.GetoptError: 120 pass 121 if len(sys.argv) > 4: 122 try: 123 instance_id = int(sys.argv[4]) 124 except ValueError: 125 instance_id = None 126 #print("opts =", opts) 127 for opt, arg in opts: 128 if opt in ("-t", "--tab"): 129 if arg.isdecimal(): 130 self.tab = int(arg) 131 elif opt in ("-i", "--id"): 132 if arg.isdecimal(): 133 instance_id = int(arg) 134 if instance_id: 135 instance_id = str(instance_id) 136 #print("self.tab =", self.tab) 137 #print("instance_id =", instance_id) 138 self.type = xlet_type 139 self.uuid = uuid 140 self.selected_instance = None 141 self.gsettings = Gio.Settings.new("org.cinnamon") 142 self.custom_modules = {} 143 144 self.load_xlet_data() 145 self.build_window() 146 self.load_instances() 147 #print("self.instance_info =", self.instance_info) 148 self.window.show_all() 149 if instance_id and len(self.instance_info) > 1: 150 for info in self.instance_info: 151 if info["id"] == instance_id: 152 self.set_instance(info) 153 break 154 else: 155 self.set_instance(self.instance_info[0]) 156 try: 157 Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, 158 "org.Cinnamon", "/org/Cinnamon", "org.Cinnamon", None, self._on_proxy_ready, None) 159 except dbus.exceptions.DBusException as e: 160 print(e) 161 162 def _on_proxy_ready (self, obj, result, data=None): 163 global proxy 164 proxy = Gio.DBusProxy.new_for_bus_finish(result) 165 166 if not proxy.get_name_owner(): 167 proxy = None 168 169 if proxy: 170 proxy.highlightXlet('(ssb)', self.uuid, self.selected_instance["id"], True) 171 172 def load_xlet_data (self): 173 self.xlet_dir = "/usr/local/share/cinnamon/%ss/%s" % (self.type, self.uuid) 174 if not os.path.exists(self.xlet_dir): 175 self.xlet_dir = "%s/.local/share/cinnamon/%ss/%s" % (home, self.type, self.uuid) 176 177 if os.path.exists("%s/metadata.json" % self.xlet_dir): 178 raw_data = open("%s/metadata.json" % self.xlet_dir).read() 179 self.xlet_meta = json.loads(raw_data) 180 else: 181 print("Could not find %s metadata for uuid %s - are you sure it's installed correctly?" % (self.type, self.uuid)) 182 quit() 183 184 def build_window(self): 185 self.window = XApp.GtkWindow() 186 self.window.set_default_size(800, 600) 187 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 188 self.window.add(main_box) 189 190 toolbar = Gtk.Toolbar() 191 toolbar.get_style_context().add_class("primary-toolbar") 192 main_box.add(toolbar) 193 194 toolitem = Gtk.ToolItem() 195 toolitem.set_expand(True) 196 toolbar.add(toolitem) 197 toolbutton_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 198 toolitem.add(toolbutton_box) 199 instance_button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 200 instance_button_box.get_style_context().add_class("linked") 201 toolbutton_box.pack_start(instance_button_box, False, False, 0) 202 203 self.prev_button = Gtk.Button.new_from_icon_name('go-previous-symbolic', Gtk.IconSize.BUTTON) 204 self.prev_button.set_tooltip_text(_("Previous instance")) 205 instance_button_box.add(self.prev_button) 206 207 self.next_button = Gtk.Button.new_from_icon_name('go-next-symbolic', Gtk.IconSize.BUTTON) 208 self.next_button.set_tooltip_text(_("Next instance")) 209 instance_button_box.add(self.next_button) 210 211 self.stack_switcher = Gtk.StackSwitcher() 212 toolbutton_box.set_center_widget(self.stack_switcher) 213 214 self.menu_button = Gtk.MenuButton() 215 image = Gtk.Image.new_from_icon_name("open-menu-symbolic", Gtk.IconSize.BUTTON) 216 self.menu_button.add(image) 217 self.menu_button.set_tooltip_text(_("More options")) 218 toolbutton_box.pack_end(self.menu_button, False, False, 0) 219 220 menu = Gtk.Menu() 221 menu.set_halign(Gtk.Align.END) 222 223 restore_option = Gtk.MenuItem(label=_("Import from a file")) 224 menu.append(restore_option) 225 restore_option.connect("activate", self.restore) 226 restore_option.show() 227 228 backup_option = Gtk.MenuItem(label=_("Export to a file")) 229 menu.append(backup_option) 230 backup_option.connect("activate", self.backup) 231 backup_option.show() 232 233 reset_option = Gtk.MenuItem(label=_("Reset to defaults")) 234 menu.append(reset_option) 235 reset_option.connect("activate", self.reset) 236 reset_option.show() 237 238 separator = Gtk.SeparatorMenuItem() 239 menu.append(separator) 240 separator.show() 241 242 reload_option = Gtk.MenuItem(label=_("Reload %s") % self.uuid) 243 menu.append(reload_option) 244 reload_option.connect("activate", self.reload_xlet) 245 reload_option.show() 246 247 self.menu_button.set_popup(menu) 248 249 scw = Gtk.ScrolledWindow() 250 scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) 251 main_box.pack_start(scw, True, True, 0) 252 self.instance_stack = Gtk.Stack() 253 scw.add(self.instance_stack) 254 255 if "icon" in self.xlet_meta: 256 self.window.set_icon_name(self.xlet_meta["icon"]) 257 else: 258 icon_path = os.path.join(self.xlet_dir, "icon.svg") 259 if os.path.exists(icon_path): 260 self.window.set_icon_from_file(icon_path) 261 else: 262 icon_path = os.path.join(self.xlet_dir, "icon.png") 263 if os.path.exists(icon_path): 264 self.window.set_icon_from_file(icon_path) 265 self.window.set_title(translate(self.uuid, self.xlet_meta["name"])) 266 267 def check_sizing(widget, data=None): 268 natreq = self.window.get_preferred_size()[1] 269 monitor = Gdk.Display.get_default().get_monitor_at_window(self.window.get_window()) 270 271 height = monitor.get_workarea().height 272 if natreq.height > height - 100: 273 self.window.resize(800, 600) 274 scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 275 276 self.window.connect("destroy", self.quit) 277 self.window.connect("realize", check_sizing) 278 self.prev_button.connect("clicked", self.previous_instance) 279 self.next_button.connect("clicked", self.next_instance) 280 281 def load_instances(self): 282 self.instance_info = [] 283 path = "%s/.cinnamon/configs/%s" % (home, self.uuid) 284 instances = 0 285 dir_items = sorted(os.listdir(path)) 286 try: 287 multi_instance = int(self.xlet_meta["max-instances"]) != 1 288 except (KeyError, ValueError): 289 multi_instance = False 290 291 for item in dir_items: 292 # ignore anything that isn't json 293 if item[-5:] != ".json": 294 continue 295 296 instance_id = item[0:-5] 297 if not multi_instance and instance_id != self.uuid: 298 continue # for single instance the file name should be [uuid].json 299 300 if multi_instance: 301 try: 302 int(instance_id) 303 except (TypeError, ValueError): 304 traceback.print_exc() 305 continue # multi-instance should have file names of the form [instance-id].json 306 307 instance_exists = False 308 enabled = self.gsettings.get_strv('enabled-%ss' % self.type) 309 for deninition in enabled: 310 if uuid in deninition and instance_id in deninition.split(':'): 311 instance_exists = True 312 break 313 314 if not instance_exists: 315 continue 316 317 settings = JSONSettingsHandler(os.path.join(path, item), self.notify_dbus) 318 settings.instance_id = instance_id 319 instance_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 320 self.instance_stack.add_named(instance_box, instance_id) 321 322 info = {"settings": settings, "id": instance_id} 323 self.instance_info.append(info) 324 325 settings_map = settings.get_settings() 326 first_key = next(iter(settings_map.values())) 327 328 try: 329 for setting in settings_map: 330 if setting == "__md5__": 331 continue 332 for key in settings_map[setting]: 333 if key in ("description", "tooltip", "units"): 334 try: 335 settings_map[setting][key] = translate(self.uuid, settings_map[setting][key]) 336 except (KeyError, ValueError): 337 traceback.print_exc() 338 elif key in "options": 339 new_opt_data = collections.OrderedDict() 340 opt_data = settings_map[setting][key] 341 for option in opt_data: 342 if opt_data[option] == "custom": 343 continue 344 new_opt_data[translate(self.uuid, option)] = opt_data[option] 345 settings_map[setting][key] = new_opt_data 346 elif key in "columns": 347 columns_data = settings_map[setting][key] 348 for column in columns_data: 349 column["title"] = translate(self.uuid, column["title"]) 350 finally: 351 # if a layout is not explicitly defined, generate the settings 352 # widgets based on the order they occur 353 if first_key["type"] == "layout": 354 self.build_with_layout(settings_map, info, instance_box, first_key) 355 else: 356 self.build_from_order(settings_map, info, instance_box, first_key) 357 358 if self.selected_instance is None: 359 self.selected_instance = info 360 if "stack" in info: 361 self.stack_switcher.set_stack(info["stack"]) 362 363 instances += 1 364 365 if instances < 2: 366 self.prev_button.set_no_show_all(True) 367 self.next_button.set_no_show_all(True) 368 369 def build_with_layout(self, settings_map, info, box, first_key): 370 layout = first_key 371 372 page_stack = SettingsStack() 373 box.pack_start(page_stack, True, True, 0) 374 self.stack_switcher.show() 375 info["stack"] = page_stack 376 377 for page_key in layout["pages"]: 378 page_def = layout[page_key] 379 if page_def['type'] == 'custom': 380 page = self.create_custom_widget(page_def, info['settings']) 381 if page is None: 382 continue 383 elif not isinstance(page, SettingsPage): 384 print('page is not of type SettingsPage') 385 continue 386 else: 387 page = SettingsPage() 388 for section_key in page_def["sections"]: 389 section_def = layout[section_key] 390 if 'dependency' in section_def: 391 revealer = JSONSettingsRevealer(info['settings'], section_def['dependency']) 392 section = page.add_reveal_section(translate(self.uuid, section_def["title"]), revealer=revealer) 393 else: 394 section = page.add_section(translate(self.uuid, section_def["title"])) 395 for key in section_def["keys"]: 396 item = settings_map[key] 397 settings_type = item["type"] 398 if settings_type == "button": 399 widget = XLETSettingsButton(item, self.uuid, info["id"]) 400 elif settings_type == "label": 401 widget = Text(translate(self.uuid, item["description"])) 402 elif settings_type == 'custom': 403 widget = self.create_custom_widget(item, key, info['settings']) 404 if widget is None: 405 continue 406 elif not isinstance(widget, SettingsWidget): 407 print('widget is not of type SettingsWidget') 408 continue 409 elif settings_type in XLET_SETTINGS_WIDGETS: 410 widget = globals()[XLET_SETTINGS_WIDGETS[settings_type]](key, info["settings"], item) 411 else: 412 continue 413 414 if 'dependency' in item: 415 revealer = JSONSettingsRevealer(info['settings'], item['dependency']) 416 section.add_reveal_row(widget, revealer=revealer) 417 else: 418 section.add_row(widget) 419 page_stack.add_titled(page, page_key, translate(self.uuid, page_def["title"])) 420 421 def build_from_order(self, settings_map, info, box, first_key): 422 page = SettingsPage() 423 box.pack_start(page, True, True, 0) 424 425 # if the first key is not of type 'header' or type 'section' we need to make a new section 426 if first_key["type"] not in ("header", "section"): 427 section = page.add_section(_("Settings for %s") % self.uuid) 428 429 for key, item in settings_map.items(): 430 if key == "__md5__": 431 continue 432 if "type" in item: 433 settings_type = item["type"] 434 if settings_type in ("header", "section"): 435 if 'dependency' in item: 436 revealer = JSONSettingsRevealer(info['settings'], item['dependency']) 437 section = page.add_reveal_section(translate(self.uuid, item["description"]), revealer=revealer) 438 else: 439 section = page.add_section(translate(self.uuid, item["description"])) 440 continue 441 442 if settings_type == "button": 443 widget = XLETSettingsButton(item, self.uuid, info["id"]) 444 elif settings_type == "label": 445 widget = Text(translate(self.uuid, item["description"])) 446 elif settings_type == 'custom': 447 widget = self.create_custom_widget(item, key, info['settings']) 448 if widget is None: 449 continue 450 elif not isinstance(widget, SettingsWidget): 451 print('widget is not of type SettingsWidget') 452 continue 453 elif settings_type in XLET_SETTINGS_WIDGETS: 454 widget = globals()[XLET_SETTINGS_WIDGETS[settings_type]](key, info["settings"], item) 455 else: 456 continue 457 458 if 'dependency' in item: 459 revealer = JSONSettingsRevealer(info['settings'], item['dependency']) 460 section.add_reveal_row(widget, revealer=revealer) 461 else: 462 section.add_row(widget) 463 464 def create_custom_widget(self, info, *args): 465 file_name = info['file'] 466 widget_name = info['widget'] 467 file_path = os.path.join(find_extension_subdir(self.xlet_dir), file_name) 468 469 try: 470 if file_name not in self.custom_modules: 471 spec = importlib.util.spec_from_file_location(self.uuid.replace('@', '') + '.' + file_name.split('.')[0], file_path) 472 module = importlib.util.module_from_spec(spec) 473 spec.loader.exec_module(module) 474 self.custom_modules[file_name] = module 475 476 except KeyError: 477 traceback.print_exc() 478 print('problem loading custom widget') 479 return None 480 481 return getattr(self.custom_modules[file_name], widget_name)(info, *args) 482 483 def notify_dbus(self, handler, key, value): 484 proxy.updateSetting('(ssss)', self.uuid, handler.instance_id, key, json.dumps(value)) 485 486 def set_instance(self, info): 487 self.instance_stack.set_visible_child_name(info["id"]) 488 if "stack" in info: 489 self.stack_switcher.set_stack(info["stack"]) 490 children = info["stack"].get_children() 491 if len(children) > 1: 492 if self.tab in range(len(children)): 493 info["stack"].set_visible_child(children[self.tab]) 494 else: 495 info["stack"].set_visible_child(children[0]) 496 if proxy: 497 proxy.highlightXlet('(ssb)', self.uuid, self.selected_instance["id"], False) 498 proxy.highlightXlet('(ssb)', self.uuid, info["id"], True) 499 self.selected_instance = info 500 501 def previous_instance(self, *args): 502 self.instance_stack.set_transition_type(Gtk.StackTransitionType.OVER_RIGHT) 503 index = self.instance_info.index(self.selected_instance) 504 self.set_instance(self.instance_info[index-1]) 505 506 def next_instance(self, *args): 507 self.instance_stack.set_transition_type(Gtk.StackTransitionType.OVER_LEFT) 508 index = self.instance_info.index(self.selected_instance) 509 if index == len(self.instance_info) - 1: 510 index = 0 511 else: 512 index +=1 513 self.set_instance(self.instance_info[index]) 514 515 # def unpack_args(self, args): 516 # args = {} 517 518 def backup(self, *args): 519 dialog = Gtk.FileChooserDialog(_("Select or enter file to export to"), 520 None, 521 Gtk.FileChooserAction.SAVE, 522 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 523 Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)) 524 dialog.set_do_overwrite_confirmation(True) 525 filter_text = Gtk.FileFilter() 526 filter_text.add_pattern("*.json") 527 filter_text.set_name(_("JSON files")) 528 dialog.add_filter(filter_text) 529 530 response = dialog.run() 531 532 if response == Gtk.ResponseType.ACCEPT: 533 filename = dialog.get_filename() 534 if ".json" not in filename: 535 filename = filename + ".json" 536 self.selected_instance["settings"].save_to_file(filename) 537 538 dialog.destroy() 539 540 def restore(self, *args): 541 dialog = Gtk.FileChooserDialog(_("Select a JSON file to import"), 542 None, 543 Gtk.FileChooserAction.OPEN, 544 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 545 Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) 546 filter_text = Gtk.FileFilter() 547 filter_text.add_pattern("*.json") 548 filter_text.set_name(_("JSON files")) 549 dialog.add_filter(filter_text) 550 551 response = dialog.run() 552 553 if response == Gtk.ResponseType.OK: 554 filename = dialog.get_filename() 555 self.selected_instance["settings"].load_from_file(filename) 556 557 dialog.destroy() 558 559 def reset(self, *args): 560 self.selected_instance["settings"].reset_to_defaults() 561 562 def reload_xlet(self, *args): 563 if proxy: 564 proxy.ReloadXlet('(ss)', self.uuid, self.type.upper()) 565 566 def quit(self, *args): 567 if proxy: 568 proxy.highlightXlet('(ssb)', self.uuid, self.selected_instance["id"], False) 569 570 self.window.destroy() 571 Gtk.main_quit() 572 573if __name__ == "__main__": 574 setproctitle("xlet-settings") 575 import signal 576 if len(sys.argv) < 3: 577 print("Error: requires type and uuid") 578 quit() 579 xlet_type = sys.argv[1] 580 if xlet_type not in ["applet", "desklet", "extension"]: 581 print("Error: Invalid xlet type %s", sys.argv[1]) 582 quit() 583 uuid = sys.argv[2] 584 window = MainWindow(xlet_type, uuid, *sys.argv[3:]) 585 signal.signal(signal.SIGINT, window.quit) 586 Gtk.main() 587