def __init__(self, textview=None): RichTextBaseBuffer.__init__(self, RichTextTagTable()) # indentation manager self._indent = IndentHandler(self) # set of all anchors in buffer self._anchors = set() self._child_uninit = set() # anchors that still need to be added, # they are defferred because textview was not available at insert-time self._anchors_deferred = set()
class RichTextBuffer (RichTextBaseBuffer): """ TextBuffer specialized for rich text editing It builds upon the features of RichTextBaseBuffer - maintains undo/redo stacks - manages "current font" behavior Additional Features - manages specific child widget actions - images - horizontal rule - manages editing of indentation levels and bullet point lists """ def __init__(self, textview=None): RichTextBaseBuffer.__init__(self, RichTextTagTable()) # indentation manager self._indent = IndentHandler(self) # set of all anchors in buffer self._anchors = set() self._child_uninit = set() # anchors that still need to be added, # they are defferred because textview was not available at insert-time self._anchors_deferred = set() def clear(self): """Clear buffer contents""" RichTextBaseBuffer.clear(self) self._anchors.clear() self._anchors_deferred.clear() def insert_contents(self, contents, it=None): """Inserts a content stream into the TextBuffer at iter 'it'""" if it is None: it = self.get_iter_at_mark(self.get_insert()) self.begin_user_action() insert_buffer_contents(self, it, contents, add_child_to_buffer, lookup_tag=lambda name: self.tag_table.lookup(name)) self.end_user_action() def copy_contents(self, start, end): """Return a content stream for copying from iter start and end""" contents = iter(iter_buffer_contents(self, start, end, ignore_tag)) # remove regions that can't be copied for item in contents: # NOTE: item = (kind, it, param) if item[0] == "begin" and not item[2].can_be_copied(): end_tag = item[2] while not (item[0] == "end" and item[2] == end_tag): item = contents.next() if item[0] not in ("text", "anchor") and \ item[2] != end_tag: yield item continue yield item def on_selection_changed(self): """Callback for when selection changes""" self.highlight_children() def on_ending_user_action(self): """ Callback for when user action is about to end Convenient for implementing extra actions that should be included in current user action """ # perfrom queued indentation updates self._indent.update_indentation() def on_paragraph_split(self, start, end): """Callback for when paragraphs split""" if self.is_interactive(): self._indent.on_paragraph_split(start, end) def on_paragraph_merge(self, start, end): """Callback for when paragraphs merge""" if self.is_interactive(): self._indent.on_paragraph_merge(start, end) def on_paragraph_change(self, start, end): """Callback for when paragraph type changes""" if self.is_interactive(): self._indent.on_paragraph_change(start, end) def is_insert_allowed(self, it, text=""): """Returns True if insertion is allowed at iter 'it'""" # ask the indentation manager whether the insert is allowed return self._indent.is_insert_allowed(it, text) and \ it.can_insert(True) def _on_delete_range(self, textbuffer, start, end): # TODO: should I add something like this back? # let indent manager prepare the delete #if self.is_interactive(): # self._indent.prepare_delete_range(start, end) # call super class RichTextBaseBuffer._on_delete_range(self, textbuffer, start, end) # deregister any deleted anchors for kind, offset, param in self._next_action.contents: if kind == "anchor": self._anchors.remove(param[0]) #========================================= # indentation interface def indent(self, start=None, end=None): """Indent paragraph level""" self._indent.change_indent(start, end, 1) def unindent(self, start=None, end=None): """Unindent paragraph level""" self._indent.change_indent(start, end, -1) def starts_par(self, it): """Returns True if iter 'it' starts a paragraph""" return self._indent.starts_par(it) def toggle_bullet_list(self, par_type=None): """Toggle the state of a bullet list""" self._indent.toggle_bullet_list(par_type) def get_indent(self, it=None): return self._indent.get_indent(it) #============================================================ # child actions def add_child(self, it, child): # preprocess child if isinstance(child, RichTextImage): self._determine_image_name(child) # setup child self._anchors.add(child) self._child_uninit.add(child) child.set_buffer(self) child.connect("activated", self._on_child_activated) child.connect("selected", self._on_child_selected) child.connect("popup-menu", self._on_child_popup_menu) self.insert_child_anchor(it, child) # let textview, if attached know we added a child self._anchors_deferred.add(child) self.emit("child-added", child) def add_deferred_anchors(self, textview): """Add anchors that were deferred""" for child in self._anchors_deferred: # only add anchor if it is still present (hasn't been deleted) if child in self._anchors: self._add_child_at_anchor(child, textview) self._anchors_deferred.clear() def _add_child_at_anchor(self, child, textview): #print "add", child.get_widget() # skip children whose insertion was rejected if child.get_deleted(): return textview.add_child_at_anchor(child.get_widget(), child) child.get_widget().show() #print "added", child.get_widget() def insert_image(self, image, filename="image.png"): """Inserts an image into the textbuffer at current position""" # set default filename if image.get_filename() is None: image.set_filename(filename) # insert image into buffer self.begin_user_action() it = self.get_iter_at_mark(self.get_insert()) self.add_child(it, image) image.get_widget().show() self.end_user_action() def insert_hr(self): """Insert Horizontal Rule""" self.begin_user_action() it = self.get_iter_at_mark(self.get_insert()) hr = RichTextHorizontalRule() self.add_child(it, hr) self.end_user_action() #=================================== # Image management def get_image_filenames(self): filenames = [] for child in self._anchors: if isinstance(child, RichTextImage): filenames.append(child.get_filename()) return filenames def _determine_image_name(self, image): """Determines image filename""" if self._is_new_pixbuf(image.get_original_pixbuf()): filename, ext = os.path.splitext(image.get_filename()) filenames = self.get_image_filenames() filename2 = keepnote.get_unique_filename_list(filenames, filename, ext) image.set_filename(filename2) image.set_save_needed(True) def _is_new_pixbuf(self, pixbuf): # cannot tell if pixbuf is new because it is not loaded if pixbuf is None: return False for child in self._anchors: if isinstance(child, RichTextImage): if pixbuf == child.get_original_pixbuf(): return False return True #============================================= # links def get_tag_region(self, it, tag): """ Get the start and end TextIters for tag occuring at TextIter it Assumes tag occurs at TextIter it """ # get bounds of link tag start = it.copy() if tag not in it.get_toggled_tags(True): start.backward_to_tag_toggle(tag) end = it.copy() if tag not in it.get_toggled_tags(False): end.forward_to_tag_toggle(tag) return start, end def get_link(self, it=None): if it is None: # use cursor sel = self.get_selection_bounds() if len(sel) > 0: it = sel[0] else: it = self.get_iter_at_mark(self.get_insert()) for tag in it.get_tags(): if isinstance(tag, RichTextLinkTag): start, end = self.get_tag_region(it, tag) return tag, start, end return None, None, None def set_link(self, url, start, end): if url is None: tag = self.tag_table.lookup(RichTextLinkTag.tag_name("")) self.clear_tag_class(tag, start, end) return None else: tag = self.tag_table.lookup(RichTextLinkTag.tag_name(url)) self.apply_tag_selected(tag, start, end) return tag #============================================== # Child callbacks def _on_child_selected(self, child): """Callback for when child object is selected Make sure buffer knows the selection """ it = self.get_iter_at_child_anchor(child) end = it.copy() end.forward_char() self.select_range(it, end) def _on_child_activated(self, child): """Callback for when child is activated (e.g. double-clicked)""" # forward callback to listeners (textview) self.emit("child-activated", child) def _on_child_popup_menu(self, child, button, activate_time): """Callback for when child's menu is visible""" # forward callback to listeners (textview) self.emit("child-menu", child, button, activate_time) def highlight_children(self): """Highlight any children that are within selection range""" # TODO: I once had an exception that said an anchor in self._anchors # was already deleted. I do not know yet how this got out of # sync, seeing as I listen for deletes. sel = self.get_selection_bounds() focus = None if len(sel) > 0: # selection exists, get range (a, b) a = sel[0].get_offset() b = sel[1].get_offset() for child in self._anchors: it = self.get_iter_at_child_anchor(child) offset = it.get_offset() if a <= offset < b: child.highlight() else: child.unhighlight() w = child.get_widget() if w: top = w.get_toplevel() if top: f = top.get_focus() if f: focus = f if focus: focus.grab_focus() else: # no selection, unselect all children for child in self._anchors: child.unhighlight() # TODO: need to overload get_font to be indent aware def get_font(self, font=None): """Get font under cursor""" if font is None: font = RichTextFont() return RichTextBaseBuffer.get_font(self, font)