#------------------------------------------------------------------------------ # Part of PC-Phone USB Sync. # Define the app's GUI, in a way that's integrated with the .py code. # # Copyright © 2023-2026 quixotely.com. All rights reserved. # License: see ./main.py and ./terms-of-use.txt # # Disclaimer: like main.py, this file was not originally meant for # publication, so please pardon any rough edges here. PPUS was a # bit of a Kivy starter project, and it still shows in spots. # # OVERVIEW # # This file uses the Kivy language, which is supposed to separate layout # from code and use a declarative style but seems a bit too magical # at times. The relationship between code here and in the .py is not # very well documented, if at all (hint: knowing how classes in the # .py and .kv are added to the Factory helps; see Frigcal's code docs). # # Moreover, the Kivy language doesn't address parts that must still be # built in the .py dynamically (e.g., the Main chooser's storage buttons # and the entire month screen in Frigcal). And in hindsight, this code # may be better manually loaded from a file and passed to the builder # in the .py, to avoid an implicit naming convention and some thorny # location issues for Kivy executables built with PyInstaller. # # On top of this, Kivy has missing bits (including a text widget that # can handle non-trivial text and labels that don't require manual text # splits to avoid blackouts); a non-orthogonal sizing/layout scheme # that smacks too much of CSS (including the endless tweaking - and # cursing); and a woeful lack of useful docs in general (expect to # scour Kivy's code to grok even fundamental things). The sum makes # Kivy seem more a GUI lib construction kit than a finished GUI lib. # # OTOH, Kivy does work with the appropriate amount of determination, # as evidenced by this app and its Frigcal sibling at quixotely.com. # But you should not expect it to be as friendly as Python's tkinter. # # For more Kivy editorial, see also the filechooser workaround ahead. # It's a great system and very pliable because it's all Python/Cython # down to the drawing, but it badly needs some finishing tweaks and docs. # Though unused in this app (see Frigcal), KivyMD improves cosmetics, # but it also adds a layer to the stack that changes more radically # than it should and seems just as thinly documented as Kivy. # # Time will hopefully improve the Kivy/KivyMD story. Beeware/Toga still # isn't there yet, and life is too short to use Java or Kotlin on Android. #------------------------------------------------------------------------------ #:kivy 2.1.0 # for refs in py code here #:import os os #:import sys sys #:import Clock kivy.clock.Clock #:import Window kivy.core.window.Window # [1.4] do in .py's on_width call back instead, for startup + rotations + multiwindow # [1.4] now moot - display_cutout abandoned for insets callback, all done in .py ##:import get_height_of_bar android.display_cutout.get_height_of_bar #------------------------------------------------------------------------------ # GLOBALS #------------------------------------------------------------------------------ # for phone/pc-specific tweaks (see also top of main.py) #:set onandroid hasattr(sys, 'getandroidapilevel') #:set onmacos sys.platform.startswith('darwin') #:set onwindows sys.platform.startswith('win') #:set onlinux sys.platform.startswith('linux') and not onandroid # Linux ONLY # [1.1] 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()); # use density-scaled pixels, textheight no longer used [1.1] #:set buttonheight dp(40) #:set tallbuttonheight buttonheight ## absolute pixels: not ideal, but they work (if size_hint_y=None) ##:set textheight 90 ##:set buttonheight 80 ## phone increment; now moot [1.1] ##:set buttonheight (buttonheight + (4 if onandroid else 0)) ## easier taps of crucials; now moot [1.1] ##:set tallbuttonheight (buttonheight + 16 if onandroid else buttonheight) #------------------------------------------------------------------------------ # CLASSES #------------------------------------------------------------------------------ # CAUTION: dynamic class names cannot be same as a Kivy class name. # This is why an here silently never worked... # Classes coded here and in the .py wind up in the Kivy Factory object. # Folder paths, extended ahead : 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 height: self.minimum_height # lines height, includes padding # [1.5] padding is weirdly brittle - don't change, else breaks valign of pathnames! #padding: [16, 6, 16, 6] # [l, t, r, b], default [6]*4 (pixels) padding: [dp(7), dp(3), dp(7), dp(3)] # scale to screen density, +t/b -l/r [1.1] # [1.5] valign properly, per current size: Main tab more usable in slab landscape padding_y: (self.height - self.line_height) / 2 # l+r still from padding (deprecated) # [1.1] do grab+move scrolling on PCs (phones already do swipe scroll); # holding longer selects text instead; a ScrollView might help too (tbd, # but horizontal text scrolls are odd - see .py enable_logs_text_hscroll()); scroll_from_swipe: True # default is False for PCs, True for phones # always show start of text on left edge of widget, until scrolled; # now done for initial open in main.py's startup_gui_and_config_tweaks(), # via text.cursor = (0, 0); halign and scroll_x here had no effect (why?); #valign: 'middle' # moot? ([1.5]: yes, and nonexistent in TI - use padding_y) #halign: 'left' # no effect? #scroll_x: 0 # no effect? # Multiple inheritance # Yes, it works, and uses mro (not simple dflr) if it's mapped to Python classes (tbd) # Global fontsize for all widgets # Yes, it works, if bound to app.dyn_font_size, not self|root (but KivyMD may override) : font_size: app.dynamic_font_size # applied to every Widget subclass instance (!) # Custom labels : # above sections in tabs 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 : # to the right of other widgets text_size: self.size halign: 'left' size_hint_y: 1 valign: 'middle' # valign works on Label, but not TextInput # padding: [32, 32] # [h, v] - but v made moot by size_hint_y! padding: [dp(16), dp(16)] # scale to screen density [1.1] # Color schemes - could be user configs someday (tbd, tmi?) background_color: 'blue' background_color: 'green' background_color: 'red' # not used here: made in .py # [1.5] Redo dialog+tab footer buttons to have a uniform fixed size # and not change with window resizes - else may become too small. # This replaces a jumble of BoxLayouts that diverged over time. # use density-scaled pixels for varying displays (defined above) ##:set buttonheight dp(40) : # one-row button grid, fixed size rows: 1 size_hint_y: None height: buttonheight # and add N DialogButton, cols = N : # two-row button grid, fixed size rows: 2 size_hint_y: None height: buttonheight * 2 # and add N DialogButton, cols = ceil(N / 2) background_color: 'navy' # [1.5] Enable markup in text displays, and split large text across N # labels for Help and About - a workaround for Label text-size limits, # and required to avoid blank/blacked-out labels for nontrivial text. # Unlike KivyMD, Kivy Label has no background color sans Canvas ops. # Text requires massive amounts of memory: see Recycleview fail ahead. : # [1.5] Custom container for Label parts of Help and About screens. # The .py adds N LabelTextDisplay instances based on text size. orientation: 'vertical' # stack children on top of each other size_hint_y: None # plain Kivy (not KivyMD) version height: self.minimum_height #adaptive_height: True # KivyMD: adjust height per childrens' content : # [1.5] UNUSED experiment that was abandoned before it worked. # Set backround_color and color as construction arguments. # Mix in to another widget class to enable bg colorization; # canvas.before ops are run before widget is drawn on canvas. # PUNT: did not work as coded, no time for struggles, and the # Kivy default dark-gray background is better for consistency. background_color: 1, 1, 1, 1 # default if not overridden by subclass canvas.before: Color: rgba: root.background_color Rectangle: size: self.size pos: self.pos : # punt: "+BackgroundColor>:" # [1.5] For showing text in labels with Kivy markup. This is used as # is in dialogs, and larger Help/About text is spread across N of these # in the .py, else nontrivially sized text may be blacked out (empty). # Kivy/KivyMD TextInput/Field handle larger text, but have no markup. # KivyMD: must bind font_size after create in .py else auto-set per MD. # Now also used for dialogs like info_message() to enable colors and # custom [H] headers implemented by this app. This sets bg to default # gray, and text is not split for dialogs: assumed to be short enough. # This replaces prior highly-customized TextInputNoSelect appearances. # Note that Kivy Label wraps if text_size or embedded \n characters; per # its code, KivyMD MDLabel autowraps if no adaptive_size or adaptive_height. # NIT: Label uses different line-wrap rules than Text - it splits on just # spaces and not periods. This works fine for prose but can be subpar for # pathnames. Label splits use split_str in kivy.core.text's LabelBase, # which defaults to ' ' and can be changed, but Text doesn't split on / or # \ either and uses . unevenly, and this is a substantial can of worms... markup: True # use '[]' for bold, italic, under, color, etc. text_size: self.width, None # enables word wrapping size_hint_y: None # use plain kivy version, no kivymd adaptive_* size: self.texture_size # no min_height (and why is sizing so wonky?) halign: 'left' padding: [dp(10), 0] # just [h, v] in Kivy 2.1.0, not [l, t, r, b], was abs 8 # these work only in Text... # harmless but pointless in Label: kivy silently ignores! # multiline: True # for Text, enable N line plus line wraps on words # do_wrap: True # tip: Label also splits text on embedded \n # oddments... # allow_copy: False # irrelevant for Label, for TextInput = no edit bubbles # theme_text_color: 'Primary' # kivymd only: also foreground_color, background_color # the usual sizing song and dance... # adaptive_height: True # kivymd only: enables scrolling = [size_hint_y: None] + [height: self.minimum_height] # size_hint: 1, 1 # relative to parent size: fill available space, default # size_hint: (None, None) # enable provided size (and its many variants) # height: self.minimum_height # for TextInput, not label (inconsistently) # height: self.texture_size[1] # non-orthogonal like CSS (same persistence required) # CATCH: Kivy 2.1.0 allows just two values for Label padding, [h, v], so we # must handle top xor bottom padding specially. One-label cases (dialogs) # can pad both, but N-label split-text cases (Help/About) must add spacers # above and below the labels container. Kivy 2.3.1 allows [l, t, r, b] # for padding, which allows Frigcal to handle Help/About padding in .py # code, but 2.3.1 is too risky to use for PPUS given PPUS's many brittle # workarounds for Kivy bugs: disable in the .py, and pad here instead. # See CATCH in the .py's set_split_label_text() for expanded coverage. # Help/About : size_hint_y: None height: dp(10) # vertical spacer, 10 density-scaled pixels high # dialogs : padding_y: dp(10) # pad both top and bottom in kivy 2.1.0 padding_x: dp(8.0) # match dialog header's indent, dialog already padded # Now defined 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 WIDGET #------------------------------------------------------------------------------ Main: # Root widget (instance), maps to .py's class Main via Factory # available in .py # run_output: runoutput # ref as this or self.ids. or self.ids['id'] from_Path: frompath to_path: topath # [1.4] negate android 15 edge-to-edge display by padding window for insets # now done in .py's on_width calback here for startup + roatations + multiwindow # padding: 0, get_height_of_bar('status'), 0, get_height_of_bar('navigation'); # PUNT: failed for small-screen landscape where navbar is on left or right; # see .py for later insets-based solution that is a more complete solution; ##on_width: root.neuter_android15_edge_to_edge() ##on_width: root.force_redraw() 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]: 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: # layout container required in each tab 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] 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] 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] 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: id: logslabel text: 'View Run Logfiles' #size_hint_y: 0.05 # [1.5] size+padding for splitter size_hint_y: None size: self.texture_size padding_y: dp(7) Label: id: logspath text: root.logfile_path text_size: root.width, None halign: 'center' #size_hint_y: 0.05 # [1.5] size+padding for splitter size_hint_y: None size: self.texture_size padding_y: dp(6) # 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 kivy 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] spacing: # scale to density, not platform [1.1] dp(2.5) # an initial resizing, superseded [1.1] # (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 # [1.5] Make Logs' middle green action-buttons row fixed sized too? # PROS: avoid growth as window expands so add as much log-viewer space # as possible. Won't shrink on window contract, but too small unusable. # # CONS: this reveals only another line or two, because the logfiles # list at the top also expands; it differs from the Main and Config # tabs, whose buttons do grow+shrink with window; and Android landscape # and split/popup multi-window modes argue for allowing row to shrink. # Logs' logfiles-list cells do not grow+shrink, but they're unmoddable. # # USED: this became fixed in the end, for the new Splitter - see below. # ALSO: changed WATCH to trigger by on_press instead of on_release, else # no callback sent and button is stuck on and requirestap to clear if # swipe over button - which happened in 1.4 too but is much more likely # in 1.5 because the slider is just below WATCH. Other buttons still # require a release over the button to trigger: okay - not toggles. #BoxLayout: # size_hint_y: 0.08 # orientation: 'horizontal' DialogButtonsRows1: # [1.5] fixed-size row, not % relative (for splitter) id: logsactionsrow ActivityButton: text: 'TAIL' id: logfiletail on_release: root.do_logs_tail(picklogitems) ToggleButton: text: 'WATCH' id: logfilewatch background_color: 'green' on_press: root.do_logs_watch(picklogitems) # [1.5] not on_release, else stuck on if swipe ActivityButton: text: 'OPEN' id: logfileopen on_release: root.do_logs_open(picklogitems) ActivityButton: text: 'EXPLORE' id: logfileexplore on_release: root.do_logs_explore() # [1.5] Wrap file-view area in a Splitter so users can resize it. # This may avoid the extra steps of OPEN in some cases, and allows # more filenames to be viewed without scrolling. Both file-view and # filename areas still scroll as before, but the green action-buttons # row is now fixed size so only the other two respond to the splitter, # and space has been hardcoded around the top label and filepath. # To avoid running widgets over top tabs or cruching the label/path, # must calculate and limit max_size of file-view area from Window. # On Android, max_size also requires deducting edge-to-edge padding; # on Windows+Linux, it uses less fudge so it fully covers files list # app.android_e2e_window_height_pad: property, so mods update max_size; # Can split long property values with indent + '\', despite Kivy docs! # Python (a if x else b if y else c) == (a if x else (b if y else c)). # The splitter bar supports both drags and double taps for resizes. # Its default pt(10) (a.k.a. '10pt' in the source) was nearly unusable # on Linux (only), and just a hair too narrow on Windows touch screens. Splitter: id: logssplitter sizable_from: 'top' # resizable from the top side of this widget # minimum height: smallest that logview can be at display bottom min_size: dp(96) # maximum height: else top tabs overlayed or label+path crunched #max_size: Window.height * .80 # more accurate: stop at bottom of label+path, deducts e-2-e padding and fudge max_size: Window.height - \ (toptabs.tab_height + \ logsactionsrow.height + logslabel.height + logspath.height + \ app.android_e2e_window_height_pad + \ (8 if onwindows else 6 if onlinux else 12)) size_hint_y: 0.50 # initial percent of window rescale_with_parent: True # retain parent % when parent resized (moot w/max?) # size of the divider bar itself, defaults to '10pt' (i.e., pt(10)) strip_size: pt(14) if onlinux else \ pt(10) if onandroid else \ pt(12) # default: Linux unusable, Windows+macOS narrowish, Android ok ScrollView: # only one child allowed in splitter # because TextInput itself would not scroll horizontally id: logfilescroll #size_hint_y: 0.50 do_scroll_x: True do_scroll_y: True effect_cls: 'ScrollEffect' # don't overscroll/snapback # Update [1.1]: 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] # 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 # [1.5] see also kivy's use_handles, use_bubbles 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] #------------------------------------------------------------------------------ # CONFIG TAB #------------------------------------------------------------------------------ TabbedPanelItem: id: configtab text: 'Config' # [1.5] Wrap the whole setings area in a ScrollView for very # small displays? NO: could not get this to scroll as coded, # and it fits most contexts as is, except for narrow-phones # landscape, which is likely unusable in general. The Main # tab is also unscrolled, because it fits every practical size. # # UPDATE: changes to properly vertically align the text inputs # in Config also made this tab much more usable in landscape # mode on slab phones - which was a primary driver for scrolls. # This similarly improves the Main tab's pathnames on slabs. BoxLayout: orientation: 'vertical' # Start setting-widgets area # ScrollView: # [1.5] PUNT # BoxLayout: # Checkbutton + Label 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] # [1.4] reordered to group now-three chooser settings 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 for UNDO?' # [1.4] shorter ConfigCheckbox: size_hint_x: 0.20 id: skipcruftscheckbox active: True LineLabel: size_hint_x: 0.80 text: 'Skip one-platform items in FROM and TO?' # [1.4] shorter 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] 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?' # [1.4] Togggle hiddens view via filechooser.show_hidden. # The newly opened popup's filechooser show_hidden is set in # .py from this Main Config-tab setting, which is persisted. # filechooser's is_hidden is already guarded against hangs. ConfigCheckbox: size_hint_x: 0.20 id: showhiddens active: False LineLabel: size_hint_x: 0.80 text: 'Show hidden folders in choosers?' 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?' # Input + Label 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] TextInput: size_hint_x: 0.20 id: maxnumbackups text: '25' # [1.5] filter limits to 0-9 but allows '' (crashes): default in .py input_filter: 'int' on_focus: if not self.focus: root.on_int_input_defocus('maxnumbackups') multiline: False #valign: 'middle' # moot, nonexistent in Text # [1.5] valign properly, per current size padding_y: (self.height - self.line_height) / 2 padding_x: dp(8) #padding: [16, 16, 16, 0] # [l, t, r, b], default [6]*4 (pixels) #padding: [dp(8), 0, dp(8), 0] # scaled per density [1.1] #height: self.minimum_height # lines height, includes padding #size_hint_y: 1 LineLabel: size_hint_x: 0.80 text: 'Keep up to this many SYNC backups' TextInput: size_hint_x: 0.20 id: maxnumlogfiles text: '30' # [1.5] filter limits to 0-9 but allows '' (crashes): default in .py input_filter: 'int' on_focus: if not self.focus: root.on_int_input_defocus('maxnumlogfiles') multiline: False #valign: 'middle' # moot, nonexistent in Text # [1.5] valign properly, per current size padding_y: (self.height - self.line_height) / 2 padding_x: dp(8) #padding: [16, 16, 16, 0] #padding: [dp(8), 0, dp(8), 0] # scaled per density [1.1] LineLabel: size_hint_x: 0.80 text: 'Keep up to this many run logfiles' TextInput: size_hint_x: 0.20 id: maxlogstailsize text: str(10 * 1024) # [1.5] filter limits to 0-9+',' but allows '' (crashes): default in .py input_filter: lambda s, u: s if s in '0123456789,' else '' on_focus: if not self.focus: root.on_int_input_defocus('maxlogstailsize') multiline: False #valign: 'middle' # moot, nonexistent in Text # [1.5] valign properly, per current size padding_y: (self.height - self.line_height) / 2 padding_x: dp(8) #padding: [16, 16, 16, 0] #padding: [dp(8), 0, dp(8), 0] # scaled per density [1.1] LineLabel: size_hint_x: 0.80 text: 'Show up to this many bytes in Logs TAIL' # Input + Button + Label 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] TextInputNoSelect: size_hint_x: 0.20 id: logsbackgroundcolor text: 'black' readonly: True disabled: True multiline: False #valign: 'middle' # moot, nonexistent in Text # [1.5] valign properly, per current size padding_y: (self.height - self.line_height) / 2 padding_x: dp(8) #padding: [16, 16, 16, 0] #padding: [dp(8), dp(8), dp(8), 0] # scaled per density [1.1] 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' # moot, nonexistent in Text # [1.5] valign properly, per current size padding_y: (self.height - self.line_height) / 2 padding_x: dp(8) #padding: [16, 16, 16, 0] #padding: [dp(8), dp(8), dp(8), 0] # scaled per density [1.1] 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' # [1.5] filter limits to 0-9 but allows '' (crashes): default in .py input_filter: 'int' on_focus: if not self.focus: root.on_int_input_defocus('globalfontsize') multiline: False #valign: 'middle' # moot, nonexistent in Text # [1.5] valign properly, per current size padding_y: (self.height - self.line_height) / 2 padding_x: dp(8) #padding: [16, 16, 16, 0] #padding: [dp(8), dp(8), dp(8), 0] # scaled per density [1.1] ActivityButton: size_hint_x: 0.20 text: 'Apply' on_release: root.set_font_size(globalfontsize.text) # [1.5] .py traps '' LineLabel: size_hint_x: 0.60 text: 'Global font size in pixels' # Radio button + Radio button + Label # 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' # End setting-widgets area # 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] # height: buttonheight # orientation: 'horizontal' Label: size_hint: None, None height: dp(8) # custom top space above buttons DialogButtonsRows1: # [1.5] uniform fixed size 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.95 # [1.5] was 0.70 size_hint_x: 1.0 id: helptextscroll do_scroll_x: False do_scroll_y: True effect_cls: 'ScrollEffect' # don't overscroll/snapback # [1.5] use multiple Labels for text markup # Text fields don't do markup, but Labels display as empty # space for any trivially-large text. To work around, split # text into N parts and span across multiple Labels in .py. # Labels allow markup: bold, italics, colors, [H] headers. # CATCH: unlike Kivy 2.31, 2.1.0 Label padding is always just # two values, [h v], so must pad manually at top and bottom of # the split labels box. See for more info. BoxLayout: orientation: 'vertical' size_hint_y: None height: self.minimum_height VerticalSpacer10: # add padding at top: kivy 2.1 padding is 2 vals SplitTextBoxLayout: # main.py adds N LabelTextDisplay for split text id: helptextbox # this precludes a common Help/About class VerticalSpacer10: # add padding at bottom: kivy 2.1 padding 2 vals #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # [1.5] prior version - retain temp # 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] # # # to enable scrolling # size_hint: (None, None) # width: helptextscroll.width # height: max(self.minimum_height, helptextscroll.height) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # [1.5] Failed attempt: replacing the ScrollView above with the # following RecycleView cut memory use substantially when Help # and About are viewed (from ~850M to ~500M), but scrolling # was too jerky to be usable. Improving this may require fixed- # size items, and that seems impossible with wrapped-text Labels. # #RecycleView: # id: helptextrview # .py sets .data with line strs # viewclass: 'LabelTextDisplay' # # RecycleBoxLayout: # padding: [dp(10), dp(10)] # pad top/bottom here, not RV or Lable # do_scroll_x: False # do_scroll_y: True # effect_cls: 'ScrollEffect' # don't overscroll/snapback # # default_size: None, None # val2 stops jumpy scrolls, may botch lines # default_size_hint: 1, None # size_hint_y: None # height: self.minimum_height # orientation: 'vertical' # # The .py code simply did this, though updates for fontsize # changes were never tested or addressed: # # # using N > 1 lines per Label did not help here: still jerky # labelbox = self.ids.helptextrview # lines = text.splitlines() # 1 line/Label # labelbox.data = [{'text': line or ' '} for line in lines] # Label.text #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #GridLayout: # cols: 2 # size_hint_y: 0.20 #BoxLayout: # size_hint_y: 0.08 # height: buttonheight DialogButtonsRows1: # [1.5] uniform fixed size, culled, shortened DialogButton: text: 'Docs' on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/User-Guide.html') DialogButton: text: 'Website' on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/index.html') DialogButton: text: 'Backups' on_release: root.do_explore_backups() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # [1.5] retain temp #DialogButton: # text: 'App Downloads' # on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/App-Packages.html') # #DialogButton: # text: 'Mergeall Website' # on_release: root.open_web_page('https://learning-python.com/mergeall.html') # #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' ScrollView: # now required for spit-text labels box 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 # [1.5] use multiple Labels for text markup, and split # text to avoid blackouts. See notes at Help above. BoxLayout: orientation: 'vertical' size_hint_y: None height: self.minimum_height VerticalSpacer10: # add padding at top: kivy 2.1 padding is 2 vals SplitTextBoxLayout: # main.py adds N LabelTextDisplay for split text id: abouttextbox # this precludes a common class VerticalSpacer10: # add padding at bottom: kivy 2.1 padding 2 vals # [1.5] former scheme - see Help #TextInputNoSelect: # foreground_color: 'white' # background_color: 'black' DialogButtonsRows1: # [1.5] uniform fixed size, Mergeall from Help DialogButton: text: 'Play Store' on_release: root.open_web_page('https://play.google.com/store/apps/details?id=com.quixotely.usbsync') DialogButton text: 'Downloads' on_release: root.open_web_page('https://quixotely.com/PC-Phone USB Sync/App-Packages.html') DialogButton: text: 'Mergeall' on_release: root.open_web_page('https://learning-python.com/mergeall.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 # [1.5] Hindsight: this coding nests a Boxlayout in a FloatLayout. # The latter is the superclass of this <> class in the .py and is # largely pointless: we could just use BoxLayout in the .py and # skip an indentation level here. OTOH, this allows configuring # the BoxLayout here instead of in the .py; and if it ain't broke... 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') # [1.4] Punt: logic differs from icon/list here, and rarer - make a config only #Button: # text: 'Hidden' # on_release: filechooser.show_hidden = not filechooser.show_hidden # nope 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 # [1.4] .py now sets on popup open from Config tab # 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 DialogButtonsRows1: # [1.5] uniform fixed size 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) # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog BoxLayout: size: root.size pos: root.pos orientation: 'vertical' # ([1.5] now moot, but retained temp for reference/context) # 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 SpacedLabelTextDisplay: # [1.5] Label for colors and headers text: root.message # with padding on top and bottom #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #retain temp #TextInputNoSelect: # text: root.message # multiline: True # readonly: True # True: no keyboard on tap [1.1] # 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] # # # to enable scrolling # size_hint: (None, None) # width: textscroll.width # height: max(self.minimum_height, textscroll.height) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #BoxLayout: # size_hint_y: None # height: buttonheight DialogButtonsRows1: # [1.5] uniform fixed size DialogButton: text: 'Okay' on_release: root.oncancel(root) # validate root=content instance #------------------------------------------------------------------------------ # CONFIRM POPUP #------------------------------------------------------------------------------ : # General modal verify popup: pass message, onyes, onno # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog 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 SpacedLabelTextDisplay: # [1.5] Label for colors and headers text: root.message # with padding on top and bottom #TextInputNoSelect: # former scheme - see InfoDialog #BoxLayout: # size_hint_y: None # height: buttonheight DialogButtonsRows1: # [1.5] uniform fixed size 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 # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog BoxLayout: size: root.size pos: root.pos orientation: 'vertical' ColorPicker id: colorpicker #BoxLayout: # orientation: 'horizontal' # size_hint_y: None # height: buttonheight DialogButtonsRows1: # [1.5] uniform fixed size 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 #------------------------------------------------------------------------------ : # NO LONGER USED, as of [1.3] # After N opens in trial app: pass message, oncancel # This might be subverted, but it's not worth the struggles # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog 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 SpacedLabelTextDisplay: # [1.5] Label for colors and headers text: root.message # with padding on top and bottom #TextInputNoSelect: # former scheme - see InfoDialog #BoxLayout: # size_hint_y: None # height: buttonheight DialogButtonsRows1: # [1.5] uniform fixed size 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 # [1.5] BoxLayout in FloatLayout - see MainPathPickDialog 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 SpacedLabelTextDisplay: # [1.5] Label for colors and headers text: root.message # with padding on top and bottom #TextInputNoSelect: # former scheme - see InfoDialog #BoxLayout: # size_hint_y: None # height: buttonheight DialogButtonsRows1: # [1.5] uniform fixed size 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 this app is deeply ashamed...) #------------------------------------------------------------------------------ # (NOTE: this might not be used in later Kivy, but is used in 2.1.0) # # 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 fix, 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 experience (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 bogus # "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 (frustration happens). # # 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 mods 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?'