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
class Table(Element): name = 'table' caption = 'Table' xoptions = gtk.EXPAND|gtk.FILL yoptions = gtk.FILL def __init__(self, rows = 2, cols = 2): Element.__init__(self, gtk.Table(rows, cols)) self.layout = Layout(0, 0) self.entry_rows = None self.entry_cols = None self.targets = {} self.child.set_row_spacings(3) self.child.set_col_spacings(3) self._resize_table(rows, cols) def has_layout(self): return True def copy(self): widget = Table(self.layout.n_rows, self.layout.n_cols) for child in self.child.get_children(): top = self.child.child_get_property(child, 'top-attach') bot = self.child.child_get_property(child, 'bottom-attach') lft = self.child.child_get_property(child, 'left-attach') rgt = self.child.child_get_property(child, 'right-attach') child = child.copy() widget.child.attach(child, lft, rgt, top, bot) widget.layout.add(child, lft, rgt, top, bot) return widget def reassign(self, widget, upper_left, lower_right): top = self.child.child_get_property(upper_left, 'top-attach') bot = self.child.child_get_property(lower_right, 'bottom-attach') lft = self.child.child_get_property(upper_left, 'left-attach') rgt = self.child.child_get_property(lower_right, 'right-attach') if top >= bot or lft >= rgt: return # Remove the old target. target = widget.get_parent_target() widget.unparent() self._remove_target(target) # Re-add it into the new cells. target = self._add_target(top, bot, lft, rgt) target.attach(widget) def _remove_target(self, target): self.layout.remove(target) self.child.remove(target) def _remove_targets_at(self, top, bot, lft, rgt): for row in range(top, bot): for col in range(lft, rgt): target = self.layout.get_widget_at(row, col) self.layout.remove(target) if target and target.parent: self.child.remove(target) def _add_target(self, top, bot, lft, rgt): # Remove the old target, if any. self._remove_targets_at(top, bot, lft, rgt) # Create a new target. target = Target() target.connect('child-attached', self._on_target_child_attached) target.connect('child-removed', self._on_target_child_removed) target.connect('child-replaced', self._on_target_child_replaced) target.set_data('row', top) target.set_data('col', lft) self.layout.add(target, lft, rgt, top, bot) self.child.attach(target, lft, rgt, top, bot) target.show_all() return target def _position_of(self, child): top = self.child.child_get_property(child, 'top-attach') bot = self.child.child_get_property(child, 'bottom-attach') lft = self.child.child_get_property(child, 'left-attach') rgt = self.child.child_get_property(child, 'right-attach') return lft, rgt, top, bot def _resize_table(self, rows, cols): old_rows, old_cols = self.layout.n_rows, self.layout.n_cols self.layout.resize(rows, cols) # Remove useless targets. for child in self.child.get_children(): if child.get_data('row') >= rows or child.get_data('col') >= cols: self.child.remove(child) # Add new targets. self.child.resize(rows, cols) for row in range(old_rows, rows): for col in range(0, old_cols): self._add_target(row, row + 1, col, col + 1) for col in range(old_cols, cols): for row in range(0, rows): self._add_target(row, row + 1, col, col + 1) def _child_at(self, x, y): for child in self.child.get_children(): child_x, child_y = self.translate_coordinates(child, x, y) if child_x < 0 or child_y < 0: continue alloc = child.get_allocation() if child_x > alloc.width or child_y > alloc.height: continue return child return None def target_at(self, x, y): child = self._child_at(x, y) if child is None: return None child_x, child_y = self.translate_coordinates(child, x, y) target = child.target_at(child_x, child_y) if target is not None: return target return child def _on_target_child_attached(self, target, child): self.child.child_set_property(target, 'x-options', child.xoptions) self.child.child_set_property(target, 'y-options', child.yoptions) def _on_target_child_removed(self, target, child): lft, rgt, top, bot = self._position_of(target) # Remove the target, as it may have a cellspan. self.child.remove(target) # Add new targets into each cell. for row in range(top, bot): for col in range(lft, rgt): self._add_target(row, row + 1, col, col + 1) def _on_target_child_replaced(self, target, child): self._on_target_child_attached(target, child)
class Table(Element): name = 'table' caption = 'Table' xoptions = gtk.EXPAND | gtk.FILL yoptions = gtk.FILL def __init__(self, rows=2, cols=2): Element.__init__(self, gtk.Table(rows, cols)) self.layout = Layout(0, 0) self.entry_rows = None self.entry_cols = None self.targets = {} self.child.set_row_spacings(3) self.child.set_col_spacings(3) self._resize_table(rows, cols) def has_layout(self): return True def copy(self): widget = Table(self.layout.n_rows, self.layout.n_cols) for child in self.child.get_children(): top = self.child.child_get_property(child, 'top-attach') bot = self.child.child_get_property(child, 'bottom-attach') lft = self.child.child_get_property(child, 'left-attach') rgt = self.child.child_get_property(child, 'right-attach') child = child.copy() widget.child.attach(child, lft, rgt, top, bot) widget.layout.add(child, lft, rgt, top, bot) return widget def reassign(self, widget, upper_left, lower_right): top = self.child.child_get_property(upper_left, 'top-attach') bot = self.child.child_get_property(lower_right, 'bottom-attach') lft = self.child.child_get_property(upper_left, 'left-attach') rgt = self.child.child_get_property(lower_right, 'right-attach') if top >= bot or lft >= rgt: return # Remove the old target. target = widget.get_parent_target() widget.unparent() self._remove_target(target) # Re-add it into the new cells. target = self._add_target(top, bot, lft, rgt) target.attach(widget) def _remove_target(self, target): self.layout.remove(target) self.child.remove(target) def _remove_targets_at(self, top, bot, lft, rgt): for row in range(top, bot): for col in range(lft, rgt): target = self.layout.get_widget_at(row, col) self.layout.remove(target) if target and target.parent: self.child.remove(target) def _add_target(self, top, bot, lft, rgt): # Remove the old target, if any. self._remove_targets_at(top, bot, lft, rgt) # Create a new target. target = Target() target.connect('child-attached', self._on_target_child_attached) target.connect('child-dropped', self._on_target_child_removed) target.connect('child-replaced', self._on_target_child_replaced) target.set_data('row', top) target.set_data('col', lft) self.layout.add(target, lft, rgt, top, bot) self.child.attach(target, lft, rgt, top, bot) target.show_all() return target def _position_of(self, child): top = self.child.child_get_property(child, 'top-attach') bot = self.child.child_get_property(child, 'bottom-attach') lft = self.child.child_get_property(child, 'left-attach') rgt = self.child.child_get_property(child, 'right-attach') return lft, rgt, top, bot def _resize_table(self, rows, cols): old_rows, old_cols = self.layout.n_rows, self.layout.n_cols self.layout.resize(rows, cols) # Remove useless targets. for child in self.child.get_children(): if child.get_data('row') >= rows or child.get_data('col') >= cols: self.child.remove(child) # Add new targets. self.child.resize(rows, cols) for row in range(old_rows, rows): for col in range(0, old_cols): self._add_target(row, row + 1, col, col + 1) for col in range(old_cols, cols): for row in range(0, rows): self._add_target(row, row + 1, col, col + 1) def _child_at(self, x, y): for child in self.child.get_children(): child_x, child_y = self.translate_coordinates(child, x, y) if child_x < 0 or child_y < 0: continue alloc = child.get_allocation() if child_x > alloc.width or child_y > alloc.height: continue return child return None def target_at(self, x, y): child = self._child_at(x, y) if child is None: return None child_x, child_y = self.translate_coordinates(child, x, y) target = child.target_at(child_x, child_y) if target is not None: return target return child def _on_target_child_attached(self, target, child): self.child.child_set_property(target, 'x-options', child.xoptions) self.child.child_set_property(target, 'y-options', child.yoptions) def _on_target_child_removed(self, target, child): lft, rgt, top, bot = self._position_of(target) # Remove the target, as it may have a cellspan. self.child.remove(target) # Add new targets into each cell. for row in range(top, bot): for col in range(lft, rgt): self._add_target(row, row + 1, col, col + 1) def _on_target_child_replaced(self, target, child): self._on_target_child_attached(target, child)
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