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    menudict = {}
178
179    menudict['window'] = menu = Menu(menubar, name='window', tearoff=0)
180    menubar.add_cascade(label='Window', menu=menu, underline=0)
181
182    def postwindowsmenu(menu=menu):
183        end = menu.index('end')
184        if end is None:
185            end = -1
186
187        if end > 0:
188            menu.delete(0, end)
189        window.add_windows_to_menu(menu)
190    window.register_callback(postwindowsmenu)
191
192    def about_dialog(event=None):
193        "Handle Help 'About IDLE' event."
194        # Synchronize with editor.EditorWindow.about_dialog.
195        from idlelib import help_about
196        help_about.AboutDialog(root)
197
198    def config_dialog(event=None):
199        "Handle Options 'Configure IDLE' event."
200        # Synchronize with editor.EditorWindow.config_dialog.
201        from idlelib import configdialog
202
203        # Ensure that the root object has an instance_dict attribute,
204        # mirrors code in EditorWindow (although that sets the attribute
205        # on an EditorWindow instance that is then passed as the first
206        # argument to ConfigDialog)
207        root.instance_dict = flist.inversedict
208        configdialog.ConfigDialog(root, 'Settings')
209
210    def help_dialog(event=None):
211        "Handle Help 'IDLE Help' event."
212        # Synchronize with editor.EditorWindow.help_dialog.
213        from idlelib import help
214        help.show_idlehelp(root)
215
216    root.bind('<<about-idle>>', about_dialog)
217    root.bind('<<open-config-dialog>>', config_dialog)
218    root.createcommand('::tk::mac::ShowPreferences', config_dialog)
219    if flist:
220        root.bind('<<close-all-windows>>', flist.close_all_callback)
221
222        # The binding above doesn't reliably work on all versions of Tk
223        # on macOS. Adding command definition below does seem to do the
224        # right thing for now.
225        root.createcommand('exit', flist.close_all_callback)
226
227    if isCarbonTk():
228        # for Carbon AquaTk, replace the default Tk apple menu
229        menudict['application'] = menu = Menu(menubar, name='apple',
230                                              tearoff=0)
231        menubar.add_cascade(label='IDLE', menu=menu)
232        mainmenu.menudefs.insert(0,
233            ('application', [
234                ('About IDLE', '<<about-idle>>'),
235                    None,
236                ]))
237    if isCocoaTk():
238        # replace default About dialog with About IDLE one
239        root.createcommand('tkAboutDialog', about_dialog)
240        # replace default "Help" item in Help menu
241        root.createcommand('::tk::mac::ShowHelp', help_dialog)
242        # remove redundant "IDLE Help" from menu
243        del mainmenu.menudefs[-1][1][0]
244
245def fixb2context(root):
246    '''Removed bad AquaTk Button-2 (right) and Paste bindings.
247
248    They prevent context menu access and seem to be gone in AquaTk8.6.
249    See issue #24801.
250    '''
251    root.unbind_class('Text', '<B2>')
252    root.unbind_class('Text', '<B2-Motion>')
253    root.unbind_class('Text', '<<PasteSelection>>')
254
255def setupApp(root, flist):
256    """
257    Perform initial OS X customizations if needed.
258    Called from pyshell.main() after initial calls to Tk()
259
260    There are currently three major versions of Tk in use on OS X:
261        1. Aqua Cocoa Tk (native default since OS X 10.6)
262        2. Aqua Carbon Tk (original native, 32-bit only, deprecated)
263        3. X11 (supported by some third-party distributors, deprecated)
264    There are various differences among the three that affect IDLE
265    behavior, primarily with menus, mouse key events, and accelerators.
266    Some one-time customizations are performed here.
267    Others are dynamically tested throughout idlelib by calls to the
268    isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which
269    are initialized here as well.
270    """
271    if isAquaTk():
272        hideTkConsole(root)
273        overrideRootMenu(root, flist)
274        addOpenEventSupport(root, flist)
275        fixb2context(root)
276
277
278if __name__ == '__main__':
279    from unittest import main
280    main('idlelib.idle_test.test_macosx', verbosity=2)
281