From 0b88f0cbbe921353fbe915d7e2b3e364197e177f Mon Sep 17 00:00:00 2001
From: Christopher Horn <cdhorn@embarqmail.com>
Date: Sat, 11 Mar 2023 01:29:27 -0500
Subject: [PATCH] Add tag list view, enable tag drag and drop

---
 gramps/gen/utils/db.py                 |   4 +
 gramps/gui/clipboard.py                |  40 +-
 gramps/gui/ddtargets.py                |   4 +-
 gramps/gui/navigator.py                |   1 +
 gramps/gui/views/listview.py           |   6 +-
 gramps/gui/views/navigationview.py     |  16 +-
 gramps/gui/widgets/monitoredwidgets.py |  21 +-
 gramps/plugins/view/tagview.py         | 583 +++++++++++++++++++++++++
 gramps/plugins/view/view.gpr.py        |  17 +
 9 files changed, 679 insertions(+), 13 deletions(-)
 create mode 100644 gramps/plugins/view/tagview.py

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
+        """
+      <placeholder id="LocalExport">
+        <item>
+          <attribute name="action">win.ExportTab</attribute>
+          <attribute name="label" translatable="yes">Export View...</attribute>
+        </item>
+      </placeholder>
+""",
+        """
+      <placeholder id="CommonGo">
+      <section>
+        <item>
+          <attribute name="action">win.Back</attribute>
+          <attribute name="label" translatable="yes">_Back</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Forward</attribute>
+          <attribute name="label" translatable="yes">_Forward</attribute>
+        </item>
+      </section>
+      </placeholder>
+""",
+        """
+      <section id='CommonEdit' groups='RW'>
+        <item>
+          <attribute name="action">win.Add</attribute>
+          <attribute name="label" translatable="yes">_Add...</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Edit</attribute>
+          <attribute name="label">%s</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Remove</attribute>
+          <attribute name="label" translatable="yes">_Delete</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Organize</attribute>
+          <attribute name="label" translatable="yes">_Organize...</attribute>
+        </item>
+      </section>
+"""
+        % _(
+            "_Edit...", "action"
+        ),  # to use sgettext() # Following are the Toolbar items
+        """
+    <placeholder id='CommonNavigation'>
+    <child groups='RO'>
+      <object class="GtkToolButton">
+        <property name="icon-name">go-previous</property>
+        <property name="action-name">win.Back</property>
+        <property name="tooltip_text" translatable="yes">"""
+        """Go to the previous object in the history</property>
+        <property name="label" translatable="yes">_Back</property>
+        <property name="use-underline">True</property>
+      </object>
+      <packing>
+        <property name="homogeneous">False</property>
+      </packing>
+    </child>
+    <child groups='RO'>
+      <object class="GtkToolButton">
+        <property name="icon-name">go-next</property>
+        <property name="action-name">win.Forward</property>
+        <property name="tooltip_text" translatable="yes">"""
+        """Go to the next object in the history</property>
+        <property name="label" translatable="yes">_Forward</property>
+        <property name="use-underline">True</property>
+      </object>
+      <packing>
+        <property name="homogeneous">False</property>
+      </packing>
+    </child>
+    </placeholder>
+""",
+        """
+    <placeholder id='BarCommonEdit'>
+    <child groups='RW'>
+      <object class="GtkToolButton">
+        <property name="icon-name">list-add</property>
+        <property name="action-name">win.Add</property>
+        <property name="tooltip_text">%s</property>
+        <property name="label" translatable="yes">_Add...</property>
+        <property name="use-underline">True</property>
+      </object>
+      <packing>
+        <property name="homogeneous">False</property>
+      </packing>
+    </child>
+    <child groups='RW'>
+      <object class="GtkToolButton">
+        <property name="icon-name">gtk-edit</property>
+        <property name="action-name">win.Edit</property>
+        <property name="tooltip_text">%s</property>
+        <property name="label" translatable="yes">Edit...</property>
+        <property name="use-underline">True</property>
+      </object>
+      <packing>
+        <property name="homogeneous">False</property>
+      </packing>
+    </child>
+    <child groups='RW'>
+      <object class="GtkToolButton">
+        <property name="icon-name">list-remove</property>
+        <property name="action-name">win.Remove</property>
+        <property name="tooltip_text">%s</property>
+        <property name="label" translatable="yes">_Delete</property>
+        <property name="use-underline">True</property>
+      </object>
+      <packing>
+        <property name="homogeneous">False</property>
+      </packing>
+    </child>
+    <child groups='RW'>
+      <object class="GtkToolButton">
+        <property name="icon-name">view-sort-descending</property>
+        <property name="action-name">win.Organize</property>
+        <property name="tooltip_text">%s</property>
+        <property name="label" translatable="yes">_Organize</property>
+        <property name="use-underline">True</property>
+      </object>
+      <packing>
+        <property name="homogeneous">False</property>
+      </packing>
+    </child>
+    </placeholder>
+"""
+        % (ADD_MSG, EDIT_MSG, DEL_MSG, ORGANIZE_MSG),
+        """
+    <menu id="Popup">
+      <section>
+        <item>
+          <attribute name="action">win.Back</attribute>
+          <attribute name="label" translatable="yes">_Back</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Forward</attribute>
+          <attribute name="label" translatable="yes">Forward</attribute>
+        </item>
+      </section>
+      <section id="PopUpTree">
+      </section>
+      <section>
+        <item>
+          <attribute name="action">win.Add</attribute>
+          <attribute name="label" translatable="yes">_Add...</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Edit</attribute>
+          <attribute name="label">%s</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Remove</attribute>
+          <attribute name="label" translatable="yes">_Delete</attribute>
+        </item>
+        <item>
+          <attribute name="action">win.Organize</attribute>
+          <attribute name="label" translatable="yes">_Organize...</attribute>
+        </item>
+      </section>
+      <section>
+        <placeholder id='QuickReport'>
+        </placeholder>
+      </section>
+    </menu>
+    """
+        % _("_Edit...", "action"),  # to use sgettext()
+    ]
+    # Leave QuickReport as placeholder
+
+    def define_actions(self):
+        """
+        Define actions for the view.
+        """
+        ListView.define_actions(self)
+        self.edit_action.add_actions(
+            [("Organize", self.organize, "<PRIMARY>Home")]
+        )
+
+    def set_active(self):
+        """
+        Set view active.
+        """
+        ListView.set_active(self)
+        self.uistate.viewmanager.tags.tag_disable()
+
+    def set_inactive(self):
+        """
+        Set view inactive.
+        """
+        ListView.set_inactive(self)
+        self.uistate.viewmanager.tags.tag_enable(update_menu=False)
+
+    def get_handle_from_gramps_id(self, gid):
+        """
+        Not applicable.
+        """
+        return None
+
+    def add(self, *obj):
+        """
+        Add new tag.
+        """
+        try:
+            EditTag(self.dbstate.db, self.uistate, [], Tag())
+        except WindowActiveError:
+            pass
+
+    def remove(self, *obj):
+        """
+        Remove selected tag.
+        """
+        handles = self.selected_handles()
+        if handles:
+            tag = self.dbstate.db.get_tag_from_handle(handles[0])
+            delete_tag(self.uistate.window, self.dbstate.db, tag)
+
+    def edit(self, *obj):
+        """
+        Edit selected tag.
+        """
+        for handle in self.selected_handles():
+            tag = self.dbstate.db.get_tag_from_handle(handle)
+            try:
+                EditTag(self.dbstate.db, self.uistate, [], tag)
+            except WindowActiveError:
+                pass
+
+    def organize(self, *_dummy_obj):
+        """
+        Launch organize tool.
+        """
+        try:
+            OrganizeTagsDialog(self.dbstate.db, self.uistate, [])
+        except WindowActiveError:
+            pass
+
+    def merge(self, *obj):
+        """
+        Not supported for now.
+        """
+
+    def tag_updated(self, handle_list):
+        """
+        Not applicable.
+        """
+
+    def get_default_gramplets(self):
+        """
+        Define the default gramplets for the sidebar and bottombar.
+        """
+        return ((), ())
+
+    def remove_object_from_handle(self, *args, **kwargs):
+        """
+        Not applicable.
+        """
+
+
+def delete_tag(window, db, tag):
+    """
+    Handle tag deletion, extracted from OrganizeTagsDialog.
+    """
+    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"),
+        parent=window,
+    )
+    prompt = yes_no.run()
+    if prompt:
+        fnc = {
+            "Person": (db.get_person_from_handle, db.commit_person),
+            "Family": (db.get_family_from_handle, db.commit_family),
+            "Event": (db.get_event_from_handle, db.commit_event),
+            "Place": (db.get_place_from_handle, db.commit_place),
+            "Source": (db.get_source_from_handle, db.commit_source),
+            "Citation": (db.get_citation_from_handle, db.commit_citation),
+            "Repository": (
+                db.get_repository_from_handle,
+                db.commit_repository,
+            ),
+            "Media": (db.get_media_from_handle, db.commit_media),
+            "Note": (db.get_note_from_handle, db.commit_note),
+        }
+
+        links = list(db.find_backlink_handles(tag.handle))
+        # Make the dialog modal so that the user can't start another
+        # database transaction while the one removing tags is still running.
+        pmon = progressdlg.ProgressMonitor(
+            progressdlg.GtkProgressDialog,
+            ("", window, Gtk.DialogFlags.MODAL),
+            popup_time=2,
+        )
+        status = progressdlg.LongOpStatus(
+            msg=_("Removing Tags"),
+            total_steps=len(links),
+            interval=len(links) // 20,
+        )
+        pmon.add_op(status)
+
+        msg = _("Delete Tag (%s)") % tag.name
+        with DbTxn(msg, db) as trans:
+            for classname, handle in links:
+                status.heartbeat()
+                obj = fnc[classname][0](handle)  # get from handle
+                obj.remove_tag(tag.handle)
+                fnc[classname][1](obj, trans)  # commit
+
+            db.remove_tag(tag.handle, trans)
+        status.end()
diff --git a/gramps/plugins/view/view.gpr.py b/gramps/plugins/view/view.gpr.py
index 53cc78908..27b410004 100644
--- a/gramps/plugins/view/view.gpr.py
+++ b/gramps/plugins/view/view.gpr.py
@@ -307,3 +307,20 @@ category = ("Sources", _("Sources")),
 viewclass = 'CitationTreeView',
 stock_icon = 'gramps-tree-select',
   )
+
+register(
+    VIEW,
+    id="tagview",
+    name=_("Tags"),
+    description=_("The view showing all the tags"),
+    version="1.0",
+    gramps_target_version=MODULE_VERSION,
+    status=STABLE,
+    fname="tagview.py",
+    authors=["The Gramps project"],
+    authors_email=["http://gramps-project.org"],
+    category=("Tags", _("Tags")),
+    stock_icon="gramps-tag",
+    viewclass="TagView",
+    order=START,
+)