Skip to content

JsonSettingsWidgets.py- Simplified monitoring state and improve file operations #12910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 96 additions & 36 deletions files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,27 @@ class JSONSettingsHandler(object):
def __init__(self, filepath, notify_callback=None):
super(JSONSettingsHandler, self).__init__()

self.resume_timeout = None
self.notify_callback = notify_callback
self._is_internal_update = False

self.filepath = filepath
self.file_obj = Gio.File.new_for_path(self.filepath)
self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None)
self.file_monitor.connect("changed", self.check_settings)
self._setup_monitor()

self.bindings = {}
self.listeners = {}
self.deps = {}

self.settings = self.get_settings()

def _setup_monitor(self):
"""Set up initial file monitoring."""
try:
self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None)
self.file_monitor.connect("changed", self.check_settings)
except GLib.Error as e:
print(f"Error initializing monitoring: {str(e)}")
self.file_monitor = None

def bind(self, key, obj, prop, direction, map_get=None, map_set=None):
if direction & (Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET) == 0:
Expand Down Expand Up @@ -132,16 +140,26 @@ def set_object_value(self, info, value):
info["obj"].set_property(info["prop"], value)

def check_settings(self, *args):
"""Check for settings changes."""
if self._is_internal_update:
return

old_settings = self.settings
self.settings = self.get_settings()

for key in self.bindings:
# Skip keys that don't exist in both old and new settings to avoid KeyError
if key not in self.settings or key not in old_settings:
continue
new_value = self.settings[key]["value"]
if new_value != old_settings[key]["value"]:
for info in self.bindings[key]:
self.set_object_value(info, new_value)

for key, callback_list in self.listeners.items():
# Skip keys that don't exist in both old and new settings to avoid KeyError
if key not in self.settings or key not in old_settings:
continue
new_value = self.settings[key]["value"]
if new_value != old_settings[key]["value"]:
for callback in callback_list:
Expand All @@ -158,37 +176,60 @@ def get_settings(self):
return settings

def save_settings(self):
self.pause_monitor()
if os.path.exists(self.filepath):
os.remove(self.filepath)
raw_data = json.dumps(self.settings, indent=4, ensure_ascii=False)
new_file = open(self.filepath, 'w+')
new_file.write(raw_data)
new_file.close()
self.resume_monitor()

def pause_monitor(self):
self.file_monitor.cancel()
self.handler = None

def resume_monitor(self):
if self.resume_timeout:
GLib.source_remove(self.resume_timeout)
self.resume_timeout = GLib.timeout_add(2000, self.do_resume)

def do_resume(self):
self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None)
self.handler = self.file_monitor.connect("changed", self.check_settings)
self.resume_timeout = None
return False
"""Save settings with real-time UI updates and proper cleanup."""
temp_filepath = self.filepath + '.tmp'

with InternalUpdateContext(self):
try:
# Data serialization
raw_data = json.dumps(self.settings, indent=4, ensure_ascii=False)

# Write to temporary file
with open(temp_filepath, 'w', encoding='utf-8') as temp_file:
temp_file.write(raw_data)
temp_file.flush()
os.fsync(temp_file.fileno())

# Atomic replacement
os.replace(temp_filepath, self.filepath)

except (IOError, OSError, json.JSONEncodeError) as e:
print(f"Error while saving settings: {str(e)}")
# Cleanup temporary file in case of error
if os.path.exists(temp_filepath):
try:
os.remove(temp_filepath)
except OSError as cleanup_error:
print(f"Error while cleaning up temporary file: {str(cleanup_error)}")
raise
except Exception as e:
print(f"Unexpected error during save: {str(e)}")
if os.path.exists(temp_filepath):
try:
os.remove(temp_filepath)
except OSError:
pass
raise

def reset_to_defaults(self):
"""Reset settings with real-time UI updates."""
changed = False

for key in self.settings:
if "value" in self.settings[key]:
self.settings[key]["value"] = self.settings[key]["default"]
self.do_key_update(key)

self.save_settings()
old_value = self.settings[key]["value"]
new_value = self.settings[key]["default"]
if old_value != new_value:
self.settings[key]["value"] = new_value
self.do_key_update(key)
changed = True

# Immediately notify callbacks
if self.notify_callback:
self.notify_callback(self, key, new_value)

if changed:
self.save_settings() # Saving won't block UI updates anymore

def do_key_update(self, key):
if key in self.bindings:
Expand Down Expand Up @@ -219,12 +260,19 @@ def load_from_file(self, filepath):
self.save_settings()

def save_to_file(self, filepath):
if os.path.exists(filepath):
os.remove(filepath)
raw_data = json.dumps(self.settings, indent=4)
new_file = open(filepath, 'w+')
new_file.write(raw_data)
new_file.close()
temp_filepath = filepath + '.tmp'
try:
with open(temp_filepath, 'w') as temp_file:
json.dump(self.settings, temp_file, indent=4)
temp_file.flush()
os.fsync(temp_file.fileno())
os.replace(temp_filepath, filepath)
finally:
if os.path.exists(temp_filepath):
try:
os.remove(temp_filepath)
except OSError:
pass

class JSONSettingsRevealer(Gtk.Revealer):
def __init__(self, settings, key):
Expand Down Expand Up @@ -333,3 +381,15 @@ def __init__(self, key, settings, properties):

for widget in can_backend:
globals()["JSONSettings"+widget] = json_settings_factory(widget)

class InternalUpdateContext:
"""Context manager for internal updates"""
def __init__(self, handler):
self.handler = handler

def __enter__(self):
self.handler._is_internal_update = True
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.handler._is_internal_update = False
Loading