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;