Commit f6bcadf0 authored by Jim Nelson's avatar Jim Nelson

#59: Rotate photo w/ EXIF, including toolbar button.

parent fe7822f8
......@@ -85,7 +85,7 @@ public class AppWindow : Gtk.Window {
private Gtk.Box layout = null;
private Gtk.TreeStore pageTreeStore = null;
private Gtk.TreeView pageTreeView = null;
private CollectionPage collectionPage = null;
private PhotoPage photoPage = null;
......
......@@ -11,6 +11,40 @@ public class CollectionPage : Gtk.ScrolledWindow {
private static const int IMPROVAL_PRIORITY = Priority.LOW;
private static const int IMPROVAL_DELAY_MS = 500;
private static Gtk.IconFactory factory = null;
private static const string STOCK_CLOCKWISE = "shotwell-rotate-clockwise";
private static const string STOCK_COUNTERCLOCKWISE = "shotwell-rotate-counterclockwise";
private static void addStockIcon(File file, string stockID) {
debug("Adding icon %s", file.get_path());
Gdk.Pixbuf pixbuf = null;
try {
pixbuf = new Gdk.Pixbuf.from_file(file.get_path());
} catch (Error err) {
error("%s", err.message);
}
debug("%d %d", pixbuf.width, pixbuf.height);
Gtk.IconSet iconSet = new Gtk.IconSet.from_pixbuf(pixbuf);
factory.add(stockID, iconSet);
}
private static void prepIcons() {
if (factory != null)
return;
factory = new Gtk.IconFactory();
File icons = AppWindow.get_exec_dir().get_child("icons");
addStockIcon(icons.get_child("object-rotate-right.svg"), STOCK_CLOCKWISE);
addStockIcon(icons.get_child("object-rotate-left.svg"), STOCK_COUNTERCLOCKWISE);
factory.add_default();
}
private PhotoTable photoTable = new PhotoTable();
private CollectionLayout layout = new CollectionLayout();
private Gtk.ActionGroup mainActionGroup = new Gtk.ActionGroup("CollectionActionGroup");
......@@ -18,6 +52,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
private Gtk.MenuBar menubar = null;
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.HScale slider = 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;
......@@ -43,12 +78,14 @@ public class CollectionPage : Gtk.ScrolledWindow {
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] RIGHT_CLICK_ACTIONS = {
{ "Remove", Gtk.STOCK_DELETE, "_Remove", "Delete", "Remove the selected photos from the library", on_remove },
{ "RotateClockwise", null, "Rotate c_lockwise", "<Ctrl>R", "Rotate the selected photos clockwise", on_rotate_clockwise },
{ "RotateCounterclockwise", null, "Rotate c_ounterclockwise", "<Ctrl><Shift>R", "Rotate the selected photos counterclockwise", on_rotate_counterclockwise },
{ "RotateClockwise", STOCK_CLOCKWISE, "Rotate c_lockwise", "<Ctrl>R", "Rotate the selected photos clockwise", on_rotate_clockwise },
{ "RotateCounterclockwise", 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 }
};
construct {
prepIcons();
// set up action group
mainActionGroup.add_actions(ACTIONS, this);
AppWindow.get_ui_manager().insert_action_group(mainActionGroup, 0);
......@@ -61,6 +98,20 @@ public class CollectionPage : Gtk.ScrolledWindow {
// set up page's toolbar (used by AppWindow for layout)
//
// rotate tool
rotateButton = new Gtk.ToolButton.from_stock(STOCK_CLOCKWISE);
rotateButton.sensitive = false;
rotateButton.clicked += on_rotate_clockwise;
toolbar.insert(rotateButton, -1);
// separator to force slider to right side of toolbar
Gtk.SeparatorToolItem separator = new Gtk.SeparatorToolItem();
separator.set_expand(true);
separator.set_draw(false);
toolbar.insert(separator, -1);
// thumbnail size slider
slider = new Gtk.HScale.with_range(0, scaleToSlider(Thumbnail.MAX_SCALE), 1);
slider.set_value(scaleToSlider(scale));
......@@ -71,12 +122,15 @@ public class CollectionPage : Gtk.ScrolledWindow {
toolitem.add(slider);
toolitem.set_expand(false);
toolitem.set_size_request(200, -1);
toolbar.insert(toolitem, -1);
// scrollbar policy
set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
AppWindow.get_main_window().key_press_event += on_key_pressed;
AppWindow.get_main_window().key_release_event += on_key_released;
// this schedules thumbnail improvement whenever the window size changes (and new thumbnails
// may be exposed)
size_allocate += schedule_thumbnail_improval;
......@@ -139,6 +193,8 @@ public class CollectionPage : Gtk.ScrolledWindow {
selectedList.add(thumbnail);
thumbnail.select();
}
rotateButton.sensitive = true;
}
public void unselect_all() {
......@@ -148,6 +204,8 @@ public class CollectionPage : Gtk.ScrolledWindow {
}
selectedList = new Gee.HashSet<Thumbnail>();
rotateButton.sensitive = false;
}
public Thumbnail[] get_selected() {
......@@ -165,11 +223,15 @@ public class CollectionPage : Gtk.ScrolledWindow {
public void select(Thumbnail thumbnail) {
thumbnail.select();
selectedList.add(thumbnail);
rotateButton.sensitive = true;
}
public void unselect(Thumbnail thumbnail) {
thumbnail.unselect();
selectedList.remove(thumbnail);
rotateButton.sensitive = (selectedList.size != 0);
}
public void toggle_select(Thumbnail thumbnail) {
......@@ -180,6 +242,8 @@ public class CollectionPage : Gtk.ScrolledWindow {
// now unselected
selectedList.remove(thumbnail);
}
rotateButton.sensitive = (selectedList.size != 0);
}
public int get_selected_count() {
......@@ -399,6 +463,9 @@ public class CollectionPage : Gtk.ScrolledWindow {
layout.remove_thumbnail(thumbnail);
}
assert(selectedList.size == 0);
rotateButton.sensitive = false;
layout.refresh();
}
......@@ -456,5 +523,30 @@ public class CollectionPage : Gtk.ScrolledWindow {
private void on_slider_changed() {
set_thumb_size(sliderToScale(slider.get_value()));
}
private static const uint KEY_CTRL_L = Gdk.keyval_from_name("Control_L");
private static const uint KEY_CTRL_R = Gdk.keyval_from_name("Control_R");
private static const uint KEY_ALT_L = Gdk.keyval_from_name("Alt_L");
private static const uint KEY_ALT_R = Gdk.keyval_from_name("Alt_R");
private bool on_key_pressed(AppWindow aw, Gdk.EventKey event) {
if ((event.keyval == KEY_CTRL_L) || (event.keyval == KEY_CTRL_R)) {
rotateButton.set_stock_id(STOCK_COUNTERCLOCKWISE);
rotateButton.clicked -= on_rotate_clockwise;
rotateButton.clicked += on_rotate_counterclockwise;
}
return false;
}
private bool on_key_released(AppWindow aw, Gdk.EventKey event) {
if ((event.keyval == KEY_CTRL_L) || (event.keyval == KEY_CTRL_R)) {
rotateButton.set_stock_id(STOCK_CLOCKWISE);
rotateButton.clicked -= on_rotate_counterclockwise;
rotateButton.clicked += on_rotate_clockwise;
}
return false;
}
}
......@@ -203,7 +203,7 @@ public errordomain ExifError {
}
extern void free(void *ptr);
public class PhotoExif {
private File file;
private Exif.Data exifData = null;
......@@ -344,7 +344,6 @@ public class PhotoExif {
assert(flattenedSize > 0);
try {
/*
if (flattenedSize == segmentLength) {
// the new EXIF data is exactly the same size as the data in the file, so simply
// overwrite
......@@ -355,19 +354,21 @@ public class PhotoExif {
fins.close(null);
fins = null;
// open for writing
FileOutputStream fouts = file.replace(null, false, FileCreateFlags.PRIVATE, null);
size_t bytesWritten = 0;
// open for writing ... don't use FileOutputStream, as it will overwrite everything
// it seeks over
FStream fs = FStream.open(file.get_path(), "r+");
if (fs == null)
throw new IOError.FAILED("%s: fopen() error".printf(file.get_path()));
// seeking with a replace() file destroys what's it seeks past (??), so just
// writing since it's all of 6 bytes
// Marker:SOI and Marker:APP1 and length (none change)
write_marker(fouts, Jpeg.Marker.SOI, 0);
write_marker(fouts, Jpeg.Marker.APP1, flattenedSize);
fouts.write_all(flattened, flattenedSize, out bytesWritten, null);
fouts.close(null);
} else */ {
// seek over Marker:SOI, Marker:APP1, and length (none change)
if (fs.seek(2 + 2 + 2, FileSeek.SET) < 0)
throw new IOError.FAILED("%s: fseek() error %d", file.get_path(), fs.error());
// write data in over current EXIF
size_t countWritten = fs.write(flattened, flattenedSize, 1);
if (countWritten != 1)
throw new IOError.FAILED("%s: fwrite() error %d", file.get_path(), errno);
} else {
// create a new photo file with the updated EXIF and move it on top of the old one
// skip past APP1
......@@ -386,6 +387,7 @@ public class PhotoExif {
// write APP1 with EXIF data
write_marker(fouts, Jpeg.Marker.APP1, flattenedSize);
fouts.write_all(flattened, flattenedSize, out bytesWritten, null);
assert(bytesWritten == flattenedSize);
// copy remainder of file into new file
uint8[] copyBuffer = new uint8[64 * 1024];
......@@ -397,6 +399,7 @@ public class PhotoExif {
assert(bytesRead > 0);
fouts.write_all(copyBuffer, bytesRead, out bytesWritten, null);
assert(bytesWritten == bytesRead);
}
// close both for move
......
......@@ -17,7 +17,8 @@ SRC_FILES = \
Exif.vala
VAPI_FILES = \
libexif.vapi
libexif.vapi \
fstream.vapi
VAPI_DIRS = \
.
......@@ -28,7 +29,8 @@ PKGS = \
gtk+-2.0 \
sqlite3 \
vala-1.0 \
libexif
libexif \
fstream
all: $(TARGET)
......
/*
* FStream is a patch of the GLib FileStream object. FileStream does not offer fread() and fwrite()
* wrappers, which is needed for Exif (and possibly other) functions. Rather than patching GLib,
* the extended code is here. Should be easily removed if/when GLib is patched.
*/
[Compact]
[CCode (cname = "FILE", free_function = "fclose", cheader_filename = "stdio.h")]
public class FStream {
[CCode (cname = "fopen")]
public static FStream? open (string path, string mode);
[CCode (cname = "fdopen")]
public static FStream? fdopen (int fildes, string mode);
[CCode (cname = "fprintf")]
[PrintfFormat ()]
public void printf (string format, ...);
[CCode (cname = "fputc", instance_pos = -1)]
public void putc (char c);
[CCode (cname = "fputs", instance_pos = -1)]
public void puts (string s);
[CCode (cname = "fgetc")]
public int getc ();
[CCode (cname = "fgets", instance_pos = -1)]
public weak string gets (char[] s);
[CCode (cname = "feof")]
public bool eof ();
[CCode (cname = "fscanf")]
public int scanf (string format, ...);
[CCode (cname = "fflush")]
public int flush ();
[CCode (cname = "fseek")]
public int seek (long offset, GLib.FileSeek whence);
[CCode (cname = "ftell")]
public long tell ();
[CCode (cname = "rewind")]
public void rewind ();
[CCode (cname = "fileno")]
public int fileno ();
[CCode (cname = "ferror")]
public int error ();
[CCode (cname = "clearerr")]
public void clearerr ();
[CCode (cname = "fread", instance_pos = -1)]
public size_t read (void *ptr, size_t size, size_t count);
[CCode (cname = "fwrite", instance_pos = -1)]
public size_t write (void *ptr, size_t size, size_t count);
}
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