From 141d9aa55de0267a192ca8933a9fdeef17b22c2c Mon Sep 17 00:00:00 2001 From: Kevin Langman Date: Sat, 31 Jan 2026 14:07:48 -0500 Subject: [PATCH] This is my proposal for improving the dependency functionality for the settings-schema.json files. Currently dependency strings can only contain a single variable, like "key=1" or "!key". This drastically limits the ability to have complex widget revealing logic. An example of complex revealing logic can be seen [here](https://github.com/linuxmint/cinnamon-spices-extensions/blob/a5d7ce54c7e3b609405e20ea6fc2bc2bfb5b0cec/BlurCinnamon%40klangman/files/BlurCinnamon%40klangman/6.0/settings-schema.json#L338). "dependency" : "enable-panels-effects & enable-panel-unique-settings & component-selector=7 & enable-panels-override" This was enabled in Blur Cinnamon by monkey patching the JSONSettingsRevealer functions to enable a feature set that this code change provides. This change adds "and" and "or" operator support for the dependency strings following the logical order of operators ("and" before "or"). I believe this code maintains compatibility with the previous logic. My testing suggests that this is true, but I have not tested all Applets/Extensions/Desklets to ensure it is true. There are other limitations that this change does NOT address, but we could consider, like: 1. The ability to handle "key vs key" comparisons (i.e. "key1=key2"). This would require some key identifier like %key% or $key in order to know if a value is meant to be interpreted as a key or a constant string. We could use the old logic when no key identifier character is found in the string, and new logic when one is found. 2. The ability to use round brackets to order the evaluation of the "and" and "or" operators. In many cases expressions can be rewritten without brackets to achieve the same logic, but some scenarios are impossible without brackets. I believe that simply adding "and" and "or" operator support will make a meaningful improvement to the usefulness of dependencies. I welcome any feedback or suggestions. --- .../bin/JsonSettingsWidgets.py | 128 ++++++++++++++---- 1 file changed, 103 insertions(+), 25 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py index 687f007aa0..1bb906814a 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py @@ -10,6 +10,7 @@ import collections import json import operator +import re can_backend = px_can_backend + c_can_backend can_backend.append('List') @@ -237,27 +238,75 @@ def save_to_file(self, filepath): new_file.write(raw_data) new_file.close() +def get_constant(settings, string): + try: + value = float(string) # Try converting to a float + except ValueError: + if string.lower() == 'true': + value = True + elif string.lower() == "false": + value = False + else: + value = string + return value + class JSONSettingsRevealer(Gtk.Revealer): def __init__(self, settings, key): super(JSONSettingsRevealer, self).__init__() self.settings = settings - self.key = None - self.op = None - self.value = None - for op in OPERATIONS: - if op in key: - self.op = op - self.key, self.value = key.split(op) - break - - if self.key is None: - if key[:1] == '!': - self.invert = True - self.key = key[1:] - else: - self.invert = False - self.key = key + # Split the dependencies into a list of keys, operations and constants + expression = re.split(r'(!=|<=|>=|[<>=&| ])', key) + # Remove any blank entries and any whitespace within entries + self.expression = [item.strip() for item in expression if item.strip()] + + # Listen to any keys found in the expression, + # expand all compares, decode constants, + # decode compare operators and check for errors + key = None + idx = 0 + count = len(self.expression) + listening = [] + #print( f"Preparing dependency: {self.expression}" ) + while idx < count: + element = self.expression[idx] + if element == '&' or element == '|': + pass + elif element in OPERATIONS: # ... key op constant ... + self.expression[idx] = OPERATIONS_MAP[element] + key = self.expression[idx-1] + if idx+1 < count and self.expression[idx+1] != '&' and self.expression[idx+1] != '|': + self.expression[idx+1] = get_constant(self.settings, self.expression[idx+1]) + else: # No constant provided, so we assume a zero length string + self.expression.insert(idx+1, "") + count += 1 + idx += 1 + elif element[0] == '!': # ... !key ... + key = element[1:] + self.expression[idx] = key + self.expression.insert(idx+1, False) + self.expression.insert(idx+1, operator.eq) + idx += 2 + count += 2 + elif idx == count-1 or self.expression[idx+1] == '&' or self.expression[idx+1] == '|': # standalone key + key = element + self.expression.insert(idx+1, True) + self.expression.insert(idx+1, operator.eq) + idx += 2 + count += 2 + if key: + if self.settings.has_key(key): + if key not in listening: + self.settings.listen(key, self.key_changed) + listening.append(key) + else: + print( f"Error in json dependency: \"{key}\" is not a valid key" ) + if idx+1 < count and self.expression[idx+1] != '&' and self.expression[idx+1] != '|': + print( f"Error in json dependency: Unexpected expression \"{self.expression[idx+1]}\"" ) + self.expression = self.expression[:idx+1] # remove the remaining elements since something is wrong with the syntax + break + key = None + idx += 1 self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15) Gtk.Revealer.add(self, self.box) @@ -265,20 +314,49 @@ def __init__(self, settings, key): self.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN) self.set_transition_duration(150) - self.settings.listen(self.key, self.key_changed) - self.key_changed(self.key, self.settings.get_value(self.key)) + # Set reveal state + self.key_changed(None, None) def add(self, widget): self.box.pack_start(widget, False, True, 0) def key_changed(self, key, value): - if self.op is not None: - val_type = type(value) - self.set_reveal_child(OPERATIONS_MAP[self.op](value, val_type(self.value))) - elif value != self.invert: - self.set_reveal_child(True) - else: - self.set_reveal_child(False) + evaluate = [] + count = len(self.expression) + #print( f"Evaluating expression: {self.expression}" ) + + # Go through the expression to evaluate all the compares + # The init ensures that the list has this format: key op const [ <&/|> key op const ]... + idx = 0 + while idx < count: + lhs = self.settings.get_value(self.expression[idx]) + op = self.expression[idx+1] + rhs = self.expression[idx+2] + evaluate.append( op(lhs, rhs) ) + idx += 3 + if idx < count: + evaluate.append( self.expression[idx] ) + idx += 1 + #print( f"Post compare evaluation: {evaluate}" ) + + # Handle all the "and" operations first in accordance with the logical order of operations + while "&" in evaluate: + idx = evaluate.index("&") + result = (evaluate[idx-1] and evaluate[idx+1]) + evaluate[idx-1:idx+2] = [] ## remove 3 elements: idx-1 through idx+1 + evaluate.insert(idx-1, result); + #print( f"After evaluating the ands: {evaluate}" ) + + # Handle all the "or" operations (there should be nothing but "or" operations at this point) + while "|" in evaluate: + idx = evaluate.index("|") + result = (evaluate[idx-1] or evaluate[idx+1]) + evaluate[idx-1:idx+2] = [] ## remove 3 elements: idx-1 through idx+1 + evaluate.insert(idx-1, result); + #print( f"After evaluating ors: {evaluate}" ) + + # At this point we should only have one entry in the list, the final result + self.set_reveal_child(evaluate[0]) class JSONSettingsBackend(object): def attach(self):