Commit 2b7e1270 authored by Jim Nelson's avatar Jim Nelson

#57: Import photos from camera. Tons of refactoring to share layout and...

#57: Import photos from camera.  Tons of refactoring to share layout and functionality between multiple pages.
parent ae606325
...@@ -3,6 +3,7 @@ public class AppWindow : Gtk.Window { ...@@ -3,6 +3,7 @@ public class AppWindow : Gtk.Window {
public static const string TITLE = "Shotwell"; public static const string TITLE = "Shotwell";
public static const string VERSION = "0.0.1"; public static const string VERSION = "0.0.1";
public static const string DATA_DIR = ".photo"; public static const string DATA_DIR = ".photo";
public static const string PHOTOS_DIR = "Pictures";
public static Gdk.Color BG_COLOR = parse_color("#777"); public static Gdk.Color BG_COLOR = parse_color("#777");
...@@ -28,7 +29,7 @@ public class AppWindow : Gtk.Window { ...@@ -28,7 +29,7 @@ public class AppWindow : Gtk.Window {
} catch (Error err) { } catch (Error err) {
error("%s", err.message); error("%s", err.message);
} }
uiManager = new Gtk.UIManager(); uiManager = new Gtk.UIManager();
File uiFile = get_exec_dir().get_child("photo.ui"); File uiFile = get_exec_dir().get_child("photo.ui");
...@@ -65,6 +66,10 @@ public class AppWindow : Gtk.Window { ...@@ -65,6 +66,10 @@ public class AppWindow : Gtk.Window {
return File.new_for_path(Environment.get_home_dir()).get_child(DATA_DIR); return File.new_for_path(Environment.get_home_dir()).get_child(DATA_DIR);
} }
public static File get_photos_dir() {
return File.new_for_path(Environment.get_home_dir()).get_child(PHOTOS_DIR);
}
public static File get_data_subdir(string name, string? subname = null) { public static File get_data_subdir(string name, string? subname = null) {
File subdir = get_data_dir().get_child(name); File subdir = get_data_dir().get_child(name);
if (subname != null) { if (subname != null) {
...@@ -86,9 +91,13 @@ public class AppWindow : Gtk.Window { ...@@ -86,9 +91,13 @@ public class AppWindow : Gtk.Window {
private Gtk.TreeStore pageTreeStore = null; private Gtk.TreeStore pageTreeStore = null;
private Gtk.TreeView pageTreeView = null; private Gtk.TreeView pageTreeView = null;
private Gtk.TreePath collectionPath = null;
private Gtk.TreePath importPath = null;
private CollectionPage collectionPage = null; private CollectionPage collectionPage = null;
private PhotoPage photoPage = null; private PhotoPage photoPage = null;
private ImportPage importPage = null;
private PhotoPage importPreviewPage = null;
private PhotoTable photoTable = new PhotoTable(); private PhotoTable photoTable = new PhotoTable();
...@@ -114,6 +123,8 @@ public class AppWindow : Gtk.Window { ...@@ -114,6 +123,8 @@ public class AppWindow : Gtk.Window {
Gtk.TreeIter parent, child; Gtk.TreeIter parent, child;
pageTreeStore.append(out parent, null); pageTreeStore.append(out parent, null);
pageTreeStore.set(parent, 0, "Photos"); pageTreeStore.set(parent, 0, "Photos");
collectionPath = pageTreeStore.get_path(parent);
pageTreeView.get_selection().select_path(collectionPath);
pageTreeStore.append(out parent, null); pageTreeStore.append(out parent, null);
pageTreeStore.set(parent, 0, "Events"); pageTreeStore.set(parent, 0, "Events");
...@@ -132,6 +143,7 @@ public class AppWindow : Gtk.Window { ...@@ -132,6 +143,7 @@ public class AppWindow : Gtk.Window {
pageTreeStore.append(out parent, null); pageTreeStore.append(out parent, null);
pageTreeStore.set(parent, 0, "Import"); pageTreeStore.set(parent, 0, "Import");
importPath = pageTreeStore.get_path(parent);
pageTreeStore.append(out parent, null); pageTreeStore.append(out parent, null);
pageTreeStore.set(parent, 0, "Recent"); pageTreeStore.set(parent, 0, "Recent");
...@@ -139,6 +151,19 @@ public class AppWindow : Gtk.Window { ...@@ -139,6 +151,19 @@ public class AppWindow : Gtk.Window {
pageTreeStore.append(out parent, null); pageTreeStore.append(out parent, null);
pageTreeStore.set(parent, 0, "Trash"); pageTreeStore.set(parent, 0, "Trash");
pageTreeView.cursor_changed += () => {
Gtk.TreePath selected;
pageTreeView.get_cursor(out selected, null);
if (selected.compare(collectionPath) == 0) {
switch_to_collection_page();
} else if (selected.compare(importPath) == 0) {
switch_to_import_page();
} else {
debug("unknown");
}
};
// set up as a drag-and-drop destination // set up as a drag-and-drop destination
// this.drag_data_received() is called when a drop occurs // this.drag_data_received() is called when a drop occurs
Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, TARGET_ENTRIES, Gdk.DragAction.COPY); Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, TARGET_ENTRIES, Gdk.DragAction.COPY);
...@@ -149,13 +174,16 @@ public class AppWindow : Gtk.Window { ...@@ -149,13 +174,16 @@ public class AppWindow : Gtk.Window {
} }
collectionPage = new CollectionPage(); collectionPage = new CollectionPage();
photoPage = new PhotoPage(); photoPage = new PhotoPage("Photo");
importPage = new ImportPage();
importPreviewPage = new PhotoPage("ImportPreview");
add_accel_group(uiManager.get_accel_group()); add_accel_group(uiManager.get_accel_group());
create_start_page(); create_start_page();
} }
private void import(File file) { public void import(File file) {
FileType type = file.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null); FileType type = file.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
if(type == FileType.REGULAR) { if(type == FileType.REGULAR) {
if (!import_file(file)) { if (!import_file(file)) {
...@@ -218,7 +246,7 @@ public class AppWindow : Gtk.Window { ...@@ -218,7 +246,7 @@ public class AppWindow : Gtk.Window {
private bool import_file(File file) { private bool import_file(File file) {
debug("Importing file %s", file.get_path()); debug("Importing file %s", file.get_path());
// load full-scale photo and convert to pixbuf // TODO: Attempt to discover photo information from its metadata
Gdk.Pixbuf original; Gdk.Pixbuf original;
try { try {
original = new Gdk.Pixbuf.from_file(file.get_path()); original = new Gdk.Pixbuf.from_file(file.get_path());
...@@ -248,11 +276,11 @@ public class AppWindow : Gtk.Window { ...@@ -248,11 +276,11 @@ public class AppWindow : Gtk.Window {
Gtk.drag_finish(context, true, false, time); Gtk.drag_finish(context, true, false, time);
// import // import
collectionPage.begin_adding();
foreach (string uri in uris) { foreach (string uri in uris) {
import(File.new_for_uri(uri)); import(File.new_for_uri(uri));
} }
collectionPage.end_adding();
collectionPage.refresh();
} }
public void switch_to_collection_page() { public void switch_to_collection_page() {
...@@ -260,10 +288,19 @@ public class AppWindow : Gtk.Window { ...@@ -260,10 +288,19 @@ public class AppWindow : Gtk.Window {
} }
public void switch_to_photo_page(PhotoID photoID) { public void switch_to_photo_page(PhotoID photoID) {
photoPage.display_photo(photoID); photoPage.display_photo(photoID, collectionPage);
switch_to_page(photoPage); switch_to_page(photoPage);
} }
public void switch_to_import_preview_page(Gdk.Pixbuf pixbuf, Exif.Data exifData) {
importPreviewPage.display_pixbuf(pixbuf, exifData, importPage);
switch_to_page(importPreviewPage);
}
public void switch_to_import_page() {
switch_to_page(importPage);
}
private Gtk.Box layout = null; private Gtk.Box layout = null;
private Gtk.Box pageBox = null; private Gtk.Box pageBox = null;
private Gtk.Box clientBox = null; private Gtk.Box clientBox = null;
...@@ -291,10 +328,16 @@ public class AppWindow : Gtk.Window { ...@@ -291,10 +328,16 @@ public class AppWindow : Gtk.Window {
layout.pack_end(clientBox, true, true, 0); layout.pack_end(clientBox, true, true, 0);
add(layout); add(layout);
currentPage.switched_to();
show_all(); show_all();
} }
private void switch_to_page(Page page) { public void switch_to_page(Page page) {
if (page == currentPage)
return;
currentPage.switching_from(); currentPage.switching_from();
pageBox.remove(currentPage); pageBox.remove(currentPage);
......
public abstract class LayoutItem : Gtk.Alignment {
public static const int LABEL_PADDING = 4;
public static const int FRAME_PADDING = 4;
public static const string TEXT_COLOR = "#FFF";
public static const string SELECTED_COLOR = "#FF0";
public static const string UNSELECTED_COLOR = "#FFF";
// Due to the potential for thousands or tens of thousands of thumbnails being present in a
// particular view, all widgets used here and by subclasses should be NOWINDOW widgets.
protected Gtk.Image image = new Gtk.Image();
protected Gtk.Label title = new Gtk.Label("");
protected Gtk.Frame frame = new Gtk.Frame(null);
private bool selected = false;
public LayoutItem() {
// bottom-align everything
set(0, 1, 0, 0);
title.set_use_underline(false);
title.set_justify(Gtk.Justification.LEFT);
title.set_alignment(0, 0);
title.modify_fg(Gtk.StateType.NORMAL, parse_color(TEXT_COLOR));
Gtk.Widget panel = get_control_panel();
// store everything in a vbox, with the expandable image on top followed by a widget
// on the bottom for display and controls
Gtk.VBox vbox = new Gtk.VBox(false, 0);
vbox.set_border_width(FRAME_PADDING);
vbox.pack_start(image, false, false, 0);
vbox.pack_end(title, false, false, LABEL_PADDING);
if (panel != null)
vbox.pack_end(panel, false, false, 0);
// surround everything with a frame
frame.set_shadow_type(Gtk.ShadowType.NONE);
frame.modify_bg(Gtk.StateType.NORMAL, parse_color(UNSELECTED_COLOR));
frame.add(vbox);
add(frame);
}
public Gtk.Widget? get_control_panel() {
return null;
}
public virtual void exposed() {
}
public virtual void unexposed() {
}
public virtual void select() {
selected = true;
frame.set_shadow_type(Gtk.ShadowType.OUT);
frame.modify_bg(Gtk.StateType.NORMAL, parse_color(SELECTED_COLOR));
title.modify_fg(Gtk.StateType.NORMAL, parse_color(SELECTED_COLOR));
Gtk.Widget panel = get_control_panel();
if (panel != null)
panel.modify_fg(Gtk.StateType.NORMAL, parse_color(SELECTED_COLOR));
}
public virtual void unselect() {
selected = false;
frame.set_shadow_type(Gtk.ShadowType.NONE);
frame.modify_bg(Gtk.StateType.NORMAL, parse_color(UNSELECTED_COLOR));
title.modify_fg(Gtk.StateType.NORMAL, parse_color(UNSELECTED_COLOR));
Gtk.Widget panel = get_control_panel();
if (panel != null)
panel.modify_fg(Gtk.StateType.NORMAL, parse_color(UNSELECTED_COLOR));
}
public bool toggle_select() {
if (selected) {
unselect();
} else {
select();
}
return selected;
}
public bool is_selected() {
return selected;
}
}
public class CollectionLayout : Gtk.Layout { public class CollectionLayout : Gtk.Layout {
public static const int TOP_PADDING = 16; public static const int TOP_PADDING = 16;
public static const int BOTTOM_PADDING = 16; public static const int BOTTOM_PADDING = 16;
...@@ -9,7 +101,7 @@ public class CollectionLayout : Gtk.Layout { ...@@ -9,7 +101,7 @@ public class CollectionLayout : Gtk.Layout {
public static const int RIGHT_PADDING = 16; public static const int RIGHT_PADDING = 16;
public static const int COLUMN_GUTTER_PADDING = 24; public static const int COLUMN_GUTTER_PADDING = 24;
private Gee.ArrayList<Thumbnail> thumbnails = new Gee.ArrayList<Thumbnail>(); private Gee.ArrayList<LayoutItem> items = new Gee.ArrayList<LayoutItem>();
public CollectionLayout() { public CollectionLayout() {
modify_bg(Gtk.StateType.NORMAL, AppWindow.BG_COLOR); modify_bg(Gtk.StateType.NORMAL, AppWindow.BG_COLOR);
...@@ -17,35 +109,43 @@ public class CollectionLayout : Gtk.Layout { ...@@ -17,35 +109,43 @@ public class CollectionLayout : Gtk.Layout {
size_allocate += on_resize; size_allocate += on_resize;
} }
public void append(Thumbnail thumbnail) { public void append(LayoutItem item) {
thumbnails.add(thumbnail); items.add(item);
// need to do this to have its size requisitioned in refresh() // need to do this to have its size requisitioned in refresh()
thumbnail.show_all(); item.show_all();
} }
public void remove_thumbnail(Thumbnail thumbnail) { public void remove_item(LayoutItem item) {
thumbnails.remove(thumbnail); items.remove(item);
remove(thumbnail); remove(item);
} }
public Thumbnail? get_thumbnail_at(double xd, double yd) { public LayoutItem? get_item_at(double xd, double yd) {
int x = (int) xd; int x = (int) xd;
int y = (int) yd; int y = (int) yd;
foreach (Thumbnail thumbnail in thumbnails) { foreach (LayoutItem item in items) {
Gtk.Allocation alloc = thumbnail.allocation; Gtk.Allocation alloc = item.allocation;
if ((x >= alloc.x) && (y >= alloc.y) && (x <= (alloc.x + alloc.width)) if ((x >= alloc.x) && (y >= alloc.y) && (x <= (alloc.x + alloc.width))
&& (y <= (alloc.y + alloc.height))) { && (y <= (alloc.y + alloc.height))) {
return thumbnail; return item;
} }
} }
return null; return null;
} }
public void clear() {
foreach (LayoutItem item in items) {
remove(item);
}
items.clear();
}
public void refresh() { public void refresh() {
if (thumbnails.size == 0) if (items.size == 0)
return; return;
// don't bother until layout is of some appreciable size // don't bother until layout is of some appreciable size
...@@ -59,12 +159,12 @@ public class CollectionLayout : Gtk.Layout { ...@@ -59,12 +159,12 @@ public class CollectionLayout : Gtk.Layout {
int rowWidth = 0; int rowWidth = 0;
int widestRow = 0; int widestRow = 0;
foreach (Thumbnail thumbnail in thumbnails) { foreach (LayoutItem item in items) {
// perform size requests first time through, but not thereafter // perform size requests first time through, but not thereafter
Gtk.Requisition req; Gtk.Requisition req;
thumbnail.size_request(out req); item.size_request(out req);
// carriage return (i.e. this thumbnail will overflow the view) // carriage return (i.e. this item will overflow the view)
if ((x + req.width + RIGHT_PADDING) > allocation.width) { if ((x + req.width + RIGHT_PADDING) > allocation.width) {
if (rowWidth > widestRow) { if (rowWidth > widestRow) {
widestRow = rowWidth; widestRow = rowWidth;
...@@ -97,12 +197,12 @@ public class CollectionLayout : Gtk.Layout { ...@@ -97,12 +197,12 @@ public class CollectionLayout : Gtk.Layout {
int totalWidth = 0; int totalWidth = 0;
col = 0; col = 0;
int[] columnWidths = new int[maxCols]; int[] columnWidths = new int[maxCols];
int[] rowHeights = new int[(thumbnails.size / maxCols) + 1]; int[] rowHeights = new int[(items.size / maxCols) + 1];
int gutter = 0; int gutter = 0;
for (;;) { for (;;) {
foreach (Thumbnail thumbnail in thumbnails) { foreach (LayoutItem item in items) {
Gtk.Requisition req = thumbnail.requisition; Gtk.Requisition req = item.requisition;
if (req.height > tallest) if (req.height > tallest)
tallest = req.height; tallest = req.height;
...@@ -126,7 +226,7 @@ public class CollectionLayout : Gtk.Layout { ...@@ -126,7 +226,7 @@ public class CollectionLayout : Gtk.Layout {
if (col != 0) if (col != 0)
rowHeights[row] = tallest; rowHeights[row] = tallest;
// Step 3: Calculate the gutter between the thumbnails as being equidistant of the // Step 3: Calculate the gutter between the items as being equidistant of the
// remaining space (adding one gutter to account for the right-hand one) // remaining space (adding one gutter to account for the right-hand one)
gutter = (allocation.width - totalWidth) / (maxCols + 1); gutter = (allocation.width - totalWidth) / (maxCols + 1);
...@@ -144,38 +244,40 @@ public class CollectionLayout : Gtk.Layout { ...@@ -144,38 +244,40 @@ public class CollectionLayout : Gtk.Layout {
tallest = 0; tallest = 0;
totalWidth = 0; totalWidth = 0;
columnWidths = new int[maxCols]; columnWidths = new int[maxCols];
rowHeights = new int[(thumbnails.size / maxCols) + 1]; rowHeights = new int[(items.size / maxCols) + 1];
debug("refresh(): readjusting columns: maxCols=%d", maxCols); debug("refresh(): readjusting columns: maxCols=%d", maxCols);
} else { } else {
break; break;
} }
} }
/*
debug("refresh(): width:%d totalWidth:%d maxCols:%d gutter:%d", allocation.width, totalWidth, debug("refresh(): width:%d totalWidth:%d maxCols:%d gutter:%d", allocation.width, totalWidth,
maxCols, gutter); maxCols, gutter);
*/
// Step 4: Lay out the thumbnails in the space using all the information gathered // Step 4: Lay out the items in the space using all the information gathered
x = gutter; x = gutter;
int y = TOP_PADDING; int y = TOP_PADDING;
col = 0; col = 0;
row = 0; row = 0;
foreach (Thumbnail thumbnail in thumbnails) { foreach (LayoutItem item in items) {
Gtk.Requisition req = thumbnail.requisition; Gtk.Requisition req = item.requisition;
// this centers the thumbnail in the column // this centers the item in the column
int xpadding = (columnWidths[col] - req.width) / 2; int xpadding = (columnWidths[col] - req.width) / 2;
assert(xpadding >= 0); assert(xpadding >= 0);
// this bottom-aligns the thumbnail along the row // this bottom-aligns the item along the row
int ypadding = (rowHeights[row] - req.height); int ypadding = (rowHeights[row] - req.height);
assert(ypadding >= 0); assert(ypadding >= 0);
// if thumbnail was recently appended, it needs to be put() rather than move()'d // if item was recently appended, it needs to be put() rather than move()'d
if (thumbnail.parent == (Gtk.Widget) this) { if (item.parent == (Gtk.Widget) this) {
move(thumbnail, x + xpadding, y + ypadding); move(item, x + xpadding, y + ypadding);
} else { } else {
put(thumbnail, x + xpadding, y + ypadding); put(item, x + xpadding, y + ypadding);
} }
x += columnWidths[col] + gutter; x += columnWidths[col] + gutter;
...@@ -190,14 +292,14 @@ public class CollectionLayout : Gtk.Layout { ...@@ -190,14 +292,14 @@ public class CollectionLayout : Gtk.Layout {
} }
// Step 5: Define the total size of the page as the size of the allocated width and // Step 5: Define the total size of the page as the size of the allocated width and
// the height of all the thumbnails plus padding // the height of all the items plus padding
set_size(allocation.width, y + rowHeights[row] + BOTTOM_PADDING); set_size(allocation.width, y + rowHeights[row] + BOTTOM_PADDING);
} }
private int lastWidth = 0; private int lastWidth = 0;
private void on_resize() { private void on_resize() {
// only refresh() if the viewport width has changed // only refresh() if the width has changed
if (allocation.width != lastWidth) { if (allocation.width != lastWidth) {
lastWidth = allocation.width; lastWidth = allocation.width;
refresh(); refresh();
...@@ -220,12 +322,12 @@ public class CollectionLayout : Gtk.Layout { ...@@ -220,12 +322,12 @@ public class CollectionLayout : Gtk.Layout {
int exposedCount = 0; int exposedCount = 0;
int unexposedCount = 0; int unexposedCount = 0;
foreach (Thumbnail thumbnail in thumbnails) { foreach (LayoutItem item in items) {
if (visibleRect.intersect((Gdk.Rectangle) thumbnail.allocation, bitbucket)) { if (visibleRect.intersect((Gdk.Rectangle) item.allocation, bitbucket)) {
thumbnail.exposed(); item.exposed();
exposedCount++; exposedCount++;
} else { } else {
thumbnail.unexposed(); item.unexposed();
unexposedCount++; unexposedCount++;
} }
} }
......
public class CollectionPage : Page { public class CollectionPage : CheckerboardPage {
public static const int THUMB_X_PADDING = 20; public static const int THUMB_X_PADDING = 20;
public static const int THUMB_Y_PADDING = 20; public static const int THUMB_Y_PADDING = 20;
...@@ -8,17 +8,14 @@ public class CollectionPage : Page { ...@@ -8,17 +8,14 @@ public class CollectionPage : Page {
public static const int SLIDER_STEPPING = 1; public static const int SLIDER_STEPPING = 1;
private static const int IMPROVAL_PRIORITY = Priority.LOW; private static const int IMPROVAL_PRIORITY = Priority.LOW;
private static const int IMPROVAL_DELAY_MS = 500; private static const int IMPROVAL_DELAY_MS = 250;
private PhotoTable photoTable = new PhotoTable(); private PhotoTable photoTable = new PhotoTable();
private CollectionLayout layout = new CollectionLayout();
private Gtk.ActionGroup mainActionGroup = new Gtk.ActionGroup("CollectionActionGroup"); private Gtk.ActionGroup mainActionGroup = new Gtk.ActionGroup("CollectionActionGroup");
private Gtk.ActionGroup contextActionGroup = new Gtk.ActionGroup("CollectionContextActionGroup"); private Gtk.ActionGroup contextActionGroup = new Gtk.ActionGroup("CollectionContextActionGroup");
private Gtk.Toolbar toolbar = new Gtk.Toolbar(); private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.HScale slider = null; private Gtk.HScale slider = null;
private Gtk.ToolButton rotateButton = null; private Gtk.ToolButton rotateButton = null;
private Gee.ArrayList<Thumbnail> thumbnailList = new Gee.ArrayList<Thumbnail>();
private Gee.HashSet<Thumbnail> selectedList = new Gee.HashSet<Thumbnail>();
private int scale = Thumbnail.DEFAULT_SCALE; private int scale = Thumbnail.DEFAULT_SCALE;
private bool improval_scheduled = false; private bool improval_scheduled = false;
private bool displayTitles = true; private bool displayTitles = true;
...@@ -48,7 +45,7 @@ public class CollectionPage : Page { ...@@ -48,7 +45,7 @@ public class CollectionPage : Page {
{ "Remove", Gtk.STOCK_DELETE, "_Remove", "Delete", "Remove the selected photos from the library", on_remove }, { "Remove", Gtk.STOCK_DELETE, "_Remove", "Delete", "Remove the selected photos from the library", on_remove },
{ "CollectionRotateClockwise", STOCK_CLOCKWISE, "Rotate c_lockwise", "<Ctrl>R", "Rotate the selected photos clockwise", on_rotate_clockwise }, { "CollectionRotateClockwise", STOCK_CLOCKWISE, "Rotate c_lockwise", "<Ctrl>R", "Rotate the selected photos clockwise", on_rotate_clockwise },
{ "CollectionRotateCounterclockwise", STOCK_COUNTERCLOCKWISE, "Rotate c_ounterclockwise", "<Ctrl><Shift>R", "Rotate the selected photos counterclockwise", on_rotate_counterclockwise }, { "CollectionRotateCounterclockwise", STOCK_COUNTERCLOCKWISE, "Rotate c_ounterclockwise", "<Ctrl><Shift>R", "Rotate the selected photos counterclockwise", on_rotate_counterclockwise },
{ "Mirror", null, "_Mirror", "<Ctrl>M", "Make mirror images of the selected photos", on_mirror } { "CollectionMirror", null, "_Mirror", "<Ctrl>M", "Make mirror images of the selected photos", on_mirror }
}; };