#------------------------------------------------------------------------------ # 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 here never worked... : 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 # yes, it works... if bound to app.dyn_font_size, not self|root : font_size: app.dynamic_font_size # applied to every Widget subclass instance (!) : 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 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?) background_color: 'blue' background_color: 'green' background_color: 'red' # not used here: made in .py background_color: 'navy' # now def in .py for conditional color # # # [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. 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 #------------------------------------------------------------------------------ : # 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 #------------------------------------------------------------------------------ : # 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 #------------------------------------------------------------------------------ : # 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 #------------------------------------------------------------------------------ : # 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 #------------------------------------------------------------------------------ : # 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 #------------------------------------------------------------------------------ : # 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... #------------------------------------------------------------------------------ #: # # # 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?'