File: pygadgets-products/unzipped/_PyClock/Clock/clock.py

"""
###############################################################################
PyClock 3.0: a clock GUI in Python/tkinter.

[SA] Sep-2017: Standalone release of PyCalc, PyClock, PyPhoto, PyToe.
Copyright 2017 M.Lutz, from book "Programming Python, 4th Edition".
License: provided freely, but with no warranties of any kind.

With both analog and digital display modes, a pop-up date label, clock face 
images, general resizing, countdown loops, etc.  May be run both standalone, 
or embedded (attached) in other GUIs that need a clock display.

New in 3.0.1 [SA2]: 
-Fix to update min and hour hands immediately after a deiconify, switch 
 to analog display, resume from suspend, and some dialog/menu views -- not 
 only at seconds=12; else, update delayed too long and time is inaccurate
-Avoid updating analog ampm label every second just like min/hour hands;
 this seems to have further reduced the memory leak while open on Macs:
 open-window growth is now just 1M/20mins, 3M/hour, 72M/day, and 500M/week; 
 this is roughly half what it was, and some popular Mac apps do worse
-Verify that no attrs here clash with those in tkinter's classes (which
 use "_x" for all but methods, 'tk', 'master', 'children', 'widgetName',
 and 2 'name*' oddballs.); this was further/futile memory-leak research

New in 3.0 [SA]: 
-Mac OS port
-Add '?' help
-Use new configs model
-Build standalone app+exes
-Use PIL/Pillow if present
-Cache the background/center image for speed 
-Work around a memory leak on Mac OS (see onUpdate() and ./MacMemoryLeak/)
-Save cpu/memory in general by skipping redraw while minimized everywhere  
TBD: auto restart after N days on Macs to address gradual memory leak?
TBD: DemoMode (like PyToe) that starts clockStyles.py script/exe?
TBD: digital mode now leaks more memory than analog when open; why?

New in 2.1: updated to run under Python 3.X (2.X no longer supported).
New in 2.0: s/m keys set seconds/minutes timer for pop-up msg; window icon.

TBD: requiring PIL or Tk 8.6+ would allow other types of images besides GIF;
in 3.0, both are now used is present (e.g., apps), but are not required.
###############################################################################
"""

from tkinter import *
from tkinter.simpledialog import askinteger
from tkinter.messagebox import showinfo
import math, time, sys, os

# show versions
print('Using Python %s, Tk %s' % (sys.version.split()[0], TkVersion))

# monitor updates
trace = lambda *args: None   # or print

RunningOnMac     = sys.platform.startswith('darwin')   # [SA] Mac port
RunningOnWindows = sys.platform.startswith('win')      # [SA] Win backport
RunningOnLinux   = sys.platform.startswith('linux')    # and so on 

# [SA] use PIL iff present (but disable PNG on Macs due to memory leak)
pillowwarning = """
Pillow 3rd-party package is not installed.
...This package is optional, but required for the clock images feature when
...using some image file types and Pythons (though not for GIFs with any Python,
...or PNGs with Pythons that use Tk 8.6 or later, including standard Windows 
...installs of Python 3.4+).  Pillow home: https://pypi.python.org/pypi/Pillow.
"""

try:
    from PIL.ImageTk import PhotoImage   # replace tkinter's version
except ImportError:
    from tkinter import PhotoImage       # else use the basic lib version
    print(pillowwarning)
    # but continue: falls back on Tk/tkinter's native PhotoImage, for PNGs, GIFs, etc.
    # Pillow install is required for source-code only: apps/exes have it "baked in"

# [SA]: set window icons on Windows and Linux
from windowicons import trySetWindowIcon

# [SA] try to limit memory leaks? - no: see onUpdate()
import gc
#gc.set_debug(gc.DEBUG_LEAK)

def defaultImage():
    "[SA] get image path in either standalone or PyGadgets context"
    if os.path.isdir('images'):
        # when CWD is my dir
        return 'images/PyGadgets1024_128x128.gif'   # '/' okay on Windows
    elif os.path.isdir('_PyClock'):
        # when CWD is PyGadgets dir
        return '_PyClock/Clock/images/PyGadgets1024_128x128.gif'
    else:
        return None   # bail nicely (no image displayed)


###############################################################################
# Option configuration classes and arg parser
###############################################################################

# [SA] DEFUNCT: now loaded from file or args


###############################################################################
# Digital display object
###############################################################################

class DigitalDisplay(Frame):
    """
    Digital display mode (too simple to doc much)
    """
    def __init__(self, parent, cfg):          # [SA] tbd: help here too?
        Frame.__init__(self, parent)
        self.hour = Label(self)
        self.mins = Label(self)
        self.secs = Label(self)
        self.ampm = Label(self)
        for label in self.hour, self.mins, self.secs, self.ampm:
            label.config(bd=4, relief=SUNKEN, 
                               bg=cfg.BgColor, 
                               fg=cfg.DigitalFgColor or cfg.FgColor)
            label.config(font=cfg.DigitalFont)   # [SA] new digital configs
            label.pack(side=LEFT)  # TBD: could expand, and scale font on resize

        # [SA] yes, make frame expandable; else tiny if window grows for digital
        for widget in (self, self.hour, self.mins, self.secs, self.ampm):
            widget.pack(expand=YES, fill=BOTH)


    def onUpdate(self, hour, mins, secs, ampm, timenow, cfg):
        """
        On timer update in parent Clock object, change time labels.
        [SA] No longer called if window minimized, to save cpu/memory. 
        [SA2] TBD: should these avoid redraws unless secs=12 too?
        """
        mins = str(mins).zfill(2)                          # or '%02d' % x
        self.hour.config(text=str(hour), width=4)
        self.mins.config(text=str(mins), width=4)
        self.secs.config(text=str(secs), width=4)
        self.ampm.config(text=str(ampm), width=4)


    def onResize(self, newWidth, newHeight, cfg):
        pass  # nothing to redraw here


###############################################################################
# Analog display object
###############################################################################

class AnalogDisplay(Canvas):
    def __init__(self, parent, cfg):
        """
        Draw analog clock at startup, to be shown/hidden by user on request.
        A Canvas, within a Clock Frame (parent), within a Tk or Toplevel window.
        This in turn nests a cached PhotoImage object, if one is being used.
        """
        self.size = int(cfg.InitialSize.split('x')[0])   # [SA] one: square
        Canvas.__init__(self, parent,
                        width=self.size, height=self.size, bg=cfg.BgColor)
        self.parent = parent   # Clock Frame
        self.image  = None
        self.lastredrawtime = 0.0
        self.drawClockface(cfg)
        self.cog = None
        # [SA] and wait for first unUpdate() to draw clock hands


    def drawClockface(self, cfg): 
        """
        On start and resize (not timer): draw ovals+picture on empty canvas.
        This is followed by an onUpdated() run from the timer loop logic. 
        """
        self.loadImage(cfg)
        if self.image != None:
            imgx = (self.size - self.image.width())  // 2          # center it
            imgy = (self.size - self.image.height()) // 2          # 3.x // div
            self.create_image(imgx+1, imgy+1,  anchor=NW, image=self.image)

        originX = originY = radius = self.size // 2                # 3.x // div
        for i in range(60):
            x, y = self.point(i, 60, radius-6, originX, originY)
            self.create_rectangle(x-1, y-1, x+1, y+1, fill=cfg.FgColor)   # mins

        for i in range(12):
            x, y = self.point(i, 12, radius-6, originX, originY)
            self.create_rectangle(x-3, y-3, x+3, y+3, fill=cfg.FgColor)   # hours

        self.ampm = self.create_text(3, 3, anchor=NW, fill=cfg.FgColor)

        # [SA] add help popup via '?' click; see Clock.makeWidgets
        help = self.create_text(self.size, 3, text='?', anchor=NE, fill=cfg.FgColor)
        self.tag_bind(help, '<Button-1>', lambda event: self.parent.onHelp(click=True))

        """
        [SA] this didn't work on Windows, and looked odd anyhow
        help = Button(self, text='?', command=self.parent.onHelp, fg=cfg.FgColor)
        self.create_window(self.size+1, 0, window=help, anchor=NE)
        """


    def point(self, tick, units, radius, originX, originY):
        """
        The geometry bit (see your favorite math textbook).
        """
        angle = tick * (360.0 / units)
        radiansPerDegree = math.pi / 180
        pointX = int( round( radius * math.sin(angle * radiansPerDegree) ))
        pointY = int( round( radius * math.cos(angle * radiansPerDegree) ))
        return (pointX + originX+1), (originY+1 - pointY)


    def loadImage(self, cfg):
        """
        Load analog background image (if any) just once.
        [SA] Factored off to cache, and make this more robust and explicit.
        [SA] Substitue a default for PNGs on Mac to avoid a memory leak in
        the both Tk 8.5 and 8.6 libs; see onUpdate() and ./MacMemoryLeak/.
        """
        if (cfg.PictureFile and 
            cfg.PictureFile.lower().endswith('.png') and
            RunningOnMac):

            self.after_idle(lambda: 
                showinfo('PyClock: PNG Replaced', 
                   'Your analog clockface image is being replaced with a '
                   'default, because PyClock disallows PNGs on Mac OS to '
                   'prevent a memory leak in the Tk GUI library.\n'
                   '\n'
                   'Please update your configurations file or arguments to '
                   'use a non-PNG image in the future.  You can use the PyPhoto '
                   'gadget or Mac\'s Preview app to convert and resize images '
                   'for use in PyClock.'
                  ))
            cfg.PictureFile = defaultImage()      

        if cfg.PictureFile and self.image == None:   # configured + not loaded?
            try:
                self.image = PhotoImage(file=cfg.PictureFile)       # try first
            except:
                try:
                    self.image = BitmapImage(file=cfg.PictureFile)   # save ref
                except:
                    pass
        return self.image


    def onUpdate(self, hour, mins, secs, ampm, timenow, cfg):
        """
        On timer update callback: redraw three clock hands and cog.
        Run each second by the after() timer-loop logic in the parent
        object's Clock.onTimer(), *unless* the window is minimized,
        or the digital-mode object is currently displayed.

        [SA] Recoded to just _move_ hands on updates 2..N by changing their 
        coords(), instead of deleting and recreating their objects each time. 
        Subtlety: this must also recreate hands after resize's delete('all').
        To save cpu/memory, also no longer called if the window is minimized, 
        and avoids moving min+hour hands except on minute rollovers, and all
        redisplays of a formerly idle or hidden analog clock (see SA2 below).

        Moves are likely more efficient, but this was initially an attempt to 
        fix a memory leak on Macs that proved futile.  The leak was determined 
        later to be a Tk bug when using PNG images only, and PNGs are now fully
        disallowed on Macs.  Other leak-fix attempts (forced gc.collect(), and 
        earlier recodings to erase and redraw both 'all' and the entire Canvas) 
        also failed; "batteries included" and "software stacks" are a mixed bag.
        """
        #gc.collect()   # [SA] had no effect, and used .x% more cpu

        """
        # [SA] change coords instead of deleting+recreating
        if self.cog:
            self.delete(self.cog)
            self.delete(self.hourHand)                      # erase prior hands
            self.delete(self.minsHand)
            self.delete(self.secsHand)
        """

        originX = originY = radius = self.size // 2         # 3.x div
        hour = hour + (mins / 60.0)                         # between points

        hx, hy = self.point(hour, 12, (radius * .80), originX, originY)
        mx, my = self.point(mins, 60, (radius * .90), originX, originY)
        sx, sy = self.point(secs, 60, (radius * .95), originX, originY)

        if not self.cog:
            #
            # [SA] create lines on first update and resizes (original code)
            #
            self.hourHand = self.create_line(originX, originY, hx, hy,
                                 width=(self.size * .04),
                                 arrow='last', arrowshape=(25,25,15), fill=cfg.HhColor)

            self.minsHand = self.create_line(originX, originY, mx, my,
                                 width=(self.size * .03),
                                 arrow='last', arrowshape=(20,20,10), fill=cfg.MhColor)

            self.secsHand = self.create_line(originX, originY, sx, sy,
                                 width=1,
                                 arrow='last', arrowshape=(5,10,5), fill=cfg.ShColor)

            cogsz = self.size * .01
            self.cog = self.create_oval(originX-cogsz, originY+cogsz,
                                        originX+cogsz, originY-cogsz, fill=cfg.CogColor)

            self.dchars(self.ampm, 0, END)
            self.insert(self.ampm, END, ampm)   # update am/pm text
            trace('created hands:', self.secsHand, (originX, originY), (sx, sy))

        else:
            #
            # [SA] move lines by changing their coords on later updates (new scheme)
            #
            if (secs == 0) or (timenow > self.lastredrawtime + 1.5):
                # 
                # [SA] optimization: update hour+min+ampm only at min rollover;
                # this reduces both cpu use everywhere and memory use on Macs;
                #
                # [SA2] _but_ also do so on the first timer update after window 
                # deiconify, switch from digital to analog display modes, resume
                # after system suspend, some menu and modal-dialog views on some
                # platforms, and any other state that precludes clock updates; 
                # else won't redraw till secs hand reaches twelve (a former bug);
                # 
                # subltle bits: setting self.cog=None won't suffice, because
                # prior hands are erased on resize only; display-mode switches
                # run resize (forcing creates) but only until first manual resize;
                # unlike flags, comparing new-to-last update time handles all cases, 
                # even those without portable Tk events like system suspend/resume;
                #
                self.coords(self.hourHand, (originX, originY, hx, hy))
                self.coords(self.minsHand, (originX, originY, mx, my))

                self.dchars(self.ampm, 0, END)
                self.insert(self.ampm, END, ampm)   # update am/pm text
                trace('updated hour+min+ampm')

            self.coords(self.secsHand, (originX, originY, sx, sy))  # last=top
            trace('changed hands:', self.secsHand, (originX, originY), (sx, sy))

        # [SA2] per docs above
        self.lastredrawtime = timenow


    def onResize(self, newWidth, newHeight, cfg):
        """
        On user resize of window, redraw clock face at new size.
        onUpdate will be run on next second to redraw clock hands.
        """
        newSize = min(newWidth, newHeight)
        if newSize != self.size+4:
            self.size = newSize-4             # 4 for canvas border
            self.delete('all')                # erase all canvas objects
            self.drawClockface(cfg) 
            self.cog = None
            # to be followed by next onUpdate() for hands


###############################################################################
# Clock composite object
###############################################################################

ChecksPerSec = 10  # second change timer

class Clock(Frame):
    """
    A custom Frame, with embedded AnalogDisplay and DigitalDisplay objects.
    Nested in a Tk or Toplevel (parent), and AnalogDisplay embeds an image.
    """
    def __init__(self, configs=object(), parent=None):
        Frame.__init__(self, parent)
        self.parent = parent                         # [SA] Tk or Toplevel
        self.cfg = configs
        self.makeWidgets(parent)                     # children are packed but
        self.labelOn = 0                             # clients pack or grid me
        self.display = self.digitalDisplay
        self.lastSec = self.lastMin = -1
        self.countdownSeconds = 0
        self.onSwitchMode(None)                      # flip to draw analog now 
        self.onToggleLabel(None)                     # [SA] label starts on
        self.onTimer()


    def makeWidgets(self, parent):
        """
        Make widgets, bind global actions (analog canvas also binds help click).
        [SA] Label now uses new config or default colors (not red/blue from PP4E).
        """
        self.digitalDisplay  = DigitalDisplay(self, self.cfg)
        self.analogDisplay   = AnalogDisplay(self,  self.cfg)
        self.dateLabel       = Label(self, bd=3) 
        self.justClickedHelp = False
 
        # [SA] keys: same on all platforms
        parent.bind('<KeyPress-x>', self.onSwitchMode)
        parent.bind('<KeyPress-d>', self.onToggleLabel)
        parent.bind('<KeyPress-s>', self.onCountdownSec)
        parent.bind('<KeyPress-m>', self.onCountdownMin)

        # [SA] question=? but portable, help key in all gadgets
        parent.bind('<KeyPress-question>', lambda event: self.onHelp())

        # [SA] clicks: platform-specific
        if not RunningOnMac:
            # original code: Windows+Linux; left-click B1 also fires when
            # the new help text is clicked, and returning 'break' doesn't
            # work for tag_bind, so we have to set and check a flag
            #
            parent.bind('<ButtonPress-1>', self.onSwitchMode)    # leftclick
            parent.bind('<ButtonPress-3>', self.onToggleLabel)   # rightclick

        else:
            # on Mac OS, can't bind B1 to root window, else also fires 
            # on border clicks (e.g., window moves); also, use B2 instead 
            # of B3 for right-click, and allow Control-B1 as a synonym;
            #
            parent.bind('<ButtonPress-2>',         self.onToggleLabel)   # rightlick
            parent.bind('<Control-ButtonPress-1>', self.onToggleLabel)   # rightclick

        # redraw analog on user resize
        parent.bind('<Configure>',  self.onResize)


    def onSwitchMode(self, event):
        """
        [SA] This is also fired on non-Macs when analog's "?" help text
        is clicked, and returning "break" doesn't work to cancel other
        bindings in tag_bind callbacks: set and check a state flag.
        """
        trace('onSwitchMode', self.display.__class__.__name__)
        if self.justClickedHelp and not RunningOnMac:
            self.justClickedHelp = False
            return

        # the normal bits          
        self.display.pack_forget()
        if self.display == self.analogDisplay:
            self.display = self.digitalDisplay
        else:
            self.display = self.analogDisplay
        self.display.pack(side=TOP, expand=YES, fill=BOTH)


    def onToggleLabel(self, event):
        """
        Show or hide the date label at window bottom.
        This is external to the analog/digitl displays.
        """
        self.labelOn += 1
        if self.labelOn % 2:
            self.dateLabel.pack(side=BOTTOM, fill=X)
        else:
            self.dateLabel.pack_forget()
        self.update()


    def onResize(self, event):
        """
        Check widget match, delegate resize to display object.
        [SA] A return 'break' here doesn't stop B1 events on Mac.
        """
        if event.widget == self.display:
            trace('onResize', event.widget.__class__.__name__)
            self.display.onResize(event.width, event.height, self.cfg)


    def onTimer(self):
        """
        On timer event: check second rollover, delegate redraw to display object.

        Checks for rollover N times per second to avoid display stuttering.
        time.time() is seconds since the epoch, as a floating point number.

        Caveat: both Tk's widget.after() and Python's time.time() are vulnerable 
        to user changes to the system's clock.  This can hang PyClock: restart.        
        """
        secsSinceEpoch = time.time()
        timeTuple      = time.localtime(secsSinceEpoch)
        hour, min, sec = timeTuple[3:6]
        if sec != self.lastSec:
            self.lastSec = sec
            if self.parent.force_state == 'Hidden':
                # [SA] skip update if minimized to save cpu+memory
                trace('skipped redraw')
            else:
                # Visible state: redraw
                trace('redrawing')
                ampm = ((hour >= 12) and 'PM') or 'AM'               # 0...23
                hour = (hour % 12) or 12                             # 12..11

                # update current display
                self.display.onUpdate(hour, min, sec, ampm, secsSinceEpoch, self.cfg)

                # [SA] better time-label format
                #self.dateLabel.config(text=time.ctime(secsSinceEpoch))
                timeFormat = '%a %b %d, %Y %X'
                self.dateLabel.config(text=time.strftime(timeFormat, timeTuple))
       
            # check countdown timer
            self.countdownSeconds -= 1
            if self.countdownSeconds == 0:
                self.onCountdownExpire()                # post alarm notice now
        self.after(1000 // ChecksPerSec, self.onTimer)  # run N times per second
                                                        # 3.x // trunc int div

    def onCountdownSec(self, event):
        secs = askinteger('PyClock Alarm', 'Seconds?')   # self-validating
        if secs: 
            self.countdownSeconds = secs                 # new sole timer


    def onCountdownMin(self, event):
        secs = askinteger('PyClock Alarm', 'Minutes')    # self-validating
        if secs: 
            self.countdownSeconds = secs * 60            # new sole timer


    def onCountdownExpire(self):
        """
        Display an expired-timer message fullscreen for attention.
        Caveat: only one active user timer, no progress indicator.
        """
        win = Toplevel()
        win.title('PyClock Alarm')
        trySetWindowIcon(win, 'icons', 'pygadgets')   # [SA] for win+lin
        if RunningOnMac or RunningOnLinux:
            # [SA] emulate colored buttons on Mac
            # [SA] avoid reversed colors on Linux
            msg = Label(win, text='Timer Expired!')
            msg.bind('<Button-1>', lambda e: win.destroy())
        else:
            msg = Button(win, text='Timer Expired!', command=win.destroy)
        msg.config(font=('courier', 80, 'normal'), fg='white', bg='navy')
        msg.config(padx=10, pady=10)
        msg.pack(expand=YES, fill=BOTH)
        win.lift()                             # raise above siblings
        if RunningOnWindows or RunningOnMac:   # go full screen mode
            win.state('zoomed')
        elif RunningOnLinux:                   # [SA] works on Mac too
            win.wm_attributes('-fullscreen', 1)


    def onHelp(self, click=False):
        """
        [SA] Analog clock display's new '?' click help callback;
        also run for '?' keypress and Mac menus in both modes.

        For Canvas tag_bind clicks only, set a flag to skip global
        click event next in onSwitchMode(); unlike other contexts,
        a return 'break' here doesn't stop B1 events for tag_bind.

        On Macs only, the display skips time updates during Help;
        must redraw all clock hands when any modal dialog is closed.
        """
        # [SA] ignore next click event
        self.justClickedHelp = click

        from helpmessage import showhelp
        showhelp(self.parent, 'PyClock', self.HelpText, forcetext=False,
                 setwinicon=lambda win:
                        trySetWindowIcon(win, 'icons', 'pygadgets'))

        #if self.parent: self.parent.focus_force()   # now done in helpmessage


    HelpText = ('PyClock 3.0\n'
                '\n'
                'A Python/tkinter clock GUI.\n'
                'For Mac OS, Windows, and Linux.\n'
                'From the book Programming Python.\n'
                'Author and © M. Lutz 2001-2017.\n'
                '\n'
                'Usage:\n'
                '▶ Key "X" switches between analog and digital '
                '(or leftclick on non-Macs)\n'
                '▶ Key "D" shows/hides date (or rightclick, '
                'Mac 2-finger press or control+click)\n'
                '▶ Keys "S" and "M" set the seconds or minutes alarm '
                'timer, respectively\n'
                '▶ Key "?" shows this help (the same as clicking the '
                'analog display\'s "?")\n'
                '▶ Resizing the window resizes the current clock display.\n'
                '\n'
                'Clock photos: analog clocks in app and executable '
                'PyClocks support most image types.  For source-code, '
                'GIF always works, PNG works for Tk 8.6+, and all types '
                'work after Pillow install (see README.txt).\n'
                '\n'
                'Mac OS users: use a GIF or JPEG for clocks; PNGs cause '
                'rapid memory leaks in the Mac\'s Tk library.  You can also '
                'limit memory use in general by minimizing PyClock to the '
                'Dock when not in use (see README.txt).\n'
                '\n'
                'On all platforms: minimizing PyClock may reduce the amount '
                'of CPU resources it consumes, as display updates are '
                'skipped.\n'
                '\n'
                'Version history:\n'
                '● 3.0: Sep 2017, standalone release\n'
                '● 2.1: May 2010, Programming Python 4E\n'
                '● 2.0: 2006 PP3E, 1.0: 2001 PP2E\n'
                '\n'
                'For downloads and more apps, visit:\n'
                'http://learning-python.com/programs.html'
               )


###############################################################################
# Standalone clocks
###############################################################################

appname = 'PyClock 3.0'

# use custom Tk, Toplevel for icons, etc.
from PP4E.Gui.Tools.windows import PopupWindow, MainWindow


class ForceState:
    """
    Mac Tk doesn't set win.state() to 'iconic' when in Dock: do manually,
    by catching window iconify/deiconify events to track visibility state.
    Needed to disable update/redraw when clock minimized to save cpu+memory.

    Caveat: in Tk versions tested, <Map> and <Unmap> do not fire on Linux 
    (except once, to set state to Visible), and Linux Tk also fails to set 
    win.state() like Mac Tk.  Only Windows Tk both fires <Map>/<Unmap> and 
    sets state() correctly (to normal or iconic).  Luckily, this is also a
    moot point: on Linux, CPU is 0% and memory doesn't grow in any state. 
    """
    def __init__(self):
        self.force_state = None
        self.bind('<Map>',   lambda event: self.setState(event, 'Visible'))
        self.bind('<Unmap>', lambda event: self.setState(event, 'Hidden'))

    def setState(self, event, what):
        if event.widget == self:
            trace('setting state to', what)
            self.force_state = what            # enable/ disable redraws


class ClockMain(MainWindow, ForceState):
    """
    A custom Tk, with an attached Clock Frame, which embeds a Canvas.
    """
    def __init__(self, configs=object(), name=''):
        MainWindow.__init__(self, appname, name)
        ForceState.__init__(self)
        clock = Clock(configs, self)
        clock.pack(expand=YES, fill=BOTH)
        self.clock = clock


class ClockPopup(PopupWindow, ForceState):
    """
    A custom Toplevel, with an attached Clock Frame, , which embeds a Canvas.
    """
    def __init__(self, configs=object(), name=''):
        PopupWindow.__init__(self, appname, name)
        ForceState.__init__(self)
        clock = Clock(configs, self)
        clock.pack(expand=YES, fill=BOTH)
        self.clock = clock


class ClockWindow(Clock, ForceState):
    """
    B/W compat: manual window borders, passed-in parent.
    """
    def __init__(self, config=object(), parent=None, name=''):
        Clock.__init__(self, configs, parent)
        ForceState.__init__(self)
        self.pack(expand=YES, fill=BOTH)
        title = appname
        if name: title = appname + ' - ' + name
        self.master.title(title)                # master=parent or default
        self.master.protocol('WM_DELETE_WINDOW', self.quit)


###############################################################################
# Program run
###############################################################################

if __name__ == '__main__':

    from getConfigs import getConfigs      # [SA] common gadgets utility
    defaults = dict(InitialSize='240x240', 
                    DigitalFont=None,      # default family and size
                    DigitalFgColor=None,   # None=FgColor
                    BgColor='beige',       # canvas
                    FgColor='brown',       # ticks 
                    HhColor='black',       # hour hand
                    MhColor='navy',        # minute hand
                    ShColor='blue',        # second hand
                    CogColor='white' ,     # center point
                    PictureFile=defaultImage())  # middle photo path (or None)
    configs = getConfigs('PyClock', defaults)    # load from file or args

    # alternatives
    #myclock = ClockWindow(Tk(), configs)
    #myclock = ClockPopup('popup', configs)

    # parent is Tk root if standalone
    myclock = ClockMain(configs)
    trySetWindowIcon(myclock, 'icons', 'pygadgets')   # [SA] for win+lin

    if RunningOnMac:
        # Mac requires menus, deiconifies, focus

        # [SA] on Mac, customize app-wide automatic top-of-display menu
        from guimaker_pp4e import fixAppleMenuBar
        fixAppleMenuBar(window=myclock,
                        appname='PyClock',
                        helpaction=myclock.clock.onHelp,
                        aboutaction=None,
                        quitaction=myclock.quit)    # app-wide quit: ask

        # [SA] reopen auto on dock/app click and fix tk focus loss on deiconify
        def onReopen():
            myclock.lift()
            myclock.update()
            temp = Toplevel()
            temp.lower()
            temp.destroy()
        myclock.createcommand('::tk::mac::ReopenApplication', onReopen)

    myclock.mainloop()



[Home page] Books Code Blog Python Author Train Find ©M.Lutz