Commit d551ac02 authored by Jim Nelson's avatar Jim Nelson

Refactoring in preparation of direct-edit window (#187): AppWindow now broken...

Refactoring in preparation of direct-edit window (#187): AppWindow now broken into AppWindow & LibraryWindow.
parent 4e3e9df4
PROGRAM = shotwell
VERSION = 0.2.0
VERSION = 0.2.1
BUILD_ROOT = 1
VALAC = valac
......@@ -45,7 +45,9 @@ SRC_FILES = \
Sidebar.vala \
ColorTransformation.vala \
EditingTools.vala \
Queryable.vala
Queryable.vala \
LibraryWindow.vala \
CameraTable.vala
VAPI_FILES = \
libexif.vapi \
......
Shotwell 0.2.0 - 05 August 2009
-----------------------------
Shotwell 0.2.0 - 7 August 2009
------------------------------
* Color histogram now available in adjustment tool.
* Drag-and-drop now supports the modifier keys for linking and asking
......@@ -11,8 +11,8 @@ Shotwell 0.2.0 - 05 August 2009
* Support for libgee-0.3.0.
Shotwell 0.1.2 - 03 August 2009
-----------------------------
Shotwell 0.1.2 - 3 August 2009
------------------------------
* Red-eye reduction feature now available.
* File/Import From Folder offers one more way to import photos into the
......
This diff is collapsed.
......@@ -207,7 +207,7 @@ public class BatchImport {
// report to AppWindow to organize into events
if (success.size > 0)
AppWindow.get_instance().batch_import_complete(success);
LibraryWindow.get_app().batch_import_complete(success);
// report completed
import_complete(import_id, success, failed, skipped);
......@@ -342,7 +342,7 @@ public class BatchImport {
success.add(photo);
// report to AppWindow for system-wide inclusion
AppWindow.get_instance().photo_imported(photo);
LibraryWindow.get_app().photo_imported(photo);
// report to observers
imported(photo);
......
/* Copyright 2009 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class DiscoveredCamera {
public GPhoto.Camera gcamera;
public string uri;
public DiscoveredCamera(GPhoto.Camera gcamera, string uri) {
this.gcamera = gcamera;
this.uri = uri;
}
}
public class CameraTable {
private const int UPDATE_DELAY_MSEC = 500;
private static CameraTable instance = null;
private static bool camera_update_scheduled = false;
// these need to be ref'd the lifetime of the instance, of which there is only one
private Hal.Context hal_context = new Hal.Context();
private DBus.Connection hal_conn = null;
private GPhoto.Context null_context = new GPhoto.Context();
private GPhoto.CameraAbilitiesList abilities_list;
private Gee.HashMap<string, DiscoveredCamera> camera_map = new Gee.HashMap<string, DiscoveredCamera>(
str_hash, str_equal, direct_equal);
public signal void camera_added(DiscoveredCamera camera);
public signal void camera_removed(DiscoveredCamera camera);
private CameraTable() {
// set up HAL connection to monitor for device insertion/removal, to look for cameras
hal_conn = DBus.Bus.get(DBus.BusType.SYSTEM);
if (!hal_context.set_dbus_connection(hal_conn.get_connection()))
error("Unable to set DBus connection for HAL");
DBus.RawError raw = DBus.RawError();
if (!hal_context.init(ref raw))
error("Unable to initialize context: %s", raw.message);
if (!hal_context.set_device_added(on_device_added))
error("Unable to register device-added callback");
if (!hal_context.set_device_removed(on_device_removed))
error("Unable to register device-removed callback");
// initialize and update camera table ... since this is the constructor, no observers
// are signalled
try {
init_camera_table();
update_camera_table();
} catch (GPhotoError err) {
error("%s", err.message);
}
}
public static CameraTable get_instance() {
if (instance == null)
instance = new CameraTable();
return instance;
}
public Gee.Iterable<DiscoveredCamera> get_cameras() {
return camera_map.get_values();
}
public int get_count() {
return camera_map.size;
}
public DiscoveredCamera? get_for_uri(string uri) {
return camera_map.get(uri);
}
private void do_op(GPhoto.Result res, string op) throws GPhotoError {
if (res != GPhoto.Result.OK)
throw new GPhotoError.LIBRARY("[%d] Unable to %s: %s", (int) res, op, res.as_string());
}
private void init_camera_table() throws GPhotoError {
do_op(GPhoto.CameraAbilitiesList.create(out abilities_list), "create camera abilities list");
do_op(abilities_list.load(null_context), "load camera abilities list");
}
// USB (or libusb) is a funny beast; if only one USB device is present (i.e. the camera),
// then a single camera is detected at port usb:. However, if multiple USB devices are
// present (including non-cameras), then the first attached camera will be listed twice,
// first at usb:, then at usb:xxx,yyy. If the usb: device is removed, another usb:xxx,yyy
// device will lose its full-path name and be referred to as usb: only.
//
// This function gleans the full port name of a particular port, even if it's the unadorned
// "usb:", by using HAL.
private string? esp_usb_to_udi(int camera_count, string port, out string full_port) {
// sanity
assert(camera_count > 0);
debug("ESP: camera_count=%d port=%s", camera_count, port);
DBus.RawError raw = DBus.RawError();
string[] udis = hal_context.find_device_by_capability("camera", ref raw);
string[] usbs = new string[0];
foreach (string udi in udis) {
if (hal_context.device_get_property_string(udi, "info.subsystem", ref raw) == "usb")
usbs += udi;
}
// if GPhoto detects one camera, and HAL reports one USB camera, all is swell
if (camera_count == 1 && usbs.length ==1) {
string usb = usbs[0];
int hal_bus = hal_context.device_get_property_int(usb, "usb.bus_number", ref raw);
int hal_device = hal_context.device_get_property_int(usb, "usb.linux.device_number",
ref raw);
if (port == "usb:") {
// the most likely case, so make a full path
full_port = "usb:%03d,%03d".printf(hal_bus, hal_device);
} else {
full_port = port;
}
debug("ESP: port=%s full_port=%s udi=%s", port, full_port, usb);
return usb;
}
// with more than one camera, skip the mirrored "usb:" port
if (port == "usb:") {
debug("ESP: Skipping %s", port);
return null;
}
// parse out the bus and device ID
int bus, device;
if (port.scanf("usb:%d,%d", out bus, out device) < 2)
error("ESP: Failed to scanf %s", port);
foreach (string usb in usbs) {
int hal_bus = hal_context.device_get_property_int(usb, "usb.bus_number", ref raw);
int hal_device = hal_context.device_get_property_int(usb, "usb.linux.device_number", ref raw);
if ((bus == hal_bus) && (device == hal_device)) {
full_port = port;
debug("ESP: port=%s full_port=%s udi=%s", port, full_port, usb);
return usb;
}
}
debug("ESP: No UDI found for port=%s", port);
return null;
}
public static string get_port_uri(string port) {
return "gphoto2://[%s]/".printf(port);
}
private void update_camera_table() throws GPhotoError {
// need to do this because virtual ports come and go in the USB world (and probably others)
GPhoto.PortInfoList port_info_list;
do_op(GPhoto.PortInfoList.create(out port_info_list), "create port list");
do_op(port_info_list.load(), "load port list");
GPhoto.CameraList camera_list;
do_op(GPhoto.CameraList.create(out camera_list), "create camera list");
do_op(abilities_list.detect(port_info_list, camera_list, null_context), "detect cameras");
Gee.HashMap<string, string> detected_map = new Gee.HashMap<string, string>(str_hash, str_equal,
str_equal);
// go through the detected camera list and glean their ports
for (int ctr = 0; ctr < camera_list.count(); ctr++) {
string name;
do_op(camera_list.get_name(ctr, out name), "get detected camera name");
string port;
do_op(camera_list.get_value(ctr, out port), "get detected camera port");
debug("Detected %s @ %s", name, port);
// do some USB ESP, skipping ports that cannot be deduced
if (port.has_prefix("usb:")) {
string full_port;
string udi = esp_usb_to_udi(camera_list.count(), port, out full_port);
if (udi == null)
continue;
port = full_port;
}
detected_map.set(port, name);
}
// find cameras that have disappeared
DiscoveredCamera[] missing = new DiscoveredCamera[0];
foreach (DiscoveredCamera camera in camera_map.get_values()) {
GPhoto.PortInfo port_info;
do_op(camera.gcamera.get_port_info(out port_info),
"retrieve missing camera port information");
GPhoto.CameraAbilities abilities;
do_op(camera.gcamera.get_abilities(out abilities), "retrieve camera abilities");
if (detected_map.contains(port_info.path)) {
debug("Found page for %s @ %s in detected cameras", abilities.model, port_info.path);
continue;
}
debug("%s @ %s missing", abilities.model, port_info.path);
missing += camera;
}
// have to remove from hash map outside of iterator
foreach (DiscoveredCamera camera in missing) {
GPhoto.PortInfo port_info;
do_op(camera.gcamera.get_port_info(out port_info),
"retrieve missing camera port information");
GPhoto.CameraAbilities abilities;
do_op(camera.gcamera.get_abilities(out abilities), "retrieve missing camera abilities");
debug("Removing from camera table: %s @ %s", abilities.model, port_info.path);
camera_map.remove(get_port_uri(port_info.path));
camera_removed(camera);
}
// add cameras which were not present before
foreach (string port in detected_map.get_keys()) {
string name = detected_map.get(port);
string uri = get_port_uri(port);
if (camera_map.contains(uri)) {
// already known about
debug("%s @ %s already registered, skipping", name, port);
continue;
}
int index = port_info_list.lookup_path(port);
if (index < 0)
do_op((GPhoto.Result) index, "lookup port %s".printf(port));
GPhoto.PortInfo port_info;
do_op(port_info_list.get_info(index, out port_info), "get port info for %s".printf(port));
// this should match, every time
assert(port == port_info.path);
index = abilities_list.lookup_model(name);
if (index < 0)
do_op((GPhoto.Result) index, "lookup camera model %s".printf(name));
GPhoto.CameraAbilities camera_abilities;
do_op(abilities_list.get_abilities(index, out camera_abilities),
"lookup camera abilities for %s".printf(name));
GPhoto.Camera gcamera;
do_op(GPhoto.Camera.create(out gcamera), "create camera object for %s".printf(name));
do_op(gcamera.set_abilities(camera_abilities), "set camera abilities for %s".printf(name));
do_op(gcamera.set_port_info(port_info), "set port info for %s on %s".printf(name, port));
debug("Adding to camera table: %s @ %s", name, port);
DiscoveredCamera camera = new DiscoveredCamera(gcamera, uri);
camera_map.set(uri, camera);
camera_added(camera);
}
}
private static void on_device_added(Hal.Context context, string udi) {
debug("on_device_added: %s", udi);
schedule_camera_update();
}
private static void on_device_removed(Hal.Context context, string udi) {
debug("on_device_removed: %s", udi);
schedule_camera_update();
}
// Device add/removes often arrive in pairs; this allows for a single
// update to occur when they come in all at once
private static void schedule_camera_update() {
if (camera_update_scheduled)
return;
Timeout.add(UPDATE_DELAY_MSEC, background_camera_update);
camera_update_scheduled = true;
}
private static bool background_camera_update() {
debug("background_camera_update");
try {
get_instance().update_camera_table();
} catch (GPhotoError err) {
debug("Error updating camera table: %s", err.message);
}
camera_update_scheduled = false;
return false;
}
}
......@@ -413,7 +413,7 @@ public class CollectionPage : CheckerboardPage {
// switch to full-page view
debug("switching to %s", thumbnail.get_photo().to_string());
AppWindow.get_instance().switch_to_photo_page(this, thumbnail);
LibraryWindow.get_app().switch_to_photo_page(this, thumbnail);
}
public override Gtk.Menu? get_context_menu() {
......
......@@ -69,8 +69,8 @@ public class EventsDirectoryPage : CheckerboardPage {
private int sort;
public CompareEventItem(EventTable event_table, int sort) {
assert(sort == AppWindow.SORT_EVENTS_ORDER_ASCENDING
|| sort == AppWindow.SORT_EVENTS_ORDER_DESCENDING);
assert(sort == LibraryWindow.SORT_EVENTS_ORDER_ASCENDING
|| sort == LibraryWindow.SORT_EVENTS_ORDER_DESCENDING);
this.event_table = event_table;
this.sort = sort;
......@@ -81,10 +81,10 @@ public class EventsDirectoryPage : CheckerboardPage {
int64 start_b = (int64) event_table.get_start_time(b.event_id);
switch (sort) {
case AppWindow.SORT_EVENTS_ORDER_ASCENDING:
case LibraryWindow.SORT_EVENTS_ORDER_ASCENDING:
return start_a - start_b;
case AppWindow.SORT_EVENTS_ORDER_DESCENDING:
case LibraryWindow.SORT_EVENTS_ORDER_DESCENDING:
default:
return start_b - start_a;
}
......@@ -112,7 +112,7 @@ public class EventsDirectoryPage : CheckerboardPage {
// scrollbar policy
set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
set_layout_comparator(new CompareEventItem(event_table, AppWindow.get_instance().get_events_sort()));
set_layout_comparator(new CompareEventItem(event_table, LibraryWindow.get_app().get_events_sort()));
}
public override Gtk.Toolbar get_toolbar() {
......@@ -121,7 +121,7 @@ public class EventsDirectoryPage : CheckerboardPage {
public override void on_item_activated(LayoutItem item) {
DirectoryItem event = (DirectoryItem) item;
AppWindow.get_instance().switch_to_event(event.event_id);
LibraryWindow.get_app().switch_to_event(event.event_id);
}
public override LayoutItem? get_fullscreen_photo() {
......@@ -135,7 +135,7 @@ public class EventsDirectoryPage : CheckerboardPage {
}
foreach (LayoutItem item in iter) {
EventPage page = AppWindow.get_instance().find_event_page(((DirectoryItem) item).event_id);
EventPage page = LibraryWindow.get_app().find_event_page(((DirectoryItem) item).event_id);
if (page != null)
return page.get_fullscreen_photo();
}
......
......@@ -294,27 +294,109 @@ public class ImportPage : CheckerboardPage {
return null;
}
public override void switched_to() {
base.switched_to();
try_refreshing_camera();
}
private void try_refreshing_camera() {
// if camera has been refreshed or is in the process of refreshing, go no further
if (refreshed || busy)
return;
RefreshResult res = refresh_camera();
switch (res) {
case ImportPage.RefreshResult.OK:
case ImportPage.RefreshResult.BUSY:
// nothing to report; if busy, let it continue doing its thing
// (although earlier check should've caught this)
break;
case ImportPage.RefreshResult.LOCKED:
// if locked because it's mounted, offer to unmount
debug("Checking if %s is mounted ...", uri);
File uri = File.new_for_uri(uri);
Mount mount = null;
try {
mount = uri.find_enclosing_mount(null);
} catch (Error err) {
// error means not mounted
}
if (mount != null) {
// it's mounted, offer to unmount for the user
Gtk.MessageDialog dialog = new Gtk.MessageDialog(AppWindow.get_instance(),
Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION,
Gtk.ButtonsType.YES_NO,
"The camera is locked for use as a mounted drive. "
+ "Shotwell can only access the camera when it's unlocked. "
+ "Do you want Shotwell to unmount it for you?");
dialog.title = Resources.APP_TITLE;
int dialog_res = dialog.run();
dialog.destroy();
if (dialog_res != Gtk.ResponseType.YES) {
set_page_message("Please unmount the camera.");
refresh();
} else {
unmount_camera(mount);
}
} else {
// it's not mounted, so another application must have it locked
Gtk.MessageDialog dialog = new Gtk.MessageDialog(AppWindow.get_instance(),
Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING,
Gtk.ButtonsType.OK,
"The camera is locked by another application. "
+ "Shotwell can only access the camera when it's unlocked. "
+ "Please close any other application using the camera and try again.");
dialog.title = Resources.APP_TITLE;
dialog.run();
dialog.destroy();
set_page_message("Please close any other application using the camera.");
refresh();
}
break;
case ImportPage.RefreshResult.LIBRARY_ERROR:
AppWindow.error_message("Unable to fetch previews from the camera:\n%s".printf(
get_refresh_message()));
break;
default:
error("Unknown result type %d", (int) res);
break;
}
}
public bool unmount_camera(Mount mount) {
if (busy)
return false;
busy = true;
refreshed = false;
progress_bar.visible = true;
progress_bar.set_fraction(0.0);
progress_bar.set_text("Unmounting ...");
debug("Unmounting camera ...");
mount.unmount(MountUnmountFlags.NONE, null, on_unmounted);
return true;
}
private void on_unmounted(Object source, AsyncResult aresult) {
debug("Unmount complete");
Mount mount = (Mount) source;
try {
mount.unmount_finish(aresult);
} catch (Error err) {
// TODO: Better error reporting
debug("%s", err.message);
AppWindow.error_message("Unable to unmount camera. Try dismounting the camera from the "
+ "file manager.");
return;
}
......@@ -327,37 +409,10 @@ public class ImportPage : CheckerboardPage {
progress_bar.set_text("");
progress_bar.visible = false;
switch_to_and_refresh();
}
public void switch_to_and_refresh() {
// jump to page
AppWindow.get_instance().switch_to_page(this);
// now with camera unmounted, refresh the view
RefreshResult res = refresh_camera();
if (res != RefreshResult.OK) {
string reason = null;
switch (res) {
case RefreshResult.LOCKED:
reason = "The camera is locked.";
break;
case RefreshResult.BUSY:
reason = "The camera is busy.";
break;
case RefreshResult.LIBRARY_ERROR:
default:
reason = "The camera is unavailable at this time.";
break;
}
AppWindow.error_message("Unable to unmount camera. %s Please try again.".printf(reason));
}
try_refreshing_camera();
}
public RefreshResult refresh_camera() {
private RefreshResult refresh_camera() {
if (busy)
return RefreshResult.BUSY;
......@@ -685,8 +740,8 @@ public class ImportPage : CheckerboardPage {
BatchImport batch_import = new BatchImport(jobs, camera_name, total_bytes);
batch_import.import_job_failed += on_import_job_failed;
batch_import.import_complete += close_import;
AppWindow.get_instance().enqueue_batch_import(batch_import);
AppWindow.get_instance().switch_to_import_queue_page();
LibraryWindow.get_app().enqueue_batch_import(batch_import);
LibraryWindow.get_app().switch_to_import_queue_page();
// camera.exit() and busy flag will be handled when the batch import completes
} else {
close_import();
......@@ -825,7 +880,7 @@ public class ImportQueuePage : SinglePhotoPage {
assert(removed);
if (failed.size > 0 || skipped.size > 0)
AppWindow.report_import_failures(batch_import.get_name(), failed, skipped);
LibraryWindow.report_import_failures(batch_import.get_name(), failed, skipped);
batch_removed(batch_import);
......
This diff is collapsed.
......@@ -164,7 +164,9 @@ public abstract class Page : Gtk.ScrolledWindow {
protected void init_ui_bind(string? menubar_path) {
ui.insert_action_group(action_group, 0);
common_action_group = AppWindow.get_instance().get_common_action_group();
common_action_group = new Gtk.ActionGroup("CommonActionGroup");
AppWindow.get_instance().add_common_actions(common_action_group);
ui.insert_action_group(common_action_group, 0);
if (menubar_path != null)
......
......@@ -338,7 +338,7 @@ public class PhotoPage : SinglePhotoPage {
}
private void on_return_to_collection() {
AppWindow.get_instance().switch_to_page(controller);
LibraryWindow.get_app().switch_to_page(controller);
}
private void on_export() {
......
......@@ -9,16 +9,17 @@ enum ShotwellCommand {
MOUNTED_CAMERA = 1
}
Unique.Response on_shotwell_message(Unique.App shotwell, int command, Unique.MessageData data, uint timestamp) {
Unique.Response on_shotwell_message(Unique.App shotwell, int command, Unique.MessageData data,
uint timestamp) {
Unique.Response response = Unique.Response.OK;
switch (command) {
case ShotwellCommand.MOUNTED_CAMERA:
AppWindow.get_instance().mounted_camera_shell_notification(data.get_text());
LibraryWindow.get_app().mounted_camera_shell_notification(data.get_text());
break;
case Unique.Command.ACTIVATE:
AppWindow.get_instance().present_with_time(timestamp);
LibraryWindow.get_app().present_with_time(timestamp);
break;
default:
......@@ -30,25 +31,8 @@ Unique.Response on_shotwell_message(Unique.App shotwell, int command, Unique.Mes
return response;
}
void main(string[] args) {
// init GTK
Gtk.init(ref args);
// init debug prior to anything else
Debug.init();
// set up GLib environment
GLib.Environment.set_application_name(Resources.APP_TITLE);
// examine command-line arguments for camera mounts
// (everything else is ignored for now)
string[] mounts = new string[0];
for (int ctr = 1; ctr < args.length; ctr++) {
if (AppWindow.is_mount_uri_supported(args[ctr]))
mounts += args[ctr];
}
// single-instance app
void library_exec(string[] mounts) {
// the library is single-instance; editing windows are one-process-per
Unique.App shotwell = new Unique.App("org.yorba.shotwell", null);
shotwell.add_command("MOUNTED_CAMERA", (int) ShotwellCommand.MOUNTED_CAMERA);
shotwell.message_received += on_shotwell_message;
......@@ -64,21 +48,30 @@ void main(string[] args) {
shotwell.send_message((int) Unique.Command.ACTIVATE, null);
Debug.terminate();
// notified running app; this one exits
return;
}
// initialize app-wide stuff
AppWindow.init(args);
Resources.init();
// init modules library relies on
DatabaseTable.init();
ThumbnailCache.init();
Photo.init();
// validate the databases prior to using them
message("Verifying databases ...");
string app_version;
if (!verify_databases(out app_version)) {
if (verify_databases(out app_version)) {
// create main library application window
LibraryWindow library_window = new LibraryWindow();
// report mount points
foreach (string mount in mounts)
library_window.mounted_camera_shell_notification(mount);
library_window.show_all();
Gtk.main();
} else {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK,
"The database for your photo library is not compatible with this version of Shotwell. "
......@@ -86,24 +79,38 @@ void main(string[] args) {
dialog.title = Resources.APP_TITLE;
dialog.run();
dialog.destroy();
} else {
// create main application window
AppWindow app_window = new AppWindow();
// report mount points
foreach (string mount in mounts)
app_window.mounted_camera_shell_notification(mount);
// throw it all on the display
app_window.show_all();
// event loop
Gtk.main();
}
Photo.terminate();
ThumbnailCache.terminate();