From d1aa42bf8dabc92e50ae767c77edaaf343dfc4ad Mon Sep 17 00:00:00 2001 From: vantu5z Date: Tue, 31 May 2022 11:55:06 +0300 Subject: [PATCH 01/15] add drag and drop function --- GraphView/graphview.py | 80 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index c4b30e0ea..3f2ab956e 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -46,6 +46,7 @@ from collections import abc, deque import gi from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, Pango +import pickle #------------------------------------------------------------------------- # @@ -70,6 +71,7 @@ from gramps.gen.utils.libformatting import FormattingHelper from gramps.gen.utils.thumbnails import get_thumbnail_path +from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import (OptionDialog, ErrorDialog, QuestionDialog2, WarningDialog) from gramps.gui.display import display_url @@ -981,11 +983,13 @@ def __init__(self, view, dbstate, uistate): self._last_x = 0 self._last_y = 0 self._in_move = False + self._in_drag = False self.view = view self.dbstate = dbstate self.uistate = uistate self.parser = None self.active_person_handle = None + self.drag_person = None self.actions = Actions(dbstate, uistate, self.view.bookmarks) self.actions.connect('rebuild-graph', self.view.build_tree) @@ -1152,6 +1156,22 @@ def __init__(self, view, dbstate, uistate): self.retest_font = True # flag indicates need to resize font self.bold_size = self.norm_size = 0 # font sizes to send to dot + # setup drag and drop + drag_widget = self.get_widget() + drag_widget.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], + Gdk.DragAction.COPY) + drag_widget.connect("drag_data_get", self.cb_drag_data_get) + drag_widget.connect("drag_begin", self.cb_drag_begin) + drag_widget.connect("drag_end", self.cb_drag_end) + + tglist = Gtk.TargetList.new([]) + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id) + # allow drag to a text document, info on drag_get will be 0L ! + tglist.add_text_targets(0) + drag_widget.drag_source_set_target_list(tglist) + def add_popover(self, widget, container): """ Add popover for button. @@ -1470,6 +1490,7 @@ def populate(self, active_person): if self.uistate.window.get_window().is_visible(): process_pending_events() + self._in_drag = False self.clear() self.active_person_handle = active_person @@ -1620,6 +1641,7 @@ def button_release(self, item, target, event): """ Exit from scroll mode when button release. """ + self._in_drag = False button = event.get_button()[1] if((button == 1 or button == 2) and event.type == getattr(Gdk.EventType, "BUTTON_RELEASE")): @@ -1648,6 +1670,35 @@ def motion_notify_event(self, _item, _target, event): (event.y_root - self._last_y) * scale_coef) self.vadjustment.set_value(new_y) return True + + if self._in_drag and (event.type == Gdk.EventType.MOTION_NOTIFY): + # start drag when cursor moved more then 5 + # to separate it from simple click + if ((abs(self._last_x - event.x) > 5) + or (abs(self._last_x - event.x) > 5)): + self.uistate.set_busy_cursor(False) + # Remove all single click events + for click_item in self.click_events: + if not click_item.is_destroyed(): + GLib.source_remove(click_item.get_id()) + self.click_events.clear() + + # translate to drag_widget coords + drag_widget = self.get_widget() + scale_coef = self.canvas.get_scale() + bounds = self.canvas.get_root_item().get_bounds() + height_canvas = bounds.y2 - bounds.y1 + x = self._last_x * scale_coef - self.hadjustment.get_value() + y = ((height_canvas + self._last_y) * scale_coef - + self.vadjustment.get_value()) + + drag_widget.drag_begin_with_coordinates( + drag_widget.drag_source_get_target_list(), + Gdk.DragAction.COPY, + Gdk.ModifierType.BUTTON1_MASK, + event, + x, y) + return True return False def set_zoom(self, value): @@ -1708,6 +1759,11 @@ def select_node(self, item, target, event): context = GLib.main_context_default() self.click_events.append(context.find_source_by_id(click_event_id)) + # go to drag mode, applyed on motion event + self._in_drag = True + self._last_x = event.x + self._last_y = event.y + elif button == 3 and node_class: # right mouse if node_class == 'node': self.menu = PopupMenu(self, 'person', handle) @@ -1723,6 +1779,30 @@ def select_node(self, item, target, event): return True + def cb_drag_begin(self, widget, data): + """Set up some inital conditions for drag. Set up icon.""" + self._in_drag = True + widget.drag_source_set_icon_name('gramps-person') + + def cb_drag_end(self, widget, data): + """Set up some inital conditions for drag. Set up icon.""" + self._in_drag = False + + def cb_drag_data_get(self, widget, context, sel_data, info, time): + """ + Returned parameters after drag. + Specified for 'person-link', for others return text info about person. + """ + tgs = [x.name() for x in context.list_targets()] + if info == DdTargets.PERSON_LINK.app_id: + data = (DdTargets.PERSON_LINK.drag_type, + id(self), self.drag_person.handle, 0) + sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) + elif ('TEXT' in tgs or 'text/plain' in tgs) and info == 0: + format_helper = FormattingHelper(self.dbstate) + sel_data.set_text( + format_helper.format_person(self.drag_person, 11),-1) + def find_a_parent(self, handle): """ Locate a parent from the first family that the selected person is a From 3b3e52327aac2a85eba1a0497961410dbbf379a8 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Mon, 23 Sep 2019 13:49:41 +0300 Subject: [PATCH 02/15] fix drag icon --- GraphView/graphview.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 3f2ab956e..6eaddec1f 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1692,12 +1692,15 @@ def motion_notify_event(self, _item, _target, event): y = ((height_canvas + self._last_y) * scale_coef - self.vadjustment.get_value()) - drag_widget.drag_begin_with_coordinates( + context = drag_widget.drag_begin_with_coordinates( drag_widget.drag_source_get_target_list(), Gdk.DragAction.COPY, Gdk.ModifierType.BUTTON1_MASK, event, x, y) + # set icon for person drag + Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) + return True return False @@ -1780,12 +1783,15 @@ def select_node(self, item, target, event): return True def cb_drag_begin(self, widget, data): - """Set up some inital conditions for drag. Set up icon.""" + """ + Called on start drag. + """ self._in_drag = True - widget.drag_source_set_icon_name('gramps-person') def cb_drag_end(self, widget, data): - """Set up some inital conditions for drag. Set up icon.""" + """ + Called when drag is end. + """ self._in_drag = False def cb_drag_data_get(self, widget, context, sel_data, info, time): From 5cc9753fda8575bfa38f1b356055567590e0773b Mon Sep 17 00:00:00 2001 From: vantu5z Date: Wed, 25 Sep 2019 08:52:17 +0300 Subject: [PATCH 03/15] little fixes --- GraphView/graphview.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 6eaddec1f..78623b7ec 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1675,7 +1675,7 @@ def motion_notify_event(self, _item, _target, event): # start drag when cursor moved more then 5 # to separate it from simple click if ((abs(self._last_x - event.x) > 5) - or (abs(self._last_x - event.x) > 5)): + or (abs(self._last_y - event.y) > 5)): self.uistate.set_busy_cursor(False) # Remove all single click events for click_item in self.click_events: @@ -1692,15 +1692,12 @@ def motion_notify_event(self, _item, _target, event): y = ((height_canvas + self._last_y) * scale_coef - self.vadjustment.get_value()) - context = drag_widget.drag_begin_with_coordinates( + drag_widget.drag_begin_with_coordinates( drag_widget.drag_source_get_target_list(), Gdk.DragAction.COPY, Gdk.ModifierType.BUTTON1_MASK, event, x, y) - # set icon for person drag - Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) - return True return False @@ -1782,13 +1779,14 @@ def select_node(self, item, target, event): return True - def cb_drag_begin(self, widget, data): + def cb_drag_begin(self, widget, context): """ Called on start drag. """ - self._in_drag = True + # set icon for person drag + Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) - def cb_drag_end(self, widget, data): + def cb_drag_end(self, widget, context): """ Called when drag is end. """ @@ -1802,7 +1800,7 @@ def cb_drag_data_get(self, widget, context, sel_data, info, time): tgs = [x.name() for x in context.list_targets()] if info == DdTargets.PERSON_LINK.app_id: data = (DdTargets.PERSON_LINK.drag_type, - id(self), self.drag_person.handle, 0) + id(widget), self.drag_person.handle, 0) sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) elif ('TEXT' in tgs or 'text/plain' in tgs) and info == 0: format_helper = FormattingHelper(self.dbstate) From edcf8054e6359406911e5cf2c25ca1faa817bdf5 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Wed, 25 Sep 2019 10:05:49 +0300 Subject: [PATCH 04/15] change DnD setup we dont need (drag_source_set) as we use (drag_begin_with_coordinates) --- GraphView/graphview.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 78623b7ec..18278261d 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1158,20 +1158,10 @@ def __init__(self, view, dbstate, uistate): # setup drag and drop drag_widget = self.get_widget() - drag_widget.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], - Gdk.DragAction.COPY) drag_widget.connect("drag_data_get", self.cb_drag_data_get) drag_widget.connect("drag_begin", self.cb_drag_begin) drag_widget.connect("drag_end", self.cb_drag_end) - tglist = Gtk.TargetList.new([]) - tglist.add(DdTargets.PERSON_LINK.atom_drag_type, - DdTargets.PERSON_LINK.target_flags, - DdTargets.PERSON_LINK.app_id) - # allow drag to a text document, info on drag_get will be 0L ! - tglist.add_text_targets(0) - drag_widget.drag_source_set_target_list(tglist) - def add_popover(self, widget, container): """ Add popover for button. @@ -1684,7 +1674,6 @@ def motion_notify_event(self, _item, _target, event): self.click_events.clear() # translate to drag_widget coords - drag_widget = self.get_widget() scale_coef = self.canvas.get_scale() bounds = self.canvas.get_root_item().get_bounds() height_canvas = bounds.y2 - bounds.y1 @@ -1692,8 +1681,18 @@ def motion_notify_event(self, _item, _target, event): y = ((height_canvas + self._last_y) * scale_coef - self.vadjustment.get_value()) + # setup targets + tglist = Gtk.TargetList.new([]) + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id) + # allow drag to a text document, info on drag_get will be 0L ! + tglist.add_text_targets(0) + + # start drag + drag_widget = self.get_widget() drag_widget.drag_begin_with_coordinates( - drag_widget.drag_source_get_target_list(), + tglist, Gdk.DragAction.COPY, Gdk.ModifierType.BUTTON1_MASK, event, From 272158ce351143fbea3a9e9dbed87a7d3ab516c5 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Wed, 25 Sep 2019 15:16:39 +0300 Subject: [PATCH 05/15] add family DnD --- GraphView/graphview.py | 88 ++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 21 deletions(-) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 18278261d..03f96a150 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1664,8 +1664,8 @@ def motion_notify_event(self, _item, _target, event): if self._in_drag and (event.type == Gdk.EventType.MOTION_NOTIFY): # start drag when cursor moved more then 5 # to separate it from simple click - if ((abs(self._last_x - event.x) > 5) - or (abs(self._last_y - event.y) > 5)): + if ((abs(self._last_x - event.x_root) > 5) + or (abs(self._last_y - event.y_root) > 5)): self.uistate.set_busy_cursor(False) # Remove all single click events for click_item in self.click_events: @@ -1683,18 +1683,29 @@ def motion_notify_event(self, _item, _target, event): # setup targets tglist = Gtk.TargetList.new([]) - tglist.add(DdTargets.PERSON_LINK.atom_drag_type, - DdTargets.PERSON_LINK.target_flags, - DdTargets.PERSON_LINK.app_id) - # allow drag to a text document, info on drag_get will be 0L ! - tglist.add_text_targets(0) + if self.drag_person is not None: + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id, + ) + # allow drag to a text document, info on drag_get will be 0 + tglist.add_text_targets(0) + if self.drag_family is not None: + tglist.add(DdTargets.FAMILY_LINK.atom_drag_type, + DdTargets.FAMILY_LINK.target_flags, + DdTargets.FAMILY_LINK.app_id, + ) + # allow drag to a text document, info on drag_get will be 1 + tglist.add_text_targets(1) - # start drag drag_widget = self.get_widget() + # change event window + event.window = drag_widget.get_window() + # start drag drag_widget.drag_begin_with_coordinates( tglist, Gdk.DragAction.COPY, - Gdk.ModifierType.BUTTON1_MASK, + Gdk.KEY_Pointer_Button1, event, x, y) return True @@ -1739,6 +1750,20 @@ def select_node(self, item, target, event): if event.type != getattr(Gdk.EventType, "BUTTON_PRESS"): return False + if button == 1 and node_class: # left mouse + # set drag mode, it will be applyed on motion event + self.drag_person = None + self.drag_family = None + if node_class == 'node': + self.drag_person = self.dbstate.db.get_person_from_handle( + handle) + if node_class == 'familynode': + self.drag_family = self.dbstate.db.get_family_from_handle( + handle) + self._in_drag = True + self._last_x = event.x_root + self._last_y = event.y_root + if button == 1 and node_class == 'node': # left mouse if handle == self.active_person_handle: # Find a parent of the active person so that they can become @@ -1758,11 +1783,6 @@ def select_node(self, item, target, event): context = GLib.main_context_default() self.click_events.append(context.find_source_by_id(click_event_id)) - # go to drag mode, applyed on motion event - self._in_drag = True - self._last_x = event.x - self._last_y = event.y - elif button == 3 and node_class: # right mouse if node_class == 'node': self.menu = PopupMenu(self, 'person', handle) @@ -1782,8 +1802,12 @@ def cb_drag_begin(self, widget, context): """ Called on start drag. """ - # set icon for person drag - Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) + tgs = [x.name() for x in context.list_targets()] + # set icon depending on person or family drag + if DdTargets.PERSON_LINK.drag_type in tgs: + Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) + if DdTargets.FAMILY_LINK.drag_type in tgs: + Gtk.drag_set_icon_name(context, 'gramps-family', 0, 0) def cb_drag_end(self, widget, context): """ @@ -1794,17 +1818,39 @@ def cb_drag_end(self, widget, context): def cb_drag_data_get(self, widget, context, sel_data, info, time): """ Returned parameters after drag. - Specified for 'person-link', for others return text info about person. + Specified for 'person-link' and 'family-link', + also to return text info about person or family. """ tgs = [x.name() for x in context.list_targets()] + if info == DdTargets.PERSON_LINK.app_id: data = (DdTargets.PERSON_LINK.drag_type, id(widget), self.drag_person.handle, 0) sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) - elif ('TEXT' in tgs or 'text/plain' in tgs) and info == 0: - format_helper = FormattingHelper(self.dbstate) - sel_data.set_text( - format_helper.format_person(self.drag_person, 11),-1) + elif info == DdTargets.FAMILY_LINK.app_id: + data = (DdTargets.FAMILY_LINK.drag_type, + id(widget), self.drag_family.handle, 0) + sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) + elif ('TEXT' in tgs or 'text/plain' in tgs): + if info == 0: + format_helper = FormattingHelper(self.dbstate) + sel_data.set_text( + format_helper.format_person(self.drag_person, 11),-1) + if info == 1: + f_handle = self.drag_family.get_father_handle() + m_handle = self.drag_family.get_mother_handle() + if f_handle: + father = self.dbstate.db.get_person_from_handle(f_handle) + father = displayer.display(father) + else: + father = '...' + if m_handle: + mother = self.dbstate.db.get_person_from_handle(m_handle) + mother = displayer.display(mother) + else: + mother = '...' + sel_data.set_text( + _('Family of %s and %s') % (father, mother), -1) def find_a_parent(self, handle): """ From 887d783ce480bc97306f3aa01f27d694ff62777b Mon Sep 17 00:00:00 2001 From: vantu5z Date: Thu, 26 Sep 2019 10:08:10 +0300 Subject: [PATCH 06/15] fix drag on Windows OS --- GraphView/graphview.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 03f96a150..4d95a6dfe 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1480,7 +1480,8 @@ def populate(self, active_person): if self.uistate.window.get_window().is_visible(): process_pending_events() - self._in_drag = False + self._in_drag = False # True - when drag can be started + self._do_drag = False # True - when drag is started self.clear() self.active_person_handle = active_person @@ -1661,7 +1662,10 @@ def motion_notify_event(self, _item, _target, event): self.vadjustment.set_value(new_y) return True - if self._in_drag and (event.type == Gdk.EventType.MOTION_NOTIFY): + if not (event.type == Gdk.EventType.MOTION_NOTIFY): + return False + + if self._in_drag and (not self._do_drag): # start drag when cursor moved more then 5 # to separate it from simple click if ((abs(self._last_x - event.x_root) > 5) @@ -1675,11 +1679,8 @@ def motion_notify_event(self, _item, _target, event): # translate to drag_widget coords scale_coef = self.canvas.get_scale() - bounds = self.canvas.get_root_item().get_bounds() - height_canvas = bounds.y2 - bounds.y1 x = self._last_x * scale_coef - self.hadjustment.get_value() - y = ((height_canvas + self._last_y) * scale_coef - - self.vadjustment.get_value()) + y = self._last_y * scale_coef - self.vadjustment.get_value() # setup targets tglist = Gtk.TargetList.new([]) @@ -1698,14 +1699,12 @@ def motion_notify_event(self, _item, _target, event): # allow drag to a text document, info on drag_get will be 1 tglist.add_text_targets(1) - drag_widget = self.get_widget() - # change event window - event.window = drag_widget.get_window() # start drag + drag_widget = self.get_widget() drag_widget.drag_begin_with_coordinates( tglist, Gdk.DragAction.COPY, - Gdk.KEY_Pointer_Button1, + 1, # left mouse button = 1 event, x, y) return True @@ -1802,6 +1801,7 @@ def cb_drag_begin(self, widget, context): """ Called on start drag. """ + self._do_drag = True tgs = [x.name() for x in context.list_targets()] # set icon depending on person or family drag if DdTargets.PERSON_LINK.drag_type in tgs: @@ -1814,6 +1814,7 @@ def cb_drag_end(self, widget, context): Called when drag is end. """ self._in_drag = False + self._do_drag = False def cb_drag_data_get(self, widget, context, sel_data, info, time): """ From da460ab70ad318ec12b18d69a01ee8218cd8f94e Mon Sep 17 00:00:00 2001 From: vantu5z Date: Thu, 2 Jun 2022 08:36:35 +0300 Subject: [PATCH 07/15] move drag-n-drop to separate class --- GraphView/drag_n_drop.py | 141 +++++++++++++++++++++++++++++++++++++++ GraphView/graphview.py | 116 +++----------------------------- 2 files changed, 152 insertions(+), 105 deletions(-) create mode 100644 GraphView/drag_n_drop.py diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py new file mode 100644 index 000000000..2a1c2cae9 --- /dev/null +++ b/GraphView/drag_n_drop.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +import pickle +from gi.repository import Gtk, Gdk + +from gramps.gen.display.name import displayer +from gramps.gui.ddtargets import DdTargets +from gramps.gen.utils.libformatting import FormattingHelper + +from gramps.gen.const import GRAMPS_LOCALE as glocale +try: + _trans = glocale.get_addon_translator(__file__) +except ValueError: + _trans = glocale.translation +_ = _trans.gettext + + +class DragAndDrop(): + """ + Add Drag-n-Drop feature to GraphView addon. + """ + + def __init__(self, widget, dbstate): + self.ready = False # True - when drag can be started + self.do_drag = False # True - when drag is started + + self.drag_person = None + self.drag_family = None + + self.dbstate = dbstate + self.widget = widget + self.widget.connect("drag_data_get", self.drag_data_get) + self.widget.connect("drag_begin", self.begin) + self.widget.connect("drag_end", self.stop) + + def begin(self, widget, context): + """ + Called when drag is start. + """ + self.do_drag = True + tgs = [x.name() for x in context.list_targets()] + # set icon depending on person or family drag + if DdTargets.PERSON_LINK.drag_type in tgs: + Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) + if DdTargets.FAMILY_LINK.drag_type in tgs: + Gtk.drag_set_icon_name(context, 'gramps-family', 0, 0) + + def stop(self, *args): + """ + Called when drag is end. + """ + self.ready = False + self.do_drag = False + self.drag_person = None + self.drag_family = None + + def is_ready(self): + """ + Check if we ready to drag. + """ + return self.ready and (not self.do_drag) + + def set_ready(self, node_class, handle): + """ + Set ready to drag state. + """ + self.stop() + if node_class == 'node': + self.drag_person = self.dbstate.db.get_person_from_handle(handle) + elif node_class == 'familynode': + self.drag_family = self.dbstate.db.get_family_from_handle(handle) + + if self.drag_person or self.drag_family: + self.ready = True + + def drag_data_get(self, widget, context, sel_data, info, time): + """ + Returned parameters after drag. + Specified for 'person-link' and 'family-link', + also to return text info about person or family. + """ + tgs = [x.name() for x in context.list_targets()] + + if info == DdTargets.PERSON_LINK.app_id: + data = (DdTargets.PERSON_LINK.drag_type, + id(widget), self.drag_person.handle, 0) + sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) + elif info == DdTargets.FAMILY_LINK.app_id: + data = (DdTargets.FAMILY_LINK.drag_type, + id(widget), self.drag_family.handle, 0) + sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) + elif ('TEXT' in tgs or 'text/plain' in tgs): + if info == 0: + format_helper = FormattingHelper(self.dbstate) + sel_data.set_text( + format_helper.format_person(self.drag_person, 11), -1) + if info == 1: + f_handle = self.drag_family.get_father_handle() + m_handle = self.drag_family.get_mother_handle() + if f_handle: + father = self.dbstate.db.get_person_from_handle(f_handle) + father = displayer.display(father) + else: + father = '...' + if m_handle: + mother = self.dbstate.db.get_person_from_handle(m_handle) + mother = displayer.display(mother) + else: + mother = '...' + sel_data.set_text( + _('Family of %s and %s') % (father, mother), -1) + + def start_drag(self, pos_x, pos_y, event): + """ + Activate drag. + """ + # setup targets + tglist = Gtk.TargetList.new([]) + if self.drag_person is not None: + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id, + ) + # allow drag to a text document, info on drag_get will be 0 + tglist.add_text_targets(0) + if self.drag_family is not None: + tglist.add(DdTargets.FAMILY_LINK.atom_drag_type, + DdTargets.FAMILY_LINK.target_flags, + DdTargets.FAMILY_LINK.app_id, + ) + # allow drag to a text document, info on drag_get will be 1 + tglist.add_text_targets(1) + + # start drag + self.widget.drag_begin_with_coordinates( + tglist, + Gdk.DragAction.COPY, + 1, # left mouse button = 1 + event, + pos_x, pos_y) + return True diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 4d95a6dfe..9cfc6189e 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -46,7 +46,6 @@ from collections import abc, deque import gi from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, Pango -import pickle #------------------------------------------------------------------------- # @@ -71,7 +70,6 @@ from gramps.gen.utils.libformatting import FormattingHelper from gramps.gen.utils.thumbnails import get_thumbnail_path -from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import (OptionDialog, ErrorDialog, QuestionDialog2, WarningDialog) from gramps.gui.display import display_url @@ -137,6 +135,7 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) from search_widget import SearchWidget, Popover, ListBoxRow, get_person_tooltip from avatars import Avatars +from drag_n_drop import DragAndDrop #------------------------------------------------------------------------- @@ -983,13 +982,11 @@ def __init__(self, view, dbstate, uistate): self._last_x = 0 self._last_y = 0 self._in_move = False - self._in_drag = False self.view = view self.dbstate = dbstate self.uistate = uistate self.parser = None self.active_person_handle = None - self.drag_person = None self.actions = Actions(dbstate, uistate, self.view.bookmarks) self.actions.connect('rebuild-graph', self.view.build_tree) @@ -1157,10 +1154,7 @@ def __init__(self, view, dbstate, uistate): self.bold_size = self.norm_size = 0 # font sizes to send to dot # setup drag and drop - drag_widget = self.get_widget() - drag_widget.connect("drag_data_get", self.cb_drag_data_get) - drag_widget.connect("drag_begin", self.cb_drag_begin) - drag_widget.connect("drag_end", self.cb_drag_end) + self.dnd = DragAndDrop(self.get_widget(), self.dbstate) def add_popover(self, widget, container): """ @@ -1175,7 +1169,7 @@ def add_popover(self, widget, container): def build_spinner(self, icon, start, end, tooltip, conf_const): """ Build spinner with icon and pack it into box. - Chenges apply to config with delay. + Changes apply to config with delay. """ box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) img = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU) @@ -1480,8 +1474,8 @@ def populate(self, active_person): if self.uistate.window.get_window().is_visible(): process_pending_events() - self._in_drag = False # True - when drag can be started - self._do_drag = False # True - when drag is started + self.dnd.stop() + self.clear() self.active_person_handle = active_person @@ -1632,7 +1626,7 @@ def button_release(self, item, target, event): """ Exit from scroll mode when button release. """ - self._in_drag = False + self.dnd.stop() button = event.get_button()[1] if((button == 1 or button == 2) and event.type == getattr(Gdk.EventType, "BUTTON_RELEASE")): @@ -1665,7 +1659,7 @@ def motion_notify_event(self, _item, _target, event): if not (event.type == Gdk.EventType.MOTION_NOTIFY): return False - if self._in_drag and (not self._do_drag): + if self.dnd.is_ready(): # start drag when cursor moved more then 5 # to separate it from simple click if ((abs(self._last_x - event.x_root) > 5) @@ -1682,32 +1676,8 @@ def motion_notify_event(self, _item, _target, event): x = self._last_x * scale_coef - self.hadjustment.get_value() y = self._last_y * scale_coef - self.vadjustment.get_value() - # setup targets - tglist = Gtk.TargetList.new([]) - if self.drag_person is not None: - tglist.add(DdTargets.PERSON_LINK.atom_drag_type, - DdTargets.PERSON_LINK.target_flags, - DdTargets.PERSON_LINK.app_id, - ) - # allow drag to a text document, info on drag_get will be 0 - tglist.add_text_targets(0) - if self.drag_family is not None: - tglist.add(DdTargets.FAMILY_LINK.atom_drag_type, - DdTargets.FAMILY_LINK.target_flags, - DdTargets.FAMILY_LINK.app_id, - ) - # allow drag to a text document, info on drag_get will be 1 - tglist.add_text_targets(1) - - # start drag - drag_widget = self.get_widget() - drag_widget.drag_begin_with_coordinates( - tglist, - Gdk.DragAction.COPY, - 1, # left mouse button = 1 - event, - x, y) - return True + self.dnd.start_drag(x, y, event) + return False def set_zoom(self, value): @@ -1750,16 +1720,8 @@ def select_node(self, item, target, event): return False if button == 1 and node_class: # left mouse - # set drag mode, it will be applyed on motion event - self.drag_person = None - self.drag_family = None - if node_class == 'node': - self.drag_person = self.dbstate.db.get_person_from_handle( - handle) - if node_class == 'familynode': - self.drag_family = self.dbstate.db.get_family_from_handle( - handle) - self._in_drag = True + # set drag mode, it will be applied on motion event + self.dnd.set_ready(node_class, handle) self._last_x = event.x_root self._last_y = event.y_root @@ -1797,62 +1759,6 @@ def select_node(self, item, target, event): return True - def cb_drag_begin(self, widget, context): - """ - Called on start drag. - """ - self._do_drag = True - tgs = [x.name() for x in context.list_targets()] - # set icon depending on person or family drag - if DdTargets.PERSON_LINK.drag_type in tgs: - Gtk.drag_set_icon_name(context, 'gramps-person', 0, 0) - if DdTargets.FAMILY_LINK.drag_type in tgs: - Gtk.drag_set_icon_name(context, 'gramps-family', 0, 0) - - def cb_drag_end(self, widget, context): - """ - Called when drag is end. - """ - self._in_drag = False - self._do_drag = False - - def cb_drag_data_get(self, widget, context, sel_data, info, time): - """ - Returned parameters after drag. - Specified for 'person-link' and 'family-link', - also to return text info about person or family. - """ - tgs = [x.name() for x in context.list_targets()] - - if info == DdTargets.PERSON_LINK.app_id: - data = (DdTargets.PERSON_LINK.drag_type, - id(widget), self.drag_person.handle, 0) - sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) - elif info == DdTargets.FAMILY_LINK.app_id: - data = (DdTargets.FAMILY_LINK.drag_type, - id(widget), self.drag_family.handle, 0) - sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) - elif ('TEXT' in tgs or 'text/plain' in tgs): - if info == 0: - format_helper = FormattingHelper(self.dbstate) - sel_data.set_text( - format_helper.format_person(self.drag_person, 11),-1) - if info == 1: - f_handle = self.drag_family.get_father_handle() - m_handle = self.drag_family.get_mother_handle() - if f_handle: - father = self.dbstate.db.get_person_from_handle(f_handle) - father = displayer.display(father) - else: - father = '...' - if m_handle: - mother = self.dbstate.db.get_person_from_handle(m_handle) - mother = displayer.display(mother) - else: - mother = '...' - sel_data.set_text( - _('Family of %s and %s') % (father, mother), -1) - def find_a_parent(self, handle): """ Locate a parent from the first family that the selected person is a From a55a20c6d4f28dadfc18892638a7a47c6fe3f006 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Fri, 3 Jun 2022 11:20:22 +0300 Subject: [PATCH 08/15] use drag_source_set instead of manual activation --- GraphView/drag_n_drop.py | 89 +++++++++++++++++----------------------- GraphView/graphview.py | 55 ++++++++----------------- 2 files changed, 55 insertions(+), 89 deletions(-) diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py index 2a1c2cae9..060099019 100644 --- a/GraphView/drag_n_drop.py +++ b/GraphView/drag_n_drop.py @@ -20,24 +20,34 @@ class DragAndDrop(): Add Drag-n-Drop feature to GraphView addon. """ - def __init__(self, widget, dbstate): - self.ready = False # True - when drag can be started - self.do_drag = False # True - when drag is started - + def __init__(self, canvas, dbstate): self.drag_person = None self.drag_family = None self.dbstate = dbstate - self.widget = widget - self.widget.connect("drag_data_get", self.drag_data_get) - self.widget.connect("drag_begin", self.begin) - self.widget.connect("drag_end", self.stop) + self.canvas = canvas + self.canvas.connect("drag_data_get", self.drag_data_get) + self.canvas.connect("drag_begin", self.begin) + self.canvas.connect("drag_end", self.stop) + + self.enable_dnd(True) + + def enable_dnd(self, state): + """ + Enable or disable drag-n-drop for canvas widget. + """ + if state: + self.canvas.drag_source_set( + Gdk.ModifierType.BUTTON1_MASK, + [], + Gdk.DragAction.COPY) + else: + self.canvas.drag_source_unset() def begin(self, widget, context): """ Called when drag is start. """ - self.do_drag = True tgs = [x.name() for x in context.list_targets()] # set icon depending on person or family drag if DdTargets.PERSON_LINK.drag_type in tgs: @@ -49,29 +59,36 @@ def stop(self, *args): """ Called when drag is end. """ - self.ready = False - self.do_drag = False self.drag_person = None self.drag_family = None - def is_ready(self): + def set_target(self, node_class, handle): """ - Check if we ready to drag. - """ - return self.ready and (not self.do_drag) - - def set_ready(self, node_class, handle): - """ - Set ready to drag state. + Set targets for drag-n-drop. """ self.stop() + tglist = Gtk.TargetList.new([]) if node_class == 'node': self.drag_person = self.dbstate.db.get_person_from_handle(handle) + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id, + ) + # allow drag to a text document, info on drag_get will be 0 + tglist.add_text_targets(0) elif node_class == 'familynode': self.drag_family = self.dbstate.db.get_family_from_handle(handle) + tglist.add(DdTargets.FAMILY_LINK.atom_drag_type, + DdTargets.FAMILY_LINK.target_flags, + DdTargets.FAMILY_LINK.app_id, + ) + # allow drag to a text document, info on drag_get will be 1 + tglist.add_text_targets(1) - if self.drag_person or self.drag_family: - self.ready = True + if tglist: + self.canvas.drag_source_set_target_list(tglist) + else: + self.enable_dnd(False) def drag_data_get(self, widget, context, sel_data, info, time): """ @@ -109,33 +126,3 @@ def drag_data_get(self, widget, context, sel_data, info, time): mother = '...' sel_data.set_text( _('Family of %s and %s') % (father, mother), -1) - - def start_drag(self, pos_x, pos_y, event): - """ - Activate drag. - """ - # setup targets - tglist = Gtk.TargetList.new([]) - if self.drag_person is not None: - tglist.add(DdTargets.PERSON_LINK.atom_drag_type, - DdTargets.PERSON_LINK.target_flags, - DdTargets.PERSON_LINK.app_id, - ) - # allow drag to a text document, info on drag_get will be 0 - tglist.add_text_targets(0) - if self.drag_family is not None: - tglist.add(DdTargets.FAMILY_LINK.atom_drag_type, - DdTargets.FAMILY_LINK.target_flags, - DdTargets.FAMILY_LINK.app_id, - ) - # allow drag to a text document, info on drag_get will be 1 - tglist.add_text_targets(1) - - # start drag - self.widget.drag_begin_with_coordinates( - tglist, - Gdk.DragAction.COPY, - 1, # left mouse button = 1 - event, - pos_x, pos_y) - return True diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 9cfc6189e..db5da42db 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1154,7 +1154,8 @@ def __init__(self, view, dbstate, uistate): self.bold_size = self.norm_size = 0 # font sizes to send to dot # setup drag and drop - self.dnd = DragAndDrop(self.get_widget(), self.dbstate) + self.dnd = DragAndDrop(self.canvas, self.dbstate) + self.canvas.connect("drag-begin", self.del_click_events) def add_popover(self, widget, container): """ @@ -1474,8 +1475,6 @@ def populate(self, active_person): if self.uistate.window.get_window().is_visible(): process_pending_events() - self.dnd.stop() - self.clear() self.active_person_handle = active_person @@ -1606,7 +1605,8 @@ def button_press(self, item, _target, event): return False button = event.get_button()[1] - if button == 1 or button == 2: + if button in (1, 2): + self.dnd.enable_dnd(False) window = self.canvas.get_parent().get_window() window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.FLEUR)) self._last_x = event.x_root @@ -1626,7 +1626,6 @@ def button_release(self, item, target, event): """ Exit from scroll mode when button release. """ - self.dnd.stop() button = event.get_button()[1] if((button == 1 or button == 2) and event.type == getattr(Gdk.EventType, "BUTTON_RELEASE")): @@ -1634,6 +1633,7 @@ def button_release(self, item, target, event): self.motion_notify_event(item, target, event) self.canvas.get_parent().get_window().set_cursor(None) self._in_move = False + self.dnd.enable_dnd(True) return True return False @@ -1655,29 +1655,6 @@ def motion_notify_event(self, _item, _target, event): (event.y_root - self._last_y) * scale_coef) self.vadjustment.set_value(new_y) return True - - if not (event.type == Gdk.EventType.MOTION_NOTIFY): - return False - - if self.dnd.is_ready(): - # start drag when cursor moved more then 5 - # to separate it from simple click - if ((abs(self._last_x - event.x_root) > 5) - or (abs(self._last_y - event.y_root) > 5)): - self.uistate.set_busy_cursor(False) - # Remove all single click events - for click_item in self.click_events: - if not click_item.is_destroyed(): - GLib.source_remove(click_item.get_id()) - self.click_events.clear() - - # translate to drag_widget coords - scale_coef = self.canvas.get_scale() - x = self._last_x * scale_coef - self.hadjustment.get_value() - y = self._last_y * scale_coef - self.vadjustment.get_value() - - self.dnd.start_drag(x, y, event) - return False def set_zoom(self, value): @@ -1688,6 +1665,15 @@ def set_zoom(self, value): self.view._config.set('interface.graphview-scale', value) self.canvas.set_scale(value / self.transform_scale) + def del_click_events(self, *args): + """ + Remove all single click events. + """ + for click_item in self.click_events: + if not click_item.is_destroyed(): + GLib.source_remove(click_item.get_id()) + self.click_events.clear() + def select_node(self, item, target, event): """ Perform actions when a node is clicked. @@ -1704,11 +1690,7 @@ def select_node(self, item, target, event): # perform double click on node by left mouse button if event.type == getattr(Gdk.EventType, "DOUBLE_BUTTON_PRESS"): - # Remove all single click events - for click_item in self.click_events: - if not click_item.is_destroyed(): - GLib.source_remove(click_item.get_id()) - self.click_events.clear() + self.del_click_events() if button == 1 and node_class == 'node': GLib.idle_add(self.actions.edit_person, None, handle) return True @@ -1719,11 +1701,8 @@ def select_node(self, item, target, event): if event.type != getattr(Gdk.EventType, "BUTTON_PRESS"): return False - if button == 1 and node_class: # left mouse - # set drag mode, it will be applied on motion event - self.dnd.set_ready(node_class, handle) - self._last_x = event.x_root - self._last_y = event.y_root + if button == 1 and node_class in ('node', 'familynode'): + self.dnd.set_target(node_class, handle) if button == 1 and node_class == 'node': # left mouse if handle == self.active_person_handle: From aa3748b7a492a32a3243136bdd1a5f51aa2c4a3e Mon Sep 17 00:00:00 2001 From: vantu5z Date: Mon, 6 Jun 2022 14:00:39 +0300 Subject: [PATCH 09/15] add drop support --- GraphView/drag_n_drop.py | 120 ++++++++++++++++++++++++++++++++++++++- GraphView/graphview.py | 3 +- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py index 060099019..e0fc73311 100644 --- a/GraphView/drag_n_drop.py +++ b/GraphView/drag_n_drop.py @@ -3,9 +3,12 @@ import pickle from gi.repository import Gtk, Gdk +from gramps.gen.db import DbTxn from gramps.gen.display.name import displayer -from gramps.gui.ddtargets import DdTargets from gramps.gen.utils.libformatting import FormattingHelper +from gramps.gen.lib import ChildRef +from gramps.gui.ddtargets import DdTargets +from gramps.gui.dialog import QuestionDialog2 from gramps.gen.const import GRAMPS_LOCALE as glocale try: @@ -13,6 +16,7 @@ except ValueError: _trans = glocale.translation _ = _trans.gettext +ngettext = glocale.translation.ngettext class DragAndDrop(): @@ -20,11 +24,14 @@ class DragAndDrop(): Add Drag-n-Drop feature to GraphView addon. """ - def __init__(self, canvas, dbstate): + def __init__(self, canvas, dbstate, uistate, h_adj, v_adj): self.drag_person = None self.drag_family = None + self.h_adj = h_adj + self.v_adj = v_adj self.dbstate = dbstate + self.uistate = uistate self.canvas = canvas self.canvas.connect("drag_data_get", self.drag_data_get) self.canvas.connect("drag_begin", self.begin) @@ -32,6 +39,24 @@ def __init__(self, canvas, dbstate): self.enable_dnd(True) + # add drop support + self.canvas.drag_dest_set( + Gtk.DestDefaults.ALL, + [], + Gdk.DragAction.COPY + ) + tglist = Gtk.TargetList.new([]) + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id, + ) + # TODO: add other targets. For now only person drop supported. + self.canvas.drag_dest_set_target_list(tglist) + self.canvas.connect("drag-motion", self.drag_motion) + self.canvas.connect("drag-data-received", self.drag_data_receive) + + self.item_cache = {} + def enable_dnd(self, state): """ Enable or disable drag-n-drop for canvas widget. @@ -55,6 +80,8 @@ def begin(self, widget, context): if DdTargets.FAMILY_LINK.drag_type in tgs: Gtk.drag_set_icon_name(context, 'gramps-family', 0, 0) + self.item_cache.clear() + def stop(self, *args): """ Called when drag is end. @@ -126,3 +153,92 @@ def drag_data_get(self, widget, context, sel_data, info, time): mother = '...' sel_data.set_text( _('Family of %s and %s') % (father, mother), -1) + + def drag_motion(self, widget, context, x, y, time): + """ + Monitor drag motion. And check if we can receive the data. + Disable drop if we are not at person or family node. + """ + if self.get_item_at_pos(x, y) is None: + # disable drop + Gdk.drag_status(context, 0, time) + + def drag_data_receive(self, widget, context, x, y, data, info, time): + """ + Handle drop event. + """ + receiver = self.get_item_at_pos(x, y) + + # unpickle data and get dropped person's handles + out_data = [] + p_data = pickle.loads(data.get_data()) + if isinstance(p_data[0], bytes): + for d in p_data: + tmp = pickle.loads(d) + if tmp[0] == 'person-link': + out_data.append(tmp[2]) + elif p_data[0] == 'person-link': + out_data.append(p_data[2]) + + # if person is dropped to family node then add them to this family + if receiver[0] == 'familynode' and out_data: + title = ngettext("Add child to family?", + "Add children to family?", + len(out_data)) + quest = ngettext("Do you want to add child to the family?", + "Do you want to add children to the family?", + len(out_data)) + dialog = QuestionDialog2(title, quest, _("Yes"), _("No"), + self.uistate.window) + if dialog.run(): + for person_handle in out_data: + self.__add_child_to_family(person_handle, receiver[1]) + + def get_item_at_pos(self, x, y): + """ + Get GooCanvas item at cursor position. + Return: (node_class, person/family object) or None. + """ + scale_coef = self.canvas.get_scale() + x_pos = (x + self.h_adj.get_value()) / scale_coef + y_pos = (y + self.v_adj.get_value()) / scale_coef + + item = self.canvas.get_item_at(x_pos, y_pos, True) + obj = self.item_cache.get(item) + if obj is not None: + return obj + try: + # data stored in GooCanvasGroup which is parent of the item + parent = item.get_parent() + handle = parent.title + node_class = parent.description + + if node_class == 'node' and handle: + obj = (node_class, + self.dbstate.db.get_person_from_handle(handle)) + elif node_class == 'familynode' and handle: + obj = (node_class, + self.dbstate.db.get_family_from_handle(handle)) + else: + return None + self.item_cache[item] = obj + return obj + except: + pass + return None + + def __add_child_to_family(self, person_handle, family): + """ + Write data to db. + """ + person = self.dbstate.db.get_person_from_handle(person_handle) + ref = ChildRef() + ref.ref = person_handle + family.add_child_ref(ref) + person.add_parent_family_handle(family.get_handle()) + + with DbTxn(_("Add Child to Family"), self.dbstate.db) as trans: + # default relationship is used + self.dbstate.db.commit_person(person, trans) + # add child to family + self.dbstate.db.commit_family(family, trans) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index db5da42db..90af34651 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1154,7 +1154,8 @@ def __init__(self, view, dbstate, uistate): self.bold_size = self.norm_size = 0 # font sizes to send to dot # setup drag and drop - self.dnd = DragAndDrop(self.canvas, self.dbstate) + self.dnd = DragAndDrop(self.canvas, self.dbstate, self.uistate, + self.hadjustment, self.vadjustment) self.canvas.connect("drag-begin", self.del_click_events) def add_popover(self, widget, container): From f3e65f65fcf44e178a2a907f74465787f66215e7 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Tue, 7 Jun 2022 15:58:20 +0300 Subject: [PATCH 10/15] popup menu with actions on drop --- GraphView/drag_n_drop.py | 163 +++++++++++++++++++++++++++++++++------ 1 file changed, 139 insertions(+), 24 deletions(-) diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py index e0fc73311..833a29950 100644 --- a/GraphView/drag_n_drop.py +++ b/GraphView/drag_n_drop.py @@ -6,9 +6,11 @@ from gramps.gen.db import DbTxn from gramps.gen.display.name import displayer from gramps.gen.utils.libformatting import FormattingHelper -from gramps.gen.lib import ChildRef +from gramps.gen.lib import ChildRef, PersonRef, Person from gramps.gui.ddtargets import DdTargets -from gramps.gui.dialog import QuestionDialog2 + +from gramps.gui.editors import EditPersonRef, EditFamily +from gramps.gen.errors import WindowActiveError from gramps.gen.const import GRAMPS_LOCALE as glocale try: @@ -18,12 +20,13 @@ _ = _trans.gettext ngettext = glocale.translation.ngettext +gtk_version = float("%s.%s" % (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION)) + class DragAndDrop(): """ Add Drag-n-Drop feature to GraphView addon. """ - def __init__(self, canvas, dbstate, uistate, h_adj, v_adj): self.drag_person = None self.drag_family = None @@ -180,19 +183,40 @@ def drag_data_receive(self, widget, context, x, y, data, info, time): elif p_data[0] == 'person-link': out_data.append(p_data[2]) + action_menu = Popover(self.canvas, _('Available actions')) + rect = Gdk.Rectangle() + rect.x = x + rect.y = y + rect.height = rect.width = 1 + action_menu.set_pointing_to(rect) + # if person is dropped to family node then add them to this family if receiver[0] == 'familynode' and out_data: - title = ngettext("Add child to family?", - "Add children to family?", - len(out_data)) - quest = ngettext("Do you want to add child to the family?", - "Do you want to add children to the family?", + title = ngettext("Add as child to family", + "Add as children to family", len(out_data)) - dialog = QuestionDialog2(title, quest, _("Yes"), _("No"), - self.uistate.window) - if dialog.run(): - for person_handle in out_data: - self.__add_child_to_family(person_handle, receiver[1]) + action_menu.add_action(title, self.add_children_to_family, + receiver[1], out_data) + if len(out_data) == 1: + person = self.dbstate.db.get_person_from_handle(out_data[0]) + gender = person.get_gender() + f_handle = receiver[1].get_father_handle() + m_handle = receiver[1].get_mother_handle() + + if not f_handle and gender in (Person.MALE, Person.UNKNOWN): + action_menu.add_action(_('Add spouse as father'), + self.add_spouse, + receiver[1], out_data[0], 'father') + elif not m_handle and gender in (Person.FEMALE, Person.UNKNOWN): + action_menu.add_action(_('Add spouse as mother'), + self.add_spouse, + receiver[1], out_data[0], 'mother') + + # if drop to person node + if receiver[0] == 'node' and len(out_data) == 1: + action_menu.add_action(_('Add relation'), self.add_personref, + receiver[1], out_data[0]) + action_menu.popup() def get_item_at_pos(self, x, y): """ @@ -227,18 +251,109 @@ def get_item_at_pos(self, x, y): pass return None - def __add_child_to_family(self, person_handle, family): + def add_spouse(self, widget, family, person_handle, kind): """ - Write data to db. + Add spouse to family. """ - person = self.dbstate.db.get_person_from_handle(person_handle) - ref = ChildRef() - ref.ref = person_handle - family.add_child_ref(ref) - person.add_parent_family_handle(family.get_handle()) + try: + dialog = EditFamily(self.dbstate, self.uistate, [], family) + if kind == 'father': + dialog.obj.set_father_handle(person_handle) + dialog.update_father(person_handle) + elif kind == 'mother': + dialog.obj.set_mother_handle(person_handle) + dialog.update_mother(person_handle) + except WindowActiveError: + pass - with DbTxn(_("Add Child to Family"), self.dbstate.db) as trans: - # default relationship is used + def add_children_to_family(self, widget, family, data): + """ + Add persons to family. + data: list of person handles + """ + for person_handle in data: + person = self.dbstate.db.get_person_from_handle(person_handle) + ref = ChildRef() + ref.ref = person_handle + family.add_child_ref(ref) + person.add_parent_family_handle(family.get_handle()) + + with DbTxn(_("Add Child to Family"), self.dbstate.db) as trans: + # default relationship is used + self.dbstate.db.commit_person(person, trans) + # add child to family + self.dbstate.db.commit_family(family, trans) + + def add_personref(self, widget, source, person_handle): + """ + Open dialog to add reference to person. + """ + ref = PersonRef() + ref.rel = _('Godfather') # default role + ref.source = source + try: + dialog = EditPersonRef(self.dbstate, self.uistate, [], + ref, self.__cb_add_ref) + dialog.update_person( + self.dbstate.db.get_person_from_handle(person_handle)) + except WindowActiveError: + pass + + def __cb_add_ref(self, obj): + """ + Save person reference. + """ + person = obj.source + person.add_person_ref(obj) + with DbTxn(_("Add Reference to Person"), self.dbstate.db) as trans: self.dbstate.db.commit_person(person, trans) - # add child to family - self.dbstate.db.commit_family(family, trans) + + +class Popover(Gtk.Popover): + """ + Display available actions on drop event. + """ + def __init__(self, widget, label): + Gtk.Popover.__init__(self, relative_to=widget) + self.set_modal(True) + + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + lbl = Gtk.Label(label) + self.box.add(lbl) + + self.cancel_btn = Gtk.Button(label=_('Cancel')) + self.box.pack_end(self.cancel_btn, True, True, 5) + self.cancel_btn.connect('clicked', self.popdown) + + # set all widgets visible + self.box.show_all() + self.add(self.box) + + def add_action(self, label, callback, *args): + action = Gtk.Button(label=label) + self.box.pack_end(action, True, True, 5) + if callback: + action.connect('clicked', callback, *args) + action.connect('clicked', self.popdown) + action.show() + # move cancel button to the bottom + self.box.reorder_child(self.cancel_btn, 0) + + def popup(self): + """ + Different popup depending on gtk version. + """ + if gtk_version >= 3.22: + super(self.__class__, self).popup() + else: + self.show() + + def popdown(self, *args): + """ + Different popdown depending on gtk version. + """ + if gtk_version >= 3.22: + super(self.__class__, self).popdown() + else: + self.hide() From d0b7403eb7a3c04f35a7c5e6db3daaf245f0277a Mon Sep 17 00:00:00 2001 From: vantu5z Date: Wed, 8 Jun 2022 10:36:38 +0300 Subject: [PATCH 11/15] disable drag after node click is handled --- GraphView/drag_n_drop.py | 4 ++++ GraphView/graphview.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py index 833a29950..2d59ad246 100644 --- a/GraphView/drag_n_drop.py +++ b/GraphView/drag_n_drop.py @@ -40,6 +40,7 @@ def __init__(self, canvas, dbstate, uistate, h_adj, v_adj): self.canvas.connect("drag_begin", self.begin) self.canvas.connect("drag_end", self.stop) + self.drag_enabled = False self.enable_dnd(True) # add drop support @@ -64,6 +65,8 @@ def enable_dnd(self, state): """ Enable or disable drag-n-drop for canvas widget. """ + if self.drag_enabled == state: + return if state: self.canvas.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, @@ -71,6 +74,7 @@ def enable_dnd(self, state): Gdk.DragAction.COPY) else: self.canvas.drag_source_unset() + self.drag_enabled = state def begin(self, widget, context): """ diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 90af34651..57df2300c 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1154,9 +1154,9 @@ def __init__(self, view, dbstate, uistate): self.bold_size = self.norm_size = 0 # font sizes to send to dot # setup drag and drop + self.canvas.connect("drag-begin", self.del_click_events) self.dnd = DragAndDrop(self.canvas, self.dbstate, self.uistate, self.hadjustment, self.vadjustment) - self.canvas.connect("drag-begin", self.del_click_events) def add_popover(self, widget, container): """ @@ -1471,6 +1471,7 @@ def populate(self, active_person): """ Populate the graph with widgets derived from Graphviz. """ + self.dnd.enable_dnd(False) # set the busy cursor, so the user knows that we are working self.uistate.set_busy_cursor(True) if self.uistate.window.get_window().is_visible(): @@ -1634,7 +1635,6 @@ def button_release(self, item, target, event): self.motion_notify_event(item, target, event) self.canvas.get_parent().get_window().set_cursor(None) self._in_move = False - self.dnd.enable_dnd(True) return True return False @@ -1656,6 +1656,7 @@ def motion_notify_event(self, _item, _target, event): (event.y_root - self._last_y) * scale_coef) self.vadjustment.set_value(new_y) return True + self.dnd.enable_dnd(True) return False def set_zoom(self, value): From 469f6c74482111e53a8f5bc0909f894c82ddc7ab Mon Sep 17 00:00:00 2001 From: vantu5z Date: Wed, 8 Jun 2022 13:32:23 +0300 Subject: [PATCH 12/15] fix click nodes handling --- GraphView/graphview.py | 60 +++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/GraphView/graphview.py b/GraphView/graphview.py index 57df2300c..976f2cc71 100644 --- a/GraphView/graphview.py +++ b/GraphView/graphview.py @@ -1141,6 +1141,7 @@ def __init__(self, view, dbstate, uistate): # for detecting double click self.click_events = [] + self.double_click = False # for timeout on changing settings by spinners self.timeout_event = False @@ -1676,10 +1677,10 @@ def del_click_events(self, *args): GLib.source_remove(click_item.get_id()) self.click_events.clear() - def select_node(self, item, target, event): + def press_node(self, item, target, event): """ - Perform actions when a node is clicked. - If middle mouse was clicked then try to set scroll mode. + Perform actions when a node is clicked (button press). + If middle mouse was pressed then try to set scroll mode. """ self.search_widget.hide_search_popover() self.hide_bkmark_popover() @@ -1695,17 +1696,48 @@ def select_node(self, item, target, event): self.del_click_events() if button == 1 and node_class == 'node': GLib.idle_add(self.actions.edit_person, None, handle) - return True elif button == 1 and node_class == 'familynode': GLib.idle_add(self.actions.edit_family, None, handle) - return True + self.double_click = True + return True if event.type != getattr(Gdk.EventType, "BUTTON_PRESS"): return False + # set targets for drag-n-drop (object type and handle) if button == 1 and node_class in ('node', 'familynode'): self.dnd.set_target(node_class, handle) + elif button == 3 and node_class: # right mouse + if node_class == 'node': + self.menu = PopupMenu(self, 'person', handle) + self.menu.show_menu(event) + elif node_class == 'familynode': + self.menu = PopupMenu(self, 'family', handle) + self.menu.show_menu(event) + + elif button == 2: # middle mouse + # to enter in scroll mode (we should change "item" to root item) + item = self.canvas.get_root_item() + self.button_press(item, target, event) + + return True + + def release_node(self, item, target, event): + """ + Perform actions when a node is clicked (button release). + Set timer to handle single click at node and wait double click. + """ + # don't handle single click if had double click before + # because we came here after DOUBLE_BUTTON_PRESS event + if self.double_click: + self.double_click = False + return True + + handle = item.title + node_class = item.description + button = event.get_button()[1] + if button == 1 and node_class == 'node': # left mouse if handle == self.active_person_handle: # Find a parent of the active person so that they can become @@ -1725,21 +1757,6 @@ def select_node(self, item, target, event): context = GLib.main_context_default() self.click_events.append(context.find_source_by_id(click_event_id)) - elif button == 3 and node_class: # right mouse - if node_class == 'node': - self.menu = PopupMenu(self, 'person', handle) - self.menu.show_menu(event) - elif node_class == 'familynode': - self.menu = PopupMenu(self, 'family', handle) - self.menu.show_menu(event) - - elif button == 2: # middle mouse - # to enter in scroll mode (we should change "item" to root item) - item = self.canvas.get_root_item() - self.button_press(item, target, event) - - return True - def find_a_parent(self, handle): """ Locate a parent from the first family that the selected person is a @@ -1954,7 +1971,8 @@ def start_g(self, attrs): self.widget.motion_notify_event) else: item = GooCanvas.CanvasGroup(parent=self.current_parent()) - item.connect("button-press-event", self.widget.select_node) + item.connect("button-press-event", self.widget.press_node) + item.connect("button-release-event", self.widget.release_node) self.items_list.append(item) item.description = attrs.get('class') From bb14d516ea733db67e235722df2dc63402c49414 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Thu, 9 Jun 2022 10:02:41 +0300 Subject: [PATCH 13/15] add DnD from search widget --- GraphView/search_widget.py | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/GraphView/search_widget.py b/GraphView/search_widget.py index a7138d450..88b874573 100755 --- a/GraphView/search_widget.py +++ b/GraphView/search_widget.py @@ -19,6 +19,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # +import pickle + from gi.repository import Gtk, Gdk, GLib, GObject from threading import Event from queue import Queue, Empty @@ -27,6 +29,7 @@ from gramps.gen.display.name import displayer from gramps.gen.utils.db import (get_birth_or_fallback, get_death_or_fallback, find_parents) +from gramps.gui.ddtargets import DdTargets from gramps.gen.const import GRAMPS_LOCALE as glocale try: @@ -504,7 +507,7 @@ def popdown(self): class ListBoxRow(Gtk.ListBoxRow): """ - Extended Gtk.ListBoxRow. + Extended Gtk.ListBoxRow with person DnD support. """ def __init__(self, person_handle=None, label='', marked=False, db=None): Gtk.ListBoxRow.__init__(self) @@ -516,6 +519,41 @@ def __init__(self, person_handle=None, label='', marked=False, db=None): self.set_has_tooltip(True) self.connect('query-tooltip', self.query_tooltip) + self.setup_dnd() + + def add(self, widget): + """ + Override "container.add" to catch drag events. + Pack content of ListBoxRow to Gtk.EventBox. + """ + ebox = Gtk.EventBox() + ebox.add(widget) + super().add(ebox) + + def setup_dnd(self): + """ + Setup drag-n-drop. + """ + self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, + [], + Gdk.DragAction.COPY) + tglist = Gtk.TargetList.new([]) + tglist.add(DdTargets.PERSON_LINK.atom_drag_type, + DdTargets.PERSON_LINK.target_flags, + DdTargets.PERSON_LINK.app_id) + self.drag_source_set_target_list(tglist) + + self.connect("drag-data-get", self.drag_data_get) + + self.drag_source_set_icon_name('gramps-person') + + def drag_data_get(self, widget, context, sel_data, info, time): + """ + Returned parameters after drag. + """ + data = (DdTargets.PERSON_LINK.drag_type, + id(widget), self.person_handle, 0) + sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): """ @@ -529,6 +567,7 @@ def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): else: self.set_has_tooltip(False) + class ScrolledListBox(Gtk.ScrolledWindow): """ Extended Gtk.ScrolledWindow with max_height property. From 03bfa87ad926e4cdd2c0d4e6f5cf79656429b1f2 Mon Sep 17 00:00:00 2001 From: vantu5z Date: Thu, 9 Jun 2022 14:34:38 +0300 Subject: [PATCH 14/15] drop action - add parents for person --- GraphView/drag_n_drop.py | 87 +++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py index 2d59ad246..47ce41928 100644 --- a/GraphView/drag_n_drop.py +++ b/GraphView/drag_n_drop.py @@ -6,7 +6,7 @@ from gramps.gen.db import DbTxn from gramps.gen.display.name import displayer from gramps.gen.utils.libformatting import FormattingHelper -from gramps.gen.lib import ChildRef, PersonRef, Person +from gramps.gen.lib import ChildRef, PersonRef, Person, Family from gramps.gui.ddtargets import DdTargets from gramps.gui.editors import EditPersonRef, EditFamily @@ -194,6 +194,10 @@ def drag_data_receive(self, widget, context, x, y, data, info, time): rect.height = rect.width = 1 action_menu.set_pointing_to(rect) + # =========================== + # Generate actions popup menu + # =========================== + # if person is dropped to family node then add them to this family if receiver[0] == 'familynode' and out_data: title = ngettext("Add as child to family", @@ -201,6 +205,7 @@ def drag_data_receive(self, widget, context, x, y, data, info, time): len(out_data)) action_menu.add_action(title, self.add_children_to_family, receiver[1], out_data) + # add spouse to family if len(out_data) == 1: person = self.dbstate.db.get_person_from_handle(out_data[0]) gender = person.get_gender() @@ -210,16 +215,55 @@ def drag_data_receive(self, widget, context, x, y, data, info, time): if not f_handle and gender in (Person.MALE, Person.UNKNOWN): action_menu.add_action(_('Add spouse as father'), self.add_spouse, - receiver[1], out_data[0], 'father') - elif not m_handle and gender in (Person.FEMALE, Person.UNKNOWN): + out_data[0], None, receiver[1]) + if not m_handle and gender in (Person.FEMALE, Person.UNKNOWN): action_menu.add_action(_('Add spouse as mother'), self.add_spouse, - receiver[1], out_data[0], 'mother') + None, out_data[0], receiver[1]) # if drop to person node - if receiver[0] == 'node' and len(out_data) == 1: - action_menu.add_action(_('Add relation'), self.add_personref, - receiver[1], out_data[0]) + if receiver[0] == 'node' and out_data: + # add relation (reference) + if len(out_data) == 1: + action_menu.add_action(_('Add relation'), self.add_personref, + receiver[1], out_data[0]) + # add as parent + if len(out_data) in (1, 2): + parent_family_list = receiver[1].get_parent_family_handle_list() + # dropped 1 person and 1 family exists + if len(parent_family_list) == 1 and len(out_data) == 1: + person = self.dbstate.db.get_person_from_handle(out_data[0]) + gender = person.get_gender() + family = self.dbstate.db.get_family_from_handle( + parent_family_list[0]) + f_handle = family.get_father_handle() + m_handle = family.get_mother_handle() + + if not f_handle and gender in (Person.MALE, Person.UNKNOWN): + action_menu.add_action(_('Add as father'), + self.add_spouse, + out_data[0], None, family) + elif not m_handle and gender in (Person.FEMALE, + Person.UNKNOWN): + action_menu.add_action(_('Add as mother'), + self.add_spouse, + None, out_data[0], family) + # create family for person + elif not parent_family_list: + father = None + mother = None + for p in out_data: + person = self.dbstate.db.get_person_from_handle(p) + gender = person.get_gender() + if gender == Person.MALE: + father = p if father is None else None + if gender == Person.FEMALE: + mother = p if mother is None else None + if father or mother: + child = receiver[1].get_handle() + action_menu.add_action(_('Add parents'), + self.add_spouse, + father, mother, None, child) action_menu.popup() def get_item_at_pos(self, x, y): @@ -255,18 +299,28 @@ def get_item_at_pos(self, x, y): pass return None - def add_spouse(self, widget, family, person_handle, kind): + def add_spouse(self, widget, father_handle, mother_handle, + family=None, child=None): """ Add spouse to family. + If family is not provided it will be created for specified child. """ + if family is None: + if child is None: + # we need child to refer to new family + return + family = Family() + childref = ChildRef() + childref.set_reference_handle(child) + family.add_child_ref(childref) + + if father_handle: + family.set_father_handle(father_handle) + if mother_handle: + family.set_mother_handle(mother_handle) + try: - dialog = EditFamily(self.dbstate, self.uistate, [], family) - if kind == 'father': - dialog.obj.set_father_handle(person_handle) - dialog.update_father(person_handle) - elif kind == 'mother': - dialog.obj.set_mother_handle(person_handle) - dialog.update_mother(person_handle) + EditFamily(self.dbstate, self.uistate, [], family) except WindowActiveError: pass @@ -335,6 +389,9 @@ def __init__(self, widget, label): self.add(self.box) def add_action(self, label, callback, *args): + """ + Add button to popover with action. + """ action = Gtk.Button(label=label) self.box.pack_end(action, True, True, 5) if callback: From 3c2ee12df9bb8c29278f2bffe8d6e3bcabd646ca Mon Sep 17 00:00:00 2001 From: vantu5z Date: Thu, 9 Jun 2022 15:09:43 +0300 Subject: [PATCH 15/15] cosmetics --- GraphView/drag_n_drop.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/GraphView/drag_n_drop.py b/GraphView/drag_n_drop.py index 47ce41928..c94d64348 100644 --- a/GraphView/drag_n_drop.py +++ b/GraphView/drag_n_drop.py @@ -187,7 +187,7 @@ def drag_data_receive(self, widget, context, x, y, data, info, time): elif p_data[0] == 'person-link': out_data.append(p_data[2]) - action_menu = Popover(self.canvas, _('Available actions')) + action_menu = Popover(self.canvas) rect = Gdk.Rectangle() rect.x = x rect.y = y @@ -371,17 +371,14 @@ class Popover(Gtk.Popover): """ Display available actions on drop event. """ - def __init__(self, widget, label): + def __init__(self, widget): Gtk.Popover.__init__(self, relative_to=widget) self.set_modal(True) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - lbl = Gtk.Label(label) - self.box.add(lbl) - - self.cancel_btn = Gtk.Button(label=_('Cancel')) - self.box.pack_end(self.cancel_btn, True, True, 5) + self.cancel_btn = Gtk.Button(label=_('Cancel'), margin_top=5) + self.box.pack_end(self.cancel_btn, True, True, 1) self.cancel_btn.connect('clicked', self.popdown) # set all widgets visible @@ -393,13 +390,11 @@ def add_action(self, label, callback, *args): Add button to popover with action. """ action = Gtk.Button(label=label) - self.box.pack_end(action, True, True, 5) + self.box.pack_start(action, True, True, 1) if callback: action.connect('clicked', callback, *args) action.connect('clicked', self.popdown) action.show() - # move cancel button to the bottom - self.box.reorder_child(self.cancel_btn, 0) def popup(self): """