diff --git a/pudb/debugger.py b/pudb/debugger.py index 69038da2..ffef75ec 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -422,10 +422,9 @@ def _runscript(self, filename): # UI stuff -------------------------------------------------------------------- - -from pudb.ui_tools import make_hotkey_markup, labelled_value, \ - SelectableText, SignalWrap, StackFrame, BreakpointFrame - +from pudb.ui_tools import double_press_input_filter, labelled_value, \ + make_hotkey_markup, SelectableText, SignalWrap, StackFrame, \ + BreakpointFrame from pudb.var_view import FrameVarInfoKeeper @@ -749,13 +748,19 @@ def change_rhs_box(name, index, direction, w, size, key): # {{{ variables listeners - def change_var_state(w, size, key): + def change_var_state(w, size, key, button=None, x=None, y=None, focus=None): var, pos = self.var_list._w.get_focus() + if not var: + return iinfo = self.get_frame_var_info(read_only=False) \ .get_inspect_info(var.id_path, read_only=False) - if key == "\\": + display_types = ["type", "repr", "str"] + if CONFIG["custom_stringifier"]: + display_types.append(CONFIG["custom_stringifier"]) + + if key == "\\" or (key == 'mouse press' and button == 3): iinfo.show_detail = not iinfo.show_detail elif key == "t": iinfo.display_type = "type" @@ -765,9 +770,9 @@ def change_var_state(w, size, key): iinfo.display_type = "str" elif key == "c": iinfo.display_type = CONFIG["custom_stringifier"] - elif key == "h": + elif key == "h" or (key == 'ctrl mouse press' and button == 1): iinfo.highlighted = not iinfo.highlighted - elif key == "@": + elif key == "@" or (key == 'ctrl mouse double press' and button == 1): iinfo.repeated_at_top = not iinfo.repeated_at_top elif key == "*": levels = ["public", "private", "all", "public"] @@ -776,10 +781,20 @@ def change_var_state(w, size, key): iinfo.wrap = not iinfo.wrap elif key == "m": iinfo.show_methods = not iinfo.show_methods + elif key == 'mouse press' and button == 5: + # SignalWrap.mouse_event by doesn't select on scroll events, + # but we want to do that here. + super(SignalWrap, self.var_list).mouse_event(size, + 'mouse press', 1, x, y, focus) + iinfo.display_type = display_types[(display_types.index(iinfo.display_type) + 1) % len(display_types)] + elif key == 'mouse press' and button == 4: + super(SignalWrap, self.var_list).mouse_event(size, + 'mouse press', 1, x, y, focus) + iinfo.display_type = display_types[(display_types.index(iinfo.display_type) - 1 + len(display_types)) % len(display_types)] self.update_var_view() - def edit_inspector_detail(w, size, key): + def edit_inspector_detail(w, size, key, button=None, x=None, y=None, focus=None): var, pos = self.var_list._w.get_focus() if var is None: @@ -911,12 +926,16 @@ def insert_watch(w, size, key): self.var_list.listen("r", change_var_state) self.var_list.listen("s", change_var_state) self.var_list.listen("c", change_var_state) + self.var_list.listen_mouse_event("mouse press", None, change_var_state) self.var_list.listen("h", change_var_state) + self.var_list.listen_mouse_event("ctrl mouse press", 1, change_var_state) self.var_list.listen("@", change_var_state) self.var_list.listen("*", change_var_state) self.var_list.listen("w", change_var_state) self.var_list.listen("m", change_var_state) + self.var_list.listen_mouse_event("ctrl mouse double press", 1, change_var_state) self.var_list.listen("enter", edit_inspector_detail) + self.var_list.listen_mouse_event("mouse double press", 1, edit_inspector_detail) self.var_list.listen("n", insert_watch) self.var_list.listen("insert", insert_watch) @@ -926,24 +945,29 @@ def insert_watch(w, size, key): # }}} # {{{ stack listeners - def examine_frame(w, size, key): + def examine_frame(w, size, key, button=None, x=None, y=None, focus=None): _, pos = self.stack_list._w.get_focus() self.debugger.set_frame_index(self.translate_ui_stack_index(pos)) self.stack_list.listen("enter", examine_frame) + self.stack_list.listen_mouse_event("mouse press", 1, examine_frame) - def move_stack_top(w, size, key): + def move_stack_top(w, size, key, button=None, x=None, y=None, focus=None): self.debugger.set_frame_index(len(self.debugger.stack)-1) - def move_stack_up(w, size, key): + def move_stack_up(w, size, key, button=None, x=None, y=None, focus=None): self.debugger.move_up_frame() - def move_stack_down(w, size, key): + def move_stack_down(w, size, key, button=None, x=None, y=None, focus=None): self.debugger.move_down_frame() self.stack_list.listen("H", move_stack_top) self.stack_list.listen("u", move_stack_up) + self.stack_list.listen_mouse_event("mouse press", 4, move_stack_up) self.stack_list.listen("d", move_stack_down) + self.stack_list.listen_mouse_event("mouse press", 5, move_stack_down) + self.source_sigwrap.listen("u", move_stack_up) + self.source_sigwrap.listen("d", move_stack_down) self.stack_list.listen("[", partial(change_rhs_box, 'stack', 1, -1)) self.stack_list.listen("]", partial(change_rhs_box, 'stack', 1, 1)) @@ -977,7 +1001,7 @@ def delete_breakpoint(w, size, key): else: self.update_breakpoints() - def enable_disable_breakpoint(w, size, key): + def enable_disable_breakpoint(w, size, key, button=None, x=None, y=None, focus=None): bp_entry, pos = self.bp_list._w.get_focus() if bp_entry is None: @@ -991,7 +1015,7 @@ def enable_disable_breakpoint(w, size, key): self.update_breakpoints() - def examine_breakpoint(w, size, key): + def examine_breakpoint(w, size, key, button=None, x=None, y=None, focus=None): bp_entry, pos = self.bp_list._w.get_focus() if bp_entry is None: @@ -1063,9 +1087,11 @@ def examine_breakpoint(w, size, key): self.update_breakpoints() self.bp_list.listen("enter", examine_breakpoint) + self.bp_list.listen_mouse_event("mouse double press", 1, examine_breakpoint) self.bp_list.listen("d", delete_breakpoint) self.bp_list.listen("s", save_breakpoints) self.bp_list.listen("e", enable_disable_breakpoint) + self.bp_list.listen_mouse_event("mouse press", 3, enable_disable_breakpoint) self.bp_list.listen("[", partial(change_rhs_box, 'breakpoints', 2, -1)) self.bp_list.listen("]", partial(change_rhs_box, 'breakpoints', 2, 1)) @@ -1078,14 +1104,14 @@ def end(): self.debugger.save_breakpoints() self.quit_event_loop = True - def next(w, size, key): + def next(w, size, key, button=None, x=None, y=None, focus=None): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: self.debugger.set_next(self.debugger.curframe) end() - def step(w, size, key): + def step(w, size, key, button=None, x=None, y=None, focus=None): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: @@ -1106,7 +1132,7 @@ def cont(w, size, key): self.debugger.set_continue() end() - def run_to_cursor(w, size, key): + def run_to_cursor(w, size, key, button=None, x=None, y=None, focus=None): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: @@ -1166,16 +1192,16 @@ def go_to_line(w, size, key): lineno = min(max(0, int(lineno_edit.value())-1), len(self.source)-1) self.source.set_focus(lineno) - def move_down(w, size, key): + def move_down(w, size, key, button=None, x=None, y=None, focus=None): w.keypress(size, "down") - def move_up(w, size, key): + def move_up(w, size, key, button=None, x=None, y=None, focus=None): w.keypress(size, "up") - def page_down(w, size, key): + def page_down(w, size, key, button=None, x=None, y=None, focus=None): w.keypress(size, "page down") - def page_up(w, size, key): + def page_up(w, size, key, button=None, x=None, y=None, focus=None): w.keypress(size, "page up") def scroll_left(w, size, key): @@ -1199,7 +1225,7 @@ def search_next(w, size, key): def search_previous(w, size, key): self.search_controller.perform_search(dir=-1, update_search_start=True) - def toggle_breakpoint(w, size, key): + def toggle_breakpoint(w, size, key, button=None, x=None, y=None, focus=None): bp_source_identifier = \ self.source_code_provider.get_breakpoint_source_identifier() @@ -1230,7 +1256,7 @@ def toggle_breakpoint(w, size, key): from pudb.lowlevel import get_breakpoint_invalid_reason invalid_reason = get_breakpoint_invalid_reason( - bp_source_identifier, pos+1) + bp_source_identifier, lineno) if invalid_reason is not None: do_set = not self.dialog( @@ -1386,16 +1412,23 @@ def keypress(self, size, key): self.translate_ui_stack_index(pos)) self.source_sigwrap.listen("n", next) + self.source_sigwrap.listen_mouse_event("meta mouse press", 5, next) self.source_sigwrap.listen("s", step) + self.source_sigwrap.listen_mouse_event("meta mouse press", 1, step) self.source_sigwrap.listen("f", finish) self.source_sigwrap.listen("r", finish) self.source_sigwrap.listen("c", cont) self.source_sigwrap.listen("t", run_to_cursor) + self.source_sigwrap.listen_mouse_event("mouse double press", 1, run_to_cursor) self.source_sigwrap.listen("j", move_down) + self.source_sigwrap.listen_mouse_event("mouse press", 5, move_down) self.source_sigwrap.listen("k", move_up) + self.source_sigwrap.listen_mouse_event("mouse press", 4, move_up) self.source_sigwrap.listen("ctrl d", page_down) + self.source_sigwrap.listen_mouse_event("ctrl mouse press", 5, page_down) self.source_sigwrap.listen("ctrl u", page_up) + self.source_sigwrap.listen_mouse_event("ctrl mouse press", 4, page_up) self.source_sigwrap.listen("ctrl f", page_down) self.source_sigwrap.listen("ctrl b", page_up) self.source_sigwrap.listen("h", scroll_left) @@ -1412,6 +1445,7 @@ def keypress(self, size, key): self.source_sigwrap.listen("L", go_to_line) self.source_sigwrap.listen("b", toggle_breakpoint) + self.source_sigwrap.listen_mouse_event("mouse press", 3, toggle_breakpoint) self.source_sigwrap.listen("m", pick_module) self.source_sigwrap.listen("H", move_stack_top) @@ -1886,6 +1920,8 @@ def help(w, size, key): self.current_line = None + self.double_press_input_filter = double_press_input_filter() + self.quit_event_loop = False # }}} @@ -2059,6 +2095,8 @@ def show_exception_dialog(self, exc_tuple): def show(self): if self.show_count == 0: self.screen.start() + if CONFIG["mouse_support"]: + self.screen.set_mouse_tracking() self.show_count += 1 def hide(self): @@ -2243,10 +2281,13 @@ def event_loop(self, toplevel=None): canvas = toplevel.render(self.size, focus=True) self.screen.draw_screen(self.size, canvas) keys = self.screen.get_input() + keys = self.double_press_input_filter(keys, None) for k in keys: if k == "window resize": self.size = self.screen.get_cols_rows() + elif urwid.is_mouse_event(k): + toplevel.mouse_event(self.size, *k, focus=True) else: toplevel.keypress(self.size, k) diff --git a/pudb/settings.py b/pudb/settings.py index e47ac754..3f50a002 100644 --- a/pudb/settings.py +++ b/pudb/settings.py @@ -79,6 +79,8 @@ def load_config(): conf_dict.setdefault("display", "auto") + conf_dict.setdefault("mouse_support", True) + conf_dict.setdefault("prompt_on_quit", True) def normalize_bool_inplace(name): @@ -92,6 +94,7 @@ def normalize_bool_inplace(name): normalize_bool_inplace("line_numbers") normalize_bool_inplace("wrap_variables") + normalize_bool_inplace("mouse_support") normalize_bool_inplace("prompt_on_quit") return conf_dict @@ -146,6 +149,9 @@ def _update_stringifier(): def _update_wrap_variables(): ui.update_var_view() + def _update_mouse_support(mouse_support): + ui.screen.set_mouse_tracking(mouse_support) + def _update_config(check_box, new_state, option_newvalue): option, newvalue = option_newvalue new_conf_dict = {option: newvalue} @@ -190,6 +196,11 @@ def _update_config(check_box, new_state, option_newvalue): conf_dict.update(new_conf_dict) _update_wrap_variables() + elif option == "mouse_support": + mouse_support = new_conf_dict["mouse_support"] = not check_box.get_state() + conf_dict.update(new_conf_dict) + _update_mouse_support(mouse_support) + heading = urwid.Text("This is the preferences screen for PuDB. " "Hit Ctrl-P at any time to get back to it.\n\n" "Configuration settings are saved in " @@ -331,6 +342,18 @@ def _update_config(check_box, new_state, option_newvalue): # }}} + + # {{{ mouse support + + cb_mouse_support = urwid.CheckBox("Mouse support", + bool(conf_dict["mouse_support"]), on_state_change=_update_config, + user_data=("mouse_support", None)) + + mouse_support_info = urwid.Text("Enable mouse support (for terminals that " + "support it)? See the help (hit '?') for information about what " + "different mouse buttons do.") + # }}} + lb_contents = ( [heading] + [urwid.AttrMap(urwid.Text("Line Numbers:\n"), "group head")] @@ -361,6 +384,10 @@ def _update_config(check_box, new_state, option_newvalue): + [urwid.AttrMap(urwid.Text("\nDisplay driver:\n"), "group head")] + [display_info] + display_rbs + + + [urwid.AttrMap(urwid.Text("\nMouse support:\n"), "group head")] + + [mouse_support_info] + + [cb_mouse_support] ) lb = urwid.ListBox(urwid.SimpleListWalker(lb_contents)) diff --git a/pudb/ui_tools.py b/pudb/ui_tools.py index 934df52a..b35e8180 100644 --- a/pudb/ui_tools.py +++ b/pudb/ui_tools.py @@ -1,3 +1,5 @@ +import time + import urwid from urwid.util import _target_encoding @@ -77,11 +79,15 @@ class SignalWrap(urwid.WidgetWrap): def __init__(self, w, is_preemptive=False): urwid.WidgetWrap.__init__(self, w) self.event_listeners = [] + self.mouse_event_listeners = [] self.is_preemptive = is_preemptive def listen(self, mask, handler): self.event_listeners.append((mask, handler)) + def listen_mouse_event(self, event, button, handler): + self.mouse_event_listeners.append((event, button, handler)) + def keypress(self, size, key): result = key @@ -101,9 +107,27 @@ def keypress(self, size, key): return result + def mouse_event(self, size, event, button, x, y, focus=True): + from pudb.debugger import CONFIG + if not CONFIG["mouse_support"]: + return False + + # Always select the element first, except for scroll events + if button not in [4, 5]: + super(SignalWrap, self).mouse_event(size, 'mouse press', 1, x, y, focus) -# {{{ debugger-specific stuff + result = self._w.mouse_event(size, event, button, x, y, focus) + if result is False: + for m_event, m_button, handler in self.mouse_event_listeners: + if (m_event is None or m_event == event) and (m_button is None or m_button == button): + return handler(self, size, event, button, x, y, focus) + + return result + + + +# debugger-specific stuff ----------------------------------------------------- class StackFrame(urwid.FlowWidget): def __init__(self, is_current, name, class_name, filename, line): self.is_current = is_current @@ -321,3 +345,48 @@ def keypress(self, size, key): return result # }}} + +class double_press_input_filter: + ''' + A filter generates new mouse event, double press. + + Usage: + + loop = urwid.MainLoop(..., input_filter=double_press_input_filter(), ...) + + When double-press the mouse buttons (1, 2, and 3. Wheels are ignored), the + handler shall receive events as follow, in order: + + ('mouse press', 1, 21, 14) + ('mouse release', 0, 21, 14) + ('mouse press', 1, 21, 14) + ('mouse double press', 1, 21, 14) + ('mouse release', 0, 21, 14) + ''' + last_press = None + last_press_time = -1 + double_press_timing = 0.25 + + @classmethod + def __call__(cls, events, raw): + i = 0 + while i < len(events): + e = events[i] + i += 1 + if not urwid.is_mouse_event(e) or not urwid.is_mouse_event(e[0]): + continue + + if cls.last_press and \ + time.time() > cls.last_press_time + cls.double_press_timing: + cls.last_press = None + + if cls.last_press: + if cls.last_press[1] == e[1]: + events.insert(i, (e[0].replace('press', 'double press'),) + e[1:]) + i += 1 + elif urwid.is_mouse_event(e[0]) and e[1] not in (4, 5): + cls.last_press = e + cls.last_press_time = time.time() + continue + cls.last_press = None + return events