def _on_end_user_action(self, buffer): self.user_action -= 1 if self.user_action != 0: return if self.current_undo is None: return if len(self.current_undo.children) == 0: return self._undo_add(self.current_undo) self.redo_stack = [] self.current_undo = UndoCollection(self)
def __init__(self, *args, **kwargs): gtk.TextBuffer.__init__(self, *args) self.undo_stack = [] self.redo_stack = [] self.current_undo = UndoCollection(self) self.lock_undo = False self.max_undo = 250 self.max_redo = 250 self.undo_freq = 300 # minimum milliseconds between actions self.undo_timeout_id = None self.user_action = 0 self.active_features = [] self.annotations = {} # Connect signals. self.connect('insert-text', self._on_insert_text) self.connect('delete-range', self._on_delete_range) self.connect('apply-tag', self._on_apply_tag) self.connect('remove-tag', self._on_remove_tag) self.connect('begin-user-action', self._on_begin_user_action) self.connect('end-user-action', self._on_end_user_action) # Create text styles. self.create_tag('bold', weight=pango.WEIGHT_BOLD) self.create_tag('italic', style=pango.STYLE_ITALIC) self.create_tag('underline', underline=pango.UNDERLINE_SINGLE) self.link_style = dict(foreground='blue', underline=pango.UNDERLINE_SINGLE) # Enable other features. features = ( #('list-indent', True, Features.ListIndent, ()), ) for name, default, feature, feature_args in features: active = kwargs.get(name, default) if active: self.activate_feature(feature, *feature_args) self._update_timestamp() self.register_serialize_tagset() self.register_deserialize_tagset()
def __init__(self, *args, **kwargs): gtk.TextBuffer.__init__(self, *args) self.undo_stack = [] self.redo_stack = [] self.current_undo = UndoCollection(self) self.lock_undo = False self.max_undo = 250 self.max_redo = 250 self.undo_freq = 300 # minimum milliseconds between actions self.undo_timeout_id = None self.user_action = 0 self.active_features = [] self.annotations = {} # Connect signals. self.connect('insert-text', self._on_insert_text) self.connect('delete-range', self._on_delete_range) self.connect('apply-tag', self._on_apply_tag) self.connect('remove-tag', self._on_remove_tag) self.connect('begin-user-action', self._on_begin_user_action) self.connect('end-user-action', self._on_end_user_action) # Create text styles. self.create_tag('bold', weight = pango.WEIGHT_BOLD) self.create_tag('italic', style = pango.STYLE_ITALIC) self.create_tag('underline', underline = pango.UNDERLINE_SINGLE) self.link_style = dict(foreground = 'blue', underline = pango.UNDERLINE_SINGLE) # Enable other features. features = ( #('list-indent', True, Features.ListIndent, ()), ) for name, default, feature, feature_args in features: active = kwargs.get(name, default) if active: self.activate_feature(feature, *feature_args) self._update_timestamp() self.register_serialize_tagset() self.register_deserialize_tagset()
class TextBuffer(gtk.TextBuffer): def __init__(self, *args, **kwargs): gtk.TextBuffer.__init__(self, *args) self.undo_stack = [] self.redo_stack = [] self.current_undo = UndoCollection(self) self.lock_undo = False self.max_undo = 250 self.max_redo = 250 self.undo_freq = 300 # minimum milliseconds between actions self.undo_timeout_id = None self.user_action = 0 self.active_features = [] self.annotations = {} # Connect signals. self.connect('insert-text', self._on_insert_text) self.connect('delete-range', self._on_delete_range) self.connect('apply-tag', self._on_apply_tag) self.connect('remove-tag', self._on_remove_tag) self.connect('begin-user-action', self._on_begin_user_action) self.connect('end-user-action', self._on_end_user_action) # Create text styles. self.create_tag('bold', weight = pango.WEIGHT_BOLD) self.create_tag('italic', style = pango.STYLE_ITALIC) self.create_tag('underline', underline = pango.UNDERLINE_SINGLE) self.link_style = dict(foreground = 'blue', underline = pango.UNDERLINE_SINGLE) # Enable other features. features = ( #('list-indent', True, Features.ListIndent, ()), ) for name, default, feature, feature_args in features: active = kwargs.get(name, default) if active: self.activate_feature(feature, *feature_args) self._update_timestamp() self.register_serialize_tagset() self.register_deserialize_tagset() def activate_feature(self, feature, *args): self.active_features.append(feature(self, *args)) def offset_range_has_tag(self, start, end, tag_name): tags_list = self.get_tags_at_offset(start, end) for tags in tags_list: names = [t.get_property('name') for t in tags] if tag_name not in names: return False return True def selection_has_tag(self, tag_name): # Check whether there is a selection. bounds = self.get_selection_bounds() if bounds: start = bounds[0].get_offset() end = bounds[1].get_offset() return self.offset_range_has_tag(start, end, tag_name) # So nothing is selected. Get the position of the cursor. mark = self.get_mark('insert') iter = self.get_iter_at_mark(mark) # Get a range of one char around that mark. iter.backward_char() start = iter.get_offset() iter.forward_chars(2) end = iter.get_offset() # Return True if both chars have the given tag, False otherwise. return self.offset_range_has_tag(start, end, tag_name) def tag_selection(self, tag_name): bounds = self.get_selection_bounds() if not bounds: return self.apply_tag_by_name(tag_name, *bounds) def untag_selection(self, tag_name): bounds = self.get_selection_bounds() if not bounds: return tag_list = self.get_tags_at_offset(bounds[0].get_offset(), bounds[1].get_offset()) for tags in tag_list: for tag in tags: name = tag.get_property('name') if name == tag_name: self.remove_tag(tag, *bounds) def toggle_selection_tag(self, tag_name): if self.selection_has_tag(tag_name): self.untag_selection(tag_name) else: self.tag_selection(tag_name) def _cancel_undo_timeout(self): if self.undo_timeout_id is None: return gobject.source_remove(self.undo_timeout_id) self.undo_timeout_id = None self.end_user_action() def _update_timestamp(self): if self.undo_timeout_id is not None: gobject.source_remove(self.undo_timeout_id) else: self.begin_user_action() self.undo_timeout_id = gobject.timeout_add(self.undo_freq, self._cancel_undo_timeout) def _on_insert_text(self, buffer, start, text, length): if self.lock_undo: return self._update_timestamp() item = UndoInsertText(self, start, text) self.current_undo.add(item) self.emit('undo-stack-changed') def _on_delete_range(self, buffer, start, end): if self.lock_undo: return self._update_timestamp() item = UndoDeleteText(self, start, end) self.current_undo.add(item) def _on_apply_tag(self, buffer, tag, start, end): if self.lock_undo: return if tag.get_property('name') == 'gtkspell-misspelled': return self._update_timestamp() item = UndoApplyTag(self, start, end, tag) self.current_undo.add(item) def _on_remove_tag(self, buffer, tag, start, end): if self.lock_undo: return if tag.get_property('name') == 'gtkspell-misspelled': return self._update_timestamp() item = UndoRemoveTag(self, start, end, tag) self.current_undo.add(item) def _on_begin_user_action(self, buffer): self.user_action += 1 def _on_end_user_action(self, buffer): self.user_action -= 1 if self.user_action != 0: return if self.current_undo is None: return if len(self.current_undo.children) == 0: return self._undo_add(self.current_undo) self.redo_stack = [] self.current_undo = UndoCollection(self) def _undo_add(self, item): self.undo_stack.append(item) while len(self.undo_stack) >= self.max_undo: self.undo_stack.pop(0) self.emit('undo-stack-changed') def _redo_add(self, item): self.redo_stack.append(item) while len(self.redo_stack) >= self.max_redo: self.redo_stack.pop(0) self.emit('undo-stack-changed') def insert_at_offset(self, offset, text): iter = self.get_iter_at_offset(offset) self.insert(iter, text) def delete_range_at_offset(self, start, end): start = self.get_iter_at_offset(start) end = self.get_iter_at_offset(end) self.delete(start, end) def apply_tag_at_offset(self, tag, start, end): start = self.get_iter_at_offset(start) end = self.get_iter_at_offset(end) self.apply_tag(tag, start, end) def remove_tag_at_offset(self, tag, start, end): start = self.get_iter_at_offset(start) end = self.get_iter_at_offset(end) self.remove_tag(tag, start, end) def get_tags_at_offset(self, start, end): taglist = [] iter = self.get_iter_at_offset(start) while True: taglist.append(iter.get_tags()) iter.forward_char() if iter.get_offset() >= end: break return taglist def apply_tags_at_offset(self, taglist, start, end): end = self.get_iter_at_offset(start + 1) start = self.get_iter_at_offset(start) for tags in taglist: for tag in tags: self.apply_tag(tag, start, end) start.forward_char() end.forward_char() def can_undo(self): if self.current_undo is not None \ and len(self.current_undo.children) > 0: return True return len(self.undo_stack) > 0 def undo(self): self._cancel_undo_timeout() if len(self.undo_stack) == 0: return self.lock_undo = True item = self.undo_stack.pop() item.undo() self._redo_add(item) self.lock_undo = False def flush_undo_stack(self): self._cancel_undo_timeout() if len(self.undo_stack) == 0: return self.undo_stack = [] self.emit('undo-stack-changed') def can_redo(self): return len(self.redo_stack) > 0 def redo(self): self._cancel_undo_timeout() if len(self.redo_stack) == 0: return self.lock_undo = True item = self.redo_stack.pop() item.redo() self._undo_add(item) self.lock_undo = False def flush_redo_stack(self): self._cancel_undo_timeout() if len(self.redo_stack) == 0: return self.redo_stack = [] self.emit('undo-stack-changed') def create_link_tag(self, name, target): tag = self.create_tag(name, **self.link_style) tag.set_data('link', target) return tag def add_annotation(self, annotation): self.annotations[annotation.start_mark] = annotation self.emit('annotation-added', annotation) def remove_annotation(self, annotation): self.delete_mark(annotation.start_mark) #FIXME: don't do this here del self.annotations[annotation.start_mark] self.emit('annotation-removed', annotation) def remove_annotations(self): for annotation in self.annotations.values(): self.remove_annotation(annotation) def get_annotation_from_mark(self, mark): return self.annotations[mark] def get_annotations(self): return self.annotations.values() def get_annotations_xml(self): xml = '<xml>\n' for annotation in self.annotations.itervalues(): xml += annotation.toxml() return xml + '</xml>\n' def add_annotations_from_xml(self, xml): from xml.dom.minidom import parseString root = parseString(xml) for node in root.getElementsByTagName('annotation'): self.add_annotation(Annotation.fromxml(self, node)) def _collect_links(self, tag, data): link = tag.get_data('link') if not link: return name = tag.get_property('name') self.link_xml += '<link name="%s">%s</link>\n' % (name, link) def _merge_data(self, **kwargs): """ Takes n different strings (or binary data) and merges them into one string such that they can be split later. """ index = '' data = '' pos = 0 for key, value in kwargs.iteritems(): value = str(value) end = pos + len(value) tuple = [str(pos), str(end), key] index += '|'.join(tuple) + "\n" data += value pos = end return index + "\n" + data def _unmerge_data(self, string): """ Takes a string that was created using _merge_data and splits it into the original data. """ result = dict() string = str(string) if string == '': return result index_end = string.index("\n\n") index = string[:index_end] data = string[index_end + 2:] for line in index.split("\n"): start, end, name = line.split('|') result[name] = data[int(start):int(end)] return result def dump(self): """ Serializes the content of the buffer, including annotations and links. """ # Serialize the content of the buffer. This does not include the # annotations or data that is attached to tags. format = 'application/x-gtk-text-buffer-rich-text' bounds = self.get_bounds() content = self.serialize(self, format, *bounds) # Secure the 'link' data that may be attached to a tag. self.link_xml = '<xml>\n' tag_table = self.get_tag_table() tag_table.foreach(self._collect_links) self.link_xml += '</xml>\n' # Also attach the annotation xml. return self._merge_data(content = content, links = self.link_xml, annotations = self.get_annotations_xml()) def restore(self, dump): from xml.dom.minidom import parseString data = self._unmerge_data(dump) self.delete(*self.get_bounds()) # Parse links and remove collisions from the tag table. tag_table = self.get_tag_table() links = {} root = parseString(data['links']) for node in root.getElementsByTagName('link'): name = node.getAttribute('name') link = node.childNodes[0].data links[name] = link # Restore links. for name, link in links.iteritems(): tag = tag_table.lookup(name) if tag: tag.set_data('link', link) else: self.create_link_tag(name, link) # Restore the content first. format = 'application/x-gtk-text-buffer-rich-text' self.deserialize_set_can_create_tags(format, False) self.deserialize(self, format, self.get_start_iter(), data['content']) # Restore annotations. self.remove_annotations() self.add_annotations_from_xml(data['annotations'])
class TextBuffer(gtk.TextBuffer): def __init__(self, *args, **kwargs): gtk.TextBuffer.__init__(self, *args) self.undo_stack = [] self.redo_stack = [] self.current_undo = UndoCollection(self) self.lock_undo = False self.max_undo = 250 self.max_redo = 250 self.undo_freq = 300 # minimum milliseconds between actions self.undo_timeout_id = None self.user_action = 0 self.active_features = [] self.annotations = {} # Connect signals. self.connect('insert-text', self._on_insert_text) self.connect('delete-range', self._on_delete_range) self.connect('apply-tag', self._on_apply_tag) self.connect('remove-tag', self._on_remove_tag) self.connect('begin-user-action', self._on_begin_user_action) self.connect('end-user-action', self._on_end_user_action) # Create text styles. self.create_tag('bold', weight=pango.WEIGHT_BOLD) self.create_tag('italic', style=pango.STYLE_ITALIC) self.create_tag('underline', underline=pango.UNDERLINE_SINGLE) self.link_style = dict(foreground='blue', underline=pango.UNDERLINE_SINGLE) # Enable other features. features = ( #('list-indent', True, Features.ListIndent, ()), ) for name, default, feature, feature_args in features: active = kwargs.get(name, default) if active: self.activate_feature(feature, *feature_args) self._update_timestamp() self.register_serialize_tagset() self.register_deserialize_tagset() def activate_feature(self, feature, *args): self.active_features.append(feature(self, *args)) def offset_range_has_tag(self, start, end, tag_name): tags_list = self.get_tags_at_offset(start, end) for tags in tags_list: names = [t.get_property('name') for t in tags] if tag_name not in names: return False return True def selection_has_tag(self, tag_name): # Check whether there is a selection. bounds = self.get_selection_bounds() if bounds: start = bounds[0].get_offset() end = bounds[1].get_offset() return self.offset_range_has_tag(start, end, tag_name) # So nothing is selected. Get the position of the cursor. mark = self.get_mark('insert') iter = self.get_iter_at_mark(mark) # Get a range of one char around that mark. iter.backward_char() start = iter.get_offset() iter.forward_chars(2) end = iter.get_offset() # Return True if both chars have the given tag, False otherwise. return self.offset_range_has_tag(start, end, tag_name) def tag_selection(self, tag_name): bounds = self.get_selection_bounds() if not bounds: return self.apply_tag_by_name(tag_name, *bounds) def untag_selection(self, tag_name): bounds = self.get_selection_bounds() if not bounds: return tag_list = self.get_tags_at_offset(bounds[0].get_offset(), bounds[1].get_offset()) for tags in tag_list: for tag in tags: name = tag.get_property('name') if name == tag_name: self.remove_tag(tag, *bounds) def toggle_selection_tag(self, tag_name): if self.selection_has_tag(tag_name): self.untag_selection(tag_name) else: self.tag_selection(tag_name) def _cancel_undo_timeout(self): if self.undo_timeout_id is None: return gobject.source_remove(self.undo_timeout_id) self.undo_timeout_id = None self.end_user_action() def _update_timestamp(self): if self.undo_timeout_id is not None: gobject.source_remove(self.undo_timeout_id) else: self.begin_user_action() self.undo_timeout_id = gobject.timeout_add(self.undo_freq, self._cancel_undo_timeout) def _on_insert_text(self, buffer, start, text, length): if self.lock_undo: return self._update_timestamp() item = UndoInsertText(self, start, text) self.current_undo.add(item) self.emit('undo-stack-changed') def _on_delete_range(self, buffer, start, end): if self.lock_undo: return self._update_timestamp() item = UndoDeleteText(self, start, end) self.current_undo.add(item) def _on_apply_tag(self, buffer, tag, start, end): if self.lock_undo: return if tag.get_property('name') == 'gtkspell-misspelled': return self._update_timestamp() item = UndoApplyTag(self, start, end, tag) self.current_undo.add(item) def _on_remove_tag(self, buffer, tag, start, end): if self.lock_undo: return if tag.get_property('name') == 'gtkspell-misspelled': return self._update_timestamp() item = UndoRemoveTag(self, start, end, tag) self.current_undo.add(item) def _on_begin_user_action(self, buffer): self.user_action += 1 def _on_end_user_action(self, buffer): self.user_action -= 1 if self.user_action != 0: return if self.current_undo is None: return if len(self.current_undo.children) == 0: return self._undo_add(self.current_undo) self.redo_stack = [] self.current_undo = UndoCollection(self) def _undo_add(self, item): self.undo_stack.append(item) while len(self.undo_stack) >= self.max_undo: self.undo_stack.pop(0) self.emit('undo-stack-changed') def _redo_add(self, item): self.redo_stack.append(item) while len(self.redo_stack) >= self.max_redo: self.redo_stack.pop(0) self.emit('undo-stack-changed') def insert_at_offset(self, offset, text): iter = self.get_iter_at_offset(offset) self.insert(iter, text) def delete_range_at_offset(self, start, end): start = self.get_iter_at_offset(start) end = self.get_iter_at_offset(end) self.delete(start, end) def apply_tag_at_offset(self, tag, start, end): start = self.get_iter_at_offset(start) end = self.get_iter_at_offset(end) self.apply_tag(tag, start, end) def remove_tag_at_offset(self, tag, start, end): start = self.get_iter_at_offset(start) end = self.get_iter_at_offset(end) self.remove_tag(tag, start, end) def get_tags_at_offset(self, start, end): taglist = [] iter = self.get_iter_at_offset(start) while True: taglist.append(iter.get_tags()) iter.forward_char() if iter.get_offset() >= end: break return taglist def apply_tags_at_offset(self, taglist, start, end): end = self.get_iter_at_offset(start + 1) start = self.get_iter_at_offset(start) for tags in taglist: for tag in tags: self.apply_tag(tag, start, end) start.forward_char() end.forward_char() def can_undo(self): if self.current_undo is not None \ and len(self.current_undo.children) > 0: return True return len(self.undo_stack) > 0 def undo(self): self._cancel_undo_timeout() if len(self.undo_stack) == 0: return self.lock_undo = True item = self.undo_stack.pop() item.undo() self._redo_add(item) self.lock_undo = False def flush_undo_stack(self): self._cancel_undo_timeout() if len(self.undo_stack) == 0: return self.undo_stack = [] self.emit('undo-stack-changed') def can_redo(self): return len(self.redo_stack) > 0 def redo(self): self._cancel_undo_timeout() if len(self.redo_stack) == 0: return self.lock_undo = True item = self.redo_stack.pop() item.redo() self._undo_add(item) self.lock_undo = False def flush_redo_stack(self): self._cancel_undo_timeout() if len(self.redo_stack) == 0: return self.redo_stack = [] self.emit('undo-stack-changed') def create_link_tag(self, name, target): tag = self.create_tag(name, **self.link_style) tag.set_data('link', target) return tag def add_annotation(self, annotation): self.annotations[annotation.start_mark] = annotation self.emit('annotation-added', annotation) def remove_annotation(self, annotation): self.delete_mark(annotation.start_mark) #FIXME: don't do this here del self.annotations[annotation.start_mark] self.emit('annotation-removed', annotation) def remove_annotations(self): for annotation in self.annotations.values(): self.remove_annotation(annotation) def get_annotation_from_mark(self, mark): return self.annotations[mark] def get_annotations(self): return self.annotations.values() def get_annotations_xml(self): xml = '<xml>\n' for annotation in self.annotations.itervalues(): xml += annotation.toxml() return xml + '</xml>\n' def add_annotations_from_xml(self, xml): from xml.dom.minidom import parseString root = parseString(xml) for node in root.getElementsByTagName('annotation'): self.add_annotation(Annotation.fromxml(self, node)) def _collect_links(self, tag, data): link = tag.get_data('link') if not link: return name = tag.get_property('name') self.link_xml += '<link name="%s">%s</link>\n' % (name, link) def _merge_data(self, **kwargs): """ Takes n different strings (or binary data) and merges them into one string such that they can be split later. """ index = '' data = '' pos = 0 for key, value in kwargs.iteritems(): value = str(value) end = pos + len(value) tuple = [str(pos), str(end), key] index += '|'.join(tuple) + "\n" data += value pos = end return index + "\n" + data def _unmerge_data(self, string): """ Takes a string that was created using _merge_data and splits it into the original data. """ result = dict() string = str(string) if string == '': return result index_end = string.index("\n\n") index = string[:index_end] data = string[index_end + 2:] for line in index.split("\n"): start, end, name = line.split('|') result[name] = data[int(start):int(end)] return result def dump(self): """ Serializes the content of the buffer, including annotations and links. """ # Serialize the content of the buffer. This does not include the # annotations or data that is attached to tags. format = 'application/x-gtk-text-buffer-rich-text' bounds = self.get_bounds() content = self.serialize(self, format, *bounds) # Secure the 'link' data that may be attached to a tag. self.link_xml = '<xml>\n' tag_table = self.get_tag_table() tag_table.foreach(self._collect_links) self.link_xml += '</xml>\n' # Also attach the annotation xml. return self._merge_data(content=content, links=self.link_xml, annotations=self.get_annotations_xml()) def restore(self, dump): from xml.dom.minidom import parseString data = self._unmerge_data(dump) self.delete(*self.get_bounds()) # Parse links and remove collisions from the tag table. tag_table = self.get_tag_table() links = {} root = parseString(data['links']) for node in root.getElementsByTagName('link'): name = node.getAttribute('name') link = node.childNodes[0].data links[name] = link # Restore links. for name, link in links.iteritems(): tag = tag_table.lookup(name) if tag: tag.set_data('link', link) else: self.create_link_tag(name, link) # Restore the content first. format = 'application/x-gtk-text-buffer-rich-text' self.deserialize_set_can_create_tags(format, False) self.deserialize(self, format, self.get_start_iter(), data['content']) # Restore annotations. self.remove_annotations() self.add_annotations_from_xml(data['annotations'])