From 40b8c2d9a21380b9c5dcc8b4585317f538ec417b Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Thu, 6 May 2010 15:54:33 +0000 Subject: [PATCH] 3914: Add a new markup for creating links to URLs and for gramps objects svn: r15340 --- po/POTFILES.in | 2 + src/gen/lib/styledtexttagtype.py | 4 + src/gen/plug/docbackend/docbackend.py | 17 +- src/glade/Makefile.am | 1 + src/glade/editlink.glade | 212 +++++++++++++++++++++++++ src/gui/editors/Makefile.am | 1 + src/gui/editors/__init__.py | 22 ++- src/gui/editors/editlink.py | 219 ++++++++++++++++++++++++++ src/gui/selectors/selectperson.py | 6 +- src/gui/widgets/styledtextbuffer.py | 92 ++++++++--- src/gui/widgets/styledtexteditor.py | 146 ++++++++++++++++- src/plugins/lib/libhtmlbackend.py | 27 +++- src/plugins/lib/libodfbackend.py | 15 +- src/plugins/webreport/NarrativeWeb.py | 36 +++++ 14 files changed, 758 insertions(+), 42 deletions(-) create mode 100644 src/glade/editlink.glade create mode 100644 src/gui/editors/editlink.py diff --git a/po/POTFILES.in b/po/POTFILES.in index 6f88819a3..5f9501306 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -213,6 +213,7 @@ src/gui/editors/editeventref.py src/gui/editors/editfamily.py src/gui/editors/editldsord.py src/gui/editors/editlocation.py +src/gui/editors/editlink.py src/gui/editors/editmedia.py src/gui/editors/editmediaref.py src/gui/editors/editname.py @@ -839,6 +840,7 @@ src/glade/editrepository.glade src/glade/editreporef.glade src/glade/editpersonref.glade src/glade/editlocation.glade +src/glade/editlink.glade src/glade/editfamily.glade src/glade/editchildref.glade src/glade/editattribute.glade diff --git a/src/gen/lib/styledtexttagtype.py b/src/gen/lib/styledtexttagtype.py index 0c033c37b..846812c39 100644 --- a/src/gen/lib/styledtexttagtype.py +++ b/src/gen/lib/styledtexttagtype.py @@ -56,6 +56,7 @@ class StyledTextTagType(GrampsType): FONTCOLOR = 5 HIGHLIGHT = 6 SUPERSCRIPT = 7 + LINK = 8 _CUSTOM = NONE_TYPE _DEFAULT = NONE_TYPE @@ -69,6 +70,7 @@ class StyledTextTagType(GrampsType): (FONTCOLOR, _("Fontcolor"), "fontcolor"), (HIGHLIGHT, _("Highlight"), "highlight"), (SUPERSCRIPT, _("Superscript"), "superscript"), + (LINK, _("Link"), "link"), ] STYLE_TYPE = { @@ -80,6 +82,7 @@ class StyledTextTagType(GrampsType): FONTFACE: str, FONTSIZE: int, SUPERSCRIPT: bool, + LINK: str, } STYLE_DEFAULT = { @@ -91,6 +94,7 @@ class StyledTextTagType(GrampsType): FONTFACE: 'Sans', FONTSIZE: 10, SUPERSCRIPT: False, + LINK: '', } def __init__(self, value=None): diff --git a/src/gen/plug/docbackend/docbackend.py b/src/gen/plug/docbackend/docbackend.py index bc7fa5e94..af511a409 100644 --- a/src/gen/plug/docbackend/docbackend.py +++ b/src/gen/plug/docbackend/docbackend.py @@ -99,6 +99,7 @@ class DocBackend(object): FONTCOLOR = 5 HIGHLIGHT = 6 SUPERSCRIPT = 7 + LINK = 8 SUPPORTED_MARKUP = [] @@ -115,6 +116,7 @@ class DocBackend(object): ITALIC : ("", ""), UNDERLINE : ("", ""), SUPERSCRIPT : ("", ""), + LINK : ("", ""), } def __init__(self, filename=None): @@ -216,7 +218,7 @@ class DocBackend(object): if not self.STYLETYPE_MAP or \ self.CLASSMAP != tagtype.__class__.__name__ : self.CLASSMAP == tagtype.__class__.__name__ - self.STYLETYPE_MAP[tagtype.__class__.BOLD] = self.BOLD + self.STYLETYPE_MAP[tagtype.BOLD] = self.BOLD self.STYLETYPE_MAP[tagtype.ITALIC] = self.ITALIC self.STYLETYPE_MAP[tagtype.UNDERLINE] = self.UNDERLINE self.STYLETYPE_MAP[tagtype.FONTFACE] = self.FONTFACE @@ -224,7 +226,10 @@ class DocBackend(object): self.STYLETYPE_MAP[tagtype.FONTCOLOR] = self.FONTCOLOR self.STYLETYPE_MAP[tagtype.HIGHLIGHT] = self.HIGHLIGHT self.STYLETYPE_MAP[tagtype.SUPERSCRIPT] = self.SUPERSCRIPT + self.STYLETYPE_MAP[tagtype.LINK] = self.LINK + if s_tag.name == 'link': + return self.format_link(s_tag.value) typeval = int(s_tag.name) s_tagvalue = s_tag.value tag_name = None @@ -351,3 +356,13 @@ class DocBackend(object): otext += opentag[1] return otext + + def format_link(self, value): + """ + Default format for links. Override for better support. + + value is: "TYPE DATA" where TYPE is 'url' or 'ref'. + + """ + return self.STYLETAG_MARKUP[DocBackend.UNDERLINE] + diff --git a/src/glade/Makefile.am b/src/glade/Makefile.am index 00afce26c..7943b977c 100644 --- a/src/glade/Makefile.am +++ b/src/glade/Makefile.am @@ -28,6 +28,7 @@ dist_pkgdata_DATA = \ editreporef.glade \ editpersonref.glade \ editlocation.glade \ + editlink.glade \ editfamily.glade \ editchildref.glade \ editattribute.glade \ diff --git a/src/glade/editlink.glade b/src/glade/editlink.glade new file mode 100644 index 000000000..2d6148cc0 --- /dev/null +++ b/src/glade/editlink.glade @@ -0,0 +1,212 @@ + + + + + + True + 600 + True + dialog + False + + + True + + + True + vertical + + + True + 12 + 3 + 3 + 12 + 6 + + + True + 0 + Gramps item: + True + center + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + Web Address: + True + center + entry1 + + + 2 + 3 + GTK_FILL + + + + + + True + 0 + Type: + True + + + GTK_FILL + + + + + + True + True + + + + 1 + 3 + 2 + 3 + + + + + + True + True + True + + + True + gtk-index + 2 + + + + + 2 + 3 + + + + + + + True + 0 + in + + + True + + + True + + + + + + + + + + 1 + 3 + 1 + 2 + + + + + + + + False + 0 + + + + + False + 1 + + + + + True + end + + + gtk-cancel + True + True + True + True + True + + + False + False + 0 + + + + + gtk-ok + True + True + True + True + True + True + Accept changes and close window + Accept changes and close window + True + + + False + False + 1 + + + + + gtk-help + True + True + True + True + True + + + False + False + 2 + + + + + False + end + 0 + + + + + + button125 + button124 + button130 + + + diff --git a/src/gui/editors/Makefile.am b/src/gui/editors/Makefile.am index 0849cd24e..9dfb1d2dc 100644 --- a/src/gui/editors/Makefile.am +++ b/src/gui/editors/Makefile.am @@ -19,6 +19,7 @@ pkgdata_PYTHON = \ editfamily.py \ editldsord.py \ editlocation.py \ + editlink.py \ editmedia.py \ editmediaref.py \ editname.py \ diff --git a/src/gui/editors/__init__.py b/src/gui/editors/__init__.py index e3e5aa90a..9904dea7a 100644 --- a/src/gui/editors/__init__.py +++ b/src/gui/editors/__init__.py @@ -38,8 +38,9 @@ from editreporef import EditRepoRef from editsource import EditSource, DeleteSrcQuery from editsourceref import EditSourceRef from editurl import EditUrl +from editlink import EditLink -# Map from gen.obj name to Editor: +# Map from gen.lib name to Editor: EDITORS = { 'Person': EditPerson, 'Event': EditEvent, @@ -58,18 +59,23 @@ def EditObject(dbstate, uistate, track, obj_class, prop, value): prop is 'handle' or 'gramps_id' value is string handle or string gramps_id """ + import logging + LOG = logging.getLogger(".Edit") if obj_class in dbstate.db.get_table_names(): if prop in ("gramps_id", "handle"): obj = dbstate.db.get_table_metadata(obj_class)[prop + "_func"](value) if obj: - EDITORS[obj_class](dbstate, uistate, track, obj) + try: + EDITORS[obj_class](dbstate, uistate, track, obj) + except Exception as msg: + LOG.warn(str(msg)) else: - raise AttributeError("gramps://%s/%s/%s not found" % - (obj_class, prop, value)) + LOG.warn("gramps://%s/%s/%s not found" % + (obj_class, prop, value)) else: - raise AttributeError("unknown property to edit '%s'; " - "should be 'gramps_id' or 'handle'" % prop) + LOG.warn("unknown property to edit '%s'; " + "should be 'gramps_id' or 'handle'" % prop) else: - raise AttributeError("unknown object to edit '%s'; " - "should be one of %s" % (obj_class, EDITORS.keys())) + LOG.warn("unknown object to edit '%s'; " + "should be one of %s" % (obj_class, EDITORS.keys())) diff --git a/src/gui/editors/editlink.py b/src/gui/editors/editlink.py new file mode 100644 index 000000000..7e707c7b0 --- /dev/null +++ b/src/gui/editors/editlink.py @@ -0,0 +1,219 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2010 Doug Blank +# +# 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: $ + +#------------------------------------------------------------------------- +# +# python modules +# +#------------------------------------------------------------------------- +import gtk +import re + +#------------------------------------------------------------------------- +# +# gramps modules +# +#------------------------------------------------------------------------- +from gen.ggettext import gettext as _ +import ManagedWindow +import GrampsDisplay +from glade import Glade +from Simple import SimpleAccess + +WEB, EVENT, FAMILY, MEDIA, NOTE, PERSON, PLACE, REPOSITORY, SOURCE = range(9) +OBJECT_MAP = { + EVENT: "Event", + FAMILY: "Family", + MEDIA: "Media", + NOTE: "Note", + PERSON: "Person", + PLACE: "Place", + REPOSITORY: "Repository", + SOURCE: "Source", + } + +#------------------------------------------------------------------------- +# +# EditUrl class +# +#------------------------------------------------------------------------- +class EditLink(ManagedWindow.ManagedWindow): + + def __init__(self, dbstate, uistate, track, url, callback): + self.url = url + self.dbstate = dbstate + self.simple_access = SimpleAccess(self.dbstate.db) + self.callback = callback + + ManagedWindow.ManagedWindow.__init__(self, uistate, track, url) + + self._local_init() + self._connect_signals() + self.show() + + def _local_init(self): + self.top = Glade() + self.set_window(self.top.toplevel, + self.top.get_object("title"), + _('Link Editor')) + self.table = self.top.get_object('table27') + self.uri_list = gtk.combo_box_new_text() + for text in [_("Web Address"), # 0 this order range above + _("Gramps Event"), # 1 + _("Gramps Family"), # 2 + _("Gramps Media"), # 3 + _("Gramps Note"), # 4 + _("Gramps Person"), # 5 + _("Gramps Place"), # 6 + _("Gramps Repository"), # 7 + _("Gramps Source"), # 8 + ]: + self.uri_list.append_text(text) + self.table.attach(self.uri_list, 1, 2, 0, 1) + self.pick_item = self.top.get_object('button1') + #self.edit_item = self.top.get_object('button2') + self.selected = self.top.get_object('label1') + self.url_link = self.top.get_object('entry1') + self.uri_list.connect("changed", self._on_type_changed) + self.pick_item.connect("clicked", self._on_pick_one) + #self.edit_item.connect("clicked", self._on_edit_one) + if self.url.startswith("gramps://"): + object_class, prop, value = self.url[9:].split("/", 2) + if object_class == "Event": + self.uri_list.set_active(EVENT) + elif object_class == "Family": + self.uri_list.set_active(FAMILY) + elif object_class == "Media": + self.uri_list.set_active(MEDIA) + elif object_class == "Note": + self.uri_list.set_active(NOTE) + elif object_class == "Person": + self.uri_list.set_active(PERSON) + elif object_class == "Place": + self.uri_list.set_active(PLACE) + elif object_class == "Repository": + self.uri_list.set_active(REPOSITORY) + elif object_class == "Source": + self.uri_list.set_active(SOURCE) + # set texts: + self.selected.set_text(self.display_link( + object_class, prop, value)) + self.url_link.set_text("gramps://%s/%s/%s" % + (object_class, prop, value)) + else: + self.uri_list.set_active(WEB) + self.url_link.set_text(self.url) + self.url_link.connect("changed", self.update_ui) + + def update_ui(self, widget): + url = self.url_link.get_text() + # text needs to have 3 or more chars://and at least one char + match = re.match("\w{3,}://\w+", url) + if match: + self.ok_button.set_sensitive(True) + else: + self.ok_button.set_sensitive(False) + + def display_link(self, obj_class, prop, value): + return self.simple_access.display(obj_class, prop, value) + + def _on_edit_one(self, widget): + from gui.editors import EditObject + uri = self.url_link.get_text() + if uri.startswith("gramps://"): + obj_class, prop, value = uri[9:].split("/", 2) + EditObject(self.dbstate, + self.uistate, + self.track, + obj_class, prop, value) + + def _on_pick_one(self, widget): + from gui.selectors import SelectorFactory + object_class = OBJECT_MAP[self.uri_list.get_active()] + Select = SelectorFactory(object_class) + uri = self.url_link.get_text() + default = None + if uri.startswith("gramps://"): + obj_class, prop, value = uri[9:].split("/", 2) + if object_class == obj_class: + if prop == "handle": + default = value + elif (prop == "gramps_id" and + object_class in self.dbstate.db.get_table_names()): + person = self.dbstate.db.get_table_metadata(object_class)["gramps_id_func"](value) + if person: + default = person.handle + d = Select(self.dbstate, self.uistate, self.track, + default=default) + + result = d.run() + if result: + prop = "handle" + value = result.handle + self.selected.set_text(self.display_link( + object_class, prop, value)) + self.url_link.set_text("gramps://%s/%s/%s" % + (object_class, prop, value)) + + def _on_type_changed(self, widget): + self.selected.set_text("") + if self.uri_list.get_active() == WEB: + self.url_link.set_sensitive(True) + self.pick_item.set_sensitive(False) + else: + self.url_link.set_sensitive(False) + self.pick_item.set_sensitive(True) + + def get_uri(self): + if self.uri_list.get_active() == WEB: + return self.url_link.get_text() + else: + #object_class = OBJECT_MAP[self.uri_list.get_active()] + #prop = "handle" + #value = "" + #return "gramps://%s/%s/%s" % (object_class, prop, value) + return self.url_link.get_text() + + def _connect_signals(self): + self.define_cancel_button(self.top.get_object('button125')) + self.ok_button = self.top.get_object('button124') + self.define_ok_button(self.ok_button, self.save) + self.define_help_button(self.top.get_object('button130')) + self.update_ui(self.url_link) + + def build_menu_names(self, obj): + etitle =_('Link Editor') + return (etitle, etitle) + + def define_ok_button(self,button,function): + button.connect('clicked',function) + + def save(self, widget): + self.callback(self.get_uri()) + self.close() + + def define_cancel_button(self,button): + button.connect('clicked',self.close) + + def define_help_button(self, button, webpage='', section=''): + button.connect('clicked', lambda x: GrampsDisplay.help(webpage, + section)) diff --git a/src/gui/selectors/selectperson.py b/src/gui/selectors/selectperson.py index 29c6164d5..cd210d3a0 100644 --- a/src/gui/selectors/selectperson.py +++ b/src/gui/selectors/selectperson.py @@ -44,8 +44,8 @@ from baseselector import BaseSelector #------------------------------------------------------------------------- class SelectPerson(BaseSelector): - def __init__(self, dbstate, uistate, track=[], title = None, filter = None, - skip=set(), show_search_bar = False): + def __init__(self, dbstate, uistate, track=[], title=None, filter=None, + skip=set(), show_search_bar=False, default=None): # SelectPerson may have a title passed to it which should be used # instead of the default defined for get_window_title() @@ -53,7 +53,7 @@ class SelectPerson(BaseSelector): self.title = title BaseSelector.__init__(self, dbstate, uistate, track, filter, - skip, show_search_bar) + skip, show_search_bar, default) def _local_init(self): """ diff --git a/src/gui/widgets/styledtextbuffer.py b/src/gui/widgets/styledtextbuffer.py index 860d82862..43704c257 100644 --- a/src/gui/widgets/styledtextbuffer.py +++ b/src/gui/widgets/styledtextbuffer.py @@ -82,6 +82,25 @@ STYLE_TO_PROPERTY = { MATCH_FLAVOR, MATCH_STRING,) = range(4) +#------------------------------------------------------------------------- +# +# LinkTag class +# +#------------------------------------------------------------------------- +class LinkTag(gtk.TextTag): + """ + Class for keeping track of link data. + """ + lid = 0 + def __init__(self, buffer, data, **properties): + LinkTag.lid += 1 + self.data = data + gtk.TextTag.__init__(self, "link-%d" % LinkTag.lid) + tag_table = buffer.get_tag_table() + for property in properties: + self.set_property(property, properties[property]) + tag_table.add(self) + #------------------------------------------------------------------------- # # GtkSpellState class @@ -432,6 +451,29 @@ class StyledTextBuffer(gtk.TextBuffer): self.remove_tag_by_name(tag_name, self.get_iter_at_offset(start), self.get_iter_at_offset(end+1)) + + def clear_selection(self): + """ + Clear tags from selection. + """ + start, end = self._get_selection() + tags = self._get_tag_from_range(start.get_offset(), end.get_offset()) + removed_something = False + for tag_name, tag_data in tags.iteritems(): + if tag_name.startswith("link"): + for start_pos, end_pos in tag_data: + self.remove_tag_by_name(tag_name, + self.get_iter_at_offset(start_pos), + self.get_iter_at_offset(end_pos+1)) + removed_something = True + + for style in ALLOWED_STYLES: + value = self.style_state[style] + if value and (value != StyledTextTagType.STYLE_DEFAULT[style]): + self.remove_tag(self._find_tag_by_name(style, value), + start, end) + removed_something = True + return removed_something def _get_tag_from_range(self, start=None, end=None): """Extract gtk.TextTags from buffer. @@ -474,7 +516,9 @@ class StyledTextBuffer(gtk.TextBuffer): If TextTag does not exist yet, it is created. """ - if StyledTextTagType.STYLE_TYPE[style] == bool: + if style not in StyledTextTagType.STYLE_TYPE: + return None + elif StyledTextTagType.STYLE_TYPE[style] == bool: tag_name = str(style) elif StyledTextTagType.STYLE_TYPE[style] == str: tag_name = "%d %s" % (style, value) @@ -507,7 +551,12 @@ class StyledTextBuffer(gtk.TextBuffer): s_tags = s_text.get_tags() for s_tag in s_tags: - g_tag = self._find_tag_by_name(int(s_tag.name), s_tag.value) + if s_tag.name == 'Link': + g_tag = LinkTag(self, s_tag.value, + foreground="blue", + underline=UNDERLINE_SINGLE) + else: + g_tag = self._find_tag_by_name(int(s_tag.name), s_tag.value) if g_tag is not None: for (start, end) in s_tag.ranges: start_iter = self.get_iter_at_offset(start) @@ -533,23 +582,30 @@ class StyledTextBuffer(gtk.TextBuffer): s_tags = [] for g_tagname, g_ranges in g_tags.items(): - style_and_value = g_tagname.split(' ', 1) + if g_tagname.startswith('link'): + tag = self.get_tag_table().lookup(g_tagname) + s_ranges = [(start, end+1) for (start, end) in g_ranges] + s_value = tag.data + s_tag = StyledTextTag('Link', s_value, s_ranges) + s_tags.append(s_tag) + else: + style_and_value = g_tagname.split(' ', 1) - try: - style = int(style_and_value[0]) - if len(style_and_value) == 1: - s_value = None - else: - s_value = StyledTextTagType.STYLE_TYPE[style]\ - (style_and_value[1]) - - if style in ALLOWED_STYLES: - s_ranges = [(start, end+1) for (start, end) in g_ranges] - s_tag = StyledTextTag(style, s_value, s_ranges) - - s_tags.append(s_tag) - except ValueError: - _LOG.debug("silently skipping gtk.TextTag '%s'" % g_tagname) + try: + style = int(style_and_value[0]) + if len(style_and_value) == 1: + s_value = None + else: + s_value = StyledTextTagType.STYLE_TYPE[style]\ + (style_and_value[1]) + + if style in ALLOWED_STYLES: + s_ranges = [(start, end+1) for (start, end) in g_ranges] + s_tag = StyledTextTag(style, s_value, s_ranges) + + s_tags.append(s_tag) + except ValueError: + _LOG.debug("silently skipping gtk.TextTag '%s'" % g_tagname) return StyledText(txt, s_tags) diff --git a/src/gui/widgets/styledtexteditor.py b/src/gui/widgets/styledtexteditor.py index 48d5d8db1..ddc7f87e3 100644 --- a/src/gui/widgets/styledtexteditor.py +++ b/src/gui/widgets/styledtexteditor.py @@ -50,8 +50,9 @@ from pango import UNDERLINE_SINGLE #------------------------------------------------------------------------- from gen.lib import StyledTextTagType from gui.widgets.styledtextbuffer import (StyledTextBuffer, ALLOWED_STYLES, - MATCH_START, MATCH_END, - MATCH_FLAVOR, MATCH_STRING) + MATCH_START, MATCH_END, + MATCH_FLAVOR, MATCH_STRING, + LinkTag) from gui.widgets.valueaction import ValueAction from gui.widgets.toolcomboentry import ToolComboEntry from gui.widgets.springseparator import SpringSeparatorAction @@ -76,6 +77,7 @@ FORMAT_TOOLBAR = ''' + @@ -87,6 +89,7 @@ FORMAT_TOOLBAR = ''' StyledTextTagType.FONTSIZE, StyledTextTagType.FONTCOLOR, StyledTextTagType.HIGHLIGHT, + StyledTextTagType.LINK, ) FONT_SIZES = [8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, @@ -101,7 +104,18 @@ SCHEME = "(file:/|https?:|ftps?:|webcal:)" USER = "[" + USERCHARS + "]+(:[" + PASSCHARS + "]+)?" URLPATH = "/[" + PATHCHARS + "]*[^]'.}>) \t\r\n,\\\"]" -(GENURL, HTTP, MAIL) = range(3) +(GENURL, HTTP, MAIL, LINK) = range(4) + +def find_parent_with_attr(self, attr="dbstate"): + """ + """ + # Find a parent with attr: + obj = self + while obj: + if hasattr(obj, attr): + break + obj = obj.get_parent() + return obj #------------------------------------------------------------------------- # @@ -204,6 +218,9 @@ class StyledTextEditor(gtk.TextView): self.textbuffer.apply_tag_by_name('hyperlink', start, end) window.set_cursor(HAND_CURSOR) self.url_match = match + elif match and (match[MATCH_FLAVOR] in (LINK,)): + window.set_cursor(HAND_CURSOR) + self.url_match = match else: window.set_cursor(REGULAR_CURSOR) self.url_match = None @@ -256,15 +273,37 @@ class StyledTextEditor(gtk.TextView): int(event.x), int(event.y)) iter_at_location = self.get_iter_at_location(x, y) self.match = self.textbuffer.match_check(iter_at_location.get_offset()) + tooltip = None + if not self.match: + for tag in (tag for tag in iter_at_location.get_tags() + if tag.get_property('name').startswith("link")): + self.match = (x, y, LINK, tag.data, tag) + tooltip = self.make_tooltip_from_link(tag) + break if self.match != self.last_match: self.emit('match-changed', self.match) self.last_match = self.match - self.window.get_pointer() + self.set_tooltip_text(tooltip) return False + def make_tooltip_from_link(self, link_tag): + """ + Return a string useful for a tooltip given a LinkTag object. + """ + from Simple import SimpleAccess + win_obj = find_parent_with_attr(self, attr="dbstate") + display = link_tag.data + if win_obj: + simple_access = SimpleAccess(win_obj.dbstate.db) + url = link_tag.data + if url.startswith("gramps://"): + obj_class, prop, value = url[9:].split("/") + display = simple_access.display(obj_class, prop, value) or url + return display + def on_button_release_event(self, widget, event): """ Copy selection to clipboard for left click if selection given @@ -325,6 +364,14 @@ class StyledTextEditor(gtk.TextView): open_menu = gtk.MenuItem(_('_Open Link')) copy_menu = gtk.MenuItem(_('Copy _Link Address')) + if flavor == LINK: + edit_menu = gtk.MenuItem(_('_Edit Link')) + edit_menu.connect('activate', self._edit_url_cb, + self.url_match[-1], # tag + ) + edit_menu.show() + menu.prepend(edit_menu) + copy_menu.connect('activate', self._copy_url_cb, url, flavor) copy_menu.show() menu.prepend(copy_menu) @@ -373,6 +420,8 @@ class StyledTextEditor(gtk.TextView): _('Font Color'), self._on_action_activate), (str(StyledTextTagType.HIGHLIGHT), 'gramps-font-bgcolor', None, None, _('Background Color'), self._on_action_activate), + (str(StyledTextTagType.LINK), gtk.STOCK_JUMP_TO, None, None, + _('Link'), self._on_link_activate), ('clear', gtk.STOCK_CLEAR, None, None, _('Clear Markup'), self._format_clear_cb), ] @@ -487,6 +536,34 @@ class StyledTextEditor(gtk.TextView): _LOG.debug("applying style '%d' with value '%s'" % (style, str(value))) self.textbuffer.apply_style(style, value) + def _on_link_activate(self, action): + """ + Create a link of a selected region of text. + """ + # Send in a default link. Could be based on active person. + selection_bounds = self.textbuffer.get_selection_bounds() + if selection_bounds: + uri_dialog(self, None, self.setlink_callback) + + def setlink_callback(self, uri, tag=None): + """ + Callback for setting or editing a link's object. + """ + if uri: + _LOG.debug("applying style 'link' with value '%s'" % uri) + if not tag: + tag = LinkTag(self.textbuffer, + data=uri, + underline=UNDERLINE_SINGLE, + foreground="blue") + selection_bounds = self.textbuffer.get_selection_bounds() + self.textbuffer.apply_tag(tag, + selection_bounds[0], + selection_bounds[1]) + else: + tag.data = uri + + def _on_action_activate(self, action): """Apply a format set from a gtk.Action type of action.""" style = int(action.get_name()) @@ -533,14 +610,27 @@ class StyledTextEditor(gtk.TextView): (text, StyledTextTagType.STYLE_TYPE[style])) def _format_clear_cb(self, action): - """Remove all formats from the selection. + """ + Remove all formats from the selection or from all. Remove only our own tags without touching other ones (e.g. gtk.Spell), thus remove_all_tags() can not be used. """ - for style in ALLOWED_STYLES: - self.textbuffer.remove_style(style) + clear_anything = self.textbuffer.clear_selection() + if not clear_anything: + for style in ALLOWED_STYLES: + self.textbuffer.remove_style(style) + + start, end = self.textbuffer.get_bounds() + tags = self.textbuffer._get_tag_from_range(start.get_offset(), + end.get_offset()) + for tag_name, tag_data in tags.iteritems(): + if tag_name.startswith("link"): + for start, end in tag_data: + self.textbuffer.remove_tag_by_name(tag_name, + self.textbuffer.get_iter_at_offset(start), + self.textbuffer.get_iter_at_offset(end+1)) def _on_buffer_style_changed(self, buffer, changed_styles): """Synchronize actions as the format changes at the buffer's cursor.""" @@ -574,9 +664,23 @@ class StyledTextEditor(gtk.TextView): url = 'mailto:' + url elif flavor == GENURL: pass + elif flavor == LINK: + # gramps://person/id/VALUE + # gramps://person/handle/VALUE + if url.startswith("gramps://"): + # if in a window: + win_obj = find_parent_with_attr(self, attr="dbstate") + if win_obj: + obj_class, prop, value = url[9:].split("/") + from gui.editors import EditObject + EditObject(win_obj.dbstate, + win_obj.uistate, + win_obj.track, + obj_class, prop, value) + return else: return - + # If ok, then let's open display_url(url) def _copy_url_cb(self, menuitem, url, flavor): @@ -586,6 +690,14 @@ class StyledTextEditor(gtk.TextView): clipboard = gtk.Clipboard(selection="PRIMARY") clipboard.set_text(url) + + + def _edit_url_cb(self, menuitem, link_tag): + """ + Edit the URI of the link. + """ + uri_dialog(self, link_tag.data, + lambda uri: self.setlink_callback(uri, link_tag)) # public methods @@ -617,6 +729,24 @@ class StyledTextEditor(gtk.TextView): """ return self.toolbar +def uri_dialog(self, uri, callback): + """ + Function to spawn the link editor. + """ + from gui.editors.editlink import EditLink + obj = find_parent_with_attr(self, attr="dbstate") + if obj: + if uri is None: + # make a default link + uri = "http://" + # Check in order for an open page: + for object_class in ["Person", "Place", "Event", "Family", + "Repository", "Source", "Media"]: + handle = obj.uistate.get_active(object_class) + if handle: + uri = "gramps://%s/handle/%s" % (object_class, handle) + EditLink(obj.dbstate, obj.uistate, obj.track, uri, callback) + #------------------------------------------------------------------------- # # Module functions diff --git a/src/plugins/lib/libhtmlbackend.py b/src/plugins/lib/libhtmlbackend.py index 44e122735..f3d926027 100644 --- a/src/plugins/lib/libhtmlbackend.py +++ b/src/plugins/lib/libhtmlbackend.py @@ -75,7 +75,9 @@ class HtmlBackend(DocBackend): DocBackend.FONTSIZE, DocBackend.FONTCOLOR, DocBackend.HIGHLIGHT, - DocBackend.SUPERSCRIPT ] + DocBackend.SUPERSCRIPT, + DocBackend.LINK, + ] STYLETAG_MARKUP = { DocBackend.BOLD : ("", ""), @@ -85,7 +87,7 @@ class HtmlBackend(DocBackend): DocBackend.SUPERSCRIPT : ("", ""), } - ESCAPE_FUNC = lambda x: escape + ESCAPE_FUNC = lambda self: escape def __init__(self, filename=None): """ @@ -97,6 +99,7 @@ class HtmlBackend(DocBackend): self.html_body = None self._subdir = None self.title = None + self.build_link = None def _create_xmltag(self, tagtype, value): """ @@ -111,7 +114,6 @@ class HtmlBackend(DocBackend): elif tagtype == DocBackend.FONTFACE: #fonts can have strange symbols in them, ' needs to be escaped value = value.replace("'", "\\'") - return ('' % (self.STYLETAG_TO_PROPERTY[tagtype] % (value)), '') @@ -172,3 +174,22 @@ class HtmlBackend(DocBackend): full path of the datadir directory """ return os.path.join(os.path.dirname(self.getf()), self.datadir()) + + def format_link(self, value): + """ + Override of base method. + """ + if value.startswith("gramps://"): + if self.build_link: + obj_class, prop, handle = value[9:].split("/", 3) + if prop in ["handle", "gramps_id"]: + value = self.build_link(prop, handle, obj_class, up=True) + if not value: + return self.STYLETAG_MARKUP[DocBackend.UNDERLINE] + else: + return self.STYLETAG_MARKUP[DocBackend.UNDERLINE] + else: + return self.STYLETAG_MARKUP[DocBackend.UNDERLINE] + return ('' % self.ESCAPE_FUNC()(value), + '') + diff --git a/src/plugins/lib/libodfbackend.py b/src/plugins/lib/libodfbackend.py index 8470e4958..6b98911c5 100644 --- a/src/plugins/lib/libodfbackend.py +++ b/src/plugins/lib/libodfbackend.py @@ -81,7 +81,9 @@ class OdfBackend(DocBackend): DocBackend.FONTSIZE, DocBackend.FONTCOLOR, DocBackend.HIGHLIGHT, - DocBackend.SUPERSCRIPT ] + DocBackend.SUPERSCRIPT, + DocBackend.LINK, + ] STYLETAG_MARKUP = { DocBackend.BOLD : @@ -129,3 +131,14 @@ class OdfBackend(DocBackend): return ('' % self.ESCAPE_FUNC()(value), '') + + def format_link(self, value): + """ + Override of base method. + """ + if value.startswith("gramps://"): + return self.STYLETAG_MARKUP[DocBackend.UNDERLINE] + else: + return ('' % self.ESCAPE_FUNC()(value), + '') + diff --git a/src/plugins/webreport/NarrativeWeb.py b/src/plugins/webreport/NarrativeWeb.py index 1e3c4eafa..90d6e16bc 100644 --- a/src/plugins/webreport/NarrativeWeb.py +++ b/src/plugins/webreport/NarrativeWeb.py @@ -304,6 +304,7 @@ class BasePage(object): self.up = False # class to do conversion of styled notes to html markup self._backend = HtmlBackend() + self._backend.build_link = report.build_link self.report = report self.title_str = title @@ -5557,6 +5558,41 @@ class NavWebReport(Report): def build_url_fname_html(self, fname, subdir = None, up = False): return self.build_url_fname(fname, subdir, up) + self.ext + def build_link(self, prop, handle, obj_class, up = False): + """ + Build a link to an item. + """ + if prop == "gramps_id": + if obj_class in self.database.get_table_names(): + obj = self.database.get_table_metadata(obj_class)["gramps_id_func"](handle) + if obj: + handle = obj.handle + else: + raise AttributeError("gramps_id '%s' not found in '%s'" % + handle, obj_class) + else: + raise AttributeError("invalid gramps_id lookup " + "in table name '%s'" % obj_class) + # handle, ppl + if obj_class == "Person": + if handle in self.person_handles: + return self.build_url_fname(handle, "ppl", up) + self.ext + else: + return None + elif obj_class == "Source": + subdir = "src" + elif obj_class == "Place": + subdir = "plc" + elif obj_class == "Event": + subdir = "evt" + elif obj_class == "Media": + subdir = "img" + elif obj_class == "Repository": + subdir = "repo" + else: + raise AttributeError("unknown object type '%s'" % obj_class) + return self.build_url_fname(handle, subdir, up) + self.ext + def build_url_fname(self, fname, subdir = None, up = False): """ Create part of the URL given the filename and optionally the subdirectory.