Commit 5f0700ec authored by Jim Nelson's avatar Jim Nelson

#193: Drag selection now available.

parent d1da9440
......@@ -54,6 +54,10 @@ public struct Box {
return Box(rect.x, rect.y, rect.x + rect.width - 1, rect.y + rect.height - 1);
}
public static Box from_allocation(Gtk.Allocation alloc) {
return Box(alloc.x, alloc.y, alloc.x + alloc.width - 1, alloc.y + alloc.height - 1);
}
// This ensures a proper box is built from the points supplied, no matter the relationship
// between the two points
public static Box from_points(Gdk.Point corner1, Gdk.Point corner2) {
......@@ -175,18 +179,13 @@ public struct Box {
return Box(left, t, right, b);
}
public bool intersects(Box compare, out Box intersection) {
public bool intersects(Box compare) {
int left_intersect = int.max(left, compare.left);
int top_intersect = int.max(top, compare.top);
int right_intersect = int.min(right, compare.right);
int bottom_intersect = int.min(bottom, compare.bottom);
if (right_intersect < left_intersect || bottom_intersect < top_intersect)
return false;
intersection = Box(left_intersect, top_intersect, right_intersect, bottom_intersect);
return true;
return (right_intersect >= left_intersect && bottom_intersect >= top_intersect);
}
public Box get_reduced(int amount) {
......@@ -256,7 +255,7 @@ public struct Box {
// This specialized method is only concerned with the complements of identical Boxes in two
// different, spatial locations. There may be overlap between the four returned Boxes. However,
// there no portion of any of the four boxes will be outside the scope of the two compared boxes.
// no portion of any of the four boxes will be outside the scope of the two compared boxes.
public BoxComplements shifted_complements(Box shifted, out Box horizontal_this,
out Box vertical_this, out Box horizontal_shifted, out Box vertical_shifted) {
assert(get_width() == shifted.get_width());
......@@ -318,7 +317,7 @@ public struct Box {
return (ipos > top_zone) && (ipos < bottom_zone);
}
public BoxLocation location(int x, int y) {
public BoxLocation approx_location(int x, int y) {
bool near_width = near_in_between(x, left, right);
bool near_height = near_in_between(y, top, bottom);
......
......@@ -14,7 +14,7 @@ public abstract class LayoutItem : Gtk.Alignment {
private bool selected = false;
private Gtk.VBox vbox = new Gtk.VBox(false, 0);
private bool titleDisplayed = true;
private bool title_displayed = true;
public LayoutItem() {
// bottom-align everything
......@@ -58,12 +58,12 @@ public abstract class LayoutItem : Gtk.Alignment {
}
public void display_title(bool display) {
if (display && !titleDisplayed) {
if (display && !title_displayed) {
vbox.pack_end(title, false, false, LABEL_PADDING);
titleDisplayed = true;
} else if (!display && titleDisplayed) {
title_displayed = true;
} else if (!display && title_displayed) {
vbox.remove(title);
titleDisplayed = false;
title_displayed = false;
}
}
......@@ -92,11 +92,10 @@ public abstract class LayoutItem : Gtk.Alignment {
}
public bool toggle_select() {
if (selected) {
if (selected)
unselect();
} else {
else
select();
}
return selected;
}
......@@ -121,6 +120,9 @@ public class CollectionLayout : Gtk.Layout {
private Gtk.Label message = new Gtk.Label("");
private int last_width = 0;
private bool refresh_on_resize = true;
private Gdk.GC selection_gc;
private bool has_selection = false;
private Gdk.Rectangle selection;
public CollectionLayout() {
modify_bg(Gtk.StateType.NORMAL, AppWindow.BG_COLOR);
......@@ -130,7 +132,6 @@ public class CollectionLayout : Gtk.Layout {
message.set_single_line_mode(false);
message.set_use_underline(false);
expose_event += on_expose;
size_allocate += on_resize;
}
......@@ -197,6 +198,53 @@ public class CollectionLayout : Gtk.Layout {
return null;
}
public Gee.List<LayoutItem> intersection(Box box) {
Gee.ArrayList<LayoutItem> intersects = new Gee.ArrayList<LayoutItem>();
foreach (LayoutItem item in items) {
Box alloc = Box.from_allocation(item.allocation);
if (box.intersects(alloc))
intersects.add(item);
// short-circuit: if past the dimensions of the box in the sorted list, bail out
if (alloc.top > box.bottom && alloc.left > box.right)
break;
}
return intersects;
}
public void set_selection_band(Box selection) {
has_selection = true;
this.selection = selection.get_rectangle();
// generate the GC on demand rather than when window is mapped
if (selection_gc == null) {
// set up GC's for painting selection
Gdk.GCValues gc_values = Gdk.GCValues();
gc_values.foreground = fetch_color(LayoutItem.SELECTED_COLOR, bin_window);
gc_values.function = Gdk.Function.COPY;
gc_values.fill = Gdk.Fill.SOLID;
gc_values.line_width = 0;
Gdk.GCValuesMask mask =
Gdk.GCValuesMask.FOREGROUND
| Gdk.GCValuesMask.FUNCTION
| Gdk.GCValuesMask.FILL
| Gdk.GCValuesMask.LINE_WIDTH;
selection_gc = new Gdk.GC.with_values(bin_window, gc_values, mask);
}
bin_window.invalidate_rect(null, false);
}
public void remove_selection_band() {
has_selection = false;
bin_window.invalidate_rect(null, false);
}
public void clear() {
// remove page message
if (message.get_text().length > 0) {
......@@ -359,11 +407,10 @@ public class CollectionLayout : Gtk.Layout {
assert(ypadding >= 0);
// if item was recently appended, it needs to be put() rather than move()'d
if (item.parent == (Gtk.Widget) this) {
if (item.parent == (Gtk.Widget) this)
move(item, x + xpadding, y + ypadding);
} else {
else
put(item, x + xpadding, y + ypadding);
}
x += columnWidths[col] + gutter;
......@@ -412,7 +459,7 @@ public class CollectionLayout : Gtk.Layout {
}
}
private bool on_expose(CollectionLayout cl, Gdk.EventExpose event) {
private override bool expose_event(Gdk.EventExpose event) {
Gdk.Rectangle visible_rect = Gdk.Rectangle();
visible_rect.x = (int) get_hadjustment().get_value();
visible_rect.y = (int) get_vadjustment().get_value();
......@@ -422,13 +469,30 @@ public class CollectionLayout : Gtk.Layout {
Gdk.Rectangle bitbucket = Gdk.Rectangle();
foreach (LayoutItem item in items) {
if (visible_rect.intersect((Gdk.Rectangle) item.allocation, bitbucket)) {
if (visible_rect.intersect((Gdk.Rectangle) item.allocation, bitbucket))
item.exposed();
} else {
else
item.unexposed();
}
}
return false;
// draw LayoutItems before drawing selection rectangle
bool result = base.expose_event(event);
if (has_selection) {
// pixelate selection rectangle interior
Gdk.Pixbuf pixbuf = Gdk.pixbuf_get_from_drawable(null, bin_window, null, selection.x,
selection.y, 0, 0, selection.width, selection.height);
pixbuf.saturate_and_pixelate(pixbuf, 1.0f, true);
// pixelated fill
Gdk.draw_pixbuf(bin_window, selection_gc, pixbuf, 0, 0, selection.x, selection.y,
-1, -1, Gdk.RgbDither.NONE, 0, 0);
// border
Gdk.draw_rectangle(bin_window, selection_gc, false, selection.x, selection.y,
selection.width, selection.height);
}
return result;
}
}
......@@ -49,10 +49,13 @@ public abstract class Page : Gtk.ScrolledWindow {
this.event_source = event_source;
// interested in mouse button actions on the event source
event_source.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK);
// interested in mouse button and motion events on the event source
event_source.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK
| Gdk.EventMask.BUTTON_MOTION_MASK);
event_source.button_press_event += on_button_pressed_internal;
event_source.button_release_event += on_button_released_internal;
event_source.motion_notify_event += on_motion_internal;
// Use the app window's signals for window move/resize, esp. for resize, as this signal
// is used to determine inner window resizes
......@@ -330,14 +333,46 @@ public abstract class Page : Gtk.ScrolledWindow {
return false;
}
protected virtual bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
return false;
}
private bool on_motion_internal(Gdk.EventMotion event) {
int x, y;
Gdk.ModifierType mask;
if (event.is_hint) {
event_source.window.get_pointer(out x, out y, out mask);
Gtk.Adjustment hadj = get_hadjustment();
Gtk.Adjustment vadj = get_vadjustment();
// adjust x and y to viewport values
x = (x + (int) hadj.get_value()).clamp((int) hadj.get_lower(), (int) hadj.get_upper());
y = (y + (int) vadj.get_value()).clamp((int) vadj.get_lower(), (int) vadj.get_upper());
} else {
x = (int) event.x;
y = (int) event.y;
mask = event.state;
}
return on_motion(event, x, y, mask);
}
}
public abstract class CheckerboardPage : Page {
private static const int AUTOSCROLL_PIXELS = 50;
private static const int AUTOSCROLL_TICKS_MSEC = 50;
private Gtk.Menu context_menu = null;
private CollectionLayout layout = new CollectionLayout();
private Gee.HashSet<LayoutItem> selected_items = new Gee.HashSet<LayoutItem>();
private string page_name = null;
private LayoutItem last_clicked_item = null;
private bool drag_select = false;
private Gdk.Point drag_start = Gdk.Point();
private Box selection_band;
private bool autoscroll_scheduled = false;
public CheckerboardPage(string page_name) {
this.page_name = page_name;
......@@ -484,7 +519,7 @@ public abstract class CheckerboardPage : Page {
}
public void select(LayoutItem item) {
assert(layout.items.index_of(item) >= 0);
assert(layout.items.contains(item));
if (!item.is_selected()) {
item.select();
......@@ -495,7 +530,7 @@ public abstract class CheckerboardPage : Page {
}
public void unselect(LayoutItem item) {
assert(layout.items.index_of(item) >= 0);
assert(layout.items.contains(item));
if (item.is_selected()) {
item.unselect();
......@@ -570,13 +605,27 @@ public abstract class CheckerboardPage : Page {
// need to determine if the signal should be passed to the DnD handlers
// Return true to block the DnD handler, false otherwise
if (!is_dnd_enabled())
return false;
if (item == null) {
drag_select = true;
drag_start.x = (int) event.x;
drag_start.y = (int) event.y;
return true;
}
return selected_items.size == 0;
}
private override bool on_left_released(Gdk.EventButton event) {
// if drag-selecting, stop here and do nothing else
if (drag_select) {
drag_select = false;
layout.remove_selection_band();
return true;
}
// only interested in non-modified button releases
if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
return false;
......@@ -652,6 +701,100 @@ public abstract class CheckerboardPage : Page {
return true;
}
private override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
if (!drag_select)
return false;
Gdk.Point drag_end = Gdk.Point();
drag_end.x = x;
drag_end.y = y;
// save new drag rectangle
selection_band = Box.from_points(drag_start, drag_end);
updated_selection_band();
// if out of bounds, schedule a check to auto-scroll the viewport
if (!autoscroll_scheduled
&& get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) {
Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll);
autoscroll_scheduled = true;
}
return true;
}
private void updated_selection_band() {
assert(drag_select);
// get all items inside the selection
Gee.List<LayoutItem> intersection = layout.intersection(selection_band);
// deselect everything not in the intersection ... needs to be done outside the iterator
Gee.ArrayList<LayoutItem> outside = new Gee.ArrayList<LayoutItem>();
foreach (LayoutItem item in selected_items) {
if (!intersection.contains(item))
outside.add(item);
}
foreach (LayoutItem item in outside)
unselect(item);
// select everything in the intersection
foreach (LayoutItem item in intersection)
select(item);
// inform the layout about it
layout.set_selection_band(selection_band);
}
private bool selection_autoscroll() {
if (!drag_select) {
autoscroll_scheduled = false;
return false;
}
// as the viewport never scrolls horizontally, only interested in vertical
Gtk.Adjustment vadj = get_vadjustment();
int x, y;
Gdk.ModifierType mask;
layout.bin_window.get_pointer(out x, out y, out mask);
int new_value = (int) vadj.get_value();
switch (get_adjustment_relation(vadj, y)) {
case AdjustmentRelation.BELOW:
new_value -= AUTOSCROLL_PIXELS;
selection_band.top -= AUTOSCROLL_PIXELS;
if (selection_band.top < (int) vadj.get_lower())
selection_band.top = (int) vadj.get_lower();
break;
case AdjustmentRelation.ABOVE:
new_value += AUTOSCROLL_PIXELS;
selection_band.bottom += AUTOSCROLL_PIXELS;
if (selection_band.bottom > (int) vadj.get_upper())
selection_band.bottom = (int) vadj.get_upper();
break;
case AdjustmentRelation.IN_RANGE:
autoscroll_scheduled = false;
return false;
default:
warn_if_reached();
break;
}
vadj.set_value(new_value);
updated_selection_band();
return true;
}
public LayoutItem? get_next_item(LayoutItem current) {
if (layout.items.size == 0)
return null;
......
......@@ -185,14 +185,11 @@ public class PhotoPage : Page {
// turn off double-buffering because all painting happens in pixmap, and is sent to the window
// wholesale in on_canvas_expose
canvas.set_double_buffered(false);
canvas.set_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK
| Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON1_MOTION_MASK
| Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.STRUCTURE_MASK | Gdk.EventMask.SUBSTRUCTURE_MASK);
canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK
| Gdk.EventMask.SUBSTRUCTURE_MASK);
viewport.size_allocate += on_viewport_resize;
canvas.expose_event += on_canvas_exposed;
canvas.motion_notify_event += on_canvas_motion;
// PhotoPage can't use the event virtuals declared in Page because it can be hosted by
// FullscreenWindow as well as AppWindow, whose signal Page captures for the configure event
......@@ -332,7 +329,7 @@ public class PhotoPage : Page {
// scaled_crop is not maintained relative to photo's position on canvas
Box offset_scaled_crop = scaled_crop.get_offset(pixbuf_rect.x, pixbuf_rect.y);
in_manipulation = offset_scaled_crop.location(x, y);
in_manipulation = offset_scaled_crop.approx_location(x, y);
last_grab_x = x -= pixbuf_rect.x;
last_grab_y = y -= pixbuf_rect.y;
......@@ -400,19 +397,10 @@ public class PhotoPage : Page {
repaint();
}
private bool on_canvas_motion(Gtk.DrawingArea da, Gdk.EventMotion event) {
private override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
if (!show_crop)
return false;
int x, y;
if (event.is_hint) {
Gdk.ModifierType mask;
canvas.window.get_pointer(out x, out y, out mask);
} else {
x = (int) event.x;
y = (int) event.y;
}
if (in_manipulation != BoxLocation.OUTSIDE)
return on_canvas_manipulation(x, y);
......@@ -439,7 +427,7 @@ public class PhotoPage : Page {
Box offset_scaled_crop = scaled_crop.get_offset(pixbuf_rect.x, pixbuf_rect.y);
Gdk.CursorType cursor_type = Gdk.CursorType.ARROW;
switch (offset_scaled_crop.location(x, y)) {
switch (offset_scaled_crop.approx_location(x, y)) {
case BoxLocation.LEFT_SIDE:
cursor_type = Gdk.CursorType.LEFT_SIDE;
break;
......
......@@ -119,3 +119,18 @@ public void disassemble_filename(string basename, out string name, out string ex
}
}
public enum AdjustmentRelation {
BELOW,
IN_RANGE,
ABOVE
}
public AdjustmentRelation get_adjustment_relation(Gtk.Adjustment adjustment, int value) {
if (value < (int) adjustment.get_value())
return AdjustmentRelation.BELOW;
else if (value > (int) (adjustment.get_value() + adjustment.get_page_size()))
return AdjustmentRelation.ABOVE;
else
return AdjustmentRelation.IN_RANGE;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment