summaryrefslogtreecommitdiff
path: root/src/faces/FaceShape.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/faces/FaceShape.vala')
-rw-r--r--src/faces/FaceShape.vala783
1 files changed, 783 insertions, 0 deletions
diff --git a/src/faces/FaceShape.vala b/src/faces/FaceShape.vala
new file mode 100644
index 0000000..c14b43b
--- /dev/null
+++ b/src/faces/FaceShape.vala
@@ -0,0 +1,783 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+#if ENABLE_FACES
+
+public abstract class FaceShape : Object {
+ public const string SHAPE_TYPE = null;
+
+ protected const int FACE_WINDOW_MARGIN = 5;
+ protected const int LABEL_MARGIN = 12;
+ protected const int LABEL_PADDING = 9;
+
+ public signal void add_me_requested(FaceShape face_shape);
+ public signal void delete_me_requested();
+
+ protected FacesTool.EditingFaceToolWindow face_window;
+ protected Gdk.CursorType current_cursor_type = Gdk.CursorType.BOTTOM_RIGHT_CORNER;
+ protected EditingTools.PhotoCanvas canvas;
+ protected string serialized = null;
+
+ private bool editable = true;
+ private bool visible = true;
+ private bool known = true;
+
+ private weak FacesTool.FaceWidget face_widget = null;
+
+ public FaceShape(EditingTools.PhotoCanvas canvas) {
+ this.canvas = canvas;
+ this.canvas.new_surface.connect(prepare_ctx);
+
+ prepare_ctx(this.canvas.get_default_ctx(), this.canvas.get_surface_dim());
+
+ face_window = new FacesTool.EditingFaceToolWindow(this.canvas.get_container());
+ face_window.key_pressed.connect(key_press_event);
+
+ face_window.show_all();
+ face_window.hide();
+
+ this.canvas.get_drawing_window().set_cursor(new Gdk.Cursor(current_cursor_type));
+ }
+
+ ~FaceShape() {
+ if (visible)
+ erase();
+
+ face_window.destroy();
+
+ canvas.new_surface.disconnect(prepare_ctx);
+
+ // make sure the cursor isn't set to a modify indicator
+ canvas.get_drawing_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR));
+ }
+
+ public static FaceShape from_serialized(EditingTools.PhotoCanvas canvas, string serialized)
+ throws FaceShapeError {
+ FaceShape face_shape;
+
+ string[] args = serialized.split(";");
+ switch (args[0]) {
+ case "Rectangle":
+ face_shape = FaceRectangle.from_serialized(canvas, args);
+
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ face_shape.serialized = serialized;
+
+ return face_shape;
+ }
+
+ public void set_name(string face_name) {
+ face_window.entry.set_text(face_name);
+ }
+
+ public string? get_name() {
+ string face_name = face_window.entry.get_text();
+
+ return face_name == "" ? null : face_name;
+ }
+
+ public void set_known(bool known) {
+ this.known = known;
+ }
+
+ public bool get_known() {
+ return known;
+ }
+
+ public void set_widget(FacesTool.FaceWidget face_widget) {
+ this.face_widget = face_widget;
+ }
+
+ public FacesTool.FaceWidget get_widget() {
+ assert(face_widget != null);
+
+ return face_widget;
+ }
+
+ public void hide() {
+ visible = false;
+ erase();
+
+ if (editable)
+ face_window.hide();
+
+ // make sure the cursor isn't set to a modify indicator
+ canvas.get_drawing_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR));
+ }
+
+ public void show() {
+ visible = true;
+ paint();
+
+ if (editable) {
+ update_face_window_position();
+ face_window.show();
+ face_window.present();
+
+ if (!known)
+ face_window.entry.select_region(0, -1);
+ }
+ }
+
+ public bool is_visible() {
+ return visible;
+ }
+
+ public bool is_editable() {
+ return editable;
+ }
+
+ public void set_editable(bool editable) {
+ if (visible && editable != is_editable()) {
+ hide();
+ this.editable = editable;
+ show();
+
+ return;
+ }
+
+ this.editable = editable;
+ }
+
+ public bool key_press_event(Gdk.EventKey event) {
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "Escape":
+ delete_me_requested();
+ break;
+ case "Return":
+ case "KP_Enter":
+ add_me_requested(this);
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ public abstract string serialize();
+ public abstract void update_face_window_position();
+ public abstract void prepare_ctx(Cairo.Context ctx, Dimensions dim);
+ public abstract void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled);
+ public abstract void on_motion(int x, int y, Gdk.ModifierType mask);
+ public abstract void on_left_released(int x, int y);
+ public abstract bool on_left_click(int x, int y);
+ public abstract bool cursor_is_over(int x, int y);
+ public abstract bool equals(FaceShape face_shape);
+ public abstract double get_distance(int x, int y);
+
+ protected abstract void paint();
+ protected abstract void erase();
+}
+
+public class FaceRectangle : FaceShape {
+ public new const string SHAPE_TYPE = "Rectangle";
+
+ private const int FACE_MIN_SIZE = 8;
+ public const int NULL_SIZE = 0;
+
+ private Box box;
+ private Box? label_box;
+ private BoxLocation in_manipulation = BoxLocation.OUTSIDE;
+ private Cairo.Context wide_black_ctx = null;
+ private Cairo.Context wide_white_ctx = null;
+ private Cairo.Context thin_white_ctx = null;
+ private int last_grab_x = -1;
+ private int last_grab_y = -1;
+
+ public FaceRectangle(EditingTools.PhotoCanvas canvas, int x, int y,
+ int half_width = NULL_SIZE, int half_height = NULL_SIZE) {
+ base(canvas);
+
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+ x -= scaled_pixbuf_pos.x;
+ y -= scaled_pixbuf_pos.y;
+
+ // If half_width is NULL_SIZE we are creating a new FaceShape,
+ // otherwise we are only showing a previously created one.
+ if (half_width == NULL_SIZE) {
+ box = Box(x, y, x, y);
+
+ in_manipulation = BoxLocation.BOTTOM_RIGHT;
+ last_grab_x = x;
+ last_grab_y = y;
+ } else {
+ Dimensions pixbuf_dimensions = Dimensions.for_pixbuf(canvas.get_scaled_pixbuf());
+ int right = (x + half_width).clamp(x, pixbuf_dimensions.width);
+ int bottom = (y + half_height).clamp(y, pixbuf_dimensions.height);
+
+ box = Box(x - half_width, y - half_height, right, bottom);
+ }
+ }
+
+ ~FaceRectangle() {
+ if (!is_editable())
+ erase_label();
+ }
+
+ public static new FaceRectangle from_serialized(EditingTools.PhotoCanvas canvas, string[] args)
+ throws FaceShapeError {
+ assert(args[0] == SHAPE_TYPE);
+
+ Photo photo = canvas.get_photo();
+ Dimensions raw_dim = photo.get_raw_dimensions();
+
+ int x = (int) (raw_dim.width * double.parse(args[1]));
+ int y = (int) (raw_dim.height * double.parse(args[2]));
+ int half_width = (int) (raw_dim.width * double.parse(args[3]));
+ int half_height = (int) (raw_dim.height * double.parse(args[4]));
+
+ Box box = Box(x - half_width, y - half_height, x + half_width, y + half_height);
+
+ Dimensions current_dim = Dimensions.for_pixbuf(canvas.get_scaled_pixbuf());
+ Box raw_cropped;
+
+ if (photo.get_raw_crop(out raw_cropped)) {
+ box.left = box.left.clamp(raw_cropped.left, box.left) - raw_cropped.left;
+ box.right = box.right.clamp(box.right, raw_cropped.right) - raw_cropped.left;
+ box.top = box.top.clamp(raw_cropped.top, box.top) - raw_cropped.top;
+ box.bottom = box.bottom.clamp(box.bottom, raw_cropped.bottom) - raw_cropped.top;
+
+ box = photo.get_orientation().rotate_box(raw_cropped.get_dimensions(), box);
+
+ Box cropped;
+ photo.get_crop(out cropped);
+ box = box.get_scaled_similar(cropped.get_dimensions(), current_dim);
+ } else {
+ box = photo.get_orientation().rotate_box(raw_dim, box);
+
+ box = box.get_scaled_similar(photo.get_dimensions(), current_dim);
+ }
+
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+ box.left += scaled_pixbuf_pos.x;
+ box.right += scaled_pixbuf_pos.x;
+ box.top += scaled_pixbuf_pos.y;
+ box.bottom += scaled_pixbuf_pos.y;
+
+ half_width = box.get_width() / 2;
+ half_height = box.get_height() / 2;
+
+ if (half_width < FACE_MIN_SIZE || half_height < FACE_MIN_SIZE)
+ throw new FaceShapeError.CANT_CREATE("FaceShape is out of cropped photo area");
+
+ return new FaceRectangle(canvas, box.left + half_width, box.top + half_height,
+ half_width, half_height);
+ }
+
+ public override void update_face_window_position() {
+ AppWindow appWindow = AppWindow.get_instance();
+ Gtk.Allocation face_window_alloc;
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+ int x = 0;
+ int y = 0;
+
+ if (canvas.get_container() == appWindow) {
+ appWindow.get_current_page().get_window().get_origin(out x, out y);
+ } else assert(canvas.get_container() is FullscreenWindow);
+
+ face_window.get_allocation(out face_window_alloc);
+
+ x += scaled_pixbuf_pos.x + box.left + ((box.get_width() - face_window_alloc.width) >> 1);
+ y += scaled_pixbuf_pos.y + box.bottom + FACE_WINDOW_MARGIN;
+
+ face_window.move(x, y);
+ }
+
+ protected override void paint() {
+ canvas.draw_box(wide_black_ctx, box);
+ canvas.draw_box(wide_white_ctx, box.get_reduced(1));
+ canvas.draw_box(wide_white_ctx, box.get_reduced(2));
+
+ canvas.invalidate_area(box);
+
+ if (!is_editable())
+ paint_label();
+ }
+
+ protected override void erase() {
+ canvas.erase_box(box);
+ canvas.erase_box(box.get_reduced(1));
+ canvas.erase_box(box.get_reduced(2));
+
+ canvas.invalidate_area(box);
+
+ if (!is_editable())
+ erase_label();
+ }
+
+ private void paint_label() {
+ Cairo.Context ctx = canvas.get_default_ctx();
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+
+ ctx.save();
+
+ Cairo.TextExtents text_extents = Cairo.TextExtents();
+ ctx.text_extents(get_name(), out text_extents);
+
+ int width = (int) text_extents.width + LABEL_PADDING;
+ int height = (int) text_extents.height;
+ int x = box.left + (box.get_width() - width) / 2;
+ int y = box.bottom + LABEL_MARGIN;
+
+ label_box = Box(x, y, x + width, y + height + LABEL_PADDING);
+
+ x += scaled_pixbuf_pos.x;
+ y += scaled_pixbuf_pos.y;
+
+ ctx.rectangle(x, y, width, height + LABEL_PADDING);
+ ctx.set_source_rgba(0, 0, 0, 0.6);
+ ctx.fill();
+
+ ctx.set_source_rgb(1, 1, 1);
+ ctx.move_to(x + LABEL_PADDING / 2, y + height + LABEL_PADDING / 2);
+ ctx.show_text(get_name());
+
+ ctx.restore();
+ }
+
+ private void erase_label() {
+ if (label_box == null)
+ return;
+
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+ int x = scaled_pixbuf_pos.x + label_box.left;
+ int y = scaled_pixbuf_pos.y + label_box.top;
+
+ Cairo.Context ctx = canvas.get_default_ctx();
+ ctx.save();
+
+ ctx.set_operator(Cairo.Operator.OVER);
+ ctx.rectangle(x, y, label_box.get_width(), label_box.get_height());
+
+ ctx.set_source_rgb(0.0, 0.0, 0.0);
+ ctx.fill_preserve();
+
+ ctx.set_source_surface(canvas.get_scaled_surface(),
+ scaled_pixbuf_pos.x, scaled_pixbuf_pos.y);
+ ctx.fill();
+
+ canvas.invalidate_area(label_box);
+ label_box = null;
+
+ ctx.restore();
+ }
+
+ public override string serialize() {
+ if (serialized != null)
+ return serialized;
+
+ double x;
+ double y;
+ double half_width;
+ double half_height;
+
+ get_geometry(out x, out y, out half_width, out half_height);
+
+ serialized = "%s;%s;%s;%s;%s".printf(SHAPE_TYPE, x.to_string(),
+ y.to_string(), half_width.to_string(), half_height.to_string());
+
+ return serialized;
+ }
+
+ public void get_geometry(out double x, out double y,
+ out double half_width, out double half_height) {
+ Photo photo = canvas.get_photo();
+ Dimensions raw_dim = photo.get_raw_dimensions();
+
+ Box temp_box = box;
+
+ Dimensions current_dim = Dimensions.for_pixbuf(canvas.get_scaled_pixbuf());
+ Box cropped;
+
+ if (photo.get_crop(out cropped)) {
+ temp_box = temp_box.get_scaled_similar(current_dim, cropped.get_dimensions());
+
+ Box raw_cropped;
+ photo.get_raw_crop(out raw_cropped);
+
+ temp_box =
+ photo.get_orientation().derotate_box(raw_cropped.get_dimensions(), temp_box);
+
+ temp_box.left += raw_cropped.left;
+ temp_box.right += raw_cropped.left;
+ temp_box.top += raw_cropped.top;
+ temp_box.bottom += raw_cropped.top;
+ } else {
+ temp_box = temp_box.get_scaled_similar(current_dim, photo.get_dimensions());
+
+ temp_box = photo.get_orientation().derotate_box(raw_dim, temp_box);
+ }
+
+ x = (temp_box.left + (temp_box.get_width() / 2)) / (double) raw_dim.width;
+ y = (temp_box.top + (temp_box.get_height() / 2)) / (double) raw_dim.height;
+
+ double width_left_end = temp_box.left / (double) raw_dim.width;
+ double width_right_end = temp_box.right / (double) raw_dim.width;
+ double height_top_end = temp_box.top / (double) raw_dim.height;
+ double height_bottom_end = temp_box.bottom / (double) raw_dim.height;
+
+ half_width = (width_right_end - width_left_end) / 2;
+ half_height = (height_bottom_end - height_top_end) / 2;
+ }
+
+ public override bool equals(FaceShape face_shape) {
+ return serialize() == face_shape.serialize();
+ }
+
+ public override void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
+ wide_black_ctx = new Cairo.Context(ctx.get_target());
+ set_source_color_from_string(wide_black_ctx, "#000");
+ wide_black_ctx.set_line_width(1);
+
+ wide_white_ctx = new Cairo.Context(ctx.get_target());
+ set_source_color_from_string(wide_black_ctx, "#FFF");
+ wide_white_ctx.set_line_width(1);
+
+ thin_white_ctx = new Cairo.Context(ctx.get_target());
+ set_source_color_from_string(wide_black_ctx, "#FFF");
+ thin_white_ctx.set_line_width(0.5);
+ }
+
+ private bool on_canvas_manipulation(int x, int y) {
+ Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
+
+ // box is maintained in coordinates non-relative to photo's position on canvas ...
+ // but bound tool to photo itself
+ x -= scaled_pos.x;
+ if (x < 0)
+ x = 0;
+ else if (x >= scaled_pos.width)
+ x = scaled_pos.width - 1;
+
+ y -= scaled_pos.y;
+ if (y < 0)
+ y = 0;
+ else if (y >= scaled_pos.height)
+ y = scaled_pos.height - 1;
+
+ // need to make manipulations outside of box structure, because its methods do sanity
+ // checking
+ int left = box.left;
+ int top = box.top;
+ int right = box.right;
+ int bottom = box.bottom;
+
+ // get extra geometric information needed to enforce constraints
+ int photo_right_edge = canvas.get_scaled_pixbuf().width - 1;
+ int photo_bottom_edge = canvas.get_scaled_pixbuf().height - 1;
+
+ switch (in_manipulation) {
+ case BoxLocation.LEFT_SIDE:
+ left = x;
+ break;
+
+ case BoxLocation.TOP_SIDE:
+ top = y;
+ break;
+
+ case BoxLocation.RIGHT_SIDE:
+ right = x;
+ break;
+
+ case BoxLocation.BOTTOM_SIDE:
+ bottom = y;
+ break;
+
+ case BoxLocation.TOP_LEFT:
+ top = y;
+ left = x;
+ break;
+
+ case BoxLocation.BOTTOM_LEFT:
+ bottom = y;
+ left = x;
+ break;
+
+ case BoxLocation.TOP_RIGHT:
+ top = y;
+ right = x;
+ break;
+
+ case BoxLocation.BOTTOM_RIGHT:
+ bottom = y;
+ right = x;
+ break;
+
+ case BoxLocation.INSIDE:
+ assert(last_grab_x >= 0);
+ assert(last_grab_y >= 0);
+
+ int delta_x = (x - last_grab_x);
+ int delta_y = (y - last_grab_y);
+
+ last_grab_x = x;
+ last_grab_y = y;
+
+ int width = right - left + 1;
+ int height = bottom - top + 1;
+
+ left += delta_x;
+ top += delta_y;
+ right += delta_x;
+ bottom += delta_y;
+
+ // bound box inside of photo
+ if (left < 0)
+ left = 0;
+
+ if (top < 0)
+ top = 0;
+
+ if (right >= scaled_pos.width)
+ right = scaled_pos.width - 1;
+
+ if (bottom >= scaled_pos.height)
+ bottom = scaled_pos.height - 1;
+
+ int adj_width = right - left + 1;
+ int adj_height = bottom - top + 1;
+
+ // don't let adjustments affect the size of the box
+ if (adj_width != width) {
+ if (delta_x < 0)
+ right = left + width - 1;
+ else left = right - width + 1;
+ }
+
+ if (adj_height != height) {
+ if (delta_y < 0)
+ bottom = top + height - 1;
+ else top = bottom - height + 1;
+ }
+ break;
+
+ default:
+ // do nothing, not even a repaint
+ return false;
+ }
+
+ // Check if the mouse has gone out of bounds, and if it has, make sure that the
+ // face shape edges stay within the photo bounds.
+ int width = right - left + 1;
+ int height = bottom - top + 1;
+
+ if (left < 0)
+ left = 0;
+ if (top < 0)
+ top = 0;
+ if (right > photo_right_edge)
+ right = photo_right_edge;
+ if (bottom > photo_bottom_edge)
+ bottom = photo_bottom_edge;
+
+ width = right - left + 1;
+ height = bottom - top + 1;
+
+ switch (in_manipulation) {
+ case BoxLocation.LEFT_SIDE:
+ case BoxLocation.TOP_LEFT:
+ case BoxLocation.BOTTOM_LEFT:
+ if (width < FACE_MIN_SIZE)
+ left = right - FACE_MIN_SIZE;
+ break;
+
+ case BoxLocation.RIGHT_SIDE:
+ case BoxLocation.TOP_RIGHT:
+ case BoxLocation.BOTTOM_RIGHT:
+ if (width < FACE_MIN_SIZE)
+ right = left + FACE_MIN_SIZE;
+ break;
+
+ default:
+ break;
+ }
+
+ switch (in_manipulation) {
+ case BoxLocation.TOP_SIDE:
+ case BoxLocation.TOP_LEFT:
+ case BoxLocation.TOP_RIGHT:
+ if (height < FACE_MIN_SIZE)
+ top = bottom - FACE_MIN_SIZE;
+ break;
+
+ case BoxLocation.BOTTOM_SIDE:
+ case BoxLocation.BOTTOM_LEFT:
+ case BoxLocation.BOTTOM_RIGHT:
+ if (height < FACE_MIN_SIZE)
+ bottom = top + FACE_MIN_SIZE;
+ break;
+
+ default:
+ break;
+ }
+
+ Box new_box = Box(left, top, right, bottom);
+
+ if (!box.equals(new_box)) {
+ erase();
+
+ if (in_manipulation != BoxLocation.INSIDE)
+ check_resized_box(new_box);
+
+ box = new_box;
+ paint();
+ }
+
+ if (is_editable())
+ update_face_window_position();
+
+ serialized = null;
+
+ return false;
+ }
+
+ private void check_resized_box(Box new_box) {
+ Box horizontal;
+ bool horizontal_enlarged;
+ Box vertical;
+ bool vertical_enlarged;
+ BoxComplements complements = box.resized_complements(new_box, out horizontal,
+ out horizontal_enlarged, out vertical, out vertical_enlarged);
+
+ // this should never happen ... this means that the operation wasn't a resize
+ assert(complements != BoxComplements.NONE);
+ }
+
+ private void update_cursor(int x, int y) {
+ // box is not maintained relative to photo's position on canvas
+ Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
+ Box offset_scaled_box = box.get_offset(scaled_pos.x, scaled_pos.y);
+
+ Gdk.CursorType cursor_type = Gdk.CursorType.LEFT_PTR;
+ switch (offset_scaled_box.approx_location(x, y)) {
+ case BoxLocation.LEFT_SIDE:
+ cursor_type = Gdk.CursorType.LEFT_SIDE;
+ break;
+
+ case BoxLocation.TOP_SIDE:
+ cursor_type = Gdk.CursorType.TOP_SIDE;
+ break;
+
+ case BoxLocation.RIGHT_SIDE:
+ cursor_type = Gdk.CursorType.RIGHT_SIDE;
+ break;
+
+ case BoxLocation.BOTTOM_SIDE:
+ cursor_type = Gdk.CursorType.BOTTOM_SIDE;
+ break;
+
+ case BoxLocation.TOP_LEFT:
+ cursor_type = Gdk.CursorType.TOP_LEFT_CORNER;
+ break;
+
+ case BoxLocation.BOTTOM_LEFT:
+ cursor_type = Gdk.CursorType.BOTTOM_LEFT_CORNER;
+ break;
+
+ case BoxLocation.TOP_RIGHT:
+ cursor_type = Gdk.CursorType.TOP_RIGHT_CORNER;
+ break;
+
+ case BoxLocation.BOTTOM_RIGHT:
+ cursor_type = Gdk.CursorType.BOTTOM_RIGHT_CORNER;
+ break;
+
+ case BoxLocation.INSIDE:
+ cursor_type = Gdk.CursorType.FLEUR;
+ break;
+
+ default:
+ // use Gdk.CursorType.LEFT_PTR
+ break;
+ }
+
+ if (cursor_type != current_cursor_type) {
+ Gdk.Cursor cursor = new Gdk.Cursor(cursor_type);
+ canvas.get_drawing_window().set_cursor(cursor);
+ current_cursor_type = cursor_type;
+ }
+ }
+
+ public override void on_motion(int x, int y, Gdk.ModifierType mask) {
+ // only deal with manipulating the box when click-and-dragging one of the edges
+ // or the interior
+ if (in_manipulation != BoxLocation.OUTSIDE)
+ on_canvas_manipulation(x, y);
+
+ update_cursor(x, y);
+ }
+
+ public override bool on_left_click(int x, int y) {
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+
+ // box is not maintained relative to photo's position on canvas
+ Box offset_scaled_box = box.get_offset(scaled_pixbuf_pos.x, scaled_pixbuf_pos.y);
+
+ // determine where the mouse down landed and store for future events
+ in_manipulation = offset_scaled_box.approx_location(x, y);
+ last_grab_x = x -= scaled_pixbuf_pos.x;
+ last_grab_y = y -= scaled_pixbuf_pos.y;
+
+ return box.approx_location(x, y) != BoxLocation.OUTSIDE;
+ }
+
+ public override void on_left_released(int x, int y) {
+ if (box.get_width() < FACE_MIN_SIZE) {
+ delete_me_requested();
+
+ return;
+ }
+
+ if (is_editable()) {
+ face_window.show();
+ face_window.present();
+ }
+
+ // nothing to do if released outside of the face box
+ if (in_manipulation == BoxLocation.OUTSIDE)
+ return;
+
+ // end manipulation
+ in_manipulation = BoxLocation.OUTSIDE;
+ last_grab_x = -1;
+ last_grab_y = -1;
+
+ update_cursor(x, y);
+ }
+
+ public override void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled) {
+ Dimensions new_dim = Dimensions.for_pixbuf(scaled);
+ Dimensions uncropped_dim = canvas.get_photo().get_original_dimensions();
+
+ Box new_box = box.get_scaled_similar(old_dim, uncropped_dim);
+
+ // rescale back to new size
+ box = new_box.get_scaled_similar(uncropped_dim, new_dim);
+ update_face_window_position();
+ }
+
+ public override bool cursor_is_over(int x, int y) {
+ // box is not maintained relative to photo's position on canvas
+ Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
+ Box offset_scaled_box = box.get_offset(scaled_pos.x, scaled_pos.y);
+
+ return offset_scaled_box.approx_location(x, y) != BoxLocation.OUTSIDE;
+ }
+
+ public override double get_distance(int x, int y) {
+ double center_x = box.left + box.get_width() / 2.0;
+ double center_y = box.top + box.get_height() / 2.0;
+
+ return Math.sqrt((center_x - x) * (center_x - x) + (center_y - y) * (center_y - y));
+ }
+}
+
+#endif