[shotwell/wip/gtk4: 71/94] Fix editing tools, part 1
- From: Jens Georg <jensgeorg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [shotwell/wip/gtk4: 71/94] Fix editing tools, part 1
- Date: Thu, 28 Jul 2022 21:57:36 +0000 (UTC)
commit 20053e60305c41bdc86f8ee399926c2d724978ca
Author: Jens Georg <mail jensge org>
Date: Sun Apr 17 13:26:03 2022 +0200
Fix editing tools, part 1
src/Commands.vala | 3 +-
src/Photo.vala | 2 -
src/editing_tools/AdjustTool.vala | 835 +++++++
src/editing_tools/CropTool.vala | 1249 ++++++++++
src/editing_tools/EditingTool.vala | 122 +
src/editing_tools/EditingToolWindow.vala | 64 +
src/editing_tools/EditingTools.vala | 2949 +-----------------------
src/editing_tools/PhotoCanvas.vala | 351 +++
src/editing_tools/RGBHistogramManipulator.vala | 136 +-
src/editing_tools/RedeyeTool.vala | 364 +++
src/meson.build | 8 +
11 files changed, 3059 insertions(+), 3024 deletions(-)
---
diff --git a/src/Commands.vala b/src/Commands.vala
index bc968878..d14469da 100644
--- a/src/Commands.vala
+++ b/src/Commands.vala
@@ -864,7 +864,7 @@ public class AdjustColorsMultipleCommand : MultiplePhotoTransformationCommand {
((Photo) source).set_color_adjustments(transformations);
}
}
-#if 0
+
public class RedeyeCommand : GenericPhotoTransformationCommand {
private EditingTools.RedeyeInstance redeye_instance;
@@ -879,7 +879,6 @@ public class RedeyeCommand : GenericPhotoTransformationCommand {
photo.add_redeye_instance(redeye_instance);
}
}
-#endif
public abstract class MovePhotosCommand : Command {
// Piggyback on a private command so that processing to determine new_event can occur before
diff --git a/src/Photo.vala b/src/Photo.vala
index 4f5b8b4c..6f7240a2 100644
--- a/src/Photo.vala
+++ b/src/Photo.vala
@@ -3027,7 +3027,6 @@ public abstract class Photo : PhotoSource, Dateable, Positionable {
}
// All instances are against the coordinate system of the unscaled, unrotated photo.
- #if 0
private EditingTools.RedeyeInstance[] get_raw_redeye_instances() {
KeyValueMap map = get_transformation("redeye");
if (map == null)
@@ -3085,7 +3084,6 @@ public abstract class Photo : PhotoSource, Dateable, Positionable {
if (set_transformation(map))
notify_altered(new Alteration("image", "redeye"));
}
- #endif
// Pixbuf generation
diff --git a/src/editing_tools/AdjustTool.vala b/src/editing_tools/AdjustTool.vala
new file mode 100644
index 00000000..2f05e573
--- /dev/null
+++ b/src/editing_tools/AdjustTool.vala
@@ -0,0 +1,835 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+public class EditingTools.AdjustTool : EditingTool {
+ private const int SLIDER_WIDTH = 200;
+ private const uint SLIDER_DELAY_MSEC = 100;
+
+ private class AdjustToolWindow : EditingToolWindow {
+ public Gtk.Scale exposure_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ ExposureTransformation.MIN_PARAMETER, ExposureTransformation.MAX_PARAMETER,
+ 1.0);
+ public Gtk.GestureClick exposure_click = new Gtk.GestureClick();
+
+ public Gtk.Scale contrast_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ ContrastTransformation.MIN_PARAMETER, ContrastTransformation.MAX_PARAMETER,
+ 1.0);
+ public Gtk.GestureClick contrast_click = new Gtk.GestureClick();
+
+ public Gtk.Scale saturation_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ SaturationTransformation.MIN_PARAMETER, SaturationTransformation.MAX_PARAMETER,
+ 1.0);
+ public Gtk.GestureClick saturation_click = new Gtk.GestureClick();
+
+ public Gtk.Scale tint_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ TintTransformation.MIN_PARAMETER, TintTransformation.MAX_PARAMETER, 1.0);
+ public Gtk.GestureClick tint_click = new Gtk.GestureClick();
+
+ public Gtk.Scale temperature_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ TemperatureTransformation.MIN_PARAMETER, TemperatureTransformation.MAX_PARAMETER,
+ 1.0);
+ public Gtk.GestureClick temperature_click = new Gtk.GestureClick();
+
+ public Gtk.Scale shadows_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ ShadowDetailTransformation.MIN_PARAMETER, ShadowDetailTransformation.MAX_PARAMETER,
+ 1.0);
+ public Gtk.GestureClick shadows_click = new Gtk.GestureClick();
+
+ public Gtk.Scale highlights_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ HighlightDetailTransformation.MIN_PARAMETER, HighlightDetailTransformation.MAX_PARAMETER,
+ 1.0);
+ public Gtk.GestureClick highlights_click = new Gtk.GestureClick();
+
+ public Gtk.Button ok_button = new Gtk.Button.with_mnemonic(Resources.OK_LABEL);
+ public Gtk.Button reset_button = new Gtk.Button.with_mnemonic(_("_Reset"));
+ public Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
+ public RGBHistogramManipulator histogram_manipulator = new RGBHistogramManipulator();
+
+ public AdjustToolWindow(Gtk.Window container) {
+ base(container);
+
+ Gtk.Grid slider_organizer = new Gtk.Grid();
+ slider_organizer.set_column_homogeneous(false);
+ slider_organizer.set_row_spacing(12);
+ slider_organizer.set_column_spacing(12);
+ slider_organizer.set_margin_start(12);
+ slider_organizer.set_margin_bottom(12);
+
+ Gtk.Label exposure_label = new Gtk.Label.with_mnemonic(_("Exposure:"));
+ exposure_label.halign = Gtk.Align.START;
+ exposure_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(exposure_label, 0, 0, 1, 1);
+ slider_organizer.attach(exposure_slider, 1, 0, 1, 1);
+ exposure_slider.set_size_request(SLIDER_WIDTH, -1);
+ exposure_slider.set_value_pos(Gtk.PositionType.RIGHT);
+ exposure_slider.set_margin_end(0);
+ exposure_click.set_button(Gdk.BUTTON_PRIMARY);
+ exposure_click.set_touch_only(false);
+ exposure_click.set_exclusive(true);
+ exposure_slider.add_controller(exposure_click);
+
+ Gtk.Label contrast_label = new Gtk.Label.with_mnemonic(_("Contrast:"));
+ contrast_label.halign = Gtk.Align.START;
+ contrast_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(contrast_label, 0, 1, 1, 1);
+ slider_organizer.attach(contrast_slider, 1, 1, 1, 1);
+ contrast_slider.set_size_request(SLIDER_WIDTH, -1);
+ contrast_slider.set_value_pos(Gtk.PositionType.RIGHT);
+ contrast_slider.set_margin_end(0);
+ contrast_click.set_button(Gdk.BUTTON_PRIMARY);
+ contrast_click.set_touch_only(false);
+ contrast_click.set_exclusive(true);
+ contrast_slider.add_controller(contrast_click);
+
+ Gtk.Label saturation_label = new Gtk.Label.with_mnemonic(_("Saturation:"));
+ saturation_label.halign = Gtk.Align.START;
+ saturation_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(saturation_label, 0, 2, 1, 1);
+ slider_organizer.attach(saturation_slider, 1, 2, 1, 1);
+ saturation_slider.set_size_request(SLIDER_WIDTH, -1);
+ saturation_slider.set_draw_value(false);
+ saturation_slider.set_margin_end(0);
+ saturation_click.set_button(Gdk.BUTTON_PRIMARY);
+ saturation_click.set_touch_only(false);
+ saturation_click.set_exclusive(true);
+ saturation_slider.add_controller(saturation_click);
+
+ Gtk.Label tint_label = new Gtk.Label.with_mnemonic(_("Tint:"));
+ tint_label.halign = Gtk.Align.START;
+ tint_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(tint_label, 0, 3, 1, 1);
+ slider_organizer.attach(tint_slider, 1, 3, 1, 1);
+ tint_slider.set_size_request(SLIDER_WIDTH, -1);
+ tint_slider.set_value_pos(Gtk.PositionType.RIGHT);
+ tint_slider.set_margin_end(0);
+ tint_click.set_button(Gdk.BUTTON_PRIMARY);
+ tint_click.set_touch_only(false);
+ tint_click.set_exclusive(true);
+ tint_slider.add_controller(tint_click);
+
+
+ Gtk.Label temperature_label =
+ new Gtk.Label.with_mnemonic(_("Temperature:"));
+ temperature_label.halign = Gtk.Align.START;
+ temperature_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(temperature_label, 0, 4, 1, 1);
+ slider_organizer.attach(temperature_slider, 1, 4, 1, 1);
+ temperature_slider.set_size_request(SLIDER_WIDTH, -1);
+ temperature_slider.set_value_pos(Gtk.PositionType.RIGHT);
+ temperature_slider.set_margin_end(0);
+ temperature_click.set_button(Gdk.BUTTON_PRIMARY);
+ temperature_click.set_touch_only(false);
+ temperature_click.set_exclusive(true);
+ temperature_slider.add_controller(temperature_click);
+
+
+ Gtk.Label shadows_label = new Gtk.Label.with_mnemonic(_("Shadows:"));
+ shadows_label.halign = Gtk.Align.START;
+ shadows_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(shadows_label, 0, 5, 1, 1);
+ slider_organizer.attach(shadows_slider, 1, 5, 1, 1);
+ shadows_slider.set_size_request(SLIDER_WIDTH, -1);
+ shadows_slider.set_value_pos(Gtk.PositionType.RIGHT);
+ // FIXME: Hack to make the slider the same length as the other. Find out why it is aligned
+ // Differently (probably because it only has positive values)
+ shadows_slider.set_margin_end(5);
+ shadows_click.set_button(Gdk.BUTTON_PRIMARY);
+ shadows_click.set_touch_only(false);
+ shadows_click.set_exclusive(true);
+ shadows_slider.add_controller(shadows_click);
+
+ Gtk.Label highlights_label = new Gtk.Label.with_mnemonic(_("Highlights:"));
+ highlights_label.halign = Gtk.Align.START;
+ highlights_label.valign = Gtk.Align.CENTER;
+ slider_organizer.attach(highlights_label, 0, 6, 1, 1);
+ slider_organizer.attach(highlights_slider, 1, 6, 1, 1);
+ highlights_slider.set_size_request(SLIDER_WIDTH, -1);
+ highlights_slider.set_value_pos(Gtk.PositionType.RIGHT);
+ highlights_slider.set_margin_end(0);
+ highlights_click.set_button(Gdk.BUTTON_PRIMARY);
+ highlights_click.set_touch_only(false);
+ highlights_click.set_exclusive(true);
+ highlights_slider.add_controller(highlights_click);
+
+ Gtk.Box button_layouter = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ button_layouter.set_homogeneous(true);
+ button_layouter.append(cancel_button);
+ button_layouter.append(reset_button);
+ button_layouter.append(ok_button);
+
+ histogram_manipulator.set_margin_start (12);
+ histogram_manipulator.set_margin_end (12);
+ histogram_manipulator.set_margin_top (12);
+ histogram_manipulator.set_margin_bottom (8);
+
+ Gtk.Box pane_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 8);
+ pane_layouter.append(histogram_manipulator);
+ pane_layouter.append(slider_organizer);
+ pane_layouter.append(button_layouter);
+
+ add(pane_layouter);
+ }
+ }
+
+ private abstract class AdjustToolCommand : Command {
+ protected weak AdjustTool owner;
+
+ protected AdjustToolCommand(AdjustTool owner, string name, string explanation) {
+ base (name, explanation);
+
+ this.owner = owner;
+ owner.deactivated.connect(on_owner_deactivated);
+ }
+
+ ~AdjustToolCommand() {
+ if (owner != null)
+ owner.deactivated.disconnect(on_owner_deactivated);
+ }
+
+ private void on_owner_deactivated() {
+ // This reset call is by design. See notes on ticket #1946 if this is undesirable or if
+ // you are planning to change it.
+ AppWindow.get_command_manager().reset();
+ }
+ }
+
+ private class AdjustResetCommand : AdjustToolCommand {
+ private PixelTransformationBundle original;
+ private PixelTransformationBundle reset;
+
+ public AdjustResetCommand(AdjustTool owner, PixelTransformationBundle current) {
+ base (owner, _("Reset Colors"), _("Reset all color adjustments to original"));
+
+ original = current.copy();
+ reset = new PixelTransformationBundle();
+ reset.set_to_identity();
+ }
+
+ public override void execute() {
+ owner.set_adjustments(reset);
+ }
+
+ public override void undo() {
+ owner.set_adjustments(original);
+ }
+
+ public override bool compress(Command command) {
+ AdjustResetCommand reset_command = command as AdjustResetCommand;
+ if (reset_command == null)
+ return false;
+
+ if (reset_command.owner != owner)
+ return false;
+
+ // multiple successive resets on the same photo as good as a single
+ return true;
+ }
+ }
+
+ private class SliderAdjustmentCommand : AdjustToolCommand {
+ private PixelTransformationType transformation_type;
+ private PixelTransformation new_transformation;
+ private PixelTransformation old_transformation;
+
+ public SliderAdjustmentCommand(AdjustTool owner, PixelTransformation old_transformation,
+ PixelTransformation new_transformation, string name) {
+ base(owner, name, name);
+
+ this.old_transformation = old_transformation;
+ this.new_transformation = new_transformation;
+ transformation_type = old_transformation.get_transformation_type();
+ assert(new_transformation.get_transformation_type() == transformation_type);
+ }
+
+ public override void execute() {
+ // don't update slider; it's been moved by the user
+ owner.update_transformation(new_transformation);
+ owner.canvas.repaint();
+ }
+
+ public override void undo() {
+ owner.update_transformation(old_transformation);
+
+ owner.unbind_window_handlers();
+ owner.update_slider(old_transformation);
+ owner.bind_window_handlers();
+
+ owner.canvas.repaint();
+ }
+
+ public override void redo() {
+ owner.update_transformation(new_transformation);
+
+ owner.unbind_window_handlers();
+ owner.update_slider(new_transformation);
+ owner.bind_window_handlers();
+
+ owner.canvas.repaint();
+ }
+
+ public override bool compress(Command command) {
+ SliderAdjustmentCommand slider_adjustment = command as SliderAdjustmentCommand;
+ if (slider_adjustment == null)
+ return false;
+
+ // same photo
+ if (slider_adjustment.owner != owner)
+ return false;
+
+ // same adjustment
+ if (slider_adjustment.transformation_type != transformation_type)
+ return false;
+
+ // execute the command
+ slider_adjustment.execute();
+
+ // save it's transformation as ours
+ new_transformation = slider_adjustment.new_transformation;
+
+ return true;
+ }
+ }
+
+ private class AdjustEnhanceCommand : AdjustToolCommand {
+ private Photo photo;
+ private PixelTransformationBundle original;
+ private PixelTransformationBundle enhanced = null;
+
+ public AdjustEnhanceCommand(AdjustTool owner, Photo photo) {
+ base(owner, Resources.ENHANCE_LABEL, Resources.ENHANCE_TOOLTIP);
+
+ this.photo = photo;
+ original = photo.get_color_adjustments();
+ }
+
+ public override void execute() {
+ if (enhanced == null)
+ enhanced = photo.get_enhance_transformations();
+
+ owner.set_adjustments(enhanced);
+ }
+
+ public override void undo() {
+ owner.set_adjustments(original);
+ }
+
+ public override bool compress(Command command) {
+ // can compress both normal enhance and one with the adjust tool running
+ EnhanceSingleCommand enhance_single = command as EnhanceSingleCommand;
+ if (enhance_single != null) {
+ Photo photo = (Photo) enhance_single.get_source();
+
+ // multiple successive enhances are as good as a single, as long as it's on the
+ // same photo
+ return photo.equals(owner.canvas.get_photo());
+ }
+
+ AdjustEnhanceCommand enhance_command = command as AdjustEnhanceCommand;
+ if (enhance_command == null)
+ return false;
+
+ if (enhance_command.owner != owner)
+ return false;
+
+ // multiple successive as good as a single
+ return true;
+ }
+ }
+
+ private AdjustToolWindow adjust_tool_window = null;
+ private bool suppress_effect_redraw = false;
+ private Gdk.Pixbuf draw_to_pixbuf = null;
+ private Gdk.Pixbuf histogram_pixbuf = null;
+ private Gdk.Pixbuf virgin_histogram_pixbuf = null;
+ private PixelTransformer transformer = null;
+ private PixelTransformer histogram_transformer = null;
+ private PixelTransformationBundle transformations = null;
+ private float[] fp_pixel_cache = null;
+ private bool disable_histogram_refresh = false;
+ private OneShotScheduler? temperature_scheduler = null;
+ private OneShotScheduler? tint_scheduler = null;
+ private OneShotScheduler? contrast_scheduler = null;
+ private OneShotScheduler? saturation_scheduler = null;
+ private OneShotScheduler? exposure_scheduler = null;
+ private OneShotScheduler? shadows_scheduler = null;
+ private OneShotScheduler? highlights_scheduler = null;
+
+ private AdjustTool() {
+ base("AdjustTool");
+ }
+
+ public static AdjustTool factory() {
+ return new AdjustTool();
+ }
+
+ public static bool is_available(Photo photo, Scaling scaling) {
+ return true;
+ }
+
+ public override void activate(PhotoCanvas canvas) {
+ adjust_tool_window = new AdjustToolWindow(canvas.get_container());
+
+ Photo photo = canvas.get_photo();
+ transformations = photo.get_color_adjustments();
+ transformer = transformations.generate_transformer();
+
+ // the histogram transformer uses all transformations but contrast expansion
+ histogram_transformer = new PixelTransformer();
+
+ /* set up expansion */
+ ExpansionTransformation expansion_trans = (ExpansionTransformation)
+ transformations.get_transformation(PixelTransformationType.TONE_EXPANSION);
+ adjust_tool_window.histogram_manipulator.set_left_nub_position(
+ expansion_trans.get_black_point());
+ adjust_tool_window.histogram_manipulator.set_right_nub_position(
+ expansion_trans.get_white_point());
+
+ /* set up shadows */
+ ShadowDetailTransformation shadows_trans = (ShadowDetailTransformation)
+ transformations.get_transformation(PixelTransformationType.SHADOWS);
+ histogram_transformer.attach_transformation(shadows_trans);
+ adjust_tool_window.shadows_slider.set_value(shadows_trans.get_parameter());
+
+ /* set up highlights */
+ HighlightDetailTransformation highlights_trans = (HighlightDetailTransformation)
+ transformations.get_transformation(PixelTransformationType.HIGHLIGHTS);
+ histogram_transformer.attach_transformation(highlights_trans);
+ adjust_tool_window.highlights_slider.set_value(highlights_trans.get_parameter());
+
+ /* set up temperature & tint */
+ TemperatureTransformation temp_trans = (TemperatureTransformation)
+ transformations.get_transformation(PixelTransformationType.TEMPERATURE);
+ histogram_transformer.attach_transformation(temp_trans);
+ adjust_tool_window.temperature_slider.set_value(temp_trans.get_parameter());
+
+ TintTransformation tint_trans = (TintTransformation)
+ transformations.get_transformation(PixelTransformationType.TINT);
+ histogram_transformer.attach_transformation(tint_trans);
+ adjust_tool_window.tint_slider.set_value(tint_trans.get_parameter());
+
+ /* set up saturation */
+ SaturationTransformation sat_trans = (SaturationTransformation)
+ transformations.get_transformation(PixelTransformationType.SATURATION);
+ histogram_transformer.attach_transformation(sat_trans);
+ adjust_tool_window.saturation_slider.set_value(sat_trans.get_parameter());
+
+ /* set up exposure */
+ ExposureTransformation exposure_trans = (ExposureTransformation)
+ transformations.get_transformation(PixelTransformationType.EXPOSURE);
+ histogram_transformer.attach_transformation(exposure_trans);
+ adjust_tool_window.exposure_slider.set_value(exposure_trans.get_parameter());
+
+ /* set up contrast */
+ ContrastTransformation contrast_trans = (ContrastTransformation)
+ transformations.get_transformation(PixelTransformationType.CONTRAST);
+ histogram_transformer.attach_transformation(contrast_trans);
+ adjust_tool_window.contrast_slider.set_value(contrast_trans.get_parameter());
+
+ bind_canvas_handlers(canvas);
+ bind_window_handlers();
+
+ draw_to_pixbuf = canvas.get_scaled_pixbuf().copy();
+ init_fp_pixel_cache(canvas.get_scaled_pixbuf());
+
+ /* if we have an 1x1 pixel image, then there's no need to deal with recomputing the
+ histogram, because a histogram for a 1x1 image is meaningless. The histogram shows the
+ distribution of color over all the many pixels in an image, but if an image only has
+ one pixel, the notion of a "distribution over pixels" makes no sense. */
+ if (draw_to_pixbuf.width == 1 && draw_to_pixbuf.height == 1)
+ disable_histogram_refresh = true;
+
+ /* don't sample the original image to create the histogram if the original image is
+ sufficiently large -- if it's over 8k pixels, then we'll get pretty much the same
+ histogram if we sample from a half-size image */
+ if (((draw_to_pixbuf.width * draw_to_pixbuf.height) > 8192) && (draw_to_pixbuf.width > 1) &&
+ (draw_to_pixbuf.height > 1)) {
+ histogram_pixbuf = draw_to_pixbuf.scale_simple(draw_to_pixbuf.width / 2,
+ draw_to_pixbuf.height / 2, Gdk.InterpType.HYPER);
+ } else {
+ histogram_pixbuf = draw_to_pixbuf.copy();
+ }
+ virgin_histogram_pixbuf = histogram_pixbuf.copy();
+
+ DataCollection? owner = canvas.get_photo().get_membership();
+ if (owner != null)
+ owner.items_altered.connect(on_photos_altered);
+
+ base.activate(canvas);
+ }
+
+ public override EditingToolWindow? get_tool_window() {
+ return adjust_tool_window;
+ }
+
+ public override void deactivate() {
+ if (canvas != null) {
+ DataCollection? owner = canvas.get_photo().get_membership();
+ if (owner != null)
+ owner.items_altered.disconnect(on_photos_altered);
+
+ unbind_canvas_handlers(canvas);
+ }
+
+ if (adjust_tool_window != null) {
+ unbind_window_handlers();
+ adjust_tool_window.hide();
+ adjust_tool_window.destroy();
+ adjust_tool_window = null;
+ }
+
+ draw_to_pixbuf = null;
+ fp_pixel_cache = null;
+
+ base.deactivate();
+ }
+
+ public override void paint(Cairo.Context ctx) {
+ if (!suppress_effect_redraw) {
+ transformer.transform_from_fp(ref fp_pixel_cache, draw_to_pixbuf);
+ histogram_transformer.transform_to_other_pixbuf(virgin_histogram_pixbuf,
+ histogram_pixbuf);
+ if (!disable_histogram_refresh)
+ adjust_tool_window.histogram_manipulator.update_histogram(histogram_pixbuf);
+ }
+
+ canvas.paint_pixbuf(draw_to_pixbuf);
+ }
+
+ public override Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
+ out Dimensions max_dim) throws Error {
+ if (!photo.has_color_adjustments()) {
+ max_dim = Dimensions();
+
+ return null;
+ }
+
+ max_dim = photo.get_dimensions();
+
+ return photo.get_pixbuf_with_options(scaling, Photo.Exception.ADJUST);
+ }
+
+ private void on_reset() {
+ AdjustResetCommand command = new AdjustResetCommand(this, transformations);
+ AppWindow.get_command_manager().execute(command);
+ }
+
+ private void on_ok() {
+ suppress_effect_redraw = true;
+
+ get_tool_window().hide();
+
+ applied(new AdjustColorsSingleCommand(canvas.get_photo(), transformations,
+ Resources.ADJUST_LABEL, Resources.ADJUST_TOOLTIP), draw_to_pixbuf,
+ canvas.get_photo().get_dimensions(), false);
+ }
+
+ private void update_transformations(PixelTransformationBundle new_transformations) {
+ foreach (PixelTransformation transformation in new_transformations.get_transformations())
+ update_transformation(transformation);
+ }
+
+ private void update_transformation(PixelTransformation new_transformation) {
+ PixelTransformation old_transformation = transformations.get_transformation(
+ new_transformation.get_transformation_type());
+
+ transformer.replace_transformation(old_transformation, new_transformation);
+ if (new_transformation.get_transformation_type() != PixelTransformationType.TONE_EXPANSION)
+ histogram_transformer.replace_transformation(old_transformation, new_transformation);
+
+ transformations.set(new_transformation);
+ }
+
+ private void slider_updated(PixelTransformation new_transformation, string name) {
+ PixelTransformation old_transformation = transformations.get_transformation(
+ new_transformation.get_transformation_type());
+ SliderAdjustmentCommand command = new SliderAdjustmentCommand(this, old_transformation,
+ new_transformation, name);
+ AppWindow.get_command_manager().execute(command);
+ }
+
+ private void on_temperature_adjustment() {
+ if (temperature_scheduler == null)
+ temperature_scheduler = new OneShotScheduler("temperature", on_delayed_temperature_adjustment);
+
+ temperature_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_temperature_adjustment() {
+ TemperatureTransformation new_temp_trans = new TemperatureTransformation(
+ (float) adjust_tool_window.temperature_slider.get_value());
+ slider_updated(new_temp_trans, _("Temperature"));
+ }
+
+ private void on_tint_adjustment() {
+ if (tint_scheduler == null)
+ tint_scheduler = new OneShotScheduler("tint", on_delayed_tint_adjustment);
+ tint_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_tint_adjustment() {
+ TintTransformation new_tint_trans = new TintTransformation(
+ (float) adjust_tool_window.tint_slider.get_value());
+ slider_updated(new_tint_trans, _("Tint"));
+ }
+
+ private void on_contrast_adjustment() {
+ if (this.contrast_scheduler == null)
+ this.contrast_scheduler = new OneShotScheduler("contrast", on_delayed_contrast_adjustment);
+ this.contrast_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_contrast_adjustment() {
+ ContrastTransformation new_exp_trans = new ContrastTransformation(
+ (float) adjust_tool_window.contrast_slider.get_value());
+ slider_updated(new_exp_trans, _("Contrast"));
+ }
+
+
+ private void on_saturation_adjustment() {
+ if (saturation_scheduler == null)
+ saturation_scheduler = new OneShotScheduler("saturation", on_delayed_saturation_adjustment);
+
+ saturation_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_saturation_adjustment() {
+ SaturationTransformation new_sat_trans = new SaturationTransformation(
+ (float) adjust_tool_window.saturation_slider.get_value());
+ slider_updated(new_sat_trans, _("Saturation"));
+ }
+
+ private void on_exposure_adjustment() {
+ if (exposure_scheduler == null)
+ exposure_scheduler = new OneShotScheduler("exposure", on_delayed_exposure_adjustment);
+
+ exposure_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_exposure_adjustment() {
+ ExposureTransformation new_exp_trans = new ExposureTransformation(
+ (float) adjust_tool_window.exposure_slider.get_value());
+ slider_updated(new_exp_trans, _("Exposure"));
+ }
+
+ private void on_shadows_adjustment() {
+ if (shadows_scheduler == null)
+ shadows_scheduler = new OneShotScheduler("shadows", on_delayed_shadows_adjustment);
+
+ shadows_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_shadows_adjustment() {
+ ShadowDetailTransformation new_shadows_trans = new ShadowDetailTransformation(
+ (float) adjust_tool_window.shadows_slider.get_value());
+ slider_updated(new_shadows_trans, _("Shadows"));
+ }
+
+ private void on_highlights_adjustment() {
+ if (highlights_scheduler == null)
+ highlights_scheduler = new OneShotScheduler("highlights", on_delayed_highlights_adjustment);
+
+ highlights_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
+ }
+
+ private void on_delayed_highlights_adjustment() {
+ HighlightDetailTransformation new_highlights_trans = new HighlightDetailTransformation(
+ (float) adjust_tool_window.highlights_slider.get_value());
+ slider_updated(new_highlights_trans, _("Highlights"));
+ }
+
+ private void on_histogram_constraint() {
+ int expansion_black_point =
+ adjust_tool_window.histogram_manipulator.get_left_nub_position();
+ int expansion_white_point =
+ adjust_tool_window.histogram_manipulator.get_right_nub_position();
+ ExpansionTransformation new_exp_trans =
+ new ExpansionTransformation.from_extrema(expansion_black_point, expansion_white_point);
+ slider_updated(new_exp_trans, _("Contrast Expansion"));
+ }
+
+ private void on_canvas_resize() {
+ draw_to_pixbuf = canvas.get_scaled_pixbuf().copy();
+ init_fp_pixel_cache(canvas.get_scaled_pixbuf());
+ }
+
+ private void on_hscale_reset(Gtk.GestureClick click, int n_press, double x, double y) {
+ Gtk.Scale source = (Gtk.Scale) click.get_widget();
+
+ if (click.get_current_button() == Gdk.BUTTON_PRIMARY
+ && has_only_key_modifier(click.get_current_event_state (), Gdk.ModifierType.CONTROL_MASK)) {
+ // Left Mouse Button and CTRL pressed
+ source.set_value(0);
+
+ var sequence = click.get_current_sequence();
+ click.set_sequence_state(sequence, Gtk.EventSequenceState.CLAIMED);
+ }
+ }
+
+ private void bind_canvas_handlers(PhotoCanvas canvas) {
+ canvas.resized_scaled_pixbuf.connect(on_canvas_resize);
+ }
+
+ private void unbind_canvas_handlers(PhotoCanvas canvas) {
+ canvas.resized_scaled_pixbuf.disconnect(on_canvas_resize);
+ }
+
+ private void bind_window_handlers() {
+ adjust_tool_window.ok_button.clicked.connect(on_ok);
+ adjust_tool_window.reset_button.clicked.connect(on_reset);
+ adjust_tool_window.cancel_button.clicked.connect(notify_cancel);
+ adjust_tool_window.exposure_slider.value_changed.connect(on_exposure_adjustment);
+ adjust_tool_window.contrast_slider.value_changed.connect(on_contrast_adjustment);
+ adjust_tool_window.saturation_slider.value_changed.connect(on_saturation_adjustment);
+ adjust_tool_window.tint_slider.value_changed.connect(on_tint_adjustment);
+ adjust_tool_window.temperature_slider.value_changed.connect(on_temperature_adjustment);
+ adjust_tool_window.shadows_slider.value_changed.connect(on_shadows_adjustment);
+ adjust_tool_window.highlights_slider.value_changed.connect(on_highlights_adjustment);
+ adjust_tool_window.histogram_manipulator.nub_position_changed.connect(on_histogram_constraint);
+
+ adjust_tool_window.saturation_click.pressed.connect(on_hscale_reset);
+ adjust_tool_window.exposure_click.pressed.connect(on_hscale_reset);
+ adjust_tool_window.contrast_click.pressed.connect(on_hscale_reset);
+ adjust_tool_window.tint_click.pressed.connect(on_hscale_reset);
+ adjust_tool_window.temperature_click.pressed.connect(on_hscale_reset);
+ adjust_tool_window.shadows_click.pressed.connect(on_hscale_reset);
+ adjust_tool_window.highlights_click.pressed.connect(on_hscale_reset);
+ }
+
+ private void unbind_window_handlers() {
+ adjust_tool_window.ok_button.clicked.disconnect(on_ok);
+ adjust_tool_window.reset_button.clicked.disconnect(on_reset);
+ adjust_tool_window.cancel_button.clicked.disconnect(notify_cancel);
+ adjust_tool_window.exposure_slider.value_changed.disconnect(on_exposure_adjustment);
+ adjust_tool_window.contrast_slider.value_changed.disconnect(on_contrast_adjustment);
+ adjust_tool_window.saturation_slider.value_changed.disconnect(on_saturation_adjustment);
+ adjust_tool_window.tint_slider.value_changed.disconnect(on_tint_adjustment);
+ adjust_tool_window.temperature_slider.value_changed.disconnect(on_temperature_adjustment);
+ adjust_tool_window.shadows_slider.value_changed.disconnect(on_shadows_adjustment);
+ adjust_tool_window.highlights_slider.value_changed.disconnect(on_highlights_adjustment);
+ adjust_tool_window.histogram_manipulator.nub_position_changed.disconnect(on_histogram_constraint);
+
+ #if 0
+ adjust_tool_window.saturation_slider.button_press_event.disconnect(on_hscale_reset);
+ adjust_tool_window.exposure_slider.button_press_event.disconnect(on_hscale_reset);
+ adjust_tool_window.contrast_slider.button_press_event.disconnect(on_hscale_reset);
+ adjust_tool_window.tint_slider.button_press_event.disconnect(on_hscale_reset);
+ adjust_tool_window.temperature_slider.button_press_event.disconnect(on_hscale_reset);
+ adjust_tool_window.shadows_slider.button_press_event.disconnect(on_hscale_reset);
+ adjust_tool_window.highlights_slider.button_press_event.disconnect(on_hscale_reset);
+ #endif
+ }
+
+ public bool enhance() {
+ AdjustEnhanceCommand command = new AdjustEnhanceCommand(this, canvas.get_photo());
+ AppWindow.get_command_manager().execute(command);
+
+ return true;
+ }
+
+ private void on_photos_altered(Gee.Map<DataObject, Alteration> map) {
+ if (!map.has_key(canvas.get_photo()))
+ return;
+
+ PixelTransformationBundle adjustments = canvas.get_photo().get_color_adjustments();
+ set_adjustments(adjustments);
+ }
+
+ private void set_adjustments(PixelTransformationBundle new_adjustments) {
+ unbind_window_handlers();
+
+ update_transformations(new_adjustments);
+
+ foreach (PixelTransformation adjustment in new_adjustments.get_transformations())
+ update_slider(adjustment);
+
+ bind_window_handlers();
+ canvas.repaint();
+ }
+
+ // Note that window handlers should be unbound (unbind_window_handlers) prior to calling this
+ // if the caller doesn't want the widget's signals to fire with the change.
+ private void update_slider(PixelTransformation transformation) {
+ switch (transformation.get_transformation_type()) {
+ case PixelTransformationType.TONE_EXPANSION:
+ ExpansionTransformation expansion = (ExpansionTransformation) transformation;
+
+ if (!disable_histogram_refresh) {
+ adjust_tool_window.histogram_manipulator.set_left_nub_position(
+ expansion.get_black_point());
+ adjust_tool_window.histogram_manipulator.set_right_nub_position(
+ expansion.get_white_point());
+ }
+ break;
+
+ case PixelTransformationType.SHADOWS:
+ adjust_tool_window.shadows_slider.set_value(
+ ((ShadowDetailTransformation) transformation).get_parameter());
+ break;
+
+ case PixelTransformationType.CONTRAST:
+ adjust_tool_window.contrast_slider.set_value(
+ ((ContrastTransformation) transformation).get_parameter());
+ break;
+
+ case PixelTransformationType.HIGHLIGHTS:
+ adjust_tool_window.highlights_slider.set_value(
+ ((HighlightDetailTransformation) transformation).get_parameter());
+ break;
+
+ case PixelTransformationType.EXPOSURE:
+ adjust_tool_window.exposure_slider.set_value(
+ ((ExposureTransformation) transformation).get_parameter());
+ break;
+
+ case PixelTransformationType.SATURATION:
+ adjust_tool_window.saturation_slider.set_value(
+ ((SaturationTransformation) transformation).get_parameter());
+ break;
+
+ case PixelTransformationType.TINT:
+ adjust_tool_window.tint_slider.set_value(
+ ((TintTransformation) transformation).get_parameter());
+ break;
+
+ case PixelTransformationType.TEMPERATURE:
+ adjust_tool_window.temperature_slider.set_value(
+ ((TemperatureTransformation) transformation).get_parameter());
+ break;
+
+ default:
+ error("Unknown adjustment: %d", (int) transformation.get_transformation_type());
+ }
+ }
+
+ private void init_fp_pixel_cache(Gdk.Pixbuf source) {
+ int source_width = source.get_width();
+ int source_height = source.get_height();
+ int source_num_channels = source.get_n_channels();
+ int source_rowstride = source.get_rowstride();
+ unowned uchar[] source_pixels = source.get_pixels();
+
+ fp_pixel_cache = new float[3 * source_width * source_height];
+ int cache_pixel_index = 0;
+
+ for (int j = 0; j < source_height; j++) {
+ int row_start_index = j * source_rowstride;
+ int row_end_index = row_start_index + (source_width * source_num_channels);
+ for (int i = row_start_index; i < row_end_index; i += source_num_channels) {
+ fp_pixel_cache[cache_pixel_index++] = rgb_lookup_table[source_pixels[i]];
+ fp_pixel_cache[cache_pixel_index++] = rgb_lookup_table[source_pixels[i + 1]];
+ fp_pixel_cache[cache_pixel_index++] = rgb_lookup_table[source_pixels[i + 2]];
+ }
+ }
+ }
+
+ public override bool on_keypress(Gtk.EventControllerKey event, uint keyval, uint keycode,
Gdk.ModifierType modifiers) {
+ if ((Gdk.keyval_name(keyval) == "KP_Enter") ||
+ (Gdk.keyval_name(keyval) == "Enter") ||
+ (Gdk.keyval_name(keyval) == "Return")) {
+ on_ok();
+ return true;
+ }
+
+ return base.on_keypress(event, keyval, keycode, modifiers);
+ }
+}
diff --git a/src/editing_tools/CropTool.vala b/src/editing_tools/CropTool.vala
new file mode 100644
index 00000000..1d5b2c34
--- /dev/null
+++ b/src/editing_tools/CropTool.vala
@@ -0,0 +1,1249 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+public class EditingTools.CropTool : EditingTool {
+ private const double CROP_INIT_X_PCT = 0.15;
+ private const double CROP_INIT_Y_PCT = 0.15;
+
+ private const int CROP_MIN_SIZE = 8;
+
+ private const float CROP_EXTERIOR_SATURATION = 0.00f;
+ private const int CROP_EXTERIOR_RED_SHIFT = -32;
+ private const int CROP_EXTERIOR_GREEN_SHIFT = -32;
+ private const int CROP_EXTERIOR_BLUE_SHIFT = -32;
+ private const int CROP_EXTERIOR_ALPHA_SHIFT = 0;
+
+ private const float ANY_ASPECT_RATIO = -1.0f;
+ private const float SCREEN_ASPECT_RATIO = -2.0f;
+ private const float ORIGINAL_ASPECT_RATIO = -3.0f;
+ private const float CUSTOM_ASPECT_RATIO = -4.0f;
+ private const float COMPUTE_FROM_BASIS = -5.0f;
+ private const float SEPARATOR = -6.0f;
+ private const float MIN_ASPECT_RATIO = 1.0f / 64.0f;
+ private const float MAX_ASPECT_RATIO = 64.0f;
+
+ private class ConstraintDescription {
+ public string name;
+ public int basis_width;
+ public int basis_height;
+ public bool is_pivotable;
+ public float aspect_ratio;
+
+ public ConstraintDescription(string new_name, int new_basis_width, int new_basis_height,
+ bool new_pivotable, float new_aspect_ratio = COMPUTE_FROM_BASIS) {
+ name = new_name;
+ basis_width = new_basis_width;
+ basis_height = new_basis_height;
+ if (new_aspect_ratio == COMPUTE_FROM_BASIS)
+ aspect_ratio = ((float) basis_width) / ((float) basis_height);
+ else
+ aspect_ratio = new_aspect_ratio;
+ is_pivotable = new_pivotable;
+ }
+
+ public bool is_separator() {
+ return !is_pivotable && aspect_ratio == SEPARATOR;
+ }
+ }
+
+ private enum ReticleOrientation {
+ LANDSCAPE,
+ PORTRAIT;
+
+ public ReticleOrientation toggle() {
+ return (this == ReticleOrientation.LANDSCAPE) ? ReticleOrientation.PORTRAIT :
+ ReticleOrientation.LANDSCAPE;
+ }
+ }
+
+ private enum ConstraintMode {
+ NORMAL,
+ CUSTOM
+ }
+
+ private class CropToolWindow : EditingToolWindow {
+ private const int CONTROL_SPACING = 8;
+
+ public Gtk.Button ok_button = new Gtk.Button.with_label(Resources.CROP_LABEL);
+ public Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
+ public Gtk.ComboBox constraint_combo;
+ public Gtk.Button pivot_reticle_button = new Gtk.Button();
+ public Gtk.Entry custom_width_entry = new Gtk.Entry();
+ public Gtk.Entry custom_height_entry = new Gtk.Entry();
+ public Gtk.Label custom_mulsign_label = new Gtk.Label.with_mnemonic("x");
+ public Gtk.Entry most_recently_edited = null;
+ public Gtk.Box response_layout = null;
+ public Gtk.Box layout = null;
+ public int normal_width = -1;
+ public int normal_height = -1;
+
+ public CropToolWindow(Gtk.Window container) {
+ base(container);
+
+ cancel_button.set_tooltip_text(_("Return to current photo dimensions"));
+ //cancel_button.set_image_position(Gtk.PositionType.LEFT);
+
+ ok_button.set_tooltip_text(_("Set the crop for this photo"));
+ //ok_button.set_image_position(Gtk.PositionType.LEFT);
+
+ constraint_combo = new Gtk.ComboBox();
+ Gtk.CellRendererText combo_text_renderer = new Gtk.CellRendererText();
+ constraint_combo.pack_start(combo_text_renderer, true);
+ constraint_combo.add_attribute(combo_text_renderer, "text", 0);
+ constraint_combo.set_row_separator_func(constraint_combo_separator_func);
+ constraint_combo.set_active(0);
+
+ pivot_reticle_button.set_icon_name ("crop-pivot-reticle-symbolic");
+ pivot_reticle_button.set_tooltip_text(_("Pivot the crop rectangle between portrait and landscape
orientations"));
+
+ custom_width_entry.set_width_chars(4);
+ custom_width_entry.editable = true;
+ custom_height_entry.set_width_chars(4);
+ custom_height_entry.editable = true;
+
+ response_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
+ response_layout.homogeneous = true;
+ response_layout.append(cancel_button);
+ response_layout.append(ok_button);
+
+ layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
+ layout.append(constraint_combo);
+ layout.append(pivot_reticle_button);
+ layout.append(response_layout);
+
+ add(layout);
+ }
+
+ private static bool constraint_combo_separator_func(Gtk.TreeModel model, Gtk.TreeIter iter) {
+ Value val;
+ model.get_value(iter, 0, out val);
+
+ return (val.dup_string() == "-");
+ }
+ }
+
+ private CropToolWindow crop_tool_window = null;
+ private string current_cursor_type = "normal";
+ 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 Cairo.Context text_ctx = null;
+
+ // This is where we draw our crop tool
+ private Cairo.Surface crop_surface = null;
+
+ // these are kept in absolute coordinates, not relative to photo's position on canvas
+ private Box scaled_crop;
+ private int last_grab_x = -1;
+ private int last_grab_y = -1;
+
+ private ConstraintDescription[] constraints = create_constraints();
+ private Gtk.ListStore constraint_list = create_constraint_list(create_constraints());
+ private ReticleOrientation reticle_orientation = ReticleOrientation.LANDSCAPE;
+ private ConstraintMode constraint_mode = ConstraintMode.NORMAL;
+ private bool entry_insert_in_progress = false;
+ private float custom_aspect_ratio = 1.0f;
+ private int custom_width = -1;
+ private int custom_height = -1;
+ private int custom_init_width = -1;
+ private int custom_init_height = -1;
+ private float pre_aspect_ratio = ANY_ASPECT_RATIO;
+
+ private CropTool() {
+ base("CropTool");
+ }
+
+ public static CropTool factory() {
+ return new CropTool();
+ }
+
+ public static bool is_available(Photo photo, Scaling scaling) {
+ Dimensions dim = scaling.get_scaled_dimensions(photo.get_original_dimensions());
+
+ return dim.width > CROP_MIN_SIZE && dim.height > CROP_MIN_SIZE;
+ }
+
+ private static ConstraintDescription[] create_constraints() {
+ ConstraintDescription[] result = new ConstraintDescription[0];
+
+ result += new ConstraintDescription(_("Unconstrained"), 0, 0, false, ANY_ASPECT_RATIO);
+ result += new ConstraintDescription(_("Square"), 1, 1, false);
+ result += new ConstraintDescription(_("Screen"), 0, 0, true, SCREEN_ASPECT_RATIO);
+ result += new ConstraintDescription(_("Original Size"), 0, 0, true, ORIGINAL_ASPECT_RATIO);
+ result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
+ result += new ConstraintDescription(_("SD Video (4 ∶ 3)"), 4, 3, true);
+ result += new ConstraintDescription(_("HD Video (16 ∶ 9)"), 16, 9, true);
+ result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
+ result += new ConstraintDescription(_("Wallet (2 × 3 in.)"), 3, 2, true);
+ result += new ConstraintDescription(_("Notecard (3 × 5 in.)"), 5, 3, true);
+ result += new ConstraintDescription(_("4 × 6 in."), 6, 4, true);
+ result += new ConstraintDescription(_("5 × 7 in."), 7, 5, true);
+ result += new ConstraintDescription(_("8 × 10 in."), 10, 8, true);
+ result += new ConstraintDescription(_("Letter (8.5 × 11 in.)"), 85, 110, true);
+ result += new ConstraintDescription(_("11 × 14 in."), 14, 11, true);
+ result += new ConstraintDescription(_("Tabloid (11 × 17 in.)"), 17, 11, true);
+ result += new ConstraintDescription(_("16 × 20 in."), 20, 16, true);
+ result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
+ result += new ConstraintDescription(_("Metric Wallet (9 × 13 cm)"), 13, 9, true);
+ result += new ConstraintDescription(_("Postcard (10 × 15 cm)"), 15, 10, true);
+ result += new ConstraintDescription(_("13 × 18 cm"), 18, 13, true);
+ result += new ConstraintDescription(_("18 × 24 cm"), 24, 18, true);
+ result += new ConstraintDescription(_("A4 (210 × 297 mm)"), 210, 297, true);
+ result += new ConstraintDescription(_("20 × 30 cm"), 30, 20, true);
+ result += new ConstraintDescription(_("24 × 40 cm"), 40, 24, true);
+ result += new ConstraintDescription(_("30 × 40 cm"), 40, 30, true);
+ result += new ConstraintDescription(_("A3 (297 × 420 mm)"), 420, 297, true);
+ result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
+ result += new ConstraintDescription(_("Custom"), 0, 0, true, CUSTOM_ASPECT_RATIO);
+
+ return result;
+ }
+
+ private static Gtk.ListStore create_constraint_list(ConstraintDescription[] constraint_data) {
+ Gtk.ListStore result = new Gtk.ListStore(1, typeof(string), typeof(string));
+
+ Gtk.TreeIter iter;
+ foreach (ConstraintDescription constraint in constraint_data) {
+ result.append(out iter);
+ result.set_value(iter, 0, constraint.name);
+ }
+
+ return result;
+ }
+
+ private void update_pivot_button_state() {
+ crop_tool_window.pivot_reticle_button.set_sensitive(
+ get_selected_constraint().is_pivotable);
+ }
+
+ private ConstraintDescription get_selected_constraint() {
+ ConstraintDescription result = constraints[crop_tool_window.constraint_combo.get_active()];
+
+ if (result.aspect_ratio == ORIGINAL_ASPECT_RATIO) {
+ result.basis_width = canvas.get_scaled_pixbuf_position().width;
+ result.basis_height = canvas.get_scaled_pixbuf_position().height;
+ } else if (result.aspect_ratio == SCREEN_ASPECT_RATIO) {
+ var dim = Scaling.get_screen_dimensions(AppWindow.get_instance());
+ result.basis_width = dim.width;
+ result.basis_height = dim.height;
+ }
+
+ return result;
+ }
+
+ #if 0
+ private bool on_width_entry_focus_out(Gdk.EventFocus event) {
+ crop_tool_window.most_recently_edited = crop_tool_window.custom_width_entry;
+ return on_custom_entry_focus_out(event);
+ }
+
+ private bool on_height_entry_focus_out(Gdk.EventFocus event) {
+ crop_tool_window.most_recently_edited = crop_tool_window.custom_height_entry;
+ return on_custom_entry_focus_out(event);
+ }
+
+ private bool on_custom_entry_focus_out(Gdk.EventFocus event) {
+ int width = int.parse(crop_tool_window.custom_width_entry.text);
+ int height = int.parse(crop_tool_window.custom_height_entry.text);
+
+ if(width < 1) {
+ width = 1;
+ crop_tool_window.custom_width_entry.set_text("%d".printf(width));
+ }
+
+ if(height < 1) {
+ height = 1;
+ crop_tool_window.custom_height_entry.set_text("%d".printf(height));
+ }
+
+ if ((width == custom_width) && (height == custom_height))
+ return false;
+
+ custom_aspect_ratio = ((float) width) / ((float) height);
+
+ if (custom_aspect_ratio < MIN_ASPECT_RATIO) {
+ if (crop_tool_window.most_recently_edited == crop_tool_window.custom_height_entry) {
+ height = (int) (width / MIN_ASPECT_RATIO);
+ crop_tool_window.custom_height_entry.set_text("%d".printf(height));
+ } else {
+ width = (int) (height * MIN_ASPECT_RATIO);
+ crop_tool_window.custom_width_entry.set_text("%d".printf(width));
+ }
+ } else if (custom_aspect_ratio > MAX_ASPECT_RATIO) {
+ if (crop_tool_window.most_recently_edited == crop_tool_window.custom_height_entry) {
+ height = (int) (width / MAX_ASPECT_RATIO);
+ crop_tool_window.custom_height_entry.set_text("%d".printf(height));
+ } else {
+ width = (int) (height * MAX_ASPECT_RATIO);
+ crop_tool_window.custom_width_entry.set_text("%d".printf(width));
+ }
+ }
+
+ custom_aspect_ratio = ((float) width) / ((float) height);
+
+ Box new_crop = constrain_crop(scaled_crop);
+
+ crop_resized(new_crop);
+ scaled_crop = new_crop;
+ canvas.invalidate_area(new_crop);
+ canvas.repaint();
+
+ custom_width = width;
+ custom_height = height;
+
+ return false;
+ }
+ #endif
+
+ private void on_width_insert_text(string text, int length, ref int position) {
+ on_entry_insert_text(crop_tool_window.custom_width_entry, text, length, ref position);
+ }
+
+ private void on_height_insert_text(string text, int length, ref int position) {
+ on_entry_insert_text(crop_tool_window.custom_height_entry, text, length, ref position);
+ }
+
+ private void on_entry_insert_text(Gtk.Entry sender, string text, int length, ref int position) {
+ if (entry_insert_in_progress)
+ return;
+
+ entry_insert_in_progress = true;
+
+ if (length == -1)
+ length = (int) text.length;
+
+ // only permit numeric text
+ string new_text = "";
+ for (int ctr = 0; ctr < length; ctr++) {
+ if (text[ctr].isdigit()) {
+ new_text += ((char) text[ctr]).to_string();
+ }
+ }
+
+ if (new_text.length > 0)
+ sender.insert_text(new_text, (int) new_text.length, ref position);
+
+ Signal.stop_emission_by_name(sender, "insert-text");
+
+ entry_insert_in_progress = false;
+ }
+
+ private float get_constraint_aspect_ratio() {
+ var result = get_selected_constraint().aspect_ratio;
+
+ if (result == ORIGINAL_ASPECT_RATIO) {
+ result = ((float) canvas.get_scaled_pixbuf_position().width) /
+ ((float) canvas.get_scaled_pixbuf_position().height);
+ } else if (result == SCREEN_ASPECT_RATIO) {
+ var dim = Scaling.get_screen_dimensions(AppWindow.get_instance());
+ result = ((float) dim.width) / ((float) dim.height);
+ } else if (result == CUSTOM_ASPECT_RATIO) {
+ result = custom_aspect_ratio;
+ }
+ if (reticle_orientation == ReticleOrientation.PORTRAIT)
+ result = 1.0f / result;
+
+ return result;
+ }
+
+ private float get_constraint_aspect_ratio_for_constraint(ConstraintDescription constraint, Photo photo) {
+ float result = constraint.aspect_ratio;
+
+ if (result == ORIGINAL_ASPECT_RATIO) {
+ Dimensions orig_dim = photo.get_original_dimensions();
+ result = ((float) orig_dim.width) / ((float) orig_dim.height);
+ } else if (result == SCREEN_ASPECT_RATIO) {
+ var dim = Scaling.get_screen_dimensions(AppWindow.get_instance());
+ result = ((float) dim.width) / ((float) dim.height);
+ } else if (result == CUSTOM_ASPECT_RATIO) {
+ result = custom_aspect_ratio;
+ }
+ if (reticle_orientation == ReticleOrientation.PORTRAIT)
+ result = 1.0f / result;
+
+ return result;
+
+ }
+
+ private void constraint_changed() {
+ ConstraintDescription selected_constraint = get_selected_constraint();
+ if (selected_constraint.aspect_ratio == CUSTOM_ASPECT_RATIO) {
+ set_custom_constraint_mode();
+ } else {
+ set_normal_constraint_mode();
+
+ if (selected_constraint.aspect_ratio != ANY_ASPECT_RATIO) {
+ // user may have switched away from 'Custom' without
+ // accepting, so set these to default back to saved
+ // values.
+ custom_init_width = Config.Facade.get_instance().get_last_crop_width();
+ custom_init_height = Config.Facade.get_instance().get_last_crop_height();
+ custom_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
+ }
+ }
+
+ update_pivot_button_state();
+
+ if (!get_selected_constraint().is_pivotable)
+ reticle_orientation = ReticleOrientation.LANDSCAPE;
+
+ if (get_constraint_aspect_ratio() != pre_aspect_ratio) {
+ Box new_crop = constrain_crop(scaled_crop);
+
+ crop_resized(new_crop);
+ scaled_crop = new_crop;
+ canvas.invalidate_area(new_crop);
+ canvas.repaint();
+
+ pre_aspect_ratio = get_constraint_aspect_ratio();
+ }
+ }
+
+ private void set_custom_constraint_mode() {
+ if (constraint_mode == ConstraintMode.CUSTOM)
+ return;
+
+ if ((crop_tool_window.normal_width == -1) || (crop_tool_window.normal_height == -1)) {
+ crop_tool_window.normal_width = crop_tool_window.default_width;
+ crop_tool_window.normal_height = crop_tool_window.default_height;
+ }
+
+ crop_tool_window.layout.remove(crop_tool_window.constraint_combo);
+ crop_tool_window.layout.remove(crop_tool_window.pivot_reticle_button);
+ crop_tool_window.layout.remove(crop_tool_window.response_layout);
+
+ crop_tool_window.layout.append(crop_tool_window.constraint_combo);
+ crop_tool_window.layout.append(crop_tool_window.custom_width_entry);
+ crop_tool_window.layout.append(crop_tool_window.custom_mulsign_label);
+ crop_tool_window.layout.append(crop_tool_window.custom_height_entry);
+ crop_tool_window.layout.append(crop_tool_window.pivot_reticle_button);
+ crop_tool_window.layout.append(crop_tool_window.response_layout);
+
+ if (reticle_orientation == ReticleOrientation.LANDSCAPE) {
+ crop_tool_window.custom_width_entry.set_text("%d".printf(custom_init_width));
+ crop_tool_window.custom_height_entry.set_text("%d".printf(custom_init_height));
+ } else {
+ crop_tool_window.custom_width_entry.set_text("%d".printf(custom_init_height));
+ crop_tool_window.custom_height_entry.set_text("%d".printf(custom_init_width));
+ }
+ custom_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
+
+ crop_tool_window.show();
+
+ constraint_mode = ConstraintMode.CUSTOM;
+ }
+
+ private void set_normal_constraint_mode() {
+ if (constraint_mode == ConstraintMode.NORMAL)
+ return;
+
+ crop_tool_window.layout.remove(crop_tool_window.constraint_combo);
+ crop_tool_window.layout.remove(crop_tool_window.custom_width_entry);
+ crop_tool_window.layout.remove(crop_tool_window.custom_mulsign_label);
+ crop_tool_window.layout.remove(crop_tool_window.custom_height_entry);
+ crop_tool_window.layout.remove(crop_tool_window.pivot_reticle_button);
+ crop_tool_window.layout.remove(crop_tool_window.response_layout);
+
+ crop_tool_window.layout.append(crop_tool_window.constraint_combo);
+ crop_tool_window.layout.append(crop_tool_window.pivot_reticle_button);
+ crop_tool_window.layout.append(crop_tool_window.response_layout);
+
+ crop_tool_window.set_default_size(crop_tool_window.normal_width,
+ crop_tool_window.normal_height);
+
+ crop_tool_window.show();
+
+ constraint_mode = ConstraintMode.NORMAL;
+ }
+
+ private Box constrain_crop(Box crop) {
+ float user_aspect_ratio = get_constraint_aspect_ratio();
+ if (user_aspect_ratio == ANY_ASPECT_RATIO)
+ return crop;
+
+ // PHASE 1: Scale to the desired aspect ratio, preserving area and center.
+ float old_area = (float) (crop.get_width() * crop.get_height());
+ crop.adjust_height((int) Math.sqrt(old_area / user_aspect_ratio));
+ crop.adjust_width((int) Math.sqrt(old_area * user_aspect_ratio));
+
+ // PHASE 2: Crop to the image boundary.
+ Dimensions image_size = get_photo_dimensions();
+ double angle;
+ canvas.get_photo().get_straighten(out angle);
+ crop = clamp_inside_rotated_image(crop, image_size.width, image_size.height, angle, false);
+
+ // PHASE 3: Crop down to the aspect ratio if necessary.
+ if (crop.get_width() >= crop.get_height() * user_aspect_ratio) // possibly too wide
+ crop.adjust_width((int) (crop.get_height() * user_aspect_ratio));
+ else // possibly too tall
+ crop.adjust_height((int) (crop.get_width() / user_aspect_ratio));
+
+ return crop;
+ }
+
+ private ConstraintDescription? get_last_constraint(out int index) {
+ index = Config.Facade.get_instance().get_last_crop_menu_choice();
+
+ return (index < constraints.length) ? constraints[index] : null;
+ }
+
+ public override void activate(PhotoCanvas canvas) {
+ bind_canvas_handlers(canvas);
+
+ prepare_ctx(canvas.get_default_ctx(), canvas.get_surface_dim());
+
+ if (crop_surface != null)
+ crop_surface = null;
+
+ crop_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
+ canvas.get_scaled_pixbuf_position().width,
+ canvas.get_scaled_pixbuf_position().height);
+
+ Cairo.Context ctx = new Cairo.Context(crop_surface);
+ ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
+ ctx.paint();
+
+ // create the crop tool window, where the user can apply or cancel the crop
+ crop_tool_window = new CropToolWindow(canvas.get_container());
+
+ // set up the constraint combo box
+ crop_tool_window.constraint_combo.set_model(constraint_list);
+ if(!canvas.get_photo().has_crop()) {
+ int index;
+ ConstraintDescription? desc = get_last_constraint(out index);
+ if (desc != null && !desc.is_separator())
+ crop_tool_window.constraint_combo.set_active(index);
+ }
+ else {
+ // get aspect ratio of current photo
+ Photo photo = canvas.get_photo();
+ Dimensions cropped_dim = photo.get_dimensions();
+ float ratio = (float) cropped_dim.width / (float) cropped_dim.height;
+ for (int index = 1; index < constraints.length; index++) {
+ if (Math.fabs(ratio - get_constraint_aspect_ratio_for_constraint(constraints[index], photo))
< 0.005)
+ crop_tool_window.constraint_combo.set_active(index);
+ }
+ }
+
+ // set up the pivot reticle button
+ update_pivot_button_state();
+ reticle_orientation = ReticleOrientation.LANDSCAPE;
+
+ bind_window_handlers();
+
+ // obtain crop dimensions and paint against the uncropped photo
+ Dimensions uncropped_dim = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
+
+ Box crop;
+ if (!canvas.get_photo().get_crop(out crop)) {
+ int xofs = (int) (uncropped_dim.width * CROP_INIT_X_PCT);
+ int yofs = (int) (uncropped_dim.height * CROP_INIT_Y_PCT);
+
+ // initialize the actual crop in absolute coordinates, not relative
+ // to the photo's position on the canvas
+ crop = Box(xofs, yofs, uncropped_dim.width - xofs, uncropped_dim.height - yofs);
+ }
+
+ // scale the crop to the scaled photo's size ... the scaled crop is maintained in
+ // coordinates not relative to photo's position on canvas
+ scaled_crop = crop.get_scaled_similar(uncropped_dim,
+ Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()));
+
+ // get the custom width and height from the saved config and
+ // set up the initial custom values with it.
+ custom_width = Config.Facade.get_instance().get_last_crop_width();
+ custom_height = Config.Facade.get_instance().get_last_crop_height();
+ custom_init_width = custom_width;
+ custom_init_height = custom_height;
+ pre_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
+
+ constraint_mode = ConstraintMode.NORMAL;
+
+ base.activate(canvas);
+
+ crop_tool_window.show();
+
+ // was 'custom' the most-recently-chosen menu item?
+ if(!canvas.get_photo().has_crop()) {
+ ConstraintDescription? desc = get_last_constraint(null);
+ if (desc != null && !desc.is_separator() && desc.aspect_ratio == CUSTOM_ASPECT_RATIO)
+ set_custom_constraint_mode();
+ }
+
+ // since we no longer just run with the default, but rather
+ // a saved value, we'll behave as if the saved constraint has
+ // just been changed to so that everything gets updated and
+ // the canvas stays in sync.
+ Box new_crop = constrain_crop(scaled_crop);
+
+ crop_resized(new_crop);
+ scaled_crop = new_crop;
+ canvas.invalidate_area(new_crop);
+ canvas.repaint();
+
+ pre_aspect_ratio = get_constraint_aspect_ratio();
+ }
+
+ private void bind_canvas_handlers(PhotoCanvas canvas) {
+ canvas.new_surface.connect(prepare_ctx);
+ canvas.resized_scaled_pixbuf.connect(on_resized_pixbuf);
+ }
+
+ private void unbind_canvas_handlers(PhotoCanvas canvas) {
+ canvas.new_surface.disconnect(prepare_ctx);
+ canvas.resized_scaled_pixbuf.disconnect(on_resized_pixbuf);
+ }
+
+ private void bind_window_handlers() {
+ crop_tool_window.ok_button.clicked.connect(on_crop_ok);
+ crop_tool_window.cancel_button.clicked.connect(notify_cancel);
+ crop_tool_window.constraint_combo.changed.connect(constraint_changed);
+ crop_tool_window.pivot_reticle_button.clicked.connect(on_pivot_button_clicked);
+
+ // set up the custom width and height entry boxes
+ #if 0
+ crop_tool_window.custom_width_entry.focus_out_event.connect(on_width_entry_focus_out);
+ crop_tool_window.custom_height_entry.focus_out_event.connect(on_height_entry_focus_out);
+ #endif
+ crop_tool_window.custom_width_entry.insert_text.connect(on_width_insert_text);
+ crop_tool_window.custom_height_entry.insert_text.connect(on_height_insert_text);
+ }
+
+ private void unbind_window_handlers() {
+ crop_tool_window.ok_button.clicked.disconnect(on_crop_ok);
+ crop_tool_window.cancel_button.clicked.disconnect(notify_cancel);
+ crop_tool_window.constraint_combo.changed.disconnect(constraint_changed);
+ crop_tool_window.pivot_reticle_button.clicked.disconnect(on_pivot_button_clicked);
+
+ // set up the custom width and height entry boxes
+ #if 0
+ crop_tool_window.custom_width_entry.focus_out_event.disconnect(on_width_entry_focus_out);
+ crop_tool_window.custom_height_entry.focus_out_event.disconnect(on_height_entry_focus_out);
+ #endif
+ crop_tool_window.custom_width_entry.insert_text.disconnect(on_width_insert_text);
+ }
+
+ public override bool on_keypress(Gtk.EventControllerKey event, uint keyval, uint keycode,
Gdk.ModifierType modifiers) {
+ if ((Gdk.keyval_name(keyval) == "KP_Enter") ||
+ (Gdk.keyval_name(keyval) == "Enter") ||
+ (Gdk.keyval_name(keyval) == "Return")) {
+ on_crop_ok();
+ return true;
+ }
+
+ return base.on_keypress(event, keyval, keycode, modifiers);
+ }
+
+ private void on_pivot_button_clicked() {
+ if (get_selected_constraint().aspect_ratio == CUSTOM_ASPECT_RATIO) {
+ string width_text = crop_tool_window.custom_width_entry.get_text();
+ string height_text = crop_tool_window.custom_height_entry.get_text();
+ crop_tool_window.custom_width_entry.set_text(height_text);
+ crop_tool_window.custom_height_entry.set_text(width_text);
+
+ int temp = custom_width;
+ custom_width = custom_height;
+ custom_height = temp;
+ }
+ reticle_orientation = reticle_orientation.toggle();
+ constraint_changed();
+ }
+
+ public override void deactivate() {
+ if (canvas != null)
+ unbind_canvas_handlers(canvas);
+
+ if (crop_tool_window != null) {
+ unbind_window_handlers();
+ crop_tool_window.hide();
+ crop_tool_window.destroy();
+ crop_tool_window = null;
+ }
+
+ // make sure the cursor isn't set to a modify indicator
+ if (canvas != null) {
+ canvas.set_cursor ("default");
+ }
+
+ crop_surface = null;
+
+ base.deactivate();
+ }
+
+ public override EditingToolWindow? get_tool_window() {
+ return crop_tool_window;
+ }
+
+ public override Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
+ out Dimensions max_dim) throws Error {
+ max_dim = photo.get_dimensions(Photo.Exception.CROP);
+
+ return photo.get_pixbuf_with_options(scaling, Photo.Exception.CROP);
+ }
+
+ private 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_white_ctx, "#FFF");
+ wide_white_ctx.set_line_width(1);
+
+ thin_white_ctx = new Cairo.Context(ctx.get_target());
+ set_source_color_from_string(thin_white_ctx, "#FFF");
+ thin_white_ctx.set_line_width(0.5);
+
+ text_ctx = new Cairo.Context(ctx.get_target());
+ text_ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ }
+
+ private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
+ Dimensions new_dim = Dimensions.for_pixbuf(scaled);
+ Dimensions uncropped_dim = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
+
+ // rescale to full crop
+ Box crop = scaled_crop.get_scaled_similar(old_dim, uncropped_dim);
+
+ // rescale back to new size
+ scaled_crop = crop.get_scaled_similar(uncropped_dim, new_dim);
+ if (crop_surface != null)
+ crop_surface = null;
+
+ crop_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, scaled.width, scaled.height);
+ Cairo.Context ctx = new Cairo.Context(crop_surface);
+ ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
+ ctx.paint();
+
+ }
+
+ public override void on_left_click(int x, int y) {
+ Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
+
+ // scaled_crop is not maintained relative to photo's position on canvas
+ Box offset_scaled_crop = scaled_crop.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_crop.approx_location(x, y);
+ last_grab_x = x -= scaled_pixbuf_pos.x;
+ last_grab_y = y -= scaled_pixbuf_pos.y;
+
+ // repaint because the crop changes on a mouse down
+ canvas.repaint();
+ }
+
+ public override void on_left_released(int x, int y) {
+ // nothing to do if released outside of the crop 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);
+
+ // repaint because crop changes when released
+ canvas.repaint();
+ }
+
+ public override void on_motion(int x, int y, Gdk.ModifierType mask) {
+ // only deal with manipulating the crop tool 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);
+ canvas.repaint();
+ }
+
+ public override void paint(Cairo.Context default_ctx) {
+ // fill region behind the crop 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(crop_surface);
+ ctx.set_operator(Cairo.Operator.SOURCE);
+ ctx.set_source_rgba(0.0, 0.0, 0.0, 0.5);
+ ctx.paint();
+
+ // paint exposed (cropped) part of pixbuf minus crop border
+ ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0);
+ ctx.rectangle(scaled_crop.left, scaled_crop.top, scaled_crop.get_width(),
+ scaled_crop.get_height());
+ ctx.fill();
+ canvas.paint_surface(crop_surface, true);
+
+ // paint crop tool last
+ paint_crop_tool(scaled_crop);
+ }
+
+ private void on_crop_ok() {
+ // user's clicked OK, save the combobox choice and width/height.
+ // safe to do, even if not in 'custom' mode - the previous values
+ // will just get saved again.
+ Config.Facade.get_instance().set_last_crop_menu_choice(
+ crop_tool_window.constraint_combo.get_active());
+ Config.Facade.get_instance().set_last_crop_width(custom_width);
+ Config.Facade.get_instance().set_last_crop_height(custom_height);
+
+ // scale screen-coordinate crop to photo's coordinate system
+ Box crop = scaled_crop.get_scaled_similar(
+ Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()),
+ canvas.get_photo().get_dimensions(Photo.Exception.CROP));
+
+ // crop the current pixbuf and offer it to the editing host
+ Gdk.Pixbuf cropped = new Gdk.Pixbuf.subpixbuf(canvas.get_scaled_pixbuf(), scaled_crop.left,
+ scaled_crop.top, scaled_crop.get_width(), scaled_crop.get_height());
+
+ // signal host; we have a cropped image, but it will be scaled upward, and so a better one
+ // should be fetched
+ applied(new CropCommand(canvas.get_photo(), crop, Resources.CROP_LABEL,
+ Resources.CROP_TOOLTIP), cropped, crop.get_dimensions(), true);
+ }
+
+ private void update_cursor(int x, int y) {
+ // scaled_crop is not maintained relative to photo's position on canvas
+ Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
+ Box offset_scaled_crop = scaled_crop.get_offset(scaled_pos.x, scaled_pos.y);
+
+ string cursor_type = "default";
+ switch (offset_scaled_crop.approx_location(x, y)) {
+ case BoxLocation.LEFT_SIDE:
+ cursor_type = "w-resize";
+ break;
+
+ case BoxLocation.TOP_SIDE:
+ cursor_type = "n-resize";
+ break;
+
+ case BoxLocation.RIGHT_SIDE:
+ cursor_type = "e-resize";
+ break;
+
+ case BoxLocation.BOTTOM_SIDE:
+ cursor_type = "s-resize";
+ break;
+
+ case BoxLocation.TOP_LEFT:
+ cursor_type = "nw-resize";
+ break;
+
+ case BoxLocation.BOTTOM_LEFT:
+ cursor_type = "sw-resize";
+ break;
+
+ case BoxLocation.TOP_RIGHT:
+ cursor_type = "ne-resize";
+ break;
+
+ case BoxLocation.BOTTOM_RIGHT:
+ cursor_type = "ne-resize";
+ break;
+
+ case BoxLocation.INSIDE:
+ cursor_type = "move";
+ break;
+
+ default:
+ // use Gdk.CursorType.LEFT_PTR
+ break;
+ }
+
+ if (cursor_type != current_cursor_type) {
+ canvas.set_cursor(cursor_type);
+ current_cursor_type = cursor_type;
+ }
+ }
+
+ private int eval_radial_line(double center_x, double center_y, double bounds_x,
+ double bounds_y, double user_x) {
+ double decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
+ double decision_intercept = bounds_y - (decision_slope * bounds_x);
+
+ return (int) (decision_slope * user_x + decision_intercept);
+ }
+
+ // Return the dimensions of the uncropped source photo scaled to canvas coordinates.
+ private Dimensions get_photo_dimensions() {
+ Dimensions photo_dims = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
+ Dimensions surface_dims = canvas.get_surface_dim();
+ double scale_factor = double.min((double) surface_dims.width / photo_dims.width,
+ (double) surface_dims.height / photo_dims.height);
+ scale_factor = double.min(scale_factor, 1.0);
+
+ photo_dims = canvas.get_photo().get_dimensions(
+ Photo.Exception.CROP | Photo.Exception.STRAIGHTEN);
+
+ return { (int) (photo_dims.width * scale_factor),
+ (int) (photo_dims.height * scale_factor) };
+ }
+
+ private bool on_canvas_manipulation(int x, int y) {
+ Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
+
+ // scaled_crop 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 = scaled_crop.left;
+ int top = scaled_crop.top;
+ int right = scaled_crop.right;
+ int bottom = scaled_crop.bottom;
+
+ // get extra geometric information needed to enforce constraints
+ int center_x = (left + right) / 2;
+ int center_y = (top + bottom) / 2;
+
+ switch (in_manipulation) {
+ case BoxLocation.LEFT_SIDE:
+ left = x;
+ if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
+ float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
+ bottom = top + ((int) new_height);
+ }
+ break;
+
+ case BoxLocation.TOP_SIDE:
+ top = y;
+ if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
+ float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
+ right = left + ((int) new_width);
+ }
+ break;
+
+ case BoxLocation.RIGHT_SIDE:
+ right = x;
+ if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
+ float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
+ bottom = top + ((int) new_height);
+ }
+ break;
+
+ case BoxLocation.BOTTOM_SIDE:
+ bottom = y;
+ if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
+ float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
+ right = left + ((int) new_width);
+ }
+ break;
+
+ case BoxLocation.TOP_LEFT:
+ if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
+ top = y;
+ left = x;
+ } else {
+ if (y < eval_radial_line(center_x, center_y, left, top, x)) {
+ top = y;
+ float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
+ left = right - ((int) new_width);
+ } else {
+ left = x;
+ float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
+ top = bottom - ((int) new_height);
+ }
+ }
+ break;
+
+ case BoxLocation.BOTTOM_LEFT:
+ if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
+ bottom = y;
+ left = x;
+ } else {
+ if (y < eval_radial_line(center_x, center_y, left, bottom, x)) {
+ left = x;
+ float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
+ bottom = top + ((int) new_height);
+ } else {
+ bottom = y;
+ float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
+ left = right - ((int) new_width);
+ }
+ }
+ break;
+
+ case BoxLocation.TOP_RIGHT:
+ if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
+ top = y;
+ right = x;
+ } else {
+ if (y < eval_radial_line(center_x, center_y, right, top, x)) {
+ top = y;
+ float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
+ right = left + ((int) new_width);
+ } else {
+ right = x;
+ float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
+ top = bottom - ((int) new_height);
+ }
+ }
+ break;
+
+ case BoxLocation.BOTTOM_RIGHT:
+ if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
+ bottom = y;
+ right = x;
+ } else {
+ if (y < eval_radial_line(center_x, center_y, right, bottom, x)) {
+ right = x;
+ float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
+ bottom = top + ((int) new_height);
+ } else {
+ bottom = y;
+ float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
+ right = left + ((int) new_width);
+ }
+ }
+ 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 crop 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 crop
+ 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
+ // crop reticle's edges stay within the photo bounds. This bounds check works
+ // differently in constrained versus unconstrained mode. In unconstrained mode,
+ // we need only to bounds clamp the one or two edge(s) that are actually out-of-bounds.
+ // In constrained mode however, we need to bounds clamp the entire box, because the
+ // positions of edges are all interdependent (so as to enforce the aspect ratio
+ // constraint).
+ int width = right - left + 1;
+ int height = bottom - top + 1;
+
+ Dimensions photo_dims = get_photo_dimensions();
+ double angle;
+ canvas.get_photo().get_straighten(out angle);
+
+ Box new_crop;
+ if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
+ 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 < CROP_MIN_SIZE)
+ left = right - CROP_MIN_SIZE;
+ break;
+
+ case BoxLocation.RIGHT_SIDE:
+ case BoxLocation.TOP_RIGHT:
+ case BoxLocation.BOTTOM_RIGHT:
+ if (width < CROP_MIN_SIZE)
+ right = left + CROP_MIN_SIZE;
+ break;
+
+ default:
+ break;
+ }
+
+ switch (in_manipulation) {
+ case BoxLocation.TOP_SIDE:
+ case BoxLocation.TOP_LEFT:
+ case BoxLocation.TOP_RIGHT:
+ if (height < CROP_MIN_SIZE)
+ top = bottom - CROP_MIN_SIZE;
+ break;
+
+ case BoxLocation.BOTTOM_SIDE:
+ case BoxLocation.BOTTOM_LEFT:
+ case BoxLocation.BOTTOM_RIGHT:
+ if (height < CROP_MIN_SIZE)
+ bottom = top + CROP_MIN_SIZE;
+ break;
+
+ default:
+ break;
+ }
+
+ // preliminary crop region has been chosen, now clamp it inside the
+ // image as needed.
+
+ new_crop = clamp_inside_rotated_image(
+ Box(left, top, right, bottom),
+ photo_dims.width, photo_dims.height, angle,
+ in_manipulation == BoxLocation.INSIDE);
+
+ } else {
+ // one of the constrained modes is active; revert instead of clamping so
+ // that aspect ratio stays intact
+
+ new_crop = Box(left, top, right, bottom);
+ Box adjusted = clamp_inside_rotated_image(new_crop,
+ photo_dims.width, photo_dims.height, angle,
+ in_manipulation == BoxLocation.INSIDE);
+
+ if (adjusted != new_crop || width < CROP_MIN_SIZE || height < CROP_MIN_SIZE) {
+ new_crop = scaled_crop; // revert crop move
+ }
+ }
+
+ if (in_manipulation != BoxLocation.INSIDE)
+ crop_resized(new_crop);
+ else
+ crop_moved(new_crop);
+
+ // load new values
+ scaled_crop = new_crop;
+
+ if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
+ custom_init_width = scaled_crop.get_width();
+ custom_init_height = scaled_crop.get_height();
+ custom_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
+ }
+
+ return false;
+ }
+
+ private void crop_resized(Box new_crop) {
+ if(scaled_crop.equals(new_crop)) {
+ // no change
+ return;
+ }
+
+ canvas.invalidate_area(scaled_crop);
+
+ Box horizontal;
+ bool horizontal_enlarged;
+ Box vertical;
+ bool vertical_enlarged;
+ BoxComplements complements = scaled_crop.resized_complements(new_crop, 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);
+
+ if (complements == BoxComplements.HORIZONTAL || complements == BoxComplements.BOTH)
+ set_area_alpha(horizontal, horizontal_enlarged ? 0.0 : 0.5);
+
+ if (complements == BoxComplements.VERTICAL || complements == BoxComplements.BOTH)
+ set_area_alpha(vertical, vertical_enlarged ? 0.0 : 0.5);
+
+ paint_crop_tool(new_crop);
+ canvas.invalidate_area(new_crop);
+ }
+
+ private void crop_moved(Box new_crop) {
+ if (scaled_crop.equals(new_crop)) {
+ // no change
+ return;
+ }
+
+ canvas.invalidate_area(scaled_crop);
+
+ set_area_alpha(scaled_crop, 0.5);
+ set_area_alpha(new_crop, 0.0);
+
+
+ // paint crop in new location
+ paint_crop_tool(new_crop);
+ canvas.invalidate_area(new_crop);
+ }
+
+ private void set_area_alpha(Box area, double alpha) {
+ Cairo.Context ctx = new Cairo.Context(crop_surface);
+ ctx.set_operator(Cairo.Operator.SOURCE);
+ ctx.set_source_rgba(0.0, 0.0, 0.0, alpha);
+ ctx.rectangle(area.left, area.top, area.get_width(), area.get_height());
+ ctx.fill();
+ canvas.paint_surface_area(crop_surface, area, true);
+ }
+
+ private void paint_crop_tool(Box crop) {
+ // paint rule-of-thirds lines and current dimensions if user is manipulating the crop
+ if (in_manipulation != BoxLocation.OUTSIDE) {
+ int one_third_x = crop.get_width() / 3;
+ int one_third_y = crop.get_height() / 3;
+
+ canvas.draw_horizontal_line(thin_white_ctx, crop.left, crop.top + one_third_y, crop.get_width());
+ canvas.draw_horizontal_line(thin_white_ctx, crop.left, crop.top + (one_third_y * 2),
crop.get_width());
+
+ canvas.draw_vertical_line(thin_white_ctx, crop.left + one_third_x, crop.top, crop.get_height());
+ canvas.draw_vertical_line(thin_white_ctx, crop.left + (one_third_x * 2), crop.top,
crop.get_height());
+
+ // current dimensions
+ // scale screen-coordinate crop to photo's coordinate system
+ Box adj_crop = scaled_crop.get_scaled_similar(
+ Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()),
+ canvas.get_photo().get_dimensions(Photo.Exception.CROP));
+ string text = adj_crop.get_width().to_string() + "x" + adj_crop.get_height().to_string();
+ int x = crop.left + crop.get_width() / 2;
+ int y = crop.top + crop.get_height() / 2;
+ canvas.draw_text(text_ctx, text, x, y);
+ }
+
+ // outer rectangle ... outer line in black, inner in white, corners fully black
+ canvas.draw_box(wide_black_ctx, crop);
+ canvas.draw_box(wide_white_ctx, crop.get_reduced(1));
+ canvas.draw_box(wide_white_ctx, crop.get_reduced(2));
+ }
+}
diff --git a/src/editing_tools/EditingTool.vala b/src/editing_tools/EditingTool.vala
new file mode 100644
index 00000000..083e9027
--- /dev/null
+++ b/src/editing_tools/EditingTool.vala
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+public abstract class EditingTool {
+ public PhotoCanvas canvas = null;
+
+ private EditingToolWindow tool_window = null;
+ private Gtk.EventControllerKey key_controller;
+ protected Cairo.Surface surface;
+ public string name;
+
+ [CCode (has_target=false)]
+ public delegate EditingTool Factory();
+
+ public signal void activated();
+
+ public signal void deactivated();
+
+ public signal void applied(Command? command, Gdk.Pixbuf? new_pixbuf, Dimensions new_max_dim,
+ bool needs_improvement);
+
+ public signal void cancelled();
+
+ public signal void aborted();
+
+
+ protected EditingTool(string name) {
+ this.name = name;
+ key_controller = new Gtk.EventControllerKey();
+ key_controller.key_pressed.connect(on_keypress);
+ }
+
+ // base.activate() should always be called by an overriding member to ensure the base class
+ // gets to set up and store the PhotoCanvas in the canvas member field. More importantly,
+ // the activated signal is called here, and should only be called once the tool is completely
+ // initialized.
+ public virtual void activate(PhotoCanvas canvas) {
+ // multiple activates are not tolerated
+ assert(this.canvas == null);
+ assert(tool_window == null);
+
+ this.canvas = canvas;
+
+ tool_window = get_tool_window();
+ if (tool_window != null)
+ ((Gtk.Widget) tool_window).add_controller(key_controller);
+
+ activated();
+ }
+
+ // Like activate(), this should always be called from an overriding subclass.
+ public virtual void deactivate() {
+ // multiple deactivates are tolerated
+ if (canvas == null && tool_window == null)
+ return;
+
+ canvas = null;
+
+ if (tool_window != null) {
+ ((Gtk.Widget) tool_window).remove_controller(key_controller);
+ tool_window = null;
+ }
+
+ deactivated();
+ }
+
+ public bool is_activated() {
+ return canvas != null;
+ }
+
+ public virtual EditingToolWindow? get_tool_window() {
+ return null;
+ }
+
+ // This allows the EditingTool to specify which pixbuf to display during the tool's
+ // operation. Returning null means the host should use the pixbuf associated with the current
+ // Photo. Note: This will be called before activate(), primarily to display the pixbuf before
+ // the tool is on the screen, and before paint_full() is hooked in. It also means the PhotoCanvas
+ // will have this pixbuf rather than one from the Photo class.
+ //
+ // If returns non-null, should also fill max_dim with the maximum dimensions of the original
+ // image, as the editing host may not always scale images up to fit the viewport.
+ //
+ // Note this this method doesn't need to be returning the "proper" pixbuf on-the-fly (i.e.
+ // a pixbuf with unsaved tool edits in it). That can be handled in the paint() virtual method.
+ public virtual Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
+ out Dimensions max_dim) throws Error {
+ max_dim = Dimensions();
+
+ return null;
+ }
+
+ public virtual void on_left_click(int x, int y) {
+ }
+
+ public virtual void on_left_released(int x, int y) {
+ }
+
+ public virtual void on_motion(int x, int y, Gdk.ModifierType mask) {
+ }
+
+ public virtual bool on_leave_notify_event(){
+ return false;
+ }
+
+ public virtual bool on_keypress(Gtk.EventControllerKey event, uint keyval, uint keycode,
Gdk.ModifierType modifiers) {
+ // check for an escape/abort first
+ if (Gdk.keyval_name(keyval) == "Escape") {
+ notify_cancel();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public virtual void paint(Cairo.Context ctx) {
+ }
+
+ // Helper function that fires the cancelled signal. (Can be connected to other signals.)
+ protected void notify_cancel() {
+ cancelled();
+ }
+}
diff --git a/src/editing_tools/EditingToolWindow.vala b/src/editing_tools/EditingToolWindow.vala
new file mode 100644
index 00000000..e11d4658
--- /dev/null
+++ b/src/editing_tools/EditingToolWindow.vala
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier:LGPL-2.1-or-later
+public abstract class EditingToolWindow : Gtk.Window {
+ private const int FRAME_BORDER = 6;
+
+ private Gtk.Frame layout_frame = new Gtk.Frame(null);
+ private bool user_moved = false;
+
+ protected EditingToolWindow(Gtk.Window container) {
+ set_decorated(false);
+ set_transient_for(container);
+
+ Gtk.Frame outer_frame = new Gtk.Frame(null);
+ outer_frame.set_child(layout_frame);
+ base.set_child(outer_frame);
+
+ // add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.KEY_PRESS_MASK);
+ focusable = true;
+ set_can_focus(true);
+ }
+
+ ~EditingToolWindow() {
+ }
+
+ public void add(Gtk.Widget widget) {
+ layout_frame.set_child(widget);
+ }
+
+ public bool has_user_moved() {
+ return user_moved;
+ }
+
+ public signal
+ #if 0
+ public override bool key_press_event(Gdk.EventKey event) {
+ if (base.key_press_event(event)) {
+ return true;
+ }
+ return AppWindow.get_instance().key_press_event(event);
+ }
+
+ public override bool button_press_event(Gdk.EventButton event) {
+ // LMB only
+ if (event.button != 1)
+ return (base.button_press_event != null) ? base.button_press_event(event) : true;
+
+ begin_move_drag((int) event.button, (int) event.x_root, (int) event.y_root, event.time);
+ user_moved = true;
+
+ return true;
+ }
+ #endif
+
+
+ public override void realize() {
+ // Force the use of gtk_widget_set_opacity; gtk_window_set_opacity is deprecated
+ ((Gtk.Widget) this).set_opacity(Resources.TRANSIENT_WINDOW_OPACITY);
+
+ base.realize();
+ }
+
+ private void suppress_warnings(string? log_domain, LogLevelFlags log_levels, string message) {
+ // do nothing.
+ }
+}
diff --git a/src/editing_tools/EditingTools.vala b/src/editing_tools/EditingTools.vala
index 9c6ee5e9..2dc119c2 100644
--- a/src/editing_tools/EditingTools.vala
+++ b/src/editing_tools/EditingTools.vala
@@ -25,2951 +25,4 @@ public void init() throws Error {
public void terminate() {
}
-public abstract class EditingToolWindow : Gtk.Window {
- private const int FRAME_BORDER = 6;
-
- private Gtk.Frame layout_frame = new Gtk.Frame(null);
- private bool user_moved = false;
-
- protected EditingToolWindow(Gtk.Window container) {
- set_decorated(false);
- set_transient_for(container);
-
- Gtk.Frame outer_frame = new Gtk.Frame(null);
- outer_frame.set_border_width(0);
- outer_frame.set_shadow_type(Gtk.ShadowType.OUT);
-
- layout_frame.set_border_width(FRAME_BORDER);
- layout_frame.set_shadow_type(Gtk.ShadowType.NONE);
-
- outer_frame.add(layout_frame);
- base.add(outer_frame);
-
- add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.KEY_PRESS_MASK);
- focus_on_map = true;
- set_accept_focus(true);
- set_can_focus(true);
-
- // Needed to prevent the (spurious) 'This event was synthesised outside of GDK'
- // warnings after a keypress.
- // TODO: Check if really still necessary
- Log.set_handler("Gdk", LogLevelFlags.LEVEL_WARNING, suppress_warnings);
- }
-
- ~EditingToolWindow() {
- Log.set_handler("Gdk", LogLevelFlags.LEVEL_WARNING, Log.default_handler);
- }
-
- public override void add(Gtk.Widget widget) {
- layout_frame.add(widget);
- }
-
- public bool has_user_moved() {
- return user_moved;
- }
-
- public override bool key_press_event(Gdk.EventKey event) {
- if (base.key_press_event(event)) {
- return true;
- }
- return AppWindow.get_instance().key_press_event(event);
- }
-
- public override bool button_press_event(Gdk.EventButton event) {
- // LMB only
- if (event.button != 1)
- return (base.button_press_event != null) ? base.button_press_event(event) : true;
-
- begin_move_drag((int) event.button, (int) event.x_root, (int) event.y_root, event.time);
- user_moved = true;
-
- return true;
- }
-
- public override void realize() {
- // Force the use of gtk_widget_set_opacity; gtk_window_set_opacity is deprecated
- ((Gtk.Widget) this).set_opacity(Resources.TRANSIENT_WINDOW_OPACITY);
-
- base.realize();
- }
-
- private void suppress_warnings(string? log_domain, LogLevelFlags log_levels, string message) {
- // do nothing.
- }
-}
-
-// The PhotoCanvas is an interface object between an EditingTool and its host. It provides objects
-// and primitives for an EditingTool to obtain information about the image, to draw on the host's
-// canvas, and to be signalled when the canvas and its pixbuf changes (is resized).
-public abstract class PhotoCanvas {
- private Gtk.Window container;
- private Gdk.Window drawing_window;
- private Photo photo;
- private Cairo.Context default_ctx;
- private Dimensions surface_dim;
- private Cairo.Surface scaled;
- private Gdk.Pixbuf scaled_pixbuf;
- private Gdk.Rectangle scaled_position;
-
- protected PhotoCanvas(Gtk.Window container, Gdk.Window drawing_window, Photo photo,
- Cairo.Context default_ctx, Dimensions surface_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position)
{
- this.container = container;
- this.drawing_window = drawing_window;
- this.photo = photo;
- this.default_ctx = default_ctx;
- this.surface_dim = surface_dim;
- this.scaled_position = scaled_position;
- this.scaled_pixbuf = scaled;
- this.scaled = pixbuf_to_surface(default_ctx, scaled, scaled_position);
- }
-
- public signal void new_surface(Cairo.Context ctx, Dimensions dim);
-
- public signal void resized_scaled_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled,
- Gdk.Rectangle scaled_position);
-
- public Gdk.Rectangle unscaled_to_raw_rect(Gdk.Rectangle rectangle) {
- return photo.unscaled_to_raw_rect(rectangle);
- }
-
- public Gdk.Point active_to_unscaled_point(Gdk.Point active_point) {
- Gdk.Rectangle scaled_position = get_scaled_pixbuf_position();
- Dimensions unscaled_dims = photo.get_dimensions();
-
- double scale_factor_x = ((double) unscaled_dims.width) /
- ((double) scaled_position.width);
- double scale_factor_y = ((double) unscaled_dims.height) /
- ((double) scaled_position.height);
-
- Gdk.Point result = {0};
- result.x = (int)(((double) active_point.x) * scale_factor_x + 0.5);
- result.y = (int)(((double) active_point.y) * scale_factor_y + 0.5);
-
- return result;
- }
-
- public Gdk.Rectangle active_to_unscaled_rect(Gdk.Rectangle active_rect) {
- Gdk.Point upper_left = {0};
- Gdk.Point lower_right = {0};
- upper_left.x = active_rect.x;
- upper_left.y = active_rect.y;
- lower_right.x = upper_left.x + active_rect.width;
- lower_right.y = upper_left.y + active_rect.height;
-
- upper_left = active_to_unscaled_point(upper_left);
- lower_right = active_to_unscaled_point(lower_right);
-
- Gdk.Rectangle unscaled_rect = Gdk.Rectangle();
- unscaled_rect.x = upper_left.x;
- unscaled_rect.y = upper_left.y;
- unscaled_rect.width = lower_right.x - upper_left.x;
- unscaled_rect.height = lower_right.y - upper_left.y;
-
- return unscaled_rect;
- }
-
- public Gdk.Point user_to_active_point(Gdk.Point user_point) {
- Gdk.Rectangle active_offsets = get_scaled_pixbuf_position();
-
- Gdk.Point result = {0};
- result.x = user_point.x - active_offsets.x;
- result.y = user_point.y - active_offsets.y;
-
- return result;
- }
-
- public Gdk.Rectangle user_to_active_rect(Gdk.Rectangle user_rect) {
- Gdk.Point upper_left = {0};
- Gdk.Point lower_right = {0};
- upper_left.x = user_rect.x;
- upper_left.y = user_rect.y;
- lower_right.x = upper_left.x + user_rect.width;
- lower_right.y = upper_left.y + user_rect.height;
-
- upper_left = user_to_active_point(upper_left);
- lower_right = user_to_active_point(lower_right);
-
- Gdk.Rectangle active_rect = Gdk.Rectangle();
- active_rect.x = upper_left.x;
- active_rect.y = upper_left.y;
- active_rect.width = lower_right.x - upper_left.x;
- active_rect.height = lower_right.y - upper_left.y;
-
- return active_rect;
- }
-
- public Photo get_photo() {
- return photo;
- }
-
- public Gtk.Window get_container() {
- return container;
- }
-
- public Gdk.Window get_drawing_window() {
- return drawing_window;
- }
-
- public Cairo.Context get_default_ctx() {
- return default_ctx;
- }
-
- public Dimensions get_surface_dim() {
- return surface_dim;
- }
-
- public Scaling get_scaling() {
- return Scaling.for_viewport(surface_dim, false);
- }
-
- public void set_surface(Cairo.Context default_ctx, Dimensions surface_dim) {
- this.default_ctx = default_ctx;
- this.surface_dim = surface_dim;
-
- new_surface(default_ctx, surface_dim);
- }
-
- public Cairo.Surface get_scaled_surface() {
- return scaled;
- }
-
- public Gdk.Pixbuf get_scaled_pixbuf() {
- return scaled_pixbuf;
- }
-
- public Gdk.Rectangle get_scaled_pixbuf_position() {
- return scaled_position;
- }
-
- public void resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
- this.scaled = pixbuf_to_surface(default_ctx, scaled, scaled_position);
- this.scaled_pixbuf = scaled;
- this.scaled_position = scaled_position;
-
- resized_scaled_pixbuf(old_dim, scaled, scaled_position);
- }
-
- public abstract void repaint();
-
- // Because the editing tool should not have any need to draw on the gutters outside the photo,
- // and it's a pain to constantly calculate where it's laid out on the drawable, these convenience
- // methods automatically adjust for its position.
- //
- // If these methods are not used, all painting to the drawable should be offet by
- // get_scaled_pixbuf_position().x and get_scaled_pixbuf_position().y
- public void paint_pixbuf(Gdk.Pixbuf pixbuf) {
- default_ctx.save();
-
- // paint black background
- set_source_color_from_string(default_ctx, "#000");
- default_ctx.rectangle(0, 0, surface_dim.width, surface_dim.height);
- default_ctx.fill();
-
- // paint the actual image
- paint_pixmap_with_background(default_ctx, pixbuf, scaled_position.x, scaled_position.y);
- default_ctx.restore();
- }
-
- // Paint a surface on top of the photo
- public void paint_surface(Cairo.Surface surface, bool over) {
- default_ctx.save();
- if (over == false)
- default_ctx.set_operator(Cairo.Operator.SOURCE);
- else
- default_ctx.set_operator(Cairo.Operator.OVER);
-
- default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
- default_ctx.paint();
- default_ctx.set_source_surface(surface, scaled_position.x, scaled_position.y);
- default_ctx.paint();
- default_ctx.restore();
- }
-
- public void paint_surface_area(Cairo.Surface surface, Box source_area, bool over) {
- default_ctx.save();
- if (over == false)
- default_ctx.set_operator(Cairo.Operator.SOURCE);
- else
- default_ctx.set_operator(Cairo.Operator.OVER);
-
- default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
- default_ctx.rectangle(scaled_position.x + source_area.left,
- scaled_position.y + source_area.top,
- source_area.get_width(), source_area.get_height());
- default_ctx.fill();
-
- default_ctx.set_source_surface(surface, scaled_position.x, scaled_position.y);
- default_ctx.rectangle(scaled_position.x + source_area.left,
- scaled_position.y + source_area.top,
- source_area.get_width(), source_area.get_height());
- default_ctx.fill();
- default_ctx.restore();
- }
-
- public void draw_box(Cairo.Context ctx, Box box) {
- Gdk.Rectangle rect = box.get_rectangle();
- rect.x += scaled_position.x;
- rect.y += scaled_position.y;
-
- ctx.rectangle(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1);
- ctx.stroke();
- }
-
- public void draw_text(Cairo.Context ctx, string text, int x, int y, bool use_scaled_pos = true) {
- if (use_scaled_pos) {
- x += scaled_position.x;
- y += scaled_position.y;
- }
- Cairo.TextExtents extents;
- ctx.text_extents(text, out extents);
- x -= (int) extents.width / 2;
-
- set_source_color_from_string(ctx, Resources.ONIMAGE_FONT_BACKGROUND);
-
- int pane_border = 5; // border around edge of pane in pixels
- ctx.rectangle(x - pane_border, y - pane_border - extents.height,
- extents.width + 2 * pane_border,
- extents.height + 2 * pane_border);
- ctx.fill();
-
- ctx.move_to(x, y);
- set_source_color_from_string(ctx, Resources.ONIMAGE_FONT_COLOR);
- ctx.show_text(text);
- }
-
- /**
- * Draw a horizontal line into the specified Cairo context at the specified position, taking
- * into account the scaled position of the image unless directed otherwise.
- *
- * @param ctx The drawing context of the surface we're drawing to.
- * @param x The horizontal position to place the line at.
- * @param y The vertical position to place the line at.
- * @param width The length of the line.
- * @param use_scaled_pos Whether to use absolute window positioning or take into account the
- * position of the scaled image.
- */
- public void draw_horizontal_line(Cairo.Context ctx, int x, int y, int width, bool use_scaled_pos = true)
{
- if (use_scaled_pos) {
- x += scaled_position.x;
- y += scaled_position.y;
- }
-
- ctx.move_to(x + 0.5, y + 0.5);
- ctx.line_to(x + width - 1, y + 0.5);
- ctx.stroke();
- }
-
- /**
- * Draw a vertical line into the specified Cairo context at the specified position, taking
- * into account the scaled position of the image unless directed otherwise.
- *
- * @param ctx The drawing context of the surface we're drawing to.
- * @param x The horizontal position to place the line at.
- * @param y The vertical position to place the line at.
- * @param width The length of the line.
- * @param use_scaled_pos Whether to use absolute window positioning or take into account the
- * position of the scaled image.
- */
- public void draw_vertical_line(Cairo.Context ctx, int x, int y, int height, bool use_scaled_pos = true) {
- if (use_scaled_pos) {
- x += scaled_position.x;
- y += scaled_position.y;
- }
-
- ctx.move_to(x + 0.5, y + 0.5);
- ctx.line_to(x + 0.5, y + height - 1);
- ctx.stroke();
- }
-
- public void erase_horizontal_line(int x, int y, int width) {
- default_ctx.save();
-
- default_ctx.set_operator(Cairo.Operator.SOURCE);
- default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
- default_ctx.rectangle(scaled_position.x + x, scaled_position.y + y,
- width - 1, 1);
- default_ctx.fill();
-
- default_ctx.restore();
- }
-
- public void draw_circle(Cairo.Context ctx, int active_center_x, int active_center_y,
- int radius) {
- int center_x = active_center_x + scaled_position.x;
- int center_y = active_center_y + scaled_position.y;
-
- ctx.arc(center_x, center_y, radius, 0, 2 * GLib.Math.PI);
- ctx.stroke();
- }
-
- public void erase_vertical_line(int x, int y, int height) {
- default_ctx.save();
-
- // Ticket #3146 - artifacting when moving the crop box or
- // enlarging it from the lower right.
- // We now no longer subtract one from the height before choosing
- // a region to erase.
- default_ctx.set_operator(Cairo.Operator.SOURCE);
- default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
- default_ctx.rectangle(scaled_position.x + x, scaled_position.y + y,
- 1, height);
- default_ctx.fill();
-
- default_ctx.restore();
- }
-
- public void erase_box(Box box) {
- erase_horizontal_line(box.left, box.top, box.get_width());
- erase_horizontal_line(box.left, box.bottom, box.get_width());
-
- erase_vertical_line(box.left, box.top, box.get_height());
- erase_vertical_line(box.right, box.top, box.get_height());
- }
-
- public void invalidate_area(Box area) {
- Gdk.Rectangle rect = area.get_rectangle();
- rect.x += scaled_position.x;
- rect.y += scaled_position.y;
-
- drawing_window.invalidate_rect(rect, false);
- }
-
- public void set_cursor(Gdk.CursorType cursor_type) {
- var display = get_drawing_window().get_display();
- var cursor = new Gdk.Cursor.for_display (display, cursor_type);
- get_drawing_window().set_cursor(cursor);
- }
-
- private Cairo.Surface pixbuf_to_surface(Cairo.Context default_ctx, Gdk.Pixbuf pixbuf,
- Gdk.Rectangle pos) {
- Cairo.Surface surface = new Cairo.Surface.similar(default_ctx.get_target(),
- Cairo.Content.COLOR_ALPHA, pos.width, pos.height);
- Cairo.Context ctx = new Cairo.Context(surface);
- paint_pixmap_with_background(ctx, pixbuf, 0, 0);
- ctx.paint();
- return surface;
- }
-}
-
-public abstract class EditingTool {
- public PhotoCanvas canvas = null;
-
- private EditingToolWindow tool_window = null;
- protected Cairo.Surface surface;
- public string name;
-
- [CCode (has_target=false)]
- public delegate EditingTool Factory();
-
- public signal void activated();
-
- public signal void deactivated();
-
- public signal void applied(Command? command, Gdk.Pixbuf? new_pixbuf, Dimensions new_max_dim,
- bool needs_improvement);
-
- public signal void cancelled();
-
- public signal void aborted();
-
- protected EditingTool(string name) {
- this.name = name;
- }
-
- // base.activate() should always be called by an overriding member to ensure the base class
- // gets to set up and store the PhotoCanvas in the canvas member field. More importantly,
- // the activated signal is called here, and should only be called once the tool is completely
- // initialized.
- public virtual void activate(PhotoCanvas canvas) {
- // multiple activates are not tolerated
- assert(this.canvas == null);
- assert(tool_window == null);
-
- this.canvas = canvas;
-
- tool_window = get_tool_window();
- if (tool_window != null)
- tool_window.key_press_event.connect(on_keypress);
-
- activated();
- }
-
- // Like activate(), this should always be called from an overriding subclass.
- public virtual void deactivate() {
- // multiple deactivates are tolerated
- if (canvas == null && tool_window == null)
- return;
-
- canvas = null;
-
- if (tool_window != null) {
- tool_window.key_press_event.disconnect(on_keypress);
- tool_window = null;
- }
-
- deactivated();
- }
-
- public bool is_activated() {
- return canvas != null;
- }
-
- public virtual EditingToolWindow? get_tool_window() {
- return null;
- }
-
- // This allows the EditingTool to specify which pixbuf to display during the tool's
- // operation. Returning null means the host should use the pixbuf associated with the current
- // Photo. Note: This will be called before activate(), primarily to display the pixbuf before
- // the tool is on the screen, and before paint_full() is hooked in. It also means the PhotoCanvas
- // will have this pixbuf rather than one from the Photo class.
- //
- // If returns non-null, should also fill max_dim with the maximum dimensions of the original
- // image, as the editing host may not always scale images up to fit the viewport.
- //
- // Note this this method doesn't need to be returning the "proper" pixbuf on-the-fly (i.e.
- // a pixbuf with unsaved tool edits in it). That can be handled in the paint() virtual method.
- public virtual Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
- out Dimensions max_dim) throws Error {
- max_dim = Dimensions();
-
- return null;
- }
-
- public virtual void on_left_click(int x, int y) {
- }
-
- public virtual void on_left_released(int x, int y) {
- }
-
- public virtual void on_motion(int x, int y, Gdk.ModifierType mask) {
- }
-
- public virtual bool on_leave_notify_event(){
- return false;
- }
-
- public virtual bool on_keypress(Gdk.EventKey event) {
- // check for an escape/abort first
- if (Gdk.keyval_name(event.keyval) == "Escape") {
- notify_cancel();
-
- return true;
- }
-
- return false;
- }
-
- public virtual void paint(Cairo.Context ctx) {
- }
-
- // Helper function that fires the cancelled signal. (Can be connected to other signals.)
- protected void notify_cancel() {
- cancelled();
- }
-}
-
-public class CropTool : EditingTool {
- private const double CROP_INIT_X_PCT = 0.15;
- private const double CROP_INIT_Y_PCT = 0.15;
-
- private const int CROP_MIN_SIZE = 8;
-
- private const float CROP_EXTERIOR_SATURATION = 0.00f;
- private const int CROP_EXTERIOR_RED_SHIFT = -32;
- private const int CROP_EXTERIOR_GREEN_SHIFT = -32;
- private const int CROP_EXTERIOR_BLUE_SHIFT = -32;
- private const int CROP_EXTERIOR_ALPHA_SHIFT = 0;
-
- private const float ANY_ASPECT_RATIO = -1.0f;
- private const float SCREEN_ASPECT_RATIO = -2.0f;
- private const float ORIGINAL_ASPECT_RATIO = -3.0f;
- private const float CUSTOM_ASPECT_RATIO = -4.0f;
- private const float COMPUTE_FROM_BASIS = -5.0f;
- private const float SEPARATOR = -6.0f;
- private const float MIN_ASPECT_RATIO = 1.0f / 64.0f;
- private const float MAX_ASPECT_RATIO = 64.0f;
-
- private class ConstraintDescription {
- public string name;
- public int basis_width;
- public int basis_height;
- public bool is_pivotable;
- public float aspect_ratio;
-
- public ConstraintDescription(string new_name, int new_basis_width, int new_basis_height,
- bool new_pivotable, float new_aspect_ratio = COMPUTE_FROM_BASIS) {
- name = new_name;
- basis_width = new_basis_width;
- basis_height = new_basis_height;
- if (new_aspect_ratio == COMPUTE_FROM_BASIS)
- aspect_ratio = ((float) basis_width) / ((float) basis_height);
- else
- aspect_ratio = new_aspect_ratio;
- is_pivotable = new_pivotable;
- }
-
- public bool is_separator() {
- return !is_pivotable && aspect_ratio == SEPARATOR;
- }
- }
-
- private enum ReticleOrientation {
- LANDSCAPE,
- PORTRAIT;
-
- public ReticleOrientation toggle() {
- return (this == ReticleOrientation.LANDSCAPE) ? ReticleOrientation.PORTRAIT :
- ReticleOrientation.LANDSCAPE;
- }
- }
-
- private enum ConstraintMode {
- NORMAL,
- CUSTOM
- }
-
- private class CropToolWindow : EditingToolWindow {
- private const int CONTROL_SPACING = 8;
-
- public Gtk.Button ok_button = new Gtk.Button.with_label(Resources.CROP_LABEL);
- public Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
- public Gtk.ComboBox constraint_combo;
- public Gtk.Button pivot_reticle_button = new Gtk.Button();
- public Gtk.Entry custom_width_entry = new Gtk.Entry();
- public Gtk.Entry custom_height_entry = new Gtk.Entry();
- public Gtk.Label custom_mulsign_label = new Gtk.Label.with_mnemonic("x");
- public Gtk.Entry most_recently_edited = null;
- public Gtk.Box response_layout = null;
- public Gtk.Box layout = null;
- public int normal_width = -1;
- public int normal_height = -1;
-
- public CropToolWindow(Gtk.Window container) {
- base(container);
-
- cancel_button.set_tooltip_text(_("Return to current photo dimensions"));
- cancel_button.set_image_position(Gtk.PositionType.LEFT);
-
- ok_button.set_tooltip_text(_("Set the crop for this photo"));
- ok_button.set_image_position(Gtk.PositionType.LEFT);
-
- constraint_combo = new Gtk.ComboBox();
- Gtk.CellRendererText combo_text_renderer = new Gtk.CellRendererText();
- constraint_combo.pack_start(combo_text_renderer, true);
- constraint_combo.add_attribute(combo_text_renderer, "text", 0);
- constraint_combo.set_row_separator_func(constraint_combo_separator_func);
- constraint_combo.set_active(0);
-
- var image = new Gtk.Image.from_icon_name("crop-pivot-reticle-symbolic",
Gtk.IconSize.LARGE_TOOLBAR);
- pivot_reticle_button.set_image (image);
- pivot_reticle_button.set_tooltip_text(_("Pivot the crop rectangle between portrait and landscape
orientations"));
-
- custom_width_entry.set_width_chars(4);
- custom_width_entry.editable = true;
- custom_height_entry.set_width_chars(4);
- custom_height_entry.editable = true;
-
- response_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
- response_layout.homogeneous = true;
- response_layout.add(cancel_button);
- response_layout.add(ok_button);
-
- layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
- layout.add(constraint_combo);
- layout.add(pivot_reticle_button);
- layout.add(response_layout);
-
- add(layout);
- }
-
- private static bool constraint_combo_separator_func(Gtk.TreeModel model, Gtk.TreeIter iter) {
- Value val;
- model.get_value(iter, 0, out val);
-
- return (val.dup_string() == "-");
- }
- }
-
- private CropToolWindow crop_tool_window = null;
- private Gdk.CursorType current_cursor_type = Gdk.CursorType.LEFT_PTR;
- 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 Cairo.Context text_ctx = null;
-
- // This is where we draw our crop tool
- private Cairo.Surface crop_surface = null;
-
- // these are kept in absolute coordinates, not relative to photo's position on canvas
- private Box scaled_crop;
- private int last_grab_x = -1;
- private int last_grab_y = -1;
-
- private ConstraintDescription[] constraints = create_constraints();
- private Gtk.ListStore constraint_list = create_constraint_list(create_constraints());
- private ReticleOrientation reticle_orientation = ReticleOrientation.LANDSCAPE;
- private ConstraintMode constraint_mode = ConstraintMode.NORMAL;
- private bool entry_insert_in_progress = false;
- private float custom_aspect_ratio = 1.0f;
- private int custom_width = -1;
- private int custom_height = -1;
- private int custom_init_width = -1;
- private int custom_init_height = -1;
- private float pre_aspect_ratio = ANY_ASPECT_RATIO;
-
- private CropTool() {
- base("CropTool");
- }
-
- public static CropTool factory() {
- return new CropTool();
- }
-
- public static bool is_available(Photo photo, Scaling scaling) {
- Dimensions dim = scaling.get_scaled_dimensions(photo.get_original_dimensions());
-
- return dim.width > CROP_MIN_SIZE && dim.height > CROP_MIN_SIZE;
- }
-
- private static ConstraintDescription[] create_constraints() {
- ConstraintDescription[] result = new ConstraintDescription[0];
-
- result += new ConstraintDescription(_("Unconstrained"), 0, 0, false, ANY_ASPECT_RATIO);
- result += new ConstraintDescription(_("Square"), 1, 1, false);
- result += new ConstraintDescription(_("Screen"), 0, 0, true, SCREEN_ASPECT_RATIO);
- result += new ConstraintDescription(_("Original Size"), 0, 0, true, ORIGINAL_ASPECT_RATIO);
- result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
- result += new ConstraintDescription(_("SD Video (4 ∶ 3)"), 4, 3, true);
- result += new ConstraintDescription(_("HD Video (16 ∶ 9)"), 16, 9, true);
- result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
- result += new ConstraintDescription(_("Wallet (2 × 3 in.)"), 3, 2, true);
- result += new ConstraintDescription(_("Notecard (3 × 5 in.)"), 5, 3, true);
- result += new ConstraintDescription(_("4 × 6 in."), 6, 4, true);
- result += new ConstraintDescription(_("5 × 7 in."), 7, 5, true);
- result += new ConstraintDescription(_("8 × 10 in."), 10, 8, true);
- result += new ConstraintDescription(_("Letter (8.5 × 11 in.)"), 85, 110, true);
- result += new ConstraintDescription(_("11 × 14 in."), 14, 11, true);
- result += new ConstraintDescription(_("Tabloid (11 × 17 in.)"), 17, 11, true);
- result += new ConstraintDescription(_("16 × 20 in."), 20, 16, true);
- result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
- result += new ConstraintDescription(_("Metric Wallet (9 × 13 cm)"), 13, 9, true);
- result += new ConstraintDescription(_("Postcard (10 × 15 cm)"), 15, 10, true);
- result += new ConstraintDescription(_("13 × 18 cm"), 18, 13, true);
- result += new ConstraintDescription(_("18 × 24 cm"), 24, 18, true);
- result += new ConstraintDescription(_("A4 (210 × 297 mm)"), 210, 297, true);
- result += new ConstraintDescription(_("20 × 30 cm"), 30, 20, true);
- result += new ConstraintDescription(_("24 × 40 cm"), 40, 24, true);
- result += new ConstraintDescription(_("30 × 40 cm"), 40, 30, true);
- result += new ConstraintDescription(_("A3 (297 × 420 mm)"), 420, 297, true);
- result += new ConstraintDescription(_("-"), 0, 0, false, SEPARATOR);
- result += new ConstraintDescription(_("Custom"), 0, 0, true, CUSTOM_ASPECT_RATIO);
-
- return result;
- }
-
- private static Gtk.ListStore create_constraint_list(ConstraintDescription[] constraint_data) {
- Gtk.ListStore result = new Gtk.ListStore(1, typeof(string), typeof(string));
-
- Gtk.TreeIter iter;
- foreach (ConstraintDescription constraint in constraint_data) {
- result.append(out iter);
- result.set_value(iter, 0, constraint.name);
- }
-
- return result;
- }
-
- private void update_pivot_button_state() {
- crop_tool_window.pivot_reticle_button.set_sensitive(
- get_selected_constraint().is_pivotable);
- }
-
- private ConstraintDescription get_selected_constraint() {
- ConstraintDescription result = constraints[crop_tool_window.constraint_combo.get_active()];
-
- if (result.aspect_ratio == ORIGINAL_ASPECT_RATIO) {
- result.basis_width = canvas.get_scaled_pixbuf_position().width;
- result.basis_height = canvas.get_scaled_pixbuf_position().height;
- } else if (result.aspect_ratio == SCREEN_ASPECT_RATIO) {
- var dim = Scaling.get_screen_dimensions(AppWindow.get_instance());
- result.basis_width = dim.width;
- result.basis_height = dim.height;
- }
-
- return result;
- }
-
- private bool on_width_entry_focus_out(Gdk.EventFocus event) {
- crop_tool_window.most_recently_edited = crop_tool_window.custom_width_entry;
- return on_custom_entry_focus_out(event);
- }
-
- private bool on_height_entry_focus_out(Gdk.EventFocus event) {
- crop_tool_window.most_recently_edited = crop_tool_window.custom_height_entry;
- return on_custom_entry_focus_out(event);
- }
-
- private bool on_custom_entry_focus_out(Gdk.EventFocus event) {
- int width = int.parse(crop_tool_window.custom_width_entry.text);
- int height = int.parse(crop_tool_window.custom_height_entry.text);
-
- if(width < 1) {
- width = 1;
- crop_tool_window.custom_width_entry.set_text("%d".printf(width));
- }
-
- if(height < 1) {
- height = 1;
- crop_tool_window.custom_height_entry.set_text("%d".printf(height));
- }
-
- if ((width == custom_width) && (height == custom_height))
- return false;
-
- custom_aspect_ratio = ((float) width) / ((float) height);
-
- if (custom_aspect_ratio < MIN_ASPECT_RATIO) {
- if (crop_tool_window.most_recently_edited == crop_tool_window.custom_height_entry) {
- height = (int) (width / MIN_ASPECT_RATIO);
- crop_tool_window.custom_height_entry.set_text("%d".printf(height));
- } else {
- width = (int) (height * MIN_ASPECT_RATIO);
- crop_tool_window.custom_width_entry.set_text("%d".printf(width));
- }
- } else if (custom_aspect_ratio > MAX_ASPECT_RATIO) {
- if (crop_tool_window.most_recently_edited == crop_tool_window.custom_height_entry) {
- height = (int) (width / MAX_ASPECT_RATIO);
- crop_tool_window.custom_height_entry.set_text("%d".printf(height));
- } else {
- width = (int) (height * MAX_ASPECT_RATIO);
- crop_tool_window.custom_width_entry.set_text("%d".printf(width));
- }
- }
-
- custom_aspect_ratio = ((float) width) / ((float) height);
-
- Box new_crop = constrain_crop(scaled_crop);
-
- crop_resized(new_crop);
- scaled_crop = new_crop;
- canvas.invalidate_area(new_crop);
- canvas.repaint();
-
- custom_width = width;
- custom_height = height;
-
- return false;
- }
-
- private void on_width_insert_text(string text, int length, ref int position) {
- on_entry_insert_text(crop_tool_window.custom_width_entry, text, length, ref position);
- }
-
- private void on_height_insert_text(string text, int length, ref int position) {
- on_entry_insert_text(crop_tool_window.custom_height_entry, text, length, ref position);
- }
-
- private void on_entry_insert_text(Gtk.Entry sender, string text, int length, ref int position) {
- if (entry_insert_in_progress)
- return;
-
- entry_insert_in_progress = true;
-
- if (length == -1)
- length = (int) text.length;
-
- // only permit numeric text
- string new_text = "";
- for (int ctr = 0; ctr < length; ctr++) {
- if (text[ctr].isdigit()) {
- new_text += ((char) text[ctr]).to_string();
- }
- }
-
- if (new_text.length > 0)
- sender.insert_text(new_text, (int) new_text.length, ref position);
-
- Signal.stop_emission_by_name(sender, "insert-text");
-
- entry_insert_in_progress = false;
- }
-
- private float get_constraint_aspect_ratio() {
- var result = get_selected_constraint().aspect_ratio;
-
- if (result == ORIGINAL_ASPECT_RATIO) {
- result = ((float) canvas.get_scaled_pixbuf_position().width) /
- ((float) canvas.get_scaled_pixbuf_position().height);
- } else if (result == SCREEN_ASPECT_RATIO) {
- var dim = Scaling.get_screen_dimensions(AppWindow.get_instance());
- result = ((float) dim.width) / ((float) dim.height);
- } else if (result == CUSTOM_ASPECT_RATIO) {
- result = custom_aspect_ratio;
- }
- if (reticle_orientation == ReticleOrientation.PORTRAIT)
- result = 1.0f / result;
-
- return result;
- }
-
- private float get_constraint_aspect_ratio_for_constraint(ConstraintDescription constraint, Photo photo) {
- float result = constraint.aspect_ratio;
-
- if (result == ORIGINAL_ASPECT_RATIO) {
- Dimensions orig_dim = photo.get_original_dimensions();
- result = ((float) orig_dim.width) / ((float) orig_dim.height);
- } else if (result == SCREEN_ASPECT_RATIO) {
- var dim = Scaling.get_screen_dimensions(AppWindow.get_instance());
- result = ((float) dim.width) / ((float) dim.height);
- } else if (result == CUSTOM_ASPECT_RATIO) {
- result = custom_aspect_ratio;
- }
- if (reticle_orientation == ReticleOrientation.PORTRAIT)
- result = 1.0f / result;
-
- return result;
-
- }
-
- private void constraint_changed() {
- ConstraintDescription selected_constraint = get_selected_constraint();
- if (selected_constraint.aspect_ratio == CUSTOM_ASPECT_RATIO) {
- set_custom_constraint_mode();
- } else {
- set_normal_constraint_mode();
-
- if (selected_constraint.aspect_ratio != ANY_ASPECT_RATIO) {
- // user may have switched away from 'Custom' without
- // accepting, so set these to default back to saved
- // values.
- custom_init_width = Config.Facade.get_instance().get_last_crop_width();
- custom_init_height = Config.Facade.get_instance().get_last_crop_height();
- custom_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
- }
- }
-
- update_pivot_button_state();
-
- if (!get_selected_constraint().is_pivotable)
- reticle_orientation = ReticleOrientation.LANDSCAPE;
-
- if (get_constraint_aspect_ratio() != pre_aspect_ratio) {
- Box new_crop = constrain_crop(scaled_crop);
-
- crop_resized(new_crop);
- scaled_crop = new_crop;
- canvas.invalidate_area(new_crop);
- canvas.repaint();
-
- pre_aspect_ratio = get_constraint_aspect_ratio();
- }
- }
-
- private void set_custom_constraint_mode() {
- if (constraint_mode == ConstraintMode.CUSTOM)
- return;
-
- if ((crop_tool_window.normal_width == -1) || (crop_tool_window.normal_height == -1))
- crop_tool_window.get_size(out crop_tool_window.normal_width,
- out crop_tool_window.normal_height);
-
- crop_tool_window.layout.remove(crop_tool_window.constraint_combo);
- crop_tool_window.layout.remove(crop_tool_window.pivot_reticle_button);
- crop_tool_window.layout.remove(crop_tool_window.response_layout);
-
- crop_tool_window.layout.add(crop_tool_window.constraint_combo);
- crop_tool_window.layout.add(crop_tool_window.custom_width_entry);
- crop_tool_window.layout.add(crop_tool_window.custom_mulsign_label);
- crop_tool_window.layout.add(crop_tool_window.custom_height_entry);
- crop_tool_window.layout.add(crop_tool_window.pivot_reticle_button);
- crop_tool_window.layout.add(crop_tool_window.response_layout);
-
- if (reticle_orientation == ReticleOrientation.LANDSCAPE) {
- crop_tool_window.custom_width_entry.set_text("%d".printf(custom_init_width));
- crop_tool_window.custom_height_entry.set_text("%d".printf(custom_init_height));
- } else {
- crop_tool_window.custom_width_entry.set_text("%d".printf(custom_init_height));
- crop_tool_window.custom_height_entry.set_text("%d".printf(custom_init_width));
- }
- custom_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
-
- crop_tool_window.show_all();
-
- constraint_mode = ConstraintMode.CUSTOM;
- }
-
- private void set_normal_constraint_mode() {
- if (constraint_mode == ConstraintMode.NORMAL)
- return;
-
- crop_tool_window.layout.remove(crop_tool_window.constraint_combo);
- crop_tool_window.layout.remove(crop_tool_window.custom_width_entry);
- crop_tool_window.layout.remove(crop_tool_window.custom_mulsign_label);
- crop_tool_window.layout.remove(crop_tool_window.custom_height_entry);
- crop_tool_window.layout.remove(crop_tool_window.pivot_reticle_button);
- crop_tool_window.layout.remove(crop_tool_window.response_layout);
-
- crop_tool_window.layout.add(crop_tool_window.constraint_combo);
- crop_tool_window.layout.add(crop_tool_window.pivot_reticle_button);
- crop_tool_window.layout.add(crop_tool_window.response_layout);
-
- crop_tool_window.resize(crop_tool_window.normal_width,
- crop_tool_window.normal_height);
-
- crop_tool_window.show_all();
-
- constraint_mode = ConstraintMode.NORMAL;
- }
-
- private Box constrain_crop(Box crop) {
- float user_aspect_ratio = get_constraint_aspect_ratio();
- if (user_aspect_ratio == ANY_ASPECT_RATIO)
- return crop;
-
- // PHASE 1: Scale to the desired aspect ratio, preserving area and center.
- float old_area = (float) (crop.get_width() * crop.get_height());
- crop.adjust_height((int) Math.sqrt(old_area / user_aspect_ratio));
- crop.adjust_width((int) Math.sqrt(old_area * user_aspect_ratio));
-
- // PHASE 2: Crop to the image boundary.
- Dimensions image_size = get_photo_dimensions();
- double angle;
- canvas.get_photo().get_straighten(out angle);
- crop = clamp_inside_rotated_image(crop, image_size.width, image_size.height, angle, false);
-
- // PHASE 3: Crop down to the aspect ratio if necessary.
- if (crop.get_width() >= crop.get_height() * user_aspect_ratio) // possibly too wide
- crop.adjust_width((int) (crop.get_height() * user_aspect_ratio));
- else // possibly too tall
- crop.adjust_height((int) (crop.get_width() / user_aspect_ratio));
-
- return crop;
- }
-
- private ConstraintDescription? get_last_constraint(out int index) {
- index = Config.Facade.get_instance().get_last_crop_menu_choice();
-
- return (index < constraints.length) ? constraints[index] : null;
- }
-
- public override void activate(PhotoCanvas canvas) {
- bind_canvas_handlers(canvas);
-
- prepare_ctx(canvas.get_default_ctx(), canvas.get_surface_dim());
-
- if (crop_surface != null)
- crop_surface = null;
-
- crop_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
- canvas.get_scaled_pixbuf_position().width,
- canvas.get_scaled_pixbuf_position().height);
-
- Cairo.Context ctx = new Cairo.Context(crop_surface);
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
- ctx.paint();
-
- // create the crop tool window, where the user can apply or cancel the crop
- crop_tool_window = new CropToolWindow(canvas.get_container());
-
- // set up the constraint combo box
- crop_tool_window.constraint_combo.set_model(constraint_list);
- if(!canvas.get_photo().has_crop()) {
- int index;
- ConstraintDescription? desc = get_last_constraint(out index);
- if (desc != null && !desc.is_separator())
- crop_tool_window.constraint_combo.set_active(index);
- }
- else {
- // get aspect ratio of current photo
- Photo photo = canvas.get_photo();
- Dimensions cropped_dim = photo.get_dimensions();
- float ratio = (float) cropped_dim.width / (float) cropped_dim.height;
- for (int index = 1; index < constraints.length; index++) {
- if (Math.fabs(ratio - get_constraint_aspect_ratio_for_constraint(constraints[index], photo))
< 0.005)
- crop_tool_window.constraint_combo.set_active(index);
- }
- }
-
- // set up the pivot reticle button
- update_pivot_button_state();
- reticle_orientation = ReticleOrientation.LANDSCAPE;
-
- bind_window_handlers();
-
- // obtain crop dimensions and paint against the uncropped photo
- Dimensions uncropped_dim = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
-
- Box crop;
- if (!canvas.get_photo().get_crop(out crop)) {
- int xofs = (int) (uncropped_dim.width * CROP_INIT_X_PCT);
- int yofs = (int) (uncropped_dim.height * CROP_INIT_Y_PCT);
-
- // initialize the actual crop in absolute coordinates, not relative
- // to the photo's position on the canvas
- crop = Box(xofs, yofs, uncropped_dim.width - xofs, uncropped_dim.height - yofs);
- }
-
- // scale the crop to the scaled photo's size ... the scaled crop is maintained in
- // coordinates not relative to photo's position on canvas
- scaled_crop = crop.get_scaled_similar(uncropped_dim,
- Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()));
-
- // get the custom width and height from the saved config and
- // set up the initial custom values with it.
- custom_width = Config.Facade.get_instance().get_last_crop_width();
- custom_height = Config.Facade.get_instance().get_last_crop_height();
- custom_init_width = custom_width;
- custom_init_height = custom_height;
- pre_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
-
- constraint_mode = ConstraintMode.NORMAL;
-
- base.activate(canvas);
-
- crop_tool_window.show_all();
-
- // was 'custom' the most-recently-chosen menu item?
- if(!canvas.get_photo().has_crop()) {
- ConstraintDescription? desc = get_last_constraint(null);
- if (desc != null && !desc.is_separator() && desc.aspect_ratio == CUSTOM_ASPECT_RATIO)
- set_custom_constraint_mode();
- }
-
- // since we no longer just run with the default, but rather
- // a saved value, we'll behave as if the saved constraint has
- // just been changed to so that everything gets updated and
- // the canvas stays in sync.
- Box new_crop = constrain_crop(scaled_crop);
-
- crop_resized(new_crop);
- scaled_crop = new_crop;
- canvas.invalidate_area(new_crop);
- canvas.repaint();
-
- pre_aspect_ratio = get_constraint_aspect_ratio();
- }
-
- private void bind_canvas_handlers(PhotoCanvas canvas) {
- canvas.new_surface.connect(prepare_ctx);
- canvas.resized_scaled_pixbuf.connect(on_resized_pixbuf);
- }
-
- private void unbind_canvas_handlers(PhotoCanvas canvas) {
- canvas.new_surface.disconnect(prepare_ctx);
- canvas.resized_scaled_pixbuf.disconnect(on_resized_pixbuf);
- }
-
- private void bind_window_handlers() {
- crop_tool_window.key_press_event.connect(on_keypress);
- crop_tool_window.ok_button.clicked.connect(on_crop_ok);
- crop_tool_window.cancel_button.clicked.connect(notify_cancel);
- crop_tool_window.constraint_combo.changed.connect(constraint_changed);
- crop_tool_window.pivot_reticle_button.clicked.connect(on_pivot_button_clicked);
-
- // set up the custom width and height entry boxes
- crop_tool_window.custom_width_entry.focus_out_event.connect(on_width_entry_focus_out);
- crop_tool_window.custom_height_entry.focus_out_event.connect(on_height_entry_focus_out);
- crop_tool_window.custom_width_entry.insert_text.connect(on_width_insert_text);
- crop_tool_window.custom_height_entry.insert_text.connect(on_height_insert_text);
- }
-
- private void unbind_window_handlers() {
- crop_tool_window.key_press_event.disconnect(on_keypress);
- crop_tool_window.ok_button.clicked.disconnect(on_crop_ok);
- crop_tool_window.cancel_button.clicked.disconnect(notify_cancel);
- crop_tool_window.constraint_combo.changed.disconnect(constraint_changed);
- crop_tool_window.pivot_reticle_button.clicked.disconnect(on_pivot_button_clicked);
-
- // set up the custom width and height entry boxes
- crop_tool_window.custom_width_entry.focus_out_event.disconnect(on_width_entry_focus_out);
- crop_tool_window.custom_height_entry.focus_out_event.disconnect(on_height_entry_focus_out);
- crop_tool_window.custom_width_entry.insert_text.disconnect(on_width_insert_text);
- }
-
- public override bool on_keypress(Gdk.EventKey event) {
- if ((Gdk.keyval_name(event.keyval) == "KP_Enter") ||
- (Gdk.keyval_name(event.keyval) == "Enter") ||
- (Gdk.keyval_name(event.keyval) == "Return")) {
- on_crop_ok();
- return true;
- }
-
- return base.on_keypress(event);
- }
-
- private void on_pivot_button_clicked() {
- if (get_selected_constraint().aspect_ratio == CUSTOM_ASPECT_RATIO) {
- string width_text = crop_tool_window.custom_width_entry.get_text();
- string height_text = crop_tool_window.custom_height_entry.get_text();
- crop_tool_window.custom_width_entry.set_text(height_text);
- crop_tool_window.custom_height_entry.set_text(width_text);
-
- int temp = custom_width;
- custom_width = custom_height;
- custom_height = temp;
- }
- reticle_orientation = reticle_orientation.toggle();
- constraint_changed();
- }
-
- public override void deactivate() {
- if (canvas != null)
- unbind_canvas_handlers(canvas);
-
- if (crop_tool_window != null) {
- unbind_window_handlers();
- crop_tool_window.hide();
- crop_tool_window.destroy();
- crop_tool_window = null;
- }
-
- // make sure the cursor isn't set to a modify indicator
- if (canvas != null) {
- canvas.set_cursor (Gdk.CursorType.LEFT_PTR);
- }
-
- crop_surface = null;
-
- base.deactivate();
- }
-
- public override EditingToolWindow? get_tool_window() {
- return crop_tool_window;
- }
-
- public override Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
- out Dimensions max_dim) throws Error {
- max_dim = photo.get_dimensions(Photo.Exception.CROP);
-
- return photo.get_pixbuf_with_options(scaling, Photo.Exception.CROP);
- }
-
- private 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_white_ctx, "#FFF");
- wide_white_ctx.set_line_width(1);
-
- thin_white_ctx = new Cairo.Context(ctx.get_target());
- set_source_color_from_string(thin_white_ctx, "#FFF");
- thin_white_ctx.set_line_width(0.5);
-
- text_ctx = new Cairo.Context(ctx.get_target());
- text_ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
- }
-
- private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
- Dimensions new_dim = Dimensions.for_pixbuf(scaled);
- Dimensions uncropped_dim = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
-
- // rescale to full crop
- Box crop = scaled_crop.get_scaled_similar(old_dim, uncropped_dim);
-
- // rescale back to new size
- scaled_crop = crop.get_scaled_similar(uncropped_dim, new_dim);
- if (crop_surface != null)
- crop_surface = null;
-
- crop_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, scaled.width, scaled.height);
- Cairo.Context ctx = new Cairo.Context(crop_surface);
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
- ctx.paint();
-
- }
-
- public override void on_left_click(int x, int y) {
- Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
-
- // scaled_crop is not maintained relative to photo's position on canvas
- Box offset_scaled_crop = scaled_crop.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_crop.approx_location(x, y);
- last_grab_x = x -= scaled_pixbuf_pos.x;
- last_grab_y = y -= scaled_pixbuf_pos.y;
-
- // repaint because the crop changes on a mouse down
- canvas.repaint();
- }
-
- public override void on_left_released(int x, int y) {
- // nothing to do if released outside of the crop 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);
-
- // repaint because crop changes when released
- canvas.repaint();
- }
-
- public override void on_motion(int x, int y, Gdk.ModifierType mask) {
- // only deal with manipulating the crop tool 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);
- canvas.repaint();
- }
-
- public override void paint(Cairo.Context default_ctx) {
- // fill region behind the crop 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(crop_surface);
- ctx.set_operator(Cairo.Operator.SOURCE);
- ctx.set_source_rgba(0.0, 0.0, 0.0, 0.5);
- ctx.paint();
-
- // paint exposed (cropped) part of pixbuf minus crop border
- ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0);
- ctx.rectangle(scaled_crop.left, scaled_crop.top, scaled_crop.get_width(),
- scaled_crop.get_height());
- ctx.fill();
- canvas.paint_surface(crop_surface, true);
-
- // paint crop tool last
- paint_crop_tool(scaled_crop);
- }
-
- private void on_crop_ok() {
- // user's clicked OK, save the combobox choice and width/height.
- // safe to do, even if not in 'custom' mode - the previous values
- // will just get saved again.
- Config.Facade.get_instance().set_last_crop_menu_choice(
- crop_tool_window.constraint_combo.get_active());
- Config.Facade.get_instance().set_last_crop_width(custom_width);
- Config.Facade.get_instance().set_last_crop_height(custom_height);
-
- // scale screen-coordinate crop to photo's coordinate system
- Box crop = scaled_crop.get_scaled_similar(
- Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()),
- canvas.get_photo().get_dimensions(Photo.Exception.CROP));
-
- // crop the current pixbuf and offer it to the editing host
- Gdk.Pixbuf cropped = new Gdk.Pixbuf.subpixbuf(canvas.get_scaled_pixbuf(), scaled_crop.left,
- scaled_crop.top, scaled_crop.get_width(), scaled_crop.get_height());
-
- // signal host; we have a cropped image, but it will be scaled upward, and so a better one
- // should be fetched
- applied(new CropCommand(canvas.get_photo(), crop, Resources.CROP_LABEL,
- Resources.CROP_TOOLTIP), cropped, crop.get_dimensions(), true);
- }
-
- private void update_cursor(int x, int y) {
- // scaled_crop is not maintained relative to photo's position on canvas
- Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
- Box offset_scaled_crop = scaled_crop.get_offset(scaled_pos.x, scaled_pos.y);
-
- Gdk.CursorType cursor_type = Gdk.CursorType.LEFT_PTR;
- switch (offset_scaled_crop.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) {
- canvas.set_cursor(cursor_type);
- current_cursor_type = cursor_type;
- }
- }
-
- private int eval_radial_line(double center_x, double center_y, double bounds_x,
- double bounds_y, double user_x) {
- double decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
- double decision_intercept = bounds_y - (decision_slope * bounds_x);
-
- return (int) (decision_slope * user_x + decision_intercept);
- }
-
- // Return the dimensions of the uncropped source photo scaled to canvas coordinates.
- private Dimensions get_photo_dimensions() {
- Dimensions photo_dims = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
- Dimensions surface_dims = canvas.get_surface_dim();
- double scale_factor = double.min((double) surface_dims.width / photo_dims.width,
- (double) surface_dims.height / photo_dims.height);
- scale_factor = double.min(scale_factor, 1.0);
-
- photo_dims = canvas.get_photo().get_dimensions(
- Photo.Exception.CROP | Photo.Exception.STRAIGHTEN);
-
- return { (int) (photo_dims.width * scale_factor),
- (int) (photo_dims.height * scale_factor) };
- }
-
- private bool on_canvas_manipulation(int x, int y) {
- Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
-
- // scaled_crop 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 = scaled_crop.left;
- int top = scaled_crop.top;
- int right = scaled_crop.right;
- int bottom = scaled_crop.bottom;
-
- // get extra geometric information needed to enforce constraints
- int center_x = (left + right) / 2;
- int center_y = (top + bottom) / 2;
-
- switch (in_manipulation) {
- case BoxLocation.LEFT_SIDE:
- left = x;
- if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
- float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
- bottom = top + ((int) new_height);
- }
- break;
-
- case BoxLocation.TOP_SIDE:
- top = y;
- if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
- float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
- right = left + ((int) new_width);
- }
- break;
-
- case BoxLocation.RIGHT_SIDE:
- right = x;
- if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
- float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
- bottom = top + ((int) new_height);
- }
- break;
-
- case BoxLocation.BOTTOM_SIDE:
- bottom = y;
- if (get_constraint_aspect_ratio() != ANY_ASPECT_RATIO) {
- float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
- right = left + ((int) new_width);
- }
- break;
-
- case BoxLocation.TOP_LEFT:
- if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
- top = y;
- left = x;
- } else {
- if (y < eval_radial_line(center_x, center_y, left, top, x)) {
- top = y;
- float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
- left = right - ((int) new_width);
- } else {
- left = x;
- float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
- top = bottom - ((int) new_height);
- }
- }
- break;
-
- case BoxLocation.BOTTOM_LEFT:
- if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
- bottom = y;
- left = x;
- } else {
- if (y < eval_radial_line(center_x, center_y, left, bottom, x)) {
- left = x;
- float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
- bottom = top + ((int) new_height);
- } else {
- bottom = y;
- float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
- left = right - ((int) new_width);
- }
- }
- break;
-
- case BoxLocation.TOP_RIGHT:
- if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
- top = y;
- right = x;
- } else {
- if (y < eval_radial_line(center_x, center_y, right, top, x)) {
- top = y;
- float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
- right = left + ((int) new_width);
- } else {
- right = x;
- float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
- top = bottom - ((int) new_height);
- }
- }
- break;
-
- case BoxLocation.BOTTOM_RIGHT:
- if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
- bottom = y;
- right = x;
- } else {
- if (y < eval_radial_line(center_x, center_y, right, bottom, x)) {
- right = x;
- float new_height = ((float) (right - left)) / get_constraint_aspect_ratio();
- bottom = top + ((int) new_height);
- } else {
- bottom = y;
- float new_width = ((float) (bottom - top)) * get_constraint_aspect_ratio();
- right = left + ((int) new_width);
- }
- }
- 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 crop 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 crop
- 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
- // crop reticle's edges stay within the photo bounds. This bounds check works
- // differently in constrained versus unconstrained mode. In unconstrained mode,
- // we need only to bounds clamp the one or two edge(s) that are actually out-of-bounds.
- // In constrained mode however, we need to bounds clamp the entire box, because the
- // positions of edges are all interdependent (so as to enforce the aspect ratio
- // constraint).
- int width = right - left + 1;
- int height = bottom - top + 1;
-
- Dimensions photo_dims = get_photo_dimensions();
- double angle;
- canvas.get_photo().get_straighten(out angle);
-
- Box new_crop;
- if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
- 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 < CROP_MIN_SIZE)
- left = right - CROP_MIN_SIZE;
- break;
-
- case BoxLocation.RIGHT_SIDE:
- case BoxLocation.TOP_RIGHT:
- case BoxLocation.BOTTOM_RIGHT:
- if (width < CROP_MIN_SIZE)
- right = left + CROP_MIN_SIZE;
- break;
-
- default:
- break;
- }
-
- switch (in_manipulation) {
- case BoxLocation.TOP_SIDE:
- case BoxLocation.TOP_LEFT:
- case BoxLocation.TOP_RIGHT:
- if (height < CROP_MIN_SIZE)
- top = bottom - CROP_MIN_SIZE;
- break;
-
- case BoxLocation.BOTTOM_SIDE:
- case BoxLocation.BOTTOM_LEFT:
- case BoxLocation.BOTTOM_RIGHT:
- if (height < CROP_MIN_SIZE)
- bottom = top + CROP_MIN_SIZE;
- break;
-
- default:
- break;
- }
-
- // preliminary crop region has been chosen, now clamp it inside the
- // image as needed.
-
- new_crop = clamp_inside_rotated_image(
- Box(left, top, right, bottom),
- photo_dims.width, photo_dims.height, angle,
- in_manipulation == BoxLocation.INSIDE);
-
- } else {
- // one of the constrained modes is active; revert instead of clamping so
- // that aspect ratio stays intact
-
- new_crop = Box(left, top, right, bottom);
- Box adjusted = clamp_inside_rotated_image(new_crop,
- photo_dims.width, photo_dims.height, angle,
- in_manipulation == BoxLocation.INSIDE);
-
- if (adjusted != new_crop || width < CROP_MIN_SIZE || height < CROP_MIN_SIZE) {
- new_crop = scaled_crop; // revert crop move
- }
- }
-
- if (in_manipulation != BoxLocation.INSIDE)
- crop_resized(new_crop);
- else
- crop_moved(new_crop);
-
- // load new values
- scaled_crop = new_crop;
-
- if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
- custom_init_width = scaled_crop.get_width();
- custom_init_height = scaled_crop.get_height();
- custom_aspect_ratio = ((float) custom_init_width) / ((float) custom_init_height);
- }
-
- return false;
- }
-
- private void crop_resized(Box new_crop) {
- if(scaled_crop.equals(new_crop)) {
- // no change
- return;
- }
-
- canvas.invalidate_area(scaled_crop);
-
- Box horizontal;
- bool horizontal_enlarged;
- Box vertical;
- bool vertical_enlarged;
- BoxComplements complements = scaled_crop.resized_complements(new_crop, 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);
-
- if (complements == BoxComplements.HORIZONTAL || complements == BoxComplements.BOTH)
- set_area_alpha(horizontal, horizontal_enlarged ? 0.0 : 0.5);
-
- if (complements == BoxComplements.VERTICAL || complements == BoxComplements.BOTH)
- set_area_alpha(vertical, vertical_enlarged ? 0.0 : 0.5);
-
- paint_crop_tool(new_crop);
- canvas.invalidate_area(new_crop);
- }
-
- private void crop_moved(Box new_crop) {
- if (scaled_crop.equals(new_crop)) {
- // no change
- return;
- }
-
- canvas.invalidate_area(scaled_crop);
-
- set_area_alpha(scaled_crop, 0.5);
- set_area_alpha(new_crop, 0.0);
-
-
- // paint crop in new location
- paint_crop_tool(new_crop);
- canvas.invalidate_area(new_crop);
- }
-
- private void set_area_alpha(Box area, double alpha) {
- Cairo.Context ctx = new Cairo.Context(crop_surface);
- ctx.set_operator(Cairo.Operator.SOURCE);
- ctx.set_source_rgba(0.0, 0.0, 0.0, alpha);
- ctx.rectangle(area.left, area.top, area.get_width(), area.get_height());
- ctx.fill();
- canvas.paint_surface_area(crop_surface, area, true);
- }
-
- private void paint_crop_tool(Box crop) {
- // paint rule-of-thirds lines and current dimensions if user is manipulating the crop
- if (in_manipulation != BoxLocation.OUTSIDE) {
- int one_third_x = crop.get_width() / 3;
- int one_third_y = crop.get_height() / 3;
-
- canvas.draw_horizontal_line(thin_white_ctx, crop.left, crop.top + one_third_y, crop.get_width());
- canvas.draw_horizontal_line(thin_white_ctx, crop.left, crop.top + (one_third_y * 2),
crop.get_width());
-
- canvas.draw_vertical_line(thin_white_ctx, crop.left + one_third_x, crop.top, crop.get_height());
- canvas.draw_vertical_line(thin_white_ctx, crop.left + (one_third_x * 2), crop.top,
crop.get_height());
-
- // current dimensions
- // scale screen-coordinate crop to photo's coordinate system
- Box adj_crop = scaled_crop.get_scaled_similar(
- Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()),
- canvas.get_photo().get_dimensions(Photo.Exception.CROP));
- string text = adj_crop.get_width().to_string() + "x" + adj_crop.get_height().to_string();
- int x = crop.left + crop.get_width() / 2;
- int y = crop.top + crop.get_height() / 2;
- canvas.draw_text(text_ctx, text, x, y);
- }
-
- // outer rectangle ... outer line in black, inner in white, corners fully black
- canvas.draw_box(wide_black_ctx, crop);
- canvas.draw_box(wide_white_ctx, crop.get_reduced(1));
- canvas.draw_box(wide_white_ctx, crop.get_reduced(2));
- }
-
-}
-
-public struct RedeyeInstance {
- public const int MIN_RADIUS = 4;
- public const int MAX_RADIUS = 32;
- public const int DEFAULT_RADIUS = 10;
-
- public Gdk.Point center;
- public int radius;
-
- RedeyeInstance() {
- Gdk.Point default_center = Gdk.Point();
- center = default_center;
- radius = DEFAULT_RADIUS;
- }
-
- public static Gdk.Rectangle to_bounds_rect(EditingTools.RedeyeInstance inst) {
- Gdk.Rectangle result = Gdk.Rectangle();
- result.x = inst.center.x - inst.radius;
- result.y = inst.center.y - inst.radius;
- result.width = 2 * inst.radius;
- result.height = result.width;
-
- return result;
- }
-
- public static RedeyeInstance from_bounds_rect(Gdk.Rectangle rect) {
- Gdk.Rectangle in_rect = rect;
-
- RedeyeInstance result = RedeyeInstance();
- result.radius = (in_rect.width + in_rect.height) / 4;
- result.center.x = in_rect.x + result.radius;
- result.center.y = in_rect.y + result.radius;
-
- return result;
- }
-}
-
-public class RedeyeTool : EditingTool {
- private class RedeyeToolWindow : EditingToolWindow {
- private const int CONTROL_SPACING = 8;
-
- private Gtk.Label slider_label = new Gtk.Label.with_mnemonic(_("Size:"));
-
- public Gtk.Button apply_button =
- new Gtk.Button.with_mnemonic(Resources.APPLY_LABEL);
- public Gtk.Button close_button =
- new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
- public Gtk.Scale slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- RedeyeInstance.MIN_RADIUS, RedeyeInstance.MAX_RADIUS, 1.0);
-
- public RedeyeToolWindow(Gtk.Window container) {
- base(container);
-
- slider.set_size_request(80, -1);
- slider.set_draw_value(false);
-
- close_button.set_tooltip_text(_("Close the red-eye tool"));
- close_button.set_image_position(Gtk.PositionType.LEFT);
-
- apply_button.set_tooltip_text(_("Remove any red-eye effects in the selected region"));
- apply_button.set_image_position(Gtk.PositionType.LEFT);
-
- Gtk.Box layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
- layout.add(slider_label);
- layout.add(slider);
- layout.add(close_button);
- layout.add(apply_button);
-
- add(layout);
- }
- }
-
- private Cairo.Context thin_white_ctx = null;
- private Cairo.Context wider_gray_ctx = null;
- private RedeyeToolWindow redeye_tool_window = null;
- private RedeyeInstance user_interaction_instance;
- private bool is_reticle_move_in_progress = false;
- private Gdk.Point reticle_move_mouse_start_point;
- private Gdk.Point reticle_move_anchor;
- private Gdk.Rectangle old_scaled_pixbuf_position;
- private Gdk.Pixbuf current_pixbuf = null;
-
- private RedeyeTool() {
- base("RedeyeTool");
- }
-
- public static RedeyeTool factory() {
- return new RedeyeTool();
- }
-
- public static bool is_available(Photo photo, Scaling scaling) {
- Dimensions dim = scaling.get_scaled_dimensions(photo.get_dimensions());
-
- return dim.width >= (RedeyeInstance.MAX_RADIUS * 2)
- && dim.height >= (RedeyeInstance.MAX_RADIUS * 2);
- }
-
- private RedeyeInstance new_interaction_instance(PhotoCanvas canvas) {
- Gdk.Rectangle photo_bounds = canvas.get_scaled_pixbuf_position();
- Gdk.Point photo_center = {0};
- photo_center.x = photo_bounds.x + (photo_bounds.width / 2);
- photo_center.y = photo_bounds.y + (photo_bounds.height / 2);
-
- RedeyeInstance result = RedeyeInstance();
- result.center.x = photo_center.x;
- result.center.y = photo_center.y;
- result.radius = RedeyeInstance.DEFAULT_RADIUS;
-
- return result;
- }
-
- private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
- wider_gray_ctx = new Cairo.Context(ctx.get_target());
- set_source_color_from_string(wider_gray_ctx, "#111");
- wider_gray_ctx.set_line_width(3);
-
- thin_white_ctx = new Cairo.Context(ctx.get_target());
- set_source_color_from_string(thin_white_ctx, "#FFF");
- thin_white_ctx.set_line_width(1);
- }
-
- private void draw_redeye_instance(RedeyeInstance inst) {
- canvas.draw_circle(wider_gray_ctx, inst.center.x, inst.center.y,
- inst.radius);
- canvas.draw_circle(thin_white_ctx, inst.center.x, inst.center.y,
- inst.radius);
- }
-
- private bool on_size_slider_adjust(Gtk.ScrollType type) {
- user_interaction_instance.radius =
- (int) redeye_tool_window.slider.get_value();
-
- canvas.repaint();
-
- return false;
- }
-
- private void on_apply() {
- Gdk.Rectangle bounds_rect_user =
- RedeyeInstance.to_bounds_rect(user_interaction_instance);
-
- Gdk.Rectangle bounds_rect_active =
- canvas.user_to_active_rect(bounds_rect_user);
- Gdk.Rectangle bounds_rect_unscaled =
- canvas.active_to_unscaled_rect(bounds_rect_active);
- Gdk.Rectangle bounds_rect_raw =
- canvas.unscaled_to_raw_rect(bounds_rect_unscaled);
-
- RedeyeInstance instance_raw =
- RedeyeInstance.from_bounds_rect(bounds_rect_raw);
-
- // transform screen coords back to image coords,
- // taking into account straightening angle.
- Dimensions dimensions = canvas.get_photo().get_dimensions(
- Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
-
- double theta = 0.0;
-
- canvas.get_photo().get_straighten(out theta);
-
- instance_raw.center = derotate_point_arb(instance_raw.center,
- dimensions.width, dimensions.height, theta);
-
- RedeyeCommand command = new RedeyeCommand(canvas.get_photo(), instance_raw,
- Resources.RED_EYE_LABEL, Resources.RED_EYE_TOOLTIP);
- AppWindow.get_command_manager().execute(command);
- }
-
- private void on_photos_altered(Gee.Map<DataObject, Alteration> map) {
- if (!map.has_key(canvas.get_photo()))
- return;
-
- try {
- current_pixbuf = canvas.get_photo().get_pixbuf(canvas.get_scaling());
- } catch (Error err) {
- warning("%s", err.message);
- aborted();
-
- return;
- }
-
- canvas.repaint();
- }
-
- private void on_close() {
- applied(null, current_pixbuf, canvas.get_photo().get_dimensions(), false);
- }
-
- private void on_canvas_resize() {
- Gdk.Rectangle scaled_pixbuf_position =
- canvas.get_scaled_pixbuf_position();
-
- user_interaction_instance.center.x -= old_scaled_pixbuf_position.x;
- user_interaction_instance.center.y -= old_scaled_pixbuf_position.y;
-
- double scale_factor = ((double) scaled_pixbuf_position.width) /
- ((double) old_scaled_pixbuf_position.width);
-
- user_interaction_instance.center.x =
- (int)(((double) user_interaction_instance.center.x) *
- scale_factor + 0.5);
- user_interaction_instance.center.y =
- (int)(((double) user_interaction_instance.center.y) *
- scale_factor + 0.5);
-
- user_interaction_instance.center.x += scaled_pixbuf_position.x;
- user_interaction_instance.center.y += scaled_pixbuf_position.y;
-
- old_scaled_pixbuf_position = scaled_pixbuf_position;
-
- current_pixbuf = null;
- }
-
- public override void activate(PhotoCanvas canvas) {
- user_interaction_instance = new_interaction_instance(canvas);
-
- prepare_ctx(canvas.get_default_ctx(), canvas.get_surface_dim());
-
- bind_canvas_handlers(canvas);
-
- old_scaled_pixbuf_position = canvas.get_scaled_pixbuf_position();
- current_pixbuf = canvas.get_scaled_pixbuf();
-
- redeye_tool_window = new RedeyeToolWindow(canvas.get_container());
- redeye_tool_window.slider.set_value(user_interaction_instance.radius);
-
- bind_window_handlers();
-
- DataCollection? owner = canvas.get_photo().get_membership();
- if (owner != null)
- owner.items_altered.connect(on_photos_altered);
-
- base.activate(canvas);
- }
-
- public override void deactivate() {
- if (canvas != null) {
- DataCollection? owner = canvas.get_photo().get_membership();
- if (owner != null)
- owner.items_altered.disconnect(on_photos_altered);
-
- unbind_canvas_handlers(canvas);
- }
-
- if (redeye_tool_window != null) {
- unbind_window_handlers();
- redeye_tool_window.hide();
- redeye_tool_window.destroy();
- redeye_tool_window = null;
- }
-
- base.deactivate();
- }
-
- private void bind_canvas_handlers(PhotoCanvas canvas) {
- canvas.new_surface.connect(prepare_ctx);
- canvas.resized_scaled_pixbuf.connect(on_canvas_resize);
- }
-
- private void unbind_canvas_handlers(PhotoCanvas canvas) {
- canvas.new_surface.disconnect(prepare_ctx);
- canvas.resized_scaled_pixbuf.disconnect(on_canvas_resize);
- }
-
- private void bind_window_handlers() {
- redeye_tool_window.apply_button.clicked.connect(on_apply);
- redeye_tool_window.close_button.clicked.connect(on_close);
- redeye_tool_window.slider.change_value.connect(on_size_slider_adjust);
- }
-
- private void unbind_window_handlers() {
- redeye_tool_window.apply_button.clicked.disconnect(on_apply);
- redeye_tool_window.close_button.clicked.disconnect(on_close);
- redeye_tool_window.slider.change_value.disconnect(on_size_slider_adjust);
- }
-
- public override EditingToolWindow? get_tool_window() {
- return redeye_tool_window;
- }
-
- public override void paint(Cairo.Context ctx) {
- canvas.paint_pixbuf((current_pixbuf != null) ? current_pixbuf : canvas.get_scaled_pixbuf());
-
- /* user_interaction_instance has its radius in user coords, and
- draw_redeye_instance expects active region coords */
- RedeyeInstance active_inst = user_interaction_instance;
- active_inst.center =
- canvas.user_to_active_point(user_interaction_instance.center);
- draw_redeye_instance(active_inst);
- }
-
- public override void on_left_click(int x, int y) {
- Gdk.Rectangle bounds_rect =
- RedeyeInstance.to_bounds_rect(user_interaction_instance);
-
- if (coord_in_rectangle(x, y, bounds_rect)) {
- is_reticle_move_in_progress = true;
- reticle_move_mouse_start_point.x = x;
- reticle_move_mouse_start_point.y = y;
- reticle_move_anchor = user_interaction_instance.center;
- }
- }
-
- public override void on_left_released(int x, int y) {
- is_reticle_move_in_progress = false;
- }
-
- public override void on_motion(int x, int y, Gdk.ModifierType mask) {
- if (is_reticle_move_in_progress) {
-
- Gdk.Rectangle active_region_rect =
- canvas.get_scaled_pixbuf_position();
-
- int x_clamp_low =
- active_region_rect.x + user_interaction_instance.radius + 1;
- int y_clamp_low =
- active_region_rect.y + user_interaction_instance.radius + 1;
- int x_clamp_high =
- active_region_rect.x + active_region_rect.width -
- user_interaction_instance.radius - 1;
- int y_clamp_high =
- active_region_rect.y + active_region_rect.height -
- user_interaction_instance.radius - 1;
-
- int delta_x = x - reticle_move_mouse_start_point.x;
- int delta_y = y - reticle_move_mouse_start_point.y;
-
- user_interaction_instance.center.x = reticle_move_anchor.x +
- delta_x;
- user_interaction_instance.center.y = reticle_move_anchor.y +
- delta_y;
-
- user_interaction_instance.center.x =
- (reticle_move_anchor.x + delta_x).clamp(x_clamp_low,
- x_clamp_high);
- user_interaction_instance.center.y =
- (reticle_move_anchor.y + delta_y).clamp(y_clamp_low,
- y_clamp_high);
-
- canvas.repaint();
- } else {
- Gdk.Rectangle bounds =
- RedeyeInstance.to_bounds_rect(user_interaction_instance);
-
- if (coord_in_rectangle(x, y, bounds)) {
- canvas.set_cursor(Gdk.CursorType.FLEUR);
- } else {
- canvas.set_cursor(Gdk.CursorType.LEFT_PTR);
- }
- }
- }
-
- public override bool on_keypress(Gdk.EventKey event) {
- if ((Gdk.keyval_name(event.keyval) == "KP_Enter") ||
- (Gdk.keyval_name(event.keyval) == "Enter") ||
- (Gdk.keyval_name(event.keyval) == "Return")) {
- on_close();
- return true;
- }
-
- return base.on_keypress(event);
- }
-}
-
-public class AdjustTool : EditingTool {
- private const int SLIDER_WIDTH = 200;
- private const uint SLIDER_DELAY_MSEC = 100;
-
- private class AdjustToolWindow : EditingToolWindow {
- public Gtk.Scale exposure_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- ExposureTransformation.MIN_PARAMETER, ExposureTransformation.MAX_PARAMETER,
- 1.0);
- public Gtk.Scale contrast_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- ContrastTransformation.MIN_PARAMETER, ContrastTransformation.MAX_PARAMETER,
- 1.0);
- public Gtk.Scale saturation_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- SaturationTransformation.MIN_PARAMETER, SaturationTransformation.MAX_PARAMETER,
- 1.0);
- public Gtk.Scale tint_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- TintTransformation.MIN_PARAMETER, TintTransformation.MAX_PARAMETER, 1.0);
- public Gtk.Scale temperature_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- TemperatureTransformation.MIN_PARAMETER, TemperatureTransformation.MAX_PARAMETER,
- 1.0);
-
- public Gtk.Scale shadows_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- ShadowDetailTransformation.MIN_PARAMETER, ShadowDetailTransformation.MAX_PARAMETER,
- 1.0);
-
- public Gtk.Scale highlights_slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
- HighlightDetailTransformation.MIN_PARAMETER, HighlightDetailTransformation.MAX_PARAMETER,
- 1.0);
-
- public Gtk.Button ok_button = new Gtk.Button.with_mnemonic(Resources.OK_LABEL);
- public Gtk.Button reset_button = new Gtk.Button.with_mnemonic(_("_Reset"));
- public Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
- public RGBHistogramManipulator histogram_manipulator = new RGBHistogramManipulator();
-
- public AdjustToolWindow(Gtk.Window container) {
- base(container);
-
- Gtk.Grid slider_organizer = new Gtk.Grid();
- slider_organizer.set_column_homogeneous(false);
- slider_organizer.set_row_spacing(12);
- slider_organizer.set_column_spacing(12);
- slider_organizer.set_margin_start(12);
- slider_organizer.set_margin_bottom(12);
-
- Gtk.Label exposure_label = new Gtk.Label.with_mnemonic(_("Exposure:"));
- exposure_label.halign = Gtk.Align.START;
- exposure_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(exposure_label, 0, 0, 1, 1);
- slider_organizer.attach(exposure_slider, 1, 0, 1, 1);
- exposure_slider.set_size_request(SLIDER_WIDTH, -1);
- exposure_slider.set_value_pos(Gtk.PositionType.RIGHT);
- exposure_slider.set_margin_end(0);
-
- Gtk.Label contrast_label = new Gtk.Label.with_mnemonic(_("Contrast:"));
- contrast_label.halign = Gtk.Align.START;
- contrast_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(contrast_label, 0, 1, 1, 1);
- slider_organizer.attach(contrast_slider, 1, 1, 1, 1);
- contrast_slider.set_size_request(SLIDER_WIDTH, -1);
- contrast_slider.set_value_pos(Gtk.PositionType.RIGHT);
- contrast_slider.set_margin_end(0);
-
- Gtk.Label saturation_label = new Gtk.Label.with_mnemonic(_("Saturation:"));
- saturation_label.halign = Gtk.Align.START;
- saturation_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(saturation_label, 0, 2, 1, 1);
- slider_organizer.attach(saturation_slider, 1, 2, 1, 1);
- saturation_slider.set_size_request(SLIDER_WIDTH, -1);
- saturation_slider.set_draw_value(false);
- saturation_slider.set_margin_end(0);
-
- Gtk.Label tint_label = new Gtk.Label.with_mnemonic(_("Tint:"));
- tint_label.halign = Gtk.Align.START;
- tint_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(tint_label, 0, 3, 1, 1);
- slider_organizer.attach(tint_slider, 1, 3, 1, 1);
- tint_slider.set_size_request(SLIDER_WIDTH, -1);
- tint_slider.set_value_pos(Gtk.PositionType.RIGHT);
- tint_slider.set_margin_end(0);
-
- Gtk.Label temperature_label =
- new Gtk.Label.with_mnemonic(_("Temperature:"));
- temperature_label.halign = Gtk.Align.START;
- temperature_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(temperature_label, 0, 4, 1, 1);
- slider_organizer.attach(temperature_slider, 1, 4, 1, 1);
- temperature_slider.set_size_request(SLIDER_WIDTH, -1);
- temperature_slider.set_value_pos(Gtk.PositionType.RIGHT);
- temperature_slider.set_margin_end(0);
-
- Gtk.Label shadows_label = new Gtk.Label.with_mnemonic(_("Shadows:"));
- shadows_label.halign = Gtk.Align.START;
- shadows_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(shadows_label, 0, 5, 1, 1);
- slider_organizer.attach(shadows_slider, 1, 5, 1, 1);
- shadows_slider.set_size_request(SLIDER_WIDTH, -1);
- shadows_slider.set_value_pos(Gtk.PositionType.RIGHT);
- // FIXME: Hack to make the slider the same length as the other. Find out why it is aligned
- // Differently (probably because it only has positive values)
- shadows_slider.set_margin_end(5);
-
- Gtk.Label highlights_label = new Gtk.Label.with_mnemonic(_("Highlights:"));
- highlights_label.halign = Gtk.Align.START;
- highlights_label.valign = Gtk.Align.CENTER;
- slider_organizer.attach(highlights_label, 0, 6, 1, 1);
- slider_organizer.attach(highlights_slider, 1, 6, 1, 1);
- highlights_slider.set_size_request(SLIDER_WIDTH, -1);
- highlights_slider.set_value_pos(Gtk.PositionType.RIGHT);
- highlights_slider.set_margin_end(0);
-
- Gtk.Box button_layouter = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
- button_layouter.set_homogeneous(true);
- button_layouter.pack_start(cancel_button, true, true, 1);
- button_layouter.pack_start(reset_button, true, true, 1);
- button_layouter.pack_start(ok_button, true, true, 1);
-
- histogram_manipulator.set_margin_start (12);
- histogram_manipulator.set_margin_end (12);
- histogram_manipulator.set_margin_top (12);
- histogram_manipulator.set_margin_bottom (8);
-
- Gtk.Box pane_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 8);
- pane_layouter.add(histogram_manipulator);
- pane_layouter.add(slider_organizer);
- pane_layouter.add(button_layouter);
- pane_layouter.set_child_packing(histogram_manipulator, false, true, 0, Gtk.PackType.START);
-
- add(pane_layouter);
- }
- }
-
- private abstract class AdjustToolCommand : Command {
- protected weak AdjustTool owner;
-
- protected AdjustToolCommand(AdjustTool owner, string name, string explanation) {
- base (name, explanation);
-
- this.owner = owner;
- owner.deactivated.connect(on_owner_deactivated);
- }
-
- ~AdjustToolCommand() {
- if (owner != null)
- owner.deactivated.disconnect(on_owner_deactivated);
- }
-
- private void on_owner_deactivated() {
- // This reset call is by design. See notes on ticket #1946 if this is undesirable or if
- // you are planning to change it.
- AppWindow.get_command_manager().reset();
- }
- }
-
- private class AdjustResetCommand : AdjustToolCommand {
- private PixelTransformationBundle original;
- private PixelTransformationBundle reset;
-
- public AdjustResetCommand(AdjustTool owner, PixelTransformationBundle current) {
- base (owner, _("Reset Colors"), _("Reset all color adjustments to original"));
-
- original = current.copy();
- reset = new PixelTransformationBundle();
- reset.set_to_identity();
- }
-
- public override void execute() {
- owner.set_adjustments(reset);
- }
-
- public override void undo() {
- owner.set_adjustments(original);
- }
-
- public override bool compress(Command command) {
- AdjustResetCommand reset_command = command as AdjustResetCommand;
- if (reset_command == null)
- return false;
-
- if (reset_command.owner != owner)
- return false;
-
- // multiple successive resets on the same photo as good as a single
- return true;
- }
- }
-
- private class SliderAdjustmentCommand : AdjustToolCommand {
- private PixelTransformationType transformation_type;
- private PixelTransformation new_transformation;
- private PixelTransformation old_transformation;
-
- public SliderAdjustmentCommand(AdjustTool owner, PixelTransformation old_transformation,
- PixelTransformation new_transformation, string name) {
- base(owner, name, name);
-
- this.old_transformation = old_transformation;
- this.new_transformation = new_transformation;
- transformation_type = old_transformation.get_transformation_type();
- assert(new_transformation.get_transformation_type() == transformation_type);
- }
-
- public override void execute() {
- // don't update slider; it's been moved by the user
- owner.update_transformation(new_transformation);
- owner.canvas.repaint();
- }
-
- public override void undo() {
- owner.update_transformation(old_transformation);
-
- owner.unbind_window_handlers();
- owner.update_slider(old_transformation);
- owner.bind_window_handlers();
-
- owner.canvas.repaint();
- }
-
- public override void redo() {
- owner.update_transformation(new_transformation);
-
- owner.unbind_window_handlers();
- owner.update_slider(new_transformation);
- owner.bind_window_handlers();
-
- owner.canvas.repaint();
- }
-
- public override bool compress(Command command) {
- SliderAdjustmentCommand slider_adjustment = command as SliderAdjustmentCommand;
- if (slider_adjustment == null)
- return false;
-
- // same photo
- if (slider_adjustment.owner != owner)
- return false;
-
- // same adjustment
- if (slider_adjustment.transformation_type != transformation_type)
- return false;
-
- // execute the command
- slider_adjustment.execute();
-
- // save it's transformation as ours
- new_transformation = slider_adjustment.new_transformation;
-
- return true;
- }
- }
-
- private class AdjustEnhanceCommand : AdjustToolCommand {
- private Photo photo;
- private PixelTransformationBundle original;
- private PixelTransformationBundle enhanced = null;
-
- public AdjustEnhanceCommand(AdjustTool owner, Photo photo) {
- base(owner, Resources.ENHANCE_LABEL, Resources.ENHANCE_TOOLTIP);
-
- this.photo = photo;
- original = photo.get_color_adjustments();
- }
-
- public override void execute() {
- if (enhanced == null)
- enhanced = photo.get_enhance_transformations();
-
- owner.set_adjustments(enhanced);
- }
-
- public override void undo() {
- owner.set_adjustments(original);
- }
-
- public override bool compress(Command command) {
- // can compress both normal enhance and one with the adjust tool running
- EnhanceSingleCommand enhance_single = command as EnhanceSingleCommand;
- if (enhance_single != null) {
- Photo photo = (Photo) enhance_single.get_source();
-
- // multiple successive enhances are as good as a single, as long as it's on the
- // same photo
- return photo.equals(owner.canvas.get_photo());
- }
-
- AdjustEnhanceCommand enhance_command = command as AdjustEnhanceCommand;
- if (enhance_command == null)
- return false;
-
- if (enhance_command.owner != owner)
- return false;
-
- // multiple successive as good as a single
- return true;
- }
- }
-
- private AdjustToolWindow adjust_tool_window = null;
- private bool suppress_effect_redraw = false;
- private Gdk.Pixbuf draw_to_pixbuf = null;
- private Gdk.Pixbuf histogram_pixbuf = null;
- private Gdk.Pixbuf virgin_histogram_pixbuf = null;
- private PixelTransformer transformer = null;
- private PixelTransformer histogram_transformer = null;
- private PixelTransformationBundle transformations = null;
- private float[] fp_pixel_cache = null;
- private bool disable_histogram_refresh = false;
- private OneShotScheduler? temperature_scheduler = null;
- private OneShotScheduler? tint_scheduler = null;
- private OneShotScheduler? contrast_scheduler = null;
- private OneShotScheduler? saturation_scheduler = null;
- private OneShotScheduler? exposure_scheduler = null;
- private OneShotScheduler? shadows_scheduler = null;
- private OneShotScheduler? highlights_scheduler = null;
-
- private AdjustTool() {
- base("AdjustTool");
- }
-
- public static AdjustTool factory() {
- return new AdjustTool();
- }
-
- public static bool is_available(Photo photo, Scaling scaling) {
- return true;
- }
-
- public override void activate(PhotoCanvas canvas) {
- adjust_tool_window = new AdjustToolWindow(canvas.get_container());
-
- Photo photo = canvas.get_photo();
- transformations = photo.get_color_adjustments();
- transformer = transformations.generate_transformer();
-
- // the histogram transformer uses all transformations but contrast expansion
- histogram_transformer = new PixelTransformer();
-
- /* set up expansion */
- ExpansionTransformation expansion_trans = (ExpansionTransformation)
- transformations.get_transformation(PixelTransformationType.TONE_EXPANSION);
- adjust_tool_window.histogram_manipulator.set_left_nub_position(
- expansion_trans.get_black_point());
- adjust_tool_window.histogram_manipulator.set_right_nub_position(
- expansion_trans.get_white_point());
-
- /* set up shadows */
- ShadowDetailTransformation shadows_trans = (ShadowDetailTransformation)
- transformations.get_transformation(PixelTransformationType.SHADOWS);
- histogram_transformer.attach_transformation(shadows_trans);
- adjust_tool_window.shadows_slider.set_value(shadows_trans.get_parameter());
-
- /* set up highlights */
- HighlightDetailTransformation highlights_trans = (HighlightDetailTransformation)
- transformations.get_transformation(PixelTransformationType.HIGHLIGHTS);
- histogram_transformer.attach_transformation(highlights_trans);
- adjust_tool_window.highlights_slider.set_value(highlights_trans.get_parameter());
-
- /* set up temperature & tint */
- TemperatureTransformation temp_trans = (TemperatureTransformation)
- transformations.get_transformation(PixelTransformationType.TEMPERATURE);
- histogram_transformer.attach_transformation(temp_trans);
- adjust_tool_window.temperature_slider.set_value(temp_trans.get_parameter());
-
- TintTransformation tint_trans = (TintTransformation)
- transformations.get_transformation(PixelTransformationType.TINT);
- histogram_transformer.attach_transformation(tint_trans);
- adjust_tool_window.tint_slider.set_value(tint_trans.get_parameter());
-
- /* set up saturation */
- SaturationTransformation sat_trans = (SaturationTransformation)
- transformations.get_transformation(PixelTransformationType.SATURATION);
- histogram_transformer.attach_transformation(sat_trans);
- adjust_tool_window.saturation_slider.set_value(sat_trans.get_parameter());
-
- /* set up exposure */
- ExposureTransformation exposure_trans = (ExposureTransformation)
- transformations.get_transformation(PixelTransformationType.EXPOSURE);
- histogram_transformer.attach_transformation(exposure_trans);
- adjust_tool_window.exposure_slider.set_value(exposure_trans.get_parameter());
-
- /* set up contrast */
- ContrastTransformation contrast_trans = (ContrastTransformation)
- transformations.get_transformation(PixelTransformationType.CONTRAST);
- histogram_transformer.attach_transformation(contrast_trans);
- adjust_tool_window.contrast_slider.set_value(contrast_trans.get_parameter());
-
- bind_canvas_handlers(canvas);
- bind_window_handlers();
-
- draw_to_pixbuf = canvas.get_scaled_pixbuf().copy();
- init_fp_pixel_cache(canvas.get_scaled_pixbuf());
-
- /* if we have an 1x1 pixel image, then there's no need to deal with recomputing the
- histogram, because a histogram for a 1x1 image is meaningless. The histogram shows the
- distribution of color over all the many pixels in an image, but if an image only has
- one pixel, the notion of a "distribution over pixels" makes no sense. */
- if (draw_to_pixbuf.width == 1 && draw_to_pixbuf.height == 1)
- disable_histogram_refresh = true;
-
- /* don't sample the original image to create the histogram if the original image is
- sufficiently large -- if it's over 8k pixels, then we'll get pretty much the same
- histogram if we sample from a half-size image */
- if (((draw_to_pixbuf.width * draw_to_pixbuf.height) > 8192) && (draw_to_pixbuf.width > 1) &&
- (draw_to_pixbuf.height > 1)) {
- histogram_pixbuf = draw_to_pixbuf.scale_simple(draw_to_pixbuf.width / 2,
- draw_to_pixbuf.height / 2, Gdk.InterpType.HYPER);
- } else {
- histogram_pixbuf = draw_to_pixbuf.copy();
- }
- virgin_histogram_pixbuf = histogram_pixbuf.copy();
-
- DataCollection? owner = canvas.get_photo().get_membership();
- if (owner != null)
- owner.items_altered.connect(on_photos_altered);
-
- base.activate(canvas);
- }
-
- public override EditingToolWindow? get_tool_window() {
- return adjust_tool_window;
- }
-
- public override void deactivate() {
- if (canvas != null) {
- DataCollection? owner = canvas.get_photo().get_membership();
- if (owner != null)
- owner.items_altered.disconnect(on_photos_altered);
-
- unbind_canvas_handlers(canvas);
- }
-
- if (adjust_tool_window != null) {
- unbind_window_handlers();
- adjust_tool_window.hide();
- adjust_tool_window.destroy();
- adjust_tool_window = null;
- }
-
- draw_to_pixbuf = null;
- fp_pixel_cache = null;
-
- base.deactivate();
- }
-
- public override void paint(Cairo.Context ctx) {
- if (!suppress_effect_redraw) {
- transformer.transform_from_fp(ref fp_pixel_cache, draw_to_pixbuf);
- histogram_transformer.transform_to_other_pixbuf(virgin_histogram_pixbuf,
- histogram_pixbuf);
- if (!disable_histogram_refresh)
- adjust_tool_window.histogram_manipulator.update_histogram(histogram_pixbuf);
- }
-
- canvas.paint_pixbuf(draw_to_pixbuf);
- }
-
- public override Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
- out Dimensions max_dim) throws Error {
- if (!photo.has_color_adjustments()) {
- max_dim = Dimensions();
-
- return null;
- }
-
- max_dim = photo.get_dimensions();
-
- return photo.get_pixbuf_with_options(scaling, Photo.Exception.ADJUST);
- }
-
- private void on_reset() {
- AdjustResetCommand command = new AdjustResetCommand(this, transformations);
- AppWindow.get_command_manager().execute(command);
- }
-
- private void on_ok() {
- suppress_effect_redraw = true;
-
- get_tool_window().hide();
-
- applied(new AdjustColorsSingleCommand(canvas.get_photo(), transformations,
- Resources.ADJUST_LABEL, Resources.ADJUST_TOOLTIP), draw_to_pixbuf,
- canvas.get_photo().get_dimensions(), false);
- }
-
- private void update_transformations(PixelTransformationBundle new_transformations) {
- foreach (PixelTransformation transformation in new_transformations.get_transformations())
- update_transformation(transformation);
- }
-
- private void update_transformation(PixelTransformation new_transformation) {
- PixelTransformation old_transformation = transformations.get_transformation(
- new_transformation.get_transformation_type());
-
- transformer.replace_transformation(old_transformation, new_transformation);
- if (new_transformation.get_transformation_type() != PixelTransformationType.TONE_EXPANSION)
- histogram_transformer.replace_transformation(old_transformation, new_transformation);
-
- transformations.set(new_transformation);
- }
-
- private void slider_updated(PixelTransformation new_transformation, string name) {
- PixelTransformation old_transformation = transformations.get_transformation(
- new_transformation.get_transformation_type());
- SliderAdjustmentCommand command = new SliderAdjustmentCommand(this, old_transformation,
- new_transformation, name);
- AppWindow.get_command_manager().execute(command);
- }
-
- private void on_temperature_adjustment() {
- if (temperature_scheduler == null)
- temperature_scheduler = new OneShotScheduler("temperature", on_delayed_temperature_adjustment);
-
- temperature_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_temperature_adjustment() {
- TemperatureTransformation new_temp_trans = new TemperatureTransformation(
- (float) adjust_tool_window.temperature_slider.get_value());
- slider_updated(new_temp_trans, _("Temperature"));
- }
-
- private void on_tint_adjustment() {
- if (tint_scheduler == null)
- tint_scheduler = new OneShotScheduler("tint", on_delayed_tint_adjustment);
- tint_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_tint_adjustment() {
- TintTransformation new_tint_trans = new TintTransformation(
- (float) adjust_tool_window.tint_slider.get_value());
- slider_updated(new_tint_trans, _("Tint"));
- }
-
- private void on_contrast_adjustment() {
- if (this.contrast_scheduler == null)
- this.contrast_scheduler = new OneShotScheduler("contrast", on_delayed_contrast_adjustment);
- this.contrast_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_contrast_adjustment() {
- ContrastTransformation new_exp_trans = new ContrastTransformation(
- (float) adjust_tool_window.contrast_slider.get_value());
- slider_updated(new_exp_trans, _("Contrast"));
- }
-
-
- private void on_saturation_adjustment() {
- if (saturation_scheduler == null)
- saturation_scheduler = new OneShotScheduler("saturation", on_delayed_saturation_adjustment);
-
- saturation_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_saturation_adjustment() {
- SaturationTransformation new_sat_trans = new SaturationTransformation(
- (float) adjust_tool_window.saturation_slider.get_value());
- slider_updated(new_sat_trans, _("Saturation"));
- }
-
- private void on_exposure_adjustment() {
- if (exposure_scheduler == null)
- exposure_scheduler = new OneShotScheduler("exposure", on_delayed_exposure_adjustment);
-
- exposure_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_exposure_adjustment() {
- ExposureTransformation new_exp_trans = new ExposureTransformation(
- (float) adjust_tool_window.exposure_slider.get_value());
- slider_updated(new_exp_trans, _("Exposure"));
- }
-
- private void on_shadows_adjustment() {
- if (shadows_scheduler == null)
- shadows_scheduler = new OneShotScheduler("shadows", on_delayed_shadows_adjustment);
-
- shadows_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_shadows_adjustment() {
- ShadowDetailTransformation new_shadows_trans = new ShadowDetailTransformation(
- (float) adjust_tool_window.shadows_slider.get_value());
- slider_updated(new_shadows_trans, _("Shadows"));
- }
-
- private void on_highlights_adjustment() {
- if (highlights_scheduler == null)
- highlights_scheduler = new OneShotScheduler("highlights", on_delayed_highlights_adjustment);
-
- highlights_scheduler.after_timeout(SLIDER_DELAY_MSEC, true);
- }
-
- private void on_delayed_highlights_adjustment() {
- HighlightDetailTransformation new_highlights_trans = new HighlightDetailTransformation(
- (float) adjust_tool_window.highlights_slider.get_value());
- slider_updated(new_highlights_trans, _("Highlights"));
- }
-
- private void on_histogram_constraint() {
- int expansion_black_point =
- adjust_tool_window.histogram_manipulator.get_left_nub_position();
- int expansion_white_point =
- adjust_tool_window.histogram_manipulator.get_right_nub_position();
- ExpansionTransformation new_exp_trans =
- new ExpansionTransformation.from_extrema(expansion_black_point, expansion_white_point);
- slider_updated(new_exp_trans, _("Contrast Expansion"));
- }
-
- private void on_canvas_resize() {
- draw_to_pixbuf = canvas.get_scaled_pixbuf().copy();
- init_fp_pixel_cache(canvas.get_scaled_pixbuf());
- }
-
- private bool on_hscale_reset(Gtk.Widget widget, Gdk.EventButton event) {
- Gtk.Scale source = (Gtk.Scale) widget;
-
- if (event.button == 1 && event.type == Gdk.EventType.BUTTON_PRESS
- && has_only_key_modifier(event.state, Gdk.ModifierType.CONTROL_MASK)) {
- // Left Mouse Button and CTRL pressed
- source.set_value(0);
-
- return true;
- }
-
- return false;
- }
-
- private void bind_canvas_handlers(PhotoCanvas canvas) {
- canvas.resized_scaled_pixbuf.connect(on_canvas_resize);
- }
-
- private void unbind_canvas_handlers(PhotoCanvas canvas) {
- canvas.resized_scaled_pixbuf.disconnect(on_canvas_resize);
- }
-
- private void bind_window_handlers() {
- adjust_tool_window.ok_button.clicked.connect(on_ok);
- adjust_tool_window.reset_button.clicked.connect(on_reset);
- adjust_tool_window.cancel_button.clicked.connect(notify_cancel);
- adjust_tool_window.exposure_slider.value_changed.connect(on_exposure_adjustment);
- adjust_tool_window.contrast_slider.value_changed.connect(on_contrast_adjustment);
- adjust_tool_window.saturation_slider.value_changed.connect(on_saturation_adjustment);
- adjust_tool_window.tint_slider.value_changed.connect(on_tint_adjustment);
- adjust_tool_window.temperature_slider.value_changed.connect(on_temperature_adjustment);
- adjust_tool_window.shadows_slider.value_changed.connect(on_shadows_adjustment);
- adjust_tool_window.highlights_slider.value_changed.connect(on_highlights_adjustment);
- adjust_tool_window.histogram_manipulator.nub_position_changed.connect(on_histogram_constraint);
-
- adjust_tool_window.saturation_slider.button_press_event.connect(on_hscale_reset);
- adjust_tool_window.exposure_slider.button_press_event.connect(on_hscale_reset);
- adjust_tool_window.contrast_slider.button_press_event.connect(on_hscale_reset);
- adjust_tool_window.tint_slider.button_press_event.connect(on_hscale_reset);
- adjust_tool_window.temperature_slider.button_press_event.connect(on_hscale_reset);
- adjust_tool_window.shadows_slider.button_press_event.connect(on_hscale_reset);
- adjust_tool_window.highlights_slider.button_press_event.connect(on_hscale_reset);
- }
-
- private void unbind_window_handlers() {
- adjust_tool_window.ok_button.clicked.disconnect(on_ok);
- adjust_tool_window.reset_button.clicked.disconnect(on_reset);
- adjust_tool_window.cancel_button.clicked.disconnect(notify_cancel);
- adjust_tool_window.exposure_slider.value_changed.disconnect(on_exposure_adjustment);
- adjust_tool_window.contrast_slider.value_changed.disconnect(on_contrast_adjustment);
- adjust_tool_window.saturation_slider.value_changed.disconnect(on_saturation_adjustment);
- adjust_tool_window.tint_slider.value_changed.disconnect(on_tint_adjustment);
- adjust_tool_window.temperature_slider.value_changed.disconnect(on_temperature_adjustment);
- adjust_tool_window.shadows_slider.value_changed.disconnect(on_shadows_adjustment);
- adjust_tool_window.highlights_slider.value_changed.disconnect(on_highlights_adjustment);
- adjust_tool_window.histogram_manipulator.nub_position_changed.disconnect(on_histogram_constraint);
-
- adjust_tool_window.saturation_slider.button_press_event.disconnect(on_hscale_reset);
- adjust_tool_window.exposure_slider.button_press_event.disconnect(on_hscale_reset);
- adjust_tool_window.contrast_slider.button_press_event.disconnect(on_hscale_reset);
- adjust_tool_window.tint_slider.button_press_event.disconnect(on_hscale_reset);
- adjust_tool_window.temperature_slider.button_press_event.disconnect(on_hscale_reset);
- adjust_tool_window.shadows_slider.button_press_event.disconnect(on_hscale_reset);
- adjust_tool_window.highlights_slider.button_press_event.disconnect(on_hscale_reset);
- }
-
- public bool enhance() {
- AdjustEnhanceCommand command = new AdjustEnhanceCommand(this, canvas.get_photo());
- AppWindow.get_command_manager().execute(command);
-
- return true;
- }
-
- private void on_photos_altered(Gee.Map<DataObject, Alteration> map) {
- if (!map.has_key(canvas.get_photo()))
- return;
-
- PixelTransformationBundle adjustments = canvas.get_photo().get_color_adjustments();
- set_adjustments(adjustments);
- }
-
- private void set_adjustments(PixelTransformationBundle new_adjustments) {
- unbind_window_handlers();
-
- update_transformations(new_adjustments);
-
- foreach (PixelTransformation adjustment in new_adjustments.get_transformations())
- update_slider(adjustment);
-
- bind_window_handlers();
- canvas.repaint();
- }
-
- // Note that window handlers should be unbound (unbind_window_handlers) prior to calling this
- // if the caller doesn't want the widget's signals to fire with the change.
- private void update_slider(PixelTransformation transformation) {
- switch (transformation.get_transformation_type()) {
- case PixelTransformationType.TONE_EXPANSION:
- ExpansionTransformation expansion = (ExpansionTransformation) transformation;
-
- if (!disable_histogram_refresh) {
- adjust_tool_window.histogram_manipulator.set_left_nub_position(
- expansion.get_black_point());
- adjust_tool_window.histogram_manipulator.set_right_nub_position(
- expansion.get_white_point());
- }
- break;
-
- case PixelTransformationType.SHADOWS:
- adjust_tool_window.shadows_slider.set_value(
- ((ShadowDetailTransformation) transformation).get_parameter());
- break;
-
- case PixelTransformationType.CONTRAST:
- adjust_tool_window.contrast_slider.set_value(
- ((ContrastTransformation) transformation).get_parameter());
- break;
-
- case PixelTransformationType.HIGHLIGHTS:
- adjust_tool_window.highlights_slider.set_value(
- ((HighlightDetailTransformation) transformation).get_parameter());
- break;
-
- case PixelTransformationType.EXPOSURE:
- adjust_tool_window.exposure_slider.set_value(
- ((ExposureTransformation) transformation).get_parameter());
- break;
-
- case PixelTransformationType.SATURATION:
- adjust_tool_window.saturation_slider.set_value(
- ((SaturationTransformation) transformation).get_parameter());
- break;
-
- case PixelTransformationType.TINT:
- adjust_tool_window.tint_slider.set_value(
- ((TintTransformation) transformation).get_parameter());
- break;
-
- case PixelTransformationType.TEMPERATURE:
- adjust_tool_window.temperature_slider.set_value(
- ((TemperatureTransformation) transformation).get_parameter());
- break;
-
- default:
- error("Unknown adjustment: %d", (int) transformation.get_transformation_type());
- }
- }
-
- private void init_fp_pixel_cache(Gdk.Pixbuf source) {
- int source_width = source.get_width();
- int source_height = source.get_height();
- int source_num_channels = source.get_n_channels();
- int source_rowstride = source.get_rowstride();
- unowned uchar[] source_pixels = source.get_pixels();
-
- fp_pixel_cache = new float[3 * source_width * source_height];
- int cache_pixel_index = 0;
-
- for (int j = 0; j < source_height; j++) {
- int row_start_index = j * source_rowstride;
- int row_end_index = row_start_index + (source_width * source_num_channels);
- for (int i = row_start_index; i < row_end_index; i += source_num_channels) {
- fp_pixel_cache[cache_pixel_index++] = rgb_lookup_table[source_pixels[i]];
- fp_pixel_cache[cache_pixel_index++] = rgb_lookup_table[source_pixels[i + 1]];
- fp_pixel_cache[cache_pixel_index++] = rgb_lookup_table[source_pixels[i + 2]];
- }
- }
- }
-
- public override bool on_keypress(Gdk.EventKey event) {
- if ((Gdk.keyval_name(event.keyval) == "KP_Enter") ||
- (Gdk.keyval_name(event.keyval) == "Enter") ||
- (Gdk.keyval_name(event.keyval) == "Return")) {
- on_ok();
- return true;
- }
-
- return base.on_keypress(event);
- }
-}
-
-
-}
-
+}
\ No newline at end of file
diff --git a/src/editing_tools/PhotoCanvas.vala b/src/editing_tools/PhotoCanvas.vala
new file mode 100644
index 00000000..c0b9e9c5
--- /dev/null
+++ b/src/editing_tools/PhotoCanvas.vala
@@ -0,0 +1,351 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// The PhotoCanvas is an interface object between an EditingTool and its host. It provides objects
+// and primitives for an EditingTool to obtain information about the image, to draw on the host's
+// canvas, and to be signalled when the canvas and its pixbuf changes (is resized).
+public abstract class PhotoCanvas {
+ private Gtk.Window container;
+ private Gdk.Surface drawing_window;
+ private Photo photo;
+ private Cairo.Context default_ctx;
+ private Dimensions surface_dim;
+ private Cairo.Surface scaled;
+ private Gdk.Pixbuf scaled_pixbuf;
+ private Gdk.Rectangle scaled_position;
+
+ protected PhotoCanvas(Gtk.Window container, Gdk.Surface drawing_window, Photo photo,
+ Cairo.Context default_ctx, Dimensions surface_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position)
{
+ this.container = container;
+ this.drawing_window = drawing_window;
+ this.photo = photo;
+ this.default_ctx = default_ctx;
+ this.surface_dim = surface_dim;
+ this.scaled_position = scaled_position;
+ this.scaled_pixbuf = scaled;
+ this.scaled = pixbuf_to_surface(default_ctx, scaled, scaled_position);
+ }
+
+ public signal void new_surface(Cairo.Context ctx, Dimensions dim);
+
+ public signal void resized_scaled_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled,
+ Gdk.Rectangle scaled_position);
+
+ public Gdk.Rectangle unscaled_to_raw_rect(Gdk.Rectangle rectangle) {
+ return photo.unscaled_to_raw_rect(rectangle);
+ }
+
+ public Gdk.Point active_to_unscaled_point(Gdk.Point active_point) {
+ Gdk.Rectangle scaled_position = get_scaled_pixbuf_position();
+ Dimensions unscaled_dims = photo.get_dimensions();
+
+ double scale_factor_x = ((double) unscaled_dims.width) /
+ ((double) scaled_position.width);
+ double scale_factor_y = ((double) unscaled_dims.height) /
+ ((double) scaled_position.height);
+
+ Gdk.Point result = {0};
+ result.x = (int)(((double) active_point.x) * scale_factor_x + 0.5);
+ result.y = (int)(((double) active_point.y) * scale_factor_y + 0.5);
+
+ return result;
+ }
+
+ public Gdk.Rectangle active_to_unscaled_rect(Gdk.Rectangle active_rect) {
+ Gdk.Point upper_left = {0};
+ Gdk.Point lower_right = {0};
+ upper_left.x = active_rect.x;
+ upper_left.y = active_rect.y;
+ lower_right.x = upper_left.x + active_rect.width;
+ lower_right.y = upper_left.y + active_rect.height;
+
+ upper_left = active_to_unscaled_point(upper_left);
+ lower_right = active_to_unscaled_point(lower_right);
+
+ Gdk.Rectangle unscaled_rect = Gdk.Rectangle();
+ unscaled_rect.x = upper_left.x;
+ unscaled_rect.y = upper_left.y;
+ unscaled_rect.width = lower_right.x - upper_left.x;
+ unscaled_rect.height = lower_right.y - upper_left.y;
+
+ return unscaled_rect;
+ }
+
+ public Gdk.Point user_to_active_point(Gdk.Point user_point) {
+ Gdk.Rectangle active_offsets = get_scaled_pixbuf_position();
+
+ Gdk.Point result = {0};
+ result.x = user_point.x - active_offsets.x;
+ result.y = user_point.y - active_offsets.y;
+
+ return result;
+ }
+
+ public Gdk.Rectangle user_to_active_rect(Gdk.Rectangle user_rect) {
+ Gdk.Point upper_left = {0};
+ Gdk.Point lower_right = {0};
+ upper_left.x = user_rect.x;
+ upper_left.y = user_rect.y;
+ lower_right.x = upper_left.x + user_rect.width;
+ lower_right.y = upper_left.y + user_rect.height;
+
+ upper_left = user_to_active_point(upper_left);
+ lower_right = user_to_active_point(lower_right);
+
+ Gdk.Rectangle active_rect = Gdk.Rectangle();
+ active_rect.x = upper_left.x;
+ active_rect.y = upper_left.y;
+ active_rect.width = lower_right.x - upper_left.x;
+ active_rect.height = lower_right.y - upper_left.y;
+
+ return active_rect;
+ }
+
+ public Photo get_photo() {
+ return photo;
+ }
+
+ public Gtk.Window get_container() {
+ return container;
+ }
+
+ public Gdk.Surface get_drawing_window() {
+ return drawing_window;
+ }
+
+ public Cairo.Context get_default_ctx() {
+ return default_ctx;
+ }
+
+ public Dimensions get_surface_dim() {
+ return surface_dim;
+ }
+
+ public Scaling get_scaling() {
+ return Scaling.for_viewport(surface_dim, false);
+ }
+
+ public void set_surface(Cairo.Context default_ctx, Dimensions surface_dim) {
+ this.default_ctx = default_ctx;
+ this.surface_dim = surface_dim;
+
+ new_surface(default_ctx, surface_dim);
+ }
+
+ public Cairo.Surface get_scaled_surface() {
+ return scaled;
+ }
+
+ public Gdk.Pixbuf get_scaled_pixbuf() {
+ return scaled_pixbuf;
+ }
+
+ public Gdk.Rectangle get_scaled_pixbuf_position() {
+ return scaled_position;
+ }
+
+ public void resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
+ this.scaled = pixbuf_to_surface(default_ctx, scaled, scaled_position);
+ this.scaled_pixbuf = scaled;
+ this.scaled_position = scaled_position;
+
+ resized_scaled_pixbuf(old_dim, scaled, scaled_position);
+ }
+
+ public abstract void repaint();
+
+ // Because the editing tool should not have any need to draw on the gutters outside the photo,
+ // and it's a pain to constantly calculate where it's laid out on the drawable, these convenience
+ // methods automatically adjust for its position.
+ //
+ // If these methods are not used, all painting to the drawable should be offet by
+ // get_scaled_pixbuf_position().x and get_scaled_pixbuf_position().y
+ public void paint_pixbuf(Gdk.Pixbuf pixbuf) {
+ default_ctx.save();
+
+ // paint black background
+ set_source_color_from_string(default_ctx, "#000");
+ default_ctx.rectangle(0, 0, surface_dim.width, surface_dim.height);
+ default_ctx.fill();
+
+ // paint the actual image
+ paint_pixmap_with_background(default_ctx, pixbuf, scaled_position.x, scaled_position.y);
+ default_ctx.restore();
+ }
+
+ // Paint a surface on top of the photo
+ public void paint_surface(Cairo.Surface surface, bool over) {
+ default_ctx.save();
+ if (over == false)
+ default_ctx.set_operator(Cairo.Operator.SOURCE);
+ else
+ default_ctx.set_operator(Cairo.Operator.OVER);
+
+ default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
+ default_ctx.paint();
+ default_ctx.set_source_surface(surface, scaled_position.x, scaled_position.y);
+ default_ctx.paint();
+ default_ctx.restore();
+ }
+
+ public void paint_surface_area(Cairo.Surface surface, Box source_area, bool over) {
+ default_ctx.save();
+ if (over == false)
+ default_ctx.set_operator(Cairo.Operator.SOURCE);
+ else
+ default_ctx.set_operator(Cairo.Operator.OVER);
+
+ default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
+ default_ctx.rectangle(scaled_position.x + source_area.left,
+ scaled_position.y + source_area.top,
+ source_area.get_width(), source_area.get_height());
+ default_ctx.fill();
+
+ default_ctx.set_source_surface(surface, scaled_position.x, scaled_position.y);
+ default_ctx.rectangle(scaled_position.x + source_area.left,
+ scaled_position.y + source_area.top,
+ source_area.get_width(), source_area.get_height());
+ default_ctx.fill();
+ default_ctx.restore();
+ }
+
+ public void draw_box(Cairo.Context ctx, Box box) {
+ Gdk.Rectangle rect = box.get_rectangle();
+ rect.x += scaled_position.x;
+ rect.y += scaled_position.y;
+
+ ctx.rectangle(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1);
+ ctx.stroke();
+ }
+
+ public void draw_text(Cairo.Context ctx, string text, int x, int y, bool use_scaled_pos = true) {
+ if (use_scaled_pos) {
+ x += scaled_position.x;
+ y += scaled_position.y;
+ }
+ Cairo.TextExtents extents;
+ ctx.text_extents(text, out extents);
+ x -= (int) extents.width / 2;
+
+ set_source_color_from_string(ctx, Resources.ONIMAGE_FONT_BACKGROUND);
+
+ int pane_border = 5; // border around edge of pane in pixels
+ ctx.rectangle(x - pane_border, y - pane_border - extents.height,
+ extents.width + 2 * pane_border,
+ extents.height + 2 * pane_border);
+ ctx.fill();
+
+ ctx.move_to(x, y);
+ set_source_color_from_string(ctx, Resources.ONIMAGE_FONT_COLOR);
+ ctx.show_text(text);
+ }
+
+ /**
+ * Draw a horizontal line into the specified Cairo context at the specified position, taking
+ * into account the scaled position of the image unless directed otherwise.
+ *
+ * @param ctx The drawing context of the surface we're drawing to.
+ * @param x The horizontal position to place the line at.
+ * @param y The vertical position to place the line at.
+ * @param width The length of the line.
+ * @param use_scaled_pos Whether to use absolute window positioning or take into account the
+ * position of the scaled image.
+ */
+ public void draw_horizontal_line(Cairo.Context ctx, int x, int y, int width, bool use_scaled_pos = true)
{
+ if (use_scaled_pos) {
+ x += scaled_position.x;
+ y += scaled_position.y;
+ }
+
+ ctx.move_to(x + 0.5, y + 0.5);
+ ctx.line_to(x + width - 1, y + 0.5);
+ ctx.stroke();
+ }
+
+ /**
+ * Draw a vertical line into the specified Cairo context at the specified position, taking
+ * into account the scaled position of the image unless directed otherwise.
+ *
+ * @param ctx The drawing context of the surface we're drawing to.
+ * @param x The horizontal position to place the line at.
+ * @param y The vertical position to place the line at.
+ * @param width The length of the line.
+ * @param use_scaled_pos Whether to use absolute window positioning or take into account the
+ * position of the scaled image.
+ */
+ public void draw_vertical_line(Cairo.Context ctx, int x, int y, int height, bool use_scaled_pos = true) {
+ if (use_scaled_pos) {
+ x += scaled_position.x;
+ y += scaled_position.y;
+ }
+
+ ctx.move_to(x + 0.5, y + 0.5);
+ ctx.line_to(x + 0.5, y + height - 1);
+ ctx.stroke();
+ }
+
+ public void erase_horizontal_line(int x, int y, int width) {
+ default_ctx.save();
+
+ default_ctx.set_operator(Cairo.Operator.SOURCE);
+ default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
+ default_ctx.rectangle(scaled_position.x + x, scaled_position.y + y,
+ width - 1, 1);
+ default_ctx.fill();
+
+ default_ctx.restore();
+ }
+
+ public void draw_circle(Cairo.Context ctx, int active_center_x, int active_center_y,
+ int radius) {
+ int center_x = active_center_x + scaled_position.x;
+ int center_y = active_center_y + scaled_position.y;
+
+ ctx.arc(center_x, center_y, radius, 0, 2 * GLib.Math.PI);
+ ctx.stroke();
+ }
+
+ public void erase_vertical_line(int x, int y, int height) {
+ default_ctx.save();
+
+ // Ticket #3146 - artifacting when moving the crop box or
+ // enlarging it from the lower right.
+ // We now no longer subtract one from the height before choosing
+ // a region to erase.
+ default_ctx.set_operator(Cairo.Operator.SOURCE);
+ default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
+ default_ctx.rectangle(scaled_position.x + x, scaled_position.y + y,
+ 1, height);
+ default_ctx.fill();
+
+ default_ctx.restore();
+ }
+
+ public void erase_box(Box box) {
+ erase_horizontal_line(box.left, box.top, box.get_width());
+ erase_horizontal_line(box.left, box.bottom, box.get_width());
+
+ erase_vertical_line(box.left, box.top, box.get_height());
+ erase_vertical_line(box.right, box.top, box.get_height());
+ }
+
+ public void invalidate_area(Box area) {
+ Gdk.Rectangle rect = area.get_rectangle();
+ rect.x += scaled_position.x;
+ rect.y += scaled_position.y;
+
+ //drawing_window.invalidate_rect(rect, false);
+ }
+
+ public void set_cursor(string cursor_type) {
+ get_drawing_window().set_cursor(new Gdk.Cursor.from_name(cursor_type, null));
+ }
+
+ private Cairo.Surface pixbuf_to_surface(Cairo.Context default_ctx, Gdk.Pixbuf pixbuf,
+ Gdk.Rectangle pos) {
+ Cairo.Surface surface = new Cairo.Surface.similar(default_ctx.get_target(),
+ Cairo.Content.COLOR_ALPHA, pos.width, pos.height);
+ Cairo.Context ctx = new Cairo.Context(surface);
+ paint_pixmap_with_background(ctx, pixbuf, 0, 0);
+ ctx.paint();
+ return surface;
+ }
+}
diff --git a/src/editing_tools/RGBHistogramManipulator.vala b/src/editing_tools/RGBHistogramManipulator.vala
index 4b0a8a24..8ad89be7 100644
--- a/src/editing_tools/RGBHistogramManipulator.vala
+++ b/src/editing_tools/RGBHistogramManipulator.vala
@@ -20,8 +20,6 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
private int left_nub_max = 255 - NUB_SIZE - 1;
private int right_nub_min = NUB_SIZE + 1;
- private static Gtk.WidgetPath slider_draw_path = new Gtk.WidgetPath();
- private static Gtk.WidgetPath frame_draw_path = new Gtk.WidgetPath();
private static bool paths_setup = false;
private RGBHistogram histogram = null;
@@ -36,33 +34,31 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
public RGBHistogramManipulator( ) {
set_size_request(CONTROL_WIDTH, CONTROL_HEIGHT);
can_focus = true;
-
- if (!paths_setup) {
- slider_draw_path.append_type(typeof(Gtk.Scale));
- slider_draw_path.iter_add_class(0, "scale");
- slider_draw_path.iter_add_class(0, "range");
-
- frame_draw_path.append_type(typeof(Gtk.Frame));
- frame_draw_path.iter_add_class(0, "default");
-
- paths_setup = true;
- }
-
- add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
- add_events(Gdk.EventMask.BUTTON_RELEASE_MASK);
- add_events(Gdk.EventMask.BUTTON_MOTION_MASK);
- add_events(Gdk.EventMask.FOCUS_CHANGE_MASK);
- add_events(Gdk.EventMask.KEY_PRESS_MASK);
-
- button_press_event.connect(on_button_press);
- button_release_event.connect(on_button_release);
- motion_notify_event.connect(on_button_motion);
-
- this.size_allocate.connect(on_size_allocate);
+
+ var focus = new Gtk.EventControllerFocus();
+ focus.leave.connect(queue_draw);
+ add_controller(focus);
+
+ var key = new Gtk.EventControllerKey();
+ key.key_pressed.connect(on_key_pressed);
+ add_controller(key);
+
+ var click = new Gtk.GestureClick();
+ click.set_touch_only(false);
+ click.set_button(Gdk.BUTTON_PRIMARY);
+ click.pressed.connect(on_button_press);
+ click.released.connect(on_button_released);
+ add_controller(click);
+
+ var motion = new Gtk.EventControllerMotion();
+ motion.motion.connect(on_button_motion);
+ add_controller(motion);
+
+ this.resize.connect(on_resize);
}
- private void on_size_allocate(Gtk.Allocation region) {
- this.offset = (region.width - RGBHistogram.GRAPHIC_WIDTH - NUB_SIZE) / 2;
+ private void on_resize(int width, int height) {
+ this.offset = (width - RGBHistogram.GRAPHIC_WIDTH - NUB_SIZE) / 2;
}
private LocationCode hit_test_point(int x, int y) {
@@ -84,29 +80,37 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
else
return LocationCode.RIGHT_TROUGH;
}
-
- private bool on_button_press(Gdk.EventButton event_record) {
+
+ private void on_button_press(Gtk.GestureClick gesture, int press, double x, double y) {
+ if (get_focus_on_click() && !has_focus) {
+ grab_focus();
+ }
+
+ if (press != 1) {
+ return;
+ }
+
// Adjust mouse position to drawing offset
// Easier to modify the event and shit the whole drawing then adjusting the nub drawing code
- event_record.x -= this.offset;
- LocationCode loc = hit_test_point((int) event_record.x, (int) event_record.y);
+ x -= this.offset;
+ LocationCode loc = hit_test_point((int) x, (int) y);
bool retval = true;
switch (loc) {
case LocationCode.LEFT_NUB:
- track_start_x = ((int) event_record.x);
+ track_start_x = ((int) x);
track_nub_start_position = left_nub_position;
is_left_nub_tracking = true;
break;
case LocationCode.RIGHT_NUB:
- track_start_x = ((int) event_record.x);
+ track_start_x = ((int) x);
track_nub_start_position = right_nub_position;
is_right_nub_tracking = true;
break;
case LocationCode.LEFT_TROUGH:
- left_nub_position = ((int) event_record.x) - NUB_HALF_WIDTH;
+ left_nub_position = ((int) x) - NUB_HALF_WIDTH;
left_nub_position = left_nub_position.clamp(0, left_nub_max);
force_update();
nub_position_changed();
@@ -114,7 +118,7 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
break;
case LocationCode.RIGHT_TROUGH:
- right_nub_position = ((int) event_record.x) - NUB_HALF_WIDTH;
+ right_nub_position = ((int) x) - NUB_HALF_WIDTH;
right_nub_position = right_nub_position.clamp(right_nub_min, 255);
force_update();
nub_position_changed();
@@ -127,12 +131,15 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
}
// Remove adjustment position to drawing offset
- event_record.x += this.offset;
+ x += this.offset;
- return retval;
+ if (retval) {
+ var sequence = gesture.get_current_sequence ();
+ gesture.set_sequence_state (sequence, Gtk.EventSequenceState.CLAIMED);
+ }
}
- private bool on_button_release(Gdk.EventButton event_record) {
+ private void on_button_released(Gtk.GestureClick gesture, int press, double x, double y) {
if (is_left_nub_tracking || is_right_nub_tracking) {
nub_position_changed();
update_nub_extrema();
@@ -140,57 +147,40 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
is_left_nub_tracking = false;
is_right_nub_tracking = false;
-
- return false;
}
- private bool on_button_motion(Gdk.EventMotion event_record) {
+ private void on_button_motion(double x, double y) {
if ((!is_left_nub_tracking) && (!is_right_nub_tracking))
- return false;
+ return;
- event_record.x -= this.offset;
+ x -= this.offset;
if (is_left_nub_tracking) {
- int track_x_delta = ((int) event_record.x) - track_start_x;
+ int track_x_delta = ((int) x) - track_start_x;
left_nub_position = (track_nub_start_position + track_x_delta);
left_nub_position = left_nub_position.clamp(0, left_nub_max);
} else { /* right nub is tracking */
- int track_x_delta = ((int) event_record.x) - track_start_x;
+ int track_x_delta = ((int) x) - track_start_x;
right_nub_position = (track_nub_start_position + track_x_delta);
right_nub_position = right_nub_position.clamp(right_nub_min, 255);
}
force_update();
- event_record.x += this.offset;
-
- return true;
+ x += this.offset;
}
- public override bool focus_out_event(Gdk.EventFocus event) {
- if (base.focus_out_event(event)) {
- return true;
- }
-
- queue_draw();
-
- return false;
- }
-
- public override bool key_press_event(Gdk.EventKey event) {
- if (base.key_press_event(event)) {
- return true;
- }
+ public bool on_key_pressed(Gtk.EventControllerKey event, uint keyval, uint keycode, Gdk.ModifierType
modifiers) {
int delta = 0;
- if (event.keyval == Gdk.Key.Left || event.keyval == Gdk.Key.Up) {
+ if (keyval == Gdk.Key.Left || keyval == Gdk.Key.Up) {
delta = -1;
}
- if (event.keyval == Gdk.Key.Right || event.keyval == Gdk.Key.Down) {
+ if (keyval == Gdk.Key.Right || keyval == Gdk.Key.Down) {
delta = 1;
}
- if (!(Gdk.ModifierType.CONTROL_MASK in event.state)) {
+ if (!(Gdk.ModifierType.CONTROL_MASK in modifiers)) {
delta *= 5;
}
@@ -198,7 +188,7 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
return false;
}
- if (Gdk.ModifierType.SHIFT_MASK in event.state) {
+ if (Gdk.ModifierType.SHIFT_MASK in modifiers) {
right_nub_position += delta;
right_nub_position = right_nub_position.clamp(right_nub_min, 255);
} else {
@@ -214,8 +204,11 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
return true;
}
- public override bool draw(Cairo.Context ctx) {
- Gtk.Border padding = get_style_context().get_padding(Gtk.StateFlags.NORMAL);
+ public void draw(Cairo.Context ctx, int width, int height) {
+ var sctx = get_style_context();
+ sctx.save();
+ sctx.set_state (Gtk.StateFlags.NORMAL);
+ Gtk.Border padding = sctx.get_padding();
Gdk.Rectangle area = Gdk.Rectangle();
area.x = padding.left + this.offset;
@@ -224,7 +217,7 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
area.height = RGBHistogram.GRAPHIC_HEIGHT + padding.bottom;
if (has_focus) {
- get_style_context().render_focus(ctx, area.x, area.y,
+ sctx.render_focus(ctx, area.x, area.y,
area.width + NUB_SIZE,
area.height + NUB_SIZE + NUB_HALF_WIDTH);
}
@@ -232,8 +225,7 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
draw_histogram(ctx, area);
draw_nub(ctx, area, left_nub_position);
draw_nub(ctx, area, right_nub_position);
-
- return true;
+ sctx.restore();
}
private void draw_histogram(Cairo.Context ctx, Gdk.Rectangle area) {
@@ -273,7 +265,7 @@ public class RGBHistogramManipulator : Gtk.DrawingArea {
}
private void force_update() {
- get_window().invalidate_rect(null, true);
+ queue_draw();
}
private void update_nub_extrema() {
diff --git a/src/editing_tools/RedeyeTool.vala b/src/editing_tools/RedeyeTool.vala
new file mode 100644
index 00000000..1b8c60b2
--- /dev/null
+++ b/src/editing_tools/RedeyeTool.vala
@@ -0,0 +1,364 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+public struct EditingTools.RedeyeInstance {
+ public const int MIN_RADIUS = 4;
+ public const int MAX_RADIUS = 32;
+ public const int DEFAULT_RADIUS = 10;
+
+ public Gdk.Point center;
+ public int radius;
+
+ RedeyeInstance() {
+ Gdk.Point default_center = Gdk.Point();
+ center = default_center;
+ radius = DEFAULT_RADIUS;
+ }
+
+ public static Gdk.Rectangle to_bounds_rect(EditingTools.RedeyeInstance inst) {
+ Gdk.Rectangle result = Gdk.Rectangle();
+ result.x = inst.center.x - inst.radius;
+ result.y = inst.center.y - inst.radius;
+ result.width = 2 * inst.radius;
+ result.height = result.width;
+
+ return result;
+ }
+
+ public static RedeyeInstance from_bounds_rect(Gdk.Rectangle rect) {
+ Gdk.Rectangle in_rect = rect;
+
+ RedeyeInstance result = RedeyeInstance();
+ result.radius = (in_rect.width + in_rect.height) / 4;
+ result.center.x = in_rect.x + result.radius;
+ result.center.y = in_rect.y + result.radius;
+
+ return result;
+ }
+}
+
+public class EditingTools.RedeyeTool : EditingTool {
+ private class RedeyeToolWindow : EditingToolWindow {
+ private const int CONTROL_SPACING = 8;
+
+ private Gtk.Label slider_label = new Gtk.Label.with_mnemonic(_("Size:"));
+
+ public Gtk.Button apply_button =
+ new Gtk.Button.with_mnemonic(Resources.APPLY_LABEL);
+ public Gtk.Button close_button =
+ new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
+ public Gtk.Scale slider = new Gtk.Scale.with_range(Gtk.Orientation.HORIZONTAL,
+ RedeyeInstance.MIN_RADIUS, RedeyeInstance.MAX_RADIUS, 1.0);
+
+ public RedeyeToolWindow(Gtk.Window container) {
+ base(container);
+
+ slider.set_size_request(80, -1);
+ slider.set_draw_value(false);
+
+ close_button.set_tooltip_text(_("Close the red-eye tool"));
+ //close_button.set_image_position(Gtk.PositionType.LEFT);
+
+ apply_button.set_tooltip_text(_("Remove any red-eye effects in the selected region"));
+ //apply_button.set_image_position(Gtk.PositionType.LEFT);
+
+ Gtk.Box layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
+ layout.append(slider_label);
+ layout.append(slider);
+ layout.append(close_button);
+ layout.append(apply_button);
+
+ add(layout);
+ }
+ }
+
+ private Cairo.Context thin_white_ctx = null;
+ private Cairo.Context wider_gray_ctx = null;
+ private RedeyeToolWindow redeye_tool_window = null;
+ private RedeyeInstance user_interaction_instance;
+ private bool is_reticle_move_in_progress = false;
+ private Gdk.Point reticle_move_mouse_start_point;
+ private Gdk.Point reticle_move_anchor;
+ private Gdk.Rectangle old_scaled_pixbuf_position;
+ private Gdk.Pixbuf current_pixbuf = null;
+
+ private RedeyeTool() {
+ base("RedeyeTool");
+ }
+
+ public static RedeyeTool factory() {
+ return new RedeyeTool();
+ }
+
+ public static bool is_available(Photo photo, Scaling scaling) {
+ Dimensions dim = scaling.get_scaled_dimensions(photo.get_dimensions());
+
+ return dim.width >= (RedeyeInstance.MAX_RADIUS * 2)
+ && dim.height >= (RedeyeInstance.MAX_RADIUS * 2);
+ }
+
+ private RedeyeInstance new_interaction_instance(PhotoCanvas canvas) {
+ Gdk.Rectangle photo_bounds = canvas.get_scaled_pixbuf_position();
+ Gdk.Point photo_center = {0};
+ photo_center.x = photo_bounds.x + (photo_bounds.width / 2);
+ photo_center.y = photo_bounds.y + (photo_bounds.height / 2);
+
+ RedeyeInstance result = RedeyeInstance();
+ result.center.x = photo_center.x;
+ result.center.y = photo_center.y;
+ result.radius = RedeyeInstance.DEFAULT_RADIUS;
+
+ return result;
+ }
+
+ private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
+ wider_gray_ctx = new Cairo.Context(ctx.get_target());
+ set_source_color_from_string(wider_gray_ctx, "#111");
+ wider_gray_ctx.set_line_width(3);
+
+ thin_white_ctx = new Cairo.Context(ctx.get_target());
+ set_source_color_from_string(thin_white_ctx, "#FFF");
+ thin_white_ctx.set_line_width(1);
+ }
+
+ private void draw_redeye_instance(RedeyeInstance inst) {
+ canvas.draw_circle(wider_gray_ctx, inst.center.x, inst.center.y,
+ inst.radius);
+ canvas.draw_circle(thin_white_ctx, inst.center.x, inst.center.y,
+ inst.radius);
+ }
+
+ private bool on_size_slider_adjust(Gtk.ScrollType type) {
+ user_interaction_instance.radius =
+ (int) redeye_tool_window.slider.get_value();
+
+ canvas.repaint();
+
+ return false;
+ }
+
+ private void on_apply() {
+ Gdk.Rectangle bounds_rect_user =
+ RedeyeInstance.to_bounds_rect(user_interaction_instance);
+
+ Gdk.Rectangle bounds_rect_active =
+ canvas.user_to_active_rect(bounds_rect_user);
+ Gdk.Rectangle bounds_rect_unscaled =
+ canvas.active_to_unscaled_rect(bounds_rect_active);
+ Gdk.Rectangle bounds_rect_raw =
+ canvas.unscaled_to_raw_rect(bounds_rect_unscaled);
+
+ RedeyeInstance instance_raw =
+ RedeyeInstance.from_bounds_rect(bounds_rect_raw);
+
+ // transform screen coords back to image coords,
+ // taking into account straightening angle.
+ Dimensions dimensions = canvas.get_photo().get_dimensions(
+ Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
+
+ double theta = 0.0;
+
+ canvas.get_photo().get_straighten(out theta);
+
+ instance_raw.center = derotate_point_arb(instance_raw.center,
+ dimensions.width, dimensions.height, theta);
+
+ RedeyeCommand command = new RedeyeCommand(canvas.get_photo(), instance_raw,
+ Resources.RED_EYE_LABEL, Resources.RED_EYE_TOOLTIP);
+ AppWindow.get_command_manager().execute(command);
+ }
+
+ private void on_photos_altered(Gee.Map<DataObject, Alteration> map) {
+ if (!map.has_key(canvas.get_photo()))
+ return;
+
+ try {
+ current_pixbuf = canvas.get_photo().get_pixbuf(canvas.get_scaling());
+ } catch (Error err) {
+ warning("%s", err.message);
+ aborted();
+
+ return;
+ }
+
+ canvas.repaint();
+ }
+
+ private void on_close() {
+ applied(null, current_pixbuf, canvas.get_photo().get_dimensions(), false);
+ }
+
+ private void on_canvas_resize() {
+ Gdk.Rectangle scaled_pixbuf_position =
+ canvas.get_scaled_pixbuf_position();
+
+ user_interaction_instance.center.x -= old_scaled_pixbuf_position.x;
+ user_interaction_instance.center.y -= old_scaled_pixbuf_position.y;
+
+ double scale_factor = ((double) scaled_pixbuf_position.width) /
+ ((double) old_scaled_pixbuf_position.width);
+
+ user_interaction_instance.center.x =
+ (int)(((double) user_interaction_instance.center.x) *
+ scale_factor + 0.5);
+ user_interaction_instance.center.y =
+ (int)(((double) user_interaction_instance.center.y) *
+ scale_factor + 0.5);
+
+ user_interaction_instance.center.x += scaled_pixbuf_position.x;
+ user_interaction_instance.center.y += scaled_pixbuf_position.y;
+
+ old_scaled_pixbuf_position = scaled_pixbuf_position;
+
+ current_pixbuf = null;
+ }
+
+ public override void activate(PhotoCanvas canvas) {
+ user_interaction_instance = new_interaction_instance(canvas);
+
+ prepare_ctx(canvas.get_default_ctx(), canvas.get_surface_dim());
+
+ bind_canvas_handlers(canvas);
+
+ old_scaled_pixbuf_position = canvas.get_scaled_pixbuf_position();
+ current_pixbuf = canvas.get_scaled_pixbuf();
+
+ redeye_tool_window = new RedeyeToolWindow(canvas.get_container());
+ redeye_tool_window.slider.set_value(user_interaction_instance.radius);
+
+ bind_window_handlers();
+
+ DataCollection? owner = canvas.get_photo().get_membership();
+ if (owner != null)
+ owner.items_altered.connect(on_photos_altered);
+
+ base.activate(canvas);
+ }
+
+ public override void deactivate() {
+ if (canvas != null) {
+ DataCollection? owner = canvas.get_photo().get_membership();
+ if (owner != null)
+ owner.items_altered.disconnect(on_photos_altered);
+
+ unbind_canvas_handlers(canvas);
+ }
+
+ if (redeye_tool_window != null) {
+ unbind_window_handlers();
+ redeye_tool_window.hide();
+ redeye_tool_window.destroy();
+ redeye_tool_window = null;
+ }
+
+ base.deactivate();
+ }
+
+ private void bind_canvas_handlers(PhotoCanvas canvas) {
+ canvas.new_surface.connect(prepare_ctx);
+ canvas.resized_scaled_pixbuf.connect(on_canvas_resize);
+ }
+
+ private void unbind_canvas_handlers(PhotoCanvas canvas) {
+ canvas.new_surface.disconnect(prepare_ctx);
+ canvas.resized_scaled_pixbuf.disconnect(on_canvas_resize);
+ }
+
+ private void bind_window_handlers() {
+ redeye_tool_window.apply_button.clicked.connect(on_apply);
+ redeye_tool_window.close_button.clicked.connect(on_close);
+ redeye_tool_window.slider.change_value.connect(on_size_slider_adjust);
+ }
+
+ private void unbind_window_handlers() {
+ redeye_tool_window.apply_button.clicked.disconnect(on_apply);
+ redeye_tool_window.close_button.clicked.disconnect(on_close);
+ redeye_tool_window.slider.change_value.disconnect(on_size_slider_adjust);
+ }
+
+ public override EditingToolWindow? get_tool_window() {
+ return redeye_tool_window;
+ }
+
+ public override void paint(Cairo.Context ctx) {
+ canvas.paint_pixbuf((current_pixbuf != null) ? current_pixbuf : canvas.get_scaled_pixbuf());
+
+ /* user_interaction_instance has its radius in user coords, and
+ draw_redeye_instance expects active region coords */
+ RedeyeInstance active_inst = user_interaction_instance;
+ active_inst.center =
+ canvas.user_to_active_point(user_interaction_instance.center);
+ draw_redeye_instance(active_inst);
+ }
+
+ public override void on_left_click(int x, int y) {
+ Gdk.Rectangle bounds_rect =
+ RedeyeInstance.to_bounds_rect(user_interaction_instance);
+
+ if (coord_in_rectangle(x, y, bounds_rect)) {
+ is_reticle_move_in_progress = true;
+ reticle_move_mouse_start_point.x = x;
+ reticle_move_mouse_start_point.y = y;
+ reticle_move_anchor = user_interaction_instance.center;
+ }
+ }
+
+ public override void on_left_released(int x, int y) {
+ is_reticle_move_in_progress = false;
+ }
+
+ public override void on_motion(int x, int y, Gdk.ModifierType mask) {
+ if (is_reticle_move_in_progress) {
+
+ Gdk.Rectangle active_region_rect =
+ canvas.get_scaled_pixbuf_position();
+
+ int x_clamp_low =
+ active_region_rect.x + user_interaction_instance.radius + 1;
+ int y_clamp_low =
+ active_region_rect.y + user_interaction_instance.radius + 1;
+ int x_clamp_high =
+ active_region_rect.x + active_region_rect.width -
+ user_interaction_instance.radius - 1;
+ int y_clamp_high =
+ active_region_rect.y + active_region_rect.height -
+ user_interaction_instance.radius - 1;
+
+ int delta_x = x - reticle_move_mouse_start_point.x;
+ int delta_y = y - reticle_move_mouse_start_point.y;
+
+ user_interaction_instance.center.x = reticle_move_anchor.x +
+ delta_x;
+ user_interaction_instance.center.y = reticle_move_anchor.y +
+ delta_y;
+
+ user_interaction_instance.center.x =
+ (reticle_move_anchor.x + delta_x).clamp(x_clamp_low,
+ x_clamp_high);
+ user_interaction_instance.center.y =
+ (reticle_move_anchor.y + delta_y).clamp(y_clamp_low,
+ y_clamp_high);
+
+ canvas.repaint();
+ } else {
+ Gdk.Rectangle bounds =
+ RedeyeInstance.to_bounds_rect(user_interaction_instance);
+
+ if (coord_in_rectangle(x, y, bounds)) {
+ canvas.set_cursor("move");
+ } else {
+ canvas.set_cursor("default");
+ }
+ }
+ }
+
+ public override bool on_keypress(Gtk.EventControllerKey event, uint keyval, uint keycode,
Gdk.ModifierType modifiers) {
+ if ((Gdk.keyval_name(keyval) == "KP_Enter") ||
+ (Gdk.keyval_name(keyval) == "Enter") ||
+ (Gdk.keyval_name(keyval) == "Return")) {
+ on_close();
+ return true;
+ }
+
+ return base.on_keypress(event, keyval, keycode, modifiers);
+ }
+}
\ No newline at end of file
diff --git a/src/meson.build b/src/meson.build
index 71d8ae00..7c8932ad 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -67,6 +67,14 @@ executable(
'db/VideoTable.vala',
'db/VersionTable.vala',
'db/SavedSearchDBTable.vala',
+ 'editing_tools/AdjustTool.vala',
+ 'editing_tools/EditingTools.vala',
+ 'editing_tools/EditingTool.vala',
+ 'editing_tools/CropTool.vala',
+ 'editing_tools/PhotoCanvas.vala',
+ 'editing_tools/RedeyeTool.vala',
+ 'editing_tools/EditingToolWindow.vala',
+ 'editing_tools/RGBHistogramManipulator.vala',
'faces/Face.vala',
'faces/FaceLocation.vala',
'slideshow/Slideshow.vala',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]