File: Frigcal/code/Frigcal--source/common.py
"""
========================================================================
COMMON NAMES (part of Frigcal)
Global names, imported or defined once here and shared by all modules
========================================================================
"""
#=======================================================================
# IMPORTS
#=======================================================================
# Python
import sys, os
import platform, webbrowser, json, pickle, glob, time, datetime
import re, threading, queue, fnmatch, traceback, shutil, math
osjoin, osexists = os.path.join, os.path.exists
# Require Python 3.X+
assert(int(sys.version[0]) >= 3)
# Platform
RunningOnAndroid = hasattr(sys, 'getandroidapilevel') # py 3.7+ (or newer sys.platform)
RunningOnMacOS = sys.platform.startswith('darwin') # intel and apple m (rosetta?)
RunningOnWindows = sys.platform.startswith('win') # Windows py, may be run by Cygwin
RunningOnLinux = sys.platform.startswith('linux') # native, Windows WSL, Android
RunningOnLinux = RunningOnLinux and not RunningOnAndroid # Linux ONLY, else Android too py3.8
wsl2sig = 'microsoft-standard-WSL2' # Windows WSL2 Linux ONLY
RunningOnWSL2 = RunningOnLinux and platform.uname().release.endswith(wsl2sig)
RunningOnCygwin = sys.platform.startswith('cygwin') # Cygwin's own py, run on Windows
# Run early tweaks on PCs now, before Kivy imports
import prestart_pc_tweaks
# Kivy: GUI core (some may be unused)
import kivy.metrics
from kivy.lang import Builder
from kivymd.app import MDApp
from kivy.clock import mainthread, Clock
from kivy.metrics import dp, sp
from kivy.properties import StringProperty, ListProperty, NumericProperty
from kivy.properties import ObjectProperty, OptionProperty
from kivy.config import Config
from kivy.core.window import Window # instance is app.root_window
from kivy.graphics import Color, Rectangle, Line
from kivy.effects.scroll import ScrollEffect
from kivy.animation import Animation
from kivy.factory import Factory # for classes defined in .kv
from kivy.utils import colormap, get_color_from_hex
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.checkbox import CheckBox
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
from kivy.uix.filechooser import FileSystemLocal # else hangware (in PPUS)
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
# KivyMD: GUI (nav drawer, screens, hamburger menu, etc., some may be unused)
from kivymd.uix.screen import MDScreen
from kivymd.uix.textfield import MDTextField
from kivymd.uix.label import MDLabel
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.anchorlayout import MDAnchorLayout
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.behaviors.backgroundcolor_behavior import BackgroundColorBehavior
from kivymd.uix.scrollview import MDScrollView
from kivymd.uix.navigationdrawer import MDNavigationDrawer
from kivymd.uix.list import MDList
from kivymd.uix.list import OneLineListItem
from kivymd.uix.filemanager import MDFileManager # or Kivy chooser, plyer, tkinter?
from kivymd.uix.pickers import MDDatePicker
from kivymd.uix.menu import MDDropdownMenu
from kivymd.app import MDApp
# pyjnius: Android Java interface
if RunningOnAndroid:
from jnius import autoclass, cast
# python-for-android (p4a): helpers
if RunningOnAndroid:
import android.storage
from android.runnable import run_on_ui_thread
from android.config import ACTIVITY_CLASS_NAME
#from android.storage import app_storage_path
#from android.storage import primary_external_storage_path
#from android.broadcast import BroadcastReceiver
else:
run_on_ui_thread = lambda func: func # no-op decorator for PCs
#=======================================================================
# GLOBALS
#=======================================================================
# logistics
APPNAME = 'Frigcal' # used for assorted folder names, etc.
VERSION = '4.0.0' # versionName in manifest, inserted in About
PUBDATE = 'January 2026' # inserted in About by on_start() too
# trace iff false
RELEASE_VERSION = False # set to True for Android release builds: don't trace
# font size before user mods (or 0 to skip settings)
# kivy label+text default=15sp, scaled for device+user per kivy.metrics
# _linuxinc was dropped: +3 seems too large on Linux (was +5 in PPUS's older Kivy)
# caveat: Windows/WSL2 settings should vary but won't if use same source-code
_linuxinc = 0 if RunningOnLinux else 0
FONT_SIZE_DEFAULT = int(sp(15 + _linuxinc))
# font family before user mods (or '' to skip settings)
# kivy default=Robot-Regular.ttf per ~/.kivy/config.ini; see also settingsgui.py picks
# now unused: see common.kv for cut's rationale
FONT_NAME_DEFAULT = 'Roboto' # or other kivy builtins: DejaVuSans, Roboto-Mono, etc.
# app private+transient resources
# on Android: in app-install (not app-private) folder, no user access
# on Windows and Linux: in '.' for source code, Pyinstaller temp unzip for exes
# on macOS: in '.' for source code, .app/Contents/Resources (and MacOS links) for app
HELP_FILE = 'help-message.txt'
ABOUT_FILE = 'about-template.txt'
TERMS_OF_USE_FILE = 'terms-of-use.txt'
# app public+persistent resources
# on Android: in app-private (not app-install) folder, backed up, no user access
# on Windows and Linux: in '.' for source code, install/unzip (exe) folder for exes
# on macOS: in ~/Library/Frigcal for source code and app
SETTINGS_FILE = 'settings.json' # was .pkl
RUNCOUNT_FILE = 'runcounter.txt'
# calendar files (user-selected folder)
ICS_FILE_EXTENSION = '.ics'
CALENDARS_FOLDER_DEFAULT_NAME = APPNAME
BACKUPS_SUBDIR_NAME = '_Backups'
# Folders - run counts + config settings: see storage_path_app_private()
# Folders - suggested calendars default: see default_calendar_folder_path()
#=======================================================================
# GLOBAL UTILITIES
#=======================================================================
def trace(*args, **kargs):
"""
-----------------------------------------------------------
Don't print trace messages in release builds for Android.
That means there's nothing to go on for errors, but most
users won't be able to run logcat anyhow, and Google Play
chides about trace messages (and is douchefully dogmatic).
-----------------------------------------------------------
"""
if RELEASE_VERSION:
return
# if not THREAD_RUNNING:
# moot in this app?
print('(FRIGCAL)', *args, **kargs, flush=True)
def traces(*args, **kargs):
for arg in args: trace(arg, **kargs) # '\n' doesn't work in logcat
class PopupStack:
"""
-----------------------------------------------------------
A simple stack for popup dialogs that may nest.
Kivy coding structure makes it easiest to save
Popups for later closes, but Popups may nest here
(e.g., an info_message while a folderchooser is open,
though this may have been impossible in earlier code).
UPDATE: Button double-tap nonsense:
1) Pops of _empty_ stack triggered excs and app aborts
when dialog close buttons were double tapped on all
platforms. This specific issue is fixed by checking
for empty here, but double taps create other issues.
2) For one, double taps can close _two_ dialogs stacked
atop each other at once. To avoid this, all dismiss calls
now pass the Popup's content, and dismiss here skips the
call if passed contents do not match that of the stacked
Popup. This avoids closing overlaid Popup on double taps.
3) Now also uses auto_dismiss=False on all Popup() so
Popup not dismissed without a pop() here. Else, bogus
auto-dismissed entries remain stacked and are dismissed
on later dbl taps (though this seems to be harmless).
Could keep auto-dismiss by catching on_dismiss and doing
pop() in catcher, but taps outside multi-button popups
are ambiguous and seem error-prone in practice anyhow.
More generally, a double tap on _any_ button may trigger
its callback twice... This seems a Kivy misfeature (bug!),
but hasn't been an issue in testing. If can only matter
if event is fired because the widget still active; the
Popup case is probably timing - Button event beats the
dismiss() event. Alt: disable Button double-tap instead?
Later: PPUS also had to handle double taps of Yes in
action-confirm dialogs, else crashed or ran action*2.
Multiple taps are a Kivy issue but may be touch related.
-----------------------------------------------------------
"""
def __init__(self):
self.popups = []
def push(self, popup):
self.popups.append(popup) # stack Popup object
def empty(self):
return not self.popups
def pop(self):
if not self.empty():
return self.popups.pop(-1) # remove+return top
def top(self):
if not self.empty(): # avoid dbl-tap excs
return self.popups[-1]
def open(self):
if not self.empty():
self.top().open() # display topmost Popup
def dismiss(self, content):
if not self.empty():
if self.top().content == content: # avoid dbl-tap closes
self.pop().dismiss() # close topmost Popup
# One self.popupStack instance is created in App
# See also: App().dismiss_popup(popupcontent)