diff -r ed694938c61a Lib/idlelib/Debugger.py --- a/Lib/idlelib/Debugger.py Tue Sep 22 13:08:42 2015 +0300 +++ b/Lib/idlelib/Debugger.py Tue Sep 22 16:31:08 2015 -0700 @@ -1,9 +1,39 @@ +""" +IDLE interactive debugger user interface. + +The organization of the window has changed greatly from earlier versions. +Most notably: + * works with both Tk 8.4 and 8.5+ + * paned window separates left and right, allowing adjusting relative sizes + * on left, toolbar with graphical/text buttons, plus message, and stack + * on right, local and global variables of selected stack frame + * running program can be interrupted via 'stop' button + * stack and variables use listbox (8.4) or tree (with resizable columns) + * removed locals, globals, and stack 'view' options + * source option changed to auto-open windows to see source + * can always view source by double-clicking or context menu in stack + * full value of variable can be seen via tooltip in variable list + +In future, this will also replace the 'stack viewer' feature for displaying +exceptions, but this has not yet been integrated. +""" + import os import bdb +import linecache from tkinter import * +from tkinter import ttk +from tkinter.font import Font from idlelib.WindowList import ListedToplevel from idlelib.ScrolledList import ScrolledList from idlelib import macosxSupport +from idlelib import ui + + + +def underscore_at_end(s): + # return a key that will sort variable names like __foo__ below others + return s.replace('_', '~') # note: ~ is after letters in ASCII class Idb(bdb.Bdb): @@ -54,11 +84,14 @@ def __init__(self, pyshell, idb=None): if idb is None: idb = Idb(self) + self.framevars = {} self.pyshell = pyshell self.idb = idb self.frame = None + self._ttk = ui.using_ttk self.make_gui() self.interacting = 0 + self.running = False def run(self, *args): try: @@ -67,12 +100,20 @@ finally: self.interacting = 0 + def beginexecuting(self): + self.running = True + + def endexecuting(self): + self.running = False + self.show_status('') + self.enable_buttons(['prefs']) + self.clear_stack() + def close(self, event=None): if self.interacting: self.top.bell() return - if self.stackviewer: - self.stackviewer.close(); self.stackviewer = None + self.abort_loop() # Clean up pyshell if user clicked debugger control close widget. # (Causes a harmless extra cycle through close_debugger() if user # toggled debugger from pyshell Debug menu) @@ -84,80 +125,205 @@ pyshell = self.pyshell self.flist = pyshell.flist self.root = root = pyshell.root + self.tooltip = None + self.var_values = {} + _ttk = self._ttk self.top = top = ListedToplevel(root) self.top.wm_title("Debug Control") self.top.wm_iconname("Debug") top.wm_protocol("WM_DELETE_WINDOW", self.close) self.top.bind("", self.close) - # - self.bframe = bframe = Frame(top) - self.bframe.pack(anchor="w") - self.buttons = bl = [] - # - self.bcont = b = Button(bframe, text="Go", command=self.cont) - bl.append(b) - self.bstep = b = Button(bframe, text="Step", command=self.step) - bl.append(b) - self.bnext = b = Button(bframe, text="Over", command=self.next) - bl.append(b) - self.bret = b = Button(bframe, text="Out", command=self.ret) - bl.append(b) - self.bret = b = Button(bframe, text="Quit", command=self.quit) - bl.append(b) - # - for b in bl: - b.configure(state="disabled") - b.pack(side="left") - # - self.cframe = cframe = Frame(bframe) - self.cframe.pack(side="left") - # - if not self.vstack: - self.__class__.vstack = BooleanVar(top) - self.vstack.set(1) - self.bstack = Checkbutton(cframe, - text="Stack", command=self.show_stack, variable=self.vstack) - self.bstack.grid(row=0, column=0) - if not self.vsource: - self.__class__.vsource = BooleanVar(top) - self.bsource = Checkbutton(cframe, - text="Source", command=self.show_source, variable=self.vsource) - self.bsource.grid(row=0, column=1) - if not self.vlocals: - self.__class__.vlocals = BooleanVar(top) - self.vlocals.set(1) - self.blocals = Checkbutton(cframe, - text="Locals", command=self.show_locals, variable=self.vlocals) - self.blocals.grid(row=1, column=0) - if not self.vglobals: - self.__class__.vglobals = BooleanVar(top) - self.bglobals = Checkbutton(cframe, - text="Globals", command=self.show_globals, variable=self.vglobals) - self.bglobals.grid(row=1, column=1) - # - self.status = Label(top, anchor="w") - self.status.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.flocals = Frame(top) - self.flocals.pack(expand=1, fill="both") - self.fglobals = Frame(top, height=1) - self.fglobals.pack(expand=1, fill="both") - # - if self.vstack.get(): - self.show_stack() - if self.vlocals.get(): - self.show_locals() - if self.vglobals.get(): - self.show_globals() + self.var_open_source_windows = BooleanVar(top, False) + + self.pane = ui.PanedWindow(self.top, orient='horizontal') + self.pane.grid(column=0, row=0, sticky='nwes') + self.top.grid_columnconfigure(0, weight=1) + self.top.grid_rowconfigure(0, weight=1) + self.left = left = ui.padframe(ui.Frame(self.pane), 5) + if _ttk: + self.pane.add(left, weight=1) + else: + self.pane.add(left, stretch='always', sticky='nsew') + controls = ui.Frame(left) + col = 0 + f = ('helvetica', 9) + self.buttondata = {} + self.buttons = ['go', 'step', 'over', 'out', 'stop', 'prefs'] + self.button_names = {'go':'Go', 'step':'Step', 'over':'Over', + 'out':'Out', 'stop':'Stop', 'prefs':'Options'} + self.button_cmds = {'go':self.cont, 'step':self.step, + 'over':self.next, 'out':self.ret, + 'stop':self.quit, 'prefs':self.options} + for key in self.buttons: + normal = ui.image('debug_'+key+'.gif') + disabled = ui.image('debug_'+key+'_disabled.gif') + b = ui.Label(controls, image=normal, text=self.button_names[key], + compound='top', font=f) + b.grid(column=col, row=0, padx=[0,5]) + self.buttondata[key] = (b, normal, disabled) + col += 1 + self.enable_buttons(['prefs']) + self.status = ui.Label(controls, text=' ', font=('helvetica', 13)) + self.status.grid(column=6, row=0, sticky='nw', padx=[25,0]) + controls.grid(column=0, row=0, sticky='new', pady=[0,6]) + controls.grid_columnconfigure(7, weight=1) + + self.current_line_img = ui.image('debug_current.gif') + self.regular_line_img = ui.image('debug_line.gif') + if _ttk: + self.stack = ttk.Treeview(left, columns=('statement', ), + height=5, selectmode='browse') + self.stack.column('#0', width=100) + self.stack.column('#1', width=150) + self.stack.tag_configure('error', foreground='red') + else: + self.stack = Listbox(left, height=5, width=35, selectmode='browse', + exportselection=False, activestyle='none') + self.stack.bind('<>' if _ttk else '<>', + lambda e: self.stack_selection_changed()) + self.stack.bind('', lambda e: self.stack_doubleclick()) + self.stack.bind('<>', self.stack_contextmenu) + + scroll = ui.Scrollbar(left, command=self.stack.yview) + self.stack['yscrollcommand'] = scroll.set + self.stack.grid(column=0, row=2, sticky='nwes') + scroll.grid(column=1, row=2, sticky='ns') + left.grid_columnconfigure(0, weight=1) + left.grid_rowconfigure(2, weight=1) + + right = ui.padframe(ui.Frame(self.pane), 5) + if _ttk: + self.pane.add(right, weight=1) + else: + self.pane.add(right, stretch='always', sticky='nsew') + if _ttk: + self.vars = ttk.Treeview(right, columns=('value',), height=5, + selectmode='none') + self.locals = self.vars.insert('', 'end', text='Locals', + open=True) + self.globals = self.vars.insert('', 'end', text='Globals', + open=False) + self.vars.column('#0', width=100) + self.vars.column('#1', width=150) + else: + self.vars = Listbox(right, height=5, width=35, selectmode='none', + exportselection=False, activestyle='none') + self.vars.bind('', self.mouse_moved_vars) + self.vars.bind('', self.leave_vars) + scroll2 = ui.Scrollbar(right, command=self.vars.yview) + self.vars['yscrollcommand'] = scroll2.set + self.vars.grid(column=0, row=0, sticky='nwes') + scroll2.grid(column=1, row=0, sticky='ns') + right.grid_columnconfigure(0, weight=1) + right.grid_rowconfigure(0, weight=1) + left.bind('', lambda e: self._adjust_layout()) + self.clear_stack() + + + def _adjust_layout(self): + # if too narrow, move message below buttons + if self.left.winfo_width() < 380: + self.status.grid(column=0, row=1, columnspan=8, padx=[5,0]) + else: + self.status.grid(column=6, row=0, columnspan=1, padx=[25,0]) + + def enable_buttons(self, buttons=None): + for key in self.buttons: + if buttons is None or not key in buttons: + self.buttondata[key][0]['image'] = self.buttondata[key][2] + self.buttondata[key][0]['foreground'] = '#aaaaaa' + self.buttondata[key][0]['cursor'] = '' + self.buttondata[key][0].bind('<1>', 'break') + self.buttondata[key][0].bind('<>', 'break') + else: + self.buttondata[key][0]['image'] = self.buttondata[key][1] + self.buttondata[key][0]['foreground'] = '#000000' + self.buttondata[key][0].bind('<1>', self.button_cmds[key]) + self.buttondata[key][0].bind('<>', + self.button_cmds[key]) + self.buttondata[key][0]['cursor'] = ui.clickable_cursor + + def stack_selection_changed(self): + self.show_vars() + + def stack_doubleclick(self): + sel = self.stack.selection() if self._ttk else \ + self.stack.curselection() + if len(sel) == 1: + self.show_source(sel[0]) + + def stack_contextmenu(self, event): + if self._ttk: + item = self.stack.identify('item', event.x, event.y) + else: + item = self.stack.nearest(event.y) + if item is not None and item != -1 and item != '': + menu = Menu(self.top, tearoff=0) + menu.add_command(label='View Source', + command = lambda: self.show_source(item)) + menu.tk_popup(event.x_root, event.y_root) + + def show_source(self, item): + if item in self.framevars: + fname = self.framevars[item][2] + lineno = self.framevars[item][3] + if fname[:1] + fname[-1:] != "<>" and os.path.exists(fname): + self.flist.gotofileline(fname, lineno) + + def show_status(self, msg, error=False): + self.status['text'] = msg + self.status['foreground'] = '#ff0000' if error else '#006600' + self.status['font'] = ('helvetica', 13, 'italic') if error \ + else ('helvetica', 13) + + def clear_stack(self): + if self._ttk: + self.stack.delete(*self.stack.get_children('')) + self.vars.delete(*self.vars.get_children(self.locals)) + self.vars.delete(*self.vars.get_children(self.globals)) + self.vars.detach(self.locals) + self.vars.detach(self.globals) + else: + self.stack.delete(0, 'end') + self.vars.delete(0, 'end') + self.var_values = {} + + def add_stackframe(self, frame, lineno, current=False): + func = frame.f_code.co_name + if func in ("?", "", None): + func = '.' + try: + selfval = frame.f_locals['self'] + if selfval.__class__.__name__ == 'str': + # we've probably got the string representation of the + # object sent from the remote debugger, see if we can + # parse it into something useful + match = re.match('^<(?:.*)\.([^\.]*) object at 0x[0-9a-f]+>$', + selfval) + if match: + func = match.group(1) + '.' + func + else: + func = selfval.__class__.__name__ + '.' + func + except Exception: + pass + stmt = linecache.getline(frame.f_code.co_filename, lineno).strip() + if self._ttk: + image=self.current_line_img if current else self.regular_line_img + item = self.stack.insert('', 'end', text=func, + values=(stmt,), image=image) + else: + self.stack.insert('end', func + ' ' + stmt) + item = self.stack.index('end') - 1 + self.framevars[item] = (frame.f_locals, frame.f_globals, + frame.f_code.co_filename, lineno) + if current: + if not self._ttk: + self.stack.selection_clear(0, 'end') + self.stack.selection_set(item) def interaction(self, message, frame, info=None): self.frame = frame - self.status.configure(text=message) + self.show_status(message) # if info: type, value, tb = info @@ -170,42 +336,89 @@ m1 = "%s: %s" % (m1, str(value)) except: pass - bg = "yellow" else: m1 = "" tb = None - bg = self.errorbg - self.error.configure(text=m1, background=bg) - # - sv = self.stackviewer - if sv: - stack, i = self.idb.get_stack(self.frame, tb) - sv.load_stack(stack, i) - # - self.show_variables(1) - # - if self.vsource.get(): - self.sync_source_line() - # - for b in self.buttons: - b.configure(state="normal") - # + if m1 != '': + self.show_status(m1, error=True) + + stack, idx = self.idb.get_stack(self.frame, tb) + self.clear_stack() + for i in range(len(stack)): + frame, lineno = stack[i] + self.add_stackframe(frame, lineno, current=(i == idx)) + self.show_vars() + self.sync_source_line() + self.enable_buttons(self.buttons) self.top.wakeup() - self.root.mainloop() - # - for b in self.buttons: - b.configure(state="disabled") - self.status.configure(text="") - self.error.configure(text="", background=self.errorbg) + # nested event loop + self.root.tk.call('vwait', '::idledebugwait') self.frame = None + def show_vars(self): + _ttk = self._ttk + if _ttk: + self.vars.move(self.locals, '', 0) + self.vars.move(self.globals, '', 1) + self.vars.delete(*self.vars.get_children(self.locals)) + self.vars.delete(*self.vars.get_children(self.globals)) + else: + self.vars.delete(0, 'end') + self.var_values = {} + sel = self.stack.selection() if _ttk else self.stack.curselection() + if len(sel) == 1 and sel[0] in self.framevars: + locals, globals, _, _ = self.framevars[sel[0]] + # note: locals/globals may be from a remotedebugger, in + # which case for reasons we don't need to get into here, + # they aren't iterable + self.add_varheader() + for name in sorted(locals.keys(), key=underscore_at_end): + self.add_var(name, locals[name]) + self.add_varheader(isGlobal=True) + for name in sorted(globals.keys(), key=underscore_at_end): + self.add_var(name, globals[name], isGlobal=True) + + def add_varheader(self, isGlobal=False): + if not self._ttk: + self.vars.insert('end', 'Globals:' if isGlobal else 'Locals:') + + def add_var(self, varname, value, isGlobal=False): + if self._ttk: + item = self.vars.insert(self.globals if isGlobal else self.locals, + 'end', text=varname, values=(value, )) + else: + self.vars.insert('end', ' ' + varname + ': ' + str(value)) + item = self.vars.index('end') - 1 + self.var_values[item] = value + + def mouse_moved_vars(self, ev): + ui.tooltip_schedule(ev, self.var_tooltip) + + def leave_vars(self, ev): + ui.tooltip_clear() + + def var_tooltip(self, ev): + # Callback from tooltip package to return text of tooltip + item = None + if self._ttk: + if self.vars.identify('column', ev.x, ev.y) == '#1': + item = self.vars.identify('item', ev.x, ev.y) + else: + item = self.vars.nearest(ev.y) + if item and item in self.var_values: + return(self.var_values[item], ev.x + self.vars.winfo_rootx() + 10, + ev.y + self.vars.winfo_rooty() + 5) + return None + def sync_source_line(self): frame = self.frame if not frame: return filename, lineno = self.__frame2fileline(frame) if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): - self.flist.gotofileline(filename, lineno) + if self.var_open_source_windows.get() or\ + self.flist.already_open(filename): + self.flist.gotofileline(filename, lineno) def __frame2fileline(self, frame): code = frame.f_code @@ -213,91 +426,49 @@ lineno = frame.f_lineno return filename, lineno - def cont(self): + def invoke_program(self): + "Called just before taking the next action in debugger, adjust state" + self.enable_buttons(['stop']) + self.show_status('Running...') + + def cont(self, ev=None): + self.invoke_program() self.idb.set_continue() - self.root.quit() + self.abort_loop() - def step(self): + def step(self, ev=None): + self.invoke_program() self.idb.set_step() - self.root.quit() + self.abort_loop() - def next(self): + def next(self, ev=None): + self.invoke_program() self.idb.set_next(self.frame) - self.root.quit() + self.abort_loop() - def ret(self): + def ret(self, ev=None): + self.invoke_program() self.idb.set_return(self.frame) - self.root.quit() + self.abort_loop() - def quit(self): - self.idb.set_quit() - self.root.quit() + def quit(self, ev=None): + if self.running: + self.pyshell.interp.restart_subprocess() + else: + self.invoke_program() + self.idb.set_quit() + self.abort_loop() + + def abort_loop(self): + self.root.tk.call('set', '::idledebugwait', '1') - stackviewer = None - - def show_stack(self): - if not self.stackviewer and self.vstack.get(): - self.stackviewer = sv = StackViewer(self.fstack, self.flist, self) - if self.frame: - stack, i = self.idb.get_stack(self.frame, None) - sv.load_stack(stack, i) - else: - sv = self.stackviewer - if sv and not self.vstack.get(): - self.stackviewer = None - sv.close() - self.fstack['height'] = 1 - - def show_source(self): - if self.vsource.get(): - self.sync_source_line() - - def show_frame(self, stackitem): - self.frame = stackitem[0] # lineno is stackitem[1] - self.show_variables() - - localsviewer = None - globalsviewer = None - - def show_locals(self): - lv = self.localsviewer - if self.vlocals.get(): - if not lv: - self.localsviewer = NamespaceViewer(self.flocals, "Locals") - else: - if lv: - self.localsviewer = None - lv.close() - self.flocals['height'] = 1 - self.show_variables() - - def show_globals(self): - gv = self.globalsviewer - if self.vglobals.get(): - if not gv: - self.globalsviewer = NamespaceViewer(self.fglobals, "Globals") - else: - if gv: - self.globalsviewer = None - gv.close() - self.fglobals['height'] = 1 - self.show_variables() - - def show_variables(self, force=0): - lv = self.localsviewer - gv = self.globalsviewer - frame = self.frame - if not frame: - ldict = gdict = None - else: - ldict = frame.f_locals - gdict = frame.f_globals - if lv and gv and ldict is gdict: - ldict = None - if lv: - lv.load_dict(ldict, force, self.pyshell.interp.rpcclt) - if gv: - gv.load_dict(gdict, force, self.pyshell.interp.rpcclt) + def options(self, ev=None): + menu = Menu(self.top, tearoff=0) + menu.add_checkbutton(label='Show Source in Open Files Only', + variable=self.var_open_source_windows, onvalue=False) + menu.add_checkbutton(label='Automatically Open Files to Show Source', + variable=self.var_open_source_windows, onvalue=True) + menu.tk_popup(ev.x_root, ev.y_root) def set_breakpoint_here(self, filename, lineno): self.idb.set_break(filename, lineno) @@ -317,174 +488,3 @@ self.set_breakpoint_here(filename, lineno) except AttributeError: continue - -class StackViewer(ScrolledList): - - def __init__(self, master, flist, gui): - if macosxSupport.isAquaTk(): - # At least on with the stock AquaTk version on OSX 10.4 you'll - # get an shaking GUI that eventually kills IDLE if the width - # argument is specified. - ScrolledList.__init__(self, master) - else: - ScrolledList.__init__(self, master, width=80) - self.flist = flist - self.gui = gui - self.stack = [] - - def load_stack(self, stack, index=None): - self.stack = stack - self.clear() - for i in range(len(stack)): - frame, lineno = stack[i] - try: - modname = frame.f_globals["__name__"] - except: - modname = "?" - code = frame.f_code - filename = code.co_filename - funcname = code.co_name - import linecache - sourceline = linecache.getline(filename, lineno) - sourceline = sourceline.strip() - if funcname in ("?", "", None): - item = "%s, line %d: %s" % (modname, lineno, sourceline) - else: - item = "%s.%s(), line %d: %s" % (modname, funcname, - lineno, sourceline) - if i == index: - item = "> " + item - self.append(item) - if index is not None: - self.select(index) - - def popup_event(self, event): - "override base method" - if self.stack: - return ScrolledList.popup_event(self, event) - - def fill_menu(self): - "override base method" - menu = self.menu - menu.add_command(label="Go to source line", - command=self.goto_source_line) - menu.add_command(label="Show stack frame", - command=self.show_stack_frame) - - def on_select(self, index): - "override base method" - if 0 <= index < len(self.stack): - self.gui.show_frame(self.stack[index]) - - def on_double(self, index): - "override base method" - self.show_source(index) - - def goto_source_line(self): - index = self.listbox.index("active") - self.show_source(index) - - def show_stack_frame(self): - index = self.listbox.index("active") - if 0 <= index < len(self.stack): - self.gui.show_frame(self.stack[index]) - - def show_source(self, index): - if not (0 <= index < len(self.stack)): - return - frame, lineno = self.stack[index] - code = frame.f_code - filename = code.co_filename - if os.path.isfile(filename): - edit = self.flist.open(filename) - if edit: - edit.gotoline(lineno) - - -class NamespaceViewer: - - def __init__(self, master, title, dict=None): - width = 0 - height = 40 - if dict: - height = 20*len(dict) # XXX 20 == observed height of Entry widget - self.master = master - self.title = title - import reprlib - self.repr = reprlib.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.canvas = canvas = Canvas(frame, - height=min(300, max(40, height)), - scrollregion=(0, 0, width, height)) - 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.load_dict(dict) - - dict = -1 - - def load_dict(self, dict, force=0, rpc_client=None): - if dict is self.dict and not force: - return - subframe = self.subframe - frame = self.frame - for c in list(subframe.children.values()): - c.destroy() - self.dict = None - if not dict: - l = Label(subframe, text="None") - l.grid(row=0, column=0) - else: - #names = sorted(dict) - ### - # Because of (temporary) limitations on the dict_keys type (not yet - # public or pickleable), have the subprocess to send a list of - # keys, not a dict_keys object. sorted() will take a dict_keys - # (no subprocess) or a list. - # - # There is also an obscure bug in sorted(dict) where the - # interpreter gets into a loop requesting non-existing dict[0], - # dict[1], dict[2], etc from the RemoteDebugger.DictProxy. - ### - keys_list = dict.keys() - names = sorted(keys_list) - ### - row = 0 - for name in names: - value = dict[name] - svalue = self.repr.repr(value) # repr(value) - # Strip extra quotes caused by calling repr on the (already) - # repr'd value sent across the RPC interface: - if rpc_client: - svalue = svalue[1:-1] - l = Label(subframe, text=name) - 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") - row = row+1 - self.dict = dict - # XXX Could we use a callback for the following? - subframe.update_idletasks() # Alas! - width = subframe.winfo_reqwidth() - height = subframe.winfo_reqheight() - canvas = self.canvas - self.canvas["scrollregion"] = (0, 0, width, height) - if height > 300: - canvas["height"] = 300 - frame.pack(expand=1) - else: - canvas["height"] = height - frame.pack(expand=0) - - def close(self): - self.frame.destroy() diff -r ed694938c61a Lib/idlelib/PyShell.py --- a/Lib/idlelib/PyShell.py Tue Sep 22 13:08:42 2015 +0300 +++ b/Lib/idlelib/PyShell.py Tue Sep 22 16:31:08 2015 -0700 @@ -34,6 +34,7 @@ from idlelib import Debugger from idlelib import RemoteDebugger from idlelib import macosxSupport +from idlelib import ui HOST = '127.0.0.1' # python execution server on localhost loopback PORT = 0 # someday pass in host, port for remote debug capability @@ -471,6 +472,7 @@ RemoteDebugger.close_subprocess_debugger(self.rpcclt) except: pass + debug.endexecuting() # Kill subprocess, spawn a new one, accept connection. self.rpcclt.close() self.terminate_subprocess() @@ -790,6 +792,8 @@ if self.tkconsole.canceled: self.tkconsole.canceled = False print("KeyboardInterrupt", file=self.tkconsole.stderr) + if self.interp.debugger: + self.interp.debugger.endexecuting() else: self.showtraceback() finally: @@ -860,6 +864,7 @@ self.interp = ModifiedInterpreter(self) if flist is None: root = Tk() + ui.init(root) fixwordbreaks(root) root.withdraw() flist = PyShellFileList(root) @@ -975,11 +980,15 @@ def beginexecuting(self): "Helper for ModifiedInterpreter" + if self.interp.debugger: + self.interp.debugger.beginexecuting() self.resetoutput() self.executing = 1 def endexecuting(self): "Helper for ModifiedInterpreter" + if self.interp.debugger: + self.interp.debugger.endexecuting() self.executing = 0 self.canceled = 0 self.showprompt() @@ -1530,6 +1539,7 @@ enable_shell = enable_shell or not enable_edit # start editor and/or shell windows: root = Tk(className="Idle") + ui.init(root, allow_ttk=True) # set application icon icondir = os.path.join(os.path.dirname(__file__), 'Icons') diff -r ed694938c61a Lib/idlelib/ui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/ui.py Tue Sep 22 16:31:08 2015 -0700 @@ -0,0 +1,157 @@ +""" +Central place for flags, variables, small utilities etc. that determine +how Tk is used throughout IDLE. +""" + +import os +import tkinter +from tkinter import ttk +from tkinter.font import Font + +# Should IDLE use themed-Tk widgets (ttk)? +using_ttk = False + +# What windowing system are we using? +windowing_system = None # will be set to 'aqua', 'win32', or 'x11' + +# Do we need to include a Sizegrip widget? +need_sizegrip = False + +# Cursor to use to indicate clickable things, like links - usually a hand +clickable_cursor = 'hand2' + +# Tk root window for our application +root = None + +# Classes for widgets that have ttk counterparts; since they'll have +# different options, code will need to check which they're using for all +# but the simplest things +Button = tkinter.Button +Frame = tkinter.Frame +Label = tkinter.Label +Scrollbar = tkinter.Scrollbar +Spinbox = tkinter.Spinbox +PanedWindow = tkinter.PanedWindow + + +# Initialize our common variables; this needs to be called before the +# variables can be used. We require this to avoid the overhead of creating +# a temporary Tk instance. + +def init(root_, allow_ttk=True): + global _initialized, root, using_ttk, windowing_system, need_sizegrip,\ + clickable_cursor, Button, Frame, Label, Scrollbar, PanedWindow,\ + Spinbox + + if _initialized: + return + root = root_ + if allow_ttk: + try: + b = ttk.Button(root) + using_ttk = True + except Exception: + pass + windowing_system = root.call('tk', 'windowingsystem') + try: + _tooltipfont = Font(name='TkTooltipFont', exists=True, root=root) + except tkinter.TclError: + _tooltipfont = Font(family='helvetica', size=10, root=root) + if using_ttk: + Button = ttk.Button + Frame = ttk.Frame + Label = ttk.Label + Scrollbar = ttk.Scrollbar + PanedWindow = ttk.PanedWindow + Spinbox = _Spinbox # see below + if windowing_system == 'aqua': + clickable_cursor = 'pointinghand' + import platform + v, _, _ = platform.mac_ver() + major, minor = v.split('.')[:2] + if (int(major) == 10 and int(minor) < 7): + need_sizegrip = True + # NOTE: Tk 8.6 defines a <> event + root.event_add('<>', '', '') + else: + root.event_add('<>', '') + _initialized = True + +_initialized = False +_tooltipfont = None + + +def padframe(frame, padding): + "Convenience procedure to add padding to a frame, ttk or otherwise" + try: + frame['padding'] = padding + except tkinter.TclError: + frame['padx'] = padding + frame['pady'] = padding + return frame + +def image(filename): + "Return an image object for a file in our 'Icons' directory" + dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'Icons') + return tkinter.PhotoImage(master=root, file=os.path.join(dir, filename)) + + +class _Spinbox(tkinter.Spinbox): + """ + A ttk::spinbox was added in Tk 8.5.9; use it if present, otherwise + use a spinbox. Note the two have different options and methods, so this + only works for the basics. + """ + def __init__(self, master=None, cnf={}, **kw): + hasTtkSpinbox = master and master.tk.call('info', 'commands', + 'ttk::spinbox') + base = 'ttk::spinbox' if hasTtkSpinbox else 'spinbox' + tkinter.Widget.__init__(self, master, base, cnf, kw) + + +# TODO - duplication from uitabs.py +_tooltip = None + +def tooltip_clear(): + global _tooltip + if _tooltip is not None: + if _tooltip['window'] is not None: + _tooltip['window'].destroy() + if _tooltip['afterid'] is not None: + _tooltip['event'].widget.after_cancel(_tooltip['afterid']) + _tooltip = None + +def tooltip_schedule(event, callback): + global _tooltip + tooltip_clear() + _tooltip = {'window': None, 'event': event, 'callback': callback, + 'afterid': event.widget.after(1500, _tooltip_display)} + +def _tooltip_display(): + global _tooltip + _tooltip['afterid'] = None + event = _tooltip['event'] + callback = _tooltip['callback'] + _tooltip['event'] = None + _tooltip['callback'] = None + ret = callback(event) + if ret is not None: + txt, x, y = ret + tw = _tooltip['window'] = tkinter.Toplevel(event.widget) + tw.wm_withdraw() + tw.wm_geometry("+%d+%d" % (x, y)) + tw.wm_overrideredirect(1) + try: + tw.tk.call("::tk::unsupported::MacWindowStyle", "style", + tw._w, "help", "noActivates") + except tkinter.TclError: + pass + lbl = tkinter.Label(tw, text=txt, justify='left', + background="#ffffe0", borderwidth=0, font=_tooltipfont) + if windowing_system != 'aqua': + lbl['borderwidth'] = 1 + lbl['relief'] = 'solid' + lbl.pack() + tw.update_idletasks() # calculate window size to avoid resize flicker + tw.deiconify() + tw.lift() # needed to work around bug in Tk 8.5.18+ (issue #24570)