#!/usr/bin/env python3 """ ================================================================================ launch-mergeall-GUI.pyw: desktop/mobile GUI launcher (part of the Mergeall system) See UserGuide.html for version, license, platforms, and attribution. A portable Python 3.X/2.X tkinter/Tkinter desktop GUI, for easily launching mergeall.py's -report and -auto (but not selective/interactive) usage modes. For screenshots of this GUI in action, see the examples/Screenshots folder. Runs on desktop (Windows, macOS, Linux) and mobile (Android, via Pydroid 3). Usage: This is a .pyw -- it runs with no console on Windows. Run this script with no arguments, via icon clicks, command lines, or other. Drag it out to a desktop shortcut for quick access. See folder docetc/launcher-configs for a Windows desktop icon (attach it to a shortcut via right-click + Properties). Uses widgets for choosing options and directory paths and viewing scrollable mergeall stdout/stderr output, and supports saving and viewing mergeall's output in log files. Uses threads to stay active while waiting for mergeall output lines. Stays open to support multiple mergeall runs in one session -- report differences, run automatic updates, report on results, etc. The underlying mergeall.py was not changed: this is just a GUI shell. This GUI is easier than launch-mergeall-Console.py (no directory typing, and quicker options selection), which is easier than running raw mergeall.py command lines directly and manually from a console or shortcuts. This GUI is also run automatically when the frozen app/executable packages are launched. Conversely, mergeall.py command lines still support more options (e.g., console interactive mode, and deltas-set apply/create runs), and can be used as part of a software stack in larger automated solutions like Android Deltas Scripts (see https://learning-python.com/android-deltas-scripts and its _README.html). As of version 1.4, this also uses threading to always remain responsive. As of version 2.0, this also supports the mergeall auto-backups option. As of version 3.0, this also supports mergeall's "-skipcruft" and Mac OS X. See below for more major changes made here, as well as open issues (TBDs). -------------------------------------------------------------------------------- VERSION 3.3 CHANGES: This Mergeall changes the "-quiet" flag to apply to Unicode normalization messages, in addition to its former per-item backup messages role. In support, the GUI here both enables the corresponding toggle in all use cases (instead of just for "-auto" + "-backup" runs), and generalizes the toggle's label. In addition, this toggle no longer belongs in the Backups section, given its broader scope. To address, the toggle was also moved to the Messages section. This leaves a sole Backups toggle, which could move to Run Mode allowing Backups to be dropped, but it's easier to leave it where it is than redesign the GUI and update all its docs for this larger change. For screenshots of the GUI before and after 3.3's minor layout changes, open: docetc/GUI-changes-3.3/index.html Caveat: prior docs will still show the GUI's former layout indefinitely, especially those with screenshots taken on apps no longer being rebuilt. The GUI also disables active-widget-restore tricks on macOS, because they no longer work on recent Pythons/Tks, and can even cause aborts on reopens. Ideally, the GUI would also support 3.2's delta sets creation/application, but this is a major redesign, and the GUI gets less dev focus today because it is no longer usable sans USB access on recent Androids (which ultimately may require a full GUI rewrite in Java to spawn mergeall.py with USB; yuck). For now, deltas GUI support is postponed (perhaps to 3.4). -------------------------------------------------------------------------------- VERSION 3.2 CHANGES (none): This Mergeall added deltas.py change sets, which are not supported in this GUI. A future version may (see 3.3's notes), but these sets are primarily for command-line use today, as employed by the new Android Deltas Scripts package. VERSION 3.1 CHANGES (none): These version made internals and command-line changes only, with no GUI impact. -------------------------------------------------------------------------------- VERSION 3.0 CHANGES: This major Mergeall release's changes were largely driven by a Mac OS X port, and new usage on Linux. Search for 'RunningOnMac' and '3.0' for Mac-ness. Mergeall grew cruft file, symlink, and Windows long-path support in 3.0, but most of these are not related to the GUI managed here. App bar icon for Linux: Linux now gets a nice mergeall app bar (launcher) icon. Mac icons remain a TBD, and Windows has always had window icons for this program. Initial desktop logs folder for both Linux and Mac: The preset default for the logs folder now uses $HOME settings on these Unix platforms (Windows uses the user's desktop as before). Redesign run verification dialog for Linux and Mac: The dialog popped up just before a run is launched was redesigned to make it more readable on Mac and Linux. Its text was formerly fine on Windows, but fairly bunched-up elsewhere. Also specialized the warning text to be less dire if backups are enabled. Top-level window hack for Mac Tk: The Mac port required a top-level window hack to show buttons in Mac active-window style initially, using the recommended Tk 8.5 install. This is a complex story; see __main__ code in this file for details. New toggle to suppress comparison lines, all platforms: The Mac port also inspired a new GUI toggle to suppress folder comparison message scrolling in the GUI (only). This is on initially for Mac because the Mac Tk Text widget is VERY slow: ~30x slower than the mergeall process's output. This toggle is off initially for Windows and Linux, because the GUI largely keeps up on these platforms, and the messages indicate progress. Still, the new toggle is available on these platforms too, because the GUI's scrolling can add a few seconds to mergeall runtime in some tests run on Windows, and may be a factor on slower machines; disable as desired. Either way, full details, including all comparison messages, are always available in saved log files popped up automatically at the end of a run. Note that this new toggle differs from 2.4's "-quiet" option and GUI toggle described below: "-quiet" disables output in mergeall itself before it ever reaches the GUI, because backup messages are arguably distracting in logs too; comparison messages are still useful in logs, if not the GUI. Unlike all other widgets (except the new post-run popup toggle), this toggle is also dynamic: setting it on/off while mergeall is running hides/shows messages currently being generated. The 'skipping' message is displayed every time this toggle is turned back on, though it does not appear until the next mergeall message is received. TBD: Mac text scrolls still seem painfully slow when there are many difference-report lines. These could be also be suppressed, in addition to folder comparison messages, by: line.startswith(('comparing', '[', ' ')). This was ruled out, as it makes report-mode runs useless. Text speed must be fixed by Tk's Mac developers (though one report on it went unreplied). UPDATE: see docetc/miscnotes/mac-weirdly-slow-tk85-text-scroll.py for a simple self-contained demo of the text scrolling slowness on ActiveState Tk 8.5.18 (the Tk recommended by python.org), Python 3.5, and OS X 10.11. It finishes scrolling in just 3-4 seconds on Windows, but 85 on Mac! UPDATE: recent heroic efforts at speeding Mac Text widget scrolling came up short. It's simply slow in all Mac Tks tested - 8.5.9 through 8.5.18. The culprit seems to be update() calls required to redraw the widget after new text is inserted. The code used to scroll uses normal techniques: statustxt.insert(END, line) # add to Text widget statustxt.see(END+'-2l') # reposition text statustxt.update() # update display from within a loop 1) Alternative scroll techniques tried had no effect on speed: statustxt.yview_scroll(2, 'units') statustxt.yview_moveto(1.0) statustxt.yview(END) 2) Using update_idletasks() speeds scrolls only slightly, and all the GUI's controls are unresponsive while the text scrolls (an unacceptable effect): statustxt.update_idletasks() 3) The after() timer loop's speed proved irrelevant, as the code mostly stays in the 'batch' inner loop, and doesn't reschedule after() events: statustxt.after(10, streamconsumer, linequeue, logfile, logpath) 4) The update() call can be avoided by processing just 1 line per after() event (instead of the batch inner loop) with a very low delay count, but this has NO impact on scroll speed - implicit updates are also too slow: statustxt.after(1, streamconsumer, linequeue, logfile, logpath) 5) This leaves disabling scrolls (the new toggle), or updating only after groups of lines are added, which makes scrolling too jumpy and chaotic: global upcnt upcnt += 1 if upcnt % 50 == 0: statustxt.update() In the end, this is a Mac Tk bug, which hopefully is or will be addressed in later Tks on the Mac. The new toggle is mergeall's best workaround. New script to delete Mac ".*" cruft files: The Mac platform has a habit of creating lots of metadata files and a few folders, whose names all start with a ".", and which are sometimes treated as hidden. These have meaning and purpose on a Mac, but are useless noise on other platforms. For mergeall users on Windows and Linux whose archive might become infected with these files by an association with a Mac, a new script, "nuke-cruft-files.py," is provided to remove such files as a pre-step or post-step to mergeall runs that copy archives off a Mac See that script's extensive docstring for more details. Disable widgets instead of erasing them, all platforms: Also partly for the Mac, the GUI was redesigned to enable/disable widgets as they fall in and out of relevance, instead of drawing/erasing them with pack() and pack_forget(). The latter scheme causes a noticeable flash on a Mac, but may have been too dramatic in general. The "GO" button didn't cause flash, but enabling/disabling worked around an old text scroll issue. New mode and toggle to skip system cruft files in both TO and FROM Inspired by the numerous ".*" files added to archives on Mac OS X, both mergeall and diffall grew a "-skipcruft" command line argument and mode, which skips known cruft files of all platforms in both FROM and TO. The GUI also grew a new toggle to support this new mode and argument. When enabled, the net effect is that cruft files do not register as differences in report runs, and are not copied to, removed from, or replaced in the TO tree in update runs. Such files thus stay on their generating platform only - they won't be transferred to intermediate drives and other computers, and won't be deleted from the generating platform by future merges. This option can be disabled, because users of a single platform may not care about their cruft, and some crufts may be more useful than others. Cruft files are defined in the mergeall_config.py file; users are encouraged to adjust the set of matching files as needed for their use. Sanitize Unicode characters in message-scroll text outside Tk's BMP range Through 8.6, at least, Tk cannot display Unicode characters whose code points are outside range U+0000..U+FFFF (BMP, UCS-2). Passing these to Tk raises an uncaught exception which leaves the GUI in an unpredictable state (usually half-drawn or hung). To work around this, replace all such characters with the standard Unicode replacement character, which renders as a generic indicator. This limit may be lifted in Tk 8.7. Update: also an issue for pathnames in Browse folder dialogs, fixed here. More descriptive GUI labels and popups Toggle labels and popups were given more explicit labels for clarity. Allow editor popup to be disabled: config setting, new toggle The automatic popup of a text editor to view a saved mergeall log file can now be disabled by a setting in mergeall_configs.py (on by default). The popup may seem overkill to users who require a view-only display. UPDATE: this is now switchable on/off by a new toggle in the GUI itself. The configs-file setting is retained, but used only for an initial value for the new toggle. The popup is normally unused, and GUI clicks are easier than config edits. This new toggle is dynamic, like the message scroll disabler; you can click it during a run to impact the popup. Configurable message text display area Users can now tailor the color, font, and initial size of the GUI's scrolled display area for mergeall messages, in mergeall_configs.py. The GUI's cosmetics were also polished in general along the way. Mac changes to open dialogs (Browse for folders TO, FROM, Logs) On Macs, use "message" ("title" is ignored), and add slide-down sheet style (versus popup window) via "parent=root" if configs file setting. Save dialogs seems to post titles correctly. Mac's standard menu is customized here too, even though this program doesn't have one per se. mergeall and cpall: copy symlinks, don't follow Avoid redundant data copies. See mergeall.py and cpall.py for details. Refocus on window after standard/common dialogs for Mac Run a root.focus_force() after all standard dialogs used here to force focus and active styling to be reset after dialog is closed. Else, Mac users must click window after dialogs. This may be AS Tk 8.5 only; has no effect on the initial style issue discussed earlier; and parent=root doesn't suffice to reset focus in the standard dialogs used here. TBD: should these dialogs also be made slide-down sheets on Mac like the open/save dialogs via parent=root, instead of modal popup windows? Some Mac purists may vote yea, but mergeall currently sides with variety, especially given that the mergeall GUI is a single-window interface. mergeall output forced to ASCII in PyInstaller Windows exes A continuation of a long-running theme here, mergeall now forces stdout text to be ASCII when running as a frozen executable on Windows ONLY. See mergeall.py for details; this works around a likely PyInstaller bug. -------------------------------------------------------------------------------- VERSION 2.4 CHANGES: Added a toggle for the new "-quiet" script option that turns off per-file "...backing up" log message printing in the underlying mergeall program. These messages may make reading the log more work than it should be, and don't add much information once backups are understood. The new "-quiet" toggle in the GUI is active only when backups are enabled. Also tweaked some GUI labels' text for clarity. -------------------------------------------------------------------------------- VERSION 2.2 and 2.3 CHANGES: These versions were optimizations and patches, and had no impact on this GUI. VERSION 2.1 CHANGES: This version's changes were command-line only, and had no effects in this GUI. -------------------------------------------------------------------------------- VERSION 2.0 CHANGES: 1) Added Backups toggle and corresponding mergeall -backup command-line arg. The new backups frame retains state but is shown/hidden when -auto is selected/deselected, as it's applicable to -auto only here (not -report). In the console launcher, -backup is applicable to both -auto and selective (=[not -report]) modes. Defaults to enabled in the GUI. 2) Moved log-file path chooser widgets down to log-file toggle button section. The chooser retains state but is now shown/hidden when logging is toggled on/off, as it's applicable when logging only. 3) Added "help" button that pops up the main usage doc in a web browser. Packed in the run-mode frame's unused space. 4) The new "finished\n\n" message issued by mergeall solves the issue here where the last output line was covered by repacking the GO button after resizes. This works better, as it doesn't require the text display to scroll abruptly. 5) Use more descriptive text for the mode radio buttons (formerly was just not "Report Only" and "Automatic Updates"). 6) Error-check the FROM and TO paths here before trying to run mergeall, so produces a GUI popup instead of mergeall text output message (log file path was already being checked this way, because it required an open here). 7) Default to Desktop for log files on Windows (initial setting value only). -------------------------------------------------------------------------------- VERSION 1.7.1 CHANGES: None here (usage note, mergeall error message format change: see Revisions.html). VERSION 1.7 CHANGES: 1) Minor bug fix for 2.X only -- add import of Tkinter's showerror when using 2.X, else dialog never appears if bad log-file name. 2) Catch PermissionError (etc.) on log file open and report error in popup; else fails silently on Windows, as ".pyw" has no console for exception text. -------------------------------------------------------------------------------- VERSION 1.6 CHANGES: a new Python 2.X Unicode workaround, verify quits Decoding an output line read from the spawned mergeall.py stream can fail rarely in Python 2.X (only), if there are non-ASCII filename characters in the line. This failure does not occur in 3.X. Patched to catch the decode error and recover, with a notation at the front of the line in the GUI. This change impacts text in the GUI display only; it does not impact text in the log file, or the underlying mergeall process's run. Also added a verify dialog for the window close button to avoid accidental exits. [Discussion] The Unicode decode failure seems a fundamental 3.X/2.X behavior difference. After initial research, the most likely culprits appear to be: a) The 2.X subprocess module mutates non-ASCII stream lines in transit. b) The 2.X print statement generates either already-decoded text, or wrongly-encoded bytes that do not respect the PYTHONIOENCODING setting. In either case, this may reflect the fact that there is no notion of a truly binary bytes stream in 2.X -- the content of subprocess output is not as clearly defined as in 3.X's strict text/bytes dichotomy worldview. Note that setting PYTHONIOENCODING in the shell does not fix this failure. This environment variable is used for prints to the console, but this GUI does only screen updates and log file writes itself, and exports this setting automatically to the mergeall subprocess for its prints. Testing verifies that os.environ settings are inherited by 2.X subprocesses too, even when spawned by the subprocess module. Whatever the exact cause, this is a fundamental 2.X/3.X difference and another 2.X/3.X semantic incompatibility that goes well beyond syntax. In this case, adding an exception handler around the offending decode fixed the failure for all cases tested in 2.X, so this issue is closed. Given the other 2.X/3.X incompatibilities found and addressed in this project, though, it seems that writing dual-version 2.X+3.X code may be a bit of a pipedream when it comes to realistic, practical programs. If in doubt, try running mergeall on Python 3.X instead of 2.X. Also note that this patch applies only to the GUI launcher: PYTHONIOENCODING must still generally be set manually in your system shell when running either the console launcher or script mergeall.py directly from a command line, if either may ever process and thus print non-ASCII filenames. Else, such filenames may cause both scripts to abort, especially in Python 3.X. The GUI launcher does not require this setting, as it automatically sets and propagates this variable to its mergeall.py subprocess, and never prints. [3.0] UPDATE: for its PyIstaller frozen executable ONLY, mergeall must force stdout text to ASCII, which isn't ideal, but is required in this context to avoid exceptions, and sidesteps this entire display-only issue. [3.2] UPDATE: PYTHONIOENCODING still matters in Py 3.6+. 3.6 made the Windows console UTF8, by stdout redirected to a file is still the Windows code page. -------------------------------------------------------------------------------- VERSION 1.5 CHANGES: Linux port and usage Pass shell=False on Linux/Unix (only) to subprocess.Popen, else starts a "python" interactive shell even though a full command sequence is passed. Linux users: see also release 1.5 notes in docs/Revisions.html for "#!" pointers, and a possible NTFS timestamp skew issue for Windows/Linux cross-platform syncs. -------------------------------------------------------------------------------- VERSION 1.4 CHANGES: threads, streams, log files 1) Threading Add threading for the mergeall subprocess stream reader, a former TBD. This structure is more complex -- it trades a simple loop for two functions and multiple levels of loops -- but prevents the GUI from becoming blocked and unresponsive while waiting for a next line from the subprocess. This was normally not an issue: the GUI updates after each new line, and is used just for viewing after starting a mergeall run (and it's "GO" button is erased during this process to avoid overlapping runs). However, blocked states are not natural in GUIs, and they can become apparent here if mergeall is busy copying large trees. See book (PP4E) for more details and examples of GUI threading. 2) Stream Unicode decoding, take 2 Force UTF8 encoding for prints in mergeall subprocess via PYTHONIOENCODING, and use binary mode Popen stream reads + manual post-read UTF8 decoding here. Version 1.2 formerly made the subprocess's streams encoding match Popen's expectation (cp1252 on Windows) by using locale.getpreferredencoding(False); that works for reading the stream, but not for prints within mergeall itself. 3) 2.X log-file compatibility Use binary mode for the log files, writing the now-binary stream data. Former versions allowed for non-ASCII filenames in the log file text by using text mode and writing characters as UTF8, but the 2.X codecs.open() doesn't expand \n the same as 3.X's text-mode open() (see also next item). 4) 2.X unbuffered streams compatibility Temporarily dropped '-u' in mergeall spawn command-line, as it makes eolns (linebreak character sequences) \n in 2.X, but \r\n in 3.X. This causes issues in log-file writes: without a \r\n on Windows, files are single lines. LATER UPDATE: the Python '-u' unbuffered flag has been reinstated. Without it, mergeall output may not appear for 10 or more seconds on some machines and slower devices due to internal buffering. Because this flag also makes line-breaks differ between Python 2.X and 3.X, though, also need to use special-case log-file writes to map all linebreaks to the platform's version. This is a complex 3.X/2.X compatibility issue, involving -u, Popen, and opens. Neither Popen text mode streams nor the 2.X codecs.open() will help. The former can't be used because its internal encoding policy (per locale) is not broad enough to handle arbitrary Unicode filenames, and the latter always opens in binary mode, and so does no translations of linebreaks in the decoded text (3.X's text-mode open() does expand linebreaks by default). Could write binary lines in 2.X text-mode open() to expand \n on Windows, but that won't work in 3.X -- its text-mode open() expects Unicode strings, and always does encoding in addition to linebreaks. Writing manually encoded text via open() in 3.X and codecs.open() in 2.X also won't work: 2.X requires manual \r\n, which 3.X will by default expand to \r\r\n. 3.X open() supports a 'newline' argument to turn off \n expansion, but this can't be used either, as it's not available in 2.X's codecs.open(). As in, *punt*: write binary data with manual \n mapping for log files. -------------------------------------------------------------------------------- TBD 1: No selective/interactive mode in GUI mergeall's interactive mode is unavailable here as is, due to an outstanding TBD regarding handling and interleaving of stdout for input() prompts in spawned Python 3.X processes. In practice, though, the -report and -auto modes have been the only modes regularly used. See launch-mergeall-Console.py for details on the issue (the console launcher supports interactive mode, but without a log file). TBD 2: 2.X compatibility for Unicode filenames? This system works well on 3.X (it's main usage platform) and is largely 2.X compatible, but the launchers may still have issues in some stream decoding for non-ASCII filenames. No such encoding exceptions occur on 2.X for the raw mergall.py script, though some non-ASCII os.listdir results seem a bit suspect in 2.X too. More complete 2.X testing remains suggested exercise. --> Update: see also the 1.6 decoding change note above. <-- TBD 3: Decoupled versus single-process models? The launchers currently use a standard decoupled model that spawns mergeall and reads and decodes its streams. There may be advantages to using a single-process model that instead imports and calls mergeall's functions. See docs\Lessons-Learned.html for more discussion on this alternative. ================================================================================ """ ################### # CODE STARTS HERE ################### #from __future__ import print_function # 2.X compatibility: not needed here # [3.0] for frozen app/exes, fix module+resource visibility (sys.path) import fixfrozenpaths APPNAME = 'mergeall' from __version__ import VERSION # don't hardcode [3.2] import sys if sys.version[0] >= '3': # Py 3.X, but allow for Py 4.X too [3.0] import _thread, queue from tkinter import * from tkinter.messagebox import askokcancel, showinfo, showerror from tkinter.filedialog import Directory # saves last dir from tkinter.scrolledtext import ScrolledText else: import thread as _thread, Queue as queue # Py 2.X compatibility from Tkinter import * from tkMessageBox import askokcancel, showinfo, showerror # [1.7] from tkFileDialog import Directory from ScrolledText import ScrolledText #import codecs #open = codecs.open # [1.4] log binary mode from stream, not text files # this script isn't too platform-specific, but avoid repeating this RunningOnMac = sys.platform.startswith('darwin') RunningOnWindows = sys.platform.startswith('win') RunningOnLinux = sys.platform.startswith('linux') import webbrowser, subprocess, time, os # [3.0] data+scripts not in os.getcwd() if run from a cmdline elsewhere, # and __file__ may not work if running as a frozen PyInstaller executable; # use __file__ of this file for Mac apps, not module: it's in a zipfile; MYDIR = fixfrozenpaths.fetchMyInstallDir(__file__) # absolute # [3.3] (Oct22) use the online doc for help, not the local file: local has # links to files that won't be present in frozen Windows/Linux exes, and the # online version is likely to be more current; assume the URL won't change; HELPFILE = 'https://learning-python.com/mergeall-products/unzipped/UserGuide.html' """CUT # [3.0] new doc, in this script's folder - but not necessarily '.' (cwd) HELPFILE = os.path.join(MYDIR, 'UserGuide.html') # [3.0] Mac OS X is pickier about file URLs if RunningOnMac: HELPFILE = 'file:' + HELPFILE CUT""" # [3.0] part of PP4E's guimaker module, copied here to avoid dependency from guimaker_pp4e import fixAppleMenuBar # [3.0] user configs: scrolled messages text area, log-file popup; # for GUI settings, None = use Tk defaults: 24 lines high, 80 chars wide; try: from mergeall_configs import ( LOGEDITORPOPUP, DEFAULTLOGDIR, MACSLIDEDOWN, TEXTAREAHEIGHT, TEXTAREAWIDTH, TEXTAREAFONT, TEXTAREACOLOR) except Exception as why: # if any fail, all default (brutal, but simple) LOGEDITORPOPUP = False # default: initial value for log-file popup toggle DEFAULTLOGDIR = None # default: Desktop folder, per running platform MACSLIDEDOWN = False # default: popup, not sheet, for folder dialogs TEXTAREAHEIGHT = 20 # initial number lines in message scroll widget TEXTAREAWIDTH = None # initial number characters per line, wrapped TEXTAREAFONT = None # scrolled messages font, None=Tk default font TEXTAREACOLOR = None # scrolled messages color(s), None=Tk default font print('Error in config file: %s' % why) # to console, if any def fixTkBMP(text): """ [3.0] (copied from PyMailGUI) Tk <= 8.6 cannot display Unicode characters outside the U+0000..U+FFFF BMP (UCS-2) code-point range, and generates uncaught exceptions when tried (emojis kill programs!). To address this, call this function to sanitize all text passed to the GUI for display. It replaces any non-BMP characters with the standard Unicode replacement character U+FFFD, which Tk displays as a highlighted question mark diamond. This workaround is coded to assume that Tk 8.7 will lift the BMP restriction, per a dev rumor. It also assumes TkVersion has been imported from tkinter. Use here: filenames in mergeall messages scrolled text (rare, but true). """ if TkVersion <= 8.6: text = ''.join((ch if ord(ch) <= 0xFFFF else '\uFFFD') for ch in text) return text def isNonBMP(text): """ [3.0] Return true if any char (codepoint) in text is outside Tk's BMP range. Used by folder dialogs to force initialfile=None when True for prior choice. """ if TkVersion <= 8.6: return any(ord(ch) > 0xFFFF for ch in text) else: return False # and assume Tk 8.7 will make this better... def refocusWindow(): """ [3.0] Call after (most) standard dialogs to reset focus on the main window, else focus and active style are lost when dialog is closed. This may be a bug in ActiveState Tk 8.5 (TBD), but the fix is simple. """ root.focus_force() # else Mac requires a user click after dialogs #################################################################################### # GUI BUILDER #################################################################################### # font for headers in controls sections; # could be user-configurable too, but seems arguably-better hardwired; # [2.4] not just 'bold': if used as family name, Tk falls back on arial! # [3.0] smaller font on Linux, else looks almost cartoonish; HDRFONT = 'arial 14 bold' # ('family', size, 'style? style?...') if RunningOnLinux: HDRFONT = 'arial 12 bold' # smaller is better on Linux (not Mac, Windows) def makewidgets(root): """ build the gui window, setup Browse/GO/other event handlers """ # used on GO global dirents # folders global modevar, logvar, bkpvar # settings global quietvar, cmpmsgsvar, cruftvar, logpopupvar # more settings global gobutton, statustxt # widgets #--------------------------------------------------------------------------- # Event handlers (less ongobutton: ahead) #--------------------------------------------------------------------------- # some use names in the enclosing func (which are actually globals) # one open dialog for entire run, saved in scope (closure) opendirdlg = Directory() def onbrowse(field, label): title = 'Choose mergeall %s folder' % label if RunningOnMac: # [3.0] specialize on Mac if MACSLIDEDOWN: # Mac: message (title ignored), slidedown window, custom text title = 'Choose mergeall %s folder' % label dlgkargs = dict(message=title, parent=root) else: # Mac: message (title ignored), popup window, standard text title = 'mergeall: Choose %s Folder' % label dlgkargs = dict(message=title) else: # Windows+Linux: the usual modal popup, standard text title = 'mergeall: Choose %s folder' % label dlgkargs = dict(title=title) # check prior pathname choice for emojis: kills dialog prior = opendirdlg.options.get('initialdir', '') if isNonBMP(prior): dlgkargs.update(dict(initialdir=None)) # for this call only chosendir = opendirdlg.show(**dlgkargs) if chosendir: field.delete('0', END) field.insert(END, fixTkBMP(chosendir)) refocusWindow() # [3.0] else requires click on Mac def onquit(): answer = askokcancel('%s: Verify Exit' % APPNAME, 'Really quit mergeall now?') if answer: win.quit() # win in enclosing scope; or sys.exit() else: refocusWindow() # [3.0] else requires click on Mac def onmodetoggle(): if modevar.get().startswith('REPORT'): # hide/show backups frame [2.0] # [3.0] bkpfrm.pack_forget() # -backup applies to -auto updates only bkpbtn.config(state=DISABLED) # enabled/disable widgets [3.0] # quietbtn.config(state=DISABLED) # [3.3] now normalization msgs too cruftbtn.config(text='Skip cruft items in FROM and TO: ' 'do not report as differences?') else: # [3.0] bkpfrm.pack(expand=YES, fill=X) bkpbtn.config(state=NORMAL) # quietbtn.config(state=NORMAL) # [3.3] it's always normal now cruftbtn.config(text='Skip cruft items in FROM and TO: ' 'do not copy, replace, or delete?') def onlogtoggle(): if logvar.get(): # show/hide log-file path chooser [2.0] # [3.0] logdirfrm.pack(expand=YES, fill=X) # chooser applies only if logging logbtn.config(state=NORMAL) logent.config(state=NORMAL) logpopupbtn.config(state=NORMAL) # [3.0] ditto for log-popup toggle else: # [3.0] logdirfrm.pack_forget() logbtn.config(state=DISABLED) logent.config(state=DISABLED) logpopupbtn.config(state=DISABLED) def onbkptoggle(): if bkpvar.get(): # show/hide -quiet toggle button [2.4] # [3.0] quietbtn.pack(anchor=NW) # -quiet applies only if doing backups, quietbtn.config(state=NORMAL) # and whether saving to log file or not else: # [3.0] quietbtn.pack_forget() # quietvar.set(False) # keep prior setting, disabled=moot [3.0] # quietbtn.config(state=DISABLED) # [3.3] now normalization msgs too pass def onhelp(): # [3.3] now opens online version webbrowser.open(HELPFILE) # popup local file in web browser #--------------------------------------------------------------------------- # Build the GUI (link to handlers) #--------------------------------------------------------------------------- # # MAIN WINDOW # win = root # [3.0] allow for TopLevel() or Tk() win.title('mergeall %.1f' % VERSION) # set main window title, [1.6] version win.protocol('WM_DELETE_WINDOW', onquit) # [1.6] catch/verify window close # replace red (no, blue...) tk icon? iconfolder = os.path.join(MYDIR, 'icons') try: if RunningOnWindows: # try Windows window icon icnpath = os.path.join(iconfolder, 'mergeall.ico') win.iconbitmap(icnpath) elif RunningOnLinux: # try Linux app-bar icon [3.0] icnpath = os.path.join(iconfolder, 'mergeall.gif') imgobj = PhotoImage(file=icnpath) win.iconphoto(True, imgobj) elif RunningOnMac or True: # Mac OS X: neither of the above works (yet?) [3.0] # Macs require apps for most icon contexts raise NotImplementedError except Exception as why: # punt: bad file/call or platform (Mac OS X TBD) pass # [3.0] on Mac, customize app-wide automatic top-of-display menu fixAppleMenuBar(root, 'mergeall', helpaction=onhelp, aboutaction=None, quitaction=onquit) ctrlfrm = Frame(win) ctrlfrm.pack(expand=NO, fill=BOTH, side=TOP) # # MAIN TO/FROM DIRECTORY CHOOSERS # dirsfrm = Frame(ctrlfrm, relief=GROOVE, border=3) dirsfrm.pack(fill=X) Label(dirsfrm, text='Main Folders', font=HDRFONT).pack() # [2.4] font rowsfrm = Frame(dirsfrm) rowsfrm.pack(expand=YES, fill=X) rowsfrm.columnconfigure(1, weight=1) dirents = {} for (row, key) in enumerate(('FROM', 'TO')): rowsfrm.rowconfigure(row, weight=1) Label(rowsfrm, text=key + ' folder:').grid(row=row, column=0, sticky=E) dirent = Entry(rowsfrm) dirent.insert(END, 'enter or browse...') dirent.grid(row=row, column=1, sticky=EW) handler = lambda dirent=dirent, key=key: onbrowse(dirent, key) # current! Button(rowsfrm, text='Browse...', command=handler).grid(row=row, column=2) dirents[key] = dirent # # RUN MODE RADIO BUTTONS: -report or -auto # radiofrm = Frame(ctrlfrm, relief=GROOVE, border=3) radiofrm.pack(fill=X) Label(radiofrm, text='Run Mode', font=HDRFONT).pack() # help button: use empty space in run mode frame [2.0] helpbtn = Button(radiofrm, text='Help', command=onhelp, relief=FLAT, bg='white') helpbtn.pack(side=RIGHT, anchor=NE) modevar = StringVar() modes = ['REPORT: show differences between FROM and TO but do not change anything', 'UPDATE: automatically resolve differences in TO to make it the same as FROM'] for mode in modes: Radiobutton(radiofrm, text=mode, variable=modevar, value=mode, command=onmodetoggle).pack(side=TOP, anchor=NW) modevar.set(modes[0]) # [3.0] skip cruft files in FROM and TO - don't copy/remove/replace or report cruftvar = BooleanVar() cruftvar.set(True) cruftbtn = Checkbutton(radiofrm, text='Skip cruft items in FROM and TO: ' 'do not report as differences?', variable=cruftvar) cruftbtn.pack(side=LEFT) # # MESSAGE TOGGLES + LOG DIRECTORY CHOOSER # msgfrm = Frame(ctrlfrm, relief=GROOVE, border=3) msgfrm.pack(fill=X) Label(msgfrm, text='Messages', font=HDRFONT).pack() logfrm = Frame(msgfrm) logfrm.pack(side=TOP, fill=X) logvar = BooleanVar() # Intvar works too logvar.set(True) # else default=off (eibti) Checkbutton(logfrm, text='Save mergeall messages to log file? ', variable=logvar, command=onlogtoggle).pack(side=LEFT) # log-file dir chooser by toggle, unhide/hide when toggled on/off [2.0] key = 'Logs' logdirfrm = Frame(logfrm) logdirfrm.pack(expand=YES, fill=X) handler = lambda: onbrowse(logent, key) # last, not current (!) [see above] logbtn = Button(logdirfrm, text='Browse...', command=handler) logbtn.pack(side=RIGHT) logent = Entry(logdirfrm) logent.insert(END, 'select or enter log file folder...') logent.pack(side=LEFT, expand=YES, fill=X) dirents[key] = logent # [3.0] logdirfrm.pack_forget() # till logs toggled on (now enabled/disabled) logbtn.config(state=NORMAL) # till logs toggled on (initially are) logent.config(state=NORMAL) # fill initial/default logs folder value if DEFAULTLOGDIR and os.path.exists(DEFAULTLOGDIR): # [3.0] allow user-configs file to give initial default defaultlogs = DEFAULTLOGDIR else: try: # [2.0] try user's Desktop on Windows (has HOMEPATH but no HOME) defaultlogs = r'C:\Users\%s\Desktop' % os.environ['username'] assert os.path.exists(defaultlogs) except: try: # [3.0] try same on Linux and Mac OS X (TBD: or Documents?) defaultlogs = os.path.join(os.environ['HOME'], 'Desktop') assert os.path.exists(defaultlogs) except: defaultlogs = None if defaultlogs: # may be unset or fail on some Unix and Windows => empty, Browse logent.delete('0', END) logent.insert(END, defaultlogs) # [3.0] allow comparison messages to be suppressed (mostly for Mac speed); # this toggle (only) is dynamic: can change effect while output scrolling; cmpmsgsvar = BooleanVar() if RunningOnMac: cmpmsgsvar.set(True) # initial=suppress on Mac OS X else: cmpmsgsvar.set(False) # off on Windws/Linux: fast GUI Checkbutton(msgfrm, text='Hide folder comparison messages in the GUI ' 'for speed?', variable=cmpmsgsvar).pack(side=TOP, anchor=NW) # [3.3] new layout # [3.0] allow log-file popup editor to be suppressed in the GUI per run; # the configs-file entry added previously now give an inital value only; logpopupvar = BooleanVar() logpopupvar.set(LOGEDITORPOPUP) logpopupbtn = Checkbutton(msgfrm, text='Show log-file popup after run?', variable=logpopupvar) logpopupbtn.pack(side=RIGHT, anchor=NE) # [3.3] new layout # [3.3] move -quiet toggle to Messages: it's now for normalization too # [2.4] support -quiet mode: omit "...backing up" per-file log messages quietvar = BooleanVar() quietbtn = Checkbutton(msgfrm, # [3.3] -quiet now also hushes Unicode normalization messages #text='Suppress per-item backup messages in both ' # 'the GUI and log for brevity?', text='Hide backup and normalization messages in ' 'GUI and log for brevity?', variable=quietvar) quietbtn.pack(side=TOP, anchor=NW) quietvar.set(True) # init=suppress (though good for errors+feedback) # [3.0] quietbtn.pack_forget() # only if backups selected, but will be initially # quietbtn.config(state=DISABLED) # till -auto AND -backups selected quietbtn.config(state=NORMAL) # [3.3] it's always on now, for normalization too # # BACKUP TOGGLE # # enable/disable when '-auto' run mode selected/deselected [2.0] [3.0] bkpfrm = Frame(ctrlfrm, relief=GROOVE, border=3) bkpfrm.pack(fill=X) Label(bkpfrm, text='Backups', font=HDRFONT).pack() bkpvar = BooleanVar() bkpbtn = Checkbutton(bkpfrm, text='Backup TO items replaced or deleted, and note items added to TO?', variable=bkpvar, command=onbkptoggle) bkpbtn.pack(anchor=NW) bkpvar.set(True) # initial=on: do backups # [3.0] bkpfrm.pack_forget() # till -auto selected bkpbtn.config(state=DISABLED) # till -auto selected # [3.3] move -quiet toggle to Messages: it's not just for Backups anymore # [2.4] support -quiet mode: omit "...backing up" per-file log messages # # SCRIPT OUTPUT: SCROLLED TEXT + 'GO' BUTTON # # 'go' always hidden during run to prevent overlapping run launches statustxt = ScrolledText(win) # [3.0] user configs: size, font, color (no border - color sets off better) try: if TEXTAREAHEIGHT: statustxt.config(height=TEXTAREAHEIGHT) if TEXTAREAWIDTH: statustxt.config(width=TEXTAREAWIDTH) if TEXTAREAFONT: statustxt.config(font=TEXTAREAFONT) if TEXTAREACOLOR: if isinstance(TEXTAREACOLOR, str): statustxt.config(bg=TEXTAREACOLOR) elif isinstance(TEXTAREACOLOR, tuple): statustxt.config(bg=TEXTAREACOLOR[0]) statustxt.config(fg=TEXTAREACOLOR[1]) else: print('Bad color value in config file') except Exception as why: print('Bad config setting: %s' % why) gobutton = Button(win, text='GO: run mergeall', font=HDRFONT, command=ongobutton) gobutton.pack(side=BOTTOM) statustxt.pack(side=TOP, expand=YES, fill=BOTH) # pack last=clip first #################################################################################### # ON "GO": SPAWN MERGEALL PROCESS #################################################################################### # [1.4] how spawned mergeall subprocess's text is written and decoded here STREAM_ENCODE = 'utf8' EOF_SENTINEL = [] # stream lines read will never be a list linequeue = queue.Queue() # infinite-size shared queue of objects def ongobutton(): """ on GO button press: fetch gui values, confirm run, launch mergeall """ # set in makewidgets global dirents # folders global modevar, logvar, bkpvar, quietvar, cruftvar # settings global gobutton, statustxt # widgets # [3.0] for comparison message suppresssion global firstcompareline firstcompareline = True # reset before each run # get inputs from GUI fromdir = dirents['FROM'].get() # directory fields todir = dirents['TO'].get() logdir = dirents['Logs'].get() mode = modevar.get() # runmode radiobtn dolog = logvar.get() # logfile checkbtn dobkp = bkpvar.get() # backups checkbtn quiet = quietvar.get() # quiet mode checkbtn [2.4] docruft = cruftvar.get() # skip cruft files [3.0] # config run modearg = '-report' if mode.startswith('REPORT') else '-auto' if dobkp and modearg != '-report': modearg += ' -backup' # [2.0] backup replacements/removals #if quiet: # [3.3] no longer just for backup+auto # modearg += ' -quiet' # [2.4] no per-file backup log messages if quiet: modearg += ' -quiet' # [3.3] now for all modes: normalization too if docruft: modearg += ' -skipcruft' # [3.0] ignore cruft files in FROM and TO if not dolog: logpath = logfile = None else: datestamp = time.strftime('date%y%m%d-time%H%M%S') logpath = logdir + os.sep + 'mergeall-%s.txt' % (datestamp) # confirm run if modearg.startswith('-report'): runtype = 'REPORT-ONLY RUN' warning = 'This run will not change anything.' else: runtype = 'AUTO-UPDATE RUN' warningbase = ( '*CAUTION*: by design, this may change your TO folder tree in-place, ' 'by adding, replacing, and deleting files and folders as needed ' 'to make TO the same as FROM.') warningmore = ( ' Because backups are disabled, any such changes ' 'will be permanent and irrevocable.') warning = warningbase + ('' if dobkp else warningmore) confirm = askokcancel('%s: Confirm Run' % APPNAME, 'About to run:\n' 'mergeall.py %s\n\n' 'FROM:\n%s\n\n' 'TO: \n%s\n\n' 'Logging output to:\n%s\n\n' '%s\n\n' 'Start this %s?' % (modearg, fromdir, todir, logpath, warning, runtype)) notruntitle = '%s: Not Run' % APPNAME if not confirm: showinfo(notruntitle, 'The mergeall run was cancelled.') elif not os.path.exists(fromdir): showerror(notruntitle, 'Please select a valid mergeall FROM folder.') # [2.0] popup elif not os.path.exists(todir): showerror(notruntitle, 'Please select a valid mergeall TO folder.') # [2.0] popup elif dolog and not os.path.exists(logdir): showerror(notruntitle, 'Please select a valid mergeall log file folder.') # or sooner? else: # [1.4] log uses binary mode for now-binary data from stream if dolog: try: logfile = open(logpath, 'wb') except: # [1.7] catch PermissionError and show popup (else silent for .pyw) showerror(notruntitle, 'Please select a writeable log file folder.') return # proceed with mergeall # [3.0] gobutton.pack_forget() # hide/erase button gobutton.config(state=DISABLED) # keep but disable statustxt.delete('1.0', END) # clear last run text # [1.4] force UTF8 prints in mergeall, use binary streams + manual decode here; # this setting is inherited by the spawned mergeall subprocess for its prints; os.environ['PYTHONIOENCODING'] = STREAM_ENCODE # config mergeall command (sequences: auto-quoted by subprocess) extras = {} if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux): # pyinstaller exe [3.0] # run frozen executable directly, not script through python freezename = 'mergeall.exe' if RunningOnWindows else 'mergeall' mergeallpath = os.path.join(MYDIR, freezename) os.environ['PYTHONUNBUFFERED'] = 'True' # -u equiv (iff env?) cmdseq = [mergeallpath, # frozen executable fromdir, # '/' ok on Windows todir] + modearg.split() # 'a b?' -> ['a', 'b'?] if RunningOnWindows: # else spawn hangs unless launcher uses --console (with popup!) # startupinfo, env=os.environ, and creationflags are irrelevant extras.update(stdin=subprocess.DEVNULL) else: # py2app Mac app or source (original code) # relative script path works whether run here or via desktop shortcut # [3.0] but not in os.getcwd() if run from a cmdline elsewhere scriptname = 'mergeall.py' mergeallpath = os.path.join(MYDIR, scriptname) cmdseq = [sys.executable, '-u', # [1.4] need -u unbufferred mergeallpath, # script file (app or source) fromdir, # '/' ok on Windows todir] + modearg.split() # 'a b?' -> ['a', 'b'?] # [1.5] shell should be True on Windows so that it uses filename associations, # but False on Linux so that it doesn't just start a "python" interactive shell doshell = RunningOnWindows # spawn mergeall # use subprocess: os.popen/spawnv not enough, popen2 is 2.X only; subproc = subprocess.Popen( cmdseq, # a string cmd may fail on Unix shell=doshell, # [1.5] see note above, platform specific universal_newlines=False, # [1.4] binary mode, manual decode/eoln stdout=subprocess.PIPE, # capture sub's stdout here stderr=subprocess.STDOUT, # route sub's stderr to its stdout **extras) # read and process mergeall's output: reader thread + timer-based poller _thread.start_new_thread(streamreader, (subproc.stdout, linequeue)) streamconsumer(linequeue, logfile, logpath) # returns here immediately: a thread and timer-event loop are now running # [3.0] for all cases, else requires click on Mac refocusWindow() #################################################################################### # MERGEALL PROCESS HANDLER: THREAD + POLLER #################################################################################### def streamreader(stream, linequeue): """ [1.4] In a parallel thread - read the mergeall subprocess's stdout/stderr stream, and post its lines to a queue for the GUI to read and display on timer event callbacks; this way, the GUI isn't blocked/paused during long-running copies or other actions in the spawned mergeall script; """ for line in stream: # read stdout+stderr lines: may block this thread linequeue.put(line) # place on queue to be picked up by GUI thread linequeue.put(EOF_SENTINEL) # write sentinel at eof and exit: subprocess ended def streamconsumer(linequeue, logfile, logpath): """ [1.4] In the main GUI thread - run a timer-based loop to poll for, read, and display and log stream lines from the shared thread queue until the reader thread sends the end-signal sentinel value on the queue. The main GUI thread running this code thus remains active between mergeall output lines. A nested loop is also used here to process lines in batches so the GUI's response to lines is quick, but it calls update() to remain active. At this point we have 2 threads and a process (connected by queue and stream) and 3 or 4 loops going at once -- the main GUI thread runs a timer event loop to poll the queue, and runs the nested line-batch loop here; the spawned stream-reader thread runs a blockable reading loop and posts lines to the queue; and the spawned mergeall process runs its own file-processing loops to create output lines eventually displayed in the GUI here. """ global statustxt, gobutton # widgets global firstcompareline # state global cmpmsgsvar, logpopupvar # settings def trydecode(binline, stream_encode): """ [1.6] line decode can fail in Python 2.X due to a TBD library incompatibility issue; see 1.6 note near top of this script; if uncaught, GUI is dead but unclosed, and the error message does not appear in GUI -- because the error occurs here in GUI instead of subproc, its text goes to console here (if any), not to subproc stream queue, and the subproc is apparently terminated in 2.X; also note that "line" text here is used for the GUI display only: it doesn't show up in the binary log file, and this has no impact on the underlying mergeall process, which proceeds unaffected; [3.0] changed the error message to be a str instead of a bytes; the latter would surely fail later in the caller under python 3.X, but the decode here probably only ever failed on python 2.X, where a bytes result works because bytes is really just a synonym for str; """ try: line = binline.decode(stream_encode) # [1.4] manual decode here, match subproc except UnicodeDecodeError: #line = b'(UNDECODABLE LINE): ' + binline # [1.6] don't let this kill the GUI in 2.X line = '(UNDECODABLE LINE: see log file)\n' # [3.0] use str, but drop the content return line try: binline = linequeue.get(block=False) # check the queue except queue.Empty: pass # nothing posted yet: reschedule and wait else: # process a batch of 1 or more objects while True: if binline != EOF_SENTINEL: # process the next line string: GUI + logfile eoln = os.linesep # local line-end: \r\n Windows, \n Unix line = trydecode(binline, STREAM_ENCODE) # [1.4] manual decode here, match subproc [1.6] line = line.replace(eoln, '\n') # [1.4] and fix any Windows eolns for tk # [3.0] sanitize Unicode in line to be displayed in GUI line = fixTkBMP(line) # [3.0] allow comparison messages to be suppressed in the GUI, dynamically anycompare = ('comparing', '"comparing', "'comparing") # ascii() in Windows exe! if line.startswith(anycompare): # in line content? if cmpmsgsvar.get(): # suppress toggle on? if firstcompareline: # show first line only for top-level dirs, plus message firstcompareline = False statustxt.insert(END, line) statustxt.insert(END, 'Folder comparison messages ' 'are being suppressed in the GUI...\n') statustxt.see(END+'-2l') # scroll to new end of text else: # skip all other compare lines in GUI (only) pass else: # show comparison line, reset to show message if toggled on+off firstcompareline = True # reshow msg if toggled again statustxt.insert(END, line) # add to end of text widget statustxt.see(END+'-2l') # scroll to new end of text else: # show all other lines normally, don't reset for message statustxt.insert(END, line) # add to end of text widget statustxt.see(END+'-2l') # scroll to new end of text # '-2l' = before empty auto \n at end # force GUI to show/respond now (else dead during batch) statustxt.update() # write binary stream line to binary logfile if logfile: # also save to log file? [1.4]: binary eoln = os.linesep.encode() # must be bytes in 3.X (no-op in 2.X) binline = binline.replace(b'\r\n', b'\n') # [1.4] got just \n from '-u' in 2.X only binline = binline.replace(b'\n', eoln) # replaces are no-op in 3.X and unix logfile.write(binline) # read next line if any, else goto reschedule and wait if linequeue.empty(): break else: binline = linequeue.get(block=False) # back to top of loop else: # reader thread posted eof sentinel and exited: close out the run showinfo('%s: Finished' % APPNAME, 'The mergeall run has finished.' + ('' if not logfile else ('\nSee its log file in the popup window.' if logpopupvar.get() else '\nSee its log file in the logs folder.'))) refocusWindow() # [3.0] else requires click on Mac # if logging: close logfile, show in editor if toggled on [3.0] if logfile: logfile.close() if logpopupvar.get(): # [3.0] Mac OS X is pickier about file URLs if RunningOnMac: logpath = 'file:' + logpath # webbrowser opens text files in Notepad on Windows, # gedit on Linux, and TextEdit on Mac OS X (but YMMV) webbrowser.open(logpath) # assume never raises exc # [3.0] reenable new runs now gobutton.config(state=NORMAL) # [3.0] the following had odd text scrolls after vertical resizes # statustxt.pack_forget() # gobutton.pack(side=BOTTOM) # unhide button # statustxt.pack(side=TOP, expand=YES, fill=BOTH) # pack last=clip first # exit the timer events loop: back to waiting on user return # end batch while loop # reschedule and wait: check queue 10 times per second (msecs) statustxt.after(100, streamconsumer, linequeue, logfile, logpath) #################################################################################### # MAIN/TOP-LEVEL LOGIC #################################################################################### if __name__ == '__main__': if not RunningOnMac: # Windows and Linux: normal root = Tk() # 'root' is used elsewhere makewidgets(root) root.mainloop() else: #---------------------------------------------------------------------------- # [3.0] Mac OS X hack: a partial workaround for a bug in the recommended AS # Mac Tk 8.5 for Python 3.5. Hide/unhide main window so it shows its radio # and check buttons in Aqua's active-window style (default blue) immediately. # This code fixes style at initial opening only; style can be lost on popups # and minimize/restore -- click this window, and possibly others first, to # reset style as needed. More: docetc/miscnotes/mac-main-hack-notes.txt. # # This may be fixed in Tk 8.6, which may be supported by py.org's Python 3.X # someday, and is supported by homebrew's Python distribution (to be tested). # Unlike frigcal and pymailgui, a lift() is not enough here, whether clicked # to open in the mac python launcher, or run from a 'python3' command line. # # Update: root.force_focus() is now used to restore window active state after # dialogs, but doesn't help for deiconifies caught via or # events, and has no impact here on initial state in all codings attempted. # # Update: the following don't help even if they do a focus_force (why?): # root.bind('', onUnhide) + root.after(2000, refocusWindow), # root.createcommand('::tk::mac::onShow', onShow), # root.createcommand('::tk::mac::ReopenApplication', onReopen) # # UPDATE: losing focus in deiconifies in AS Tk 8.5 was finally fixed by the # hideous workaround below, which creates and then immediately destroys a # new but lowered (and hence invisible) top-level window on the Mac # reopen-app even (i.e., Doc and app icon clicks). The widgets flash off # and on momentarily (and the temp window may flash if mergeall is not at # fullscreen), but otherwise are active styled. Tk 8.6 status tbd... # # UPDATE: [3.3] the active-widget restore tricks don't seem to do anything # in recent Pythons/Tks... though they were the prime suspect in an abort 6 # crash after inminimizing a long-running sync: skip post Python 3.5. #---------------------------------------------------------------------------- root = Tk() makewidgets(root) # fix tk focus loss on startup if float(sys.version[:3]) <= 3.5: # [3.3] this doesn't restore active widgets in recent Pys/Tks root.withdraw() root.lift() root.after_idle(root.deiconify) # fix tk focus loss on deiconify if float(sys.version[:3]) <= 3.5: # [3.3] this doesn't restore active widgets in recent Pys/Tks--and may crash! def onReopen(): #print(root.state()) # always normal root.lift() root.update() temp = Toplevel() temp.lower() temp.destroy() root.createcommand('::tk::mac::ReopenApplication', onReopen) root.mainloop() """ # an alternative open workaround: a bogus Tk root, iconified after 2 seconds: root = Tk() root.protocol('WM_DELETE_WINDOW', lambda: None) Label(root, text='Welcome to mergeall', width=25, height=5).pack() makewidgets(Toplevel()) root.after(2000, root.iconify) root.mainloop() """ """ # a 15-line AppleScript alternative omitted here for space (and humanity...) """