""" =============================================================================== 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 /Contents/MacOS and access them by relative CWD path per the chdir here. Public items are added to /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)