diff --git a/src/Filters/Rules/_HasTagBase.py b/src/Filters/Rules/_HasTagBase.py index f7321e401..afac97f9a 100644 --- a/src/Filters/Rules/_HasTagBase.py +++ b/src/Filters/Rules/_HasTagBase.py @@ -51,10 +51,17 @@ class HasTagBase(Rule): description = _("Matches objects with the given tag") category = _('General filters') + def prepare(self, db): + """ + Prepare the rule. Things we want to do just once. + """ + tag = db.get_tag_from_name(self.list[0]) + self.tag_handle = tag.get_handle() + 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() + return self.tag_handle in obj.get_tag_list() diff --git a/src/Filters/SideBar/_PersonSidebarFilter.py b/src/Filters/SideBar/_PersonSidebarFilter.py index 65744291d..4c13997f5 100644 --- a/src/Filters/SideBar/_PersonSidebarFilter.py +++ b/src/Filters/SideBar/_PersonSidebarFilter.py @@ -106,8 +106,6 @@ 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) @@ -274,25 +272,13 @@ class PersonSidebarFilter(SidebarFilter): 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): + def on_tags_changed(self, tag_list): """ 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,)) + for tag_name in tag_list: + model.append((tag_name,)) 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 b96f6f3c3..5e90498df 100644 --- a/src/Filters/SideBar/_SidebarFilter.py +++ b/src/Filters/SideBar/_SidebarFilter.py @@ -21,20 +21,29 @@ # $Id$ from gen.ggettext import gettext as _ +from bisect import insort_left import gtk import pango from gui import widgets +from gui.dbguielement import DbGUIElement import config _RETURN = gtk.gdk.keyval_from_name("Return") _KP_ENTER = gtk.gdk.keyval_from_name("KP_Enter") -class SidebarFilter(object): +class SidebarFilter(DbGUIElement): _FILTER_WIDTH = 200 _FILTER_ELLIPSIZE = pango.ELLIPSIZE_END def __init__(self, dbstate, uistate, namespace): + self.signal_map = { + 'tag-add' : self._tag_add, + 'tag-delete' : self._tag_delete, + 'tag-rebuild' : self._tag_rebuild + } + DbGUIElement.__init__(self, dbstate.db) + self.position = 1 self.table = gtk.Table(4, 11) self.table.set_border_width(6) @@ -47,10 +56,11 @@ class SidebarFilter(object): self._init_interface() uistate.connect('filters-changed', self.on_filters_changed) dbstate.connect('database-changed', self._db_changed) - dbstate.db.connect('tags-changed', self.on_tags_changed) self.uistate = uistate self.dbstate = dbstate self.namespace = namespace + self.__tag_list = [] + self._tag_rebuild() def _init_interface(self): self.table.attach(widgets.MarkupLabel(_('Filter')), @@ -148,8 +158,9 @@ class SidebarFilter(object): """ Called when the database is changed. """ - db.connect('tags-changed', self.on_tags_changed) + self._change_db(db) self.on_db_changed(db) + self._tag_rebuild() def on_db_changed(self, db): """ @@ -157,7 +168,42 @@ class SidebarFilter(object): """ pass - def on_tags_changed(self): + def _connect_db_signals(self): + """ + Connect database signals defined in the signal map. + """ + for sig in self.signal_map: + self.callman.add_db_signal(sig, self.signal_map[sig]) + + def _tag_add(self, handle_list): + """ + Called when tags are added. + """ + for handle in handle_list: + tag = self.dbstate.db.get_tag_from_handle(handle) + insort_left(self.__tag_list, tag.get_name()) + self.on_tags_changed(self.__tag_list) + + def _tag_delete(self, handle_list): + """ + Called when tags are deleted. + """ + for handle in handle_list: + tag = self.dbstate.db.get_tag_from_handle(handle) + self.__tag_list.remove(tag.get_name()) + self.on_tags_changed(self.__tag_list) + + def _tag_rebuild(self): + """ + Called when the tag list needs to be rebuilt. + """ + self.__tag_list = [] + for handle in self.dbstate.db.get_tag_handles(): + tag = self.dbstate.db.get_tag_from_handle(handle) + self.__tag_list.append(tag.get_name()) + self.on_tags_changed(self.__tag_list) + + def on_tags_changed(self, tag_list): """ Called when tags are changed. """ @@ -207,4 +253,3 @@ class SidebarFilter(object): filterdb.save() reload_custom_filters() self.on_filters_changed(self.namespace) - diff --git a/src/gen/db/backup.py b/src/gen/db/backup.py index 5368497b0..cc4f2513c 100644 --- a/src/gen/db/backup.py +++ b/src/gen/db/backup.py @@ -60,7 +60,7 @@ import cPickle as pickle #------------------------------------------------------------------------ from gen.db.exceptions import DbException from gen.db.write import FAMILY_TBL, PLACES_TBL, SOURCES_TBL, MEDIA_TBL, \ - EVENTS_TBL, PERSON_TBL, REPO_TBL, NOTE_TBL, META + EVENTS_TBL, PERSON_TBL, REPO_TBL, NOTE_TBL, TAG_TBL, META #------------------------------------------------------------------------ # @@ -205,5 +205,6 @@ def __build_tbl_map(database): ( NOTE_TBL, database.note_map.db), ( MEDIA_TBL, database.media_map.db), ( EVENTS_TBL, database.event_map.db), + ( TAG_TBL, database.tag_map.db), ( META, database.metadata.db), ] diff --git a/src/gen/db/base.py b/src/gen/db/base.py index 9e407f17b..173238ed1 100644 --- a/src/gen/db/base.py +++ b/src/gen/db/base.py @@ -452,6 +452,12 @@ class DbReadBase(object): """ raise NotImplementedError + def get_number_of_tags(self): + """ + Return the number of tags currently in the database. + """ + raise NotImplementedError + def get_object_from_gramps_id(self, val): """ Find a MediaObject in the database from the passed gramps' ID. @@ -601,6 +607,12 @@ class DbReadBase(object): """ raise NotImplementedError + def get_raw_tag_data(self, handle): + """ + Return raw (serialized and pickled) Tag object from handle + """ + raise NotImplementedError + def get_reference_map_cursor(self): """ Returns a reference to a cursor over the reference map @@ -726,6 +738,38 @@ class DbReadBase(object): """ raise NotImplementedError + def get_tag_cursor(self): + """ + Return a reference to a cursor over Tag objects + """ + raise NotImplementedError + + def get_tag_from_handle(self, handle): + """ + Find a Tag in the database from the passed handle. + + If no such Tag exists, None is returned. + """ + raise NotImplementedError + + def get_tag_from_name(self, val): + """ + Find a Tag in the database from the passed Tag name. + + If no such Tag exists, None is returned. + Needs to be overridden by the derived class. + """ + raise NotImplementedError + + def get_tag_handles(self, sort_handles=True): + """ + Return a list of database handles, one handle for each Tag in + the database. + + If sort_handles is True, the list is sorted by Tag name. + """ + raise NotImplementedError + def get_url_types(self): """ Return a list of all custom names types associated with Url instances @@ -765,30 +809,6 @@ 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. @@ -825,6 +845,12 @@ class DbReadBase(object): """ raise NotImplementedError + def has_tag_handle(self, handle): + """ + Return True if the handle exists in the current Tag database. + """ + raise NotImplementedError + def is_open(self): """ Return True if the database has been opened. @@ -927,6 +953,18 @@ class DbReadBase(object): """ raise NotImplementedError + def iter_tag_handles(self): + """ + Return an iterator over handles for Tags in the database + """ + raise NotImplementedError + + def iter_tags(self): + """ + Return an iterator over objects for Tags in the database + """ + raise NotImplementedError + def load(self, name, callback, mode=None, upgrade=False): """ Open the specified database. @@ -1192,6 +1230,13 @@ class DbWriteBase(object): """ raise NotImplementedError + def add_tag(self, tag, transaction): + """ + Add a Tag to the database, assigning a handle if it has not already + been defined. + """ + raise NotImplementedError + def add_to_surname_list(self, person, batch_transaction, name): """ Add surname from given person to list of surnames @@ -1281,6 +1326,13 @@ class DbWriteBase(object): """ raise NotImplementedError + def commit_tag(self, tag, transaction, change_time=None): + """ + Commit the specified Tag to the database, storing the changes as + part of the transaction. + """ + raise NotImplementedError + def delete_primary_from_reference_map(self, handle, transaction): """ Called each time an object is removed from the database. @@ -1390,6 +1442,15 @@ class DbWriteBase(object): """ raise NotImplementedError + def remove_tag(self, handle, transaction): + """ + Remove the Tag specified by the database handle from the + database, preserving the change in the passed transaction. + + This method must be overridden in the derived class. + """ + raise NotImplementedError + def set_auto_remove(self): """ BSDDB change log settings using new method with renamed attributes @@ -1410,14 +1471,6 @@ 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/dbconst.py b/src/gen/db/dbconst.py index 83578248d..8e3635751 100644 --- a/src/gen/db/dbconst.py +++ b/src/gen/db/dbconst.py @@ -43,7 +43,7 @@ __all__ = ( ('PERSON_KEY', 'FAMILY_KEY', 'SOURCE_KEY', 'EVENT_KEY', 'MEDIA_KEY', 'PLACE_KEY', 'REPOSITORY_KEY', 'NOTE_KEY', - 'REFERENCE_KEY' + 'REFERENCE_KEY', 'TAG_KEY' ) + ('TXNADD', 'TXNUPD', 'TXNDEL') @@ -53,7 +53,7 @@ DBEXT = ".db" # File extension to be used for database files DBUNDOFN = "undo.db" # File name of 'undo' database DBLOCKFN = "lock" # File name of lock file DBRECOVFN = "need_recover" # File name of recovery file -DBLOGNAME = ".Db" # Name of logger +DBLOGNAME = ".Db" # Name of logger DBMODE_R = "r" # Read-only access DBMODE_W = "w" # Full Reaw/Write access DBPAGE = 16384 # Size of the pages used to hold items in the database @@ -77,5 +77,6 @@ PLACE_KEY = 5 REPOSITORY_KEY = 6 REFERENCE_KEY = 7 NOTE_KEY = 8 +TAG_KEY = 9 TXNADD, TXNUPD, TXNDEL = 0, 1, 2 diff --git a/src/gen/db/read.py b/src/gen/db/read.py index e9ffb141c..301419d01 100644 --- a/src/gen/db/read.py +++ b/src/gen/db/read.py @@ -46,7 +46,7 @@ import logging # #------------------------------------------------------------------------- from gen.lib import (MediaObject, Person, Family, Source, Event, Place, - Repository, Note, GenderStats, Researcher) + Repository, Note, Tag, GenderStats, Researcher) from gen.db.dbconst import * from gen.utils.callback import Callback from gen.db import (BsddbBaseCursor, DbReadBase) @@ -62,7 +62,7 @@ LOG = logging.getLogger(DBLOGNAME) from gen.db.dbconst import * _SIGBASE = ('person', 'family', 'source', 'event', - 'media', 'place', 'repository', 'reference', 'note') + 'media', 'place', 'repository', 'reference', 'note', 'tag') DBERRS = (db.DBRunRecoveryError, db.DBAccessError, db.DBPageNotFoundError, db.DBInvalidArgError) @@ -230,6 +230,13 @@ class DbBsddbRead(DbReadBase, Callback): "class_func": Note, "cursor_func": self.get_note_cursor, }, + 'Tag': + { + "handle_func": self.get_tag_from_handle, + "gramps_id_func": None, + "class_func": Tag, + "cursor_func": self.get_tag_cursor, + }, } self.set_person_id_prefix('I%04d') @@ -280,6 +287,7 @@ class DbBsddbRead(DbReadBase, Callback): self.rid_trans = {} self.nid_trans = {} self.eid_trans = {} + self.tag_trans = {} self.env = None self.person_map = {} self.family_map = {} @@ -291,7 +299,6 @@ 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 @@ -374,6 +381,9 @@ class DbBsddbRead(DbReadBase, Callback): def get_note_cursor(self, *args, **kwargs): return self.get_cursor(self.note_map, *args, **kwargs) + def get_tag_cursor(self, *args, **kwargs): + return self.get_cursor(self.tag_map, *args, **kwargs) + def close(self): """ Close the specified database. @@ -401,6 +411,7 @@ class DbBsddbRead(DbReadBase, Callback): self.emit('event-rebuild') self.emit('repository-rebuild') self.emit('note-rebuild') + self.emit('tag-rebuild') @staticmethod def __find_next_gramps_id(prefix, map_index, trans): @@ -525,7 +536,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_person_from_handle(self, handle): """ - Find a Person in the database from the passed gramps' ID. + Find a Person in the database from the passed handle. If no such Person exists, None is returned. """ @@ -533,7 +544,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_source_from_handle(self, handle): """ - Find a Source in the database from the passed gramps' ID. + Find a Source in the database from the passed handle. If no such Source exists, None is returned. """ @@ -541,7 +552,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_object_from_handle(self, handle): """ - Find an Object in the database from the passed gramps' ID. + Find an Object in the database from the passed handle. If no such Object exists, None is returned. """ @@ -549,7 +560,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_place_from_handle(self, handle): """ - Find a Place in the database from the passed gramps' ID. + Find a Place in the database from the passed handle. If no such Place exists, None is returned. """ @@ -557,7 +568,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_event_from_handle(self, handle): """ - Find a Event in the database from the passed gramps' ID. + Find a Event in the database from the passed handle. If no such Event exists, None is returned. """ @@ -565,7 +576,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_family_from_handle(self, handle): """ - Find a Family in the database from the passed gramps' ID. + Find a Family in the database from the passed handle. If no such Family exists, None is returned. """ @@ -573,7 +584,7 @@ class DbBsddbRead(DbReadBase, Callback): def get_repository_from_handle(self, handle): """ - Find a Repository in the database from the passed gramps' ID. + Find a Repository in the database from the passed handle. If no such Repository exists, None is returned. """ @@ -581,12 +592,20 @@ class DbBsddbRead(DbReadBase, Callback): def get_note_from_handle(self, handle): """ - Find a Note in the database from the passed gramps' ID. + Find a Note in the database from the passed handle. If no such Note exists, None is returned. """ return self.get_from_handle(handle, Note, self.note_map) + def get_tag_from_handle(self, handle): + """ + Find a Tag in the database from the passed handle. + + If no such Tag exists, None is returned. + """ + return self.get_from_handle(handle, Tag, self.tag_map) + def __get_obj_from_gramps_id(self, val, tbl, class_, prim_tbl): try: if tbl.has_key(str(val)): @@ -679,6 +698,15 @@ class DbBsddbRead(DbReadBase, Callback): """ return self.__get_obj_from_gramps_id(val, self.nid_trans, Note, self.note_map) + + def get_tag_from_name(self, val): + """ + Find a Tag in the database from the passed Tag name. + + If no such Tag exists, None is returned. + """ + return self.__get_obj_from_gramps_id(val, self.tag_trans, Tag, + self.tag_map) def get_name_group_mapping(self, name): """ @@ -698,30 +726,6 @@ 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 @@ -778,6 +782,12 @@ class DbBsddbRead(DbReadBase, Callback): """ return self.get_number_of_records(self.note_map) + def get_number_of_tags(self): + """ + Return the number of tags currently in the database. + """ + return self.get_number_of_records(self.tag_map) + def all_handles(self, table): return table.keys() @@ -894,6 +904,20 @@ class DbBsddbRead(DbReadBase, Callback): return self.all_handles(self.note_map) return [] + def get_tag_handles(self, sort_handles=True): + """ + Return a list of database handles, one handle for each Tag in + the database. + + If sort_handles is True, the list is sorted by Tag name. + """ + if self.db_is_open: + handle_list = self.all_handles(self.tag_map) + if sort_handles: + handle_list.sort(key=self.__sortbytag_key) + return handle_list + return [] + def _f(curs_): """ Closure that returns an iterator over handles in the database. @@ -914,6 +938,7 @@ class DbBsddbRead(DbReadBase, Callback): iter_media_object_handles = _f(get_media_cursor) iter_repository_handles = _f(get_repository_cursor) iter_note_handles = _f(get_note_cursor) + iter_tag_handles = _f(get_tag_cursor) del _f def _f(curs_, obj_): @@ -938,6 +963,7 @@ class DbBsddbRead(DbReadBase, Callback): iter_media_objects = _f(get_media_cursor, MediaObject) iter_repositories = _f(get_repository_cursor, Repository) iter_notes = _f(get_note_cursor, Note) + iter_tags = _f(get_tag_cursor, Tag) del _f def get_gramps_ids(self, obj_key): @@ -1296,7 +1322,10 @@ class DbBsddbRead(DbReadBase, Callback): def get_raw_note_data(self, handle): return self.__get_raw_data(self.note_map, handle) - + + def get_raw_tag_data(self, handle): + return self.__get_raw_data(self.tag_map, handle) + def __has_handle(self, table, handle): """ Helper function for has__handle methods @@ -1355,6 +1384,12 @@ class DbBsddbRead(DbReadBase, Callback): """ return self.__has_handle(self.source_map, handle) + def has_tag_handle(self, handle): + """ + Return True if the handle exists in the current Tag database. + """ + return self.__has_handle(self.tag_map, handle) + def __sortbyperson_key(self, person): return locale.strxfrm(self.person_map.get(str(person))[3][5]) @@ -1383,6 +1418,15 @@ class DbBsddbRead(DbReadBase, Callback): media = self.media_map[str(key)][4] return locale.strxfrm(media) + def __sortbytag(self, first, second): + tag1 = self.tag_map[str(first)][1] + tag2 = self.tag_map[str(second)][1] + return locale.strcoll(tag1, tag2) + + def __sortbytag_key(self, key): + tag = self.tag_map[str(key)][1] + return locale.strxfrm(tag) + def set_mediapath(self, path): """Set the default media path for database, path should be utf-8.""" if (self.metadata is not None) and (not self.readonly): @@ -1452,6 +1496,10 @@ class DbBsddbRead(DbReadBase, Callback): 'cursor_func': self.get_note_cursor, 'class_func': Note, }, + 'Tag': { + 'cursor_func': self.get_tag_cursor, + 'class_func': Tag, + }, } # Find which tables to iterate over diff --git a/src/gen/db/txn.py b/src/gen/db/txn.py index 00c912580..778614a2c 100644 --- a/src/gen/db/txn.py +++ b/src/gen/db/txn.py @@ -129,6 +129,7 @@ class DbTxn(defaultdict): REPOSITORY_KEY: (self.db.repository_map, 'repository'), #REFERENCE_KEY: (self.db.reference_map, 'reference'), NOTE_KEY: (self.db.note_map, 'note'), + TAG_KEY: (self.db.tag_map, 'tag'), } def get_description(self): diff --git a/src/gen/db/undoredo.py b/src/gen/db/undoredo.py index a21408a5c..0ffa394e0 100644 --- a/src/gen/db/undoredo.py +++ b/src/gen/db/undoredo.py @@ -55,7 +55,7 @@ DBERRS = (db.DBRunRecoveryError, db.DBAccessError, db.DBPageNotFoundError, db.DBInvalidArgError) _SIGBASE = ('person', 'family', 'source', 'event', 'media', - 'place', 'repository', 'reference', 'note') + 'place', 'repository', 'reference', 'note', 'tag') #------------------------------------------------------------------------- # # DbUndo class @@ -89,6 +89,7 @@ class DbUndo(object): self.db.repository_map, self.db.reference_map, self.db.note_map, + self.db.tag_map, ) def clear(self): @@ -459,6 +460,7 @@ def testundo(): self.media_map = {} self.place_map = {} self.note_map = {} + self.tag_map = {} self.repository_map = {} self.reference_map = {} diff --git a/src/gen/db/write.py b/src/gen/db/write.py index 294496f5d..46d8ac563 100644 --- a/src/gen/db/write.py +++ b/src/gen/db/write.py @@ -49,7 +49,7 @@ from sys import maxint # #------------------------------------------------------------------------- from gen.lib import (GenderStats, Person, Family, Event, Place, Source, - MediaObject, Repository, Note) + MediaObject, Repository, Note, Tag) from gen.db import (DbBsddbRead, DbWriteBase, BSDDBTxn, DbTxn, BsddbBaseCursor, DbVersionError, DbUpgradeRequiredError, @@ -71,9 +71,9 @@ EIDTRANS = "event_id" RIDTRANS = "repo_id" NIDTRANS = "note_id" SIDTRANS = "source_id" +TAGTRANS = "tag_name" SURNAMES = "surnames" NAME_GROUP = "name_group" -TAGS = "tags" META = "meta_data" FAMILY_TBL = "family" @@ -84,6 +84,7 @@ EVENTS_TBL = "event" PERSON_TBL = "person" REPO_TBL = "repo" NOTE_TBL = "note" +TAG_TBL = "tag" REF_MAP = "reference_map" REF_PRI = "primary_map" @@ -105,7 +106,8 @@ CLASS_TO_KEY_MAP = {Person.__name__: PERSON_KEY, MediaObject.__name__: MEDIA_KEY, Place.__name__: PLACE_KEY, Repository.__name__:REPOSITORY_KEY, - Note.__name__: NOTE_KEY} + Note.__name__: NOTE_KEY, + Tag.__name__: TAG_KEY} KEY_TO_CLASS_MAP = {PERSON_KEY: Person.__name__, FAMILY_KEY: Family.__name__, @@ -114,7 +116,8 @@ KEY_TO_CLASS_MAP = {PERSON_KEY: Person.__name__, MEDIA_KEY: MediaObject.__name__, PLACE_KEY: Place.__name__, REPOSITORY_KEY: Repository.__name__, - NOTE_KEY: Note.__name__} + NOTE_KEY: Note.__name__, + TAG_KEY: Tag.__name__} #------------------------------------------------------------------------- # @@ -179,7 +182,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): __signals__ = dict((obj+'-'+op, signal) for obj in ['person', 'family', 'event', 'place', - 'source', 'media', 'note', 'repository'] + 'source', 'media', 'note', 'repository', 'tag'] for op, signal in zip( ['add', 'update', 'delete', 'rebuild'], [(list,), (list,), (list,), None] @@ -198,10 +201,6 @@ 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.""" @@ -453,6 +452,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): ("person_map", PERSON_TBL, db.DB_HASH), ("repository_map", REPO_TBL, db.DB_HASH), ("note_map", NOTE_TBL, db.DB_HASH), + ("tag_map", TAG_TBL, db.DB_HASH), ("reference_map", REF_MAP, db.DB_BTREE), ] @@ -468,9 +468,6 @@ 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 @@ -591,6 +588,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): ("oid_trans", OIDTRANS, db.DB_HASH, 0), ("rid_trans", RIDTRANS, db.DB_HASH, 0), ("nid_trans", NIDTRANS, db.DB_HASH, 0), + ("tag_trans", TAGTRANS, db.DB_HASH, 0), ("reference_map_primary_map", REF_PRI, db.DB_BTREE, 0), ("reference_map_referenced_map", REF_REF, db.DB_BTREE, db.DB_DUPSORT), ] @@ -612,6 +610,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): (self.media_map, self.oid_trans, find_idmap), (self.repository_map, self.rid_trans, find_idmap), (self.note_map, self.nid_trans, find_idmap), + (self.tag_map, self.tag_trans, find_idmap), (self.reference_map, self.reference_map_primary_map, find_primary_handle), (self.reference_map, self.reference_map_referenced_map, @@ -650,6 +649,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): ( self.eid_trans, EIDTRANS ), ( self.rid_trans, RIDTRANS ), ( self.nid_trans, NIDTRANS ), + ( self.tag_trans, TAGTRANS ), ( self.reference_map_primary_map, REF_PRI), ( self.reference_map_referenced_map, REF_REF), ] @@ -924,6 +924,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): (self.get_media_cursor, MediaObject), (self.get_repository_cursor, Repository), (self.get_note_cursor, Note), + (self.get_tag_cursor, Tag), ) # Now we use the functions and classes defined above @@ -1014,7 +1015,6 @@ 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() @@ -1024,6 +1024,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.oid_trans.close() self.sid_trans.close() self.pid_trans.close() + self.tag_trans.close() self.reference_map_primary_map.close() self.reference_map_referenced_map.close() self.reference_map.close() @@ -1039,6 +1040,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.source_map.close() self.media_map.close() self.event_map.close() + self.tag_map.close() self.env.close() self.__close_undodb() @@ -1050,8 +1052,8 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.source_map = None self.media_map = None self.event_map = None + self.tag_map = None self.surnames = None - self.tags = None self.env = None self.metadata = None self.db_is_open = False @@ -1181,6 +1183,13 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.find_next_note_gramps_id if set_gid else None, self.commit_note) + def add_tag(self, obj, transaction): + """ + Add a Tag to the database, assigning a handle if it has not already + been defined. + """ + return self.__add_object(obj, transaction, None, self.commit_tag) + def __do_remove(self, handle, transaction, data_map, key): if self.readonly or not handle: return @@ -1272,6 +1281,14 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.__do_remove(handle, transaction, self.note_map, NOTE_KEY) + def remove_tag(self, handle, transaction): + """ + Remove the Tag specified by the database handle from the + database, preserving the change in the passed transaction. + """ + self.__do_remove(handle, transaction, self.tag_map, + TAG_KEY) + @catch_db_error def set_name_group_mapping(self, name, group): if not self.readonly: @@ -1289,24 +1306,6 @@ 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) @@ -1560,6 +1559,14 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): if note.type.is_custom(): self.note_types.add(str(note.type)) + def commit_tag(self, tag, transaction, change_time=None): + """ + Commit the specified Tag to the database, storing the changes as part + of the transaction. + """ + self.commit_base(tag, self.tag_map, TAG_KEY, + transaction, change_time) + def get_from_handle(self, handle, class_type, data_map): try: data = data_map.get(str(handle), txn=self.txn) diff --git a/src/gen/lib/Makefile.am b/src/gen/lib/Makefile.am index 4bacb685c..96ac6d3ab 100644 --- a/src/gen/lib/Makefile.am +++ b/src/gen/lib/Makefile.am @@ -60,6 +60,8 @@ pkgdata_PYTHON = \ styledtext.py \ styledtexttag.py \ styledtexttagtype.py \ + tableobj.py \ + tag.py \ tagbase.py \ urlbase.py \ url.py \ diff --git a/src/gen/lib/__init__.py b/src/gen/lib/__init__.py index f7b486e1b..3a283c841 100644 --- a/src/gen/lib/__init__.py +++ b/src/gen/lib/__init__.py @@ -52,6 +52,9 @@ from gen.lib.mediaobj import MediaObject from gen.lib.repo import Repository from gen.lib.note import Note +# Table objects +from gen.lib.tag import Tag + # These are actually metadata from gen.lib.genderstats import GenderStats from gen.lib.researcher import Researcher diff --git a/src/gen/lib/person.py b/src/gen/lib/person.py index 680bef38e..e2509e108 100644 --- a/src/gen/lib/person.py +++ b/src/gen/lib/person.py @@ -235,6 +235,8 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, elif classname == 'Place': return any(ordinance.place == handle for ordinance in self.lds_ord_list) + elif classname == 'Tag': + return handle in self.tag_list return False def _remove_handle_references(self, classname, handle_list): @@ -277,6 +279,9 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, for ordinance in self.lds_ord_list: if ordinance.place in handle_list: ordinance.place = None + elif classname == 'Tag': + for handle in handle_list: + self.tag_list.remove(handle) def _replace_handle_reference(self, classname, old_handle, new_handle): if classname == 'Event': @@ -335,7 +340,6 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, while old_handle in self.parent_family_list: ix = self.parent_family_list.index(old_handle) self.parent_family_list[ix] = new_handle - elif classname == 'Place': handle_list = [ordinance.place for ordinance in self.lds_ord_list] while old_handle in handle_list: ix = handle_list.index(old_handle) @@ -403,7 +407,8 @@ class Person(SourceBase, NoteBase, AttributeBase, MediaBase, """ return [('Family', handle) for handle in (self.family_list + self.parent_family_list)] \ - + self.get_referenced_note_handles() + + self.get_referenced_note_handles() \ + + self.get_referenced_tag_handles() def get_handle_referents(self): """ diff --git a/src/gen/lib/primaryobj.py b/src/gen/lib/primaryobj.py index 0849a8886..dcd8a50ec 100644 --- a/src/gen/lib/primaryobj.py +++ b/src/gen/lib/primaryobj.py @@ -24,41 +24,23 @@ Basic Primary Object class for GRAMPS. """ -#------------------------------------------------------------------------- -# -# standard python modules -# -#------------------------------------------------------------------------- -import time -import locale - #------------------------------------------------------------------------- # # GRAMPS modules # #------------------------------------------------------------------------- -from gen.lib.baseobj import BaseObject +from gen.lib.tableobj import TableObject from gen.lib.privacybase import PrivacyBase from gen.lib.markertype import MarkerType from gen.lib.srcbase import SourceBase from gen.lib.mediabase import MediaBase -#------------------------------------------------------------------------- -# -# Localized constants -# -#------------------------------------------------------------------------- -try: - CODESET = locale.nl_langinfo(locale.CODESET) -except: - CODESET = locale.getpreferredencoding() - #------------------------------------------------------------------------- # # Basic Primary Object class # #------------------------------------------------------------------------- -class BasicPrimaryObject(BaseObject, PrivacyBase): +class BasicPrimaryObject(TableObject, PrivacyBase): """ The BasicPrimaryObject is the base class for Note objects. @@ -82,73 +64,15 @@ class BasicPrimaryObject(BaseObject, PrivacyBase): :param source: Object used to initialize the new object :type source: PrimaryObject """ + TableObject.__init__(self, source) PrivacyBase.__init__(self, source) if source: self.gramps_id = source.gramps_id - self.handle = source.handle - self.change = source.change self.marker = source.marker else: self.gramps_id = None - self.handle = None - self.change = 0 self.marker = MarkerType() - def get_change_time(self): - """ - Return the time that the data was last changed. - - The value in the format returned by the time.time() command. - - :returns: Time that the data was last changed. The value in the format - returned by the time.time() command. - :rtype: int - """ - return self.change - - def set_change_time(self, change): - """ - Modify the time that the data was last changed. - - The value must be in the format returned by the time.time() command. - - @param change: new time - @type change: int in format as time.time() command - """ - self.change = change - - def get_change_display(self): - """ - Return the string representation of the last change time. - - :returns: string representation of the last change time. - :rtype: str - - """ - if self.change: - return unicode(time.strftime('%x %X', time.localtime(self.change)), - CODESET) - else: - return u'' - - def set_handle(self, handle): - """ - Set the database handle for the primary object. - - :param handle: object database handle - :type handle: str - """ - self.handle = handle - - def get_handle(self): - """ - Return the database handle for the primary object. - - :returns: database handle associated with the object - :rtype: str - """ - return self.handle - def set_gramps_id(self, gramps_id): """ Set the GRAMPS ID for the primary object. diff --git a/src/gen/lib/tableobj.py b/src/gen/lib/tableobj.py new file mode 100644 index 000000000..1872e889b --- /dev/null +++ b/src/gen/lib/tableobj.py @@ -0,0 +1,139 @@ +# +# 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$ + +""" +Table Object class for Gramps. +""" + +#------------------------------------------------------------------------- +# +# standard python modules +# +#------------------------------------------------------------------------- +import time +import locale + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.lib.baseobj import BaseObject + +#------------------------------------------------------------------------- +# +# Localized constants +# +#------------------------------------------------------------------------- +try: + CODESET = locale.nl_langinfo(locale.CODESET) +except: + CODESET = locale.getpreferredencoding() + +#------------------------------------------------------------------------- +# +# Table Object class +# +#------------------------------------------------------------------------- +class TableObject(BaseObject): + """ + The TableObject is the base class for all objects that are stored in a + seperate database table. Each object has a database handle and a last + changed time. The database handle is used as the unique key for a record + in the database. This is not the same as the Gramps ID, which is a user + visible identifier for a record. + + It is the base class for the BasicPrimaryObject class and Tag class. + """ + + def __init__(self, source=None): + """ + Initialize a TableObject. + + If source is None, the handle is assigned as an empty string. + If source is not None, then the handle is initialized from the value in + the source object. + + :param source: Object used to initialize the new object + :type source: TableObject + """ + if source: + self.handle = source.handle + self.change = source.change + else: + self.handle = None + self.change = 0 + + def get_change_time(self): + """ + Return the time that the data was last changed. + + The value in the format returned by the time.time() command. + + :returns: Time that the data was last changed. The value in the format + returned by the time.time() command. + :rtype: int + """ + return self.change + + def set_change_time(self, change): + """ + Modify the time that the data was last changed. + + The value must be in the format returned by the time.time() command. + + @param change: new time + @type change: int in format as time.time() command + """ + self.change = change + + def get_change_display(self): + """ + Return the string representation of the last change time. + + :returns: string representation of the last change time. + :rtype: str + + """ + if self.change: + return unicode(time.strftime('%x %X', time.localtime(self.change)), + CODESET) + else: + return u'' + + def set_handle(self, handle): + """ + Set the database handle for the primary object. + + :param handle: object database handle + :type handle: str + """ + self.handle = handle + + def get_handle(self): + """ + Return the database handle for the primary object. + + :returns: database handle associated with the object + :rtype: str + """ + return self.handle diff --git a/src/gen/lib/tag.py b/src/gen/lib/tag.py new file mode 100644 index 000000000..a79513040 --- /dev/null +++ b/src/gen/lib/tag.py @@ -0,0 +1,202 @@ +# +# 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 object for GRAMPS. +""" + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from gen.lib.tableobj import TableObject + +#------------------------------------------------------------------------- +# +# Tag class +# +#------------------------------------------------------------------------- +class Tag(TableObject): + """ + The Tag record is used to store information about a tag that can be + attached to a primary object. + """ + + def __init__(self, source=None): + """ + Create a new Tag instance, copying from the source if present. + + :param source: A tag used to initialize the new tag + :type source: Tag + """ + + TableObject.__init__(self, source) + + if source: + self.__name = source.__name + self.__color = source.__color + self.__priority = source.__priority + else: + self.__name = "" + self.__color = "#000000000000" # Black + self.__priority = 0 + + def serialize(self): + """ + Convert the data held in the event to a Python tuple that + represents all the data elements. + + This method is used to convert the object into a form that can easily + be saved to a database. + + These elements may be primitive Python types (string, integers), + complex Python types (lists or tuples, or Python objects. If the + target database cannot handle complex types (such as objects or + lists), the database is responsible for converting the data into + a form that it can use. + + :returns: Returns a python tuple containing the data that should + be considered persistent. + :rtype: tuple + """ + return (self.handle, + self.__name, + self.__color, + self.__priority, + self.change) + + def unserialize(self, data): + """ + Convert the data held in a tuple created by the serialize method + back into the data in a Tag structure. + + :param data: tuple containing the persistent data associated the + Person object + :type data: tuple + """ + (self.handle, + self.__name, + self.__color, + self.__priority, + self.change) = data + + def get_text_data_list(self): + """ + Return the list of all textual attributes of the object. + + :returns: Returns the list of all textual attributes of the object. + :rtype: list + """ + return [self.__name] + + def is_empty(self): + """ + Return True if the Tag is an empty object (no values set). + + :returns: True if the Tag is empty + :rtype: bool + """ + return self.__name != "" + + def are_equal(self, other): + """ + Return True if the passed Tag is equivalent to the current Tag. + + :param other: Tag to compare against + :type other: Tag + :returns: True if the Tags are equal + :rtype: bool + """ + if other is None: + other = Tag() + + if self.__name != other.__name or \ + self.__color != other.__color or \ + self.__priority != other.__priority: + return False + return True + + def set_name(self, name): + """ + Set the name of the Tag to the passed string. + + :param the_type: Name to assign to the Tag + :type the_type: str + """ + self.__name = name + + def get_name(self): + """ + Return the name of the Tag. + + :returns: Name of the Tag + :rtype: str + """ + return self.__name + name = property(get_name, set_name, None, + 'Returns or sets name of the tag') + + def set_color(self, color): + """ + Set the color of the Tag to the passed string. + + The string is of the format #rrrrggggbbbb. + + :param color: Color to assign to the Tag + :type color: str + """ + self.__color = color + + def get_color(self) : + """ + Return the color of the Tag. + + :returns: Returns the color of the Tag + :rtype: str + """ + return self.__color + color = property(get_color, set_color, None, + 'Returns or sets color of the tag') + + def set_priority(self, priority): + """ + Set the priority of the Tag to the passed integer. + + The lower the value the higher the priority. + + :param priority: Priority to assign to the Tag + :type priority: int + """ + self.__priority = priority + + def get_priority(self) : + """ + Return the priority of the Tag. + + :returns: Returns the priority of the Tag + :rtype: int + """ + return self.__priority + priority = property(get_priority, set_priority, None, + 'Returns or sets priority of the tag') + diff --git a/src/gen/lib/tagbase.py b/src/gen/lib/tagbase.py index c15613eac..12f9372b2 100644 --- a/src/gen/lib/tagbase.py +++ b/src/gen/lib/tagbase.py @@ -105,3 +105,15 @@ class TagBase(object): :type tag_list: list """ self.tag_list = tag_list + + def get_referenced_tag_handles(self): + """ + Return the list of (classname, handle) tuples for all referenced tags. + + This method should be used to get the :class:`~gen.lib.tag.Tag` portion + of the list by objects that store tag lists. + + :returns: List of (classname, handle) tuples for referenced objects. + :rtype: list + """ + return [('Tag', handle) for handle in self.tag_list] diff --git a/src/gui/editors/editperson.py b/src/gui/editors/editperson.py index de08b8da1..712ecc297 100644 --- a/src/gui/editors/editperson.py +++ b/src/gui/editors/editperson.py @@ -310,7 +310,7 @@ class EditPerson(EditPrimary): self.top.get_object("tag_button"), self.obj.set_tag_list, self.obj.get_tag_list, - self.db.get_all_tags(), + self.db, self.uistate, self.track, self.db.readonly) diff --git a/src/gui/views/tags.py b/src/gui/views/tags.py index 61956945e..3cb4821d8 100644 --- a/src/gui/views/tags.py +++ b/src/gui/views/tags.py @@ -26,7 +26,7 @@ Provide tagging functionality. # Python modules # #------------------------------------------------------------------------- -import locale +from bisect import insort_left #------------------------------------------------------------------------- # @@ -41,7 +41,9 @@ import gtk # #------------------------------------------------------------------------- from gen.ggettext import sgettext as _ -from ListModel import ListModel, NOSORT, COLOR +from gen.lib import Tag +from gui.dbguielement import DbGUIElement +from ListModel import ListModel, NOSORT, COLOR, INTEGER import const import GrampsDisplay from QuestionDialog import QuestionDialog2 @@ -85,19 +87,27 @@ WIKI_HELP_SEC = _('manual|Tags') # Tags # #------------------------------------------------------------------------- -class Tags(object): +class Tags(DbGUIElement): """ Provide tagging functionality. """ def __init__(self, uistate, dbstate): + self.signal_map = { + 'tag-add' : self._tag_add, + 'tag-delete' : self._tag_delete, + 'tag-rebuild' : self._tag_rebuild + } + DbGUIElement.__init__(self, dbstate.db) + self.db = dbstate.db self.uistate = uistate self.tag_id = None self.tag_ui = None self.tag_action = None + self.__tag_list = [] - dbstate.connect('database-changed', self.db_changed) + dbstate.connect('database-changed', self._db_changed) self._build_tag_menu() @@ -118,12 +128,47 @@ class Tags(object): self.uistate.uimanager.ensure_update() self.tag_id = None - def db_changed(self, db): + def _db_changed(self, db): """ - When the database chages update the tag list and rebuild the menus. + Called when the database is changed. """ self.db = db - self.db.connect('tags-changed', self.update_tag_menu) + self._change_db(db) + self._tag_rebuild() + + def _connect_db_signals(self): + """ + Connect database signals defined in the signal map. + """ + for sig in self.signal_map: + self.callman.add_db_signal(sig, self.signal_map[sig]) + + def _tag_add(self, handle_list): + """ + Called when tags are added. + """ + for handle in handle_list: + tag = self.db.get_tag_from_handle(handle) + insort_left(self.__tag_list, (tag.get_name(), handle)) + self.update_tag_menu() + + def _tag_delete(self, handle_list): + """ + Called when tags are deleted. + """ + for handle in handle_list: + tag = self.db.get_tag_from_handle(handle) + self.__tag_list.remove((tag.get_name(), handle)) + self.update_tag_menu() + + def _tag_rebuild(self): + """ + Called when the tag list needs to be rebuilt. + """ + self.__tag_list = [] + for handle in self.db.get_tag_handles(): + tag = self.db.get_tag_from_handle(handle) + self.__tag_list.append((tag.get_name(), tag.get_handle())) self.update_tag_menu() def update_tag_menu(self): @@ -151,10 +196,10 @@ class Tags(object): tag_menu = '' tag_menu += '' tag_menu += '' - for tag_name in sorted(self.db.get_all_tags(), key=locale.strxfrm): + for tag_name, handle in self.__tag_list: tag_menu += '' % tag_name actions.append(('TAG_%s' % tag_name, None, tag_name, None, None, - make_callback(self.tag_selected, tag_name))) + make_callback(self.tag_selected_rows, handle))) self.tag_ui = TAG_1 + tag_menu + TAG_2 + tag_menu + TAG_3 @@ -190,17 +235,40 @@ class Tags(object): """ 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() + if tag_name and not self.db.get_tag_from_name(tag_name): + trans = self.db.transaction_begin() + tag = Tag() + tag.set_name(tag_name) + tag.set_color(color_str) + tag.set_priority(self.db.get_number_of_tags()) + self.db.add_tag(tag, trans) + self.db.transaction_commit(trans, _('Add Tag (%s)') % tag_name) + self.tag_selected_rows(tag.get_handle()) - def tag_selected(self, tag_name): + def tag_selected_rows(self, tag_handle): """ - Tag the selected objects with the given tag. + Tag the selected rows with the given tag. """ view = self.uistate.viewmanager.active_page - view.add_tag(tag_name) + selected = view.selected_handles() + pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog, + popup_time=2) + status = progressdlg.LongOpStatus(msg=_("Adding Tags"), + total_steps=len(selected), + interval=len(selected)//20, + can_cancel=True) + pmon.add_op(status) + trans = self.db.transaction_begin() + for object_handle in selected: + status.heartbeat() + if status.should_cancel(): + break + view.add_tag(trans, object_handle, tag_handle) + if not status.was_cancelled(): + tag = self.db.get_tag_from_handle(tag_handle) + msg = _('Tag Selection (%s)') % tag.get_name() + self.db.transaction_commit(trans, msg) + status.end() def cb_menu_position(menu, button): """ @@ -212,11 +280,11 @@ def cb_menu_position(menu, button): return (x_pos, y_pos, False) -def make_callback(func, tag_name): +def make_callback(func, tag_handle): """ Generates a callback function based off the passed arguments """ - return lambda x: func(tag_name) + return lambda x: func(tag_handle) #------------------------------------------------------------------------- # @@ -246,15 +314,43 @@ class OrganizeTagsDialog(object): section=WIKI_HELP_SEC) else: break + + # Save changed priority values + trans = self.db.transaction_begin() + if self.__change_tag_priority(trans): + self.db.transaction_commit(trans, _('Change Tag Priority')) + self.top.destroy() + def __change_tag_priority(self, trans): + """ + Change the priority of the tags. The order of the list corresponds to + the priority of the tags. The top tag in the list is the highest + priority tag. + """ + changed = False + for new_priority, row in enumerate(self.namemodel.model): + if row[0] != new_priority: + changed = True + tag = self.db.get_tag_from_handle(row[1]) + tag.set_priority(new_priority) + self.db.commit_tag(tag, trans) + return changed + 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)]) + tags = [] + for tag in self.db.iter_tags(): + tags.append((tag.get_priority(), + tag.get_handle(), + tag.get_name(), + tag.get_color())) + + for row in sorted(tags): + self.namemodel.add(row) def _create_dialog(self): """ @@ -275,7 +371,9 @@ class OrganizeTagsDialog(object): box = gtk.HBox() top.vbox.pack_start(box, 1, 1, 5) - name_titles = [(_('Name'), NOSORT, 200), + name_titles = [('', NOSORT, 20, INTEGER), # Priority + ('', NOSORT, 100), # Handle + (_('Name'), NOSORT, 200), (_('Color'), NOSORT, 50, COLOR)] self.namelist = gtk.TreeView() self.namemodel = ListModel(self.namelist, name_titles) @@ -287,14 +385,20 @@ class OrganizeTagsDialog(object): bbox = gtk.VButtonBox() bbox.set_layout(gtk.BUTTONBOX_START) bbox.set_spacing(6) + up = gtk.Button(stock=gtk.STOCK_GO_UP) + down = gtk.Button(stock=gtk.STOCK_GO_DOWN) add = gtk.Button(stock=gtk.STOCK_ADD) edit = gtk.Button(stock=gtk.STOCK_EDIT) remove = gtk.Button(stock=gtk.STOCK_REMOVE) + up.connect('clicked', self.cb_up_clicked) + down.connect('clicked', self.cb_down_clicked) 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(up) + bbox.add(down) bbox.add(add) bbox.add(edit) bbox.add(remove) @@ -302,15 +406,36 @@ class OrganizeTagsDialog(object): top.show_all() return top + def cb_up_clicked(self, obj): + """ + Move the current selection up one row. + """ + row = self.namemodel.get_selected_row() + self.namemodel.move_up(row) + + def cb_down_clicked(self, obj): + """ + Move the current selection down one row. + """ + row = self.namemodel.get_selected_row() + self.namemodel.move_down(row) + 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() + if tag_name and not self.db.get_tag_from_name(tag_name): + trans = self.db.transaction_begin() + tag = Tag() + tag.set_name(tag_name) + tag.set_color(color_str) + priority = self.db.get_number_of_tags() # Lowest + tag.set_priority(priority) + handle = self.db.add_tag(tag, trans) + self.db.transaction_commit(trans, _('Add Tag (%s)') % tag_name) + self.namemodel.add((priority, handle, tag_name, color_str)) def cb_edit_clicked(self, button): """ @@ -320,8 +445,9 @@ class OrganizeTagsDialog(object): 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)) + handle = store.get_value(iter_, 1) + tag_name = store.get_value(iter_, 2) + old_color = gtk.gdk.Color(store.get_value(iter_, 3)) title = _("%(title)s - Gramps") % {'title': _("Pick a Color")} colorseldlg = gtk.ColorSelectionDialog(title) @@ -331,8 +457,12 @@ class OrganizeTagsDialog(object): 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) + trans = self.db.transaction_begin() + tag = self.db.get_tag_from_handle(handle) + tag.set_color(color_str) + self.db.commit_tag(tag, trans) + self.db.transaction_commit(trans, _('Edit Tag (%s)') % tag_name) + store.set_value(iter_, 3, color_str) colorseldlg.destroy() def cb_remove_clicked(self, button, top): @@ -342,7 +472,8 @@ class OrganizeTagsDialog(object): store, iter_ = self.namemodel.get_selected() if iter_ is None: return - tag_name = store.get_value(iter_, 0) + tag_handle = store.get_value(iter_, 1) + tag_name = store.get_value(iter_, 2) yes_no = QuestionDialog2( _("Remove tag '%s'?") % tag_name, @@ -352,36 +483,49 @@ class OrganizeTagsDialog(object): _("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() + fnc = {'Person': (self.db.get_person_from_handle, + self.db.commit_person), + 'Family': (self.db.get_family_from_handle, + self.db.commit_family), + 'Event': (self.db.get_event_from_handle, + self.db.commit_event), + 'Place': (self.db.get_place_from_handle, + self.db.commit_place), + 'Source': (self.db.get_source_from_handle, + self.db.commit_source), + 'Repository': (self.db.get_repository_from_handle, + self.db.commit_repository), + 'MediaObject': (self.db.get_object_from_handle, + self.db.commit_media_object), + 'Note': (self.db.get_note_from_handle, + self.db.commit_note)} + + links = [link for link in self.db.find_backlink_handles(tag_handle)] + pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog, + popup_time=2) + status = progressdlg.LongOpStatus(msg=_("Removing Tags"), + total_steps=len(links), + interval=len(links)//20, + can_cancel=True) + pmon.add_op(status) + + trans = self.db.transaction_begin() + for classname, handle in links: + status.heartbeat() + if status.should_cancel(): + break + obj = fnc[classname][0](handle) # get from handle + obj.remove_tag(tag_handle) + fnc[classname][1](obj, trans) # commit + + self.db.remove_tag(tag_handle, trans) + self.__change_tag_priority(trans) + if not status.was_cancelled(): + msg = _('Delete Tag (%s)') % tag_name + self.db.transaction_commit(trans, msg) + store.remove(iter_) + status.end() #------------------------------------------------------------------------- # diff --git a/src/gui/views/treemodels/peoplemodel.py b/src/gui/views/treemodels/peoplemodel.py index c346e9c94..cec9fa0a6 100644 --- a/src/gui/views/treemodels/peoplemodel.py +++ b/src/gui/views/treemodels/peoplemodel.py @@ -34,6 +34,7 @@ TreeModel for the GRAMPS Person tree. #------------------------------------------------------------------------- from gen.ggettext import gettext as _ import cgi +import locale #------------------------------------------------------------------------- # @@ -149,21 +150,6 @@ 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. @@ -450,13 +436,32 @@ class PeopleBaseModel(object): def column_int_id(self, data): return data[0] + def get_tag_name(self, tag_handle): + """ + Return the tag name from the given tag handle. + """ + return self.db.get_tag_from_handle(tag_handle).get_name() + def column_tag_color(self, data): - if len(data[COLUMN_TAGS]) > 0: - return self.tag_colors.get(data[COLUMN_TAGS][0]) - return None + """ + Return the tag color. + """ + tag_color = None + tag_priority = None + for handle in data[COLUMN_TAGS]: + tag = self.db.get_tag_from_handle(handle) + this_priority = tag.get_priority() + if tag_priority is None or this_priority < tag_priority: + tag_color = tag.get_color() + tag_priority = this_priority + return tag_color def column_tags(self, data): - return ','.join(data[COLUMN_TAGS]) + """ + Return the sorted list of tags. + """ + tag_list = map(self.get_tag_name, data[COLUMN_TAGS]) + return ','.join(sorted(tag_list, key=locale.strxfrm)) class PersonListModel(PeopleBaseModel, FlatBaseModel): """ diff --git a/src/gui/widgets/monitoredwidgets.py b/src/gui/widgets/monitoredwidgets.py index bf5effffc..ced95a936 100644 --- a/src/gui/widgets/monitoredwidgets.py +++ b/src/gui/widgets/monitoredwidgets.py @@ -50,7 +50,7 @@ import gtk from gen.ggettext import gettext as _ import AutoComp import DateEdit -from tageditor import TagEditor +from gui.widgets.tageditor import TagEditor #------------------------------------------------------------------------- # @@ -59,7 +59,8 @@ from tageditor import TagEditor #------------------------------------------------------------------------- class MonitoredCheckbox(object): - def __init__(self, obj, button, set_val, get_val, on_toggle=None, readonly = False): + def __init__(self, obj, button, set_val, get_val, on_toggle=None, + readonly = False): self.button = button self.button.connect('toggled', self._on_toggle) self.on_toggle = on_toggle @@ -610,15 +611,23 @@ 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, + def __init__(self, label, button, set_list, get_list, db, uistate, track, readonly=False): self.uistate = uistate self.track = track - + self.db = db self.set_list = set_list - self.tag_list = get_list() - self.all_tags = full_list + + self.tag_list = [] + for handle in get_list(): + tag = self.db.get_tag_from_handle(handle) + self.tag_list.append((handle, tag.get_name())) + + self.all_tags = [] + for tag in self.db.iter_tags(): + self.all_tags.append((tag.get_handle(), tag.get_name())) + self.label = label self.label.set_alignment(0, 0.5) image = gtk.Image() @@ -636,7 +645,7 @@ class MonitoredTagList(object): """ Display the tag list. """ - tag_text = ','.join(self.tag_list) + tag_text = ','.join(item[1] for item in self.tag_list) self.label.set_text(tag_text) self.label.set_tooltip_text(tag_text) @@ -649,5 +658,4 @@ class MonitoredTagList(object): if editor.return_list is not None: self.tag_list = editor.return_list self._display() - self.set_list(self.tag_list) - + self.set_list([item[0] for item in self.tag_list]) diff --git a/src/gui/widgets/tageditor.py b/src/gui/widgets/tageditor.py index 687af92c9..2a5b6fdf2 100644 --- a/src/gui/widgets/tageditor.py +++ b/src/gui/widgets/tageditor.py @@ -21,13 +21,6 @@ """ Tag editing module for Gramps. """ -#------------------------------------------------------------------------- -# -# Python modules -# -#------------------------------------------------------------------------- -import locale - #------------------------------------------------------------------------- # # GNOME modules @@ -75,8 +68,8 @@ class TagEditor(ManagedWindow.ManagedWindow): 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]) + for tag in full_list: + self.namemodel.add([tag[0], tag[1], tag in tag_list]) self.namemodel.connect_model() # The dialog is modal. We don't want to have several open dialogs of @@ -93,8 +86,9 @@ class TagEditor(ManagedWindow.ManagedWindow): break else: if response == gtk.RESPONSE_OK: - self.return_list = [row[0] for row in self.namemodel.model - if row[1]] + self.return_list = [(row[0], row[1]) + for row in self.namemodel.model + if row[2]] self.close() break @@ -110,8 +104,9 @@ class TagEditor(ManagedWindow.ManagedWindow): top.set_has_separator(False) top.vbox.set_spacing(5) - columns = [(_('Tag'), -1, 300), - (_(' '), -1, 25, TOGGLE, True, None)] + columns = [('', -1, 300), + (_('Tag'), -1, 300), + (' ', -1, 25, TOGGLE, True, None)] view = gtk.TreeView() self.namemodel = ListModel(view, columns) diff --git a/src/plugins/lib/libpersonview.py b/src/plugins/lib/libpersonview.py index 189f117ab..00fff9b55 100644 --- a/src/plugins/lib/libpersonview.py +++ b/src/plugins/lib/libpersonview.py @@ -58,7 +58,6 @@ 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 #------------------------------------------------------------------------- # @@ -66,7 +65,6 @@ import gui.widgets.progressdialog as progressdlg # #------------------------------------------------------------------------- from gen.ggettext import sgettext as _ -from bisect import insort_left #------------------------------------------------------------------------- # @@ -106,7 +104,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_TAGS, COL_CHAN]), + 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") @@ -146,6 +145,9 @@ class BasePersonView(ListView): uistate.connect('nameformat-changed', self.build_tree) def navigation_type(self): + """ + Return the navigation type of the view. + """ return 'Person' def get_bookmarks(self): @@ -163,8 +165,9 @@ class BasePersonView(ListView): def exact_search(self): """ Returns a tuple indicating columns requiring an exact search + 'female' contains the string 'male' so we need an exact search """ - return (BasePersonView.COL_GEN,) # Gender ('female' contains the string 'male') + return (BasePersonView.COL_GEN,) def get_stock(self): """ @@ -242,6 +245,9 @@ class BasePersonView(ListView): ''' def get_handle_from_gramps_id(self, gid): + """ + Return the handle of the person having the given Gramps ID. + """ obj = self.dbstate.db.get_person_from_gramps_id(gid) if obj: return obj.get_handle() @@ -249,6 +255,9 @@ class BasePersonView(ListView): return None def add(self, obj): + """ + Add a new person to the database. + """ person = gen.lib.Person() try: @@ -257,6 +266,9 @@ class BasePersonView(ListView): pass def edit(self, obj): + """ + Edit an existing person in the database. + """ for handle in self.selected_handles(): person = self.dbstate.db.get_person_from_handle(handle) try: @@ -265,6 +277,9 @@ class BasePersonView(ListView): pass def remove(self, obj): + """ + Remove a person from the database. + """ for sel in self.selected_handles(): person = self.dbstate.db.get_person_from_handle(sel) self.active_person = person @@ -331,8 +346,8 @@ class BasePersonView(ListView): self.all_action.add_actions([ ('FilterEdit', None, _('Person Filter Editor'), None, None, self.filter_editor), - ('Edit', gtk.STOCK_EDIT, _("action|_Edit..."), "Return", - _("Edit the selected person"), self.edit), + ('Edit', gtk.STOCK_EDIT, _("action|_Edit..."), + "Return", _("Edit the selected person"), self.edit), ('QuickReport', None, _("Quick View"), None, None, None), ('WebConnect', None, _("Web Connection"), None, None, None), ('Dummy', None, ' ', None, None, self.dummy_report), @@ -347,19 +362,26 @@ class BasePersonView(ListView): _("Remove the Selected Person"), self.remove), ('Merge', 'gramps-merge', _('_Merge...'), None, None, self.merge), - ('ExportTab', None, _('Export View...'), None, None, self.export), + ('ExportTab', None, _('Export View...'), None, None, + self.export), ]) self._add_action_group(self.edit_action) self._add_action_group(self.all_action) def enable_action_group(self, obj): + """ + Turns on the visibility of the View's action group. + """ ListView.enable_action_group(self, obj) self.all_action.set_visible(True) self.edit_action.set_visible(True) self.edit_action.set_sensitive(not self.dbstate.db.readonly) def disable_action_group(self): + """ + Turns off the visibility of the View's action group. + """ ListView.disable_action_group(self) self.all_action.set_visible(False) @@ -395,56 +417,22 @@ class BasePersonView(ListView): import Merge Merge.MergePeople(self.dbstate, self.uistate, mlist[0], mlist[1]) - def tag_updated(self, tag_name, tag_color): + def tag_updated(self, handle_list): """ 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() + all_links = set([]) + for tag_handle in handle_list: + links = set([link[1] for link in + self.dbstate.db.find_backlink_handles(tag_handle, + include_classes='Person')]) + all_links = all_links.union(links) + self.row_update(list(all_links)) - def add_tag(self, tag): + def add_tag(self, transaction, person_handle, tag_handle): """ - Add the given tag to the selected objects. + Add the given tag to the given person. """ - 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() + person = self.dbstate.db.get_person_from_handle(person_handle) + person.add_tag(tag_handle) + self.dbstate.db.commit_person(person, transaction)