From bf8146b9d118016c4a9e45f0cc7bdd126e45aa08 Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Sun, 29 Aug 2010 18:36:42 +0000 Subject: [PATCH] GEPS 011: Tagging svn: r15830 --- src/Filters/Rules/Makefile.am | 1 + src/Filters/Rules/Person/Makefile.am | 1 + src/Filters/Rules/Person/_HasTag.py | 50 +++ src/Filters/Rules/Person/__init__.py | 2 + src/Filters/Rules/_HasTagBase.py | 60 +++ src/Filters/SideBar/_PersonSidebarFilter.py | 51 ++- src/Filters/SideBar/_SidebarFilter.py | 23 + src/gen/db/base.py | 32 ++ src/gen/db/read.py | 25 ++ src/gen/db/upgrade.py | 20 + src/gen/db/write.py | 35 +- src/gen/lib/Makefile.am | 1 + src/gen/lib/person.py | 9 +- src/gen/lib/tagbase.py | 107 +++++ src/glade/editperson.glade | 56 ++- src/gui/editors/editperson.py | 16 +- src/gui/grampsgui.py | 2 + src/gui/viewmanager.py | 61 +-- src/gui/views/Makefile.am | 3 +- src/gui/views/tags.py | 440 ++++++++++++++++++++ src/gui/views/treemodels/peoplemodel.py | 50 ++- src/gui/widgets/Makefile.am | 1 + src/gui/widgets/monitoredwidgets.py | 56 ++- src/gui/widgets/tageditor.py | 133 ++++++ src/images/16x16/Makefile.am | 2 + src/images/16x16/gramps-tag-new.png | Bin 0 -> 747 bytes src/images/16x16/gramps-tag.png | Bin 0 -> 681 bytes src/images/22x22/Makefile.am | 2 + src/images/22x22/gramps-tag-new.png | Bin 0 -> 1096 bytes src/images/22x22/gramps-tag.png | Bin 0 -> 967 bytes src/images/48x48/Makefile.am | 2 + src/images/48x48/gramps-tag-new.png | Bin 0 -> 3060 bytes src/images/48x48/gramps-tag.png | Bin 0 -> 2612 bytes src/images/scalable/Makefile.am | 2 + src/images/scalable/gramps-tag-new.svg | 269 ++++++++++++ src/images/scalable/gramps-tag.svg | 256 ++++++++++++ src/plugins/lib/libpersonview.py | 81 +++- 37 files changed, 1785 insertions(+), 64 deletions(-) create mode 100644 src/Filters/Rules/Person/_HasTag.py create mode 100644 src/Filters/Rules/_HasTagBase.py create mode 100644 src/gen/lib/tagbase.py create mode 100644 src/gui/views/tags.py create mode 100644 src/gui/widgets/tageditor.py create mode 100644 src/images/16x16/gramps-tag-new.png create mode 100644 src/images/16x16/gramps-tag.png create mode 100644 src/images/22x22/gramps-tag-new.png create mode 100644 src/images/22x22/gramps-tag.png create mode 100644 src/images/48x48/gramps-tag-new.png create mode 100644 src/images/48x48/gramps-tag.png create mode 100644 src/images/scalable/gramps-tag-new.svg create mode 100644 src/images/scalable/gramps-tag.svg diff --git a/src/Filters/Rules/Makefile.am b/src/Filters/Rules/Makefile.am index 037a1c32a..2e7f0e881 100644 --- a/src/Filters/Rules/Makefile.am +++ b/src/Filters/Rules/Makefile.am @@ -17,6 +17,7 @@ pkgdata_PYTHON = \ _HasNoteSubstrBase.py\ _HasReferenceCountBase.py \ _HasSourceBase.py \ + _HasTagBase.py \ _HasTextMatchingRegexpOf.py\ _HasTextMatchingSubstringOf.py\ __init__.py\ diff --git a/src/Filters/Rules/Person/Makefile.am b/src/Filters/Rules/Person/Makefile.am index 71df47f48..92dc02698 100644 --- a/src/Filters/Rules/Person/Makefile.am +++ b/src/Filters/Rules/Person/Makefile.am @@ -28,6 +28,7 @@ pkgdata_PYTHON = \ _HasRelationship.py \ _HasSource.py \ _HasSourceOf.py \ + _HasTag.py \ _HasTextMatchingRegexpOf.py \ _HasTextMatchingSubstringOf.py \ _HasUnknownGender.py \ diff --git a/src/Filters/Rules/Person/_HasTag.py b/src/Filters/Rules/Person/_HasTag.py new file mode 100644 index 000000000..58c2e627a --- /dev/null +++ b/src/Filters/Rules/Person/_HasTag.py @@ -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 ') + description = _("Matches people with the particular tag") diff --git a/src/Filters/Rules/Person/__init__.py b/src/Filters/Rules/Person/__init__.py index 92a612deb..6a8b81383 100644 --- a/src/Filters/Rules/Person/__init__.py +++ b/src/Filters/Rules/Person/__init__.py @@ -49,6 +49,7 @@ from _HasNoteRegexp import HasNoteRegexp from _HasRelationship import HasRelationship from _HasSource import HasSource from _HasSourceOf import HasSourceOf +from _HasTag import HasTag from _HasTextMatchingRegexpOf import HasTextMatchingRegexpOf from _HasTextMatchingSubstringOf import HasTextMatchingSubstringOf from _HasUnknownGender import HasUnknownGender @@ -126,6 +127,7 @@ editor_rule_list = [ HasFamilyEvent, HasAttribute, HasFamilyAttribute, + HasTag, HasSource, HasSourceOf, HasMarkerOf, diff --git a/src/Filters/Rules/_HasTagBase.py b/src/Filters/Rules/_HasTagBase.py new file mode 100644 index 000000000..f7321e401 --- /dev/null +++ b/src/Filters/Rules/_HasTagBase.py @@ -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 ') + 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() diff --git a/src/Filters/SideBar/_PersonSidebarFilter.py b/src/Filters/SideBar/_PersonSidebarFilter.py index ebc768e2e..65744291d 100644 --- a/src/Filters/SideBar/_PersonSidebarFilter.py +++ b/src/Filters/SideBar/_PersonSidebarFilter.py @@ -26,6 +26,7 @@ # #------------------------------------------------------------------------- from gen.ggettext import gettext as _ +import locale #------------------------------------------------------------------------- # @@ -45,8 +46,8 @@ import DateHandler from Filters.SideBar import SidebarFilter from Filters.Rules.Person import (RegExpName, SearchName, RegExpIdOf, - MatchIdOf, IsMale, IsFemale, - HasUnknownGender, HasMarkerOf, HasEvent, + MatchIdOf, IsMale, IsFemale, HasUnknownGender, + HasMarkerOf, HasEvent, HasTag, HasBirth, HasDeath, HasNoteRegexp, HasNoteMatchingSubstringOf, MatchesFilter) from Filters import GenericFilter, build_filter_model, Rules @@ -91,6 +92,8 @@ class PersonSidebarFilter(SidebarFilter): self.filter_marker.set_marker, self.filter_marker.get_marker) + self.tag = gtk.ComboBox() + self.filter_note = gtk.Entry() self.filter_gender = gtk.combo_box_new_text() map(self.filter_gender.append_text, @@ -103,6 +106,8 @@ class PersonSidebarFilter(SidebarFilter): SidebarFilter.__init__(self, dbstate, uistate, "Person") + self.update_tag_list() + def create_widget(self): cell = gtk.CellRendererText() cell.set_property('width', self._FILTER_WIDTH) @@ -111,6 +116,12 @@ class PersonSidebarFilter(SidebarFilter): self.generic.add_attribute(cell, 'text', 0) 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() exdate2 = gen.lib.Date() 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)) self.add_entry(_('Event'), self.etype) self.add_entry(_('Marker'), self.mtype) + self.add_entry(_('Tag'), self.tag) self.add_text_entry(_('Note'), self.filter_note) self.add_filter_entry(_('Custom filter'), self.generic) self.add_entry(None, self.filter_regex) @@ -144,6 +156,7 @@ class PersonSidebarFilter(SidebarFilter): self.filter_gender.set_active(0) self.etype.child.set_text(u'') self.mtype.child.set_text(u'') + self.tag.set_active(0) self.generic.set_active(0) def get_filter(self): @@ -165,12 +178,13 @@ class PersonSidebarFilter(SidebarFilter): gender = self.filter_gender.get_active() regex = self.filter_regex.get_active() 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 # we don't build a filter 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: generic_filter = None else: @@ -209,6 +223,14 @@ class PersonSidebarFilter(SidebarFilter): rule = HasMarkerOf([mtype]) 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 if etype: rule = HasEvent([etype, u'', u'', u'']) @@ -251,3 +273,26 @@ class PersonSidebarFilter(SidebarFilter): all_filter.add_rule(Rules.Person.Everyone([])) self.generic.set_model(build_filter_model('Person', [all_filter])) 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) diff --git a/src/Filters/SideBar/_SidebarFilter.py b/src/Filters/SideBar/_SidebarFilter.py index cff573e40..f421550cb 100644 --- a/src/Filters/SideBar/_SidebarFilter.py +++ b/src/Filters/SideBar/_SidebarFilter.py @@ -46,6 +46,7 @@ class SidebarFilter(object): self._init_interface() uistate.connect('filters-changed', self.on_filters_changed) + dbstate.connect('database-changed', self._db_changed) self.uistate = uistate self.dbstate = dbstate self.namespace = namespace @@ -137,6 +138,28 @@ class SidebarFilter(object): self.position += 1 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 def add_filter_entry(self, text, widget): diff --git a/src/gen/db/base.py b/src/gen/db/base.py index 884145b8e..9e407f17b 100644 --- a/src/gen/db/base.py +++ b/src/gen/db/base.py @@ -765,6 +765,30 @@ class DbReadBase(object): """ 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): """ Return True if the handle exists in the current Note database. @@ -1386,6 +1410,14 @@ class DbWriteBase(object): """ 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): """ Sort the list of surnames contained in the database by locale ordering. diff --git a/src/gen/db/read.py b/src/gen/db/read.py index 7769b3f67..e9ffb141c 100644 --- a/src/gen/db/read.py +++ b/src/gen/db/read.py @@ -291,6 +291,7 @@ class DbBsddbRead(DbReadBase, Callback): self.event_map = {} self.metadata = {} self.name_group = {} + self.tags = {} self.undo_callback = None self.redo_callback = None self.undo_history_callback = None @@ -697,6 +698,30 @@ class DbBsddbRead(DbReadBase, Callback): """ 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): if not self.db_is_open: return 0 diff --git a/src/gen/db/upgrade.py b/src/gen/db/upgrade.py index 78bef4ffd..273d6abfd 100644 --- a/src/gen/db/upgrade.py +++ b/src/gen/db/upgrade.py @@ -26,6 +26,26 @@ from gen.db import BSDDBTxn 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): """Upgrade database from version 13 to 14.""" # This upgrade modifies notes and dates diff --git a/src/gen/db/write.py b/src/gen/db/write.py index e3086bf80..294496f5d 100644 --- a/src/gen/db/write.py +++ b/src/gen/db/write.py @@ -61,7 +61,7 @@ import Errors _LOG = logging.getLogger(DBLOGNAME) _MINVERSION = 9 -_DBVERSION = 14 +_DBVERSION = 15 IDTRANS = "person_id" FIDTRANS = "family_id" @@ -73,6 +73,7 @@ NIDTRANS = "note_id" SIDTRANS = "source_id" SURNAMES = "surnames" NAME_GROUP = "name_group" +TAGS = "tags" META = "meta_data" FAMILY_TBL = "family" @@ -197,6 +198,10 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): # 4. Signal for change in person group name, parameters are __signals__['person-groupname-rebuild'] = (unicode, unicode) + # 5. Signals for change ins tags + __signals__['tags-changed'] = None + __signals__['tag-update'] = (str, str) + def __init__(self): """Create a new GrampsDB.""" @@ -463,6 +468,9 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.name_group = self.__open_db(self.full_name, NAME_GROUP, 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. # If secondary indices change, then they should removed # or rebuilt by upgrade as well. In any case, the @@ -1006,6 +1014,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.__close_metadata() self.name_group.close() + self.tags.close() self.surnames.close() self.id_trans.close() self.fid_trans.close() @@ -1042,7 +1051,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.media_map = None self.event_map = None self.surnames = None - self.name_group = None + self.tags = None self.env = None self.metadata = None self.db_is_open = False @@ -1280,6 +1289,24 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): grouppar = group 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): self.surname_list.sort(key=locale.strxfrm) @@ -1652,9 +1679,11 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): t = time.time() + import upgrade if version < 14: - import upgrade upgrade.gramps_upgrade_14(self) + if version < 15: + upgrade.gramps_upgrade_15(self) print "Upgrade time:", int(time.time()-t), "seconds" diff --git a/src/gen/lib/Makefile.am b/src/gen/lib/Makefile.am index d8d51074b..4bacb685c 100644 --- a/src/gen/lib/Makefile.am +++ b/src/gen/lib/Makefile.am @@ -60,6 +60,7 @@ pkgdata_PYTHON = \ styledtext.py \ styledtexttag.py \ styledtexttagtype.py \ + tagbase.py \ urlbase.py \ url.py \ urltype.py \ diff --git a/src/gen/lib/person.py b/src/gen/lib/person.py index 301367ab9..680bef38e 100644 --- a/src/gen/lib/person.py +++ b/src/gen/lib/person.py @@ -38,6 +38,7 @@ from gen.lib.attrbase import AttributeBase from gen.lib.addressbase import AddressBase from gen.lib.ldsordbase import LdsOrdBase from gen.lib.urlbase import UrlBase +from gen.lib.tagbase import TagBase from gen.lib.name import Name from gen.lib.eventref import EventRef from gen.lib.personref import PersonRef @@ -53,7 +54,7 @@ from gen.lib.const import IDENTICAL, EQUAL, DIFFERENT # #------------------------------------------------------------------------- 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 individual person. It contains all the information related to @@ -91,6 +92,7 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, AddressBase.__init__(self) UrlBase.__init__(self) LdsOrdBase.__init__(self) + TagBase.__init__(self) self.primary_name = Name() self.marker = MarkerType() self.event_ref_list = [] @@ -153,7 +155,8 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, self.change, # 17 self.marker.serialize(), # 18 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): @@ -186,6 +189,7 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, marker, # 18 self.private, # 19 person_ref_list, # 20 + tag_list, # 21 ) = data self.marker = MarkerType() @@ -205,6 +209,7 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, UrlBase.unserialize(self, urls) SourceBase.unserialize(self, source_list) NoteBase.unserialize(self, note_list) + TagBase.unserialize(self, tag_list) return self def _has_handle_reference(self, classname, handle): diff --git a/src/gen/lib/tagbase.py b/src/gen/lib/tagbase.py new file mode 100644 index 000000000..c15613eac --- /dev/null +++ b/src/gen/lib/tagbase.py @@ -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 diff --git a/src/glade/editperson.glade b/src/glade/editperson.glade index e86b2a081..c078b60a6 100644 --- a/src/glade/editperson.glade +++ b/src/glade/editperson.glade @@ -17,7 +17,7 @@ True - 6 + 7 6 12 6 @@ -490,6 +490,60 @@ Title: A title used to refer to the person, such as 'Dr.' or 'Rev.' GTK_FILL + + + True + 0 + _Tags: + True + + + 1 + 2 + 6 + 7 + + + + + True + + + True + + + 0 + + + + + True + True + True + + + True + gramps-tag + + + + + False + False + 1 + + + + + 2 + 6 + 6 + 7 + + + + + diff --git a/src/gui/editors/editperson.py b/src/gui/editors/editperson.py index 70b40f981..de08b8da1 100644 --- a/src/gui/editors/editperson.py +++ b/src/gui/editors/editperson.py @@ -31,7 +31,6 @@ to edit information about a particular Person. # Standard python modules # #------------------------------------------------------------------------- -import locale from gen.ggettext import sgettext as _ #------------------------------------------------------------------------- @@ -306,6 +305,15 @@ class EditPerson(EditPrimary): self.db.readonly, 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.top.get_object("gid"), self.obj.set_gramps_id, @@ -724,9 +732,9 @@ class EditPerson(EditPrimary): name = self.name_displayer.display(prim_object) msg1 = _("Cannot save person. ID already exists.") msg2 = _("You have attempted to use the existing Gramps ID with " - "value %(id)s. This value is already used by '" - "%(prim_object)s'. Please enter a different ID or leave " - "blank to get the next available ID value.") % { + "value %(id)s. This value is already used by '" + "%(prim_object)s'. Please enter a different ID or leave " + "blank to get the next available ID value.") % { 'id' : id, 'prim_object' : name } ErrorDialog(msg1, msg2) self.ok_button.set_sensitive(True) diff --git a/src/gui/grampsgui.py b/src/gui/grampsgui.py index 774a0c1f8..cc457e0ce 100644 --- a/src/gui/grampsgui.py +++ b/src/gui/grampsgui.py @@ -136,6 +136,8 @@ def register_stock_icons (): ('gramps-repository', _('Repositories'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-source', _('Sources'), 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-tree-group', _('Grouped List'), gtk.gdk.CONTROL_MASK, 0, ''), ('gramps-tree-list', _('List'), gtk.gdk.CONTROL_MASK, 0, ''), diff --git a/src/gui/viewmanager.py b/src/gui/viewmanager.py index 06b4cd69a..f6b36911e 100644 --- a/src/gui/viewmanager.py +++ b/src/gui/viewmanager.py @@ -89,6 +89,7 @@ from gen.db.backup import backup from gen.db.exceptions import DbException from GrampsAboutDialog import GrampsAboutDialog from gui.sidebar import Sidebar +from gui.views.tags import Tags from gen.utils.configmanager import safe_eval #------------------------------------------------------------------------- @@ -120,6 +121,8 @@ UIDEFAULT = ''' + + @@ -172,6 +175,8 @@ UIDEFAULT = ''' + + @@ -536,6 +541,8 @@ class ViewManager(CLIManager): self.dbstate.connect('database-changed', self.uistate.db_changed) + self.tags = Tags(self.uistate, self.dbstate) + self.filter_menu = self.uimanager.get_widget( '/MenuBar/ViewMenu/Filter/') @@ -1700,34 +1707,34 @@ def by_menu_name(first, second): return cmp(first.name, second.name) def run_plugin(pdata, dbstate, uistate): - """ - run a plugin based on it's PluginData: - 1/ load plugin. - 2/ the report is run - """ - mod = GuiPluginManager.get_instance().load_plugin(pdata) - if not mod: - #import of plugin failed - ErrorDialog( - _('Failed Loading Plugin'), - _('The plugin did not load. See Help Menu, Plugin Manager' - ' for more info.\nUse http://bugs.gramps-project.org to' - ' submit bugs of official plugins, contact the plugin ' - 'author otherwise. ')) - return + """ + run a plugin based on it's PluginData: + 1/ load plugin. + 2/ the report is run + """ + mod = GuiPluginManager.get_instance().load_plugin(pdata) + if not mod: + #import of plugin failed + ErrorDialog( + _('Failed Loading Plugin'), + _('The plugin did not load. See Help Menu, Plugin Manager' + ' for more info.\nUse http://bugs.gramps-project.org to' + ' submit bugs of official plugins, contact the plugin ' + 'author otherwise. ')) + return - if pdata.ptype == REPORT: - report(dbstate, uistate, uistate.get_active('Person'), - getattr(mod, pdata.reportclass), - getattr(mod, pdata.optionclass), - pdata.name, pdata.id, - pdata.category, pdata.require_active) - else: - tool.gui_tool(dbstate, uistate, - getattr(mod, pdata.toolclass), - getattr(mod, pdata.optionclass), - pdata.name, pdata.id, pdata.category, - dbstate.db.request_rebuild) + if pdata.ptype == REPORT: + report(dbstate, uistate, uistate.get_active('Person'), + getattr(mod, pdata.reportclass), + getattr(mod, pdata.optionclass), + pdata.name, pdata.id, + pdata.category, pdata.require_active) + else: + tool.gui_tool(dbstate, uistate, + getattr(mod, pdata.toolclass), + getattr(mod, pdata.optionclass), + pdata.name, pdata.id, pdata.category, + dbstate.db.request_rebuild) def make_plugin_callback(pdata, dbstate, uistate): """ diff --git a/src/gui/views/Makefile.am b/src/gui/views/Makefile.am index 2bece7156..3d7fac2c7 100644 --- a/src/gui/views/Makefile.am +++ b/src/gui/views/Makefile.am @@ -12,7 +12,8 @@ pkgdata_PYTHON = \ __init__.py \ listview.py \ navigationview.py \ - pageview.py + pageview.py \ + tags.py pkgpyexecdir = @pkgpyexecdir@/gui/views pkgpythondir = @pkgpythondir@/gui/views diff --git a/src/gui/views/tags.py b/src/gui/views/tags.py new file mode 100644 index 000000000..61956945e --- /dev/null +++ b/src/gui/views/tags.py @@ -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 = ''' + + + + +''' + +TAG_2 = ''' + + + + + + + + + + +''' + +TAG_3 = ''' + +''' + +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 = '' + tag_menu += '' + tag_menu += '' + for tag_name in sorted(self.db.get_all_tags(), key=locale.strxfrm): + tag_menu += '' % 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('%s' + % _("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 diff --git a/src/gui/views/treemodels/peoplemodel.py b/src/gui/views/treemodels/peoplemodel.py index afd2e52d9..c346e9c94 100644 --- a/src/gui/views/treemodels/peoplemodel.py +++ b/src/gui/views/treemodels/peoplemodel.py @@ -78,6 +78,7 @@ COLUMN_DEATH = 5 COLUMN_BIRTH = 6 COLUMN_EVENT = 7 COLUMN_FAMILY = 8 +COLUMN_TAGS = 21 COLUMN_CHANGE = 17 COLUMN_MARKER = 18 @@ -103,6 +104,7 @@ class PeopleBaseModel(object): """ Initialize the model building the initial data """ + self.db = db self.gen_cursor = db.get_person_cursor self.map = db.get_raw_person_data @@ -115,10 +117,11 @@ class PeopleBaseModel(object): self.column_death_day, self.column_death_place, self.column_spouse, + self.column_tags, self.column_change, self.column_int_id, self.column_marker_text, - self.column_marker_color, + self.column_tag_color, self.column_tooltip, ] self.smap = [ @@ -130,10 +133,11 @@ class PeopleBaseModel(object): self.sort_death_day, self.column_death_place, self.column_spouse, + self.column_tags, self.sort_change, self.column_int_id, self.column_marker_text, - self.column_marker_color, + self.column_tag_color, self.column_tooltip, ] @@ -145,11 +149,26 @@ class PeopleBaseModel(object): self.lru_bdate = 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): """ Return the column for marker colour. """ - return 11 + return 12 def clear_local_cache(self, handle=None): """ Clear the LRU cache """ @@ -419,19 +438,6 @@ class PeopleBaseModel(object): return str(data[COLUMN_MARKER]) 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): if const.USE_TIPS: return ToolTips.TipFromFunction( @@ -444,6 +450,14 @@ class PeopleBaseModel(object): def column_int_id(self, data): 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): """ Listed people model. @@ -453,7 +467,7 @@ class PersonListModel(PeopleBaseModel, FlatBaseModel): PeopleBaseModel.__init__(self, db) FlatBaseModel.__init__(self, db, search=search, skip=skip, - tooltip_column=12, + tooltip_column=13, scol=scol, order=order, sort_map=sort_map) def clear_cache(self, handle=None): @@ -468,7 +482,7 @@ class PersonTreeModel(PeopleBaseModel, TreeBaseModel): skip=set(), sort_map=None): 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) def _set_base_data(self): diff --git a/src/gui/widgets/Makefile.am b/src/gui/widgets/Makefile.am index 9a96ada61..059dd5245 100644 --- a/src/gui/widgets/Makefile.am +++ b/src/gui/widgets/Makefile.am @@ -21,6 +21,7 @@ pkgdata_PYTHON = \ statusbar.py \ styledtextbuffer.py \ styledtexteditor.py \ + tageditor.py \ toolcomboentry.py \ undoablebuffer.py \ validatedcomboentry.py \ diff --git a/src/gui/widgets/monitoredwidgets.py b/src/gui/widgets/monitoredwidgets.py index 9f27021df..bf5effffc 100644 --- a/src/gui/widgets/monitoredwidgets.py +++ b/src/gui/widgets/monitoredwidgets.py @@ -23,7 +23,7 @@ __all__ = ["MonitoredCheckbox", "MonitoredEntry", "MonitoredSpinButton", "MonitoredText", "MonitoredType", "MonitoredDataType", "MonitoredMenu", "MonitoredStrMenu", "MonitoredDate", - "MonitoredComboSelectedEntry"] + "MonitoredComboSelectedEntry", "MonitoredTagList"] #------------------------------------------------------------------------- # @@ -47,8 +47,10 @@ import gtk # Gramps modules # #------------------------------------------------------------------------- +from gen.ggettext import gettext as _ import AutoComp 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 """ 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) + diff --git a/src/gui/widgets/tageditor.py b/src/gui/widgets/tageditor.py new file mode 100644 index 000000000..687af92c9 --- /dev/null +++ b/src/gui/widgets/tageditor.py @@ -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) diff --git a/src/images/16x16/Makefile.am b/src/images/16x16/Makefile.am index 86cc2e6f8..b43cacf69 100644 --- a/src/images/16x16/Makefile.am +++ b/src/images/16x16/Makefile.am @@ -47,6 +47,8 @@ dist_pkgdata_DATA = \ gramps-repository.png \ gramps-source.png \ gramps-spouse.png \ + gramps-tag.png \ + gramps-tag-new.png \ gramps-tools.png \ gramps-tree-group.png \ gramps-tree-list.png \ diff --git a/src/images/16x16/gramps-tag-new.png b/src/images/16x16/gramps-tag-new.png new file mode 100644 index 0000000000000000000000000000000000000000..3da6a3c93d0a73de35e132cc66da424b42d696d0 GIT binary patch literal 747 zcmV5U^<#QVh3xZc|6=2xsb7JIeWUTIYj1MS{kC`C-kzaqm9kKfSFx7^yR@KL?RJY1t{ftURru`J&U3Fztosg ze|f@U{yJ${$n$)#Qt2hjQveQ69_d+H{Oi1k$Y9kc5L)vguMyIhFMx%PKQL3W98{IQ zzCKPIpDrfJF4k+yY}6i+uHVM#CLuXamb+Wd(#IVD5k*yT&O?l>*K3FfF_u_+Balzy z+2r^8OL?09c=PlRb3xkys){qmzpJZw=UG`#rG6}n;sBH{TZ}vujP77*8 zi!_@}obxmqn&YuCb5D`Rn0Eh@63=i)Igz>Qhs2USv2NBt- ztH{SKh)9~Hnd|8;;hb&xRU^cJs*)sKs2XqIHW85s#mgd$VSelj!wGb*vY@@E+#I5=W z?!=8dsTKtV5u!B}d<78|_dbQ)Xse6jWtT-SNNv(2ko@PkNTUg{oz*ax-^@2dAq4c> zYNcFoPG7g%om_uhM7GWB=Dp?nANu3e4_K{~QzG&$U&x)Bo|zs1@9ykj{o{H)ilXew z-3QfPT>Fbu_0{Z|+4%I#4EA<+@%ht6-GU*X&yAm+oTO|vJG!;C^&Y_GUJv)2=W9=I zjE#+5Sva>a63p;r^J}fqXe_syty>!#n}=o*&d$$iJP|KcUOc-!1Q3xYOU1<)5D*Z+ z%m9!8kaX$+02U0z;!=!=9uEPcD9WT#6EHIXz=cZ}<5N>pk20CeTB%fui73nr$z&2{ zHnCbMj||;FgwcrrW=1-l#0W8RNq|KJzuhc$%2AYu?v2qDlAfQZ4&P<22E_+8rv z5g`P?sRI*1WT54r1fV-vMX|HJ4W|y(Y8B3Tc<12MLDfNZ5HKi#1wjIu%_dYG8jVAE z=MgM`xYKPoaRU*ENVo84l2eZ#Kfgo8K}7%NkKf?v3%yCsIn?S0P!X6(@4d(0fS3>) zkAazyOr+qQM>3HDvw(;|bOL~g01)Qp<^d4$x$|Ha6mp9o>gJi~{}PC(-D!6;KAwQ8 zboH{pf`OUg{Ro%^tyU966aYj60A?<~efQ>}nR%RGMAT8$m&N?lllZd?c|DrylVEe(mUlZ zdGEM*FR_!Exbv*uefRR6@7#OdeWI%TAK|u|c=N3{?rSpcq+bp8Z=SQxE_7vQ-g)@0 zFDBEwzdg1*d+$dYE|-9*Y)=I1+hd2E$oHe82UFcW-2kX6A_jof<)67yIK6iF zJ&(?}bv`p#S$zKwulT}uz$EZhHmB$7z!MV_+XDRQ(a}5ldwP4tD|^gL{pOWQmt1FO zhkqc~-qM}DOe&cq-SH%d1zp3t{A$I&@#RlJ<=iRYW3$<|s{VUBJ3Bji`}!mdLuRIC zD#gDFpZlx9aIskYup`?ObPYU5y7Q?B3LptmJI9lmJ!kK4?fdju;MN-O*om=+63#wx z=e^hxa|u>wSSdbV0J>|yiG+D&^Nb=~>2Ps#|EHC9z z2dY*4m2U{jXGo+U!8Q*gCUr&wz6R#&`dExW0LB=sv0wzxTcKR`08q_PuJ$vZpFq}r zrOEuw#mP^&ec#Y$XmAQRoy}?e2Vz7BMYytXm7d-ntg#qlnZGzk|G+L(l`!lfun!P6 z<-lI!Z2kuZyFYjlC}nfe+0+}0!CK4eY6WK<)<$5j?9t>L`Pn=J1G@-AhiGhU#DIy)vBt6Dt>9b? zjuTkx@H}s0n;J!3I%Dj{onT^{n~m2iS)aFCfa{HoL8%4X4RCB6!^D`Vi?%YBD4Z%1 z*+z@EGM9KPA{%Q&#EFR1fB_3+R8^wI(2{CF1lrothzRYOTQJ6wNw*^it*vbc@Gpu? z%!$BpmISa}Q6nO#!tULJs4Bzz_M@sC963x71{}zZpsMV>Z3vC6eV-xtn z)$7-nQ|*~{jImf_(GaTDDnSt4cBrBXMl1=}%(ZJvfGn}85u8T#`@rJlxyjcMUb5Eh zWTVW+hXYZ9Fbu9vPfecy76D)5Yf++Kj!0~u1e*SX);I(BKn19-gZ}~HH*T`d2~a)& O0000&YacMq$d&U}B?`*ND%+N0W} z75Sx7xzjmteE@(E0wM;0{k=UF7Jff?T|as-Jw5aDg~0Rk^Oqd)Ql;|F(DlJViRux{ zi+@EM8!v2s|CM-be6*`rEV8?^%WuCfY{y4_q*|@+wt`LTK@#F`ulDy}8N6{ry!R|G zE+4M1t^O1rx$*V&^&kFt^5?(adrGAe!^6YHe24ycn&K1S+Fb3!ywy)8?^Z5(?*Z7@ zctH%cYPI?_v0-*=bE^=-R+F6lP{; zzZ}=tOb-nW9f?R57pA7BimLWuEBJ&fybHH3moDR+Yo3+Mh%anZD*}TyvMl&5otxV<dnAR=sSZll&>)naXq^|dvu z&0%d0Rf|76$}#UXMxj7&2tm?h z=<4i31iHI>5E1%%-@q89x2F$5C>FaB;FyhjGfoQ5s^o!EAei(XfZ^e{2q7>&HbDr1 zJLS7L@3?)dObCIIw?_$?>TH#Od_(o|85zlHz+ literal 0 HcmV?d00001 diff --git a/src/images/48x48/Makefile.am b/src/images/48x48/Makefile.am index 2c0da0cde..e1ddf5910 100644 --- a/src/images/48x48/Makefile.am +++ b/src/images/48x48/Makefile.am @@ -47,6 +47,8 @@ dist_pkgdata_DATA = \ gramps-repository.png \ gramps-source.png \ gramps-spouse.png \ + gramps-tag.png \ + gramps-tag-new.png \ gramps-tools.png \ gramps-tree-group.png \ gramps-tree-list.png \ diff --git a/src/images/48x48/gramps-tag-new.png b/src/images/48x48/gramps-tag-new.png new file mode 100644 index 0000000000000000000000000000000000000000..0e00d85cfcb989a322138c78675b748daf14d6d6 GIT binary patch literal 3060 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 WholW7DcR8g0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/src/images/scalable/gramps-tag.svg b/src/images/scalable/gramps-tag.svg new file mode 100644 index 000000000..cacf95a34 --- /dev/null +++ b/src/images/scalable/gramps-tag.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/plugins/lib/libpersonview.py b/src/plugins/lib/libpersonview.py index 3e532f04e..189f117ab 100644 --- a/src/plugins/lib/libpersonview.py +++ b/src/plugins/lib/libpersonview.py @@ -58,13 +58,15 @@ from DdTargets import DdTargets from gui.editors import EditPerson from Filters.SideBar import PersonSidebarFilter from gen.plug import CATEGORY_QR_PERSON +import gui.widgets.progressdialog as progressdlg #------------------------------------------------------------------------- # -# internationalization +# Python modules # #------------------------------------------------------------------------- from gen.ggettext import sgettext as _ +from bisect import insort_left #------------------------------------------------------------------------- # @@ -83,7 +85,8 @@ class BasePersonView(ListView): COL_DDAT = 5 COL_DPLAC = 6 COL_SPOUSE = 7 - COL_CHAN = 8 + COL_TAGS = 8 + COL_CHAN = 9 #name of the columns COLUMN_NAMES = [ _('Name'), @@ -94,6 +97,7 @@ class BasePersonView(ListView): _('Death Date'), _('Death Place'), _('Spouse'), + _('Tags'), _('Last Changed'), ] # columns that contain markup @@ -102,8 +106,8 @@ class BasePersonView(ListView): CONFIGSETTINGS = ( ('columns.visible', [COL_NAME, COL_ID, COL_GEN, COL_BDAT, COL_DDAT]), ('columns.rank', [COL_NAME, COL_ID, COL_GEN, COL_BDAT, COL_BPLAC, - COL_DDAT, COL_DPLAC, COL_SPOUSE, COL_CHAN]), - ('columns.size', [250, 75, 75, 100, 175, 100, 175, 100, 100]) + COL_DDAT, COL_DPLAC, COL_SPOUSE, COL_TAGS, COL_CHAN]), + ('columns.size', [250, 75, 75, 100, 175, 100, 175, 100, 100, 100]) ) ADD_MSG = _("Add a new person") EDIT_MSG = _("Edit the selected person") @@ -121,6 +125,7 @@ class BasePersonView(ListView): 'person-delete' : self.row_delete, 'person-rebuild' : self.object_build, 'person-groupname-rebuild' : self.object_build, + 'tag-update' : self.tag_updated } ListView.__init__( @@ -360,6 +365,20 @@ class BasePersonView(ListView): self.all_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): """ Merge the selected people. @@ -375,3 +394,57 @@ class BasePersonView(ListView): else: import Merge 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()