def __init__(self, buf):
        self.edit_only = buf.worksheet.edit_only

        if not self.edit_only:
            buf.worksheet.sig_chunk_inserted.connect( self.on_chunk_inserted )
            buf.worksheet.sig_chunk_changed.connect( self.on_chunk_changed )
            buf.worksheet.sig_chunk_status_changed.connect( self.on_chunk_status_changed )
            buf.worksheet.sig_chunk_deleted.connect( self.on_chunk_deleted )
            buf.worksheet.sig_state.connect( self.on_notify_state )

            # Track changes to update completion
            buf.connect_after('insert-text', self.on_after_insert_text)
            buf.connect_after('delete-range', self.on_after_delete_range)
            buf.connect_after('end-user-action', self.on_after_end_user_action)

            self.__inserted_in_user_action = False
            self.__deleted_in_user_action = False

        if not self.edit_only:
            self.sidebar = Sidebar()
        else:
            self.sidebar = None

        buf.connect('add-custom-result', self.on_add_custom_result)
        buf.connect('add-sidebar-results', self.on_add_sidebar_results)
        buf.connect('remove-sidebar-results', self.on_remove_sidebar_results)
        buf.connect('pair-location-changed', self.on_pair_location_changed)
            
        gtk.TextView.__init__(self, buf)
        if not self.edit_only:
            self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, LEFT_MARGIN_WIDTH)
        self.set_left_margin(2)

        # Attach a "behavior object" to the view which, by ugly hacks, makes it
        # do simply and reasonable things for cut-and-paste and DND
        sanitize_textview_ipc.sanitize_view(self)

        self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)

        self.__completion_popup = CompletionPopup(self)
        self.__doc_popup = DocPopup()
        self.__mouse_over_object = None
        self.__mouse_over_timeout = None

        self.__mouse_over_start = buf.create_mark(None, buf.get_start_iter(), True)

        self.__arg_highlight_start = None
        self.__arg_highlight_end = None
        buf.connect('mark-set', self.on_mark_set)

        self.__cursor_chunk = None
        self.__scroll_to_result = False
        self.__scroll_to = buf.create_mark(None, buf.get_start_iter(), True)
        self.__scroll_idle = None

        self.__update_sidebar_positions_idle = 0
        self.__pixels_below_buffer = 0
        self.__last_chunk = None

        self.connect('destroy', self.on_destroy)
    def __init__(self, view):
        Popup.__init__(self)
        self.set_size_request(WIDTH, HEIGHT)
        
        self.__view = view

        sw = gtk.ScrolledWindow()
        self.add(sw)

        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        
        self.__tree_model = gtk.ListStore(str, str, object)
        self.__tree = gtk.TreeView(self.__tree_model)
        self.__tree.set_headers_visible(False)
        
        self.__tree.get_selection().connect('changed', self.__on_selection_changed)

        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn(None, cell, text=0)
        self.__tree.append_column(column)

        sw.add(self.__tree)
        sw.show_all()

        self.set_default_size(WIDTH, HEIGHT)

        # A small amount of background shows between the scrollbar and the list;
        # which looks ugly if it is the only gray thing in the window, so change
        # the window background to white
        self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535, 65535, 65535))

        self.__doc_popup= DocPopup(fixed_width=True, fixed_height=True, max_height=HEIGHT, can_focus=False)

        self._in_change = False
        self.showing = False
Exemple #3
0
    def __init__(self, buf):
        buf.worksheet.connect('chunk-inserted', self.on_chunk_inserted)
        buf.worksheet.connect('chunk-changed', self.on_chunk_changed)
        buf.worksheet.connect('chunk-status-changed', self.on_chunk_status_changed)
        buf.worksheet.connect('notify::state', self.on_notify_state)
        buf.connect('add-custom-result', self.on_add_custom_result)
        buf.connect('pair-location-changed', self.on_pair_location_changed)
            
        gtk.TextView.__init__(self, buf)
        self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, 10)
        self.set_left_margin(2)

        # Attach a "behavior object" to the view which, by ugly hacks, makes it
        # do simply and reasonable things for cut-and-paste and DND
        sanitize_textview_ipc.sanitize_view(self)

        self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)

        self.__completion_popup = CompletionPopup(self)
        self.__doc_popup = DocPopup()
        self.__mouse_over_object = None
        self.__mouse_over_timeout = None

        buf = self.get_buffer()
        self.__mouse_over_start = buf.create_mark(None, buf.get_start_iter(), True)
    def __init__(self, view):
        Popup.__init__(self)
        self.set_size_request(WIDTH, HEIGHT)
        
        self.__view = view

        sw = gtk.ScrolledWindow()
        self.add(sw)

        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        
        self.__tree_model = gtk.ListStore(str, str, object)
        self.__tree = gtk.TreeView(self.__tree_model)
        self.__tree.set_headers_visible(False)

        self.__tree.get_selection().connect('changed', self.__on_selection_changed)
        self.__tree.connect('row-activated', self.__on_row_activated)

        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn(None, cell, text=0)
        self.__tree.append_column(column)

        sw.add(self.__tree)
        sw.show_all()

        self.set_default_size(WIDTH, HEIGHT)

        # A small amount of background shows between the scrollbar and the list;
        # which looks ugly if it is the only gray thing in the window, so change
        # the window background to white
        self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535, 65535, 65535))

        self.__doc_popup= DocPopup(fixed_width=True, fixed_height=True, max_height=HEIGHT, can_focus=False)

        self._in_change = False
        self.spontaneous = False
        self.showing = False

        self.connect('destroy', self.on_destroy)
class CompletionPopup(Popup):
    
    """Class implementing a completion popup for ShellView

    This class encapsulates the user interface logic for completion
    popups. The actual code to determine possible completions lives in
    tokenized_statement.py. 
    
    """
    
    def __init__(self, view):
        Popup.__init__(self)
        self.set_size_request(WIDTH, HEIGHT)
        
        self.__view = view

        sw = gtk.ScrolledWindow()
        self.add(sw)

        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        
        self.__tree_model = gtk.ListStore(str, str, object)
        self.__tree = gtk.TreeView(self.__tree_model)
        self.__tree.set_headers_visible(False)
        
        self.__tree.get_selection().connect('changed', self.__on_selection_changed)

        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn(None, cell, text=0)
        self.__tree.append_column(column)

        sw.add(self.__tree)
        sw.show_all()

        self.set_default_size(WIDTH, HEIGHT)

        # A small amount of background shows between the scrollbar and the list;
        # which looks ugly if it is the only gray thing in the window, so change
        # the window background to white
        self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535, 65535, 65535))

        self.__doc_popup= DocPopup(fixed_width=True, fixed_height=True, max_height=HEIGHT, can_focus=False)

        self._in_change = False
        self.showing = False

    def __update_completions(self):
        buf = self.__view.get_buffer()

        self.__in_change = True
        self.__tree_model.clear()
        line, offset = buf.iter_to_pos(buf.get_iter_at_mark(buf.get_insert()), adjust=ADJUST_NONE)
        if line == None:
            completions = []
        else:
            completions = buf.worksheet.find_completions(line, offset)
        for display, completion, obj in completions:
            self.__tree_model.append([display, completion, obj])
        
        self.__tree.set_cursor(0)
        self.__in_change = False
        self.__update_doc_popup()

    def __update_position(self):
        buf = self.__view.get_buffer()
        
        self.position_at_location(self.__view,
                                  buf.get_iter_at_mark(buf.get_insert()))

    def __update_doc_popup(self):
        if not self.showing:
            self.__doc_popup.popdown()
            return

        model, iter = self.__tree.get_selection().get_selected()
        if not iter:
            self.__doc_popup.popdown()
            return

        obj = model.get_value(iter, 2)

        # Long term it would be nice to preview the value of the
        # object, but it's distracting to show the class docs on int
        # for every integer constant, etc, which is what the DocPopup
        # does currently.
        if (obj == None or is_data_object(obj)):
            self.__doc_popup.popdown()
            return
        
        self.__doc_popup.set_target(obj)
        self.__doc_popup.popup()

    def __insert_selected(self):
        model, iter = self.__tree.get_selection().get_selected()
        completion = model.get_value(iter, 1)

        self.__view.get_buffer().insert_interactive_at_cursor(completion, True)
            
    def __on_selection_changed(self, selection):
        if not self.__in_change:
            self.__update_doc_popup()
        
    def popup(self):
        """Pop up the completion popup.

        If there are no possibilities completion at the insert cursor
        location, the popup is not popped up. If there is exactly one
        possibility, then completion is done immediately to that one
        possibility.

        """
        
        if self.showing:
            return
        
        self.__update_completions()
        if len(self.__tree_model) < 2:
            if len(self.__tree_model) == 0:
                return
            self.__insert_selected()
            return
        
        self.__update_position()

        self.show()
        self.showing = True

        self.__doc_popup.position_next_to_window(self)
        self.__update_doc_popup()

        self.focus()

    def update(self):
        """Update the completion popup after the cursor is moved, or text is inserted.

        If there are no completion possibilities at the cursor when this is called,
        the popup is popped down.

        """
        
        if not self.showing:
            return
        
        self.__update_completions()
        if len(self.__tree_model) == 0:
            self.popdown()
            return
        
        self.__update_position()
        
    def popdown(self):
        """Hide the completion if it is currently showing"""

        if not self.showing:
            return

        self.showing = False
        self.focused = False
        
        if self.__doc_popup.showing:
            self.__doc_popup.popdown()
        
        self.hide()

    def on_key_press_event(self, event):
        """Do key press handling while the popup is active.

        Returns True if the key press is handled, False otherwise.

        """
        
        if event.keyval == gtk.keysyms.Escape:
            self.popdown()
            return True
        elif event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
            self.__insert_selected()
            self.popdown()
            return True
        # These keys are forwarded to the popup to move the selected row
        elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.KP_Up,
                              gtk.keysyms.Down, gtk.keysyms.KP_Down,
                              gtk.keysyms.Page_Up, gtk.keysyms.KP_Page_Up,
                              gtk.keysyms.Page_Down, gtk.keysyms.KP_Page_Down):
            self.event(event)
            return True

        return False
class CompletionPopup(Popup):

    """Class implementing a completion popup for ShellView

    This class encapsulates the user interface logic for completion
    popups. The actual code to determine possible completions lives in
    tokenized_statement.py. 
    
    """

    def __init__(self, view):
        Popup.__init__(self)
        self.set_size_request(WIDTH, HEIGHT)

        self.__view = view

        sw = gtk.ScrolledWindow()
        self.add(sw)

        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

        self.__tree_model = gtk.ListStore(str, str, object)
        self.__tree = gtk.TreeView(self.__tree_model)
        self.__tree.set_headers_visible(False)

        self.__tree.get_selection().connect("changed", self.__on_selection_changed)
        self.__tree.connect("row-activated", self.__on_row_activated)

        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn(None, cell, text=0)
        self.__tree.append_column(column)

        sw.add(self.__tree)
        sw.show_all()

        self.set_default_size(WIDTH, HEIGHT)

        # A small amount of background shows between the scrollbar and the list;
        # which looks ugly if it is the only gray thing in the window, so change
        # the window background to white
        self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535, 65535, 65535))

        self.__doc_popup = DocPopup(fixed_width=True, fixed_height=True, max_height=HEIGHT, can_focus=False)

        self._in_change = False
        self.spontaneous = False
        self.showing = False

        self.connect("destroy", self.on_destroy)

    def __update_completions(self, spontaneous=False):
        buf = self.__view.get_buffer()

        self.__in_change = True
        self.__tree_model.clear()
        line, offset = buf.iter_to_pos(buf.get_iter_at_mark(buf.get_insert()), adjust=ADJUST_NONE)
        if line is None:
            completions = []
        else:
            if spontaneous:
                min_length = SPONTANEOUS_MIN_LENGTH
            else:
                min_length = 0
            completions = buf.worksheet.find_completions(line, offset, min_length)
        for display, completion, obj in completions:
            self.__tree_model.append([display, completion, obj])

        if len(completions) > 0:
            self.__tree.set_cursor(0)
        self.__in_change = False
        self.__update_doc_popup()

    def __update_position(self):
        buf = self.__view.get_buffer()

        self.position_at_location(self.__view, buf.get_iter_at_mark(buf.get_insert()))

    def __update_doc_popup(self):
        if not self.showing:
            self.__doc_popup.popdown()
            return

        model, iter = self.__tree.get_selection().get_selected()
        if not iter:
            self.__doc_popup.popdown()
            return

        obj = model.get_value(iter, 2)

        # Long term it would be nice to preview the value of the
        # object, but it's distracting to show the class docs on int
        # for every integer constant, etc, which is what the DocPopup
        # does currently.
        if obj is None or is_data_object(obj):
            self.__doc_popup.popdown()
            return

        self.__doc_popup.set_target(obj)
        self.__doc_popup.popup()

    def __insert_completion(self, iter):
        completion = self.__tree_model.get_value(iter, 1)
        obj = self.__tree_model.get_value(iter, 2)

        buf = self.__view.get_buffer()
        default_editable = self.__view.get_editable()

        buf.insert_interactive_at_cursor(completion, default_editable)
        if inspect.isclass(obj) or inspect.isroutine(obj):
            # Show the doc popup to give the user information about what arguments
            # are posssible/required
            self.__view.show_doc_popup()

            # Insert a () and put the cursor in the middle
            buf.insert_interactive_at_cursor("(", default_editable)
            insert = buf.get_iter_at_mark(buf.get_insert())
            mark_between_parens = buf.create_mark(None, insert, left_gravity=True)
            buf.insert_interactive_at_cursor(")", default_editable)
            iter = buf.get_iter_at_mark(mark_between_parens)
            self.__view.highlight_arg_region(iter, iter)
            buf.place_cursor(iter)
            buf.delete_mark(mark_between_parens)

    def __insert_selected(self):
        model, iter = self.__tree.get_selection().get_selected()
        self.__insert_completion(iter)

    def __on_selection_changed(self, selection):
        if not self.__in_change:
            self.__update_doc_popup()

    def __on_row_activated(self, view, path, column):
        self.__insert_completion(self.__tree_model.get_iter(path))
        self.popdown()

    def popup(self, spontaneous=False):
        """Pop up the completion popup.

        If there are no possibilities completion at the insert cursor
        location, the popup is not popped up. If there is exactly one
        possibility and the spontaneous parameter is not provided , then
        completion is done immediately to that one possibility.

        @param spontaneous set to True if we're popping this up as a result
           of editing, rather than because of an explicit key shortcut.

        """

        self.__update_completions(spontaneous=spontaneous)
        num_completions = len(self.__tree_model)
        if num_completions == 0:
            return
        elif num_completions == 1 and not spontaneous:
            self.__insert_selected()
            return

        self.__update_position()

        self.spontaneous = spontaneous

        if self.showing:
            return

        self.show()
        self.showing = True

        self.__doc_popup.position_next_to_window(self)
        self.__update_doc_popup()

        self.focus()

    def update(self):
        """Update the completion popup after the cursor is moved, or text is inserted.

        If there are no completion possibilities at the cursor when this is called,
        the popup is popped down.

        """

        if not self.showing:
            return

        self.__update_completions(spontaneous=self.spontaneous)
        if len(self.__tree_model) == 0:
            self.popdown()
            return

        self.__update_position()

    def popdown(self):
        """Hide the completion if it is currently showing"""

        if not self.showing:
            return

        self.showing = False
        self.focused = False

        if self.__doc_popup.showing:
            self.__doc_popup.popdown()

        self.hide()

    def on_destroy(self, obj):
        self.__doc_popup.destroy()
        self.__doc_popup = None

    def on_key_press_event(self, event):
        """Do key press handling while the popup is active.

        Returns True if the key press is handled, False otherwise.

        """

        if event.keyval == gtk.keysyms.Escape:
            self.popdown()
            return True
        elif event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
            self.__insert_selected()
            self.popdown()
            return True
        # These keys are forwarded to the popup to move the selected row
        elif event.keyval in (
            gtk.keysyms.Up,
            gtk.keysyms.KP_Up,
            gtk.keysyms.Down,
            gtk.keysyms.KP_Down,
            gtk.keysyms.Page_Up,
            gtk.keysyms.KP_Page_Up,
            gtk.keysyms.Page_Down,
            gtk.keysyms.KP_Page_Down,
        ):
            self.event(event)
            return True

        return False
Exemple #7
0
class ShellView(gtk.TextView):
    __gsignals__ = {
        'backspace' : 'override',
        'expose-event': 'override',
        'focus-out-event': 'override',
        'button-press-event': 'override',
        'button-release-event': 'override',
        'motion-notify-event': 'override',
        'key-press-event': 'override',
        'leave-notify-event': 'override',
        'motion-notify-event': 'override',
        'realize': 'override',
        'unrealize': 'override',
        'size-allocate': 'override'
   }
        
    def __init__(self, buf):
        self.edit_only = buf.worksheet.edit_only

        if not self.edit_only:
            buf.worksheet.connect('chunk-inserted', self.on_chunk_inserted)
            buf.worksheet.connect('chunk-changed', self.on_chunk_changed)
            buf.worksheet.connect('chunk-status-changed', self.on_chunk_status_changed)
            buf.worksheet.connect('notify::state', self.on_notify_state)

            # Track changes to update completion
            buf.connect_after('insert-text', self.on_after_insert_text)
            buf.connect_after('delete-range', self.on_after_delete_range)
            buf.connect_after('end-user-action', self.on_after_end_user_action)

            self.__inserted_in_user_action = False
            self.__deleted_in_user_action = False

        buf.connect('add-custom-result', self.on_add_custom_result)
        buf.connect('pair-location-changed', self.on_pair_location_changed)
            
        gtk.TextView.__init__(self, buf)
        if not self.edit_only:
            self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, LEFT_MARGIN_WIDTH)
        self.set_left_margin(2)

        # Attach a "behavior object" to the view which, by ugly hacks, makes it
        # do simply and reasonable things for cut-and-paste and DND
        sanitize_textview_ipc.sanitize_view(self)

        self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)

        self.__completion_popup = CompletionPopup(self)
        self.__doc_popup = DocPopup()
        self.__mouse_over_object = None
        self.__mouse_over_timeout = None

        self.__mouse_over_start = buf.create_mark(None, buf.get_start_iter(), True)

        self.__arg_highlight_start = None
        self.__arg_highlight_end = None
        buf.connect('mark-set', self.on_mark_set)

    def __get_worksheet_line_yrange(self, line):
        buffer_line = self.get_buffer().pos_to_iter(line)
        return self.get_line_yrange(buffer_line)

    def __get_worksheet_line_at_y(self, y, adjust):
        buf = self.get_buffer()
        (buffer_line, _) = self.get_line_at_y(y)
        return buf.iter_to_pos(buffer_line, adjust)[0]

    def paint_chunk(self, cr, area, chunk, fill_color, outline_color):
        buf = self.get_buffer()
        
        (y, _) = self.__get_worksheet_line_yrange(chunk.start)
        (end_y, end_height) = self.__get_worksheet_line_yrange(chunk.end - 1)
        height = end_y + end_height - y
        
        (_, window_y) = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, y)
        cr.rectangle(area.x, window_y, area.width, height)
        cr.set_source_rgb(*fill_color)
        cr.fill()
                
        cr.rectangle(0.5, window_y + 0.5, LEFT_MARGIN_WIDTH - 1, height - 1)
        cr.set_source_rgb(*outline_color)
        cr.set_line_width(1)
        cr.stroke()

    def do_realize(self):
        gtk.TextView.do_realize(self)

        if not self.edit_only:
            self.get_window(gtk.TEXT_WINDOW_LEFT).set_background(self.style.white)

        # While the the worksheet is executing, we want to display a watch cursor
        # Trying to override the cursor setting of GtkTextView is really hard because
        # of things like hiding the cursor when typing, so we take the simple approach
        # of using an input-only "cover window" that we set the cursor on and
        # show on top of the GtkTextView's normal window.

        self.__watch_window = gtk.gdk.Window(self.window,
                                             self.allocation.width, self.allocation.height,
                                             gtk.gdk.WINDOW_CHILD,
                                             (gtk.gdk.SCROLL_MASK |
                                              gtk.gdk.BUTTON_PRESS_MASK |
                                              gtk.gdk.BUTTON_RELEASE_MASK |
                                              gtk.gdk.POINTER_MOTION_MASK |
                                              gtk.gdk.POINTER_MOTION_HINT_MASK),
                                             gtk.gdk.INPUT_ONLY,
                                             x=0, y=0)
        self.__watch_window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
        self.__watch_window.set_user_data(self)

        if self.get_buffer().worksheet.state == NotebookFile.EXECUTING:
            self.__watch_window.show()
            self.__watch_window.raise_()

    def do_unrealize(self):
        self.__watch_window.set_user_data(None)
        self.__watch_window.destroy()
        self.__watch_window = None
        gtk.TextView.do_unrealize(self)

    def do_size_allocate(self, allocation):
        gtk.TextView.do_size_allocate(self, allocation)
        if (self.flags() & gtk.REALIZED) != 0:
            self.__watch_window.resize(allocation.width, allocation.height)

    def __expose_window_left(self, event):
        (_, start_y) = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y)
        start_line = self.__get_worksheet_line_at_y(start_y, adjust=ADJUST_AFTER)
        
        (_, end_y) = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y + event.area.height - 1)
        end_line = self.__get_worksheet_line_at_y(end_y, adjust=ADJUST_BEFORE)

        buf = self.get_buffer()

        cr = event.window.cairo_create()

        for chunk in buf.worksheet.iterate_chunks(start_line, end_line + 1):
            if isinstance(chunk, StatementChunk):
                if chunk.executing:
                    self.paint_chunk(cr, event.area, chunk, (0, 1, 0), (0, 0.5, 0))
                elif chunk.error_message is not None:
                    self.paint_chunk(cr, event.area, chunk, (1, 0, 0), (0.5, 0, 0))
                elif chunk.needs_compile:
                    self.paint_chunk(cr, event.area, chunk, (1, 1, 0), (0.5, 0.5, 0))
                elif chunk.needs_execute:
                    self.paint_chunk(cr, event.area, chunk, (1, 0, 1), (0.5, 0.5, 0))
                else:
                    self.paint_chunk(cr, event.area, chunk, (0, 0, 1), (0, 0, 0.5))

    def __draw_rect_outline(self, event, rect):
        if (rect.y + rect.height <= event.area.y or rect.y >= event.area.y + event.area.height):
            return

        cr = event.window.cairo_create()
        cr.set_line_width(1.)
        cr.rectangle(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1)
        cr.set_source_rgb(0.6, 0.6, 0.6)
        cr.stroke()

    def __expose_arg_highlight(self, event):
        buf = self.get_buffer()

        # We want a rectangle enclosing the range between arg_highlight_start and
        # arg_highlight_end; the method here isn't correct in the presence of
        # RTL text, but the necessary Pango functionality isn't exposed to
        # a GtkTextView user. RTL code is rare. We also don't handle multiple-line
        # highlight regions right. (Return ends the highlight, so you'd need to paste)
        rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_start))
        rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                                      rect.x, rect.y)
        rect.width = 0
        end_rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_end))
        end_rect.x, end_rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                                              end_rect.x, end_rect.y)
        end_rect.width = 0

        rect = rect.union(end_rect)

        self.__draw_rect_outline(event, rect)

    def __expose_pair_location(self, event):
        pair_location = self.get_buffer().get_pair_location()
        if pair_location is None:
            return
        
        rect = self.get_iter_location(pair_location)

        rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, rect.x, rect.y)

        self.__draw_rect_outline(event, rect)

    def do_expose_event(self, event):
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
            self.__expose_window_left(event)
            return False
        
        gtk.TextView.do_expose_event(self, event)

        if event.window == self.get_window(gtk.TEXT_WINDOW_TEXT):
            if self.__arg_highlight_start:
                self.__expose_arg_highlight(event)
            else:
                self.__expose_pair_location(event)
        
        return False

    # This is likely overengineered, since we're going to try as hard as possible not to
    # have tabs in our worksheets. We don't do the funky handling of \f.
    def __count_indent(self, text):
        indent = 0
        for c in text:
            if c == ' ':
                indent += 1
            elif c == '\t':
                indent += 8 - (indent % 8)
            else:
                break

        return indent

    def __find_outdent(self, iter):
        buf = self.get_buffer()
        line, _ = buf.iter_to_pos(iter)

        current_indent = self.__count_indent(buf.worksheet.get_line(line))

        while line > 0:
            line -= 1
            line_text = buf.worksheet.get_line(line)
            # Empty lines don't establish indentation
            if ALL_WHITESPACE_RE.match(line_text):
                continue

            indent = self.__count_indent(line_text)
            if indent < current_indent:
                return re.match(r"^[\t ]*", line_text).group(0)

        return ""

    def __find_default_indent(self, iter):
        buf = self.get_buffer()
        line, offset = buf.iter_to_pos(iter)

        while line > 0:
            line -= 1
            chunk = buf.worksheet.get_chunk(line)
            if isinstance(chunk, StatementChunk):
                return chunk.tokenized.get_next_line_indent(line - chunk.start)
            elif isinstance(chunk, CommentChunk):
                return " " * self.__count_indent(buf.worksheet.get_line(line))

        return ""

    def __reindent_line(self, iter, indent_text):
        buf = self.get_buffer()

        line, pos = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
        if line == None:
            return

        line_text = buf.worksheet.get_line(line)
        prefix = re.match(r"^[\t ]*", line_text).group(0)

        diff = self.__count_indent(indent_text) - self.__count_indent(prefix)
        if diff == 0:
            return 0

        common_len = 0
        for a, b in zip(prefix, indent_text):
            if a != b:
                break
            common_len += 1
    
        start = iter.copy()
        start.set_line_offset(common_len)
        end = iter.copy()
        end.set_line_offset(len(prefix))

        # Nitpicky-detail. If the selection starts at the start of the line, and we are
        # inserting white-space there, then the whitespace should be *inside* the selection
        mark_to_start = None
        if common_len == 0 and buf.get_has_selection():
            mark = buf.get_insert()
            if buf.get_iter_at_mark(mark).compare(start) == 0:
                mark_to_start = mark
                
            mark = buf.get_selection_bound()
            if buf.get_iter_at_mark(mark).compare(start) == 0:
                mark_to_start = mark
        
        buf.delete(start, end)
        buf.insert(end, indent_text[common_len:])

        if mark_to_start is not None:
            end.set_line_offset(0)
            buf.move_mark(mark_to_start, end)

        return diff

    def __reindent_selection(self, outdent):
        buf = self.get_buffer()

        bounds = buf.get_selection_bounds()
        if bounds == ():
            insert_mark = buf.get_insert()
            bounds = buf.get_iter_at_mark(insert_mark), buf.get_iter_at_mark(insert_mark)
        start, end = bounds

        line, _ = buf.iter_to_pos(start, adjust=ADJUST_AFTER)
        end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)
        if end_offset == 0 and end_line > line:
            end_line -= 1

        iter = buf.pos_to_iter(line)

        if outdent:
            indent_text = self.__find_outdent(iter)
        else:
            indent_text = self.__find_default_indent(iter)

        diff = self.__reindent_line(iter, indent_text)
        while True:
            line += 1
            if line > end_line:
                return

            iter = buf.pos_to_iter(line)
            current_indent = self.__count_indent(buf.worksheet.get_line(line))
            self.__reindent_line(iter, max(0, " " * (current_indent + diff)))

    def __hide_completion(self):
        if self.__completion_popup.showing:
            self.__completion_popup.popdown()
            
    def do_focus_out_event(self, event):
        self.__hide_completion()
        self.__doc_popup.popdown()
        return gtk.TextView.do_focus_out_event(self, event)

    def __rewrite_window(self, event):
        # Mouse events on the "watch window" that covers the text view
        # during calculation need to be forwarded to the real text window
        # since it looks bad if keynav works, but you can't click on the
        # window to set the cursor, select text, and so forth

        if event.window == self.__watch_window:
            event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)

        # Events on the left-margin window also get written so the user can
        # click there when starting a drag selection. We need to adjust the
        # X coordinate in that case
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
            event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)
            if event.type == gtk.gdk._3BUTTON_PRESS:
                # Workaround for http://bugzilla.gnome.org/show_bug.cgi?id=573664
                event.x = 50.
            else:
                event.x -= LEFT_MARGIN_WIDTH

    def do_button_press_event(self, event):
        self.__rewrite_window(event)

        self.__doc_popup.popdown()

        return gtk.TextView.do_button_press_event(self, event)

    def do_button_release_event(self, event):
        self.__rewrite_window(event)

        return gtk.TextView.do_button_release_event(self, event)

    def do_motion_notify_event(self, event):
        self.__rewrite_window(event)

        return gtk.TextView.do_motion_notify_event(self, event)

    def __remove_arg_highlight(self, cursor_to_end=True):
        buf = self.get_buffer()

        end = buf.get_iter_at_mark (self.__arg_highlight_end)

        buf.delete_mark(self.__arg_highlight_start)
        self.__arg_highlight_start = None
        buf.delete_mark(self.__arg_highlight_end)
        self.__arg_highlight_end = None

        if cursor_to_end:
            # If the arg_highlight ends at closing punctuation, skip over it
            tmp = end.copy()
            tmp.forward_char()
            text = buf.get_slice(end, tmp)

            if text in (")", "]", "}"):
                buf.place_cursor(tmp)
            else:
                buf.place_cursor(end)

    def do_key_press_event(self, event):
        buf = self.get_buffer()

        if self.__completion_popup.focused and self.__completion_popup.on_key_press_event(event):
            return True
        
        if self.__doc_popup.focused:
            self.__doc_popup.on_key_press_event(event)
            return True

        if not self.edit_only and event.keyval in (gtk.keysyms.F2, gtk.keysyms.KP_F2):
            self.__hide_completion()

            if self.__doc_popup.showing:
                self.__doc_popup.focus()
            else:
                self.show_doc_popup(focus_popup=True)

            return True

        if not self.__arg_highlight_start:
            self.__doc_popup.popdown()
        
        if event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
            self.__hide_completion()

            if self.__arg_highlight_start:
                self.__remove_arg_highlight()
                self.__doc_popup.popdown()
                return True
            
            increase_indent = False
            insert = buf.get_iter_at_mark(buf.get_insert())
            line, pos = buf.iter_to_pos(insert, adjust=ADJUST_NONE)

            # Inserting return inside a ResultChunk would normally do nothing
            # but we want to make it insert a line after the chunk
            if line is None and not buf.get_has_selection():
                line, pos = buf.iter_to_pos(insert, adjust=ADJUST_BEFORE)
                iter = buf.pos_to_iter(line, -1)
                buf.place_cursor(iter)
                buf.insert_interactive(iter, "\n", True)
                
                return True

            buf.begin_user_action()
            
            gtk.TextView.do_key_press_event(self, event)
            # We need the chunks to be updated when computing the line indents
            buf.worksheet.rescan()

            insert = buf.get_iter_at_mark(buf.get_insert())
            
            self.__reindent_line(insert, self.__find_default_indent(insert))

            # If we have two comment lines in a row, assume a block comment
            if (line > 0 and
                isinstance(buf.worksheet.get_chunk(line), CommentChunk) and
                isinstance(buf.worksheet.get_chunk(line - 1), CommentChunk)):
                self.get_buffer().insert_interactive_at_cursor("# ", True)

            buf.end_user_action()
                
            return True
        elif event.keyval in (gtk.keysyms.Tab, gtk.keysyms.KP_Tab) and event.state & gtk.gdk.CONTROL_MASK == 0:
            buf.begin_user_action()
            self.__reindent_selection(outdent=False)
            buf.end_user_action()

            return True
        elif event.keyval == gtk.keysyms.ISO_Left_Tab and event.state & gtk.gdk.CONTROL_MASK == 0:
            buf.begin_user_action()
            self.__reindent_selection(outdent=True)
            buf.end_user_action()

            return True
        elif event.keyval == gtk.keysyms.space and event.state & gtk.gdk.CONTROL_MASK != 0:
            if self.__completion_popup.showing:
                if self.__completion_popup.spontaneous:
                    self.__completion_popup.popup(spontaneous=False)
                else:
                    self.__completion_popup.popdown()
            else:
                if self.__doc_popup.showing:
                    self.__doc_popup.popdown()
                self.__completion_popup.popup(spontaneous=False)
            return True
        elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
                (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
                (event.state & gtk.gdk.SHIFT_MASK) == 0:
            buf.worksheet.undo()
            
            return True
        # This is the gedit/gtksourceview binding to cause your hands to fall off
        elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
                (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
                (event.state & gtk.gdk.SHIFT_MASK) != 0:
            buf.worksheet.redo()
            
            return True
        # This is the familiar binding (Eclipse, etc). Firefox supports both
        elif event.keyval in (gtk.keysyms.y, gtk.keysyms.Y) and event.state & gtk.gdk.CONTROL_MASK != 0:
            buf.worksheet.redo()

            return True
        
        return gtk.TextView.do_key_press_event(self, event)

    def __show_mouse_over(self):
        self.__mouse_over_timeout = None
        
        if self.__completion_popup.showing:
            return
        
        self.__doc_popup.set_target(self.__mouse_over_object)
        location = self.get_buffer().get_iter_at_mark(self.__mouse_over_start)
        self.__doc_popup.position_at_location(self, location)
        self.__doc_popup.popup()

        return False
        
    def __stop_mouse_over(self):
        if self.__mouse_over_timeout:
            gobject.source_remove(self.__mouse_over_timeout)
            self.__mouse_over_timeout = None
            
        self.__mouse_over_object = None
        
    def do_motion_notify_event(self, event):
        # Successful mousing-over depends on knowing the types of symbols so doing the
        # checks are pointless in edit-only mode
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_TEXT) and not self.__doc_popup.focused:
            buf = self.get_buffer()
            
            x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(event.x), int(event.y))
            iter, _ = self.get_iter_at_position(x, y)
            line, offset = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
            if line is not None:
                obj, start_line, start_offset, _,_ = buf.worksheet.get_object_at_location(line, offset)
            else:
                obj = None

            if not obj is self.__mouse_over_object:
                self.__stop_mouse_over()
                self.__doc_popup.popdown()
                if obj is not None:
                    start = buf.pos_to_iter(start_line, start_offset)
                    buf.move_mark(self.__mouse_over_start, start)

                    self.__mouse_over_object = obj
                    try:
                        timeout = self.get_settings().get_property('gtk-tooltip-timeout')
                    except TypeError: # GTK+ < 2.12
                        timeout = 500
                    self.__mouse_over_timeout = gobject.timeout_add(timeout, self.__show_mouse_over)
                
        return gtk.TextView.do_motion_notify_event(self, event)

    def do_leave_notify_event(self, event):
        self.__stop_mouse_over()
        if not self.__doc_popup.focused:
            self.__doc_popup.popdown()
        return False

    def do_backspace(self):
        buf = self.get_buffer()
        
        insert = buf.get_iter_at_mark(buf.get_insert())
        line, offset = buf.iter_to_pos(insert)
        
        current_chunk = buf.worksheet.get_chunk(line)
        if isinstance(current_chunk, StatementChunk) or isinstance(current_chunk, BlankChunk):
            line_text = buf.worksheet.get_line(line)[0:offset]

            if re.match(r"^[\t ]+$", line_text):
                self.__reindent_line(insert, self.__find_outdent(insert))
                return
                       
        return gtk.TextView.do_backspace(self)

    def __invalidate_status(self, chunk):
        buf = self.get_buffer()
        
        (start_y, start_height) = self.__get_worksheet_line_yrange(chunk.start)
        (end_y, end_height) = self.__get_worksheet_line_yrange(chunk.end - 1)

        (_, window_y) = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, start_y)

        if self.window:
            left_margin_window = self.get_window(gtk.TEXT_WINDOW_LEFT)
            left_margin_window.invalidate_rect((0, window_y, LEFT_MARGIN_WIDTH, end_y + end_height - start_y),
                                               False)

    def on_chunk_inserted(self, worksheet, chunk):
        self.__invalidate_status(chunk)

    def on_chunk_changed(self, worksheet, chunk, changed_lines):
        self.__invalidate_status(chunk)

    def on_chunk_status_changed(self, worksheet, chunk):
        self.__invalidate_status(chunk)

    def on_notify_state(self, worksheet, param_spec):
        if (self.flags() & gtk.REALIZED) != 0:
            if worksheet.state == NotebookFile.EXECUTING:
                self.__watch_window.show()
                self.__watch_window.raise_()
            else:
                self.__watch_window.hide()

    def on_after_insert_text(self, buf, location, text, len):
        if buf.worksheet.in_user_action() and not buf.in_modification():
            self.__inserted_in_user_action = True

    def on_after_delete_range(self, buf, start, end):
        if buf.worksheet.in_user_action() and not buf.in_modification():
            self.__deleted_in_user_action = True

    def on_after_end_user_action(self, buf):
        if not buf.worksheet.in_user_action():
            if self.__completion_popup.showing:
                if self.__inserted_in_user_action or self.__deleted_in_user_action:
                    self.__completion_popup.update()
            else:
                if self.__inserted_in_user_action and global_settings.autocomplete:
                    self.__completion_popup.popup(spontaneous=True)
            self.__inserted_in_user_action = False
            self.__deleted_in_user_action = False

    def on_add_custom_result(self, buf, result, anchor):
        widget = result.create_widget()
        widget.show()
        self.add_child_at_anchor(widget, anchor)

    def on_mark_set(self, buffer, iter, mark):
        if self.__arg_highlight_start:
            # See if the user moved the cursor out of the highlight regionb
            buf = self.get_buffer()
            if mark != buf.get_insert():
                return

            if (iter.compare(buf.get_iter_at_mark(self.__arg_highlight_start)) < 0 or
                iter.compare(buf.get_iter_at_mark(self.__arg_highlight_end)) > 0):
                self.__remove_arg_highlight(cursor_to_end=False)

    def __invalidate_char_position(self, iter):
        y, height = self.get_line_yrange(iter)
        if self.window:
            text_window = self.get_window(gtk.TEXT_WINDOW_TEXT)
            width, _ = text_window.get_size()
            text_window.invalidate_rect((0, y, width, height), False)
        
    def on_pair_location_changed(self, buf, old_position, new_position):
        if old_position:
            self.__invalidate_char_position(old_position)
        if new_position:
            self.__invalidate_char_position(new_position)

    def calculate(self):
        buf = self.get_buffer()

        buf.worksheet.calculate()

        # This is a hack to work around the fact that scroll_mark_onscreen()
        # doesn't wait for a size-allocate cycle, so doesn't properly handle
        # embedded request widgets
        self.size_request()
        self.size_allocate((self.allocation.x, self.allocation.y,
                            self.allocation.width, self.allocation.height))

        self.scroll_mark_onscreen(buf.get_insert())

    def copy_as_doctests(self):
        buf = self.get_buffer()

        bounds = buf.get_selection_bounds()
        if bounds == ():
            start, end = buf.get_iter_at_mark(buf.get_insert())
        else:
            start, end = bounds

        start_line, start_offset = buf.iter_to_pos(start, adjust=ADJUST_BEFORE)
        end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)

        doctests = buf.worksheet.get_doctests(start_line, end_line + 1)
        self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD).set_text(doctests)

    def show_doc_popup(self, focus_popup=False):
        """Pop up the doc popup for the symbol at the insertion point, if any

        @param focus_popup: if True, the popup will be given keyboard focus

        """

        buf = self.get_buffer()

        insert = buf.get_iter_at_mark(buf.get_insert())
        line, offset = buf.iter_to_pos(insert, adjust=ADJUST_NONE)
        if line is not None:
            obj, start_line, start_offset, _, _ = buf.worksheet.get_object_at_location(line, offset, include_adjacent=True)
        else:
            obj = None

        if obj is not None:
            start = buf.pos_to_iter(start_line, start_offset)
            self.__stop_mouse_over()
            self.__doc_popup.set_target(obj)
            self.__doc_popup.position_at_location(self, start)
            if focus_popup:
                self.__doc_popup.popup_focused()
            else:
                self.__doc_popup.popup()

    def highlight_arg_region(self, start, end):
        """Highlight the region between start and end for argument insertion.
        A box will be drawn around the region as long as the cursor is inside
        the region. If the user hits return, the cursor will be moved to the
        end (and past a single closing punctuation at the end, if any.)

        @param start iter at the start of the highlight region
        @param end iter at the end of the highlight region

        """

        buf = self.get_buffer()

        self.__arg_highlight_start = buf.create_mark(None, start, left_gravity=True)
        self.__arg_highlight_end = buf.create_mark(None, end, left_gravity=False)
class CompletionPopup(Popup):
    
    """Class implementing a completion popup for ShellView

    This class encapsulates the user interface logic for completion
    popups. The actual code to determine possible completions lives in
    tokenized_statement.py. 
    
    """
    
    def __init__(self, view):
        Popup.__init__(self)
        self.set_size_request(WIDTH, HEIGHT)
        
        self.__view = view

        sw = gtk.ScrolledWindow()
        self.add(sw)

        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        
        self.__tree_model = gtk.ListStore(str, str, object)
        self.__tree = gtk.TreeView(self.__tree_model)
        self.__tree.set_headers_visible(False)

        self.__tree.get_selection().connect('changed', self.__on_selection_changed)
        self.__tree.connect('row-activated', self.__on_row_activated)

        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn(None, cell, text=0)
        self.__tree.append_column(column)

        sw.add(self.__tree)
        sw.show_all()

        self.set_default_size(WIDTH, HEIGHT)

        # A small amount of background shows between the scrollbar and the list;
        # which looks ugly if it is the only gray thing in the window, so change
        # the window background to white
        self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535, 65535, 65535))

        self.__doc_popup= DocPopup(fixed_width=True, fixed_height=True, max_height=HEIGHT, can_focus=False)

        self._in_change = False
        self.spontaneous = False
        self.showing = False

        self.connect('destroy', self.on_destroy)

    def __update_completions(self, spontaneous=False):
        buf = self.__view.get_buffer()

        self.__in_change = True
        self.__tree_model.clear()
        line, offset = buf.iter_to_pos(buf.get_iter_at_mark(buf.get_insert()), adjust=ADJUST_NONE)
        if line is None:
            completions = []
        else:
            if spontaneous:
                min_length = SPONTANEOUS_MIN_LENGTH
            else:
                min_length = 0
            completions = buf.worksheet.find_completions(line, offset, min_length)
        for display, completion, obj in completions:
            self.__tree_model.append([display, completion, obj])

        if len(completions) > 0:
            self.__tree.set_cursor(0)
        self.__in_change = False
        self.__update_doc_popup()

    def __update_position(self):
        buf = self.__view.get_buffer()
        
        self.position_at_location(self.__view,
                                  buf.get_iter_at_mark(buf.get_insert()))

    def __update_doc_popup(self):
        if not self.showing:
            self.__doc_popup.popdown()
            return

        model, iter = self.__tree.get_selection().get_selected()
        if not iter:
            self.__doc_popup.popdown()
            return

        obj = model.get_value(iter, 2)

        # Long term it would be nice to preview the value of the
        # object, but it's distracting to show the class docs on int
        # for every integer constant, etc, which is what the DocPopup
        # does currently.
        if (obj is None or is_data_object(obj)):
            self.__doc_popup.popdown()
            return
        
        self.__doc_popup.set_target(obj)
        self.__doc_popup.popup()

    def __insert_completion(self, iter):
        completion = self.__tree_model.get_value(iter, 1)
        obj = self.__tree_model.get_value(iter, 2)

        buf = self.__view.get_buffer()
        default_editable = self.__view.get_editable()

        buf.insert_interactive_at_cursor(completion, default_editable)
        if inspect.isclass(obj) or inspect.isroutine(obj):
            # Show the doc popup to give the user information about what arguments
            # are posssible/required
            self.__view.show_doc_popup()

            # Insert a () and put the cursor in the middle
            buf.insert_interactive_at_cursor('(', default_editable)
            insert = buf.get_iter_at_mark(buf.get_insert())
            mark_between_parens = buf.create_mark(None, insert, left_gravity=True)
            buf.insert_interactive_at_cursor(')', default_editable)
            iter = buf.get_iter_at_mark(mark_between_parens)
            self.__view.highlight_arg_region(iter, iter)
            buf.place_cursor(iter)
            buf.delete_mark(mark_between_parens)

    def __insert_selected(self):
        model, iter = self.__tree.get_selection().get_selected()
        self.__insert_completion(iter)
            
    def __on_selection_changed(self, selection):
        if not self.__in_change:
            self.__update_doc_popup()

    def __on_row_activated(self, view, path, column):
        self.__insert_completion(self.__tree_model.get_iter(path))
        self.popdown()

    def popup(self, spontaneous=False):
        """Pop up the completion popup.

        If there are no possibilities completion at the insert cursor
        location, the popup is not popped up. If there is exactly one
        possibility and the spontaneous parameter is not provided , then
        completion is done immediately to that one possibility.

        @param spontaneous set to True if we're popping this up as a result
           of editing, rather than because of an explicit key shortcut.

        """
        
        self.__update_completions(spontaneous=spontaneous)
        num_completions = len(self.__tree_model)
        if num_completions == 0:
            return
        elif num_completions == 1 and not spontaneous:
            self.__insert_selected()
            return
        
        self.__update_position()

        self.spontaneous = spontaneous

        if self.showing:
            return

        self.show()
        self.showing = True

        self.__doc_popup.position_next_to_window(self)
        self.__update_doc_popup()

        self.focus()

    def update(self):
        """Update the completion popup after the cursor is moved, or text is inserted.

        If there are no completion possibilities at the cursor when this is called,
        the popup is popped down.

        """
        
        if not self.showing:
            return
        
        self.__update_completions(spontaneous=self.spontaneous)
        if len(self.__tree_model) == 0:
            self.popdown()
            return
        
        self.__update_position()
        
    def popdown(self):
        """Hide the completion if it is currently showing"""

        if not self.showing:
            return

        self.showing = False
        self.focused = False
        
        if self.__doc_popup.showing:
            self.__doc_popup.popdown()
        
        self.hide()

    def on_destroy(self, obj):
        self.__doc_popup.destroy()
        self.__doc_popup = None

    def on_key_press_event(self, event):
        """Do key press handling while the popup is active.

        Returns True if the key press is handled, False otherwise.

        """
        
        if event.keyval == gtk.keysyms.Escape:
            self.popdown()
            return True
        elif event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
            self.__insert_selected()
            self.popdown()
            return True
        # These keys are forwarded to the popup to move the selected row
        elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.KP_Up,
                              gtk.keysyms.Down, gtk.keysyms.KP_Down,
                              gtk.keysyms.Page_Up, gtk.keysyms.KP_Page_Up,
                              gtk.keysyms.Page_Down, gtk.keysyms.KP_Page_Down):
            self.event(event)
            return True

        return False
class ShellView(gtk.TextView):
    __gsignals__ = {}

    sidebar_open = gobject.property(type=bool, default=False)

    def __init__(self, buf):
        self.edit_only = buf.worksheet.edit_only

        if not self.edit_only:
            buf.worksheet.sig_chunk_inserted.connect( self.on_chunk_inserted )
            buf.worksheet.sig_chunk_changed.connect( self.on_chunk_changed )
            buf.worksheet.sig_chunk_status_changed.connect( self.on_chunk_status_changed )
            buf.worksheet.sig_chunk_deleted.connect( self.on_chunk_deleted )
            buf.worksheet.sig_state.connect( self.on_notify_state )

            # Track changes to update completion
            buf.connect_after('insert-text', self.on_after_insert_text)
            buf.connect_after('delete-range', self.on_after_delete_range)
            buf.connect_after('end-user-action', self.on_after_end_user_action)

            self.__inserted_in_user_action = False
            self.__deleted_in_user_action = False

        if not self.edit_only:
            self.sidebar = Sidebar()
        else:
            self.sidebar = None

        buf.connect('add-custom-result', self.on_add_custom_result)
        buf.connect('add-sidebar-results', self.on_add_sidebar_results)
        buf.connect('remove-sidebar-results', self.on_remove_sidebar_results)
        buf.connect('pair-location-changed', self.on_pair_location_changed)
            
        gtk.TextView.__init__(self, buf)
        if not self.edit_only:
            self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, LEFT_MARGIN_WIDTH)
        self.set_left_margin(2)

        # Attach a "behavior object" to the view which, by ugly hacks, makes it
        # do simply and reasonable things for cut-and-paste and DND
        sanitize_textview_ipc.sanitize_view(self)

        self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)

        self.__completion_popup = CompletionPopup(self)
        self.__doc_popup = DocPopup()
        self.__mouse_over_object = None
        self.__mouse_over_timeout = None

        self.__mouse_over_start = buf.create_mark(None, buf.get_start_iter(), True)

        self.__arg_highlight_start = None
        self.__arg_highlight_end = None
        buf.connect('mark-set', self.on_mark_set)

        self.__cursor_chunk = None
        self.__scroll_to_result = False
        self.__scroll_to = buf.create_mark(None, buf.get_start_iter(), True)
        self.__scroll_idle = None

        self.__update_sidebar_positions_idle = 0
        self.__pixels_below_buffer = 0
        self.__last_chunk = None

        self.connect('destroy', self.on_destroy)

    def __get_worksheet_line_yrange(self, line):
        buffer_line = self.get_buffer().pos_to_iter(line)
        return self.get_line_yrange(buffer_line)

    def __get_chunk_yrange(self, chunk, include_padding=False, include_results=False):
        # include_padding: whether to include pixels_above/pixels_below
        # include_results: whether to include the results of the chunk

        y, _ = self.__get_worksheet_line_yrange(chunk.start)
        end_y, end_height = self.__get_worksheet_line_yrange(chunk.end - 1)
        height = end_y + end_height - y

        if isinstance(chunk, StatementChunk) and chunk.results_end_mark is not None:
            if include_results:
                buf = self.get_buffer()
                end_iter = buf.get_iter_at_mark(chunk.results_end_mark)
                end_line_y, end_line_height = self.get_line_yrange(end_iter)
                height = end_line_y + end_line_height - y

                if not include_padding:
                    y += chunk.pixels_above
                    height -= chunk.pixels_above + chunk.pixels_below
            else:
                # In this case, pixels_below is part of the results, which we don't include
                if not include_padding:
                    y += chunk.pixels_above
                    height -= chunk.pixels_above
        elif not include_padding:
            y += chunk.pixels_above
            height -= chunk.pixels_above + chunk.pixels_below

        return y, height

    def __get_worksheet_line_at_y(self, y, adjust):
        buf = self.get_buffer()
        (buffer_line, _) = self.get_line_at_y(y)
        return buf.iter_to_pos(buffer_line, adjust)[0]

    def paint_chunk(self, cr, area, chunk, fill_color, outline_color):
        buf = self.get_buffer()

        chunk_y, chunk_height = self.__get_chunk_yrange(chunk)

        _, window_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, chunk_y)
        cr.rectangle(area.x, window_y, area.width, chunk_height)
        cr.set_source_rgb(*fill_color)
        cr.fill()

        cr.rectangle(0.5, window_y + 0.5, LEFT_MARGIN_WIDTH - 1, chunk_height - 1)
        cr.set_source_rgb(*outline_color)
        cr.set_line_width(1)
        cr.stroke()

    def do_realize(self):
        gtk.TextView.do_realize(self)

        if not self.edit_only:
            self.get_window(gtk.TEXT_WINDOW_LEFT).set_background(self.style.white)

        # While the the worksheet is executing, we want to display a watch cursor
        # Trying to override the cursor setting of GtkTextView is really hard because
        # of things like hiding the cursor when typing, so we take the simple approach
        # of using an input-only "cover window" that we set the cursor on and
        # show on top of the GtkTextView's normal window.

        self.__watch_window = gtk.gdk.Window(self.window,
                                             self.allocation.width, self.allocation.height,
                                             gtk.gdk.WINDOW_CHILD,
                                             (gtk.gdk.SCROLL_MASK |
                                              gtk.gdk.BUTTON_PRESS_MASK |
                                              gtk.gdk.BUTTON_RELEASE_MASK |
                                              gtk.gdk.POINTER_MOTION_MASK |
                                              gtk.gdk.POINTER_MOTION_HINT_MASK),
                                             gtk.gdk.INPUT_ONLY,
                                             x=0, y=0)
        self.__watch_window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
        self.__watch_window.set_user_data(self)

        if self.get_buffer().worksheet.state == NotebookFile.EXECUTING:
            self.__watch_window.show()
            self.__watch_window.raise_()

    def on_destroy(self, obj):
        self.__completion_popup.destroy()
        self.__completion_popup = None
        self.__doc_popup.destroy()
        self.__doc_popup = None

        if self.__scroll_idle is not None:
            glib.source_remove(self.__scroll_idle)

    def do_unrealize(self):
        self.__watch_window.set_user_data(None)
        self.__watch_window.destroy()
        self.__watch_window = None

        gtk.TextView.do_unrealize(self)

    def do_size_allocate(self, allocation):
        gtk.TextView.do_size_allocate(self, allocation)
        if (self.flags() & gtk.REALIZED) != 0:
            self.__watch_window.resize(allocation.width, allocation.height)

    def __iterate_expose_chunks(self, event):
        _, start_y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y)
        start_line = self.__get_worksheet_line_at_y(start_y, adjust=ADJUST_AFTER)

        _, end_y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y + event.area.height - 1)
        end_line = self.__get_worksheet_line_at_y(end_y, adjust=ADJUST_BEFORE)

        return self.get_buffer().worksheet.iterate_chunks(start_line, end_line + 1)

    def __expose_window_left(self, event):
        cr = event.window.cairo_create()

        for chunk in self.__iterate_expose_chunks(event):
            if isinstance(chunk, StatementChunk):
                if chunk.executing:
                    self.paint_chunk(cr, event.area, chunk, (0, 1, 0), (0, 0.5, 0))
                elif chunk.error_message is not None:
                    self.paint_chunk(cr, event.area, chunk, (1, 0, 0), (0.5, 0, 0))
                elif chunk.needs_compile:
                    self.paint_chunk(cr, event.area, chunk, (1, 1, 0), (0.5, 0.5, 0))
                elif chunk.needs_execute:
                    self.paint_chunk(cr, event.area, chunk, (1, 0, 1), (0.5, 0, 0.5))
                else:
                    self.paint_chunk(cr, event.area, chunk, (0, 0, 1), (0, 0, 0.5))

    def __draw_rect_outline(self, event, rect):
        if (rect.y + rect.height <= event.area.y or rect.y >= event.area.y + event.area.height):
            return

        cr = event.window.cairo_create()
        cr.set_line_width(1.)
        cr.rectangle(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1)
        cr.set_source_rgb(0.6, 0.6, 0.6)
        cr.stroke()

    def __expose_arg_highlight(self, event):
        buf = self.get_buffer()

        # We want a rectangle enclosing the range between arg_highlight_start and
        # arg_highlight_end; the method here isn't correct in the presence of
        # RTL text, but the necessary Pango functionality isn't exposed to
        # a GtkTextView user. RTL code is rare. We also don't handle multiple-line
        # highlight regions right. (Return ends the highlight, so you'd need to paste)
        rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_start))
        rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                                      rect.x, rect.y)
        rect.width = 0
        end_rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_end))
        end_rect.x, end_rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                                              end_rect.x, end_rect.y)
        end_rect.width = 0

        rect = rect.union(end_rect)

        self.__draw_rect_outline(event, rect)

    def __expose_pair_location(self, event):
        pair_location = self.get_buffer().get_pair_location()
        if pair_location is None:
            return
        
        rect = self.get_iter_location(pair_location)

        rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, rect.x, rect.y)

        self.__draw_rect_outline(event, rect)

    def __line_boundary_in_selection(self, line):
        buf = self.get_buffer()

        try:
            line_iter = buf.pos_to_iter(line)
        except IndexError:
            return

        sel_start, sel_end = buf.get_selection_bounds()
        return sel_start.compare(line_iter) < 0 and sel_end.compare(line_iter) >= 0

    def __expose_padding_areas(self, event):
        buf = self.get_buffer()

        # This is a fixup for the padding areas we add to chunks when leaving
        # space for sidebar widgets - gtk.TextView draws these areas as part
        # of the chunk that is being padded (so partially selected when the
        # line is partially selected.) This just looks wrong, so we paint over
        # that so that padding areas are _between_ lines.

        cr = event.window.cairo_create()

        left_margin = self.get_property('left-margin')
        right_margin = self.get_property('right-margin')
        selection_left, _ = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                                         left_margin, 0)
        window_width, _ = event.window.get_size()
        # 1 here is gtktextview.c:SPACE_FOR_CURSOR - padding on the right for the cursor
        selection_width = window_width - left_margin - right_margin - 1

        for chunk in self.__iterate_expose_chunks(event):
            if chunk.pixels_above != 0 or chunk.pixels_below != 0:
                total_y, total_height = self.__get_chunk_yrange(chunk, include_padding=True, include_results=True)
                no_pad_y, no_pad_height = self.__get_chunk_yrange(chunk, include_padding=False, include_results=True)
                _, total_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, 0, total_y)
                _, no_pad_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, 0, no_pad_y)

                if chunk.pixels_above != 0:
                    cr.rectangle(selection_left, total_y,
                                 selection_width, no_pad_y - total_y)
                    if self.__line_boundary_in_selection(chunk.start):
                        cr.set_source_color(self.style.base[gtk.STATE_SELECTED])
                    else:
                        cr.set_source_color(self.style.base[self.state])
                    cr.fill()

                if chunk.pixels_below != 0:
                    cr.rectangle(selection_left, no_pad_y + no_pad_height,
                                 selection_width, total_y + total_height - (no_pad_y + no_pad_height))
                    if self.__line_boundary_in_selection(chunk.end):
                        cr.set_source_color(self.style.base[gtk.STATE_SELECTED])
                    else:
                        cr.set_source_color(self.style.base[self.state])
                    cr.fill()

    def __update_last_chunk(self, new_last_chunk):
        buf = self.get_buffer()

        if self.__last_chunk and self.__pixels_below_buffer != 0:
            buf.set_pixels_below(self.__last_chunk, 0)

        self.__last_chunk = new_last_chunk
        if self.__pixels_below_buffer != 0:
            buf.set_pixels_below(self.__last_chunk, self.__pixels_below_buffer)

    def __set_pixels_below_buffer(self, pixels_below):
        self.set_pixels_below_lines(pixels_below)
        self.get_buffer().set_pixels_below(self.__last_chunk, pixels_below)
        self.__pixels_below_buffer = pixels_below

    def __update_sidebar_positions(self):
        # Each StatementChunk with sidebar widgets has already been added as a "slot"
        # to the sidebar. For each of these slots we determine a vertical
        # range which includes the StatementChunk, and any preceding CommentChunk.
        #
        # We also handle adding space before slots with sidebar widgets and at the end
        # of the buffer so that previous sidebar widgets have enough room.
        #
        # Rather than trying to update things incrementally, we just run
        # through the chunks in the buffer twice and compute the positions from
        # scratch.

        self.__update_sidebar_positions_idle = 0
        buf = self.get_buffer()

        set_chunks = set()
        widget_end_y = None

        slots = self.sidebar.slots
        if len(slots) > 0:
            self.sidebar.freeze_positions()

            # Main position loop where we determine the slot positions
            #
            # The "slot_extra" here is the number of pixels inside the slot which were
            # added as chunk_above/chunk_below in previous runs of this code and will
            # be removed unless we add them again in this run.

            slot_index = 0
            for chunk in buf.worksheet.iterate_chunks(0, slots[-1].chunk.end):
                if chunk == slots[slot_index].chunk:
                    if slot_start_chunk is None:
                        slot_start_chunk = chunk
                        slot_extra = chunk.pixels_above + chunk.pixels_below
                    else:
                        slot_extra += chunk.pixels_above + chunk.pixels_below

                    slot_start_y, _ = self.__get_chunk_yrange(slot_start_chunk,
                                                              include_results=True, include_padding=True)

                    chunk_start_y, chunk_height =  self.__get_chunk_yrange(chunk,
                                                                           include_results=True, include_padding=True)
                    slot_height = chunk_start_y + chunk_height - slot_start_y - slot_extra

                    if widget_end_y is not None and widget_end_y > slot_start_y:
                        buf.set_pixels_above(slot_start_chunk, widget_end_y - slot_start_y)
                        set_chunks.add(slot_start_chunk)
                        slot_start_y = widget_end_y

                    slots[slot_index].set_position(slot_start_y, slot_height)

                    widget_end_y = slot_start_y + slots[slot_index].get_results_height()
                    slot_index += 1
                    slot_start_chunk = None
                else:
                    if isinstance(chunk, CommentChunk):
                        slot_start_chunk = chunk
                        slot_extra = chunk.pixels_above + chunk.pixels_below
                    elif isinstance(chunk, StatementChunk):
                        slot_start_chunk = None
                    elif slot_start_chunk is not None:
                        slot_extra += chunk.pixels_above + chunk.pixels_below

            self.sidebar.thaw_positions()

        # Any chunk we where didn't assign pixels_above needs to have pixels_above
        # set back to zero - it might have been set for some previous pass
        for chunk in buf.worksheet.iterate_chunks():
            if chunk.pixels_above != 0 and not chunk in set_chunks:
                buf.set_pixels_above(chunk, 0)

        # Finally, add space at the end of the buffer, if necessary

        pixels_below = 0
        if widget_end_y is not None:
            end_line_y, end_line_height = self.get_line_yrange(buf.get_end_iter())
            end_y = end_line_y + end_line_height - self.__pixels_below_buffer
            if widget_end_y > end_y:
                pixels_below = widget_end_y - end_y

        self.__set_pixels_below_buffer(pixels_below)

        return False

    def __queue_update_sidebar_positions(self, before_resize=False):

        # We want to position the sidebar results with respect to the buffer
        # contents, so position has to be done after validation finishes, but there
        # is no hook to figure that out for gtk.TextView(). So, what we do is
        # that we figure that if anything _interesting_ gets repositioned there
        # will be an expose event, and in the expose event, add an idle that
        # is low enough priority to run after validation finishes.
        #
        # Now, in the positioning process, we actually _modify_ the buffer
        # contents by adding space above chunks to give previoous sidebar
        # results enough room. This produces a quasi-circular dependency
        # and a loop - but since the addition of space modifies the layout
        # in a predictable way, we can just subtract that back out. What we
        # expect to happen is:
        #
        #  - Buffer modification
        #  - Expose event
        #  - Our idle runs, we compute positions, and add space
        #  - Expose event
        #  - Our idle runs, we compute positions, compensating for the added
        #    space; the new computation is the same, and nothing further happens.
        #
        # (If there is a bug in the compensation code, an infinite loop will
        # be triggered.)

        if len(self.sidebar.slots) == 0 and self.__pixels_below_buffer == 0:
            return

        if before_resize and self.__update_sidebar_positions_idle != 0:
            glib.source_remove(self.__update_sidebar_positions_idle)
            self.__update_sidebar_positions_idle = 0

        if self.__update_sidebar_positions_idle == 0:
            if before_resize:
                priority = PRIORITY_SIDEBAR_BEFORE_RESIZE
            else:
                priority = PRIORITY_SIDEBAR_AFTER_VALIDATE

            self.__update_sidebar_positions_idle = glib.idle_add(self.__update_sidebar_positions,
                                                                 priority=priority)

    def do_expose_event(self, event):
        buf = self.get_buffer()

        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
            self.__queue_update_sidebar_positions()
            self.__expose_window_left(event)
            return False

        gtk.TextView.do_expose_event(self, event)

        if event.window == self.get_window(gtk.TEXT_WINDOW_TEXT):
            if self.__arg_highlight_start:
                self.__expose_arg_highlight(event)
            else:
                self.__expose_pair_location(event)
            if buf.get_has_selection():
                self.__expose_padding_areas(event)

        return False

    # This is likely overengineered, since we're going to try as hard as possible not to
    # have tabs in our worksheets. We don't do the funky handling of \f.
    def __count_indent(self, text):
        indent = 0
        for c in text:
            if c == ' ':
                indent += 1
            elif c == '\t':
                indent += 8 - (indent % 8)
            else:
                break

        return indent

    def __find_outdent(self, iter):
        buf = self.get_buffer()
        line, _ = buf.iter_to_pos(iter)

        current_indent = self.__count_indent(buf.worksheet.get_line(line))

        while line > 0:
            line -= 1
            line_text = buf.worksheet.get_line(line)
            # Empty lines don't establish indentation
            if ALL_WHITESPACE_RE.match(line_text):
                continue

            indent = self.__count_indent(line_text)
            if indent < current_indent:
                return re.match(r"^[\t ]*", line_text).group(0)

        return ""

    def __find_default_indent(self, iter):
        buf = self.get_buffer()
        line, offset = buf.iter_to_pos(iter)

        while line > 0:
            line -= 1
            chunk = buf.worksheet.get_chunk(line)
            if isinstance(chunk, StatementChunk):
                return chunk.tokenized.get_next_line_indent(line - chunk.start)
            elif isinstance(chunk, CommentChunk) or isinstance(chunk, BlankChunk):
                return " " * self.__count_indent(buf.worksheet.get_line(line))

        return ""

    def __reindent_line(self, iter, indent_text):
        buf = self.get_buffer()
        insert_mark = buf.get_insert()

        line, pos = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
        if line == None:
            return

        line_text = buf.worksheet.get_line(line)
        prefix = re.match(r"^[\t ]*", line_text).group(0)

        diff = self.__count_indent(indent_text) - self.__count_indent(prefix)
        if diff == 0:
            return 0

        common_len = 0
        for a, b in zip(prefix, indent_text):
            if a != b:
                break
            common_len += 1
    
        start = iter.copy()
        start.set_line_offset(common_len)
        end = iter.copy()
        end.set_line_offset(len(prefix))

        # Nitpicky-detail. If the selection starts at the start of the line, and we are
        # inserting white-space there, then the whitespace should be *inside* the selection
        mark_to_start = None
        if common_len == 0 and buf.get_has_selection():
            if buf.get_iter_at_mark(insert_mark).compare(start) == 0:
                mark_to_start = mark
                
            mark = buf.get_selection_bound()
            if buf.get_iter_at_mark(mark).compare(start) == 0:
                mark_to_start = mark
        
        buf.delete(start, end)
        buf.insert(end, indent_text[common_len:])

        if mark_to_start is not None:
            end.set_line_offset(0)
            buf.move_mark(mark_to_start, end)

        insert_iter = buf.get_iter_at_mark(insert_mark)
        insert_line, _ = buf.iter_to_pos(insert_iter, adjust=ADJUST_NONE)
        if insert_line == line:
            # We shifted the insertion cursor around behind gtk.TextView's back,
            # by inserting text on the same line; this will result in a wrong
            # virtual cursor position. Calling buf.place_cursor() will cause
            # the virtual cursor position to be reset to the proper value.
            buf.place_cursor(insert_iter)

        return diff

    def __reindent_selection(self, outdent):
        buf = self.get_buffer()

        bounds = buf.get_selection_bounds()
        if bounds == ():
            insert_mark = buf.get_insert()
            bounds = buf.get_iter_at_mark(insert_mark), buf.get_iter_at_mark(insert_mark)
        start, end = bounds

        line, _ = buf.iter_to_pos(start, adjust=ADJUST_AFTER)
        end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)
        if end_offset == 0 and end_line > line:
            end_line -= 1

        iter = buf.pos_to_iter(line)

        if outdent:
            indent_text = self.__find_outdent(iter)
        else:
            indent_text = self.__find_default_indent(iter)

        diff = self.__reindent_line(iter, indent_text)

        if not buf.get_has_selection():
            iter = buf.get_iter_at_mark(buf.get_insert())
            if iter.get_line_offset() < len(indent_text):
                iter.set_line_offset(len(indent_text))
                buf.place_cursor(iter)

        while True:
            line += 1
            if line > end_line:
                return

            iter = buf.pos_to_iter(line)
            current_indent = self.__count_indent(buf.worksheet.get_line(line))
            self.__reindent_line(iter, max(0, " " * (current_indent + diff)))

    def __hide_completion(self):
        if self.__completion_popup.showing:
            self.__completion_popup.popdown()
            
    def do_focus_out_event(self, event):
        self.__hide_completion()
        self.__doc_popup.popdown()
        return gtk.TextView.do_focus_out_event(self, event)

    def __rewrite_window(self, event):
        # Mouse events on the "watch window" that covers the text view
        # during calculation need to be forwarded to the real text window
        # since it looks bad if keynav works, but you can't click on the
        # window to set the cursor, select text, and so forth

        if event.window == self.__watch_window:
            event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)

        # Events on the left-margin window also get written so the user can
        # click there when starting a drag selection. We need to adjust the
        # X coordinate in that case
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
            event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)
            if event.type == gtk.gdk._3BUTTON_PRESS:
                # Workaround for http://bugzilla.gnome.org/show_bug.cgi?id=573664
                event.x = 50.
            else:
                event.x -= LEFT_MARGIN_WIDTH

    def do_button_press_event(self, event):
        self.__rewrite_window(event)

        self.__doc_popup.popdown()

        return gtk.TextView.do_button_press_event(self, event)

    def do_button_release_event(self, event):
        self.__rewrite_window(event)

        return gtk.TextView.do_button_release_event(self, event)

    def do_motion_notify_event(self, event):
        self.__rewrite_window(event)

        return gtk.TextView.do_motion_notify_event(self, event)

    def __remove_arg_highlight(self, cursor_to_end=True):
        buf = self.get_buffer()

        end = buf.get_iter_at_mark (self.__arg_highlight_end)

        buf.delete_mark(self.__arg_highlight_start)
        self.__arg_highlight_start = None
        buf.delete_mark(self.__arg_highlight_end)
        self.__arg_highlight_end = None

        if cursor_to_end:
            # If the arg_highlight ends at closing punctuation, skip over it
            tmp = end.copy()
            tmp.forward_char()
            text = buf.get_slice(end, tmp)

            if text in (")", "]", "}"):
                buf.place_cursor(tmp)
            else:
                buf.place_cursor(end)

    def do_key_press_event(self, event):
        buf = self.get_buffer()

        if self.__completion_popup.focused and self.__completion_popup.on_key_press_event(event):
            return True
        
        if self.__doc_popup.focused:
            self.__doc_popup.on_key_press_event(event)
            return True

        if not self.edit_only and event.keyval in (gtk.keysyms.F2, gtk.keysyms.KP_F2):
            self.__hide_completion()

            if self.__doc_popup.showing:
                self.__doc_popup.focus()
            else:
                self.show_doc_popup(focus_popup=True)

            return True

        if not self.__arg_highlight_start:
            self.__doc_popup.popdown()
        
        if event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
            self.__hide_completion()

            if self.__arg_highlight_start:
                self.__remove_arg_highlight()
                self.__doc_popup.popdown()
                return True
            
            increase_indent = False
            insert = buf.get_iter_at_mark(buf.get_insert())
            line, pos = buf.iter_to_pos(insert, adjust=ADJUST_NONE)

            # Inserting return inside a ResultChunk would normally do nothing
            # but we want to make it insert a line after the chunk
            if line is None and not buf.get_has_selection():
                line, pos = buf.iter_to_pos(insert, adjust=ADJUST_BEFORE)
                iter = buf.pos_to_iter(line, -1)
                buf.place_cursor(iter)
                buf.insert_interactive(iter, "\n", True)
                
                return True

            buf.begin_user_action()
            
            gtk.TextView.do_key_press_event(self, event)
            # We need the chunks to be updated when computing the line indents
            buf.worksheet.rescan()

            insert = buf.get_iter_at_mark(buf.get_insert())
            
            self.__reindent_line(insert, self.__find_default_indent(insert))

            # If we have two comment lines in a row, assume a block comment
            if (line > 0 and
                isinstance(buf.worksheet.get_chunk(line), CommentChunk) and
                isinstance(buf.worksheet.get_chunk(line - 1), CommentChunk)):
                self.get_buffer().insert_interactive_at_cursor("# ", True)

            buf.end_user_action()
                
            return True
        elif event.keyval in (gtk.keysyms.Tab, gtk.keysyms.KP_Tab) and event.state & gtk.gdk.CONTROL_MASK == 0:
            buf.begin_user_action()
            self.__reindent_selection(outdent=False)
            buf.end_user_action()

            return True
        elif event.keyval == gtk.keysyms.ISO_Left_Tab and event.state & gtk.gdk.CONTROL_MASK == 0:
            buf.begin_user_action()
            self.__reindent_selection(outdent=True)
            buf.end_user_action()

            return True
        elif event.keyval == gtk.keysyms.space and event.state & gtk.gdk.CONTROL_MASK != 0:
            if self.__completion_popup.showing:
                if self.__completion_popup.spontaneous:
                    self.__completion_popup.popup(spontaneous=False)
                else:
                    self.__completion_popup.popdown()
            else:
                if self.__doc_popup.showing:
                    self.__doc_popup.popdown()
                self.__completion_popup.popup(spontaneous=False)
            return True
        elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
                (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
                (event.state & gtk.gdk.SHIFT_MASK) == 0:
            buf.worksheet.undo()
            
            return True
        # This is the gedit/gtksourceview binding to cause your hands to fall off
        elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
                (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
                (event.state & gtk.gdk.SHIFT_MASK) != 0:
            buf.worksheet.redo()
            
            return True
        # This is the familiar binding (Eclipse, etc). Firefox supports both
        elif event.keyval in (gtk.keysyms.y, gtk.keysyms.Y) and event.state & gtk.gdk.CONTROL_MASK != 0:
            buf.worksheet.redo()

            return True
        
        return gtk.TextView.do_key_press_event(self, event)

    def __show_mouse_over(self):
        self.__mouse_over_timeout = None
        
        if self.__completion_popup.showing:
            return
        
        self.__doc_popup.set_target(self.__mouse_over_object)
        location = self.get_buffer().get_iter_at_mark(self.__mouse_over_start)
        self.__doc_popup.position_at_location(self, location)
        self.__doc_popup.popup()

        return False
        
    def __stop_mouse_over(self):
        if self.__mouse_over_timeout:
            glib.source_remove(self.__mouse_over_timeout)
            self.__mouse_over_timeout = None
            
        self.__mouse_over_object = None
        
    def do_motion_notify_event(self, event):
        # Successful mousing-over depends on knowing the types of symbols so doing the
        # checks are pointless in edit-only mode
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_TEXT) and not self.__doc_popup.focused:
            buf = self.get_buffer()
            
            x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(event.x), int(event.y))
            iter, _ = self.get_iter_at_position(x, y)
            line, offset = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
            if line is not None:
                obj, start_line, start_offset, _,_ = buf.worksheet.get_object_at_location(line, offset)
            else:
                obj = None

            if not obj is self.__mouse_over_object:
                self.__stop_mouse_over()
                self.__doc_popup.popdown()
                if obj is not None:
                    start = buf.pos_to_iter(start_line, start_offset)
                    buf.move_mark(self.__mouse_over_start, start)

                    self.__mouse_over_object = obj
                    try:
                        timeout = self.get_settings().get_property('gtk-tooltip-timeout')
                    except TypeError: # GTK+ < 2.12
                        timeout = 500
                    self.__mouse_over_timeout = glib.timeout_add(timeout, self.__show_mouse_over)
                
        return gtk.TextView.do_motion_notify_event(self, event)

    def do_leave_notify_event(self, event):
        self.__stop_mouse_over()
        if not self.__doc_popup.focused:
            self.__doc_popup.popdown()
        return False

    def do_backspace(self):
        buf = self.get_buffer()
        
        if buf.get_has_selection():
            return gtk.TextView.do_backspace(self)

        insert = buf.get_iter_at_mark(buf.get_insert())
        line, offset = buf.iter_to_pos(insert)
        
        current_chunk = buf.worksheet.get_chunk(line)
        if isinstance(current_chunk, StatementChunk) or isinstance(current_chunk, BlankChunk):
            line_text = buf.worksheet.get_line(line)[0:offset]

            if re.match(r"^[\t ]+$", line_text) and not (line > 0 and self.is_continued(line - 1)):
                self.__reindent_selection(outdent=True)
                return
                       
        return gtk.TextView.do_backspace(self)

    def is_continued(self, line):
        """Determine if line causes a continuation."""
        buf = self.get_buffer()
        chunk = buf.worksheet.get_chunk(line)
        while not isinstance(chunk, StatementChunk) and line > 0:
            line -= 1
            chunk = buf.worksheet.get_chunk(line)
        return isinstance(chunk, StatementChunk) and chunk.tokenized.is_continued(line - chunk.start)

    def __invalidate_status(self, chunk):
        buf = self.get_buffer()
        
        chunk_y, chunk_height = self.__get_chunk_yrange(chunk, include_padding=True)

        _, window_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, chunk_y)

        if self.window:
            left_margin_window = self.get_window(gtk.TEXT_WINDOW_LEFT)
            left_margin_window.invalidate_rect((0, window_y, LEFT_MARGIN_WIDTH, chunk_height),
                                               False)

    def on_chunk_inserted(self, worksheet, chunk):
        buf = self.get_buffer()

        self.__invalidate_status(chunk)
        if chunk.end == buf.worksheet.get_line_count():
            self.__update_last_chunk(chunk)

    def on_chunk_changed(self, worksheet, chunk, changed_lines):
        self.__invalidate_status(chunk)

    def on_chunk_status_changed(self, worksheet, chunk):
        self.__invalidate_status(chunk)
        
        if self.__cursor_chunk == chunk and not chunk.executing:
            # This is the chunk with the cursor and it's done executing
            self.__scroll_idle = glib.idle_add(self.scroll_result_onscreen,
                                               priority=PRIORITY_SCROLL_RESULT_ONSCREEN)

    def scroll_result_onscreen(self):
        """Scroll so that both the insertion cursor and the following result
        are onscreen.  If we cannot get both, get as much of the result while
        still getting the insertion cursor."""

        buf = self.get_buffer()
        if self.__scroll_to_result:
            if self.__cursor_chunk.sidebar_results:
                # We assume that we don't have both sidebar and normal results
                for slot in self.sidebar.slots:
                    if slot.chunk == self.__cursor_chunk:
                        self.sidebar.scroll_to_slot_results(slot)
                        break
            else:
                try:
                    iter = buf.pos_to_iter(self.__cursor_chunk.end) # Start of next chunk
                except IndexError:
                    iter = buf.get_end_iter()
                else:
                    iter.backward_line() # Move to line before start of next chunk

                buf.move_mark(self.__scroll_to, iter)
                self.scroll_mark_onscreen(self.__scroll_to)

        self.scroll_mark_onscreen(buf.get_insert())

        self.__cursor_chunk = None
        self.__scroll_idle = None
        return False

    def on_chunk_deleted(self, worksheet, chunk):
        buf = self.get_buffer()

        if self.__cursor_chunk == chunk:
            self.__cursor_chunk = None
            if self.__scroll_idle is not None:
                glib.source_remove(self.__scroll_idle)
            self.__scroll_idle = None

        if self.__last_chunk == chunk:
            self.__last_chunk = None

            new_last_chunk = buf.worksheet.get_chunk(buf.worksheet.get_line_count() - 1)
            # We might find a newly created chunk that hasn't been "inserted" yet -
            # in that case, we wait until we get 'chunk_inserted'
            if hasattr(new_last_chunk, 'pixels_above'):
                self.__update_last_chunk(new_last_chunk)

    def on_notify_state(self, worksheet, param_spec):
        if (self.flags() & gtk.REALIZED) != 0:
            if worksheet.state == NotebookFile.EXECUTING:
                self.__watch_window.show()
                self.__watch_window.raise_()
            else:
                self.__watch_window.hide()

        self.set_cursor_visible(worksheet.state != NotebookFile.EXECUTING)

    def on_after_insert_text(self, buf, location, text, len):
        if buf.worksheet.in_user_action() and not buf.in_modification():
            self.__inserted_in_user_action = True

    def on_after_delete_range(self, buf, start, end):
        if buf.worksheet.in_user_action() and not buf.in_modification():
            self.__deleted_in_user_action = True

    def on_after_end_user_action(self, buf):
        if not buf.worksheet.in_user_action():
            if self.__completion_popup.showing:
                if self.__inserted_in_user_action or self.__deleted_in_user_action:
                    self.__completion_popup.update()
            else:
                if self.__inserted_in_user_action and global_settings.autocomplete:
                    self.__completion_popup.popup(spontaneous=True)
            self.__inserted_in_user_action = False
            self.__deleted_in_user_action = False

    def on_add_custom_result(self, buf, result, anchor):
        widget = result.create_widget()
        widget.show()
        self.add_child_at_anchor(widget, anchor)

    def on_add_sidebar_results(self, buf, chunk):
        if len(self.sidebar.slots) == 0:
            self.sidebar_open = True

        widgets = []
        for result in chunk.sidebar_results:
            widget = result.create_widget()
            widget.show()
            widgets.append(widget)

        self.sidebar.add_slot(chunk, widgets)

        # The final sidebar position calculation aren't be done until
        # we've finished revalidating the text view, but we need
        # to get some approproximate guess done before we allocate
        # the sidebar, or we'll get flashing
        self.__queue_update_sidebar_positions(before_resize=True)

    def on_remove_sidebar_results(self, buf, chunk):
        if len(self.sidebar.slots) == 1:
            self.__queue_update_sidebar_positions()
            self.sidebar_open = False

        self.sidebar.remove_slot(chunk)

    def on_mark_set(self, buffer, iter, mark):
        if self.__arg_highlight_start:
            # See if the user moved the cursor out of the highlight regionb
            buf = self.get_buffer()
            if mark != buf.get_insert():
                return

            if (iter.compare(buf.get_iter_at_mark(self.__arg_highlight_start)) < 0 or
                iter.compare(buf.get_iter_at_mark(self.__arg_highlight_end)) > 0):
                self.__remove_arg_highlight(cursor_to_end=False)

    def __invalidate_char_position(self, iter):
        y, height = self.get_line_yrange(iter)
        if self.window:
            text_window = self.get_window(gtk.TEXT_WINDOW_TEXT)
            width, _ = text_window.get_size()
            text_window.invalidate_rect((0, y, width, height), False)
        
    def on_pair_location_changed(self, buf, old_position, new_position):
        if old_position:
            self.__invalidate_char_position(old_position)
        if new_position:
            self.__invalidate_char_position(new_position)

    #######################################################
    # Public API
    #######################################################

    def calculate(self, end_at_insert=False):
        buf = self.get_buffer()
        line, _ = buf.iter_to_pos(buf.get_iter_at_mark(buf.get_insert()), ADJUST_BEFORE)

        if end_at_insert:
            end_line = line + 1 # +1 to include line with cursor
        else:
            end_line = None

        buf.worksheet.calculate(end_line=end_line)

        # If the cursor is in a StatementChunk or we are executing up to
        # the cursor, we will scroll the result into view.  Otherwise,
        # we will just scroll the cursor into view.  If there is no
        # StatementChunk between the cursor and the beginning of the
        # worksheet, or if the StatementChunk has already been executed,
        # there is no need to scroll at all.
        statement_line = line
        in_statement = True
        while line >= 0:
            chunk = buf.worksheet.get_chunk(statement_line)
            if isinstance(chunk, StatementChunk):
                if chunk.needs_compile or chunk.needs_execute:
                    self.__cursor_chunk = chunk
                    self.__scroll_to_result = end_at_insert or in_statement
                return
            statement_line -= 1
            in_statement = False

    def copy_as_doctests(self):
        buf = self.get_buffer()

        bounds = buf.get_selection_bounds()
        if bounds == ():
            start, end = buf.get_iter_at_mark(buf.get_insert())
        else:
            start, end = bounds

        start_line, start_offset = buf.iter_to_pos(start, adjust=ADJUST_BEFORE)
        end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)

        doctests = buf.worksheet.get_doctests(start_line, end_line + 1)
        self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD).set_text(doctests)

    def show_doc_popup(self, focus_popup=False):
        """Pop up the doc popup for the symbol at the insertion point, if any

        @param focus_popup: if True, the popup will be given keyboard focus

        """

        buf = self.get_buffer()

        insert = buf.get_iter_at_mark(buf.get_insert())
        line, offset = buf.iter_to_pos(insert, adjust=ADJUST_NONE)
        if line is not None:
            obj, start_line, start_offset, _, _ = buf.worksheet.get_object_at_location(line, offset, include_adjacent=True)
        else:
            obj = None

        if obj is not None:
            start = buf.pos_to_iter(start_line, start_offset)
            self.__stop_mouse_over()
            self.__doc_popup.set_target(obj)
            self.__doc_popup.position_at_location(self, start)
            if focus_popup:
                self.__doc_popup.popup_focused()
            else:
                self.__doc_popup.popup()

    def highlight_arg_region(self, start, end):
        """Highlight the region between start and end for argument insertion.
        A box will be drawn around the region as long as the cursor is inside
        the region. If the user hits return, the cursor will be moved to the
        end (and past a single closing punctuation at the end, if any.)

        @param start iter at the start of the highlight region
        @param end iter at the end of the highlight region

        """

        buf = self.get_buffer()

        self.__arg_highlight_start = buf.create_mark(None, start, left_gravity=True)
        self.__arg_highlight_end = buf.create_mark(None, end, left_gravity=False)