diff -r b076b62731b7 Lib/idlelib/ClassBrowser.py --- a/Lib/idlelib/ClassBrowser.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/ClassBrowser.py Tue Sep 08 15:42:51 2015 -0700 @@ -15,14 +15,15 @@ import pyclbr from idlelib import PyShell -from idlelib.WindowList import ListedToplevel from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas from idlelib.configHandler import idleConf +from idlelib.component import Component +from idlelib.container import Container file_open = None # Method...Item and Class...Item use this. # Normally PyShell.flist.open, but there is no PyShell.flist for htest. -class ClassBrowser: +class ClassBrowser(Component): def __init__(self, flist, name, path, _htest=False): # XXX This API should change, if the file doesn't end in ".py" @@ -30,6 +31,7 @@ """ _htest - bool, change box when location running htest. """ + Component.__init__(self, flist) global file_open if not _htest: file_open = PyShell.flist.open @@ -38,8 +40,8 @@ self._htest = _htest self.init(flist) - def close(self, event=None): - self.top.destroy() + def close(self): + # NOTE: container will invoke this, so we don't need to destroy it self.node.destroy() def init(self, flist): @@ -47,18 +49,19 @@ # reset pyclbr pyclbr._modules.clear() # create top - self.top = top = ListedToplevel(flist.root) - top.protocol("WM_DELETE_WINDOW", self.close) - top.bind("", self.close) + # NOTE: later we will be passed in container, rather than creating it + self.top = top = Container(flist) + self.top.component = self + top.top.bind("", self.close) if self._htest: # place dialog below parent if running htest - top.geometry("+%d+%d" % + top.top.geometry("+%d+%d" % (flist.root.winfo_rootx(), flist.root.winfo_rooty() + 200)) self.settitle() - top.focus_set() + top.top.focus_set() # create scrolled canvas theme = idleConf.GetOption('main','Theme','name') background = idleConf.GetHighlight(theme, 'normal')['background'] - sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1) + sc = ScrolledCanvas(top.w, bg=background, highlightthickness=0, takefocus=1) sc.frame.pack(expand=1, fill="both") item = self.rootnode() self.node = node = TreeNode(sc.canvas, None, item) @@ -66,8 +69,7 @@ node.expand() def settitle(self): - self.top.wm_title("Class Browser - " + self.name) - self.top.wm_iconname("Class Browser") + self.top.set_title("Class Browser - " + self.name, "Class Browser") def rootnode(self): return ModuleBrowserTreeItem(self.file) diff -r b076b62731b7 Lib/idlelib/Debugger.py --- a/Lib/idlelib/Debugger.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/Debugger.py Tue Sep 08 15:42:51 2015 -0700 @@ -1,9 +1,10 @@ import os import bdb from tkinter import * -from idlelib.WindowList import ListedToplevel from idlelib.ScrolledList import ScrolledList from idlelib import macosxSupport +from idlelib.component import Component +from idlelib.container import Container class Idb(bdb.Bdb): @@ -47,11 +48,12 @@ return message -class Debugger: +class Debugger(Component): vstack = vsource = vlocals = vglobals = None def __init__(self, pyshell, idb=None): + Component.__init__(self, pyshell.flist) if idb is None: idb = Idb(self) self.pyshell = pyshell @@ -69,7 +71,7 @@ def close(self, event=None): if self.interacting: - self.top.bell() + self.top.top.bell() return if self.stackviewer: self.stackviewer.close(); self.stackviewer = None @@ -77,20 +79,19 @@ # (Causes a harmless extra cycle through close_debugger() if user # toggled debugger from pyshell Debug menu) self.pyshell.close_debugger() - # Now close the debugger control window.... - self.top.destroy() + # NOTE: container will invoke this, so we don't need to destroy it def make_gui(self): pyshell = self.pyshell self.flist = pyshell.flist self.root = root = pyshell.root - self.top = top = ListedToplevel(root) - self.top.wm_title("Debug Control") - self.top.wm_iconname("Debug") - top.wm_protocol("WM_DELETE_WINDOW", self.close) - self.top.bind("", self.close) + # NOTE: later we will be passed in container, rather than creating it + self.top = top = Container(self.flist) + self.top.component = self + self.top.set_title("Debug Control", "Debug") + self.top.top.bind("", self.close) # - self.bframe = bframe = Frame(top) + self.bframe = bframe = Frame(top.w) self.bframe.pack(anchor="w") self.buttons = bl = [] # @@ -113,39 +114,39 @@ self.cframe.pack(side="left") # if not self.vstack: - self.__class__.vstack = BooleanVar(top) + self.__class__.vstack = BooleanVar(top.w) self.vstack.set(1) self.bstack = Checkbutton(cframe, text="Stack", command=self.show_stack, variable=self.vstack) self.bstack.grid(row=0, column=0) if not self.vsource: - self.__class__.vsource = BooleanVar(top) + self.__class__.vsource = BooleanVar(top.w) self.bsource = Checkbutton(cframe, text="Source", command=self.show_source, variable=self.vsource) self.bsource.grid(row=0, column=1) if not self.vlocals: - self.__class__.vlocals = BooleanVar(top) + self.__class__.vlocals = BooleanVar(top.w) self.vlocals.set(1) self.blocals = Checkbutton(cframe, text="Locals", command=self.show_locals, variable=self.vlocals) self.blocals.grid(row=1, column=0) if not self.vglobals: - self.__class__.vglobals = BooleanVar(top) + self.__class__.vglobals = BooleanVar(top.w) self.bglobals = Checkbutton(cframe, text="Globals", command=self.show_globals, variable=self.vglobals) self.bglobals.grid(row=1, column=1) # - self.status = Label(top, anchor="w") + self.status = Label(top.w, anchor="w") self.status.pack(anchor="w") - self.error = Label(top, anchor="w") + self.error = Label(top.w, anchor="w") self.error.pack(anchor="w", fill="x") self.errorbg = self.error.cget("background") # - self.fstack = Frame(top, height=1) + self.fstack = Frame(top.w, height=1) self.fstack.pack(expand=1, fill="both") - self.flocals = Frame(top) + self.flocals = Frame(top.w) self.flocals.pack(expand=1, fill="both") - self.fglobals = Frame(top, height=1) + self.fglobals = Frame(top.w, height=1) self.fglobals.pack(expand=1, fill="both") # if self.vstack.get(): @@ -190,7 +191,7 @@ for b in self.buttons: b.configure(state="normal") # - self.top.wakeup() + self.top.move_to_front(self) self.root.mainloop() # for b in self.buttons: diff -r b076b62731b7 Lib/idlelib/EditorWindow.py --- a/Lib/idlelib/EditorWindow.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/EditorWindow.py Tue Sep 08 15:42:51 2015 -0700 @@ -13,7 +13,6 @@ import webbrowser from idlelib.MultiCall import MultiCallCreator -from idlelib import WindowList from idlelib import SearchDialog from idlelib import GrepDialog from idlelib import ReplaceDialog @@ -21,6 +20,8 @@ from idlelib.configHandler import idleConf from idlelib import aboutDialog, textView, configDialog from idlelib import macosxSupport +from idlelib.component import Component +from idlelib.container import Container # The default tab setting for a Text widget, in average-width characters. TK_TABWIDTH_DEFAULT = 8 @@ -84,7 +85,7 @@ helpDialog.show_dialog(parent) -class EditorWindow(object): +class EditorWindow(Component): from idlelib.Percolator import Percolator from idlelib.ColorDelegator import ColorDelegator from idlelib.UndoDelegator import UndoDelegator @@ -96,6 +97,8 @@ help_url = None def __init__(self, flist=None, filename=None, key=None, root=None): + Component.__init__(self, flist) + self.type = 'editor' if EditorWindow.help_url is None: dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html') if sys.platform.count('linux'): @@ -132,7 +135,15 @@ except AttributeError: sys.ps1 = '>>> ' self.menubar = Menu(root) - self.top = top = WindowList.ListedToplevel(root, menu=self.menubar) + # Note: It's important that this be stored in self.top, as + # Component relies on this to communicate with the + # Container. This restriction won't be necessary in the + # future, as we will soon be passing in a Container for + # the Component to use, rather than having the Component + # create one. + self.top = top = Container(flist) + self.top.component = self + top.set_menubar(self.menubar) if flist: self.tkinter_vars = flist.vars #self.top.instance_dict makes flist.inversedict available to @@ -144,7 +155,7 @@ self.top.instance_dict = {} self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(), 'recent-files.lst') - self.text_frame = text_frame = Frame(top) + self.text_frame = text_frame = Frame(top.w) self.vbar = vbar = Scrollbar(text_frame, name='vbar') self.width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') @@ -161,13 +172,10 @@ # older tk versions. text_options['tabstyle'] = 'wordprocessor' self.text = text = MultiCallCreator(Text)(text_frame, **text_options) - self.top.focused_widget = self.text self.createmenubar() self.apply_bindings() - self.top.protocol("WM_DELETE_WINDOW", self.close) - self.top.bind("<>", self.close_event) if macosxSupport.isAquaTk(): # Command-W on editorwindows doesn't work without this. text.bind('<>', self.close_event) @@ -283,6 +291,15 @@ self.good_load = False self.set_indentation_params(False) self.color = None # initialized below in self.ResetColorizer + menu = self.menudict.get('windows') + if menu: + end = menu.index("end") + if end is None: + end = -1 + if end >= 0: + menu.add_separator() + end = end + 1 + self.wmenu_end = end if filename: if os.path.exists(filename) and not os.path.isdir(filename): if io.loadfile(filename): @@ -297,16 +314,6 @@ self.saved_change_hook() self.update_recent_files_list() self.load_extensions() - menu = self.menudict.get('windows') - if menu: - end = menu.index("end") - if end is None: - end = -1 - if end >= 0: - menu.add_separator() - end = end + 1 - self.wmenu_end = end - WindowList.register_callback(self.postwindowsmenu) # Some abstractions so IDLE extensions are cross-IDE self.askyesno = tkMessageBox.askyesno @@ -358,6 +365,10 @@ # Replace non-BMP char with diamond questionmark. return re.sub('[\U00010000-\U0010FFFF]', '\ufffd', filename) + def wakeup(self): + Component.wakeup(self) + self.text.focus_set() + def new_callback(self, event): dirname, basename = self.io.defaultfilename() self.flist.new(dirname) @@ -407,7 +418,7 @@ return "break" def set_status_bar(self): - self.status_bar = self.MultiStatusBar(self.top) + self.status_bar = self.MultiStatusBar(self.top.w) if sys.platform == "darwin": # Insert some padding to avoid obscuring some of the statusbar # by the resize widget. @@ -464,7 +475,7 @@ end = -1 if end > self.wmenu_end: menu.delete(self.wmenu_end+1, end) - WindowList.add_windows_to_menu(menu) + self.flist.add_windows_to_menu(menu) rmenu = None @@ -531,19 +542,19 @@ return 'normal' def about_dialog(self, event=None): - aboutDialog.AboutDialog(self.top,'About IDLE') + aboutDialog.AboutDialog(self.top.top,'About IDLE') def config_dialog(self, event=None): - configDialog.ConfigDialog(self.top,'Settings') + configDialog.ConfigDialog(self.top.top,'Settings') def config_extensions_dialog(self, event=None): - configDialog.ConfigExtensionsDialog(self.top) + configDialog.ConfigExtensionsDialog(self.top.top) def help_dialog(self, event=None): if self.root: parent = self.root else: - parent = self.top - helpDialog.display(parent, near=self.top) + parent = self.top.top + helpDialog.display(parent, near=self.top.top) def python_docs(self, event=None): if sys.platform[:3] == 'win': @@ -737,9 +748,14 @@ if self.flist: self.flist.filename_changed_edit(self) self.saved_change_hook() - self.top.update_windowlist_registry(self) self.ResetColorizer() + def filenames_changed(self): + "Callback when one or more filenames changed; rebuild windows menu" + # NOTE: This replaces the callbacks formerly invoked by WindowList. + if self.menudict.get('windows'): + self.postwindowsmenu() + def _addcolorizer(self): if self.color: return @@ -945,8 +961,7 @@ if not self.get_saved(): title = "*%s*" % title icon = "*%s" % icon - self.top.wm_title(title) - self.top.wm_iconname(icon) + self.top.set_title(title, short_title=icon) def get_saved(self): return self.undo.get_saved() @@ -995,22 +1010,13 @@ text = self.text return int(float(text.index(mark))) - def get_geometry(self): - "Return (width, height, x, y)" - geom = self.top.wm_geometry() - m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) - return list(map(int, m.groups())) - def close_event(self, event): - self.close() + self.top._close() # container in charge of closing now def maybesave(self): if self.io: if not self.get_saved(): - if self.top.state()!='normal': - self.top.deiconify() - self.top.lower() - self.top.lift() + self.top.move_to_front(self) return self.io.maybesave() def close(self): @@ -1022,7 +1028,6 @@ def _close(self): if self.io.filename: self.update_recent_files_list(new_file=self.io.filename) - WindowList.unregister_callback(self.postwindowsmenu) self.unload_extensions() self.io.close() self.io = None @@ -1034,7 +1039,7 @@ self.tkinter_vars = None self.per.close() self.per = None - self.top.destroy() + # NOTE: container invoked this, so no need for us to destroy container if self.close_hook: # unless override: unregister from flist, terminate if last window self.close_hook() diff -r b076b62731b7 Lib/idlelib/FileList.py --- a/Lib/idlelib/FileList.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/FileList.py Tue Sep 08 15:42:51 2015 -0700 @@ -12,6 +12,7 @@ self.root = root self.dict = {} self.inversedict = {} + self.containers = {} self.vars = {} # For EditorWindow.getrawvar (shared Tcl variables) def open(self, filename, action=None): @@ -27,7 +28,7 @@ key = os.path.normcase(filename) if key in self.dict: edit = self.dict[key] - edit.top.wakeup() + edit.wakeup() return edit if action: # Don't create window, perform 'action', e.g. open in same window @@ -98,6 +99,42 @@ del self.dict[key] except KeyError: pass + self.root.after_idle(self.filenames_changed) + + # note: replacement for WindowList.add + def add_container(self, container): + container.w.after_idle(self.filenames_changed) + self.containers[str(container)] = container + + # note: replacement for WindowList.delete + def delete_container(self, container): + try: + del self.containers[str(container)] + except KeyError: + # Sometimes, destroy() is called twice + pass + self.filenames_changed() + + # note: replaces callbacks from WindowList; whereas those needed to be + # explicitly registered for and unregistered from, here we just send + # the notice to every component + def filenames_changed(self): + "Callback when one or more filenames changed" + for w in self.inversedict.keys(): + w.filenames_changed() + + def add_windows_to_menu(self, menu): + list = [] + for key in self.containers: + container = self.containers[key] + try: + title = container.get_title() + except TclError: + continue + list.append((title, key, container)) + list.sort() + for title, key, container in list: + menu.add_command(label=title, command=container.component.wakeup) def canonize(self, filename): if not os.path.isabs(filename): diff -r b076b62731b7 Lib/idlelib/PyShell.py --- a/Lib/idlelib/PyShell.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/PyShell.py Tue Sep 08 15:42:51 2015 -0700 @@ -315,7 +315,7 @@ def open_shell(self, event=None): if self.pyshell: - self.pyshell.top.wakeup() + self.pyshell.wakeup() else: self.pyshell = PyShell(self) if self.pyshell: diff -r b076b62731b7 Lib/idlelib/WindowList.py --- a/Lib/idlelib/WindowList.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/WindowList.py Tue Sep 08 15:42:51 2015 -0700 @@ -1,90 +1,1 @@ -from tkinter import * - -class WindowList: - - def __init__(self): - self.dict = {} - self.callbacks = [] - - def add(self, window): - window.after_idle(self.call_callbacks) - self.dict[str(window)] = window - - def delete(self, window): - try: - del self.dict[str(window)] - except KeyError: - # Sometimes, destroy() is called twice - pass - self.call_callbacks() - - def add_windows_to_menu(self, menu): - list = [] - for key in self.dict: - window = self.dict[key] - try: - title = window.get_title() - except TclError: - continue - list.append((title, key, window)) - list.sort() - for title, key, window in list: - menu.add_command(label=title, command=window.wakeup) - - def register_callback(self, callback): - self.callbacks.append(callback) - - def unregister_callback(self, callback): - try: - self.callbacks.remove(callback) - except ValueError: - pass - - def call_callbacks(self): - for callback in self.callbacks: - try: - callback() - except: - t, v, tb = sys.exc_info() - print("warning: callback failed in WindowList", t, ":", v) - -registry = WindowList() - -add_windows_to_menu = registry.add_windows_to_menu -register_callback = registry.register_callback -unregister_callback = registry.unregister_callback - - -class ListedToplevel(Toplevel): - - def __init__(self, master, **kw): - Toplevel.__init__(self, master, kw) - registry.add(self) - self.focused_widget = self - - def destroy(self): - registry.delete(self) - Toplevel.destroy(self) - # If this is Idle's last window then quit the mainloop - # (Needed for clean exit on Windows 98) - if not registry.dict: - self.quit() - - def update_windowlist_registry(self, window): - registry.call_callbacks() - - def get_title(self): - # Subclass can override - return self.wm_title() - - def wakeup(self): - try: - if self.wm_state() == "iconic": - self.wm_withdraw() - self.wm_deiconify() - self.tkraise() - self.focused_widget.focus_set() - except TclError: - # This can happen when the window menu was torn off. - # Simply ignore it. - pass +# TO BE DELETED \ No newline at end of file diff -r b076b62731b7 Lib/idlelib/component.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/component.py Tue Sep 08 15:42:51 2015 -0700 @@ -0,0 +1,39 @@ +""" +IDLE Component (e.g. editor, shell, dialog, etc.) +""" + +class Component(object): + + def __init__(self, flist): + self.flist = flist + self.top = None # component should set this to its Container + self.type = 'component' + + def close(self): + "Called e.g. when close button on window containing us is clicked" + self.flist.unregister_maybe_terminate(self) + self.flist = None + # TODO - container will destroy all widgets after this returns + + def wakeup(self): + "Bring component to front and set focus" + self.top.move_to_front(self) + # NOTE: we used to call 'wakeup' on ListedTopLevel widgets + # (now Container) rather than the component itself; + # because containers will eventually contain more than + # one component, this is a better approach. + + # Below here are various notifications that subclasses may choose + # to act on if needed + + def configuration_will_change(self): + "Callback from configuration dialog before settings are applied." + pass + + def configuration_changed(self): + "Callback from configuration dialog after settings are applied." + pass + + def filenames_changed(self): + "Callback when one or more filenames changed; rebuild windows menu" + pass diff -r b076b62731b7 Lib/idlelib/container.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/container.py Tue Sep 08 15:42:51 2015 -0700 @@ -0,0 +1,60 @@ +""" +Object that holds a Component, e.g. editor + +For now, a toplevel, but later a frame too... +""" + +from tkinter import Toplevel + +class Container(object): + def __init__(self, flist): + self.flist = flist + self.component = None # component should set this + self.focus_widget = None + self.w = self.make_widget() # w: widget component packs into + self.top = self.w.winfo_toplevel() # top: toplevel for this container + if self.flist: + self.flist.add_container(self) + + def make_widget(self): + t = Toplevel(self.flist.root) + t.protocol('WM_DELETE_WINDOW', self._close) + t.bind("<>", lambda e: self._close()) + return t + + def _close(self): + if self.component is not None: + try: + self.component.close() + self.w.destroy() + self.w = None + self.component = None + if self.flist: + self.flist.delete_container(self) + except Exception: # if file needs saving, user may abort + pass + + def set_title(self, title, short_title=None): + self.w.wm_title(title) + if short_title is not None: + self.w.wm_iconname(short_title) + + def get_title(self): + return self.w.wm_title() + + def set_menubar(self, menu): + self.w['menu'] = menu + + def move_to_front(self, component): + "Adjust the container so the given component is brought forward." + # we can ignore component parameter for now, as base container + # supports only a single component + try: + if self.w.wm_state() == "iconic": + self.w.wm_withdraw() + self.w.wm_deiconify() + self.w.tkraise() + except TclError: + # This can happen when the window menu was torn off. + # Simply ignore it. + pass diff -r b076b62731b7 Lib/idlelib/macosxSupport.py --- a/Lib/idlelib/macosxSupport.py Mon Sep 07 19:59:38 2015 +0300 +++ b/Lib/idlelib/macosxSupport.py Tue Sep 08 15:42:51 2015 -0700 @@ -125,7 +125,6 @@ # menu. from tkinter import Menu from idlelib import Bindings - from idlelib import WindowList closeItem = Bindings.menudefs[0][1][-2] @@ -155,8 +154,8 @@ if end > 0: menu.delete(0, end) - WindowList.add_windows_to_menu(menu) - WindowList.register_callback(postwindowsmenu) + if flist: + flist.add_windows_to_menu(menu) def about_dialog(event=None): from idlelib import aboutDialog