Commit 6ca26236 authored by Jim Nelson's avatar Jim Nelson

#229: Basic export via drag-and-drop. More work can be done on the generated...

#229: Basic export via drag-and-drop.  More work can be done on the generated JPEG (if it's not simply a copy of the file stored in the library, i.e. it's been modified).  #293: Darker background, although not configurable at this time.  
parent a7386215
......@@ -202,7 +202,7 @@ public class AppWindow : Gtk.Window {
public static const int PAGE_MIN_WIDTH =
Thumbnail.MAX_SCALE + CollectionLayout.LEFT_PADDING + CollectionLayout.RIGHT_PADDING;
public static Gdk.Color BG_COLOR = parse_color("#777");
public static Gdk.Color BG_COLOR = parse_color("#444");
public static const long EVENT_LULL_SEC = 3 * 60 * 60;
public static const long EVENT_MAX_DURATION_SEC = 12 * 60 * 60;
......@@ -210,8 +210,7 @@ public class AppWindow : Gtk.Window {
private static AppWindow instance = null;
private static string[] args = null;
// drag and drop target entries
private const Gtk.TargetEntry[] TARGET_ENTRIES = {
private const Gtk.TargetEntry[] DEST_TARGET_ENTRIES = {
{ "text/uri-list", 0, 0 }
};
......@@ -292,6 +291,13 @@ public class AppWindow : Gtk.Window {
}
}
public static void error_message(string message) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(get_instance(), Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", message);
dialog.run();
dialog.destroy();
}
// this needs to be ref'd the lifetime of the application
private Hal.Context hal_context = new Hal.Context();
private DBus.Connection hal_conn = null;
......@@ -392,8 +398,8 @@ public class AppWindow : Gtk.Window {
create_layout(collection_page);
// set up main window as a drag-and-drop destination (rather than each page; assume
// a drag and drop is for general library importation, which means it goes to collection_page)
Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, TARGET_ENTRIES, Gdk.DragAction.COPY);
// a drag and drop is for general library import, which means it goes to collection_page)
Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, DEST_TARGET_ENTRIES, Gdk.DragAction.COPY);
// set up HAL connection to monitor for device insertion/removal, to look for cameras
hal_conn = DBus.Bus.get(DBus.BusType.SYSTEM);
......@@ -611,7 +617,11 @@ public class AppWindow : Gtk.Window {
try {
info = row.file.query_info("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
} catch (Error err) {
error("%s", err.message);
// treat this as the file has been deleted from the filesystem
debug("Unable to locate %s: Removing from photo library", row.file.get_path());
photo_table.remove(photo_id);
continue;
}
TimeVal timestamp = TimeVal();
......@@ -666,25 +676,24 @@ public class AppWindow : Gtk.Window {
}
}
public override void drag_data_received(Gdk.DragContext context, int x, int y,
private override void drag_data_received(Gdk.DragContext context, int x, int y,
Gtk.SelectionData selection_data, uint info, uint time) {
// don't accept drops from our own application
if (Gtk.drag_get_source_widget(context) != null) {
Gtk.drag_finish(context, false, false, time);
return;
}
// grab URIs and release back to system
string[] uris = selection_data.get_uris();
Gtk.drag_finish(context, true, false, time);
// do the importing from within the idle loop, so the DnD transaction is completed
// TODO: Configure if photos are copied into library
BatchImport batch_import = new BatchImport(uris);
batch_import.schedule();
}
public static void error_message(string message) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(get_instance(), Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", message);
dialog.run();
dialog.destroy();
}
public void switch_to_collection_page() {
switch_to_page(collection_page);
}
......
......@@ -54,7 +54,7 @@ public class BatchImport {
dir = dir.get_child("%02u".printf(tm.day));
try {
if (dir.query_exists(null) == false)
if (!dir.query_exists(null))
dir.make_directory_with_parents(null);
} catch (Error err) {
error("Unable to create photo library directory %s", dir.get_path());
......
......@@ -52,16 +52,15 @@ public class CollectionPage : CheckerboardPage {
return timeb - timea;
}
}
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.HScale slider = null;
private Gtk.ToolButton rotate_button = null;
private int scale = Thumbnail.DEFAULT_SCALE;
private bool improval_scheduled = false;
private bool in_view = false;
private int last_width = 0;
private int last_height = 0;
private bool reschedule_improval = false;
private Gee.ArrayList<File> drag_items = new Gee.ArrayList<File>();
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] ACTIONS = {
......@@ -163,6 +162,8 @@ public class CollectionPage : CheckerboardPage {
show_all();
schedule_thumbnail_improval();
enable_drag_source(Gdk.DragAction.COPY);
}
public override Gtk.Toolbar get_toolbar() {
......@@ -233,19 +234,71 @@ public class CollectionPage : CheckerboardPage {
return null;
}
protected override bool on_resize(Gdk.Rectangle rect) {
protected override void on_resize(Gdk.Rectangle rect) {
// this schedules thumbnail improvement whenever the window size changes (and new thumbnails
// may be exposed), therefore, uninterested in window position move
if ((last_width != rect.width) || (last_height != rect.height)) {
last_width = rect.width;
last_height = rect.height;
schedule_thumbnail_improval();
}
private override void drag_begin(Gdk.DragContext context) {
assert(get_selected_count() != 0);
drag_items.clear();
schedule_thumbnail_improval();
// because drag_data_get may be called multiple times in a single drag, prepare all the exported
// files first
Gdk.Pixbuf icon = null;
foreach (LayoutItem item in get_selected()) {
Photo photo = ((Thumbnail) item).get_photo();
File file = null;
try {
file = photo.generate_exportable();
} catch (Error err) {
error("%s", err.message);
}
drag_items.add(file);
// set up icon using the "first" photo, although Sets are not ordered
if (icon == null)
icon = photo.get_thumbnail(ThumbnailCache.MEDIUM_SCALE);
debug("Prepared %s for export", file.get_path());
}
assert(icon != null);
Gtk.drag_source_set_icon_pixbuf(get_event_source(), icon);
}
private override void drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
uint target_type, uint time) {
assert(target_type == TargetType.URI_LIST);
if (drag_items.size == 0)
return;
// prepare list of uris
string[] uris = new string[drag_items.size];
int ctr = 0;
foreach (File file in drag_items)
uris[ctr++] = file.get_uri();
selection_data.set_uris(uris);
}
private override void drag_end(Gdk.DragContext context) {
drag_items.clear();
}
private override bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
debug("Drag failed: %d", (int) drag_result);
drag_items.clear();
return false;
}
public void add_photo(Photo photo) {
// search for duplicates
if (get_thumbnail_for_photo(photo) != null)
......
......@@ -267,6 +267,21 @@ public class PhotoTable : DatabaseTable {
return (time_t) stmt.column_int64(0);
}
public time_t get_timestamp(PhotoID photoID) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT timestamp FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photoID.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.ROW)
return 0;
return (time_t) stmt.column_int64(0);
}
public bool remove_by_file(File file) {
Sqlite.Statement stmt;
int res = db.prepare_v2("DELETE FROM PhotoTable WHERE filename=?", -1, out stmt);
......
......@@ -50,7 +50,6 @@ namespace GPhoto {
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error allocating camera file: %s", (int) res, res.as_string());
debug("folder=%s filename=%s", folder, filename);
res = camera.get_file(folder, filename, GPhoto.CameraFileType.NORMAL, camera_file, context);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error retrieving file object for %s/%s: %s",
......
......@@ -23,13 +23,18 @@ public abstract class Page : Gtk.ScrolledWindow {
public static const string STOCK_CLOCKWISE = "shotwell-rotate-clockwise";
public static const string STOCK_COUNTERCLOCKWISE = "shotwell-rotate-counterclockwise";
public static const Gdk.Color BG_COLOR = parse_color("#777");
protected enum TargetType {
URI_LIST
}
// For now, assuming all drag-and-drop source functions are providing the same set of targets
protected const Gtk.TargetEntry[] SOURCE_TARGET_ENTRIES = {
{ "text/uri-list", Gtk.TargetFlags.OTHER_APP, TargetType.URI_LIST }
};
private static Gtk.IconFactory factory = null;
private static void addStockIcon(File file, string stockID) {
debug("Adding icon %s", file.get_path());
private static void add_stock_icon(File file, string stock_id) {
Gdk.Pixbuf pixbuf = null;
try {
pixbuf = new Gdk.Pixbuf.from_file(file.get_path());
......@@ -37,35 +42,59 @@ public abstract class Page : Gtk.ScrolledWindow {
error("%s", err.message);
}
Gtk.IconSet iconSet = new Gtk.IconSet.from_pixbuf(pixbuf);
factory.add(stockID, iconSet);
Gtk.IconSet icon_set = new Gtk.IconSet.from_pixbuf(pixbuf);
factory.add(stock_id, icon_set);
}
private static void prepIcons() {
private static void prep_icons() {
if (factory != null)
return;
factory = new Gtk.IconFactory();
File icons_dir = AppWindow.get_ui_dir().get_child("icons");
addStockIcon(icons_dir.get_child("object-rotate-right.svg"), STOCK_CLOCKWISE);
addStockIcon(icons_dir.get_child("object-rotate-left.svg"), STOCK_COUNTERCLOCKWISE);
add_stock_icon(icons_dir.get_child("object-rotate-right.svg"), STOCK_CLOCKWISE);
add_stock_icon(icons_dir.get_child("object-rotate-left.svg"), STOCK_COUNTERCLOCKWISE);
factory.add_default();
}
public Gtk.UIManager ui = new Gtk.UIManager();
public Gtk.ActionGroup action_group = null;
public Gtk.MenuBar menu_bar = null;
public PageMarker marker = null;
private Gtk.MenuBar menu_bar = null;
private PageMarker marker = null;
private Gdk.Rectangle last_position = Gdk.Rectangle();
private Gtk.Widget event_source = null;
private bool dnd_enabled = false;
public Page() {
prepIcons();
button_press_event += on_click;
prep_icons();
}
public void set_event_source(Gtk.Widget event_source) {
assert(this.event_source == null);
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);
event_source.button_press_event += on_button_pressed_internal;
event_source.button_release_event += on_button_released_internal;
// interested in keypresses to the application itself
AppWindow.get_instance().add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK
| Gdk.EventMask.STRUCTURE_MASK);
AppWindow.get_instance().key_press_event += on_key_pressed_internal;
AppWindow.get_instance().key_release_event += on_key_released_internal;
AppWindow.get_instance().configure_event += on_configure;
// Use the app window's signals for window move/resize, esp. for resize, as this signal
// is used to determine inner window resizes
AppWindow.get_instance().configure_event += on_configure_internal;
}
public Gtk.Widget? get_event_source() {
return event_source;
}
public void set_marker(PageMarker marker) {
......@@ -98,18 +127,6 @@ public abstract class Page : Gtk.ScrolledWindow {
ui.get_widget(path).sensitive = sensitive;
}
protected virtual bool on_left_click(Gdk.EventButton event) {
return false;
}
protected virtual bool on_middle_click(Gdk.EventButton event) {
return false;
}
protected virtual bool on_right_click(Gdk.EventButton event) {
return false;
}
protected void init_ui(string ui_filename, string? menubar_path, string action_group_name,
Gtk.ActionEntry[]? entries = null, Gtk.ToggleActionEntry[]? toggle_entries = null) {
init_ui_start(ui_filename, action_group_name, entries, toggle_entries);
......@@ -147,7 +164,81 @@ public abstract class Page : Gtk.ScrolledWindow {
ui.ensure_update();
}
private bool on_click(Page p, Gdk.EventButton event) {
// This method enables drag-and-drop on the event source and routes its events through this
// object
public void enable_drag_source(Gdk.DragAction actions) {
if (dnd_enabled)
return;
assert(event_source != null);
Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES, actions);
event_source.drag_begin += on_drag_begin;
event_source.drag_data_get += on_drag_data_get;
event_source.drag_data_delete += on_drag_data_delete;
event_source.drag_end += on_drag_end;
event_source.drag_failed += on_drag_failed;
dnd_enabled = true;
}
public bool is_dnd_enabled() {
return dnd_enabled;
}
private void on_drag_begin(Gdk.DragContext context) {
drag_begin(context);
}
private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
uint info, uint time) {
drag_data_get(context, selection_data, info, time);
}
private void on_drag_data_delete(Gdk.DragContext context) {
drag_data_delete(context);
}
private void on_drag_end(Gdk.DragContext context) {
drag_end(context);
}
// wierdly, Gtk 2.16.1 doesn't supply a drag_failed virtual method in the GtkWidget impl ...
// Vala binds to it, but it's not available in gtkwidget.h, and so gcc complains. Have to
// makeshift one for now.
public virtual bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
return false;
}
private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
return source_drag_failed(context, drag_result);
}
protected virtual bool on_left_click(Gdk.EventButton event) {
return false;
}
protected virtual bool on_middle_click(Gdk.EventButton event) {
return false;
}
protected virtual bool on_right_click(Gdk.EventButton event) {
return false;
}
protected virtual bool on_left_released(Gdk.EventButton event) {
return false;
}
protected virtual bool on_middle_released(Gdk.EventButton event) {
return false;
}
protected virtual bool on_right_released(Gdk.EventButton event) {
return false;
}
private bool on_button_pressed_internal(Gdk.EventButton event) {
switch (event.button) {
case 1:
return on_left_click(event);
......@@ -162,6 +253,22 @@ public abstract class Page : Gtk.ScrolledWindow {
return false;
}
}
private bool on_button_released_internal(Gdk.EventButton event) {
switch (event.button) {
case 1:
return on_left_released(event);
case 2:
return on_middle_released(event);
case 3:
return on_right_released(event);
default:
return false;
}
}
protected virtual bool on_key_pressed(Gdk.EventKey event) {
return false;
......@@ -187,42 +294,48 @@ public abstract class Page : Gtk.ScrolledWindow {
return false;
}
protected virtual bool on_resize(Gdk.Rectangle rect) {
return false;
protected virtual void on_move(Gdk.Rectangle rect) {
}
protected virtual void on_resize(Gdk.Rectangle rect) {
}
private bool on_key_pressed_internal(AppWindow aw, Gdk.EventKey event) {
if ((event.keyval == KEY_CTRL_L) || (event.keyval == KEY_CTRL_R)) {
private bool on_key_pressed_internal(Gdk.EventKey event) {
if ((event.keyval == KEY_CTRL_L) || (event.keyval == KEY_CTRL_R))
return on_ctrl_pressed(event);
}
if ((event.keyval == KEY_ALT_L) || (event.keyval == KEY_ALT_R)) {
if ((event.keyval == KEY_ALT_L) || (event.keyval == KEY_ALT_R))
return on_alt_pressed(event);
}
return on_key_pressed(event);
}
private bool on_key_released_internal(AppWindow aw, Gdk.EventKey event) {
if ((event.keyval == KEY_CTRL_L) || (event.keyval == KEY_CTRL_R)) {
private bool on_key_released_internal(Gdk.EventKey event) {
if ((event.keyval == KEY_CTRL_L) || (event.keyval == KEY_CTRL_R))
return on_ctrl_released(event);
}
if ((event.keyval == KEY_ALT_L) || (event.keyval == KEY_ALT_R)) {
if ((event.keyval == KEY_ALT_L) || (event.keyval == KEY_ALT_R))
return on_alt_released(event);
}
return on_key_released(event);
}
private bool on_configure(AppWindow aw, Gdk.EventConfigure event) {
private bool on_configure_internal(Gdk.EventConfigure event) {
Gdk.Rectangle rect = Gdk.Rectangle();
rect.x = event.x;
rect.y = event.y;
rect.width = event.width;
rect.height = event.height;
return on_resize(rect);
if (last_position.x != rect.x || last_position.y != rect.y)
on_move(rect);
if (last_position.width != rect.width || last_position.height != rect.height)
on_resize(rect);
last_position = rect;
return false;
}
}
......@@ -231,10 +344,13 @@ public abstract class CheckerboardPage : Page {
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;
public CheckerboardPage(string page_name) {
this.page_name = page_name;
set_event_source(layout);
add(layout);
}
......@@ -325,15 +441,23 @@ public abstract class CheckerboardPage : Page {
}
public void select_all() {
bool changed = false;
foreach (LayoutItem item in layout.items) {
selected_items.add(item);
item.select();
if (!item.is_selected()) {
selected_items.add(item);
item.select();
changed = true;
}
}
on_selection_changed(selected_items.size);
if (changed)
on_selection_changed(selected_items.size);
}
public void unselect_all() {
if (selected_items.size == 0)
return;
foreach (LayoutItem item in selected_items) {
assert(item.is_selected());
item.unselect();
......@@ -343,23 +467,49 @@ public abstract class CheckerboardPage : Page {
on_selection_changed(0);
}
public void unselect_all_but(LayoutItem exception) {
assert(exception.is_selected());
if (selected_items.size == 0)
return;
bool changed = false;
foreach (LayoutItem item in selected_items) {
assert(item.is_selected());
if (item != exception) {
item.unselect();
changed = true;
}
}
selected_items.clear();
selected_items.add(exception);
if (changed)
on_selection_changed(1);
}
public void select(LayoutItem item) {
assert(layout.items.index_of(item) >= 0);
item.select();
selected_items.add(item);
on_selection_changed(selected_items.size);
if (!item.is_selected()) {
item.select();
selected_items.add(item);
on_selection_changed(selected_items.size);
}
}
public void unselect(LayoutItem item) {
assert(layout.items.index_of(item) >= 0);
item.unselect();
selected_items.remove(item);
on_selection_changed(selected_items.size);
if (item.is_selected()) {
item.unselect();
selected_items.remove(item);
on_selection_changed(selected_items.size);
}
}
public void toggle_select(LayoutItem item) {
......@@ -377,57 +527,91 @@ public abstract class CheckerboardPage : Page {
public int get_selected_count() {
return selected_items.size;
}
private override bool on_left_click(Gdk.EventButton event) {
// only interested in single-click and double-clicks for now
if ((event.type != Gdk.EventType.BUTTON_PRESS)
&& (event.type != Gdk.EventType.2BUTTON_PRESS)) {
if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS))
return false;
}
// mask out the modifiers we're interested in
uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
// use clicks for multiple selection and activation only; single selects are handled by
// button release, to allow for multiple items to be selected then dragged
LayoutItem item = get_item_at(event.x, event.y);
if (item != null) {
switch (state) {
case Gdk.ModifierType.CONTROL_MASK: {
case Gdk.ModifierType.CONTROL_MASK:
// with only Ctrl pressed, multiple selections are possible ... chosen item
// is toggled
toggle_select(item);
} break;
break;
case Gdk.ModifierType.SHIFT_MASK: {
case Gdk.ModifierType.SHIFT_MASK:
// TODO
} break;
break;
case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK: {
case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
// TODO
} break;
break;
default: {
if (event.type == Gdk.EventType.2BUTTON_PRESS) {
default:
if (event.type == Gdk.EventType.2BUTTON_PRESS)
on_item_activated(item);
} else {
// a "raw" single-click deselects all thumbnails and selects the single chosen
unselect_all();
else {
// if user has selected multiple items and is preparing for a drag, don't
// want to unselect immediately, otherwise, let the released handler deal
// with it
if (get_selected_count() == 1)
unselect_all();
select(item);
}
} break;
break;
}
} else {
// user clicked on "dead" area
unselect_all();
}
last_clicked_item = item;
// 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;
return selected_items.size == 0;
}
private override bool on_left_released(Gdk.EventButton event) {
// only interested in non-modified button releases
if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
return false;
LayoutItem item = get_item_at(event.x, event.y);
if (item == null) {
// released button on "dead" area
return false;
}
if (last_clicked_item != item) {
// user released mouse button after moving it off the initial item, or moved from dead
// space onto one. either way, unselect everything
unselect_all();
} else {
// the idea is, if a user single-clicks on an item with no modifiers, then all other items
// should be deselected, however, if they single-click in order to drag one or more items,
// they should remain selected, hence performing this here rather than on_left_click
unselect_all_but(item);
}
return true;
}
private override bool on_right_click(Gdk.EventButton event) {
// only interested in single-clicks for now
if (event.type != Gdk.EventType.BUTTON_PRESS) {
if (event.type != Gdk.EventType.BUTTON_PRESS)
return false;
}
LayoutItem item = get_item_at(event.x, event.y);
if (item != null) {
......@@ -437,16 +621,15 @@ public abstract class CheckerboardPage : Page {
}
Gtk.Menu context_menu = get_context_menu(item);
if (context_menu != null) {
if (!on_context_invoked(context_menu))
return false;
if (context_menu == null)
return false;
context_menu.popup(null, null, null, event.button, event.time);
return true;
}
if (!on_context_invoked(context_menu))
return false;
return false;
context_menu.popup(null, null, null, event.button, event.time);