Introducing StyledText in Notes.

svn: r10410
This commit is contained in:
Zsolt Foldvari 2008-03-28 23:22:46 +00:00
parent 52ad89909c
commit 0542a9b78c
10 changed files with 879 additions and 804 deletions

View File

@ -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

View File

@ -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 = '''
# <ui>
# <toolbar name="ToolBar">
# <toolitem action="italic"/>
# <toolitem action="bold"/>
# <toolitem action="underline"/>
# <separator/>
# <toolitem action="font"/>
# <toolitem action="foreground"/>
# <toolitem action="background"/>
# <separator/>
# <toolitem action="clear"/>
# </toolbar>
# </ui>
# '''
#
# 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 = '''
<ui>
<toolbar name="ToolBar">
<toolitem action="italic"/>
<toolitem action="bold"/>
<toolitem action="underline"/>
<separator/>
<toolitem action="font"/>
<toolitem action="foreground"/>
<toolitem action="background"/>
<separator/>
<toolitem action="clear"/>
</toolbar>
</ui>
'''
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:

View File

@ -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 = '</' + ROOT_ELEMENT + '>'
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)

View File

@ -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):

View File

@ -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

View File

@ -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_<x>: Position of <x> attribute in the serialized format of
an instance.
@type POS_<x>: int
@attention: The POS_<x> 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()]

View File

@ -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_<x>: Position of <x> attribute in the serialized format of
an instance.
@type POS_<x>: int
@attention: The POS_<x> 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

162
src/gen/lib/styledtext.py Normal file
View File

@ -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_<x> 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

View File

@ -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)

View File

@ -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)