Commit 41659771 authored by Jim Nelson's avatar Jim Nelson

#809: Final step of model/view refactoring. Data objects and their...

#809: Final step of model/view refactoring.  Data objects and their collections are now maintained in the 
classes in DataObject.vala and DataCollection.vala.  All other objects throughout the system can now access 
photos and events in a unified way, and keep their own state synchronized consistently.
parent 27635109
......@@ -50,8 +50,8 @@ SRC_FILES = \
Sidebar.vala \
ColorTransformation.vala \
EditingTools.vala \
Queryable.vala \
DataSource.vala \
DataObject.vala \
DataCollection.vala \
LibraryWindow.vala \
CameraTable.vala \
DirectWindow.vala \
......
......@@ -55,7 +55,7 @@ public class FullscreenWindow : PageWindow {
if (page is SlideshowPage) {
// slideshow page doesn't own toolbar to hide it, subscribe to signal instead
((SlideshowPage) current_page).hide_toolbar += hide_toolbar;
((SlideshowPage) current_page).hide_toolbar += hide_toolbar;
} else {
// only non-slideshow pages should have pin button
toolbar.insert(pin_button, -1);
......
......@@ -205,7 +205,7 @@ public class BatchImport {
}
}
// report to AppWindow to organize into events
// report to Event to organize into events
if (success.size > 0)
Event.generate_events(success);
......@@ -341,9 +341,6 @@ public class BatchImport {
success.add(photo);
// report to AppWindow for system-wide inclusion
LibraryWindow.get_app().photo_imported(photo);
// report to observers
imported(photo);
......
......@@ -4,7 +4,7 @@
* See the COPYING file in this distribution.
*/
public abstract class LayoutItem : Object, Queryable {
public abstract class LayoutItem : ThumbnailView {
public const int FRAME_WIDTH = 1;
public const int LABEL_PADDING = 4;
public const int FRAME_PADDING = 4;
......@@ -18,28 +18,32 @@ public abstract class LayoutItem : Object, Queryable {
public Gdk.Rectangle allocation = Gdk.Rectangle();
private CollectionLayout parent = null;
private CheckerboardLayout parent = null;
private Pango.Layout pango_layout = null;
private string title = "";
private bool title_displayed = true;
private bool exposure = false;
private Gdk.Pixbuf pixbuf = null;
private Gdk.Pixbuf display_pixbuf = null;
private Gdk.Pixbuf brightened = null;
private Dimensions pixbuf_dim = Dimensions();
private bool selected = false;
private int col = -1;
private int row = -1;
public LayoutItem() {
public LayoutItem(ThumbnailSource source, Dimensions initial_pixbuf_dim) {
base(source);
pixbuf_dim = initial_pixbuf_dim;
}
public void set_parent(CollectionLayout parent) {
// allocation will be invalid (or unset) until this call is made
public void set_parent(CheckerboardLayout parent) {
assert(this.parent == null);
this.parent = parent;
update_pango();
recalc_size(false);
recalc_size();
}
public void abandon_parent() {
......@@ -56,13 +60,15 @@ public abstract class LayoutItem : Object, Queryable {
update_pango();
recalc_size();
notify_view_altered();
}
public string get_title() {
return title;
}
public string get_name() {
public override string get_name() {
return get_title();
}
......@@ -71,9 +77,15 @@ public abstract class LayoutItem : Object, Queryable {
}
public virtual void exposed() {
exposure = true;
}
public virtual void unexposed() {
exposure = false;
}
public virtual bool is_exposed() {
return exposure;
}
public void display_title(bool display) {
......@@ -83,6 +95,8 @@ public abstract class LayoutItem : Object, Queryable {
title_displayed = display;
recalc_size();
notify_view_altered();
}
public void set_image(Gdk.Pixbuf pixbuf) {
......@@ -91,6 +105,8 @@ public abstract class LayoutItem : Object, Queryable {
pixbuf_dim = Dimensions.for_pixbuf(pixbuf);
recalc_size();
notify_view_altered();
}
public void clear_image(int width, int height) {
......@@ -99,6 +115,8 @@ public abstract class LayoutItem : Object, Queryable {
pixbuf_dim = Dimensions(width, height);
recalc_size();
notify_view_altered();
}
private void update_pango() {
......@@ -119,7 +137,7 @@ public abstract class LayoutItem : Object, Queryable {
pango_layout.get_pixel_size(null, out cached_pango_height);
}
public void recalc_size(bool force_invalidate = true) {
public void recalc_size() {
// resize the text width to be no more than the pixbuf's
if (pango_layout != null && pixbuf_dim.width > 0)
pango_layout.set_width(pixbuf_dim.width * Pango.SCALE);
......@@ -135,21 +153,12 @@ public abstract class LayoutItem : Object, Queryable {
// + height of text + label padding (between pixbuf and text)
allocation.height = (FRAME_WIDTH * 2) + (FRAME_PADDING * 2) + pixbuf_dim.height
+ text_height + LABEL_PADDING;
if (force_invalidate)
invalidate();
}
public void invalidate() {
if (parent != null) {
parent.repaint_item(this);
}
}
public void paint(Gdk.GC gc, Gdk.Drawable drawable) {
// frame of FRAME_WIDTH size (determined by GC) only if selected ... however, this is
// accounted for in allocation so the frame can appear without resizing the item
if (selected)
if (is_selected())
drawable.draw_rectangle(gc, false, allocation.x, allocation.y, allocation.width - 1,
allocation.height - 1);
......@@ -165,36 +174,7 @@ public abstract class LayoutItem : Object, Queryable {
pango_layout);
}
}
public virtual void select() {
if (selected)
return;
selected = true;
invalidate();
}
public virtual void unselect() {
if (!selected)
return;
selected = false;
invalidate();
}
public bool toggle_select() {
if (selected)
unselect();
else
select();
return selected;
}
public bool is_selected() {
return selected;
}
public void set_pixel_coordinates(int x, int y) {
allocation.x = x;
allocation.y = y;
......@@ -223,7 +203,8 @@ public abstract class LayoutItem : Object, Queryable {
shift_colors(brightened, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, 0);
display_pixbuf = brightened;
invalidate();
notify_view_altered();
}
public void unbrighten() {
......@@ -235,11 +216,12 @@ public abstract class LayoutItem : Object, Queryable {
// return to the normal image
display_pixbuf = pixbuf;
invalidate();
notify_view_altered();
}
}
public class CollectionLayout : Gtk.DrawingArea {
public class CheckerboardLayout : Gtk.DrawingArea {
public const int TOP_PADDING = 16;
public const int BOTTOM_PADDING = 16;
public const int ROW_GUTTER_PADDING = 24;
......@@ -251,8 +233,7 @@ public class CollectionLayout : Gtk.DrawingArea {
private static Gdk.Pixbuf selection_interior = null;
public SortedList<LayoutItem> items = new SortedList<LayoutItem>();
private ViewCollection view;
private Gtk.Adjustment hadjustment = null;
private Gtk.Adjustment vadjustment = null;
private string message = null;
......@@ -269,7 +250,21 @@ public class CollectionLayout : Gtk.DrawingArea {
private Gdk.Rectangle selection_band = Gdk.Rectangle();
private uint32 selection_transparency_color = 0;
public CollectionLayout() {
public CheckerboardLayout(ViewCollection view) {
this.view = view;
// set existing items to be part of this layout
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
item.set_parent(this);
}
// subscribe to the new collection
view.contents_altered += on_contents_altered;
view.items_state_changed += on_items_state_changed;
view.ordering_changed += on_ordering_changed;
view.item_view_altered += on_item_view_altered;
modify_bg(Gtk.StateType.NORMAL, AppWindow.BG_COLOR);
}
......@@ -306,10 +301,55 @@ public class CollectionLayout : Gtk.DrawingArea {
update_visible_page();
}
public void set_message(string text) {
// remove all items from layout
clear();
private void on_contents_altered(Gee.Iterable<DataObject>? added,
Gee.Iterable<DataObject>? removed) {
if (added != null) {
foreach (DataObject object in added) {
LayoutItem item = (LayoutItem) object;
item.set_parent(this);
}
// this clears the message
message = null;
}
if (removed != null) {
foreach (DataObject object in removed) {
LayoutItem item = (LayoutItem) object;
item.abandon_parent();
}
}
if (in_view) {
refresh();
queue_draw();
}
}
private void on_items_state_changed(Gee.Iterable<DataView> changed) {
if (!in_view)
return;
foreach (DataView view in changed)
repaint_item((LayoutItem) view);
}
private void on_ordering_changed() {
if (!in_view)
return;
refresh();
queue_draw();
}
private void on_item_view_altered(DataView view) {
if (in_view)
repaint_item((LayoutItem) view);
}
public void set_message(string text) {
message = text;
// set the layout's size to be exactly the same as the parent's
......@@ -338,7 +378,9 @@ public class CollectionLayout : Gtk.DrawingArea {
// reason the loop doesn't bail out at some point or attempt to be smart about finding
// only the exposed items
Gdk.Rectangle bitbucket = Gdk.Rectangle();
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
// only expose/unexpose if the item has been placed on the layout
if (!item.get_requisition().has_area())
continue;
......@@ -362,32 +404,17 @@ public class CollectionLayout : Gtk.DrawingArea {
visible_page = Gdk.Rectangle();
// unload everything now that not in view
foreach (LayoutItem item in items)
item.unexposed();
}
public void set_comparator(Comparator<LayoutItem> cmp) {
items.resort(cmp);
foreach (DataObject object in view.get_all())
((LayoutItem) object).unexposed();
}
public void add_item(LayoutItem item) {
items.add(item);
item.set_parent(this);
// this demolishes any message that's been set
message = null;
}
public void remove_item(LayoutItem item) {
items.remove(item);
item.abandon_parent();
}
public LayoutItem? get_item_at_pixel(double xd, double yd) {
int x = (int) xd;
int y = (int) yd;
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
Gdk.Rectangle alloc = item.allocation;
if ((x >= alloc.x) && (y >= alloc.y) && (x <= (alloc.x + alloc.width))
&& (y <= (alloc.y + alloc.height))) {
......@@ -406,7 +433,9 @@ public class CollectionLayout : Gtk.DrawingArea {
Gdk.Rectangle bitbucket = Gdk.Rectangle();
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
if (rect.intersect((Gdk.Rectangle) item.allocation, bitbucket))
intersects.add(item);
......@@ -419,7 +448,7 @@ public class CollectionLayout : Gtk.DrawingArea {
}
public LayoutItem? get_item_relative_to(LayoutItem item, CompassPoint point) {
if (items.size == 0)
if (view.get_count() == 0)
return null;
assert(columns > 0);
......@@ -468,7 +497,9 @@ public class CollectionLayout : Gtk.DrawingArea {
public LayoutItem? get_item_at_coordinate(int col, int row) {
// TODO: If searching by coordinates becomes more vital, the items could be stored
// in an array of arrays for quicker lookup.
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
if (item.get_column() == col && item.get_row() == row)
return item;
}
......@@ -476,20 +507,6 @@ public class CollectionLayout : Gtk.DrawingArea {
return null;
}
public void clear() {
// remove page message
message = null;
// abandon children
foreach (LayoutItem item in items)
item.abandon_parent();
// clear internal list
items.clear();
columns = 0;
rows = 0;
}
public void set_drag_select_origin(int x, int y) {
clear_drag_select();
......@@ -533,7 +550,7 @@ public class CollectionLayout : Gtk.DrawingArea {
public void refresh() {
// if set in message mode, nothing to do here
if (message!= null)
if (message != null)
return;
// don't bother until layout is of some appreciable size (even this is too low)
......@@ -541,7 +558,7 @@ public class CollectionLayout : Gtk.DrawingArea {
return;
// need to set_size in case all items were removed and the viewport size has changed
if (items.size == 0) {
if (view.get_count() == 0) {
set_size_request(allocation.width, 0);
return;
......@@ -554,7 +571,8 @@ public class CollectionLayout : Gtk.DrawingArea {
int row_width = 0;
int widest_row = 0;
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
Dimensions req = item.get_requisition();
// the items must be requisitioned for this code to work
......@@ -593,11 +611,12 @@ public class CollectionLayout : Gtk.DrawingArea {
int total_width = 0;
col = 0;
int[] column_widths = new int[max_cols];
int[] row_heights = new int[(items.size / max_cols) + 1];
int[] row_heights = new int[(view.get_count() / max_cols) + 1];
int gutter = 0;
for (;;) {
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
Dimensions req = item.get_requisition();
if (req.height > tallest)
......@@ -640,7 +659,7 @@ public class CollectionLayout : Gtk.DrawingArea {
tallest = 0;
total_width = 0;
column_widths = new int[max_cols];
row_heights = new int[(items.size / max_cols) + 1];
row_heights = new int[(view.get_count() / max_cols) + 1];
/*
debug("refresh(): readjusting columns: max_cols=%d", max_cols);
*/
......@@ -662,7 +681,8 @@ public class CollectionLayout : Gtk.DrawingArea {
bool report_exposure = (visible_page.width > 1 && visible_page.height > 1);
Gdk.Rectangle bitbucket = Gdk.Rectangle();
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
Dimensions req = item.get_requisition();
// this centers the item in the column
......@@ -705,7 +725,7 @@ public class CollectionLayout : Gtk.DrawingArea {
}
public void repaint_item(LayoutItem item) {
assert(items.contains(item));
assert(view.contains(item));
assert(item.allocation.width > 0 && item.allocation.height > 0);
queue_draw_area(item.allocation.x, item.allocation.y, item.allocation.width,
......@@ -764,7 +784,9 @@ public class CollectionLayout : Gtk.DrawingArea {
int right = event.area.x + event.area.width + 1;
Gdk.Rectangle bitbucket = Gdk.Rectangle();
foreach (LayoutItem item in items) {
foreach (DataObject object in view.get_all()) {
LayoutItem item = (LayoutItem) object;
if (event.area.intersect(item.allocation, bitbucket))
item.paint(item.is_selected() ? selected_gc : unselected_gc, window);
......
This diff is collapsed.
This diff is collapsed.
/* Copyright 2009 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
//
// DataObject
//
public abstract class DataObject {
private DataCollection member_of = null;
// This signal is fired when the source of the data is altered in a way that's significant
// to how it's represented in the application. This base signal must be called by child
// classes if the collection it is a member of is to be notified.
public virtual signal void altered() {
}
// This signal is fired when some attribute or property of the data is altered, but not its
// primary representation. This base signal must be called by child classes if the collection
// this source is a member of is to be notifed.
public virtual signal void metadata_altered() {
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_altered() {
// fire signal on self
altered();
// notify DataCollection
if (member_of != null)
member_of.internal_notify_altered(this);
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_metadata_altered() {
// fire signal on self
metadata_altered();
// notify DataCollection
if (member_of != null)
member_of.internal_notify_metadata_altered(this);
}
public abstract string get_name();
public abstract string to_string();
public DataCollection? get_membership() {
return member_of;
}
// This method is only called by DataCollection.
public void internal_set_membership(DataCollection collection) {
assert(member_of == null);
member_of = collection;
}
// This method is only called by DataCollection
public void internal_clear_membership() {
member_of = null;
}
}
//
// DataSource
//
// A DataSource is an object that is unique throughout the system. DataSources
// commonly have external and/or persistent representations, hence they have a notion of being
// destroyed (versus removed or freed). Several DataViews may exist that reference a single
// DataSource.
//
public abstract class DataSource : DataObject {
protected delegate void ContactSubscriber(DataView view);
private Gee.ArrayList<DataView> subscribers = new Gee.ArrayList<DataView>();
private bool in_contact = false;
private bool marked_for_destroy = false;
public override void notify_altered() {
// signal reflection
contact_subscribers(subscriber_altered);
base.notify_altered();
}
private void subscriber_altered(DataView view) {
view.notify_altered();
}
public override void notify_metadata_altered() {
// signal reflection
contact_subscribers(subscriber_metadata_altered);
base.notify_metadata_altered();
}
private void subscriber_metadata_altered(DataView view) {
view.notify_metadata_altered();
}
// This signal is fired prior to the object being destroyed. It is up to all observers to
// drop their references to the DataObject.
public virtual signal void destroyed() {
}
// This method is called by SourceCollection. It should not be called otherwise.
public void internal_mark_for_destroy() {
marked_for_destroy = true;
}
// This method is called by SourceCollection. It should not be called otherwise. To destroy
// a DataSource, destroy it from its SourceCollection.
//
// Child classes should call this base class to ensure that the collection this object is
// a member of is notified and the signal is properly called. The collection will remove this
// object automatically.
public virtual void destroy() {
assert(marked_for_destroy);
// notify DataViews first that the source is being destroyed
contact_subscribers(subscriber_source_destroyed);
// clear the subscriber list
subscribers.clear();
// propagate the signal
destroyed();
}
private void subscriber_source_destroyed(DataView view) {
view.internal_source_destroyed();
}
// DataViews subscribe to the DataSource to inform it of their existance. Not only does this
// allow for signal reflection (i.e. DataSource.altered -> DataView.altered) it also makes
// them first-in-line for notification of destruction, so they can remove themselves from
// their ViewCollections automatically.
//
// This method is only called by DataView.
public void internal_subscribe(DataView view) {
assert(!in_contact);
subscribers.add(view);
}
// This method is only called by DataView.
public void internal_unsubscribe(DataView view) {
assert(!in_contact);
bool removed = subscribers.remove(view);
assert(removed);
}
protected void contact_subscribers(ContactSubscriber contact_subscriber) {
assert(!in_contact);
in_contact = true;
foreach (DataView view in subscribers)
contact_subscriber(view);
in_contact = false;
}
}
public abstract class ThumbnailSource : DataSource {
public virtual signal void thumbnail_altered() {
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_thumbnail_altered() {
// fire signal on self
thumbnail_altered();
// signal reflection
contact_subscribers(subscriber_thumbnail_altered);
}
private void subscriber_thumbnail_altered(DataView view) {
((ThumbnailView) view).notify_thumbnail_altered();
}
public abstract Gdk.Pixbuf? get_thumbnail(int scale) throws Error;
}
public abstract class PhotoSource : ThumbnailSource {
public abstract time_t get_exposure_time();
public abstract Dimensions get_dimensions();
public abstract uint64 get_filesize();
public abstract Exif.Data? get_exif();
public abstract Gdk.Pixbuf get_pixbuf(Scaling scaling) throws Error;
}
public abstract class EventSource : ThumbnailSource {
public abstract time_t get_start_time();
public abstract time_t get_end_time();
public abstract uint64 get_total_filesize();
public abstract int get_photo_count();
public abstract Gee.Iterable<PhotoSource> get_photos();
}
//
// DataView
//
public class DataView : DataObject {
private DataSource source;
private bool selected = false;
public virtual signal void state_changed(bool selected) {
}
public virtual signal void view_altered() {
}
public DataView(DataSource source) {
this.source = source;
// subscribe to the DataSource, which sets up signal reflection and gives the DataView
// first notification of destruction.
source.internal_subscribe(this);
}