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