Пример #1
0
class TextEditor(gtk.TextView):
    def __init__(self, textbuffer = None, *args, **kwargs):
        if textbuffer is None:
            textbuffer = TextBuffer()
        gtk.TextView.__init__(self, textbuffer, *args, **kwargs)
        self.buffer           = textbuffer
        self.anno_width       = 200
        self.anno_padding     = 10
        self.anno_layout      = Layout(self)
        self.anno_views       = {}
        self.show_annotations = True
        self.handle_links     = True
        self.set_right_margin(50 + self.anno_padding)
        self.connect('map-event',           self._on_map_event)
        self.connect('expose-event',        self._on_expose_event)
        self.connect('motion-notify-event', self._on_motion_notify_event)
        self.connect('event-after',         self._on_event_after)
        self.buffer.connect('mark-set',           self._on_buffer_mark_set)
        self.buffer.connect('annotation-added',   self._on_annotation_added)
        self.buffer.connect('annotation-removed', self._on_annotation_removed)
        self.set_wrap_mode(gtk.WRAP_WORD)


    def _on_motion_notify_event(self, editor, event):
        if not self.handle_links:
            return
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x),
                                            int(event.y))
        tags = self.get_iter_at_location(x, y).get_tags()

        # Without this call, further motion notify events don't get
        # triggered.
        self.window.get_pointer()

        # If any of the tags are links, show a hand.
        cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
        for tag in tags:
            if tag.get_data('link'):
                cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
                break

        self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor)
        return False


    def _on_event_after(self, textview, event):
        # Handle links here. Only when a button was released.
        if event.type != gtk.gdk.BUTTON_RELEASE:
            return False
        if event.button != 1:
            return False
        if not self.handle_links:
            return

        # Don't follow a link if the user has selected something.
        bounds = self.buffer.get_selection_bounds()
        if bounds:
            start, end = bounds
            if start.get_offset() != end.get_offset():
                return False

        # Check whether the cursor is pointing at a link.
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x),
                                            int(event.y))
        iter = textview.get_iter_at_location(x, y)

        for tag in iter.get_tags():
            link = tag.get_data('link')
            if not link:
                continue
            self.emit('link-clicked', link)
            break
        return False


    def _on_buffer_mark_set(self, buffer, iter, mark):
        self._update_annotations()


    def _on_map_event(self, widget, event):
        self._update_annotation_area()
        self._update_annotations()


    def _on_expose_event(self, widget, event):
        text_window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
        if event.window != text_window:
            return

        # Create the cairo context.
        ctx = event.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        ctx.rectangle(event.area.x,
                      event.area.y,
                      event.area.width,
                      event.area.height)
        ctx.clip()

        self.draw(ctx, *event.window.get_size())


    def draw(self, ctx, w, h):
        if ctx is None:
            return
        if not self.show_annotations:
            return

        # Draw the dashes that connect annotations to their marker.
        ctx.set_line_width(1)
        ctx.set_dash((3, 2))
        right_margin = self.get_right_margin()
        for annotation in self.anno_layout.get_children():
            mark_x, mark_y = self._get_annotation_mark_position(annotation)
            anno_x, anno_y, anno_w, anno_h, d = annotation.window.get_geometry()
            path = [(mark_x,           mark_y - 5),
                    (mark_x,           mark_y),
                    (w - right_margin, mark_y),
                    (w,                anno_y + anno_h / 2)]

            stroke_color = annotation.get_border_color()
            ctx.set_source_rgba(*color.to_rgba(stroke_color))
            ctx.move_to(*path[0])
            for x, y in path[1:]:
                ctx.line_to(x, y)
            ctx.stroke()


    def _get_annotation_mark_offset(self, view1, view2):
        mark1 = view1.annotation.start_mark
        mark2 = view2.annotation.start_mark
        iter1 = self.get_buffer().get_iter_at_mark(mark1)
        iter2 = self.get_buffer().get_iter_at_mark(mark2)
        rect1 = self.get_iter_location(iter1)
        rect2 = self.get_iter_location(iter2)
        if rect1.y != rect2.y:
            return rect1.y - rect2.y
        return rect2.x - rect1.x


    def _get_annotation_mark_position(self, view):
        start_mark     = view.annotation.start_mark
        iter           = self.get_buffer().get_iter_at_mark(start_mark)
        rect           = self.get_iter_location(iter)
        mark_x, mark_y = rect.x, rect.y + rect.height
        return self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                            mark_x, mark_y)


    def _update_annotation(self, annotation):
        # Resize the annotation.
        item_width = self.anno_width - 2 * self.anno_padding
        annotation.set_size_request(item_width, -1)

        # Find the x, y of the annotation's mark.
        mark_x, mark_y = self._get_annotation_mark_position(annotation)
        self.anno_layout.pull(annotation, mark_y)


    def _update_annotation_area(self):
        # Update the width and color of the annotation area.
        self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, self.anno_width)
        bg_color = self.get_style().base[gtk.STATE_NORMAL]
        window   = self.get_window(gtk.TEXT_WINDOW_RIGHT)
        if window:
            window.set_background(bg_color)


    def _update_annotations(self):
        if not self.show_annotations:
            return

        # Sort the annotations by line/char number and update them.
        iter   = self.get_buffer().get_end_iter()
        rect   = self.get_iter_location(iter)
        height = rect.y + rect.height
        self.anno_layout.sort(self._get_annotation_mark_offset)
        for annotation in self.anno_layout.get_children():
            self._update_annotation(annotation)
        self.anno_layout.update(self.anno_width, height)

        # Update lines.
        self.queue_draw()


    def set_annotation_area_width(self, width, padding = 10):
        self.anno_width   = width
        self.anno_padding = padding
        self._update_annotations()


    def _on_annotation_added(self, buffer, annotation):
        if self.show_annotations == False:
            return
        annotation.set_display_buffer(self.buffer)
        view = AnnotationView(annotation)
        self.anno_views[annotation] = view
        for event in ('focus-in-event', 'focus-out-event'):
            view.connect(event, self._on_annotation_event, annotation, event)
        view.show_all()
        self._update_annotation_area()
        self.anno_layout.add(view)
        self.add_child_in_window(view,
                                 gtk.TEXT_WINDOW_RIGHT,
                                 self.anno_padding,
                                 0)
        self._update_annotations()


    def _on_annotation_removed(self, buffer, annotation):
        view = self.anno_views[annotation]
        self.anno_layout.remove(view)
        self.remove(view)


    def _on_annotation_event(self, buffer, *args):
        annotation = args[-2]
        event_name = args[-1]
        self.emit('annotation-' + event_name, annotation)


    def set_show_annotations(self, active = True):
        if self.show_annotations == active:
            return

        # Unfortunately gtk.TextView deletes all children from the
        # border window if its size is 0. So we must re-add them when the
        # window reappears.
        self.show_annotations = active
        if active:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_added(self.buffer, annotation)
        else:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_removed(self.buffer, annotation)
            self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, 0)


    def set_handle_links(self, handle):
        """
        Defines whether the cursor is changed and whether clicks are accepted
        when hovering over text with tags that have data named "link"
        attached.
        """
        self.handle_links = handle
Пример #2
0
class TextEditor(gtk.TextView):
    def __init__(self, textbuffer=None, *args, **kwargs):
        if textbuffer is None:
            textbuffer = TextBuffer()
        gtk.TextView.__init__(self, textbuffer, *args, **kwargs)
        self.buffer = textbuffer
        self.anno_width = 200
        self.anno_padding = 10
        self.anno_layout = Layout(self)
        self.anno_views = {}
        self.show_annotations = True
        self.handle_links = True
        self.set_right_margin(50 + self.anno_padding)
        self.connect('map-event', self._on_map_event)
        self.connect('expose-event', self._on_expose_event)
        self.connect('motion-notify-event', self._on_motion_notify_event)
        self.connect('event-after', self._on_event_after)
        self.buffer.connect('mark-set', self._on_buffer_mark_set)
        self.buffer.connect('annotation-added', self._on_annotation_added)
        self.buffer.connect('annotation-removed', self._on_annotation_removed)
        self.set_wrap_mode(gtk.WRAP_WORD)

    def _on_motion_notify_event(self, editor, event):
        if not self.handle_links:
            return
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x), int(event.y))
        tags = self.get_iter_at_location(x, y).get_tags()

        # Without this call, further motion notify events don't get
        # triggered.
        self.window.get_pointer()

        # If any of the tags are links, show a hand.
        cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
        for tag in tags:
            if tag.get_data('link'):
                cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
                break

        self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor)
        return False

    def _on_event_after(self, textview, event):
        # Handle links here. Only when a button was released.
        if event.type != gtk.gdk.BUTTON_RELEASE:
            return False
        if event.button != 1:
            return False
        if not self.handle_links:
            return

        # Don't follow a link if the user has selected something.
        bounds = self.buffer.get_selection_bounds()
        if bounds:
            start, end = bounds
            if start.get_offset() != end.get_offset():
                return False

        # Check whether the cursor is pointing at a link.
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x), int(event.y))
        iter = textview.get_iter_at_location(x, y)

        for tag in iter.get_tags():
            link = tag.get_data('link')
            if not link:
                continue
            self.emit('link-clicked', link)
            break
        return False

    def _on_buffer_mark_set(self, buffer, iter, mark):
        self._update_annotations()

    def _on_map_event(self, widget, event):
        self._update_annotation_area()
        self._update_annotations()

    def _on_expose_event(self, widget, event):
        text_window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
        if event.window != text_window:
            return

        # Create the cairo context.
        ctx = event.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        ctx.rectangle(event.area.x, event.area.y, event.area.width,
                      event.area.height)
        ctx.clip()

        self.draw(ctx, *event.window.get_size())

    def draw(self, ctx, w, h):
        if ctx is None:
            return
        if not self.show_annotations:
            return

        # Draw the dashes that connect annotations to their marker.
        ctx.set_line_width(1)
        ctx.set_dash((3, 2))
        right_margin = self.get_right_margin()
        for annotation in self.anno_layout.get_children():
            mark_x, mark_y = self._get_annotation_mark_position(annotation)
            anno_x, anno_y, anno_w, anno_h, d = annotation.window.get_geometry(
            )
            path = [(mark_x, mark_y - 5), (mark_x, mark_y),
                    (w - right_margin, mark_y), (w, anno_y + anno_h / 2)]

            stroke_color = annotation.get_border_color()
            ctx.set_source_rgba(*color.to_rgba(stroke_color))
            ctx.move_to(*path[0])
            for x, y in path[1:]:
                ctx.line_to(x, y)
            ctx.stroke()

    def _get_annotation_mark_offset(self, view1, view2):
        mark1 = view1.annotation.start_mark
        mark2 = view2.annotation.start_mark
        iter1 = self.get_buffer().get_iter_at_mark(mark1)
        iter2 = self.get_buffer().get_iter_at_mark(mark2)
        rect1 = self.get_iter_location(iter1)
        rect2 = self.get_iter_location(iter2)
        if rect1.y != rect2.y:
            return rect1.y - rect2.y
        return rect2.x - rect1.x

    def _get_annotation_mark_position(self, view):
        start_mark = view.annotation.start_mark
        iter = self.get_buffer().get_iter_at_mark(start_mark)
        rect = self.get_iter_location(iter)
        mark_x, mark_y = rect.x, rect.y + rect.height
        return self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, mark_x,
                                            mark_y)

    def _update_annotation(self, annotation):
        # Resize the annotation.
        item_width = self.anno_width - 2 * self.anno_padding
        annotation.set_size_request(item_width, -1)

        # Find the x, y of the annotation's mark.
        mark_x, mark_y = self._get_annotation_mark_position(annotation)
        self.anno_layout.pull(annotation, mark_y)

    def _update_annotation_area(self):
        # Update the width and color of the annotation area.
        self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, self.anno_width)
        bg_color = self.get_style().base[gtk.STATE_NORMAL]
        window = self.get_window(gtk.TEXT_WINDOW_RIGHT)
        if window:
            window.set_background(bg_color)

    def _update_annotations(self):
        if not self.show_annotations:
            return

        # Sort the annotations by line/char number and update them.
        iter = self.get_buffer().get_end_iter()
        rect = self.get_iter_location(iter)
        height = rect.y + rect.height
        self.anno_layout.sort(self._get_annotation_mark_offset)
        for annotation in self.anno_layout.get_children():
            self._update_annotation(annotation)
        self.anno_layout.update(self.anno_width, height)

        # Update lines.
        self.queue_draw()

    def set_annotation_area_width(self, width, padding=10):
        self.anno_width = width
        self.anno_padding = padding
        self._update_annotations()

    def _on_annotation_added(self, buffer, annotation):
        if self.show_annotations == False:
            return
        annotation.set_display_buffer(self.buffer)
        view = AnnotationView(annotation)
        self.anno_views[annotation] = view
        for event in ('focus-in-event', 'focus-out-event'):
            view.connect(event, self._on_annotation_event, annotation, event)
        view.show_all()
        self._update_annotation_area()
        self.anno_layout.add(view)
        self.add_child_in_window(view, gtk.TEXT_WINDOW_RIGHT,
                                 self.anno_padding, 0)
        self._update_annotations()

    def _on_annotation_removed(self, buffer, annotation):
        view = self.anno_views[annotation]
        self.anno_layout.remove(view)
        self.remove(view)

    def _on_annotation_event(self, buffer, *args):
        annotation = args[-2]
        event_name = args[-1]
        self.emit('annotation-' + event_name, annotation)

    def set_show_annotations(self, active=True):
        if self.show_annotations == active:
            return

        # Unfortunately gtk.TextView deletes all children from the
        # border window if its size is 0. So we must re-add them when the
        # window reappears.
        self.show_annotations = active
        if active:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_added(self.buffer, annotation)
        else:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_removed(self.buffer, annotation)
            self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, 0)

    def set_handle_links(self, handle):
        """
        Defines whether the cursor is changed and whether clicks are accepted
        when hovering over text with tags that have data named "link"
        attached.
        """
        self.handle_links = handle