From 767430f93f84812e3e82b1ce804390d3d093402f Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Sat, 26 Oct 2013 17:56:39 +0000 Subject: [PATCH] Modify media reference editor to use new selection widget svn: r23421 --- gramps/gui/editors/editmediaref.py | 249 +++------- gramps/gui/glade/editmediaref.glade | 233 ++++----- gramps/gui/widgets/__init__.py | 1 + gramps/gui/widgets/grabbers.py | 277 +++++++++++ gramps/gui/widgets/selectionwidget.py | 661 ++++++++++++++++++++++++++ 5 files changed, 1080 insertions(+), 341 deletions(-) create mode 100644 gramps/gui/widgets/grabbers.py create mode 100644 gramps/gui/widgets/selectionwidget.py diff --git a/gramps/gui/editors/editmediaref.py b/gramps/gui/editors/editmediaref.py index df61f955f..6a41f661c 100644 --- a/gramps/gui/editors/editmediaref.py +++ b/gramps/gui/editors/editmediaref.py @@ -6,6 +6,7 @@ # 2009 Gary Burton # 2011 Robert Cheramy # Copyright (C) 2011 Tim G L Lyons +# Copyright (C) 2013 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -58,7 +59,7 @@ from ..glade import Glade from .displaytabs import (CitationEmbedList, AttrEmbedList, MediaBackRefList, NoteTab) from ..widgets import (MonitoredSpinButton, MonitoredEntry, PrivacyButton, - MonitoredDate, MonitoredTagList) + MonitoredDate, MonitoredTagList, SelectionWidget, Region) from .editreference import RefTab, EditReference from .addmedia import AddMediaObject @@ -136,7 +137,7 @@ class EditMediaRef(EditReference): if not self.source.get_mime_type(): self.mimetext.set_text(_('Note')) - def draw_preview(self, onlysub=False): + def draw_preview(self): """ Draw the two preview images. This method can be called on eg change of the path. @@ -144,33 +145,18 @@ class EditMediaRef(EditReference): mtype = self.source.get_mime_type() if mtype: fullpath = media_path_full(self.db, self.source.get_path()) - if not onlysub: - pb = get_thumbnail_image(fullpath, mtype) - self.pixmap.set_from_pixbuf(pb) - subpix = get_thumbnail_image(fullpath, mtype, - self.rectangle) - self.subpixmap.set_from_pixbuf(subpix) + pb = get_thumbnail_image(fullpath, mtype) + self.pixmap.set_from_pixbuf(pb) + self.selection.load_image(fullpath) else: pb = find_mime_type_pixbuf('text/plain') - if not onlysub: - self.pixmap.set_from_pixbuf(pb) - self.subpixmap.set_from_pixbuf(pb) + self.pixmap.set_from_pixbuf(pb) + self.selection.load_image('') def _setup_fields(self): ebox_shared = self.top.get_object('eventbox') ebox_shared.connect('button-press-event', self.button_press_event) - - if not self.dbstate.db.readonly: - self.button_press_coords = (0, 0) - ebox_ref = self.top.get_object('eventbox1') - ebox_ref.connect('button-press-event', self.button_press_event_ref) - ebox_ref.connect('button-release-event', - self.button_release_event_ref) - ebox_ref.connect('motion-notify-event', self.motion_notify_event_ref) - ebox_ref.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) - ebox_ref.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) - self.pixmap = self.top.get_object("pixmap") self.mimetext = self.top.get_object("type") self.track_ref_for_deletion("mimetext") @@ -185,13 +171,22 @@ class EditMediaRef(EditReference): ): coord = None - self.rectangle = coord - self.subpixmap = self.top.get_object("subpixmap") - self.track_ref_for_deletion("subpixmap") + if coord is not None: + self.rectangle = coord + else: + self.rectangle = (0, 0, 100, 100) + + self.selection = SelectionWidget() + self.selection.set_multiple_selection(False) + self.selection.connect("region-modified", self.region_modified) + self.selection.connect("region-created", self.region_modified) + frame = self.top.get_object("frame9") + frame.add(self.selection) + self.track_ref_for_deletion("selection") + self.selection.show() self.setup_filepath() self.determine_mime() - self.draw_preview() corners = ["corner1_x", "corner1_y", "corner2_x", "corner2_y"] @@ -278,6 +273,13 @@ class EditMediaRef(EditReference): self.uistate, self.track, self.db.readonly) + def _post_init(self): + """ + Initialization that must happen after the window is shown. + """ + self.draw_preview() + self.update_region() + def set_corner1_x(self, value): """ Callback for the signal handling of the spinbutton for the first @@ -287,10 +289,8 @@ class EditMediaRef(EditReference): @param value: the first corner x coordinate of the subsection in int """ - if self.rectangle is None: - self.rectangle = (0,0,100,100) self.rectangle = (value,) + self.rectangle[1:] - self.update_subpixmap() + self.update_region() def set_corner1_y(self, value): """ @@ -301,10 +301,8 @@ class EditMediaRef(EditReference): @param value: the first corner y coordinate of the subsection in int """ - if self.rectangle is None: - self.rectangle = (0,0,100,100) self.rectangle = self.rectangle[:1] + (value,) + self.rectangle[2:] - self.update_subpixmap() + self.update_region() def set_corner2_x(self, value): """ @@ -315,10 +313,8 @@ class EditMediaRef(EditReference): @param value: the second corner x coordinate of the subsection in int """ - if self.rectangle is None: - self.rectangle = (0,0,100,100) self.rectangle = self.rectangle[:2] + (value,) + self.rectangle[3:] - self.update_subpixmap() + self.update_region() def set_corner2_y(self, value): """ @@ -329,10 +325,8 @@ class EditMediaRef(EditReference): @param value: the second corner y coordinate of the subsection in int """ - if self.rectangle is None: - self.rectangle = (0,0,100,100) self.rectangle = self.rectangle[:3] + (value,) - self.update_subpixmap() + self.update_region() def get_corner1_x(self): """ @@ -342,11 +336,7 @@ class EditMediaRef(EditReference): @returns: the first corner x coordinate of the subsection or 0 if there is no selection """ - - if self.rectangle is not None: - return self.rectangle[0] - else: - return 0 + return self.rectangle[0] def get_corner1_y(self): """ @@ -356,11 +346,7 @@ class EditMediaRef(EditReference): @returns: the first corner y coordinate of the subsection or 0 if there is no selection """ - - if self.rectangle is not None: - return self.rectangle[1] - else: - return 0 + return self.rectangle[1] def get_corner2_x(self): """ @@ -370,11 +356,7 @@ class EditMediaRef(EditReference): @returns: the second corner x coordinate of the subsection or 100 if there is no selection """ - - if self.rectangle is not None: - return self.rectangle[2] - else: - return 100 + return self.rectangle[2] def get_corner2_y(self): """ @@ -384,17 +366,29 @@ class EditMediaRef(EditReference): @returns: the second corner x coordinate of the subsection or 100 if there is no selection """ - - if self.rectangle is not None: - return self.rectangle[3] - else: - return 100 + return self.rectangle[3] - def update_subpixmap(self): + def update_region(self): """ - Updates the thumbnail of the specified subsection + Updates the thumbnail of the specified subsection. """ - self.draw_preview(onlysub=True) + if not self.selection.is_image_loaded(): + return + real = self.selection.proportional_to_real_rect(self.rectangle) + region = Region(real[0], real[1], real[2], real[3]) + self.selection.set_regions([region]) + self.selection.refresh() + + def region_modified(self, widget): + """ + Update new values when the selection is changed. + """ + real = self.selection.get_selection() + coords = self.selection.real_to_proportional_rect(real) + self.corner1_x_spinbutton.set_value(coords[0]) + self.corner1_y_spinbutton.set_value(coords[1]) + self.corner2_x_spinbutton.set_value(coords[2]) + self.corner2_y_spinbutton.set_value(coords[3]) def build_menu_names(self, person): """ @@ -412,139 +406,6 @@ class EditMediaRef(EditReference): photo_path = media_path_full(self.db, self.source.get_path()) open_file_with_default_application(photo_path) - def button_press_event_ref(self, widget, event): - """ - Handle the button-press-event generated by the eventbox - parent of the subpixmap. Remember these coordinates - so we can crop the picture when button-release-event - is received. - """ - self.button_press_coords = (event.x, event.y) - # prepare drawing of a feedback rectangle - self.orig_subpixbuf = self.subpixmap.get_pixbuf() - gtkimg_win = self.subpixmap.get_window() - self.rect_pixbuf = Gdk.pixbuf_get_from_window( - gtkimg_win, 0, 0, - gtkimg_win.get_width(), - gtkimg_win.get_height()) - - def motion_notify_event_ref(self, widget, event): - # get the image size and calculate the X and Y offsets - # (image is centered *horizontally* when smaller than THUMBSCALE) - w, h = self.orig_subpixbuf.get_width(), self.orig_subpixbuf.get_height() - offset_x = (THUMBSCALE - w) / 2 - offset_y = 0 - - # get coordinates of the rectangle, so that x1 < x2 and y1 < y2 - x1 = min(self.button_press_coords[0], event.x) - x2 = max(self.button_press_coords[0], event.x) - y1 = min(self.button_press_coords[1], event.y) - y2 = max(self.button_press_coords[1], event.y) - - width = int(x2 - x1) - height = int(y2 - y1) - x1 = int(x1 - offset_x) - y1 = int(y1 - offset_y) - - cr_pixbuf = self.subpixmap.get_window().cairo_create() - cr_pixbuf.reset_clip() - #reset the pixbuf - Gdk.cairo_set_source_pixbuf(cr_pixbuf, self.rect_pixbuf, 0, 0) - cr_pixbuf.paint() - cr_pixbuf.set_source_rgba(0, 0, 1, 0.5) #blue transparant - cr_pixbuf.move_to(x1,y1) - cr_pixbuf.line_to(x1+width, y1) - cr_pixbuf.line_to(x1+width, y1+height) - cr_pixbuf.line_to(x1, y1+height) - cr_pixbuf.close_path() - cr_pixbuf.fill() - - def button_release_event_ref(self, widget, event): - """ - Handle the button-release-event generated by the eventbox - parent of the subpixmap. Crop the picture accordingly. - """ - - # reset the crop on double-click or click+CTRL - if (event.button==1 and event.type == Gdk.EventType._2BUTTON_PRESS) or \ - (event.button==1 and (event.get_state() & Gdk.ModifierType.CONTROL_MASK) ): - self.corner1_x_spinbutton.set_value(0) - self.corner1_y_spinbutton.set_value(0) - self.corner2_x_spinbutton.set_value(100) - self.corner2_y_spinbutton.set_value(100) - - else: - if (self.orig_subpixbuf == None): - return - self.subpixmap.set_from_pixbuf(self.orig_subpixbuf) - - # ensure the clicks happened at least 5 pixels away from each other - new_x1 = min(self.button_press_coords[0], event.x) - new_y1 = min(self.button_press_coords[1], event.y) - new_x2 = max(self.button_press_coords[0], event.x) - new_y2 = max(self.button_press_coords[1], event.y) - - if new_x2 - new_x1 >= 5 and new_y2 - new_y1 >= 5: - - # get the image size and calculate the X and Y offsets - # (image is centered *horizontally* when smaller than THUMBSCALE) - w = self.orig_subpixbuf.get_width() - h = self.orig_subpixbuf.get_height() - x = (THUMBSCALE - w) / 2 - y = 0 - - # if the click was outside of the image, - # bring it within the boundaries - if new_x1 < x: - new_x1 = x - if new_y1 < y: - new_y1 = y - if new_x2 >= x + w: - new_x2 = x + w - 1 - if new_y2 >= y + h: - new_y2 = y + h - 1 - - # get the old spinbutton % values - old_x1 = self.corner1_x_spinbutton.get_value() - old_y1 = self.corner1_y_spinbutton.get_value() - old_x2 = self.corner2_x_spinbutton.get_value() - old_y2 = self.corner2_y_spinbutton.get_value() - delta_x = old_x2 - old_x1 # horizontal scale - delta_y = old_y2 - old_y1 # vertical scale - - # Took a while to figure out the math here. - # - # 1) figure out the current crop % values - # by doing the following: - # - # xp = click_location_x / width * 100 - # yp = click_location_y / height * 100 - # - # but remember that click_location_x and _y - # might not be zero-based for non-rectangular - # images, so subtract the pixbuf "x" and "y" - # to bring the values back to zero-based - # - # 2) the minimum value cannot be less than the - # existing crop value, so add the current - # minimum to the new values - - new_x1 = old_x1 + delta_x * (new_x1 - x) / w - new_y1 = old_y1 + delta_y * (new_y1 - y) / h - new_x2 = old_x1 + delta_x * (new_x2 - x) / w - new_y2 = old_y1 + delta_y * (new_y2 - y) / h - - # set the new values - self.corner1_x_spinbutton.set_value(new_x1) - self.corner1_y_spinbutton.set_value(new_y1) - self.corner2_x_spinbutton.set_value(new_x2) - self.corner2_y_spinbutton.set_value(new_y2) - - # Free the pixbuf as it is not needed anymore - self.orig_subpixbuf = None - self.rect_pixbuf = None - self.draw_preview() - def _update_addmedia(self, obj): """ Called when the add media dialog has been called. diff --git a/gramps/gui/glade/editmediaref.glade b/gramps/gui/glade/editmediaref.glade index a7cc8d04e..34816df7d 100644 --- a/gramps/gui/glade/editmediaref.glade +++ b/gramps/gui/glade/editmediaref.glade @@ -39,12 +39,10 @@ gtk-cancel - False True True True True - False True @@ -56,13 +54,11 @@ gtk-ok - False True True True True True - False True @@ -74,12 +70,10 @@ gtk-help - False True True True True - False True @@ -111,24 +105,15 @@ True False 12 - 5 - 5 + 6 + 3 12 6 - - - - - - - - - True False - 0 + 1 _Corner 2: X True corner2_x @@ -136,46 +121,28 @@ 1 2 - 3 - 4 - - + 4 + 5 + GTK_FILL + GTK_FILL True False - 0 + 1 Y coordinate|Y True corner2_y - 3 - 4 - 3 - 4 - - - - - - - True - False - 0 - Referenced Region - - - - - - 5 - 1 - 2 + 1 + 2 + 5 + 6 GTK_FILL - + GTK_FILL @@ -190,11 +157,12 @@ You can use the mouse on the picture to select a region, or use these spinbutton True - 4 - 5 - 3 - 4 - + 2 + 3 + 5 + 6 + GTK_FILL + @@ -211,9 +179,10 @@ You can use the mouse on the picture to select a region, or use these spinbutton 2 3 - 3 - 4 - + 4 + 5 + GTK_FILL + @@ -224,19 +193,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton Select a region with clicking and holding the mouse button on the top left corner of the region you want, dragging the mouse to the bottom right corner of the region, and then releasing the mouse button. 0 - - True - False - - - 100 - 100 - True - False - 0 - - - + @@ -250,17 +207,14 @@ Select a region with clicking and holding the mouse button on the top left corne - 2 - 5 - - + 6 True False - 0 + 1 _Corner 1: X True corner1_x @@ -270,13 +224,10 @@ Select a region with clicking and holding the mouse button on the top left corne 2 2 3 - - + GTK_FILL + GTK_FILL - - - True @@ -285,16 +236,19 @@ Select a region with clicking and holding the mouse button on the top left corne You can use the mouse on the picture to select a region, or use these spinbuttons to set the top left, and bottom right corner of the referenced region. Point (0,0) is the top left corner of the picture, and (100,100) the bottom right corner. + none adjustment2 1 True + if-valid 2 3 2 3 - + GTK_FILL + @@ -309,67 +263,52 @@ You can use the mouse on the picture to select a region, or use these spinbutton True - 4 - 5 - 2 - 3 - + 2 + 3 + 3 + 4 + GTK_FILL + True False - 0 + 1 Y coordinate|Y True corner1_y - 3 - 4 - 2 - 3 - - + 1 + 2 + 3 + 4 + GTK_FILL + GTK_FILL - - - True False - 2 - 3 4 5 - + - - - - - - - - - True False - False True True True - False none @@ -400,10 +339,10 @@ You can use the mouse on the picture to select a region, or use these spinbutton - 4 - 5 + 2 + 3 GTK_FILL - + @@ -413,22 +352,17 @@ You can use the mouse on the picture to select a region, or use these spinbutton - - - - - - - - - - - - - - - - + + True + False + + + 2 + 3 + 1 + 2 + + @@ -447,7 +381,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton - False + True True 10 0 @@ -499,7 +433,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 3 4 GTK_FILL - + @@ -516,7 +450,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 1 2 GTK_FILL - + @@ -550,7 +484,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 3 1 2 - + @@ -595,6 +529,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 6 7 GTK_FILL + GTK_FILL @@ -650,7 +585,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 5 6 GTK_FILL - + @@ -666,7 +601,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 5 6 GTK_FILL - + @@ -684,7 +619,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 2 3 GTK_FILL - + @@ -698,18 +633,16 @@ You can use the mouse on the picture to select a region, or use these spinbutton 3 2 3 - + - False True True True True Invoke date editor - False none @@ -739,16 +672,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton 2 3 GTK_FILL - + - False True True True - False none @@ -772,6 +703,8 @@ You can use the mouse on the picture to select a region, or use these spinbutton 3 4 + GTK_FILL + GTK_FILL @@ -783,17 +716,16 @@ You can use the mouse on the picture to select a region, or use these spinbutton 2 3 + GTK_FILL - False True True True True Select a file - False none @@ -822,6 +754,8 @@ You can use the mouse on the picture to select a region, or use these spinbutton 4 3 4 + GTK_FILL + GTK_FILL @@ -835,6 +769,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 3 3 4 + GTK_FILL @@ -852,7 +787,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton 4 5 GTK_FILL - + @@ -865,15 +800,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton 3 4 5 + GTK_FILL - False True True True - False @@ -888,9 +822,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton 4 4 5 + GTK_FILL + GTK_FILL + + False + @@ -919,14 +858,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton - True - True + False + False 1 - False + True True 1 diff --git a/gramps/gui/widgets/__init__.py b/gramps/gui/widgets/__init__.py index 6ffd47ab9..9e869727a 100644 --- a/gramps/gui/widgets/__init__.py +++ b/gramps/gui/widgets/__init__.py @@ -31,6 +31,7 @@ from .labels import * from .linkbox import * from .photo import * from .monitoredwidgets import * +from .selectionwidget import SelectionWidget, Region from .shortlistcomboentry import * from .springseparator import * from .statusbar import Statusbar diff --git a/gramps/gui/widgets/grabbers.py b/gramps/gui/widgets/grabbers.py new file mode 100644 index 000000000..999845eee --- /dev/null +++ b/gramps/gui/widgets/grabbers.py @@ -0,0 +1,277 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2013 Artem Glebov +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +#------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +#------------------------------------------------------------------------- +from gi.repository import Gdk + +#------------------------------------------------------------------------- +# +# grabbers constants and routines +# +#------------------------------------------------------------------------- + +GRABBER_INSIDE = 0 +GRABBER_OUTSIDE = 1 + +MIN_CORNER_GRABBER = 20 +MIN_SIDE_GRABBER = 20 +MIN_GRABBER_PADDING = 10 +MIN_SIDE_FOR_INSIDE_GRABBERS = (2 * (MIN_CORNER_GRABBER + MIN_GRABBER_PADDING) + + MIN_SIDE_GRABBER) + +INSIDE = 0 +GRABBER_UPPER_LEFT = 1 +GRABBER_UPPER = 2 +GRABBER_UPPER_RIGHT = 3 +GRABBER_RIGHT = 4 +GRABBER_LOWER_RIGHT = 5 +GRABBER_LOWER = 6 +GRABBER_LOWER_LEFT = 7 +GRABBER_LEFT = 8 + +def upper_left_grabber_inner(x1, y1, x2, y2): + return (x1, y1, x1 + MIN_CORNER_GRABBER, y1 + MIN_CORNER_GRABBER) + +def upper_grabber_inner(x1, y1, x2, y2): + return (x1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING, + y1, + x2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING, + y1 + MIN_CORNER_GRABBER) + +def upper_right_grabber_inner(x1, y1, x2, y2): + return (x2 - MIN_CORNER_GRABBER, y1, x2, y1 + MIN_CORNER_GRABBER) + +def right_grabber_inner(x1, y1, x2, y2): + return (x2 - MIN_CORNER_GRABBER, + y1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING, + x2, + y2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING) + +def lower_right_grabber_inner(x1, y1, x2, y2): + return (x2 - MIN_CORNER_GRABBER, y2 - MIN_CORNER_GRABBER, x2, y2) + +def lower_grabber_inner(x1, y1, x2, y2): + return (x1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING, + y2 - MIN_CORNER_GRABBER, + x2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING, + y2) + +def lower_left_grabber_inner(x1, y1, x2, y2): + return (x1, y2 - MIN_CORNER_GRABBER, x1 + MIN_CORNER_GRABBER, y2) + +def left_grabber_inner(x1, y1, x2, y2): + return (x1, + y1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING, + x1 + MIN_CORNER_GRABBER, + y2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING) + +# outer + +def upper_left_grabber_outer(x1, y1, x2, y2): + return (x1 - MIN_CORNER_GRABBER, y1 - MIN_CORNER_GRABBER, x1, y1) + +def upper_grabber_outer(x1, y1, x2, y2): + return (x1, y1 - MIN_CORNER_GRABBER, x2, y1) + +def upper_right_grabber_outer(x1, y1, x2, y2): + return (x2, y1 - MIN_CORNER_GRABBER, x2 + MIN_CORNER_GRABBER, y1) + +def right_grabber_outer(x1, y1, x2, y2): + return (x2, y1, x2 + MIN_CORNER_GRABBER, y2) + +def lower_right_grabber_outer(x1, y1, x2, y2): + return (x2, y2, x2 + MIN_CORNER_GRABBER, y2 + MIN_CORNER_GRABBER) + +def lower_grabber_outer(x1, y1, x2, y2): + return (x1, y2, x2, y2 + MIN_CORNER_GRABBER) + +def lower_left_grabber_outer(x1, y1, x2, y2): + return (x1 - MIN_CORNER_GRABBER, y2, x1, y2 + MIN_CORNER_GRABBER) + +def left_grabber_outer(x1, y1, x2, y2): + return (x1 - MIN_CORNER_GRABBER, y1, x1, y2) + +# motion + +def inside_moved(x1, y1, x2, y2, dx, dy): + return (x1, y1, x2, y2) + +def upper_left_moved(x1, y1, x2, y2, dx, dy): + return (x1 + dx, y1 + dy, x2, y2) + +def upper_moved(x1, y1, x2, y2, dx, dy): + return (x1, y1 + dy, x2, y2) + +def upper_right_moved(x1, y1, x2, y2, dx, dy): + return (x1, y1 + dy, x2 + dx, y2) + +def right_moved(x1, y1, x2, y2, dx, dy): + return (x1, y1, x2 + dx, y2) + +def lower_right_moved(x1, y1, x2, y2, dx, dy): + return (x1, y1, x2 + dx, y2 + dy) + +def lower_moved(x1, y1, x2, y2, dx, dy): + return (x1, y1, x2, y2 + dy) + +def lower_left_moved(x1, y1, x2, y2, dx, dy): + return (x1 + dx, y1, x2, y2 + dy) + +def left_moved(x1, y1, x2, y2, dx, dy): + return (x1 + dx, y1, x2, y2) + +# switching + +GRABBERS = [INSIDE, + GRABBER_UPPER_LEFT, + GRABBER_UPPER, + GRABBER_UPPER_RIGHT, + GRABBER_RIGHT, + GRABBER_LOWER_RIGHT, + GRABBER_LOWER, + GRABBER_LOWER_LEFT, + GRABBER_LEFT] + +INNER_GRABBERS = [None, + upper_left_grabber_inner, + upper_grabber_inner, + upper_right_grabber_inner, + right_grabber_inner, + lower_right_grabber_inner, + lower_grabber_inner, + lower_left_grabber_inner, + left_grabber_inner] + +OUTER_GRABBERS = [None, + upper_left_grabber_outer, + upper_grabber_outer, + upper_right_grabber_outer, + right_grabber_outer, + lower_right_grabber_outer, + lower_grabber_outer, + lower_left_grabber_outer, + left_grabber_outer] + +MOTION_FUNCTIONS = [inside_moved, + upper_left_moved, + upper_moved, + upper_right_moved, + right_moved, + lower_right_moved, + lower_moved, + lower_left_moved, + left_moved] + +GRABBERS_SWITCH = [ + [INSIDE, INSIDE, INSIDE], + [GRABBER_UPPER_RIGHT, GRABBER_LOWER_RIGHT, GRABBER_LOWER_LEFT], + [GRABBER_UPPER, GRABBER_LOWER, GRABBER_LOWER], + [GRABBER_UPPER_LEFT, GRABBER_LOWER_LEFT, GRABBER_UPPER_RIGHT], + [GRABBER_LEFT, GRABBER_LEFT, GRABBER_RIGHT], + [GRABBER_LOWER_LEFT, GRABBER_UPPER_LEFT, GRABBER_UPPER_RIGHT], + [GRABBER_LOWER, GRABBER_UPPER, GRABBER_UPPER], + [GRABBER_LOWER_RIGHT, GRABBER_UPPER_RIGHT, GRABBER_UPPER_LEFT], + [GRABBER_RIGHT, GRABBER_RIGHT, GRABBER_LEFT] +] + +# cursors + +CURSOR_UPPER = Gdk.Cursor.new(Gdk.CursorType.TOP_SIDE) +CURSOR_LOWER = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_SIDE) +CURSOR_LEFT = Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE) +CURSOR_RIGHT = Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE) +CURSOR_UPPER_LEFT = Gdk.Cursor.new(Gdk.CursorType.TOP_LEFT_CORNER) +CURSOR_UPPER_RIGHT = Gdk.Cursor.new(Gdk.CursorType.TOP_RIGHT_CORNER) +CURSOR_LOWER_LEFT = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_LEFT_CORNER) +CURSOR_LOWER_RIGHT = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_RIGHT_CORNER) + +CURSORS = [None, + CURSOR_UPPER_LEFT, + CURSOR_UPPER, + CURSOR_UPPER_RIGHT, + CURSOR_RIGHT, + CURSOR_LOWER_RIGHT, + CURSOR_LOWER, + CURSOR_LOWER_LEFT, + CURSOR_LEFT] + +# helper functions + +def grabber_position(rect): + x1, y1, x2, y2 = rect + if (x2 - x1 >= MIN_SIDE_FOR_INSIDE_GRABBERS and + y2 - y1 >= MIN_SIDE_FOR_INSIDE_GRABBERS): + return GRABBER_INSIDE + else: + return GRABBER_OUTSIDE + +def grabber_generators(rect): + if grabber_position(rect) == GRABBER_INSIDE: + return INNER_GRABBERS + else: + return OUTER_GRABBERS + +def switch_grabber(grabber, x1, y1, x2, y2): + switch_row = GRABBERS_SWITCH[grabber] + if x1 > x2: + if y1 > y2: + return switch_row[1] + else: + return switch_row[0] + else: + if y1 > y2: + return switch_row[2] + else: + return grabber + +def inside_rect(rect, x, y): + x1, y1, x2, y2 = rect + return x1 <= x <= x2 and y1 <= y <= y2 + +def can_grab(rect, x, y): + """ + Checks if (x,y) lies within one of the grabbers of rect. + """ + (x1, y1, x2, y2) = rect + if (x2 - x1 >= MIN_SIDE_FOR_INSIDE_GRABBERS and + y2 - y1 >= MIN_SIDE_FOR_INSIDE_GRABBERS): + # grabbers are inside + if x < x1 or x > x2 or y < y1 or y > y2: + return None + for grabber in GRABBERS[1:]: + grabber_area = INNER_GRABBERS[grabber](x1, y1, x2, y2) + if inside_rect(grabber_area, x, y): + return grabber + return INSIDE + else: + # grabbers are outside + if x1 <= x <= x2 and y1 <= y <= y2: + return INSIDE + for grabber in GRABBERS[1:]: + grabber_area = OUTER_GRABBERS[grabber](x1, y1, x2, y2) + if inside_rect(grabber_area, x, y): + return grabber + return None diff --git a/gramps/gui/widgets/selectionwidget.py b/gramps/gui/widgets/selectionwidget.py new file mode 100644 index 000000000..b91d5aeb6 --- /dev/null +++ b/gramps/gui/widgets/selectionwidget.py @@ -0,0 +1,661 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2013 Artem Glebov +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +#------------------------------------------------------------------------- +# +# Standard python modules +# +#------------------------------------------------------------------------- +from __future__ import division + +#------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +#------------------------------------------------------------------------- +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GdkPixbuf +from gi.repository import GObject + +#------------------------------------------------------------------------- +# +# gramps modules +# +#------------------------------------------------------------------------- +from gramps.gen.display.name import displayer as name_displayer + +#------------------------------------------------------------------------- +# +# grabbers constants and routines +# +#------------------------------------------------------------------------- +from .grabbers import (grabber_generators, can_grab, grabber_position, + switch_grabber, CURSORS, GRABBER_INSIDE, INSIDE, + INNER_GRABBERS, OUTER_GRABBERS, MOTION_FUNCTIONS) + +#------------------------------------------------------------------------- +# +# PhotoTaggingGramplet +# +#------------------------------------------------------------------------- + +RESIZE_RATIO = 1.5 +MAX_ZOOM = 10 +MIN_ZOOM = 0.05 +MAX_SIZE = 2000 +MIN_SIZE = 50 +SHADING_OPACITY = 0.7 +MIN_SELECTION_SIZE = 10 + +def scale_to_fit(orig_x, orig_y, target_x, target_y): + orig_aspect = orig_x / orig_y + target_aspect = target_x / target_y + if orig_aspect > target_aspect: + return target_x / orig_x + else: + return target_y / orig_y + +def resize_keep_aspect(orig_x, orig_y, target_x, target_y): + orig_aspect = orig_x / orig_y + target_aspect = target_x / target_y + if orig_aspect > target_aspect: + return (target_x, target_x * orig_y // orig_x) + else: + return (target_y * orig_x // orig_y, target_y) + +def order_coordinates(point1, point2): + """ + Returns the rectangle (x1, y1, x2, y2) based on point1 and point2, + such that x1 <= x2 and y1 <= y2. + """ + x1 = min(point1[0], point2[0]) + x2 = max(point1[0], point2[0]) + y1 = min(point1[1], point2[1]) + y2 = max(point1[1], point2[1]) + return (x1, y1, x2, y2) + + +class Region(object): + """ + Representation of a region of image that can be associated with + a person. + """ + + def __init__(self, x1, y1, x2, y2): + self.set_coords(x1, y1, x2, y2) + self.person = None + self.mediaref = None + + def coords(self): + return (self.x1, self.y1, self.x2, self.y2) + + def set_coords(self, x1, y1, x2, y2): + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + + def contains(self, x, y): + return self.x1 <= x <= self.x2 and self.y1 <= y <= self.y2 + + def contains_rect(self, other): + return (self.contains(other.x1, other.y1) and + self.contains(other.x2, other.y2)) + + def area(self): + return abs(self.x1 - self.x2) * abs(self.y1 - self.y2) + + def intersects(self, other): + # assumes that x1 <= x2 and y1 <= y2 + return not (self.x2 < other.x1 or self.x1 > other.x2 or + self.y2 < other.y1 or self.y1 > other.y2) + +class SelectionWidget(Gtk.ScrolledWindow): + + __gsignals__ = { + "region-modified": (GObject.SIGNAL_RUN_FIRST, None, ()), + "region-created": (GObject.SIGNAL_RUN_FIRST, None, ()), + "region-selected": (GObject.SIGNAL_RUN_FIRST, None, ()), + "selection-cleared": (GObject.SIGNAL_RUN_FIRST, None, ()), + "right-button-clicked": (GObject.SIGNAL_RUN_FIRST, None, ()), + "zoomed-in": (GObject.SIGNAL_RUN_FIRST, None, ()), + "zoomed-out": (GObject.SIGNAL_RUN_FIRST, None, ()) + } + + def __init__(self): + self.multiple_selection = True + + self.loaded = False + self.start_point_screen = None + self.selection = None + self.current = None + self.in_region = None + self.grabber = None + self.regions = [] + self.translation = None + self.pixbuf = None + self.scaled_pixbuf = None + self.scale = 1.0 + + Gtk.ScrolledWindow.__init__(self) + self.add(self.build_gui()) + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + def build_gui(self): + self.image = Gtk.Image() + self.image.set_has_tooltip(True) + self.image.connect_after("draw", self.expose_handler) + self.image.connect("query-tooltip", self.show_tooltip) + + self.event_box = Gtk.EventBox() + self.event_box.connect('button-press-event', + self.button_press_event) + self.event_box.connect('button-release-event', + self.button_release_event) + self.event_box.connect('motion-notify-event', + self.motion_notify_event) + self.event_box.connect('scroll-event', + self.motion_scroll_event) + self.event_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) + self.event_box.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) + self.event_box.add_events(Gdk.EventMask.POINTER_MOTION_MASK) + + self.event_box.add(self.image) + + self.viewport = Gtk.Viewport() + self.viewport.add(self.event_box) + return self.viewport + + # ====================================================== + # field accessors + # ====================================================== + + def get_multiple_selection(self): + """ + Return whether multiple selection is enabled. + """ + return self.multiple_selection + + def set_multiple_selection(self, enable): + """ + Enables or disables multiple selection. + """ + self.multiple_selection = enable + + def set_regions(self, regions): + self.regions = regions + + def get_current(self): + return self.current + + def set_current(self, region): + self.current = region + + def get_selection(self): + return self.selection + + # ====================================================== + # loading the image + # ====================================================== + + def load_image(self, image_path): + self.start_point_screen = None + self.selection = None + self.in_region = None + self.grabber_position = None + self.grabber_to_draw = None + + try: + self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path) + self.original_image_size = (self.pixbuf.get_width(), + self.pixbuf.get_height()) + + viewport_size = self.viewport.get_allocation() + self.scale = scale_to_fit(self.pixbuf.get_width(), + self.pixbuf.get_height(), + viewport_size.width, + viewport_size.height) + self.rescale() + self.loaded = True + except (GObject.GError, OSError): + self.show_missing() + + def show_missing(self): + self.pixbuf = None + self.image.set_from_stock(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG) + self.image.queue_draw() + + # ====================================================== + # utility functions for retrieving properties + # ====================================================== + + def is_image_loaded(self): + return self.loaded + + def get_original_image_size(self): + return self.original_image_size + + def get_scaled_image_size(self): + unscaled_size = self.get_original_image_size() + return (unscaled_size[0] * self.scale, unscaled_size[1] * self.scale) + + def get_viewport_size(self): + rect = self.viewport.get_allocation() + return (rect.width, rect.height) + + def get_used_screen_size(self): + scaled_image_size = self.get_scaled_image_size() + viewport_size = self.get_viewport_size() + return (min(scaled_image_size[0], viewport_size[0]), + min(scaled_image_size[1], viewport_size[1])) + + # ====================================================== + # coordinate transformations + # ====================================================== + + def proportional_to_real(self, coord): + """ + Translate proportional (ranging from 0 to 100) coordinates to image + coordinates (in pixels). + """ + w, h = self.original_image_size + return (int(round(coord[0] * w / 100)), int(round(coord[1] * h / 100))) + + def real_to_proportional(self, coord): + """ + Translate image coordinates (in pixels) to proportional (ranging + from 0 to 100). + """ + w, h = self.original_image_size + return (int(round(coord[0] * 100 / w)), int(round(coord[1] * 100 / h))) + + def proportional_to_real_rect(self, rect): + x1, y1, x2, y2 = rect + return (self.proportional_to_real((x1, y1)) + + self.proportional_to_real((x2, y2))) + + def real_to_proportional_rect(self, rect): + x1, y1, x2, y2 = rect + return (self.real_to_proportional((x1, y1)) + + self.real_to_proportional((x2, y2))) + + def image_to_screen(self, coords): + """ + Translate image coordinates to viewport coordinates using the current + scale and viewport size. + """ + viewport_rect = self.viewport.get_allocation() + image_rect = self.scaled_size + if image_rect[0] < viewport_rect.width: + offset_x = (image_rect[0] - viewport_rect.width) / 2 + else: + offset_x = 0.0 + if image_rect[1] < viewport_rect.height: + offset_y = (image_rect[1] - viewport_rect.height) / 2 + else: + offset_y = 0.0 + return (int(coords[0] * self.scale - offset_x), + int(coords[1] * self.scale - offset_y)) + + def screen_to_image(self, coords): + """ + Translate viewport coordinates to original (unscaled) image coordinates + using the current scale and viewport size. + """ + viewport_rect = self.viewport.get_allocation() + image_rect = self.scaled_size + if image_rect[0] < viewport_rect.width: + offset_x = (image_rect[0] - viewport_rect.width) / 2 + else: + offset_x = 0.0 + if image_rect[1] < viewport_rect.height: + offset_y = (image_rect[1] - viewport_rect.height) / 2 + else: + offset_y = 0.0 + return (int((coords[0] + offset_x) / self.scale), + int((coords[1] + offset_y) / self.scale)) + + def truncate_to_image_size(self, coords): + x, y = coords + (image_width, image_height) = self.get_original_image_size() + x = max(x, 0) + x = min(x, image_width) + y = max(y, 0) + y = min(y, image_height) + return self.proportional_to_real(self.real_to_proportional((x, y))) + + def screen_to_truncated(self, coords): + return self.truncate_to_image_size(self.screen_to_image(coords)) + + def rect_image_to_screen(self, rect): + x1, y1, x2, y2 = rect + x1, y1 = self.image_to_screen((x1, y1)) + x2, y2 = self.image_to_screen((x2, y2)) + return (x1, y1, x2, y2) + + # ====================================================== + # drawing, refreshing and zooming the image + # ====================================================== + + def draw_selection(self): + if not self.scaled_size: + return + + w, h = self.scaled_size + offset_x, offset_y = self.image_to_screen((0, 0)) + offset_x -= 1 + offset_y -= 1 + + cr = self.image.get_window().cairo_create() + + if self.selection: + x1, y1, x2, y2 = self.rect_image_to_screen(self.selection) + + # transparent shading + self.draw_transparent_shading(cr, x1, y1, x2, y2, w, h, + offset_x, offset_y) + + # selection frame + self.draw_selection_frame(cr, x1, y1, x2, y2) + + # draw grabber + self.draw_grabber(cr) + else: + # selection frame + for region in self.regions: + x1, y1, x2, y2 = self.rect_image_to_screen(region.coords()) + self.draw_region_frame(cr, x1, y1, x2, y2) + + def draw_transparent_shading(self, cr, x1, y1, x2, y2, w, h, + offset_x, offset_y): + cr.set_source_rgba(1.0, 1.0, 1.0, SHADING_OPACITY) + cr.rectangle(offset_x, offset_y, x1 - offset_x, y1 - offset_y) + cr.rectangle(offset_x, y1, x1 - offset_x, y2 - y1) + cr.rectangle(offset_x, y2, x1 - offset_x, h - y2 + offset_y) + cr.rectangle(x1, y2 + 1, x2 - x1 + 1, h - y2 + offset_y) + cr.rectangle(x2 + 1, y2 + 1, w - x2 + offset_x, h - y2 + offset_y) + cr.rectangle(x2 + 1, y1, w - x2 + offset_x, y2 - y1 + 1) + cr.rectangle(x2 + 1, offset_y, w - x2 + offset_x, y2 - offset_y) + cr.rectangle(x1, offset_y, x2 - x1 + 1, y1 - offset_y) + cr.fill() + + def draw_selection_frame(self, cr, x1, y1, x2, y2): + self.draw_region_frame(cr, x1, y1, x2, y2) + + def draw_region_frame(self, cr, x1, y1, x2, y2): + cr.set_source_rgb(1.0, 1.0, 1.0) # white + cr.rectangle(x1, y1, x2 - x1, y2 - y1) + cr.stroke() + cr.set_source_rgb(0.0, 0.0, 1.0) # blue + cr.rectangle(x1 - 2, y1 - 2, x2 - x1 + 4, y2 - y1 + 4) + cr.stroke() + + def draw_grabber(self, cr): + if self.selection is not None and self.grabber is not None: + selection_rect = self.rect_image_to_screen(self.selection) + cr.set_source_rgb(1.0, 0, 0) + if self.grabber_position is None: + generators = grabber_generators(selection_rect) + elif self.grabber_position == GRABBER_INSIDE: + generators = INNER_GRABBERS + else: + generators = OUTER_GRABBERS + if self.grabber_to_draw is not None: + generator = generators[self.grabber_to_draw] + else: + generator = generators[self.grabber] + if generator is not None: + x1, y1, x2, y2 = generator(*selection_rect) + cr.rectangle(x1, y1, x2 - x1, y2 - y1) + cr.stroke() + + def refresh(self): + self.image.queue_draw() + + def rescale(self): + self.scaled_size = (int(self.original_image_size[0] * self.scale), + int(self.original_image_size[1] * self.scale)) + self.scaled_image = self.pixbuf.scale_simple(self.scaled_size[0], + self.scaled_size[1], + GdkPixbuf.InterpType.BILINEAR) + self.image.set_from_pixbuf(self.scaled_image) + self.image.set_size_request(*self.scaled_size) + self.event_box.set_size_request(*self.scaled_size) + + def can_zoom_in(self): + scaled_size = (self.original_image_size[0] * self.scale * RESIZE_RATIO, + self.original_image_size[1] * self.scale * RESIZE_RATIO) + return scaled_size[0] < MAX_SIZE and scaled_size[1] < MAX_SIZE + + def can_zoom_out(self): + scaled_size = (self.original_image_size[0] * self.scale * RESIZE_RATIO, + self.original_image_size[1] * self.scale * RESIZE_RATIO) + return scaled_size[0] >= MIN_SIZE and scaled_size[1] >= MIN_SIZE + + def zoom_in(self): + if self.can_zoom_in(): + self.scale *= RESIZE_RATIO + self.rescale() + self.emit("zoomed-in") + + def zoom_out(self): + if self.can_zoom_out(): + self.scale /= RESIZE_RATIO + self.rescale() + self.emit("zoomed-out") + + def expose_handler(self, widget, event): + if self.pixbuf: + self.draw_selection() + + def select(self, region): + self.current = region + if self.current is not None: + self.selection = self.current.coords() + self.image.queue_draw() + + def clear_selection(self): + self.current = None + self.selection = None + self.image.queue_draw() + + # ====================================================== + # managing regions + # ====================================================== + + def find_region(self, x, y): + result = None + for region in self.regions: + if region.contains(x, y): + if result is None or result.area() > region.area(): + result = region + return result + + # ====================================================== + # thumbnails + # ====================================================== + + def get_thumbnail(self, region, thumbnail_size): + w = region.x2 - region.x1 + h = region.y2 - region.y1 + if w >= 1 and h >= 1: + subpixbuf = self.pixbuf.new_subpixbuf(region.x1, region.y1, w, h) + size = resize_keep_aspect(w, h, *thumbnail_size) + return subpixbuf.scale_simple(size[0], size[1], + GdkPixbuf.InterpType.BILINEAR) + else: + return None + + # ====================================================== + # mouse event handlers + # ====================================================== + + def button_press_event(self, obj, event): + if not self.is_image_loaded(): + return + if event.button == 1: # left button + self.start_point_screen = (event.x, event.y) + if self.current is not None and self.grabber is None: + self.current = None + self.selection = None + self.refresh() + self.emit("selection-cleared") + elif event.button == 3: # right button + # select a region, if clicked inside one + click_point = self.screen_to_image((event.x, event.y)) + self.current = self.find_region(*click_point) + self.selection = \ + self.current.coords() if self.current is not None else None + self.start_point_screen = None + self.refresh() + if self.current is not None: + self.emit("region-selected") + self.emit("right-button-clicked") + else: + self.emit("selection-cleared") + return True # don't propagate the event further + + def button_release_event(self, obj, event): + if not self.is_image_loaded(): + return + if event.button == 1: + if self.start_point_screen: + if self.current is not None: + # a box is currently selected + if self.grabber is None: + # clicked outside of the grabbing area + self.current = None + self.selection = None + self.emit("selection-cleared") + elif self.grabber != INSIDE: + # clicked on one of the grabbers + dx, dy = (event.x - self.start_point_screen[0], + event.y - self.start_point_screen[1]) + self.grabber_to_draw = self.modify_selection(dx, dy) + self.current.set_coords(*self.selection) + self.emit("region-modified") + else: + # nothing is currently selected + if (self.minimum_region(self.start_point_screen, + (event.x, event.y)) and + self.can_select()): + # region selection + region = Region(*self.selection) + self.regions.append(region) + self.current = region + self.emit("region-created") + else: + # nothing selected, just a click + click_point = \ + self.screen_to_image(self.start_point_screen) + self.current = self.find_region(*click_point) + self.selection = \ + self.current.coords() if self.current is not None \ + else None + self.emit("region-selected") + + self.start_point_screen = None + self.refresh() + + def motion_notify_event(self, widget, event): + if not self.is_image_loaded(): + return + end_point_orig = self.screen_to_image((event.x, event.y)) + end_point = self.truncate_to_image_size(end_point_orig) + if self.start_point_screen: + # selection or dragging (mouse button pressed) + if self.grabber is not None and self.grabber != INSIDE: + # dragging the grabber + dx, dy = (event.x - self.start_point_screen[0], + event.y - self.start_point_screen[1]) + self.grabber_to_draw = self.modify_selection(dx, dy) + elif self.can_select(): + # making new selection + start_point = self.screen_to_truncated(self.start_point_screen) + self.selection = order_coordinates(start_point, end_point) + else: + # motion (mouse button is not pressed) + self.in_region = self.find_region(*end_point_orig) + if self.current is not None: + # a box is active, so check if the pointer is inside a grabber + rect = self.rect_image_to_screen(self.current.coords()) + self.grabber = can_grab(rect, event.x, event.y) + if self.grabber is not None: + self.grabber_to_draw = self.grabber + self.grabber_position = grabber_position(rect) + self.event_box.get_window().set_cursor(CURSORS[self.grabber]) + else: + self.grabber_to_draw = None + self.grabber_position = None + self.event_box.get_window().set_cursor(None) + else: + # nothing is active + self.grabber = None + self.grabber_to_draw = None + self.grabber_position = None + self.event_box.get_window().set_cursor(None) + self.image.queue_draw() + + def motion_scroll_event(self, widget, event): + if not self.is_image_loaded(): + return + if event.direction == Gdk.ScrollDirection.UP: + self.zoom_in() + elif event.direction == Gdk.ScrollDirection.DOWN: + self.zoom_out() + + # ====================================================== + # helpers for mouse event handlers + # ====================================================== + + def minimum_region(self, point1, point2): + return (abs(point1[0] - point2[0]) >= MIN_SELECTION_SIZE and + abs(point1[1] - point2[1]) >= MIN_SELECTION_SIZE) + + def can_select(self): + return self.multiple_selection or len(self.regions) < 1 + + def modify_selection(self, dx, dy): + x1, y1, x2, y2 = self.rect_image_to_screen(self.current.coords()) + x1, y1, x2, y2 = MOTION_FUNCTIONS[self.grabber](x1, y1, x2, y2, dx, dy) + (x1, y1) = self.screen_to_truncated((x1, y1)) + (x2, y2) = self.screen_to_truncated((x2, y2)) + grabber = switch_grabber(self.grabber, x1, y1, x2, y2) + self.selection = order_coordinates((x1, y1), (x2, y2)) + return grabber + + # ====================================================== + # tooltips + # ====================================================== + + def show_tooltip(self, widget, x, y, keyboard_mode, tooltip): + if self.in_region: + person = self.in_region.person + if person: + name = name_displayer.display(person) + else: + return False + tooltip.set_text(name) + return True + else: + return False