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)