Add tag list view, enable tag drag and drop

This commit is contained in:
Christopher Horn 2023-03-11 01:29:27 -05:00
parent 47f392ef70
commit 0b88f0cbbe
9 changed files with 679 additions and 13 deletions

@ -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)

@ -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(

@ -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')

@ -68,6 +68,7 @@ CATEGORY_ICON = {
'Media': 'gramps-media',
'Notes': 'gramps-notes',
'Citations': 'gramps-citation',
'Tags': 'gramps-tag'
}
#-------------------------------------------------------------------------

@ -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()

@ -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)

@ -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

@ -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()

@ -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,
)