diff -r 2c124e30a324 Lib/idlelib/EditorWindow.py --- a/Lib/idlelib/EditorWindow.py Fri Aug 07 00:43:39 2015 -0700 +++ b/Lib/idlelib/EditorWindow.py Tue Aug 11 09:55:38 2015 -0700 @@ -217,9 +217,7 @@ text.bind("<>", self.home_callback) if flist: - flist.inversedict[self] = key - if key: - flist.dict[key] = self + flist.register_editor_window(self, key) text.bind("<>", self.new_callback) text.bind("<>", self.flist.close_all_callback) text.bind("<>", self.open_class_browser) @@ -531,10 +529,13 @@ return 'normal' def about_dialog(self, event=None): - aboutDialog.AboutDialog(self.top,'About IDLE') + from idlelib.uifactory import open_about + open_about() def config_dialog(self, event=None): - configDialog.ConfigDialog(self.top,'Settings') + from idlelib.uifactory import open_preferences + open_preferences(self.top) + def config_extensions_dialog(self, event=None): configDialog.ConfigExtensionsDialog(self.top) @@ -758,6 +759,19 @@ self.per.removefilter(self.color) self.color = None + + def configuration_will_change(self): + "Callback from configuration dialog before settings are applied." + self.RemoveKeybindings() + + def configuration_changed(self): + "Callback from configuration dialog after settings are applied." + self.ResetColorizer() + self.ResetFont() + self.set_notabs_indentwidth() + self.ApplyKeybindings() + self.reset_help_menu_entries() + def ResetColorizer(self): "Update the color theme" # Called from self.filename_change_hook and from configDialog.py diff -r 2c124e30a324 Lib/idlelib/FileList.py --- a/Lib/idlelib/FileList.py Fri Aug 07 00:43:39 2015 -0700 +++ b/Lib/idlelib/FileList.py Tue Aug 11 09:55:38 2015 -0700 @@ -1,6 +1,7 @@ import os from tkinter import * import tkinter.messagebox as tkMessageBox +from idlelib.uifactory import other_windows_open, set_allclosed_callback class FileList: @@ -13,6 +14,7 @@ self.dict = {} self.inversedict = {} self.vars = {} # For EditorWindow.getrawvar (shared Tcl variables) + set_allclosed_callback(self.auxilliary_windows_closed) def open(self, filename, action=None): assert filename @@ -55,6 +57,16 @@ break return "break" + def auxilliary_windows_closed(self): + "Callback from UIFactory when the last auxilliary window is closed" + if not self.inversedict: + self.root.quit() + + def register_editor_window(self, win, key=None): + self.inversedict[win] = key + if key: + flist.dict[key] = win + def unregister_maybe_terminate(self, edit): try: key = self.inversedict[edit] @@ -64,7 +76,7 @@ if key: del self.dict[key] del self.inversedict[edit] - if not self.inversedict: + if not self.inversedict and not other_windows_open(): self.root.quit() def filename_changed_edit(self, edit): @@ -109,6 +121,16 @@ filename = os.path.join(pwd, filename) return os.path.normpath(filename) + def configuration_will_change(self): + "Callback from configuration dialog before settings are applied." + for w in self.inversedict.keys(): + w.configuration_will_change() + + def configuration_changed(self): + "Callback from configuration dialog after settings are applied." + for w in self.inversedict.keys(): + w.configuration_changed() + def _test(): from idlelib.EditorWindow import fixwordbreaks diff -r 2c124e30a324 Lib/idlelib/PyShell.py --- a/Lib/idlelib/PyShell.py Fri Aug 07 00:43:39 2015 -0700 +++ b/Lib/idlelib/PyShell.py Tue Aug 11 09:55:38 2015 -0700 @@ -34,6 +34,7 @@ from idlelib import Debugger from idlelib import RemoteDebugger from idlelib import macosxSupport +from idlelib import uifactory HOST = '127.0.0.1' # python execution server on localhost loopback PORT = 0 # someday pass in host, port for remote debug capability @@ -1529,6 +1530,7 @@ enable_shell = enable_shell or not enable_edit # start editor and/or shell windows: root = Tk(className="Idle") + uifactory.initialize(root) # set application icon icondir = os.path.join(os.path.dirname(__file__), 'Icons') @@ -1598,7 +1600,8 @@ if tkversionwarning: shell.interp.runcommand("print('%s')" % tkversionwarning) - while flist.inversedict: # keep IDLE running while files are open. + # keep IDLE running while files or other windows (e.g. prefs) are open + while flist.inversedict or uifactory.other_windows_open(): root.mainloop() root.destroy() capture_warnings(False) diff -r 2c124e30a324 Lib/idlelib/aboutDialog.py --- a/Lib/idlelib/aboutDialog.py Fri Aug 07 00:43:39 2015 -0700 +++ b/Lib/idlelib/aboutDialog.py Tue Aug 11 09:55:38 2015 -0700 @@ -5,142 +5,137 @@ import os from sys import version from tkinter import * -from idlelib import textView + class AboutDialog(Toplevel): - """Modal about dialog for idle - - """ - def __init__(self, parent, title, _htest=False): + """About dialog for IDLE - optionally modal""" + def __init__(self, parent, title, _htest=False, must_be_modal=True, + destroy_callback=None): """ _htest - bool, change box location when running htest """ Toplevel.__init__(self, parent) + self.destroy_callback = destroy_callback self.configure(borderwidth=5) # place dialog below parent if running htest self.geometry("+%d+%d" % ( parent.winfo_rootx()+30, parent.winfo_rooty()+(30 if not _htest else 100))) - self.bg = "#707070" - self.fg = "#ffffff" + self.bg = "#bbbbbb" + self.fg = "#000000" self.CreateWidgets() self.resizable(height=FALSE, width=FALSE) self.title(title) - self.transient(parent) - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.Ok) + #self.transient(parent) + if must_be_modal: + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.close) self.parent = parent - self.buttonOk.focus_set() - self.bind('',self.Ok) #dismiss dialog - self.bind('',self.Ok) #dismiss dialog - self.wait_window() + # keep??? self.bind('',self.close) #dismiss dialog + # keep??? self.bind('',self.close) #dismiss dialog + if must_be_modal: + self.wait_window() def CreateWidgets(self): + self['borderwidth'] = 0 release = version[:version.index(' ')] - frameMain = Frame(self, borderwidth=2, relief=SUNKEN) - frameButtons = Frame(self) - frameButtons.pack(side=BOTTOM, fill=X) - frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) - self.buttonOk = Button(frameButtons, text='Close', - command=self.Ok) - self.buttonOk.pack(padx=5, pady=5) - #self.picture = Image('photo', data=self.pictureData) - frameBg = Frame(frameMain, bg=self.bg) - frameBg.pack(expand=TRUE, fill=BOTH) + logofn = os.path.join(os.path.abspath(os.path.dirname(__file__)), + "Icons", "idle_48.gif") + self.picture = PhotoImage(master=self._root(), file=logofn) + self.frameBg = frameBg = Frame(self, bg=self.bg, borderwidth=0) + frameBg.grid(sticky='nsew') labelTitle = Label(frameBg, text='IDLE', fg=self.fg, bg=self.bg, font=('courier', 24, 'bold')) - labelTitle.grid(row=0, column=0, sticky=W, padx=10, pady=10) - #labelPicture = Label(frameBg, text='[picture]') - #image=self.picture, bg=self.bg) - #labelPicture.grid(row=1, column=1, sticky=W, rowspan=2, - # padx=0, pady=3) - byline = "Python's Integrated DeveLopment Environment" + 5*'\n' + labelTitle.grid(row=0, column=1, sticky=W, padx=10, pady=[10,0]) + labelPicture = Label(frameBg, image=self.picture, bg=self.bg) + labelPicture.grid(row=0, column=0, sticky=NE, rowspan=2, + padx=10, pady=10) + byline = "Python's Integrated DeveLopment Environment" labelDesc = Label(frameBg, text=byline, justify=LEFT, fg=self.fg, bg=self.bg) - labelDesc.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5) + labelDesc.grid(row=1, column=1, sticky=W, columnspan=3, padx=10, + pady=[0,20]) labelEmail = Label(frameBg, text='email: idle-dev@python.org', justify=LEFT, fg=self.fg, bg=self.bg) - labelEmail.grid(row=6, column=0, columnspan=2, + labelEmail.grid(row=6, column=1, columnspan=2, sticky=W, padx=10, pady=0) labelWWW = Label(frameBg, text='https://docs.python.org/' + version[:3] + '/library/idle.html', justify=LEFT, fg=self.fg, bg=self.bg) - labelWWW.grid(row=7, column=0, columnspan=2, sticky=W, padx=10, pady=0) - Frame(frameBg, borderwidth=1, relief=SUNKEN, - height=2, bg=self.bg).grid(row=8, column=0, sticky=EW, - columnspan=3, padx=5, pady=5) - labelPythonVer = Label(frameBg, text='Python version: ' + - release, fg=self.fg, bg=self.bg) - labelPythonVer.grid(row=9, column=0, sticky=W, padx=10, pady=0) + labelWWW.grid(row=7, column=1, columnspan=2, sticky=W, padx=10, pady=0) tkVer = self.tk.call('info', 'patchlevel') - labelTkVer = Label(frameBg, text='Tk version: '+ - tkVer, fg=self.fg, bg=self.bg) - labelTkVer.grid(row=9, column=1, sticky=W, padx=2, pady=0) - py_button_f = Frame(frameBg, bg=self.bg) - py_button_f.grid(row=10, column=0, columnspan=2, sticky=NSEW) - buttonLicense = Button(py_button_f, text='License', width=8, - highlightbackground=self.bg, - command=self.ShowLicense) - buttonLicense.pack(side=LEFT, padx=10, pady=10) - buttonCopyright = Button(py_button_f, text='Copyright', width=8, - highlightbackground=self.bg, - command=self.ShowCopyright) - buttonCopyright.pack(side=LEFT, padx=10, pady=10) - buttonCredits = Button(py_button_f, text='Credits', width=8, - highlightbackground=self.bg, - command=self.ShowPythonCredits) - buttonCredits.pack(side=LEFT, padx=10, pady=10) - Frame(frameBg, borderwidth=1, relief=SUNKEN, - height=2, bg=self.bg).grid(row=11, column=0, sticky=EW, - columnspan=3, padx=5, pady=5) - idle_v = Label(frameBg, text='IDLE version: ' + release, - fg=self.fg, bg=self.bg) - idle_v.grid(row=12, column=0, sticky=W, padx=10, pady=0) - idle_button_f = Frame(frameBg, bg=self.bg) - idle_button_f.grid(row=13, column=0, columnspan=3, sticky=NSEW) - idle_about_b = Button(idle_button_f, text='README', width=8, - highlightbackground=self.bg, - command=self.ShowIDLEAbout) - idle_about_b.pack(side=LEFT, padx=10, pady=10) - idle_news_b = Button(idle_button_f, text='NEWS', width=8, - highlightbackground=self.bg, - command=self.ShowIDLENEWS) - idle_news_b.pack(side=LEFT, padx=10, pady=10) - idle_credits_b = Button(idle_button_f, text='Credits', width=8, - highlightbackground=self.bg, - command=self.ShowIDLECredits) - idle_credits_b.pack(side=LEFT, padx=10, pady=10) + labelVersion = Label(frameBg, text='Python ' + + release + ' (with Tk '+tkVer+')', + fg=self.fg, bg=self.bg) + labelVersion.grid(row=4, column=1, sticky=W, padx=10, pady=[0,5]) + self.morelink = Label(frameBg, text='More...', fg='blue', bg=self.bg, + cursor='hand2') + self.morelink.grid(column=0, columnspan=3, pady=10, padx=10, sticky=E) + self.morelink.bind('<1>', self.showMore) - def ShowLicense(self): - self.display_printer_text('About - License', license) + def showMore(self, ev=None): + self.morelink.grid_forget() + fmore = Frame(self.frameBg, borderwidth=2, relief='ridge', bg='white') + fmore.grid_columnconfigure(0, weight=1) + self.t = Text(fmore, height=15, width=80, borderwidth=0, + highlightthickness=0, state='disabled', bg='white') + s = Scrollbar(fmore, command=self.t.yview) + self.t['yscrollcommand'] = s.set + self.load_moreinfo() + self.which = StringVar(self) + self.which.set(self.info[0][0]) + self.which.trace_variable('w', self.changeInfo) + fmore.grid(column=0, row=20, sticky='nwes', padx=5, pady=[15,5], + columnspan=4) + l = [] + for k, t in self.info: + l.append(k) + wh = OptionMenu(fmore, self.which, *l) + wh.grid(column=0, row=0, pady=[2,10]) + self.t.grid(column=0, row=1) + s.grid(column=1, row=0, sticky='ns', rowspan=2) + self.changeInfo() - def ShowCopyright(self): - self.display_printer_text('About - Copyright', copyright) + def changeInfo(self, *params): + for key, txt in self.info: + if key == self.which.get(): + self.t['state'] = 'normal' + self.t.delete('1.0', 'end') + self.t.insert('1.0', txt) + self.t['state'] = 'disabled' - def ShowPythonCredits(self): - self.display_printer_text('About - Python Credits', credits) + def load_moreinfo(self): + self.info = [] + self.load_from_file('IDLE Readme', 'README.txt') + self.load_from_file('IDLE News', 'NEWS.txt') + self.load_from_file('IDLE Credits', 'CREDITS.txt', 'iso-8859-1') + self.load_from_printer('Python License', license) + self.load_from_printer('Python Copyright', copyright) + self.load_from_printer('Python Credits', credits) - def ShowIDLECredits(self): - self.display_file_text('About - Credits', 'CREDITS.txt', 'iso-8859-1') + def load_from_file(self, key, filename, encoding=None): + fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), filename) + try: + with open(fn, 'r', encoding=encoding) as file: + contents = file.read() + except IOError: + pass + else: + self.info.append((key, contents)) - def ShowIDLEAbout(self): - self.display_file_text('About - Readme', 'README.txt') + def load_from_printer(self, key, printercmd): + printercmd._Printer__setup() + self.info.append((key, '\n'.join(printercmd._Printer__lines))) - def ShowIDLENEWS(self): - self.display_file_text('About - NEWS', 'NEWS.txt') - - def display_printer_text(self, title, printer): - printer._Printer__setup() - text = '\n'.join(printer._Printer__lines) - textView.view_text(self, title, text) - - def display_file_text(self, title, filename, encoding=None): - fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), filename) - textView.view_file(self, title, fn, encoding) - - def Ok(self, event=None): + def close(self, event=None): + if self.destroy_callback: + self.destroy_callback() self.destroy() if __name__ == '__main__': - from idlelib.idle_test.htest import run - run(AboutDialog) + # from idlelib.idle_test.htest import run + # run(AboutDialog) + root = Tk() + #root.wm_withdraw() + AboutDialog(root, 'About IDLE') diff -r 2c124e30a324 Lib/idlelib/configDialog.py --- a/Lib/idlelib/configDialog.py Fri Aug 07 00:43:39 2015 -0700 +++ b/Lib/idlelib/configDialog.py Tue Aug 11 09:55:38 2015 -0700 @@ -21,17 +21,29 @@ from idlelib.configHelpSourceEdit import GetHelpSourceDialog from idlelib.tabbedpages import TabbedPageSet from idlelib import macosxSupport + + class ConfigDialog(Toplevel): - def __init__(self, parent, title='', _htest=False, _utest=False): + def __init__(self, parent, title='', _htest=False, _utest=False, + must_be_modal=True, destroy_callback=None): """ _htest - bool, change box location when running htest _utest - bool, don't wait_window when running unittest """ Toplevel.__init__(self, parent) self.parent = parent - if _htest: - parent.instance_dict = {} + self.destroy_callback = destroy_callback + # Hold onto the list of files the parent belongs to, as the parent + # may go away if we're not modal. The file list may not be present + # e.g. if testing where we won't be passed an editor window; to + # prevent an API change we'll try to extract it here, rather than + # asking it to be passed to us. + try: + self.flist = parent.flist + except AttributeError: + self.flist = None + self.wm_withdraw() self.configure(borderwidth=5) @@ -60,8 +72,9 @@ self.ResetChangedItems() #load initial values in changed items dict self.CreateWidgets() self.resizable(height=FALSE, width=FALSE) - self.transient(parent) - self.grab_set() + if must_be_modal: + self.transient(parent) + self.grab_set() self.protocol("WM_DELETE_WINDOW", self.Cancel) self.tabPages.focus_set() #key bindings for this dialog @@ -70,11 +83,18 @@ #self.bind('', self.Help) #context help self.LoadConfigs() self.AttachVarCallbacks() #avoid callbacks during LoadConfigs + self.parent = None # after start, can't guarantee parent still exists if not _utest: - self.wm_deiconify() + self.activate() + + if _utest or must_be_modal: self.wait_window() + def activate(self): + self.wm_deiconify() + self.lift() + def CreateWidgets(self): self.tabPages = TabbedPageSet(self, page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General']) @@ -1140,21 +1160,17 @@ def DeactivateCurrentConfig(self): #Before a config is saved, some cleanup of current #config must be done - remove the previous keybindings - winInstances = self.parent.instance_dict.keys() - for instance in winInstances: - instance.RemoveKeybindings() + if self.flist: + self.flist.configuration_will_change() def ActivateConfigChanges(self): "Dynamically apply configuration changes" - winInstances = self.parent.instance_dict.keys() - for instance in winInstances: - instance.ResetColorizer() - instance.ResetFont() - instance.set_notabs_indentwidth() - instance.ApplyKeybindings() - instance.reset_help_menu_entries() + if self.flist: + self.flist.configuration_changed() def Cancel(self): + if self.destroy_callback: + self.destroy_callback() self.destroy() def Ok(self): diff -r 2c124e30a324 Lib/idlelib/macosxSupport.py --- a/Lib/idlelib/macosxSupport.py Fri Aug 07 00:43:39 2015 -0700 +++ b/Lib/idlelib/macosxSupport.py Tue Aug 11 09:55:38 2015 -0700 @@ -159,11 +159,11 @@ WindowList.register_callback(postwindowsmenu) def about_dialog(event=None): - from idlelib import aboutDialog - aboutDialog.AboutDialog(root, 'About IDLE') + from idlelib.uifactory import open_about + open_about() def config_dialog(event=None): - from idlelib import configDialog + from idlelib.uifactory import open_preferences # Ensure that the root object has an instance_dict attribute, # mirrors code in EditorWindow (although that sets the attribute @@ -171,7 +171,8 @@ # argument to ConfigDialog) root.instance_dict = flist.inversedict root.instance_dict = flist.inversedict - configDialog.ConfigDialog(root, 'Settings') + root.flist = flist + open_preferences(root) def help_dialog(event=None): from idlelib import textView diff -r 2c124e30a324 Lib/idlelib/uifactory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/uifactory.py Tue Aug 11 09:55:38 2015 -0700 @@ -0,0 +1,100 @@ +"""Create IDLE user interface components, factoring in any backwards + compatibility constraints, e.g. Tk8.4 vs. Tk8.5. + + We also keep track of the non-editor windows running in the application, + so there is some overlap/coordination with FileList.py +""" + +from tkinter import * + + +def initialize(root, avoid_ttk=False): + "Should be called before using rest of this module." + global _inst + _inst = _UIFactory(root, avoid_ttk) + + +def open_about(): + "Open the application's about dialog." + _inst.open_about() + + +def open_preferences(editor_window): + """Open the preferences dialog, as requested by the given editor window. + + Note that we use the editor window only when starting, to position the + preferences dialog, as well as retrieve the file list that the editor + window is part of. The provided editor window may be destroyed while + the preferences dialog is still active (e.g. if the dialog is not modal). + """ + _inst.open_preferences(editor_window) + + +def using_ttk(): + return _inst.using_ttk() + + +def other_windows_open(): + "Return True if any of the non-editor windows we manage is open." + return _inst.other_windows_open() + + +def set_allclosed_callback(cmd): + "We'll notify when the last window we manage closes; used by FileList" + _inst.set_allclosed_callback(cmd) + + +class _UIFactory(object): + def __init__(self, root, avoid_ttk=False): + self.root = root + self.using_ttk = False + self.windows = {} + self.allclosed_callback = None + if not avoid_ttk and TkVersion >= 8.5: + try: + from tkinter import ttk + self.using_ttk = True + except: + pass + + def open_about(self): + if 'about' not in self.windows.keys(): + from idlelib.aboutDialog import AboutDialog + self.windows['about'] = AboutDialog(self.root, 'About IDLE', + must_be_modal=False, + destroy_callback=lambda: self._destroyed('about')) + self.windows['about'].lift() + + def open_preferences(self, editor_window): + if 'prefs' not in self.windows.keys(): + # Later on, we'll do something like: + # if self.using_ttk: + # ... + # else: + # ... + from idlelib.configDialog import ConfigDialog + self.windows['prefs'] = ConfigDialog(editor_window, + 'Settings', must_be_modal=False, + destroy_callback=lambda: self._destroyed('prefs')) + self.windows['prefs'].lift() + + def _destroyed(self, key): + if key in self.windows.keys(): + del self.windows[key] + if not self.other_windows_open() and self.allclosed_callback: + self.allclosed_callback() + + def using_ttk(self): + return self.using_ttk + + def other_windows_open(self): + return len(self.windows) > 0 + + def set_allclosed_callback(self, cmd): + self.allclosed_callback = cmd + +_inst = None + +if __name__ == '__main__': + root = Tk() + initialize(root)