Commit e7f351d3 authored by Jim Nelson's avatar Jim Nelson

#177: Fullscreen F11 with floating control bar. #71: Let use select key photo...

#177: Fullscreen F11 with floating control bar.  #71: Let use select key photo for each event.  Added gphoto macros to prevent worrisome compiler warnings.
parent bf0a233a
public class FullscreenWindow : Gtk.Window {
public static const int TOOLBAR_DISMISSAL_MSEC = 2500;
private Gtk.Window toolbar_window = new Gtk.Window(Gtk.WindowType.POPUP);
private PhotoPage photo_page = new PhotoPage();
private Gtk.ToolButton close_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_CLOSE);
private Gtk.ToggleToolButton pin_button = new Gtk.ToggleToolButton();
private bool is_toolbar_shown = false;
public FullscreenWindow(Gdk.Screen screen, CheckerboardPage controller, Thumbnail start) {
set_screen(screen);
set_border_width(0);
photo_page.display(controller, start);
pin_button.set_label("Pin toolbar");
close_button.clicked += on_close;
Gtk.Toolbar toolbar = photo_page.get_toolbar();
toolbar.insert(pin_button, -1);
toolbar.insert(close_button, -1);
// set up toolbar along bottom of screen, but don't show yet
toolbar_window.set_screen(get_screen());
toolbar_window.set_default_size(screen.get_width(), -1);
toolbar_window.set_border_width(0);
toolbar_window.add(toolbar);
add(photo_page);
// need to do this to create a Gdk.Window to set masks
fullscreen();
show_all();
// want to receive motion events
Gdk.EventMask mask = window.get_events();
mask |= Gdk.EventMask.POINTER_MOTION_MASK;
window.set_events(mask);
motion_notify_event += on_motion;
}
private void on_close() {
toolbar_window.hide();
toolbar_window = null;
AppWindow.get_instance().end_fullscreen();
}
private bool on_motion(FullscreenWindow fsw, Gdk.EventMotion event) {
show_toolbar();
return false;
}
private void show_toolbar() {
if (is_toolbar_shown)
return;
toolbar_window.show_all();
Gtk.Requisition req;
toolbar_window.size_request(out req);
toolbar_window.move(0, toolbar_window.get_screen().get_height() - req.height);
toolbar_window.present();
is_toolbar_shown = true;
Timeout.add(TOOLBAR_DISMISSAL_MSEC, on_check_toolbar_dismissal);
}
private bool on_check_toolbar_dismissal() {
if (!is_toolbar_shown)
return false;
if (toolbar_window == null)
return false;
// if pinned, keep open but keep checking
if (pin_button.get_active())
return true;
// if the pointer is on the window, keep it alive, but keep checking
int x, y, width, height, px, py;
toolbar_window.window.get_geometry(out x, out y, out width, out height, null);
toolbar_window.get_display().get_pointer(null, out px, out py, null);
if ((px >= x) && (px <= x + width) && (py >= y) && (py <= y + height))
return true;
toolbar_window.hide();
is_toolbar_shown = false;
return false;
}
}
public class AppWindow : Gtk.Window {
public static const string TITLE = "Shotwell";
public static const string VERSION = "0.0.1";
......@@ -27,7 +125,8 @@ public class AppWindow : Gtk.Window {
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] COMMON_ACTIONS = {
{ "CommonQuit", Gtk.STOCK_QUIT, "_Quit", "<Ctrl>Q", "Quit Shotwell", Gtk.main_quit },
{ "CommonAbout", Gtk.STOCK_ABOUT, "_About", null, "About Shotwell", on_about }
{ "CommonAbout", Gtk.STOCK_ABOUT, "_About", null, "About Shotwell", on_about },
{ "CommonFullscreen", Gtk.STOCK_FULLSCREEN, "_Fullscreen", "F11", "Use Shotwell at fullscreen", on_fullscreen }
};
public static void init(string[] args) {
......@@ -117,6 +216,8 @@ public class AppWindow : Gtk.Window {
private SortedList<int64?> imported_photos = null;
private ImportID import_id = ImportID();
private FullscreenWindow fullscreen_window = null;
construct {
// if this is the first AppWindow, it's the main AppWindow
assert(instance == null);
......@@ -209,7 +310,7 @@ public class AppWindow : Gtk.Window {
return actionGroup;
}
public void on_about() {
private void on_about() {
// TODO: More thorough About box
Gtk.show_about_dialog(this,
"version", AppWindow.VERSION,
......@@ -219,6 +320,50 @@ public class AppWindow : Gtk.Window {
);
}
private void on_fullscreen() {
if (fullscreen_window != null) {
fullscreen_window.present();
return;
}
if (current_page is CheckerboardPage) {
LayoutItem item = ((CheckerboardPage) current_page).get_fullscreen_photo();
if (item == null) {
debug("No fullscreen photo for this view");
return;
}
// needs to be a thumbnail
assert(item is Thumbnail);
// set up fullscreen view and hide ourselves until it's closed
fullscreen_window = new FullscreenWindow(get_screen(), (CheckerboardPage) current_page,
(Thumbnail) item);
} else if (current_page is PhotoPage) {
fullscreen_window = new FullscreenWindow(get_screen(), ((PhotoPage) current_page).get_controller(),
((PhotoPage) current_page).get_thumbnail());
} else {
error("Unable to present fullscreen view for this page");
}
fullscreen_window.present();
hide();
}
public void end_fullscreen() {
if (fullscreen_window == null)
return;
show_all();
fullscreen_window.hide();
fullscreen_window = null;
present();
}
public class DateComparator : Comparator<int64?> {
private PhotoTable photo_table;
......@@ -627,7 +772,7 @@ public class AppWindow : Gtk.Window {
switch_to_page(photoPage);
}
private EventPage? find_event_page(EventID event_id) {
public EventPage? find_event_page(EventID event_id) {
foreach (EventPage page in event_list) {
if (page.event_id.id == event_id.id)
return page;
......@@ -835,7 +980,7 @@ public class AppWindow : Gtk.Window {
if (marker.get_row() != null)
sidebar.get_selection().select_path(marker.get_row().get_path());
show_all();
page.show_all();
page.switched_to();
......
......@@ -23,6 +23,7 @@ public class CollectionPage : CheckerboardPage {
private Gtk.ToolButton rotateButton = null;
private int scale = Thumbnail.DEFAULT_SCALE;
private bool improval_scheduled = false;
private bool in_view = false;
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] ACTIONS = {
......@@ -39,7 +40,7 @@ public class CollectionPage : CheckerboardPage {
{ "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 },
{ "ViewMenu", null, "_View", null, null, null },
{ "ViewMenu", null, "_View", null, null, on_view_menu },
{ "SortPhotos", null, "_Sort Photos", null, null, null },
{ "HelpMenu", null, "_Help", null, null, null }
......@@ -59,10 +60,17 @@ public class CollectionPage : CheckerboardPage {
{ "SortDescending", null, "D_escending", null, "Sort photos in a descending order", SORT_ORDER_DESCENDING }
};
public CollectionPage(PhotoID[] photos) {
public CollectionPage(PhotoID[] photos, string? ui_filename = null, Gtk.ActionEntry[]? child_actions = null) {
init_ui_start("collection.ui", "CollectionActionGroup", ACTIONS, TOGGLE_ACTIONS);
actionGroup.add_radio_actions(SORT_CRIT_ACTIONS, SORT_BY_NAME, on_sort_changed);
actionGroup.add_radio_actions(SORT_ORDER_ACTIONS, SORT_ORDER_ASCENDING, on_sort_changed);
if (ui_filename != null)
init_load_ui(ui_filename);
if (child_actions != null)
actionGroup.add_actions(child_actions, this);
init_ui_bind("/CollectionMenuBar");
init_context_menu("/CollectionContextMenu");
......@@ -122,7 +130,13 @@ public class CollectionPage : CheckerboardPage {
return toolbar;
}
public override void switching_from() {
in_view = false;
}
public override void switched_to() {
in_view = true;
// need to refresh the layout in case any of the thumbnail dimensions were altered while we
// were gone
refresh();
......@@ -145,6 +159,24 @@ public class CollectionPage : CheckerboardPage {
AppWindow.get_instance().switch_to_photo_page(this, thumbnail);
}
public override LayoutItem? get_fullscreen_photo() {
Gee.Iterable<LayoutItem> iter = null;
// if no selection, use the first item
if (get_selected_count() > 0) {
iter = get_selected();
} else {
iter = get_items();
}
// use the first item of the selected collection to start things off
foreach (LayoutItem item in iter) {
return item;
}
return null;
}
private int lastWidth = 0;
private int lastHeight = 0;
......@@ -242,6 +274,10 @@ public class CollectionPage : CheckerboardPage {
private bool reschedule_improval = false;
private void schedule_thumbnail_improval() {
// don't bother if not in view
if (!in_view)
return;
if (improval_scheduled == false) {
improval_scheduled = true;
Timeout.add_full(IMPROVAL_PRIORITY, IMPROVAL_DELAY_MS, improve_thumbnail_quality);
......@@ -281,7 +317,7 @@ public class CollectionPage : CheckerboardPage {
select_all();
}
private void on_photos_menu() {
protected virtual void on_photos_menu() {
bool selected = (get_selected_count() > 0);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/IncreaseSize", scale < Thumbnail.MAX_SCALE);
......@@ -372,6 +408,10 @@ public class CollectionPage : CheckerboardPage {
});
}
private void on_view_menu() {
set_item_sensitive("/CollectionMenuBar/ViewMenu/Fullscreen", get_count() > 0);
}
private bool display_titles() {
Gtk.ToggleAction action = (Gtk.ToggleAction) ui.get_action("/CollectionMenuBar/ViewMenu/ViewTitle");
......
......@@ -35,6 +35,8 @@ public class EventsDirectoryPage : CheckerboardPage {
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] ACTIONS = {
{ "FileMenu", null, "_File", null, null, null },
{ "ViewMenu", null, "_View", null, null, on_view_menu },
{ "HelpMenu", null, "_Help", null, null, null }
};
......@@ -59,6 +61,25 @@ public class EventsDirectoryPage : CheckerboardPage {
AppWindow.get_instance().switch_to_event(event.event_id);
}
public override LayoutItem? get_fullscreen_photo() {
Gee.Iterable<LayoutItem> iter = null;
// use first selected item, otherwise use first item
if (get_selected_count() > 0) {
iter = get_selected();
} else {
iter = get_items();
}
foreach (LayoutItem item in iter) {
EventPage page = AppWindow.get_instance().find_event_page(((DirectoryItem) item).event_id);
if (page != null)
return page.get_fullscreen_photo();
}
return null;
}
public void add_event(EventID event_id) {
DirectoryItem item = new DirectoryItem(event_id);
add_item(item);
......@@ -76,15 +97,43 @@ public class EventsDirectoryPage : CheckerboardPage {
refresh();
}
private void on_view_menu() {
set_item_sensitive("/EventsDirectoryMenuBar/ViewMenu/Fullscreen", get_count() > 0);
}
}
public class EventPage : CollectionPage {
public EventID event_id;
private EventTable event_table = new EventTable();
private const Gtk.ActionEntry[] ACTIONS = {
{ "MakePrimary", null, "Make _Primary", null, null, on_make_primary }
};
public EventPage(EventID event_id, PhotoID[] photos) {
base(photos);
base(photos, "event.ui", ACTIONS);
this.event_id = event_id;
}
protected override void on_photos_menu() {
set_item_sensitive("/CollectionMenuBar/PhotosMenu/MakePrimary", get_selected_count() == 1);
base.on_photos_menu();
}
private void on_make_primary() {
assert(get_selected_count() == 1);
// iterate to first one, use that, bail out
foreach (LayoutItem item in get_selected()) {
Thumbnail thumbnail = (Thumbnail) item;
event_table.set_primary_photo(event_id, thumbnail.get_photo_id());
break;
}
}
}
......@@ -41,21 +41,22 @@ namespace GPhoto {
return new Gdk.Pixbuf.from_file("shotwell.tmp");
}
public void save_image(Context context, Camera camera, string folder, string filename, File destFile) throws Error {
GPhoto.CameraFile cameraFile;
GPhoto.Result res = GPhoto.CameraFile.create(out cameraFile);
public void save_image(Context context, Camera camera, string folder, string filename, File dest_file) throws Error {
GPhoto.CameraFile camera_file;
GPhoto.Result res = GPhoto.CameraFile.create(out camera_file);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error allocating camera file: %s", (int) res, res.as_string());
res = camera.get_file(folder, filename, GPhoto.CameraFileType.NORMAL, cameraFile, context);
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",
(int) res, folder, filename, res.as_string());
res = cameraFile.save(destFile.get_path());
res = camera_file.save(dest_file.get_path());
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error copying file %s/%s to %s: %s", (int) res,
folder, filename, destFile.get_path(), res.as_string());
folder, filename, dest_file.get_path(), res.as_string());
}
public Exif.Data? load_exif(Context context, Camera camera, string folder, string filename,
......
......@@ -212,6 +212,12 @@ public class ImportPage : CheckerboardPage {
importSelectedButton.sensitive = !busy && refreshed && (count > 0);
}
public override LayoutItem? get_fullscreen_photo() {
error("No fullscreen support for import pages");
return null;
}
public void on_unmounted(Object source, AsyncResult aresult) {
debug("on_unmounted");
......@@ -408,28 +414,48 @@ public class ImportPage : CheckerboardPage {
}
private void import(Gee.Iterable<LayoutItem> items) {
File photosDir = AppWindow.get_photos_dir();
File photos_dir = AppWindow.get_photos_dir();
GPhoto.Result res = camera.init(initContext.context);
if (res != GPhoto.Result.OK) {
AppWindow.error_message("Unable to lock camera: %s".printf(res.as_string()));
return;
}
busy = true;
importSelectedButton.sensitive = false;
importAllButton.sensitive = false;
ProgressBarContext savingContext = new ProgressBarContext(progressBar, "");
ProgressBarContext saving_context = new ProgressBarContext(progressBar, "");
AppWindow.get_instance().start_import_batch();
try {
foreach (LayoutItem item in items) {
ImportPreview preview = (ImportPreview) item;
// XXX: There is a condition in libgphoto where a dirty flag isn't being set on a directory
// object and it's not updated from the camera, which means the file won't be found.
// This forces a refresh of the folder.
GPhoto.CameraList files;
res = GPhoto.CameraList.create(out files);
assert(res == GPhoto.Result.OK);
res = camera.list_files(preview.folder, files, saving_context.context);
if (res != GPhoto.Result.OK) {
// log message and continue
debug("Unable to list files for %s: %d %s", preview.folder, (int) res, res.as_string());
}
// TODO: Currently, files are stored flat in the directory and imported photos will
// overwrite ones with the same name
File destFile = photosDir.get_child(preview.filename);
File dest_file = photos_dir.get_child(preview.filename);
savingContext.set_message("Importing %s".printf(preview.filename));
saving_context.set_message("Importing %s".printf(preview.filename));
try {
GPhoto.save_image(savingContext.context, camera, preview.folder, preview.filename, destFile);
GPhoto.save_image(saving_context.context, camera, preview.folder, preview.filename,
dest_file);
} catch (Error err) {
// TODO: Give user option to cancel operation entirely
AppWindow.error_message("Unable to import %s: %s".printf(preview.filename, err.message));
......@@ -437,7 +463,7 @@ public class ImportPage : CheckerboardPage {
continue;
}
AppWindow.get_instance().import(destFile);
AppWindow.get_instance().import(dest_file);
// have to do this because of bug: return will skip finally block
bool quit = false;
......@@ -454,9 +480,16 @@ public class ImportPage : CheckerboardPage {
} finally {
importSelectedButton.sensitive = get_selected_count() > 0;
importAllButton.sensitive = get_count() > 0;
saving_context.set_message("");
AppWindow.get_instance().end_import_batch();
res = camera.exit(initContext.context);
if (res != GPhoto.Result.OK) {
// log but don't fail
message("Unable to unlock camera: %s (%d)", res.as_string(), (int) res);
}
busy = false;
}
}
......
......@@ -30,6 +30,9 @@ RESOURCE_FILES = \
photo.ui \
collection.ui \
import.ui
HEADER_FILES = \
gphoto.h
VAPI_DIRS = \
.
......@@ -70,7 +73,7 @@ uninstall:
rm -fr /usr/local/share/shotwell
rm -f /usr/share/applications/shotwell.desktop
$(TARGET): $(SRC_FILES) $(VAPI_FILES) Makefile
$(TARGET): $(SRC_FILES) $(VAPI_FILES) $(HEADER_FILES) Makefile
pkg-config --print-errors --exists $(EXT_PKGS)
valac $(VALAC_OPTS) \
$(foreach pkg,$(PKGS),--pkg=$(pkg)) \
......
......@@ -121,21 +121,25 @@ public abstract class Page : Gtk.ScrolledWindow {
init_ui_bind(menuBarPath);
}
protected void init_ui_start(string uiFilename, string actionGroupName,
Gtk.ActionEntry[]? entries = null, Gtk.ToggleActionEntry[]? toggleEntries = null) {
File uiFile = data_dir.get_child(uiFilename);
protected void init_load_ui(string ui_filename) {
File ui_file = data_dir.get_child(ui_filename);
try {
ui.add_ui_from_file(uiFile.get_path());
ui.add_ui_from_file(ui_file.get_path());
} catch (Error gle) {
error("Error loading UI file %s: %s", uiFilename, gle.message);
error("Error loading UI file %s: %s", ui_filename, gle.message);
}
actionGroup = new Gtk.ActionGroup(actionGroupName);
}
protected void init_ui_start(string ui_filename, string action_group_name,
Gtk.ActionEntry[]? entries = null, Gtk.ToggleActionEntry[]? toggle_entries = null) {
init_load_ui(ui_filename);
actionGroup = new Gtk.ActionGroup(action_group_name);
if (entries != null)
actionGroup.add_actions(entries, this);
if (toggleEntries != null)
actionGroup.add_toggle_actions(toggleEntries, this);
if (toggle_entries != null)
actionGroup.add_toggle_actions(toggle_entries, this);
}
protected void init_ui_bind(string? menuBarPath) {
......@@ -250,6 +254,8 @@ public abstract class CheckerboardPage : Page {
public virtual void on_item_activated(LayoutItem item) {
}
public abstract LayoutItem? get_fullscreen_photo();
public void refresh() {
show_all();
layout.refresh();
......
......@@ -19,6 +19,8 @@ public class PhotoPage : Page {
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] ACTIONS = {
{ "FileMenu", null, "_File", null, null, null },
{ "ViewMenu", null, "_View", null, null, null },
{ "PhotoMenu", null, "_Photo", null, null, null },
{ "PrevPhoto", Gtk.STOCK_GO_BACK, "_Previous Photo", null, "Previous Photo", on_previous_photo },
......@@ -32,7 +34,7 @@ public class PhotoPage : Page {
private PhotoTable photo_table = new PhotoTable();
construct {
public PhotoPage() {
init_ui("photo.ui", "/PhotoMenuBar", "PhotoActionGroup", ACTIONS);
// set up page's toolbar (used by AppWindow for layout)
......@@ -80,16 +82,22 @@ public class PhotoPage : Page {
update_sensitivity();
}
public CheckerboardPage get_controller() {
return controller;
}
public Thumbnail get_thumbnail() {
return thumbnail;
}
private void update_display() {
if (thumbnail == null) {
// TODO: Display error message
return;
}
orientation = photo_table.get_orientation(thumbnail.get_photo_id());
original = thumbnail.get_full_pixbuf();
if (original == null)
if (original == null) {
debug("Unable to fetch full pixbuf for %s", thumbnail.get_name());
return;
}
rotated = rotate_to_exif(original, orientation);
rotatedDim = Dimensions.for_pixbuf(rotated);
......
......@@ -29,6 +29,7 @@
<menuitem name="SortAscending" action="SortAscending" />
<menuitem name="SortDescending" action="SortDescending" />
</menu>
<menuitem name="Fullscreen" action="CommonFullscreen" />
</menu>
<menu name="HelpMenu" action="HelpMenu">
......
<ui>
<menubar name="CollectionMenuBar">
<menu name="PhotosMenu" action="PhotosMenu">
<separator />
<menuitem name="MakePrimary" action="MakePrimary" />
</menu>
</menubar>
<popup name="CollectionContextMenu">
<separator />
<menuitem name="ContextMakePrimary" action="MakePrimary" />
</popup>
</ui>
......@@ -5,6 +5,10 @@
<menuitem name="Quit" action="CommonQuit" />
</menu>
<menu name="ViewMenu" action="ViewMenu">
<menuitem name="Fullscreen" action="CommonFullscreen" />
</menu>
<menu name="HelpMenu" action="HelpMenu">
<menuitem name="About" action="CommonAbout" />
</menu>
......
#ifndef GPHOTO_H
#define GPHOTO_H
#define GPHOTO_REF_CAMERA(c) (gp_camera_ref(c) == GP_OK ? c : NULL)
#define GPHOTO_REF_FILE(c) (gp_file_ref(c) == GP_OK ? c : NULL)
#define GPHOTO_REF_LIST(c) (gp_list_ref(c) == GP_OK ? c : NULL)
#define GPHOTO_REF_CONTEXT(c) (gp_context_ref(c) == GP_OK ? c : NULL)
#endif /* GPHOTO_H */
......@@ -44,11 +44,10 @@ namespace GPhoto {
[Compact]
[CCode (
cname="Camera",
ref_function="gp_camera_ref",
ref_function_void=true,
ref_function="GPHOTO_REF_CAMERA",
unref_function="gp_camera_unref",
free_function="gp_camera_free",
cheader_filename="gphoto2/gphoto2-camera.h"
cheader_filename="gphoto2/gphoto2-camera.h,gphoto.h"
)]
public class Camera {
[CCode (cname="gp_camera_new")]
......@@ -91,11 +90,10 @@ namespace GPhoto {
[CCode (
cname="CameraFile",
cprefix="gp_file_",
ref_function="gp_file_ref",
ref_function_void=true,
ref_function="GPHOTO_REF_FILE",
unref_function="gp_file_unref",
free_function="gp_file_free",
cheader_filename="gphoto2/gphoto2-file.h"
cheader_filename="gphoto2/gphoto2-file.h,gphoto.h"
)]
public class CameraFile {
[CCode (cname="gp_file_new")]
......@@ -247,11 +245,10 @@ namespace GPhoto {
[CCode (
cname="CameraList",
cprefix="gp_list_",
ref_function="gp_list_ref",
ref_function_void=true,