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 000000000..3da6a3c93 Binary files /dev/null and b/src/images/16x16/gramps-tag-new.png differ diff --git a/src/images/16x16/gramps-tag.png b/src/images/16x16/gramps-tag.png new file mode 100644 index 000000000..604db2c27 Binary files /dev/null and b/src/images/16x16/gramps-tag.png differ diff --git a/src/images/22x22/Makefile.am b/src/images/22x22/Makefile.am index 972e3662a..4acf99330 100644 --- a/src/images/22x22/Makefile.am +++ b/src/images/22x22/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/22x22/gramps-tag-new.png b/src/images/22x22/gramps-tag-new.png new file mode 100644 index 000000000..3eff07d4f Binary files /dev/null and b/src/images/22x22/gramps-tag-new.png differ diff --git a/src/images/22x22/gramps-tag.png b/src/images/22x22/gramps-tag.png new file mode 100644 index 000000000..aa9bfa6c6 Binary files /dev/null and b/src/images/22x22/gramps-tag.png differ 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 000000000..0e00d85cf Binary files /dev/null and b/src/images/48x48/gramps-tag-new.png differ diff --git a/src/images/48x48/gramps-tag.png b/src/images/48x48/gramps-tag.png new file mode 100644 index 000000000..4ebbb1770 Binary files /dev/null and b/src/images/48x48/gramps-tag.png differ diff --git a/src/images/scalable/Makefile.am b/src/images/scalable/Makefile.am index 33fc83474..e11e90841 100644 --- a/src/images/scalable/Makefile.am +++ b/src/images/scalable/Makefile.am @@ -47,6 +47,8 @@ dist_pkgdata_DATA = \ gramps-repository.svg \ gramps-source.svg \ gramps-spouse.svg \ + gramps-tag.svg \ + gramps-tag-new.svg \ gramps-tools.svg \ gramps-tree-group.svg \ gramps-tree-list.svg \ diff --git a/src/images/scalable/gramps-tag-new.svg b/src/images/scalable/gramps-tag-new.svg new file mode 100644 index 000000000..19fba4744 --- /dev/null +++ b/src/images/scalable/gramps-tag-new.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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()