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