diff --git a/gramps/gen/utils/db.py b/gramps/gen/utils/db.py
index bb842275f..d1ca39cd2 100644
--- a/gramps/gen/utils/db.py
+++ b/gramps/gen/utils/db.py
@@ -369,6 +369,10 @@ def navigation_label(db, nav_type, handle):
label = " ".join(label.split())
if len(label) > 40:
label = label[:40] + "..."
+ elif nav_type == 'Tag':
+ obj = db.get_tag_from_handle(handle)
+ if obj:
+ return ('[%s] %s' % (_('Tag'), obj.get_name()), obj)
if label and obj:
label = '[%s] %s' % (obj.get_gramps_id(), label)
diff --git a/gramps/gui/clipboard.py b/gramps/gui/clipboard.py
index e04f88f41..6fef3cb4a 100644
--- a/gramps/gui/clipboard.py
+++ b/gramps/gui/clipboard.py
@@ -94,7 +94,8 @@ for (name, icon) in (("media", "gramps-media"),
('source', 'gramps-source'),
('citation', 'gramps-citation'),
('text', 'gramps-font'),
- ('url', 'gramps-geo')):
+ ('url', 'gramps-geo'),
+ ('tag', 'gramps-tag')):
ICONS[name] = theme.load_icon(icon, 16, 0)
@@ -118,6 +119,7 @@ def map2class(target):
'place-link': ClipPlace,
'placeref': ClipPlaceRef,
'note-link': ClipNote,
+ 'tag': ClipTag,
'TEXT': ClipText}
return _d_[target] if target in _d_ else None
@@ -131,7 +133,8 @@ def obj2class(target):
'Event': ClipEvent,
'Media': ClipMediaObj,
'Place': ClipPlace,
- 'Note': ClipNote}
+ 'Note': ClipNote,
+ 'Tag': ClipTag}
return _d_[target] if target in _d_ else None
OBJ2TARGET = {"Person": Gdk.atom_intern('person-link', False),
@@ -142,7 +145,8 @@ OBJ2TARGET = {"Person": Gdk.atom_intern('person-link', False),
'Event': Gdk.atom_intern('pevent', False),
'Media': Gdk.atom_intern('media', False),
'Place': Gdk.atom_intern('place-link', False),
- 'Note': Gdk.atom_intern('note-link', False)}
+ 'Note': Gdk.atom_intern('note-link', False),
+ "Tag": Gdk.atom_intern('tag', False)}
def obj2target(target):
@@ -280,7 +284,7 @@ class ClipObjWrapper(ClipWrapper):
return False
for (clname, handle) in self._obj.get_referenced_handles_recursively():
- if obj2class(clname): # a class we care about (not tag)
+ if obj2class(clname): # a class we care about
if not clipdb.method("has_%s_handle", clname)(handle):
return False
@@ -424,6 +428,26 @@ class ClipUrl(ClipObjWrapper):
self._value = self._obj.get_description()
+class ClipTag(ClipHandleWrapper):
+
+ DROP_TARGETS = [DdTargets.TAG_LINK]
+ DRAG_TARGET = DdTargets.TAG_LINK
+ ICON = ICONS['tag']
+
+ def __init__(self, obj):
+ super(ClipTag, self).__init__(obj)
+ self._type = _("Tag")
+ self._objclass = "Tag"
+ self.refresh()
+
+ def refresh(self):
+ if self._handle:
+ value = clipdb.get_tag_from_handle(self._handle)
+ if value:
+ self._title = value.get_name()
+ self._value = value.get_color()
+
+
class ClipAttribute(ClipObjWrapper):
DROP_TARGETS = [DdTargets.ATTRIBUTE]
@@ -993,7 +1017,9 @@ class ClipboardListView:
'event-rebuild',
'repository-update',
'repository-rebuild',
- 'note-rebuild')
+ 'note-rebuild',
+ 'tag-update',
+ 'tag-rebuild')
for signal in db_signals:
clipdb.connect(signal, self.refresh_objects)
@@ -1022,6 +1048,8 @@ class ClipboardListView:
gen_del_obj(self.delete_object, 'place-link'))
clipdb.connect('note-delete',
gen_del_obj(self.delete_object, 'note-link'))
+ clipdb.connect('tag-delete',
+ gen_del_obj(self.delete_object, 'tag'))
# family-delete not needed, cannot be dragged!
self.refresh_objects()
@@ -1087,6 +1115,7 @@ class ClipboardListView:
self.register_wrapper_class(ClipChildRef)
self.register_wrapper_class(ClipText)
self.register_wrapper_class(ClipNote)
+ self.register_wrapper_class(ClipTag)
def register_wrapper_class(self, wrapper_class):
for drop_target in wrapper_class.DROP_TARGETS:
@@ -1593,6 +1622,7 @@ class MultiTreeView(Gtk.TreeView):
from .editors import (EditPerson, EditEvent, EditFamily, EditSource,
EditPlace, EditRepository, EditNote, EditMedia,
EditCitation)
+ from .views.tags import EditTag
if obj2class(objclass): # make sure it is an editable object
if self.dbstate.db.method('has_%s_handle', objclass)(handle):
g_object = self.dbstate.db.method(
diff --git a/gramps/gui/ddtargets.py b/gramps/gui/ddtargets.py
index cdb1ee937..5b1dee327 100644
--- a/gramps/gui/ddtargets.py
+++ b/gramps/gui/ddtargets.py
@@ -154,6 +154,7 @@ class _DdTargets:
self.URL = _DdType(self, 'url')
self.SURNAME = _DdType(self, 'surname')
self.CITATION_LINK = _DdType(self, 'citation-link')
+ self.TAG_LINK = _DdType(self, 'tag')
# List of all types that are used between
# gramps widgets but should not be exported
@@ -185,7 +186,8 @@ class _DdTargets:
self.SRCATTRIBUTE,
self.URL,
self.SURNAME,
- self.CITATION_LINK
+ self.CITATION_LINK,
+ self.TAG_LINK,
]
self.CHILD = _DdType(self, 'child')
diff --git a/gramps/gui/navigator.py b/gramps/gui/navigator.py
index faaf18f3a..4c2f3078f 100644
--- a/gramps/gui/navigator.py
+++ b/gramps/gui/navigator.py
@@ -68,6 +68,7 @@ CATEGORY_ICON = {
'Media': 'gramps-media',
'Notes': 'gramps-notes',
'Citations': 'gramps-citation',
+ 'Tags': 'gramps-tag'
}
#-------------------------------------------------------------------------
diff --git a/gramps/gui/views/listview.py b/gramps/gui/views/listview.py
index 4032d10dd..a3011440a 100644
--- a/gramps/gui/views/listview.py
+++ b/gramps/gui/views/listview.py
@@ -781,7 +781,8 @@ class ListView(NavigationView):
#force rebuild of the model on build of tree
self.dirty = True
self.build_tree()
- self.bookmarks.redraw()
+ if self.bookmarks:
+ self.bookmarks.redraw()
else:
self.dirty = True
@@ -894,7 +895,8 @@ class ListView(NavigationView):
if self.active:
# Save the currently selected handles, if any:
selected_ids = self.selected_handles()
- self.bookmarks.redraw()
+ if self.bookmarks:
+ self.bookmarks.redraw()
self.build_tree()
# Reselect one, if it still exists after rebuild:
nav_type = self.navigation_type()
diff --git a/gramps/gui/views/navigationview.py b/gramps/gui/views/navigationview.py
index df5a3fdcb..495b3c894 100644
--- a/gramps/gui/views/navigationview.py
+++ b/gramps/gui/views/navigationview.py
@@ -75,7 +75,12 @@ class NavigationView(PageView):
def __init__(self, title, pdata, state, uistate, bm_type, nav_group):
PageView.__init__(self, title, pdata, state, uistate)
- self.bookmarks = bm_type(self.dbstate, self.uistate, self.change_active)
+ if bm_type:
+ self.bookmarks = bm_type(
+ self.dbstate, self.uistate, self.change_active
+ )
+ else:
+ self.bookmarks = None
self.fwd_action = None
self.back_action = None
@@ -103,7 +108,8 @@ class NavigationView(PageView):
Define menu actions.
"""
PageView.define_actions(self)
- self.bookmark_actions()
+ if self.bookmarks:
+ self.bookmark_actions()
self.navigation_actions()
def disable_action_group(self):
@@ -151,7 +157,8 @@ class NavigationView(PageView):
Called when the page becomes active (displayed).
"""
PageView.set_active(self)
- self.bookmarks.display()
+ if self.bookmarks:
+ self.bookmarks.display()
hobj = self.get_history()
self.active_signal = hobj.connect('active-changed', self.goto_active)
@@ -166,7 +173,8 @@ class NavigationView(PageView):
"""
if self.active:
PageView.set_inactive(self)
- self.bookmarks.undisplay()
+ if self.bookmarks:
+ self.bookmarks.undisplay()
hobj = self.get_history()
hobj.disconnect(self.active_signal)
hobj.disconnect(self.mru_signal)
diff --git a/gramps/gui/widgets/monitoredwidgets.py b/gramps/gui/widgets/monitoredwidgets.py
index 3b694f690..05c52d51c 100644
--- a/gramps/gui/widgets/monitoredwidgets.py
+++ b/gramps/gui/widgets/monitoredwidgets.py
@@ -30,6 +30,7 @@ __all__ = ["MonitoredCheckbox", "MonitoredEntry",
# Standard python modules
#
#-------------------------------------------------------------------------
+import pickle
import logging
_LOG = logging.getLogger(".widgets.monitoredwidgets")
@@ -54,13 +55,13 @@ from ..autocomp import StandardCustomSelector, fill_entry
from gramps.gen.datehandler import displayer, parser
from gramps.gen.lib.date import Date, NextYear
from gramps.gen.errors import ValidationError
+from gramps.gui.ddtargets import DdTargets
#-------------------------------------------------------------------------
#
# constants
#
#------------------------------------------------------------------------
-
_RETURN = Gdk.keyval_from_name("Return")
_KP_ENTER = Gdk.keyval_from_name("KP_Enter")
@@ -813,6 +814,8 @@ class MonitoredTagList:
self.label = label
self.label.set_halign(Gtk.Align.START)
self.label.set_ellipsize(Pango.EllipsizeMode.END)
+ self.label.drag_dest_set(Gtk.DestDefaults.ALL, [DdTargets.TAG_LINK.target()], Gdk.DragAction.COPY)
+ self.label.connect('drag_data_received', self.tag_dropped)
image = Gtk.Image()
image.set_from_icon_name('gramps-tag', Gtk.IconSize.MENU)
button.set_image (image)
@@ -856,3 +859,19 @@ class MonitoredTagList:
self.set_list([item[0] for item in self.tag_list])
return True
return False
+
+ def tag_dropped(self, _dummy_widget, _dummy_context, _dummy_x,
+ _dummy_y, data, _dummy_info, _dummy_time):
+ """
+ Add dropped tag if not in list.
+ """
+ if data and data.get_data():
+ (dnd_type, obj_id, handle, val) = pickle.loads(data.get_data())
+ for item in self.tag_list:
+ if item[0] == handle:
+ return True
+ tag = self.db.get_tag_from_handle(handle)
+ self.tag_list.append((handle, tag.get_name()))
+ self._display()
+ self.set_list([item[0] for item in self.tag_list])
+ return True
diff --git a/gramps/plugins/view/tagview.py b/gramps/plugins/view/tagview.py
new file mode 100644
index 000000000..770d1ff57
--- /dev/null
+++ b/gramps/plugins/view/tagview.py
@@ -0,0 +1,583 @@
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2001-2006 Donald N. Allingham
+# Copyright (C) 2008 Gary Burton
+# Copyright (C) 2010 Nick Hall
+# Copyright (C) 2022 Christopher Horn
+#
+# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+"""
+Tag View.
+"""
+
+# -------------------------------------------------------------------------
+#
+# GTK/Gnome Modules
+#
+# -------------------------------------------------------------------------
+from gi.repository import Gtk
+
+# -------------------------------------------------------------------------
+#
+# Gramps Modules
+#
+# -------------------------------------------------------------------------
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+from gramps.gen.datehandler import format_time
+from gramps.gen.db import DbTxn
+from gramps.gen.errors import WindowActiveError
+from gramps.gen.lib import Tag
+from gramps.gui.ddtargets import DdTargets
+from gramps.gui.dialog import QuestionDialog2
+from gramps.gui.views.listview import ListView, TEXT
+from gramps.gui.views.tags import EditTag, OrganizeTagsDialog
+from gramps.gui.views.treemodels.flatbasemodel import FlatBaseModel
+import gramps.gui.widgets.progressdialog as progressdlg
+
+_ = glocale.translation.sgettext
+
+
+(POS_HANDLE, POS_NAME, POS_COLOR, POS_PRIORITY, POS_CHANGE) = list(range(5))
+
+
+# -------------------------------------------------------------------------
+#
+# TagModel
+#
+# -------------------------------------------------------------------------
+class TagModel(FlatBaseModel):
+ """
+ Basic model for a Tag list
+ """
+
+ def __init__(
+ self,
+ db,
+ uistate,
+ scol=0,
+ order=Gtk.SortType.ASCENDING,
+ search=None,
+ skip=None,
+ sort_map=None,
+ ):
+ """Setup initial values for instance variables."""
+ skip = skip or set()
+ self.gen_cursor = db.get_tag_cursor
+ self.map = db.get_raw_tag_data
+ self.fmap = [
+ self.column_name,
+ self.column_color,
+ self.column_priority,
+ self.column_change,
+ self.column_count,
+ ]
+ self.smap = [
+ self.column_name,
+ self.column_color,
+ self.column_priority,
+ self.sort_change,
+ self.sort_count,
+ ]
+ FlatBaseModel.__init__(
+ self,
+ db,
+ uistate,
+ scol,
+ order,
+ search=search,
+ skip=skip,
+ sort_map=sort_map,
+ )
+
+ def destroy(self):
+ """
+ Unset all elements that can prevent garbage collection
+ """
+ self.db = None
+ self.gen_cursor = None
+ self.map = None
+ self.fmap = None
+ self.smap = None
+ FlatBaseModel.destroy(self)
+
+ def color_column(self):
+ """
+ Return the color column.
+ """
+ return 1
+
+ def on_get_n_columns(self):
+ """
+ Return the column number of the Tag tab.
+ """
+ return len(self.fmap) + 1
+
+ def column_handle(self, data):
+ """
+ Return the handle of the Tag.
+ """
+ return data[POS_HANDLE]
+
+ def column_name(self, data):
+ """
+ Return the name of the Tag in readable format.
+ """
+ return data[POS_NAME]
+
+ def column_priority(self, data):
+ """
+ Return the priority of the Tag.
+ """
+ return "%03d" % data[POS_PRIORITY]
+
+ def column_color(self, data):
+ """
+ Return the color.
+ """
+ return data[POS_COLOR]
+
+ def sort_change(self, data):
+ """
+ Return sort value for change.
+ """
+ return "%012x" % data[POS_CHANGE]
+
+ def column_change(self, data):
+ """
+ Return formatted change time.
+ """
+ return format_time(data[POS_CHANGE])
+
+ def sort_count(self, data):
+ """
+ Return sort value for count of tagged items.
+ """
+ return "%012d" % len(
+ list(self.db.find_backlink_handles(data[POS_HANDLE]))
+ )
+
+ def column_count(self, data):
+ """
+ Return count of tagged items.
+ """
+ return int(len(list(self.db.find_backlink_handles(data[POS_HANDLE]))))
+
+
+# -------------------------------------------------------------------------
+#
+# TagView
+#
+# -------------------------------------------------------------------------
+class TagView(ListView):
+ """
+ TagView, a normal flat listview for the tags
+ """
+
+ COL_NAME = 0
+ COL_COLO = 1
+ COL_PRIO = 2
+ COL_CHAN = 3
+ COL_COUNT = 4
+
+ # column definitions
+ COLUMNS = [
+ (_("Name"), TEXT, None),
+ (_("Color"), TEXT, None),
+ (_("Priority"), TEXT, None),
+ (_("Last Changed"), TEXT, None),
+ (_("Tagged Items"), TEXT, None),
+ ]
+ # default setting with visible columns, order of the col, and their size
+ CONFIGSETTINGS = (
+ (
+ "columns.visible",
+ [COL_NAME, COL_COLO, COL_PRIO, COL_CHAN, COL_COUNT],
+ ),
+ ("columns.rank", [COL_NAME, COL_COLO, COL_PRIO, COL_CHAN, COL_COUNT]),
+ ("columns.size", [330, 150, 70, 200, 50]),
+ )
+
+ ADD_MSG = _("Add a new tag")
+ EDIT_MSG = _("Edit the selected tag")
+ DEL_MSG = _("Delete the selected tag")
+ ORGANIZE_MSG = _("Organize tags")
+
+ FILTER_TYPE = "Tag"
+ QR_CATEGORY = -1
+
+ def __init__(self, pdata, dbstate, uistate, nav_group=0):
+ signal_map = {
+ "tag-add": self.row_add,
+ "tag-update": self.row_update,
+ "tag-delete": self.row_delete,
+ "tag-rebuild": self.object_build,
+ }
+
+ # Work around for modify_statusbar issues
+ if "Tag" not in uistate.NAV2MES:
+ uistate.NAV2MES["Tag"] = ""
+
+ ListView.__init__(
+ self,
+ _("Tags"),
+ pdata,
+ dbstate,
+ uistate,
+ TagModel,
+ signal_map,
+ None,
+ nav_group,
+ filter_class=None,
+ multiple=False,
+ )
+
+ self.additional_uis.append(self.additional_ui)
+
+ def navigation_type(self):
+ """
+ Return the navigation type.
+ """
+ return "Tag"
+
+ def drag_info(self):
+ """
+ Return a drag type of TAG_LINK
+ """
+ return DdTargets.TAG_LINK
+
+ def get_stock(self):
+ """
+ Return the gramps-tag stock icon
+ """
+ return "gramps-tag"
+
+ additional_ui = [ # Defines the UI string for UIManager
+ """
+