GEPS 011: Tagging

svn: r15830
This commit is contained in:
Nick Hall 2010-08-29 18:36:42 +00:00
parent 5d3480997b
commit bf8146b9d1
37 changed files with 1785 additions and 64 deletions

View File

@ -17,6 +17,7 @@ pkgdata_PYTHON = \
_HasNoteSubstrBase.py\ _HasNoteSubstrBase.py\
_HasReferenceCountBase.py \ _HasReferenceCountBase.py \
_HasSourceBase.py \ _HasSourceBase.py \
_HasTagBase.py \
_HasTextMatchingRegexpOf.py\ _HasTextMatchingRegexpOf.py\
_HasTextMatchingSubstringOf.py\ _HasTextMatchingSubstringOf.py\
__init__.py\ __init__.py\

View File

@ -28,6 +28,7 @@ pkgdata_PYTHON = \
_HasRelationship.py \ _HasRelationship.py \
_HasSource.py \ _HasSource.py \
_HasSourceOf.py \ _HasSourceOf.py \
_HasTag.py \
_HasTextMatchingRegexpOf.py \ _HasTextMatchingRegexpOf.py \
_HasTextMatchingSubstringOf.py \ _HasTextMatchingSubstringOf.py \
_HasUnknownGender.py \ _HasUnknownGender.py \

View File

@ -0,0 +1,50 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2010 Nick Hall
#
# 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$
"""
Rule that checks for a person with a particular tag.
"""
#-------------------------------------------------------------------------
#
# Standard Python modules
#
#-------------------------------------------------------------------------
from gen.ggettext import gettext as _
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from Filters.Rules._HasTagBase import HasTagBase
#-------------------------------------------------------------------------
#
# HasTag
#
#-------------------------------------------------------------------------
class HasTag(HasTagBase):
"""
Rule that checks for a person with a particular tag.
"""
labels = [ _('Tag:') ]
name = _('People with the <tag>')
description = _("Matches people with the particular tag")

View File

@ -49,6 +49,7 @@ from _HasNoteRegexp import HasNoteRegexp
from _HasRelationship import HasRelationship from _HasRelationship import HasRelationship
from _HasSource import HasSource from _HasSource import HasSource
from _HasSourceOf import HasSourceOf from _HasSourceOf import HasSourceOf
from _HasTag import HasTag
from _HasTextMatchingRegexpOf import HasTextMatchingRegexpOf from _HasTextMatchingRegexpOf import HasTextMatchingRegexpOf
from _HasTextMatchingSubstringOf import HasTextMatchingSubstringOf from _HasTextMatchingSubstringOf import HasTextMatchingSubstringOf
from _HasUnknownGender import HasUnknownGender from _HasUnknownGender import HasUnknownGender
@ -126,6 +127,7 @@ editor_rule_list = [
HasFamilyEvent, HasFamilyEvent,
HasAttribute, HasAttribute,
HasFamilyAttribute, HasFamilyAttribute,
HasTag,
HasSource, HasSource,
HasSourceOf, HasSourceOf,
HasMarkerOf, HasMarkerOf,

View File

@ -0,0 +1,60 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2010 Nick Hall
#
# 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$
"""
Rule that checks for an object with a particular tag.
"""
#-------------------------------------------------------------------------
#
# Standard Python modules
#
#-------------------------------------------------------------------------
from gen.ggettext import gettext as _
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from Filters.Rules import Rule
#-------------------------------------------------------------------------
#
# HasTag
#
#-------------------------------------------------------------------------
class HasTagBase(Rule):
"""
Rule that checks for an object with a particular tag.
"""
labels = [ _('Tag:') ]
name = _('Objects with the <tag>')
description = _("Matches objects with the given tag")
category = _('General filters')
def apply(self, db, obj):
"""
Apply the rule. Return True for a match.
"""
if not self.list[0]:
return False
return self.list[0] in obj.get_tag_list()

View File

@ -26,6 +26,7 @@
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
from gen.ggettext import gettext as _ from gen.ggettext import gettext as _
import locale
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -45,8 +46,8 @@ import DateHandler
from Filters.SideBar import SidebarFilter from Filters.SideBar import SidebarFilter
from Filters.Rules.Person import (RegExpName, SearchName, RegExpIdOf, from Filters.Rules.Person import (RegExpName, SearchName, RegExpIdOf,
MatchIdOf, IsMale, IsFemale, MatchIdOf, IsMale, IsFemale, HasUnknownGender,
HasUnknownGender, HasMarkerOf, HasEvent, HasMarkerOf, HasEvent, HasTag,
HasBirth, HasDeath, HasNoteRegexp, HasBirth, HasDeath, HasNoteRegexp,
HasNoteMatchingSubstringOf, MatchesFilter) HasNoteMatchingSubstringOf, MatchesFilter)
from Filters import GenericFilter, build_filter_model, Rules from Filters import GenericFilter, build_filter_model, Rules
@ -91,6 +92,8 @@ class PersonSidebarFilter(SidebarFilter):
self.filter_marker.set_marker, self.filter_marker.set_marker,
self.filter_marker.get_marker) self.filter_marker.get_marker)
self.tag = gtk.ComboBox()
self.filter_note = gtk.Entry() self.filter_note = gtk.Entry()
self.filter_gender = gtk.combo_box_new_text() self.filter_gender = gtk.combo_box_new_text()
map(self.filter_gender.append_text, map(self.filter_gender.append_text,
@ -103,6 +106,8 @@ class PersonSidebarFilter(SidebarFilter):
SidebarFilter.__init__(self, dbstate, uistate, "Person") SidebarFilter.__init__(self, dbstate, uistate, "Person")
self.update_tag_list()
def create_widget(self): def create_widget(self):
cell = gtk.CellRendererText() cell = gtk.CellRendererText()
cell.set_property('width', self._FILTER_WIDTH) cell.set_property('width', self._FILTER_WIDTH)
@ -111,6 +116,12 @@ class PersonSidebarFilter(SidebarFilter):
self.generic.add_attribute(cell, 'text', 0) self.generic.add_attribute(cell, 'text', 0)
self.on_filters_changed('Person') self.on_filters_changed('Person')
cell = gtk.CellRendererText()
cell.set_property('width', self._FILTER_WIDTH)
cell.set_property('ellipsize', self._FILTER_ELLIPSIZE)
self.tag.pack_start(cell, True)
self.tag.add_attribute(cell, 'text', 0)
exdate1 = gen.lib.Date() exdate1 = gen.lib.Date()
exdate2 = gen.lib.Date() exdate2 = gen.lib.Date()
exdate1.set(gen.lib.Date.QUAL_NONE, gen.lib.Date.MOD_RANGE, exdate1.set(gen.lib.Date.QUAL_NONE, gen.lib.Date.MOD_RANGE,
@ -131,6 +142,7 @@ class PersonSidebarFilter(SidebarFilter):
_('example: "%s" or "%s"') % (msg1, msg2)) _('example: "%s" or "%s"') % (msg1, msg2))
self.add_entry(_('Event'), self.etype) self.add_entry(_('Event'), self.etype)
self.add_entry(_('Marker'), self.mtype) self.add_entry(_('Marker'), self.mtype)
self.add_entry(_('Tag'), self.tag)
self.add_text_entry(_('Note'), self.filter_note) self.add_text_entry(_('Note'), self.filter_note)
self.add_filter_entry(_('Custom filter'), self.generic) self.add_filter_entry(_('Custom filter'), self.generic)
self.add_entry(None, self.filter_regex) self.add_entry(None, self.filter_regex)
@ -144,6 +156,7 @@ class PersonSidebarFilter(SidebarFilter):
self.filter_gender.set_active(0) self.filter_gender.set_active(0)
self.etype.child.set_text(u'') self.etype.child.set_text(u'')
self.mtype.child.set_text(u'') self.mtype.child.set_text(u'')
self.tag.set_active(0)
self.generic.set_active(0) self.generic.set_active(0)
def get_filter(self): def get_filter(self):
@ -165,12 +178,13 @@ class PersonSidebarFilter(SidebarFilter):
gender = self.filter_gender.get_active() gender = self.filter_gender.get_active()
regex = self.filter_regex.get_active() regex = self.filter_regex.get_active()
generic = self.generic.get_active() > 0 generic = self.generic.get_active() > 0
tag = self.tag.get_active() > 0
# check to see if the filter is empty. If it is empty, then # check to see if the filter is empty. If it is empty, then
# we don't build a filter # we don't build a filter
empty = not (name or gid or birth or death or etype or mtype empty = not (name or gid or birth or death or etype or mtype
or note or gender or regex or generic) or note or gender or regex or generic or tag)
if empty: if empty:
generic_filter = None generic_filter = None
else: else:
@ -209,6 +223,14 @@ class PersonSidebarFilter(SidebarFilter):
rule = HasMarkerOf([mtype]) rule = HasMarkerOf([mtype])
generic_filter.add_rule(rule) generic_filter.add_rule(rule)
# check the Tag
if tag:
model = self.tag.get_model()
node = self.tag.get_active_iter()
attr = model.get_value(node, 0)
rule = HasTag([attr])
generic_filter.add_rule(rule)
# Build an event filter if needed # Build an event filter if needed
if etype: if etype:
rule = HasEvent([etype, u'', u'', u'']) rule = HasEvent([etype, u'', u'', u''])
@ -251,3 +273,26 @@ class PersonSidebarFilter(SidebarFilter):
all_filter.add_rule(Rules.Person.Everyone([])) all_filter.add_rule(Rules.Person.Everyone([]))
self.generic.set_model(build_filter_model('Person', [all_filter])) self.generic.set_model(build_filter_model('Person', [all_filter]))
self.generic.set_active(0) self.generic.set_active(0)
def on_db_changed(self, db):
"""
Called when the database is changed.
"""
self.update_tag_list()
def on_tags_changed(self):
"""
Called when tags are changed.
"""
self.update_tag_list()
def update_tag_list(self):
"""
Update the list of tags in the tag filter.
"""
model = gtk.ListStore(str)
model.append(('',))
for tag in sorted(self.dbstate.db.get_all_tags(), key=locale.strxfrm):
model.append((tag,))
self.tag.set_model(model)
self.tag.set_active(0)

View File

@ -46,6 +46,7 @@ class SidebarFilter(object):
self._init_interface() self._init_interface()
uistate.connect('filters-changed', self.on_filters_changed) uistate.connect('filters-changed', self.on_filters_changed)
dbstate.connect('database-changed', self._db_changed)
self.uistate = uistate self.uistate = uistate
self.dbstate = dbstate self.dbstate = dbstate
self.namespace = namespace self.namespace = namespace
@ -137,6 +138,28 @@ class SidebarFilter(object):
self.position += 1 self.position += 1
def on_filters_changed(self, namespace): def on_filters_changed(self, namespace):
"""
Called when filters are changed.
"""
pass
def _db_changed(self, db):
"""
Called when the database is changed.
"""
db.connect('tags-changed', self.on_tags_changed)
self.on_db_changed(db)
def on_db_changed(self, db):
"""
Called when the database is changed.
"""
pass
def on_tags_changed(self):
"""
Called when tags are changed.
"""
pass pass
def add_filter_entry(self, text, widget): def add_filter_entry(self, text, widget):

View File

@ -765,6 +765,30 @@ class DbReadBase(object):
""" """
raise NotImplementedError raise NotImplementedError
def get_tag(self, tag_name):
"""
Return the color of the tag.
"""
raise NotImplementedError
def get_tag_colors(self):
"""
Return a list of all the tags in the database.
"""
raise NotImplementedError
def get_all_tags(self):
"""
Return a dictionary of tags with their associated colors.
"""
raise NotImplementedError
def has_tag(self, tag_name):
"""
Return if a tag exists in the tags table.
"""
raise NotImplementedError
def has_note_handle(self, handle): def has_note_handle(self, handle):
""" """
Return True if the handle exists in the current Note database. Return True if the handle exists in the current Note database.
@ -1386,6 +1410,14 @@ class DbWriteBase(object):
""" """
raise NotImplementedError raise NotImplementedError
def set_tag(self, tag_name, color_str):
"""
Set the color of a tag.
Needs to be overridden in the derived class.
"""
raise NotImplementedError
def sort_surname_list(self): def sort_surname_list(self):
""" """
Sort the list of surnames contained in the database by locale ordering. Sort the list of surnames contained in the database by locale ordering.

View File

@ -291,6 +291,7 @@ class DbBsddbRead(DbReadBase, Callback):
self.event_map = {} self.event_map = {}
self.metadata = {} self.metadata = {}
self.name_group = {} self.name_group = {}
self.tags = {}
self.undo_callback = None self.undo_callback = None
self.redo_callback = None self.redo_callback = None
self.undo_history_callback = None self.undo_history_callback = None
@ -697,6 +698,30 @@ class DbBsddbRead(DbReadBase, Callback):
""" """
return self.name_group.has_key(str(name)) return self.name_group.has_key(str(name))
def get_tag(self, tag_name):
"""
Return the color of the tag.
"""
return self.tags.get(tag_name)
def get_tag_colors(self):
"""
Return a list of all the tags in the database.
"""
return dict([(k, self.tags.get(k)) for k in self.tags.keys()])
def get_all_tags(self):
"""
Return a dictionary of tags with their associated colors.
"""
return self.tags.keys()
def has_tag(self, tag_name):
"""
Return if a tag exists in the tags table.
"""
return self.tags.has_key(tag_name)
def get_number_of_records(self, table): def get_number_of_records(self, table):
if not self.db_is_open: if not self.db_is_open:
return 0 return 0

View File

@ -26,6 +26,26 @@ from gen.db import BSDDBTxn
upgrade upgrade
""" """
def gramps_upgrade_15(self):
"""Upgrade database from version 14 to 15."""
# This upgrade adds tagging
length = len(self.person_map)
self.set_total(length)
# ---------------------------------
# Modify Person
# ---------------------------------
# Append the new tag field
for handle in self.person_map.keys():
person = self.person_map[handle]
with BSDDBTxn(self.env, self.person_map) as txn:
txn.put(str(handle), person.append([]))
self.update()
# Bump up database version. Separate transaction to save metadata.
with BSDDBTxn(self.env, self.metadata) as txn:
txn.put('version', 15)
def gramps_upgrade_14(self): def gramps_upgrade_14(self):
"""Upgrade database from version 13 to 14.""" """Upgrade database from version 13 to 14."""
# This upgrade modifies notes and dates # This upgrade modifies notes and dates

View File

@ -61,7 +61,7 @@ import Errors
_LOG = logging.getLogger(DBLOGNAME) _LOG = logging.getLogger(DBLOGNAME)
_MINVERSION = 9 _MINVERSION = 9
_DBVERSION = 14 _DBVERSION = 15
IDTRANS = "person_id" IDTRANS = "person_id"
FIDTRANS = "family_id" FIDTRANS = "family_id"
@ -73,6 +73,7 @@ NIDTRANS = "note_id"
SIDTRANS = "source_id" SIDTRANS = "source_id"
SURNAMES = "surnames" SURNAMES = "surnames"
NAME_GROUP = "name_group" NAME_GROUP = "name_group"
TAGS = "tags"
META = "meta_data" META = "meta_data"
FAMILY_TBL = "family" FAMILY_TBL = "family"
@ -197,6 +198,10 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback):
# 4. Signal for change in person group name, parameters are # 4. Signal for change in person group name, parameters are
__signals__['person-groupname-rebuild'] = (unicode, unicode) __signals__['person-groupname-rebuild'] = (unicode, unicode)
# 5. Signals for change ins tags
__signals__['tags-changed'] = None
__signals__['tag-update'] = (str, str)
def __init__(self): def __init__(self):
"""Create a new GrampsDB.""" """Create a new GrampsDB."""
@ -463,6 +468,9 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback):
self.name_group = self.__open_db(self.full_name, NAME_GROUP, self.name_group = self.__open_db(self.full_name, NAME_GROUP,
db.DB_HASH, db.DB_DUP) db.DB_HASH, db.DB_DUP)
# Open tags database
self.tags = self.__open_db(self.full_name, TAGS, db.DB_HASH, db.DB_DUP)
# Here we take care of any changes in the tables related to new code. # Here we take care of any changes in the tables related to new code.
# If secondary indices change, then they should removed # If secondary indices change, then they should removed
# or rebuilt by upgrade as well. In any case, the # or rebuilt by upgrade as well. In any case, the
@ -1006,6 +1014,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback):
self.__close_metadata() self.__close_metadata()
self.name_group.close() self.name_group.close()
self.tags.close()
self.surnames.close() self.surnames.close()
self.id_trans.close() self.id_trans.close()
self.fid_trans.close() self.fid_trans.close()
@ -1042,7 +1051,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback):
self.media_map = None self.media_map = None
self.event_map = None self.event_map = None
self.surnames = None self.surnames = None
self.name_group = None self.tags = None
self.env = None self.env = None
self.metadata = None self.metadata = None
self.db_is_open = False self.db_is_open = False
@ -1280,6 +1289,24 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback):
grouppar = group grouppar = group
self.emit('person-groupname-rebuild', (name, grouppar)) self.emit('person-groupname-rebuild', (name, grouppar))
@catch_db_error
def set_tag(self, tag_name, color_str):
"""
Set the color of a tag.
"""
if not self.readonly:
# Start transaction
with BSDDBTxn(self.env, self.tags) as txn:
data = txn.get(tag_name)
if data is not None:
txn.delete(tag_name)
if color_str is not None:
txn.put(tag_name, color_str)
if data is not None and color_str is not None:
self.emit('tag-update', (tag_name, color_str))
else:
self.emit('tags-changed')
def sort_surname_list(self): def sort_surname_list(self):
self.surname_list.sort(key=locale.strxfrm) self.surname_list.sort(key=locale.strxfrm)
@ -1652,9 +1679,11 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback):
t = time.time() t = time.time()
import upgrade
if version < 14: if version < 14:
import upgrade
upgrade.gramps_upgrade_14(self) upgrade.gramps_upgrade_14(self)
if version < 15:
upgrade.gramps_upgrade_15(self)
print "Upgrade time:", int(time.time()-t), "seconds" print "Upgrade time:", int(time.time()-t), "seconds"

View File

@ -60,6 +60,7 @@ pkgdata_PYTHON = \
styledtext.py \ styledtext.py \
styledtexttag.py \ styledtexttag.py \
styledtexttagtype.py \ styledtexttagtype.py \
tagbase.py \
urlbase.py \ urlbase.py \
url.py \ url.py \
urltype.py \ urltype.py \

View File

@ -38,6 +38,7 @@ from gen.lib.attrbase import AttributeBase
from gen.lib.addressbase import AddressBase from gen.lib.addressbase import AddressBase
from gen.lib.ldsordbase import LdsOrdBase from gen.lib.ldsordbase import LdsOrdBase
from gen.lib.urlbase import UrlBase from gen.lib.urlbase import UrlBase
from gen.lib.tagbase import TagBase
from gen.lib.name import Name from gen.lib.name import Name
from gen.lib.eventref import EventRef from gen.lib.eventref import EventRef
from gen.lib.personref import PersonRef from gen.lib.personref import PersonRef
@ -53,7 +54,7 @@ from gen.lib.const import IDENTICAL, EQUAL, DIFFERENT
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
class Person(SourceBase, NoteBase, AttributeBase, MediaBase, class Person(SourceBase, NoteBase, AttributeBase, MediaBase,
AddressBase, UrlBase, LdsOrdBase, PrimaryObject): AddressBase, UrlBase, LdsOrdBase, TagBase, PrimaryObject):
""" """
The Person record is the GRAMPS in-memory representation of an The Person record is the GRAMPS in-memory representation of an
individual person. It contains all the information related to individual person. It contains all the information related to
@ -91,6 +92,7 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase,
AddressBase.__init__(self) AddressBase.__init__(self)
UrlBase.__init__(self) UrlBase.__init__(self)
LdsOrdBase.__init__(self) LdsOrdBase.__init__(self)
TagBase.__init__(self)
self.primary_name = Name() self.primary_name = Name()
self.marker = MarkerType() self.marker = MarkerType()
self.event_ref_list = [] self.event_ref_list = []
@ -153,7 +155,8 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase,
self.change, # 17 self.change, # 17
self.marker.serialize(), # 18 self.marker.serialize(), # 18
self.private, # 19 self.private, # 19
[pr.serialize() for pr in self.person_ref_list] # 20 [pr.serialize() for pr in self.person_ref_list], # 20
TagBase.serialize(self) # 21
) )
def unserialize(self, data): def unserialize(self, data):
@ -186,6 +189,7 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase,
marker, # 18 marker, # 18
self.private, # 19 self.private, # 19
person_ref_list, # 20 person_ref_list, # 20
tag_list, # 21
) = data ) = data
self.marker = MarkerType() self.marker = MarkerType()
@ -205,6 +209,7 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase,
UrlBase.unserialize(self, urls) UrlBase.unserialize(self, urls)
SourceBase.unserialize(self, source_list) SourceBase.unserialize(self, source_list)
NoteBase.unserialize(self, note_list) NoteBase.unserialize(self, note_list)
TagBase.unserialize(self, tag_list)
return self return self
def _has_handle_reference(self, classname, handle): def _has_handle_reference(self, classname, handle):

107
src/gen/lib/tagbase.py Normal file
View File

@ -0,0 +1,107 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2010 Nick Hall
#
# 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$
"""
TagBase class for Gramps.
"""
#-------------------------------------------------------------------------
#
# TagBase class
#
#-------------------------------------------------------------------------
class TagBase(object):
"""
Base class for tag-aware objects.
"""
def __init__(self, source=None):
"""
Initialize a TagBase.
If the source is not None, then object is initialized from values of
the source object.
:param source: Object used to initialize the new object
:type source: TagBase
"""
if source:
self.tag_list = source.tag_list
else:
self.tag_list = []
def serialize(self):
"""
Convert the object to a serialized tuple of data.
"""
return self.tag_list
def unserialize(self, data):
"""
Convert a serialized tuple of data to an object.
"""
self.tag_list = data
def add_tag(self, tag):
"""
Add the tag to the object's list of tags.
:param tag: unicode tag to add.
:type tag: unicode
"""
if tag not in self.tag_list:
self.tag_list.append(tag)
def remove_tag(self, tag):
"""
Remove the specified tag from the tag list.
If the tag does not exist in the list, the operation has no effect.
:param tag: tag to remove from the list.
:type tag: unicode
:returns: True if the tag was removed, False if it was not in the list.
:rtype: bool
"""
if tag in self.tag_list:
self.tag_list.remove(tag)
return True
else:
return False
def get_tag_list(self):
"""
Return the list of tags associated with the object.
:returns: Returns the list of tags.
:rtype: list
"""
return self.tag_list
def set_tag_list(self, tag_list):
"""
Assign the passed list to the objects's list of tags.
:param tag_list: List of tags to ba associated with the object.
:type tag_list: list
"""
self.tag_list = tag_list

View File

@ -17,7 +17,7 @@
<child> <child>
<object class="GtkTable" id="table15"> <object class="GtkTable" id="table15">
<property name="visible">True</property> <property name="visible">True</property>
<property name="n_rows">6</property> <property name="n_rows">7</property>
<property name="n_columns">6</property> <property name="n_columns">6</property>
<property name="column_spacing">12</property> <property name="column_spacing">12</property>
<property name="row_spacing">6</property> <property name="row_spacing">6</property>
@ -490,6 +490,60 @@ Title: A title used to refer to the person, such as 'Dr.' or 'Rev.'</property>
<property name="y_options">GTK_FILL</property> <property name="y_options">GTK_FILL</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">_Tags:</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<child>
<object class="GtkLabel" id="tag_label">
<property name="visible">True</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="tag_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage" id="tag_image">
<property name="visible">True</property>
<property name="stock">gramps-tag</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">6</property>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child> <child>
<placeholder/> <placeholder/>
</child> </child>

View File

@ -31,7 +31,6 @@ to edit information about a particular Person.
# Standard python modules # Standard python modules
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
import locale
from gen.ggettext import sgettext as _ from gen.ggettext import sgettext as _
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
@ -306,6 +305,15 @@ class EditPerson(EditPrimary):
self.db.readonly, self.db.readonly,
autolist=self.db.get_surname_list() if not self.db.readonly else []) autolist=self.db.get_surname_list() if not self.db.readonly else [])
self.tags = widgets.MonitoredTagList(
self.top.get_object("tag_label"),
self.top.get_object("tag_button"),
self.obj.set_tag_list,
self.obj.get_tag_list,
self.db.get_all_tags(),
self.uistate, self.track,
self.db.readonly)
self.gid = widgets.MonitoredEntry( self.gid = widgets.MonitoredEntry(
self.top.get_object("gid"), self.top.get_object("gid"),
self.obj.set_gramps_id, self.obj.set_gramps_id,
@ -724,9 +732,9 @@ class EditPerson(EditPrimary):
name = self.name_displayer.display(prim_object) name = self.name_displayer.display(prim_object)
msg1 = _("Cannot save person. ID already exists.") msg1 = _("Cannot save person. ID already exists.")
msg2 = _("You have attempted to use the existing Gramps ID with " msg2 = _("You have attempted to use the existing Gramps ID with "
"value %(id)s. This value is already used by '" "value %(id)s. This value is already used by '"
"%(prim_object)s'. Please enter a different ID or leave " "%(prim_object)s'. Please enter a different ID or leave "
"blank to get the next available ID value.") % { "blank to get the next available ID value.") % {
'id' : id, 'prim_object' : name } 'id' : id, 'prim_object' : name }
ErrorDialog(msg1, msg2) ErrorDialog(msg1, msg2)
self.ok_button.set_sensitive(True) self.ok_button.set_sensitive(True)

View File

@ -136,6 +136,8 @@ def register_stock_icons ():
('gramps-repository', _('Repositories'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-repository', _('Repositories'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-source', _('Sources'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-source', _('Sources'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-spouse', _('Add Spouse'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-spouse', _('Add Spouse'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-tag', _('Tag'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-tag-new', _('New Tag'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-tools', _('Tools'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-tools', _('Tools'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-tree-group', _('Grouped List'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-tree-group', _('Grouped List'), gtk.gdk.CONTROL_MASK, 0, ''),
('gramps-tree-list', _('List'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-tree-list', _('List'), gtk.gdk.CONTROL_MASK, 0, ''),

View File

@ -89,6 +89,7 @@ from gen.db.backup import backup
from gen.db.exceptions import DbException from gen.db.exceptions import DbException
from GrampsAboutDialog import GrampsAboutDialog from GrampsAboutDialog import GrampsAboutDialog
from gui.sidebar import Sidebar from gui.sidebar import Sidebar
from gui.views.tags import Tags
from gen.utils.configmanager import safe_eval from gen.utils.configmanager import safe_eval
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
@ -120,6 +121,8 @@ UIDEFAULT = '''<ui>
<separator/> <separator/>
<placeholder name="CommonEdit"/> <placeholder name="CommonEdit"/>
<separator/> <separator/>
<placeholder name="TagMenu"/>
<separator/>
<menuitem action="ScratchPad"/> <menuitem action="ScratchPad"/>
<separator/> <separator/>
<menuitem action="Preferences"/> <menuitem action="Preferences"/>
@ -172,6 +175,8 @@ UIDEFAULT = '''<ui>
<toolitem action="Reports"/> <toolitem action="Reports"/>
<toolitem action="Tools"/> <toolitem action="Tools"/>
<separator/> <separator/>
<placeholder name="TagTool"/>
<separator/>
<placeholder name="CommonEdit"/> <placeholder name="CommonEdit"/>
<separator/> <separator/>
<placeholder name="ViewsInCategory"/> <placeholder name="ViewsInCategory"/>
@ -536,6 +541,8 @@ class ViewManager(CLIManager):
self.dbstate.connect('database-changed', self.uistate.db_changed) self.dbstate.connect('database-changed', self.uistate.db_changed)
self.tags = Tags(self.uistate, self.dbstate)
self.filter_menu = self.uimanager.get_widget( self.filter_menu = self.uimanager.get_widget(
'/MenuBar/ViewMenu/Filter/') '/MenuBar/ViewMenu/Filter/')
@ -1700,34 +1707,34 @@ def by_menu_name(first, second):
return cmp(first.name, second.name) return cmp(first.name, second.name)
def run_plugin(pdata, dbstate, uistate): def run_plugin(pdata, dbstate, uistate):
""" """
run a plugin based on it's PluginData: run a plugin based on it's PluginData:
1/ load plugin. 1/ load plugin.
2/ the report is run 2/ the report is run
""" """
mod = GuiPluginManager.get_instance().load_plugin(pdata) mod = GuiPluginManager.get_instance().load_plugin(pdata)
if not mod: if not mod:
#import of plugin failed #import of plugin failed
ErrorDialog( ErrorDialog(
_('Failed Loading Plugin'), _('Failed Loading Plugin'),
_('The plugin did not load. See Help Menu, Plugin Manager' _('The plugin did not load. See Help Menu, Plugin Manager'
' for more info.\nUse http://bugs.gramps-project.org to' ' for more info.\nUse http://bugs.gramps-project.org to'
' submit bugs of official plugins, contact the plugin ' ' submit bugs of official plugins, contact the plugin '
'author otherwise. ')) 'author otherwise. '))
return return
if pdata.ptype == REPORT: if pdata.ptype == REPORT:
report(dbstate, uistate, uistate.get_active('Person'), report(dbstate, uistate, uistate.get_active('Person'),
getattr(mod, pdata.reportclass), getattr(mod, pdata.reportclass),
getattr(mod, pdata.optionclass), getattr(mod, pdata.optionclass),
pdata.name, pdata.id, pdata.name, pdata.id,
pdata.category, pdata.require_active) pdata.category, pdata.require_active)
else: else:
tool.gui_tool(dbstate, uistate, tool.gui_tool(dbstate, uistate,
getattr(mod, pdata.toolclass), getattr(mod, pdata.toolclass),
getattr(mod, pdata.optionclass), getattr(mod, pdata.optionclass),
pdata.name, pdata.id, pdata.category, pdata.name, pdata.id, pdata.category,
dbstate.db.request_rebuild) dbstate.db.request_rebuild)
def make_plugin_callback(pdata, dbstate, uistate): def make_plugin_callback(pdata, dbstate, uistate):
""" """

View File

@ -12,7 +12,8 @@ pkgdata_PYTHON = \
__init__.py \ __init__.py \
listview.py \ listview.py \
navigationview.py \ navigationview.py \
pageview.py pageview.py \
tags.py
pkgpyexecdir = @pkgpyexecdir@/gui/views pkgpyexecdir = @pkgpyexecdir@/gui/views
pkgpythondir = @pkgpythondir@/gui/views pkgpythondir = @pkgpythondir@/gui/views

440
src/gui/views/tags.py Normal file
View File

@ -0,0 +1,440 @@
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2010 Nick Hall
#
# 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 021111307 USA
#
# $Id$
"""
Provide tagging functionality.
"""
#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
import locale
#-------------------------------------------------------------------------
#
# GTK/Gnome modules
#
#-------------------------------------------------------------------------
import gtk
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from gen.ggettext import sgettext as _
from ListModel import ListModel, NOSORT, COLOR
import const
import GrampsDisplay
from QuestionDialog import QuestionDialog2
import gui.widgets.progressdialog as progressdlg
#-------------------------------------------------------------------------
#
# Constants
#
#-------------------------------------------------------------------------
TAG_1 = '''<ui>
<menubar name="MenuBar">
<menu action="EditMenu">
<placeholder name="TagMenu">
<menu action="Tag">
'''
TAG_2 = '''
</menu>
</placeholder>
</menu>
</menubar>
<toolbar name="ToolBar">
<placeholder name="TagTool">
<toolitem action="TagButton"/>
</placeholder>
</toolbar>
<popup name="TagPopup">
'''
TAG_3 = '''
</popup>
</ui>'''
WIKI_HELP_PAGE = '%s_-_Entering_and_Editing_Data:_Detailed_-_part_3' % \
const.URL_MANUAL_PAGE
WIKI_HELP_SEC = _('manual|Tags')
#-------------------------------------------------------------------------
#
# Tags
#
#-------------------------------------------------------------------------
class Tags(object):
"""
Provide tagging functionality.
"""
def __init__(self, uistate, dbstate):
self.db = dbstate.db
self.uistate = uistate
self.tag_id = None
self.tag_ui = None
self.tag_action = None
dbstate.connect('database-changed', self.db_changed)
self._build_tag_menu()
def tag_enable(self):
"""
Enables the UI and action groups for the tag menu.
"""
self.uistate.uimanager.insert_action_group(self.tag_action, 1)
self.tag_id = self.uistate.uimanager.add_ui_from_string(self.tag_ui)
self.uistate.uimanager.ensure_update()
def tag_disable(self):
"""
Remove the UI and action groups for the tag menu.
"""
self.uistate.uimanager.remove_ui(self.tag_id)
self.uistate.uimanager.remove_action_group(self.tag_action)
self.uistate.uimanager.ensure_update()
self.tag_id = None
def db_changed(self, db):
"""
When the database chages update the tag list and rebuild the menus.
"""
self.db = db
self.db.connect('tags-changed', self.update_tag_menu)
self.update_tag_menu()
def update_tag_menu(self):
"""
Re-build the menu when a tag is added or removed.
"""
enabled = self.tag_id is not None
if enabled:
self.tag_disable()
self._build_tag_menu()
if enabled:
self.tag_enable()
def _build_tag_menu(self):
"""
Builds the UI and action group for the tag menu.
"""
actions = []
if self.db is None:
self.tag_ui = ''
self.tag_action = gtk.ActionGroup('Tag')
return
tag_menu = '<menuitem action="NewTag"/>'
tag_menu += '<menuitem action="OrganizeTags"/>'
tag_menu += '<separator/>'
for tag_name in sorted(self.db.get_all_tags(), key=locale.strxfrm):
tag_menu += '<menuitem action="TAG_%s"/>' % tag_name
actions.append(('TAG_%s' % tag_name, None, tag_name, None, None,
make_callback(self.tag_selected, tag_name)))
self.tag_ui = TAG_1 + tag_menu + TAG_2 + tag_menu + TAG_3
actions.append(('Tag', 'gramps-tag', _('Tag'), None, None, None))
actions.append(('NewTag', 'gramps-tag-new', _('New Tag...'), None, None,
self.cb_new_tag))
actions.append(('OrganizeTags', None, _('Organize Tags...'), None, None,
self.cb_organize_tags))
actions.append(('TagButton', 'gramps-tag', _('Tag'), None,
_('Tag selected rows'), self.cb_tag_button))
self.tag_action = gtk.ActionGroup('Tag')
self.tag_action.add_actions(actions)
def cb_tag_button(self, action):
"""
Display the popup menu when the toolbar button is clicked.
"""
menu = self.uistate.uimanager.get_widget('/TagPopup')
button = self.uistate.uimanager.get_widget('/ToolBar/TagTool/TagButton')
menu.popup(None, None, cb_menu_position, 0, 0, button)
def cb_organize_tags(self, action):
"""
Display the Organize Tags dialog.
"""
organize_dialog = OrganizeTagsDialog(self.db, self.uistate.window)
organize_dialog.run()
def cb_new_tag(self, action):
"""
Create a new tag and tag the selected objects.
"""
new_dialog = NewTagDialog(self.uistate.window)
tag_name, color_str = new_dialog.run()
if tag_name and not self.db.has_tag(tag_name):
self.db.set_tag(tag_name, color_str)
self.tag_selected(tag_name)
self.update_tag_menu()
def tag_selected(self, tag_name):
"""
Tag the selected objects with the given tag.
"""
view = self.uistate.viewmanager.active_page
view.add_tag(tag_name)
def cb_menu_position(menu, button):
"""
Determine the position of the popup menu.
"""
x_pos, y_pos = button.window.get_origin()
x_pos += button.allocation.x
y_pos += button.allocation.y + button.allocation.height
return (x_pos, y_pos, False)
def make_callback(func, tag_name):
"""
Generates a callback function based off the passed arguments
"""
return lambda x: func(tag_name)
#-------------------------------------------------------------------------
#
# Organize Tags Dialog
#
#-------------------------------------------------------------------------
class OrganizeTagsDialog(object):
"""
A dialog to enable the user to organize tags.
"""
def __init__(self, db, parent_window):
self.db = db
self.parent_window = parent_window
self.namelist = None
self.namemodel = None
self.top = self._create_dialog()
def run(self):
"""
Run the dialog and return the result.
"""
self._populate_model()
while True:
response = self.top.run()
if response == gtk.RESPONSE_HELP:
GrampsDisplay.help(webpage=WIKI_HELP_PAGE,
section=WIKI_HELP_SEC)
else:
break
self.top.destroy()
def _populate_model(self):
"""
Populate the model.
"""
self.namemodel.clear()
for tag in sorted(self.db.get_all_tags(), key=locale.strxfrm):
self.namemodel.add([tag, self.db.get_tag(tag)])
def _create_dialog(self):
"""
Create a dialog box to organize tags.
"""
# pylint: disable-msg=E1101
title = _("%(title)s - Gramps") % {'title': _("Organize Tags")}
top = gtk.Dialog(title)
top.set_default_size(400, 350)
top.set_modal(True)
top.set_transient_for(self.parent_window)
top.set_has_separator(False)
top.vbox.set_spacing(5)
label = gtk.Label('<span size="larger" weight="bold">%s</span>'
% _("Organize Tags"))
label.set_use_markup(True)
top.vbox.pack_start(label, 0, 0, 5)
box = gtk.HBox()
top.vbox.pack_start(box, 1, 1, 5)
name_titles = [(_('Name'), NOSORT, 200),
(_('Color'), NOSORT, 50, COLOR)]
self.namelist = gtk.TreeView()
self.namemodel = ListModel(self.namelist, name_titles)
slist = gtk.ScrolledWindow()
slist.add_with_viewport(self.namelist)
slist.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
box.pack_start(slist, 1, 1, 5)
bbox = gtk.VButtonBox()
bbox.set_layout(gtk.BUTTONBOX_START)
bbox.set_spacing(6)
add = gtk.Button(stock=gtk.STOCK_ADD)
edit = gtk.Button(stock=gtk.STOCK_EDIT)
remove = gtk.Button(stock=gtk.STOCK_REMOVE)
add.connect('clicked', self.cb_add_clicked, top)
edit.connect('clicked', self.cb_edit_clicked)
remove.connect('clicked', self.cb_remove_clicked, top)
top.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
top.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
bbox.add(add)
bbox.add(edit)
bbox.add(remove)
box.pack_start(bbox, 0, 0, 5)
top.show_all()
return top
def cb_add_clicked(self, button, top):
"""
Create a new tag.
"""
new_dialog = NewTagDialog(top)
tag_name, color_str = new_dialog.run()
if tag_name and not self.db.has_tag(tag_name):
self.db.set_tag(tag_name, color_str)
self._populate_model()
def cb_edit_clicked(self, button):
"""
Edit the color of an existing tag.
"""
# pylint: disable-msg=E1101
store, iter_ = self.namemodel.get_selected()
if iter_ is None:
return
tag_name = store.get_value(iter_, 0)
old_color = gtk.gdk.Color(store.get_value(iter_, 1))
title = _("%(title)s - Gramps") % {'title': _("Pick a Color")}
colorseldlg = gtk.ColorSelectionDialog(title)
colorseldlg.set_transient_for(self.top)
colorseldlg.colorsel.set_current_color(old_color)
colorseldlg.colorsel.set_previous_color(old_color)
response = colorseldlg.run()
if response == gtk.RESPONSE_OK:
color_str = colorseldlg.colorsel.get_current_color().to_string()
self.db.set_tag(tag_name, color_str)
store.set_value(iter_, 1, color_str)
colorseldlg.destroy()
def cb_remove_clicked(self, button, top):
"""
Remove the selected tag.
"""
store, iter_ = self.namemodel.get_selected()
if iter_ is None:
return
tag_name = store.get_value(iter_, 0)
yes_no = QuestionDialog2(
_("Remove tag '%s'?") % tag_name,
_("The tag definition will be removed. "
"The tag will be also removed from all objects in the database."),
_("Yes"),
_("No"))
prompt = yes_no.run()
if prompt:
self.remove_tag(tag_name)
store.remove(iter_)
def remove_tag(self, tag_name):
"""
Remove the tag from all objects and delete the tag.
"""
items = self.db.get_number_of_people()
pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog,
popup_time=2)
status = progressdlg.LongOpStatus(msg=_("Removing Tags"),
total_steps=items,
interval=items//20,
can_cancel=True)
pmon.add_op(status)
trans = self.db.transaction_begin()
for handle in self.db.get_person_handles():
status.heartbeat()
if status.should_cancel():
break
person = self.db.get_person_from_handle(handle)
tags = person.get_tag_list()
if tag_name in tags:
tags.remove(tag_name)
person.set_tag_list(tags)
self.db.commit_person(person, trans)
if not status.was_cancelled():
self.db.set_tag(tag_name, None)
self.db.transaction_commit(trans, _('Remove tag %s') % tag_name)
status.end()
#-------------------------------------------------------------------------
#
# New Tag Dialog
#
#-------------------------------------------------------------------------
class NewTagDialog(object):
"""
A dialog to enable the user to create a new tag.
"""
def __init__(self, parent_window):
self.parent_window = parent_window
self.entry = None
self.color = None
self.top = self._create_dialog()
def run(self):
"""
Run the dialog and return the result.
"""
result = (None, None)
response = self.top.run()
if response == gtk.RESPONSE_OK:
result = (self.entry.get_text(), self.color.get_color().to_string())
self.top.destroy()
return result
def _create_dialog(self):
"""
Create a dialog box to enter a new tag.
"""
# pylint: disable-msg=E1101
title = _("%(title)s - Gramps") % {'title': _("New Tag")}
top = gtk.Dialog(title)
top.set_default_size(300, 100)
top.set_modal(True)
top.set_transient_for(self.parent_window)
top.set_has_separator(False)
top.vbox.set_spacing(5)
hbox = gtk.HBox()
top.vbox.pack_start(hbox, False, False, 10)
label = gtk.Label(_('Tag Name:'))
self.entry = gtk.Entry()
self.color = gtk.ColorButton()
title = _("%(title)s - Gramps") % {'title': _("Pick a Color")}
self.color.set_title(title)
hbox.pack_start(label, False, False, 5)
hbox.pack_start(self.entry, True, True, 5)
hbox.pack_start(self.color, False, False, 5)
top.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
top.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
top.show_all()
return top

View File

@ -78,6 +78,7 @@ COLUMN_DEATH = 5
COLUMN_BIRTH = 6 COLUMN_BIRTH = 6
COLUMN_EVENT = 7 COLUMN_EVENT = 7
COLUMN_FAMILY = 8 COLUMN_FAMILY = 8
COLUMN_TAGS = 21
COLUMN_CHANGE = 17 COLUMN_CHANGE = 17
COLUMN_MARKER = 18 COLUMN_MARKER = 18
@ -103,6 +104,7 @@ class PeopleBaseModel(object):
""" """
Initialize the model building the initial data Initialize the model building the initial data
""" """
self.db = db
self.gen_cursor = db.get_person_cursor self.gen_cursor = db.get_person_cursor
self.map = db.get_raw_person_data self.map = db.get_raw_person_data
@ -115,10 +117,11 @@ class PeopleBaseModel(object):
self.column_death_day, self.column_death_day,
self.column_death_place, self.column_death_place,
self.column_spouse, self.column_spouse,
self.column_tags,
self.column_change, self.column_change,
self.column_int_id, self.column_int_id,
self.column_marker_text, self.column_marker_text,
self.column_marker_color, self.column_tag_color,
self.column_tooltip, self.column_tooltip,
] ]
self.smap = [ self.smap = [
@ -130,10 +133,11 @@ class PeopleBaseModel(object):
self.sort_death_day, self.sort_death_day,
self.column_death_place, self.column_death_place,
self.column_spouse, self.column_spouse,
self.column_tags,
self.sort_change, self.sort_change,
self.column_int_id, self.column_int_id,
self.column_marker_text, self.column_marker_text,
self.column_marker_color, self.column_tag_color,
self.column_tooltip, self.column_tooltip,
] ]
@ -145,11 +149,26 @@ class PeopleBaseModel(object):
self.lru_bdate = LRU(PeopleBaseModel._CACHE_SIZE) self.lru_bdate = LRU(PeopleBaseModel._CACHE_SIZE)
self.lru_ddate = LRU(PeopleBaseModel._CACHE_SIZE) self.lru_ddate = LRU(PeopleBaseModel._CACHE_SIZE)
db.connect('tags-changed', self._tags_changed)
self._tags_changed()
def _tags_changed(self):
"""
Refresh the tag colors when a tag is added or deleted.
"""
self.tag_colors = self.db.get_tag_colors()
def update_tag(self, tag_name, color_str):
"""
Update the tag color and signal that affected rows have been updated.
"""
self.tag_colors[tag_name] = color_str
def marker_column(self): def marker_column(self):
""" """
Return the column for marker colour. Return the column for marker colour.
""" """
return 11 return 12
def clear_local_cache(self, handle=None): def clear_local_cache(self, handle=None):
""" Clear the LRU cache """ """ Clear the LRU cache """
@ -419,19 +438,6 @@ class PeopleBaseModel(object):
return str(data[COLUMN_MARKER]) return str(data[COLUMN_MARKER])
return "" return ""
def column_marker_color(self, data):
try:
if data[COLUMN_MARKER]:
if data[COLUMN_MARKER][0] == MarkerType.COMPLETE:
return self.complete_color
if data[COLUMN_MARKER][0] == MarkerType.TODO_TYPE:
return self.todo_color
if data[COLUMN_MARKER][0] == MarkerType.CUSTOM:
return self.custom_color
except IndexError:
pass
return None
def column_tooltip(self, data): def column_tooltip(self, data):
if const.USE_TIPS: if const.USE_TIPS:
return ToolTips.TipFromFunction( return ToolTips.TipFromFunction(
@ -444,6 +450,14 @@ class PeopleBaseModel(object):
def column_int_id(self, data): def column_int_id(self, data):
return data[0] return data[0]
def column_tag_color(self, data):
if len(data[COLUMN_TAGS]) > 0:
return self.tag_colors.get(data[COLUMN_TAGS][0])
return None
def column_tags(self, data):
return ','.join(data[COLUMN_TAGS])
class PersonListModel(PeopleBaseModel, FlatBaseModel): class PersonListModel(PeopleBaseModel, FlatBaseModel):
""" """
Listed people model. Listed people model.
@ -453,7 +467,7 @@ class PersonListModel(PeopleBaseModel, FlatBaseModel):
PeopleBaseModel.__init__(self, db) PeopleBaseModel.__init__(self, db)
FlatBaseModel.__init__(self, db, search=search, skip=skip, FlatBaseModel.__init__(self, db, search=search, skip=skip,
tooltip_column=12, tooltip_column=13,
scol=scol, order=order, sort_map=sort_map) scol=scol, order=order, sort_map=sort_map)
def clear_cache(self, handle=None): def clear_cache(self, handle=None):
@ -468,7 +482,7 @@ class PersonTreeModel(PeopleBaseModel, TreeBaseModel):
skip=set(), sort_map=None): skip=set(), sort_map=None):
PeopleBaseModel.__init__(self, db) PeopleBaseModel.__init__(self, db)
TreeBaseModel.__init__(self, db, 12, search=search, skip=skip, TreeBaseModel.__init__(self, db, 13, search=search, skip=skip,
scol=scol, order=order, sort_map=sort_map) scol=scol, order=order, sort_map=sort_map)
def _set_base_data(self): def _set_base_data(self):

View File

@ -21,6 +21,7 @@ pkgdata_PYTHON = \
statusbar.py \ statusbar.py \
styledtextbuffer.py \ styledtextbuffer.py \
styledtexteditor.py \ styledtexteditor.py \
tageditor.py \
toolcomboentry.py \ toolcomboentry.py \
undoablebuffer.py \ undoablebuffer.py \
validatedcomboentry.py \ validatedcomboentry.py \

View File

@ -23,7 +23,7 @@
__all__ = ["MonitoredCheckbox", "MonitoredEntry", "MonitoredSpinButton", __all__ = ["MonitoredCheckbox", "MonitoredEntry", "MonitoredSpinButton",
"MonitoredText", "MonitoredType", "MonitoredDataType", "MonitoredText", "MonitoredType", "MonitoredDataType",
"MonitoredMenu", "MonitoredStrMenu", "MonitoredDate", "MonitoredMenu", "MonitoredStrMenu", "MonitoredDate",
"MonitoredComboSelectedEntry"] "MonitoredComboSelectedEntry", "MonitoredTagList"]
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -47,8 +47,10 @@ import gtk
# Gramps modules # Gramps modules
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
from gen.ggettext import gettext as _
import AutoComp import AutoComp
import DateEdit import DateEdit
from tageditor import TagEditor
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -597,3 +599,55 @@ class MonitoredComboSelectedEntry(object):
Eg: name editor save brings you back to person editor that must update Eg: name editor save brings you back to person editor that must update
""" """
self.entry_reinit() self.entry_reinit()
#-------------------------------------------------------------------------
#
# MonitoredTagList class
#
#-------------------------------------------------------------------------
class MonitoredTagList(object):
"""
A MonitoredTagList consists of a label to display a list of tags and a
button to invoke the tag editor.
"""
def __init__(self, label, button, set_list, get_list, full_list,
uistate, track, readonly=False):
self.uistate = uistate
self.track = track
self.set_list = set_list
self.tag_list = get_list()
self.all_tags = full_list
self.label = label
self.label.set_alignment(0, 0.5)
image = gtk.Image()
image.set_from_stock('gramps-tag', gtk.ICON_SIZE_BUTTON)
button.set_image (image)
#button.set_label('...')
button.set_tooltip_text(_('Edit the tag list'))
button.connect('button-press-event', self.cb_edit)
button.connect('key-press-event', self.cb_edit)
button.set_sensitive(not readonly)
self._display()
def _display(self):
"""
Display the tag list.
"""
tag_text = ','.join(self.tag_list)
self.label.set_text(tag_text)
self.label.set_tooltip_text(tag_text)
def cb_edit(self, button, event):
"""
Invoke the tag editor.
"""
editor = TagEditor(self.tag_list, self.all_tags,
self.uistate, self.track)
if editor.return_list is not None:
self.tag_list = editor.return_list
self._display()
self.set_list(self.tag_list)

View File

@ -0,0 +1,133 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2010 Nick Hall
#
# 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$
"""
Tag editing module for Gramps.
"""
#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
import locale
#-------------------------------------------------------------------------
#
# GNOME modules
#
#-------------------------------------------------------------------------
import gtk
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from gen.ggettext import sgettext as _
import ManagedWindow
import const
import GrampsDisplay
from ListModel import ListModel, TOGGLE
#-------------------------------------------------------------------------
#
# Constants
#
#-------------------------------------------------------------------------
WIKI_HELP_PAGE = '%s_-_Entering_and_Editing_Data:_Detailed_-_part_3' % \
const.URL_MANUAL_PAGE
WIKI_HELP_SEC = _('manual|Tags')
#-------------------------------------------------------------------------
#
# TagEditor
#
#-------------------------------------------------------------------------
class TagEditor(ManagedWindow.ManagedWindow):
"""
Dialog to allow the user to edit a list of tags.
"""
def __init__(self, tag_list, full_list, uistate, track):
"""
Initiate and display the dialog.
"""
ManagedWindow.ManagedWindow.__init__(self, uistate, track, self)
self.namemodel = None
top = self._create_dialog()
self.set_window(top, None, _('Tag selection'))
for tag_name in sorted(full_list, key=locale.strxfrm):
self.namemodel.add([tag_name, tag_name in tag_list])
self.namemodel.connect_model()
# The dialog is modal. We don't want to have several open dialogs of
# this type, since then the user will loose track of which is which.
self.return_list = None
self.show()
while True:
response = self.window.run()
if response == gtk.RESPONSE_HELP:
GrampsDisplay.help(webpage=WIKI_HELP_PAGE,
section=WIKI_HELP_SEC)
elif response == gtk.RESPONSE_DELETE_EVENT:
break
else:
if response == gtk.RESPONSE_OK:
self.return_list = [row[0] for row in self.namemodel.model
if row[1]]
self.close()
break
def _create_dialog(self):
"""
Create a dialog box to select tags.
"""
# pylint: disable-msg=E1101
title = _("%(title)s - Gramps") % {'title': _("Edit Tags")}
top = gtk.Dialog(title)
top.set_default_size(340, 400)
top.set_modal(True)
top.set_has_separator(False)
top.vbox.set_spacing(5)
columns = [(_('Tag'), -1, 300),
(_(' '), -1, 25, TOGGLE, True, None)]
view = gtk.TreeView()
self.namemodel = ListModel(view, columns)
slist = gtk.ScrolledWindow()
slist.add_with_viewport(view)
slist.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
top.vbox.pack_start(slist, 1, 1, 5)
top.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
top.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
top.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
top.show_all()
return top
def build_menu_names(self, obj):
"""
Define the menu entry for the ManagedWindows.
"""
return (_("Tag selection"), None)

View File

@ -47,6 +47,8 @@ dist_pkgdata_DATA = \
gramps-repository.png \ gramps-repository.png \
gramps-source.png \ gramps-source.png \
gramps-spouse.png \ gramps-spouse.png \
gramps-tag.png \
gramps-tag-new.png \
gramps-tools.png \ gramps-tools.png \
gramps-tree-group.png \ gramps-tree-group.png \
gramps-tree-list.png \ gramps-tree-list.png \

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

View File

@ -47,6 +47,8 @@ dist_pkgdata_DATA = \
gramps-repository.png \ gramps-repository.png \
gramps-source.png \ gramps-source.png \
gramps-spouse.png \ gramps-spouse.png \
gramps-tag.png \
gramps-tag-new.png \
gramps-tools.png \ gramps-tools.png \
gramps-tree-group.png \ gramps-tree-group.png \
gramps-tree-list.png \ gramps-tree-list.png \

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

View File

@ -47,6 +47,8 @@ dist_pkgdata_DATA = \
gramps-repository.png \ gramps-repository.png \
gramps-source.png \ gramps-source.png \
gramps-spouse.png \ gramps-spouse.png \
gramps-tag.png \
gramps-tag-new.png \
gramps-tools.png \ gramps-tools.png \
gramps-tree-group.png \ gramps-tree-group.png \
gramps-tree-list.png \ gramps-tree-list.png \

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -47,6 +47,8 @@ dist_pkgdata_DATA = \
gramps-repository.svg \ gramps-repository.svg \
gramps-source.svg \ gramps-source.svg \
gramps-spouse.svg \ gramps-spouse.svg \
gramps-tag.svg \
gramps-tag-new.svg \
gramps-tools.svg \ gramps-tools.svg \
gramps-tree-group.svg \ gramps-tree-group.svg \
gramps-tree-list.svg \ gramps-tree-list.svg \

View File

@ -0,0 +1,269 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
id="svg4542"
sodipodi:version="0.32"
inkscape:version="0.45+devel"
sodipodi:docbase="/home/jimmac/gfx/icons/application-icons/tomboy/22x22"
sodipodi:docname="f-spot-new-tag-22.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.0">
<defs
id="defs4544">
<linearGradient
id="linearGradient8668">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop8670" />
<stop
id="stop8676"
offset="0.5"
style="stop-color:#edd400;stop-opacity:1;" />
<stop
style="stop-color:#edd400;stop-opacity:0;"
offset="1"
id="stop8672" />
</linearGradient>
<linearGradient
id="linearGradient5154">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5156" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1;"
offset="1"
id="stop5158" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5146">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5148" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5150" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5135">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5137" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5139" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5135"
id="radialGradient5141"
cx="7.9991455"
cy="6.3656702"
fx="7.9991455"
fy="6.3656702"
r="3.019068"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5146"
id="linearGradient5152"
x1="9.1521292"
y1="15"
x2="8.0315027"
y2="0.062499985"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3320387,0,0,1.2877478,-0.1982722,0.8276555)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5154"
id="radialGradient5160"
cx="8.0300226"
cy="11.565026"
fx="8.0300226"
fy="11.565026"
r="6.0042586"
gradientTransform="matrix(1.3188775,0,0,1.7490954,-5.4317078e-2,-3.3022448)"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:collect="always"
x="-0.11192513"
width="1.2238503"
y="-0.50945233"
height="2.0189047"
id="filter5242">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.76948529"
id="feGaussianBlur5244" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient8668"
id="radialGradient8674"
cx="16.528622"
cy="4.3223305"
fx="16.528622"
fy="4.3223305"
r="5.2149124"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#ebebeb"
borderopacity="1"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="13.72296"
inkscape:cy="14.108264"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:window-width="820"
inkscape:window-height="764"
inkscape:window-x="730"
inkscape:window-y="408"
width="22px"
height="22px">
<inkscape:grid
type="xygrid"
id="grid8089" />
</sodipodi:namedview>
<metadata
id="metadata4547">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<rect
style="opacity:0.35882353;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;filter:url(#filter5242);enable-background:accumulate"
id="rect5212"
width="16.5"
height="3.625"
x="2.25"
y="17.25"
rx="1.8125"
ry="1.8125" />
<path
style="color:#000000;fill:url(#radialGradient5160);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 8.0075554,1.5000049 C 7.9261297,1.5000049 7.1704875,1.5648405 6.6633357,2.2083456 C 5.8189525,3.2797502 4.6237633,4.9273413 3.8958247,6.1042196 C 3.4977305,6.7478297 3.5795378,7.3012987 3.5795377,7.4815488 L 3.5795377,19.129819 C 3.5795377,19.837629 4.1810296,20.389091 4.9237574,20.389091 L 16.151945,20.389091 C 16.894673,20.389091 17.496165,19.837629 17.496165,19.129819 L 17.496165,7.4815488 C 17.496165,7.3065686 17.558573,6.7602173 17.179878,6.1435719 C 16.420161,4.9064935 15.235183,3.1290493 14.372831,2.0509366 C 13.926232,1.4926004 13.221994,1.500005 13.147219,1.5000049 L 8.0075554,1.5000049 z M 10.537851,3.4880253 C 11.646305,3.4880253 12.552287,4.3897989 12.552287,5.493106 C 12.552287,6.5964131 11.646306,7.4981868 10.537851,7.4981868 C 9.4293957,7.4981865 8.5234152,6.5964132 8.5234152,5.493106 C 8.5234153,4.3897989 9.4293962,3.4880253 10.537851,3.4880253 z"
id="rect4550"
sodipodi:nodetypes="cssccccccsscccsssc" />
<path
sodipodi:type="arc"
style="opacity:1;color:#000000;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient5141);stroke-width:0.88136292;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path5133"
sodipodi:cx="7.9991455"
sodipodi:cy="3.8907964"
sodipodi:rx="2.519068"
sodipodi:ry="2.519068"
d="M 10.518214,3.8907964 A 2.519068,2.519068 0 1 1 5.4800775,3.8907964 A 2.519068,2.519068 0 1 1 10.518214,3.8907964 z"
transform="matrix(1.1372503,0,0,1.1319689,1.4002041,1.2894864)" />
<path
style="opacity:0.48235294;color:#000000;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5152);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 7.8355863,2.7592772 C 8.1479175,2.7592772 7.858081,2.7617019 7.7523339,2.7995194 C 7.6465867,2.8373367 7.5430527,2.8995475 7.4609504,3.00073 C 6.6044824,4.0562358 5.3465131,5.7394375 4.6303682,6.8639734 C 4.5315216,7.0191886 4.5178409,7.1273694 4.5054895,7.2261524 C 4.4931381,7.3249356 4.5054899,7.3022556 4.5054895,7.5883316 L 4.5054895,19.499999 C 4.5054895,19.497658 4.4691641,19.499999 4.588742,19.499999 L 16.410586,19.499999 C 16.530165,19.499999 16.493838,19.497658 16.493838,19.499999 L 16.493838,7.5883316 C 16.493838,7.3464779 16.507701,7.3260914 16.493838,7.2261524 C 16.479974,7.1262134 16.460034,7.0482525 16.368959,6.9042155 C 15.594416,5.6792417 14.330701,3.8524027 13.49675,2.8397614 C 13.455128,2.7892186 13.414069,2.7592772 13.246994,2.7592772 L 10.499664,2.7592772 L 7.8355863,2.7592772 z"
id="path5143"
sodipodi:nodetypes="cssssccccccsssccc" />
<rect
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect5162"
width="0.99999976"
height="8"
x="6"
y="10"
rx="0.5"
ry="0.5" />
<rect
ry="0.5"
rx="0.5"
y="12"
x="8"
height="6"
width="0.99999976"
id="rect5204"
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect5206"
width="0.99999976"
height="8"
x="10"
y="10"
rx="0.5"
ry="0.5" />
<rect
ry="0.5"
rx="0.5"
y="11"
x="12"
height="7"
width="0.99999976"
id="rect5208"
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect5210"
width="0.99999976"
height="6"
x="14"
y="12"
rx="0.5"
ry="0.5" />
<path
sodipodi:type="arc"
style="opacity:1;color:#000000;fill:url(#radialGradient8674);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path8087"
sodipodi:cx="16.528622"
sodipodi:cy="4.3223305"
sodipodi:rx="5.2149124"
sodipodi:ry="5.2149124"
d="M 21.743534,4.3223305 A 5.2149124,5.2149124 0 1 1 11.313709,4.3223305 A 5.2149124,5.2149124 0 1 1 21.743534,4.3223305 z"
transform="matrix(1.0423729,0,0,1.0423729,-0.7445595,0.9217048)" />
<path
sodipodi:type="star"
style="opacity:1;color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path8678"
sodipodi:sides="5"
sodipodi:cx="16.970564"
sodipodi:cy="7.0623693"
sodipodi:r1="4.6359005"
sodipodi:r2="2.7623203"
sodipodi:arg1="0.96157066"
sodipodi:arg2="1.5898892"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 19.623377,10.864231 L 16.917826,9.8241862 L 14.174543,10.760185 L 14.327623,7.8656614 L 12.589714,5.545883 L 15.389874,4.7970143 L 17.059071,2.4273138 L 18.636585,4.8590109 L 21.406114,5.7142338 L 19.580911,7.9659739 L 19.623377,10.864231 z"
transform="translate(-0.5303301,-1.4273138)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,256 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
id="svg4542"
sodipodi:version="0.32"
inkscape:version="0.46+devel"
sodipodi:docbase="/home/jimmac/gfx/icons/application-icons/tomboy/22x22"
sodipodi:docname="tag-new-22.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.0"
inkscape:export-filename="/home/nx/Desktop/tag-22.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4544">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 11 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="22 : 11 : 1"
inkscape:persp3d-origin="11 : 7.3333333 : 1"
id="perspective38" />
<linearGradient
id="linearGradient8668">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop8670" />
<stop
id="stop8676"
offset="0.5"
style="stop-color:#edd400;stop-opacity:1;" />
<stop
style="stop-color:#edd400;stop-opacity:0;"
offset="1"
id="stop8672" />
</linearGradient>
<linearGradient
id="linearGradient5154">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5156" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1;"
offset="1"
id="stop5158" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5146">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5148" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5150" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5135">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5137" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5139" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5135"
id="radialGradient5141"
cx="7.9991455"
cy="6.3656702"
fx="7.9991455"
fy="6.3656702"
r="3.019068"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5146"
id="linearGradient5152"
x1="9.1521292"
y1="15"
x2="8.0315027"
y2="0.062499985"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3320387,0,0,1.2877478,-0.1982722,0.8276555)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5154"
id="radialGradient5160"
cx="8.0300226"
cy="11.565026"
fx="8.0300226"
fy="11.565026"
r="6.0042586"
gradientTransform="matrix(1.3188775,0,0,1.7490954,-5.4317078e-2,-3.3022448)"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:collect="always"
x="-0.11192513"
width="1.2238503"
y="-0.50945233"
height="2.0189047"
id="filter5242">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.76948529"
id="feGaussianBlur5244" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient8668"
id="radialGradient8674"
cx="16.528622"
cy="4.3223305"
fx="16.528622"
fy="4.3223305"
r="5.2149124"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#ebebeb"
borderopacity="1"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-14.069783"
inkscape:cy="11.175821"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
borderlayer="true"
inkscape:showpageshadow="false"
inkscape:window-width="820"
inkscape:window-height="764"
inkscape:window-x="730"
inkscape:window-y="408"
width="22px"
height="22px">
<inkscape:grid
type="xygrid"
id="grid8089"
empspacing="5"
visible="true"
enabled="true" />
</sodipodi:namedview>
<metadata
id="metadata4547">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<rect
style="opacity:0.35882353;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;filter:url(#filter5242);enable-background:accumulate"
id="rect5212"
width="16.5"
height="3.625"
x="2.25"
y="17.25"
rx="1.8125"
ry="1.8125" />
<path
style="color:#000000;fill:url(#radialGradient5160);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 8.0075554,1.5000049 C 7.9261297,1.5000049 7.1704875,1.5648405 6.6633357,2.2083456 C 5.8189525,3.2797502 4.6237633,4.9273413 3.8958247,6.1042196 C 3.4977305,6.7478297 3.5795378,7.3012987 3.5795377,7.4815488 L 3.5795377,19.129819 C 3.5795377,19.837629 4.1810296,20.389091 4.9237574,20.389091 L 16.151945,20.389091 C 16.894673,20.389091 17.496165,19.837629 17.496165,19.129819 L 17.496165,7.4815488 C 17.496165,7.3065686 17.558573,6.7602173 17.179878,6.1435719 C 16.420161,4.9064935 15.235183,3.1290493 14.372831,2.0509366 C 13.926232,1.4926004 13.221994,1.500005 13.147219,1.5000049 L 8.0075554,1.5000049 z M 10.537851,3.4880253 C 11.646305,3.4880253 12.552287,4.3897989 12.552287,5.493106 C 12.552287,6.5964131 11.646306,7.4981868 10.537851,7.4981868 C 9.4293957,7.4981865 8.5234152,6.5964132 8.5234152,5.493106 C 8.5234153,4.3897989 9.4293962,3.4880253 10.537851,3.4880253 z"
id="rect4550"
sodipodi:nodetypes="cssccccccsscccsssc" />
<path
sodipodi:type="arc"
style="opacity:1;color:#000000;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient5141);stroke-width:0.88136292;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path5133"
sodipodi:cx="7.9991455"
sodipodi:cy="3.8907964"
sodipodi:rx="2.519068"
sodipodi:ry="2.519068"
d="M 10.518214,3.8907964 A 2.519068,2.519068 0 1 1 5.4800775,3.8907964 A 2.519068,2.519068 0 1 1 10.518214,3.8907964 z"
transform="matrix(1.1372503,0,0,1.1319689,1.4002041,1.2894864)" />
<path
style="opacity:0.48235294;color:#000000;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5152);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 7.8355863,2.7592772 C 8.1479175,2.7592772 7.858081,2.7617019 7.7523339,2.7995194 C 7.6465867,2.8373367 7.5430527,2.8995475 7.4609504,3.00073 C 6.6044824,4.0562358 5.3465131,5.7394375 4.6303682,6.8639734 C 4.5315216,7.0191886 4.5178409,7.1273694 4.5054895,7.2261524 C 4.4931381,7.3249356 4.5054899,7.3022556 4.5054895,7.5883316 L 4.5054895,19.499999 C 4.5054895,19.497658 4.4691641,19.499999 4.588742,19.499999 L 16.410586,19.499999 C 16.530165,19.499999 16.493838,19.497658 16.493838,19.499999 L 16.493838,7.5883316 C 16.493838,7.3464779 16.507701,7.3260914 16.493838,7.2261524 C 16.479974,7.1262134 16.460034,7.0482525 16.368959,6.9042155 C 15.594416,5.6792417 14.330701,3.8524027 13.49675,2.8397614 C 13.455128,2.7892186 13.414069,2.7592772 13.246994,2.7592772 L 10.499664,2.7592772 L 7.8355863,2.7592772 z"
id="path5143"
sodipodi:nodetypes="cssssccccccsssccc" />
<rect
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect5162"
width="0.99999976"
height="8"
x="6"
y="10"
rx="0.5"
ry="0.5" />
<rect
ry="0.5"
rx="0.5"
y="12"
x="8"
height="6"
width="0.99999976"
id="rect5204"
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect5206"
width="0.99999976"
height="8"
x="10"
y="10"
rx="0.5"
ry="0.5" />
<rect
ry="0.5"
rx="0.5"
y="11"
x="12"
height="7"
width="0.99999976"
id="rect5208"
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect5210"
width="0.99999976"
height="6"
x="14"
y="12"
rx="0.5"
ry="0.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -58,13 +58,15 @@ from DdTargets import DdTargets
from gui.editors import EditPerson from gui.editors import EditPerson
from Filters.SideBar import PersonSidebarFilter from Filters.SideBar import PersonSidebarFilter
from gen.plug import CATEGORY_QR_PERSON from gen.plug import CATEGORY_QR_PERSON
import gui.widgets.progressdialog as progressdlg
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
# internationalization # Python modules
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
from gen.ggettext import sgettext as _ from gen.ggettext import sgettext as _
from bisect import insort_left
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -83,7 +85,8 @@ class BasePersonView(ListView):
COL_DDAT = 5 COL_DDAT = 5
COL_DPLAC = 6 COL_DPLAC = 6
COL_SPOUSE = 7 COL_SPOUSE = 7
COL_CHAN = 8 COL_TAGS = 8
COL_CHAN = 9
#name of the columns #name of the columns
COLUMN_NAMES = [ COLUMN_NAMES = [
_('Name'), _('Name'),
@ -94,6 +97,7 @@ class BasePersonView(ListView):
_('Death Date'), _('Death Date'),
_('Death Place'), _('Death Place'),
_('Spouse'), _('Spouse'),
_('Tags'),
_('Last Changed'), _('Last Changed'),
] ]
# columns that contain markup # columns that contain markup
@ -102,8 +106,8 @@ class BasePersonView(ListView):
CONFIGSETTINGS = ( CONFIGSETTINGS = (
('columns.visible', [COL_NAME, COL_ID, COL_GEN, COL_BDAT, COL_DDAT]), ('columns.visible', [COL_NAME, COL_ID, COL_GEN, COL_BDAT, COL_DDAT]),
('columns.rank', [COL_NAME, COL_ID, COL_GEN, COL_BDAT, COL_BPLAC, ('columns.rank', [COL_NAME, COL_ID, COL_GEN, COL_BDAT, COL_BPLAC,
COL_DDAT, COL_DPLAC, COL_SPOUSE, COL_CHAN]), COL_DDAT, COL_DPLAC, COL_SPOUSE, COL_TAGS, COL_CHAN]),
('columns.size', [250, 75, 75, 100, 175, 100, 175, 100, 100]) ('columns.size', [250, 75, 75, 100, 175, 100, 175, 100, 100, 100])
) )
ADD_MSG = _("Add a new person") ADD_MSG = _("Add a new person")
EDIT_MSG = _("Edit the selected person") EDIT_MSG = _("Edit the selected person")
@ -121,6 +125,7 @@ class BasePersonView(ListView):
'person-delete' : self.row_delete, 'person-delete' : self.row_delete,
'person-rebuild' : self.object_build, 'person-rebuild' : self.object_build,
'person-groupname-rebuild' : self.object_build, 'person-groupname-rebuild' : self.object_build,
'tag-update' : self.tag_updated
} }
ListView.__init__( ListView.__init__(
@ -360,6 +365,20 @@ class BasePersonView(ListView):
self.all_action.set_visible(False) self.all_action.set_visible(False)
self.edit_action.set_visible(False) self.edit_action.set_visible(False)
def set_active(self):
"""
Called when the page is displayed.
"""
ListView.set_active(self)
self.uistate.viewmanager.tags.tag_enable()
def set_inactive(self):
"""
Called when the page is no longer displayed.
"""
ListView.set_inactive(self)
self.uistate.viewmanager.tags.tag_disable()
def merge(self, obj): def merge(self, obj):
""" """
Merge the selected people. Merge the selected people.
@ -375,3 +394,57 @@ class BasePersonView(ListView):
else: else:
import Merge import Merge
Merge.MergePeople(self.dbstate, self.uistate, mlist[0], mlist[1]) Merge.MergePeople(self.dbstate, self.uistate, mlist[0], mlist[1])
def tag_updated(self, tag_name, tag_color):
"""
Update tagged rows when a tag color changes.
"""
self.model.update_tag(tag_name, tag_color)
if not self.active:
return
items = self.dbstate.db.get_number_of_people()
pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog,
popup_time=2)
status = progressdlg.LongOpStatus(msg=_("Updating View"),
total_steps=items, interval=items//20,
can_cancel=True)
pmon.add_op(status)
for handle in self.dbstate.db.get_person_handles():
person = self.dbstate.db.get_person_from_handle(handle)
status.heartbeat()
if status.should_cancel():
break
tags = person.get_tag_list()
if len(tags) > 0 and tags[0] == tag_name:
self.row_update([handle])
if not status.was_cancelled():
status.end()
def add_tag(self, tag):
"""
Add the given tag to the selected objects.
"""
selected = self.selected_handles()
items = len(selected)
pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog,
popup_time=2)
status = progressdlg.LongOpStatus(msg=_("Adding Tags"),
total_steps=items,
interval=items//20,
can_cancel=True)
pmon.add_op(status)
trans = self.dbstate.db.transaction_begin()
for handle in selected:
status.heartbeat()
if status.should_cancel():
break
person = self.dbstate.db.get_person_from_handle(handle)
tags = person.get_tag_list()
if tag not in tags:
insort_left(tags, tag)
person.set_tag_list(tags)
self.dbstate.db.commit_person(person, trans)
if not status.was_cancelled():
msg = _('Tag people with %s') % tag
self.dbstate.db.transaction_commit(trans, msg)
status.end()