jpayne@68: """ jpayne@68: A number of functions that enhance IDLE on macOS. jpayne@68: """ jpayne@68: from os.path import expanduser jpayne@68: import plistlib jpayne@68: from sys import platform # Used in _init_tk_type, changed by test. jpayne@68: jpayne@68: import tkinter jpayne@68: jpayne@68: jpayne@68: ## Define functions that query the Mac graphics type. jpayne@68: ## _tk_type and its initializer are private to this section. jpayne@68: jpayne@68: _tk_type = None jpayne@68: jpayne@68: def _init_tk_type(): jpayne@68: """ jpayne@68: Initializes OS X Tk variant values for jpayne@68: isAquaTk(), isCarbonTk(), isCocoaTk(), and isXQuartz(). jpayne@68: """ jpayne@68: global _tk_type jpayne@68: if platform == 'darwin': jpayne@68: root = tkinter.Tk() jpayne@68: ws = root.tk.call('tk', 'windowingsystem') jpayne@68: if 'x11' in ws: jpayne@68: _tk_type = "xquartz" jpayne@68: elif 'aqua' not in ws: jpayne@68: _tk_type = "other" jpayne@68: elif 'AppKit' in root.tk.call('winfo', 'server', '.'): jpayne@68: _tk_type = "cocoa" jpayne@68: else: jpayne@68: _tk_type = "carbon" jpayne@68: root.destroy() jpayne@68: else: jpayne@68: _tk_type = "other" jpayne@68: jpayne@68: def isAquaTk(): jpayne@68: """ jpayne@68: Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon). jpayne@68: """ jpayne@68: if not _tk_type: jpayne@68: _init_tk_type() jpayne@68: return _tk_type == "cocoa" or _tk_type == "carbon" jpayne@68: jpayne@68: def isCarbonTk(): jpayne@68: """ jpayne@68: Returns True if IDLE is using a Carbon Aqua Tk (instead of the jpayne@68: newer Cocoa Aqua Tk). jpayne@68: """ jpayne@68: if not _tk_type: jpayne@68: _init_tk_type() jpayne@68: return _tk_type == "carbon" jpayne@68: jpayne@68: def isCocoaTk(): jpayne@68: """ jpayne@68: Returns True if IDLE is using a Cocoa Aqua Tk. jpayne@68: """ jpayne@68: if not _tk_type: jpayne@68: _init_tk_type() jpayne@68: return _tk_type == "cocoa" jpayne@68: jpayne@68: def isXQuartz(): jpayne@68: """ jpayne@68: Returns True if IDLE is using an OS X X11 Tk. jpayne@68: """ jpayne@68: if not _tk_type: jpayne@68: _init_tk_type() jpayne@68: return _tk_type == "xquartz" jpayne@68: jpayne@68: jpayne@68: def tkVersionWarning(root): jpayne@68: """ jpayne@68: Returns a string warning message if the Tk version in use appears to jpayne@68: be one known to cause problems with IDLE. jpayne@68: 1. Apple Cocoa-based Tk 8.5.7 shipped with Mac OS X 10.6 is unusable. jpayne@68: 2. Apple Cocoa-based Tk 8.5.9 in OS X 10.7 and 10.8 is better but jpayne@68: can still crash unexpectedly. jpayne@68: """ jpayne@68: jpayne@68: if isCocoaTk(): jpayne@68: patchlevel = root.tk.call('info', 'patchlevel') jpayne@68: if patchlevel not in ('8.5.7', '8.5.9'): jpayne@68: return False jpayne@68: return ("WARNING: The version of Tcl/Tk ({0}) in use may" jpayne@68: " be unstable.\n" jpayne@68: "Visit http://www.python.org/download/mac/tcltk/" jpayne@68: " for current information.".format(patchlevel)) jpayne@68: else: jpayne@68: return False jpayne@68: jpayne@68: jpayne@68: def readSystemPreferences(): jpayne@68: """ jpayne@68: Fetch the macOS system preferences. jpayne@68: """ jpayne@68: if platform != 'darwin': jpayne@68: return None jpayne@68: jpayne@68: plist_path = expanduser('~/Library/Preferences/.GlobalPreferences.plist') jpayne@68: try: jpayne@68: with open(plist_path, 'rb') as plist_file: jpayne@68: return plistlib.load(plist_file) jpayne@68: except OSError: jpayne@68: return None jpayne@68: jpayne@68: jpayne@68: def preferTabsPreferenceWarning(): jpayne@68: """ jpayne@68: Warn if "Prefer tabs when opening documents" is set to "Always". jpayne@68: """ jpayne@68: if platform != 'darwin': jpayne@68: return None jpayne@68: jpayne@68: prefs = readSystemPreferences() jpayne@68: if prefs and prefs.get('AppleWindowTabbingMode') == 'always': jpayne@68: return ( jpayne@68: 'WARNING: The system preference "Prefer tabs when opening' jpayne@68: ' documents" is set to "Always". This will cause various problems' jpayne@68: ' with IDLE. For the best experience, change this setting when' jpayne@68: ' running IDLE (via System Preferences -> Dock).' jpayne@68: ) jpayne@68: return None jpayne@68: jpayne@68: jpayne@68: ## Fix the menu and related functions. jpayne@68: jpayne@68: def addOpenEventSupport(root, flist): jpayne@68: """ jpayne@68: This ensures that the application will respond to open AppleEvents, which jpayne@68: makes is feasible to use IDLE as the default application for python files. jpayne@68: """ jpayne@68: def doOpenFile(*args): jpayne@68: for fn in args: jpayne@68: flist.open(fn) jpayne@68: jpayne@68: # The command below is a hook in aquatk that is called whenever the app jpayne@68: # receives a file open event. The callback can have multiple arguments, jpayne@68: # one for every file that should be opened. jpayne@68: root.createcommand("::tk::mac::OpenDocument", doOpenFile) jpayne@68: jpayne@68: def hideTkConsole(root): jpayne@68: try: jpayne@68: root.tk.call('console', 'hide') jpayne@68: except tkinter.TclError: jpayne@68: # Some versions of the Tk framework don't have a console object jpayne@68: pass jpayne@68: jpayne@68: def overrideRootMenu(root, flist): jpayne@68: """ jpayne@68: Replace the Tk root menu by something that is more appropriate for jpayne@68: IDLE with an Aqua Tk. jpayne@68: """ jpayne@68: # The menu that is attached to the Tk root (".") is also used by AquaTk for jpayne@68: # all windows that don't specify a menu of their own. The default menubar jpayne@68: # contains a number of menus, none of which are appropriate for IDLE. The jpayne@68: # Most annoying of those is an 'About Tck/Tk...' menu in the application jpayne@68: # menu. jpayne@68: # jpayne@68: # This function replaces the default menubar by a mostly empty one, it jpayne@68: # should only contain the correct application menu and the window menu. jpayne@68: # jpayne@68: # Due to a (mis-)feature of TkAqua the user will also see an empty Help jpayne@68: # menu. jpayne@68: from tkinter import Menu jpayne@68: from idlelib import mainmenu jpayne@68: from idlelib import window jpayne@68: jpayne@68: closeItem = mainmenu.menudefs[0][1][-2] jpayne@68: jpayne@68: # Remove the last 3 items of the file menu: a separator, close window and jpayne@68: # quit. Close window will be reinserted just above the save item, where jpayne@68: # it should be according to the HIG. Quit is in the application menu. jpayne@68: del mainmenu.menudefs[0][1][-3:] jpayne@68: mainmenu.menudefs[0][1].insert(6, closeItem) jpayne@68: jpayne@68: # Remove the 'About' entry from the help menu, it is in the application jpayne@68: # menu jpayne@68: del mainmenu.menudefs[-1][1][0:2] jpayne@68: # Remove the 'Configure Idle' entry from the options menu, it is in the jpayne@68: # application menu as 'Preferences' jpayne@68: del mainmenu.menudefs[-3][1][0:2] jpayne@68: menubar = Menu(root) jpayne@68: root.configure(menu=menubar) jpayne@68: menudict = {} jpayne@68: jpayne@68: menudict['window'] = menu = Menu(menubar, name='window', tearoff=0) jpayne@68: menubar.add_cascade(label='Window', menu=menu, underline=0) jpayne@68: jpayne@68: def postwindowsmenu(menu=menu): jpayne@68: end = menu.index('end') jpayne@68: if end is None: jpayne@68: end = -1 jpayne@68: jpayne@68: if end > 0: jpayne@68: menu.delete(0, end) jpayne@68: window.add_windows_to_menu(menu) jpayne@68: window.register_callback(postwindowsmenu) jpayne@68: jpayne@68: def about_dialog(event=None): jpayne@68: "Handle Help 'About IDLE' event." jpayne@68: # Synchronize with editor.EditorWindow.about_dialog. jpayne@68: from idlelib import help_about jpayne@68: help_about.AboutDialog(root) jpayne@68: jpayne@68: def config_dialog(event=None): jpayne@68: "Handle Options 'Configure IDLE' event." jpayne@68: # Synchronize with editor.EditorWindow.config_dialog. jpayne@68: from idlelib import configdialog jpayne@68: jpayne@68: # Ensure that the root object has an instance_dict attribute, jpayne@68: # mirrors code in EditorWindow (although that sets the attribute jpayne@68: # on an EditorWindow instance that is then passed as the first jpayne@68: # argument to ConfigDialog) jpayne@68: root.instance_dict = flist.inversedict jpayne@68: configdialog.ConfigDialog(root, 'Settings') jpayne@68: jpayne@68: def help_dialog(event=None): jpayne@68: "Handle Help 'IDLE Help' event." jpayne@68: # Synchronize with editor.EditorWindow.help_dialog. jpayne@68: from idlelib import help jpayne@68: help.show_idlehelp(root) jpayne@68: jpayne@68: root.bind('<>', about_dialog) jpayne@68: root.bind('<>', config_dialog) jpayne@68: root.createcommand('::tk::mac::ShowPreferences', config_dialog) jpayne@68: if flist: jpayne@68: root.bind('<>', flist.close_all_callback) jpayne@68: jpayne@68: # The binding above doesn't reliably work on all versions of Tk jpayne@68: # on macOS. Adding command definition below does seem to do the jpayne@68: # right thing for now. jpayne@68: root.createcommand('exit', flist.close_all_callback) jpayne@68: jpayne@68: if isCarbonTk(): jpayne@68: # for Carbon AquaTk, replace the default Tk apple menu jpayne@68: menudict['application'] = menu = Menu(menubar, name='apple', jpayne@68: tearoff=0) jpayne@68: menubar.add_cascade(label='IDLE', menu=menu) jpayne@68: mainmenu.menudefs.insert(0, jpayne@68: ('application', [ jpayne@68: ('About IDLE', '<>'), jpayne@68: None, jpayne@68: ])) jpayne@68: if isCocoaTk(): jpayne@68: # replace default About dialog with About IDLE one jpayne@68: root.createcommand('tkAboutDialog', about_dialog) jpayne@68: # replace default "Help" item in Help menu jpayne@68: root.createcommand('::tk::mac::ShowHelp', help_dialog) jpayne@68: # remove redundant "IDLE Help" from menu jpayne@68: del mainmenu.menudefs[-1][1][0] jpayne@68: jpayne@68: def fixb2context(root): jpayne@68: '''Removed bad AquaTk Button-2 (right) and Paste bindings. jpayne@68: jpayne@68: They prevent context menu access and seem to be gone in AquaTk8.6. jpayne@68: See issue #24801. jpayne@68: ''' jpayne@68: root.unbind_class('Text', '') jpayne@68: root.unbind_class('Text', '') jpayne@68: root.unbind_class('Text', '<>') jpayne@68: jpayne@68: def setupApp(root, flist): jpayne@68: """ jpayne@68: Perform initial OS X customizations if needed. jpayne@68: Called from pyshell.main() after initial calls to Tk() jpayne@68: jpayne@68: There are currently three major versions of Tk in use on OS X: jpayne@68: 1. Aqua Cocoa Tk (native default since OS X 10.6) jpayne@68: 2. Aqua Carbon Tk (original native, 32-bit only, deprecated) jpayne@68: 3. X11 (supported by some third-party distributors, deprecated) jpayne@68: There are various differences among the three that affect IDLE jpayne@68: behavior, primarily with menus, mouse key events, and accelerators. jpayne@68: Some one-time customizations are performed here. jpayne@68: Others are dynamically tested throughout idlelib by calls to the jpayne@68: isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which jpayne@68: are initialized here as well. jpayne@68: """ jpayne@68: if isAquaTk(): jpayne@68: hideTkConsole(root) jpayne@68: overrideRootMenu(root, flist) jpayne@68: addOpenEventSupport(root, flist) jpayne@68: fixb2context(root) jpayne@68: jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_macosx', verbosity=2)