Commit 84379cbc authored by Lucas Beeler's avatar Lucas Beeler

Allows user to zoom in and out of photos in the single picture library view,...

Allows user to zoom in and out of photos in the single picture library view, the direct edit view, and the full screen view. Closes #1162.
parent 44479a9c
......@@ -428,3 +428,212 @@ public struct Scaling {
}
}
public struct ZoomState {
private Dimensions content_dimensions;
private Dimensions viewport_dimensions;
private double zoom_factor;
private double interpolation_factor;
private double min_factor;
private double max_factor;
private Gdk.Point viewport_center;
public ZoomState(Dimensions content_dimensions, Dimensions viewport_dimensions,
double slider_val = 0.0, Gdk.Point? viewport_center = null) {
this.content_dimensions = content_dimensions;
this.viewport_dimensions = viewport_dimensions;
this.interpolation_factor = slider_val;
compute_zoom_factors();
if ((viewport_center == null) || ((viewport_center.x == 0) && (viewport_center.y == 0)) ||
(slider_val == 0.0)) {
center_viewport();
} else {
this.viewport_center = viewport_center;
clamp_viewport_center();
}
}
public ZoomState.rescale(ZoomState existing, double new_slider_val) {
this.content_dimensions = existing.content_dimensions;
this.viewport_dimensions = existing.viewport_dimensions;
this.interpolation_factor = new_slider_val;
compute_zoom_factors();
if (new_slider_val == 0.0) {
center_viewport();
} else {
viewport_center.x = (int) (zoom_factor * (existing.viewport_center.x /
existing.zoom_factor));
viewport_center.y = (int) (zoom_factor * (existing.viewport_center.y /
existing.zoom_factor));
clamp_viewport_center();
}
}
public ZoomState.pan(ZoomState existing, Gdk.Point new_viewport_center) {
this.content_dimensions = existing.content_dimensions;
this.viewport_dimensions = existing.viewport_dimensions;
this.interpolation_factor = existing.interpolation_factor;
compute_zoom_factors();
this.viewport_center = new_viewport_center;
clamp_viewport_center();
}
private void clamp_viewport_center() {
int zoomed_width = get_zoomed_width();
int zoomed_height = get_zoomed_height();
viewport_center.x = viewport_center.x.clamp(viewport_dimensions.width / 2,
zoomed_width - (viewport_dimensions.width / 2) - 1);
viewport_center.y = viewport_center.y.clamp(viewport_dimensions.height / 2,
zoomed_height - (viewport_dimensions.height / 2) - 1);
}
private void center_viewport() {
viewport_center.x = get_zoomed_width() / 2;
viewport_center.y = get_zoomed_height() / 2;
}
private void compute_zoom_factors() {
max_factor = 2.0;
double viewport_to_content_x;
double viewport_to_content_y;
content_dimensions.get_scale_ratios(viewport_dimensions, out viewport_to_content_x,
out viewport_to_content_y);
min_factor = double.min(viewport_to_content_x, viewport_to_content_y);
if (min_factor > 1.0)
min_factor = 1.0;
zoom_factor = min_factor + (max_factor - min_factor) * interpolation_factor;
}
public double get_interpolation_factor() {
return interpolation_factor;
}
public Gdk.Rectangle get_viewing_rectangle() {
int zoomed_width = get_zoomed_width();
int zoomed_height = get_zoomed_height();
Gdk.Rectangle result = {0};
if (viewport_dimensions.width < zoomed_width) {
result.x = viewport_center.x - (viewport_dimensions.width / 2);
} else {
result.x = (zoomed_width - viewport_dimensions.width) / 2;
}
if (result.x < 0)
result.x = 0;
if (viewport_dimensions.height < zoomed_height) {
result.y = viewport_center.y - (viewport_dimensions.height / 2);
} else {
result.y = (zoomed_height - viewport_dimensions.height) / 2;
}
if (result.y < 0)
result.y = 0;
int right = result.x + viewport_dimensions.width;
if (right > zoomed_width)
right = zoomed_width;
result.width = right - result.x;
int bottom = result.y + viewport_dimensions.height;
if (bottom > zoomed_height)
bottom = zoomed_height;
result.height = bottom - result.y;
return result;
}
public Gdk.Rectangle get_viewing_rectangle_projection(Gdk.Pixbuf for_pixbuf) {
double zoomed_width = get_zoomed_width();
double zoomed_height = get_zoomed_height();
double horiz_scale = for_pixbuf.width / zoomed_width;
double vert_scale = for_pixbuf.height / zoomed_height;
double scale = (horiz_scale + vert_scale) / 2.0;
Gdk.Rectangle viewing_rectangle = get_viewing_rectangle();
Gdk.Rectangle result = {0};
result.x = (int) (viewing_rectangle.x * scale);
result.x = result.x.clamp(0, for_pixbuf.width);
result.y = (int) (viewing_rectangle.y * scale);
result.y = result.y.clamp(0, for_pixbuf.height);
int right = (int) ((viewing_rectangle.x + viewing_rectangle.width) * scale);
right = right.clamp(0, for_pixbuf.width);
int bottom = (int) ((viewing_rectangle.y + viewing_rectangle.height) * scale);
bottom = bottom.clamp(0, for_pixbuf.height);
result.width = right - result.x;
result.height = bottom - result.y;
return result;
}
public double get_zoom_factor() {
return zoom_factor;
}
public int get_zoomed_width() {
return (int) (content_dimensions.width * zoom_factor);
}
public int get_zoomed_height() {
return (int) (content_dimensions.height * zoom_factor);
}
public Gdk.Point get_viewport_center() {
return viewport_center;
}
public string to_string() {
string named_modes = "";
if (is_min())
named_modes = named_modes + ((named_modes == "") ? "MIN" : ", MIN");
if (is_default())
named_modes = named_modes + ((named_modes == "") ? "DEFAULT" : ", DEFAULT");
if (is_isomorphic())
named_modes = named_modes + ((named_modes =="") ? "ISOMORPHIC" : ", ISOMORPHIC");
if (is_max())
named_modes = named_modes + ((named_modes =="") ? "MAX" : ", MAX");
if (named_modes == "")
named_modes = "(none)";
Gdk.Rectangle viewing_rect = get_viewing_rectangle();
return (("ZoomState {\n content dimensions = %d x %d;\n viewport dimensions = " +
"%d x %d;\n min factor = %f;\n max factor = %f;\n current factor = %f;" +
"\n zoomed width = %d;\n zoomed height = %d;\n named modes = %s;" +
"\n viewing rectangle = { x: %d, y: %d, width: %d, height: %d };" +
"\n viewport center = (%d, %d);\n}\n").printf(
content_dimensions.width, content_dimensions.height, viewport_dimensions.width,
viewport_dimensions.height, min_factor, max_factor, zoom_factor, get_zoomed_width(),
get_zoomed_height(), named_modes, viewing_rect.x, viewing_rect.y, viewing_rect.width,
viewing_rect.height, viewport_center.x, viewport_center.y));
}
public bool is_min() {
return (zoom_factor == min_factor);
}
public bool is_default() {
return is_min();
}
public bool is_max() {
return (zoom_factor == max_factor);
}
public bool is_isomorphic() {
return (zoom_factor == 1.0);
}
}
......@@ -1343,7 +1343,10 @@ public abstract class SinglePhotoPage : Page {
private Dimensions max_dim = Dimensions();
private Gdk.Pixbuf scaled = null;
private Gdk.Rectangle scaled_pos = Gdk.Rectangle();
private ZoomState static_zoom_state;
private bool zoom_high_quality = true;
private ZoomState saved_zoom_state;
public SinglePhotoPage(string page_name, bool scale_up_to_viewport) {
base(page_name);
......@@ -1370,10 +1373,95 @@ public abstract class SinglePhotoPage : Page {
viewport.size_allocate += on_viewport_resize;
canvas.expose_event += on_canvas_exposed;
set_event_source(canvas);
}
private void render_zoomed_to_pixmap(ZoomState zoom_state) {
Gdk.Pixbuf basis_pixbuf = (zoom_high_quality) ? get_zoom_optimized_pixbuf(zoom_state) :
unscaled;
if (basis_pixbuf == null)
basis_pixbuf = unscaled;
Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle();
Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(basis_pixbuf);
Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(basis_pixbuf, view_rect_proj.x,
view_rect_proj.y, view_rect_proj.width, view_rect_proj.height);
Gdk.Pixbuf zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height,
Gdk.InterpType.BILINEAR);
int draw_x = (pixmap_dim.width - view_rect.width) / 2;
if (draw_x < 0)
draw_x = 0;
int draw_y = (pixmap_dim.height - view_rect.height) / 2;
if (draw_y < 0)
draw_y = 0;
pixmap.draw_pixbuf(canvas_gc, zoomed, 0, 0, draw_x, draw_y, -1, -1, Gdk.RgbDither.NORMAL,
0, 0);
}
protected void on_interactive_zoom(ZoomState interactive_zoom_state) {
assert(is_zoom_supported());
pixmap.draw_rectangle(canvas.style.black_gc, true, 0, 0, -1, -1);
bool old_quality_setting = zoom_high_quality;
zoom_high_quality = false;
render_zoomed_to_pixmap(interactive_zoom_state);
zoom_high_quality = old_quality_setting;
canvas.window.draw_drawable(canvas_gc, pixmap, 0, 0, 0, 0, -1, -1);
}
protected void on_interactive_pan(ZoomState interactive_zoom_state) {
assert(is_zoom_supported());
pixmap.draw_rectangle(canvas.style.black_gc, true, 0, 0, -1, -1);
bool old_quality_setting = zoom_high_quality;
zoom_high_quality = true;
render_zoomed_to_pixmap(interactive_zoom_state);
zoom_high_quality = old_quality_setting;
canvas.window.draw_drawable(canvas_gc, pixmap, 0, 0, 0, 0, -1, -1);
}
protected virtual bool is_zoom_supported() {
return false;
}
protected virtual Gdk.Pixbuf? get_zoom_optimized_pixbuf(ZoomState zoom_state) {
return null;
}
protected virtual void cancel_zoom() {
if (pixmap != null)
pixmap.draw_rectangle(canvas.style.black_gc, true, 0, 0, -1, -1);
}
protected virtual void save_zoom_state() {
saved_zoom_state = static_zoom_state;
}
protected virtual void restore_zoom_state() {
static_zoom_state = saved_zoom_state;
}
protected ZoomState get_saved_zoom_state() {
return saved_zoom_state;
}
protected void set_zoom_state(ZoomState zoom_state) {
assert(is_zoom_supported());
static_zoom_state = zoom_state;
}
protected ZoomState get_zoom_state() {
assert(is_zoom_supported());
return static_zoom_state;
}
public override void switched_to() {
base.switched_to();
......@@ -1394,7 +1482,11 @@ public abstract class SinglePhotoPage : Page {
// the caller capable of producing larger ones depending on the viewport size). max_dim
// is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if
// max_dim should be ignored (i.e. scale_up_to_viewport is false).
public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim) {
public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim) {
static_zoom_state = ZoomState(max_dim, pixmap_dim,
static_zoom_state.get_interpolation_factor(),
static_zoom_state.get_viewport_center());
this.unscaled = unscaled;
this.max_dim = max_dim;
scaled = null;
......@@ -1465,7 +1557,7 @@ public abstract class SinglePhotoPage : Page {
private override void on_resize_finished(Gdk.Rectangle rect) {
base.on_resize_finished(rect);
// when the resize is completed, do a high-quality repaint
repaint();
}
......@@ -1495,8 +1587,14 @@ public abstract class SinglePhotoPage : Page {
}
protected virtual void paint(Gdk.GC gc, Gdk.Drawable drawable) {
drawable.draw_pixbuf(gc, scaled, 0, 0, scaled_pos.x, scaled_pos.y, -1, -1,
Gdk.RgbDither.NORMAL, 0, 0);
if (is_zoom_supported() && (!static_zoom_state.is_default())) {
pixmap.draw_rectangle(canvas.style.black_gc, true, 0, 0, -1, -1);
render_zoomed_to_pixmap(static_zoom_state);
canvas.window.draw_drawable(canvas_gc, pixmap, 0, 0, 0, 0, -1, -1);
} else {
drawable.draw_pixbuf(gc, scaled, 0, 0, scaled_pos.x, scaled_pos.y, -1, -1,
Gdk.RgbDither.NORMAL, 0, 0);
}
}
public void repaint() {
......@@ -1573,10 +1671,15 @@ public abstract class SinglePhotoPage : Page {
reason = UpdateReason.NEW_PIXBUF;
else if (!new_pixmap && interp == QUALITY_INTERP)
reason = UpdateReason.QUALITY_IMPROVEMENT;
static_zoom_state = ZoomState(max_dim, pixmap_dim,
static_zoom_state.get_interpolation_factor(),
static_zoom_state.get_viewport_center());
updated_pixbuf(scaled, reason, old_scaled_dim);
}
zoom_high_quality = !fast;
paint(canvas_gc, pixmap);
// invalidate everything
......
......@@ -35,6 +35,7 @@ public abstract class EditingHostPage : SinglePhotoPage {
private Gtk.ToggleToolButton redeye_button = null;
private Gtk.ToggleToolButton adjust_button = null;
private Gtk.ToolButton enhance_button = null;
private Gtk.HScale zoom_slider = null;
private Gtk.ToolButton prev_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_GO_BACK);
private Gtk.ToolButton next_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_GO_FORWARD);
private EditingTool current_tool = null;
......@@ -44,8 +45,14 @@ public abstract class EditingHostPage : SinglePhotoPage {
private bool photo_missing = false;
private PixbufCache cache = null;
private PixbufCache original_cache = null;
private PixbufCache zoom_cache = null;
private PhotoDragAndDropHandler dnd_handler = null;
private Gdk.Pixbuf? zoom_optimized_pixbuf = null;
private bool enable_interactive_zoom_refresh = false;
private Gdk.Point zoom_pan_start_point;
private bool is_pan_in_progress = false;
private double saved_slider_val = 0.0;
public EditingHostPage(SourceCollection sources, string name) {
base(name, false);
......@@ -103,6 +110,18 @@ public abstract class EditingHostPage : SinglePhotoPage {
separator.set_draw(false);
toolbar.insert(separator, -1);
// zoom slider
zoom_slider = new Gtk.HScale(new Gtk.Adjustment(0.0, 0.0, 1.1, 0.1, 0.1, 0.1));
zoom_slider.set_draw_value(false);
Gtk.ToolItem zoom_slider_wrapper = new Gtk.ToolItem();
zoom_slider_wrapper.add(zoom_slider);
toolbar.insert(zoom_slider_wrapper, -1);
zoom_slider.set_size_request(120, -1);
zoom_slider.value_changed += on_zoom_slider_value_changed;
zoom_slider.button_press_event += on_zoom_slider_drag_begin;
zoom_slider.button_release_event += on_zoom_slider_drag_end;
zoom_slider.key_press_event += on_zoom_slider_key_press;
// previous button
prev_button.set_tooltip_text(_("Previous photo"));
prev_button.clicked += on_previous_photo;
......@@ -117,7 +136,111 @@ public abstract class EditingHostPage : SinglePhotoPage {
~EditingHostPage() {
sources.item_altered -= on_photo_altered;
}
private void on_zoom_slider_value_changed() {
ZoomState new_zoom_state = ZoomState.rescale(get_zoom_state(), zoom_slider.get_value());
if (enable_interactive_zoom_refresh) {
on_interactive_zoom(new_zoom_state);
if (new_zoom_state.is_default())
set_zoom_state(new_zoom_state);
} else {
if (new_zoom_state.is_default()) {
cancel_zoom();
} else {
set_zoom_state(new_zoom_state);
fetch_zoom_optimized_pixbuf(new_zoom_state);
}
repaint();
}
}
private bool on_zoom_slider_drag_begin(Gdk.EventButton event) {
enable_interactive_zoom_refresh = true;
return false;
}
private bool on_zoom_slider_drag_end(Gdk.EventButton event) {
enable_interactive_zoom_refresh = false;
ZoomState zoom_state = ZoomState.rescale(get_zoom_state(), zoom_slider.get_value());
fetch_zoom_optimized_pixbuf(zoom_state);
set_zoom_state(zoom_state);
return false;
}
private void fetch_zoom_optimized_pixbuf(ZoomState zoom_state) {
if (zoom_cache != null)
zoom_cache.fetched -= on_zoomed_photo_fetched;
int fetch_width = zoom_state.get_zoomed_width().clamp(0,
get_photo().get_dimensions().width);
int fetch_height = zoom_state.get_zoomed_height().clamp(0,
get_photo().get_dimensions().height);
int fetch_major_axis = (fetch_width > fetch_height) ? fetch_width : fetch_height;
Scaling fetch_scaling = Scaling.for_best_fit(fetch_major_axis, false);
zoom_cache = new PixbufCache(sources, PixbufCache.PhotoType.REGULAR, fetch_scaling,
PIXBUF_CACHE_COUNT);
zoom_cache.prefetch(get_photo());
zoom_cache.fetched += on_zoomed_photo_fetched;
}
private void on_zoomed_photo_fetched(TransformablePhoto photo, Gdk.Pixbuf? pixbuf, Error? err) {
zoom_cache.fetched -= on_zoomed_photo_fetched;
zoom_optimized_pixbuf = pixbuf;
repaint();
}
private virtual bool on_zoom_slider_key_press(Gdk.EventKey event) {
return false;
}
protected virtual void on_increase_size() {
double interp = get_zoom_state().get_interpolation_factor() + 0.1;
if (interp < 0.03)
interp = 0.0;
else if (interp > 0.97)
interp = 1.0;
zoom_slider.set_value(interp);
}
protected virtual void on_decrease_size() {
double interp = get_zoom_state().get_interpolation_factor() - 0.1;
if (interp < 0.03)
interp = 0.0;
else if (interp > 0.97)
interp = 1.0;
zoom_slider.set_value(interp);
}
protected override Gdk.Pixbuf? get_zoom_optimized_pixbuf(ZoomState zoom_state) {
return zoom_optimized_pixbuf;
}
protected override void save_zoom_state() {
base.save_zoom_state();
saved_slider_val = zoom_slider.get_value();
}
protected override void restore_zoom_state() {
base.restore_zoom_state();
zoom_slider.value_changed -= on_zoom_slider_value_changed;
zoom_slider.set_value(saved_slider_val);
zoom_slider.value_changed += on_zoom_slider_value_changed;
fetch_zoom_optimized_pixbuf(get_zoom_state());
}
public override bool is_zoom_supported() {
return true;
}
public override void set_container(Gtk.Window container) {
base.set_container(container);
......@@ -133,7 +256,7 @@ public abstract class EditingHostPage : SinglePhotoPage {
public bool has_photo() {
// ViewCollection should have either zero or one photos in it at all times
assert(get_view().get_count() <= 1);
return get_view().get_count() == 1;
}
......@@ -151,6 +274,10 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
private void set_photo(TransformablePhoto photo) {
zoom_slider.value_changed -= on_zoom_slider_value_changed;
zoom_slider.set_value(0.0);
zoom_slider.value_changed += on_zoom_slider_value_changed;
// clear out the collection and use this as its sole member, selecting it so it's seen
// as the item to be operated upon by various observers (including drag-and-drop)
get_view().clear();
......@@ -179,6 +306,9 @@ public abstract class EditingHostPage : SinglePhotoPage {
public override void switching_from() {
base.switching_from();
cancel_zoom();
is_pan_in_progress = false;
deactivate_tool();
}
......@@ -187,6 +317,9 @@ public abstract class EditingHostPage : SinglePhotoPage {
deactivate_tool();
cancel_zoom();
is_pan_in_progress = false;
if (controller != null)
controller.items_selected += on_selection_changed;
}
......@@ -393,7 +526,6 @@ public abstract class EditingHostPage : SinglePhotoPage {
// swap out new photo and old photo and process change
TransformablePhoto old_photo = get_photo();
set_photo(new_photo);
set_page_name(new_photo.get_name());
// clear out the swap buffer
......@@ -411,12 +543,25 @@ public abstract class EditingHostPage : SinglePhotoPage {
if (old_photo != null)
cancel_prefetch_neighbors(old_controller, old_photo, new_controller, new_photo);
cancel_zoom();
quick_update_pixbuf();
prefetch_neighbors(new_controller, new_photo);
}
protected override void cancel_zoom() {
base.cancel_zoom();
zoom_slider.value_changed -= on_zoom_slider_value_changed;
zoom_slider.set_value(0.0);
zoom_slider.value_changed += on_zoom_slider_value_changed;
set_zoom_state(ZoomState(get_photo().get_dimensions(), get_drawable_dim(), 0.0));
zoom_optimized_pixbuf = null;
}
private void quick_update_pixbuf() {
Gdk.Pixbuf pixbuf = cache.get_ready_pixbuf(get_photo());
if (pixbuf != null) {
......@@ -565,6 +710,11 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
private void activate_tool(EditingTool tool) {
// cancel any zoom -- we don't currently allow tools to be used when an image is zoomed,
// though we may at some point in the future.
save_zoom_state();
cancel_zoom();
// deactivate current tool ... current implementation is one tool at a time. In the future,
// tools may be allowed to be executing at the same time.
deactivate_tool();
......@@ -662,16 +812,27 @@ public abstract class EditingHostPage : SinglePhotoPage {
int x = (int) event.x;
int y = (int) event.y;
// if no editing tool, then determine whether we should start a pan operation over the
// zoomed photo or fall through to the default DnD behavior if we're not zoomed
if ((current_tool == null) && (zoom_slider.get_value() != 0.0)) {
zoom_pan_start_point.x = (int) event.x;
zoom_pan_start_point.y = (int) event.y;
is_pan_in_progress = true;
return true;
}
// default behavior when photo isn't zoomed -- return false to start DnD operation
if (current_tool == null) {
return false;
}
// only concerned about mouse-downs on the pixbuf ... return true prevents DnD when the
// user drags outside the displayed photo
if (!is_inside_pixbuf(x, y))
return true;
// if no editing tool, then done
if (current_tool == null)
return false;
current_tool.on_left_click(x, y);
// block DnD handlers if tool is enabled
......@@ -679,6 +840,22 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
protected override bool on_left_released(Gdk.EventButton event) {
if (is_pan_in_progress) {
Gdk.Point viewport_center = get_zoom_state().get_viewport_center();
int delta_x = ((int) event.x) - zoom_pan_start_point.x;
int delta_y = ((int) event.y) - zoom_pan_start_point.y;
viewport_center.x -= delta_x;
viewport_center.y -= delta_y;
ZoomState zoom_state = ZoomState.pan(get_zoom_state(), viewport_center);
set_zoom_state(zoom_state);
repaint();
is_pan_in_progress = false;
}
// report all releases, as it's possible the user click and dragged from inside the
// pixbuf to the gutters
if (current_tool != null)
......@@ -716,8 +893,29 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
private override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
if (current_tool != null)
if (current_tool != null) {
current_tool.on_motion(x, y, mask);
return false;
}
if (get_zoom_state().is_default()) {
canvas.window.set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR));
} else {
canvas.window.set_cursor(new Gdk.Cursor(Gdk.CursorType.FLEUR));
}
if (is_pan_in_progress) {
int delta_x = ((int) event.x) - zoom_pan_start_point.x;
int delta_y = ((int) event.y) - zoom_pan_start_point.y;
Gdk.Point viewport_center = get_zoom_state().get_viewport_center();
viewport_center.x -= delta_x;
viewport_center.y -= delta_y;
ZoomState zoom_state = ZoomState.pan(get_zoom_state(), viewport_center);
on_interactive_pan(zoom_state);
}
return false;
}
......@@ -750,6 +948,7 @@ public abstract class EditingHostPage : SinglePhotoPage {
bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC;
bool handled = true;
switch (Gdk.keyval_name(event.keyval)) {
case "Left":
case "KP_Left":
......@@ -767,10 +966,23 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
break;
// mark as handled to prevent base from moving focus to toolbar
// this block is only here to prevent base from moving focus to toolbar
case "Down":
case "KP_Down":
handled = true;
;
break;
case "equal":
case "plus":
case "KP_Add":
on_increase_size();
break;
// underscore is the keysym generated by SHIFT-[minus sign] -- this means zoom out
case "minus":
case "underscore":
case "KP_Subtract":
on_decrease_size();
break;
default:
......@@ -815,6 +1027,8 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
private void rotate(Rotation rotation, string name, string description