File: PC-Phone USB Sync/pcphoneusbsync.kv

#------------------------------------------------------------------------------
# Define the app's GUI, in a way that's integrated with the .py.
#
# [Please note: this file was not meant for public consumption.  If
# you're viewing it in builds or online anyhow, mind the rough edges.]
#
# Copyright © 2024 quixotely.com.  All rights reserved.
# License: this file is provided without warranties of any kind, and 
# only for online viewing and vetting.  It may not be copied, posted to 
# GitHub, modified, repackaged, or sold.  It is a view-only resource.
#
# This uses the Kivy language, which is supposed to separate layout
# from code and use a declarative style, but seems a bit too magical
# for my tastes (the relationship between code here and in the .py 
# is not documented well, and requires scouring Kivy's code to grok),
# and its sizing and layout model seems a non-orthogonal convolution 
# that smacks too much of CSS (including the endless tweaking).
#
# Moreover, this scheme doesn't address parts that must still be built 
# in the .py dynamically (e.g., the Main chooser's storage buttons).
# And in hindsight, this may be better manually loaded from a file and
# passed to the builder in the .py, to avoid some thorny location issues 
# for Kivy executables built with PyInstaller.  OTOH, this works...
#
# For more Kivy editorial, see also the filechooser workaround ahead;
# it's a great system, but badly needs some finishing tweaks and docs.
#------------------------------------------------------------------------------


#:kivy 2.1.0

# for refs in py code here
#:import os os
#:import sys sys
#:import Clock kivy.clock.Clock

# for phone/pc-specific tweaks
#:set onandroid hasattr(sys, 'getandroidapilevel')
#:set onmacos   sys.platform.startswith('darwin')

# [1.1.0] use dp(N) instead of N globally, both here and in the .py's code;
# this scales pixels (and widgets) to the host display's density, plus|minus;
# else buttons too small on hi-res displays, and too large on low-res displays;
# these may be overriden by size_hint_y %s, and/or sizing to self.texture_size;
# also use kivy.metrics.dp(N) instead of N in .py for dynamically built widgets;
# see also the related global font size's sp() scaling applied in the .py;
# all per https://kivy.org/doc/stable/api-kivy.metrics.html (see also cm());

## absolute pixels: not ideal, but they work (if size_hint_y=None)
##:set textheight   90
##:set buttonheight 80

# use density-scaled pixels, textheight no longer used [1.1.0]
#:set buttonheight dp(40)

## phone increment; now moot [1.1.0]
##:set buttonheight (buttonheight + (4 if onandroid else 0))

## easier taps of crucials; now moot [1.1.0]
##:set tallbuttonheight (buttonheight + 16 if onandroid else buttonheight)
#:set tallbuttonheight buttonheight


#------------------------------------------------------------------------------
# GLOBALS
#------------------------------------------------------------------------------


# CAUTION: dynamic class names cannot be same as a Kivy class name
# This is why an <ActionButton@Button> here never worked...


<PathDisplay@TextInput>:
    multiline: False                         # one-line editable text field
    foreground_color: 'black'
    background_color: 'white'

    # fix text clipping for higher user fontsize settings, and rightcrunch/curves

    size_hint_y: None
    # padding: [16, 6, 16, 6]                # [l, t, r, b], default [6]*4 (pixels)
    padding: [dp(7), dp(3), dp(6), dp(3)]    # scale to screen density, +t/b -l/r [1.1.0]
    height: self.minimum_height              # lines height, includes padding

    # always show start of text on left edge of widget, until scrolled

    valign: 'middle'    # moot?
    # halign: 'left'
    # scroll_x: 0

    # [1.1.0] do grab+move scrolling on PCs (phones already do swipe scroll); 
    # holding longer selects text instead; a ScrollView might help too (tbd);

    scroll_from_swipe: True    # default is False for PCs, True for phones


# yes, it works... multiple inheritance
<PathDisplayNoSelect@TextInputNoSelect+PathDisplay>


# yes, it works... if bound to app.dyn_font_size, not self|root
<Widget>:
    font_size: app.dynamic_font_size    # applied to every Widget subclass instance (!)


<SectionLabel@Label>:
    halign: 'center'
    text_size: root.width, None         # this is used, but may have no effect at all...
    size: self.texture_size
    color: 'cyan'
    font_name: 'Roboto-BoldItalic'      # for the kids


<LineLabel@Label>
    text_size: self.size
    halign: 'left'
    size_hint_y: 1
    valign: 'middle'
    # padding: [32, 32]          # [h, v] - but v moot by size_hint_y!
    padding: [dp(16), dp(16)]    # scale to screen density [1.1.0]


# color schemes - could be user configs someday (tbd, tmi?)
<PathButton@Button>
    background_color: 'blue'

<ActivityButton@Button>
    background_color: 'green'

<StorageButton@Button>
    background_color: 'red'    # not used here: made in .py

<DialogButton@Button>
    background_color: 'navy'

# now def in .py for conditional color
# <ConfigCheckbox@CheckBox>
#     # [r, g, b, a] for image tinting - too dim on macos only
#     color: [1, 3, 5, 4] if sys.platform.startswith('darwin') else [1, 1, 1, 1]
#     color: self.check_box_color  # handle in .py, auto-update on changes


#------------------------------------------------------------------------------
# ROOT
#------------------------------------------------------------------------------


Main:

    # Root widget (instance), maps to .py's class Main

    # available in .py
    # run_output: runoutput

    # ref as this or self.ids.<id> or self.ids['id']
    from_Path: frompath
    to_path: topath

    TabbedPanel:
        # size_hint: .5, .5
        # pos_hint: {'center_x': .5, 'center_y': .5}
        id: toptabs
        do_default_tab: False
        on_current_tab: root.on_tab_switch()

        # Failed [1.1.0]: force redraw for Android phone rotations;
        # neither this nor root.canvas.ask_update() worked; may be
        # an sdl2 glitch: see docs in .py enable_logs_text_hscroll()
        # and later on_start() tries - display size events are being
        # dropped by SDL2 or Kivy, or botched by Samsung Android.
        #
        # on_width: 
        #    (print('redraw'), 
        #    (Clock.schedule_once(lambda dt: (print('layout'), self._trigger_layout())), 1.0)
        #    if onandroid else None) 


#------------------------------------------------------------------------------
# MAIN TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: maintab
            text: 'Main'

            BoxLayout:
                orientation: 'vertical'

                SectionLabel:
                    size_hint_y: .10
                    text: 'Choose Content Folders'

                GridLayout:
                    cols: 2
                    size_hint_y: .15
                    # cols_minimum: {0: dp(20)}
                    # padding: [10, 0, 10, 0]          # [l, t, r, b]
                    padding: [dp(0), 0, dp(5), 0]      # drop pads, scale to density [1.1.0]
                    
                    PathButton:
                        size_hint_x: 0.40
                        text: 'FROM'
                        on_release: root.do_main_path('FROM', 'frompath')

                    PathDisplay:
                        size_hint_y: 1
                        id: frompath
                        text: 'This comes from settings default or save...'
                        font_name: 'DejaVuSans'

                    PathButton:
                        size_hint_x: 0.40
                        text: 'TO'
                        on_release: root.do_main_path('TO', 'topath')

                    PathDisplay:
                        size_hint_y: 1
                        id: topath
                        text: 'This comes from settings default or save...'
                        font_name: 'DejaVuSans'
                        on_text:
                            # [1.1.0] Never reenable UNDO if any action is in 
                            # progress, else user could launch a parallel UNDO!
                            # But clear undo_verboten so enabled on script exit,
                            # and enable UNDO now iff no action running now.
                            # This fires for both manual and chooser TO mods.

                            root.undo_verboten = False;
                            if not root.script_running: undobutton.disabled = False

                SectionLabel:
                    size_hint_y: 0.10
                    text: 'Start Action'

                GridLayout:
                    cols: 2
                    size_hint_y: 0.50
                    # cols_minimum: {0: 0.25, 1: 0.75}
                    # padding: [10, 0, 10, 0]           # [l, t, r, b]
                    padding: [dp(0), 0, dp(0), 0]       # drop pads, scale to density [1.1.0]

                    ActivityButton:
                        size_hint_x: 0.40
                        id: syncbutton
                        text: 'SYNC'
                        on_release: root.do_sync(frompath.text, topath.text)

                    LineLabel:
                        text: 'Make TO the same as FROM'
                        # text: 'Propagate changes in FROM to TO'

                    ActivityButton:
                        size_hint_x: 0.40
                        id: showbutton
                        text: 'SHOW'
                        on_release: root.do_show(frompath.text, topath.text)
 
                    LineLabel:
                        text: 'Report FROM/TO differences only'

                    ActivityButton:
                        size_hint_x: 0.40
                        id: undobutton
                        text: 'UNDO'
                        on_release: root.do_undo(topath.text)

                    LineLabel:
                        text: 'Roll back TO\'s most recent SYNC'

                    ActivityButton:
                        size_hint_x: 0.40
                        id: copybutton
                        text: 'COPY'
                        on_release: root.do_copy(frompath.text, topath.text)

                    LineLabel:
                        text: 'Make a full copy of FROM in TO'

                    ActivityButton:
                        size_hint_x: 0.40
                        id: diffbutton
                        text: 'DIFF'
                        on_release: root.do_diff(frompath.text, topath.text)

                    LineLabel:
                        text: 'Compare FROM to TO byte for byte'

                    ActivityButton:
                        size_hint_x: 0.40
                        id: namebutton
                        text: 'NAME'
                        on_release: root.do_name(frompath.text)

                    LineLabel:
                        id: namelabel
                        text: 'Make filenames portable in FROM'

                SectionLabel:
                    size_hint_y: 0.10
                    id: statuslabel
                    text: 'Action Status'

                Image:
                    size_hint_y: 0.20
                    id: statusimg
                    source: 'usbsync-anim.gif'
                    anim_delay: -1                # frames/sec, -1=stop, 0.20=go
                    anim_loop: 0                  # reps till stop, 0=keep looping  


#------------------------------------------------------------------------------
# LOGS TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            text: 'Logs'
            id: logstab

            BoxLayout:
                orientation: 'vertical'

                SectionLabel:
                    size_hint_y: 0.05
                    text: 'View Run Logfiles'

                Label:
                    size_hint_y: 0.05
                    text: root.logfile_path
                    size: self.texture_size
                    text_size: root.width, None
                    halign: 'center'

                    # else small/big display/font wraps to a line 2, which is
                    # partly vclipped no reason to scroll this: rarely sortened, 
                    # it's info only (+in docs), and can open|explore to check;
                    # this shortens on 'right', because it's a path, and always
                    # ends in long appname; could hack Main's chooser popup to do
                    # same in list view, but its 'center' is useful for basenames;

                    shorten: True
                    shorten_from: 'right'    # default 'center' obscures worse

                # using a filechooser here was a major pain in the app...
                # autoselect was elusive, prior selections were not cleared after 
                # _update_files() (autoselect or not), icon-view didn't show full 
                # filename, and popup requires pointless taps; punt and go custom

                AnchorLayout:
                    size_hint_y: 0.30
                    anchor_x: 'center'    # center scroll+filelist in window 

                    ScrollView:
                        # pos_hint: {'center_x': .5}
                        # width: picklogitems.width
                        size_hint_x: None            # which is like (None, 1)

                        id: picklogscroll
                        do_scroll_x: True
                        do_scroll_y: True
                        effect_cls: 'ScrollEffect'    # don't overscroll/snapback

                        # one of the more subtle bits here: this ensures that 
                        # the filelist scrolls horizontally if the window is too
                        # narrow to display it, by listening for changes in the
                        # root's width; this also ensures hscrolls if the filelist
                        # doesn't fit the window due to large fontsize settings (on
                        # Apply, startup, or restore), because it's listening to both
                        # window size and filelist size; the filelist normally fits 
                        # the display and the scrollbar is set the same width - this
                        # is just for pathologically narrow displays or large fonts;

                        width: min(picklogitems.minimum_width, root.width)

                        BoxLayout:
                            id: picklogitems
                            orientation: 'vertical'              # was grid - cols: 1

                            # use less padding on PCs, to match phones [1.1.0]
                            spacing: 
                                # scale to density, not platform [1.1.0]
                                dp(2.5)
                                # an initial resizing, superseded [1.1.0]
                                # (8 if onandroid
                                # else (5 if onmacos else 4))    # was grid - spacing: (0, 8) # (h, v)                        

                            # do in .py for dynamic creation
                            # size_hint: (None, None)
                            # width:  self.minimum_width
                            # height: self.minimum_height
 
                BoxLayout:
                    size_hint_y: 0.08
                    orientation: 'horizontal'

                    ActivityButton:
                        text: 'TAIL'
                        id: logfiletail
                        on_release: root.do_logs_tail(picklogitems)

                    ToggleButton:
                        text: 'WATCH'
                        id: logfilewatch
                        background_color: 'green'
                        on_release: root.do_logs_watch(picklogitems)

                    ActivityButton:
                        text: 'OPEN'
                        id: logfileopen
                        on_release: root.do_logs_open(picklogitems)

                    ActivityButton:
                        text: 'EXPLORE'
                        id: logfileexplore
                        on_release: root.do_logs_explore()

                ScrollView:
                    size_hint_y: 0.50
                    # because TextInput itself would not scroll horizontally 
                    id: logfilescroll
                    do_scroll_x: True
                    do_scroll_y: True
                    effect_cls: 'ScrollEffect'             # don't overscroll/snapback

                    # Update [1.1.0]: there's no need to do the following
                    # on _both_ height and width - vscroll is never lost, 
                    # and just width catches both Android device rotations
                    # and PC width-only resizes; but it is required on width
                    # for both android and PCs, else hscroll is lost on all

                    # else hscroll lost on rotation (kivy bug)
                    # on_height: root.enable_logs_text_hscroll()    # extraneous [1.1.0]

                    # else hscroll lost on width-only PC resizes
                    on_width: root.enable_logs_text_hscroll()       # necessary+sufficient

                    TextInputNoSelect:
                        id: logfiletext
                        text: 'Logfile content...'
                        multiline: True
                        do_wrap: False
                        background_color: logsbackgroundcolor.text
                        foreground_color: logsforegroundcolor.text

                        # and selection+handles+bubbles disabled in .py
                        readonly: True    # else keyboard still covers (iff font?)

                        # default Roboto-Regular
                        # also DejaVuSans, Roboto-{Bold, Italic, BoldItalic}

                        font_name: 'RobotoMono-Regular' 

                        # to enable scrolling
                        size_hint: (None, None)
                        width: logfilescroll.width
                        height: max(self.minimum_height, logfilescroll.height)

                        # on each text change? NO - not required here
                        # run this only on logfile load + scroll height
                        # on_text: root.enable_logs_text_hscroll()

                        # unlike others, this is hscrolled and not wrapped
                        # CAUTION: the .py's hscroll size calc includes this padding
                        # padding: [24, 24]          # [h, v], mind the screen curves
                        padding: [dp(12), dp(12)]    # scale to screen density [1.1.0] 


#------------------------------------------------------------------------------
# CONFIG TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: configtab
            text: 'Config'

            BoxLayout:
                orientation: 'vertical'

                GridLayout:
                    size_hint_y: 0.40
                    cols: 2
                    # padding: [16, 16, 0, 0]        # [l, t, r, b]
                    padding: [dp(8), dp(8), 0, 0]    # scale to screen density [1.1.0]


                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: backupscheckbox
                        active: True
                        on_press: root.on_backups_clicked(self.active)    # not on_touch 

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Back up SYNC changes in TO, enable UNDO?'

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: skipcruftscheckbox
                        active: True

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Skip platform-unique items in FROM and TO?'

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: rootfolderselections
                        active: False
                        
                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Show root folder in choosers if possible?'

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: appfolderselections
                        active: True

                    LineLabel:
                        size_hint_x: 0.80
                        id: appfolderselectionslabel
                        text: 'Show app folder in choosers on Android?'

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: runactionasservice   # force off in .py for Android 8
                        active: True

                    LineLabel:
                        size_hint_x: 0.80
                        id: runactionasservicelabel
                        text: 'Run Main actions as services on Android?'
                
                    # [1.1.0] allow users to toggle keep-screen-on;
                    # for both Android (timeout) and PCs (sceensaver);
                    # also reordered toggles and tweaked sizes for fit;

                    ConfigCheckbox:
                        size_hint_x: 0.20
                        id: keepscreenon
                        active: True
                        on_press: root.on_keepscreenon_clicked(self.active)
                        
                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Keep the screen on while using this app?'

                GridLayout:
                    size_hint_y: 0.25
                    cols: 2
                    # padding: [16, 16, 0, 0]          # [l, t, r, b]
                    padding: [dp(8), dp(8), 0, 0]      # scale to screen density [1.1.0]

                    TextInput:
                        size_hint_x: 0.20
                        id: maxnumbackups
                        text: '25'
                        input_filter: 'int'

                        # height: self.minimum_height    # lines height, includes padding
                        # size_hint_y: 1
                        multiline: False
                        valign: 'middle'
                        # padding: [16, 16, 16, 0]          # [l, t, r, b], default [6]*4 (pixels)
                        padding: [dp(8), dp(8), dp(8), 0]   # scaled per density [1.1.0]

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Keep up to this many SYNC backups'

                    TextInput:
                        size_hint_x: 0.20
                        id: maxnumlogfiles
                        text: '30'
                        input_filter: 'int'

                        multiline: False
                        valign: 'middle'
                        # padding: [16, 16, 16, 0]
                        padding: [dp(8), dp(8), dp(8), 0]   # scaled per density [1.1.0]

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Keep up to this many run logfiles'

                    TextInput:
                        size_hint_x: 0.20
                        id: maxlogstailsize
                        text: '30'
                        input_filter: lambda s, u: s if s in '0123456789,' else '' 

                        multiline: False
                        valign: 'middle'
                        # padding: [16, 16, 16, 0]
                        padding: [dp(8), dp(8), dp(8), 0]   # scaled per density [1.1.0]

                    LineLabel:
                        size_hint_x: 0.80
                        text: 'Show up to this many bytes in Logs TAIL'

                GridLayout:
                    size_hint_y: 0.35
                    cols: 3
                    # padding: [16, 16, 0, 0]        # [l, t, r, b]
                    padding: [dp(8), dp(8), 0, 0]    # scaled per density [1.1.0]


                    TextInputNoSelect:
                        size_hint_x: 0.20
                        id: logsbackgroundcolor
                        text: 'black'
                        readonly: True
                        disabled: True

                        multiline: False
                        valign: 'middle'
                        # padding: [16, 16, 16, 0]
                        padding: [dp(8), dp(8), dp(8), 0]    # scaled per density [1.1.0]

                    ActivityButton:
                        size_hint_x: 0.20
                        text: 'Pick'
                        on_release: root.pickcolor(root.do_bg_color, 'Background')

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Logs-display background color'

                    TextInputNoSelect:
                        size_hint_x: 0.20
                        id: logsforegroundcolor
                        text: 'white'
                        readonly: True
                        disabled: True

                        multiline: False
                        valign: 'middle'
                        # padding: [16, 16, 16, 0]
                        padding: [dp(8), dp(8), dp(8), 0]    # scaled per density [1.1.0]

                    ActivityButton:
                        size_hint_x: 0.20
                        text: 'Pick'
                        on_release: root.pickcolor(root.do_fg_color, 'Foreground')

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Logs-display foreground color'

                    TextInput:
                        size_hint_x: 0.20
                        id: globalfontsize
                        text: '40'
                        input_filter: 'int'

                        multiline: False
                        valign: 'middle'
                        # padding: [16, 16, 16, 0]
                        padding: [dp(8), dp(8), dp(8), 0]    # scaled per density [1.1.0]

                    ActivityButton:
                        size_hint_x: 0.20
                        text: 'Apply'
                        on_release: root.set_font_size(globalfontsize.text)

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Global font size in pixels'

                    # chooser popup uses these on open, and sets them on changes;
                    # setting in config doesn't impact chooser until popup open;

                    # one might expect that pressing a radio-group button selects it 
                    # and clears the other, but one would be wrong in kivy - without 
                    # the on_press, it's possible to have none selected in a group.
                    # UPDATE: the scantly documented allow_no_selection does the 
                    # trick, but weirdly must be declared for each item in the group.
                    # also now used in Logs' file chooser too, but in .py, not .kv.

                    ToggleButton:
                        size_hint_x: 0.20
                        id: mainchoosericon
                        text:  'Icon'
                        group: 'mainchooserstyle'
                        state: 'down'
                        # on_press: self.state = 'down'; mainchooserlist.state = 'normal'
                        allow_no_selection: False

                    ToggleButton:
                        size_hint_x: 0.20
                        id: mainchooserlist
                        text:  'List'
                        group: 'mainchooserstyle'
                        # on_press: self.state = 'down'; mainchoosericon.state = 'normal'
                        allow_no_selection: False

                    LineLabel:
                        size_hint_x: 0.60
                        text: 'Style of Main folder chooser'


                # Tab remembers, but saves for non-run items here only on 
                # manual Save tap, not auto on Main run.  Save here saves  
                # just this tab's settings, Main run saves Main paths only.

                BoxLayout:
                    size_hint_y: 0.10
                    # padding: [0, 16, 0, 0]     # [l, t, r, b]
                    padding: [0, dp(8), 0, 0]    # scaled per density [1.1.0]
                    height: buttonheight
                    orientation: 'horizontal'

                    DialogButton:
                        text: 'Save Changes'
                        on_release: root.save_persisted_settings_Config()

                    DialogButton:
                        text: 'Restore Defaults'
                        on_release: root.reset_persisted_settings_Config()


#------------------------------------------------------------------------------
# HELP TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: helptab
            text: 'Help'

            BoxLayout:
                orientation: 'vertical'

                #SectionLabel:
                #    size_hint_y: 0.10
                #    text: 'Usage Essentials'

                ScrollView:
                    # because TextInput itself would not scroll horizontally 
                    size_hint_y: 0.70
                    size_hint_x: 1.0
                    id: helptextscroll
                    do_scroll_x: False
                    do_scroll_y: True
                    effect_cls: 'ScrollEffect'             # don't overscroll/snapback

                    # but this text is wrapped, and does not hscroll
                    # else hscroll lost on rotation (kivy bug)
                    # on_height: root.enable_logs_text_hscroll()

                    TextInputNoSelect:
                        id: helptext
                        text: root.help_message
                        multiline: True
                        readonly: True
                        allow_copy: False  # else odd select-all popup 
                        do_wrap: True
                        foreground_color: 'white'
                        background_color: '#0f2f2f'        # 'black' is hard to read
                        font_name: 'RobotoMono-Regular'    # default is hard to read
                        # padding: [42, 16]                # [h, v], mind the screen curves
                        padding: [dp(18), dp(8)]           # scale to screen density [1.1.0]

                        # to enable scrolling
                        size_hint: (None, None)
                        width: helptextscroll.width
                        height: max(self.minimum_height, helptextscroll.height)

                GridLayout:
                    cols: 2
                    size_hint_y: 0.20

                    #SectionLabel:                    # iff BoxLayout, former coding
                    #    size_hint_y: 0.10
                    #    text: 'Main Resources'

                    DialogButton:
                        text: 'User Guide'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/User-Guide.html')

                    DialogButton:
                        text: 'PC Downloads'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/App-Packages.html')

                    #SectionLabel:
                    #    size_hint_y: 0.10
                    #    text: 'Web Pages'

                    DialogButton:
                        text: 'App Website'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/index.html')

                    DialogButton:
                        text: 'Mergeall Website'
                        on_release: root.open_web_page('https://learning-python.com/mergeall.html')

                    #SectionLabel:
                    #    size_hint_y: 0.10
                    #    text: 'Etcetera'

                    DialogButton:
                        text: 'Explore TO Backups'
                        on_release: root.do_explore_backups()

                    DialogButton:
                        text: 'Mergeall Docs'
                        on_release: root.open_web_page('https://learning-python.com/mergeall-products/unzipped/UserGuide.html')


#------------------------------------------------------------------------------
# ABOUT TAB
#------------------------------------------------------------------------------


        TabbedPanelItem:
            id: abouttab
            text: 'About'

            BoxLayout:
                orientation: 'vertical'

                #SectionLabel:
                #    text: 'About This App'
                #    size_hint_y: 0.10

                ScrollView:
                    # because TextInput itself would not scroll horizontally 
                    size_hint_y: 0.95
                    size_hint_x: 1.0
                    id: abouttextscroll
                    do_scroll_x: False
                    do_scroll_y: True
                    effect_cls: 'ScrollEffect'             # don't overscroll/snapback

                    # but this text is wrapped, and does not hscroll
                    # else hscroll lost on rotation (kivy bug)
                    # on_height: root.enable_logs_text_hscroll()

                    TextInputNoSelect:
                        text: root.about_message
                        multiline: True
                        readonly: True
                        allow_copy: False  # else odd select-all popup 
                        do_wrap: True
                        foreground_color: 'white'
                        background_color: 'black'
                        # padding: [42, 16]                # [h, v], mind screen curves
                        padding: [dp(18), dp(8)]           # scale to density [1.1.0]
                        font_name: 'RobotoMono-Regular'    # default nearly unreadable

                        # to enable scrolling
                        size_hint: (None, None)
                        width: abouttextscroll.width
                        height: max(self.minimum_height, abouttextscroll.height)

                #SectionLabel:
                #    text: 'Resources'
                #    size_hint_y: 0.08

                BoxLayout:
                    size_hint_y: 0.08
                    height: buttonheight

                    DialogButton:
                        text: 'Play Store Info'
                        on_release: root.open_web_page('https://play.google.com/store/apps/details?id=com.quixotely.usbsync')

                    DialogButton
                        text: 'PC Downloads'
                        on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/App-Packages.html')


#------------------------------------------------------------------------------
# MAIN-TAB FOLDER-CHOOSER POPUP
#------------------------------------------------------------------------------


<MainPathPickDialog>:

    # File chooser modal popup, for Main tab's FROM/TO paths
    # Pass oncanel, onpick, etc, and fill in storage buttons at top

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        ScrollView:

            # scroll horizontally for large fonts or names, and many devices
            # nit: may be moot on phones (>1 removable?), pc windows can expand
            # but usb hubs _do_ work on android, and large fonts are possible
            # nit: fixed-size child seems to preclude expanding into hspace,
            # but kivy doesn't have css's minwidth equiv (scroll at <= this);

            # that is, kivy seems to dictate scroll or expand, but not both;
            # in principle, might bind scrollbar size to a callback that calcs
            # normal texture-based size of buttons and hence boxlayout, and 
            # distributes excess evenly among buttons if scrollbar is wider;
            # but new layout seems better: space at end implies more options;  

            size_hint: (1, None)
            id: devicebuttonsscroll
            do_scroll_x: True              # auto opens at top of text
            effect_cls: 'ScrollEffect'     # don't overscroll/snapback

            height: tallbuttonheight       # a wee bit taller for taps
            width: root.width
        
            BoxLayout:

                # the .py adds variable number ToggleButton childen here;
                # note: dynamic is a whole lot harder with scrolling;
                # also catches tap of already down toggle button: goto root
                # note: catching down taps adds a full order of complexity,
                # because normal button events don't fire: see on_touch_down;

                id: devicebuttons
                orientation: 'horizontal'

                # all sizing/scrolling now done in .py do_main_path()
                # size_hint: (None, None)
                # height: devicebuttonsscroll.height
                # width: max(self.minimum_width, devicebuttonsscroll.width)
                # width: self.minimum_width  <= avoid triggering scroll too soon

                # UPDATE: boxlayout sizing is now here - simpler than in .py,
                # and works sans buttons (though buttons must be sized in .py
                # when built); in kivy, the docs are so thin that you have to 
                # do it the wrong way first - and a few times; please improve!

                size_hint: (None, 1)
                width: self.minimum_width     # set to min-width when computed

        BoxLayout:
            size_hint_y: None
            height: tallbuttonheight          # rarely used, but match others
            orientation: 'horizontal'

            # these are not radio buttons (group) because the down shading 
            # would be too distracting here;  their counterparts in the 
            # Configs tab are radios (and set by the on_releases below) 

            Button:
                text: 'Icon View'
                on_release: filechooser.view_mode = 'icon'; root.onviewmode('icon')

            Button:
                text: 'List View'
                on_release: filechooser.view_mode = 'list'; root.onviewmode('list')
 
        PathDisplayNoSelect:
            id: pathname
            text: root.pickstart

            # and selection+handles+bubbles disabled in .py
            # PUNT: True disables scrolls, ScrollView hurts, and inputs goto Main 
            # readonly: True    # because changes have no effect on chooser

            size_hint: (1, None)
            height: self.minimum_height

            # width: max(self._get_row_width(0), pathnamescroll.width)
            # width: max(len(pathname.text), pathnamescroll.width)

        FileChooser:
            id: filechooser
            path: root.pickstart       # reset on device change (creation arg property)
            show_hidden: False

            # per source code, mode names 'list' and 'icon' don't exist until 
            # after choosers below are built, but no code allowed after here (!$#);
            # set this only in the .py after popup built, and by buttons above
 
            # view_mode: root.viewmode   # set in .py by id, based on Config tab radio btns

            # limit navigation to the start|reset folder and below
            # removes '..' to avoid uncaught excs for folders sans access

            rootpath: root.rootpath     # reset on device change (creation arg property)
 
            # filter out files: show+select dirs only 
            # filters filters _in_, filter_dirs: False adds all dirs _in_
            # or filters: [lambda p, f: os.path.isdir(os.path.join(p, f))]
            # sort_func(x, y): default=folders first, uppercase first (macOS)

            dirselect: True                           # allow dir slections
            filter_dirs: False                        # add back all dirs post filters
            filters: [(lambda path, file: False)]     # remove all files+dirs at first

            # update text field on taps, fetch from there

            on_selection: pathname.text = self.selection and self.selection[0] or ''

            # view_mode: 'list'
            # choose one by magic buttons above, icons first per order;
            # font size in the first is fixed by a template copy/mod ahead here;
            # label "Size" in the second is nuked by children-lists crawl in .py;

            FileChooserIconLayout
                id: filechoosericonlayout

            FileChooserListLayout
                id: filechooserlistlayout    # for 'Size' nuke

        BoxLayout:
            size_hint_y: None
            height: tallbuttonheight      # no need for tall at bottom, but match

            DialogButton:
                text: 'Cancel'
                on_release: root.oncancel(root)    # validate root=content instance

            DialogButton:
                text: 'Pick'
                on_release: root.onpick(pathname.text, root)


#------------------------------------------------------------------------------
# INFO POPUP
#------------------------------------------------------------------------------


<InfoDialog>:
  
    # General modal info popup: pass message, oncancel
    # Toast alternative (must also declare class in .py)

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # because the TextInput itself would not scroll vertically;
        # text normally fits, and this uses default [do_wrap: True],
        # but add scrollview for large text or fontsize - textinput
        # won't vscroll by itself; scrolling is also much more likely 
        # to be needed in landscape mode on phones; caveat: this code 
        # is repeated redundantly: root.message won't work otherwise?

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            TextInputNoSelect:
                text: root.message
                multiline: True
                readonly: True               # True: no keybrd on tap [1.1.0]
                foreground_color: 'white'
                background_color: 'black'
                # padding: [24, 24]          # [h, v], mind the screen curves
                padding: [dp(10), dp(10)]    # scale to screen density [1.1.0]

                # to enable scrolling
                size_hint: (None, None)
                width: textscroll.width
                height: max(self.minimum_height, textscroll.height)

        BoxLayout:
            size_hint_y: None
            height: buttonheight
            DialogButton:
                text: 'Okay'
                on_release: root.oncancel(root)    # validate root=content instance


#------------------------------------------------------------------------------
# CONFIRM POPUP
#------------------------------------------------------------------------------


<ConfirmDialog>:

    # General modal verify popup: pass message, onyes, onno

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # see scrolling docs at InfoDialog above

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            TextInputNoSelect:
                text: root.message
                multiline: True
                readonly: True
                foreground_color: 'white'
                background_color: 'black'
                # padding: [24, 24]          # [h, v], mind the screen curves?
                padding: [dp(10), dp(10)]    # scale to screen density [1.1.0]

                # to enable scrolling
                size_hint: (None, None)
                width: textscroll.width
                height: max(self.minimum_height, textscroll.height)

        BoxLayout:
            size_hint_y: None
            height: buttonheight

            DialogButton:
                text: 'No'
                on_release: root.onno(root)    # validate root=content instance

            DialogButton:
                text: 'Yes'
                on_release: root.onyes(root)


#------------------------------------------------------------------------------
# COLOR POPUP
#------------------------------------------------------------------------------


<ColorPickDialog>:

    # Prebuilt content: pass oncancel, onpick

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        ColorPicker
            id: colorpicker

        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: None
            height: buttonheight

            DialogButton:
                text: 'Cancel'
                on_release: root.oncancel(root)    # validate root=content instance

            DialogButton:
                text: 'Pick'
                on_release: root.onpick(colorpicker.color, colorpicker.hex_color, root)


#------------------------------------------------------------------------------
# TRIAL-ENDED POPUP
#------------------------------------------------------------------------------


<TrialEndedDialog>:

    # After N opens in trial app: pass message, oncancel
    # This might be subverted, but it's not worth the struggles.

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # see scrolling docs at InfoDialog above

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            TextInputNoSelect:
                text: root.message
                multiline: True
                readonly: True
                foreground_color: 'white'
                background_color: 'black'
                # padding: [24, 24]          # [h, v], mind the screen curves?
                padding: [dp(10), dp(10)]    # scale to screen density [1.1.0]

                # to enable scrolling
                size_hint: (None, None)
                width: textscroll.width
                height: max(self.minimum_height, textscroll.height)

        BoxLayout:
            size_hint_y: None
            height: buttonheight

            DialogButton:
                # mind the app.root.method
                text: 'Get Full App'
                on_release: app.root.open_web_page('https://play.google.com/store/apps/details?id=com.quixotely.usbsync')

            DialogButton:
                text: 'Exit Trial App'
                on_release: root.oncancel()    # no root=content validate: stopping (moot)


#------------------------------------------------------------------------------
# NAME-MODE POPUP
#------------------------------------------------------------------------------


<ConfirmNameDialog>:
  
    # Confirm NAME action by a report|update mode choice
    # Pass message, onrunreport, onrunupdate, oncancel

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'

        # see scrolling docs at InfoDialog above

        ScrollView:
            size_hint: (1.0, 1.0)
            id: textscroll
            do_scroll_y: True                # auto opens at top of text
            effect_cls: 'ScrollEffect'       # don't overscroll/snapback

            TextInputNoSelect:
                text: root.message
                multiline: True
                readonly: True               # True: no keybrd on tap [1.1.0]
                foreground_color: 'white'
                background_color: 'black'
                # padding: [24, 24]          # [h, v], mind the screen curves?
                padding: [dp(10), dp(10)]    # scale to screen density [1.1.0]

                # to enable scrolling
                size_hint: (None, None)
                width: textscroll.width
                height: max(self.minimum_height, textscroll.height)

        BoxLayout:
            size_hint_y: None
            height: buttonheight

            DialogButton:
                text: 'Cancel'
                on_release: root.oncancel(root)    # validate root=content instance

            DialogButton:
                text: 'Run Report'
                on_release: root.onrunreport(root)

            DialogButton:
                text: 'Run Update'
                on_release: root.onrunupdate(root)


#------------------------------------------------------------------------------
# KIVY FILECHOOSER BUG WORKAROUND (for which I am deeply ashamed...)
#------------------------------------------------------------------------------


# The filechooser's icon view clips text vertically when fonts are 
# set larger than usual by the app or user settings.  This may be rare
# on phones, but is common elsewhere (e.g., Linux + high-res screens).
#
# There seems no way to fix this apart from the following.  Kivy loads
# its own "style.kv" file first from its intall's data/ folder, before 
# loading user .kv files (like this) or running Builder calls in the .py.
# The style.kv file defines styles for all Kivy builtin widgets, including
# the filechooser, which uses both list+icon views and a generic controller.
#
# To mod, copy the FileIconEntry template code in style.kv to here, and 
# mod the filename label to use size settings that make it as tall as needed
# for the text's 'texture' (font/scaling) size.  See the '##[ML' mods below.
# This avoids changing the built-in style.kv, but depends on its contents.
# Note that Label has texture, but TextInput has minumum_height (confusingy).
#
# This relies perilously on Kivy implementation details and is subpar, 
# but there is no other known option, and filename clips are a quite bad
# user eperience (along with the ~dozen other Kivy bugs worked around).
# Why this sizing scheme isn't used in the filechooser is anybody's guess.
#
# In Kivy's defense, there usually ARE ways to work around such things 
# because it's all Python down to the graphics layer.  But its user base 
# might expand with a few crucial docs and fixes; see also orange dots 
# left by double clicks on PCs; black boxes in TextInput for long lines;
# lag for loading text into TextInputs; hangs on app close on Android;
# builder battles; the .py's most horrible hack ever to kill the stupid
# "Size" label in FileChooserListLayout which is useless for folders and
# showed up just as an "e" for some phones and/or font sizes; plus all 
# the other bugs zapped here (and in the .py).  (Yes, I'm bitter...)
#
# Hindsight: this may have been do-able with children-list crawls like 
# the list-layout "Size" fix in the .py, instead of this code copy+mod.
# OTOH, this works as is, and children lists seem just as brittle.
# This, and more, should really be exposed for mode via ids or API.
#
# References:
# https://github.com/kivy/kivy/blob/master/kivy/uix/filechooser.py
# https://github.com/kivy/kivy/blob/master/kivy/data/style.kv
# https://kivy.org/doc/stable/api-kivy.uix.label.html#sizing-and-text-content


# REDEF BUILT-IN WIDGET'S STYLE


[FileIconEntry@Widget]:
    locked: False
    path: ctx.path
    selected: self.path in ctx.controller().selection
    size_hint: None, None

    on_touch_down: self.collide_point(*args[1].pos) and ctx.controller().entry_touched(self, args[1])
    on_touch_up: self.collide_point(*args[1].pos) and ctx.controller().entry_released(self, args[1])
    size: '100dp', '100dp'

    canvas:
        Color:
            rgba: 1, 1, 1, 1 if self.selected else 0
            ##[ML test] rgba: 99, 99, 99, 99 if self.selected else 0
        BorderImage:
            border: 8, 8, 8, 8
            pos: root.pos
            size: root.size
            source: 'atlas://data/images/defaulttheme/filechooser_selected'

    Image:
        size: '48dp', '48dp'
        source: 'atlas://data/images/defaulttheme/filechooser_%s' % ('folder' if ctx.isdir else 'file')
        pos: root.x + dp(24), root.y + dp(40)
    Label:
        text: ctx.name
        font_name: ctx.controller().font_name
        ##[ML mod] text_size: (root.width, self.height)
        halign: 'center'
        shorten: True
        ##[ML mod] size: '100dp', '16dp'
        pos: root.x, root.y + dp(16)

        ##[ML add]
        text_size: root.width, None
        size: self.texture_size

    Label:
        text: '{}'.format(ctx.get_nice_size())
        font_name: ctx.controller().font_name
        font_size: '11sp'
        color: .8, .8, .8, 1
        size: '100dp', '16sp'
        pos: root.pos
        halign: 'center'




#******************************************************************************
# ONLY DEFUNCT CODE FOLLOWS
#******************************************************************************


#------------------------------------------------------------------------------
# NO LONGER USED: now embedded in Logs tab...
#------------------------------------------------------------------------------


#<LogfilePickDialog>:
#
#    # File chooser modal popup, for Logs tab's logfile path
#    # Use popup so always has surrent folder contents.
#    # Only show logfiles: no folder navigation required.
#
#    BoxLayout:
#        size: root.size
#        pos: root.pos
#        orientation: 'vertical'
#       
#        PathDisplay:
#            id: pathname
#            text: root.pickstart
#            readonly: True
#
#        FileChooser:
#            id: filechooser
#            path: root.pickstart
#            show_hidden: False
#            rootpath: root.pickstart    # drop '..' in picker
#
#            # show+select logfiles only, no dirs, listview only
#            dirselect: False
#            filter_dirs: True
#            filters: ['*date*-time*--*.txt']
#
#            on_selection: pathname.text = self.selection and self.selection[0] or ''
#            FileChooserListLayout
#
#        BoxLayout:
#            size_hint_y: None
#            height: buttonheight
#
#            DialogButton:
#                text: 'Cancel'
#                on_release: root.oncancel(root)
#
#            DialogButton:
#                text: 'Pick'
#                on_release: root.onpick(pathname.text, root)


#------------------------------------------------------------------------------
# NO LONGER USED: old configs and config-tab cruft, tmi...
#------------------------------------------------------------------------------


#SectionLabel:
#    size_hint_y: 0.10
#    text: 'Run Options'
#
#SectionLabel:
#    size_hint_y: 0.10
#    text: 'Appearance'
#
#Button:
#    text: 'Tip'
#    on_release: 
#        root.info_message('SYNC backups are in TO/__bkp__',
#        usetoast=True, longtime=True)


#Just always log, but make the folder self-cleanding to avoid space bloat
#BoxLayout:
#    orientation: 'horizontal'
#    CheckBox:
#        id: logfilescheckbox
#        active: True
#    Label:
#        text: 'Save Logfiles for Main-Tab Actions?'
                

#Show on Logs tab, as uneditable/unselectable text; else tmi...
#BoxLayout:
#    orientation: 'horizontal'
#    Label:
#        text: 'Logfiles folder:'
#    TextInput:
#        id: logfilesfolder


#Just test os.listdir and auto display ROOT if have access to "/"
#BoxLayout:
#    id: rootfolderbox
#    orientation: 'horizontal'
#    CheckBox:
#        id: rootfolderselections
#        active: False
#    Label:
#        text: 'Allow Root Folder Selections'


#Way too much: skip list + keep list, case, pattern syntax, module mods
#BoxLayout:
#    orientation: 'horizontal'
#    Label:
#        text: 'Cruft Names Pattern:'
#    TextInput:
#        id: cruftnamespattern


#Now a Main action: prestep too odd to wedge into thread-launch model 
#Label:
#    text: ''
#
#BoxLayout:
#    id: fixfilenamesbox
#    orientation: 'horizontal'
#    CheckBox:
#        id: fixfilenamescheckbox
#        active: True
#    Label:
#        text: 'Fix Nonportable Filenames in FROM?'