Commit 63454895 authored by Jim Nelson's avatar Jim Nelson

#3143: New data model for direct-edit mode to fix navigation problem.

parent b6bb10e6
......@@ -60,7 +60,6 @@ UNUNITIZED_SRC_FILES = \
DataCollection.vala \
LibraryWindow.vala \
CameraTable.vala \
DirectWindow.vala \
Properties.vala \
CustomComponents.vala \
Config.vala \
......@@ -553,7 +552,7 @@ $(UNITIZE_DIR):
$(UNITIZE_STAMP): $(UNITIZE_DIR) $(MAKE_FILES) src/unit/rc/UnitInternals.m4 src/unit/rc/unitize_entry.m4
@$(foreach group,$(APP_GROUPS),\
`m4 '--define=_APP_GROUP_=$(group)' '--define=_UNIT_ENTRY_POINTS_=$(foreach nm,$(UNIT_NAMESPACES),$(nm).init_entry,)' '--define=_UNIT_TERMINATE_POINTS_=$(foreach nm,$(UNIT_NAMESPACES),$(nm).terminate_entry,)' src/unit/rc/unitize_entry.m4 > $(UNITIZE_DIR)/_$(group)_unitize_entry.vala`)
`m4 '--define=_APP_GROUP_=$(group)' '--define=_UNIT_ENTRY_POINTS_=$(foreach nm,$($(group)_UNITS),$(nm).init_entry,)' '--define=_UNIT_TERMINATE_POINTS_=$(foreach nm,$($(group)_UNITS),$(nm).terminate_entry,)' src/unit/rc/unitize_entry.m4 > $(UNITIZE_DIR)/_$(group)_unitize_entry.vala`)
@$(foreach nm,$(UNIT_NAMESPACES),\
`m4 '--define=_UNIT_NAME_=$(nm)' '--define=_UNIT_USES_INITS_=$($(nm)_USES_INITS)' '--define=_UNIT_USES_TERMINATORS_=$($(nm)_USES_TERMINATORS)' src/unit/rc/UnitInternals.m4 > $(UNITIZE_DIR)/_$(nm)Internals.vala`)
@touch $@
......
......@@ -1301,7 +1301,7 @@ private class WorkSniffer : BackgroundImportJob {
if ((skipset != null) && skipset.contains(child))
continue; /* don't enqueue if this file is to be skipped */
if ((Photo.is_file_image(child) && Photo.is_file_supported(child)) ||
if ((Photo.is_file_image(child) && PhotoFileFormat.is_file_supported(child)) ||
VideoReader.is_supported_video_file(child)) {
total_bytes += info.get_size();
files_to_prepare.add(new FileToPrepare(job, child, copy_to_library));
......@@ -1469,7 +1469,7 @@ private class PrepareFilesJob : BackgroundImportJob {
if ((!is_video) && (!Photo.is_file_image(file)))
return ImportResult.NOT_AN_IMAGE;
if ((!is_video) && (!Photo.is_file_supported(file)))
if ((!is_video) && (!PhotoFileFormat.is_file_supported(file)))
return ImportResult.UNSUPPORTED_FORMAT;
import_file_count++;
......
......@@ -74,6 +74,10 @@ public class DataSet {
list.resort(order_added_comparator);
}
public Comparator get_comparator() {
return user_comparator;
}
public void set_comparator(Comparator user_comparator, ComparatorPredicate? comparator_predicate) {
this.user_comparator = user_comparator;
this.comparator_predicate = comparator_predicate;
......@@ -565,6 +569,10 @@ public class DataCollection {
return true;
}
public Comparator get_comparator() {
return dataset.get_comparator();
}
public virtual void set_comparator(Comparator comparator, ComparatorPredicate? predicate) {
dataset.set_comparator(comparator, predicate);
notify_ordering_changed();
......@@ -1781,6 +1789,7 @@ public class ViewCollection : DataCollection {
SourceCollection, MonitorImpl>();
private ViewCollection mirroring = null;
private CreateView mirroring_ctor = null;
private CreateViewPredicate should_mirror = null;
private ViewFilter filter = null;
private DataSet selected = new DataSet();
private DataSet visible = null;
......@@ -1946,25 +1955,32 @@ public class ViewCollection : DataCollection {
monitors.clear();
}
public void mirror(ViewCollection to_mirror, CreateView mirroring_ctor) {
public void mirror(ViewCollection to_mirror, CreateView mirroring_ctor,
CreateViewPredicate? should_mirror) {
halt_mirroring();
halt_all_monitoring();
clear();
mirroring = to_mirror;
this.mirroring_ctor = mirroring_ctor;
this.should_mirror = should_mirror;
// used to mirror the ordering of to_mirror
set_comparator(mirroring_comparator, null);
// load up with current items
on_mirror_contents_added(mirroring.get_all());
mirroring.items_added.connect(on_mirror_contents_added);
mirroring.items_removed.connect(on_mirror_contents_removed);
mirroring.ordering_changed.connect(on_mirror_ordering_changed);
}
public void halt_mirroring() {
if (mirroring != null) {
mirroring.items_added.disconnect(on_mirror_contents_added);
mirroring.items_removed.disconnect(on_mirror_contents_removed);
mirroring.ordering_changed.disconnect(on_mirror_ordering_changed);
}
mirroring = null;
......@@ -2176,12 +2192,25 @@ public class ViewCollection : DataCollection {
notify_ordering_changed();
}
private int64 mirroring_comparator(void *a, void *b) {
assert(mirroring != null);
int aindex = mirroring.index_of_source(((DataView *) a)->get_source());
assert(aindex >= 0);
int bindex = mirroring.index_of_source(((DataView *) b)->get_source());
assert(bindex >= 0);
return aindex - bindex;
}
private void on_mirror_contents_added(Gee.Iterable<DataObject> added) {
Gee.ArrayList<DataView> to_add = new Gee.ArrayList<DataView>();
foreach (DataObject object in added) {
DataView view = (DataView) object;
DataSource source = ((DataView) object).get_source();
to_add.add(mirroring_ctor(view.get_source()));
if (should_mirror == null || should_mirror(source))
to_add.add(mirroring_ctor(source));
}
if (to_add.size > 0)
......@@ -2193,13 +2222,23 @@ public class ViewCollection : DataCollection {
foreach (DataObject object in removed) {
DataView view = (DataView) object;
DataView our_view = get_view_for_source(view.get_source());
DataView? our_view = get_view_for_source(view.get_source());
assert(our_view != null);
marker.mark(our_view);
}
remove_marked(marker);
}
private void on_mirror_ordering_changed() {
// By re-setting the comparator, this forces a resort of the collection (but only do it
// if the comparator is still set to the mirroring_comparator, as the user may have changed
// it after mirroring started)
if (get_comparator() == mirroring_comparator)
set_comparator(mirroring_comparator, null);
}
// Keep the source map and state tables synchronized
protected override void notify_items_added(Gee.Iterable<DataObject> added) {
Gee.ArrayList<DataView> added_visible = null;
......@@ -2412,8 +2451,8 @@ public class ViewCollection : DataCollection {
}
next_view = get_next(next_view);
}
prev = null;
prev = null;
DataView? prev_view = get_previous(home_view);
while (prev_view != home_view) {
if ((type_selector == null) || (prev_view.get_source().get_typename() == type_selector)) {
......@@ -2422,7 +2461,7 @@ public class ViewCollection : DataCollection {
}
prev_view = get_previous(prev_view);
}
return true;
}
......@@ -2689,11 +2728,25 @@ public class ViewCollection : DataCollection {
public DataView? get_view_for_source(DataSource source) {
return source_map.get(source);
}
public Gee.Collection<DataSource> get_sources() {
return source_map.keys.read_only_view;
}
public Gee.Collection<DataSource>? get_sources_of_type(Type t) {
Gee.Collection<DataSource>? sources = null;
foreach (DataSource source in source_map.keys) {
if (source.get_type().is_a(t)) {
if (sources == null)
sources = new Gee.ArrayList<DataSource>();
sources.add(source);
}
}
return sources;
}
public Gee.Collection<DataSource> get_selected_sources() {
Gee.Collection<DataSource> sources = new Gee.ArrayList<DataSource>();
......@@ -2710,6 +2763,13 @@ public class ViewCollection : DataCollection {
return (object != null) ? ((DataView) object).get_source() : null;
}
// Returns -1 if source is not in the ViewCollection.
public int index_of_source(DataSource source) {
DataView? view = get_view_for_source(source);
return (view != null) ? index_of(view) : -1;
}
// This is only used by DataView.
public void internal_notify_view_altered(DataView view) {
if (!are_notifications_frozen()) {
......
......@@ -564,6 +564,26 @@ public class SourceBacklink {
}
}
//
// The DataSourcePlaceholder interface indicates that a DataSource is actually a placeholder
// for another "real" DataSource and that it will generate it on-the-fly. DataView.get_source()
// will recognize this and return the results of fetch_real_source() rather than the
// DataSourcePlaceholder itself. If fetch_real_source() is called multiple times, it's assumed
// it will not keep creating DataSources, but rather get a previously-created one that's stored
// in its natural SourceCollection. It's also assumed that the DataSourcePlaceholder will monitor
// the "real" DataSource and destroy itself if the real one is destroyed.
//
// fetch_real_source() may NOT return null if the real source cannot be generated. (There is no
// provision in the DataObject system for a DataView not to return a DataSource.) If unable to
// generate a real source, a DummyDataSource must be returned.
//
public interface DataSourcePlaceholder : DataSource {
public abstract DataSource fetch_real_source();
}
public interface DummyDataSource : DataSource {
}
public abstract class DataSource : DataObject {
protected delegate void ContactSubscriber(DataView view);
protected delegate void ContactSubscriberAlteration(DataView view, Alteration alteration);
......@@ -1215,6 +1235,7 @@ public interface Proxyable {
public class DataView : DataObject {
private DataSource source;
private DataSourcePlaceholder? placeholder;
private bool selected = false;
private bool visible = true;
......@@ -1240,6 +1261,7 @@ public class DataView : DataObject {
public DataView(DataSource source) {
this.source = source;
placeholder = source as DataSourcePlaceholder;
// subscribe to the DataSource, which sets up signal reflection and gives the DataView
// first notification of destruction.
......@@ -1262,7 +1284,13 @@ public class DataView : DataObject {
}
public DataSource get_source() {
return source;
if (placeholder == null)
return source;
DataSource real_source = placeholder.fetch_real_source();
assert(real_source != null);
return real_source;
}
public bool is_selected() {
......
/* Copyright 2010-2011 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.
*/
// DelayedQueue is a specialized collection class. It holds items in order, but rather than being
// manually dequeued, they are dequeued automatically after a specified amount of time has elapsed
// for that item. As of today, it's possible the item will be dequeued a bit later than asked
// for, but it will never be early. Future implementations might tighten up the lateness.
//
// The original design was to use a signal to notify when an item has been dequeued, but Vala has
// a bug with passing an unnamed type as a signal parameter:
// https://bugzilla.gnome.org/show_bug.cgi?id=628639
//
// The rate the items come off the queue can be spaced out. Note that this can cause backing up
// of work. As of today, DelayedQueue makes no effort to combat this.
public delegate void DequeuedCallback<G>(G item);
public class DelayedQueue<G> {
private class Element<G> {
public G item;
public time_t ready;
public Element(G item, time_t ready) {
this.item = item;
this.ready = ready;
}
public static int64 comparator(void *a, void *b) {
return (int64) ((Element *) a)->ready - (int64) ((Element *) b)->ready;
}
}
private uint hold_msec;
private DequeuedCallback<G> callback;
private EqualFunc equal_func;
private int priority;
private uint timer_id = 0;
private SortedList<Element<G>> queue;
private uint dequeue_spacing_msec = 0;
private time_t last_dequeue = 0;
// Initial design was to have a signal that passed the dequeued G, but bug in valac meant
// finding a workaround, namely using a delegate:
// https://bugzilla.gnome.org/show_bug.cgi?id=628639
public DelayedQueue(uint hold_msec, DequeuedCallback<G> callback, EqualFunc? equal_func = null,
int priority = Priority.DEFAULT) requires (hold_msec > 0) {
this.hold_msec = hold_msec;
this.callback = callback;
this.equal_func = (equal_func != null) ? equal_func : Gee.Functions.get_equal_func_for(typeof(G));
this.priority = priority;
queue = new SortedList<Element<G>>(Element.comparator);
timer_id = Timeout.add(get_heartbeat_timeout(), on_heartbeat, priority);
}
~DelayedQueue() {
if (timer_id != 0)
Source.remove(timer_id);
}
public uint get_dequeue_spacing_msec() {
return dequeue_spacing_msec;
}
public void set_dequeue_spacing_msec(uint msec) {
if (msec == dequeue_spacing_msec)
return;
if (timer_id != 0)
Source.remove(timer_id);
dequeue_spacing_msec = msec;
timer_id = Timeout.add(get_heartbeat_timeout(), on_heartbeat, priority);
}
private uint get_heartbeat_timeout() {
return ((dequeue_spacing_msec == 0)
? (hold_msec / 10)
: (dequeue_spacing_msec / 2)).clamp(10, uint.MAX);
}
protected virtual void notify_dequeued(G item) {
callback(item);
}
public virtual void clear() {
queue.clear();
}
public virtual bool contains(G item) {
foreach (Element<G> e in queue) {
if (equal_func(item, e.item))
return true;
}
return false;
}
public virtual bool enqueue(G item) {
return queue.add(new Element<G>(item, calc_ready_time()));
}
public virtual bool remove_first(G item) {
Gee.Iterator<Element<G>> iter = queue.iterator();
while (iter.next()) {
Element<G> e = iter.get();
if (equal_func(item, e.item)) {
iter.remove();
return true;
}
}
return false;
}
public virtual int size {
get {
return queue.size;
}
}
private time_t calc_ready_time() {
return (time_t) (now_ms() + hold_msec);
}
private bool on_heartbeat() {
time_t now = 0;
for (;;) {
if (queue.size == 0)
break;
Element<G>? head = queue.get_at(0);
assert(head != null);
if (now == 0)
now = (time_t) now_ms();
if (head.ready > now)
break;
// if a space of time is required between dequeues, check now
if ((dequeue_spacing_msec != 0) && ((now - last_dequeue) < dequeue_spacing_msec))
break;
Element<G>? h = queue.remove_at(0);
assert(head == h);
notify_dequeued(head.item);
last_dequeue = now;
// if a dequeue spacing is in place, it's a lock that only one item is dequeued per
// heartbeat
if (dequeue_spacing_msec != 0)
break;
}
return true;
}
}
// HashDelayedQueue uses a HashMap for quick lookups of elements via contains().
public class HashDelayedQueue<G> : DelayedQueue<G> {
private Gee.HashMap<G, int> item_count;
public HashDelayedQueue(uint hold_msec, DequeuedCallback<G> callback, HashFunc? hash_func = null,
EqualFunc? equal_func = null, int priority = Priority.DEFAULT) {
base (hold_msec, callback, equal_func, priority);
item_count = new Gee.HashMap<G, int>(hash_func, equal_func);
}
protected override void notify_dequeued(G item) {
removed(item);
base.notify_dequeued(item);
}
public override void clear() {
item_count.clear();
base.clear();
}
public override bool contains(G item) {
return item_count.has_key(item);
}
public override bool enqueue(G item) {
if (!base.enqueue(item))
return false;
item_count.set(item, item_count.has_key(item) ? item_count.get(item) + 1 : 1);
return true;
}
public override bool remove_first(G item) {
if (!base.remove_first(item))
return false;
removed(item);
return true;
}
private void removed(G item) {
assert(item_count.has_key(item));
int count = item_count.get(item);
assert(count > 0);
if (--count == 0)
item_count.unset(item);
else
item_count.set(item, count);
}
}
......@@ -788,7 +788,7 @@ public class DirectoryMonitor : Object {
return started;
}
public void start_discovery() throws Error {
public void start_discovery() {
assert(!started);
has_discovery_started = true;
......
......@@ -742,7 +742,7 @@ public class Event : EventSource, ContainerSource, Proxyable {
}
public void mirror_photos(ViewCollection view, CreateView mirroring_ctor) {
view.mirror(this.view, mirroring_ctor);
view.mirror(this.view, mirroring_ctor, null);
}
private void on_primary_thumbnail_altered() {
......
......@@ -88,12 +88,7 @@ public class LibraryMonitorPool {
if (monitor == null)
return false;
try {
monitor.start_discovery();
} catch (Error err) {
warning("Unable to start discovery of %s: %s", monitor.get_root().get_path(),
err.message);
}
monitor.start_discovery();
return false;
}
......
......@@ -650,7 +650,7 @@ public abstract class Photo : PhotoSource {
return ImportResult.NOT_AN_IMAGE;
}
if (!is_file_supported(file)) {
if (!PhotoFileFormat.is_file_supported(file)) {
message("Not importing %s: Unsupported extension", file.get_path());
return ImportResult.UNSUPPORTED_FORMAT;
......@@ -1074,34 +1074,13 @@ public abstract class Photo : PhotoSource {
public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error {
return get_pixbuf(Scaling.for_best_fit(scale, true));
}
public static bool is_file_supported(File file) {
return is_basename_supported(file.get_basename());
}
public static bool is_basename_supported(string basename) {
string name, ext;
disassemble_filename(basename, out name, out ext);
if (ext == null)
return false;
// treat extensions as case-insensitive
ext = ext.down();
// search support file formats
foreach (PhotoFileFormat file_format in PhotoFileFormat.get_supported()) {
if (file_format.get_properties().is_recognized_extension(ext))
return true;
}
return false;
}
public static bool is_file_image(File file) {
// if it's a supported image file, by definition it's an image file, otherwise check the
// master list of common image extensions (checking this way allows for extensions to be
// added to various PhotoFileFormats without having to also add them to IMAGE_EXTENSIONS)
return is_file_supported(file) ? true : is_extension_found(file.get_basename(), IMAGE_EXTENSIONS);
return PhotoFileFormat.is_file_supported(file)
? true : is_extension_found(file.get_basename(), IMAGE_EXTENSIONS);
}
private static bool is_extension_found(string basename, string[] extensions) {
......@@ -4100,181 +4079,3 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
}
}
//
// DirectPhoto
//
public class DirectPhotoSourceCollection : DatabaseSourceCollection {
private Gee.HashMap<File, DirectPhoto> file_map = new Gee.HashMap<File, DirectPhoto>(file_hash,
file_equal, direct_equal);
public DirectPhotoSourceCollection() {
base("DirectPhotoSourceCollection", get_direct_key);
}
public override bool holds_type_of_source(DataSource source) {
return source is DirectPhoto;
}
private static int64 get_direct_key(DataSource source) {
DirectPhoto photo = (DirectPhoto) source;
PhotoID photo_id = photo.get_photo_id();
return photo_id.id;
}
public override void notify_items_added(Gee.Iterable<DataObject> added) {
foreach (DataObject object in added) {
DirectPhoto photo = (DirectPhoto) object;
File file = photo.get_file();
assert(!file_map.has_key(file));
file_map.set(file, photo);
}
base.notify_items_added(added);
}
public override void notify_items_removed(Gee.Iterable<DataObject> removed) {
foreach (DataObject object in removed) {
DirectPhoto photo = (DirectPhoto) object;
File file = photo.get_file();
bool is_removed = file_map.unset(file);
assert(is_removed);
}
base.notify_items_removed(removed);
}
public ImportResult fetch(File file, out DirectPhoto? photo, bool reset = false) throws Error {
// fetch from the map first, which ensures that only one DirectPhoto exists for each file
photo = file_map.get(file);
if (photo != null) {
// if a reset is necessary, the database (and the object) need to reset to original
// easiest way to do this: perform an in-place re-import
if (reset) {
Photo.ReimportMasterState reimport_state;
if (!photo.prepare_for_reimport_master(out reimport_state))
return ImportResult.FILE_ERROR;
photo.finish_reimport_master(reimport_state);
}
return ImportResult.SUCCESS;
}
// for DirectPhoto, a fetch on an unknown file is an implicit import into the in-memory
// database (which automatically adds the new DirectPhoto object to DirectPhoto.global,
// which be us)
return DirectPhoto.internal_import(file, out photo);
}
public DirectPhoto? get_file_source(File file) {
return file_map.get(file);
}
}
public class DirectPhoto : Photo {
private const int PREVIEW_BEST_FIT = 360;
public static DirectPhotoSourceCollection global = null;
private Gdk.Pixbuf preview = null;
private DirectPhoto(PhotoRow row) {
base (row);
}
public static void init() {
global = new DirectPhotoSourceCollection();
}
public static void terminate() {
}
// This method should only be called by DirectPhotoSourceCollection. Use
// DirectPhoto.global.fetch to import files into the system.
public static ImportResult internal_import(File file, out DirectPhoto? photo) {
PhotoImportParams params = new PhotoImportParams(file, ImportID.generate(),
PhotoFileSniffer.Options.NO_MD5, null, null, null);
ImportResult result = Photo.prepare_for_import(params);
if (result != ImportResult.SUCCESS) {
// this should never happen; DirectPhotoSourceCollection guarantees it.
assert(result != ImportResult.PHOTO_EXISTS);
photo = null;
return result;
}
PhotoTable.get_instance().add(ref params.row);
// create DataSource and add to SourceCollection
photo = new DirectPhoto(params.row);
global.add(photo);
return result;
}
public override Gdk.Pixbuf get_preview_pixbuf(Scaling scaling) throws Error {
if (preview == null) {
preview = get_thumbnail(PREVIEW_BEST_FIT);
if (preview == null)
preview = get_pixbuf(scaling);
}
return scaling.perform_on_pixbuf(preview, Gdk.InterpType.BILINEAR, true);
}
public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error {
return (get_metadata().get_preview_count() == 0) ? null :
get_orientation().rotate_pixbuf(get_metadata().get_preview(0).get_pixbuf());
}
protected override void notify_altered(Alteration alteration) {
preview = null;
base.notify_altered(alteration);
}
protected override bool has_user_generated_metadata() {
// TODO: implement this method
return false;
}
protected override void set_user_metadata_for_export(PhotoMetadata metadata) {
// TODO: implement this method, see ticket
}
protected override void apply_user_metadata_for_reimport(PhotoMetadata metadata) {
}
public override bool is_trashed() {