Beispiel #1
0
    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() 
Beispiel #2
0
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)