diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 92992fd9cce9cd..09d101a5ffe66e 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -31,6 +31,7 @@ from tkinter.font import Font import idlelib +from idlelib import macosx class InvalidConfigType(Exception): pass class InvalidConfigSet(Exception): pass @@ -660,6 +661,17 @@ def GetCoreKeys(self, keySetName=None): '<>': [''], } + if macosx.isAquaTk(): + # There appears to be a system default binding for `Command-Q` + # on macOS that interferes with IDLE's setup. Don't add it + # to the key bindings an rely the default binding. + # + # Without this IDLE will prompt twice about closing a file with + # unsaved changes when the user quits IDLE using the keyboard + # shortcutand then chooses "Cancel" the first time the dialog + # appears. + del keyBindings['<>'] + if keySetName: if not (self.userCfg['keys'].has_section(keySetName) or self.defaultCfg['keys'].has_section(keySetName)): diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 8ee8eba64367a5..c57d50acd10a05 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -453,7 +453,7 @@ def set_line_and_column(self, event=None): * mainmenu.menudefs - a list of tuples, one for each menubar item. Each tuple pairs a lower-case name and list of dropdown items. Each item is a name, virtual event pair or None for separator. - * mainmenu.default_keydefs - maps events to keys. + * mainmenu.get_default_keydefs() - maps events to keys. * text.keydefs - same. * cls.menu_specs - menubar name, titlecase display form pairs with Alt-hotkey indicator. A subset of menudefs items. @@ -902,7 +902,8 @@ def RemoveKeybindings(self): Leaves the default Tk Text keybindings. """ # Called from configdialog.deactivate_current_config. - self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + keydefs = idleConf.GetCurrentKeySet() + self.mainmenu.set_default_keydefs(keydefs) for event, keylist in keydefs.items(): self.text.event_delete(event, *keylist) for extensionName in self.get_standard_extension_names(): @@ -917,7 +918,8 @@ def ApplyKeybindings(self): Alse update hotkeys to current keyset. """ # Called from configdialog.activate_config_changes. - self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + keydefs = idleConf.GetCurrentKeySet() + self.mainmenu.set_default_keydefs(keydefs) self.apply_bindings() for extensionName in self.get_standard_extension_names(): xkeydefs = idleConf.GetExtensionBindings(extensionName) @@ -1205,7 +1207,7 @@ def load_extension(self, name): def apply_bindings(self, keydefs=None): """Add events with keys to self.text.""" if keydefs is None: - keydefs = self.mainmenu.default_keydefs + keydefs = self.mainmenu.get_default_keydefs() text = self.text text.keydefs = keydefs for event, keylist in keydefs.items(): @@ -1221,7 +1223,7 @@ def fill_menus(self, menudefs=None, keydefs=None): if menudefs is None: menudefs = self.mainmenu.menudefs if keydefs is None: - keydefs = self.mainmenu.default_keydefs + keydefs = self.mainmenu.get_default_keydefs() menudict = self.menudict text = self.text for mname, entrylist in menudefs: diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py index 6d75cf7aa67dcc..a859d759799d40 100644 --- a/Lib/idlelib/idle_test/test_config.py +++ b/Lib/idlelib/idle_test/test_config.py @@ -197,9 +197,14 @@ def setUpClass(cls): cls.orig_warn = config._warn config._warn = Func() + cls._patcher = mock.patch("idlelib.macosx._tk_type", new="cocoa" if sys.platform == "darwin" else "other") + cls._patcher.start() + + @classmethod def tearDownClass(cls): config._warn = cls.orig_warn + cls._patcher.stop() def new_config(self, _utest=False): return config.IdleConf(_utest=_utest) diff --git a/Lib/idlelib/idle_test/test_macosx.py b/Lib/idlelib/idle_test/test_macosx.py index 86da8849e5ca00..c80613dbd47300 100644 --- a/Lib/idlelib/idle_test/test_macosx.py +++ b/Lib/idlelib/idle_test/test_macosx.py @@ -41,6 +41,9 @@ def tearDownClass(cls): def test_init_sets_tktype(self): "Test that _init_tk_type sets _tk_type according to platform." for platform, types in ('darwin', alltypes), ('other', nontypes): + orig_root = None + macosx._idle_root = self.root + self.addCleanup(lambda: setattr(macosx, "_idle_root", orig_root)) with self.subTest(platform=platform): macosx.platform = platform macosx._tk_type = None diff --git a/Lib/idlelib/idle_test/test_mainmenu.py b/Lib/idlelib/idle_test/test_mainmenu.py index 51d2accfe48a1c..bdc64d5ce43eaf 100644 --- a/Lib/idlelib/idle_test/test_mainmenu.py +++ b/Lib/idlelib/idle_test/test_mainmenu.py @@ -3,10 +3,18 @@ from idlelib import mainmenu import re +import sys import unittest +from unittest import mock class MainMenuTest(unittest.TestCase): + def setUp(self): + self._patcher = mock.patch("idlelib.macosx._tk_type", new="cocoa" if sys.platform == "darwin" else "other") + self._patcher.start() + + def tearDown(self): + self._patcher.stop() def test_menudefs(self): actual = [item[0] for item in mainmenu.menudefs] @@ -15,7 +23,7 @@ def test_menudefs(self): self.assertEqual(actual, expect) def test_default_keydefs(self): - self.assertGreaterEqual(len(mainmenu.default_keydefs), 50) + self.assertGreaterEqual(len(mainmenu.get_default_keydefs()), 50) def test_tcl_indexes(self): # Test tcl patterns used to find menuitem to alter. diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py index 86c5d41b629719..524e0afdcf144a 100644 --- a/Lib/idlelib/idle_test/test_squeezer.py +++ b/Lib/idlelib/idle_test/test_squeezer.py @@ -1,5 +1,6 @@ "Test squeezer, coverage 95%" +import sys from textwrap import dedent from tkinter import Text, Tk import unittest @@ -22,8 +23,11 @@ def get_test_tk_root(test_instance): requires('gui') root = Tk() root.withdraw() + patcher = patch("idlelib.macosx._idle_root", new=root) + patcher.start() def cleanup_root(): + patcher.stop() root.update_idletasks() root.destroy() test_instance.addCleanup(cleanup_root) @@ -170,27 +174,28 @@ def test_write_not_stdout(self): def test_write_stdout(self): """Test Squeezer's overriding of the EditorWindow's write() method.""" - editwin = self.make_mock_editor_window() - - for text in ['', 'TEXT']: - editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) - squeezer = self.make_squeezer_instance(editwin) - squeezer.auto_squeeze_min_lines = 50 - - self.assertEqual(squeezer.editwin.write(text, "stdout"), - SENTINEL_VALUE) - self.assertEqual(orig_write.call_count, 1) - orig_write.assert_called_with(text, "stdout") - self.assertEqual(len(squeezer.expandingbuttons), 0) - - for text in ['LONG TEXT' * 1000, 'MANY_LINES\n' * 100]: - editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) - squeezer = self.make_squeezer_instance(editwin) - squeezer.auto_squeeze_min_lines = 50 + with patch("idlelib.macosx._tk_type", new="cocoa" if sys.platform == "darwin" else "other"): + editwin = self.make_mock_editor_window() - self.assertEqual(squeezer.editwin.write(text, "stdout"), None) - self.assertEqual(orig_write.call_count, 0) - self.assertEqual(len(squeezer.expandingbuttons), 1) + for text in ['', 'TEXT']: + editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) + squeezer = self.make_squeezer_instance(editwin) + squeezer.auto_squeeze_min_lines = 50 + + self.assertEqual(squeezer.editwin.write(text, "stdout"), + SENTINEL_VALUE) + self.assertEqual(orig_write.call_count, 1) + orig_write.assert_called_with(text, "stdout") + self.assertEqual(len(squeezer.expandingbuttons), 0) + + for text in ['LONG TEXT' * 1000, 'MANY_LINES\n' * 100]: + editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) + squeezer = self.make_squeezer_instance(editwin) + squeezer.auto_squeeze_min_lines = 50 + + self.assertEqual(squeezer.editwin.write(text, "stdout"), None) + self.assertEqual(orig_write.call_count, 0) + self.assertEqual(len(squeezer.expandingbuttons), 1) def test_auto_squeeze(self): """Test that the auto-squeezing creates an ExpandingButton properly.""" diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosx.py index 332952f4572cbd..841cc85c539f80 100644 --- a/Lib/idlelib/macosx.py +++ b/Lib/idlelib/macosx.py @@ -12,6 +12,7 @@ ## _tk_type and its initializer are private to this section. _tk_type = None +_idle_root = None def _init_tk_type(): """ Initialize _tk_type for isXyzTk functions. @@ -33,17 +34,20 @@ def _init_tk_type(): _tk_type = "cocoa" return - root = tkinter.Tk() - ws = root.tk.call('tk', 'windowingsystem') + else: + if _idle_root is None: + _tk_type = "cocoa" + return + + ws = _idle_root.tk.call('tk', 'windowingsystem') if 'x11' in ws: _tk_type = "xquartz" elif 'aqua' not in ws: _tk_type = "other" - elif 'AppKit' in root.tk.call('winfo', 'server', '.'): + elif 'AppKit' in _idle_root.tk.call('winfo', 'server', '.'): _tk_type = "cocoa" else: _tk_type = "carbon" - root.destroy() else: _tk_type = "other" return @@ -216,11 +220,6 @@ def help_dialog(event=None): root.bind('<>', config_dialog) root.createcommand('::tk::mac::ShowPreferences', config_dialog) if flist: - root.bind('<>', flist.close_all_callback) - - # The binding above doesn't reliably work on all versions of Tk - # on macOS. Adding command definition below does seem to do the - # right thing for now. root.createcommand('::tk::mac::Quit', flist.close_all_callback) if isCarbonTk(): @@ -266,6 +265,8 @@ def setupApp(root, flist): isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which are initialized here as well. """ + global _idle_root + _idle_root = root if isAquaTk(): hideTkConsole(root) overrideRootMenu(root, flist) diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py index 91a32cebb513f9..f81f996a4c3d25 100644 --- a/Lib/idlelib/mainmenu.py +++ b/Lib/idlelib/mainmenu.py @@ -119,7 +119,18 @@ if find_spec('turtledemo'): menudefs[-1][1].append(('Turtle Demo', '<>')) -default_keydefs = idleConf.GetCurrentKeySet() +_default_keydefs = None + +def get_default_keydefs(): + global _default_keydefs + if _default_keydefs is None: + _default_keydefs = idleConf.GetCurrentKeySet() + return _default_keydefs + +def set_default_keydefs(keydefs): + global _default_keydefs + _default_keydefs = keydefs + if __name__ == '__main__': from unittest import main diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 1524fccd5d20f8..ae09608fe9634d 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1611,6 +1611,11 @@ def main(): from idlelib.run import fix_scaling fix_scaling(root) + fixwordbreaks(root) + fix_x11_paste(root) + flist = PyShellFileList(root) + macosx.setupApp(root, flist) + # set application icon icondir = os.path.join(os.path.dirname(__file__), 'Icons') if system() == 'Windows': @@ -1629,12 +1634,8 @@ def main(): for iconfile in iconfiles] root.wm_iconphoto(True, *icons) - # start editor and/or shell windows: - fixwordbreaks(root) - fix_x11_paste(root) - flist = PyShellFileList(root) - macosx.setupApp(root, flist) + # start editor and/or shell windows: if enable_edit: if not (cmd or script): for filename in args[:]: