File: Frigcal/code/Frigcal--source/settingsgui.py
"""
========================================================================
USER APP SETTINGS - SCREEN (part of Frigcal)
Implement the settings screen, for editing configuration options
that are stored in a JSON text file by settings.py. The screen's
UI is defined in allscreens.kv.
========================================================================
"""
from common import *
# for folder-change verify
from storage import CalendarsTable, CalendarsDirty
# for folder-change verify
# import main for ConfirmDialog fails: use Kivy Factory
class SettingsGUI:
allsettings = ['colortheme',
'globalfontsize',
#'globalfontname', # CUT
'maxbackups',
'editbubbles', # new
'calendarsfolder']
def __init__(self, app, screen):
self.app = app
self.screen = screen
def fix_widgets_for_theme(self):
"""
Ad-hoc theme fixes, called on theme changes
Some already-built widgets do not auto-adjust
"""
ids = self.screen.ids
for field in self.allsettings:
widget = getattr(ids, field)
widget.text_color_normal = self.app.manual_theme_color_fg()
widget.text_color_focus = self.app.manual_theme_color_fg()
# ----------
# FILL/FETCH
# ----------
def fill_widgets_from_settings(self):
"""
Load GUI fields from all settings
Run on startup, restores, and possibly more
Settings are set explicitly in GUI callbacks
"""
for field in self.allsettings:
value = getattr(self.app.settings, field) # like settings.field
if field == 'calendarsfolder': # None till ask1
value = value or ''
elif field in ['globalfontsize', 'maxbackups']: # integers
value = str(value) # else str as is
self.screen.ids[field].text = value # set in GUI, ids.field
def fetch_settings_from_widgets(self):
"""
Load temp settings dict from all GUI fields
Used before storing settings persistently
Settings are saved explicitly on Settings Save
"""
values = {}
for field in self.allsettings:
value = self.screen.ids[field].text # like ids.field
if field in ['globalfontsize', 'maxbackups']: # integers
value = int(value) # else str as is
values[field] = value # set in settings table
return values
# ----------
# COLORTHEME
# ----------
def on_colortheme_touch(self, touch):
trace('on_colortheme_touch')
guifield = self.screen.ids.colortheme
if not guifield.collide_point(*touch.pos): # or touch.x, touch.y
return False
allthemes = ['Dark', 'Light'] # future expansion?
menu_items = [ # a list of dicts
dict(viewclass='OneLineListItem',
text=themename,
on_release=
lambda themename=themename: # else last loop-var value
self.on_colortheme_menu_tap(themename),
)
for themename in allthemes
]
# bind nested MDLabels' font_size+name _after_ creation else kivmd overrides
# PUNT/TBD: the OLLIs don't seem to be accessible here?
self.colortheme_menu = MDDropdownMenu(
items=menu_items,
caller=guifield,
# else default 4 may run into android edge-to-edge navbar
border_margin=dp(60),
)
self.colortheme_menu.open()
return True # consume touch event
def on_colortheme_menu_tap(self, themename):
"""
Set text field to menu pick, app theme auto changed on close.
Text field need not be verified: it's readonly in .kv to prevent
a pointless onscreen keyboard. App.settings settings table is
not updated till Save to file.
"""
trace('on_colortheme_menu_tap')
self.colortheme_menu.dismiss()
self.screen.ids.colortheme.text = themename # settings screen
if themename != self.app.theme_cls.theme_style: # whole app
self.app.on_theme(self.screen, toggle=False) # don't open main menu
# -------------------
# GLOBALFONTNAME: CUT
# -------------------
'''CUT (see common.kv)
def on_fontname_touch(self, touch):
trace('on_fontname_touch')
guifield = self.screen.ids.globalfontname
if not guifield.collide_point(*touch.pos): # or touch.x, touch.y
return False
allnames = ['Roboto', 'DejaVuSans', 'RobotoMono-Regular'] # kivy builtins only
menu_items = [ # a list of dicts
dict(viewclass='OneLineListItem',
text=fontname,
on_release=
lambda fontname=fontname: # else last loop-var value
self.on_fontname_menu_tap(fontname),
)
for fontname in allnames
]
# bind nested MDLabels' font_size+name _after_ creation else kivmd overrides
# PUNT/TBD: the OLLIs don't seem to be accessible here?
self.fontname_menu = MDDropdownMenu(
items=menu_items,
caller=guifield,
# else default 4 may run into android edge-to-edge navbar
border_margin=dp(60),
)
self.fontname_menu.open()
return True # consume touch event
CUT'''
'''CUT (see common.kv)
def on_fontname_menu_tap(self, fontname):
"""
Set text field to menu pick, app theme auto changed on close.
changed on close. Text field need not be verified: it's
readonly in .kv to prevent a pointless onscreen keyboard.
App.settings settings table is not updated till Save to file.
"""
trace('on_fontname_menu_tap')
self.fontname_menu.dismiss()
self.screen.ids.globalfontname.text = fontname # settings screen
self.app.set_font_name(fontname) # reset widgets now
CUT'''
# --------------
# GLOBALFONTSIZE
# --------------
def on_fontsize_defocus(self):
"""
Unlike fontname, fontsize works globally and already-displayed text
is auto-updated. Only caveats: with KivyMD, doesn't apply to text in
drop-down menus and requires manual post-create rebinds for some widgets.
NB: this triggers a later refill of Help/About text to rescale headers.
"""
trace('on_fontsize_defocus')
guifield = self.screen.ids.globalfontsize
try:
intval = int(guifield.text)
except:
self.app.info_message('Invalid font size: try again', usetoast=True)
guifield.text = '?'
else:
# self.app.settings.globalfontsize = intval # not to file till Settings Save
self.app.set_font_size(intval) # set property to resize text
# ----------
# MAXBACKUPS
# ----------
def on_maxbackups_defocus(self):
trace('on_maxbackups_defocus')
guifield = self.screen.ids.maxbackups
try:
intval = int(guifield.text)
except:
self.app.info_message('Invalid max backups: try again', usetoast=True)
guifield.text = '?'
else:
# self.app.settings.maxbackups = intval # not to file till Settings Save
self.app.storage.maxbackups = intval # set in calendars-storage manager
# -----------
# EDITBUBBLES
# -----------
def on_editbubbles_touch(self, touch):
# could be a toggle, but text for uniformity and descriptiveness
trace('on_editbubbles_touch')
guifield = self.screen.ids.editbubbles
if not guifield.collide_point(*touch.pos): # or touch.x, touch.y
return False
allbubbles = ['Enabled', 'Disabled'] # future expansion?
menu_items = [ # a list of dicts
dict(viewclass='OneLineListItem',
text=bubblename,
on_release=
lambda bubblename=bubblename: # else last loop-var value
self.on_editbubbles_menu_tap(bubblename),
)
for bubblename in allbubbles
]
# bind nested MDLabels' font_size+name _after_ creation else kivmd overrides
# PUNT/TBD: the OLLIs don't seem to be accessible here?
self.editbubbles_menu = MDDropdownMenu(
items=menu_items,
caller=guifield,
# else default 4 may run into android edge-to-edge navbar
border_margin=dp(60),
)
self.editbubbles_menu.open()
return True # consume touch event
def on_editbubbles_menu_tap(self, bubblename):
"""
Set text field to menu pick. Text field need not be verified:
it's readonly in .kv to prevent a pointless onscreen keyboard.
App.settings settings table is not updated till Save to file.
"""
trace('on_editbubbles_menu_tap')
self.editbubbles_menu.dismiss()
self.screen.ids.editbubbles.text = bubblename # settings screen
# self.app.settings.editbubbles = bubblename # not to file till Settings Save
self.app.editbubbles = bubblename # set in app for use in .kv code
# ---------------
# CALENDARSFOLDER
# ---------------
def on_calendarsfolder_touch(self, touch):
trace('on_calendarsfolder_touch')
guifield = self.screen.ids.calendarsfolder
if not guifield.collide_point(*touch.pos): # or touch.x, touch.y
return False
def on_do_change_calendar():
# when no changes or changes discarded
# skip if new path == current path? no: this
# allows user to reload after adding ICS files
# load_all_ics_files does most of the work here:
# - resets calendar and event globals before load
# - refills gui's events on load-thread exit
# - checks for and informs user if no calendars
# - updates folder in settings and Settings screen
self.app.storage.run_with_calendars_folder(
self.app.storage.load_all_ics_files, # iff selection
forcedask=True)
# warn user first if current folder's calendars have changes
if not any(CalendarsDirty[icsfilename] for icsfilename in CalendarsTable):
on_do_change_calendar()
else:
message = (
'[b]Unsaved calendar changes[/b]'
'\n\n'
'[u][i]Caution[/i][/u]: '
'you have asked to change the calendars folder, but '
'the currently loaded calendars have unsaved changes.'
'\n\n'
'To save these changes, tap Cancel and then Save Calendars '
'in the main menu. '
'To discard changes and change folders now, tap Confirm.')
confirmer = Factory.ConfirmDialog(
message=message,
onyes=lambda root:
[self.app.dismiss_popup(root), # confirm dialog
on_do_change_calendar()], # start change steps
onno=self.app.dismiss_popup) # confirm dialog
popup = Popup(
title='Confirm Folder Change',
content=confirmer,
auto_dismiss=False, # ignore taps outside
size_hint=(0.9, 0.8), # phones wide, e-2-e short
**self.app.themeForPopup()) # fg/bg colors if 'light'
self.app.popupStack.push(popup)
self.app.popupStack.open() # show top Popup with Dialog
return True # consume touch event
def enable_calendardsfolder_hscroll(self, *args):
"""
UNUSED: an attempt to hscroll calandardsfolder TextField
From PPUS: must calc TextField width manually this way
for horizontal scroll in a ScrollView; just awful...
"""
scrollwidth = self.screen.ids.calendarsfolderscroll.width
llabelwidth = self.screen.ids.calendarsfolder._lines_labels[0].width # not multiline
widthrecalc = max(scrollwidth, llabelwidth + kivy.metrics.dp(25)) # padding
self.screen.ids.calendarsfolder.width = widthrecalc # scroll, dammit
trace('e_c_h', scrollwidth, llabelwidth, widthrecalc)
# PUNT: calendar colors dropped, categories/color unchangeable
"""
def on_calendarcolors_touched(self, *args):
trace('on_calendarcolors_touched')
"""
# ------------
# SAVE/RESTORE
# ------------
def on_settings_save(self, screen):
# update settings file from settings-screen widgets
allvalues = self.fetch_settings_from_widgets()
self.app.settings.update_and_save_settings(**allvalues)
# not required: screen fields updated as changed
# self.fill_widgets_from_settings()
def on_settings_restore(self, screen):
# restore settings and widgets from preset defaults
self.app.settings.restore_settings()
self.fill_widgets_from_settings()
# apply Settings' dynamic configs now, else Restore == no-op,
# even though the Settings screen has been reset to presets
self.app.set_font_size(self.app.settings.globalfontsize)
#self.app.set_font_name(self.app.settings.globalfontname) # CUT
self.app.storage.maxbackups = self.app.settings.maxbackups
self.app.editbubbles = self.app.settings.editbubbles
if self.app.settings.colortheme != self.app.theme_cls.theme_style:
self.app.on_theme(self.screen, toggle=False)
# calendarsfolder is fetched when used, others not user changable