Home | History | Annotate | Download | only in idlelib
      1 """
      2 A number of functions that enhance IDLE on macOS.
      3 """
      4 from os.path import expanduser
      5 import plistlib
      6 from sys import platform  # Used in _init_tk_type, changed by test.
      7 
      8 import tkinter
      9 
     10 
     11 ## Define functions that query the Mac graphics type.
     12 ## _tk_type and its initializer are private to this section.
     13 
     14 _tk_type = None
     15 
     16 def _init_tk_type():
     17     """
     18     Initializes OS X Tk variant values for
     19     isAquaTk(), isCarbonTk(), isCocoaTk(), and isXQuartz().
     20     """
     21     global _tk_type
     22     if platform == 'darwin':
     23         root = tkinter.Tk()
     24         ws = root.tk.call('tk', 'windowingsystem')
     25         if 'x11' in ws:
     26             _tk_type = "xquartz"
     27         elif 'aqua' not in ws:
     28             _tk_type = "other"
     29         elif 'AppKit' in root.tk.call('winfo', 'server', '.'):
     30             _tk_type = "cocoa"
     31         else:
     32             _tk_type = "carbon"
     33         root.destroy()
     34     else:
     35         _tk_type = "other"
     36 
     37 def isAquaTk():
     38     """
     39     Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon).
     40     """
     41     if not _tk_type:
     42         _init_tk_type()
     43     return _tk_type == "cocoa" or _tk_type == "carbon"
     44 
     45 def isCarbonTk():
     46     """
     47     Returns True if IDLE is using a Carbon Aqua Tk (instead of the
     48     newer Cocoa Aqua Tk).
     49     """
     50     if not _tk_type:
     51         _init_tk_type()
     52     return _tk_type == "carbon"
     53 
     54 def isCocoaTk():
     55     """
     56     Returns True if IDLE is using a Cocoa Aqua Tk.
     57     """
     58     if not _tk_type:
     59         _init_tk_type()
     60     return _tk_type == "cocoa"
     61 
     62 def isXQuartz():
     63     """
     64     Returns True if IDLE is using an OS X X11 Tk.
     65     """
     66     if not _tk_type:
     67         _init_tk_type()
     68     return _tk_type == "xquartz"
     69 
     70 
     71 def tkVersionWarning(root):
     72     """
     73     Returns a string warning message if the Tk version in use appears to
     74     be one known to cause problems with IDLE.
     75     1. Apple Cocoa-based Tk 8.5.7 shipped with Mac OS X 10.6 is unusable.
     76     2. Apple Cocoa-based Tk 8.5.9 in OS X 10.7 and 10.8 is better but
     77         can still crash unexpectedly.
     78     """
     79 
     80     if isCocoaTk():
     81         patchlevel = root.tk.call('info', 'patchlevel')
     82         if patchlevel not in ('8.5.7', '8.5.9'):
     83             return False
     84         return ("WARNING: The version of Tcl/Tk ({0}) in use may"
     85                 " be unstable.\n"
     86                 "Visit http://www.python.org/download/mac/tcltk/"
     87                 " for current information.".format(patchlevel))
     88     else:
     89         return False
     90 
     91 
     92 def readSystemPreferences():
     93     """
     94     Fetch the macOS system preferences.
     95     """
     96     if platform != 'darwin':
     97         return None
     98 
     99     plist_path = expanduser('~/Library/Preferences/.GlobalPreferences.plist')
    100     try:
    101         with open(plist_path, 'rb') as plist_file:
    102             return plistlib.load(plist_file)
    103     except OSError:
    104         return None
    105 
    106 
    107 def preferTabsPreferenceWarning():
    108     """
    109     Warn if "Prefer tabs when opening documents" is set to "Always".
    110     """
    111     if platform != 'darwin':
    112         return None
    113 
    114     prefs = readSystemPreferences()
    115     if prefs and prefs.get('AppleWindowTabbingMode') == 'always':
    116         return (
    117             'WARNING: The system preference "Prefer tabs when opening'
    118             ' documents" is set to "Always". This will cause various problems'
    119             ' with IDLE. For the best experience, change this setting when'
    120             ' running IDLE (via System Preferences -> Dock).'
    121         )
    122     return None
    123 
    124 
    125 ## Fix the menu and related functions.
    126 
    127 def addOpenEventSupport(root, flist):
    128     """
    129     This ensures that the application will respond to open AppleEvents, which
    130     makes is feasible to use IDLE as the default application for python files.
    131     """
    132     def doOpenFile(*args):
    133         for fn in args:
    134             flist.open(fn)
    135 
    136     # The command below is a hook in aquatk that is called whenever the app
    137     # receives a file open event. The callback can have multiple arguments,
    138     # one for every file that should be opened.
    139     root.createcommand("::tk::mac::OpenDocument", doOpenFile)
    140 
    141 def hideTkConsole(root):
    142     try:
    143         root.tk.call('console', 'hide')
    144     except tkinter.TclError:
    145         # Some versions of the Tk framework don't have a console object
    146         pass
    147 
    148 def overrideRootMenu(root, flist):
    149     """
    150     Replace the Tk root menu by something that is more appropriate for
    151     IDLE with an Aqua Tk.
    152     """
    153     # The menu that is attached to the Tk root (".") is also used by AquaTk for
    154     # all windows that don't specify a menu of their own. The default menubar
    155     # contains a number of menus, none of which are appropriate for IDLE. The
    156     # Most annoying of those is an 'About Tck/Tk...' menu in the application
    157     # menu.
    158     #
    159     # This function replaces the default menubar by a mostly empty one, it
    160     # should only contain the correct application menu and the window menu.
    161     #
    162     # Due to a (mis-)feature of TkAqua the user will also see an empty Help
    163     # menu.
    164     from tkinter import Menu
    165     from idlelib import mainmenu
    166     from idlelib import window
    167 
    168     closeItem = mainmenu.menudefs[0][1][-2]
    169 
    170     # Remove the last 3 items of the file menu: a separator, close window and
    171     # quit. Close window will be reinserted just above the save item, where
    172     # it should be according to the HIG. Quit is in the application menu.
    173     del mainmenu.menudefs[0][1][-3:]
    174     mainmenu.menudefs[0][1].insert(6, closeItem)
    175 
    176     # Remove the 'About' entry from the help menu, it is in the application
    177     # menu
    178     del mainmenu.menudefs[-1][1][0:2]
    179     # Remove the 'Configure Idle' entry from the options menu, it is in the
    180     # application menu as 'Preferences'
    181     del mainmenu.menudefs[-3][1][0:2]
    182     menubar = Menu(root)
    183     root.configure(menu=menubar)
    184     menudict = {}
    185 
    186     menudict['window'] = menu = Menu(menubar, name='window', tearoff=0)
    187     menubar.add_cascade(label='Window', menu=menu, underline=0)
    188 
    189     def postwindowsmenu(menu=menu):
    190         end = menu.index('end')
    191         if end is None:
    192             end = -1
    193 
    194         if end > 0:
    195             menu.delete(0, end)
    196         window.add_windows_to_menu(menu)
    197     window.register_callback(postwindowsmenu)
    198 
    199     def about_dialog(event=None):
    200         "Handle Help 'About IDLE' event."
    201         # Synchronize with editor.EditorWindow.about_dialog.
    202         from idlelib import help_about
    203         help_about.AboutDialog(root)
    204 
    205     def config_dialog(event=None):
    206         "Handle Options 'Configure IDLE' event."
    207         # Synchronize with editor.EditorWindow.config_dialog.
    208         from idlelib import configdialog
    209 
    210         # Ensure that the root object has an instance_dict attribute,
    211         # mirrors code in EditorWindow (although that sets the attribute
    212         # on an EditorWindow instance that is then passed as the first
    213         # argument to ConfigDialog)
    214         root.instance_dict = flist.inversedict
    215         configdialog.ConfigDialog(root, 'Settings')
    216 
    217     def help_dialog(event=None):
    218         "Handle Help 'IDLE Help' event."
    219         # Synchronize with editor.EditorWindow.help_dialog.
    220         from idlelib import help
    221         help.show_idlehelp(root)
    222 
    223     root.bind('<<about-idle>>', about_dialog)
    224     root.bind('<<open-config-dialog>>', config_dialog)
    225     root.createcommand('::tk::mac::ShowPreferences', config_dialog)
    226     if flist:
    227         root.bind('<<close-all-windows>>', flist.close_all_callback)
    228 
    229         # The binding above doesn't reliably work on all versions of Tk
    230         # on macOS. Adding command definition below does seem to do the
    231         # right thing for now.
    232         root.createcommand('exit', flist.close_all_callback)
    233 
    234     if isCarbonTk():
    235         # for Carbon AquaTk, replace the default Tk apple menu
    236         menudict['application'] = menu = Menu(menubar, name='apple',
    237                                               tearoff=0)
    238         menubar.add_cascade(label='IDLE', menu=menu)
    239         mainmenu.menudefs.insert(0,
    240             ('application', [
    241                 ('About IDLE', '<<about-idle>>'),
    242                     None,
    243                 ]))
    244     if isCocoaTk():
    245         # replace default About dialog with About IDLE one
    246         root.createcommand('tkAboutDialog', about_dialog)
    247         # replace default "Help" item in Help menu
    248         root.createcommand('::tk::mac::ShowHelp', help_dialog)
    249         # remove redundant "IDLE Help" from menu
    250         del mainmenu.menudefs[-1][1][0]
    251 
    252 def fixb2context(root):
    253     '''Removed bad AquaTk Button-2 (right) and Paste bindings.
    254 
    255     They prevent context menu access and seem to be gone in AquaTk8.6.
    256     See issue #24801.
    257     '''
    258     root.unbind_class('Text', '<B2>')
    259     root.unbind_class('Text', '<B2-Motion>')
    260     root.unbind_class('Text', '<<PasteSelection>>')
    261 
    262 def setupApp(root, flist):
    263     """
    264     Perform initial OS X customizations if needed.
    265     Called from pyshell.main() after initial calls to Tk()
    266 
    267     There are currently three major versions of Tk in use on OS X:
    268         1. Aqua Cocoa Tk (native default since OS X 10.6)
    269         2. Aqua Carbon Tk (original native, 32-bit only, deprecated)
    270         3. X11 (supported by some third-party distributors, deprecated)
    271     There are various differences among the three that affect IDLE
    272     behavior, primarily with menus, mouse key events, and accelerators.
    273     Some one-time customizations are performed here.
    274     Others are dynamically tested throughout idlelib by calls to the
    275     isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which
    276     are initialized here as well.
    277     """
    278     if isAquaTk():
    279         hideTkConsole(root)
    280         overrideRootMenu(root, flist)
    281         addOpenEventSupport(root, flist)
    282         fixb2context(root)
    283 
    284 
    285 if __name__ == '__main__':
    286     from unittest import main
    287     main('idlelib.idle_test.test_macosx', verbosity=2)
    288