diff --git a/data/org.cinnamon.gschema.xml b/data/org.cinnamon.gschema.xml index 0166d08fec..8f0d2bcf34 100644 --- a/data/org.cinnamon.gschema.xml +++ b/data/org.cinnamon.gschema.xml @@ -124,6 +124,14 @@ + + '[]' + JSON string of shared panel information + + Stores the panel ids that are being shared. + + + ['1:false'] Auto-hide panel diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py index 806813f812..8604d8ec6e 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"]: @@ -147,10 +147,20 @@ def check_settings(self, *args): for callback in callback_list: callback(key, new_value) + for key in self.settings: + if ("value" in self.settings[key] + and self.settings[key]["value"] != old_settings[key]["value"] + and self.notify_callback + ): + self.notify_callback(self, 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/bin/Spices.py b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py index a00c300ec8..8b0f2b429c 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py @@ -14,6 +14,7 @@ from PIL import Image import datetime import time + import json except Exception as error_message: print(error_message) sys.exit(1) @@ -856,19 +857,32 @@ def enable_extension(self, uuid, panel=1, box='right', position=0): if self.collection_type == 'applet': entries = [] applet_id = self.settings.get_int('next-applet-id') - self.settings.set_int('next-applet-id', (applet_id+1)) + shared_panels = json.loads(self.settings.get_string('shared-panels')) for entry in self.settings.get_strv(self.enabled_key): info = entry.split(':') pos = int(info[2]) - if info[0] == f'panel{panel}' and info[1] == box and position <= pos: - info[2] = str(pos+1) + panelId = int(info[0][-1]) + if ( + (info[0] == f'panel{panel}' or panelId in shared_panels) + and info[1] == box + and position <= pos + ): + new_pos = pos+1 + info[2] = str(new_pos) entries.append(':'.join(info)) else: entries.append(entry) - entries.append(f'panel{panel}:{box}:{position}:{uuid}:{applet_id}') + if panel in shared_panels: + for panel in shared_panels: + entries.append(f'panel{panel}:{box}:{position}:{uuid}:{applet_id}') + applet_id += 1 + else: + entries.append(f'panel{panel}:{box}:{position}:{uuid}:{applet_id}') + applet_id += 1 + self.settings.set_int('next-applet-id', (applet_id)) self.settings.set_strv(self.enabled_key, entries) elif self.collection_type == 'desklet': desklet_id = self.settings.get_int('next-desklet-id') diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py index e0952d0264..77fc216c6a 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py @@ -7,6 +7,7 @@ from Spices import Spice_Harvester from gi.repository import GLib, Gtk, Gdk import config +import json class Module: name = "applets" @@ -90,30 +91,29 @@ def __init__(self, parent, spices, window): self.top_box.pack_start(self.panel_select_buttons, False, False, 0) - def previous_panel(self, *args): + def getNextPanel(self, positive_direction = True): self.spices.send_proxy_signal('highlightPanel', '(ib)', self.panel_id, False) + shared_panels = json.loads(self.spices.settings.get_string("shared-panels")) + step = 1 if positive_direction else -1 - if self.current_panel_index - 1 >= 0: - self.current_panel_index -= 1 - else: - self.current_panel_index = len(self.panels) - 1 - self.panel_id = int(self.panels[self.current_panel_index].split(":")[0]) + for _ in self.panels: + self.current_panel_index = (self.current_panel_index + step) % len(self.panels) + checked_panel_id = int(self.panels[self.current_panel_index].split(":")[0]) + if checked_panel_id not in shared_panels or self.panel_id not in shared_panels: + self.panel_id = checked_panel_id + break self.spices.send_proxy_signal('highlightPanel', '(ib)', self.panel_id, True) - def next_panel(self, widget): - self.spices.send_proxy_signal('highlightPanel', '(ib)', self.panel_id, False) - - if self.current_panel_index + 1 < len(self.panels): - self.current_panel_index += 1 - else: - self.current_panel_index = 0 - self.panel_id = int(self.panels[self.current_panel_index].split(":")[0]) + def previous_panel(self, *args): + self.getNextPanel(False) - self.spices.send_proxy_signal('highlightPanel', '(ib)', self.panel_id, True) + def next_panel(self, widget): + self.getNextPanel() def panels_changed(self, *args): self.panels = [] + shared_panels = json.loads(self.spices.settings.get_string("shared-panels")) n_mons = Gdk.Screen.get_default().get_n_monitors() # we only want to select panels that are on a connected screen @@ -131,7 +131,7 @@ def panels_changed(self, *args): self.current_panel_index = 0 self.panel_id = int(self.panels[self.current_panel_index].split(":")[0]) - if len(self.panels) > 1: + if len(self.panels) > 1 and len(self.panels) != len(shared_panels): self.previous_button.show() self.next_button.show() # just in case, we'll make sure the current panel is highlighted diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py index 9c39453c3c..cef8fa31d0 100755 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py @@ -250,46 +250,37 @@ def on_add_panel(self, widget): if self.proxy: self.proxy.addPanelQuery() - def on_previous_panel(self, widget): + def getNextPanel(self, positive_direction = True): if self.panel_id and self.proxy: self.proxy.highlightPanel('(ib)', int(self.panel_id), False) - current = self.panels.index(self.current_panel) + index = current = self.panels.index(self.current_panel) + shared_panels = json.loads(self.settings.get_string("shared-panels")) - if current - 1 >= 0: - self.current_panel = self.panels[current - 1] - self.panel_id = self.current_panel.panel_id - else: - self.current_panel = self.panels[len(self.panels) - 1] - self.panel_id = self.current_panel.panel_id + step = 1 if positive_direction else -1 - self.config_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT) + for _ in self.panels: + index = (index + step) % len(self.panels) - if self.proxy: - self.proxy.highlightPanel('(ib)', int(self.panel_id), True) - - self.config_stack.set_visible_child(self.current_panel) - - def on_next_panel(self, widget): - if self.panel_id and self.proxy: - self.proxy.highlightPanel('(ib)', int(self.panel_id), False) - - current = self.panels.index(self.current_panel) - - if current + 1 < len(self.panels): - self.current_panel = self.panels[current + 1] - self.panel_id = self.current_panel.panel_id - else: - self.current_panel = self.panels[0] - self.panel_id = self.current_panel.panel_id + if int(self.panels[index].panel_id) not in shared_panels or int(self.panel_id) not in shared_panels: + self.current_panel = self.panels[index] + self.panel_id = self.current_panel.panel_id + break - self.config_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT) + self.config_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT if positive_direction + else Gtk.StackTransitionType.SLIDE_RIGHT) if self.proxy: self.proxy.highlightPanel('(ib)', int(self.panel_id), True) self.config_stack.set_visible_child(self.current_panel) + def on_previous_panel(self, widget): + self.getNextPanel(False) + + def on_next_panel(self, widget): + self.getNextPanel() + def id_or_monitor_position_used(self, kept_panels, monitor_layout, panel_id, monitor_id, position): for keeper in kept_panels: if keeper.panel_id == panel_id: @@ -384,8 +375,10 @@ def on_panel_list_changed(self, *args): self.next_button.show() self.previous_button.show() - # Disable the panel switch buttons if there's only one panel - if len(self.panels) == 1: + # Disable the panel switch buttons if there's only one panel or if there is only shared panels + if len(self.panels) == 1 or ( + len(self.panels) - len(json.loads(self.settings.get_string("shared-panels"))) == 0 + ): self.next_button.set_sensitive(False) self.previous_button.set_sensitive(False) else: @@ -438,11 +431,16 @@ def connect_to_settings(self, schema, key): self.connect_widget_handlers() def set_value(self, value): + shared_panels = json.loads(self.settings['shared-panels']) vals = self.settings[self.key] newvals = [] for val in vals: - if val.split(":")[0] == self.panel_id: - newvals.append(self.panel_id + ":" + self.stringify(value)) + val_panel_id = val.split(":")[0] + if val_panel_id == self.panel_id or ( + int(self.panel_id) in shared_panels + and int(val_panel_id) in shared_panels + ): + newvals.append(val_panel_id + ":" + self.stringify(value)) else: newvals.append(val) self.settings[self.key] = newvals @@ -511,36 +509,24 @@ def on_setting_changed(self, *args): if value is not None and value != int(self.content_widget.get_value()): self.content_widget.set_value(value) -class PanelJSONSpinButton(SpinButton, PanelWidgetBackend): - def __init__(self, label, schema, key, panel_id, zone, *args, **kwargs): - self.panel_id = panel_id - self.zone = zone - super(PanelJSONSpinButton, self).__init__(label, *args, **kwargs) - - self.connect_to_settings(schema, key) - - def get_range(self): - return - - # We use integer directly here because that is all the panel currently uses. - # If that changes in the future, we will need to fix this. - def stringify(self, value): - return str(int(value)) - - def unstringify(self, value): - return int(value) - - def on_setting_changed(self, *args): - self.content_widget.set_value(self.get_value()) +class PanelJSONHelper: + def __init__(self, isSpinBtn, *args, **kwargs): + self.isSpinBtn = isSpinBtn + super().__init__(*args, **kwargs) def set_value(self, value): + shared_panels = json.loads(self.settings['shared-panels']) vals = json.loads(self.settings[self.key]) + panel_id = int(self.panel_id) for obj in vals: - if obj['panelId'] != int(self.panel_id): + if obj['panelId'] != panel_id and ( + panel_id not in shared_panels + or obj['panelId'] not in shared_panels + ): continue for key, val in obj.items(): if key == self.zone: - obj[key] = int(value) + obj[key] = int(value) if self.isSpinBtn else self.valtype(value) break self.settings[self.key] = json.dumps(vals) @@ -553,8 +539,30 @@ def get_value(self): continue for key, val in obj.items(): if key == self.zone: - return int(val) - return 0 # prevent warnings if key is reset + return int(val) if self.isSpinBtn else self.valtype(val) + if self.isSpinBtn: return 0 # prevent warnings if key is reset + +class PanelJSONSpinButton(PanelJSONHelper, SpinButton, PanelWidgetBackend): + def __init__(self, label, schema, key, panel_id, zone, *args, **kwargs): + self.panel_id = panel_id + self.zone = zone + super(PanelJSONSpinButton, self).__init__(True, label, *args, **kwargs) + + self.connect_to_settings(schema, key) + + def get_range(self): + return + + # We use integer directly here because that is all the panel currently uses. + # If that changes in the future, we will need to fix this. + def stringify(self, value): + return str(int(value)) + + def unstringify(self, value): + return int(value) + + def on_setting_changed(self, *args): + self.content_widget.set_value(self.get_value()) class PanelComboBox(ComboBox, PanelWidgetBackend): def __init__(self, label, schema, key, panel_id, *args, **kwargs): @@ -569,11 +577,11 @@ def stringify(self, value): def unstringify(self, value): return value -class PanelJSONComboBox(ComboBox, PanelWidgetBackend): +class PanelJSONComboBox(PanelJSONHelper, ComboBox, PanelWidgetBackend): def __init__(self, label, schema, key, panel_id, zone, *args, **kwargs): self.panel_id = panel_id self.zone = zone - super(PanelJSONComboBox, self).__init__(label, *args, **kwargs) + super(PanelJSONComboBox, self).__init__(False, label, *args, **kwargs) self.connect_to_settings(schema, key) @@ -583,28 +591,6 @@ def stringify(self, value): def unstringify(self, value): return value - def set_value(self, value): - vals = json.loads(self.settings[self.key]) - for obj in vals: - if obj['panelId'] != int(self.panel_id): - continue - for key, val in obj.items(): - if key == self.zone: - obj[key] = self.valtype(value) - break - - self.settings[self.key] = json.dumps(vals) - - def get_value(self): - vals = self.settings[self.key] - vals = json.loads(vals) - for obj in vals: - if obj['panelId'] != int(self.panel_id): - continue - for key, val in obj.items(): - if key == self.zone: - return self.valtype(val) - class PanelRange(Range, PanelWidgetBackend): def __init__(self, label, schema, key, panel_id, *args, **kwargs): self.panel_id = panel_id diff --git a/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py b/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py index 770bc337d0..af56a09f94 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,19 +250,25 @@ 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) + shared_panels = json.loads(self.gsettings.get_string("shared-panels")) + 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: + applet_info = {} # ignore anything that isn't json if item[-5:] != ".json": continue @@ -271,66 +285,129 @@ 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 + if self.type == "applet": + applet_info.update( + panel = int(definition[0].split("panel")[1]), + location = definition[1], + order = definition[2] + ) break if not instance_exists: continue + elif self.type == "applet": + first = True + for definition in enabled: + panel_id = int(definition[0].split("panel")[1]) + if self.uuid not in definition or panel_id not in shared_panels: continue + if first: + applet_info.update( + panel = panel_id, + location = definition[1], + order = definition[2] + ) + first = False + applet_info.setdefault("extra_infos", []).append({"panel": panel_id, "id": definition[-1]}) + + config_path = os.path.join(path if item in new_items else old_path, item) + self.create_settings_page(config_path, applet_info) + + 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() and not self.has_only_shared_instances()\ + else self.hide_prev_next_buttons() + + def create_settings_page(self, config_path, applet_info = {}): + 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} + infos = [info] + + if applet_info: + info.update( + panel = applet_info["panel"], + location = applet_info["location"], + order = applet_info["order"] + ) + for i, extra_info in enumerate(applet_info.get("extra_infos", [])): + if i == 0: + info.update(extra_info) + continue + info_copy = info.copy() + info_copy.update(extra_info) + infos.append(info_copy) - 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) - - 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) - - if self.selected_instance is None: - self.selected_instance = info - if "stack" in info: - self.stack_switcher.set_stack(info["stack"]) - - instances += 1 + self.instance_info.extend(infos) + settings_map = settings.get_settings() + first_key = next(iter(settings_map.values())) - if instances < 2: - self.prev_button.set_no_show_all(True) - self.next_button.set_no_show_all(True) + 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"]) + + def has_multiple_instances(self): + return len(self.instance_info) > 1 + + def has_only_shared_instances(self): + if self.type != "applet": return False + shared_panels = json.loads(self.gsettings.get_string("shared-panels")) + first = self.instance_info[0] + return all( + info["panel"] in shared_panels + and info["location"] == first["location"] + and info["order"] == first["order"] + for info in self.instance_info + ) + + def hide_prev_next_buttons(self): + self.prev_button.hide() + self.next_button.hide() + + 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 +537,131 @@ 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 + start = self.instance_info.index(self.selected_instance) + next_index = self.get_next_not_shared_index(start, step) + self.set_instance(self.instance_info[next_index]) + + def get_next_not_shared_index(self, start, step): + next_index = (start + step) % len(self.instance_info) + if self.type != "applet": return next_index + shared_panels = json.loads(self.gsettings.get_string("shared-panels")) + if self.selected_instance["panel"] in shared_panels and self.instance_info[next_index]["panel"] in shared_panels: + current = self.selected_instance + while next_index != start: + info = self.instance_info[next_index] + if (info["panel"] not in shared_panels + or info["location"] != current["location"] + or info["order"] != current["order"] + ): + break + next_index = (next_index + step) % len(self.instance_info) + continue - # def unpack_args(self, args): - # args = {} + return next_index + + 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() + new_instances = {} + added_instances = {} + 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) + if self.type == "applet": + new_instances[instance_id] = { + "panel": int(definition[0].split("panel")[1]), + "location": definition[1], + "order": definition[2] + } + if instance_id in current_ids: continue + added_instances[instance_id] = new_instances[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 + + if self.type == "applet": + for info in self.instance_info: + updated = new_instances.get(info["id"], {}) + info.update(updated) + + for id in added_ids: + for dir in self.g_directories: + file = dir.get_child(id + ".json") + if file.query_exists(None): + added_instance = added_instances.get(id) + if added_instance: self.create_new_settings_page(file.get_path(), **added_instance) + else: 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", lambda *args: self.on_config_file_added(added_instances, *args)) + 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) + + if not self.has_multiple_instances() or self.has_only_shared_instances(): self.hide_prev_next_buttons() + + def on_config_file_added(self, added_instances, *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] + applet_info = added_instances.get(instance, {}) + self.create_new_settings_page(file.get_path(), applet_info) + + + def create_new_settings_page(self, path, applet_info = {}): + self.create_settings_page(path, applet_info) + self.window.show_all() + if self.has_multiple_instances() and not self.has_only_shared_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 +713,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() diff --git a/js/ui/appletManager.js b/js/ui/appletManager.js index 903b572c4b..1c8e536960 100644 --- a/js/ui/appletManager.js +++ b/js/ui/appletManager.js @@ -23,6 +23,8 @@ var appletMeta; var appletObj = []; var appletsLoaded = false; +const appletConfigMonitors = {}; + // FIXME: This role stuff is checked in extension.js, why not move checks from here to there? var Roles = { NOTIFICATIONS: 'notifications', @@ -32,6 +34,7 @@ var Roles = { }; var rawDefinitions; +/** @type {AppletDefinitionObject[]} */ var definitions = []; var clipboard = []; var promises = []; @@ -72,6 +75,11 @@ function init() { }); } +/** + * Get the first corresponding applet definition that matches all keys and values provided. + * @param {Partial} definition Object containing the keys and values to search by. + * @returns {AppletDefinitionObject?} + */ function getAppletDefinition(definition) { return queryCollection(definitions, definition); } @@ -127,6 +135,11 @@ function prepareExtensionReload(extension) { } } +/** + * Get all applet definitions as Applet Definition Objects. + * Existing objects are **not** copied. + * @returns {AppletDefinitionObject[]} + */ function getDefinitions() { let _definitions = []; rawDefinitions = global.settings.get_strv('enabled-applets'); @@ -144,12 +157,50 @@ function getDefinitions() { return _definitions; } +/** + * Helper function to set enabled-applets gsetting. + * @param {AppletDefinitionObject[]} definitions Definitions to set. + * @returns {string[]} Stringified definitions for gsetting. + */ +function setDefinitions(definitions) { + const definitionStrings = definitions.map(definition => stringifyAppletDefinition(definition)); + global.settings.set_strv("enabled-applets", definitionStrings); +} + +/** + * @typedef {Object} AppletDefinitionObject + * @property {number} panelId - ID of the panel + * @property {number} orientation - The side of the screen the panel is on. + * Orientation is 0 at the top and increases by 1 for each side moving clockwise. + * @property {AppletLocation} location_label - Label for the location. + * @property {boolean} center - Whether it's centered + * @property {number} order - Position/order of the applet + * @property {string} uuid - UUID of the applet instance + * @property {string} real_uuid - UUID without !. The exclamation mark is sometimes present to override version check. + * @property {string} applet_id - ID of the applet + * @property {object} applet - The applet object itself + */ + +/** + * @typedef {string} AppletDefinitionString + * - String in the format `'::::'`. + * - `` is like 'panel1'. + * - `` 'left', 'center', or 'right'. + * - `` integer representing order of applet in location. 1,2,3, ... + * - `` Unique ID of the applet. Determines the type of applet. e.g. menu\@cinnamon.org + * - `` Unique ID assigned to the applet instance when created. + */ + +/** + * @typedef {"left"|"center"|"right"} AppletLocation + */ + +/** + * Creates corresponding object from provided definition string. + * @param {AppletDefinitionString} definition - Applet String Definition + * @returns {AppletDefinitionObject} + */ function createAppletDefinition(definition) { - // format used in gsettings is 'panel:location:order:uuid:applet_id' where: - // - panel is something like 'panel1', - // - location is either 'left', 'center' or 'right', - // - order is an integer representing the order of the applet within the panel/location (i.e. 1st, 2nd etc..). - // - applet_id is a unique id assigned to the applet instance when added. let elements = definition.split(":"); if (elements.length > 4) { let panelId = parseInt(elements[0].split('panel')[1]); @@ -183,7 +234,6 @@ function createAppletDefinition(definition) { // Its important we check if the definition object already exists before creating a new object, otherwise we are // creating duplicate references that could cause memory leaks. let existingDefinition = getAppletDefinition(appletDefinition); - if (existingDefinition) { return existingDefinition; } @@ -198,6 +248,17 @@ function createAppletDefinition(definition) { return null; } +/** + * Converts AppletDefinitionObject into AppletDefinitionString to + * be used in global settings enabled-applets array. + * @param {AppletDefinitionObject} definition - Applet definition object + * @returns {AppletDefinitionString} `'::::'` + */ +function stringifyAppletDefinition(definition) { + const { panelId, location_label: location, order, uuid, applet_id } = definition + return `panel${panelId}:${location}:${order}:${uuid}:${applet_id}`; +} + function setOrientationForPanel(panelPos) { let orientation; switch (panelPos) @@ -240,6 +301,12 @@ function checkForUpgrade(newEnabledApplets) { return newEnabledApplets; } +/** + * Compares panel, orientation, location, and order to determine equality. + * @param {AppletDefinitionObject} a + * @param {AppletDefinitionObject} b + * @returns {boolean} Whether applet definitions are mostly equal. + */ function appletDefinitionsEqual(a, b) { return (a != null && b != null && a.panelId === b.panelId @@ -254,11 +321,12 @@ function onEnabledAppletsChanged() { let addedApplets = []; let removedApplets = []; let unChangedApplets = []; + const sharedPanels = Panel.getSharedPanels(); for (let i = 0; i < definitions.length; i++) { let {uuid, real_uuid, applet_id} = definitions[i]; + /** @type {AppletDefinitionObject?} */ let oldDefinition = queryCollection(oldDefinitions, {real_uuid, applet_id}); - let isEqualToOldDefinition = appletDefinitionsEqual(definitions[i], oldDefinition); if (oldDefinition && !isEqualToOldDefinition) { @@ -305,9 +373,8 @@ function onEnabledAppletsChanged() { if (!extension) { continue; } - - // is_new will get this applet flashied. - addAppletToPanels(extension, definition, null, appletsLoaded && is_new); + const flashApplet = appletsLoaded && is_new && !sharedPanels.includes(definition.panelId); + addAppletToPanels(extension, definition, null, flashApplet); } // Make sure all applet extensions are loaded. @@ -315,8 +382,14 @@ function onEnabledAppletsChanged() { initEnabledApplets(); } +/** + * Removes applet from panel. + * @param {AppletDefinitionObject} appletDefinition + * @param {boolean} deleteConfig Deletes config file when true. + * @param {boolean} changed + */ function removeAppletFromPanels(appletDefinition, deleteConfig, changed = false) { - let {applet, uuid, applet_id} = appletDefinition; + let {applet, uuid, applet_id } = appletDefinition; if (applet) { try { if (changed) { @@ -335,6 +408,7 @@ function removeAppletFromPanels(appletDefinition, deleteConfig, changed = false) appletDefinition.applet = null; if (deleteConfig) { + removeSharedAppletCleanup(appletDefinition); _removeAppletConfigFile(uuid, applet_id); } @@ -347,10 +421,7 @@ function removeAppletFromPanels(appletDefinition, deleteConfig, changed = false) } function _removeAppletConfigFile(uuid, instanceId) { - let config_paths = [ - [GLib.get_home_dir(), ".cinnamon", "configs", uuid, instanceId + ".json"].join("/"), - [GLib.get_user_config_dir(), "cinnamon", "spices", uuid, instanceId + ".json"].join("/") - ]; + let config_paths = _getConfigPaths(uuid, instanceId); for (let i = 0; i < config_paths.length; i++) { const config_path = config_paths[i]; @@ -365,9 +436,193 @@ function _removeAppletConfigFile(uuid, instanceId) { } } +/** + * Returns the possible paths to an applet's config file or uuid directory. + * @param {string} uuid Applet UUID. + * @param {string|number|undefined} instanceId Applet Instance ID. If undefined, will search for the uuid directory + * path instead. + * @returns {string[]} Possible applet config file paths or uuid directory path. + */ +function _getConfigPaths(uuid, instanceId) { + let paths = [ + `${GLib.get_home_dir()}/.cinnamon/configs/${uuid}/`, + `${GLib.get_user_config_dir()}/cinnamon/spices/${uuid}/` + ]; + if (instanceId) paths = paths.map(e => e + `${instanceId}.json`); + return paths; +} + +/** + * Replaces the configuration file for the toInstance applet with the configuration from the fromInstance applet. + * @param {string} uuid UUID of applet. e.g. "menu\@cinnamon.org". + * @param {number|string} fromInstance Instance ID of the applet to copy configuration from. + * @param {number|string} toInstance Instance ID of the applet to copy configuration to. + */ +function _shareAppletConfiguration(uuid, fromInstance, toInstance) { + const fromConfigPaths = _getConfigPaths(uuid, fromInstance); + const toConfigPaths = _getConfigPaths(uuid, toInstance); + for (let i = 0; i < fromConfigPaths.length; i++) { + const fromFile = Gio.File.new_for_path(fromConfigPaths[i]); + const toFile = Gio.File.new_for_path(toConfigPaths[i]); + try{ + fromFile.copy(toFile, Gio.FileCopyFlags.OVERWRITE, null, null); + } + catch { + continue; + } + } + +} + +function _addAppletConfigListeners(uuid, instanceId) { + if (uuid in appletConfigMonitors) return; + const maxInstances = Extension.get_max_instances(uuid, Extension.Type.APPLET) + if (maxInstances === 1) return; + const directoryPaths = _getConfigPaths(uuid, instanceId).map(e => e.slice(0, e.lastIndexOf("/"))); + for (const directory of directoryPaths) { + const gFileDirectory = Gio.File.new_for_path(directory); + const monitor = gFileDirectory.monitor_directory(Gio.FileMonitorFlags.NONE, null); + monitor.connect("changed", _onAppletConfigChanged); + (appletConfigMonitors[uuid] ??= []).push(monitor); + } +} + +function _removeAppletConfigListeners(uuid) { + if (!appletConfigMonitors[uuid]) return; + appletConfigMonitors[uuid].forEach(e => e.cancel()); + delete appletConfigMonitors[uuid] +} + +async function _readJsonFile(file) { + const settings = await new Promise(resolve=> file.load_contents_async(null, + (source, res) => { + try{ + const [ok, contents] = source.load_contents_finish(res); + if (!ok) { + resolve(null); + return; + } + const decoder = new TextDecoder(); + resolve(JSON.parse(decoder.decode(contents))); + } + catch(e) { + global.logError(e); + resolve(null) + } + } + )); + return settings; +} + +async function _copyFile(sourceFile, destFile) { + return await new Promise(resolve => sourceFile.copy_async( + destFile, + Gio.FileCopyFlags.OVERWRITE, + GLib.PRIORITY_DEFAULT, + null, + null, + (source, res) => { + try{ + source.copy_finish(res); + resolve(); + } + catch(e) { + global.logError(e) + resolve(); + } + } + )); +} + +function _unlistenedSettingChanged(oldSettings, newSettings) { + const layout = oldSettings.layout; + if (!layout) return false; + const listenedKeys = []; + const unlistenedKeys = []; + + for (const key in layout) { + if (layout[key].type !== "section") continue; + listenedKeys.push(...layout[key].keys); + } + + for (const key in oldSettings) { + if (listenedKeys.includes(key)) continue; + if (typeof oldSettings[key] !== "object") continue; + if (!("value" in oldSettings[key])) continue; + unlistenedKeys.push(key); + } + + const changed = unlistenedKeys.some(key => { + const oldValue = JSON.stringify(oldSettings[key]["value"]); + const newValue = JSON.stringify(newSettings[key]["value"]); + return oldValue !== newValue; + }); + return changed; +} + +async function _onAppletConfigChanged(monitor, file, otherFile, eventType) { + const updatedInstance = file.get_path().match("(?<=^.+/)\\d+(?=\\.json$)")?.[0]; + if (!updatedInstance) return; + if (eventType !== Gio.FileMonitorEvent.CHANGES_DONE_HINT) return; + const definitions = getDefinitions(); + const sharedPanels = Panel.getSharedPanels(); + const sharingApplet = definitions.find(e => sharedPanels.includes(e.panelId) && e.applet_id === updatedInstance); + if (!sharingApplet) return; + _removeAppletConfigListeners(sharingApplet.real_uuid); + const newSettings = await _readJsonFile(file); + if (!newSettings) return; + const sharedDefinitions = definitions.filter(e => { + return sharedPanels.includes(e.panelId) + && e.location_label === sharingApplet.location_label + && e.order === sharingApplet.order + }); + + for (const definition of sharedDefinitions) { + try{ + const { applet_id: instance } = definition; + if (instance === updatedInstance) continue; + const toConfigFile = file.get_parent().get_child(`${instance}.json`); + const oldSettings = await _readJsonFile(toConfigFile); + if (!oldSettings) continue; + const unlistenedChange = _unlistenedSettingChanged(oldSettings, newSettings); + await _copyFile(file, toConfigFile); + if (unlistenedChange) { + removeAppletFromPanels(definition, false, true); + addAppletToPanels(Extension.getExtension(definition.real_uuid), definition); + } + } + catch(e) { + global.logError(e); + } + } + _addAppletConfigListeners(sharingApplet.real_uuid); +} + +/** + * Shared applet removal cleanup + * @param {AppletDefinitionObject} appletDefinition Definition of applet to remove. + */ +function removeSharedAppletCleanup(appletDefinition) { + const sharedPanels = Panel.getSharedPanels(); + const { panelId, location_label, order, real_uuid: uuid } = appletDefinition; + if (!sharedPanels.includes(panelId)) return; + const sharedApplets = getDefinitions().filter(e => { + return sharedPanels.includes(e.panelId) + && e.real_uuid === uuid + }); + if (!sharedApplets.length) _removeAppletConfigListeners(uuid); +} + +/** + * + * @param {any} extension + * @param {AppletDefinitionObject} appletDefinition + * @param {any} panel + * @param {boolean} user_action When true, flashes applet. + * @returns {boolean} Applet exists or was created. + */ function addAppletToPanels(extension, appletDefinition, panel = null, user_action=false) { if (!appletDefinition.panelId) return true; - try { // Create the applet let applet = createApplet(extension, appletDefinition, panel); @@ -377,6 +632,7 @@ function addAppletToPanels(extension, appletDefinition, panel = null, user_actio return true; } + // Now actually lock the applets role and set the provider extension.lockRole(applet); @@ -415,6 +671,10 @@ function addAppletToPanels(extension, appletDefinition, panel = null, user_actio removeAppletFromInappropriatePanel (extension, appletDefinition); + const sharedPanels = Panel.getSharedPanels(); + if (sharedPanels.includes(appletDefinition.panelId)) { + _addAppletConfigListeners(appletDefinition.real_uuid, appletDefinition.applet_id); + } return true; } catch (e) { extension.unlockRoles(); @@ -639,42 +899,123 @@ function _removeAppletFromPanel(uuid, applet_id) { Mainloop.idle_add(() => { let real_uuid = uuid; let definition = queryCollection(definitions, {real_uuid, applet_id}); - if (definition) + if (definition) { + const sharedPanels = Panel.getSharedPanels(); removeApplet(definition); + if (sharedPanels.includes(definition.panelId)) { + const enabled = getDefinitions(); + for (const otherDefinition of enabled) { + const { panelId, location_label, order } = otherDefinition; + if (!sharedPanels.includes(panelId)) continue; + if (definition.location_label !== location_label) continue; + if (definition.order !== order) continue; + removeApplet(otherDefinition); + } + } + } return false; }); } function saveAppletsPositions() { - let enabled = global.settings.get_strv('enabled-applets'); - let newEnabled = []; + const sharedPanels = Panel.getSharedPanels(); + // Ensure we do not modify the existing objects. + const enabled = getDefinitions().map(e => { return { ...e } }); + const changedSharedDefinitions = []; + let sourceAppletInfo; for (let i = 0; i < enabled.length; i++) { - let info = enabled[i].split(':'); - let {applet} = getAppletDefinition({ - uuid: info[3], - applet_id: info[4], - location_label: info[1] - }); + const definition = enabled[i]; + const { applet, panelId: oldPanel, location_label: oldLocation, order: oldOrder } = definition; if (!applet) { continue; } if (applet._newOrder !== null) { + let newPanel; if (applet._newPanelId !== null) { - info[0] = 'panel' + applet._newPanelId; - info[1] = applet._zoneString; + definition.panelId = (newPanel = applet._newPanelId); + definition.location_label = applet._zoneString; applet._newPanelId = null; } - info[2] = applet._newOrder; + definition.order = applet._newOrder; applet._newOrder = null; - newEnabled.push(info.join(':')); + + const oldPanelIsShared = sharedPanels.includes(oldPanel); + const newPanelIsShared = sharedPanels.includes(newPanel); + if (oldPanelIsShared || newPanelIsShared) { + const { panelId: newPanel, location_label: newLocation, order: newOrder } = definition; + if (oldPanelIsShared && newPanelIsShared) { + sourceAppletInfo = { + definition, + oldPanel + }; + } + if (oldPanel === newPanel + && newLocation === oldLocation + && newOrder === oldOrder + ) continue; + + const currentChangedDefinitions = { + definitions: [], + indices: [], + newLocation, + newOrder, + added: false, + sharingInstance: undefined, + removed: false + }; + + if (oldPanelIsShared) { + const length = enabled.length; + for (let i = 0; i < length; i++) { + const sharedDefinition = enabled[i]; + if (sharedDefinition.panelId === oldPanel + || !sharedPanels.includes(sharedDefinition.panelId) + || sharedDefinition.location_label !== oldLocation + || sharedDefinition.order !== oldOrder) continue; + currentChangedDefinitions.definitions.push(sharedDefinition); + currentChangedDefinitions.indices.push(i); + } + if (newPanel && !sharedPanels.includes(newPanel)) currentChangedDefinitions.removed = true; + } + else if (newPanelIsShared){ + let nextAppletId = global.settings.get_int("next-applet-id"); + currentChangedDefinitions.added = true; + currentChangedDefinitions.sharingInstance = definition.applet_id; + for (const panel of sharedPanels) { + if (newPanel === panel) continue; + currentChangedDefinitions.definitions.push({ + ...definition, + panelId: panel, + applet_id: nextAppletId++ + }); + } + global.settings.set_int("next-applet-id", nextAppletId); + } + changedSharedDefinitions.push(currentChangedDefinitions); + } + } + } + + if (sourceAppletInfo) sourceAppletInfo.definition.panelId = sourceAppletInfo.oldPanel; + + for (const sharedInfo of changedSharedDefinitions) { + const { definitions, newLocation, newOrder, indices, added, sharingInstance, removed } = sharedInfo; + if (added && sharingInstance) { + enabled.push(...definitions); + for (const { real_uuid: uuid, applet_id: toInstance } of definitions) { + _shareAppletConfiguration(uuid, sharingInstance, toInstance); + } } + else if (removed) for (const index of indices) enabled.splice(index, 1); else { - newEnabled.push(enabled[i]); + for (const definition of definitions) { + definition.location_label = newLocation; + definition.order = newOrder; + } } } - - global.settings.set_strv('enabled-applets', newEnabled); + setDefinitions(enabled); } // Deprecated, kept for compatibility reasons @@ -811,6 +1152,30 @@ function pasteAppletConfiguration(panelId) { } } + +/** + * Set up shared applets on another panel. + * Configurations of applets on from and to panels will be kept in sync based on location and order. + * @param {number} fromPanelId Panel id to be copied from + * @param {number} toPanelId Panel id to copy to + */ +function setupSharedApplets(fromPanelId, toPanelId) { + const definitions = getDefinitions(); + const fromAppletDefinitions = definitions.filter(e => e.panelId === fromPanelId); + let nextAppletId = global.settings.get_int("next-applet-id"); + fromAppletDefinitions.forEach(definition => { + /** @type {AppletDefinitionObject} */ + const toAppletDefinition = {...definition, applet_id: String(nextAppletId), panelId: toPanelId }; + const maxInstances = Extension.get_max_instances(definition.real_uuid, Extension.Type.APPLET); + if (maxInstances !== 1) _shareAppletConfiguration(definition.real_uuid, definition.applet_id, nextAppletId); + definitions.push(toAppletDefinition); + nextAppletId++; + }); + + global.settings.set_int("next-applet-id", nextAppletId); + setDefinitions(definitions); +} + function getRunningInstancesForUuid(uuid) { let definitions = filterDefinitionsByUUID(uuid); let result = []; diff --git a/js/ui/cinnamonDBus.js b/js/ui/cinnamonDBus.js index 9867368285..2b2853851d 100644 --- a/js/ui/cinnamonDBus.js +++ b/js/ui/cinnamonDBus.js @@ -7,6 +7,8 @@ const Config = imports.misc.config; const Extension = imports.ui.extension; const Flashspot = imports.ui.flashspot; const Main = imports.ui.main; +const Panel = imports.ui.panel; +const Applet = imports.ui.applet; const AppletManager = imports.ui.appletManager; const DeskletManager = imports.ui.deskletManager; const ExtensionSystem = imports.ui.extensionSystem; @@ -345,11 +347,36 @@ CinnamonDBus.prototype = { highlightXlet: function(uuid, instance_id, highlight) { let obj = this._getXletObject(uuid, instance_id); if (obj && obj.highlight) obj.highlight(highlight); + if (obj instanceof Applet.Applet) { + const highlightedApplet = AppletManager.getAppletDefinition({ uuid, applet_id: instance_id }); + if (!highlightedApplet) { + global.logError("Can't find highlighted applet") + return; + } + const sharedPanels = Panel.getSharedPanels(); + if (!sharedPanels.includes(highlightedApplet.panelId)) return; + const otherSharedApplets = AppletManager.getDefinitions().filter(e => { + const { panelId, location_label, order, applet } = e; + return applet !== obj + && sharedPanels.includes(panelId) + && location_label === highlightedApplet.location_label + && order === highlightedApplet.order + }); + otherSharedApplets.forEach(({applet}) => applet.highlight(highlight)); + } }, highlightPanel: function(id, highlight) { - if (Main.panelManager.panels[id]) + const sharedPanels = Panel.getSharedPanels(); + if (Main.panelManager.panels[id]) { Main.panelManager.panels[id].highlight(highlight); + if (sharedPanels.includes(id)) { + for (let panel of sharedPanels) { + if (panel === id || !Main.panelManager.panels[id]) continue; + Main.panelManager.panels[panel].highlight(highlight); + } + } + } }, addPanelQuery: function() { diff --git a/js/ui/panel.js b/js/ui/panel.js index d6b3b8b80d..0c963d46b0 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -371,6 +371,56 @@ function updatePanelsMeta(meta, panel_props) { return changed; } +/** + * @typedef {number[]} SharedPanels + */ + +/** + * Returns array of shared panel ids. + * @returns {SharedPanels} Parsed shared-panels value from gsettings. + */ +function getSharedPanels() { + const jsonString = global.settings.get_string("shared-panels"); + return JSON.parse(jsonString); +} + +/** + * Set the shared-panels gsetting. + * @param {SharedPanels} sharedPanels + */ +function setSharedPanels(sharedPanels) { + const jsonString = JSON.stringify(sharedPanels); + global.settings.set_string("shared-panels", jsonString); +} + +/** + * Adds a new shared panel to shared-panels gsetting. + * @param {number} sharedPanelId Sharing panel id. + * @param {number} newPanelId New panel id. + */ +function addSharedPanels(sharedPanelId, newPanelId) { + const sharedPanels = getSharedPanels(); + if (!sharedPanels.includes(sharedPanelId)) sharedPanels.push(sharedPanelId); + sharedPanels.push(newPanelId); + setSharedPanels(sharedPanels); +} + +/** + * Removes panel id from shared-panels gsetting if panel was a shared panel. + * @param {number} panelId Panel id to be removed. + */ +function removeSharedPanel(panelId) { + const sharedPanels = getSharedPanels(); + const INDEX = sharedPanels.findIndex(id => id === panelId); + if (INDEX === -1) return; + sharedPanels.splice(INDEX, 1); + if (sharedPanels.length < 2) { + global.settings.reset("shared-panels"); + return; + } + setSharedPanels(sharedPanels); +} + /** * #PanelManager * @@ -646,7 +696,10 @@ PanelManager.prototype = { break; } } - + if (getSharedPanels().includes(panelId)) { + removeSharedPanel(panelId); + AppletManager.clearAppletConfiguration(panelId); + } setPanelsEnabledList(list); }, @@ -657,52 +710,112 @@ PanelManager.prototype = { * * Adds a new panel to the specified position */ - addPanel: function(monitorIndex, panelPosition) { + addPanel: function(monitorIndex, panelPosition, sharedPanelId) { let list = getPanelsEnabledList(); - let i = 0; // Start counting at 1 for compatibility + let panelId = 0; // Start counting at 1 for compatibility // Magic: Keep recursing until there is a free panel id while (true) - if (!this.panelsMeta[++i]) + if (!this.panelsMeta[++panelId]) break; - // Add default values - outerLoop: - for (let key in DEFAULT_PANEL_VALUES) { - let settings = global.settings.get_strv(key); - for (let j = 0; j < settings.length; j++){ - if (settings[j].split(":")[0] == i){ - continue outerLoop; + // Copy values from shared panel or add default values + if (sharedPanelId) { + const STRING_ARRAY_KEYS = [PANEL_AUTOHIDE_KEY, PANEL_SHOW_DELAY_KEY, PANEL_HIDE_DELAY_KEY, PANEL_HEIGHT_KEY]; + const OBJECT_ARRAY_KEYS = [PANEL_ZONE_ICON_SIZES, PANEL_ZONE_SYMBOLIC_ICON_SIZES, PANEL_ZONE_TEXT_SIZES]; + + for (let key of STRING_ARRAY_KEYS) { + this._copyStringArraySettings(key, panelId, sharedPanelId); + } + + for (let key of OBJECT_ARRAY_KEYS) { + this._copyObjectArraySettings(key, panelId, sharedPanelId); + } + } + else { + outerLoop: + for (let key in DEFAULT_PANEL_VALUES) { + let settings = global.settings.get_strv(key); + for (let j = 0; j < settings.length; j++){ + if (settings[j].split(":")[0] == panelId){ + continue outerLoop; + } } + settings.push(panelId + ":" + DEFAULT_PANEL_VALUES[key]); + global.settings.set_strv(key, settings); } - settings.push(i + ":" + DEFAULT_PANEL_VALUES[key]); - global.settings.set_strv(key, settings); } switch (panelPosition) { case PanelLoc.top: - list.push(i + ":" + monitorIndex + ":" + "top"); + list.push(panelId + ":" + monitorIndex + ":" + "top"); break; case PanelLoc.bottom: - list.push(i + ":" + monitorIndex + ":" + "bottom"); + list.push(panelId + ":" + monitorIndex + ":" + "bottom"); break; case PanelLoc.left: - list.push(i + ":" + monitorIndex + ":" + "left"); + list.push(panelId + ":" + monitorIndex + ":" + "left"); break; case PanelLoc.right: - list.push(i + ":" + monitorIndex + ":" + "right"); + list.push(panelId + ":" + monitorIndex + ":" + "right"); break; default: global.log("addPanel - unrecognised panel position "+panelPosition); } + setPanelsEnabledList(list); + if (sharedPanelId != undefined) { + AppletManager.clearAppletConfiguration(panelId); + addSharedPanels(sharedPanelId, panelId); + AppletManager.setupSharedApplets(sharedPanelId, panelId); + } + // Delete all panel dummies if (this.addPanelMode) this._destroyDummyPanels(); }, + /** + * Copies the string array settings of the shared panel to another panel. + * @param {string} key Gsetting panel setting key of type `as` (String Array). + * @param {number} panelId Panel to copy settings to. + * @param {number} sharedPanelId Panel to copy settings from. + */ + _copyStringArraySettings(key, panelId, sharedPanelId) { + /** @type {string[]} Each string is in the form `':'`*/ + const settings = global.settings.get_strv(key); + const splitSettings = settings.map(e => e.split(":")); + const splitSharedSetting = splitSettings.find(e => e[0] == sharedPanelId); + if (!splitSharedSetting) return; + const newSetting = [panelId, splitSharedSetting[1]].join(":"); + const existingIndex = splitSettings.findIndex(e => e[0] == panelId); + if (existingIndex !== -1) settings[existingIndex] = newSetting; + else settings.push(newSetting); + global.settings.set_strv(key, settings); + }, + + /** + * Copies the object array settings of the shared panel to another panel. + * @param {string} key Gsetting panel setting key of type `s` (String) which is valid JSON. + * @param {number} panelId Panel to copy settings to. + * @param {number} sharedPanelId Panel to copy settings from. + */ + _copyObjectArraySettings(key, panelId, sharedPanelId) { + /** @type {string} JSON string */ + const settings = global.settings.get_string(key); + /** @type {{panelId: number}[]} Each element is an object with a panelId property */ + const parsedSettings = JSON.parse(settings); + let sharedSetting = parsedSettings.find(e => e.panelId == sharedPanelId); + if (!sharedSetting) return; + sharedSetting = { ...sharedSetting, panelId }; + const existingSetting = parsedSettings.findIndex(e => e.panelId == panelId); + if (existingSetting !== -1) parsedSettings[existingSetting] = sharedSetting; + else parsedSettings.push(sharedSetting); + global.settings.set_string(key, JSON.stringify(parsedSettings)); + }, + /** * movePanel: * @monitorIndex (integer): monitor to be added to @@ -928,6 +1041,7 @@ PanelManager.prototype = { this.handling_panels_changed = true; let newPanels = new Array(this.panels.length); + const addedPanelIndices = []; let newMeta = new Array(this.panels.length); let drawcorner = [false,false]; @@ -965,15 +1079,13 @@ PanelManager.prototype = { // on_orientation_changed function } } else { // new panel - let panel = this._loadPanel(ID, mon, ploc, drawcorner, newPanels, newMeta); - if (panel) - AppletManager.loadAppletsOnPanel(panel); + if (panel) addedPanelIndices.push(ID); } } @@ -992,6 +1104,7 @@ PanelManager.prototype = { this.panels = newPanels; this.panelsMeta = newMeta; + addedPanelIndices.forEach(index => AppletManager.loadAppletsOnPanel(this.panels[index])); // // Adjust any vertical panel heights so as to fit snugly between horizontal panels // Scope for minor optimisation here, doesn't need to adjust verticals if no horizontals added or removed @@ -1188,11 +1301,11 @@ PanelManager.prototype = { * * Prompts user where to add the panel */ - addPanelQuery: function() { + addPanelQuery: function(sharedPanelId) { if (this.addPanelMode || !this.canAdd) return; - this._showDummyPanels(Lang.bind(this, this.addPanel)); + this._showDummyPanels((monitorIndex, panelPosition) => this.addPanel(monitorIndex, panelPosition, sharedPanelId)); this._addOsd.show(); }, @@ -1763,6 +1876,14 @@ PanelContextMenu.prototype = { }); menu.addMenuItem(menu.addPanelItem); + menu.addSharedPanel = new PopupMenu.PopupIconMenuItem(_("Add a shared panel"), "list-add", St.IconType.SYMBOLIC); // submenu item add shared panel + menu.addSharedPanel.activate = Lang.bind(menu, function() { + Main.panelManager.addPanelQuery(this.panelId); + this.close(true); + }); + menu.addMenuItem(menu.addSharedPanel); + + // menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); // separator line @@ -1824,9 +1945,12 @@ PanelContextMenu.prototype = { open: function(animate) { PopupMenu.PopupMenu.prototype.open.call(this, animate); - + const sharedPanels = getSharedPanels(); this.movePanelItem.setSensitive(Main.panelManager.canAdd); this.addPanelItem.setSensitive(Main.panelManager.canAdd); + this.addSharedPanel.setSensitive(Main.panelManager.canAdd + && (sharedPanels.length == 0 || sharedPanels.includes(this.panelId)) + ); // this.pasteAppletItem.setSensitive(AppletManager.clipboard.length != 0); let {definitions} = AppletManager; @@ -1970,7 +2094,12 @@ PanelZoneDNDHandler.prototype = { let children = this._panelZone.get_children(); let curAppletPos = 0; - let insertAppletPos = 0; + let insertAppletPos = -1; + + const { + panel: { panelId: sourceAppletPanel }, + locationLabel: sourceAppletLocation, + } = source.actor._applet; for (let i = 0, len = children.length; i < len; i++) { if (children[i]._delegate instanceof Applet.Applet){ @@ -1982,7 +2111,12 @@ PanelZoneDNDHandler.prototype = { } } - source.actor._applet._newOrder = insertAppletPos; + const isSameLocation = ( + sourceAppletPanel === this._panelId + && sourceAppletLocation === this._zoneString + && insertAppletPos === -1 + ); + if (!isSameLocation) source.actor._applet._newOrder = insertAppletPos === -1 ? 0 : insertAppletPos; source.actor._applet._newPanelLocation = this._panelZone; source.actor._applet._zoneString = this._zoneString; source.actor._applet._newPanelId = this._panelId;