File: Frigcal/code/Frigcal--source/settings.py
"""
========================================================================
USER APP SETTINGS - FILE (part of Frigcal)
Fetch and store user configurations in a JSON text file.
FC uses a JSON instead of PPUS's pickle: readability is useful.
This started with PPUS's code but has been trimmed much because:
- There are only 4 settings in FC, so automation isn't warranted
- Settings-screen mods are applied immediately in widget callbacks
In FC, self.settings here always reflects the settings file's
contents, and Settings-screen widget mods only mod running app
behavior. self.settings is not updated until a call to
update_and_save_settings() pushes user mods out to the file.
This ensures that calendar-folder auto-saves on asks change
only the folder in the settings file, but requires settings
to be managed explicitly on startup and restores and changes
in the Settings screen:
- globalfontsize, globalfontname: loaded and applied on startup
in App.start(), reapplied on Settings Restore and change here,
saved on Save in Settings here (CUT: globalfontname)
- maxbackups: loaded and passed into storage object on startup in
App.start(), resaved in storage object on Settings restores and
changes here, used by storage object on calendar saves
- editbubbles: loaded and set in app on startup in App.start(),
reset in app object on Settings restores and changes here,
used by .kv UI code when dialogs and screens opened
- colortheme: loaded and applied on startup in App.build(),
reapplied on Settings Restore and change, saved on Save in Settings
- calendarsfolder: loaded at startup; changed and auto-saved by
the storage object on folder asks, including Settings changes
here; and used on file access by storage object
Unlike others, calendarsfolder is not reset or repplied on Settings
Restores: file value is always current because auto-saved on asks,
and going back to preset None would require a check for changes, a
new ask, and a calendars reload, which seems too much UI complexity
========================================================================
"""
from common import *
class AppSettings:
"""
Manage persistent user-configurable settings.
One instance, created by and stored in App instance.
Public interface: attribute get/set for specific settings,
update_and_save_settings(), and restore_setting().
"""
def __init__(self, app):
self.app = app # link back to main.py's App instance
self._init_factory_presets() # setup preset defaults
self._load_settings_from_file() # initial load, file => self.settings
def __getattr__(self, name):
"""
Map undef attrs to settings dict for brevity:
App.settings.x => App.settings.settings['x']
NB: name won't be a setting if same as a real attr name.
"""
if 'settings' in self.__dict__ and name in self.settings:
trace('getattr setting:', name)
return self.settings[name]
else:
trace('getattr other')
raise NameError(f'Undefined name: {name}')
def __setattr__(self, name, value):
"""
Map settings attrs to settings dict for brevity:
App.settings.x=y => App.settings.settings['x']=y
NB: unlike getattr, this works even if same as real attr.
"""
if 'settings' in self.__dict__ and name in self.settings:
trace('setattr setting:', name, value)
self.settings[name] = value
else:
trace('setattr other:', name)
object.__setattr__(self, name, value)
def _init_factory_presets(self):
"""
--------------------------------------------------------------------
The 1:1 mapping from setting name to its GUI filed isn't as
clear-cut as in PPUS - some settings may be structured, and
all are manually propagated to/from GUI fields as needed.
Calendars are all *.ics files, calendar events are stored in
.ics files, and category colors are stored in settings here
and are the sole source of event colors in 4.0. This is in
a method in case self is ever required for a setting value.
All settings are in the Settings screen. Calendar folder is set
both by Settings on demand and by asks before loads and saves,
and it is auto-saved after asks because a manual Save would be
onerous. Theme is also in main menu where it can only be toggled.
--------------------------------------------------------------------
"""
self.factory_presets = dict(
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Calendar folder in Settings screen
# Auto saved on asks, manually saved/restored in Settings
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Used on load/save, set by asks or Settings, initial falsy None
# On PCs: always a pathname str, for POSIX calls
# On Android: content URI py str, converted from/to java Uri for SAF
calendarsfolder = None, # path or URI, false till ask
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Misc Settings-screen settings
# Manually saved/restored in Settings
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
maxbackups = 25, # number saves in _Backups/
editbubbles = 'Enabled', # or 'Disabled', text handles+bubbles
colortheme = 'Dark', # 'Dark'|'Light', also a menu toggle
globalfontsize = FONT_SIZE_DEFAULT, # '15sp' to abs, set_font_size()
#globalfontname = FONT_NAME_DEFAULT, # CUT; roboto, etc., set_font_name()
# toggles; TBD: user visible?
retainLiteralBackslashes = True, # icalendar format workaround in icsfiletools
chooserShowHiddenFiles = False, # folder-chooser option (TBD)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Event colors shown in event-edit dialog and event-list dialog
# Not user changeable (apart from unsupported JSON file edits...)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Major change in 4.0: calendars can no longer be colored,
# because doing so requires users to sync or reset the
# settings file on each device they use. Instead, colors
# are now based only on event category, which must be set
# to one of a list of preset colors. This ensures that a
# calendar file renders the same on all devices with app.
# Also now colors just event overscore bars: not (fg, bg).
categorycolors = {
# Used for events with a category: demoed in pulldown, not in Settings
# Predefined primary, named, and theme-friendly colors, unchangeable
# Now: just {'isocolorname': 'isocolorname'} for UX simplicity
# Was: {'categoryname': ([r, g, b, a?] | #rrggbb(aa?) | 'isocolorname')}
color: color for color in
['red', 'green', 'blue', 'cyan',
'yellow', 'purple', 'lime', 'orange',
'salmon', 'teal', 'pink', 'gray',
'steelblue', 'magenta', 'tan', 'maroon',
'violet', 'chocolate', 'blueviolet', 'khaki',
'tomato', 'bisque', 'cadetblue', 'gold',
'navy', 'yellowgreen', 'slateblue', 'sienna',
'black', 'white',
]
# Poor in light mode: 'azure', 'wheat', 'beige', 'lavender'
# Indistinguishable: 'aqua'=='cyan', 'fuchsia'=='magenta'
# Material-design accent color for blue looks like yellow
# black/white are mapped to #121212 /#fafafa: black-ish
# and white-ish, useful for no color bar on Month per theme
},
)
def _load_settings_from_file(self):
"""
--------------------------------------------------------------------
Load all settings from dict in app-private JSON text file.
Can use POSIX/filepath on Android: it's in app's own storage.
On PCs: in install folder (Windows/Linux) or ~ folder (macOS).
Was pickle originally:
settings_file = open(settings_path, 'rb')
settings = pickle.load(settings_file)
--------------------------------------------------------------------
"""
# in platform-specific folder
app_priv_path = self.app.storage_path_app_private() or ''
settings_path = osjoin(app_priv_path, SETTINGS_FILE)
trace(settings_path)
if not osexists(settings_path):
settings = self.factory_presets.copy() # first run: defaults
else:
try:
# JSON text => Python objects
settings_file = open(settings_path, 'r', encoding='utf8')
settings = json.load(settings_file)
settings_file.close()
except:
settings = self.factory_presets.copy()
self.app.info_message('Cannot load settings: please report.', usetoast=True)
# save file mirror
self.settings = settings
# update loaded settings for anything new in presets
for key in self.factory_presets:
if key not in self.settings:
self.settings[key] = self.factory_presets[key]
def _save_settings_to_file(self, settings):
"""
--------------------------------------------------------------------
Save all settings to dict in app-private JSON text file.
Can use POSIX/filepath on Android: it's in app's own storage.
On PCs, this is either in install folder or macOS ~ folder.
Was pickle originally:
settings_file = open(settings_path, 'wb')
pickle.dump(settings, settings_file)
--------------------------------------------------------------------
"""
# in platform-specific folder
app_priv_path = self.app.storage_path_app_private() or ''
settings_path = osjoin(app_priv_path, SETTINGS_FILE)
try:
# Python => JSON
settings_file = open(settings_path, 'w', encoding='utf8')
json.dump(settings, settings_file, indent=4)
settings_file.close()
except:
self.app.info_message('Cannot save settings: please report.', usetoast=True)
def update_and_save_settings(self, **newsettings):
"""
On Settings-screen Save and callbacks of its widgets.
Call with one or more keyword args for setting=value:
<this>(calendarsfolder=newpath)
<this>(globalfontsize=20, colortheme='Dark', ...)
Updates all passed settings, only, both in memory and
in the persistence file. Settings not passed are not
changed in either. Nit: it's inefficient to save all
each time, but the set is small.
"""
# pick up all passed settings from GUI's fields
self.settings.update(**newsettings)
# save full settings dict to json file
self._save_settings_to_file(self.settings)
def restore_settings(self):
"""
Restore all Settings-screen settings to original "factory" presets.
This resets the in-memory dict but does not update any GUI widgets.
Prior tbd: should this also write them to persistence file now?
No: this proved to be unwarranted in the PPUS app; user must Save.
Update: this skips and does not restore calendarsfield because
doing so would be a UI mess; see the top of thisfile for info.
"""
# repopulate internal copy from defaults
refills = self.factory_presets.copy() # all preset values
del refills['calendarsfolder'] # except for folder...
self.settings.update(refills)
# Additional docs...
"""
--------------------------------------------------------------------
Settings start with the presets above before any saves, and
settings are saved manually on each Settings-screen Save tap.
The Settings-screen Restore button restores all settings, including
the calendar-folder path, and the path is auto-saved after asks.
Stored in app-private storage (not app install), in a JSON-file
dict, where the .kv file's IDs == dict keys in settings.
On PCS, app-private is install or ~/Library/appname. On Android,
app-private is app's data folder, can't be seen by explorers or
other apps, and goes away on app uninstall unless user toggles
the save option in uninstall popup? (manifest fragile setting tbd).
Copies any unique items in presets to settings loaded from the
file, so that they added to file on later saves. Else, first
value may come from .kv, not presets. This also makes new versions
of the app backward compatible with existing settings files: new
keys are auto-added to the file as presets. Running an old version
of the app with a new settings will simply ignore newer keys.
--------------------------------------------------------------------
"""