Update selection widget with latest version

svn: r23426
This commit is contained in:
Nick Hall 2013-10-27 12:59:41 +00:00
parent 09388cf0aa
commit 046f89a2a7
2 changed files with 324 additions and 155 deletions

View File

@ -189,7 +189,7 @@ GRABBERS_SWITCH = [
[INSIDE, INSIDE, INSIDE], [INSIDE, INSIDE, INSIDE],
[GRABBER_UPPER_RIGHT, GRABBER_LOWER_RIGHT, GRABBER_LOWER_LEFT], [GRABBER_UPPER_RIGHT, GRABBER_LOWER_RIGHT, GRABBER_LOWER_LEFT],
[GRABBER_UPPER, GRABBER_LOWER, GRABBER_LOWER], [GRABBER_UPPER, GRABBER_LOWER, GRABBER_LOWER],
[GRABBER_UPPER_LEFT, GRABBER_LOWER_LEFT, GRABBER_UPPER_RIGHT], [GRABBER_UPPER_LEFT, GRABBER_LOWER_LEFT, GRABBER_LOWER_RIGHT],
[GRABBER_LEFT, GRABBER_LEFT, GRABBER_RIGHT], [GRABBER_LEFT, GRABBER_LEFT, GRABBER_RIGHT],
[GRABBER_LOWER_LEFT, GRABBER_UPPER_LEFT, GRABBER_UPPER_RIGHT], [GRABBER_LOWER_LEFT, GRABBER_UPPER_LEFT, GRABBER_UPPER_RIGHT],
[GRABBER_LOWER, GRABBER_UPPER, GRABBER_UPPER], [GRABBER_LOWER, GRABBER_UPPER, GRABBER_UPPER],

View File

@ -68,6 +68,10 @@ SHADING_OPACITY = 0.7
MIN_SELECTION_SIZE = 10 MIN_SELECTION_SIZE = 10
def scale_to_fit(orig_x, orig_y, target_x, target_y): def scale_to_fit(orig_x, orig_y, target_x, target_y):
"""
Calculates the scale factor to fit the rectangle
orig_x * orig_y by scaling keeping the aspect ratio.
"""
orig_aspect = orig_x / orig_y orig_aspect = orig_x / orig_y
target_aspect = target_x / target_y target_aspect = target_x / target_y
if orig_aspect > target_aspect: if orig_aspect > target_aspect:
@ -76,6 +80,11 @@ def scale_to_fit(orig_x, orig_y, target_x, target_y):
return target_y / orig_y return target_y / orig_y
def resize_keep_aspect(orig_x, orig_y, target_x, target_y): def resize_keep_aspect(orig_x, orig_y, target_x, target_y):
"""
Calculates the dimensions of the rectangle obtained from
the rectangle orig_x * orig_y by scaling to fit
target_x * target_y keeping the aspect ratio.
"""
orig_aspect = orig_x / orig_y orig_aspect = orig_x / orig_y
target_aspect = target_x / target_y target_aspect = target_x / target_y
if orig_aspect > target_aspect: if orig_aspect > target_aspect:
@ -94,6 +103,14 @@ def order_coordinates(point1, point2):
y2 = max(point1[1], point2[1]) y2 = max(point1[1], point2[1])
return (x1, y1, x2, y2) return (x1, y1, x2, y2)
def minimum_region(point1, point2):
"""
Returns whether the rectangle defined by the corner points point1
and point2 exceeds the minimum dimensions.
"""
return (abs(point1[0] - point2[0]) >= MIN_SELECTION_SIZE and
abs(point1[1] - point2[1]) >= MIN_SELECTION_SIZE)
class Region(object): class Region(object):
""" """
@ -102,35 +119,65 @@ class Region(object):
""" """
def __init__(self, x1, y1, x2, y2): def __init__(self, x1, y1, x2, y2):
self.set_coords(x1, y1, x2, y2) """
Creates a new region with the specified coordinates.
"""
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.person = None self.person = None
self.mediaref = None self.mediaref = None
def coords(self): def coords(self):
"""
Returns the coordinates of the region as a 4-tuple in the
format (x1, y1, x2, y2).
"""
return (self.x1, self.y1, self.x2, self.y2) return (self.x1, self.y1, self.x2, self.y2)
def set_coords(self, x1, y1, x2, y2): def set_coords(self, x1, y1, x2, y2):
"""
Sets the coordinates of this region.
"""
self.x1 = x1 self.x1 = x1
self.y1 = y1 self.y1 = y1
self.x2 = x2 self.x2 = x2
self.y2 = y2 self.y2 = y2
def contains(self, x, y): def contains(self, x, y):
"""
Returns whether the point with coordinates (x, y) lies insided
this region.
"""
return self.x1 <= x <= self.x2 and self.y1 <= y <= self.y2 return self.x1 <= x <= self.x2 and self.y1 <= y <= self.y2
def contains_rect(self, other): def contains_rect(self, other):
"""
Returns whether this region fully contains the region other.
"""
return (self.contains(other.x1, other.y1) and return (self.contains(other.x1, other.y1) and
self.contains(other.x2, other.y2)) self.contains(other.x2, other.y2))
def area(self): def area(self):
"""
Returns the area of this region.
"""
return abs(self.x1 - self.x2) * abs(self.y1 - self.y2) return abs(self.x1 - self.x2) * abs(self.y1 - self.y2)
def intersects(self, other): def intersects(self, other):
"""
Returns whether the current region intersects other.
"""
# assumes that x1 <= x2 and y1 <= y2 # assumes that x1 <= x2 and y1 <= y2
return not (self.x2 < other.x1 or self.x1 > other.x2 or return not (self.x2 < other.x1 or self.x1 > other.x2 or
self.y2 < other.y1 or self.y1 > other.y2) self.y2 < other.y1 or self.y1 > other.y2)
class SelectionWidget(Gtk.ScrolledWindow): class SelectionWidget(Gtk.ScrolledWindow):
"""
A widget that displays an image and permits GIMP-like selection of regions
within the image. The widget derives from gtk.ScrolledWindow.
"""
__gsignals__ = { __gsignals__ = {
"region-modified": (GObject.SIGNAL_RUN_FIRST, None, ()), "region-modified": (GObject.SIGNAL_RUN_FIRST, None, ()),
@ -143,6 +190,9 @@ class SelectionWidget(Gtk.ScrolledWindow):
} }
def __init__(self): def __init__(self):
"""
Creates a new selection widget.
"""
self.multiple_selection = True self.multiple_selection = True
self.loaded = False self.loaded = False
@ -158,24 +208,27 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.scale = 1.0 self.scale = 1.0
Gtk.ScrolledWindow.__init__(self) Gtk.ScrolledWindow.__init__(self)
self.add(self.build_gui()) self.add(self._build_gui())
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
def build_gui(self): def _build_gui(self):
"""
Builds and lays out the GUI of the widget.
"""
self.image = Gtk.Image() self.image = Gtk.Image()
self.image.set_has_tooltip(True) self.image.set_has_tooltip(True)
self.image.connect_after("draw", self.expose_handler) self.image.connect_after("draw", self._expose_handler)
self.image.connect("query-tooltip", self.show_tooltip) self.image.connect("query-tooltip", self._show_tooltip)
self.event_box = Gtk.EventBox() self.event_box = Gtk.EventBox()
self.event_box.connect('button-press-event', self.event_box.connect('button-press-event',
self.button_press_event) self._button_press_event)
self.event_box.connect('button-release-event', self.event_box.connect('button-release-event',
self.button_release_event) self._button_release_event)
self.event_box.connect('motion-notify-event', self.event_box.connect('motion-notify-event',
self.motion_notify_event) self._motion_notify_event)
self.event_box.connect('scroll-event', self.event_box.connect('scroll-event',
self.motion_scroll_event) self._motion_scroll_event)
self.event_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) 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.BUTTON_RELEASE_MASK)
self.event_box.add_events(Gdk.EventMask.POINTER_MOTION_MASK) self.event_box.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
@ -187,7 +240,7 @@ class SelectionWidget(Gtk.ScrolledWindow):
return self.viewport return self.viewport
# ====================================================== # ======================================================
# field accessors # public field accessors
# ====================================================== # ======================================================
def get_multiple_selection(self): def get_multiple_selection(self):
@ -202,16 +255,34 @@ class SelectionWidget(Gtk.ScrolledWindow):
""" """
self.multiple_selection = enable self.multiple_selection = enable
def is_image_loaded(self):
"""
Returns whether an image has been loaded into this selection widget.
"""
return self.loaded
def set_regions(self, regions): def set_regions(self, regions):
"""
Sets the list of regions to be displayed in the widget.
"""
self.regions = regions self.regions = regions
def get_current(self): def get_current(self):
"""
Returns the currently active region.
"""
return self.current return self.current
def set_current(self, region): def set_current(self, region):
"""
Activates the given region in the widget.
"""
self.current = region self.current = region
def get_selection(self): def get_selection(self):
"""
Returns the coordinates of the current selection.
"""
return self.selection return self.selection
# ====================================================== # ======================================================
@ -219,6 +290,9 @@ class SelectionWidget(Gtk.ScrolledWindow):
# ====================================================== # ======================================================
def load_image(self, image_path): def load_image(self, image_path):
"""
Loads an image from a given path into this selection widget.
"""
self.start_point_screen = None self.start_point_screen = None
self.selection = None self.selection = None
self.in_region = None self.in_region = None
@ -235,73 +309,160 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.pixbuf.get_height(), self.pixbuf.get_height(),
viewport_size.width, viewport_size.width,
viewport_size.height) viewport_size.height)
self.rescale() self._rescale()
self.loaded = True self.loaded = True
except (GObject.GError, OSError): except (GObject.GError, OSError):
self.show_missing() self.show_missing()
def show_missing(self): def show_missing(self):
"""
Displays a 'missing image' icon in the widget.
"""
self.pixbuf = None self.pixbuf = None
self.image.set_from_stock(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG) self.image.set_from_stock(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
self.image.queue_draw() self.image.queue_draw()
# ======================================================
# coordinate transformations (public methods)
# ======================================================
def proportional_to_real_rect(self, rect):
"""
Translates proportional (ranging from 0 to 100) coordinates to image
coordinates (in pixels).
"""
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):
"""
Translates image coordinates (in pixels) to proportional (ranging
from 0 to 100).
"""
x1, y1, x2, y2 = rect
return (self._real_to_proportional((x1, y1)) +
self._real_to_proportional((x2, y2)))
# ======================================================
# widget manipulation
# ======================================================
def refresh(self):
"""
Schedules a redraw of the image.
"""
self.image.queue_draw()
def can_zoom_in(self):
"""
Returns whether it is possible to zoom in the image.
"""
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):
"""
Returns whether it is possible to zoom out the image.
"""
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):
"""
Zooms in the image. The zoom factor is defined by RESIZE_RATIO.
"""
if self.can_zoom_in():
self.scale *= RESIZE_RATIO
self._rescale()
self.emit("zoomed-in")
def zoom_out(self):
"""
Zooms out the image. The zoom factor is defined by RESIZE_RATIO.
"""
if self.can_zoom_out():
self.scale /= RESIZE_RATIO
self._rescale()
self.emit("zoomed-out")
def select(self, region):
"""
Highlights the given region in the image.
"""
self.current = region
if self.current is not None:
self.selection = self.current.coords()
self.image.queue_draw()
def clear_selection(self):
"""
Clears the selection.
"""
self.current = None
self.selection = None
self.image.queue_draw()
# ======================================================
# thumbnails
# ======================================================
def get_thumbnail(self, region, thumbnail_size):
"""
Returns the thumbnail of the given region.
"""
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
# ====================================================== # ======================================================
# utility functions for retrieving properties # utility functions for retrieving properties
# ====================================================== # ======================================================
def is_image_loaded(self): def _get_original_image_size(self):
return self.loaded """
Returns the size of the image before scaling.
def get_original_image_size(self): """
return self.original_image_size return self.original_image_size
def get_scaled_image_size(self): def _get_scaled_image_size(self):
unscaled_size = self.get_original_image_size() """
Returns the size of images scaled with the current scaled.
"""
unscaled_size = self._get_original_image_size()
return (unscaled_size[0] * self.scale, unscaled_size[1] * self.scale) 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 # coordinate transformations
# ====================================================== # ======================================================
def proportional_to_real(self, coord): def _proportional_to_real(self, coord):
""" """
Translate proportional (ranging from 0 to 100) coordinates to image Translates proportional (ranging from 0 to 100) coordinates to image
coordinates (in pixels). coordinates (in pixels).
""" """
w, h = self.original_image_size w, h = self.original_image_size
return (int(round(coord[0] * w / 100)), int(round(coord[1] * h / 100))) return (int(round(coord[0] * w / 100)), int(round(coord[1] * h / 100)))
def real_to_proportional(self, coord): def _real_to_proportional(self, coord):
""" """
Translate image coordinates (in pixels) to proportional (ranging Translates image coordinates (in pixels) to proportional (ranging
from 0 to 100). from 0 to 100).
""" """
w, h = self.original_image_size w, h = self.original_image_size
return (int(round(coord[0] * 100 / w)), int(round(coord[1] * 100 / h))) return (int(round(coord[0] * 100 / w)), int(round(coord[1] * 100 / h)))
def proportional_to_real_rect(self, rect): def _image_to_screen(self, coords):
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 Translates image coordinates to viewport coordinates using the current
scale and viewport size. scale and viewport size.
""" """
viewport_rect = self.viewport.get_allocation() viewport_rect = self.viewport.get_allocation()
@ -317,9 +478,9 @@ class SelectionWidget(Gtk.ScrolledWindow):
return (int(coords[0] * self.scale - offset_x), return (int(coords[0] * self.scale - offset_x),
int(coords[1] * self.scale - offset_y)) int(coords[1] * self.scale - offset_y))
def screen_to_image(self, coords): def _screen_to_image(self, coords):
""" """
Translate viewport coordinates to original (unscaled) image coordinates Translates viewport coordinates to original (unscaled) image coordinates
using the current scale and viewport size. using the current scale and viewport size.
""" """
viewport_rect = self.viewport.get_allocation() viewport_rect = self.viewport.get_allocation()
@ -335,59 +496,85 @@ class SelectionWidget(Gtk.ScrolledWindow):
return (int((coords[0] + offset_x) / self.scale), return (int((coords[0] + offset_x) / self.scale),
int((coords[1] + offset_y) / self.scale)) int((coords[1] + offset_y) / self.scale))
def truncate_to_image_size(self, coords): def _truncate_to_image_size(self, coords):
"""
Modifies the coordinates of the given point to ensure that it lies
within the image. Negative values are replaced with 0, positive values
exceeding the image dimensions - with those corresponding dimensions.
"""
x, y = coords x, y = coords
(image_width, image_height) = self.get_original_image_size() (image_width, image_height) = self._get_original_image_size()
x = max(x, 0) x = max(x, 0)
x = min(x, image_width) x = min(x, image_width)
y = max(y, 0) y = max(y, 0)
y = min(y, image_height) y = min(y, image_height)
return self.proportional_to_real(self.real_to_proportional((x, y))) return self._proportional_to_real(self._real_to_proportional((x, y)))
def screen_to_truncated(self, coords): def _screen_to_truncated(self, coords):
return self.truncate_to_image_size(self.screen_to_image(coords)) """
Transforms the screen coordinates to image coordinates and truncate to
the image size.
"""
return self._truncate_to_image_size(self._screen_to_image(coords))
def rect_image_to_screen(self, rect): def _rect_image_to_screen(self, rect):
"""
Translates the coordinates of the rectangle from image to screen.
"""
x1, y1, x2, y2 = rect x1, y1, x2, y2 = rect
x1, y1 = self.image_to_screen((x1, y1)) x1, y1 = self._image_to_screen((x1, y1))
x2, y2 = self.image_to_screen((x2, y2)) x2, y2 = self._image_to_screen((x2, y2))
return (x1, y1, x2, y2) return (x1, y1, x2, y2)
# ====================================================== # ======================================================
# drawing, refreshing and zooming the image # drawing and scaling the image
# ====================================================== # ======================================================
def draw_selection(self): def _expose_handler(self, widget, event):
"""
Handles the expose-event signal of the underlying widget.
"""
if self.pixbuf:
self._draw_selection()
def _draw_selection(self):
"""
Draws the image, the selection boxes and does the necessary
shading.
"""
if not self.scaled_size: if not self.scaled_size:
return return
w, h = self.scaled_size w, h = self.scaled_size
offset_x, offset_y = self.image_to_screen((0, 0)) offset_x, offset_y = self._image_to_screen((0, 0))
offset_x -= 1 offset_x -= 1
offset_y -= 1 offset_y -= 1
cr = self.image.get_window().cairo_create() cr = self.image.get_window().cairo_create()
if self.selection: if self.selection:
x1, y1, x2, y2 = self.rect_image_to_screen(self.selection) x1, y1, x2, y2 = self._rect_image_to_screen(self.selection)
# transparent shading # transparent shading
self.draw_transparent_shading(cr, x1, y1, x2, y2, w, h, self._draw_transparent_shading(cr, x1, y1, x2, y2, w, h,
offset_x, offset_y) offset_x, offset_y)
# selection frame # selection frame
self.draw_selection_frame(cr, x1, y1, x2, y2) self._draw_selection_frame(cr, x1, y1, x2, y2)
# draw grabber # draw grabber
self.draw_grabber(cr) self._draw_grabber(cr)
else: else:
# selection frame # selection frame
for region in self.regions: for region in self.regions:
x1, y1, x2, y2 = self.rect_image_to_screen(region.coords()) x1, y1, x2, y2 = self._rect_image_to_screen(region.coords())
self.draw_region_frame(cr, x1, y1, x2, y2) self._draw_region_frame(cr, x1, y1, x2, y2)
def draw_transparent_shading(self, cr, x1, y1, x2, y2, w, h, def _draw_transparent_shading(self, cr, x1, y1, x2, y2, w, h,
offset_x, offset_y): offset_x, offset_y):
"""
Draws the shading for a selection box.
"""
cr.set_source_rgba(1.0, 1.0, 1.0, SHADING_OPACITY) 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, offset_y, x1 - offset_x, y1 - offset_y)
cr.rectangle(offset_x, y1, x1 - offset_x, y2 - y1) cr.rectangle(offset_x, y1, x1 - offset_x, y2 - y1)
@ -399,10 +586,16 @@ class SelectionWidget(Gtk.ScrolledWindow):
cr.rectangle(x1, offset_y, x2 - x1 + 1, y1 - offset_y) cr.rectangle(x1, offset_y, x2 - x1 + 1, y1 - offset_y)
cr.fill() cr.fill()
def draw_selection_frame(self, cr, x1, y1, x2, y2): def _draw_selection_frame(self, cr, x1, y1, x2, y2):
self.draw_region_frame(cr, x1, y1, x2, y2) """
Draws the frame during selection.
"""
self._draw_region_frame(cr, x1, y1, x2, y2)
def draw_region_frame(self, cr, x1, y1, x2, y2): def _draw_region_frame(self, cr, x1, y1, x2, y2):
"""
Draws a region frame.
"""
cr.set_source_rgb(1.0, 1.0, 1.0) # white cr.set_source_rgb(1.0, 1.0, 1.0) # white
cr.rectangle(x1, y1, x2 - x1, y2 - y1) cr.rectangle(x1, y1, x2 - x1, y2 - y1)
cr.stroke() cr.stroke()
@ -410,9 +603,12 @@ class SelectionWidget(Gtk.ScrolledWindow):
cr.rectangle(x1 - 2, y1 - 2, x2 - x1 + 4, y2 - y1 + 4) cr.rectangle(x1 - 2, y1 - 2, x2 - x1 + 4, y2 - y1 + 4)
cr.stroke() cr.stroke()
def draw_grabber(self, cr): def _draw_grabber(self, cr):
"""
Draws a grabber.
"""
if self.selection is not None and self.grabber is not None: if self.selection is not None and self.grabber is not None:
selection_rect = self.rect_image_to_screen(self.selection) selection_rect = self._rect_image_to_screen(self.selection)
cr.set_source_rgb(1.0, 0, 0) cr.set_source_rgb(1.0, 0, 0)
if self.grabber_position is None: if self.grabber_position is None:
generators = grabber_generators(selection_rect) generators = grabber_generators(selection_rect)
@ -429,10 +625,11 @@ class SelectionWidget(Gtk.ScrolledWindow):
cr.rectangle(x1, y1, x2 - x1, y2 - y1) cr.rectangle(x1, y1, x2 - x1, y2 - y1)
cr.stroke() cr.stroke()
def refresh(self): def _rescale(self):
self.image.queue_draw() """
Recalculates the sizes using the current scale and updates
def rescale(self): the buffers.
"""
self.scaled_size = (int(self.original_image_size[0] * self.scale), self.scaled_size = (int(self.original_image_size[0] * self.scale),
int(self.original_image_size[1] * self.scale)) int(self.original_image_size[1] * self.scale))
self.scaled_image = self.pixbuf.scale_simple(self.scaled_size[0], self.scaled_image = self.pixbuf.scale_simple(self.scaled_size[0],
@ -442,48 +639,14 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.image.set_size_request(*self.scaled_size) self.image.set_size_request(*self.scaled_size)
self.event_box.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 # managing regions
# ====================================================== # ======================================================
def find_region(self, x, y): def _find_region(self, x, y):
"""
Finds the smallest region containing point (x, y).
"""
result = None result = None
for region in self.regions: for region in self.regions:
if region.contains(x, y): if region.contains(x, y):
@ -491,26 +654,14 @@ class SelectionWidget(Gtk.ScrolledWindow):
result = region result = region
return result 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 # mouse event handlers
# ====================================================== # ======================================================
def button_press_event(self, obj, event): def _button_press_event(self, obj, event):
"""
Handles the button-press-event signal.
"""
if not self.is_image_loaded(): if not self.is_image_loaded():
return return
if event.button == 1: # left button if event.button == 1: # left button
@ -522,8 +673,8 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.emit("selection-cleared") self.emit("selection-cleared")
elif event.button == 3: # right button elif event.button == 3: # right button
# select a region, if clicked inside one # select a region, if clicked inside one
click_point = self.screen_to_image((event.x, event.y)) click_point = self._screen_to_image((event.x, event.y))
self.current = self.find_region(*click_point) self.current = self._find_region(*click_point)
self.selection = \ self.selection = \
self.current.coords() if self.current is not None else None self.current.coords() if self.current is not None else None
self.start_point_screen = None self.start_point_screen = None
@ -535,7 +686,10 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.emit("selection-cleared") self.emit("selection-cleared")
return True # don't propagate the event further return True # don't propagate the event further
def button_release_event(self, obj, event): def _button_release_event(self, obj, event):
"""
Handles the button-release-event signal.
"""
if not self.is_image_loaded(): if not self.is_image_loaded():
return return
if event.button == 1: if event.button == 1:
@ -551,14 +705,14 @@ class SelectionWidget(Gtk.ScrolledWindow):
# clicked on one of the grabbers # clicked on one of the grabbers
dx, dy = (event.x - self.start_point_screen[0], dx, dy = (event.x - self.start_point_screen[0],
event.y - self.start_point_screen[1]) event.y - self.start_point_screen[1])
self.grabber_to_draw = self.modify_selection(dx, dy) self.grabber_to_draw = self._modify_selection(dx, dy)
self.current.set_coords(*self.selection) self.current.set_coords(*self.selection)
self.emit("region-modified") self.emit("region-modified")
else: else:
# nothing is currently selected # nothing is currently selected
if (self.minimum_region(self.start_point_screen, if (minimum_region(self.start_point_screen,
(event.x, event.y)) and (event.x, event.y)) and
self.can_select()): self._can_select()):
# region selection # region selection
region = Region(*self.selection) region = Region(*self.selection)
self.regions.append(region) self.regions.append(region)
@ -567,8 +721,8 @@ class SelectionWidget(Gtk.ScrolledWindow):
else: else:
# nothing selected, just a click # nothing selected, just a click
click_point = \ click_point = \
self.screen_to_image(self.start_point_screen) self._screen_to_image(self.start_point_screen)
self.current = self.find_region(*click_point) self.current = self._find_region(*click_point)
self.selection = \ self.selection = \
self.current.coords() if self.current is not None \ self.current.coords() if self.current is not None \
else None else None
@ -577,28 +731,31 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.start_point_screen = None self.start_point_screen = None
self.refresh() self.refresh()
def motion_notify_event(self, widget, event): def _motion_notify_event(self, widget, event):
"""
Handles the motion-notify-event signal.
"""
if not self.is_image_loaded(): if not self.is_image_loaded():
return return
end_point_orig = self.screen_to_image((event.x, event.y)) end_point_orig = self._screen_to_image((event.x, event.y))
end_point = self.truncate_to_image_size(end_point_orig) end_point = self._truncate_to_image_size(end_point_orig)
if self.start_point_screen: if self.start_point_screen:
# selection or dragging (mouse button pressed) # selection or dragging (mouse button pressed)
if self.grabber is not None and self.grabber != INSIDE: if self.grabber is not None and self.grabber != INSIDE:
# dragging the grabber # dragging the grabber
dx, dy = (event.x - self.start_point_screen[0], dx, dy = (event.x - self.start_point_screen[0],
event.y - self.start_point_screen[1]) event.y - self.start_point_screen[1])
self.grabber_to_draw = self.modify_selection(dx, dy) self.grabber_to_draw = self._modify_selection(dx, dy)
elif self.can_select(): elif self._can_select():
# making new selection # making new selection
start_point = self.screen_to_truncated(self.start_point_screen) start_point = self._screen_to_truncated(self.start_point_screen)
self.selection = order_coordinates(start_point, end_point) self.selection = order_coordinates(start_point, end_point)
else: else:
# motion (mouse button is not pressed) # motion (mouse button is not pressed)
self.in_region = self.find_region(*end_point_orig) self.in_region = self._find_region(*end_point_orig)
if self.current is not None: if self.current is not None:
# a box is active, so check if the pointer is inside a grabber # a box is active, so check if the pointer is inside a grabber
rect = self.rect_image_to_screen(self.current.coords()) rect = self._rect_image_to_screen(self.current.coords())
self.grabber = can_grab(rect, event.x, event.y) self.grabber = can_grab(rect, event.x, event.y)
if self.grabber is not None: if self.grabber is not None:
self.grabber_to_draw = self.grabber self.grabber_to_draw = self.grabber
@ -616,7 +773,10 @@ class SelectionWidget(Gtk.ScrolledWindow):
self.event_box.get_window().set_cursor(None) self.event_box.get_window().set_cursor(None)
self.image.queue_draw() self.image.queue_draw()
def motion_scroll_event(self, widget, event): def _motion_scroll_event(self, widget, event):
"""
Handles the motion-scroll-event signal.
"""
if not self.is_image_loaded(): if not self.is_image_loaded():
return return
if event.direction == Gdk.ScrollDirection.UP: if event.direction == Gdk.ScrollDirection.UP:
@ -628,18 +788,24 @@ class SelectionWidget(Gtk.ScrolledWindow):
# helpers for mouse event handlers # helpers for mouse event handlers
# ====================================================== # ======================================================
def minimum_region(self, point1, point2): def _can_select(self):
return (abs(point1[0] - point2[0]) >= MIN_SELECTION_SIZE and """
abs(point1[1] - point2[1]) >= MIN_SELECTION_SIZE) Returns whether selection is currently possible, which is when
multiple selection is enabled or otherwise when no region is
def can_select(self): currently selected.
"""
return self.multiple_selection or len(self.regions) < 1 return self.multiple_selection or len(self.regions) < 1
def modify_selection(self, dx, dy): def _modify_selection(self, dx, dy):
x1, y1, x2, y2 = self.rect_image_to_screen(self.current.coords()) """
Changes the selection when a grabber is dragged, returns the new
grabber if a grabber switch has happened, and the current grabber
otherwise.
"""
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, x2, y2 = MOTION_FUNCTIONS[self.grabber](x1, y1, x2, y2, dx, dy)
(x1, y1) = self.screen_to_truncated((x1, y1)) (x1, y1) = self._screen_to_truncated((x1, y1))
(x2, y2) = self.screen_to_truncated((x2, y2)) (x2, y2) = self._screen_to_truncated((x2, y2))
grabber = switch_grabber(self.grabber, x1, y1, x2, y2) grabber = switch_grabber(self.grabber, x1, y1, x2, y2)
self.selection = order_coordinates((x1, y1), (x2, y2)) self.selection = order_coordinates((x1, y1), (x2, y2))
return grabber return grabber
@ -648,7 +814,10 @@ class SelectionWidget(Gtk.ScrolledWindow):
# tooltips # tooltips
# ====================================================== # ======================================================
def show_tooltip(self, widget, x, y, keyboard_mode, tooltip): def _show_tooltip(self, widget, x, y, keyboard_mode, tooltip):
"""
Handles the query-tooltip signal.
"""
if self.in_region: if self.in_region:
person = self.in_region.person person = self.in_region.person
if person: if person: