Update selection widget with latest version
svn: r23426
This commit is contained in:
parent
09388cf0aa
commit
046f89a2a7
@ -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],
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user