From 0542a9b78cd4a0bcdbb1943b1e811573016a01e0 Mon Sep 17 00:00:00 2001 From: Zsolt Foldvari Date: Fri, 28 Mar 2008 23:22:46 +0000 Subject: [PATCH] Introducing StyledText in Notes. svn: r10410 --- src/DisplayModels/_NoteModel.py | 72 ++- src/Editors/_EditNote.py | 476 +++++++-------- .../_StyledTextBuffer.py} | 577 ++++-------------- src/gen/db/dbdir.py | 41 +- src/gen/lib/__init__.py | 5 + src/gen/lib/grampstype.py | 66 +- src/gen/lib/note.py | 138 +++-- src/gen/lib/styledtext.py | 162 +++++ src/gen/lib/styledtexttag.py | 73 +++ src/gen/lib/styledtexttagtype.py | 73 +++ 10 files changed, 879 insertions(+), 804 deletions(-) rename src/{MarkupText.py => Editors/_StyledTextBuffer.py} (51%) create mode 100644 src/gen/lib/styledtext.py create mode 100644 src/gen/lib/styledtexttag.py create mode 100644 src/gen/lib/styledtexttagtype.py diff --git a/src/DisplayModels/_NoteModel.py b/src/DisplayModels/_NoteModel.py index 4bef39d8a..fd08cd1a7 100644 --- a/src/DisplayModels/_NoteModel.py +++ b/src/DisplayModels/_NoteModel.py @@ -1,7 +1,7 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2000-2006 Donald N. Allingham +# Copyright (C) 2000-2007 Donald N. Allingham # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,7 @@ # #------------------------------------------------------------------------- import logging -log = logging.getLogger(".") +_LOG = logging.getLogger(".DisplayModels.NoteModel") #------------------------------------------------------------------------- # @@ -40,20 +40,19 @@ import gtk # #------------------------------------------------------------------------- from _BaseModel import BaseModel -import gen.lib +from gen.lib import (Note, NoteType, MarkerType, StyledText) #------------------------------------------------------------------------- # -# PlaceModel +# NoteModel # #------------------------------------------------------------------------- class NoteModel(BaseModel): - - HANDLE_COL = 4 - _MARKER_COL = 6 - - def __init__(self,db,scol=0, order=gtk.SORT_ASCENDING,search=None, + """ + """ + def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None, skip=set(), sort_map=None): + """Setup initial values for instance variables.""" self.gen_cursor = db.get_note_cursor self.map = db.get_raw_note_data self.fmap = [ @@ -63,7 +62,7 @@ class NoteModel(BaseModel): self.column_marker, self.column_handle, self.column_marker_color - ] + ] self.smap = [ self.column_preview, self.column_id, @@ -71,48 +70,57 @@ class NoteModel(BaseModel): self.column_marker, self.column_handle, self.column_marker_color - ] + ] self.marker_color_column = 5 - BaseModel.__init__(self, db, scol, order, - search=search, skip=skip, sort_map=sort_map) + BaseModel.__init__(self, db, scol, order, search=search, + skip=skip, sort_map=sort_map) def on_get_n_columns(self): - return len(self.fmap)+1 + """Return the column number of the Note tab.""" + return len(self.fmap) + 1 - def column_handle(self,data): - return data[0] + def column_handle(self, data): + """Return the handle of the Note.""" + return data[Note.POS_HANDLE] - def column_id(self,data): - return unicode(data[1]) + def column_id(self, data): + """Return the id of the Note.""" + return unicode(data[Note.POS_ID]) - def column_type(self,data): - temp = gen.lib.NoteType() - temp.set(data[4]) + def column_type(self, data): + """Return the type of the Note in readable format.""" + temp = NoteType() + temp.set(data[Note.POS_TYPE]) return unicode(str(temp)) def column_marker(self, data): - temp = gen.lib.MarkerType() - temp.set(data[6]) + """Return the marker type of the Note in readable format.""" + temp = MarkerType() + temp.set(data[Note.POS_MARKER]) return unicode(str(temp)) - def column_preview(self,data): + def column_preview(self, data): + """Return a shortend version of the Note's text.""" #data is the encoding in the database, make it a unicode object #for universal work - note = " ".join(unicode(data[2]).split()) + note = unicode(data[Note.POS_TEXT][StyledText.POS_TEXT]) + note = " ".join(note.split()) if len(note) > 80: - return note[:80]+"..." + return note[:80] + "..." else: return note def column_marker_color(self, data): + """Return the color of the Note's marker type if exist.""" try: - col = data[NoteModel._MARKER_COL][0] - if col == gen.lib.MarkerType.COMPLETE: + col = data[Note.POS_MARKER][MarkerType.POS_VALUE] + if col == MarkerType.COMPLETE: return self.complete_color - elif col == gen.lib.MarkerType.TODO_TYPE: + elif col == MarkerType.TODO_TYPE: return self.todo_color - elif col == gen.lib.MarkerType.CUSTOM: + elif col == MarkerType.CUSTOM: return self.custom_color + else: + return None except IndexError: - pass - return None + return None diff --git a/src/Editors/_EditNote.py b/src/Editors/_EditNote.py index 3b9dcc76f..ee932d7b0 100644 --- a/src/Editors/_EditNote.py +++ b/src/Editors/_EditNote.py @@ -28,7 +28,8 @@ from gettext import gettext as _ import logging -log = logging.getLogger(".") +_LOG = logging.getLogger(".Editors.EditNote") + #------------------------------------------------------------------------- # # GTK libraries @@ -41,19 +42,20 @@ import pango #------------------------------------------------------------------------- # -# GRAMPS classes +# GRAMPS modules # #------------------------------------------------------------------------- -import const -import Spell import Config -import GrampsDisplay -import MarkupText +from const import GLADE_FILE +from Spell import Spell +from GrampsDisplay import url +from Editors._StyledTextBuffer import (StyledTextBuffer, MATCH_START, + MATCH_END, MATCH_FLAVOR, MATCH_STRING) from Editors._EditPrimary import EditPrimary from DisplayTabs import GrampsTab, NoteBackRefList from GrampsWidgets import (MonitoredDataType, MonitoredCheckbox, MonitoredEntry, PrivacyButton) -import gen.lib +from gen.lib import Note from QuestionDialog import ErrorDialog #------------------------------------------------------------------------- @@ -61,17 +63,16 @@ from QuestionDialog import ErrorDialog # Constants # #------------------------------------------------------------------------- -#USERCHARS = "-A-Za-z0-9" -#PASSCHARS = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" -#HOSTCHARS = "-A-Za-z0-9" -#PATHCHARS = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%" -##SCHEME = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" -#SCHEME = "(file:/|https?:|ftps?:|webcal:)" -#USER = "[" + USERCHARS + "]+(:[" + PASSCHARS + "]+)?" -#URLPATH = "/[" + PATHCHARS + "]*[^]'.}>) \t\r\n,\\\"]" -# -#(GENERAL, HTTP, MAIL) = range(3) +USERCHARS = "-A-Za-z0-9" +PASSCHARS = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" +HOSTCHARS = "-A-Za-z0-9" +PATHCHARS = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%" +#SCHEME = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" +SCHEME = "(file:/|https?:|ftps?:|webcal:)" +USER = "[" + USERCHARS + "]+(:[" + PASSCHARS + "]+)?" +URLPATH = "/[" + PATHCHARS + "]*[^]'.}>) \t\r\n,\\\"]" +(GENERAL, HTTP, MAIL) = range(3) #------------------------------------------------------------------------- @@ -91,17 +92,17 @@ class NoteTab(GrampsTab): the database, along with other state information. The GrampsTab uses this to access the database and to pass to and created child windows (such as edit dialogs). - @type dbstate: DbState + @type dbstate: L{DbState.DbState} @param uistate: The UI state. Used primarily to pass to any created subwindows. - @type uistate: DisplayState + @type uistate: L{DisplayState.DisplayState} @param track: The window tracking mechanism used to manage windows. This is only used to pass to generted child windows. @type track: list @param name: Notebook label name @type name: str/unicode @param widget: widget to be shown in the tab - @type widge: gtk widget + @type widget: gtk widget """ GrampsTab.__init__(self, dbstate, uistate, track, name) eventbox = gtk.EventBox() @@ -131,11 +132,12 @@ class EditNote(EditPrimary): callertitle = None, extratype = None): """Create an EditNote window. Associate a note with the window. - @param callertitle: a text passed by calling object to add to title + @param callertitle: Text passed by calling object to add to title @type callertitle: str - @param extratype: extra NoteType values to add to the default types - They are removed from the ignorelist of NoteType. + @param extratype: Extra L{NoteType} values to add to the default types. + They are removed from the ignorelist of L{NoteType}. @type extratype: list of int + """ self.callertitle = callertitle self.extratype = extratype @@ -146,9 +148,10 @@ class EditNote(EditPrimary): def empty_object(self): """Return an empty Note object for comparison for changes. - It is used by the base class (EditPrimary). + It is used by the base class L{EditPrimary}. + """ - empty_note = gen.lib.Note(); + empty_note = Note(); if self.extratype: empty_note.set_type(self.extratype[0]) return empty_note @@ -157,16 +160,16 @@ class EditNote(EditPrimary): if self.obj.get_handle(): if self.callertitle : title = _('Note: %(id)s - %(context)s') % { - 'id' : self.obj.get_gramps_id(), - 'context' : self.callertitle - } + 'id' : self.obj.get_gramps_id(), + 'context' : self.callertitle + } else : title = _('Note: %s') % self.obj.get_gramps_id() else: if self.callertitle : title = _('New Note - %(context)s') % { - 'context' : self.callertitle - } + 'context' : self.callertitle + } else : title = _('New Note') @@ -179,11 +182,11 @@ class EditNote(EditPrimary): """Local initialization function. Perform basic initialization, including setting up widgets - and the glade interface. It is called by the base class (EditPrimary), + and the glade interface. It is called by the base class L{EditPrimary}, and overridden here. """ - self.top = glade.XML(const.GLADE_FILE, "edit_note", "gramps") + self.top = glade.XML(GLADE_FILE, "edit_note", "gramps") win = self.top.get_widget("edit_note") self.set_window(win, None, self.get_menu_title()) @@ -242,7 +245,7 @@ class EditNote(EditPrimary): def _connect_signals(self): """Connects any signals that need to be connected. - Called by the init routine of the base class (_EditPrimary). + Called by the init routine of the base class L{EditPrimary}. """ self.define_ok_button(self.top.get_widget('ok'), self.save) @@ -250,122 +253,68 @@ class EditNote(EditPrimary): self.define_help_button(self.top.get_widget('help'), '') def _create_tabbed_pages(self): - """ - Create the notebook tabs and inserts them into the main - window. - """ + """Create the notebook tabs and inserts them into the main window.""" notebook = self.top.get_widget("note_notebook") self._add_tab(notebook, self.ntab) - self.backref_tab = self._add_tab( - notebook, - NoteBackRefList(self.dbstate, self.uistate, self.track, - self.dbstate.db.find_backlink_handles( - self.obj.handle)) - ) + handles = self.dbstate.db.find_backlink_handles(self.obj.handle) + rlist = NoteBackRefList(self.dbstate, self.uistate, self.track, handles) + self.backref_tab = self._add_tab(notebook, rlist) - self._setup_notebook_tabs( notebook) + self._setup_notebook_tabs(notebook) # THIS IS THE MARKUP VERSION - enable for markup -# def build_interface(self): -# FORMAT_TOOLBAR = ''' -# -# -# -# -# -# -# -# -# -# -# -# -# -# ''' -# -# buffer_ = MarkupText.MarkupBuffer() -# buffer_.create_tag('hyperlink', -# underline=pango.UNDERLINE_SINGLE, -# foreground='blue') -# buffer_.match_add("(www|ftp)[" + HOSTCHARS + "]*\\.[" + HOSTCHARS + -# ".]+" + "(:[0-9]+)?(" + URLPATH + ")?/?", HTTP) -# buffer_.match_add("(mailto:)?[a-z0-9][a-z0-9.-]*@[a-z0-9][a-z0-9-]*" -# "(\\.[a-z0-9][a-z0-9-]*)+", MAIL) -# buffer_.match_add(SCHEME + "//(" + USER + "@)?[" + HOSTCHARS + ".]+" + -# "(:[0-9]+)?(" + URLPATH + ")?/?", GENERAL) -# self.match = None -# self.last_match = None -# -# self.text = self.top.get_widget('text') -# self.text.set_editable(not self.dbstate.db.readonly) -# self.text.set_buffer(buffer_) -# self.text.connect('key-press-event', -# self.on_textview_key_press_event) -# self.text.connect('insert-at-cursor', -# self.on_textview_insert_at_cursor) -# self.text.connect('delete-from-cursor', -# self.on_textview_delete_from_cursor) -# self.text.connect('paste-clipboard', -# self.on_textview_paste_clipboard) -# self.text.connect('motion-notify-event', -# self.on_textview_motion_notify_event) -# self.text.connect('button-press-event', -# self.on_textview_button_press_event) -# self.text.connect('populate-popup', -# self.on_textview_populate_popup) -# -# # setup spell checking interface -# spellcheck = Spell.Spell(self.text) -# liststore = gtk.ListStore(gobject.TYPE_STRING) -# cell = gtk.CellRendererText() -# lang_selector = self.top.get_widget('spell') -# lang_selector.set_model(liststore) -# lang_selector.pack_start(cell, True) -# lang_selector.add_attribute(cell, 'text', 0) -# act_lang = spellcheck.get_active_language() -# idx = 0 -# for lang in spellcheck.get_all_languages(): -# lang_selector.append_text(lang) -# if lang == act_lang: -# act_idx = idx -# idx = idx + 1 -# lang_selector.set_active(act_idx) -# lang_selector.connect('changed', self.on_spell_change, spellcheck) -# #lang_selector.set_sensitive(Config.get(Config.SPELLCHECK)) -# -# # create a formatting toolbar -# if not self.dbstate.db.readonly: -# uimanager = gtk.UIManager() -# uimanager.insert_action_group(buffer_.format_action_group, 0) -# uimanager.add_ui_from_string(FORMAT_TOOLBAR) -# uimanager.ensure_update() -# -# toolbar = uimanager.get_widget('/ToolBar') -# toolbar.set_style(gtk.TOOLBAR_ICONS) -# vbox = self.top.get_widget('container') -# vbox.pack_start(toolbar) -# -# # setup initial values for textview and buffer_ -# if self.obj: -# self.empty = False -# self.flow_changed(self.obj.get_format()) -# buffer_.set_text(self.obj.get(markup=True)) -# log.debug("Initial Note: %s" % buffer_.get_text()) -# else: -# self.empty = True - -# NON-MARKUP VERSION - Disable for markup def build_interface(self): - buffer_ = gtk.TextBuffer() + FORMAT_TOOLBAR = ''' + + + + + + + + + + + + + + ''' + + textbuffer = StyledTextBuffer() + textbuffer.create_tag('hyperlink', + underline=pango.UNDERLINE_SINGLE, + foreground='blue') + textbuffer.match_add("(www|ftp)[" + HOSTCHARS + "]*\\.[" + HOSTCHARS + + ".]+" + "(:[0-9]+)?(" + URLPATH + ")?/?", HTTP) + textbuffer.match_add("(mailto:)?[a-z0-9][a-z0-9.-]*@[a-z0-9][a-z0-9-]*" + "(\\.[a-z0-9][a-z0-9-]*)+", MAIL) + textbuffer.match_add(SCHEME + "//(" + USER + "@)?[" + HOSTCHARS + + ".]+" + "(:[0-9]+)?(" + URLPATH + ")?/?", GENERAL) + self.match = None + self.last_match = None self.text = self.top.get_widget('text') self.text.set_editable(not self.dbstate.db.readonly) - self.text.set_buffer(buffer_) + self.text.set_buffer(textbuffer) + self.text.connect('key-press-event', + self.on_textview_key_press_event) + self.text.connect('insert-at-cursor', + self.on_textview_insert_at_cursor) + self.text.connect('delete-from-cursor', + self.on_textview_delete_from_cursor) + self.text.connect('paste-clipboard', + self.on_textview_paste_clipboard) + self.text.connect('motion-notify-event', + self.on_textview_motion_notify_event) + self.text.connect('button-press-event', + self.on_textview_button_press_event) + self.text.connect('populate-popup', + self.on_textview_populate_popup) # setup spell checking interface - spellcheck = Spell.Spell(self.text) + spellcheck = Spell(self.text) liststore = gtk.ListStore(gobject.TYPE_STRING) cell = gtk.CellRendererText() lang_selector = self.top.get_widget('spell') @@ -382,14 +331,62 @@ class EditNote(EditPrimary): lang_selector.set_active(act_idx) lang_selector.connect('changed', self.on_spell_change, spellcheck) #lang_selector.set_sensitive(Config.get(Config.SPELLCHECK)) + + # create a formatting toolbar + if not self.dbstate.db.readonly: + uimanager = gtk.UIManager() + uimanager.insert_action_group(textbuffer.format_action_group, 0) + uimanager.add_ui_from_string(FORMAT_TOOLBAR) + uimanager.ensure_update() + + toolbar = uimanager.get_widget('/ToolBar') + toolbar.set_style(gtk.TOOLBAR_ICONS) + vbox = self.top.get_widget('container') + vbox.pack_start(toolbar) - # setup initial values for textview and buffer_ + # setup initial values for textview and textbuffer if self.obj: self.empty = False self.flow_changed(self.obj.get_format()) - buffer_.set_text(self.obj.get()) + textbuffer.set_text(self.obj.get_styledtext()) + _LOG.debug("Initial Note: %s" % str(textbuffer.get_text())) else: self.empty = True + +# NON-MARKUP VERSION - Disable for markup + #def build_interface(self): + #textbuffer = gtk.TextBuffer() + + #self.text = self.top.get_widget('text') + #self.text.set_editable(not self.dbstate.db.readonly) + #self.text.set_buffer(textbuffer) + + ## setup spell checking interface + #spellcheck = Spell(self.text) + #liststore = gtk.ListStore(gobject.TYPE_STRING) + #cell = gtk.CellRendererText() + #lang_selector = self.top.get_widget('spell') + #lang_selector.set_model(liststore) + #lang_selector.pack_start(cell, True) + #lang_selector.add_attribute(cell, 'text', 0) + #act_lang = spellcheck.get_active_language() + #idx = 0 + #for lang in spellcheck.get_all_languages(): + #lang_selector.append_text(lang) + #if lang == act_lang: + #act_idx = idx + #idx = idx + 1 + #lang_selector.set_active(act_idx) + #lang_selector.connect('changed', self.on_spell_change, spellcheck) + ##lang_selector.set_sensitive(Config.get(Config.SPELLCHECK)) + + ## setup initial values for textview and textbuffer + #if self.obj: + #self.empty = False + #self.flow_changed(self.obj.get_format()) + #textbuffer.set_text(self.obj.get()) + #else: + #self.empty = True def build_menu_names(self, person): """ @@ -401,119 +398,116 @@ class EditNote(EditPrimary): def _post_init(self): self.text.grab_focus() -# enable for markup -# def on_textview_key_press_event(self, textview, event): -# """Handle shortcuts in the TextView.""" -# return textview.get_buffer().on_key_press_event(textview, event) -# -# def on_textview_insert_at_cursor(self, textview, string): -# log.debug("Textview insert '%s'" % string) -# -# def on_textview_delete_from_cursor(self, textview, type, count): -# log.debug("Textview delete type %d count %d" % (type, count)) -# -# def on_textview_paste_clipboard(self, textview): -# log.debug("Textview paste clipboard") -# -# def on_textview_motion_notify_event(self, textview, event): -# window = textview.get_window(gtk.TEXT_WINDOW_TEXT) -# x, y = textview.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, -# int(event.x), int(event.y)) -# iter = textview.get_iter_at_location(x, y) -# buffer_ = textview.get_buffer() -# self.match = buffer_.match_check(iter.get_offset()) -# -# if self.match != self.last_match: -# start, end = buffer_.get_bounds() -# buffer_.remove_tag_by_name('hyperlink', start, end) -# if self.match: -# start_offset = self.match[MarkupText.MATCH_START] -# end_offset = self.match[MarkupText.MATCH_END] -# -# start = buffer_.get_iter_at_offset(start_offset) -# end = buffer_.get_iter_at_offset(end_offset) -# -# buffer_.apply_tag_by_name('hyperlink', start, end) -# window.set_cursor(self.hand_cursor) -# else: -# window.set_cursor(self.regular_cursor) -# -# self.last_match = self.match -# -# textview.window.get_pointer() -# return False -# -# def on_textview_button_press_event(self, textview, event): -# if ((event.type == gtk.gdk.BUTTON_PRESS) and -# (event.button == 1) and -# (event.state and gtk.gdk.CONTROL_MASK) and -# (self.match)): -# -# flavor = self.match[MarkupText.MATCH_FLAVOR] -# url = self.match[MarkupText.MATCH_STRING] -# self.open_url_cb(None, url, flavor) -# -# return False -# -# def on_textview_populate_popup(self, textview, menu): -# """Insert extra menuitems according to matched pattern.""" -# if self.match: -# flavor = self.match[MarkupText.MATCH_FLAVOR] -# url = self.match[MarkupText.MATCH_STRING] -# -# if flavor == MAIL: -# open_menu = gtk.MenuItem(_('_Send Mail To...')) -# copy_menu = gtk.MenuItem(_('Copy _E-mail Address')) -# else: -# open_menu = gtk.MenuItem(_('_Open Link')) -# copy_menu = gtk.MenuItem(_('Copy _Link Address')) -# -# copy_menu.connect('activate', self.copy_url_cb, url, flavor) -# copy_menu.show() -# menu.prepend(copy_menu) -# -# open_menu.connect('activate', self.open_url_cb, url, flavor) -# open_menu.show() -# menu.prepend(open_menu) + def on_textview_key_press_event(self, textview, event): + """Handle shortcuts in the TextView.""" + return textview.get_buffer().on_key_press_event(textview, event) + + def on_textview_insert_at_cursor(self, textview, string): + _LOG.debug("Textview insert '%s'" % string) + + def on_textview_delete_from_cursor(self, textview, type, count): + _LOG.debug("Textview delete type %d count %d" % (type, count)) + + def on_textview_paste_clipboard(self, textview): + _LOG.debug("Textview paste clipboard") + + def on_textview_motion_notify_event(self, textview, event): + window = textview.get_window(gtk.TEXT_WINDOW_TEXT) + x, y = textview.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, + int(event.x), int(event.y)) + iter = textview.get_iter_at_location(x, y) + textbuffer = textview.get_buffer() + self.match = textbuffer.match_check(iter.get_offset()) + + if self.match != self.last_match: + start, end = textbuffer.get_bounds() + textbuffer.remove_tag_by_name('hyperlink', start, end) + if self.match: + start_offset = self.match[MATCH_START] + end_offset = self.match[MATCH_END] + + start = textbuffer.get_iter_at_offset(start_offset) + end = textbuffer.get_iter_at_offset(end_offset) + + textbuffer.apply_tag_by_name('hyperlink', start, end) + window.set_cursor(self.hand_cursor) + else: + window.set_cursor(self.regular_cursor) + + self.last_match = self.match + + textview.window.get_pointer() + return False + + def on_textview_button_press_event(self, textview, event): + if ((event.type == gtk.gdk.BUTTON_PRESS) and + (event.button == 1) and + (event.state and gtk.gdk.CONTROL_MASK) and + (self.match)): + + flavor = self.match[MATCH_FLAVOR] + url = self.match[MATCH_STRING] + self.open_url_cb(None, url, flavor) + + return False + + def on_textview_populate_popup(self, textview, menu): + """Insert extra menuitems according to matched pattern.""" + if self.match: + flavor = self.match[MATCH_FLAVOR] + url = self.match[MATCH_STRING] + + if flavor == MAIL: + open_menu = gtk.MenuItem(_('_Send Mail To...')) + copy_menu = gtk.MenuItem(_('Copy _E-mail Address')) + else: + open_menu = gtk.MenuItem(_('_Open Link')) + copy_menu = gtk.MenuItem(_('Copy _Link Address')) + + copy_menu.connect('activate', self.copy_url_cb, url, flavor) + copy_menu.show() + menu.prepend(copy_menu) + + open_menu.connect('activate', self.open_url_cb, url, flavor) + open_menu.show() + menu.prepend(open_menu) def on_spell_change(self, combobox, spell): """Set spell checker language according to user selection.""" lang = combobox.get_active_text() spell.set_active_language(lang) -# enable for markup -# def open_url_cb(self, menuitem, url, flavor): -# if not url: -# return -# -# if flavor == HTTP: -# url = 'http:' + url -# elif flavor == MAIL: -# if not url.startswith('mailto:'): -# url = 'mailto:' + url -# elif flavor == GENERAL: -# pass -# else: -# return -# -# GrampsDisplay.url(url) -# -# def copy_url_cb(self, menuitem, url, flavor): -# """Copy url to both useful selections.""" -# clipboard = gtk.Clipboard(selection="CLIPBOARD") -# clipboard.set_text(url) -# -# clipboard = gtk.Clipboard(selection="PRIMARY") -# clipboard.set_text(url) + def open_url_cb(self, menuitem, url, flavor): + if not url: + return + + if flavor == HTTP: + url = 'http:' + url + elif flavor == MAIL: + if not url.startswith('mailto:'): + url = 'mailto:' + url + elif flavor == GENERAL: + pass + else: + return + + url(url) + + def copy_url_cb(self, menuitem, url, flavor): + """Copy url to both useful selections.""" + clipboard = gtk.Clipboard(selection="CLIPBOARD") + clipboard.set_text(url) + + clipboard = gtk.Clipboard(selection="PRIMARY") + clipboard.set_text(url) def update_note(self): """Update the Note object with current value.""" if self.obj: - buffer_ = self.text.get_buffer() - (start, stop) = buffer_.get_bounds() - text = buffer_.get_text(start, stop) - self.obj.set(text) - log.debug(text) + textbuffer = self.text.get_buffer() + text = textbuffer.get_text() + self.obj.set_styledtext(text) + _LOG.debug(str(text)) def flow_changed(self, active): if active: diff --git a/src/MarkupText.py b/src/Editors/_StyledTextBuffer.py similarity index 51% rename from src/MarkupText.py rename to src/Editors/_StyledTextBuffer.py index f92de629a..0e7c32569 100644 --- a/src/MarkupText.py +++ b/src/Editors/_StyledTextBuffer.py @@ -1,7 +1,7 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2000-2006 Donald N. Allingham +# Copyright (C) 2007-2008 Zsolt Foldvari # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,28 +20,18 @@ # $Id$ -"Handling formatted ('rich text') strings" +"Text buffer subclassed from gtk.TextBuffer handling L{StyledText}." #------------------------------------------------------------------------- # # Python modules # #------------------------------------------------------------------------- -from xml.sax import saxutils, xmlreader, ContentHandler -from xml.sax import parseString, SAXParseException +from gettext import gettext as _ import re -try: - from cStringIO import StringIO -except: - from StringIO import StringIO -#------------------------------------------------------------------------- -# -# Set up logging -# -#------------------------------------------------------------------------- import logging -log = logging.getLogger(".MarkupText") +_LOG = logging.getLogger(".StyledTextBuffer") #------------------------------------------------------------------------- # @@ -50,318 +40,49 @@ log = logging.getLogger(".MarkupText") #------------------------------------------------------------------------- import gtk from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE -import gobject + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.lib import (StyledText, StyledTextTag, StyledTextTagType) #------------------------------------------------------------------------- # # Constants # #------------------------------------------------------------------------- -ROOT_ELEMENT = 'gramps' -ROOT_START_TAG = '<' + ROOT_ELEMENT + '>' -ROOT_END_TAG = '' -LEN_ROOT_START_TAG = len(ROOT_START_TAG) -LEN_ROOT_END_TAG = len(ROOT_END_TAG) - (MATCH_START, MATCH_END, MATCH_FLAVOR, MATCH_STRING,) = range(4) -def is_gramps_markup(text): - return (text[:LEN_ROOT_START_TAG] == ROOT_START_TAG and - text[-LEN_ROOT_END_TAG:] == ROOT_END_TAG) - -def clear_root_tags(text): - return text[LEN_ROOT_START_TAG:len(text)-LEN_ROOT_END_TAG] - -class MarkupParser(ContentHandler): - """A simple ContentHandler class to parse Gramps markup'ed text. - - Use it with xml.sax.parse() or xml.sax.parseString(). A root tag is - required. Parsing result can be obtained via the public attributes of - the class: - @attr content: clean text - @attr type: str - @attr elements: list of markup elements - @attr type: list[tuple((start, end), name, attrs),] - - """ - def startDocument(self): - self._open_document = False - self._open_elements = [] - self.elements = [] - self.content = "" - - def endDocument(self): - self._open_document = False - if len(self._open_elements): - raise SAXParseException('Unclosed tags') - - def startElement(self, name, attrs): - if not self._open_document: - if name == ROOT_ELEMENT: - self._open_document = True - else: - raise SAXParseException('Root element missing') - else: - self._open_elements.append({'name': name, - 'attrs': attrs.copy(), - 'start': len(self.content), - }) - - def endElement(self, name): - # skip root element - if name == ROOT_ELEMENT: - return - - for e in self._open_elements: - if e['name'] == name: - self.elements.append(((e['start'], len(self.content)), - e['name'], e['attrs'])) - - self._open_elements.remove(e) - return - - def characters (self, chunk): - self.content += chunk - -class MarkupWriter: - """Generate XML markup text for Notes. - - Provides additional feature of accounting opened tags and closing them - properly in case of partially overlapping elements. - - """ - (EVENT_START, - EVENT_END) = range(2) - - def __init__(self, encoding='utf-8'): - self._output = StringIO() - self._encoding = encoding - self._writer = saxutils.XMLGenerator(self._output, self._encoding) - - self._attrs = xmlreader.AttributesImpl({}) - - self._open_elements = [] - self.content = '' - - # Private - - def _elements_to_events(self, elements): - """Create an event list for XML writer. - - @param elements: list of XML elements with start/end indices and attrs - @param type: [((start, end), xml_element_name, attrs),] - @return: eventdict - @rtype: {index: [(xml_element_name, attrs, event_type, pair_index),]} - index: place of the event - xml_element_name: element to apply - attrs: attributes of the tag (xml.sax.xmlreader.AttrubutesImpl) - event_type: START or END event - pair_index: index of the pair event, used for sorting - - """ - eventdict = {} - for (start, end), name, attrs in elements: - # append START events - if eventdict.has_key(start): - eventdict[start].append((name, attrs, self.EVENT_START, end)) - else: - eventdict[start] = [(name, attrs, self.EVENT_START, end)] - # END events have to prepended to avoid creating empty elements - if eventdict.has_key(end): - eventdict[end].insert(0, (name, attrs, self.EVENT_END, start)) - else: - eventdict[end] = [(name, attrs, self.EVENT_END, start)] - - # first round optimization - active_tags = {} - active_idx = {} - - indices = eventdict.keys() - indices.sort() - for index in indices: - # separate the events by tag names - tagdict = {} - for event in eventdict[index]: - # we care only about tags having attributes - if event[1].getLength(): - if tagdict.has_key(event[0]): - tagdict[event[0]].append(event) - else: - tagdict[event[0]] = [event] - - # let's handle each tag - for tag_name in tagdict.keys(): - - # first we close the tag if it's already open - if active_tags.has_key(tag_name): - tmp_attrs = xmlreader.AttributesImpl({}) - tmp_attrs._attrs.update(active_tags[tag_name]) - eventdict[index].insert(0, (name, tmp_attrs, - self.EVENT_END, - active_idx[tag_name])) - # go back where the tag was opened and update the pair_idx, - # i.e. with the current index. - # FIXME this is ugly - for event in eventdict[active_idx[tag_name]]: - if (event[0] == tag_name and - event[2] == self.EVENT_START): - new_event = (event[0], event[1], event[2], index) - eventdict[active_idx[tag_name]].remove(event) - eventdict[active_idx[tag_name]].append(new_event) - else: - active_tags[tag_name] = xmlreader.AttributesImpl({}) - - # update - for event in tagdict[tag_name]: - # remove this event, we will insert new ones instead - eventdict[index].remove(event) - - # update the active attribute object for the tag - (name, attrs, type, pair_idx) = event - if type == self.EVENT_START: - active_tags[name]._attrs.update(attrs) - elif type == self.EVENT_END: - for attr_name in attrs.getNames(): - try: - del active_tags[name]._attrs[attr_name] - except: - pass # error - else: - pass # error - - # if the tag's attr list is empty after the updates - # delete the tag completely from the list of active tags - if not active_tags[name].getLength(): - del active_tags[name] - ##del active_idx[name] - - # re-open all tags with updated attrs - if active_tags.has_key(tag_name): - tmp_attrs = xmlreader.AttributesImpl({}) - tmp_attrs._attrs.update(active_tags[tag_name]) - eventdict[index].append((tag_name, tmp_attrs, - self.EVENT_START, 0)) - # also save the index of tag opening - active_idx[tag_name] = index - - # sort events at the same index - indices = eventdict.keys() - for idx in indices: - if len(eventdict[idx]) > 1: - eventdict[idx].sort(self._sort_events) - - return eventdict - - def _sort_events(self, event_a, event_b): - """Sort events that are at the same index. - - Sorting with the following rules: - 1. END event goes always before START event; - 2. from two START events the one goes first, which has it's own END - event later; - 3. from two END events the one goes first, which has it's own START - event later. - - """ - tag_a, attr_a, type_a, pair_a = event_a - tag_b, attr_b, type_b, pair_b = event_b - - if (type_a + type_b) == (self.EVENT_START + self.EVENT_END): - return type_b - type_a - else: - return pair_b - pair_a - - def _startElement(self, name, attrs=None): - """Insert start tag.""" - if not attrs: - attrs = self._attrs - self._writer.startElement(name, attrs) - self._open_elements.append((name, attrs)) - - def _endElement(self, name): - """Insert end tag.""" - if not len(self._open_elements): - log.debug("Trying to close element '%s' when non is open" % name) - return - - tmp_list = [] - elem = '' - - # close all open elements until we reach to the requested one - while elem != name: - try: - elem, attrs = self._open_elements.pop() - self._writer.endElement(elem) - if elem != name: - tmp_list.append((elem, attrs)) - except: - # we need to do something smart here... - log.debug("Trying to close non open element '%s'" % name) - break - - # open all other elements again - while True: - try: - elem, attrs = tmp_list.pop() - self._startElement(elem, attrs) - except: - break - - # Public - - def generate(self, text, elements): - # reset output and start root element - self._output.truncate(0) - self._writer.startElement(ROOT_ELEMENT, self._attrs) - - # split the elements to events - events = self._elements_to_events(elements) - - # feed the events into the xml generator - last_pos = 0 - indices = events.keys() - indices.sort() - for index in indices: - self._writer.characters(text[last_pos:index]) - for name, attrs, event_type, p in events[index]: - if event_type == self.EVENT_START: - self._startElement(name, attrs) - elif event_type == self.EVENT_END: - self._endElement(name) - last_pos = index - self._writer.characters(text[last_pos:]) - - # close root element and end doc - self._writer.endElement(ROOT_ELEMENT) - self._writer.endDocument() - - # copy result - self.content = self._output.getvalue() - log.debug("Gramps XML: %s" % self.content) - +#------------------------------------------------------------------------- +# +# GtkSpellState class +# +#------------------------------------------------------------------------- class GtkSpellState: """A simple state machine kinda thingy. - Try tracking gtk.Spell activities on a buffer and reapply formatting + Trying to track gtk.Spell activities on a buffer and re-apply formatting after gtk.Spell replaces a misspelled word. - + """ (STATE_NONE, STATE_CLICKED, STATE_DELETED, STATE_INSERTING) = range(4) - def __init__(self, buffer): - if not isinstance(buffer, gtk.TextBuffer): + def __init__(self, textbuffer): + if not isinstance(textbuffer, gtk.TextBuffer): raise TypeError("Init parameter must be instance of gtk.TextBuffer") - buffer.connect('mark-set', self.on_buffer_mark_set) - buffer.connect('delete-range', self.on_buffer_delete_range) - buffer.connect('insert-text', self.on_buffer_insert_text) - buffer.connect_after('insert-text', self.after_buffer_insert_text) + textbuffer.connect('mark-set', self.on_buffer_mark_set) + textbuffer.connect('delete-range', self.on_buffer_delete_range) + textbuffer.connect('insert-text', self.on_buffer_insert_text) + textbuffer.connect_after('insert-text', self.after_buffer_insert_text) self.reset_state() @@ -371,36 +92,37 @@ class GtkSpellState: self.end = 0 self.tags = None - def on_buffer_mark_set(self, buffer, iter, mark): + def on_buffer_mark_set(self, textbuffer, iter, mark): mark_name = mark.get_name() if mark_name == 'gtkspell-click': self.state = self.STATE_CLICKED - self.start, self.end = self.get_word_extents_from_mark(buffer, mark) - log.debug("SpellState got start %d end %d" % (self.start, self.end)) + self.start, self.end = self.get_word_extents_from_mark(textbuffer, + mark) + _LOG.debug("SpellState got start %d end %d" % (self.start, self.end)) elif mark_name == 'insert': self.reset_state() - def on_buffer_delete_range(self, buffer, start, end): + def on_buffer_delete_range(self, textbuffer, start, end): if ((self.state == self.STATE_CLICKED) and (start.get_offset() == self.start) and (end.get_offset() == self.end)): self.state = self.STATE_DELETED self.tags = start.get_tags() - def on_buffer_insert_text(self, buffer, iter, text, length): + def on_buffer_insert_text(self, textbuffer, iter, text, length): if self.state == self.STATE_DELETED and iter.get_offset() == self.start: self.state = self.STATE_INSERTING - def after_buffer_insert_text(self, buffer, iter, text, length): + def after_buffer_insert_text(self, textbuffer, iter, text, length): if self.state == self.STATE_INSERTING: - mark = buffer.get_mark('gtkspell-insert-start') - insert_start = buffer.get_iter_at_mark(mark) + mark = textbuffer.get_mark('gtkspell-insert-start') + insert_start = textbuffer.get_iter_at_mark(mark) for tag in self.tags: - buffer.apply_tag(tag, insert_start, iter) + textbuffer.apply_tag(tag, insert_start, iter) self.reset_state() - def get_word_extents_from_mark(self, buffer, mark): + def get_word_extents_from_mark(self, textbuffer, mark): """Get the word extents as gtk.Spell does. Used to get the beginning of the word, in which user right clicked. @@ -408,7 +130,7 @@ class GtkSpellState: misspelled words. """ - start = buffer.get_iter_at_mark(mark) + start = textbuffer.get_iter_at_mark(mark) if not start.starts_word(): #start.backward_word_start() self.backward_word_start(start) @@ -455,14 +177,27 @@ class GtkSpellState: return True -class MarkupBuffer(gtk.TextBuffer): - """An extended TextBuffer with Gramps XML markup string interface. +#------------------------------------------------------------------------- +# +# StyledTextBuffer class +# +#------------------------------------------------------------------------- +class StyledTextBuffer(gtk.TextBuffer): + """An extended TextBuffer for handling StyledText strings. - It implements MarkupParser and MarkupWriter on the input/output interfaces. - Also translates Gramps XML markup language to gtk.TextTag's and vice versa. + StyledTextBuffer is an interface between GRAMPS' L{StyledText} format + and gtk.TextBuffer. To get/set the text use the L{get_text} and + L{set_text} methods. + + It provides an action group (L{format_action_group}) for GUIs. + + StyledTextBuffer has a regexp pattern matching mechanism too. To add a + regexp pattern to match in the text use the L{match_add} method. To check + if there's a match at a certain position in the text use the L{match_check} + method. """ - __gtype_name__ = 'MarkupBuffer' + __gtype_name__ = 'StyledTextBuffer' formats = ('italic', 'bold', 'underline', 'font', 'foreground', 'background',) @@ -470,9 +205,6 @@ class MarkupBuffer(gtk.TextBuffer): def __init__(self): gtk.TextBuffer.__init__(self) - self.parser = MarkupParser() - self.writer = MarkupWriter() - # Create fix tags. # Other tags (e.g. color) have to be created on the fly self.create_tag('bold', weight=WEIGHT_BOLD) @@ -520,6 +252,8 @@ class MarkupBuffer(gtk.TextBuffer): self.bold = False self.underline = False self.font = None + # TODO could we separate font name and size? + ##self.size = None self.foreground = None self.background = None @@ -545,15 +279,15 @@ class MarkupBuffer(gtk.TextBuffer): # Virtual methods - def on_insert_text(self, buffer, iter, text, length): - log.debug("Will insert at %d length %d" % (iter.get_offset(), length)) + def on_insert_text(self, textbuffer, iter, text, length): + _LOG.debug("Will insert at %d length %d" % (iter.get_offset(), length)) # let's remember where we started inserting self.move_mark(self.mark_insert, iter) - def after_insert_text(self, buffer, iter, text, length): + def after_insert_text(self, textbuffer, iter, text, length): """Format inserted text.""" - log.debug("Have inserted at %d length %d (%s)" % + _LOG.debug("Have inserted at %d length %d (%s)" % (iter.get_offset(), length, text)) if not length: @@ -572,8 +306,8 @@ class MarkupBuffer(gtk.TextBuffer): self.apply_tag(self._find_tag_by_name(format, value), insert_start, iter) - def after_delete_range(self, buffer, start, end): - log.debug("Deleted from %d till %d" % + def after_delete_range(self, textbuffer, start, end): + _LOG.debug("Deleted from %d till %d" % (start.get_offset(), end.get_offset())) # move 'insert' marker to have the format attributes updated @@ -592,16 +326,15 @@ class MarkupBuffer(gtk.TextBuffer): match = iter.next() self.matches.append((match.start(), match.end(), flavor, match.group())) - log.debug("Matches: %d, %d: %s [%d]" % + _LOG.debug("Matches: %d, %d: %s [%d]" % (match.start(), match.end(), match.group(), flavor)) except StopIteration: break - def do_mark_set(self, iter, mark): """Update format attributes each time the cursor moves.""" - log.debug("Setting mark %s at %d" % + _LOG.debug("Setting mark %s at %d" % (mark.get_name(), iter.get_offset())) if mark.get_name() != 'insert': @@ -629,72 +362,32 @@ class MarkupBuffer(gtk.TextBuffer): # Private - def _xmltag_to_texttag(self, name, attrs): - """Convert XML tag to gtk.TextTag. - - Return only the name of the TextTag. + def _tagname_to_tagtype(self, name): + """Convert gtk.TextTag names to StyledTextTagType values.""" + tag2type = { + 'bold': StyledTextTagType.BOLD, + 'italic': StyledTextTagType.ITALIC, + 'underline': StyledTextTagType.UNDERLINE, + 'foreground': StyledTextTagType.FONTCOLOR, + 'background': StyledTextTagType.HIGHLIGHT, + 'font': StyledTextTagType.FONTFACE, + } - @param name: name of the XML tag - @param type: string - @param attrs: attributes of the XML tag - @param type: xmlreader.AttributesImpl - @return: property of gtk.TextTag, value of property - @rtype: [(string, string), ] - - """ - if name == 'b': - return [('bold', None)] - elif name == 'i': - return [('italic', None)] - elif name == 'u': - return [('underline', None)] - elif name == 'font': - ret = [] - attr_names = attrs.getNames() - if 'color' in attr_names: - ret.append(('foreground', attrs.getValue('color'))) - if 'highlight' in attr_names: - ret.append(('background', attrs.getValue('highlight'))) - if ('face' in attr_names) and ('size' in attr_names): - ret.append(('font', '%s %s' % (attrs.getValue('face'), - attrs.getValue('size')))) - if len(ret): - return ret - else: - return [(None, None)] - else: - return [(None, None)] - - def _texttag_to_xmltag(self, name): - """Convert gtk.TextTag to XML tag. - - @param name: name of the gtk.TextTag - @param type: string - @return: XML tag name, attribute - @rtype: string, xmlreader.AttributesImpl - - """ - attrs = xmlreader.AttributesImpl({}) - if name == 'bold': - return 'b', attrs - elif name == 'italic': - return 'i', attrs - elif name == 'underline': - return 'u', attrs - elif name.startswith('foreground'): - attrs._attrs['color'] = name.split()[1] - return 'font', attrs - elif name.startswith('background'): - attrs._attrs['highlight'] = name.split()[1] - return 'font', attrs - elif name.startswith('font'): - name = name.replace('font ', '') - attrs._attrs['face'] = name.rsplit(' ', 1)[0] - attrs._attrs['size'] = name.rsplit(' ', 1)[1] - return 'font', attrs - else: - return None, None + return StyledTextTagType(tag2type[name]) + + def _tagtype_to_tagname(self, tagtype): + """Convert StyledTextTagType values to gtk.TextTag names.""" + type2tag = { + StyledTextTagType.BOLD: 'bold', + StyledTextTagType.ITALIC: 'italic', + StyledTextTagType.UNDERLINE: 'underline', + StyledTextTagType.FONTCOLOR: 'foreground', + StyledTextTagType.HIGHLIGHT: 'background', + StyledTextTagType.FONTFACE: 'font', + } + return type2tag[tagtype] + ##def get_tag_value_at_insert(self, name): ##"""Get the value of the given tag at the insertion point.""" ##tags = self.get_iter_at_mark(self._insert).get_tags() @@ -835,12 +528,7 @@ class MarkupBuffer(gtk.TextBuffer): setattr(self, action.get_name(), action.get_active()) def on_action_activate(self, action): - """Apply a format. - - Other tags for the same format have to be removed from the range - first otherwise XML would get messy. - - """ + """Apply a format.""" format = action.get_name() if format == 'foreground': @@ -870,11 +558,11 @@ class MarkupBuffer(gtk.TextBuffer): value = font_selection.fontsel.get_font_name() font_selection.destroy() else: - log.debug("unknown format: '%s'" % format) + _LOG.debug("unknown format: '%s'" % format) return if response == gtk.RESPONSE_OK: - log.debug("applying format '%s' with value '%s'" % (format, value)) + _LOG.debug("applying format '%s' with value '%s'" % (format, value)) tag = self._find_tag_by_name(format, value) self.remove_format_from_selection(format) @@ -905,64 +593,51 @@ class MarkupBuffer(gtk.TextBuffer): # Public API - def set_text(self, xmltext): + def set_text(self, r_text): """Set the content of the buffer with markup tags.""" - try: - parseString(xmltext.encode('utf-8'), self.parser) - text = self.parser.content - except: - # if parse fails remove all tags and use clear text instead - text = re.sub(r'(<.*?>)', '', xmltext) - text = saxutils.unescape(text) - - gtk.TextBuffer.set_text(self, text) - - for element in self.parser.elements: - (start, end), xmltag_name, attrs = element - - #texttag_name, value = self._xmltag_to_texttag(xmltag_name, attrs) - tags = self._xmltag_to_texttag(xmltag_name, attrs) - - for texttag_name, value in tags: - if texttag_name is not None: + gtk.TextBuffer.set_text(self, str(r_text)) + + r_tags = r_text.get_tags() + for r_tag in r_tags: + tagname = self._tagtype_to_tagname(int(r_tag.name)) + g_tag = self._find_tag_by_name(tagname, r_tag.value) + if g_tag is not None: + for (start, end) in r_tag.ranges: start_iter = self.get_iter_at_offset(start) end_iter = self.get_iter_at_offset(end) - tag = self._find_tag_by_name(texttag_name, value) - if tag is not None: - self.apply_tag(tag, start_iter, end_iter) - + self.apply_tag(g_tag, start_iter, end_iter) + def get_text(self, start=None, end=None, include_hidden_chars=True): - """Return the buffer text with xml markup tags. - - If no markup was applied returns clean text - (i.e. without even root tags). - - """ - # get the clear text from the buffer + """Return the buffer text.""" if not start: start = self.get_start_iter() if not end: end = self.get_end_iter() - txt = unicode(gtk.TextBuffer.get_text(self, start, end)) + txt = gtk.TextBuffer.get_text(self, start, end, include_hidden_chars) + txt = unicode(txt) + # extract tags out of the buffer - texttag = self.get_tag_from_range() + g_tags = self.get_tag_from_range() + r_tags = [] - if len(texttag): - # convert the texttags to xml elements - xml_elements = [] - for texttag_name, indices in texttag.items(): - xml_tag_name, attrs = self._texttag_to_xmltag(texttag_name) - if xml_tag_name is not None: - for start_idx, end_idx in indices: - xml_elements.append(((start_idx, end_idx+1), - xml_tag_name, attrs)) + for g_tagname, g_ranges in g_tags.items(): + name_value = g_tagname.split(' ', 1) - # feed the elements into the xml writer - self.writer.generate(txt, xml_elements) - txt = self.writer.content + if len(name_value) == 1: + name = name_value[0] + r_value = None + else: + (name, r_value) = name_value + + if name in self.formats: + r_tagtype = self._tagname_to_tagtype(name) + r_ranges = [(start, end+1) for (start, end) in g_ranges] + r_tag = StyledTextTag(r_tagtype, r_value, r_ranges) + + r_tags.append(r_tag) - return txt + return StyledText(txt, r_tags) def match_add(self, pattern, flavor): """Add a pattern to look for in the text.""" @@ -972,11 +647,7 @@ class MarkupBuffer(gtk.TextBuffer): def match_check(self, pos): """Check if pos falls into any of the matched patterns.""" for match in self.matches: - if pos >= match[0] and pos <= match[1]: + if pos >= match[MATCH_START] and pos <= match[MATCH_END]: return match return None - - -if gtk.pygtk_version < (2,8,0): - gobject.type_register(MarkupBuffer) diff --git a/src/gen/db/dbdir.py b/src/gen/db/dbdir.py index 0315fb0d8..ffb1ed3bc 100644 --- a/src/gen/db/dbdir.py +++ b/src/gen/db/dbdir.py @@ -39,7 +39,7 @@ from gettext import gettext as _ from bsddb import dbshelve, db import logging -log = logging.getLogger(".GrampsDb") +_LOG = logging.getLogger(".GrampsDb") #------------------------------------------------------------------------- # @@ -56,7 +56,7 @@ from gen.db.cursor import GrampsCursor import Errors _MINVERSION = 9 -_DBVERSION = 13 +_DBVERSION = 14 IDTRANS = "person_id" FIDTRANS = "family_id" @@ -1485,7 +1485,7 @@ class GrampsDBDir(GrampsDbBase, UpdateCallback): # under certain circumstances during a database reload, # data_map can be none. If so, then don't report an error if data_map: - log.error("Failed to get from handle", exc_info=True) + _LOG.error("Failed to get from handle", exc_info=True) if data: newobj = InstanceType(class_type) newobj.unserialize(data) @@ -1665,13 +1665,42 @@ class GrampsDBDir(GrampsDbBase, UpdateCallback): def gramps_upgrade(self, callback=None): UpdateCallback.__init__(self, callback) -# version = self.metadata.get('version', default=_MINVERSION) + version = self.metadata.get('version', default=_MINVERSION) t = time.time() -# if version < 13: -# self.gramps_upgrade_13() + + if version < 14: + self.gramps_upgrade_14() + print "Upgrade time:", int(time.time()-t), "seconds" + + def gramps_upgrade_14(self): + """Upgrade database from version 13 to 14.""" + # This upgrade modifies notes + length = len(self.note_map) + self.set_total(length) + + # replace clear text with StyledText in Notes + for handle in self.note_map.keys(): + note = self.note_map[handle] + (junk_handle, gramps_id, text, format, note_type, + change, marker, private) = note + + styled_text = (text, []) + + new_note = (handle, gramps_id, styled_text, format, note_type, + change, marker, private) + + the_txn = self.env.txn_begin() + self.note_map.put(str(handle), new_note, txn=the_txn) + the_txn.commit() + self.update() + + # Bump up database version. Separate transaction to save metadata. + the_txn = self.env.txn_begin() + self.metadata.put('version', 14, txn=the_txn) + the_txn.commit() class BdbTransaction(Transaction): def __init__(self, msg, db, batch=False, no_magic=False): diff --git a/src/gen/lib/__init__.py b/src/gen/lib/__init__.py index ba42be364..0dd489f61 100644 --- a/src/gen/lib/__init__.py +++ b/src/gen/lib/__init__.py @@ -69,3 +69,8 @@ from gen.lib.srcmediatype import SourceMediaType from gen.lib.eventroletype import EventRoleType from gen.lib.markertype import MarkerType from gen.lib.notetype import NoteType +from gen.lib.styledtexttagtype import StyledTextTagType + +# Text +from gen.lib.styledtexttag import StyledTextTag +from gen.lib.styledtext import StyledText diff --git a/src/gen/lib/grampstype.py b/src/gen/lib/grampstype.py index 2f93786a1..8430934fc 100644 --- a/src/gen/lib/grampstype.py +++ b/src/gen/lib/grampstype.py @@ -31,19 +31,26 @@ Base type for all gramps types. #------------------------------------------------------------------------ from gettext import gettext as _ +#------------------------------------------------------------------------- +# +# _init_map function +# +#------------------------------------------------------------------------- def _init_map(data, key_col, data_col, blacklist=None): - """ - Initialize the map, building a new map from the specified columns. - """ + """Initialize the map, building a new map from the specified columns.""" if blacklist: - new_data = dict([ (item[key_col], item[data_col]) - for item in data - if not item[0] in blacklist ]) + new_data = dict([(item[key_col], item[data_col]) + for item in data if not item[0] in blacklist]) else: - new_data = dict([ (item[key_col], item[data_col]) - for item in data ]) + new_data = dict([(item[key_col], item[data_col]) for item in data]) + return new_data +#------------------------------------------------------------------------- +# +# GrampsTypeMeta class +# +#------------------------------------------------------------------------- class GrampsTypeMeta(type): """Metaclass for L{GrampsType}. @@ -55,15 +62,30 @@ class GrampsTypeMeta(type): type.__init__(mcs, name, bases, namespace) mcs.__class_init__(namespace) +#------------------------------------------------------------------------- +# +# GrampsType class +# +#------------------------------------------------------------------------- class GrampsType(object): """Base class for all Gramps object types. - _DATAMAP is a 3-tuple like (index, localized_string, english_string) - _BLACKLIST is a list of indices to ignore (obsolete/retired entries) - (gramps policy is never to delete type values, - or reuse the name (TOKEN) of any specific type value) + @cvar _DATAMAP: 3-tuple like (index, localized_string, english_string). + @type _DATAMAP: list + @cvar _BLACKLIST: List of indices to ignore (obsolete/retired entries). + (gramps policy is never to delete type values, or reuse the name (TOKEN) + of any specific type value) + @type _BLACKLIST: list + @cvar POS_: Position of attribute in the serialized format of + an instance. + @type POS_: int + + @attention: The POS_ class variables reflect the serialized object, they + have to be updated in case the data structure or the L{serialize} method + changes! """ + (POS_VALUE, POS_STRING) = range(2) _CUSTOM = 0 _DEFAULT = 0 @@ -89,8 +111,8 @@ class GrampsType(object): self.set(value) def __set_tuple(self, value): - v,s = self._DEFAULT,u'' - if len(value) > 0: + v, s = self._DEFAULT, u'' + if value: v = value[0] if len(value) > 1: s = value[1] @@ -148,15 +170,11 @@ class GrampsType(object): return self._I2EMAP[self.val] def serialize(self): - """ - Convert the object to a serialized tuple of data. - """ + """Convert the object to a serialized tuple of data. """ return (self.val, self.string) def unserialize(self, data): - """ - Convert a serialized tuple of data to an object. - """ + """Convert a serialized tuple of data to an object.""" self.val, self.string = data def __str__(self): @@ -172,16 +190,12 @@ class GrampsType(object): return self._I2SMAP def get_standard_names(self): - """ - Return the list of localized names for all standard types. - """ + """Return the list of localized names for all standard types.""" return [s for (i, s) in self._I2SMAP.items() if (i != self._CUSTOM) and s.strip()] def get_standard_xml(self): - """ - Return the list of XML (english) names for all standard types. - """ + """Return the list of XML (english) names for all standard types.""" return [s for (i, s) in self._I2EMAP.items() if (i != self._CUSTOM) and s.strip()] diff --git a/src/gen/lib/note.py b/src/gen/lib/note.py index c4eb51e45..329c7d774 100644 --- a/src/gen/lib/note.py +++ b/src/gen/lib/note.py @@ -32,6 +32,7 @@ Note class for GRAMPS. from gen.lib.primaryobj import BasicPrimaryObject from gen.lib.notetype import NoteType from gen.lib.markertype import MarkerType +from gen.lib.styledtext import StyledText #------------------------------------------------------------------------- # @@ -39,118 +40,163 @@ from gen.lib.markertype import MarkerType # #------------------------------------------------------------------------- class Note(BasicPrimaryObject): - """ - Introduction - ============ - The Note class defines a text note. The note may be preformatted - or 'flowed', which indicates that it text string is considered - to be in paragraphs, separated by newlines. - """ + """Define a text note. - FLOWED = 0 - FORMATTED = 1 + Starting from GRAMPS 3.1 Note object stores the text in L{StyledText} + instance, thus it can have text formatting information. - def __init__(self, text = ""): - """ - Create a new Note object, initializing from the passed string. - """ + To get and set only the clear text of the note use the L{get} and L{set} + methods. + + To get and set the formatted version of the Note's text use the + L{get_styledtext} and L{set_styledtext} methods. + + The note may be 'preformatted' or 'flowed', which indicates that the + text string is considered to be in paragraphs, separated by newlines. + + @cvar POS_: Position of attribute in the serialized format of + an instance. + @type POS_: int + + @attention: The POS_ class variables reflect the serialized object, they + have to be updated in case the data structure or the L{serialize} method + changes! + + """ + (FLOWED, FORMATTED) = range(2) + + (POS_HANDLE, + POS_ID, + POS_TEXT, + POS_FORMAT, + POS_TYPE, + POS_CHANGE, + POS_MARKER, + POS_PRIVATE,) = range(8) + + def __init__(self, text=""): + """Create a new Note object, initializing from the passed string.""" BasicPrimaryObject.__init__(self) - self.text = text + self.text = StyledText(text) self.format = Note.FLOWED self.type = NoteType() def serialize(self): + """Convert the object to a serialized tuple of data. + + @returns: The serialized format of the instance. + @rtype: tuple + """ - Convert the object to a serialized tuple of data. - """ - return (self.handle, self.gramps_id, self.text, self.format, + return (self.handle, self.gramps_id, self.text.serialize(), self.format, self.type.serialize(), self.change, self.marker.serialize(), self.private) def unserialize(self, data): + """Convert a serialized tuple of data to an object. + + @param data: The serialized format of a Note. + @type: data: tuple + """ - Convert a serialized tuple of data to an object. - """ - (self.handle, self.gramps_id, self.text, self.format, + (self.handle, self.gramps_id, the_text, self.format, the_type, self.change, the_marker, self.private) = data + self.text = StyledText() + self.text.unserialize(the_text) self.marker = MarkerType() self.marker.unserialize(the_marker) self.type = NoteType() self.type.unserialize(the_type) def get_text_data_list(self): - """ - Return the list of all textual attributes of the object. + """Return the list of all textual attributes of the object. - @return: Returns the list of all textual attributes of the object. + @returns: The list of all textual attributes of the object. @rtype: list + """ - return [self.text] + return [str(self.text)] def set(self, text): - """ - Set the text associated with the note to the passed string. + """Set the text associated with the note to the passed string. - @param text: Text string defining the note contents. + @param text: The I{clear} text defining the note contents. @type text: str + """ - self.text = text + self.text = StyledText(text) def get(self): - """ - Return the text string associated with the note. + """Return the text string associated with the note. - @returns: Returns the text string defining the note contents. + @returns: The I{clear} text of the note contents. @rtype: str + """ - text = self.text - return text + return str(self.text) - def append(self, text): + def set_styledtext(self, text): + """Set the text associated with the note to the passed string. + + @param text: The I{formatted} text defining the note contents. + @type text: L{StyledText} + """ - Append the specified text to the text associated with the note. + self.text = text + + def get_styledtext(self): + """Return the text string associated with the note. + + @returns: The I{formatted} text of the note contents. + @rtype: L{StyledText} + + """ + return self.text + + def append(self, text): + """Append the specified text to the text associated with the note. @param text: Text string to be appended to the note. - @type text: str + @type text: str or L{StyledText} + """ self.text = self.text + text def set_format(self, format): - """ - Set the format of the note to the passed value. + """Set the format of the note to the passed value. - The value can either indicate Flowed or Preformatted. - + @param: format: The value can either indicate Flowed or Preformatted. @type format: int + """ self.format = format def get_format(self): - """ - Return the format of the note. + """Return the format of the note. The value can either indicate Flowed or Preformatted. @returns: 0 indicates Flowed, 1 indicates Preformated @rtype: int + """ return self.format def set_type(self, the_type): - """ - Set descriptive type of the Note. + """Set descriptive type of the Note. @param the_type: descriptive type of the Note @type the_type: str + """ self.type.set(the_type) def get_type(self): - """ - Get descriptive type of the Note. + """Get descriptive type of the Note. @returns: the descriptive type of the Note @rtype: str + """ return self.type diff --git a/src/gen/lib/styledtext.py b/src/gen/lib/styledtext.py new file mode 100644 index 000000000..ebf52cc6e --- /dev/null +++ b/src/gen/lib/styledtext.py @@ -0,0 +1,162 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2008 Zsolt Foldvari +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +"Handling formatted ('rich text') strings" + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.lib.styledtexttag import StyledTextTag + +#------------------------------------------------------------------------- +# +# StyledText class +# +#------------------------------------------------------------------------- +class StyledText(object): + """Helper class to enable character based text formatting. + + + @ivar string: The clear text part. + @type string: str + @ivar tags: Text tags holding formatting information for the string. + @type tags: list of L{StyledTextTag} + + @cvar POS_TEXT: Position of I{string} attribute in the serialized format of + an instance. + @type POS_TEXT: int + @cvar POS_TAGS: Position of I{tags} attribute in the serialized format of + an instance. + @type POS_TAGS: int + + @attention: The POS_ class variables reflect the serialized object, they + have to be updated in case the data structure or the L{serialize} method + changes! + + """ + ##StyledText provides interface + ##Provide interface for: + ##- tag manipulation for editor access: + ##. get_tags + ##. set_tags + ##- explicit formatting for reports; at the moment: + ##. start_bold() - end_bold() + ##. start_superscript() - end_superscript() + + (POS_TEXT, POS_TAGS) = range(2) + + def __init__(self, text="", tags=None): + """Setup initial instance variable values.""" + self._string = text + # TODO we might want to make simple sanity check first + if tags: + self._tags = tags + else: + self._tags = [] + + # special methods + + def __str__(self): return self._string.__str__() + def __repr__(self): return self._string.__repr__() + + def __add__(self, other): + if isinstance(other, StyledText): + # FIXME merging tags missing + return self.__class__("".join([self._string, other.string])) + elif isinstance(other, basestring): + # in this case tags remain the same, only text becomes longer + return self.__class__("".join([self._string, other])) + else: + return self.__class__("".join([self._string, str(other)])) + + # string methods in alphabetical order: + + def join(self, seq): + # FIXME handling tags missing + return self.__class__(self._string.join(seq)) + + def replace(self, old, new, maxsplit=-1): + # FIXME handling tags missing + return self.__class__(self._string.replace(old, new, maxsplit)) + + def split(self, sep=None, maxsplit=-1): + # FIXME handling tags missing + string_list = self._string.split(sep, maxsplit) + return [self.__class__(string) for string in string_list] + + # other public methods + + def serialize(self): + """Convert the object to a serialized tuple of data. + + @returns: Serialized format of the instance. + @returntype: tuple + + """ + if self._tags: + the_tags = [tag.serialize() for tag in self._tags] + else: + the_tags = [] + + return (self._string, the_tags) + + def unserialize(self, data): + """Convert a serialized tuple of data to an object. + + @param data: Serialized format of instance variables. + @type data: tuple + + """ + (self._string, the_tags) = data + + # I really wonder why this doesn't work... it does for all other types + #self._tags = [StyledTextTag().unserialize(tag) for tag in the_tags] + for tag in the_tags: + gtt = StyledTextTag() + gtt.unserialize(tag) + self._tags.append(gtt) + + def get_tags(self): + """Return the list of formatting tags. + + @returns: The formatting tags applied on the text. + @returntype: list of 0 or more L{StyledTextTag} instances. + + """ + return self._tags + + ##def set_tags(self, tags): + ##"""Set all the formatting tags at once. + + ##@param tags: The formatting tags to be applied on the text. + ##@type tags: list of 0 or more StyledTextTag instances. + + ##""" + ### TODO we might want to make simple sanity check first + ##self._tags = tags + + +if __name__ == '__main__': + GT = StyledText("asbcde") + print GT \ No newline at end of file diff --git a/src/gen/lib/styledtexttag.py b/src/gen/lib/styledtexttag.py new file mode 100644 index 000000000..6c8a95136 --- /dev/null +++ b/src/gen/lib/styledtexttag.py @@ -0,0 +1,73 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2008 Zsolt Foldvari +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +"Provide formatting tag definition for StyledText." + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.lib.styledtexttagtype import StyledTextTagType + +#------------------------------------------------------------------------- +# +# StyledTextTag class +# +#------------------------------------------------------------------------- +class StyledTextTag(): + """Hold formatting information for StyledText. + + @ivar name: Type or name of the tag instance. E.g. bold, etc. + @type name: L{gen.lib.StyledTextTagType} instace + @ivar value: Value of the tag. E.g. color hex string for font color, etc. + @type value: str or None + @ivar ranges: Pointer pairs into the string, where the tag applies. + @type ranges: list of (int(start), int(end)) tuples. + + """ + def __init__(self, name=None, value=None, ranges=None): + """Setup initial instance variable values.""" + self.name = StyledTextTagType(name) + self.value = value + self.ranges = ranges + + def serialize(self): + """Convert the object to a serialized tuple of data. + + @returns: Serialized format of the instance. + @returntype: tuple + + """ + return (self.name.serialize(), self.value, self.ranges) + + def unserialize(self, data): + """Convert a serialized tuple of data to an object. + + @param data: Serialized format of instance variables. + @type data: tuple + + """ + (the_name, self.value, self.ranges) = data + + self.name = StyledTextTagType() + self.name.unserialize(the_name) diff --git a/src/gen/lib/styledtexttagtype.py b/src/gen/lib/styledtexttagtype.py new file mode 100644 index 000000000..4d29d56c2 --- /dev/null +++ b/src/gen/lib/styledtexttagtype.py @@ -0,0 +1,73 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2008 Zsolt Foldvari +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +"Define text formatting tag types." + +#------------------------------------------------------------------------ +# +# Python modules +# +#------------------------------------------------------------------------ +from gettext import gettext as _ + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.lib.grampstype import GrampsType + +#------------------------------------------------------------------------- +# +# StyledTextTagType class +# +#------------------------------------------------------------------------- +class StyledTextTagType(GrampsType): + """Text formatting tag type definition. + + Here we only define new class variables. For details see L{GrampsType}. + + """ + NONE_ = -1 + BOLD = 0 + ITALIC = 1 + UNDERLINE = 2 + FONTFACE = 3 + FONTCOLOR = 4 + HIGHLIGHT = 5 + SUPERSCRIPT = 6 + + _CUSTOM = NONE_ + _DEFAULT = NONE_ + + _DATAMAP = [ + (BOLD, _("Bold"), "bold"), + (ITALIC, _("Italic"), "italic"), + (UNDERLINE, _("Underline"), "underline"), + (FONTFACE, _("Fontface"), "fontface"), + (FONTCOLOR, _("Fontcolor"), "fontcolor"), + (HIGHLIGHT, _("Highlight"), "highlight"), + (SUPERSCRIPT, _("Superscript"), "superscript"), + ] + + def __init__(self, value=None): + GrampsType.__init__(self, value)