Index: ToolTip.py =================================================================== --- ToolTip.py (revision 63995) +++ ToolTip.py (revision 65573) @@ -1,89 +0,0 @@ -# general purpose 'tooltip' routines - currently unused in idlefork -# (although the 'calltips' extension is partly based on this code) -# may be useful for some purposes in (or almost in ;) the current project scope -# Ideas gleaned from PySol - -from Tkinter import * - -class ToolTipBase: - - def __init__(self, button): - self.button = button - self.tipwindow = None - self.id = None - self.x = self.y = 0 - self._id1 = self.button.bind("", self.enter) - self._id2 = self.button.bind("", self.leave) - self._id3 = self.button.bind("", self.leave) - - def enter(self, event=None): - self.schedule() - - def leave(self, event=None): - self.unschedule() - self.hidetip() - - def schedule(self): - self.unschedule() - self.id = self.button.after(1500, self.showtip) - - def unschedule(self): - id = self.id - self.id = None - if id: - self.button.after_cancel(id) - - def showtip(self): - if self.tipwindow: - return - # The tip window must be completely outside the button; - # otherwise when the mouse enters the tip window we get - # a leave event and it disappears, and then we get an enter - # event and it reappears, and so on forever :-( - x = self.button.winfo_rootx() + 20 - y = self.button.winfo_rooty() + self.button.winfo_height() + 1 - self.tipwindow = tw = Toplevel(self.button) - tw.wm_overrideredirect(1) - tw.wm_geometry("+%d+%d" % (x, y)) - self.showcontents() - - def showcontents(self, text="Your text here"): - # Override this in derived class - label = Label(self.tipwindow, text=text, justify=LEFT, - background="#ffffe0", relief=SOLID, borderwidth=1) - label.pack() - - def hidetip(self): - tw = self.tipwindow - self.tipwindow = None - if tw: - tw.destroy() - -class ToolTip(ToolTipBase): - def __init__(self, button, text): - ToolTipBase.__init__(self, button) - self.text = text - def showcontents(self): - ToolTipBase.showcontents(self, self.text) - -class ListboxToolTip(ToolTipBase): - def __init__(self, button, items): - ToolTipBase.__init__(self, button) - self.items = items - def showcontents(self): - listbox = Listbox(self.tipwindow, background="#ffffe0") - listbox.pack() - for item in self.items: - listbox.insert(END, item) - -def main(): - # Test code - root = Tk() - b = Button(root, text="Hello", command=root.destroy) - b.pack() - root.update() - tip = ListboxToolTip(b, ["Hello", "world"]) - root.mainloop() - -if __name__ == '__main__': - main() Index: AutoCompleteWindow.py =================================================================== --- AutoCompleteWindow.py (revision 63995) +++ AutoCompleteWindow.py (revision 65573) @@ -1,10 +1,16 @@ """ An auto-completion window for IDLE, used by the AutoComplete extension """ -from Tkinter import * +from Tkinter import Toplevel, Listbox, Scrollbar, TclError +from Tkconstants import END, LEFT, RIGHT, BOTH, VERTICAL, Y + +import AutoComplete from MultiCall import MC_SHIFT -import AutoComplete +from configHandler import idleConf +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Scrollbar + HIDE_VIRTUAL_EVENT_NAME = "<>" HIDE_SEQUENCES = ("", "") KEYPRESS_VIRTUAL_EVENT_NAME = "<>" Index: UndoDelegator.py =================================================================== --- UndoDelegator.py (revision 63995) +++ UndoDelegator.py (revision 65573) @@ -1,5 +1,5 @@ import string -from Tkinter import * + from Delegator import Delegator #$ event <> @@ -74,7 +74,7 @@ if is_saved != self.was_saved: self.was_saved = is_saved if self.saved_change_hook: - self.saved_change_hook() + self.saved_change_hook(None) def insert(self, index, chars, tags=None): self.addcmd(InsertCommand(index, chars, tags)) @@ -336,6 +336,7 @@ return self.depth def main(): + from Tkinter import Tk, Text from Percolator import Percolator root = Tk() root.wm_protocol("WM_DELETE_WINDOW", root.quit) Index: Bindings.py =================================================================== --- Bindings.py (revision 63995) +++ Bindings.py (revision 65573) @@ -6,15 +6,16 @@ makes it possible, for example, to define a Debug menu which is only present in the PythonShell window, and a Format menu which is only present in the Editor windows. - """ import sys + from configHandler import idleConf menudefs = [ # underscore prefixes character to underscore ('file', [ ('_New Window', '<>'), + ('New _Tab', '<>'), ('_Open...', '<>'), ('Open _Module...', '<>'), ('Class _Browser', '<>'), @@ -26,6 +27,7 @@ None, ('Prin_t Window', '<>'), None, + ('Close Tab', '<>'), ('_Close', '<>'), ('E_xit', '<>'), ]), @@ -80,7 +82,6 @@ ]), ] -import sys if sys.platform == 'darwin' and '.app' in sys.executable: # Running as a proper MacOS application bundle. This block restructures # the menus a little to make them conform better to the HIG. Index: AutoComplete.py =================================================================== --- AutoComplete.py (revision 63995) +++ AutoComplete.py (revision 65573) @@ -38,11 +38,11 @@ popupwait = idleConf.GetOption("extensions", "AutoComplete", "popupwait", type="int", default=0) - def __init__(self, editwin=None): - self.editwin = editwin - if editwin is None: # subprocess and test + def __init__(self, editpage=None): + self.editpage = editpage + if editpage is None: # subprocess and test return - self.text = editwin.text + self.text = editpage.text self.autocompletewindow = None # id of delayed call, and the index of the text insert when the delayed @@ -120,7 +120,7 @@ self.text.after_cancel(self._delayed_completion_id) self._delayed_completion_id = None - hp = HyperParser(self.editwin, "insert") + hp = HyperParser(self.editpage, "insert") curline = self.text.get("insert linestart", "insert") i = j = len(curline) if hp.is_in_string() and (not mode or mode==COMPLETE_FILES): @@ -176,7 +176,7 @@ module may be inoperative if the module was not the last to run. """ try: - rpcclt = self.editwin.flist.pyshell.interp.rpcclt + rpcclt = self.editpage.editwin.flist.pyshell.interp.rpcclt except: rpcclt = None if rpcclt: Index: configHandler.py =================================================================== --- configHandler.py (revision 63995) +++ configHandler.py (revision 65573) @@ -19,10 +19,10 @@ """ import os import sys -import string -import macosxSupport from ConfigParser import ConfigParser, NoOptionError, NoSectionError +import macosxSupport + class InvalidConfigType(Exception): pass class InvalidConfigSet(Exception): pass class InvalidFgBg(Exception): pass @@ -295,7 +295,10 @@ back=themeDict['normal-background'] else: back=themeDict[element+'-background'] - highlight={"foreground": fore,"background": back} + #bold = 0 + #if element + '-bold' in themeDict: + # bold = themeDict[element + '-bold'] + highlight={"foreground": fore, "background": back}#, "font": bold} if not fgBg: #return dict of both colours return highlight else: #return specified colour only @@ -558,6 +561,10 @@ defined here. """ keyBindings={ + '<>': [''], + '<>': [''], + '<>': ['>': ['>': ['', ''], '<>': ['', ''], '<>': ['', ''], @@ -649,7 +656,7 @@ menuItem='' #make these empty helpPath='' #so value won't be added to list else: #config entry contains ';' as expected - value=string.split(value,';') + value = value.split(';') menuItem=value[0].strip() helpPath=value[1].strip() if menuItem and helpPath: #neither are empty strings Index: HyperParser.py =================================================================== --- HyperParser.py (revision 63995) +++ HyperParser.py (revision 65573) @@ -10,22 +10,22 @@ import string import keyword + import PyParse +from editorpage import index2line class HyperParser: - def __init__(self, editwin, index): + def __init__(self, editpage, index): """Initialize the HyperParser to analyze the surroundings of the given index. """ - self.editwin = editwin - self.text = text = editwin.text + self.editwin = editwin = editpage.editwin + self.text = text = editpage.text parser = PyParse.Parser(editwin.indentwidth, editwin.tabwidth) - def index2line(index): - return int(float(index)) lno = index2line(text.index(index)) if not editwin.context_use_ps1: @@ -38,7 +38,7 @@ # its status will be the same as the char before it, if should. parser.set_str(text.get(startatindex, stopatindex)+' \n') bod = parser.find_good_parse_start( - editwin._build_char_in_string_func(startatindex)) + editwin.build_char_in_string_func(startatindex)) if bod is not None or startat == 1: break parser.set_lo(bod or 0) Index: ColorDelegator.py =================================================================== --- ColorDelegator.py (revision 63995) +++ ColorDelegator.py (revision 65573) @@ -1,8 +1,8 @@ +import re import time -import re import keyword import __builtin__ -from Tkinter import * + from Delegator import Delegator from configHandler import idleConf @@ -182,8 +182,11 @@ ok = False while not ok: mark = next - next = self.index(mark + "+%d lines linestart" % - lines_to_get) + # XXX self could be None here in some cases. + # I can reproduce it by clicking "Apply" then "Ok" + # quickly in the config dialog (while editing a not so + # small file). + next = self.index(mark + "+%d lines linestart" % lines_to_get) lines_to_get = min(lines_to_get * 2, 100) ok = "SYNC" in self.tag_names(next + "-1c") line = self.get(mark, next) @@ -248,6 +251,7 @@ self.tag_remove(tag, "1.0", "end") def main(): + from Tkinter import Tk, Text from Percolator import Percolator root = Tk() root.wm_protocol("WM_DELETE_WINDOW", root.quit) Index: configSectionNameDialog.py =================================================================== --- configSectionNameDialog.py (revision 63995) +++ configSectionNameDialog.py (revision 65573) @@ -2,96 +2,111 @@ Dialog that allows user to specify a new config file section name. Used to get new highlight theme and keybinding set names. """ -from Tkinter import * +from Tkinter import Toplevel, Entry, Frame, Label, Button, StringVar +from Tkconstants import TOP, BOTTOM, RIGHT, BOTH, SUNKEN, X import tkMessageBox +from configHandler import idleConf + +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Entry, Frame, Label, Button + class GetCfgSectionNameDialog(Toplevel): - def __init__(self,parent,title,message,usedNames): + def __init__(self, parent, title, message, usedNames): """ message - string, informational message to display usedNames - list, list of names already in use for validity check """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) - self.resizable(height=FALSE,width=FALSE) + self.resizable(height=False, width=False) self.title(title) self.transient(parent) self.grab_set() self.protocol("WM_DELETE_WINDOW", self.Cancel) self.parent = parent - self.message=message - self.usedNames=usedNames - self.result='' + self.message = message + self.usedNames = usedNames + self.result = '' self.CreateWidgets() self.withdraw() #hide while setting geometry self.update_idletasks() - #needs to be done here so that the winfo_reqwidth is valid - self.messageInfo.config(width=self.frameMain.winfo_reqwidth()) self.geometry("+%d+%d" % - ((parent.winfo_rootx()+((parent.winfo_width()/2) - -(self.winfo_reqwidth()/2)), - parent.winfo_rooty()+((parent.winfo_height()/2) - -(self.winfo_reqheight()/2)) )) ) #centre dialog over parent + ((parent.winfo_rootx() + ((parent.winfo_width() / 2) + - (self.winfo_reqwidth() / 2)), + parent.winfo_rooty() + ((parent.winfo_height() / 2) + - (self.winfo_reqheight() / 2)) )) ) #centre dialog over parent self.deiconify() #geometry set, unhide self.wait_window() def CreateWidgets(self): - self.name=StringVar(self) - self.fontSize=StringVar(self) - self.frameMain = Frame(self,borderwidth=2,relief=SUNKEN) - self.frameMain.pack(side=TOP,expand=TRUE,fill=BOTH) - self.messageInfo=Message(self.frameMain,anchor=W,justify=LEFT,padx=5,pady=5, - text=self.message)#,aspect=200) - entryName=Entry(self.frameMain,textvariable=self.name,width=30) + self.name = StringVar(self) + self.fontSize = StringVar(self) + self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN) + self.messageInfo = Label(self.frameMain, text=self.message) + entryName = Entry(self.frameMain, textvariable=self.name, width=30) + frameButtons = Frame(self) + self.buttonOk = Button(frameButtons, text='Ok', command=self.Ok) + self.buttonCancel = Button(frameButtons, text='Cancel', + command=self.Cancel) + entryName.focus_set() - self.messageInfo.pack(padx=5,pady=5)#,expand=TRUE,fill=BOTH) - entryName.pack(padx=5,pady=5) - frameButtons=Frame(self) - frameButtons.pack(side=BOTTOM,fill=X) - self.buttonOk = Button(frameButtons,text='Ok', - width=8,command=self.Ok) - self.buttonOk.grid(row=0,column=0,padx=5,pady=5) - self.buttonCancel = Button(frameButtons,text='Cancel', - width=8,command=self.Cancel) - self.buttonCancel.grid(row=0,column=1,padx=5,pady=5) + self.frameMain.pack(side=TOP, expand=True, fill=BOTH) + self.messageInfo.pack(padx=5, pady=5) + entryName.pack(padx=5, pady=5) + frameButtons.pack(side=BOTTOM, fill=X) + self.buttonOk.pack(padx=1, pady=5, side=RIGHT) + self.buttonCancel.pack(pady=5, padx=5, side=RIGHT) + + if TTK: + self.messageInfo['padding'] = 5 + frameButtons['style'] = 'RootColor.TFrame' + else: + self.messageInfo.configure(padx=5, pady=5) + def NameOk(self): #simple validity check for a sensible #ConfigParser file section name - nameOk=1 - name=self.name.get() + nameOk = 1 + name = self.name.get() name.strip() + if not name: #no name specified tkMessageBox.showerror(title='Name Error', - message='No name specified.', parent=self) - nameOk=0 - elif len(name)>30: #name too long + message='No name specified.', parent=self) + nameOk = 0 + elif len(name) > 30: #name too long tkMessageBox.showerror(title='Name Error', - message='Name too long. It should be no more than '+ - '30 characters.', parent=self) + message=('Name too long. It should be no more than ' + '30 characters.'), parent=self) nameOk=0 elif name in self.usedNames: tkMessageBox.showerror(title='Name Error', message='This name is already in use.', parent=self) nameOk=0 + return nameOk def Ok(self, event=None): if self.NameOk(): - self.result=self.name.get().strip() + self.result = self.name.get().strip() self.destroy() def Cancel(self, event=None): - self.result='' + self.result = '' self.destroy() if __name__ == '__main__': + from Tkinter import Tk #test the dialog - root=Tk() def run(): - keySeq='' - dlg=GetCfgSectionNameDialog(root,'Get Name', - 'The information here should need to be word wrapped. Test.') + keySeq = '' + dlg = GetCfgSectionNameDialog(root, 'Get Name', + 'The information here should need to be word wrapped. Test.', []) print dlg.result - Button(root,text='Dialog',command=run).pack() + + root=Tk() + Button(root, text='Dialog', command=run).pack() root.mainloop() Index: stylist.py =================================================================== --- stylist.py (revision 0) +++ stylist.py (revision 65573) @@ -0,0 +1,29 @@ +from configHandler import idleConf + +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') + +class PoorManStyle(object): + def __init__(self, parent, styles=None, cfgstyles=None): + self.parent = parent + self.cfgstyles = cfgstyles + self.styles = styles + + def configure(self, style, lookup=None, background=None): + if style not in self.cfgstyles: # passed wrong style probably + return + + widget = getattr(self.parent, self.cfgstyles[style]) + if lookup: + return widget.cget('bg') + + widget.configure(bg=background) + + def style_it(self, w, style): + if TTK: + w['style'] = style + return + + if not style in self.styles: # may not need to be styled + return + + w.configure(**self.styles[style]) Index: ZoomHeight.py =================================================================== --- ZoomHeight.py (revision 63995) +++ ZoomHeight.py (revision 65573) @@ -12,11 +12,11 @@ ]) ] - def __init__(self, editwin): - self.editwin = editwin + def __init__(self, editpage): + self.editpage = editpage def zoom_height_event(self, event): - top = self.editwin.top + top = self.editpage.editwin.top zoom_height(top) def zoom_height(top): Index: PyShell.py =================================================================== --- PyShell.py (revision 63995) +++ PyShell.py (revision 65573) @@ -1,40 +1,50 @@ #! /usr/bin/env python - import os -import os.path +import re import sys +import time +import types import string import getopt -import re import socket -import time +import linecache import threading -import traceback -import types -import macosxSupport - -import linecache from code import InteractiveInterpreter try: - from Tkinter import * + from Tkinter import Tk, Toplevel, TclError + from Tkconstants import END except ImportError: print>>sys.__stderr__, "** IDLE can't import Tkinter. " \ "Your Python may not be configured for Tk. **" sys.exit(1) +try: + from ttk import Style + TTK = 1 +except ImportError: + print >> sys.stderr, "** IDLE can't import ttk." + TTK = 0 + import tkMessageBox -from EditorWindow import EditorWindow, fixwordbreaks -from FileList import FileList -from ColorDelegator import ColorDelegator -from UndoDelegator import UndoDelegator -from OutputWindow import OutputWindow +import macosxSupport from configHandler import idleConf -import idlever +# store ttk availability +idleConf.SetOption('main', 'General', 'use-ttk', str(TTK)) +idleConf.SaveUserCfgFiles() + import rpc +import utils +import idlever import Debugger +import IOBinding import RemoteDebugger +from FileList import FileList +from OutputWindow import OutputWindow +from EditorWindow import EditorWindow, fixwordbreaks +from UndoDelegator import UndoDelegator +from ColorDelegator import ColorDelegator IDENTCHARS = string.ascii_letters + string.digits + "_" LOCALHOST = '127.0.0.1' @@ -55,18 +65,20 @@ except ImportError: pass else: - def idle_showwarning(message, category, filename, lineno): + def idle_showwarning(message, category, filename, lineno, line=None): file = warning_stream try: - file.write(warnings.formatwarning(message, category, filename, lineno)) + file.write(warnings.formatwarning(message, category, filename, + lineno, line)) except IOError: pass ## file (probably __stderr__) is invalid, warning dropped. warnings.showwarning = idle_showwarning - def idle_formatwarning(message, category, filename, lineno): + def idle_formatwarning(message, category, filename, lineno, line=None): """Format warnings the IDLE way""" s = "\nWarning (from warnings module):\n" s += ' File \"%s\", line %s\n' % (filename, lineno) - line = linecache.getline(filename, lineno).strip() + if line is None: + line = linecache.getline(filename, lineno).strip() if line: s += " %s\n" % line s += "%s: %s\n>>> " % (category.__name__, message) @@ -101,57 +113,63 @@ "Regular text edit window in IDLE, supports breakpoints" def __init__(self, *args): - self.breakpoints = [] EditorWindow.__init__(self, *args) - self.text.bind("<>", self.set_breakpoint_here) - self.text.bind("<>", self.clear_breakpoint_here) - self.text.bind("<>", self.flist.open_shell) self.breakpointPath = os.path.join(idleConf.GetUserCfgDir(), - 'breakpoints.lst') + 'breakpoints.lst') + self.top.bind('<>', self.__configure_new_tab) + self.__configure_new_tab() + + def __configure_new_tab(self, event=None): + page = self.text_notebook.last_page().editpage + page.breakpoints = [] + text = page.text + + text.bind("<>", + utils.callback(self._set_breakpoint_here, text, page)) + text.bind("<>", + utils.callback(self._clear_breakpoint_here, text, page)) + text.bind("<>", self.flist.open_shell) + # whenever a file is changed, restore breakpoints - if self.io.filename: self.restore_file_breaks() - def filename_changed_hook(old_hook=self.io.filename_change_hook, + if page.io.filename: + self.restore_file_breaks(text, page) + + def filename_changed_hook(old_hook=page.io.filename_change_hook, self=self): - self.restore_file_breaks() + self.restore_file_breaks(text, page) old_hook() - self.io.set_filename_change_hook(filename_changed_hook) + page.io.set_filename_change_hook(filename_changed_hook) - rmenu_specs = [("Set Breakpoint", "<>"), - ("Clear Breakpoint", "<>")] - - def set_breakpoint(self, lineno): - text = self.text - filename = self.io.filename + def set_breakpoint(self, lineno, text, page): + filename = page.io.filename text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1)) try: - i = self.breakpoints.index(lineno) + i = page.breakpoints.index(lineno) except ValueError: # only add if missing, i.e. do once - self.breakpoints.append(lineno) + page.breakpoints.append(lineno) try: # update the subprocess debugger debug = self.flist.pyshell.interp.debugger debug.set_breakpoint_here(filename, lineno) except: # but debugger may not be active right now.... pass - def set_breakpoint_here(self, event=None): - text = self.text - filename = self.io.filename + def _set_breakpoint_here(self, event=None, text=None, page=None): + filename = page.io.filename if not filename: text.bell() return lineno = int(float(text.index("insert"))) - self.set_breakpoint(lineno) + self.set_breakpoint(lineno, text, page) - def clear_breakpoint_here(self, event=None): - text = self.text - filename = self.io.filename + def _clear_breakpoint_here(self, event=None, text=None, page=None): + filename = page.io.filename if not filename: text.bell() return lineno = int(float(text.index("insert"))) try: - self.breakpoints.remove(lineno) + page.breakpoints.remove(lineno) except: pass text.tag_remove("BREAK", "insert linestart",\ @@ -163,13 +181,17 @@ pass def clear_file_breaks(self): - if self.breakpoints: - text = self.text - filename = self.io.filename + for page in self.text_notebook.pages.itervalues(): + page = page.editpage + text = page.text + filename = page.io.filename + + if not page.breakpoints: + continue if not filename: text.bell() - return - self.breakpoints = [] + continue + page.breakpoints = [] text.tag_remove("BREAK", "1.0", END) try: debug = self.flist.pyshell.interp.debugger @@ -177,7 +199,7 @@ except: pass - def store_file_breaks(self): + def store_file_breaks(self, page): "Save breakpoints when file is saved" # XXX 13 Dec 2002 KBK Currently the file must be saved before it can # be run. The breaks are saved at that time. If we introduce @@ -200,8 +222,8 @@ # debugger is loaded) is updated during the save, the visible # breaks stay synched with the subprocess even if one of these # unexpected breakpoint deletions occurs. - breaks = self.breakpoints - filename = self.io.filename + breaks = page.breakpoints + filename = page.io.filename try: lines = open(self.breakpointPath,"r").readlines() except IOError: @@ -210,49 +232,42 @@ for line in lines: if not line.startswith(filename + '='): new_file.write(line) - self.update_breakpoints() - breaks = self.breakpoints + self.update_breakpoints(page) + breaks = page.breakpoints if breaks: new_file.write(filename + '=' + str(breaks) + '\n') new_file.close() - def restore_file_breaks(self): - self.text.update() # this enables setting "BREAK" tags to be visible - filename = self.io.filename + def restore_file_breaks(self, text, page): + text.update() # this enables setting "BREAK" tags to be visible + filename = page.io.filename if filename is None: return if os.path.isfile(self.breakpointPath): - lines = open(self.breakpointPath,"r").readlines() + lines = open(self.breakpointPath, "r").readlines() for line in lines: if line.startswith(filename + '='): breakpoint_linenumbers = eval(line[len(filename)+1:]) for breakpoint_linenumber in breakpoint_linenumbers: - self.set_breakpoint(breakpoint_linenumber) + self.set_breakpoint(breakpoint_linenumber, text, page) - def update_breakpoints(self): - "Retrieves all the breakpoints in the current window" - text = self.text + def update_breakpoints(self, page): + "Retrieves all the breakpoints in the current page" + text = page.text ranges = text.tag_ranges("BREAK") linenumber_list = self.ranges_to_linenumbers(ranges) - self.breakpoints = linenumber_list + page.breakpoints = linenumber_list def ranges_to_linenumbers(self, ranges): lines = [] for index in range(0, len(ranges), 2): - lineno = int(float(ranges[index])) - end = int(float(ranges[index+1])) + lineno = int(float(str(ranges[index]))) + end = int(float(str(ranges[index+1]))) while lineno < end: lines.append(lineno) lineno += 1 return lines -# XXX 13 Dec 2002 KBK Not used currently -# def saved_change_hook(self): -# "Extend base method - clear breaks if module is modified" -# if not self.get_saved(): -# self.clear_file_breaks() -# EditorWindow.saved_change_hook(self) - def _close(self): "Extend base method - clear breaks when module is closed" self.clear_file_breaks() @@ -589,7 +604,6 @@ self.save_warnings_filters = warnings.filters[:] warnings.filterwarnings(action="error", category=SyntaxWarning) if isinstance(source, types.UnicodeType): - import IOBinding try: source = source.encode(IOBinding.encoding) except UnicodeError: @@ -814,30 +828,23 @@ flist = PyShellFileList(root) # OutputWindow.__init__(self, flist, None, None) + # remove things related to tabs. The window running the shell is + # considered special enough to have a single window for it. + self.remove_tab_controls() + # -## self.config(usetabs=1, indentwidth=8, context_use_ps1=1) self.usetabs = True # indentwidth must be 8 when using tabs. See note in EditorWindow: self.indentwidth = 8 self.context_use_ps1 = True # - text = self.text - text.configure(wrap="char") - text.bind("<>", self.enter_callback) - text.bind("<>", self.linefeed_callback) - text.bind("<>", self.cancel_callback) - text.bind("<>", self.eof_callback) - text.bind("<>", self.open_stack_viewer) - text.bind("<>", self.toggle_debugger) - text.bind("<>", self.toggle_jit_stack_viewer) - if use_subprocess: - text.bind("<>", self.view_restart_mark) - text.bind("<>", self.restart_shell) - # + self.wtext = None + self.textpage = None + self.__configure_new_tab() + self.save_stdout = sys.stdout self.save_stderr = sys.stderr self.save_stdin = sys.stdin - import IOBinding self.stdout = PseudoFile(self, "stdout", IOBinding.encoding) self.stderr = PseudoFile(self, "stderr", IOBinding.encoding) self.console = PseudoFile(self, "console", IOBinding.encoding) @@ -845,11 +852,37 @@ sys.stdout = self.stdout sys.stderr = self.stderr sys.stdin = self - # - self.history = self.History(self.text) - # + self.pollinterval = 50 # millisec + if TTK: + self.set_theme(Style(self.root)) + + def __configure_new_tab(self): + # select the last page (the one added) + page = self.text_notebook.last_page().editpage + self.wtext = text = page.text + self.textpage = page + + text.configure(wrap="char") + text.bind("<>", + utils.callback(self._enter_callback, text)) + text.bind("<>", + utils.callback(self._linefeed_callback, text)) + text.bind("<>", + utils.callback(self._cancel_callback, text)) + text.bind("<>", + utils.callback(self._eof_callback, text)) + text.bind("<>", self._toggle_debugger) + text.bind("<>", self._toggle_jit_stack_viewer) + text.bind("<>", self.open_stack_viewer) + if use_subprocess: + text.bind("<>", + utils.callback(self._view_restart_mark, text)) + text.bind("<>", self.restart_shell) + + page.history = self.History(text) + def get_standard_extension_names(self): return idleConf.GetExtensions(shell_only=True) @@ -866,11 +899,11 @@ def get_warning_stream(self): return warning_stream - def toggle_debugger(self, event=None): + def _toggle_debugger(self, event=None): if self.executing: tkMessageBox.showerror("Don't debug now", "You can only toggle the debugger when idle", - master=self.text) + master=self.text_notebook) self.set_debugger_indicator() return "break" else: @@ -884,7 +917,7 @@ db = self.interp.getdebugger() self.setvar("<>", not not db) - def toggle_jit_stack_viewer(self, event=None): + def _toggle_jit_stack_viewer(self, event=None): pass # All we need is the variable def close_debugger(self): @@ -930,7 +963,7 @@ "Kill?", "The program is still running!\n Do you want to kill it?", default="ok", - parent=self.text) + parent=self.text_notebook) if response is False: return "cancel" if self.reading: @@ -938,7 +971,7 @@ self.canceled = True self.closing = True # Wait for poll_subprocess() rescheduling to stop - self.text.after(2 * self.pollinterval, self.close2) + self.text_notebook.after(2 * self.pollinterval, self.close2) def close2(self): return EditorWindow.close(self) @@ -956,7 +989,9 @@ self.interp = None self.console = None self.flist.pyshell = None - self.history = None + for page in self.text_notebook.pages.itervalues(): + page.editpage.history = None + EditorWindow._close(self) def ispythonsource(self, filename): @@ -996,38 +1031,12 @@ Tkinter._default_root = None # 03Jan04 KBK What's this? return True - def readline(self): - save = self.reading - try: - self.reading = 1 - self.top.mainloop() # nested mainloop() - finally: - self.reading = save - line = self.text.get("iomark", "end-1c") - if len(line) == 0: # may be EOF if we quit our mainloop with Ctrl-C - line = "\n" - if isinstance(line, unicode): - import IOBinding - try: - line = line.encode(IOBinding.encoding) - except UnicodeError: - pass - self.resetoutput() - if self.canceled: - self.canceled = 0 - if not use_subprocess: - raise KeyboardInterrupt - if self.endoffile: - self.endoffile = 0 - line = "" - return line - def isatty(self): return True - def cancel_callback(self, event=None): + def _cancel_callback(self, event=None, text=None): try: - if self.text.compare("sel.first", "!=", "sel.last"): + if text.compare("sel.first", "!=", "sel.last"): return # Active selection -- always use default binding except: pass @@ -1047,11 +1056,11 @@ self.top.quit() # exit the nested mainloop() in readline() return "break" - def eof_callback(self, event): + def _eof_callback(self, event, text): if self.executing and not self.reading: return # Let the default binding (delete next char) take over - if not (self.text.compare("iomark", "==", "insert") and - self.text.compare("insert", "==", "end-1c")): + if not (text.compare("iomark", "==", "insert") and + text.compare("insert", "==", "end-1c")): return # Let the default binding (delete next char) take over if not self.executing: self.resetoutput() @@ -1062,24 +1071,24 @@ self.top.quit() return "break" - def linefeed_callback(self, event): + def _linefeed_callback(self, event, text): # Insert a linefeed without entering anything (still autoindented) if self.reading: - self.text.insert("insert", "\n") - self.text.see("insert") + text.insert("insert", "\n") + text.see("insert") else: self.newline_and_indent_event(event) return "break" - def enter_callback(self, event): + def _enter_callback(self, event, text): if self.executing and not self.reading: return # Let the default binding (insert '\n') take over # If some text is selected, recall the selection # (but only if this before the I/O mark) try: - sel = self.text.get("sel.first", "sel.last") + sel = text.get("sel.first", "sel.last") if sel: - if self.text.compare("sel.last", "<=", "iomark"): + if text.compare("sel.last", "<=", "iomark"): self.recall(sel, event) return "break" except: @@ -1087,51 +1096,52 @@ # If we're strictly before the line containing iomark, recall # the current line, less a leading prompt, less leading or # trailing whitespace - if self.text.compare("insert", "<", "iomark linestart"): + if text.compare("insert", "<", "iomark linestart"): # Check if there's a relevant stdin range -- if so, use it - prev = self.text.tag_prevrange("stdin", "insert") - if prev and self.text.compare("insert", "<", prev[1]): - self.recall(self.text.get(prev[0], prev[1]), event) + prev = text.tag_prevrange("stdin", "insert") + if prev and text.compare("insert", "<", prev[1]): + self.recall(text.get(prev[0], prev[1]), event) return "break" - next = self.text.tag_nextrange("stdin", "insert") - if next and self.text.compare("insert lineend", ">=", next[0]): - self.recall(self.text.get(next[0], next[1]), event) + next = text.tag_nextrange("stdin", "insert") + if next and text.compare("insert lineend", ">=", next[0]): + self.recall(text.get(next[0], next[1]), event) return "break" # No stdin mark -- just get the current line, less any prompt - indices = self.text.tag_nextrange("console", "insert linestart") + indices = text.tag_nextrange("console", "insert linestart") if indices and \ - self.text.compare(indices[0], "<=", "insert linestart"): - self.recall(self.text.get(indices[1], "insert lineend"), event) + text.compare(indices[0], "<=", "insert linestart"): + self.recall(text.get(indices[1], "insert lineend"), event) else: - self.recall(self.text.get("insert linestart", "insert lineend"), event) + self.recall(text.get("insert linestart", "insert lineend"), + event) return "break" # If we're between the beginning of the line and the iomark, i.e. # in the prompt area, move to the end of the prompt - if self.text.compare("insert", "<", "iomark"): - self.text.mark_set("insert", "iomark") + if text.compare("insert", "<", "iomark"): + text.mark_set("insert", "iomark") # If we're in the current input and there's only whitespace # beyond the cursor, erase that whitespace first - s = self.text.get("insert", "end-1c") + s = text.get("insert", "end-1c") if s and not s.strip(): - self.text.delete("insert", "end-1c") + text.delete("insert", "end-1c") # If we're in the current input before its last line, # insert a newline right at the insert point - if self.text.compare("insert", "<", "end-1c linestart"): + if text.compare("insert", "<", "end-1c linestart"): self.newline_and_indent_event(event) return "break" # We're in the last line; append a newline and submit it - self.text.mark_set("insert", "end-1c") + text.mark_set("insert", "end-1c") if self.reading: - self.text.insert("insert", "\n") - self.text.see("insert") + text.insert("insert", "\n") + text.see("insert") else: self.newline_and_indent_event(event) - self.text.tag_add("stdin", "iomark", "end-1c") - self.text.update_idletasks() + text.tag_add("stdin", "iomark", "end-1c") + text.update_idletasks() if self.reading: self.top.quit() # Break out of recursive mainloop() in raw_input() else: - self.runit() + self._runit(text) return "break" def recall(self, s, event): @@ -1139,15 +1149,15 @@ s = re.sub(r'^\s*\n', '' , s) s = re.sub(r'\n\s*$', '', s) lines = s.split('\n') - self.text.undo_block_start() + self.wtext.undo_block_start() try: - self.text.tag_remove("sel", "1.0", "end") - self.text.mark_set("insert", "end-1c") - prefix = self.text.get("insert linestart", "insert") + self.wtext.tag_remove("sel", "1.0", "end") + self.wtext.mark_set("insert", "end-1c") + prefix = self.wtext.get("insert linestart", "insert") if prefix.rstrip().endswith(':'): self.newline_and_indent_event(event) - prefix = self.text.get("insert linestart", "insert") - self.text.insert("insert", lines[0].strip()) + prefix = self.wtext.get("insert linestart", "insert") + self.wtext.insert("insert", lines[0].strip()) if len(lines) > 1: orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) new_base_indent = re.search(r'^([ \t]*)', prefix).group(0) @@ -1155,13 +1165,13 @@ if line.startswith(orig_base_indent): # replace orig base indentation with new indentation line = new_base_indent + line[len(orig_base_indent):] - self.text.insert('insert', '\n'+line.rstrip()) + self.wtext.insert('insert', '\n'+line.rstrip()) finally: - self.text.see("insert") - self.text.undo_block_stop() + self.wtext.see("insert") + self.wtext.undo_block_stop() - def runit(self): - line = self.text.get("iomark", "end-1c") + def _runit(self, text): + line = text.get("iomark", "end-1c") # Strip off last newline and surrounding whitespace. # (To allow you to hit return twice to end a statement.) i = len(line) @@ -1179,18 +1189,18 @@ return self.interp.remote_stack_viewer() try: sys.last_traceback - except: + except AttributeError: tkMessageBox.showerror("No stack trace", "There is no stack trace yet.\n" "(sys.last_traceback is not defined)", - master=self.text) + master=self.text_notebook) return from StackViewer import StackBrowser sv = StackBrowser(self.root, self.flist) - def view_restart_mark(self, event=None): - self.text.see("iomark") - self.text.see("restart") + def _view_restart_mark(self, event=None, text=None): + text.see("iomark") + text.see("restart") def restart_shell(self, event=None): self.interp.restart_subprocess() @@ -1202,25 +1212,30 @@ except: s = "" self.console.write(s) - self.text.mark_set("insert", "end-1c") + + curr_page = self.current_page + if not curr_page: + return + + curr_page.text.mark_set("insert", "end-1c") self.set_line_and_column() - self.io.reset_undo() + curr_page.io.reset_undo() def resetoutput(self): - source = self.text.get("iomark", "end-1c") - if self.history: - self.history.history_store(source) - if self.text.get("end-2c") != "\n": - self.text.insert("end-1c", "\n") - self.text.mark_set("iomark", "end-1c") + source = self.wtext.get("iomark", "end-1c") + if self.textpage.history: + self.textpage.history.history_store(source) + if self.wtext.get("end-2c") != "\n": + self.wtext.insert("end-1c", "\n") + self.wtext.mark_set("iomark", "end-1c") self.set_line_and_column() sys.stdout.softspace = 0 def write(self, s, tags=()): try: - self.text.mark_gravity("iomark", "right") - OutputWindow.write(self, s, tags, "iomark") - self.text.mark_gravity("iomark", "left") + self.wtext.mark_gravity("iomark", "right") + OutputWindow.write(self, s, tags, "iomark", self.wtext) + self.wtext.mark_gravity("iomark", "left") except: pass if self.canceled: @@ -1381,6 +1396,19 @@ # start editor and/or shell windows: root = Tk(className="Idle") + if TTK: + # create base styles used along idle files + style = Style() + + rootbg = style.map('.', 'background') + fstyle = {'background': []} + for sspec in rootbg: + if 'active' in sspec[:-1]: + fstyle['background'].append(('!disabled', sspec[-1])) + break + style.map('RootColor.TFrame', **fstyle) + # end styles + fixwordbreaks(root) root.withdraw() flist = PyShellFileList(root) Index: ParenMatch.py =================================================================== --- ParenMatch.py (revision 63995) +++ ParenMatch.py (revision 65573) @@ -56,14 +56,13 @@ RESTORE_SEQUENCES = ("", "", "", "") - def __init__(self, editwin): - self.editwin = editwin - self.text = editwin.text + def __init__(self, editpage): + self.editpage = editpage + self.text = editpage.text # Bind the check-restore event to the function restore_event, # so that we can then use activate_restore (which calls event_add) # and deactivate_restore (which calls event_delete). - editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, - self.restore_event) + editpage.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, self.restore_event) self.counter = 0 self.is_restore_active = 0 self.set_style(self.STYLE) @@ -90,7 +89,7 @@ self.set_timeout = self.set_timeout_none def flash_paren_event(self, event): - indices = HyperParser(self.editwin, "insert").get_surrounding_brackets() + indices = HyperParser(self.editpage, "insert").get_surrounding_brackets() if indices is None: self.warn_mismatched() return @@ -103,7 +102,7 @@ closer = self.text.get("insert-1c") if closer not in _openers: return - hp = HyperParser(self.editwin, "insert-1c") + hp = HyperParser(self.editpage, "insert-1c") if not hp.is_in_code(): return indices = hp.get_surrounding_brackets(_openers[closer], True) @@ -159,14 +158,13 @@ if index != self.text.index("insert"): self.handle_restore_timer(c) else: - self.editwin.text_frame.after(CHECK_DELAY, callme, callme) - self.editwin.text_frame.after(CHECK_DELAY, callme, callme) + self.text.master.after(CHECK_DELAY, callme, callme) + self.text.master.after(CHECK_DELAY, callme, callme) def set_timeout_last(self): """The last highlight created will be removed after .5 sec""" # associate a counter with an event; only disable the "paren" # tag if the event is for the most recent timer. self.counter += 1 - self.editwin.text_frame.after(self.FLASH_DELAY, - lambda self=self, c=self.counter: \ - self.handle_restore_timer(c)) + self.text.master.after(self.FLASH_DELAY, + lambda self=self, c=self.counter: self.handle_restore_timer(c)) Index: config-keys.def =================================================================== --- config-keys.def (revision 63995) +++ config-keys.def (revision 65573) @@ -8,6 +8,10 @@ # configuration gui. [IDLE Classic Windows] +close-tab= +new-tab= +next-tab= +prev-tab= copy= cut= paste= @@ -59,6 +63,10 @@ del-word-right= [IDLE Classic Unix] +close-tab= +new-tab= +next-tab= +prev-tab= copy= cut= paste= @@ -110,6 +118,10 @@ del-word-right= [IDLE Classic Mac] +close-tab= +new-tab= +next-tab= +prev-tab= copy= cut= paste= @@ -161,6 +173,10 @@ del-word-right= [IDLE Classic OSX] +close-tab= +new-tab = +next-tab= +prev-tab= toggle-tabs = interrupt-execution = untabify-region = Index: Debugger.py =================================================================== --- Debugger.py (revision 63995) +++ Debugger.py (revision 65573) @@ -1,11 +1,17 @@ import os import bdb import types -from Tkinter import * +from Tkinter import Frame, Button, Entry, Checkbutton, Label, Scrollbar, \ + Canvas, BooleanVar +from Tkconstants import W, LEFT, DISABLED, X, Y, BOTH, NW, GROOVE, RIGHT + +import macosxSupport from WindowList import ListedToplevel from ScrolledList import ScrolledList -import macosxSupport +from configHandler import idleConf +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Frame, Button, Checkbutton, Scrollbar class Idb(bdb.Bdb): @@ -92,7 +98,7 @@ self.top.bind("", self.close) # self.bframe = bframe = Frame(top) - self.bframe.pack(anchor="w") + self.bframe.pack(anchor=W) self.buttons = bl = [] # self.bcont = b = Button(bframe, text="Go", command=self.cont) @@ -107,11 +113,11 @@ bl.append(b) # for b in bl: - b.configure(state="disabled") - b.pack(side="left") + b.configure(state=DISABLED) + b.pack(side=LEFT) # self.cframe = cframe = Frame(bframe) - self.cframe.pack(side="left") + self.cframe.pack(side=LEFT) # if not self.vstack: self.__class__.vstack = BooleanVar(top) @@ -136,18 +142,18 @@ text="Globals", command=self.show_globals, variable=self.vglobals) self.bglobals.grid(row=1, column=1) # - self.status = Label(top, anchor="w") - self.status.pack(anchor="w") - self.error = Label(top, anchor="w") - self.error.pack(anchor="w", fill="x") + self.status = Label(top, anchor=W) + self.status.pack(anchor=W) + self.error = Label(top, anchor=W) + self.error.pack(anchor=W, fill=X) self.errorbg = self.error.cget("background") # self.fstack = Frame(top, height=1) - self.fstack.pack(expand=1, fill="both") + self.fstack.pack(expand=1, fill=BOTH) self.flocals = Frame(top) - self.flocals.pack(expand=1, fill="both") + self.flocals.pack(expand=1, fill=BOTH) self.fglobals = Frame(top, height=1) - self.fglobals.pack(expand=1, fill="both") + self.fglobals.pack(expand=1, fill=BOTH) # if self.vstack.get(): self.show_stack() @@ -155,6 +161,7 @@ self.show_locals() if self.vglobals.get(): self.show_globals() + # def interaction(self, message, frame, info=None): self.frame = frame @@ -313,12 +320,14 @@ "Load PyShellEditorWindow breakpoints into subprocess debugger" pyshell_edit_windows = self.pyshell.flist.inversedict.keys() for editwin in pyshell_edit_windows: - filename = editwin.io.filename - try: - for lineno in editwin.breakpoints: - self.set_breakpoint_here(filename, lineno) - except AttributeError: - continue + for page in editwin.text_notebook.pages.itervalues(): + editpage = page.editpage + filename = editpage.io.filename + try: + for lineno in editpage.breakpoints: + self.set_breakpoint_here(filename, lineno) + except AttributeError: + continue class StackViewer(ScrolledList): @@ -348,8 +357,7 @@ funcname = code.co_name import linecache sourceline = linecache.getline(filename, lineno) - import string - sourceline = string.strip(sourceline) + sourceline = sourceline.strip() if funcname in ("?", "", None): item = "%s, line %d: %s" % (modname, lineno, sourceline) else: @@ -413,24 +421,25 @@ height = 20*len(dict) # XXX 20 == observed height of Entry widget self.master = master self.title = title + import repr self.repr = repr.Repr() self.repr.maxstring = 60 self.repr.maxother = 60 self.frame = frame = Frame(master) - self.frame.pack(expand=1, fill="both") - self.label = Label(frame, text=title, borderwidth=2, relief="groove") - self.label.pack(fill="x") - self.vbar = vbar = Scrollbar(frame, name="vbar") - vbar.pack(side="right", fill="y") + self.frame.pack(expand=1, fill=BOTH) + self.label = Label(frame, text=title, borderwidth=2, relief=GROOVE) + self.label.pack(fill=X) + vbar = Scrollbar(frame, name="vbar") + vbar.pack(side=RIGHT, fill=Y) self.canvas = canvas = Canvas(frame, height=min(300, max(40, height)), scrollregion=(0, 0, width, height)) - canvas.pack(side="left", fill="both", expand=1) + canvas.pack(side=LEFT, fill=BOTH, expand=1) vbar["command"] = canvas.yview canvas["yscrollcommand"] = vbar.set self.subframe = subframe = Frame(canvas) - self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw") + self.sfid = canvas.create_window(0, 0, window=subframe, anchor=NW) self.load_dict(dict) dict = -1 @@ -458,10 +467,10 @@ if rpc_client: svalue = svalue[1:-1] l = Label(subframe, text=name) - l.grid(row=row, column=0, sticky="nw") + l.grid(row=row, column=0, sticky=NW) l = Entry(subframe, width=0, borderwidth=0) l.insert(0, svalue) - l.grid(row=row, column=1, sticky="nw") + l.grid(row=row, column=1, sticky=NW) row = row+1 self.dict = dict # XXX Could we use a callback for the following? Index: configDialog.py =================================================================== --- configDialog.py (revision 63995) +++ configDialog.py (revision 65573) @@ -7,19 +7,30 @@ Note that tab width in IDLE is currently fixed at eight due to Tk issues. Refer to comments in EditorWindow autoindent code for details. - """ -from Tkinter import * +from Tkinter import Toplevel, Frame, Button, Scale, Label, LabelFrame, Text, \ + Listbox, Scrollbar, Checkbutton, Radiobutton, Entry, \ + Checkbutton, StringVar, BooleanVar, IntVar +from Tkconstants import LEFT, RIGHT, BOTTOM, TOP, BOTH, GROOVE, SOLID, NONE, \ + END, DISABLED, NSEW, Y, X, W, E, HORIZONTAL, NS, EW, \ + N, ANCHOR, NORMAL import tkMessageBox, tkColorChooser, tkFont -import string +from stylist import PoorManStyle +from tabbedpages import get_tabbedpage from configHandler import idleConf +from keybindingDialog import GetKeysDialog from dynOptionMenuWidget import DynOptionMenu -from tabbedpages import TabbedPageSet -from keybindingDialog import GetKeysDialog +from configHelpSourceEdit import GetHelpSourceDialog from configSectionNameDialog import GetCfgSectionNameDialog -from configHelpSourceEdit import GetHelpSourceDialog +TabbedPageSet = get_tabbedpage() +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Frame, Button, Checkbutton, LabelFrame, LabeledScale, \ + Combobox, Checkbutton, Entry, Radiobutton, Scrollbar, \ + Label, Style + class ConfigDialog(Toplevel): def __init__(self,parent,title): @@ -27,8 +38,8 @@ self.wm_withdraw() self.configure(borderwidth=5) - self.geometry("+%d+%d" % (parent.winfo_rootx()+20, - parent.winfo_rooty()+30)) + self.geometry("+%d+%d" % (parent.winfo_rootx() + 20, + parent.winfo_rooty() + 30)) #Theme Elements. Each theme element key is its display name. #The first value of the tuple is the sample area tag name. #The second value is the display name list sort index. @@ -47,8 +58,9 @@ 'Shell Stderr Text':('stderr','12'), } self.ResetChangedItems() #load initial values in changed items dict + self.SetupStyles() self.CreateWidgets() - self.resizable(height=FALSE,width=FALSE) + self.resizable(height=False, width=False) self.transient(parent) self.grab_set() self.protocol("WM_DELETE_WINDOW", self.Cancel) @@ -64,34 +76,54 @@ self.wm_deiconify() self.wait_window() + def SetupStyles(self): + if TTK: + style = Style(self.master) + style.configure('S.TButton', padding=[6, 3]) + style.configure('S2.TFrame', padding=2) + style.configure('Color.TFrame', background='blue') + self.ttkstyle = style + self.style = lambda w, style: w.configure(style=style) + else: + self.ttkstyle = PoorManStyle(self, styles={ + 'S.TButton': {'pady': 6, 'padx': 3}, + 'S2.TFrame': {'padx': 2, 'pady': 2} + }, cfgstyles={'Color.TFrame': 'frameColourSet'}) + self.style = self.ttkstyle.style_it + def CreateWidgets(self): self.tabPages = TabbedPageSet(self, - page_names=['Fonts/Tabs','Highlighting','Keys','General']) - frameActionButtons = Frame(self,pady=2) + page_names=['Fonts/Tabs','Highlighting','Keys','General']) + frameActionButtons = Frame(self) #action buttons self.buttonHelp = Button(frameActionButtons,text='Help', - command=self.Help,takefocus=FALSE, - padx=6,pady=3) - self.buttonOk = Button(frameActionButtons,text='Ok', - command=self.Ok,takefocus=FALSE, - padx=6,pady=3) - self.buttonApply = Button(frameActionButtons,text='Apply', - command=self.Apply,takefocus=FALSE, - padx=6,pady=3) - self.buttonCancel = Button(frameActionButtons,text='Cancel', - command=self.Cancel,takefocus=FALSE, - padx=6,pady=3) + command=self.Help, takefocus=False) + self.buttonOk = Button(frameActionButtons, text='Ok', + command=self.Ok, takefocus=False) + self.buttonApply = Button(frameActionButtons, text='Apply', + command=self.Apply, takefocus=False) + self.buttonCancel = Button(frameActionButtons, text='Cancel', + command=self.Cancel, takefocus=False) + + # Apply styles + s = self.style + s(frameActionButtons, 'RootColor.TFrame') + s(self.buttonHelp, 'S.TButton') + s(self.buttonOk, 'S.TButton') + s(self.buttonApply, 'S.TButton') + s(self.buttonCancel, 'S.TButton') + self.CreatePageFontTab() self.CreatePageHighlight() self.CreatePageKeys() self.CreatePageGeneral() - self.buttonHelp.pack(side=RIGHT,padx=5) - self.buttonOk.pack(side=LEFT,padx=5) - self.buttonApply.pack(side=LEFT,padx=5) - self.buttonCancel.pack(side=LEFT,padx=5) - frameActionButtons.pack(side=BOTTOM) + self.buttonHelp.pack(side=LEFT, pady=6) + self.buttonApply.pack(side=RIGHT, pady=6) + self.buttonOk.pack(side=RIGHT, padx=6, pady=6) + self.buttonCancel.pack(side=RIGHT, pady=6) + frameActionButtons.pack(side=BOTTOM, fill=X, expand=True) Frame(self, height=2, borderwidth=0).pack(side=BOTTOM) - self.tabPages.pack(side=TOP,expand=TRUE,fill=BOTH) + self.tabPages.pack(side=TOP,expand=True,fill=BOTH) def CreatePageFontTab(self): #tkVars @@ -113,8 +145,8 @@ frameFontParam=Frame(frameFont) labelFontNameTitle=Label(frameFontName,justify=LEFT, text='Font Face :') - self.listFontName=Listbox(frameFontName,height=5,takefocus=FALSE, - exportselection=FALSE) + self.listFontName=Listbox(frameFontName,height=5,takefocus=False, + exportselection=False) self.listFontName.bind('',self.OnListFontButtonRelease) scrollFont=Scrollbar(frameFontName) scrollFont.config(command=self.listFontName.yview) @@ -127,122 +159,143 @@ frameFontSample=Frame(frameFont,relief=SOLID,borderwidth=1) self.labelFontSample=Label(frameFontSample, text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]', - justify=LEFT,font=self.editFont) + justify=LEFT, font=self.editFont) #frameIndent frameIndentSize=Frame(frameIndent) labelSpaceNumTitle=Label(frameIndentSize, justify=LEFT, text='Python Standard: 4 Spaces!') - self.scaleSpaceNum=Scale(frameIndentSize, variable=self.spaceNum, - orient='horizontal', - tickinterval=2, from_=2, to=16) #widget packing #body - frameFont.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameFont.pack(side=LEFT,padx=5,pady=5,expand=True,fill=BOTH) frameIndent.pack(side=LEFT,padx=5,pady=5,fill=Y) #frameFont frameFontName.pack(side=TOP,padx=5,pady=5,fill=X) frameFontParam.pack(side=TOP,padx=5,pady=5,fill=X) labelFontNameTitle.pack(side=TOP,anchor=W) - self.listFontName.pack(side=LEFT,expand=TRUE,fill=X) + self.listFontName.pack(side=LEFT,expand=True,fill=X) scrollFont.pack(side=LEFT,fill=Y) labelFontSizeTitle.pack(side=LEFT,anchor=W) self.optMenuFontSize.pack(side=LEFT,anchor=W) checkFontBold.pack(side=LEFT,anchor=W,padx=20) - frameFontSample.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=BOTH) - self.labelFontSample.pack(expand=TRUE,fill=BOTH) + frameFontSample.pack(side=TOP,padx=5,pady=5,expand=True,fill=BOTH) + self.labelFontSample.pack(expand=1, fill=Y) #frameIndent frameIndentSize.pack(side=TOP,fill=X) - labelSpaceNumTitle.pack(side=TOP,anchor=W,padx=5) - self.scaleSpaceNum.pack(side=TOP,padx=5,fill=X) + labelSpaceNumTitle.pack(side=TOP, anchor=W, padx=5, pady=6) + + if TTK: + self.scaleSpaceNum = LabeledScale(frameIndentSize, self.spaceNum, + from_=2, to=16, padding=2) + else: + self.scaleSpaceNum=Scale(frameIndentSize, variable=self.spaceNum, + orient='horizontal', from_=2, to=16, tickinterval=2) + + self.scaleSpaceNum.pack(side=TOP, padx=5, fill=X) + return frame def CreatePageHighlight(self): - self.builtinTheme=StringVar(self) - self.customTheme=StringVar(self) - self.fgHilite=BooleanVar(self) - self.colour=StringVar(self) - self.fontName=StringVar(self) - self.themeIsBuiltin=BooleanVar(self) - self.highlightTarget=StringVar(self) + self.builtinTheme = StringVar(self) + self.customTheme = StringVar(self) + self.fgHilite = BooleanVar(self) + self.colour = StringVar(self) + self.fontName = StringVar(self) + self.themeIsBuiltin = BooleanVar(self) + self.highlightTarget = StringVar(self) + #self.themeFontBold = BooleanVar(self) ##widget creation #body frame - frame=self.tabPages.pages['Highlighting'].frame + frame = self.tabPages.pages['Highlighting'].frame #body section frames - frameCustom=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Custom Highlighting ') - frameTheme=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Highlighting Theme ') + frameCustom = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Custom Highlighting ') + frameTheme = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Highlighting Theme ') #frameCustom - self.textHighlightSample=Text(frameCustom,relief=SOLID,borderwidth=1, - font=('courier',12,''),cursor='hand2',width=21,height=10, - takefocus=FALSE,highlightthickness=0,wrap=NONE) - text=self.textHighlightSample - text.bind('',lambda e: 'break') - text.bind('',lambda e: 'break') - textAndTags=(('#you can click here','comment'),('\n','normal'), - ('#to choose items','comment'),('\n','normal'),('def','keyword'), - (' ','normal'),('func','definition'),('(param):','normal'), - ('\n ','normal'),('"""string"""','string'),('\n var0 = ','normal'), - ("'string'",'string'),('\n var1 = ','normal'),("'selected'",'hilite'), - ('\n var2 = ','normal'),("'found'",'hit'), - ('\n var3 = ','normal'),('list', 'builtin'), ('(','normal'), - ('None', 'builtin'),(')\n\n','normal'), - (' error ','error'),(' ','normal'),('cursor |','cursor'), - ('\n ','normal'),('shell','console'),(' ','normal'),('stdout','stdout'), - (' ','normal'),('stderr','stderr'),('\n','normal')) + self.textHighlightSample = Text(frameCustom, relief=SOLID, + borderwidth=1, font=('courier', 12, ''), cursor='hand2', width=21, + height=11, takefocus=False, highlightthickness=0, wrap=NONE) + text = self.textHighlightSample + text.bind('', lambda e: 'break') + text.bind('', lambda e: 'break') + textAndTags = ( + ('#you can click here','comment'), ('\n','normal'), + ('#to choose items', 'comment'), ('\n', 'normal'), + ('def', 'keyword'), (' ', 'normal'), ('func', 'definition'), + ('(param):', 'normal'), ('\n ', 'normal'), + ('"""string"""', 'string'), + ('\n var0 = ','normal'), ("'string'", 'string'), + ('\n var1 = ', 'normal'), ("'selected'", 'hilite'), + ('\n var2 = ', 'normal'), ("'found'", 'hit'), + ('\n var3 = ', 'normal'), ('list', 'builtin'), ('(', 'normal'), + ('None', 'builtin'), (')\n\n', 'normal'), + (' error ', 'error'), (' ', 'normal'), ('cursor |', 'cursor'), + ('\n ', 'normal'), ('shell', 'console'), (' ', 'normal'), + ('stdout', 'stdout'), (' ', 'normal'), ('stderr', 'stderr'), + ('\n', 'normal') + ) for txTa in textAndTags: - text.insert(END,txTa[0],txTa[1]) + text.insert(END, txTa[0], txTa[1]) for element in self.themeElements.keys(): - text.tag_bind(self.themeElements[element][0],'', - lambda event,elem=element: event.widget.winfo_toplevel() - .highlightTarget.set(elem)) + text.tag_bind(self.themeElements[element][0], '', + lambda event, elem=element: + event.widget.winfo_toplevel().highlightTarget.set(elem)) text.config(state=DISABLED) - self.frameColourSet=Frame(frameCustom,relief=SOLID,borderwidth=1) - frameFgBg=Frame(frameCustom) - buttonSetColour=Button(self.frameColourSet,text='Choose Colour for :', - command=self.GetColour,highlightthickness=0) - self.optMenuHighlightTarget=DynOptionMenu(self.frameColourSet, - self.highlightTarget,None,highlightthickness=0)#,command=self.SetHighlightTargetBinding - self.radioFg=Radiobutton(frameFgBg,variable=self.fgHilite, - value=1,text='Foreground',command=self.SetColourSampleBinding) - self.radioBg=Radiobutton(frameFgBg,variable=self.fgHilite, - value=0,text='Background',command=self.SetColourSampleBinding) + + self.frameColourSet = Frame(frameCustom, relief=SOLID, borderwidth=1) + self.style(self.frameColourSet, 'Color.TFrame') + + frameFgBg = Frame(frameCustom) + buttonSetColour = Button(self.frameColourSet, + text='Choose Colour for :', command=self.GetColour) + self.optMenuHighlightTarget = DynOptionMenu(self.frameColourSet, + self.highlightTarget, None) + #self.optBoldText = Checkbutton(self.frameColourSet, text="Bold", + # variable=self.themeFontBold) + self.radioFg = Radiobutton(frameFgBg, variable=self.fgHilite, + value=1, text='Foreground', command=self.SetColourSampleBinding) + self.radioBg = Radiobutton(frameFgBg, variable=self.fgHilite, + value=0, text='Background', command=self.SetColourSampleBinding) self.fgHilite.set(1) - buttonSaveCustomTheme=Button(frameCustom, - text='Save as New Custom Theme',command=self.SaveAsNewTheme) + buttonSaveCustomTheme = Button(frameCustom, + text='Save as New Custom Theme', command=self.SaveAsNewTheme) #frameTheme - labelTypeTitle=Label(frameTheme,text='Select : ') - self.radioThemeBuiltin=Radiobutton(frameTheme,variable=self.themeIsBuiltin, - value=1,command=self.SetThemeType,text='a Built-in Theme') - self.radioThemeCustom=Radiobutton(frameTheme,variable=self.themeIsBuiltin, - value=0,command=self.SetThemeType,text='a Custom Theme') - self.optMenuThemeBuiltin=DynOptionMenu(frameTheme, - self.builtinTheme,None,command=None) - self.optMenuThemeCustom=DynOptionMenu(frameTheme, - self.customTheme,None,command=None) - self.buttonDeleteCustomTheme=Button(frameTheme,text='Delete Custom Theme', - command=self.DeleteCustomTheme) + labelTypeTitle = Label(frameTheme, text='Select : ') + self.radioThemeBuiltin = Radiobutton(frameTheme, + variable=self.themeIsBuiltin, value=1, + command=self.SetThemeType, text='a Built-in Theme') + self.radioThemeCustom = Radiobutton(frameTheme, + variable=self.themeIsBuiltin, value=0, + command=self.SetThemeType, text='a Custom Theme') + self.optMenuThemeBuiltin = DynOptionMenu(frameTheme, + self.builtinTheme, None, command=None) + self.optMenuThemeCustom = DynOptionMenu(frameTheme, + self.customTheme, None, command=None) + self.buttonDeleteCustomTheme = Button(frameTheme, + text='Delete Custom Theme', command=self.DeleteCustomTheme) ##widget packing #body - frameCustom.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) - frameTheme.pack(side=LEFT,padx=5,pady=5,fill=Y) + frameCustom.pack(side=LEFT, padx=5, pady=5, expand=True, fill=BOTH) + frameTheme.pack(side=LEFT, padx=5, pady=5, fill=Y) #frameCustom - self.frameColourSet.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=X) - frameFgBg.pack(side=TOP,padx=5,pady=0) - self.textHighlightSample.pack(side=TOP,padx=5,pady=5,expand=TRUE, + self.frameColourSet.pack(side=TOP, padx=5, pady=5, expand=True, fill=X) + frameFgBg.pack(side=TOP, padx=5, pady=0) + self.textHighlightSample.pack(side=TOP, padx=5, pady=5, expand=True, fill=BOTH) - buttonSetColour.pack(side=TOP,expand=TRUE,fill=X,padx=8,pady=4) - self.optMenuHighlightTarget.pack(side=TOP,expand=TRUE,fill=X,padx=8,pady=3) - self.radioFg.pack(side=LEFT,anchor=E) - self.radioBg.pack(side=RIGHT,anchor=W) - buttonSaveCustomTheme.pack(side=BOTTOM,fill=X,padx=5,pady=5) + buttonSetColour.pack(side=TOP, expand=True, fill=X, padx=8, pady=4) + self.optMenuHighlightTarget.pack(side=LEFT, anchor=N, expand=True, + fill=X, padx=8, pady=3) + #self.optBoldText.pack(side=RIGHT, anchor=N, padx=8, pady=3) + self.radioFg.pack(side=LEFT, anchor=E) + self.radioBg.pack(side=RIGHT, anchor=W) + buttonSaveCustomTheme.pack(side=BOTTOM, fill=X, padx=5, pady=5) #frameTheme - labelTypeTitle.pack(side=TOP,anchor=W,padx=5,pady=5) - self.radioThemeBuiltin.pack(side=TOP,anchor=W,padx=5) - self.radioThemeCustom.pack(side=TOP,anchor=W,padx=5,pady=2) - self.optMenuThemeBuiltin.pack(side=TOP,fill=X,padx=5,pady=5) - self.optMenuThemeCustom.pack(side=TOP,fill=X,anchor=W,padx=5,pady=5) - self.buttonDeleteCustomTheme.pack(side=TOP,fill=X,padx=5,pady=5) + labelTypeTitle.pack(side=TOP, anchor=W, padx=5, pady=5) + self.radioThemeBuiltin.pack(side=TOP, anchor=W, padx=5) + self.radioThemeCustom.pack(side=TOP, anchor=W, padx=5, pady=2) + self.optMenuThemeBuiltin.pack(side=TOP, fill=X, padx=5, pady=5) + self.optMenuThemeCustom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) + self.buttonDeleteCustomTheme.pack(side=TOP, fill=X, padx=5, pady=5) return frame def CreatePageKeys(self): @@ -265,8 +318,8 @@ labelTargetTitle=Label(frameTarget,text='Action - Key(s)') scrollTargetY=Scrollbar(frameTarget) scrollTargetX=Scrollbar(frameTarget,orient=HORIZONTAL) - self.listBindings=Listbox(frameTarget,takefocus=FALSE, - exportselection=FALSE) + self.listBindings=Listbox(frameTarget,takefocus=False, + exportselection=False) self.listBindings.bind('',self.KeyBindingSelected) scrollTargetY.config(command=self.listBindings.yview) scrollTargetX.config(command=self.listBindings.xview) @@ -275,8 +328,11 @@ self.buttonNewKeys=Button(frameCustom,text='Get New Keys for Selection', command=self.GetNewKeys,state=DISABLED) #frameKeySets - frames = [Frame(frameKeySets, padx=2, pady=2, borderwidth=0) - for i in range(2)] + frames = [] + for i in range(2): + f = Frame(frameKeySets, borderwidth=0) + self.style(f, 'S2.TFrame') + frames.append(f) self.radioKeysBuiltin=Radiobutton(frames[0],variable=self.keysAreBuiltin, value=1,command=self.SetKeysType,text='Use a Built-in Key Set') self.radioKeysCustom=Radiobutton(frames[0],variable=self.keysAreBuiltin, @@ -291,11 +347,11 @@ text='Save as New Custom Key Set',command=self.SaveAsNewKeySet) ##widget packing #body - frameCustom.pack(side=BOTTOM,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameCustom.pack(side=BOTTOM,padx=5,pady=5,expand=True,fill=BOTH) frameKeySets.pack(side=BOTTOM,padx=5,pady=5,fill=BOTH) #frameCustom self.buttonNewKeys.pack(side=BOTTOM,fill=X,padx=5,pady=5) - frameTarget.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameTarget.pack(side=LEFT,padx=5,pady=5,expand=True,fill=BOTH) #frame target frameTarget.columnconfigure(0,weight=1) frameTarget.rowconfigure(1,weight=1) @@ -316,14 +372,16 @@ def CreatePageGeneral(self): #tkVars - self.winWidth=StringVar(self) - self.winHeight=StringVar(self) - self.paraWidth=StringVar(self) - self.startupEdit=IntVar(self) - self.autoSave=IntVar(self) - self.encoding=StringVar(self) - self.userHelpBrowser=BooleanVar(self) - self.helpBrowser=StringVar(self) + self.winWidth = StringVar(self) + self.winHeight = StringVar(self) + self.paraWidth = StringVar(self) + self.startupEdit = IntVar(self) + self.autoSave = IntVar(self) + self.encoding = StringVar(self) + self.themename = StringVar(self) + self.fileintab = BooleanVar(self) + self.userHelpBrowser = BooleanVar(self) + self.helpBrowser = StringVar(self) #widget creation #body frame=self.tabPages.pages['General'].frame @@ -335,6 +393,9 @@ frameWinSize=Frame(frame,borderwidth=2,relief=GROOVE) frameParaSize=Frame(frame,borderwidth=2,relief=GROOVE) frameEncoding=Frame(frame,borderwidth=2,relief=GROOVE) + frameModTab = Frame(frame, borderwidth=2, relief=GROOVE) + if TTK: + frameTheme = Frame(frame, borderwidth=2, relief=GROOVE) frameHelp=LabelFrame(frame,borderwidth=2,relief=GROOVE, text=' Additional Help Sources ') #frameRun @@ -371,12 +432,21 @@ value="utf-8",text="UTF-8") radioEncNone=Radiobutton(frameEncoding,variable=self.encoding, value="none",text="None") + # frameModTab + labelMTab = Label(frameModTab, text="Open files and modules in tabs") + checkMTab = Checkbutton(frameModTab, variable=self.fileintab, + text="Yes") + #frameTheme + if TTK: + labelTheme = Label(frameTheme, text="Display Theme") + comboTheme = Combobox(frameTheme, textvariable=self.themename, + values=self.ttkstyle.theme_names(), state='readonly') #frameHelp frameHelpList=Frame(frameHelp) frameHelpListButtons=Frame(frameHelpList) scrollHelpList=Scrollbar(frameHelpList) - self.listHelp=Listbox(frameHelpList,height=5,takefocus=FALSE, - exportselection=FALSE) + self.listHelp=Listbox(frameHelpList, height=4, takefocus=False, + exportselection=False) scrollHelpList.config(command=self.listHelp.yview) self.listHelp.config(yscrollcommand=scrollHelpList.set) self.listHelp.bind('',self.HelpSourceSelected) @@ -393,7 +463,10 @@ frameWinSize.pack(side=TOP,padx=5,pady=5,fill=X) frameParaSize.pack(side=TOP,padx=5,pady=5,fill=X) frameEncoding.pack(side=TOP,padx=5,pady=5,fill=X) - frameHelp.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameModTab.pack(side=TOP, padx=5, pady=5, fill=X) + if TTK: + frameTheme.pack(side=TOP, padx=5, pady=5, fill=X) + frameHelp.pack(side=TOP,padx=5,pady=5,expand=True,fill=BOTH) #frameRun labelRunChoiceTitle.pack(side=LEFT,anchor=W,padx=5,pady=5) radioStartupShell.pack(side=RIGHT,anchor=W,padx=5,pady=5) @@ -416,14 +489,22 @@ radioEncNone.pack(side=RIGHT,anchor=E,pady=5) radioEncUTF8.pack(side=RIGHT,anchor=E,pady=5) radioEncLocale.pack(side=RIGHT,anchor=E,pady=5) + #frameModTab + labelMTab.pack(side=LEFT, anchor=W, padx=5, pady=5) + checkMTab.pack(side=RIGHT, anchor=E, padx=5, pady=5) + #frameTheme + if TTK: + labelTheme.pack(side=LEFT, anchor=W, padx=5, pady=5) + comboTheme.pack(side=RIGHT, anchor=E, padx=5, pady=5) #frameHelp frameHelpListButtons.pack(side=RIGHT,padx=5,pady=5,fill=Y) - frameHelpList.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameHelpList.pack(side=TOP,padx=5,pady=5,expand=True,fill=BOTH) scrollHelpList.pack(side=RIGHT,anchor=W,fill=Y) - self.listHelp.pack(side=LEFT,anchor=E,expand=TRUE,fill=BOTH) + self.listHelp.pack(side=LEFT,anchor=E,expand=True,fill=BOTH) self.buttonHelpListEdit.pack(side=TOP,anchor=W,pady=5) self.buttonHelpListAdd.pack(side=TOP,anchor=W) self.buttonHelpListRemove.pack(side=TOP,anchor=W,pady=5) + return frame def AttachVarCallbacks(self): @@ -432,6 +513,7 @@ self.fontBold.trace_variable('w',self.VarChanged_fontBold) self.spaceNum.trace_variable('w',self.VarChanged_spaceNum) self.colour.trace_variable('w',self.VarChanged_colour) + #self.themeFontBold.trace_variable('w', self.VarChanged_themeFontBold) self.builtinTheme.trace_variable('w',self.VarChanged_builtinTheme) self.customTheme.trace_variable('w',self.VarChanged_customTheme) self.themeIsBuiltin.trace_variable('w',self.VarChanged_themeIsBuiltin) @@ -446,18 +528,20 @@ self.startupEdit.trace_variable('w',self.VarChanged_startupEdit) self.autoSave.trace_variable('w',self.VarChanged_autoSave) self.encoding.trace_variable('w',self.VarChanged_encoding) + self.themename.trace_variable('w', self.VarChanged_themename) + self.fileintab.trace_variable('w', self.VarChanged_fileintab) def VarChanged_fontSize(self,*params): value=self.fontSize.get() - self.AddChangedItem('main','EditorWindow','font-size',value) + self.AddChangedItem('main','EditorPage','font-size',value) def VarChanged_fontName(self,*params): value=self.fontName.get() - self.AddChangedItem('main','EditorWindow','font',value) + self.AddChangedItem('main','EditorPage','font',value) def VarChanged_fontBold(self,*params): value=self.fontBold.get() - self.AddChangedItem('main','EditorWindow','font-bold',value) + self.AddChangedItem('main','EditorPage','font-bold',value) def VarChanged_spaceNum(self,*params): value=self.spaceNum.get() @@ -466,6 +550,9 @@ def VarChanged_colour(self,*params): self.OnNewColourSet() + #def VarChanged_themeFontBold(self, *params): + # self.OnBoldChanged() + def VarChanged_builtinTheme(self,*params): value=self.builtinTheme.get() self.AddChangedItem('main','Theme','name',value) @@ -521,11 +608,11 @@ def VarChanged_winWidth(self,*params): value=self.winWidth.get() - self.AddChangedItem('main','EditorWindow','width',value) + self.AddChangedItem('main', 'EditorWindow', 'width', value) def VarChanged_winHeight(self,*params): value=self.winHeight.get() - self.AddChangedItem('main','EditorWindow','height',value) + self.AddChangedItem('main', 'EditorWindow', 'height', value) def VarChanged_paraWidth(self,*params): value=self.paraWidth.get() @@ -541,8 +628,16 @@ def VarChanged_encoding(self,*params): value=self.encoding.get() - self.AddChangedItem('main','EditorWindow','encoding',value) + self.AddChangedItem('main','EditorPage','encoding',value) + def VarChanged_themename(self, *params): + value = self.themename.get() + self.AddChangedItem('main', 'Theme', 'displaytheme', value) + + def VarChanged_fileintab(self, *params): + value = self.fileintab.get() + self.AddChangedItem('main', 'EditorWindow', 'file-in-tab', value) + def ResetChangedItems(self): #When any config item is changed in this dialog, an entry #should be made in the relevant section (config type) of this @@ -552,10 +647,10 @@ self.changedItems={'main':{},'highlight':{},'keys':{},'extensions':{}} def AddChangedItem(self,type,section,item,value): - value=str(value) #make sure we use a string + value = str(value) #make sure we use a string if not self.changedItems[type].has_key(section): - self.changedItems[type][section]={} - self.changedItems[type][section][item]=value + self.changedItems[type][section] = {} + self.changedItems[type][section][item] = value def GetDefaultItems(self): dItems={'main':{},'highlight':{},'keys':{},'extensions':{}} @@ -653,7 +748,7 @@ newKeys={} for event in prevKeys.keys(): #add key set to changed items eventName=event[2:-2] #trim off the angle brackets - binding=string.join(prevKeys[event]) + binding=' '.join(prevKeys[event]) newKeys[eventName]=binding #handle any unsaved changes to prev key set if prevKeySetName in self.changedItems['keys'].keys(): @@ -680,7 +775,7 @@ bindNames.sort() self.listBindings.delete(0,END) for bindName in bindNames: - key=string.join(keySet[bindName]) #make key(s) into a string + key=' '.join(keySet[bindName]) #make key(s) into a string bindName=bindName[2:-2] #trim off the angle brackets if keySetName in self.changedItems['keys'].keys(): #handle any unsaved changes to this key set @@ -694,9 +789,9 @@ def DeleteCustomKeys(self): keySetName=self.customKeys.get() - if not tkMessageBox.askyesno('Delete Key Set','Are you sure you wish '+ - 'to delete the key set %r ?' % (keySetName), - parent=self): + if not tkMessageBox.askyesno("Delete Key Set", + "Are you sure you wish to delete the key set %r ?" % keySetName, + parent=self): return #remove key set from config idleConf.userCfg['keys'].remove_section(keySetName) @@ -705,25 +800,26 @@ #write changes idleConf.userCfg['keys'].Save() #reload user key set list - itemList=idleConf.GetSectionList('user','keys') + itemList = idleConf.GetSectionList('user', 'keys') itemList.sort() if not itemList: self.radioKeysCustom.config(state=DISABLED) - self.optMenuKeysCustom.SetMenu(itemList,'- no custom keys -') + self.optMenuKeysCustom.SetMenu(itemList, "- no custom keys -") else: self.optMenuKeysCustom.SetMenu(itemList,itemList[0]) #revert to default key set - self.keysAreBuiltin.set(idleConf.defaultCfg['main'].Get('Keys','default')) - self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys','name')) + self.keysAreBuiltin.set(idleConf.defaultCfg['main'].Get('Keys', + 'default')) + self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')) #user can't back out of these changes, they must be applied now self.Apply() self.SetKeysType() def DeleteCustomTheme(self): - themeName=self.customTheme.get() - if not tkMessageBox.askyesno('Delete Theme','Are you sure you wish '+ - 'to delete the theme %r ?' % (themeName,), - parent=self): + themeName = self.customTheme.get() + if not tkMessageBox.askyesno("Delete Theme", + "Are you sure you wish to delete the theme %r ?" % themeName, + parent=self): return #remove theme from config idleConf.userCfg['highlight'].remove_section(themeName) @@ -732,30 +828,31 @@ #write changes idleConf.userCfg['highlight'].Save() #reload user theme list - itemList=idleConf.GetSectionList('user','highlight') + itemList = idleConf.GetSectionList('user', 'highlight') itemList.sort() if not itemList: self.radioThemeCustom.config(state=DISABLED) - self.optMenuThemeCustom.SetMenu(itemList,'- no custom themes -') + self.optMenuThemeCustom.SetMenu(itemList, "- no custom themes -") else: - self.optMenuThemeCustom.SetMenu(itemList,itemList[0]) + self.optMenuThemeCustom.SetMenu(itemList, itemList[0]) #revert to default theme - self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme','default')) - self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme','name')) + self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme', + 'default')) + self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) #user can't back out of these changes, they must be applied now self.Apply() self.SetThemeType() def GetColour(self): target=self.highlightTarget.get() - prevColour=self.frameColourSet.cget('bg') + prevColour = self.ttkstyle.configure('Color.TFrame', 'background') rgbTuplet, colourString = tkColorChooser.askcolor(parent=self, title='Pick new colour for : '+target,initialcolor=prevColour) if colourString and (colourString!=prevColour): #user didn't cancel, and they chose a new colour if self.themeIsBuiltin.get(): #current theme is a built-in - message=('Your changes will be saved as a new Custom Theme. '+ - 'Enter a name for your new Custom Theme below.') + message=("Your changes will be saved as a new Custom Theme. " + "Enter a name for your new Custom Theme below.") newTheme=self.GetNewThemeName(message) if not newTheme: #user cancelled custom theme creation return @@ -767,16 +864,33 @@ def OnNewColourSet(self): newColour=self.colour.get() - self.frameColourSet.config(bg=newColour)#set sample + self.ttkstyle.configure('Color.TFrame', background=newColour) if self.fgHilite.get(): plane='foreground' else: plane='background' sampleElement=self.themeElements[self.highlightTarget.get()][0] - self.textHighlightSample.tag_config(sampleElement, **{plane:newColour}) + self.textHighlightSample.tag_config(sampleElement, **{plane: newColour}) theme=self.customTheme.get() themeElement=sampleElement+'-'+plane self.AddChangedItem('highlight',theme,themeElement,newColour) + #def OnBoldChanged(self): + # bold = self.themeFontBold.get() and tkFont.BOLD or tkFont.NORMAL + # sampleElement = self.themeElements[self.highlightTarget.get()][0] + + # #fontName = self.fontName.get() + # #self.textHighlightSample.tag_config(sampleElement, + # # font=(fontName, self.fontSize.get(), bold)) + + # self.textHighlightSample.tag_config(sampleElement, + # font=('courier', 12, bold)) + + # theme = self.customTheme.get() + # themeElement = "%s-%s" % (sampleElement, 'bold') + # self.AddChangedItem('highlight', theme, themeElement, + # (bold == tkFont.BOLD) and 1 or 0) + def GetNewThemeName(self,message): + # XXX idle bug here usedNames=(idleConf.GetSectionList('user','highlight')+ idleConf.GetSectionList('default','highlight')) newTheme=GetCfgSectionNameDialog(self,'New Custom Theme', @@ -835,37 +949,45 @@ self.radioFg.config(state=NORMAL) self.radioBg.config(state=NORMAL) self.fgHilite.set(1) + #tag = self.themeElements[self.highlightTarget.get()][0] + #font = self.textHighlightSample.tag_cget(tag, 'font') + #if font: + # self.themeFontBold.set(font.split()[2] == tkFont.BOLD) + #else: + # self.themeFontBold.set(0) self.SetColourSample() def SetColourSampleBinding(self,*args): self.SetColourSample() def SetColourSample(self): - #set the colour smaple area - tag=self.themeElements[self.highlightTarget.get()][0] - if self.fgHilite.get(): plane='foreground' - else: plane='background' - colour=self.textHighlightSample.tag_cget(tag,plane) - self.frameColourSet.config(bg=colour) + # set the colour sample area + tag = self.themeElements[self.highlightTarget.get()][0] + if self.fgHilite.get(): + plane = 'foreground' + else: + plane = 'background' + colour = self.textHighlightSample.tag_cget(tag, plane) + self.ttkstyle.configure('Color.TFrame', background=colour) def PaintThemeSample(self): if self.themeIsBuiltin.get(): #a default theme - theme=self.builtinTheme.get() + theme = self.builtinTheme.get() else: #a user theme - theme=self.customTheme.get() + theme = self.customTheme.get() for elementTitle in self.themeElements.keys(): - element=self.themeElements[elementTitle][0] - colours=idleConf.GetHighlight(theme,element) - if element=='cursor': #cursor sample needs special painting - colours['background']=idleConf.GetHighlight(theme, - 'normal', fgBg='bg') + element = self.themeElements[elementTitle][0] + colours = idleConf.GetHighlight(theme, element) + if element == 'cursor': #cursor sample needs special painting + colours['background'] = idleConf.GetHighlight(theme, + 'normal', fgBg='bg') #handle any unsaved changes to this theme if theme in self.changedItems['highlight'].keys(): - themeDict=self.changedItems['highlight'][theme] - if themeDict.has_key(element+'-foreground'): - colours['foreground']=themeDict[element+'-foreground'] - if themeDict.has_key(element+'-background'): - colours['background']=themeDict[element+'-background'] + themeDict = self.changedItems['highlight'][theme] + if themeDict.has_key(element + '-foreground'): + colours['foreground'] = themeDict[element + '-foreground'] + if themeDict.has_key(element + '-background'): + colours['background'] = themeDict[element + '-background'] self.textHighlightSample.tag_config(element, **colours) self.SetColourSample() @@ -917,7 +1039,7 @@ self.changedItems['main']['HelpFiles'] = {} for num in range(1,len(self.userHelpList)+1): self.AddChangedItem('main','HelpFiles',str(num), - string.join(self.userHelpList[num-1][:2],';')) + ';'.join(self.userHelpList[num - 1][:2])) def LoadFontCfg(self): ##base editor font selection list @@ -925,7 +1047,7 @@ fonts.sort() for font in fonts: self.listFontName.insert(END,font) - configuredFont=idleConf.GetOption('main','EditorWindow','font', + configuredFont=idleConf.GetOption('main','EditorPage','font', default='courier') lc_configuredFont = configuredFont.lower() self.fontName.set(lc_configuredFont) @@ -936,13 +1058,13 @@ self.listFontName.select_set(currentFontIndex) self.listFontName.select_anchor(currentFontIndex) ##font size dropdown - fontSize=idleConf.GetOption('main','EditorWindow','font-size', + fontSize=idleConf.GetOption('main', 'EditorPage', 'font-size', default='10') self.optMenuFontSize.SetMenu(('7','8','9','10','11','12','13','14', '16','18','20','22'),fontSize ) ##fontWeight - self.fontBold.set(idleConf.GetOption('main','EditorWindow', - 'font-bold',default=0,type='bool')) + self.fontBold.set(idleConf.GetOption('main', 'EditorPage', + 'font-bold', default=0, type='bool')) ##font sample self.SetFontSample() @@ -954,37 +1076,41 @@ def LoadThemeCfg(self): ##current theme type radiobutton - self.themeIsBuiltin.set(idleConf.GetOption('main','Theme','default', - type='bool',default=1)) + self.themeIsBuiltin.set(idleConf.GetOption('main', 'Theme', 'default', + type='bool', default=1)) ##currently set theme - currentOption=idleConf.CurrentTheme() + currentOption = idleConf.CurrentTheme() ##load available theme option menus if self.themeIsBuiltin.get(): #default theme selected - itemList=idleConf.GetSectionList('default','highlight') + itemList = idleConf.GetSectionList('default', 'highlight') itemList.sort() - self.optMenuThemeBuiltin.SetMenu(itemList,currentOption) - itemList=idleConf.GetSectionList('user','highlight') + self.optMenuThemeBuiltin.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('user', 'highlight') itemList.sort() if not itemList: self.radioThemeCustom.config(state=DISABLED) - self.customTheme.set('- no custom themes -') + self.customTheme.set("- no custom themes -") else: self.optMenuThemeCustom.SetMenu(itemList,itemList[0]) else: #user theme selected - itemList=idleConf.GetSectionList('user','highlight') + itemList = idleConf.GetSectionList('user', 'highlight') itemList.sort() - self.optMenuThemeCustom.SetMenu(itemList,currentOption) - itemList=idleConf.GetSectionList('default','highlight') + self.optMenuThemeCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'highlight') itemList.sort() - self.optMenuThemeBuiltin.SetMenu(itemList,itemList[0]) + self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0]) self.SetThemeType() ##load theme element option menu - themeNames=self.themeElements.keys() + themeNames = self.themeElements.keys() themeNames.sort(self.__ThemeNameIndexCompare) - self.optMenuHighlightTarget.SetMenu(themeNames,themeNames[0]) + self.optMenuHighlightTarget.SetMenu(themeNames, themeNames[0]) self.PaintThemeSample() self.SetHighlightTarget() + if TTK: + displaytheme = idleConf.GetOption('main', 'Theme', 'displaytheme') + self.themename.set(displaytheme) + def __ThemeNameIndexCompare(self,a,b): if self.themeElements[a][1]>'), ('Run Module', '<>'), ]), ] - def __init__(self, editwin): - self.editwin = editwin + def __init__(self, editpage): + self.editpage = editpage + self.editwin = editpage.editwin # Provide instance variables referenced by Debugger # XXX This should be done differently self.flist = self.editwin.flist @@ -70,7 +71,7 @@ msgtxt, (lineno, start) = msg self.editwin.gotoline(lineno) self.errorbox("Tabnanny Tokenizing Error", - "Token Error: %s" % msgtxt) + "Token Error: %s" % msgtxt) return False except tabnanny.NannyNag, nag: # The error messages from tabnanny are too confusing... @@ -91,7 +92,7 @@ source = re.sub(r"\r", "\n", source) if source and source[-1] != '\n': source = source + '\n' - text = self.editwin.text + text = self.editpage.text text.tag_remove("ERROR", "1.0", "end") try: try: @@ -113,7 +114,7 @@ shell.set_warning_stream(saved_stream) def colorize_syntax_error(self, msg, lineno, offset): - text = self.editwin.text + text = self.editpage.text pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1) text.tag_add("ERROR", pos) char = text.get(pos) @@ -175,20 +176,20 @@ If the user has configured IDLE for Autosave, the file will be silently saved if it already exists and is dirty. - """ - filename = self.editwin.io.filename - if not self.editwin.get_saved(): + page = self.editpage + filename = page.io.filename + if not page.get_saved(): autosave = idleConf.GetOption('main', 'General', 'autosave', type='bool') if autosave and filename: - self.editwin.io.save(None) + page.io.save(None) else: reply = self.ask_save_dialog() - self.editwin.text.focus_set() + page.text.focus_set() if reply == "ok": - self.editwin.io.save(None) - filename = self.editwin.io.filename + page.io.save(None) + filename = page.io.filename else: filename = None return filename @@ -196,14 +197,12 @@ def ask_save_dialog(self): msg = "Source Must Be Saved\n" + 5*' ' + "OK to Save?" mb = tkMessageBox.Message(title="Save Before Run or Check", - message=msg, - icon=tkMessageBox.QUESTION, - type=tkMessageBox.OKCANCEL, - default=tkMessageBox.OK, - master=self.editwin.text) + message=msg, icon=tkMessageBox.QUESTION, + type=tkMessageBox.OKCANCEL, default=tkMessageBox.OK, + master=self.editpage.text) return mb.show() def errorbox(self, title, message): # XXX This should really be a function of EditorWindow... - tkMessageBox.showerror(title, message, master=self.editwin.text) - self.editwin.text.focus_set() + tkMessageBox.showerror(title, message, master=self.editpage.text) + self.editpage.text.focus_set() Index: tabbedpages.py =================================================================== --- tabbedpages.py (revision 63995) +++ tabbedpages.py (revision 65573) @@ -1,490 +1,12 @@ -"""An implementation of tabbed pages using only standard Tkinter. - -Originally developed for use in IDLE. Based on tabpage.py. - -Classes exported: -TabbedPageSet -- A Tkinter implementation of a tabbed-page widget. -TabSet -- A widget containing tabs (buttons) in one or more rows. - -""" -from Tkinter import * - +# Exceptions used on both versions of tabbedpages class InvalidNameError(Exception): pass class AlreadyExistsError(Exception): pass +def get_tabbedpage(): + """Returns the TabbedPageSet available for use.""" + try: + from tabbedpages_new import TabbedPageSet + except ImportError: + from tabbedpages_old import TabbedPageSet -class TabSet(Frame): - """A widget containing tabs (buttons) in one or more rows. - - Only one tab may be selected at a time. - - """ - def __init__(self, page_set, select_command, - tabs=None, n_rows=1, max_tabs_per_row=5, - expand_tabs=False, **kw): - """Constructor arguments: - - select_command -- A callable which will be called when a tab is - selected. It is called with the name of the selected tab as an - argument. - - tabs -- A list of strings, the names of the tabs. Should be specified in - the desired tab order. The first tab will be the default and first - active tab. If tabs is None or empty, the TabSet will be initialized - empty. - - n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is - None, then the number of rows will be decided by TabSet. See - _arrange_tabs() for details. - - max_tabs_per_row -- Used for deciding how many rows of tabs are needed, - when the number of rows is not constant. See _arrange_tabs() for - details. - - """ - Frame.__init__(self, page_set, **kw) - self.select_command = select_command - self.n_rows = n_rows - self.max_tabs_per_row = max_tabs_per_row - self.expand_tabs = expand_tabs - self.page_set = page_set - - self._tabs = {} - self._tab2row = {} - if tabs: - self._tab_names = list(tabs) - else: - self._tab_names = [] - self._selected_tab = None - self._tab_rows = [] - - self.padding_frame = Frame(self, height=2, - borderwidth=0, relief=FLAT, - background=self.cget('background')) - self.padding_frame.pack(side=TOP, fill=X, expand=False) - - self._arrange_tabs() - - def add_tab(self, tab_name): - """Add a new tab with the name given in tab_name.""" - if not tab_name: - raise InvalidNameError("Invalid Tab name: '%s'" % tab_name) - if tab_name in self._tab_names: - raise AlreadyExistsError("Tab named '%s' already exists" %tab_name) - - self._tab_names.append(tab_name) - self._arrange_tabs() - - def remove_tab(self, tab_name): - """Remove the tab named """ - if not tab_name in self._tab_names: - raise KeyError("No such Tab: '%s" % page_name) - - self._tab_names.remove(tab_name) - self._arrange_tabs() - - def set_selected_tab(self, tab_name): - """Show the tab named as the selected one""" - if tab_name == self._selected_tab: - return - if tab_name is not None and tab_name not in self._tabs: - raise KeyError("No such Tab: '%s" % page_name) - - # deselect the current selected tab - if self._selected_tab is not None: - self._tabs[self._selected_tab].set_normal() - self._selected_tab = None - - if tab_name is not None: - # activate the tab named tab_name - self._selected_tab = tab_name - tab = self._tabs[tab_name] - tab.set_selected() - # move the tab row with the selected tab to the bottom - tab_row = self._tab2row[tab] - tab_row.pack_forget() - tab_row.pack(side=TOP, fill=X, expand=0) - - def _add_tab_row(self, tab_names, expand_tabs): - if not tab_names: - return - - tab_row = Frame(self) - tab_row.pack(side=TOP, fill=X, expand=0) - self._tab_rows.append(tab_row) - - for tab_name in tab_names: - tab = TabSet.TabButton(tab_name, self.select_command, - tab_row, self) - if expand_tabs: - tab.pack(side=LEFT, fill=X, expand=True) - else: - tab.pack(side=LEFT) - self._tabs[tab_name] = tab - self._tab2row[tab] = tab_row - - # tab is the last one created in the above loop - tab.is_last_in_row = True - - def _reset_tab_rows(self): - while self._tab_rows: - tab_row = self._tab_rows.pop() - tab_row.destroy() - self._tab2row = {} - - def _arrange_tabs(self): - """ - Arrange the tabs in rows, in the order in which they were added. - - If n_rows >= 1, this will be the number of rows used. Otherwise the - number of rows will be calculated according to the number of tabs and - max_tabs_per_row. In this case, the number of rows may change when - adding/removing tabs. - - """ - # remove all tabs and rows - for tab_name in self._tabs.keys(): - self._tabs.pop(tab_name).destroy() - self._reset_tab_rows() - - if not self._tab_names: - return - - if self.n_rows is not None and self.n_rows > 0: - n_rows = self.n_rows - else: - # calculate the required number of rows - n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1 - - # not expanding the tabs with more than one row is very ugly - expand_tabs = self.expand_tabs or n_rows > 1 - i = 0 # index in self._tab_names - for row_index in xrange(n_rows): - # calculate required number of tabs in this row - n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1 - tab_names = self._tab_names[i:i + n_tabs] - i += n_tabs - self._add_tab_row(tab_names, expand_tabs) - - # re-select selected tab so it is properly displayed - selected = self._selected_tab - self.set_selected_tab(None) - if selected in self._tab_names: - self.set_selected_tab(selected) - - class TabButton(Frame): - """A simple tab-like widget.""" - - bw = 2 # borderwidth - - def __init__(self, name, select_command, tab_row, tab_set): - """Constructor arguments: - - name -- The tab's name, which will appear in its button. - - select_command -- The command to be called upon selection of the - tab. It is called with the tab's name as an argument. - - """ - Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED) - - self.name = name - self.select_command = select_command - self.tab_set = tab_set - self.is_last_in_row = False - - self.button = Radiobutton( - self, text=name, command=self._select_event, - padx=5, pady=1, takefocus=FALSE, indicatoron=FALSE, - highlightthickness=0, selectcolor='', borderwidth=0) - self.button.pack(side=LEFT, fill=X, expand=True) - - self._init_masks() - self.set_normal() - - def _select_event(self, *args): - """Event handler for tab selection. - - With TabbedPageSet, this calls TabbedPageSet.change_page, so that - selecting a tab changes the page. - - Note that this does -not- call set_selected -- it will be called by - TabSet.set_selected_tab, which should be called when whatever the - tabs are related to changes. - - """ - self.select_command(self.name) - return - - def set_selected(self): - """Assume selected look""" - self._place_masks(selected=True) - - def set_normal(self): - """Assume normal look""" - self._place_masks(selected=False) - - def _init_masks(self): - page_set = self.tab_set.page_set - background = page_set.pages_frame.cget('background') - # mask replaces the middle of the border with the background color - self.mask = Frame(page_set, borderwidth=0, relief=FLAT, - background=background) - # mskl replaces the bottom-left corner of the border with a normal - # left border - self.mskl = Frame(page_set, borderwidth=0, relief=FLAT, - background=background) - self.mskl.ml = Frame(self.mskl, borderwidth=self.bw, - relief=RAISED) - self.mskl.ml.place(x=0, y=-self.bw, - width=2*self.bw, height=self.bw*4) - # mskr replaces the bottom-right corner of the border with a normal - # right border - self.mskr = Frame(page_set, borderwidth=0, relief=FLAT, - background=background) - self.mskr.mr = Frame(self.mskr, borderwidth=self.bw, - relief=RAISED) - - def _place_masks(self, selected=False): - height = self.bw - if selected: - height += self.bw - - self.mask.place(in_=self, - relx=0.0, x=0, - rely=1.0, y=0, - relwidth=1.0, width=0, - relheight=0.0, height=height) - - self.mskl.place(in_=self, - relx=0.0, x=-self.bw, - rely=1.0, y=0, - relwidth=0.0, width=self.bw, - relheight=0.0, height=height) - - page_set = self.tab_set.page_set - if selected and ((not self.is_last_in_row) or - (self.winfo_rootx() + self.winfo_width() < - page_set.winfo_rootx() + page_set.winfo_width()) - ): - # for a selected tab, if its rightmost edge isn't on the - # rightmost edge of the page set, the right mask should be one - # borderwidth shorter (vertically) - height -= self.bw - - self.mskr.place(in_=self, - relx=1.0, x=0, - rely=1.0, y=0, - relwidth=0.0, width=self.bw, - relheight=0.0, height=height) - - self.mskr.mr.place(x=-self.bw, y=-self.bw, - width=2*self.bw, height=height + self.bw*2) - - # finally, lower the tab set so that all of the frames we just - # placed hide it - self.tab_set.lower() - -class TabbedPageSet(Frame): - """A Tkinter tabbed-pane widget. - - Constains set of 'pages' (or 'panes') with tabs above for selecting which - page is displayed. Only one page will be displayed at a time. - - Pages may be accessed through the 'pages' attribute, which is a dictionary - of pages, using the name given as the key. A page is an instance of a - subclass of Tk's Frame widget. - - The page widgets will be created (and destroyed when required) by the - TabbedPageSet. Do not call the page's pack/place/grid/destroy methods. - - Pages may be added or removed at any time using the add_page() and - remove_page() methods. - - """ - class Page(object): - """Abstract base class for TabbedPageSet's pages. - - Subclasses must override the _show() and _hide() methods. - - """ - uses_grid = False - - def __init__(self, page_set): - self.frame = Frame(page_set, borderwidth=2, relief=RAISED) - - def _show(self): - raise NotImplementedError - - def _hide(self): - raise NotImplementedError - - class PageRemove(Page): - """Page class using the grid placement manager's "remove" mechanism.""" - uses_grid = True - - def _show(self): - self.frame.grid(row=0, column=0, sticky=NSEW) - - def _hide(self): - self.frame.grid_remove() - - class PageLift(Page): - """Page class using the grid placement manager's "lift" mechanism.""" - uses_grid = True - - def __init__(self, page_set): - super(TabbedPageSet.PageLift, self).__init__(page_set) - self.frame.grid(row=0, column=0, sticky=NSEW) - self.frame.lower() - - def _show(self): - self.frame.lift() - - def _hide(self): - self.frame.lower() - - class PagePackForget(Page): - """Page class using the pack placement manager's "forget" mechanism.""" - def _show(self): - self.frame.pack(fill=BOTH, expand=True) - - def _hide(self): - self.frame.pack_forget() - - def __init__(self, parent, page_names=None, page_class=PageLift, - n_rows=1, max_tabs_per_row=5, expand_tabs=False, - **kw): - """Constructor arguments: - - page_names -- A list of strings, each will be the dictionary key to a - page's widget, and the name displayed on the page's tab. Should be - specified in the desired page order. The first page will be the default - and first active page. If page_names is None or empty, the - TabbedPageSet will be initialized empty. - - n_rows, max_tabs_per_row -- Parameters for the TabSet which will - manage the tabs. See TabSet's docs for details. - - page_class -- Pages can be shown/hidden using three mechanisms: - - * PageLift - All pages will be rendered one on top of the other. When - a page is selected, it will be brought to the top, thus hiding all - other pages. Using this method, the TabbedPageSet will not be resized - when pages are switched. (It may still be resized when pages are - added/removed.) - - * PageRemove - When a page is selected, the currently showing page is - hidden, and the new page shown in its place. Using this method, the - TabbedPageSet may resize when pages are changed. - - * PagePackForget - This mechanism uses the pack placement manager. - When a page is shown it is packed, and when it is hidden it is - unpacked (i.e. pack_forget). This mechanism may also cause the - TabbedPageSet to resize when the page is changed. - - """ - Frame.__init__(self, parent, **kw) - - self.page_class = page_class - self.pages = {} - self._pages_order = [] - self._current_page = None - self._default_page = None - - self.columnconfigure(0, weight=1) - self.rowconfigure(1, weight=1) - - self.pages_frame = Frame(self) - self.pages_frame.grid(row=1, column=0, sticky=NSEW) - if self.page_class.uses_grid: - self.pages_frame.columnconfigure(0, weight=1) - self.pages_frame.rowconfigure(0, weight=1) - - # the order of the following commands is important - self._tab_set = TabSet(self, self.change_page, n_rows=n_rows, - max_tabs_per_row=max_tabs_per_row, - expand_tabs=expand_tabs) - if page_names: - for name in page_names: - self.add_page(name) - self._tab_set.grid(row=0, column=0, sticky=NSEW) - - self.change_page(self._default_page) - - def add_page(self, page_name): - """Add a new page with the name given in page_name.""" - if not page_name: - raise InvalidNameError("Invalid TabPage name: '%s'" % page_name) - if page_name in self.pages: - raise AlreadyExistsError( - "TabPage named '%s' already exists" % page_name) - - self.pages[page_name] = self.page_class(self.pages_frame) - self._pages_order.append(page_name) - self._tab_set.add_tab(page_name) - - if len(self.pages) == 1: # adding first page - self._default_page = page_name - self.change_page(page_name) - - def remove_page(self, page_name): - """Destroy the page whose name is given in page_name.""" - if not page_name in self.pages: - raise KeyError("No such TabPage: '%s" % page_name) - - self._pages_order.remove(page_name) - - # handle removing last remaining, default, or currently shown page - if len(self._pages_order) > 0: - if page_name == self._default_page: - # set a new default page - self._default_page = self._pages_order[0] - else: - self._default_page = None - - if page_name == self._current_page: - self.change_page(self._default_page) - - self._tab_set.remove_tab(page_name) - page = self.pages.pop(page_name) - page.frame.destroy() - - def change_page(self, page_name): - """Show the page whose name is given in page_name.""" - if self._current_page == page_name: - return - if page_name is not None and page_name not in self.pages: - raise KeyError("No such TabPage: '%s'" % page_name) - - if self._current_page is not None: - self.pages[self._current_page]._hide() - self._current_page = None - - if page_name is not None: - self._current_page = page_name - self.pages[page_name]._show() - - self._tab_set.set_selected_tab(page_name) - -if __name__ == '__main__': - # test dialog - root=Tk() - tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0, - expand_tabs=False, - ) - tabPage.pack(side=TOP, expand=TRUE, fill=BOTH) - Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack() - Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack() - Label(tabPage.pages['Baz'].frame, text='Baz').pack() - entryPgName=Entry(root) - buttonAdd=Button(root, text='Add Page', - command=lambda:tabPage.add_page(entryPgName.get())) - buttonRemove=Button(root, text='Remove Page', - command=lambda:tabPage.remove_page(entryPgName.get())) - labelPgName=Label(root, text='name of page to add/remove:') - buttonAdd.pack(padx=5, pady=5) - buttonRemove.pack(padx=5, pady=5) - labelPgName.pack(padx=5) - entryPgName.pack(padx=5) - root.mainloop() + return TabbedPageSet Index: keybindingDialog.py =================================================================== --- keybindingDialog.py (revision 63995) +++ keybindingDialog.py (revision 65573) @@ -1,10 +1,18 @@ """ Dialog for building Tkinter accelerator key bindings """ -from Tkinter import * +from Tkinter import Toplevel, Frame, Entry, Button, Checkbutton, Label, \ + Listbox, Scrollbar, StringVar +from Tkconstants import TOP, BOTH, BOTTOM, X, NSEW, SUNKEN, LEFT, GROOVE, W, \ + END, EW, NS, SINGLE, VERTICAL, ANCHOR, MOVETO import tkMessageBox import string +from idlelib.configHandler import idleConf +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Frame, Entry, Button, Checkbutton, Label, Scrollbar + class GetKeysDialog(Toplevel): def __init__(self,parent,title,action,currentKeySequences): """ @@ -15,7 +23,7 @@ """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) - self.resizable(height=FALSE,width=FALSE) + self.resizable(height=False, width=False) self.title(title) self.transient(parent) self.grab_set() @@ -46,10 +54,10 @@ self.wait_window() def CreateWidgets(self): - frameMain = Frame(self,borderwidth=2,relief=SUNKEN) - frameMain.pack(side=TOP,expand=TRUE,fill=BOTH) + frameMain = Frame(self, borderwidth=2, relief=SUNKEN) + frameMain.pack(side=TOP, expand=True, fill=BOTH) frameButtons=Frame(self) - frameButtons.pack(side=BOTTOM,fill=X) + frameButtons.pack(side=BOTTOM, fill=X) self.buttonOK = Button(frameButtons,text='OK', width=8,command=self.OK) self.buttonOK.grid(row=0,column=0,padx=5,pady=5) @@ -124,6 +132,9 @@ "separated by a space, eg., ." ) labelHelpAdvanced.grid(row=0,column=0,sticky=NSEW) + if TTK: + frameButtons['style'] = 'RootColor.TFrame' + def SetModifiersForPlatform(self): """Determine list of names of key modifiers for this platform. @@ -163,7 +174,7 @@ if finalKey: finalKey = self.TranslateKey(finalKey, modifiers) keyList.append(finalKey) - self.keyString.set('<' + string.join(keyList,'-') + '>') + self.keyString.set('<%s>' % '-'.join(keyList)) def GetModifiers(self): modList = [variable.get() for variable in self.modifier_vars] @@ -258,6 +269,7 @@ return keysOK if __name__ == '__main__': + from Tkinter import Tk #test the dialog root=Tk() def run(): Index: configHelpSourceEdit.py =================================================================== --- configHelpSourceEdit.py (revision 63995) +++ configHelpSourceEdit.py (revision 65573) @@ -1,12 +1,17 @@ "Dialog to specify or edit the parameters for a user configured help source." - import os import sys - -from Tkinter import * +from Tkinter import Toplevel, Frame, Entry, Button, Label, StringVar +from Tkconstants import GROOVE, LEFT, RIGHT, W, ACTIVE, X, BOTH, TOP, BOTTOM import tkMessageBox import tkFileDialog +from configHandler import idleConf + +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Frame, Entry, Button, Label + class GetHelpSourceDialog(Toplevel): def __init__(self, parent, title, menuItem='', filePath=''): """Get menu entry and url/ local file location for Additional Help @@ -18,13 +23,14 @@ """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) - self.resizable(height=FALSE, width=FALSE) + self.resizable(height=False, width=False) self.title(title) self.transient(parent) self.grab_set() self.protocol("WM_DELETE_WINDOW", self.Cancel) self.parent = parent self.result = None + self.CreateWidgets() self.menu.set(menuItem) self.path.set(filePath) @@ -46,33 +52,36 @@ self.path = StringVar(self) self.fontSize = StringVar(self) self.frameMain = Frame(self, borderwidth=2, relief=GROOVE) - self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) labelMenu = Label(self.frameMain, anchor=W, justify=LEFT, - text='Menu Item:') - self.entryMenu = Entry(self.frameMain, textvariable=self.menu, - width=30) - self.entryMenu.focus_set() + text='Menu Item:') + self.entryMenu = Entry(self.frameMain, textvariable=self.menu) labelPath = Label(self.frameMain, anchor=W, justify=LEFT, - text='Help File Path: Enter URL or browse for file') + text='Help File Path: Enter URL or browse for file') self.entryPath = Entry(self.frameMain, textvariable=self.path, - width=40) + width=30) + browseButton = Button(self.frameMain, text='Browse', width=8, + command=self.browseFile) + frameButtons = Frame(self) + self.buttonOk = Button(frameButtons, text='OK', width=8, + default=ACTIVE, command=self.Ok) + self.buttonCancel = Button(frameButtons, text='Cancel', width=8, + command=self.Cancel) + self.entryMenu.focus_set() + + self.frameMain.pack(side=TOP, expand=True, fill=BOTH) labelMenu.pack(anchor=W, padx=5, pady=3) - self.entryMenu.pack(anchor=W, padx=5, pady=3) + self.entryMenu.pack(anchor=W, padx=5, pady=3, fill=X) labelPath.pack(anchor=W, padx=5, pady=3) - self.entryPath.pack(anchor=W, padx=5, pady=3) - browseButton = Button(self.frameMain, text='Browse', width=8, - command=self.browseFile) - browseButton.pack(pady=3) - frameButtons = Frame(self) + self.entryPath.pack(anchor=W, padx=5, pady=3, side=LEFT, fill=X) + browseButton.pack(pady=3, padx=5, side=RIGHT) frameButtons.pack(side=BOTTOM, fill=X) - self.buttonOk = Button(frameButtons, text='OK', - width=8, default=ACTIVE, command=self.Ok) - self.buttonOk.grid(row=0, column=0, padx=5,pady=5) - self.buttonCancel = Button(frameButtons, text='Cancel', - width=8, command=self.Cancel) - self.buttonCancel.grid(row=0, column=1, padx=5, pady=5) + self.buttonOk.pack(pady=5, side=RIGHT) + self.buttonCancel.pack(padx=5, pady=5, side=RIGHT) + if TTK: + frameButtons['style'] = 'RootColor.TFrame' + def browseFile(self): filetypes = [ ("HTML Files", "*.htm *.html", "TEXT"), @@ -159,6 +168,7 @@ self.destroy() if __name__ == '__main__': + from Tkinter import Tk #test the dialog root = Tk() def run(): Index: WidgetRedirector.py =================================================================== --- WidgetRedirector.py (revision 63995) +++ WidgetRedirector.py (revision 65573) @@ -1,4 +1,4 @@ -from Tkinter import * +from Tkinter import TclError class WidgetRedirector: @@ -105,6 +105,7 @@ def main(): + from Tkinter import Tk, Text root = Tk() root.wm_protocol("WM_DELETE_WINDOW", root.quit) text = Text() Index: GrepDialog.py =================================================================== --- GrepDialog.py (revision 63995) +++ GrepDialog.py (revision 65573) @@ -1,10 +1,15 @@ import os -import fnmatch import sys -from Tkinter import * +import fnmatch +from Tkinter import StringVar, BooleanVar, Checkbutton + import SearchEngine from SearchDialogBase import SearchDialogBase +from configHandler import idleConf +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Checkbutton + def grep(text, io=None, flist=None): root = text._root() engine = SearchEngine.get(root) @@ -15,10 +20,10 @@ dialog.open(text, searchphrase, io) class GrepDialog(SearchDialogBase): - title = "Find in Files Dialog" icon = "Grep" needwrapbutton = 0 + bottom_btns = [("Search Files", 'default_command', 1)] def __init__(self, root, engine, flist): SearchDialogBase.__init__(self, root, engine) @@ -40,20 +45,18 @@ def create_entries(self): SearchDialogBase.create_entries(self) - self.globent = self.make_entry("In files:", self.globvar) + self.globent = self.make_entry("In files", self.globvar) def create_other_buttons(self): f = self.make_frame() - btn = Checkbutton(f, anchor="w", - variable=self.recvar, + btn = Checkbutton(f, variable=self.recvar, text="Recurse down subdirectories") btn.pack(side="top", fill="both") - btn.select() + btn.invoke() def create_command_buttons(self): SearchDialogBase.create_command_buttons(self) - self.make_button("Search Files", self.default_command, 1) def default_command(self, event=None): prog = self.engine.getprog() @@ -126,8 +129,3 @@ for subdir in subdirs: list.extend(self.findfiles(subdir, base, rec)) return list - - def close(self, event=None): - if self.top: - self.top.grab_release() - self.top.withdraw() Index: FormatParagraph.py =================================================================== --- FormatParagraph.py (revision 63995) +++ FormatParagraph.py (revision 65573) @@ -25,28 +25,30 @@ ]) ] - def __init__(self, editwin): - self.editwin = editwin + def __init__(self, editpage): + self.editpage = editpage def close(self): - self.editwin = None + self.editpage = None def format_paragraph_event(self, event): - maxformatwidth = int(idleConf.GetOption('main','FormatParagraph','paragraph')) - text = self.editwin.text - first, last = self.editwin.get_selection_indices() + maxformatwidth = int(idleConf.GetOption('main', 'FormatParagraph', + 'paragraph')) + text = self.editpage.text + first, last = self.editpage.get_selection_indices() if first and last: data = text.get(first, last) comment_header = '' else: - first, last, comment_header, data = \ - find_paragraph(text, text.index("insert")) + first, last, comment_header, data = find_paragraph(text, + text.index("insert")) if comment_header: # Reformat the comment lines - convert to text sans header. lines = data.split("\n") lines = map(lambda st, l=len(comment_header): st[l:], lines) data = "\n".join(lines) - # Reformat to maxformatwidth chars or a 20 char width, whichever is greater. + # Reformat to maxformatwidth chars or a 20 char width, whichever is + # greater. format_width = max(maxformatwidth - len(comment_header), 20) newdata = reformat_paragraph(data, format_width) # re-split and re-insert the comment header. Index: EditorWindow.py =================================================================== --- EditorWindow.py (revision 63995) +++ EditorWindow.py (revision 65573) @@ -1,56 +1,51 @@ -import sys import os import re -import imp -from itertools import count -from Tkinter import * -import tkSimpleDialog +import sys +import traceback +import webbrowser import tkMessageBox -from MultiCall import MultiCallCreator +import tkSimpleDialog +from Tkinter import Menu, Scrollbar, TclError, BooleanVar +from Tkconstants import INSERT, END, RIGHT, BOTTOM, TOP, X, Y, BOTH -import webbrowser -import idlever +import macosxSupport +import Bindings import WindowList -import SearchDialog -import GrepDialog -import ReplaceDialog -import PyParse +from editorpage import EditorPage, classifyws, filename_to_unicode +from tabbedpages import get_tabbedpage from configHandler import idleConf -import aboutDialog, textView, configDialog -import macosxSupport +from MultiStatusBar import MultiStatusBar +TabbedPageSet = get_tabbedpage() + +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Style, Scrollbar + # The default tab setting for a Text widget, in average-width characters. TK_TABWIDTH_DEFAULT = 8 -def _find_module(fullname, path=None): - """Version of imp.find_module() that handles hierarchical module names""" - - file = None - for tgt in fullname.split('.'): - if file is not None: - file.close() # close intermediate files - (file, filename, descr) = imp.find_module(tgt, path) - if descr[2] == imp.PY_SOURCE: - break # find but not load the source file - module = imp.load_module(tgt, file, filename, descr) - try: - path = module.__path__ - except AttributeError: - raise ImportError, 'No source for module ' + module.__name__ - return file, filename, descr - class EditorWindow(object): - from Percolator import Percolator - from ColorDelegator import ColorDelegator - from UndoDelegator import UndoDelegator - from IOBinding import IOBinding, filesystemencoding, encoding - import Bindings - from Tkinter import Toplevel - from MultiStatusBar import MultiStatusBar + from ColorDelegator import ColorDelegator # overridden by PyShell + from UndoDelegator import UndoDelegator # overridden by PyShell help_url = None + menu_specs = [ + ("file", "_File"), + ("edit", "_Edit"), + ("format", "F_ormat"), + ("run", "_Run"), + ("options", "_Options"), + ("windows", "_Windows"), + ("help", "_Help"), + ] - def __init__(self, flist=None, filename=None, key=None, root=None): + if macosxSupport.runningAsOSXApp(): + del menu_specs[-3] + menu_specs[-2] = ("windows", "_Window") + + def __init__(self, flist=None, filename=None, key=None, root=None, + start_page=EditorPage): if EditorWindow.help_url is None: dochome = os.path.join(sys.prefix, 'Doc', 'index.html') if sys.platform.count('linux'): @@ -81,7 +76,7 @@ EditorWindow.help_url = 'file://' + EditorWindow.help_url else: EditorWindow.help_url = "http://www.python.org/doc/current" - currentTheme=idleConf.CurrentTheme() + self.flist = flist root = root or flist.root self.root = root @@ -90,94 +85,45 @@ except AttributeError: sys.ps1 = '>>> ' self.menubar = Menu(root) - self.top = top = WindowList.ListedToplevel(root, menu=self.menubar) + self.top = WindowList.ListedToplevel(root, menu=self.menubar) if flist: self.tkinter_vars = flist.vars - #self.top.instance_dict makes flist.inversedict avalable to - #configDialog.py so it can access all EditorWindow instaces + # self.top.instance_dict makes flist.inversedict avalable to + # configDialog.py so it can access all EditorWindow instaces self.top.instance_dict = flist.inversedict else: self.tkinter_vars = {} # keys: Tkinter event names # values: Tkinter variable instances self.top.instance_dict = {} self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(), - 'recent-files.lst') - self.text_frame = text_frame = Frame(top) - self.vbar = vbar = Scrollbar(text_frame, name='vbar') - self.width = idleConf.GetOption('main','EditorWindow','width') - self.text = text = MultiCallCreator(Text)( - text_frame, name='text', padx=5, wrap='none', - width=self.width, - height=idleConf.GetOption('main','EditorWindow','height') ) - self.top.focused_widget = self.text + 'recent-files.lst') - self.createmenubar() - self.apply_bindings() - - self.top.protocol("WM_DELETE_WINDOW", self.close) - self.top.bind("<>", self.close_event) - if macosxSupport.runningAsOSXApp(): - # Command-W on editorwindows doesn't work without this. - text.bind('<>', self.close_event) - text.bind("<>", self.cut) - text.bind("<>", self.copy) - text.bind("<>", self.paste) - text.bind("<>", self.center_insert_event) - text.bind("<>", self.help_dialog) - text.bind("<>", self.python_docs) - text.bind("<>", self.about_dialog) - text.bind("<>", self.config_dialog) - text.bind("<>", self.open_module) - text.bind("<>", lambda event: "break") - text.bind("<>", self.select_all) - text.bind("<>", self.remove_selection) - text.bind("<>", self.find_event) - text.bind("<>", self.find_again_event) - text.bind("<>", self.find_in_files_event) - text.bind("<>", self.find_selection_event) - text.bind("<>", self.replace_event) - text.bind("<>", self.goto_line_event) - text.bind("<3>", self.right_menu_event) - text.bind("<>",self.smart_backspace_event) - text.bind("<>",self.newline_and_indent_event) - text.bind("<>",self.smart_indent_event) - text.bind("<>",self.indent_region_event) - text.bind("<>",self.dedent_region_event) - text.bind("<>",self.comment_region_event) - text.bind("<>",self.uncomment_region_event) - text.bind("<>",self.tabify_region_event) - text.bind("<>",self.untabify_region_event) - text.bind("<>",self.toggle_tabs_event) - text.bind("<>",self.change_indentwidth_event) - text.bind("", self.move_at_edge_if_selection(0)) - text.bind("", self.move_at_edge_if_selection(1)) - text.bind("<>", self.del_word_left) - text.bind("<>", self.del_word_right) - text.bind("<>", self.home_callback) - if flist: flist.inversedict[self] = key if key: flist.dict[key] = self - text.bind("<>", self.new_callback) - text.bind("<>", self.flist.close_all_callback) - text.bind("<>", self.open_class_browser) - text.bind("<>", self.open_path_browser) - self.set_status_bar() - vbar['command'] = text.yview - vbar.pack(side=RIGHT, fill=Y) - text['yscrollcommand'] = vbar.set - fontWeight = 'normal' - if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'): - fontWeight='bold' - text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'), - idleConf.GetOption('main', 'EditorWindow', 'font-size'), - fontWeight)) - text_frame.pack(side=LEFT, fill=BOTH, expand=1) - text.pack(side=TOP, fill=BOTH, expand=1) - text.focus_set() + self.menudict = None + # create a Notebook where the text pages for this EditorWindow will + # reside + self.text_notebook = TabbedPageSet(self.top) + self.text_notebook.pack(fill=BOTH, expand=True) + self.text_notebook.bind('<>', self._update_controls) + self.new_tab(filename=filename, load_ext=False, ptype=start_page) + self.text = self.current_page.text # XXX + self.top.focused_widget = self.text + self.top.bind('<>', self._post_tab_close) + + # The following "width" attribute is used by PyShell, so keep it here + self.width = idleConf.GetOption('main', 'EditorPage', 'width') + + self.top.protocol("WM_DELETE_WINDOW", self.close) + self.top.bind("<>", self.close_event) + + self._create_statusbar() + self.top.after_idle(self.set_line_and_column) + # usetabs true -> literal tab characters are used by indent and # dedent cmds, possibly mixed with spaces if # indentwidth is not a multiple of tabwidth, @@ -187,7 +133,8 @@ # Although use-spaces=0 can be configured manually in config-main.def, # configuration of tabs v. spaces is not supported in the configuration # dialog. IDLE promotes the preferred Python indentation: use spaces! - usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool') + usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', + type='bool') self.usetabs = not usespaces # tabwidth is the display width of a literal tab character. @@ -214,38 +161,15 @@ # Making the initial values larger slows things down more often. self.num_context_lines = 50, 500, 5000000 - self.per = per = self.Percolator(text) + if hasattr(self, 'ispythonsource'): # PyShell + self.set_indentation_params(self.ispythonsource(filename)) + else: + self.set_indentation_params( + self.current_page.ispythonsource(filename)) - self.undo = undo = self.UndoDelegator() - per.insertfilter(undo) - text.undo_block_start = undo.undo_block_start - text.undo_block_stop = undo.undo_block_stop - undo.set_saved_change_hook(self.saved_change_hook) + self.extensions = {} + self._load_extensions() - # IOBinding implements file I/O and printing functionality - self.io = io = self.IOBinding(self) - io.set_filename_change_hook(self.filename_change_hook) - - # Create the recent files submenu - self.recent_files_menu = Menu(self.menubar) - self.menudict['file'].insert_cascade(3, label='Recent Files', - underline=0, - menu=self.recent_files_menu) - self.update_recent_files_list() - - self.color = None # initialized below in self.ResetColorizer - if filename: - if os.path.exists(filename) and not os.path.isdir(filename): - io.loadfile(filename) - else: - io.set_filename(filename) - self.ResetColorizer() - self.saved_change_hook() - - self.set_indentation_params(self.ispythonsource(filename)) - - self.load_extensions() - menu = self.menudict.get('windows') if menu: end = menu.index("end") @@ -262,120 +186,130 @@ self.askinteger = tkSimpleDialog.askinteger self.showerror = tkMessageBox.showerror - def _filename_to_unicode(self, filename): - """convert filename to unicode in order to display it in Tk""" - if isinstance(filename, unicode) or not filename: - return filename + @property + def current_page(self): + """Return the active EditorPage in EditorWindow.""" + curr_tab = self.text_notebook.select() + if not curr_tab: + return None + + if TTK: + page = self.text_notebook.pages[self.text_notebook.tab( + curr_tab)['text']].editpage else: - try: - return filename.decode(self.filesystemencoding) - except UnicodeDecodeError: - # XXX - try: - return filename.decode(self.encoding) - except UnicodeDecodeError: - # byte-to-byte conversion - return filename.decode('iso8859-1') + page = self.text_notebook.pages[curr_tab].editpage + return page - def new_callback(self, event): - dirname, basename = self.io.defaultfilename() - self.flist.new(dirname) - return "break" - - def home_callback(self, event): - if (event.state & 12) != 0 and event.keysym == "Home": - # state&1==shift, state&4==control, state&8==alt - return # ; fall back to class binding - - if self.text.index("iomark") and \ - self.text.compare("iomark", "<=", "insert lineend") and \ - self.text.compare("insert linestart", "<=", "iomark"): - insertpt = int(self.text.index("iomark").split(".")[1]) + def remove_tab_controls(self): + """Remove tab area and most tab bindings from this window.""" + if TTK: + self.text_notebook['style'] = 'PyShell.TNotebook' + style = Style(self.top) + style.layout('PyShell.TNotebook.Tab', [('null', '')]) else: - line = self.text.get("insert linestart", "insert lineend") - for insertpt in xrange(len(line)): - if line[insertpt] not in (' ','\t'): + self.text_notebook._tab_set.grid_forget() + + # remove commands related to tab + if 'file' in self.menudict: + menu = self.menudict['file'] + curr_entry = None + i = 0 + while True: + last_entry, curr_entry = curr_entry, menu.entryconfigure(i) + if last_entry == curr_entry: + # no more menu entries break - else: - insertpt=len(line) - lineat = int(self.text.index("insert").split('.')[1]) + if 'label' in curr_entry and 'Tab' in curr_entry['label'][-1]: + if 'Close' not in ' '.join(curr_entry['label'][-1]): + menu.delete(i) + i += 1 - if insertpt == lineat: - insertpt = 0 + self.current_page.text.unbind('<>') + # close-tab is still available! - dest = "insert linestart+"+str(insertpt)+"c" + def short_title(self): + # overriden by PyShell + return self.current_page.short_title() - if (event.state&1) == 0: - # shift not pressed - self.text.tag_remove("sel", "1.0", "end") - else: - if not self.text.index("sel.first"): - self.text.mark_set("anchor","insert") + def next_tab(self, event): + """Show next tab if not in the last tab already.""" + index = self.text_notebook.index(self.text_notebook.select()) + if index == len(self.text_notebook.tabs()) - 1: + return + self.text_notebook.select(index + 1) - first = self.text.index(dest) - last = self.text.index("anchor") + def prev_tab(self, event): + """Show the previous tab if not in the first tab already.""" + index = self.text_notebook.index(self.text_notebook.select()) + if index == 0: + return + self.text_notebook.select(index - 1) - if self.text.compare(first,">",last): - first,last = last,first + def new_tab(self, event=None, filename=None, load_ext=True, ptype=None): + """Create a new EditorPage and insert it into the notebook.""" + page_title = "#%d" % (len(self.text_notebook.pages) + 1) + page = self.text_notebook.add_page(page_title) - self.text.tag_remove("sel", "1.0", "end") - self.text.tag_add("sel", first, last) + vbar = Scrollbar(page.frame, name='vbar') + ptype = ptype or EditorPage + page.editpage = ptype(page.frame, self, title=page_title, + name='text', padx=5, wrap='none') - self.text.mark_set("insert", dest) - self.text.see("insert") - return "break" + firstpage = False # don't update window's title + if self.menudict is None: + # This EditorWindow is being created now, perform the following + # tasks before. + firstpage = True # will cause window's title to be updated + self.menudict = {} + self._createmenubar(page.editpage.text) + # Create the recent files submenu + self.recent_files_menu = Menu(self.menubar) + self.menudict['file'].insert_cascade(3, label='Recent Files', + underline=0, menu=self.recent_files_menu) + self.update_recent_files_list() - def set_status_bar(self): - self.status_bar = self.MultiStatusBar(self.top) - if macosxSupport.runningAsOSXApp(): - # Insert some padding to avoid obscuring some of the statusbar - # by the resize widget. - self.status_bar.set_label('_padding1', ' ', side=RIGHT) - self.status_bar.set_label('column', 'Col: ?', side=RIGHT) - self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) - self.status_bar.pack(side=BOTTOM, fill=X) - self.text.bind("<>", self.set_line_and_column) - self.text.event_add("<>", - "", "") - self.text.after_idle(self.set_line_and_column) + # pack widgets + text = page.editpage.text + vbar['command'] = text.yview + vbar.pack(side=RIGHT, fill=Y) + text['yscrollcommand'] = vbar.set + fontWeight = 'normal' + if idleConf.GetOption('main', 'EditorPage', 'font-bold', type='bool'): + fontWeight = 'bold' + text.config(font=(idleConf.GetOption('main', 'EditorPage', 'font'), + idleConf.GetOption('main', 'EditorPage', 'font-size'), + fontWeight)) + text.pack(side=TOP, fill=BOTH, expand=1) + text.focus_set() - def set_line_and_column(self, event=None): - line, column = self.text.index(INSERT).split('.') - self.status_bar.set_label('column', 'Col: %s' % column) - self.status_bar.set_label('line', 'Ln: %s' % line) + self.apply_bindings(tab=page) + if load_ext: + self._load_extensions() - menu_specs = [ - ("file", "_File"), - ("edit", "_Edit"), - ("format", "F_ormat"), - ("run", "_Run"), - ("options", "_Options"), - ("windows", "_Windows"), - ("help", "_Help"), - ] + # select the just created page + self.text_notebook.select(len(self.text_notebook.pages) - 1) - if macosxSupport.runningAsOSXApp(): - del menu_specs[-3] - menu_specs[-2] = ("windows", "_Window") + page.editpage.post_init(filename=filename, + update_window_title=firstpage) + self.top.event_generate('<>') + return "break" - def createmenubar(self): - mbar = self.menubar - self.menudict = menudict = {} - for name, label in self.menu_specs: - underline, label = prepstr(label) - menudict[name] = menu = Menu(mbar, name=name) - mbar.add_cascade(label=label, menu=menu, underline=underline) + def new_callback(self, event, page): + dirname, basename = page.io.defaultfilename() + self.flist.new(dirname) + return "break" - if sys.platform == 'darwin' and '.framework' in sys.executable: - # Insert the application menu - menudict['application'] = menu = Menu(mbar, name='apple') - mbar.add_cascade(label='IDLE', menu=menu) + def set_line_and_column(self, event=None): + # Used by PyShell too + curr_page = self.current_page + if not curr_page: + return - self.fill_menus() - self.base_helpmenu_length = self.menudict['help'].index(END) - self.reset_help_menu_entries() + line, column = curr_page.text.index(INSERT).split('.') + self.status_bar.set_label('column', 'Col: %s' % column) + self.status_bar.set_label('line', 'Ln: %s' % line) def postwindowsmenu(self): # Only called when Windows menu exists @@ -387,210 +321,28 @@ menu.delete(self.wmenu_end+1, end) WindowList.add_windows_to_menu(menu) - rmenu = None + def newline_and_indent_event(self, event): + """Call newline_and_indent_event on current EditorPage.""" + self.current_page.newline_and_indent_event(event) - def right_menu_event(self, event): - self.text.tag_remove("sel", "1.0", "end") - self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) - if not self.rmenu: - self.make_rmenu() - rmenu = self.rmenu - self.event = event - iswin = sys.platform[:3] == 'win' - if iswin: - self.text.config(cursor="arrow") - rmenu.tk_popup(event.x_root, event.y_root) - if iswin: - self.text.config(cursor="ibeam") + def get_selection_indices(self): + """Call get_selection_indices on current EditorPage.""" + return self.current_page.get_selection_indices() - rmenu_specs = [ - # ("Label", "<>"), ... - ("Close", "<>"), # Example - ] + def build_char_in_string_func(self, startindex): + """Call build_char_in_string_func on current EditorPage.""" + return self.current_page.build_char_in_string_func(startindex) - def make_rmenu(self): - rmenu = Menu(self.text, tearoff=0) - for label, eventname in self.rmenu_specs: - def command(text=self.text, eventname=eventname): - text.event_generate(eventname) - rmenu.add_command(label=label, command=command) - self.rmenu = rmenu - - def about_dialog(self, event=None): - aboutDialog.AboutDialog(self.top,'About IDLE') - - def config_dialog(self, event=None): - configDialog.ConfigDialog(self.top,'Settings') - - def help_dialog(self, event=None): - fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt') - textView.view_file(self.top,'Help',fn) - - def python_docs(self, event=None): - if sys.platform[:3] == 'win': - os.startfile(self.help_url) - else: - webbrowser.open(self.help_url) - return "break" - - def cut(self,event): - self.text.event_generate("<>") - return "break" - - def copy(self,event): - if not self.text.tag_ranges("sel"): - # There is no selection, so do nothing and maybe interrupt. - return - self.text.event_generate("<>") - return "break" - - def paste(self,event): - self.text.event_generate("<>") - self.text.see("insert") - return "break" - - def select_all(self, event=None): - self.text.tag_add("sel", "1.0", "end-1c") - self.text.mark_set("insert", "1.0") - self.text.see("insert") - return "break" - - def remove_selection(self, event=None): - self.text.tag_remove("sel", "1.0", "end") - self.text.see("insert") - - def move_at_edge_if_selection(self, edge_index): - """Cursor move begins at start or end of selection - - When a left/right cursor key is pressed create and return to Tkinter a - function which causes a cursor move from the associated edge of the - selection. - - """ - self_text_index = self.text.index - self_text_mark_set = self.text.mark_set - edges_table = ("sel.first+1c", "sel.last-1c") - def move_at_edge(event): - if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed - try: - self_text_index("sel.first") - self_text_mark_set("insert", edges_table[edge_index]) - except TclError: - pass - return move_at_edge - - def del_word_left(self, event): - self.text.event_generate('') - return "break" - - def del_word_right(self, event): - self.text.event_generate('') - return "break" - - def find_event(self, event): - SearchDialog.find(self.text) - return "break" - - def find_again_event(self, event): - SearchDialog.find_again(self.text) - return "break" - - def find_selection_event(self, event): - SearchDialog.find_selection(self.text) - return "break" - - def find_in_files_event(self, event): - GrepDialog.grep(self.text, self.io, self.flist) - return "break" - - def replace_event(self, event): - ReplaceDialog.replace(self.text) - return "break" - - def goto_line_event(self, event): - text = self.text - lineno = tkSimpleDialog.askinteger("Goto", - "Go to line number:",parent=text) - if lineno is None: - return "break" - if lineno <= 0: - text.bell() - return "break" - text.mark_set("insert", "%d.0" % lineno) - text.see("insert") - - def open_module(self, event=None): - # XXX Shouldn't this be in IOBinding or in FileList? - try: - name = self.text.get("sel.first", "sel.last") - except TclError: - name = "" - else: - name = name.strip() - name = tkSimpleDialog.askstring("Module", - "Enter the name of a Python module\n" - "to search on sys.path and open:", - parent=self.text, initialvalue=name) - if name: - name = name.strip() - if not name: - return - # XXX Ought to insert current file's directory in front of path - try: - (f, file, (suffix, mode, type)) = _find_module(name) - except (NameError, ImportError), msg: - tkMessageBox.showerror("Import error", str(msg), parent=self.text) - return - if type != imp.PY_SOURCE: - tkMessageBox.showerror("Unsupported type", - "%s is not a source module" % name, parent=self.text) - return - if f: - f.close() - if self.flist: - self.flist.open(file) - else: - self.io.loadfile(file) - - def open_class_browser(self, event=None): - filename = self.io.filename - if not filename: - tkMessageBox.showerror( - "No filename", - "This buffer has no associated filename", - master=self.text) - self.text.focus_set() - return None - head, tail = os.path.split(filename) - base, ext = os.path.splitext(tail) - import ClassBrowser - ClassBrowser.ClassBrowser(self.flist, base, [head]) - - def open_path_browser(self, event=None): - import PathBrowser - PathBrowser.PathBrowser(self.flist) - def gotoline(self, lineno): + page = self.current_page + text = page.text + if lineno is not None and lineno > 0: - self.text.mark_set("insert", "%d.0" % lineno) - self.text.tag_remove("sel", "1.0", "end") - self.text.tag_add("sel", "insert", "insert +1l") - self.center() + text.mark_set("insert", "%d.0" % lineno) + text.tag_remove("sel", "1.0", "end") + text.tag_add("sel", "insert", "insert +1l") + page.center() - def ispythonsource(self, filename): - if not filename or os.path.isdir(filename): - return True - base, ext = os.path.splitext(os.path.basename(filename)) - if os.path.normcase(ext) in (".py", ".pyw"): - return True - try: - f = open(filename) - line = f.readline() - f.close() - except IOError: - return False - return line.startswith('#!') and line.find('python') >= 0 - def close_hook(self): if self.flist: self.flist.unregister_maybe_terminate(self) @@ -599,82 +351,59 @@ def set_close_hook(self, close_hook): self.close_hook = close_hook - def filename_change_hook(self): - if self.flist: - self.flist.filename_changed_edit(self) - self.saved_change_hook() - self.top.update_windowlist_registry(self) - self.ResetColorizer() + def set_theme(self, ttkstyle): + # called from configDialog.py + ttkstyle.theme_use(idleConf.GetOption('main', 'Theme', 'displaytheme')) - def _addcolorizer(self): - if self.color: - return - if self.ispythonsource(self.io.filename): - self.color = self.ColorDelegator() - # can add more colorizers here... - if self.color: - self.per.removefilter(self.undo) - self.per.insertfilter(self.color) - self.per.insertfilter(self.undo) - - def _rmcolorizer(self): - if not self.color: - return - self.color.removecolors() - self.per.removefilter(self.color) - self.color = None - def ResetColorizer(self): "Update the colour theme" # Called from self.filename_change_hook and from configDialog.py - self._rmcolorizer() - self._addcolorizer() - theme = idleConf.GetOption('main','Theme','name') - normal_colors = idleConf.GetHighlight(theme, 'normal') - cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg') - select_colors = idleConf.GetHighlight(theme, 'hilite') - self.text.config( - foreground=normal_colors['foreground'], - background=normal_colors['background'], - insertbackground=cursor_color, - selectforeground=select_colors['foreground'], - selectbackground=select_colors['background'], - ) + for page in self.text_notebook.pages.itervalues(): + page.editpage.reset_colorizer() def ResetFont(self): "Update the text widgets' font if it is changed" # Called from configDialog.py - fontWeight='normal' - if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'): - fontWeight='bold' - self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'), - idleConf.GetOption('main','EditorWindow','font-size'), + fontWeight = 'normal' + if idleConf.GetOption('main', 'EditorPage', 'font-bold', type='bool'): + fontWeight = 'bold' + + for page in self.text_notebook.pages.itervalues(): + text = page.editpage.text + text.config(font=(idleConf.GetOption('main', 'EditorPage', 'font'), + idleConf.GetOption('main', 'EditorPage', 'font-size'), fontWeight)) def RemoveKeybindings(self): "Remove the keybindings before they are changed." # Called from configDialog.py - self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() - for event, keylist in keydefs.items(): - self.text.event_delete(event, *keylist) - for extensionName in self.get_standard_extension_names(): + Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + + for page in self.text_notebook.pages.itervalues(): + text = page.editpage.text + for event, keylist in keydefs.items(): + text.event_delete(event, *keylist) + + for extensionName in self._get_standard_extension_names(): xkeydefs = idleConf.GetExtensionBindings(extensionName) if xkeydefs: - for event, keylist in xkeydefs.items(): - self.text.event_delete(event, *keylist) + for page in self.text_notebook.pages.itervalues(): + text = page.editpage.text + for event, keylist in xkeydefs.items(): + text.event_delete(event, *keylist) def ApplyKeybindings(self): "Update the keybindings after they are changed" # Called from configDialog.py - self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() self.apply_bindings() - for extensionName in self.get_standard_extension_names(): + for extensionName in self._get_standard_extension_names(): xkeydefs = idleConf.GetExtensionBindings(extensionName) if xkeydefs: self.apply_bindings(xkeydefs) #update menu accelerators menuEventDict = {} - for menu in self.Bindings.menudefs: + for menu in Bindings.menudefs: menuEventDict[menu[0]] = {} for item in menu[1]: if item: @@ -695,13 +424,6 @@ accel = get_accelerator(keydefs, event) menu.entryconfig(index, accelerator=accel) - def set_notabs_indentwidth(self): - "Update the indentwidth if changed and not using tabs in this window" - # Called from configDialog.py - if not self.usetabs: - self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', - type='int') - def reset_help_menu_entries(self): "Update the additional help entries on the Help menu" help_list = idleConf.GetAllExtraHelpSourcesList() @@ -719,19 +441,16 @@ # and update the menu dictionary self.menudict['help'] = helpmenu - def __extra_help_callback(self, helpfile): - "Create a callback with the helpfile value frozen at definition time" - def display_extra_help(helpfile=helpfile): - if not helpfile.startswith(('www', 'http')): - url = os.path.normpath(helpfile) - if sys.platform[:3] == 'win': - os.startfile(helpfile) - else: - webbrowser.open(helpfile) - return display_extra_help + def set_notabs_indentwidth(self): + "Update the indentwidth if changed and not using tabs in this window" + # Called from configDialog.py + if not self.usetabs: + self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', + type='int') def update_recent_files_list(self, new_file=None): "Load and update the recent files list and menus" + # IOBinding calls this rf_list = [] if os.path.exists(self.recent_files_path): rf_list_file = open(self.recent_files_path,'r') @@ -761,83 +480,15 @@ for instance in self.top.instance_dict.keys(): menu = instance.recent_files_menu menu.delete(1, END) # clear, and rebuild: - for i, file in zip(count(), rf_list): + for i, file in enumerate(rf_list): file_name = file[0:-1] # zap \n # make unicode string to display non-ASCII chars correctly - ufile_name = self._filename_to_unicode(file_name) + ufile_name = filename_to_unicode(file_name) callback = instance.__recent_file_callback(file_name) menu.add_command(label=ulchars[i] + " " + ufile_name, command=callback, underline=0) - def __recent_file_callback(self, file_name): - def open_recent_file(fn_closure=file_name): - self.io.open(editFile=fn_closure) - return open_recent_file - - def saved_change_hook(self): - short = self.short_title() - long = self.long_title() - if short and long: - title = short + " - " + long - elif short: - title = short - elif long: - title = long - else: - title = "Untitled" - icon = short or long or title - if not self.get_saved(): - title = "*%s*" % title - icon = "*%s" % icon - self.top.wm_title(title) - self.top.wm_iconname(icon) - - def get_saved(self): - return self.undo.get_saved() - - def set_saved(self, flag): - self.undo.set_saved(flag) - - def reset_undo(self): - self.undo.reset_undo() - - def short_title(self): - filename = self.io.filename - if filename: - filename = os.path.basename(filename) - # return unicode string to display non-ASCII chars correctly - return self._filename_to_unicode(filename) - - def long_title(self): - # return unicode string to display non-ASCII chars correctly - return self._filename_to_unicode(self.io.filename or "") - - def center_insert_event(self, event): - self.center() - - def center(self, mark="insert"): - text = self.text - top, bot = self.getwindowlines() - lineno = self.getlineno(mark) - height = bot - top - newtop = max(1, lineno - height//2) - text.yview(float(newtop)) - - def getwindowlines(self): - text = self.text - top = self.getlineno("@0,0") - bot = self.getlineno("@0,65535") - if top == bot and text.winfo_height() == 1: - # Geometry manager hasn't run yet - height = int(text['height']) - bot = top + height - 1 - return top, bot - - def getlineno(self, mark="insert"): - text = self.text - return int(float(text.index(mark))) - def get_geometry(self): "Return (width, height, x, y)" geom = self.top.wm_geometry() @@ -848,132 +499,49 @@ def close_event(self, event): self.close() - 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() - return self.io.maybesave() - def close(self): - reply = self.maybesave() - if str(reply) != "cancel": - self._close() - return reply + to_check = self.text_notebook.pages.copy() + while to_check: + curr_tab = self.text_notebook.select() + if TTK: + page_name = self.text_notebook.tab(curr_tab)['text'] + else: + page_name = curr_tab + page = to_check.pop(page_name) + editpage = page.editpage + reply = editpage.close_tab() + if reply == "cancel": + break + 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 - self.undo = None - if self.color: - self.color.close(False) - self.color = None - self.text = None + self._unload_extensions() self.tkinter_vars = None - self.per.close() - self.per = None + + for page in self.text_notebook.pages.itervalues(): + page.editpage.close() + self.top.destroy() if self.close_hook: # unless override: unregister from flist, terminate if last window self.close_hook() - def load_extensions(self): - self.extensions = {} - self.load_standard_extensions() - - def unload_extensions(self): - for ins in self.extensions.values(): - if hasattr(ins, "close"): - ins.close() - self.extensions = {} - - def load_standard_extensions(self): - for name in self.get_standard_extension_names(): - try: - self.load_extension(name) - except: - print "Failed to load extension", repr(name) - import traceback - traceback.print_exc() - - def get_standard_extension_names(self): - return idleConf.GetExtensions(editor_only=True) - - def load_extension(self, name): - try: - mod = __import__(name, globals(), locals(), []) - except ImportError: - print "\nFailed to import extension: ", name - return - cls = getattr(mod, name) - keydefs = idleConf.GetExtensionBindings(name) - if hasattr(cls, "menudefs"): - self.fill_menus(cls.menudefs, keydefs) - ins = cls(self) - self.extensions[name] = ins - if keydefs: - self.apply_bindings(keydefs) - for vevent in keydefs.keys(): - methodname = vevent.replace("-", "_") - while methodname[:1] == '<': - methodname = methodname[1:] - while methodname[-1:] == '>': - methodname = methodname[:-1] - methodname = methodname + "_event" - if hasattr(ins, methodname): - self.text.bind(vevent, getattr(ins, methodname)) - - def apply_bindings(self, keydefs=None): + def apply_bindings(self, keydefs=None, tab=None): if keydefs is None: - keydefs = self.Bindings.default_keydefs - text = self.text - text.keydefs = keydefs - for event, keylist in keydefs.items(): - if keylist: - text.event_add(event, *keylist) + keydefs = Bindings.default_keydefs - def fill_menus(self, menudefs=None, keydefs=None): - """Add appropriate entries to the menus and submenus + if tab: + iter_over = [tab] + else: + iter_over = self.text_notebook.pages.itervalues() - Menus that are absent or None in self.menudict are ignored. - """ - if menudefs is None: - menudefs = self.Bindings.menudefs - if keydefs is None: - keydefs = self.Bindings.default_keydefs - menudict = self.menudict - text = self.text - for mname, entrylist in menudefs: - menu = menudict.get(mname) - if not menu: - continue - for entry in entrylist: - if not entry: - menu.add_separator() - else: - label, eventname = entry - checkbutton = (label[:1] == '!') - if checkbutton: - label = label[1:] - underline, label = prepstr(label) - accelerator = get_accelerator(keydefs, eventname) - def command(text=text, eventname=eventname): - text.event_generate(eventname) - if checkbutton: - var = self.get_var_obj(eventname, BooleanVar) - menu.add_checkbutton(label=label, underline=underline, - command=command, accelerator=accelerator, - variable=var) - else: - menu.add_command(label=label, underline=underline, - command=command, - accelerator=accelerator) + for page in iter_over: + text = page.editpage.text + text.keydefs = keydefs + for event, keylist in keydefs.items(): + if keylist: + text.event_add(event, *keylist) def getvar(self, name): var = self.get_var_obj(name) @@ -990,52 +558,25 @@ else: raise NameError, name - def get_var_obj(self, name, vartype=None): + def get_var_obj(self, name, vartype=None, text=None): var = self.tkinter_vars.get(name) if not var and vartype: # create a Tkinter variable object with self.text as master: - self.tkinter_vars[name] = var = vartype(self.text) + self.tkinter_vars[name] = var = vartype(text or self.text) return var # Tk implementations of "virtual text methods" -- each platform # reusing IDLE's support code needs to define these for its GUI's # flavor of widget. - # Is character at text_index in a Python string? Return 0 for - # "guaranteed no", true for anything else. This info is expensive - # to compute ab initio, but is probably already known by the - # platform's colorizer. - - def is_char_in_string(self, text_index): - if self.color: - # Return true iff colorizer hasn't (re)gotten this far - # yet, or the character is tagged as being in a string - return self.text.tag_prevrange("TODO", text_index) or \ - "STRING" in self.text.tag_names(text_index) - else: - # The colorizer is missing: assume the worst - return 1 - - # If a selection is defined in the text widget, return (start, - # end) as Tkinter text indices, otherwise return (None, None) - def get_selection_indices(self): - try: - first = self.text.index("sel.first") - last = self.text.index("sel.last") - return first, last - except TclError: - return None, None - # Return the text widget's current view of what a tab stop means # (equivalent width in spaces). - - def get_tabwidth(self): + def get_tabwidth(self): # XXX depends on self.text current = self.text['tabs'] or TK_TABWIDTH_DEFAULT return int(current) # Set the text widget's current view of what a tab stop means. - - def set_tabwidth(self, newtabwidth): + def set_tabwidth(self, newtabwidth): # XXX depends on self.text text = self.text if self.get_tabwidth() != newtabwidth: pixels = text.tk.call("font", "measure", text["font"], @@ -1048,7 +589,6 @@ # indentwidth != tabwidth set usetabs false. # In any case, adjust the Text widget's view of what a tab # character means. - def set_indentation_params(self, ispythonsource, guess=True): if guess and ispythonsource: i = self.guess_indent() @@ -1058,386 +598,187 @@ self.usetabs = False self.set_tabwidth(self.tabwidth) - def smart_backspace_event(self, event): - text = self.text - first, last = self.get_selection_indices() - if first and last: - text.delete(first, last) - text.mark_set("insert", first) - return "break" - # Delete whitespace left, until hitting a real char or closest - # preceding virtual tab stop. - chars = text.get("insert linestart", "insert") - if chars == '': - if text.compare("insert", ">", "1.0"): - # easy: delete preceding newline - text.delete("insert-1c") - else: - text.bell() # at start of buffer - return "break" - if chars[-1] not in " \t": - # easy: delete preceding real char - text.delete("insert-1c") - return "break" - # Ick. It may require *inserting* spaces if we back up over a - # tab character! This is written to be clear, not fast. - tabwidth = self.tabwidth - have = len(chars.expandtabs(tabwidth)) - assert have > 0 - want = ((have - 1) // self.indentwidth) * self.indentwidth - # Debug prompt is multilined.... - last_line_of_prompt = sys.ps1.split('\n')[-1] - ncharsdeleted = 0 - while 1: - if chars == last_line_of_prompt: - break - chars = chars[:-1] - ncharsdeleted = ncharsdeleted + 1 - have = len(chars.expandtabs(tabwidth)) - if have <= want or chars[-1] not in " \t": - break - text.undo_block_start() - text.delete("insert-%dc" % ncharsdeleted, "insert") - if have < want: - text.insert("insert", ' ' * (want - have)) - text.undo_block_stop() - return "break" + # Guess indentwidth from text content. + # Return guessed indentwidth. This should not be believed unless + # it's in a reasonable range (e.g., it will be 0 if no indented + # blocks are found). + def guess_indent(self): # XXX depends on self.text + opener, indented = IndentSearcher(self.text, self.tabwidth).run() + if opener and indented: + raw, indentsmall = classifyws(opener, self.tabwidth) + raw, indentlarge = classifyws(indented, self.tabwidth) + else: + indentsmall = indentlarge = 0 + return indentlarge - indentsmall - def smart_indent_event(self, event): - # if intraline selection: - # delete it - # elif multiline selection: - # do indent-region - # else: - # indent one level - text = self.text - first, last = self.get_selection_indices() - text.undo_block_start() - try: - if first and last: - if index2line(first) != index2line(last): - return self.indent_region_event(event) - text.delete(first, last) - text.mark_set("insert", first) - prefix = text.get("insert linestart", "insert") - raw, effective = classifyws(prefix, self.tabwidth) - if raw == len(prefix): - # only whitespace to the left - self.reindent_to(effective + self.indentwidth) - else: - # tab to the next 'stop' within or to right of line's text: - if self.usetabs: - pad = '\t' - else: - effective = len(prefix.expandtabs(self.tabwidth)) - n = self.indentwidth - pad = ' ' * (n - effective % n) - text.insert("insert", pad) - text.see("insert") - return "break" - finally: - text.undo_block_stop() + # Private methods/attributes - def newline_and_indent_event(self, event): - text = self.text - first, last = self.get_selection_indices() - text.undo_block_start() - try: - if first and last: - text.delete(first, last) - text.mark_set("insert", first) - line = text.get("insert linestart", "insert") - i, n = 0, len(line) - while i < n and line[i] in " \t": - i = i+1 - if i == n: - # the cursor is in or at leading indentation in a continuation - # line; just inject an empty line at the start - text.insert("insert linestart", '\n') - return "break" - indent = line[:i] - # strip whitespace before insert point unless it's in the prompt - i = 0 - last_line_of_prompt = sys.ps1.split('\n')[-1] - while line and line[-1] in " \t" and line != last_line_of_prompt: - line = line[:-1] - i = i+1 - if i: - text.delete("insert - %d chars" % i, "insert") - # strip whitespace after insert point - while text.get("insert") in " \t": - text.delete("insert") - # start new line - text.insert("insert", '\n') + # extensions won't have more than one instance per window + _unique_extensions = ['CodeContext', 'ScriptBinding', 'FormatParagraph'] - # adjust indentation for continuations and block - # open/close first need to find the last stmt - lno = index2line(text.index('insert')) - y = PyParse.Parser(self.indentwidth, self.tabwidth) - if not self.context_use_ps1: - for context in self.num_context_lines: - startat = max(lno - context, 1) - startatindex = `startat` + ".0" - rawtext = text.get(startatindex, "insert") - y.set_str(rawtext) - bod = y.find_good_parse_start( - self.context_use_ps1, - self._build_char_in_string_func(startatindex)) - if bod is not None or startat == 1: - break - y.set_lo(bod or 0) - else: - r = text.tag_prevrange("console", "insert") - if r: - startatindex = r[1] - else: - startatindex = "1.0" - rawtext = text.get(startatindex, "insert") - y.set_str(rawtext) - y.set_lo(0) + def _unload_extensions(self): + for ins in self.extensions.values(): + if hasattr(ins, "close"): + ins.close() + self.extensions = {} - c = y.get_continuation_type() - if c != PyParse.C_NONE: - # The current stmt hasn't ended yet. - if c == PyParse.C_STRING_FIRST_LINE: - # after the first line of a string; do not indent at all - pass - elif c == PyParse.C_STRING_NEXT_LINES: - # inside a string which started before this line; - # just mimic the current indent - text.insert("insert", indent) - elif c == PyParse.C_BRACKET: - # line up with the first (if any) element of the - # last open bracket structure; else indent one - # level beyond the indent of the line with the - # last open bracket - self.reindent_to(y.compute_bracket_indent()) - elif c == PyParse.C_BACKSLASH: - # if more than one line in this stmt already, just - # mimic the current indent; else if initial line - # has a start on an assignment stmt, indent to - # beyond leftmost =; else to beyond first chunk of - # non-whitespace on initial line - if y.get_num_lines_in_stmt() > 1: - text.insert("insert", indent) - else: - self.reindent_to(y.compute_backslash_indent()) - else: - assert 0, "bogus continuation type %r" % (c,) - return "break" + def _load_extension(self, name, tab): + ext_loaded = self.extensions.get(name) - # This line starts a brand new stmt; indent relative to - # indentation of initial line of closest preceding - # interesting stmt. - indent = y.get_base_indent_string() - text.insert("insert", indent) - if y.is_block_opener(): - self.smart_indent_event(event) - elif indent and y.is_block_closer(): - self.smart_backspace_event(event) - return "break" - finally: - text.see("insert") - text.undo_block_stop() + try: + mod = __import__(name, globals(), locals(), []) + except ImportError: + print "\nFailed to import extension: ", name + return - # Our editwin provides a is_char_in_string function that works - # with a Tk text index, but PyParse only knows about offsets into - # a string. This builds a function for PyParse that accepts an - # offset. + keydefs = idleConf.GetExtensionBindings(name) - def _build_char_in_string_func(self, startindex): - def inner(offset, _startindex=startindex, - _icis=self.is_char_in_string): - return _icis(_startindex + "+%dc" % offset) - return inner + if name not in self._unique_extensions or not ext_loaded: + # create a new instance + cls = getattr(mod, name) + ins = cls(tab.editpage) + self.extensions.setdefault(name, []).append(ins) + if not ext_loaded: + # create new items in menu only if this is the first time this + # extension is being loaded in this window + if hasattr(cls, "menudefs"): + self._fill_menus(cls.menudefs, keydefs) + elif name in self._unique_extensions and ext_loaded: + # get an existing instance + ins = self.extensions[name][0] - def indent_region_event(self, event): - head, tail, chars, lines = self.get_region() - for pos in range(len(lines)): - line = lines[pos] - if line: - raw, effective = classifyws(line, self.tabwidth) - effective = effective + self.indentwidth - lines[pos] = self._make_blanks(effective) + line[raw:] - self.set_region(head, tail, chars, lines) - return "break" + if keydefs: + self.apply_bindings(keydefs, tab) + for vevent in keydefs.keys(): + methodname = vevent.replace("-", "_") + while methodname[:1] == '<': + methodname = methodname[1:] + while methodname[-1:] == '>': + methodname = methodname[:-1] + methodname = methodname + "_event" + if hasattr(ins, methodname): + tab.editpage.text.bind(vevent, getattr(ins, methodname)) - def dedent_region_event(self, event): - head, tail, chars, lines = self.get_region() - for pos in range(len(lines)): - line = lines[pos] - if line: - raw, effective = classifyws(line, self.tabwidth) - effective = max(effective - self.indentwidth, 0) - lines[pos] = self._make_blanks(effective) + line[raw:] - self.set_region(head, tail, chars, lines) - return "break" + def _load_extensions(self): + self._load_standard_extensions(self.text_notebook.last_page()) - def comment_region_event(self, event): - head, tail, chars, lines = self.get_region() - for pos in range(len(lines) - 1): - line = lines[pos] - lines[pos] = '##' + line - self.set_region(head, tail, chars, lines) + def _load_standard_extensions(self, tab): + for name in self._get_standard_extension_names(): + try: + self._load_extension(name, tab) + except: + print "Failed to load extension", repr(name) + traceback.print_exc() - def uncomment_region_event(self, event): - head, tail, chars, lines = self.get_region() - for pos in range(len(lines)): - line = lines[pos] - if not line: - continue - if line[:2] == '##': - line = line[2:] - elif line[:1] == '#': - line = line[1:] - lines[pos] = line - self.set_region(head, tail, chars, lines) + def _get_standard_extension_names(self): + return idleConf.GetExtensions(editor_only=True) - def tabify_region_event(self, event): - head, tail, chars, lines = self.get_region() - tabwidth = self._asktabwidth() - for pos in range(len(lines)): - line = lines[pos] - if line: - raw, effective = classifyws(line, tabwidth) - ntabs, nspaces = divmod(effective, tabwidth) - lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:] - self.set_region(head, tail, chars, lines) + def _post_tab_close(self, event): + if not self.current_page: + # no tabs now, close window + self._close() + return - def untabify_region_event(self, event): - head, tail, chars, lines = self.get_region() - tabwidth = self._asktabwidth() - for pos in range(len(lines)): - lines[pos] = lines[pos].expandtabs(tabwidth) - self.set_region(head, tail, chars, lines) + def _update_controls(self, event): + curr_page = self.current_page + if not curr_page: + return - def toggle_tabs_event(self, event): - if self.askyesno( - "Toggle tabs", - "Turn tabs " + ("on", "off")[self.usetabs] + - "?\nIndent width " + - ("will be", "remains at")[self.usetabs] + " 8." + - "\n Note: a tab is always 8 columns", - parent=self.text): - self.usetabs = not self.usetabs - # Try to prevent inconsistent indentation. - # User must change indent width manually after using tabs. - self.indentwidth = 8 - return "break" + self.text = curr_page.text + curr_page.saved_change_hook(True, False) # update window title + curr_page.text.focus_set() + self.set_line_and_column() - # XXX this isn't bound to anything -- see tabwidth comments -## def change_tabwidth_event(self, event): -## new = self._asktabwidth() -## if new != self.tabwidth: -## self.tabwidth = new -## self.set_indentation_params(0, guess=0) -## return "break" + # update references in extensions that are loaded only once + for ext in self._unique_extensions: + if ext not in self.extensions: + continue + ext = self.extensions[ext][0] + ext.editpage = curr_page - def change_indentwidth_event(self, event): - new = self.askinteger( - "Indent width", - "New indent width (2-16)\n(Always use 8 when using tabs)", - parent=self.text, - initialvalue=self.indentwidth, - minvalue=2, - maxvalue=16) - if new and new != self.indentwidth and not self.usetabs: - self.indentwidth = new - return "break" + def _create_statusbar(self): + self.status_bar = MultiStatusBar(self.top) + if macosxSupport.runningAsOSXApp(): + # Insert some padding to avoid obscuring some of the statusbar + # by the resize widget. + self.status_bar.set_label('_padding1', ' ', side=RIGHT) + self.status_bar.set_label('column', 'Col: ?', side=RIGHT) + self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) + self.status_bar.pack(side=BOTTOM, fill=X) - def get_region(self): - text = self.text - first, last = self.get_selection_indices() - if first and last: - head = text.index(first + " linestart") - tail = text.index(last + "-1c lineend +1c") - else: - head = text.index("insert linestart") - tail = text.index("insert lineend +1c") - chars = text.get(head, tail) - lines = chars.split("\n") - return head, tail, chars, lines + def _createmenubar(self, text): + mbar = self.menubar + menudict = self.menudict + for name, label in self.menu_specs: + underline, label = prepstr(label) + menudict[name] = menu = Menu(mbar, name=name, tearoff=0) + mbar.add_cascade(label=label, menu=menu, underline=underline) - def set_region(self, head, tail, chars, lines): - text = self.text - newchars = "\n".join(lines) - if newchars == chars: - text.bell() - return - text.tag_remove("sel", "1.0", "end") - text.mark_set("insert", head) - text.undo_block_start() - text.delete(head, tail) - text.insert(head, newchars) - text.undo_block_stop() - text.tag_add("sel", head, "insert") + if sys.platform == 'darwin' and '.framework' in sys.executable: + # Insert the application menu + menudict['application'] = menu = Menu(mbar, name='apple') + mbar.add_cascade(label='IDLE', menu=menu) - # Make string that displays as n leading blanks. + self._fill_menus(text=text) + self.base_helpmenu_length = self.menudict['help'].index(END) + self.reset_help_menu_entries() - def _make_blanks(self, n): - if self.usetabs: - ntabs, nspaces = divmod(n, self.tabwidth) - return '\t' * ntabs + ' ' * nspaces - else: - return ' ' * n + def _fill_menus(self, menudefs=None, keydefs=None, text=None): + """Add appropriate entries to the menus and submenus - # Delete from beginning of line to insert point, then reinsert - # column logical (meaning use tabs if appropriate) spaces. + Menus that are absent or None in self.menudict are ignored. + """ + if menudefs is None: + menudefs = Bindings.menudefs + if keydefs is None: + keydefs = Bindings.default_keydefs + menudict = self.menudict + for mname, entrylist in menudefs: + menu = menudict.get(mname) + if not menu: + continue + for entry in entrylist: + if not entry: + menu.add_separator() + else: + label, eventname = entry + checkbutton = (label[:1] == '!') + if checkbutton: + label = label[1:] + underline, label = prepstr(label) + accelerator = get_accelerator(keydefs, eventname) + def command(eventname=eventname): + self.text.event_generate(eventname) + if checkbutton: + var = self.get_var_obj(eventname, BooleanVar, text) + menu.add_checkbutton(label=label, underline=underline, + command=command, accelerator=accelerator, + variable=var) + else: + menu.add_command(label=label, underline=underline, + command=command, accelerator=accelerator) - def reindent_to(self, column): - text = self.text - text.undo_block_start() - if text.compare("insert linestart", "!=", "insert"): - text.delete("insert linestart", "insert") - if column: - text.insert("insert", self._make_blanks(column)) - text.undo_block_stop() + def __recent_file_callback(self, file_name): + def open_recent_file(fn_closure=file_name): + self.current_page.io.open(editFile=fn_closure) + return open_recent_file - def _asktabwidth(self): - return self.askinteger( - "Tab width", - "Columns per tab? (2-16)", - parent=self.text, - initialvalue=self.indentwidth, - minvalue=2, - maxvalue=16) or self.tabwidth + def __extra_help_callback(self, helpfile): + "Create a callback with the helpfile value frozen at definition time" + def display_extra_help(helpfile=helpfile): + if not helpfile.startswith(('www', 'http')): + url = os.path.normpath(helpfile) + if sys.platform[:3] == 'win': + os.startfile(helpfile) + else: + webbrowser.open(helpfile) + return display_extra_help - # Guess indentwidth from text content. - # Return guessed indentwidth. This should not be believed unless - # it's in a reasonable range (e.g., it will be 0 if no indented - # blocks are found). - - def guess_indent(self): - opener, indented = IndentSearcher(self.text, self.tabwidth).run() - if opener and indented: - raw, indentsmall = classifyws(opener, self.tabwidth) - raw, indentlarge = classifyws(indented, self.tabwidth) - else: - indentsmall = indentlarge = 0 - return indentlarge - indentsmall - -# "line.col" -> line, as an int -def index2line(index): - return int(float(index)) - # Look at the leading whitespace in s. # Return pair (# of leading ws characters, # effective # of leading blanks after expanding # tabs to width tabwidth) -def classifyws(s, tabwidth): - raw = effective = 0 - for ch in s: - if ch == ' ': - raw = raw + 1 - effective = effective + 1 - elif ch == '\t': - raw = raw + 1 - effective = (effective // tabwidth + 1) * tabwidth - else: - break - return raw, effective - import tokenize _tokenize = tokenize del tokenize @@ -1534,6 +875,7 @@ def test(): + from Tkinter import Tk root = Tk() fixwordbreaks(root) root.withdraw() Index: help.txt =================================================================== --- help.txt (revision 63995) +++ help.txt (revision 65573) @@ -6,6 +6,7 @@ File Menu: New Window -- Create a new editing window + New Tab -- Create a new editing tab Open... -- Open an existing file Recent Files... -- Open a list of recent files Open Module... -- Open an existing module (searches sys.path) @@ -23,6 +24,7 @@ --- Print Window -- Print the current window --- + Close Tab -- Close current tab (asks to save if unsaved) Close -- Close current window (asks to save if unsaved) Exit -- Close all windows, quit (asks to save if unsaved) Index: editorpage.py =================================================================== --- editorpage.py (revision 0) +++ editorpage.py (revision 65573) @@ -0,0 +1,1023 @@ +import os +import sys +import imp +import webbrowser +import tkMessageBox +import tkSimpleDialog +from Tkinter import Text, Menu, TclError + +import utils +import textView +import aboutDialog +import configDialog +import macosxSupport +import PyParse +import IOBinding +import GrepDialog +import PathBrowser +import ClassBrowser +import SearchDialog +import ReplaceDialog +from configHandler import idleConf +from MultiCall import MultiCallCreator +from Percolator import Percolator + +def classifyws(s, tabwidth): + raw = effective = 0 + for ch in s: + if ch == ' ': + raw = raw + 1 + effective = effective + 1 + elif ch == '\t': + raw = raw + 1 + effective = (effective // tabwidth + 1) * tabwidth + else: + break + return raw, effective + +def index2line(index): + """"line.col" -> line, as an int""" + return int(float(index)) + +def filename_to_unicode(filename): + """Convert filename to unicode in order to display it in Tk""" + if isinstance(filename, unicode) or not filename: + return filename + else: + try: + return filename.decode(IOBinding.filesystemencoding) + except UnicodeDecodeError: + # XXX + try: + return filename.decode(IOBinding.encoding) + except UnicodeDecodeError: + # byte-to-byte conversion + return filename.decode('iso8859-1') + +def _find_module(fullname, path=None): + """Version of imp.find_module() that handles hierarchical module names""" + file = None + + for tgt in fullname.split('.'): + if file is not None: + file.close() # close intermediate files + (file, filename, descr) = imp.find_module(tgt, path) + if descr[2] == imp.PY_SOURCE: + break # find but not load the source file + module = imp.load_module(tgt, file, filename, descr) + try: + path = module.__path__ + except AttributeError: + raise ImportError('No source for module %s' % module.__name__) + + return file, filename, descr + +class EditorPage(object): + rmenu = None + rmenu_specs = [ + ("Set Breakpoint", "<>"), + ("Clear Breakpoint", "<>") + ] + + def __init__(self, parent_frame, editwin, title=None, **kwargs): + self.editwin = editwin + self.title = title + self.tab_initialized = False + kwargs.setdefault('width', idleConf.GetOption('main', 'EditorPage', + 'width')) + kwargs.setdefault('height', idleConf.GetOption('main', 'EditorPage', + 'height')) + + self.text = MultiCallCreator(Text)(parent_frame, **kwargs) + self.color = None # initialized in reset_colorizer + self.per = Percolator(self.text) + self.undo = self.editwin.UndoDelegator() + self.per.insertfilter(self.undo) + self.text.undo_block_start = self.undo.undo_block_start + self.text.undo_block_stop = self.undo.undo_block_stop + self.io = IOBinding.IOBinding(self) + + self.undo.set_saved_change_hook(self.saved_change_hook) + self.io.set_filename_change_hook(self.filename_change_hook) + self.reset_colorizer() + self._setup_bindings() + + def post_init(self, filename=None, update_window_title=False): + if filename: + if os.path.exists(filename) and not os.path.isdir(filename): + self.io.loadfile(filename) + else: + self.io.set_filename(filename) + self.saved_change_hook(update_window_title=update_window_title) + self.tab_initialized = True + + def close_tab(self, event=None): + """Close current tab, if no more tabs present, close the window.""" + if hasattr(self.editwin, 'interp'): + # this is a PyShell, don't ask to save + reply = 'yes' + else: + reply = str(self.maybesave()) + if reply != "cancel": + if self.io.filename: + self.editwin.update_recent_files_list(new_file=self.io.filename) + self.close() + self.editwin.text_notebook.remove_page(self.title) + self.editwin.top.event_generate('<>') + + return reply + + def close(self): + """Perform necessary cleanup for this page before closing it.""" + self.io.close() + self.io = None + + self.undo = None + + if self.color: + self.color.close(False) + self.color = None + + self.per.close() + self.per = None + self.text = None + + # XXX (1) mark where these functions are used + def saved_change_hook(self,update_window_title=False,update_tab_title=True): + short = self.editwin.short_title() + long = self.long_title() + + if short and long: + title = short + " - " + long + tabtitle = os.path.split(long)[-1] + elif short: + title = short + tabtitle = short + elif long: + title = long + tabtitle = os.path.split(long)[-1] + else: + title = tabtitle = "Untitled" + icon = short or long or title + if not self.get_saved(): + title = "*%s*" % title + tabtitle = "*%s*" % tabtitle + icon = "*%s" % icon + + if update_tab_title: + self.editwin.text_notebook.update_tabtitle(self, tabtitle) + if update_window_title or ( + update_window_title is None and self.tab_initialized): + self.editwin.top.wm_title(title) + self.editwin.top.wm_iconname(icon) + + def get_saved(self): + return self.undo.get_saved() + + def set_saved(self, flag): + self.undo.set_saved(flag) + + def filename_change_hook(self): + if self.editwin.flist: + self.editwin.flist.filename_changed_edit(self, self.editwin) + self.saved_change_hook(self.tab_initialized) + self.editwin.top.update_windowlist_registry(self.editwin) + self.reset_colorizer() + + def reset_undo(self): + self.undo.reset_undo() + + def reset_colorizer(self): + "Update the colour theme" + # Called from self.filename_change_hook and from configDialog.py + self.__rmcolorizer() + self.__addcolorizer() + theme = idleConf.GetOption('main','Theme','name') + normal_colors = idleConf.GetHighlight(theme, 'normal') + cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg') + select_colors = idleConf.GetHighlight(theme, 'hilite') + + self.text.config( + foreground=normal_colors['foreground'], + background=normal_colors['background'], + insertbackground=cursor_color, + selectforeground=select_colors['foreground'], + selectbackground=select_colors['background']) + + def short_title(self): + filename = self.io.filename + if filename: + filename = os.path.basename(filename) + # return unicode string to display non-ASCII chars correctly + return filename_to_unicode(filename) + + def long_title(self): + # return unicode string to display non-ASCII chars correctly + return filename_to_unicode(self.io.filename or "") + + def maybesave(self): + if self.io: + if not self.get_saved(): + if self.editwin.top.state()!= 'normal': + self.editiwn.top.deiconify() + self.editwin.top.lower() + self.editwin.top.lift() + return self.io.maybesave() + # XXX (1) end + + def center(self, mark="insert"): + # Used by EditorWindow.gotoline + text = self.text + top, bot = self._getwindowlines() + lineno = self._getlineno(mark) + height = bot - top + newtop = max(1, lineno - height//2) + text.yview(float(newtop)) + + def ispythonsource(self, filename): + if not filename or os.path.isdir(filename): + return True + base, ext = os.path.splitext(os.path.basename(filename)) + if os.path.normcase(ext) in (".py", ".pyw"): + return True + try: + f = open(filename) + line = f.readline() + f.close() + except IOError: + return False + return line.startswith('#!') and line.find('python') >= 0 + + def newline_and_indent_event(self, event): + # Used by EditorWindow.newline_and_indent_event which is used by PyShell + text = self.text + first, last = self.get_selection_indices() + text.undo_block_start() + try: + if first and last: + text.delete(first, last) + text.mark_set("insert", first) + line = text.get("insert linestart", "insert") + i, n = 0, len(line) + while i < n and line[i] in " \t": + i = i+1 + if i == n: + # the cursor is in or at leading indentation in a continuation + # line; just inject an empty line at the start + text.insert("insert linestart", '\n') + return "break" + indent = line[:i] + # strip whitespace before insert point unless it's in the prompt + i = 0 + last_line_of_prompt = sys.ps1.split('\n')[-1] + while line and line[-1] in " \t" and line != last_line_of_prompt: + line = line[:-1] + i = i+1 + if i: + text.delete("insert - %d chars" % i, "insert") + # strip whitespace after insert point + while text.get("insert") in " \t": + text.delete("insert") + # start new line + text.insert("insert", '\n') + + # adjust indentation for continuations and block + # open/close first need to find the last stmt + lno = index2line(text.index('insert')) + #print self.editwin.indentwidth, self.editwin.tabwidth + y = PyParse.Parser(self.editwin.indentwidth, self.editwin.tabwidth) + if not self.editwin.context_use_ps1: + for context in self.editwin.num_context_lines: + startat = max(lno - context, 1) + startatindex = `startat` + ".0" + rawtext = text.get(startatindex, "insert") + y.set_str(rawtext) + bod = y.find_good_parse_start( + self.editwin.context_use_ps1, + self.build_char_in_string_func(startatindex)) + if bod is not None or startat == 1: + break + y.set_lo(bod or 0) + else: + r = text.tag_prevrange("console", "insert") + if r: + startatindex = r[1] + else: + startatindex = "1.0" + rawtext = text.get(startatindex, "insert") + y.set_str(rawtext) + y.set_lo(0) + + c = y.get_continuation_type() + if c != PyParse.C_NONE: + # The current stmt hasn't ended yet. + if c == PyParse.C_STRING_FIRST_LINE: + # after the first line of a string; do not indent at all + pass + elif c == PyParse.C_STRING_NEXT_LINES: + # inside a string which started before this line; + # just mimic the current indent + text.insert("insert", indent) + elif c == PyParse.C_BRACKET: + # line up with the first (if any) element of the + # last open bracket structure; else indent one + # level beyond the indent of the line with the + # last open bracket + self.__reindent_to(y.compute_bracket_indent()) + elif c == PyParse.C_BACKSLASH: + # if more than one line in this stmt already, just + # mimic the current indent; else if initial line + # has a start on an assignment stmt, indent to + # beyond leftmost =; else to beyond first chunk of + # non-whitespace on initial line + if y.get_num_lines_in_stmt() > 1: + text.insert("insert", indent) + else: + self.__reindent_to(y.compute_backslash_indent()) + else: + assert 0, "bogus continuation type %r" % (c,) + return "break" + + # This line starts a brand new stmt; indent relative to + # indentation of initial line of closest preceding + # interesting stmt. + indent = y.get_base_indent_string() + text.insert("insert", indent) + if y.is_block_opener(): + self._smart_indent_event(event) + elif indent and y.is_block_closer(): + self._smart_backspace_event(event) + return "break" + finally: + text.see("insert") + text.undo_block_stop() + + # If a selection is defined in the text widget, return (start, + # end) as Tkinter text indices, otherwise return (None, None) + def get_selection_indices(self): + # Used by EditorWindow.get_selection_indices which is used by + # FormatParagraph + try: + first = self.text.index("sel.first") + last = self.text.index("sel.last") + return first, last + except TclError: + return None, None + + # Our editpage provides a __is_char_in_string function that works + # with a Tk text index, but PyParse only knows about offsets into + # a string. This builds a function for PyParse that accepts an + # offset. + def build_char_in_string_func(self, startindex): + # Used by EditorWindow.build_char_in_string_func which is used by + # HyperParser + def inner(offset, _startindex=startindex, + _icis=self.__is_char_in_string): + return _icis(_startindex + "+%dc" % offset) + return inner + + def _setup_bindings(self): + text = self.text + def bind_them(to_bind, prefix='_%s'): + for tb in to_bind: + prefix_size = tb.count('<') + method_name = tb[prefix_size:-prefix_size].replace('-', '_') + text.bind(tb, getattr(self, prefix % method_name.lower())) + + actions = ('<>', '<>', + '<>', '<>', '<>', + '<>', '<>', '<>', '<>', + '<>', '<>', '<>', + '<>') + events = ('<>', '<>', '<>', + '<>', '<>', '<>', + '<>', '<>', '<>', + '<>', '<>', '<>', + '<>', '<>', '<>', + '<>') + parent_actions = ('<>', '<>', '<>') + + bind_them(actions) + bind_them(events, prefix="_%s_event") + for action in parent_actions: + prefix_size = action.count('<') + method_name = action[prefix_size:-prefix_size].replace('-', '_') + text.bind(action, getattr(self.editwin, method_name)) + + text.bind('<>', self.close_tab) + text.bind('<>', self.newline_and_indent_event) + text.bind("<>", lambda event: "break") + text.bind("", self._move_at_edge_if_selection(0)) + text.bind("", self._move_at_edge_if_selection(1)) + text.bind("<3>", self._right_menu) + text.bind('<>', self.editwin.set_line_and_column) + text.event_add("<>", + "", "") + + if self.editwin.flist: + text.bind("<>", + utils.callback(self.editwin.new_callback, self)) + text.bind("<>", + self.editwin.flist.close_all_callback) + text.bind("<>", self._open_class_browser) + text.bind("<>", self._open_path_browser) + + if macosxSupport.runningAsOSXApp(): + # Command-W on editorwindows doesn't work without this. + text.bind('<>', self.editwin.close_event) + + def _help(self, event=None): + fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'help.txt') + textView.view_file(self.text, 'Help', fn) + + def _python_docs(self, event=None): + if sys.platform[:3] == 'win': + os.startfile(self.editwin.help_url) + else: + webbrowser.open(self.editwin.help_url) + return "break" + + def _about_idle(self, event=None): + aboutDialog.AboutDialog(self.text, 'About IDLE') + + def _open_class_browser(self, event=None): + filename = self.io.filename + if not filename: + tkMessageBox.showerror("No filename", + "This buffer has no associated filename", + master=self.text) + self.text.focus_set() + return None + head, tail = os.path.split(filename) + base, ext = os.path.splitext(tail) + ClassBrowser.ClassBrowser(self.editwin.flist, base, [head]) + + def _open_path_browser(self, event=None): + PathBrowser.PathBrowser(self.editwin.flist) + + def _open_config_dialog(self, event=None): + # When changing colors and saving it, it requires the attribute + # instance_dict making necessary to pass self.editwin.top as the + # parent + configDialog.ConfigDialog(self.editwin.top, 'Settings') + + def _open_module(self, event=None): + try: + name = self.text.get("sel.first", "sel.last") + except TclError: + name = "" + else: + name = name.strip() + + name = tkSimpleDialog.askstring("Module", + "Enter the name of a Python module\n" + "to search on sys.path and open:", + parent=self.text, initialvalue=name) + + if name: + name = name.strip() + if not name: + return + # XXX Ought to insert current file's directory in front of path + try: + (f, file, (suffix, mode, type)) = _find_module(name) + except (NameError, ImportError), msg: + tkMessageBox.showerror("Import error", str(msg), parent=self.text) + return + + if type != imp.PY_SOURCE: + tkMessageBox.showerror("Unsupported type", + "%s is not a source module" % name, parent=self.text) + return + if f: + f.close() + if self.editwin.flist: + if idleConf.GetOption('main', 'EditorWindow', 'file-in-tab', + default=1, type='bool'): + self.editwin.flist.open(file) + else: + self.io.loadfile(file) + + def _find_event(self, event): + SearchDialog.find(self.text) + return "break" + + def _find_again_event(self, event): + SearchDialog.find_again(self.text) + return "break" + + def _find_selection_event(self, event): + SearchDialog.find_selection(self.text) + return "break" + + def _find_in_files_event(self, event): + GrepDialog.grep(self.text, self.io, self.editwin.flist) + return "break" + + def _replace_event(self, event): + ReplaceDialog.replace(self.text) + return "break" + + def _center_insert_event(self, event): + self.center() + + def _getwindowlines(self): + text = self.text + top = self._getlineno("@0,0") + bot = self._getlineno("@0,65535") + if top == bot and text.winfo_height() == 1: + # Geometry manager hasn't run yet + height = int(text['height']) + bot = top + height - 1 + return top, bot + + def _getlineno(self, mark="insert"): + return int(float(self.text.index(mark))) + + def _goto_line_event(self, event): + text = self.text + lineno = tkSimpleDialog.askinteger("Goto", "Go to line number:", + parent=text) + + if lineno is None: + return "break" + + if lineno <= 0: + text.bell() + return "break" + + text.mark_set("insert", "%d.0" % lineno) + text.see("insert") + + def _cut(self, event): + self.text.event_generate("<>") + return "break" + + def _copy(self, event): + if not self.text.tag_ranges("sel"): + # There is no selection, so do nothing and maybe interrupt. + return + + self.text.event_generate("<>") + return "break" + + def _paste(self, event): + self.text.event_generate("<>") + self.text.see("insert") + return "break" + + def _select_all(self, event=None): + self.text.tag_add("sel", "1.0", "end-1c") + self.text.mark_set("insert", "1.0") + self.text.see("insert") + return "break" + + def _remove_selection(self, event=None): + self.text.tag_remove("sel", "1.0", "end") + self.text.see("insert") + + def _del_word_left(self, event): + self.text.event_generate('') + return "break" + + def _del_word_right(self, event): + self.text.event_generate('') + return "break" + + def _move_at_edge_if_selection(self, edge_index): + """Cursor move begins at start or end of selection + When a left/right cursor key is pressed create and return to Tkinter a + function which causes a cursor move from the associated edge of the + selection. + """ + text_index = self.text.index + text_mark_set = self.text.mark_set + edges_table = ("sel.first+1c", "sel.last-1c") + + def move_at_edge(event): + if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed + try: + text_index("sel.first") + text_mark_set("insert", edges_table[edge_index]) + except TclError: + pass + + return move_at_edge + + def _beginning_of_line(self, event): + if (event.state & 12) != 0 and event.keysym == "Home": + # state&1==shift, state&4==control, state&8==alt + return # ; fall back to class binding + + text = self.text + + if text.index("iomark") and \ + text.compare("iomark", "<=", "insert lineend") and \ + text.compare("insert linestart", "<=", "iomark"): + insertpt = int(text.index("iomark").split(".")[1]) + else: + line = text.get("insert linestart", "insert lineend") + for insertpt in xrange(len(line)): + if line[insertpt] not in (' ','\t'): + break + else: + insertpt=len(line) + + lineat = int(text.index("insert").split('.')[1]) + + if insertpt == lineat: + insertpt = 0 + + dest = "insert linestart+%sc" % str(insertpt) + + if (event.state & 1) == 0: + # shift not pressed + text.tag_remove("sel", "1.0", "end") + else: + if not text.index("sel.first"): + text.mark_set("anchor", "insert") + + first = text.index(dest) + last = text.index("anchor") + + if text.compare(first, ">", last): + first, last = last, first + + text.tag_remove("sel", "1.0", "end") + text.tag_add("sel", first, last) + + text.mark_set("insert", dest) + text.see("insert") + return "break" + + def _smart_backspace_event(self, event): + text = self.text + first, last = self.get_selection_indices() + if first and last: + text.delete(first, last) + text.mark_set("insert", first) + return "break" + # Delete whitespace left, until hitting a real char or closest + # preceding virtual tab stop. + chars = text.get("insert linestart", "insert") + if chars == '': + if text.compare("insert", ">", "1.0"): + # easy: delete preceding newline + text.delete("insert-1c") + else: + text.bell() # at start of buffer + return "break" + if chars[-1] not in " \t": + # easy: delete preceding real char + text.delete("insert-1c") + return "break" + # Ick. It may require *inserting* spaces if we back up over a + # tab character! This is written to be clear, not fast. + tabwidth = self.editwin.tabwidth + have = len(chars.expandtabs(tabwidth)) + assert have > 0 + want = ((have - 1) // + self.editwin.indentwidth) * self.editwin.indentwidth + # Debug prompt is multilined.... + last_line_of_prompt = sys.ps1.split('\n')[-1] + ncharsdeleted = 0 + while 1: + if chars == last_line_of_prompt: + break + chars = chars[:-1] + ncharsdeleted = ncharsdeleted + 1 + have = len(chars.expandtabs(tabwidth)) + if have <= want or chars[-1] not in " \t": + break + text.undo_block_start() + text.delete("insert-%dc" % ncharsdeleted, "insert") + if have < want: + text.insert("insert", ' ' * (want - have)) + text.undo_block_stop() + return "break" + + def _smart_indent_event(self, event): + # if intraline selection: + # delete it + # elif multiline selection: + # do indent-region + # else: + # indent one level + text = self.text + first, last = self.get_selection_indices() + text.undo_block_start() + try: + if first and last: + if index2line(first) != index2line(last): + return self._indent_region_event(event) + text.delete(first, last) + text.mark_set("insert", first) + prefix = text.get("insert linestart", "insert") + raw, effective = classifyws(prefix, self.editwin.tabwidth) + if raw == len(prefix): + # only whitespace to the left + self.__reindent_to(effective + self.editwin.indentwidth) + else: + # tab to the next 'stop' within or to right of line's text: + if self.editwin.usetabs: + pad = '\t' + else: + effective = len(prefix.expandtabs(self.editwin.tabwidth)) + n = self.editwin.indentwidth + pad = ' ' * (n - effective % n) + text.insert("insert", pad) + text.see("insert") + return "break" + finally: + text.undo_block_stop() + + def _indent_region_event(self, event): + head, tail, chars, lines = self.__get_region() + for pos in range(len(lines)): + line = lines[pos] + if line: + raw, effective = classifyws(line, self.editwin.tabwidth) + effective = effective + self.editwin.indentwidth + lines[pos] = self.__make_blanks(effective) + line[raw:] + self.__set_region(head, tail, chars, lines) + return "break" + + def _dedent_region_event(self, event): + head, tail, chars, lines = self.__get_region() + for pos in range(len(lines)): + line = lines[pos] + if line: + raw, effective = classifyws(line, self.editwin.tabwidth) + effective = max(effective - self.editwin.indentwidth, 0) + lines[pos] = self.__make_blanks(effective) + line[raw:] + self.__set_region(head, tail, chars, lines) + return "break" + + def _comment_region_event(self, event): + head, tail, chars, lines = self.__get_region() + for pos in range(len(lines) - 1): + line = lines[pos] + lines[pos] = '##' + line + self.__set_region(head, tail, chars, lines) + + def _uncomment_region_event(self, event): + head, tail, chars, lines = self.__get_region() + for pos in range(len(lines)): + line = lines[pos] + if not line: + continue + if line[:2] == '##': + line = line[2:] + elif line[:1] == '#': + line = line[1:] + lines[pos] = line + self.__set_region(head, tail, chars, lines) + + def _tabify_region_event(self, event): + head, tail, chars, lines = self.__get_region() + tabwidth = self.__asktabwidth() + for pos in range(len(lines)): + line = lines[pos] + if line: + raw, effective = classifyws(line, tabwidth) + ntabs, nspaces = divmod(effective, tabwidth) + lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:] + self.__set_region(head, tail, chars, lines) + + def _untabify_region_event(self, event): + head, tail, chars, lines = self.__get_region() + tabwidth = self.__asktabwidth() + for pos in range(len(lines)): + lines[pos] = lines[pos].expandtabs(tabwidth) + self.__set_region(head, tail, chars, lines) + + def _toggle_tabs_event(self, event): + if self.editwin.askyesno( + "Toggle tabs", + "Turn tabs " + ("on", "off")[self.editwin.usetabs] + + "?\nIndent width " + + ("will be", "remains at")[self.editwin.usetabs] + " 8." + + "\n Note: a tab is always 8 columns", + parent=self.text): + self.editwin.usetabs = not self.editwin.usetabs + # Try to prevent inconsistent indentation. + # User must change indent width manually after using tabs. + self.editwin.indentwidth = 8 + return "break" + + def _change_indentwidth_event(self, event): + new = self.editwin.askinteger( + "Indent width", + "New indent width (2-16)\n(Always use 8 when using tabs)", + parent=self.text, + initialvalue=self.editwin.indentwidth, + minvalue=2, + maxvalue=16) + if new and new != self.editwin.indentwidth and not self.editwin.usetabs: + self.editwin.indentwidth = new + return "break" + + def _right_menu(self, event): + self.text.tag_remove("sel", "1.0", "end") + self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) + + if not self.rmenu: + self.__make_rmenu() + + rmenu = self.rmenu + self.event = event + iswin = sys.platform[:3] == 'win' + if iswin: + self.text.config(cursor="arrow") + rmenu.tk_popup(event.x_root, event.y_root) + if iswin: + self.text.config(cursor="ibeam") + + def __make_rmenu(self): + rmenu = Menu(self.text, tearoff=False) + + for label, eventname in self.rmenu_specs: + def command(text=self.text, eventname=eventname): + text.event_generate(eventname) + rmenu.add_command(label=label, command=command) + + self.rmenu = rmenu + + def __rmcolorizer(self): + if not self.color: + return + self.color.removecolors() + self.per.removefilter(self.color) + self.color = None + + def __addcolorizer(self): + if self.color: + return + + if hasattr(self.editwin, 'ispythonsource'): # PyShell window + pychecker = self.editwin.ispythonsource + else: + pychecker = self.ispythonsource + if pychecker(self.io.filename): + self.color = self.editwin.ColorDelegator() + + # can add more colorizers here... + if self.color: + self.per.removefilter(self.undo) + self.per.insertfilter(self.color) + self.per.insertfilter(self.undo) + + def __asktabwidth(self): + return self.editwin.askinteger( + "Tab width", + "Columns per tab? (2-16)", + parent=self.text, + initialvalue=self.editwin.indentwidth, + minvalue=2, + maxvalue=16) or self.editwin.tabwidth + + # Make string that displays as n leading blanks. + def __make_blanks(self, n): + if self.editwin.usetabs: + ntabs, nspaces = divmod(n, self.editwin.tabwidth) + return '\t' * ntabs + ' ' * nspaces + else: + return ' ' * n + + def __get_region(self): + text = self.text + first, last = self.get_selection_indices() + if first and last: + head = text.index(first + " linestart") + tail = text.index(last + "-1c lineend +1c") + else: + head = text.index("insert linestart") + tail = text.index("insert lineend +1c") + chars = text.get(head, tail) + lines = chars.split("\n") + return head, tail, chars, lines + + def __set_region(self, head, tail, chars, lines): + text = self.text + newchars = "\n".join(lines) + if newchars == chars: + text.bell() + return + text.tag_remove("sel", "1.0", "end") + text.mark_set("insert", head) + text.undo_block_start() + text.delete(head, tail) + text.insert(head, newchars) + text.undo_block_stop() + text.tag_add("sel", head, "insert") + + # Delete from beginning of line to insert point, then reinsert + # column logical (meaning use tabs if appropriate) spaces. + def __reindent_to(self, column): + text = self.text + text.undo_block_start() + if text.compare("insert linestart", "!=", "insert"): + text.delete("insert linestart", "insert") + if column: + text.insert("insert", self.__make_blanks(column)) + text.undo_block_stop() + + # Tk implementations of "virtual text methods" -- each platform + # reusing IDLE's support code needs to define these for its GUI's + # flavor of widget. + + # Is character at text_index in a Python string? Return 0 for + # "guaranteed no", true for anything else. This info is expensive + # to compute ab initio, but is probably already known by the + # platform's colorizer. + def __is_char_in_string(self, text_index): + if self.color: + # Return true iff colorizer hasn't (re)gotten this far + # yet, or the character is tagged as being in a string + return self.text.tag_prevrange("TODO", text_index) or \ + "STRING" in self.text.tag_names(text_index) + else: + # The colorizer is missing: assume the worst + return 1 + + +import re + +class OutputPage(EditorPage): + rmenu_specs = [ + ("Go to file/line", "<>"), + ] + + file_line_pats = [ + r'file "([^"]*)", line (\d+)', + r'([^\s]+)\((\d+)\)', + r'([^\s]+):\s*(\d+):', + ] + + file_line_progs = None + + def __init__(self, parent_frame, editwin, title=None, **kwargs): + EditorPage.__init__(self, parent_frame, editwin, title, **kwargs) + self.text.bind("<>", self.goto_file_line) + + def ispythonsource(self, filename): + # No colorization needed + return 0 + + def short_title(self): + return "Output" + + def maybesave(self): + # Override base class method -- don't ask any questions + if self.get_saved(): + return "yes" + else: + return "no" + + def goto_file_line(self, event): + text = self.text + if self.file_line_progs is None: + l = [] + for pat in self.file_line_pats: + l.append(re.compile(pat, re.IGNORECASE)) + self.file_line_progs = l + + line = text.get("insert linestart", "insert lineend") + result = self._file_line_helper(line) + if not result: + # Try the previous line. This is handy e.g. in tracebacks, + # where you tend to right-click on the displayed source line + line = text.get("insert -1line linestart", "insert -1line ineend") + result = self._file_line_helper(line) + if not result: + tkMessageBox.showerror("No special line", + "The line you point at doesn't look like " + "a valid file name followed by a line number.", + master=text) + return + filename, lineno = result + self._open_file_at_line(filename, lineno) + + def _file_line_helper(self, line): + for prog in self.file_line_progs: + m = prog.search(line) + if m: + break + else: + return None + + filename, lineno = m.group(1, 2) + try: + f = open(filename, "r") + f.close() + except IOError: + return None + try: + return filename, int(lineno) + except TypeError: + return None + + def _open_file_at_line(self, filename, lineno): + edit = self.editwin.flist.open(filename) + edit.gotoline(lineno) Index: OutputWindow.py =================================================================== --- OutputWindow.py (revision 63995) +++ OutputWindow.py (revision 65573) @@ -1,11 +1,12 @@ -from Tkinter import * -from EditorWindow import EditorWindow import re import tkMessageBox + +import utils import IOBinding +from editorpage import OutputPage +from EditorWindow import EditorWindow class OutputWindow(EditorWindow): - """An editor window that can serve as an output file. Also the future base class for the Python shell window. @@ -13,28 +14,14 @@ """ def __init__(self, *args): - EditorWindow.__init__(self, *args) - self.text.bind("<>", self.goto_file_line) + EditorWindow.__init__(self, start_page=OutputPage, *args) + self.remove_tab_controls() - # Customize EditorWindow - - def ispythonsource(self, filename): - # No colorization needed - return 0 - - def short_title(self): - return "Output" - - def maybesave(self): - # Override base class method -- don't ask any questions - if self.get_saved(): - return "yes" - else: - return "no" - # Act as output file - def write(self, s, tags=(), mark="insert"): + def write(self, s, tags=(), mark="insert", text=None): + if text is None: + text = self.current_page.text # Tk assumes that byte strings are Latin-1; # we assume that they are in the locale's encoding if isinstance(s, str): @@ -43,115 +30,12 @@ except UnicodeError: # some other encoding; let Tcl deal with it pass - self.text.insert(mark, s, tags) - self.text.see(mark) - self.text.update() + text.insert(mark, s, tags) + text.see(mark) + text.update() def writelines(self, l): - map(self.write, l) + map(self.write, l, text=self.current_page.text) def flush(self): pass - - # Our own right-button menu - - rmenu_specs = [ - ("Go to file/line", "<>"), - ] - - file_line_pats = [ - r'file "([^"]*)", line (\d+)', - r'([^\s]+)\((\d+)\)', - r'([^\s]+):\s*(\d+):', - ] - - file_line_progs = None - - def goto_file_line(self, event=None): - if self.file_line_progs is None: - l = [] - for pat in self.file_line_pats: - l.append(re.compile(pat, re.IGNORECASE)) - self.file_line_progs = l - # x, y = self.event.x, self.event.y - # self.text.mark_set("insert", "@%d,%d" % (x, y)) - line = self.text.get("insert linestart", "insert lineend") - result = self._file_line_helper(line) - if not result: - # Try the previous line. This is handy e.g. in tracebacks, - # where you tend to right-click on the displayed source line - line = self.text.get("insert -1line linestart", - "insert -1line lineend") - result = self._file_line_helper(line) - if not result: - tkMessageBox.showerror( - "No special line", - "The line you point at doesn't look like " - "a valid file name followed by a line number.", - master=self.text) - return - filename, lineno = result - edit = self.flist.open(filename) - edit.gotoline(lineno) - - def _file_line_helper(self, line): - for prog in self.file_line_progs: - m = prog.search(line) - if m: - break - else: - return None - filename, lineno = m.group(1, 2) - try: - f = open(filename, "r") - f.close() - except IOError: - return None - try: - return filename, int(lineno) - except TypeError: - return None - -# These classes are currently not used but might come in handy - -class OnDemandOutputWindow: - - tagdefs = { - # XXX Should use IdlePrefs.ColorPrefs - "stdout": {"foreground": "blue"}, - "stderr": {"foreground": "#007700"}, - } - - def __init__(self, flist): - self.flist = flist - self.owin = None - - def write(self, s, tags, mark): - if not self.owin: - self.setup() - self.owin.write(s, tags, mark) - - def setup(self): - self.owin = owin = OutputWindow(self.flist) - text = owin.text - for tag, cnf in self.tagdefs.items(): - if cnf: - text.tag_configure(tag, **cnf) - text.tag_raise('sel') - self.write = self.owin.write - -#class PseudoFile: -# -# def __init__(self, owin, tags, mark="end"): -# self.owin = owin -# self.tags = tags -# self.mark = mark - -# def write(self, s): -# self.owin.write(s, self.tags, self.mark) - -# def writelines(self, l): -# map(self.write, l) - -# def flush(self): -# pass Index: aboutDialog.py =================================================================== --- aboutDialog.py (revision 63995) +++ aboutDialog.py (revision 65573) @@ -1,13 +1,18 @@ -"""About Dialog for IDLE +"""About Dialog for IDLE""" +import os +import sys +from Tkinter import Toplevel, Frame, Button, Label, TkVersion +from Tkconstants import LEFT, NSEW, SUNKEN, EW, W, BOTH, TOP, BOTTOM -""" - -from Tkinter import * -import os -import os.path +import idlever import textView -import idlever +from stylist import PoorManStyle +from configHandler import idleConf +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Frame, Button, Label, Style + class AboutDialog(Toplevel): """Modal about dialog for idle @@ -15,12 +20,15 @@ def __init__(self,parent,title): Toplevel.__init__(self, parent) self.configure(borderwidth=5) + self.geometry("+%d+%d" % (parent.winfo_rootx()+30, parent.winfo_rooty()+30)) self.bg = "#707070" self.fg = "#ffffff" + + self.SetupStyles() self.CreateWidgets() - self.resizable(height=FALSE, width=FALSE) + self.resizable(height=False, width=False) self.title(title) self.transient(parent) self.grab_set() @@ -31,40 +39,44 @@ self.bind('',self.Ok) #dismiss dialog self.wait_window() + def SetupStyles(self): + if TTK: + style = Style(self.master) + style.configure('Color.TLabel', foreground=self.fg, + background=self.bg) + style.configure('Color.TFrame', background=self.bg) + self.ttkstyle = style + self.style = lambda w, style: w.configure(style=style) + else: + self.style = PoorManStyle(self, + styles={'Color.TLabel': {'fg': self.fg, 'bg': self.bg}, + 'Color.TFrame': {'bg': self.bg}}).style_it + def CreateWidgets(self): 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) - labelTitle = Label(frameBg, text='IDLE', fg=self.fg, bg=self.bg, - font=('courier', 24, 'bold')) + frameButtons.pack(side=BOTTOM, pady=3) + frameMain.pack(side=TOP, expand=True, fill=BOTH) + self.buttonOk = Button(frameButtons, text='Close', command=self.Ok) + self.buttonOk.pack() + frameBg = Frame(frameMain) + frameBg.pack(expand=True, fill=BOTH) + labelTitle = Label(frameBg, text='IDLE', 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' - labelDesc = Label(frameBg, text=byline, justify=LEFT, - fg=self.fg, bg=self.bg) + labelDesc = Label(frameBg, text=byline, justify=LEFT) labelDesc.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5) labelEmail = Label(frameBg, text='email: idle-dev@python.org', - justify=LEFT, fg=self.fg, bg=self.bg) + justify=LEFT) labelEmail.grid(row=6, column=0, columnspan=2, sticky=W, padx=10, pady=0) labelWWW = Label(frameBg, text='www: http://www.python.org/idle/', - justify=LEFT, fg=self.fg, bg=self.bg) + justify=LEFT) 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) + fbg = Frame(frameBg, borderwidth=1, relief=SUNKEN, height=2) + fbg.grid(row=8, column=0, sticky=EW, columnspan=3, padx=5, pady=5) labelPythonVer = Label(frameBg, text='Python version: ' + \ - sys.version.split()[0], fg=self.fg, bg=self.bg) + sys.version.split()[0]) labelPythonVer.grid(row=9, column=0, sticky=W, padx=10, pady=0) # handle weird tk version num in windoze python >= 1.6 (?!?) tkVer = repr(TkVersion).split('.') @@ -72,44 +84,50 @@ if tkVer[len(tkVer)-1] == '': tkVer[len(tkVer)-1] = '0' tkVer = '.'.join(tkVer) - labelTkVer = Label(frameBg, text='Tk version: '+ - tkVer, fg=self.fg, bg=self.bg) + labelTkVer = Label(frameBg, text='Tk version: '+ tkVer) labelTkVer.grid(row=9, column=1, sticky=W, padx=2, pady=0) - py_button_f = Frame(frameBg, bg=self.bg) + py_button_f = Frame(frameBg) 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: ' + idlever.IDLE_VERSION, - fg=self.fg, bg=self.bg) + fbg2 = Frame(frameBg, borderwidth=1, relief=SUNKEN, height=2) + fbg2.grid(row=11, column=0, sticky=EW, columnspan=3, padx=5, pady=5) + idle_v = Label(frameBg, text='IDLE version: ' + idlever.IDLE_VERSION) idle_v.grid(row=12, column=0, sticky=W, padx=10, pady=0) - idle_button_f = Frame(frameBg, bg=self.bg) + idle_button_f = Frame(frameBg) 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) + s = self.style + s(frameButtons, 'RootColor.TFrame') + s(frameBg, 'Color.TFrame') + s(labelTitle, 'Color.TLabel') + s(labelDesc, 'Color.TLabel') + s(labelEmail, 'Color.TLabel') + s(labelWWW, 'Color.TLabel') + s(fbg, 'Color.TFrame') + s(labelPythonVer, 'Color.TLabel') + s(labelTkVer, 'Color.TLabel') + s(py_button_f, 'Color.TFrame') + s(fbg2, 'Color.TFrame') + s(idle_v, 'Color.TLabel') + s(idle_button_f, 'Color.TFrame') + def ShowLicense(self): self.display_printer_text('About - License', license) @@ -142,9 +160,9 @@ if __name__ == '__main__': # test the dialog + from Tkinter import Tk root = Tk() def run(): - import aboutDialog - aboutDialog.AboutDialog(root, 'About') + AboutDialog(root, 'About') Button(root, text='Dialog', command=run).pack() root.mainloop() Index: config-main.def =================================================================== --- config-main.def (revision 63995) +++ config-main.def (revision 65573) @@ -49,8 +49,14 @@ print-command-posix=lpr %s print-command-win=start /min notepad /p %s delete-exitfunc= 1 +use-ttk = 0 [EditorWindow] +width = 80 +height = 40 +file-in-tab = 1 + +[EditorPage] width= 80 height= 40 font= courier @@ -68,6 +74,7 @@ [Theme] default= 1 name= IDLE Classic +displaytheme = default [Keys] default= 1 Index: PathBrowser.py =================================================================== --- PathBrowser.py (revision 63995) +++ PathBrowser.py (revision 65573) @@ -15,25 +15,29 @@ self.top.wm_iconname("Path Browser") def rootnode(self): - return PathBrowserTreeItem() + return PathBrowserTreeItem(self.flist) class PathBrowserTreeItem(TreeItem): + def __init__(self, flist): + self.flist = flist + def GetText(self): return "sys.path" def GetSubList(self): sublist = [] for dir in sys.path: - item = DirBrowserTreeItem(dir) + item = DirBrowserTreeItem(dir, flist=self.flist) sublist.append(item) return sublist class DirBrowserTreeItem(TreeItem): - def __init__(self, dir, packages=[]): + def __init__(self, dir, packages=[], flist=None): self.dir = dir self.packages = packages + self.flist = flist def GetText(self): if not self.packages: @@ -55,10 +59,11 @@ packages.sort() sublist = [] for nn, name, file in packages: - item = DirBrowserTreeItem(file, self.packages + [name]) + item = DirBrowserTreeItem(file, self.packages + [name], self.flist) sublist.append(item) for nn, name in self.listmodules(names): - item = ModuleBrowserTreeItem(os.path.join(self.dir, name)) + item = ModuleBrowserTreeItem(os.path.join(self.dir, name), + self.flist) sublist.append(item) return sublist Index: IOBinding.py =================================================================== --- IOBinding.py (revision 63995) +++ IOBinding.py (revision 65573) @@ -6,17 +6,20 @@ # which will only understand the local convention. import os +import re +import sys import types -import sys import codecs import tempfile import tkFileDialog import tkMessageBox -import re -from Tkinter import * +from Tkinter import Toplevel, Entry, Frame, Button, Label +from Tkconstants import W, X, TOP, LEFT, BOTH from SimpleDialog import SimpleDialog from configHandler import idleConf +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Entry, Frame, Button, Label try: from codecs import BOM_UTF8 @@ -93,11 +96,11 @@ # For some reason, the text is not selectable anymore if the # widget is disabled. # l2['state'] = DISABLED - l2.pack(side=TOP, anchor = W, fill=X) + l2.pack(side=TOP, anchor=W, fill=X) l3 = Label(top, text="to your file\n" "Choose OK to save this file as %s\n" "Edit your general options to silence this warning" % enc) - l3.pack(side=TOP, anchor = W) + l3.pack(side=TOP, anchor=W) buttons = Frame(top) buttons.pack(side=TOP, fill=X) @@ -142,10 +145,14 @@ class IOBinding: + filename_change_hook = None + filename = None + dirname = None - def __init__(self, editwin): - self.editwin = editwin - self.text = editwin.text + def __init__(self, editpage): + self.editpage = editpage + self.editwin = editpage.editwin + self.text = editpage.text self.__id_open = self.text.bind("<>", self.open) self.__id_save = self.text.bind("<>", self.save) self.__id_saveas = self.text.bind("<>", @@ -163,27 +170,23 @@ self.text.unbind("<>", self.__id_savecopy) self.text.unbind("<>", self.__id_print) # Break cycles + self.text = None + self.editpage = None self.editwin = None - self.text = None self.filename_change_hook = None def get_saved(self): - return self.editwin.get_saved() + return self.editpage.get_saved() def set_saved(self, flag): - self.editwin.set_saved(flag) + self.editpage.set_saved(flag) def reset_undo(self): - self.editwin.reset_undo() + self.editpage.reset_undo() - filename_change_hook = None - def set_filename_change_hook(self, hook): self.filename_change_hook = hook - filename = None - dirname = None - def set_filename(self, filename): if filename and os.path.isdir(filename): self.filename = None @@ -200,21 +203,25 @@ if not editFile: filename = self.askopenfile() else: - filename=editFile + filename = editFile if filename: # If the current window has no filename and hasn't been - # modified, we replace its contents (no loss). Otherwise - # we open a new window. But we won't replace the - # shell window (which has an interp(reter) attribute), which - # gets set to "not modified" at every new prompt. + # modified, we replace its contents (no loss). Otherwise + # we open a new window, or maybe open in a tab. + # But we won't replace the shell window (which has an + # interp(reter) attribute), which gets set to "not modified" + # at every new prompt. try: interp = self.editwin.interp except AttributeError: interp = None + if not self.filename and self.get_saved() and not interp: self.editwin.flist.open(filename, self.loadfile) else: - self.editwin.flist.open(filename) + if idleConf.GetOption('main', 'EditorWindow', + 'file-in-tab', default=1, type='bool'): + self.editwin.flist.open(filename) else: self.text.focus_set() return "break" @@ -341,8 +348,8 @@ if self.writefile(self.filename): self.set_saved(1) try: - self.editwin.store_file_breaks() - except AttributeError: # may be a PyShell + self.editwin.store_file_breaks(self.editpage) + except AttributeError: # may not be a PyShell pass self.text.focus_set() return "break" @@ -354,7 +361,7 @@ self.set_filename(filename) self.set_saved(1) try: - self.editwin.store_file_breaks() + self.editwin.store_file_breaks(self.editpage) except AttributeError: pass self.text.focus_set() @@ -381,8 +388,7 @@ f.close() return True except IOError, msg: - tkMessageBox.showerror("I/O Error", str(msg), - master=self.text) + tkMessageBox.showerror("I/O Error", str(msg), master=self.text) return False def encode(self, chars): @@ -429,8 +435,7 @@ return BOM_UTF8 + chars.encode("utf-8") # Nothing was declared, and we had not determined an encoding # on loading. Recommend an encoding line. - config_encoding = idleConf.GetOption("main","EditorWindow", - "encoding") + config_encoding = idleConf.GetOption("main", "EditorPage", "encoding") if config_encoding == 'utf-8': # User has requested that we save files as UTF-8 return BOM_UTF8 + chars.encode("utf-8") @@ -563,6 +568,7 @@ self.editwin.update_recent_files_list(filename) def test(): + from Tkinter import Tk, Text root = Tk() class MyEditWin: def __init__(self, text): Index: WindowList.py =================================================================== --- WindowList.py (revision 63995) +++ WindowList.py (revision 65573) @@ -1,4 +1,4 @@ -from Tkinter import * +from Tkinter import Toplevel, TclError class WindowList: @@ -45,8 +45,8 @@ try: callback() except: - print "warning: callback failed in WindowList", \ - sys.exc_type, ":", sys.exc_value + print ("warning: callback failed in WindowList", + sys.exc_type, ":", sys.exc_value) registry = WindowList() Index: ScrolledList.py =================================================================== --- ScrolledList.py (revision 63995) +++ ScrolledList.py (revision 65573) @@ -1,5 +1,9 @@ -from Tkinter import * +from Tkinter import Frame, Menu, Listbox, Scrollbar +from idlelib.configHandler import idleConf +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Frame, Scrollbar + class ScrolledList: default = "(None)" @@ -120,6 +124,7 @@ def test(): + from Tkinter import Tk root = Tk() root.protocol("WM_DELETE_WINDOW", root.destroy) class MyScrolledList(ScrolledList): Index: ClassBrowser.py =================================================================== --- ClassBrowser.py (revision 63995) +++ ClassBrowser.py (revision 65573) @@ -14,7 +14,6 @@ import sys import pyclbr -import PyShell from WindowList import ListedToplevel from TreeWidget import TreeNode, TreeItem, ScrolledCanvas from configHandler import idleConf @@ -26,6 +25,7 @@ # XXX the code here is bogus! self.name = name self.file = os.path.join(path[0], self.name + ".py") + self.flist = None self.init(flist) def close(self, event=None): @@ -57,12 +57,13 @@ self.top.wm_iconname("Class Browser") def rootnode(self): - return ModuleBrowserTreeItem(self.file) + return ModuleBrowserTreeItem(self.file, self.flist) class ModuleBrowserTreeItem(TreeItem): - def __init__(self, file): + def __init__(self, file, flist): self.file = file + self.flist = flist def GetText(self): return os.path.basename(self.file) @@ -73,7 +74,8 @@ def GetSubList(self): sublist = [] for name in self.listclasses(): - item = ClassBrowserTreeItem(name, self.classes, self.file) + item = ClassBrowserTreeItem(name, self.classes, self.file, + self.flist) sublist.append(item) return sublist @@ -82,7 +84,7 @@ return if not os.path.exists(self.file): return - PyShell.flist.open(self.file) + self.flist.open(self.file) def IsExpandable(self): return os.path.normcase(self.file[-3:]) == ".py" @@ -122,10 +124,11 @@ class ClassBrowserTreeItem(TreeItem): - def __init__(self, name, classes, file): + def __init__(self, name, classes, file, flist): self.name = name self.classes = classes self.file = file + self.flist = flist try: self.cl = self.classes[self.name] except (IndexError, KeyError): @@ -163,7 +166,7 @@ def OnDoubleClick(self): if not os.path.exists(self.file): return - edit = PyShell.flist.open(self.file) + edit = self.flist.open(self.file) if hasattr(self.cl, 'lineno'): lineno = self.cl.lineno edit.gotoline(lineno) @@ -199,10 +202,11 @@ def OnDoubleClick(self): if not os.path.exists(self.file): return - edit = PyShell.flist.open(self.file) + edit = self.flist.open(self.file) edit.gotoline(self.cl.methods[self.name]) def main(): + import PyShell try: file = __file__ except NameError: Index: FileList.py =================================================================== --- FileList.py (revision 63995) +++ FileList.py (revision 65573) @@ -1,8 +1,18 @@ import os -from Tkinter import * import tkMessageBox +from configHandler import idleConf +def _canonize(filename): + if not os.path.isabs(filename): + try: + pwd = os.getcwd() + except os.error: + pass + else: + filename = os.path.join(pwd, filename) + return os.path.normpath(filename) + class FileList: from EditorWindow import EditorWindow # class variable, may be overridden @@ -16,7 +26,7 @@ def open(self, filename, action=None): assert filename - filename = self.canonize(filename) + filename = _canonize(filename) if os.path.isdir(filename): # This can happen when bad filename is passed on command line: tkMessageBox.showerror( @@ -29,9 +39,26 @@ edit = self.dict[key] edit.top.wakeup() return edit + if action: # Don't create window, perform 'action', e.g. open in same window - return action(filename) + return action(filename=filename) + + elif idleConf.GetOption('main', 'EditorWindow', 'file-in-tab', + default=1, type='bool'): + # check if there is some PyShellEditorWindow running to open this + # new tab + for entry in self.inversedict: + if hasattr(entry, 'set_breakpoint'): + # PyShellEditorWindow, good + entry.new_tab(filename=filename) + # select the last page created + entry.text_notebook.select(len(entry.text_notebook.pages)-1) + return entry + else: + # no PyShellEditorWindows, create one + return self.EditorWindow(self, filename, key) + else: return self.EditorWindow(self, filename, key) @@ -62,20 +89,20 @@ if not self.inversedict: self.root.quit() - def filename_changed_edit(self, edit): - edit.saved_change_hook() + def filename_changed_edit(self, page, editwin): + page.saved_change_hook(page.tab_initialized) try: - key = self.inversedict[edit] + key = self.inversedict[editwin] except KeyError: print "Don't know this EditorWindow object. (rename)" return - filename = edit.io.filename + filename = page.io.filename if not filename: if key: del self.dict[key] - self.inversedict[edit] = None + self.inversedict[editwin] = None return - filename = self.canonize(filename) + filename = _canonize(filename) newkey = os.path.normcase(filename) if newkey == key: return @@ -86,28 +113,19 @@ "Name Conflict", "You now have multiple edit windows open for %r" % (filename,), master=self.root) - self.dict[newkey] = edit - self.inversedict[edit] = newkey + self.dict[newkey] = editwin + self.inversedict[editwin] = newkey if key: try: del self.dict[key] except KeyError: pass - def canonize(self, filename): - if not os.path.isabs(filename): - try: - pwd = os.getcwd() - except os.error: - pass - else: - filename = os.path.join(pwd, filename) - return os.path.normpath(filename) - def _test(): + import sys + from Tkinter import Tk from EditorWindow import fixwordbreaks - import sys root = Tk() fixwordbreaks(root) root.withdraw() Index: CallTips.py =================================================================== --- CallTips.py (revision 63995) +++ CallTips.py (revision 65573) @@ -22,12 +22,12 @@ ]) ] - def __init__(self, editwin=None): - if editwin is None: # subprocess and test - self.editwin = None + def __init__(self, editpage=None): + if editpage is None: # subprocess and test + self.editpage = None return - self.editwin = editwin - self.text = editwin.text + self.editpage = editpage + self.text = editpage.text self.calltip = None self._make_calltip_window = self._make_tk_calltip_window @@ -66,7 +66,7 @@ def open_calltip(self, evalfuncs): self._remove_calltip_window() - hp = HyperParser(self.editwin, "insert") + hp = HyperParser(self.editpage, "insert") sur_paren = hp.get_surrounding_brackets('(') if not sur_paren: return @@ -95,7 +95,7 @@ """ try: - rpcclt = self.editwin.flist.pyshell.interp.rpcclt + rpcclt = self.editpage.editwin.flist.pyshell.interp.rpcclt except: rpcclt = None if rpcclt: Index: CodeContext.py =================================================================== --- CodeContext.py (revision 63995) +++ CodeContext.py (revision 65573) @@ -23,7 +23,7 @@ getspacesfirstword =\ lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups() -class CodeContext: +class CodeContext(object): menudefs = [('options', [('!Code Conte_xt', '<>')])] context_depth = idleConf.GetOption("extensions", "CodeContext", "numlines", type="int", default=3) @@ -31,10 +31,10 @@ "bgcolor", type="str", default="LightGray") fgcolor = idleConf.GetOption("extensions", "CodeContext", "fgcolor", type="str", default="Black") - def __init__(self, editwin): - self.editwin = editwin - self.text = editwin.text - self.textfont = self.text["font"] + + def __init__(self, editpage): + self.editpage = editpage + self.editwin = editpage.editwin self.label = None # self.info is a list of (line number, indent level, line text, block # keyword) tuples providing the block structure associated with @@ -43,14 +43,11 @@ # starts the toplevel 'block' of the module. self.info = [(0, -1, "", False)] self.topvisible = 1 - visible = idleConf.GetOption("extensions", "CodeContext", - "visible", type="bool", default=False) + visible = idleConf.GetOption("extensions", "CodeContext", "visible", + type="bool", default=False) if visible: self.toggle_code_context_event() self.editwin.setvar('<>', True) - # Start two update cycles, one for context lines, one for font changes. - self.text.after(UPDATEINTERVAL, self.timer_event) - self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) def toggle_code_context_event(self, event=None): if not self.label: @@ -59,33 +56,27 @@ # # All values are passed through int(str()), since some # values may be pixel objects, which can't simply be added to ints. - widgets = self.editwin.text, self.editwin.text_frame - # Calculate the required vertical padding - padx = 0 - for widget in widgets: - padx += int(str( widget.pack_info()['padx'] )) - padx += int(str( widget.cget('padx') )) - # Calculate the required border width - border = 0 - for widget in widgets: - border += int(str( widget.cget('border') )) + + # Calculate the required horizontal padding + padx = int(str(self.editwin.text_notebook.pack_info()['padx'])) + self.label = Tkinter.Label(self.editwin.top, text="\n" * (self.context_depth - 1), anchor=W, justify=LEFT, font=self.textfont, bg=self.bgcolor, fg=self.fgcolor, width=1, #don't request more than we get - padx=padx, border=border, + padx=padx, relief=SUNKEN) - # Pack the label widget before and above the text_frame widget, - # thus ensuring that it will appear directly above text_frame + # Pack the label widget before and above the text_notebook widget, + # thus ensuring that it will appear directly above text_notebook self.label.pack(side=TOP, fill=X, expand=False, - before=self.editwin.text_frame) + before=self.editwin.text_notebook) else: self.label.destroy() self.label = None idleConf.SetOption("extensions", "CodeContext", "visible", - str(self.label is not None)) + str(self.label is not None)) idleConf.SaveUserCfgFiles() def get_line_info(self, linenum): @@ -132,15 +123,12 @@ return lines, lastindent def update_code_context(self): - """Update context information and lines visible in the context pane. - - """ + """Update context information and lines visible in the context pane.""" new_topvisible = int(self.text.index("@0,0").split('.')[0]) if self.topvisible == new_topvisible: # haven't scrolled return if self.topvisible < new_topvisible: # scroll down - lines, lastindent = self.get_context(new_topvisible, - self.topvisible) + lines, lastindent = self.get_context(new_topvisible, self.topvisible) # retain only context info applicable to the region # between topvisible and new_topvisible: while self.info[-1][1] >= lastindent: @@ -153,8 +141,7 @@ stopindent = self.info[-1][1] del self.info[-1] lines, lastindent = self.get_context(new_topvisible, - self.info[-1][0]+1, - stopindent) + self.info[-1][0] + 1, stopindent) self.info.extend(lines) self.topvisible = new_topvisible # empty lines in context pane: @@ -174,3 +161,18 @@ self.textfont = newtextfont self.label["font"] = self.textfont self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) + + # Private methods + + def _get_editpage(self): + return self._editpage + + def _set_editpage(self, page): + self._editpage = page + self.text = page.text + self.textfont = self.text["font"] + # Start two update cycles, one for context lines, one for font changes. + self.text.after(UPDATEINTERVAL, self.timer_event) + self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) + + editpage = property(_get_editpage, _set_editpage) Index: textView.py =================================================================== --- textView.py (revision 63995) +++ textView.py (revision 65573) @@ -1,10 +1,16 @@ -"""Simple text browser for IDLE +"""Simple text browser for IDLE""" -""" - -from Tkinter import * import tkMessageBox +from Tkinter import Toplevel, Frame, Button, Scrollbar, Text +from Tkconstants import DISABLED, SUNKEN, VERTICAL, WORD, RIGHT, Y, TOP, \ + LEFT, BOTH, BOTTOM +from configHandler import idleConf + +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Frame, Button, Scrollbar + class TextViewer(Toplevel): """A simple text viewer dialog for IDLE @@ -40,19 +46,22 @@ frameText = Frame(self, relief=SUNKEN, height=700) frameButtons = Frame(self) self.buttonOk = Button(frameButtons, text='Close', - command=self.Ok, takefocus=FALSE) + command=self.Ok, takefocus=False) self.scrollbarView = Scrollbar(frameText, orient=VERTICAL, - takefocus=FALSE, highlightthickness=0) - self.textView = Text(frameText, wrap=WORD, highlightthickness=0, - fg=self.fg, bg=self.bg) + takefocus=False) + self.textView = Text(frameText, wrap=WORD, fg=self.fg, bg=self.bg, + highlightthickness=0) self.scrollbarView.config(command=self.textView.yview) self.textView.config(yscrollcommand=self.scrollbarView.set) self.buttonOk.pack() self.scrollbarView.pack(side=RIGHT,fill=Y) - self.textView.pack(side=LEFT,expand=TRUE,fill=BOTH) - frameButtons.pack(side=BOTTOM,fill=X) - frameText.pack(side=TOP,expand=TRUE,fill=BOTH) + self.textView.pack(side=LEFT,expand=True,fill=BOTH) + frameButtons.pack(side=BOTTOM) + frameText.pack(side=TOP,expand=True,fill=BOTH) + if TTK: + frameButtons['style'] = 'RootColor.TFrame' + def Ok(self, event=None): self.destroy() @@ -68,7 +77,6 @@ else: textFile = open(filename, 'r') except IOError: - import tkMessageBox tkMessageBox.showerror(title='File Load Error', message='Unable to load file %r .' % filename, parent=parent) @@ -77,6 +85,7 @@ if __name__ == '__main__': + from Tkinter import Tk #test the dialog root=Tk() root.title('textView test') Index: SearchDialogBase.py =================================================================== --- SearchDialogBase.py (revision 63995) +++ SearchDialogBase.py (revision 65573) @@ -1,35 +1,42 @@ -from Tkinter import * +from Tkinter import Toplevel, Frame, Label, Entry, Button, Checkbutton, \ + Radiobutton +from configHandler import idleConf + +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Frame, Label, Entry, Button, Checkbutton, Radiobutton + class SearchDialogBase: title = "Search Dialog" icon = "Search" needwrapbutton = 1 + bottom_btns = None def __init__(self, root, engine): self.root = root self.engine = engine - self.top = None + self.ttop = None def open(self, text, searchphrase=None): self.text = text - if not self.top: + if not self.ttop: self.create_widgets() else: - self.top.deiconify() - self.top.tkraise() + self.ttop.deiconify() + self.ttop.tkraise() if searchphrase: - self.ent.delete(0,"end") - self.ent.insert("end",searchphrase) + self.ent.delete(0, "end") + self.ent.insert("end", searchphrase) self.ent.focus_set() self.ent.selection_range(0, "end") self.ent.icursor(0) - self.top.grab_set() + self.ttop.grab_set() def close(self, event=None): - if self.top: - self.top.grab_release() - self.top.withdraw() + if self.ttop: + self.ttop.grab_release() + self.ttop.withdraw() def create_widgets(self): top = Toplevel(self.root) @@ -38,103 +45,96 @@ top.protocol("WM_DELETE_WINDOW", self.close) top.wm_title(self.title) top.wm_iconname(self.icon) - self.top = top + top.resizable(height=False, width=False) + self.ttop = top + self.top = Frame(top) self.row = 0 - self.top.grid_columnconfigure(0, pad=2, weight=0) - self.top.grid_columnconfigure(1, pad=2, minsize=100, weight=100) + self.top.grid(sticky='news') self.create_entries() self.create_option_buttons() self.create_other_buttons() - return self.create_command_buttons() + self.create_command_buttons() + def make_entry(self, label, var): l = Label(self.top, text=label) - l.grid(row=self.row, column=0, sticky="nw") + l.grid(row=self.row, column=0, sticky="ne", padx=6, pady=6) e = Entry(self.top, textvariable=var, exportselection=0) - e.grid(row=self.row, column=1, sticky="nwe") + e.grid(row=self.row, column=1, sticky="nwe", padx=6, pady=6) self.row = self.row + 1 return e def make_frame(self,labeltext=None): if labeltext: l = Label(self.top, text=labeltext) - l.grid(row=self.row, column=0, sticky="nw") + l.grid(row=self.row, column=0, sticky="ne", padx=6, pady=6) f = Frame(self.top) - f.grid(row=self.row, column=1, columnspan=1, sticky="nwe") + f.grid(row=self.row, column=1, columnspan=1, sticky="nwe", + padx=6, pady=6 if labeltext else 0) self.row = self.row + 1 return f - def make_button(self, label, command, isdef=0): - b = Button(self.buttonframe, - text=label, command=command, - default=isdef and "active" or "normal") - cols,rows=self.buttonframe.grid_size() - b.grid(pady=1,row=rows,column=0,sticky="ew") - self.buttonframe.grid(rowspan=rows+1) - return b - def create_entries(self): - self.ent = self.make_entry("Find:", self.engine.patvar) + self.ent = self.make_entry("Find", self.engine.patvar) def create_option_buttons(self): f = self.make_frame("Options") - btn = Checkbutton(f, anchor="w", - variable=self.engine.revar, - text="Regular expression") + btn = Checkbutton(f, variable=self.engine.revar, + text="Regular expression") btn.pack(side="left", fill="both") if self.engine.isre(): - btn.select() + btn.invoke() - btn = Checkbutton(f, anchor="w", - variable=self.engine.casevar, - text="Match case") + btn = Checkbutton(f, variable=self.engine.casevar, text="Match case") btn.pack(side="left", fill="both") if self.engine.iscase(): - btn.select() + btn.invoke() - btn = Checkbutton(f, anchor="w", - variable=self.engine.wordvar, - text="Whole word") + btn = Checkbutton(f, variable=self.engine.wordvar, text="Whole word") btn.pack(side="left", fill="both") if self.engine.isword(): - btn.select() + btn.invoke() if self.needwrapbutton: - btn = Checkbutton(f, anchor="w", - variable=self.engine.wrapvar, - text="Wrap around") + btn = Checkbutton(f, variable=self.engine.wrapvar, + text="Wrap around") btn.pack(side="left", fill="both") if self.engine.iswrap(): - btn.select() + btn.invoke() def create_other_buttons(self): f = self.make_frame("Direction") - #lbl = Label(f, text="Direction: ") - #lbl.pack(side="left") - - btn = Radiobutton(f, anchor="w", - variable=self.engine.backvar, value=1, - text="Up") - btn.pack(side="left", fill="both") + btn = Radiobutton(f, variable=self.engine.backvar, value=1, text="Up") + btn.pack(side="left") if self.engine.isback(): - btn.select() + btn.invoke() - btn = Radiobutton(f, anchor="w", - variable=self.engine.backvar, value=0, - text="Down") - btn.pack(side="left", fill="both") + btn = Radiobutton(f, variable=self.engine.backvar, value=0, text="Down") + btn.pack(side="left") if not self.engine.isback(): - btn.select() + btn.invoke() def create_command_buttons(self): - # - # place button frame on the right - f = self.buttonframe = Frame(self.top) - f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2) + self.bottom_btns = self.bottom_btns or [] + f = Frame(self.top) + f.grid(row=self.row, column=0, columnspan=len(self.bottom_btns) + 1, + pady=6) - b = self.make_button("close", self.close) - b.lower() + column = 0 + b = Button(f, text="Close", command=self.close) + b.grid(row=self.row, column=column, padx=6, pady=6) + column += 1 + + btns = {} + for tbtn in self.bottom_btns: + opts = {'text': tbtn[0], 'command': getattr(self, tbtn[1])} + if len(tbtn) == 3: + opts['default'] = tbtn[2] and 'active' or 'normal' + + btns[opts['text']] = Button(f, **opts).grid(row=self.row, padx=6, + pady=6, column=column) + column += 1 Index: CallTipWindow.py =================================================================== --- CallTipWindow.py (revision 63995) +++ CallTipWindow.py (revision 65573) @@ -4,8 +4,14 @@ Used by the CallTips IDLE extension. """ -from Tkinter import * +from Tkinter import Toplevel, Label, TclError +from Tkconstants import SOLID, LEFT +from configHandler import idleConf + +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Label + HIDE_VIRTUAL_EVENT_NAME = "<>" HIDE_SEQUENCES = ("", "") CHECKHIDE_VIRTUAL_EVENT_NAME = "<>" @@ -142,6 +148,8 @@ # class container: # Conceptually an editor_window def __init__(self): + from Tkinter import Tk, Text + from Tkconstants import BOTH root = Tk() text = self.text = Text(root) text.pack(side=LEFT, fill=BOTH, expand=1) @@ -163,6 +171,8 @@ def calltip_hide(self, event): self.calltip.hidetip() +# XXX Bugged test + def main(): # Test code c=container() Index: SearchDialog.py =================================================================== --- SearchDialog.py (revision 63995) +++ SearchDialog.py (revision 65573) @@ -1,8 +1,8 @@ -from Tkinter import * +from Tkinter import TclError + import SearchEngine from SearchDialogBase import SearchDialogBase - def _setup(text): root = text._root() engine = SearchEngine.get(root) @@ -12,7 +12,7 @@ def find(text): pat = text.get("sel.first", "sel.last") - return _setup(text).open(text,pat) + return _setup(text).open(text, pat) def find_again(text): return _setup(text).find_again(text) @@ -21,10 +21,10 @@ return _setup(text).find_selection(text) class SearchDialog(SearchDialogBase): + bottom_btns = [("Find", 'default_command', 1)] def create_widgets(self): - f = SearchDialogBase.create_widgets(self) - self.make_button("Find", self.default_command, 1) + SearchDialogBase.create_widgets(self) def default_command(self, event=None): if not self.engine.getprog(): Index: idlever.py =================================================================== --- idlever.py (revision 63995) +++ idlever.py (revision 65573) @@ -1 +1 @@ -IDLE_VERSION = "2.6a3" +IDLE_VERSION = "2.6a3-gpolo" Index: tabbedpages_old.py =================================================================== --- tabbedpages_old.py (revision 0) +++ tabbedpages_old.py (revision 65573) @@ -0,0 +1,547 @@ +"""An implementation of tabbed pages using only standard Tkinter. + +Originally developed for use in IDLE. Based on tabpage.py. + +Classes exported: +TabbedPageSet -- A Tkinter implementation of a tabbed-page widget. +TabSet -- A widget containing tabs (buttons) in one or more rows. + +""" +from Tkinter import Frame, Radiobutton +from Tkconstants import BOTH, TOP, X, RAISED, NSEW, FLAT, LEFT + +from tabbedpages import InvalidNameError, AlreadyExistsError + + +class TabSet(Frame): + """A widget containing tabs (buttons) in one or more rows. + + Only one tab may be selected at a time. + + """ + def __init__(self, page_set, select_command, + tabs=None, n_rows=1, max_tabs_per_row=5, + expand_tabs=False, **kw): + """Constructor arguments: + + select_command -- A callable which will be called when a tab is + selected. It is called with the name of the selected tab as an + argument. + + tabs -- A list of strings, the names of the tabs. Should be specified in + the desired tab order. The first tab will be the default and first + active tab. If tabs is None or empty, the TabSet will be initialized + empty. + + n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is + None, then the number of rows will be decided by TabSet. See + _arrange_tabs() for details. + + max_tabs_per_row -- Used for deciding how many rows of tabs are needed, + when the number of rows is not constant. See _arrange_tabs() for + details. + + """ + Frame.__init__(self, page_set, **kw) + self.select_command = select_command + self.n_rows = n_rows + self.max_tabs_per_row = max_tabs_per_row + self.expand_tabs = expand_tabs + self.page_set = page_set + + self._tabs = {} + self._tab2row = {} + if tabs: + self._tab_names = list(tabs) + else: + self._tab_names = [] + self._selected_tab = None + self._tab_rows = [] + + self.padding_frame = Frame(self, height=2, + borderwidth=0, relief=FLAT, + background=self.cget('background')) + self.padding_frame.pack(side=TOP, fill=X, expand=False) + + self._arrange_tabs() + + def add_tab(self, tab_name): + """Add a new tab with the name given in tab_name.""" + if not tab_name: + raise InvalidNameError("Invalid Tab name: '%s'" % tab_name) + if tab_name in self._tab_names: + raise AlreadyExistsError("Tab named '%s' already exists" %tab_name) + + self._tab_names.append(tab_name) + self._arrange_tabs() + + def remove_tab(self, tab_name): + """Remove the tab named """ + if not tab_name in self._tab_names: + raise KeyError("No such Tab: '%s" % tab_name) + + self._tab_names.remove(tab_name) + self._arrange_tabs() + + def set_selected_tab(self, tab_name): + """Show the tab named as the selected one""" + if tab_name == self._selected_tab: + return + if tab_name is not None and tab_name not in self._tabs: + raise KeyError("No such Tab: '%s" % tab_name) + + # deselect the current selected tab + if self._selected_tab is not None: + self._tabs[self._selected_tab].set_normal() + self._selected_tab = None + + if tab_name is not None: + # activate the tab named tab_name + self._selected_tab = tab_name + tab = self._tabs[tab_name] + tab.set_selected() + # move the tab row with the selected tab to the bottom + tab_row = self._tab2row[tab] + tab_row.pack_forget() + tab_row.pack(side=TOP, fill=X, expand=0) + + def _add_tab_row(self, tab_names, expand_tabs): + if not tab_names: + return + + tab_row = Frame(self) + tab_row.pack(side=TOP, fill=X, expand=0) + self._tab_rows.append(tab_row) + + for tab_name in tab_names: + tab = TabSet.TabButton(tab_name, self.select_command, + tab_row, self) + if expand_tabs: + tab.pack(side=LEFT, fill=X, expand=True) + else: + tab.pack(side=LEFT) + self._tabs[tab_name] = tab + self._tab2row[tab] = tab_row + + # tab is the last one created in the above loop + tab.is_last_in_row = True + + def _reset_tab_rows(self): + while self._tab_rows: + tab_row = self._tab_rows.pop() + tab_row.destroy() + self._tab2row = {} + + def _arrange_tabs(self): + """ + Arrange the tabs in rows, in the order in which they were added. + + If n_rows >= 1, this will be the number of rows used. Otherwise the + number of rows will be calculated according to the number of tabs and + max_tabs_per_row. In this case, the number of rows may change when + adding/removing tabs. + + """ + # remove all tabs and rows + while self._tabs: + self._tabs.popitem()[1].destroy() + self._reset_tab_rows() + + if not self._tab_names: + return + + if self.n_rows is not None and self.n_rows > 0: + n_rows = self.n_rows + else: + # calculate the required number of rows + n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1 + + # not expanding the tabs with more than one row is very ugly + expand_tabs = self.expand_tabs or n_rows > 1 + i = 0 # index in self._tab_names + for row_index in range(n_rows): + # calculate required number of tabs in this row + n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1 + tab_names = self._tab_names[i:i + n_tabs] + i += n_tabs + self._add_tab_row(tab_names, expand_tabs) + + # re-select selected tab so it is properly displayed + selected = self._selected_tab + self.set_selected_tab(None) + if selected in self._tab_names: + self.set_selected_tab(selected) + + class TabButton(Frame): + """A simple tab-like widget.""" + + bw = 2 # borderwidth + + def __init__(self, name, select_command, tab_row, tab_set): + """Constructor arguments: + + name -- The tab's name, which will appear in its button. + + select_command -- The command to be called upon selection of the + tab. It is called with the tab's name as an argument. + + """ + Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED) + + self.name = name + self.select_command = select_command + self.tab_set = tab_set + self.is_last_in_row = False + + self.button = Radiobutton( + self, text=name, command=self._select_event, + padx=5, pady=1, takefocus=False, indicatoron=False, + highlightthickness=0, selectcolor='', borderwidth=0) + self.button.pack(side=LEFT, fill=X, expand=True) + + self._init_masks() + self.set_normal() + + def _select_event(self, *args): + """Event handler for tab selection. + + With TabbedPageSet, this calls TabbedPageSet.change_page, so that + selecting a tab changes the page. + + Note that this does -not- call set_selected -- it will be called by + TabSet.set_selected_tab, which should be called when whatever the + tabs are related to changes. + + """ + self.select_command(self.name) + return + + def set_selected(self): + """Assume selected look""" + self._place_masks(selected=True) + + def set_normal(self): + """Assume normal look""" + self._place_masks(selected=False) + + def _init_masks(self): + page_set = self.tab_set.page_set + background = page_set.pages_frame.cget('background') + # mask replaces the middle of the border with the background color + self.mask = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + # mskl replaces the bottom-left corner of the border with a normal + # left border + self.mskl = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + self.mskl.ml = Frame(self.mskl, borderwidth=self.bw, + relief=RAISED) + self.mskl.ml.place(x=0, y=-self.bw, + width=2*self.bw, height=self.bw*4) + # mskr replaces the bottom-right corner of the border with a normal + # right border + self.mskr = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + self.mskr.mr = Frame(self.mskr, borderwidth=self.bw, + relief=RAISED) + + def _place_masks(self, selected=False): + height = self.bw + if selected: + height += self.bw + + self.mask.place(in_=self, + relx=0.0, x=0, + rely=1.0, y=0, + relwidth=1.0, width=0, + relheight=0.0, height=height) + + self.mskl.place(in_=self, + relx=0.0, x=-self.bw, + rely=1.0, y=0, + relwidth=0.0, width=self.bw, + relheight=0.0, height=height) + + page_set = self.tab_set.page_set + if selected and ((not self.is_last_in_row) or + (self.winfo_rootx() + self.winfo_width() < + page_set.winfo_rootx() + page_set.winfo_width()) + ): + # for a selected tab, if its rightmost edge isn't on the + # rightmost edge of the page set, the right mask should be one + # borderwidth shorter (vertically) + height -= self.bw + + self.mskr.place(in_=self, + relx=1.0, x=0, + rely=1.0, y=0, + relwidth=0.0, width=self.bw, + relheight=0.0, height=height) + + self.mskr.mr.place(x=-self.bw, y=-self.bw, + width=2*self.bw, height=height + self.bw*2) + + # finally, lower the tab set so that all of the frames we just + # placed hide it + self.tab_set.lower() + +class TabbedPageSet(Frame): + """A Tkinter tabbed-pane widget. + + Constains set of 'pages' (or 'panes') with tabs above for selecting which + page is displayed. Only one page will be displayed at a time. + + Pages may be accessed through the 'pages' attribute, which is a dictionary + of pages, using the name given as the key. A page is an instance of a + subclass of Tk's Frame widget. + + The page widgets will be created (and destroyed when required) by the + TabbedPageSet. Do not call the page's pack/place/grid/destroy methods. + + Pages may be added or removed at any time using the add_page() and + remove_page() methods. + + """ + class Page(object): + """Abstract base class for TabbedPageSet's pages. + + Subclasses must override the _show() and _hide() methods. + + """ + uses_grid = False + + def __init__(self, page_set): + self.frame = Frame(page_set, borderwidth=2, relief=RAISED) + + def _show(self): + raise NotImplementedError + + def _hide(self): + raise NotImplementedError + + class PageRemove(Page): + """Page class using the grid placement manager's "remove" mechanism.""" + uses_grid = True + + def _show(self): + self.frame.grid(row=0, column=0, sticky=NSEW) + + def _hide(self): + self.frame.grid_remove() + + class PageLift(Page): + """Page class using the grid placement manager's "lift" mechanism.""" + uses_grid = True + + def __init__(self, page_set): + super(TabbedPageSet.PageLift, self).__init__(page_set) + self.frame.grid(row=0, column=0, sticky=NSEW) + self.frame.lower() + + def _show(self): + self.frame.lift() + + def _hide(self): + self.frame.lower() + + class PagePackForget(Page): + """Page class using the pack placement manager's "forget" mechanism.""" + def _show(self): + self.frame.pack(fill=BOTH, expand=True) + + def _hide(self): + self.frame.pack_forget() + + def __init__(self, parent, page_names=None, page_class=PageLift, + n_rows=1, max_tabs_per_row=5, expand_tabs=False, + **kw): + """Constructor arguments: + + page_names -- A list of strings, each will be the dictionary key to a + page's widget, and the name displayed on the page's tab. Should be + specified in the desired page order. The first page will be the default + and first active page. If page_names is None or empty, the + TabbedPageSet will be initialized empty. + + n_rows, max_tabs_per_row -- Parameters for the TabSet which will + manage the tabs. See TabSet's docs for details. + + page_class -- Pages can be shown/hidden using three mechanisms: + + * PageLift - All pages will be rendered one on top of the other. When + a page is selected, it will be brought to the top, thus hiding all + other pages. Using this method, the TabbedPageSet will not be resized + when pages are switched. (It may still be resized when pages are + added/removed.) + + * PageRemove - When a page is selected, the currently showing page is + hidden, and the new page shown in its place. Using this method, the + TabbedPageSet may resize when pages are changed. + + * PagePackForget - This mechanism uses the pack placement manager. + When a page is shown it is packed, and when it is hidden it is + unpacked (i.e. pack_forget). This mechanism may also cause the + TabbedPageSet to resize when the page is changed. + + """ + Frame.__init__(self, parent, **kw) + + self.page_class = page_class + self.pages = {} + self._pages_order = [] + self._current_page = None + self._default_page = None + + self.columnconfigure(0, weight=1) + self.rowconfigure(1, weight=1) + + self.pages_frame = Frame(self) + self.pages_frame.grid(row=1, column=0, sticky=NSEW) + if self.page_class.uses_grid: + self.pages_frame.columnconfigure(0, weight=1) + self.pages_frame.rowconfigure(0, weight=1) + + # the order of the following commands is important + self._tab_set = TabSet(self, self.change_page, n_rows=n_rows, + max_tabs_per_row=max_tabs_per_row, + expand_tabs=expand_tabs) + if page_names: + for name in page_names: + self.add_page(name) + self._tab_set.grid(row=0, column=0, sticky=NSEW) + + self.change_page(self._default_page) + + def update_tabtitle(self, tab, newtitle): + """Update tab title to newtitle.""" + currpage = self.pages[tab.title] + old = tab.title + + # resolve title duplicate + if newtitle in self.pages: + count = 1 + temptitle = newtitle + while temptitle in self.pages: + temptitle = "%s #%d" % (newtitle, count) + count += 1 + newtitle = temptitle + + tab.title = newtitle + # now update 1 million places.. yeh.. + self.pages[newtitle] = self.pages.pop(old) + self._pages_order[self._pages_order.index(old)] = newtitle + self._tab_set._tab_names[self._tab_set._tab_names.index(old)] = newtitle + self._tab_set._tabs[newtitle] = self._tab_set._tabs.pop(old) + self._tab_set._tabs[newtitle].button['text'] = newtitle + self._tab_set._tabs[newtitle].name = newtitle + if self._tab_set._selected_tab == old: + self._tab_set._selected_tab = newtitle + if self._current_page == old: + self._current_page = newtitle + if self._default_page == old: + self._default_page = newtitle + + def add_page(self, page_name): + """Add a new page with the name given in page_name.""" + if not page_name: + raise InvalidNameError("Invalid TabPage name: '%s'" % page_name) + if page_name in self.pages: + raise AlreadyExistsError( + "TabPage named '%s' already exists" % page_name) + + self.pages[page_name] = self.page_class(self.pages_frame) + self._pages_order.append(page_name) + self._tab_set.add_tab(page_name) + + if len(self.pages) == 1: # adding first page + self._default_page = page_name + self.change_page(page_name) + + return self.pages[page_name] + + def remove_page(self, page_name): + """Destroy the page whose name is given in page_name.""" + if not page_name in self.pages: + raise KeyError("No such TabPage: '%s" % page_name) + + self._pages_order.remove(page_name) + # handle removing last remaining, default, or currently shown page + if len(self._pages_order) > 0: + if page_name == self._default_page: + # set a new default page + self._default_page = self._pages_order[0] + else: + self._default_page = None + + if page_name == self._current_page: + self.change_page(self._default_page) + + self._tab_set.remove_tab(page_name) + page = self.pages.pop(page_name) + page.frame.destroy() + + def change_page(self, page_name): + """Show the page whose name is given in page_name.""" + if self._current_page == page_name: + return + if page_name is not None and page_name not in self.pages: + raise KeyError("No such TabPage: '%s'" % page_name) + + if self._current_page is not None: + self.pages[self._current_page]._hide() + self._current_page = None + + if page_name is not None: + self._current_page = page_name + self.pages[page_name]._show() + + self._tab_set.set_selected_tab(page_name) + self.event_generate('<>') # conform to ttk.Notebook + + def last_page(self): + return self.pages[self._pages_order[-1]] + + # Some methods to make this Notebook compatible with the ttk Notebook + + def select(self, page_id=None): + """Return the name of the currently selected page, otherwise + selects page_id. + + page_id may be an integer or a page name.""" + if page_id is None: + return self._current_page + elif isinstance(page_id, int): + self.change_page(self._pages_order[page_id]) + elif isinstance(page_id, str): + self.change_page(page_id) + + def index(self, page_name): + """Return the index of page_name.""" + return self._pages_order.index(page_name) + + def tabs(self): + """Return a list of page names.""" + return self._pages_order + +if __name__ == '__main__': + from Tkinter import Tk, Label, Entry, Button + # test dialog + root=Tk() + tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0, + expand_tabs=False, + ) + tabPage.pack(side=TOP, expand=True, fill=BOTH) + Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack() + Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack() + Label(tabPage.pages['Baz'].frame, text='Baz').pack() + entryPgName=Entry(root) + buttonAdd=Button(root, text='Add Page', + command=lambda:tabPage.add_page(entryPgName.get())) + buttonRemove=Button(root, text='Remove Page', + command=lambda:tabPage.remove_page(entryPgName.get())) + labelPgName=Label(root, text='name of page to add/remove:') + buttonAdd.pack(padx=5, pady=5) + buttonRemove.pack(padx=5, pady=5) + labelPgName.pack(padx=5) + entryPgName.pack(padx=5) + root.mainloop() Index: utils.py =================================================================== --- utils.py (revision 0) +++ utils.py (revision 65573) @@ -0,0 +1,4 @@ +def callback(func, *myargs): + def w(*args): + return func(*(args + myargs)) + return w Index: TreeWidget.py =================================================================== --- TreeWidget.py (revision 63995) +++ TreeWidget.py (revision 65573) @@ -15,12 +15,16 @@ # - optimize tree redraw after expand of subnode import os -from Tkinter import * -import imp +from Tkinter import Tk, Label, Entry, Frame, Canvas, Scrollbar, PhotoImage +from Tkconstants import ALL, END import ZoomHeight from configHandler import idleConf +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import Label, Entry, Frame, Scrollbar + ICONDIR = "Icons" # Look for Icons subdirectory in the same directory as this module @@ -248,7 +252,9 @@ label = self.label except AttributeError: # padding carefully selected (on Windows) to match Entry widget: - self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) + self.label = Label(self.canvas, text=text) + if not TTK: + self.label.configure(bd=0, padx=2, pady=2) theme = idleConf.GetOption('main','Theme','name') if self.selected: self.label.configure(idleConf.GetHighlight(theme, 'hilite')) @@ -451,7 +457,10 @@ # Testing functions +# XXX Can't run these tests + def test(): + from Tkinter import Toplevel import PyShell root = Toplevel(PyShell.root) root.configure(bd=0, bg="yellow") Index: run.py =================================================================== --- run.py (revision 63995) +++ run.py (revision 65573) @@ -24,11 +24,13 @@ except ImportError: pass else: - def idle_formatwarning_subproc(message, category, filename, lineno): + def idle_formatwarning_subproc(message, category, filename, lineno, + line=None): """Format warnings the IDLE way""" s = "\nWarning (from warnings module):\n" s += ' File \"%s\", line %s\n' % (filename, lineno) - line = linecache.getline(filename, lineno).strip() + if line is None: + line = linecache.getline(filename, lineno).strip() if line: s += " %s\n" % line s += "%s: %s\n" % (category.__name__, message) Index: AutoExpand.py =================================================================== --- AutoExpand.py (revision 63995) +++ AutoExpand.py (revision 65573) @@ -15,8 +15,8 @@ wordchars = string.ascii_letters + string.digits + "_" - def __init__(self, editwin): - self.text = editwin.text + def __init__(self, editpage): + self.text = editpage.text self.state = None def expand_word_event(self, event): Index: Percolator.py =================================================================== --- Percolator.py (revision 63995) +++ Percolator.py (revision 65573) @@ -1,5 +1,5 @@ +from Delegator import Delegator from WidgetRedirector import WidgetRedirector -from Delegator import Delegator class Percolator: @@ -81,5 +81,5 @@ root.mainloop() if __name__ == "__main__": - from Tkinter import * + from Tkinter import Tk, Text main() Index: tabbedpages_new.py =================================================================== --- tabbedpages_new.py (revision 0) +++ tabbedpages_new.py (revision 65573) @@ -0,0 +1,116 @@ +"""Classes exported: + +TabbedPageSet -- A custom ttk.Notebook used by IDLE. +""" +from ttk import Frame, Notebook + +from tabbedpages import InvalidNameError, AlreadyExistsError + +class FramePage(object): + def __init__(self, notebook): + self.frame = Frame(notebook) + +class TabbedPageSet(Notebook): + """ + Pages may be accessed through the 'pages' attribute, which is a dictionary + of pages, using the name given as the key. A page is an instance of a + subclass of ttk's Frame widget. + + Pages may be added or removed at any time using the add_page() and + remove_page() methods. + """ + + def __init__(self, master, page_names=None, **kw): + """Constructor arguments: + + page_names -- A list of strings, each will be the dictionary key to a + page's widget, and the name displayed on the page's tab. Should be + specified in the desired page order. The first page will be the default + and first active page. If page_names is None or empty, the + TabbedPageSet will be initialized empty. + """ + Notebook.__init__(self, master, **kw) + + self.pages = {} + page_names = page_names or () + for name in page_names: + self.add_page(name) + + def update_tabtitle(self, tab, newtitle): + """Update tab title to newtitle.""" + currpage = self.pages[tab.title].frame + old = tab.title + + # resolve title duplicate + if newtitle in self.pages and currpage != self.pages[newtitle].frame: + # newtitle is already present, and the current tab is not the + # one who owns it + count = 1 + temptitle = newtitle + while temptitle in self.pages: + if currpage == self.pages[temptitle].frame: + break + temptitle = "%s #%d" % (newtitle, count) + count += 1 + newtitle = temptitle + + tab.title = newtitle + self.pages[newtitle] = self.pages.pop(old) + self.tab(currpage, text=newtitle) + + def add_page(self, page_name): + """Add a new page with the name given in page_name.""" + if not page_name: + raise InvalidNameError("Invalid TabPage name: '%s'" % page_name) + if page_name in self.pages: + raise AlreadyExistsError( + "TabPage named '%s' already exists" % page_name) + + fpage = FramePage(self) + self.pages[page_name] = fpage + self.add(fpage.frame, text=page_name, padding=6) + + # workaround for bug #1878298 at tktoolkit sf bug tracker + self.event_generate('') + + return fpage + + def remove_page(self, page_name): + """Remove page_name from the notebook.""" + if not page_name in self.pages: + raise KeyError("No such TabPage: '%s" % page_name) + + self.forget(self.index(self.pages[page_name].frame)) + del self.pages[page_name] + + # workaround for bug #1878298 at tktoolkit sf bug tracker + self.event_generate('') + + def last_page(self): + """Return the last page in the notebook.""" + return self.pages[self.tab(self.index('end') - 1)['text']] + +if __name__ == '__main__': + from Tkinter import Tk + from Tkconstants import TOP, BOTH + from ttk import Label, Entry, Button, Style + # test dialog + root=Tk() + style = Style() + style.configure('C.TLabel', padding=20) + tabPage=TabbedPageSet(root, page_names=['Foobar','Baz']) + tabPage.pack(side=TOP, expand=True, fill=BOTH) + Label(tabPage.pages['Foobar'].frame, text='Foo', style='C.TLabel').pack() + Label(tabPage.pages['Foobar'].frame, text='Bar', style='C.TLabel').pack() + Label(tabPage.pages['Baz'].frame, text='Baz').pack() + entryPgName=Entry(root) + buttonAdd=Button(root, text='Add Page', + command=lambda:tabPage.add_page(entryPgName.get())) + buttonRemove=Button(root, text='Remove Page', + command=lambda:tabPage.remove_page(entryPgName.get())) + labelPgName=Label(root, text='name of page to add/remove:') + buttonAdd.pack(padx=5, pady=5) + buttonRemove.pack(padx=5, pady=5) + labelPgName.pack(padx=5) + entryPgName.pack(padx=5) + root.mainloop() Index: dynOptionMenuWidget.py =================================================================== --- dynOptionMenuWidget.py (revision 63995) +++ dynOptionMenuWidget.py (revision 65573) @@ -2,34 +2,41 @@ OptionMenu widget modified to allow dynamic menu reconfiguration and setting of highlightthickness """ -from Tkinter import OptionMenu +from Tkinter import OptionMenu, Menu from Tkinter import _setit import copy +from configHandler import idleConf +TTK = idleConf.GetOption('main', 'General', 'use-ttk', type='int') +if TTK: + from ttk import * + class DynOptionMenu(OptionMenu): - """ - unlike OptionMenu, our kwargs can include highlightthickness - """ + """Unlike OptionMenu, our kwargs can include highlightthickness""" def __init__(self, master, variable, value, *values, **kwargs): #get a copy of kwargs before OptionMenu.__init__ munges them kwargsCopy=copy.copy(kwargs) if 'highlightthickness' in kwargs.keys(): del(kwargs['highlightthickness']) + self.command=kwargs.get('command') + self.variable=variable + OptionMenu.__init__(self, master, variable, value, *values, **kwargs) self.config(highlightthickness=kwargsCopy.get('highlightthickness')) - #self.menu=self['menu'] - self.variable=variable - self.command=kwargs.get('command') - def SetMenu(self,valueList,value=None): + def SetMenu(self, valueList, value=None): """ clear and reload the menu with a new set of options. valueList - list of new options value - initial value to set the optionmenu's menubutton to """ - self['menu'].delete(0,'end') - for item in valueList: - self['menu'].add_command(label=item, + if TTK: + self.set_menu(value, *valueList) + else: + menu = self['menu'] + menu.delete(0,'end') + for item in valueList: + menu.add_command(label=item, command=_setit(self.variable,item,self.command)) - if value: - self.variable.set(value) + if value: + self.variable.set(value) Index: extend.txt =================================================================== --- extend.txt (revision 63995) +++ extend.txt (revision 65573) @@ -81,3 +81,10 @@ For further information on binding refer to the Tkinter Resources web page at python.org and to the Tk Command "bind" man page. + + +Note +---- + +Given that this branch is using tabbed pages, don't expect the previous +description to be correct related to the code present here. Index: MultiStatusBar.py =================================================================== --- MultiStatusBar.py (revision 63995) +++ MultiStatusBar.py (revision 65573) @@ -1,5 +1,11 @@ -from Tkinter import * +from Tkinter import Tk, Frame, Label +from Tkconstants import LEFT, SUNKEN, W +from configHandler import idleConf + +if idleConf.GetOption('main', 'General', 'use-ttk', type='int'): + from ttk import Frame, Label + class MultiStatusBar(Frame): def __init__(self, master=None, **kw): @@ -10,7 +16,7 @@ def set_label(self, name, text='', side=LEFT): if not self.labels.has_key(name): - label = Label(self, bd=1, relief=SUNKEN, anchor=W) + label = Label(self, relief=SUNKEN, anchor=W) label.pack(side=side) self.labels[name] = label else: @@ -18,6 +24,8 @@ label.config(text=text) def _test(): + from Tkinter import Text + from Tkconstants import TOP, BOTTOM, X b = Frame() c = Text(b) c.pack(side=TOP) Property changes on: . ___________________________________________________________________ Name: svnmerge-integrated + /python/trunk/Lib/idlelib:1-63994