From 52f37b29b2ec0d73e41140f8037bbdf1dea4646a Mon Sep 17 00:00:00 2001 From: Samuel Wilton Date: Sun, 24 Aug 2025 11:17:56 -0400 Subject: [PATCH] Added check to exit early if no existing instances of xlet are found. Added logic to handle adding and removing xlet while xlet-settings is open. - Arrows will be hidden if number of instances is less than 2 - Arrows will be shown if number of instances is greater than 1 - Next instance will be selected if currently selected xlet is removed - xlet-settings will exit if last instance of xlet is removed. - Corresponding setting pages are created/removed when xlets are created/removed --- .../bin/JsonSettingsWidgets.py | 11 +- .../cinnamon-settings/xlet-settings.py | 230 +++++++++++++----- 2 files changed, 170 insertions(+), 71 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py index 806813f812..b73f4bf19c 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py @@ -134,7 +134,7 @@ def set_object_value(self, info, value): def check_settings(self, *args): old_settings = self.settings self.settings = self.get_settings() - + if self.settings is None: return for key in self.bindings: new_value = self.settings[key]["value"] if new_value != old_settings[key]["value"]: @@ -148,9 +148,12 @@ def check_settings(self, *args): callback(key, new_value) def get_settings(self): - file = open(self.filepath) - raw_data = file.read() - file.close() + try: + file = open(self.filepath) + raw_data = file.read() + file.close() + except FileNotFoundError: + return try: settings = json.loads(raw_data, object_pairs_hook=collections.OrderedDict) except: diff --git a/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py b/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py index 770bc337d0..87b704074b 100755 --- a/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py +++ b/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py @@ -95,13 +95,21 @@ def __init__(self, args): self.type = args.type self.uuid = args.uuid self.tab = 0 + self.instance_info = [] self.instance_id = str(args.id) if args.tab is not None: self.tab = int(args.tab) self.selected_instance = None self.gsettings = Gio.Settings.new("org.cinnamon") + self.monitors = {} + self.g_directories = [] self.custom_modules = {} + if self.type == "applet": changed_key = "enabled-applets" + elif self.type == "desklet": changed_key = "enabled-desklets" + else: changed_key = None + if changed_key: + self.gsettings.connect("changed::" + changed_key, lambda *args: self.on_enabled_xlets_changed(changed_key, *args)) self.load_xlet_data() self.build_window() @@ -128,7 +136,7 @@ def _on_proxy_ready (self, obj, result, data=None): proxy = None if proxy: - proxy.highlightXlet('(ssb)', self.uuid, self.selected_instance["id"], True) + self.highlight_xlet(self.selected_instance, True) def load_xlet_data (self): self.xlet_dir = "/usr/share/cinnamon/%ss/%s" % (self.type, self.uuid) @@ -242,18 +250,22 @@ def check_sizing(widget, data=None): self.next_button.connect("clicked", self.next_instance) def load_instances(self): - self.instance_info = [] path = Path(os.path.join(settings_dir, self.uuid)) old_path = Path("%s/.cinnamon/configs/%s" % (home, self.uuid)) - instances = 0 + for p in path, old_path: + if not p.exists(): continue + self.g_directories.append(Gio.File.new_for_path(str(p))) + new_items = os.listdir(path) if path.exists() else [] old_items = os.listdir(old_path) if old_path.exists() else [] dir_items = sorted(new_items + old_items) + try: multi_instance = int(self.xlet_meta["max-instances"]) != 1 except (KeyError, ValueError): multi_instance = False + enabled = [x.split(":") for x in self.gsettings.get_strv('enabled-%ss' % self.type)] for item in dir_items: # ignore anything that isn't json if item[-5:] != ".json": @@ -271,66 +283,82 @@ def load_instances(self): continue # multi-instance should have file names of the form [instance-id].json instance_exists = False - enabled = self.gsettings.get_strv('enabled-%ss' % self.type) for definition in enabled: - if self.uuid in definition and instance_id in definition.split(':'): + if self.uuid in definition and instance_id in definition: instance_exists = True break if not instance_exists: continue - settings = JSONSettingsHandler(os.path.join(path if item in new_items else old_path, item), self.notify_dbus) - settings.instance_id = instance_id - instance_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.instance_stack.add_named(instance_box, instance_id) - - info = {"settings": settings, "id": instance_id} - self.instance_info.append(info) + config_path = os.path.join(path if item in new_items else old_path, item) + self.create_settings_page(config_path) + + if not self.instance_info: + print(f"No instances were found for {self.uuid}. Exiting...") + sys.exit() + + self.next_button.set_no_show_all(True) + self.prev_button.set_no_show_all(True) + self.show_prev_next_buttons() if self.has_multiple_instances() else self.hide_prev_next_buttons() + + def create_settings_page(self, config_path): + instance_id = os.path.basename(config_path)[:-5] + if self.instance_stack.get_child_by_name(instance_id) is not None: return + settings = JSONSettingsHandler(config_path, self.notify_dbus) + settings.instance_id = instance_id + instance_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.instance_stack.add_named(instance_box, instance_id) + info = {"settings": settings, "id": instance_id} + self.instance_info.append(info) + settings_map = settings.get_settings() + first_key = next(iter(settings_map.values())) - settings_map = settings.get_settings() - first_key = next(iter(settings_map.values())) + try: + for setting in settings_map: + if setting == "__md5__": + continue + for key in settings_map[setting]: + if key in ("description", "tooltip", "units"): + try: + settings_map[setting][key] = translate(self.uuid, settings_map[setting][key]) + except (KeyError, ValueError): + traceback.print_exc() + elif key in "options": + new_opt_data = collections.OrderedDict() + opt_data = settings_map[setting][key] + for option in opt_data: + if opt_data[option] == "custom": + continue + new_opt_data[translate(self.uuid, option)] = opt_data[option] + settings_map[setting][key] = new_opt_data + elif key in "columns": + columns_data = settings_map[setting][key] + for column in columns_data: + column["title"] = translate(self.uuid, column["title"]) + finally: + # if a layout is not explicitly defined, generate the settings + # widgets based on the order they occur + if first_key["type"] == "layout": + self.build_with_layout(settings_map, info, instance_box, first_key) + else: + self.build_from_order(settings_map, info, instance_box, first_key) - try: - for setting in settings_map: - if setting == "__md5__": - continue - for key in settings_map[setting]: - if key in ("description", "tooltip", "units"): - try: - settings_map[setting][key] = translate(self.uuid, settings_map[setting][key]) - except (KeyError, ValueError): - traceback.print_exc() - elif key in "options": - new_opt_data = collections.OrderedDict() - opt_data = settings_map[setting][key] - for option in opt_data: - if opt_data[option] == "custom": - continue - new_opt_data[translate(self.uuid, option)] = opt_data[option] - settings_map[setting][key] = new_opt_data - elif key in "columns": - columns_data = settings_map[setting][key] - for column in columns_data: - column["title"] = translate(self.uuid, column["title"]) - finally: - # if a layout is not explicitly defined, generate the settings - # widgets based on the order they occur - if first_key["type"] == "layout": - self.build_with_layout(settings_map, info, instance_box, first_key) - else: - self.build_from_order(settings_map, info, instance_box, first_key) + if self.selected_instance is None: + self.selected_instance = info + if "stack" in info: + self.stack_switcher.set_stack(info["stack"]) - if self.selected_instance is None: - self.selected_instance = info - if "stack" in info: - self.stack_switcher.set_stack(info["stack"]) + def has_multiple_instances(self): + return len(self.instance_info) > 1 - instances += 1 + def hide_prev_next_buttons(self): + self.prev_button.hide() + self.next_button.hide() - if instances < 2: - self.prev_button.set_no_show_all(True) - self.next_button.set_no_show_all(True) + def show_prev_next_buttons(self): + self.prev_button.show() + self.next_button.show() def build_with_layout(self, settings_map, info, box, first_key): layout = first_key @@ -460,26 +488,95 @@ def set_instance(self, info): else: info["stack"].set_visible_child(children[0]) if proxy: - proxy.highlightXlet('(ssb)', self.uuid, self.selected_instance["id"], False) - proxy.highlightXlet('(ssb)', self.uuid, info["id"], True) + old_info = self.selected_instance + new_info = info + self.highlight_xlet(old_info, False) + self.highlight_xlet(new_info, True) self.selected_instance = info + def highlight_xlet(self, info, highlighted): + try: + proxy.highlightXlet('(ssb)', self.uuid, info["id"], highlighted) + except: + return + def previous_instance(self, *args): - self.instance_stack.set_transition_type(Gtk.StackTransitionType.OVER_RIGHT) - index = self.instance_info.index(self.selected_instance) - self.set_instance(self.instance_info[index-1]) + self.get_next_instance(False) def next_instance(self, *args): - self.instance_stack.set_transition_type(Gtk.StackTransitionType.OVER_LEFT) - index = self.instance_info.index(self.selected_instance) - if index == len(self.instance_info) - 1: - index = 0 - else: - index +=1 - self.set_instance(self.instance_info[index]) + self.get_next_instance() + + def get_next_instance(self, positive_direction = True): + transition = Gtk.StackTransitionType.OVER_LEFT if positive_direction else Gtk.StackTransitionType.OVER_RIGHT + self.instance_stack.set_transition_type(transition) + step = 1 if positive_direction else -1 + instances_length = len(self.instance_info) + start = self.instance_info.index(self.selected_instance) + nextIndex = (start + step) % instances_length + self.set_instance(self.instance_info[nextIndex]) + + def on_enabled_xlets_changed(self, key, *args): + """ + Args: + key ("enabled-applets"|"enabled-desklets") + """ + current_ids = {info["id"] for info in self.instance_info} + new_ids = set() + for definition in self.gsettings.get_strv(key): + definition = definition.split(":") + uuid, instance_id = (definition[-2], definition[-1]) if key == "enabled-applets"\ + else (definition[0], definition[1]) + if uuid != self.uuid: continue + new_ids.add(instance_id) + added_ids = new_ids - current_ids + + removed_indices = [] + selected_removed_index = -1 + for i, info in enumerate(self.instance_info): + if info["id"] in new_ids: continue + removed_indices.append(i) + if info == self.selected_instance: selected_removed_index = i + + if len(current_ids) + len(added_ids) == len(removed_indices): + self.quit() + return + + for id in added_ids: + for dir in self.g_directories: + file = dir.get_child(id + ".json") + if file.query_exists(None): + self.create_new_settings_page(file.get_path()) + continue + # Config files have not been added yet, need to monitor directories + monitor = dir.monitor_directory(Gio.FileMonitorFlags.NONE, None) + monitor.connect("changed", self.on_config_file_added) + self.monitors.setdefault(id, []).append(monitor) + + if (selected_removed_index != -1): + self.get_next_instance() + + for index in sorted(removed_indices, reverse=True): + self.monitors.get(self.instance_info[index]["id"], []).clear() + self.instance_stack.remove(self.instance_stack.get_child_by_name(self.instance_info[index]["id"])) + self.instance_info.pop(index) - # def unpack_args(self, args): - # args = {} + if not self.has_multiple_instances(): self.hide_prev_next_buttons() + + def on_config_file_added(self, *args): + file, event_type = args[1], args[-1] + instance = file.get_basename()[:-5] + if event_type != Gio.FileMonitorEvent.CHANGES_DONE_HINT : return + if instance not in self.monitors: return + for monitor in self.monitors[instance]: monitor.cancel() + del self.monitors[instance] + self.create_new_settings_page(file.get_path()) + + + def create_new_settings_page(self, path): + self.create_settings_page(path) + self.window.show_all() + if self.has_multiple_instances(): self.show_prev_next_buttons() + self.highlight_xlet(self.selected_instance, True) def backup(self, *args): dialog = Gtk.FileChooserDialog(_("Select or enter file to export to"), @@ -531,8 +628,7 @@ def reload_xlet(self, *args): def quit(self, *args): if proxy: - proxy.highlightXlet('(ssb)', self.uuid, self.selected_instance["id"], False) - + self.highlight_xlet(self.selected_instance, False) self.window.destroy() Gtk.main_quit()