Commit 8fecedd2 authored by Jim Nelson's avatar Jim Nelson

#150: Progress bar is in better shape. Fine-tuning discussed in #92. #145...

#150: Progress bar is in better shape.  Fine-tuning discussed in #92.  #145 and #146:Cameras detected 
at initialization and runtime, and appear as children of "Cameras" in the sidebar.
parent 29f6358e
......@@ -84,8 +84,11 @@ public class AppWindow : Gtk.Window {
return subdir;
}
// this needs to be ref'd the lifetime of the application
private Hal.Context halContext = new Hal.Context();
private DBus.Connection halConn = null;
private CollectionPage collectionPage = null;
private ImportPage importPage = null;
private PhotoPage photoPage = null;
private PhotoTable photoTable = new PhotoTable();
......@@ -101,17 +104,36 @@ public class AppWindow : Gtk.Window {
destroy += Gtk.main_quit;
build_sidebar();
collectionPage = new CollectionPage();
importPage = new ImportPage();
photoPage = new PhotoPage();
build_sidebar();
create_start_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 collectionPage)
Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, TARGET_ENTRIES, Gdk.DragAction.COPY);
halConn = DBus.Bus.get(DBus.BusType.SYSTEM);
if (!halContext.set_dbus_connection(halConn.get_connection()))
error("Unable to set DBus connection for HAL");
DBus.RawError raw = DBus.RawError();
if (!halContext.init(ref raw))
error("Unable to initialize context: %s", raw.message);
if (!halContext.set_device_added(on_device_added))
error("Unable to register device-added callback");
if (!halContext.set_device_removed(on_device_removed))
error("Unable to register device-removed callback");
try {
init_camera_table();
update_camera_table();
} catch (GPhotoError err) {
error("%s", err.message);
}
}
public Gtk.ActionGroup get_common_action_group() {
......@@ -233,12 +255,15 @@ public class AppWindow : Gtk.Window {
collectionPage.refresh();
}
public void switch_to_collection_page() {
switch_to_page(collectionPage);
public static void error_message(string message) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(get_main_window(), Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", message);
dialog.run();
dialog.destroy();
}
public void switch_to_import_page() {
switch_to_page(importPage);
public void switch_to_collection_page() {
switch_to_page(collectionPage);
}
public void switch_to_photo_page(CheckerboardPage controller, LayoutItem current) {
......@@ -316,6 +341,10 @@ public class AppWindow : Gtk.Window {
page.switched_to();
unowned Gtk.TreeRowReference row = page.get_tree_row();
if (row != null)
sidebar.get_selection().select_path(row.get_path());
layout.show_all();
currentPage = page;
......@@ -323,8 +352,7 @@ public class AppWindow : Gtk.Window {
private Gtk.TreeView sidebar = null;
private Gtk.TreeStore sidebarStore = null;
private Gtk.TreePath collectionPath = null;
private Gtk.TreePath importPath = null;
private Gtk.TreeRowReference camerasRow = null;
private void build_sidebar() {
sidebarStore = new Gtk.TreeStore(1, typeof(string));
......@@ -342,9 +370,7 @@ public class AppWindow : Gtk.Window {
Gtk.TreeIter parent, child;
sidebarStore.append(out parent, null);
sidebarStore.set(parent, 0, "Photos");
collectionPath = sidebarStore.get_path(parent);
// start in the collection page
sidebar.get_selection().select_path(collectionPath);
collectionPage.set_tree_row(sidebarStore, parent);
sidebarStore.append(out parent, null);
sidebarStore.set(parent, 0, "Events");
......@@ -359,31 +385,288 @@ public class AppWindow : Gtk.Window {
sidebarStore.set(child, 0, "Parties");
sidebarStore.append(out parent, null);
sidebarStore.set(parent, 0, null);
sidebarStore.append(out parent, null);
sidebarStore.set(parent, 0, "Import");
importPath = sidebarStore.get_path(parent);
sidebarStore.append(out parent, null);
sidebarStore.set(parent, 0, "Recent");
sidebarStore.set(parent, 0, "Cameras");
camerasRow = new Gtk.TreeRowReference(sidebarStore, sidebarStore.get_path(parent));
sidebarStore.append(out parent, null);
sidebarStore.set(parent, 0, "Trash");
sidebar.cursor_changed += on_sidebar_cursor_changed;
// start in the collection page & control selection aspects
Gtk.TreeSelection selection = sidebar.get_selection();
selection.select_path(collectionPage.get_tree_row().get_path());
selection.set_mode(Gtk.SelectionMode.BROWSE);
sidebar.expand_all();
}
private void on_sidebar_cursor_changed() {
Gtk.TreePath selected;
sidebar.get_cursor(out selected, null);
if (selected.compare(collectionPath) == 0) {
if (selected.compare(collectionPage.get_tree_row().get_path()) == 0) {
switch_to_collection_page();
} else if (selected.compare(importPath) == 0) {
switch_to_import_page();
} else {
debug("unknown");
foreach (ImportPage page in cameraTable.get_values()) {
if (selected.compare(page.get_tree_row().get_path()) == 0) {
switch_to_page(page);
return;
}
}
debug("Unimplemented page selected");
}
}
private GPhoto.Context nullContext = new GPhoto.Context();
private GPhoto.CameraAbilitiesList abilitiesList;
private Gee.HashMap<string, ImportPage> cameraTable = new Gee.HashMap<string, ImportPage>(
str_hash, str_equal, direct_equal);
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 abilitiesList), "create camera abilities list");
do_op(abilitiesList.load(nullContext), "load camera abilities list");
}
private string? esp_usb_to_udi(int cameraCount, string port, out string fullPort) {
// sanity
assert(cameraCount > 0);
debug("ESP: cameraCount=%d port=%s", cameraCount, port);
DBus.RawError raw = DBus.RawError();
string[] udis = halContext.find_device_by_capability("camera", ref raw);
string[] usbs = new string[0];
foreach (string udi in udis) {
if (halContext.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 (cameraCount == 1) {
if (usbs.length == 1) {
string usb = usbs[0];
int halBus = halContext.device_get_property_int(usb, "usb.bus_number", ref raw);
int halDevice = halContext.device_get_property_int(usb, "usb.linux.device_number", ref raw);
if (port == "usb:") {
// the most likely case, so make a full path
fullPort = "usb:%03d,%03d".printf(halBus, halDevice);
} else {
fullPort = port;
}
debug("ESP: port=%s fullPort=%s udi=%s", port, fullPort, 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 halBus = halContext.device_get_property_int(usb, "usb.bus_number", ref raw);
int halDevice = halContext.device_get_property_int(usb, "usb.linux.device_number", ref raw);
if ((bus == halBus) && (device == halDevice)) {
fullPort = port;
debug("ESP: port=%s fullPort=%s udi=%s", port, fullPort, usb);
return usb;
}
}
debug("ESP: No UDI found for port=%s", port);
return null;
}
//
// NOTE:
// 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.
//
// For now, relying on the model name reported by libgphoto2 to find the duplicate. This is
// problematic, especially when you have cameras who do not report a model name and are referred
// to as "USB PTP Class Camera" by libgphoto2.
//
// A better strategy needs to be developed (probably involving HAL UID's).
//
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 portInfoList;
do_op(GPhoto.PortInfoList.create(out portInfoList), "create port list");
do_op(portInfoList.load(), "load port list");
GPhoto.CameraList cameraList;
do_op(GPhoto.CameraList.create(out cameraList), "create camera list");
do_op(abilitiesList.detect(portInfoList, cameraList, nullContext), "detect cameras");
Gee.HashMap<string, string> detectedMap = new Gee.HashMap<string, string>(str_hash, str_equal,
str_equal);
for (int ctr = 0; ctr < cameraList.count(); ctr++) {
string name;
do_op(cameraList.get_name(ctr, out name), "get detected camera name");
string port;
do_op(cameraList.get_value(ctr, out port), "get detected camera port");
debug("Detected %s @ %s", name, port);
// do some USB ESP
if (port.has_prefix("usb:")) {
string fullPort;
string udi = esp_usb_to_udi(cameraList.count(), port, out fullPort);
if (udi == null)
continue;
port = fullPort;
}
detectedMap.set(port, name);
}
// first, find cameras that have disappeared
ImportPage[] missing = new ImportPage[0];
foreach (ImportPage page in cameraTable.get_values()) {
GPhoto.Camera camera = page.get_camera();
GPhoto.PortInfo portInfo;
do_op(camera.get_port_info(out portInfo), "retrieve missing camera port information");
GPhoto.CameraAbilities abilities;
do_op(camera.get_abilities(out abilities), "retrieve camera abilities");
if (detectedMap.contains(portInfo.path)) {
debug("Found page for %s @ %s in detected cameras", abilities.model, portInfo.path);
continue;
}
debug("%s @ %s missing", abilities.model, portInfo.path);
missing += page;
}
// have to remove from hash map outside of iterator
foreach (ImportPage page in missing) {
GPhoto.Camera camera = page.get_camera();
GPhoto.PortInfo portInfo;
do_op(camera.get_port_info(out portInfo), "retrieve missing camera port information");
GPhoto.CameraAbilities abilities;
do_op(camera.get_abilities(out abilities), "retrieve missing camera abilities");
debug("Removing from camera table: %s @ %s", abilities.model, portInfo.path);
Gtk.TreeIter cameraIter;
sidebarStore.get_iter(out cameraIter, page.get_tree_row().get_path());
sidebarStore.remove(cameraIter);
cameraTable.remove(portInfo.path);
// switch away if necessary
if (currentPage == page)
switch_to_collection_page();
}
// add cameras which were not present before
foreach (string port in detectedMap.get_keys()) {
string name = detectedMap.get(port);
if (cameraTable.contains(port)) {
// already known about
debug("%s @ %s already registered, skipping", name, port);
continue;
}
int index = portInfoList.lookup_path(port);
if (index < 0)
do_op((GPhoto.Result) index, "lookup port %s".printf(port));
GPhoto.PortInfo portInfo;
do_op(portInfoList.get_info(index, out portInfo), "get port info for %s".printf(port));
// this should match, every time
assert(port == portInfo.path);
index = abilitiesList.lookup_model(name);
if (index < 0)
do_op((GPhoto.Result) index, "lookup camera model %s".printf(name));
GPhoto.CameraAbilities cameraAbilities;
do_op(abilitiesList.get_abilities(index, out cameraAbilities),
"lookup camera abilities for %s".printf(name));
GPhoto.Camera camera;
do_op(GPhoto.Camera.create(out camera), "create camera object for %s".printf(name));
do_op(camera.set_abilities(cameraAbilities), "set camera abilities for %s".printf(name));
do_op(camera.set_port_info(portInfo), "set port info for %s on %s".printf(name, port));
debug("Adding to camera table: %s @ %s", name, port);
Gtk.TreeIter camerasIter, child;
sidebarStore.get_iter(out camerasIter, camerasRow.get_path());
sidebarStore.append(out child, camerasIter);
sidebarStore.set(child, 0, name);
/*
Gtk.TreeRowReference pageRow = new Gtk.TreeRowReference(sidebarStore,
sidebarStore.get_path(child));
*/
ImportPage page = new ImportPage(camera);
page.set_tree_row(sidebarStore, child);
cameraTable.set(port, page);
page.refresh_camera();
}
}
private static void on_device_added(Hal.Context context, string udi) {
debug("******* on_device_added: %s", udi);
try {
AppWindow.get_main_window().update_camera_table();
} catch (GPhotoError err) {
debug("Error updating camera table: %s", err.message);
}
}
private static void on_device_removed(Hal.Context context, string udi) {
debug("******** on_device_removed: %s", udi);
try {
AppWindow.get_main_window().update_camera_table();
} catch (GPhotoError err) {
debug("Error updating camera table: %s", err.message);
}
}
}
......
......@@ -91,17 +91,16 @@ class ProgressBarContext {
public class ImportPage : CheckerboardPage {
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.Label cameraLabel = new Gtk.Label(null);
private Gtk.ToolButton refreshButton = new Gtk.ToolButton.from_stock(Gtk.STOCK_REFRESH);
private Gtk.ToolButton importSelectedButton;
private Gtk.ToolButton importAllButton;
private Gtk.ProgressBar progressBar = new Gtk.ProgressBar();
private GPhoto.PortInfoList portInfoList;
private GPhoto.CameraAbilitiesList abilitiesList;
private GPhoto.CameraAbilities cameraAbilities;
private GPhoto.Camera camera;
private ProgressBarContext initContext = null;
private ProgressBarContext loadingContext = null;
private bool busy = false;
private bool refreshed = false;
private GPhoto.Result refreshResult = GPhoto.Result.OK;
private string refreshError = null;
private int fileCount = 0;
private int completedCount = 0;
......@@ -114,26 +113,13 @@ public class ImportPage : CheckerboardPage {
{ "HelpMenu", null, "_Help", null, null, null }
};
public static void error_message(string message) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(AppWindow.get_main_window(),
Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", message);
dialog.run();
dialog.destroy();
}
construct {
init_ui("import.ui", "/ImportMenuBar", "ImportActionGroup", ACTIONS);
initContext = new ProgressBarContext(progressBar, "Initializing camera ...");
loadingContext = new ProgressBarContext(progressBar, "Loading previews ..");
loadingContext = new ProgressBarContext(progressBar, "Fetching photo previews ..");
// toolbar
// Refresh button
refreshButton.sensitive = false;
refreshButton.clicked += on_refresh_camera;
toolbar.insert(refreshButton, -1);
// Camera label
Gtk.ToolItem cameraLabelItem = new Gtk.ToolItem();
cameraLabelItem.add(cameraLabel);
......@@ -169,40 +155,55 @@ public class ImportPage : CheckerboardPage {
set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
show_all();
// persistent gphoto stuff
GPhoto.Result res = GPhoto.PortInfoList.create(out portInfoList);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = portInfoList.load();
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = GPhoto.CameraAbilitiesList.create(out abilitiesList);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = abilitiesList.load(initContext.context);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
}
public ImportPage(GPhoto.Camera camera) {
this.camera = camera;
res = GPhoto.Camera.create(out camera);
GPhoto.CameraAbilities abilities;
GPhoto.Result res = camera.get_abilities(out abilities);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
debug("[%d] Unable to get camera abilities: %s", (int) res, res.as_string());
} else {
cameraLabel.set_text(abilities.model);
}
}
public GPhoto.Camera get_camera() {
return camera;
}
public override Gtk.Toolbar get_toolbar() {
return toolbar;
}
public override void switched_to() {
if (busy)
return;
string msg;
if (refreshError != null) {
msg = refreshError;
} else if (refreshResult == GPhoto.Result.OK) {
// all went well
return;
} else {
switch (refreshResult) {
case GPhoto.Result.IO_LOCK: {
msg = "Please close any other applications which may be using the camera.";
} break;
default: {
msg = "%s (%d)".printf(refreshResult.as_string(), (int) refreshResult);
} break;
}
}
AppWindow.error_message("Unable to fetch previews from camera.\n%s".printf(msg));
}
public override void on_selection_changed(int count) {
importSelectedButton.sensitive = !busy && (count > 0);
importSelectedButton.sensitive = !busy && refreshed && (count > 0);
}
public override void on_item_activated(LayoutItem item) {
......@@ -214,138 +215,64 @@ public class ImportPage : CheckerboardPage {
AppWindow.get_main_window().switch_to_photo_page(this, preview);
}
public override void switched_to() {
public void refresh_camera() {
if (busy)
return;
GPhoto.CameraList cameraList;
GPhoto.Result res = GPhoto.CameraList.create(out cameraList);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = abilitiesList.detect(portInfoList, cameraList, initContext.context);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
if (cameraList.count() == 0) {
refreshButton.sensitive = false;
cameraLabel.sensitive = false;
cameraLabel.set_text("No camera attached");
remove_all();
refresh();
importSelectedButton.sensitive = false;
importAllButton.sensitive = false;
return;
}
string name;
string port;
res = cameraList.get_name(0, out name);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = cameraList.get_value(0, out port);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
int index = abilitiesList.lookup_model(name);
if (index < 0) {
error("%d", index);
}
res = abilitiesList.get_abilities(index, out cameraAbilities);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = camera.set_abilities(cameraAbilities);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
index = portInfoList.lookup_path(port);
if (index < 0) {
error("%d", index);
}
GPhoto.PortInfo portInfo;
res = portInfoList.get_info(index, out portInfo);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
res = camera.set_port_info(portInfo);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
refreshed = false;
cameraLabel.set_text("%s (%s)".printf(name, port));
cameraLabel.sensitive = true;
refreshButton.sensitive = true;
}
private void on_refresh_camera() {
Gdk.Cursor busyCursor = new Gdk.Cursor(Gdk.CursorType.WATCH);
AppWindow.get_main_window().window.set_cursor(busyCursor);
GPhoto.Result res = camera.init(initContext.context);
AppWindow.get_main_window().window.set_cursor(null);
if (res != GPhoto.Result.OK) {
error_message("Unable to access camera: %s".printf(res.as_string()));
refreshError = null;
refreshResult = camera.init(initContext.context);
if (refreshResult != GPhoto.Result.OK)
return;
}
busy = true;
fileCount = 0;
completedCount = 0;
refreshButton.sensitive = false;
importSelectedButton.sensitive = false;
importAllButton.sensitive = false;
progressBar.set_fraction(0.0);
progressBar.set_text("Loading photo previews");
// Vala bug http://bugzilla.gnome.org/show_bug.cgi?id=579101
// Return statements route around the finally block, hence the nature of this
// code
try {
GPhoto.CameraStorageInformation *sifs = null;
int count = 0;
res = camera.get_storageinfo(&sifs, out count, initContext.context);
if (res != GPhoto.Result.OK) {
error("%s", res.as_string());
}
remove_all();
refresh();
GPhoto.CameraStorageInformation *ifs = sifs;
for (int ctr = 0; ctr < count; ctr++, ifs++) {
string basedir = "/";
if ((ifs->fields & GPhoto.CameraStorageInfoFields.BASE) != 0)
basedir = ifs->basedir;
debug ("fs %s", basedir);
refreshResult = camera.get_storageinfo(&sifs, out count, initContext.context);
if (refreshResult == GPhoto.Result.OK) {
remove_all();
refresh();
if (!load_preview(basedir))
return;
GPhoto.CameraStorageInformation *ifs = sifs;
for (int ctr = 0; ctr < count; ctr++, ifs++) {
string basedir = "/";
if ((ifs->fields & GPhoto.CameraStorageInfoFields.BASE) != 0)
basedir = ifs->basedir;
if (!load_preview(basedir))
break;
}