/* Copyright 2018 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 errordomain FaceShapeError {
CANT_CREATE
}
public class FacesTool : EditingTools.EditingTool {
protected const int CONTROL_SPACING = 8;
protected const int FACE_LABEL_MAX_CHARS = 15;
private enum EditingPhase {
CLICK_TO_EDIT,
NOT_EDITING,
CREATING_DRAGGING,
CREATING_EDITING,
EDITING,
DETECTING_FACES,
DETECTING_FACES_FINISHED
}
public class FaceWidget : Gtk.Box {
private static Pango.AttrList attrs_bold;
private static Pango.AttrList attrs_normal;
public signal void face_hidden();
public Gtk.Button edit_button;
public Gtk.Button delete_button;
public Gtk.Label label;
public weak FaceShape face_shape;
static construct {
attrs_bold = new Pango.AttrList();
attrs_bold.insert(Pango.attr_weight_new(Pango.Weight.BOLD));
attrs_normal = new Pango.AttrList();
attrs_normal.insert(Pango.attr_weight_new(Pango.Weight.NORMAL));
}
public FaceWidget (FaceShape face_shape) {
spacing = CONTROL_SPACING;
edit_button = new Gtk.Button.with_label(Resources.EDIT_LABEL);
edit_button.set_use_underline(true);
delete_button = new Gtk.Button.with_label(Resources.DELETE_LABEL);
delete_button.set_use_underline(true);
label = new Gtk.Label(face_shape.get_name());
label.halign = Gtk.Align.START;
label.valign = Gtk.Align.CENTER;
label.ellipsize = Pango.EllipsizeMode.END;
label.width_chars = FACE_LABEL_MAX_CHARS;
pack_start(label, true);
pack_start(edit_button, false);
pack_start(delete_button, false);
this.face_shape = face_shape;
face_shape.set_widget(this);
}
public bool on_enter_notify_event() {
activate_label();
if (face_shape.is_editable())
return false;
// This check is necessary to avoid painting the face twice --see
// note in on_leave_notify_event.
if (!face_shape.is_visible())
face_shape.show();
return true;
}
public bool on_leave_notify_event() {
// This check is necessary because GTK+ will throw enter/leave_notify
// events when the pointer passes though windows, even if one window
// belongs to a widget that is a child of the widget that throws this
// signal. So, this check is necessary to avoid "deactivation" of
// the label if the pointer enters one of the buttons in this FaceWidget.
if (!is_pointer_over(get_window())) {
deactivate_label();
if (face_shape.is_editable())
return false;
face_shape.hide();
face_hidden();
}
return true;
}
public void activate_label() {
label.set_attributes(attrs_bold);
}
public void deactivate_label() {
label.set_attributes(attrs_normal);
}
}
private class FacesToolWindow : EditingTools.EditingToolWindow {
public signal void face_hidden();
public signal void face_edit_requested(string face_name);
public signal void face_delete_requested(string face_name);
public signal void detection_canceled();
public Gtk.Button detection_button = new Gtk.Button.with_label(_("Detect faces…"));
public Gtk.Button ok_button;
public Gtk.Button cancel_button;
public Gtk.Button cancel_detection_button;
private EditingPhase editing_phase = EditingPhase.NOT_EDITING;
private Gtk.Box help_layout = null;
private Gtk.Box response_layout = null;
private Gtk.HSeparator buttons_text_separator = null;
private Gtk.Label help_text = null;
private Gtk.Box face_widgets_layout = null;
private Gtk.Box layout = null;
public FacesToolWindow(Gtk.Window container) {
base(container);
ok_button = new Gtk.Button.with_label(Resources.OK_LABEL);
ok_button.set_use_underline(true);
cancel_button = new Gtk.Button.with_label(Resources.CANCEL_LABEL);
cancel_button.set_use_underline(true);
cancel_detection_button = new Gtk.Button.with_label(Resources.CANCEL_LABEL);
cancel_detection_button.set_use_underline(true);
detection_button.set_tooltip_text(_("Detect faces on this photo"));
cancel_detection_button.set_tooltip_text(_("Cancel face detection"));
cancel_detection_button.set_image_position(Gtk.PositionType.LEFT);
cancel_detection_button.clicked.connect(on_cancel_detection);
cancel_button.set_tooltip_text(_("Close the Faces tool without saving changes"));
cancel_button.set_image_position(Gtk.PositionType.LEFT);
ok_button.set_image_position(Gtk.PositionType.LEFT);
face_widgets_layout = new Gtk.Box(Gtk.Orientation.VERTICAL, CONTROL_SPACING);
help_text = new Gtk.Label(_("Click and drag to tag a face"));
help_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
help_layout.pack_start(help_text, true);
response_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
response_layout.add(detection_button);
response_layout.add(cancel_button);
response_layout.add(ok_button);
layout = new Gtk.Box(Gtk.Orientation.VERTICAL, CONTROL_SPACING);
layout.pack_start(face_widgets_layout, false);
layout.pack_start(help_layout, false);
layout.pack_start(new Gtk.HSeparator(), false);
layout.pack_start(response_layout, false);
add(layout);
}
public void set_editing_phase(EditingPhase phase, FaceShape? face_shape = null) {
if (editing_phase == EditingPhase.DETECTING_FACES &&
phase != EditingPhase.DETECTING_FACES_FINISHED)
return;
switch (phase) {
case EditingPhase.CLICK_TO_EDIT:
assert(face_shape != null);
help_text.set_markup(Markup.printf_escaped(_("Click to edit face %s"),
face_shape.get_name()));
break;
case EditingPhase.NOT_EDITING:
help_text.set_text(_("Click and drag to tag a face"));
break;
case EditingPhase.CREATING_DRAGGING:
help_text.set_text(_("Stop dragging to add your face and name it."));
break;
case EditingPhase.CREATING_EDITING:
help_text.set_text(_("Type a name for this face, then press Enter"));
break;
case EditingPhase.EDITING:
help_text.set_text(_("Move or modify the face shape or name and press Enter"));
break;
case EditingPhase.DETECTING_FACES:
help_text.set_text(_("Detecting faces"));
if (cancel_detection_button.get_parent() == null)
help_layout.pack_start(cancel_detection_button, false);
detection_button.set_sensitive(false);
cancel_detection_button.set_sensitive(true);
cancel_detection_button.show();
break;
case EditingPhase.DETECTING_FACES_FINISHED:
help_text.set_text(_("If you don’t set the name of unknown faces they won’t be saved."));
break;
default:
assert_not_reached();
}
if (editing_phase == EditingPhase.DETECTING_FACES && editing_phase != phase) {
cancel_detection_button.hide();
detection_button.set_sensitive(true);
}
editing_phase = phase;
}
public EditingPhase get_editing_phase() {
return editing_phase;
}
public void ok_button_set_sensitive(bool sensitive) {
if (sensitive)
ok_button.set_tooltip_text(_("Save changes and close the Faces tool"));
else
ok_button.set_tooltip_text(_("No changes to save"));
ok_button.set_sensitive(sensitive);
}
public void add_face(FaceShape face_shape) {
FaceWidget face_widget = new FaceWidget(face_shape);
face_widget.face_hidden.connect(on_face_hidden);
face_widget.edit_button.clicked.connect(edit_face);
face_widget.delete_button.clicked.connect(delete_face);
Gtk.EventBox event_box = new Gtk.EventBox();
event_box.add(face_widget);
event_box.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK);
event_box.enter_notify_event.connect(face_widget.on_enter_notify_event);
event_box.leave_notify_event.connect(face_widget.on_leave_notify_event);
face_widgets_layout.pack_start(event_box, false);
if (buttons_text_separator == null) {
buttons_text_separator = new Gtk.HSeparator();
face_widgets_layout.pack_end(buttons_text_separator, false);
}
face_widgets_layout.show_all();
}
private void edit_face(Gtk.Button button) {
FaceWidget widget = (FaceWidget) button.get_parent();
face_edit_requested(widget.label.get_text());
}
private void delete_face(Gtk.Button button) {
FaceWidget widget = (FaceWidget) button.get_parent();
face_delete_requested(widget.label.get_text());
widget.get_parent().destroy();
if (face_widgets_layout.get_children().length() == 1) {
buttons_text_separator.destroy();
buttons_text_separator = null;
}
}
private void on_face_hidden() {
face_hidden();
}
private void on_cancel_detection() {
detection_canceled();
}
}
public class EditingFaceToolWindow : EditingTools.EditingToolWindow {
public signal bool key_pressed(Gdk.EventKey event);
public Gtk.Entry entry;
private Gtk.Box layout = null;
public EditingFaceToolWindow(Gtk.Window container) {
base(container);
entry = new Gtk.Entry();
layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
layout.add(entry);
add(layout);
}
public override bool key_press_event(Gdk.EventKey event) {
return key_pressed(event) || base.key_press_event(event);
}
}
private class FaceDetectionJob : BackgroundJob {
private Gee.Queue faces = null;
private string image_path;
private string output;
public SpawnError? spawnError;
public FaceDetectionJob(FacesToolWindow owner, string image_path,
CompletionCallback completion_callback, Cancellable cancellable,
CancellationCallback cancellation_callback) {
base(owner, completion_callback, cancellable, cancellation_callback);
this.image_path = image_path;
}
public override void execute() {
try {
string[] argv = {
AppDirs.get_facedetect_bin().get_path(),
"--cascade=" + AppDirs.get_haarcascade_file().get_path(),
"--scale=1.2",
image_path
};
Process.spawn_sync(null, argv, null, SpawnFlags.STDERR_TO_DEV_NULL, null, out output);
} catch (SpawnError e) {
spawnError = e;
critical(e.message);
return;
}
faces = new Gee.PriorityQueue();
string[] lines = output.split("\n");
foreach (string line in lines) {
if (line.length == 0)
continue;
string[] type_and_serialized = line.split(";");
if (type_and_serialized.length != 2) {
critical("Wrong serialized line in face detection program output.");
assert_not_reached();
}
switch (type_and_serialized[0]) {
case "face":
StringBuilder serialized_geometry = new StringBuilder();
serialized_geometry.append(FaceRectangle.SHAPE_TYPE);
serialized_geometry.append(";");
serialized_geometry.append(parse_serialized_geometry(type_and_serialized[1]));
faces.add(serialized_geometry.str);
break;
case "warning":
warning("%s\n", type_and_serialized[1]);
break;
case "error":
critical("%s\n", type_and_serialized[1]);
assert_not_reached();
default:
assert_not_reached();
}
}
}
private string parse_serialized_geometry(string serialized_geometry) {
string[] serialized_geometry_pieces = serialized_geometry.split("&");
if (serialized_geometry_pieces.length != 4) {
critical("Wrong serialized line in face detection program output.");
assert_not_reached();
}
double x = 0;
double y = 0;
double width = 0;
double height = 0;
foreach (string piece in serialized_geometry_pieces) {
string[] name_and_value = piece.split("=");
if (name_and_value.length != 2) {
critical("Wrong serialized line in face detection program output.");
assert_not_reached();
}
switch (name_and_value[0]) {
case "x":
x = name_and_value[1].to_double();
break;
case "y":
y = name_and_value[1].to_double();
break;
case "width":
width = name_and_value[1].to_double();
break;
case "height":
height = name_and_value[1].to_double();
break;
default:
critical("Wrong serialized line in face detection program output.");
assert_not_reached();
}
}
double half_width = width / 2;
double half_height = height / 2;
return "%s;%s;%s;%s".printf((x + half_width).to_string(), (y + half_height).to_string(),
half_width.to_string(), half_height.to_string());
}
public string? get_next() {
if (faces == null)
return null;
return faces.poll();
}
public void reset() {
faces = null;
}
}
private Cairo.Surface image_surface = null;
private Gee.HashMap face_shapes;
private Gee.HashMap original_face_locations;
private Cancellable face_detection_cancellable;
private FaceDetectionJob face_detection;
private Workers workers;
private FaceShape editing_face_shape = null;
private FacesToolWindow faces_tool_window = null;
private FacesTool() {
base("FacesTool");
}
public static FacesTool factory() {
return new FacesTool();
}
public override void activate(EditingTools.PhotoCanvas canvas) {
face_shapes = new Gee.HashMap();
original_face_locations = new Gee.HashMap();
bind_canvas_handlers(canvas);
if (image_surface != null)
image_surface = null;
Gdk.Rectangle scaled_pixbuf_position = canvas.get_scaled_pixbuf_position();
image_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
scaled_pixbuf_position.width,
scaled_pixbuf_position.height);
faces_tool_window = new FacesToolWindow(canvas.get_container());
Gee.Map? face_locations =
FaceLocation.get_locations_by_photo(canvas.get_photo());
if (face_locations != null)
foreach (Gee.Map.Entry entry in face_locations.entries) {
FaceShape new_face_shape;
string serialized_geometry = entry.value.get_serialized_geometry();
try {
new_face_shape = FaceShape.from_serialized(canvas, serialized_geometry);
} catch (FaceShapeError e) {
if (e is FaceShapeError.CANT_CREATE)
continue;
assert_not_reached();
}
Face? face = Face.global.fetch(entry.key);
assert(face != null);
string face_name = face.get_name();
new_face_shape.set_name(face_name);
add_face(new_face_shape);
original_face_locations.set(face_name, serialized_geometry);
}
set_ok_button_sensitivity();
face_detection_cancellable = new Cancellable();
workers = new Workers(1, false);
face_detection = new FaceDetectionJob(faces_tool_window,
canvas.get_photo().get_file().get_path(), on_faces_detected,
face_detection_cancellable, on_detection_cancelled);
bind_window_handlers();
base.activate(canvas);
}
public override void deactivate() {
if (canvas != null)
unbind_canvas_handlers(canvas);
if (faces_tool_window != null) {
unbind_window_handlers();
faces_tool_window.hide();
faces_tool_window.destroy();
faces_tool_window = null;
}
base.deactivate();
}
private void bind_canvas_handlers(EditingTools.PhotoCanvas canvas) {
canvas.new_surface.connect(prepare_ctx);
canvas.resized_scaled_pixbuf.connect(on_resized_pixbuf);
}
private void unbind_canvas_handlers(EditingTools.PhotoCanvas canvas) {
canvas.new_surface.disconnect(prepare_ctx);
canvas.resized_scaled_pixbuf.disconnect(on_resized_pixbuf);
}
private void bind_window_handlers() {
faces_tool_window.key_press_event.connect(on_keypress);
faces_tool_window.ok_button.clicked.connect(on_faces_ok);
faces_tool_window.cancel_button.clicked.connect(notify_cancel);
faces_tool_window.detection_button.clicked.connect(detect_faces);
faces_tool_window.face_hidden.connect(on_face_hidden);
faces_tool_window.face_edit_requested.connect(edit_face);
faces_tool_window.face_delete_requested.connect(delete_face);
faces_tool_window.detection_canceled.connect(cancel_face_detection);
}
private void unbind_window_handlers() {
faces_tool_window.key_press_event.disconnect(on_keypress);
faces_tool_window.ok_button.clicked.disconnect(on_faces_ok);
faces_tool_window.cancel_button.clicked.disconnect(notify_cancel);
faces_tool_window.detection_button.clicked.disconnect(detect_faces);
faces_tool_window.face_hidden.disconnect(on_face_hidden);
faces_tool_window.face_edit_requested.disconnect(edit_face);
faces_tool_window.face_delete_requested.disconnect(delete_face);
faces_tool_window.detection_canceled.disconnect(cancel_face_detection);
}
private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
if (editing_face_shape != null)
editing_face_shape.prepare_ctx(ctx, dim);
}
private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
if (image_surface != null)
image_surface = null;
image_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, scaled.width, scaled.height);
Cairo.Context ctx = new Cairo.Context(image_surface);
ctx.set_source_rgba(255.0, 255.0, 255.0, 0.0);
ctx.paint();
if (editing_face_shape != null)
editing_face_shape.on_resized_pixbuf(old_dim, scaled);
if (face_shapes != null)
foreach (FaceShape face_shape in face_shapes.values)
face_shape.on_resized_pixbuf(old_dim, scaled);
}
public override bool on_keypress(Gdk.EventKey event) {
string event_keyval = Gdk.keyval_name(event.keyval);
if (event_keyval == "Return" || event_keyval == "KP_Enter") {
on_faces_ok();
return true;
}
return base.on_keypress(event);
}
public override void on_left_click(int x, int y) {
if (editing_face_shape != null && editing_face_shape.on_left_click(x, y))
return;
foreach (FaceShape face_shape in face_shapes.values) {
if (face_shape.is_visible() && face_shape.cursor_is_over(x, y)) {
edit_face_shape(face_shape);
face_shape.set_editable(true);
return;
}
}
new_face_shape(x, y);
}
public override void on_left_released(int x, int y) {
if (editing_face_shape != null) {
editing_face_shape.on_left_released(x, y);
if (faces_tool_window.get_editing_phase() == EditingPhase.CREATING_DRAGGING)
faces_tool_window.set_editing_phase(EditingPhase.CREATING_EDITING);
}
}
public override void on_motion(int x, int y, Gdk.ModifierType mask) {
if (editing_face_shape == null) {
FaceShape to_show = null;
double distance = 0;
double new_distance;
foreach (FaceShape face_shape in face_shapes.values) {
bool cursor_is_over = face_shape.cursor_is_over(x, y);
// The FaceShape that will be shown needs to be repainted
// even if it is already visible, since it could be erased by
// another hiding FaceShape -and for the same
// reason it needs to be painted after all
// hiding faces are already erased.
// Also, we paint the FaceShape whose center is closer
// to the pointer.
if (cursor_is_over) {
face_shape.hide();
face_shape.get_widget().deactivate_label();
if (to_show == null) {
to_show = face_shape;
distance = face_shape.get_distance(x, y);
} else {
new_distance = face_shape.get_distance(x, y);
if (new_distance < distance) {
to_show = face_shape;
distance = new_distance;
}
}
} else if (!cursor_is_over && face_shape.is_visible()) {
face_shape.hide();
face_shape.get_widget().deactivate_label();
}
}
if (to_show == null) {
faces_tool_window.set_editing_phase(EditingPhase.NOT_EDITING);
} else {
faces_tool_window.set_editing_phase(EditingPhase.CLICK_TO_EDIT, to_show);
to_show.show();
to_show.get_widget().activate_label();
}
} else editing_face_shape.on_motion(x, y, mask);
}
public override bool on_leave_notify_event() {
// This check is a workaround for bug #3896.
if (is_pointer_over(canvas.get_drawing_window()) &&
!is_pointer_over(faces_tool_window.get_window()))
return false;
if (editing_face_shape != null)
return base.on_leave_notify_event();
foreach (FaceShape face_shape in face_shapes.values) {
if (face_shape.is_editable())
return base.on_leave_notify_event();
if (face_shape.is_visible()) {
face_shape.hide();
face_shape.get_widget().deactivate_label();
break;
}
}
faces_tool_window.set_editing_phase(EditingPhase.NOT_EDITING);
return base.on_leave_notify_event();
}
public override EditingTools.EditingToolWindow? get_tool_window() {
return faces_tool_window;
}
public override void paint(Cairo.Context default_ctx) {
// fill region behind the image surface with neutral color
int w = canvas.get_drawing_window().get_width();
int h = canvas.get_drawing_window().get_height();
default_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
default_ctx.rectangle(0, 0, w, h);
default_ctx.fill();
default_ctx.paint();
Cairo.Context ctx = new Cairo.Context(image_surface);
ctx.set_operator(Cairo.Operator.SOURCE);
ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0);
ctx.paint();
canvas.paint_surface(image_surface, true);
// paint face shape last
if (editing_face_shape != null)
editing_face_shape.show();
}
private void new_face_shape(int x, int y) {
edit_face_shape(new FaceRectangle(canvas, x, y), true);
}
private void edit_face_shape(FaceShape face_shape, bool creating = false) {
hide_visible_face();
if (editing_face_shape != null) {
// We need to do this because it could be one of the already
// created faces being edited, and if that is the case it
// will not be destroyed.
editing_face_shape.hide();
editing_face_shape.set_editable(false);
// This is to allow the user to edit a FaceShape's shape
// without pressing the Enter button.
if (face_shapes.values.contains(editing_face_shape))
set_ok_button_sensitivity();
editing_face_shape = null;
}
if (creating) {
faces_tool_window.set_editing_phase(EditingPhase.CREATING_DRAGGING);
} else {
face_shape.show();
faces_tool_window.set_editing_phase(EditingPhase.EDITING);
}
editing_face_shape = face_shape;
editing_face_shape.add_me_requested.connect(add_face);
editing_face_shape.delete_me_requested.connect(release_face_shape);
}
private void release_face_shape() {
if (editing_face_shape == null)
return;
// We need to do this because it could be one of the already
// created faces being edited, and if that is the case it
// will not be destroyed.
if (editing_face_shape in face_shapes.values) {
editing_face_shape.hide();
editing_face_shape.set_editable(false);
editing_face_shape.get_widget().deactivate_label();
}
editing_face_shape = null;
faces_tool_window.set_editing_phase(EditingPhase.NOT_EDITING);
faces_tool_window.present();
}
private void hide_visible_face() {
foreach (FaceShape face_shape in face_shapes.values) {
if (face_shape.is_visible()) {
face_shape.hide();
break;
}
}
}
private void on_faces_ok() {
if (face_shapes == null)
return;
Gee.Map new_faces = new Gee.HashMap();
foreach (FaceShape face_shape in face_shapes.values) {
if (!face_shape.get_known())
continue;
Face new_face = Face.for_name(face_shape.get_name());
new_faces.set(new_face, face_shape.serialize());
}
ModifyFacesCommand command = new ModifyFacesCommand(canvas.get_photo(), new_faces);
applied(command, null, canvas.get_photo().get_dimensions(), false);
}
private void on_face_hidden() {
if (editing_face_shape != null)
editing_face_shape.show();
}
private void add_face(FaceShape face_shape) {
string? prepared_face_name = Face.prep_face_name(face_shape.get_name());
if (prepared_face_name != null) {
face_shape.set_name(prepared_face_name);
if (face_shapes.values.contains(face_shape)) {
foreach (Gee.Map.Entry entry in face_shapes.entries) {
if (entry.value == face_shape) {
if (entry.key == prepared_face_name)
break;
face_shapes.unset(entry.key);
face_shapes.set(prepared_face_name, face_shape);
face_shape.set_known(true);
face_shape.get_widget().label.set_text(face_shape.get_name());
break;
}
}
} else if (!face_shapes.has_key(prepared_face_name)) {
faces_tool_window.add_face(face_shape);
face_shapes.set(prepared_face_name, face_shape);
} else return;
face_shape.hide();
face_shape.set_editable(false);
set_ok_button_sensitivity();
release_face_shape();
}
}
private void edit_face(string face_name) {
FaceShape face_shape = face_shapes.get(face_name);
assert(face_shape != null);
face_shape.set_editable(true);
edit_face_shape(face_shape);
}
private void delete_face(string face_name) {
face_shapes.unset(face_name);
// It is posible to have two visible faces at the same time, this happens
// if you are editing one face and you move the pointer around the
// FaceWidgets area in FacesToolWindow. And you can delete one of that
// faces, so the other visible face must be repainted.
foreach (FaceShape face_shape in face_shapes.values) {
if (face_shape.is_visible()) {
face_shape.hide();
face_shape.show();
break;
}
}
set_ok_button_sensitivity();
}
private void set_ok_button_sensitivity() {
Gee.Map known_face_shapes = new Gee.HashMap();
foreach (Gee.Map.Entry face_shape in face_shapes.entries) {
if (face_shape.value.get_known()) {
known_face_shapes.set(face_shape.key, face_shape.value);
}
}
if (original_face_locations.size != known_face_shapes.size) {
faces_tool_window.ok_button_set_sensitive(true);
return;
}
foreach (Gee.Map.Entry face_shape in known_face_shapes.entries) {
bool found = false;
foreach (Gee.Map.Entry face_location in original_face_locations.entries) {
if (face_location.key == face_shape.key) {
if (face_location.value == face_shape.value.serialize()) {
found = true;
break;
} else {
faces_tool_window.ok_button_set_sensitive(true);
return;
}
}
}
if (!found) {
faces_tool_window.ok_button_set_sensitive(true);
return;
}
}
faces_tool_window.ok_button_set_sensitive(false);
}
private void detect_faces() {
faces_tool_window.detection_button.set_sensitive(false);
faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES);
workers.enqueue(face_detection);
}
private void pick_faces_from_autodetected() {
int c = 0;
while (true) {
string? serialized_geometry = face_detection.get_next();
if (serialized_geometry == null) {
faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);
return;
}
FaceShape face_shape;
try {
face_shape = FaceShape.from_serialized(canvas, serialized_geometry);
} catch (FaceShapeError e) {
if (e is FaceShapeError.CANT_CREATE)
continue;
assert_not_reached();
}
bool found = false;
foreach (FaceShape existing_face_shape in face_shapes.values) {
if (existing_face_shape.equals(face_shape)) {
found = true;
break;
}
}
if (found)
continue;
c++;
face_shape.set_name("Unknown face #%d".printf(c));
face_shape.set_known(false);
add_face(face_shape);
}
}
private void on_faces_detected() {
face_detection_cancellable.reset();
if (face_detection.spawnError != null){
string spawnErrorMessage = _("Error trying to spawn face detection program:\n");
AppWindow.error_message(spawnErrorMessage + face_detection.spawnError.message + "\n");
faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);
} else
pick_faces_from_autodetected();
}
private void on_detection_cancelled(BackgroundJob job) {
((FaceDetectionJob) job).reset();
face_detection_cancellable.reset();
faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);
}
private void cancel_face_detection() {
faces_tool_window.cancel_detection_button.set_sensitive(false);
face_detection.cancel();
}
}
#endif