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