File: Frigcal/code/Frigcal--source/prestart_pc_tweaks.py

"""
===============================================================================
PC STARTUP TWEAKS (part of Frigcal)

PC-platform (mostly) mods run at top/startup of main.py, before gui build.
Imported from common.py so runs before importing Kivy (required for some).

See also other workarounds in main.py's startup_gui_and_config_tweaks()  
and App.on_start() post-build tweaks.  Among these workarounds are text 
splits for label size limits and keyboard panning for touch.  Tweaks and 
workarounds here don't have or require a 'self' from the App.
===============================================================================
"""

# for platforms
from common import *    # from gets only names before recursive import of this




if RunningOnMacOS:

    """
    -----------------------------------------------------------
    Keep app's icon in Dock while app runs, for macOS app.

    On macOS only, force the Dock's icon to be the app's custom
    icon while the app is running.  Else, KivyMD's default icon
    (or Kivy's, if KivyMD's self.icon setting code is disabled)
    replaces that app's custom in the Dock during program runs.  

    This feels like a bug in PyInstaller: in former apps (PPUS), 
    there is a subsecond mod back to Kivy's icon while the app 
    is initializing, but the app's custom icon is then restored.
    In FC, the default icon remains during app execution, despite
    the same icon code in build.py and the .spec file.
 
    The workaround here is simple, and does not impact Finder 
    or Launcher, where the app's custom icon always appears.
    It doesn't fix source-code runs: PyLauncher's icon wins.

    UPDATE: this also required disabling KivyMD's source code 
    in its app.py that sets its own MDApp.icon property.  Do
    the following on the macOS build machine (currently mbv):
 
      % vi ~/fc4/fc4-venv/lib/python3.12/site-packages/kivymd/app.py
      ##icon = StringProperty("kivymd/images/logo/kivymd-icon-512.png")

    With this, there are two parts to this workaround:

    - The Config.set below prevents the KivyMD (or Kivy) icon 
      from appearing in the Dock while the app is running.

    - The mod to KivyMD's source code above prevents the KivyMD 
      icon from appearing momentarily while the app is starting up.
    
    The combo ensures that the app's icon shows up in the Dock
    always and only.  Sans the Config code here, though, Kivy's 
    icon takes KivyMD's place during runs.  By comparison, PPUS
    currently does not do either part and suffers only from the 
    momentary startup switch to Kivy's icon (it doesn't use KivyMD).  
   
    The KivyMD code mod is arguably a hack, but this seems to 
    be broken in the Kivy+KivyMD stack, and there are better 
    uses of dev time...
    -----------------------------------------------------------
    """

    from kivy.config import Config    # not yet in common
    Config.set('kivy', 'window_icon', 'icons/frigcal.icns')




if not RunningOnAndroid:    

    """
    -----------------------------------------------------------
    Avoid Kivy's dreaded orange/red dots, for all PCs.

    On all PCs, kill the bizarro orange-red circles drawn on 
    right/context clicks and more.  Seen on Windows and macoOS, 
    this is Kivy's built-in multi-touch emulation, but it's  
    highly unusual and unexpected in UIs (and why is this 
    enabled by default?).

    Some sources state the fix must be run before kivy imports;
    it is in FC, but this isn't required.  Some also suggest the
    following variant - same but it cannot be reenabled?:
        Config.set('input', 'mouse', 'mouse, disable_multitouch')
    
    Don't do this on Android - some sources suggest this may 
    double tap event (and tap repeats are already problematic).
    -----------------------------------------------------------
    """

    from kivy.config import Config    # not yet in common
    Config.set('input', 'mouse', 'mouse,multitouch_on_demand')




if RunningOnLinux and RunningOnWSL2:

    """
    -----------------------------------------------------------
    Try to address a WSL2 button-tap glitch (docs only).

    On WSL2 Linux only (seen with an Ubuntu distro), dialogs 
    sometimes are not erased after a close-button click until
    the mouse cursor is moved outside the button by the user.

    This is a known glitch in WSL2 likely related to graphics
    rendering, focus, or event translation between Windows and
    WSLg; try web searches like these or similar:

      "wsl2 dialog is not closed until move cursor after click"
      "kivymd linux popup is not closed until move cursor after click button"
 
    The only remedies seem restarting or updating WSL2 and/or
    graphics drivers, or using keyboard Alt+F4 to close dialogs
    (lame, all).  A suggested fix below did not help, and a 
    Clock.schedule to defer the popup dismiss() had no effect.  

    PUNT: it's an unfixable and possibly temporary WSL2 bug!
    -----------------------------------------------------------
    """

    # this did not fix it...
    # from kivy.config import Config
    # Config.set('input', 'mouse', 'mouse,disable_on_activity')

    pass   # TBD




if RunningOnWindows:

    """
    -----------------------------------------------------------
    Import win32 now, for Windows exe and source.

    Run win32 imports now so they will run once at script 
    startup, instead of later.  This avoids import aborts 
    if Windows 10+'s Storage Sense (or other?) wrongly
    deletes the PytInstaller --onedir _MEI* temp folder 
    prematurely (actuallly seen in the wild).  Requires 
    'pip3 install pywin32' on Windows.  TBD: used in FC?
    -----------------------------------------------------------
    """

    # get tools, drive roots, network paths
    import win32api     # unused here: win32file, win32wnet




if RunningOnWindows:

    """
    -----------------------------------------------------------
    Deblur the GUI, for Windows exe and source.

    Like tkinter, Kivy does not do DPI saling well on Windows,
    which makes GUIs open large and blurry.  This code must be 
    run before importing kivy and addresses the issue, but is 
    provisional, pending a fix in SDL2 layers in the Kivy stack.

    Note that this applies to python.exe for source-code runs,
    but to the frozen executable for its run: per-process call.
    Used for tkinter: windll.shcore.SetProcessDpiAwareness(1)

    https://github.com/kivy/kivy/pull/7299
    https://github.cwindll.user32.SetProcessDpiAwarenessContext(c_int64(-4))om/kivy/kivy/issues/3705
    https://learning-python.com/post-release-updates.html#win10blurryguis
    -----------------------------------------------------------
    """

    from ctypes import windll, c_int64
    windll.user32.SetProcessDpiAwarenessContext(c_int64(-4))




if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):

    """
    ---------------------------------------------------------------
    Fix logging bug, for Windows and Linux exes.

    In --windowed mode, Kivy logging writes to sys.stdout/err that
    are now set to None by PyInstaller instead of dummy objects, 
    thereby triggerring a  recursion-limit error.  Doesn't happen 
    if --windowed not used.  This must be called early in the
    script, else logs happen.

    Increasing call-stack depth with sys.setrecursionlimit(2000) 
    didn't help: pyinstaller code was stuck in a loop.
    
    Likely to be fixed very soon; TBD: is this still an issue? 
    https://github.com/kivy/kivy/issues/8074
    https://github.com/pyinstaller/pyinstaller/issues/7329
    ---------------------------------------------------------------
    """

    os.environ['KIVY_NO_CONSOLELOG'] = '1'




if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):

    """
    ---------------------------------------------------------------
    Close splash screen, for Windows and Linux exes.

    Tell the PyInstaller splash screen to close, else it doesn't.
    The splash screen isn't supported on macOS, but the --onedir 
    build for macOS opens quickly, unlike Windows/Linux --onefile.
    Android apps get splash screens from buildozer that just work.
    
    Nits: the splash screen (only) uses tkinter, which requires 
    Tcl/Tk to be present.  This causes issues on Windows WSL2 Linux:
    users must manually install Tcl/Tk to use this app there because 
    PyInstaller assumes they're present and they're not in WSL2 
    Ubuntu.  There was also an obscure issue on desktop Linux with
    Tcl/Tk version numbers.  Avoid all this would likely require an
    extra non-splash build just for WSL2; runtime checks+skips seem
    moot because this is an automatic thing invoked by builds.
    ---------------------------------------------------------------
    """

    import pyi_splash

    # Update the text on the splash screen?
    # pyi_splash.update_text('Loading program...')    # currently does nothing...

    # Close the splash screen. It does not matter when the call
    # to this function is made; the splash screen remains open until
    # this function is called or the Python program is terminated.
    
    # pyi_splash.close()    # see next function, called later

    def close_pc_exe_splash_screen():
        """
        Run this in App.on_start() instead, to wait until app window built
        """
        pyi_splash.close()




if hasattr(sys, 'frozen') and (RunningOnMacOS or RunningOnWindows or RunningOnLinux):

    """
    ---------------------------------------------------------------
    Set up runtime CWD context for relative-path file access in
    all PC apps and exes, but not for the Android app.  

    This began with the PPUS app but has been revised for FC's very
    different policies: FC's install folder is minimal because it 
    has no writeable code files, and its help text is not user readable.

    TL;DR: 
    - Private and transient data items (e.g., .kv, icons/, help)
      are stored in the folder created by PyInstaller: either the 
      sys._MEIPASS (a.k.a. dirname(__file__)) temporary unzip folder 
      made on each --onefile exe run for Windows and Linux, or the
      permanent install folder made for --onedir once at build time
      for macOS.  PyInstaller commands in build.py use --add-data 
      arguments to add these items, and the os.chdir() call here 
      makes them accessible at paths relative to the '.' CWD.

    - Public and persistent data items (e.g., tools and settings) are 
      instead stored in the permanent unzip folder created by a user 
      download and install.  Code in build.py copies these into the 
      download zip, and code in main.py resolves the path for access,
      using either sys.executable's path for Windows and Linux, or 
      ~/Library to avoid translocation app-folder issues for macOS.
  
    Details:
    When run as a frozen app or exe (in shipped package), the CWD
    may not be the folder containing the program.  Set the CWD to 
    app/exe dir for all later relative file ops (e.g, vitals check).

    PyInstaller unzips --onefile exes and their added data to a
    temporary folder, _MEI*, which is deleted after app exit.  
    This differs from the app's install folder, where the download
    is unzipped.  In this context, this app must arrange access to
    both persistent and non-persistent data items by cwd or other:

    - shipped icalendar, pytz, distro module subdirs
    - shipped help, about, term-of-use text files
    - shipped icons/ animated progress gif in .kv, .py
    - shipped icons/ PCs icons, set in .py, used in build
    - shipped fonts/ for Kivys, loaded even if unused
    - shipped .kv GUI-def files not handled auto (4)
    - created run-counter text and settings JSON files
    - created calendar files and calendar-file backups

    Some of these these may not work in _MEI* temporary/transient 
    unzip dir, because they are changed and must persist between 
    app runs, but others are better hidden from users in _MEI*:

    icalendar, pytz, and distro modules: private + transient
        Can be .py or .pyc; are read-only and never changed; are 
        already open source, so they need not be hidden; and are 
        confusing extras for most users.
 
    help, about, and terms-of-use files: private + transient
        Unchanged and read-only, and in FC, have mark-up code that's
        unreadable by user and makes them better not stored in the 
        user-visible install unzip.

    icons/ and fonts/: private + transient
        Items in these folders are read only and can be in any cwd, 
        but they need not be user visible.  Icon files in icons/ are 
        both used at build time (for buildozer and PyInstaller), and 
        used at runtime by PC icon calls in .py files.

    .kv files: private + transient 
        These are read only too, and their code can be embedded in 
        .py files if need be.

    Run-counter and settings files: public + persistent
        Unlike other data items, these are created when the app runs, 
        not shipped with it.  They cannot be in _MEI*, because they
        must be changed and retained between runs.  They can be in 
        the install dir, except on macOS, which maps them to users'
        ~/Library because apps may not be able to write their own 
        folders (see translocation ahead).  ~/Documents could be 
        used as a host everywhere but is not.
    
    Calendars and their backups: public + persistent + user choice
        These are also used by but not shipped with the app.  They
        must be in a folder with both user access and permission.  
        For both, use a folder chosen by the user in UI.  They can 
        be anywhere on PCs, and in a SAF-picked folder on Android.

    To handle these cases, this app uses these policies for PC builds:

    For Windows and Linux: 
        Private + transient data items are stored in the executable 
        folder made by PyInstaller (either per-run for --onefile or on
        ce for --ondir) and accessed relative to '.' per os.chdir() here.

        Public + persistent items are stored alongside the --onefile 
        executable in a manually created zipfile, and accessed via 
        sys.executable's dir (the install's unzip folder) when needed.
        There is a lag for the _MEI* unzip at app startup, but these 
        platforms post a splash screen during the pause.

    For macOS:
        Since PyInstaller builds an app folder anyhow, use --onedir to 
        avoid lag for startup unzips sans splash sceeen (unsupported on
        macOS), and use --add-data to store all private items alongside 
        the executable in the app folder's <app>/Contents/MacOS and 
        access them by relative CWD path per the chdir here.  Public 
        items are added to <app>/Contents/Resources manually.  Really, 
        MacOS/ contains symlinks to some items stored in Resources/,
        but this is irrelevant to code that accesses privates in the .py.

        The app install dir persists between runs and gives access to 
        all shipped items (text, images, modules, .kvs).  For changables, 
        the run-count and settings files are mapped to ~/Library/appname
        because macOS translocation _may_ make the exe's folder unusable 
        (this vary oddly: PPUS can change files nested in a subfolder 
        there, but PyEdit cannot write an autosave folder there).

    In short, the chdir here makes relative pathnames map to the app's 
    PyInstaller folder, which is temporary for --onefile or permanent 
    for --onedir.  Mods here are _not_ run for either source code or 
    the Android app, which both keep working normally and as is.
    
    See also: pc-builds/build.py applies these policies in builds.
    For other PyInstaller data-item examples, see Frigcal 3.0's 
    fixfrozenpaths.py and PPUS's main.py and build.py.
    ---------------------------------------------------------------
    """
    
    # sys.executable is the frozen exe's path for --onefile and --onedir
    # sys.argv[0] is the path used in the launch command, possibly relative
    # sys._MEIPASS is the TEMPORARY uzip folder for --onefile (only)
    # __file__ is sys._MEIPASS+script for --onefile, else install folder 

    if RunningOnMacOS:
        exepath = sys.executable                              
        exedir  = os.path.dirname(os.path.abspath(exepath))    # install unzip folder
        os.chdir(exedir)                                       # .app/Contents/MacOS links

    elif RunningOnWindows or RunningOnLinux:                   # dir(__file__)==sys.MEIPASS
        exedir = sys._MEIPASS                                  # temp per-run unzip folder
        os.chdir(exedir)                                      

    # not required: imports are based on abs(cwd)
    # sys.path.append(exedir)    # for shipped modules?

    # and use relative '.' for access to private+transient resources
    # and use sys.executable's path for access to public+persistent resources



if not hasattr(sys, 'frozen'):

    """
    ---------------------------------------------------------------
    Set up runtime CWD context, for source-code runs on all.

    When main.py is run as source code (dev, or from a source-code 
    package?), it may be run from a different dir and CWD is not 
    auto set the main script's dir.  Set the CWD to main.py's dir
    for all later relative file ops (e.g., vitals startup check).

    Note that __file__ is the same as os.path.abspath(sys.argv[0])
    for a launched script on Pythons 3.9+, but __file__ may not 
    be the abspath on earlier Pythons.
    ---------------------------------------------------------------
    """

    scriptdir = os.path.dirname(os.path.abspath(sys.argv[0]))
    os.chdir(scriptdir)