[Pkg-cli-apps-commits] [SCM] f-spot branch, master, updated. debian/0.6.1.5-3-14-g486a410

Iain Lane laney at ubuntu.com
Wed May 19 15:42:21 UTC 2010


The following commit has been merged in the master branch:
commit 12c8ba201bb3a11cfca7e12912ffc4691373f53a
Author: Iain Lane <laney at ubuntu.com>
Date:   Wed May 19 13:31:27 2010 +0100

    Steal and rebase Ubuntu patches
    
    * debian/patches/ubuntu*: Steal patches from Ubuntu package to improve
      --view mode and add an undo/redo stack. Rebase on new upstream version.
      Thanks to Chris Halse Rogers.

diff --git a/debian/changelog b/debian/changelog
index 290e027..1b09154 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -45,6 +45,9 @@ f-spot (0.6.2-1) UNRELEASED; urgency=low
   * debian/patches/*: Refresh to apply to new upstream version 
   * debian/rules: Pass include directories to autoreconf to have the correct
     macros in scope for the new build system 
+  * debian/patches/ubuntu*: Steal patches from Ubuntu package to improve
+    --view mode and add an undo/redo stack. Rebase on new upstream version.
+    Thanks to Chris Halse Rogers.
   * debian/patches/ubuntu_fname_quote_percent.patch: Drop, now upstream.
 
  -- Iain Lane <laney at ubuntu.com>  Mon, 17 May 2010 11:59:58 +0100
diff --git a/debian/patches/series b/debian/patches/series
index 610be21..87364b2 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -7,7 +7,20 @@ debian_link-system-gnome-keyring.patch
 debian_disable-unit-tests
 debian_fix_f-spot.exe.config.patch
 
+# Patches from Ubuntu
 ubuntu_importer-targetdir-selector.patch
 ubuntu_fix_export_crash_FlickrRemote.cs.patch
 ubuntu_nofuse_fix_photo_import.patch
 ubuntu_xdg-photo-dir.patch
+
+ubuntu_add_editing_to_view_mode.patch
+ubuntu_fix_disposed_pixbuf_errors_in_adjustment.patch
+ubuntu_add_save_and_undo.patch
+ubuntu_add_filmstrip_browsing_to_view_mode.patch
+ubuntu_add_other_files_in_directory_in_view_mode.patch
+ubuntu_clear_selection_on_crop.patch
+ubuntu_only_update_timestamp_of_unprotected_versions.patch
+ubuntu_handle_broken_uris_to_view_mode.patch
+
+ubuntu_default_view_size.patch
+ubuntu_fix_folder_export_hang.patch
diff --git a/debian/patches/ubuntu_add_editing_to_view_mode.patch b/debian/patches/ubuntu_add_editing_to_view_mode.patch
new file mode 100644
index 0000000..e17ef1c
--- /dev/null
+++ b/debian/patches/ubuntu_add_editing_to_view_mode.patch
@@ -0,0 +1,341 @@
+Description: Enable editing in View mode
+Author: Christopher Halse Rogers <christopher.halse.rogers at canonical.com>
+Author: Ken VanDine <ken.vandine at canonical.com>
+Bug-Ubuntu: https://bugs.edge.launchpad.net/ubuntu/+source/f-spot/+bug/484888
+Bug: https://bugzilla.gnome.org/show_bug.cgi?id=513561	
+
+=== modified file 'src/Editors/Editor.cs'
+Index: f-spot.git/src/Editors/Editor.cs
+===================================================================
+--- f-spot.git.orig/src/Editors/Editor.cs	2010-05-19 13:51:16.967958461 +0100
++++ f-spot.git/src/Editors/Editor.cs	2010-05-19 13:51:50.877959347 +0100
+@@ -18,6 +18,7 @@
+ using Mono.Unix;
+ 
+ using System;
++using System.IO;
+ 
+ namespace FSpot.Editors {
+ 	[ExtensionNode ("Editor")]
+@@ -61,6 +62,9 @@
+ 		public event ProcessingStepHandler ProcessingStep;
+ 		public event ProcessingFinishedHandler ProcessingFinished;
+ 
++		public PhotoImageView View { get; set; }
++		public Gtk.Window ParentWindow {get; set; }
++
+ 		// Contains the current selection, the items being edited, ...
+ 		private EditorState state;
+ 		public EditorState State {
+@@ -96,7 +100,7 @@
+ 		}
+ 
+ 
+-		protected void LoadPhoto (Photo photo, out Pixbuf photo_pixbuf, out Cms.Profile photo_profile) {
++		protected void LoadPhoto (IBrowsableItem photo, out Pixbuf photo_pixbuf, out Cms.Profile photo_profile) {
+ 			// FIXME: We might get this value from the PhotoImageView.
+ 			using (ImageFile img = ImageFile.Create (photo.DefaultVersionUri)) {
+ 				photo_pixbuf = img.Load ();
+@@ -143,18 +147,29 @@
+ 			}
+ 
+ 			int done = 0;
+-			foreach (Photo photo in State.Items) {
++			foreach (IBrowsableItem item in State.Items) {
+ 				Pixbuf input;
+ 				Cms.Profile input_profile;
+-				LoadPhoto (photo, out input, out input_profile);
++				LoadPhoto (item, out input, out input_profile);
+ 
+ 				Pixbuf edited = Process (input, input_profile);
+ 				input.Dispose ();
+ 
+-				bool create_version = photo.DefaultVersion.IsProtected;
+-				photo.SaveVersion (edited, create_version);
+-				photo.Changes.DataChanged = true;
+-				App.Instance.Database.Photos.Commit (photo);
++				if (item is Photo) {
++					var photo = item as Photo;
++					bool create_version = photo.DefaultVersion.IsProtected;
++					photo.SaveVersion (edited, create_version);
++					photo.Changes.DataChanged = true;
++					App.Instance.Database.Photos.Commit (photo);
++				} else {
++					var pb = edited.Copy ();
++					using (ImageFile img = ImageFile.Create (item.DefaultVersionUri)) {
++						using (Stream stream = System.IO.File.OpenWrite (item.DefaultVersionUri.LocalPath)) {
++							img.Save (edited, stream);
++						}
++					}
++					State.PhotoImageView.Pixbuf = pb; 
++				}
+ 
+ 				done++;
+ 				if (ProcessingStep != null) {
+@@ -205,7 +220,11 @@
+ 			Pixbuf previewed = ProcessFast (preview, null);
+ 			State.PhotoImageView.Pixbuf = previewed;
+ 			State.PhotoImageView.ZoomFit (false);
+-			MainWindow.Toplevel.InfoBox.UpdateHistogram (previewed);
++			if (MainWindow.Toplevel != null) {
++				//MainWindow.Toplevel is null if we're in View mode.
++				//If we're in View mode we don't have a histogram, so we don't need to update it.
++				MainWindow.Toplevel.InfoBox.UpdateHistogram (previewed);
++			}
+ 
+ 			if (old_preview != null) {
+ 				old_preview.Dispose ();
+@@ -238,7 +257,11 @@
+ 				State.PhotoImageView.Pixbuf = original;
+ 				State.PhotoImageView.ZoomFit (false);
+ 
+-				MainWindow.Toplevel.InfoBox.UpdateHistogram (null);
++				if (MainWindow.Toplevel != null) {
++					//MainWindow.Toplevel is null if we're in View mode.
++					//If we're in View mode we don't have a histogram, so we dont' need to update it.
++					MainWindow.Toplevel.InfoBox.UpdateHistogram (null);
++				}
+ 			}
+ 
+ 			Reset ();
+Index: f-spot.git/src/FSpot.addin.xml
+===================================================================
+--- f-spot.git.orig/src/FSpot.addin.xml	2010-05-19 13:51:16.927958582 +0100
++++ f-spot.git/src/FSpot.addin.xml	2010-05-19 13:51:50.877959347 +0100
+@@ -61,8 +61,8 @@
+ 
+ 	<Extension path = "/FSpot/Sidebar">
+ 		<SidebarPage sidebar_page_type = "FSpot.Widgets.MetadataDisplayPage" />
++		<SidebarPage sidebar_page_type = "FSpot.Widgets.EditorPage" />
+ 		<Condition id="ViewMode" mode="library">
+-			<SidebarPage sidebar_page_type = "FSpot.Widgets.EditorPage" />
+ 			<SidebarPage sidebar_page_type = "FSpot.Widgets.FolderTreePage" />
+ 		</Condition>
+ 	</Extension>
+Index: f-spot.git/src/MainWindow.cs
+===================================================================
+--- f-spot.git.orig/src/MainWindow.cs	2010-05-19 13:51:16.867959974 +0100
++++ f-spot.git/src/MainWindow.cs	2010-05-19 13:51:50.877959347 +0100
+@@ -375,10 +375,8 @@
+ 	
+ 			Sidebar.AppendPage (tag_selection_scrolled, Catalog.GetString ("Tags"), "tag");
+ 	
+-			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);
+-	
+ 			Sidebar.Context = ViewContext.Library;
+-	 		
++			
+ 			Sidebar.CloseRequested += HideSidebar;
+ 			Sidebar.Show ();
+ 	
+@@ -454,7 +452,7 @@
+ 			new FSpot.PreviewPopup (icon_view);
+ 	
+ 			Gtk.Drag.SourceSet (icon_view, Gdk.ModifierType.Button1Mask | Gdk.ModifierType.Button3Mask,
+-					    icon_source_target_table, DragAction.Copy | DragAction.Move);
++						icon_source_target_table, DragAction.Copy | DragAction.Move);
+ 			
+ 			icon_view.DragBegin += HandleIconViewDragBegin;
+ 			icon_view.DragDataGet += HandleIconViewDragDataGet;
+@@ -487,6 +485,9 @@
+ 			photo_view.UpdateFinished += HandlePhotoViewUpdateFinished;
+ 	
+ 			photo_view.View.ZoomChanged += HandleZoomChanged;
++
++ 			// Sidebar extensions need access to PhotoView, so this has to be delayed until after we've constructed photo_view
++ 			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);
+ 	
+ 			// Tag typing: focus the tag entry if the user starts typing a tag
+ 			icon_view.KeyPressEvent += HandlePossibleTagTyping;
+@@ -567,8 +568,12 @@
+ 	
+ 		private void OnSidebarExtensionChanged (object s, ExtensionNodeEventArgs args) {
+ 			// FIXME: No sidebar page removal yet!
+-			if (args.Change == ExtensionChange.Add)
+-				Sidebar.AppendPage ((args.ExtensionNode as SidebarPageNode).GetPage ());
++ 			if (args.Change == ExtensionChange.Add) {
++				var page = ((SidebarPageNode)args.ExtensionNode).GetPage ();
++				page.PhotoImageView = PhotoView.View;
++				page.ParentWindow = Window;
++				Sidebar.AppendPage (page);
++			}
+ 		}
+ 	
+ 		private Photo CurrentPhoto {
+Index: f-spot.git/src/SingleView.cs
+===================================================================
+--- f-spot.git.orig/src/SingleView.cs	2010-05-19 13:51:16.887960131 +0100
++++ f-spot.git/src/SingleView.cs	2010-05-19 13:51:50.877959347 +0100
+@@ -114,9 +114,6 @@
+ 			info_vbox.Add (sidebar);
+ 			sidebar.AppendPage (directory_scrolled, Catalog.GetString ("Folder"), "gtk-directory");
+ 
+-			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);
+- 		
+-			sidebar.Context = ViewContext.Single;
+ 
+ 			sidebar.CloseRequested += HandleHideSidePane;
+ 			sidebar.Show ();
+@@ -136,6 +133,9 @@
+ 			
+ 			Window.ShowAll ();
+ 
++			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);	
++			sidebar.Context = ViewContext.Single;
++
+ 			zoom_scale.ValueChanged += HandleZoomScaleValueChanged;
+ 		
+ 			LoadPreference (Preferences.VIEWER_SHOW_TOOLBAR);
+@@ -143,7 +143,9 @@
+ 			LoadPreference (Preferences.VIEWER_TRANSPARENCY);
+ 			LoadPreference (Preferences.VIEWER_TRANS_COLOR);
+ 
+-			ShowSidebar = collection.Count > 1;
++			// We always want to start by showing the sidebar to make it
++			// more obvious how to edit.
++			ShowSidebar = true;
+ 
+ 			LoadPreference (Preferences.VIEWER_SHOW_FILENAMES);
+ 
+@@ -176,8 +178,13 @@
+ 
+ 		private void OnSidebarExtensionChanged (object s, ExtensionNodeEventArgs args) {
+ 			// FIXME: No sidebar page removal yet!
+-			if (args.Change == ExtensionChange.Add)
+-				sidebar.AppendPage ((args.ExtensionNode as SidebarPageNode).GetPage ());
++			if (args.Change == ExtensionChange.Add) {
++				var page = (args.ExtensionNode as SidebarPageNode).GetPage ();
++				Log.Debug ("OnSidebarExtensionAdded {0}  / {1} / {2}", page, image_view, window);
++				page.PhotoImageView = image_view;
++				page.ParentWindow = window;
++				sidebar.AppendPage (page); 
++			}
+ 		}
+ 
+ 		void HandleExportActivated (object o, EventArgs e)
+Index: f-spot.git/src/Widgets/EditorPage.cs
+===================================================================
+--- f-spot.git.orig/src/Widgets/EditorPage.cs	2010-05-19 13:51:16.947959526 +0100
++++ f-spot.git/src/Widgets/EditorPage.cs	2010-05-19 13:51:50.877959347 +0100
+@@ -24,7 +24,7 @@
+ namespace FSpot.Widgets {
+ 	public class EditorPage : SidebarPage {
+ 		internal bool InPhotoView;
+-		private readonly EditorPageWidget EditorPageWidget;
++		readonly EditorPageWidget EditorPageWidget;
+ 
+ 		public EditorPage () : base (new EditorPageWidget (),
+ 									   Catalog.GetString ("Edit"),
+@@ -35,6 +35,16 @@
+ 			EditorPageWidget.Page = this;
+ 		}
+ 
++		public override PhotoImageView PhotoImageView {
++			get { return EditorPageWidget.PhotoImageView; }
++			set { EditorPageWidget.PhotoImageView = value; }
++		}
++
++		public override Gtk.Window ParentWindow {
++			get { return EditorPageWidget.ParentWindow; }
++			set { EditorPageWidget.ParentWindow = value ;}
++		}
++
+ 		protected override void AddedToSidebar () {
+ 			(Sidebar as Sidebar).SelectionChanged += delegate (IBrowsableCollection collection) { EditorPageWidget.ShowTools (); };
+ 			(Sidebar as Sidebar).ContextChanged += HandleContextChanged;
+@@ -42,18 +52,21 @@
+ 
+ 		private void HandleContextChanged (object sender, EventArgs args)
+ 		{
+-			InPhotoView = ((Sidebar as Sidebar).Context == ViewContext.Edit);
++            InPhotoView = (Sidebar.Context == ViewContext.Edit) || (Sidebar.Context == ViewContext.Single);
+ 			EditorPageWidget.ChangeButtonVisibility ();
+ 		}
+ 	}
+ 
+ 	public class EditorPageWidget : ScrolledWindow {
+-		private VBox widgets;
+-		private VButtonBox buttons;
+-		private Widget active_editor;
++		VBox widgets;
++		VButtonBox buttons;
++		Widget active_editor;
++
++		List<Editor> editors;
++		Editor current_editor;
+ 
+-		private List<Editor> editors;
+-		private Editor current_editor;
++        public PhotoImageView PhotoImageView { get; set;}
++        public Gtk.Window ParentWindow { get; set; }
+ 
+ 		// Used to make buttons insensitive when selecting multiple images.
+ 		private Dictionary<Editor, Button> editor_buttons;
+@@ -79,6 +92,8 @@
+ 				editor.ProcessingStarted += OnProcessingStarted;
+ 				editor.ProcessingStep += OnProcessingStep;
+ 				editor.ProcessingFinished += OnProcessingFinished;
++				editor.View = PhotoImageView;
++				editor.ParentWindow = ParentWindow;
+ 				editors.Add (editor);
+ 				PackButton (editor);
+ 			}
+@@ -87,7 +102,7 @@
+ 		private ProgressDialog progress;
+ 
+ 		private void OnProcessingStarted (string name, int count) {
+-			progress = new ProgressDialog (name, ProgressDialog.CancelButtonType.None, count, MainWindow.Toplevel.Window);
++			progress = new ProgressDialog (name, ProgressDialog.CancelButtonType.None, count, ParentWindow);
+ 		}
+ 
+ 		private void OnProcessingStep (int done) {
+@@ -172,8 +187,7 @@
+ 		private bool SetupEditor (Editor editor) {
+ 			EditorState state = editor.CreateState ();
+ 
+-			PhotoImageView photo_view = MainWindow.Toplevel.PhotoView.View;
+-
++			PhotoImageView photo_view = PhotoImageView;
+ 			if (Page.InPhotoView && photo_view != null) {
+ 				state.Selection = photo_view.Selection;
+ 				state.PhotoImageView = photo_view;
+@@ -197,7 +211,7 @@
+ 				string msg = Catalog.GetString ("No selection available");
+ 				string desc = Catalog.GetString ("This tool requires an active selection. Please select a region of the photo and try the operation again");
+ 
+-				HigMessageDialog md = new HigMessageDialog (MainWindow.Toplevel.Window,
++				HigMessageDialog md = new HigMessageDialog (ParentWindow,
+ 										DialogFlags.DestroyWithParent,
+ 										Gtk.MessageType.Error, ButtonsType.Ok,
+ 										msg,
+@@ -218,7 +232,7 @@
+ 				string desc = String.Format (Catalog.GetString ("Received exception \"{0}\". Note that you have to develop RAW files into JPEG before you can edit them."),
+ 							     e.Message);
+ 
+-				HigMessageDialog md = new HigMessageDialog (MainWindow.Toplevel.Window,
++				HigMessageDialog md = new HigMessageDialog (ParentWindow,
+ 									    DialogFlags.DestroyWithParent,
+ 									    Gtk.MessageType.Error, ButtonsType.Ok,
+ 									    msg,
+Index: f-spot.git/src/Extensions/SidebarPage.cs
+===================================================================
+--- f-spot.git.orig/src/Extensions/SidebarPage.cs	2010-05-19 13:51:16.907958803 +0100
++++ f-spot.git/src/Extensions/SidebarPage.cs	2010-05-19 13:52:47.296709401 +0100
+@@ -9,6 +9,7 @@
+  * This is free software. See COPYING for details.
+  */
+ 
++using FSpot.Widgets;
+ using FSpot.Extensions;
+ using FSpot.Utils;
+ using Gtk;
+@@ -64,6 +65,9 @@
+ 		// Can be overriden to get notified as soon as we're added to a sidebar.
+ 		protected virtual void AddedToSidebar () { }
+ 
++        public virtual PhotoImageView PhotoImageView { get; set; }
++        public virtual Gtk.Window ParentWindow { get; set; }
++
+ //		// Whether this page is currently visible
+ //		public bool IsActive {
+ //			get { return Sidebar.IsActive (this); }
diff --git a/debian/patches/ubuntu_add_filmstrip_browsing_to_view_mode.patch b/debian/patches/ubuntu_add_filmstrip_browsing_to_view_mode.patch
new file mode 100644
index 0000000..817a81a
--- /dev/null
+++ b/debian/patches/ubuntu_add_filmstrip_browsing_to_view_mode.patch
@@ -0,0 +1,274 @@
+Description: Add a filmstrip and previous/next buttons to View mode
+ Without this, it's frustrating to switch between images to edit.
+Author: Christopher Halse Rogers <christopher.halse.rogers at canonical.com>
+Forwarded: No
+
+=== modified file 'src/PhotoImageView.cs'
+Index: f-spot.git/src/PhotoImageView.cs
+===================================================================
+--- f-spot.git.orig/src/PhotoImageView.cs	2010-05-19 14:28:58.127960001 +0100
++++ f-spot.git/src/PhotoImageView.cs	2010-05-19 14:28:58.147959007 +0100
+@@ -24,7 +24,7 @@
+ 		{
+ 		}
+ 		
+-		public PhotoImageView (IBrowsableCollection query, UndoCache history) : this(query)
++		public PhotoImageView (BrowsablePointer query, UndoCache history) : this(query)
+ 		{
+ 			history_cache = history;
+ 		}
+Index: f-spot.git/src/SingleView.cs
+===================================================================
+--- f-spot.git.orig/src/SingleView.cs	2010-05-19 14:28:58.127960001 +0100
++++ f-spot.git/src/SingleView.cs	2010-05-19 14:30:47.816709694 +0100
+@@ -21,6 +21,7 @@
+ 		[Glade.Widget] Gtk.ScrolledWindow image_scrolled;
+ 		[Glade.Widget] Gtk.HPaned info_hpaned;
+ 
++		[Glade.Widget] Gtk.VBox display_vbox;
+ 		Gtk.ScrolledWindow directory_scrolled;
+ 
+ 		[Glade.Widget] Gtk.CheckMenuItem side_pane_item;
+@@ -41,12 +42,17 @@
+ 
+ 		ToolButton rr_button, rl_button;
+ 		ToolButton undo;
++		
++		ToolButton prev_image;
++		ToolButton next_image;
++		Label count_label;
+ 
+ 		Sidebar sidebar;
+ 
+ 		protected Glade.XML xml;
+ 		private Gtk.Window window;
+ 		PhotoImageView image_view;
++		Filmstrip filmstrip;
+ 		FSpot.Widgets.IconView directory_view;
+ 		private Uri uri;
+ 		
+@@ -93,7 +99,6 @@
+ 			toolbar.Insert (undo, -1);
+ 			
+ 			toolbar.Insert (new SeparatorToolItem (), -1);
+-
+ 			
+ 			ToolButton fs_button = GtkUtil.ToolButtonFromTheme ("view-fullscreen", Catalog.GetString ("Fullscreen"), true);
+ 			fs_button.Clicked += HandleViewFullscreen;
+@@ -104,12 +109,33 @@
+ 			ss_button.Clicked += HandleViewSlideshow;
+ 			ss_button.SetTooltip (toolTips, Catalog.GetString ("View photos in a slideshow"), null);
+ 			toolbar.Insert (ss_button, -1);
++			
++			SeparatorToolItem white_space = new SeparatorToolItem ();
++			white_space.Draw = false;
++			white_space.Expand = true;
++			toolbar.Insert (white_space, -1);
++			
++			ToolItem label_item = new ToolItem ();
++			count_label = new Label (String.Empty);
++			label_item.Child = count_label;
++			toolbar.Insert (label_item, -1);
++			
++			prev_image = new ToolButton (Stock.GoBack);
++			toolbar.Insert (prev_image, -1);
++			prev_image.TooltipText = Catalog.GetString ("Previous photo");
++			prev_image.Clicked += (sender, args) => { image_view.Item.MovePrevious (); };
++			
++			next_image = new ToolButton (Stock.GoForward);
++			toolbar.Insert (next_image, -1);
++			next_image.TooltipText = Catalog.GetString ("Next photo");
++			next_image.Clicked += (sender, args) => { image_view.Item.MoveNext (); };
+ 
++			
+ 			collection = new UriCollection (uris);
+ 			undo_history = new UndoCache ();
+ 			undo_history.UndoStateChanged += HandleUndoStateChanged;
+ 
+-			TargetEntry [] dest_table = {
++			TargetEntry[] dest_table = {
+ 				FSpot.DragDropTargets.UriListEntry,
+ 				FSpot.DragDropTargets.PlainTextEntry
+ 			};
+@@ -118,12 +144,12 @@
+ 			directory_view.Selection.Changed += HandleSelectionChanged;
+ 			directory_view.DragDataReceived += HandleDragDataReceived;
+ 			Gtk.Drag.DestSet (directory_view, DestDefaults.All, dest_table, 
+-					DragAction.Copy | DragAction.Move); 
++					DragAction.Copy | DragAction.Move);
+ 			directory_view.DisplayTags = false;
+ 			directory_view.DisplayDates = false;
+ 			directory_view.DisplayRatings = false;
+ 
+-			directory_scrolled = new ScrolledWindow();
++			directory_scrolled = new ScrolledWindow ();
+ 			directory_scrolled.Add (directory_view);
+ 
+ 			sidebar = new Sidebar ();
+@@ -137,7 +163,8 @@
+ 
+ 			ThumbnailGenerator.Default.OnPixbufLoaded += delegate { directory_view.QueueDraw (); };
+ 
+-			image_view = new PhotoImageView (collection, undo_history);
++			BrowsablePointer bp = new BrowsablePointer (collection, -1);
++			image_view = new PhotoImageView (bp, undo_history);
+ 			GtkUtil.ModifyColors (image_view);
+ 			GtkUtil.ModifyColors (image_scrolled);
+ 			image_view.ZoomChanged += HandleZoomChanged;
+@@ -145,9 +172,19 @@
+ 			image_view.ButtonPressEvent += HandleImageViewButtonPressEvent;
+ 			image_view.DragDataReceived += HandleDragDataReceived;
+ 			Gtk.Drag.DestSet (image_view, DestDefaults.All, dest_table,
+-					DragAction.Copy | DragAction.Move); 
++					DragAction.Copy | DragAction.Move);
+ 			image_scrolled.Add (image_view);
+ 			
++			filmstrip = new Filmstrip (bp);
++			Gdk.Pixbuf bg = new Gdk.Pixbuf (Gdk.Colorspace.Rgb, true, 8, 1, 69);
++			bg.Fill (0x00000000);
++			filmstrip.BackgroundTile = bg;
++			filmstrip.ThumbOffset = 1;
++			filmstrip.Spacing = 4;
++			ToggleFilmstrip (true);
++			filmstrip.Visible = true;
++			GtkUtil.ModifyColors (filmstrip);
++			
+ 			Window.ShowAll ();
+ 
+ 			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);	
+@@ -184,6 +221,7 @@
+ 			image_view.Item.Collection.ItemsChanged += sidebar.HandleSelectionItemsChanged;
+ 
+ 			UpdateStatusLabel ();
++			UpdateToolbar (image_view.Item.Collection);
+ 			
+ 			if (collection.Count > 0)
+ 				directory_view.Selection.Add (0);
+@@ -193,6 +231,28 @@
+ 			export.Activated += HandleExportActivated ;
+ 		}
+ 
++		void ToggleFilmstrip (bool enabled)
++		{
++			Gtk.Widget packed_filmstrip = display_vbox.AllChildren.Cast<Gtk.Widget> ().FirstOrDefault ((widget) => {return widget == filmstrip;});
++			if (enabled && packed_filmstrip == null) {
++				// Filmstrip isn't in the vbox, add it
++				Log.Debug ("Packing filmstrip");
++				display_vbox.PackStart (filmstrip, false, false, 0);
++			} else if (!enabled && packed_filmstrip != null) {
++				// Filmstrip is in the vbox, remove it
++				display_vbox.Remove (packed_filmstrip);
++			}
++		}
++		
++		void UpdateToolbar (IBrowsableCollection collection)
++		{
++			// Note for translators: This indicates the current photo is photo {0} of {1} out of photos
++			count_label.Text = String.Format (Catalog.GetString ("{0} of {1}"), 
++				collection.Count == 0 ? 0 : image_view.Item.Index + 1, collection.Count);
++			prev_image.Sensitive = collection.Count > 0 && image_view.Item.Index != 0;
++			next_image.Sensitive = collection.Count > 0 && image_view.Item.Index != image_view.Item.Collection.Count - 1;
++		}
++		
+ 		void HandleUndoStateChanged (object sender, UndoStateChangedArgs e)
+ 		{
+ 			if (e.ChangedFor == image_view.Item.Current.DefaultVersionUri) {
+@@ -244,6 +304,7 @@
+ 			rotate_left.Sensitive = rotate_right.Sensitive = rr_button.Sensitive = rl_button.Sensitive = collection.Count != 0;
+ 
+ 			UpdateStatusLabel ();
++			UpdateToolbar (collection);
+ 		}
+ 
+ 		public bool ShowSidebar {
+@@ -308,6 +369,7 @@
+ 				undo.Sensitive = false;
+ 			}
+ 			UpdateStatusLabel ();
++			UpdateToolbar (image_view.Item.Collection);
+ 		}
+ 
+ 		void UpdateTitle ()
+@@ -370,8 +432,9 @@
+ 	
+ 		private void HandleViewFilenames (object sender, System.EventArgs args)
+ 		{
+-			directory_view.DisplayFilenames = filenames_item.Active; 
++			directory_view.DisplayFilenames = filenames_item.Active;
+ 			UpdateStatusLabel ();
++			UpdateToolbar (image_view.Item.Collection);
+ 		}
+ 
+ 		private void HandleAbout (object sender, System.EventArgs args)
+@@ -672,6 +735,7 @@
+ 		{
+ 			if (PresentUnsavedChangesDialog ()) {
+ 				SavePreferences ();
++				filmstrip.Dispose ();
+ 				undo_history.Dispose ();
+ 				this.Window.Destroy ();
+ 			}
+@@ -714,6 +778,7 @@
+ 			if (PresentUnsavedChangesDialog ()) {
+ 				SavePreferences ();
+ 				undo_history.Dispose ();
++				filmstrip.Dispose ();
+ 				this.Window.Destroy ();
+ 			}
+ 		}
+Index: f-spot.git/src/f-spot.glade
+===================================================================
+--- f-spot.git.orig/src/f-spot.glade	2010-05-19 14:28:58.127960001 +0100
++++ f-spot.git/src/f-spot.glade	2010-05-19 14:28:58.147959007 +0100
+@@ -2699,15 +2699,28 @@
+               </packing>
+             </child>
+             <child>
+-              <widget class="GtkScrolledWindow" id="image_scrolled">
++              <widget class="GtkVBox" id="display_vbox">
+                 <property name="visible">True</property>
+-                <property name="can_focus">True</property>
+-                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+-                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+-                <property name="shadow_type">GTK_SHADOW_IN</property>
++                <property name="orientation">vertical</property>
+                 <child>
+                   <placeholder/>
+                 </child>
++                <child>
++                  <widget class="GtkScrolledWindow" id="image_scrolled">
++                    <property name="visible">True</property>
++                    <property name="can_focus">True</property>
++                    <property name="hscrollbar_policy">automatic</property>
++                    <property name="vscrollbar_policy">automatic</property>
++                    <property name="shadow_type">in</property>
++                    <child>
++                      <placeholder/>
++                    </child>
++                  </widget>
++                  <packing>
++                    <property name="pack_type">end</property>
++                    <property name="position">0</property>
++                  </packing>
++                </child>
+               </widget>
+               <packing>
+                 <property name="resize">True</property>
+Index: f-spot.git/src/Widgets/Filmstrip.cs
+===================================================================
+--- f-spot.git.orig/src/Widgets/Filmstrip.cs	2010-05-19 13:49:44.436739359 +0100
++++ f-spot.git/src/Widgets/Filmstrip.cs	2010-05-19 14:28:58.147959007 +0100
+@@ -587,8 +587,11 @@
+ 
+ 		protected override bool OnButtonPressEvent (EventButton evnt)
+ 		{
+-			if (evnt.Button == 3)
+-				return DrawOrientationMenu (evnt); 
++			// We don't handle changing the orientation in View mode at this point
++			if (MainWindow.Toplevel != null) {
++				if (evnt.Button == 3)
++					return DrawOrientationMenu (evnt); 
++			}
+ 
+ 			if (evnt.Button != 1 || (
+ 				(Orientation == Orientation.Horizontal && (evnt.X > filmstrip_end_pos || evnt.X < filmstrip_start_pos)) || 
diff --git a/debian/patches/ubuntu_add_other_files_in_directory_in_view_mode.patch b/debian/patches/ubuntu_add_other_files_in_directory_in_view_mode.patch
new file mode 100644
index 0000000..565cd37
--- /dev/null
+++ b/debian/patches/ubuntu_add_other_files_in_directory_in_view_mode.patch
@@ -0,0 +1,382 @@
+Description: Display all images in directories passed to --view
+ When a file is passed to --view, also view all the other images in the same
+ directory.
+Author: Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
+Bug-LP: https://bugs.edge.launchpad.net/ubuntu/+source/f-spot/+bug/484887
+Bug: https://bugzilla.gnome.org/show_bug.cgi?id=333189
+
+Index: f-spot.git/src/SingleView.cs
+===================================================================
+--- f-spot.git.orig/src/SingleView.cs	2010-05-19 14:28:58.147959007 +0100
++++ f-spot.git/src/SingleView.cs	2010-05-19 14:28:58.167959093 +0100
+@@ -60,6 +60,8 @@
+ 		UndoCache undo_history;
+ 		
+ 		FullScreenView fsview;
++		
++		List<FileSystemWatcher> directory_watchers;
+ 
+ 		private static Gtk.Tooltips toolTips = new Gtk.Tooltips ();
+ 
+@@ -68,28 +70,53 @@
+ 			string glade_name = "single_view";
+ 			this.uri = uris[0];
+ 			
++			directory_watchers = new List<FileSystemWatcher> ();
++			// If we are passed the URI to a file we want to
++			// (a) Display & select that file, and 
++			// (b) Display the other files in the same directory.
++			List<Uri> uris_to_display = new List<Uri> ();
++			foreach (var uri in uris) {
++				Uri candidate_uri;
++				if (!Directory.Exists (uri.LocalPath)) {
++					candidate_uri = uri.GetDirectoryUri ();
++				} else {
++					candidate_uri = uri;
++				}
++				if (!uris_to_display.Contains (candidate_uri)) {
++					uris_to_display.Add (candidate_uri);
++					var directory_watcher = new FileSystemWatcher (candidate_uri.LocalPath);
++					Log.Debug ("Watching directory {0} for changes.", candidate_uri.LocalPath);
++					directory_watcher.IncludeSubdirectories = false;
++					directory_watcher.Changed += HandleFileChangedEvent;
++					directory_watcher.Created += HandleFileCreatedEvent;
++					directory_watcher.Deleted += HandleFileDeletedEvent;
++					directory_watcher.EnableRaisingEvents = true;
++					directory_watchers.Add (directory_watcher);
++				}
++			}
++			
+ 			xml = new Glade.XML (null, "f-spot.glade", glade_name, "f-spot");
+ 			xml.Autoconnect (this);
+ 			window = (Gtk.Window)xml.GetWidget (glade_name);
+-		
++			
+ 			LoadPreference (Preferences.VIEWER_WIDTH);
+ 			LoadPreference (Preferences.VIEWER_MAXIMIZED);
+-
++			
+ 			Gtk.Toolbar toolbar = new Gtk.Toolbar ();
+ 			toolbar_hbox.PackStart (toolbar);
+-		
++			
+ 			rl_button = GtkUtil.ToolButtonFromTheme ("object-rotate-left", Catalog.GetString ("Rotate Left"), true);
+ 			rl_button.Clicked += HandleRotate270Command;
+ 			rl_button.SetTooltip (toolTips, Catalog.GetString ("Rotate photo left"), null);
+ 			toolbar.Insert (rl_button, -1);
+-
++			
+ 			rr_button = GtkUtil.ToolButtonFromTheme ("object-rotate-right", Catalog.GetString ("Rotate Right"), true);
+ 			rr_button.Clicked += HandleRotate90Command;
+ 			rr_button.SetTooltip (toolTips, Catalog.GetString ("Rotate photo right"), null);
+ 			toolbar.Insert (rr_button, -1);
+-
++			
+ 			toolbar.Insert (new SeparatorToolItem (), -1);
+-
++			
+ 			StockItem undo_info = Stock.Lookup (Stock.Undo);
+ 			undo = new ToolButton (undo_info.StockId);
+ 			undo.Label = undo_info.Label;
+@@ -104,7 +131,7 @@
+ 			fs_button.Clicked += HandleViewFullscreen;
+ 			fs_button.SetTooltip (toolTips, Catalog.GetString ("View photos fullscreen"), null);
+ 			toolbar.Insert (fs_button, -1);
+-
++			
+ 			ToolButton ss_button = GtkUtil.ToolButtonFromTheme ("media-playback-start", Catalog.GetString ("Slideshow"), true);
+ 			ss_button.Clicked += HandleViewSlideshow;
+ 			ss_button.SetTooltip (toolTips, Catalog.GetString ("View photos in a slideshow"), null);
+@@ -129,9 +156,10 @@
+ 			toolbar.Insert (next_image, -1);
+ 			next_image.TooltipText = Catalog.GetString ("Next photo");
+ 			next_image.Clicked += (sender, args) => { image_view.Item.MoveNext (); };
+-
+ 			
+-			collection = new UriCollection (uris);
++			collection = new UriCollection (uris_to_display.ToArray ());
++			collection.LoadingComplete += HandleCollectionLoadingComplete;
++			
+ 			undo_history = new UndoCache ();
+ 			undo_history.UndoStateChanged += HandleUndoStateChanged;
+ 
+@@ -187,7 +215,7 @@
+ 			
+ 			Window.ShowAll ();
+ 
+-			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);	
++			AddinManager.AddExtensionNodeHandler ("/FSpot/Sidebar", OnSidebarExtensionChanged);
+ 			sidebar.Context = ViewContext.Single;
+ 
+ 			zoom_scale.ValueChanged += HandleZoomScaleValueChanged;
+@@ -223,14 +251,119 @@
+ 			UpdateStatusLabel ();
+ 			UpdateToolbar (image_view.Item.Collection);
+ 			
+-			if (collection.Count > 0)
+-				directory_view.Selection.Add (0);
++			if (collection.Count > 0) {
++				if (File.Exists (uris[0].AbsolutePath)) {
++					Log.Debug ("File {0} exists, displaying", uris[0].AbsolutePath);
++					var selected_image = collection.Items.FirstOrDefault ((item) => item.DefaultVersionUri == uris[0]);
++					if (selected_image != null) {
++						directory_view.Selection.Add (selected_image);
++					} else {
++						Log.Warning ("Unable to open requested file {0}", uris[0].AbsolutePath);
++					}
++				} else {
++					directory_view.Selection.Add (0);
++				}
++			}
+ 
+ 			export.Submenu = (Mono.Addins.AddinManager.GetExtensionNode ("/FSpot/Menus/Exports") as FSpot.Extensions.SubmenuNode).GetMenuItem (this).Submenu;
+ 			export.Submenu.ShowAll ();
+ 			export.Activated += HandleExportActivated ;
+ 		}
+ 
++		void HandleFileCreatedEvent (object sender, FileSystemEventArgs e)
++		{
++			Log.Debug ("Trying to add new file {0} to viewer", e.FullPath);
++			GLib.Timeout.Add (500, () =>
++			{
++				if (!File.Exists (e.FullPath)) {
++					// The file may have been deleted after being created, particularly if it was a temporary file
++					// for write => rename over save methods.  In this case, we've got nothing to do now.
++					return false;
++				}
++				if ((DateTime.Now - File.GetLastWriteTime (e.FullPath)).TotalMilliseconds < 400) {
++					Log.Debug ("A process is still writing to {0}.  Waiting another 500ms then trying again", e.FullPath);
++					return true;
++				}
++				collection.Add (new Uri (e.FullPath));
++				return false;
++			});
++		}
++		
++		void HandleFileDeletedEvent (object sender, FileSystemEventArgs e)
++		{
++			Log.Debug ("File {0} deleted, removing references from viewer", e.FullPath);
++			Uri deleted_uri = new Uri (e.FullPath);
++			// File monitor events will come from an arbitrary thread.  Proxy them to the main thread via the main loop to make things easier.
++			GLib.Timeout.Add(0, () => {
++				foreach (var deleted_item in collection.Items.Where ((item) => item.DefaultVersionUri == deleted_uri)) {
++					if (deleted_item != null) {
++						var current_item = image_view.Item.Current;
++						if (current_item == deleted_item) {
++							if (collection.Count == 1) {
++								// Someone's deleted our only file!  Waaaa!
++								// The user can still edit this copy, though.
++								return false;
++							}
++							if (image_view.Item.Index < collection.Count - 1) {
++								// We're not at the last item, so we can move to the next one
++								current_item = collection[image_view.Item.Index + 1];
++							} else {
++								// We are at the last item.  Move to the previous item
++								current_item = collection[image_view.Item.Index - 1];
++							}
++						}
++						while (undo_history.ContainsUndoInformationFor (deleted_uri)) {
++							undo_history.PopEdit (deleted_uri);
++						}
++						collection.Items = collection.Items.Where ((item) => item != deleted_item).ToArray ();
++						image_view.Item.MoveFirst ();
++						image_view.Item.Index = collection.IndexOf (current_item);
++					}
++				}
++				return false;
++			});
++		}
++		
++		HashSet<string> file_change_processors = new HashSet<string> ();
++		void HandleFileChangedEvent (object sender, FileSystemEventArgs e)
++		{
++			Uri changed_uri = new Uri (e.FullPath);
++			var changed_item = collection.Items.FirstOrDefault ((item) => item.DefaultVersionUri == changed_uri);
++			if (changed_item != null) {
++				if (!file_change_processors.Contains (e.FullPath)) {
++					Log.Debug ("File {0} changed, triggering update.", e.FullPath);
++					file_change_processors.Add (e.FullPath);
++					GLib.Timeout.Add (500, () => {
++						if ((DateTime.Now - File.GetLastWriteTime (e.FullPath)).TotalMilliseconds < 400) {
++							Log.Debug ("A process is still writing to {0}.  Waiting 500ms for it to finish", e.FullPath);
++							return true;
++						}
++						collection.MarkChanged (new BrowsableEventArgs (collection.IndexOf (changed_item), new FullInvalidate ()));
++						file_change_processors.Remove (e.FullPath);
++						return false;
++					});
++				}
++			}
++		}
++		
++		void HandleCollectionLoadingComplete (object sender, EventArgs e)
++		{
++			if (collection.Count > 0) {
++				if (File.Exists (Uri.UnescapeDataString (uri.AbsolutePath))) {
++					Log.Debug ("{0} exists, displaying", uri);
++					var selected_image = collection.Items.FirstOrDefault ((item) => item.DefaultVersionUri == uri);
++					
++					if (selected_image != null && directory_view != null) {
++						directory_view.Selection.Clear ();
++						directory_view.Selection.Add (selected_image);
++					}
++				} else {
++					Log.Warning ("Unable to open requested file {0}", uri);
++				}
++			}
++			collection.LoadingComplete -= HandleCollectionLoadingComplete;
++		}
++
+ 		void ToggleFilmstrip (bool enabled)
+ 		{
+ 			Gtk.Widget packed_filmstrip = display_vbox.AllChildren.Cast<Gtk.Widget> ().FirstOrDefault ((widget) => {return widget == filmstrip;});
+Index: f-spot.git/src/UriCollection.cs
+===================================================================
+--- f-spot.git.orig/src/UriCollection.cs	2010-05-19 13:49:44.436739359 +0100
++++ f-spot.git/src/UriCollection.cs	2010-05-19 14:28:58.167959093 +0100
+@@ -21,25 +21,31 @@
+ 
+ namespace FSpot {
+ 	public class UriCollection : PhotoList {
+-		public UriCollection () : base (new IBrowsableItem [0])
++		public UriCollection () : base(new IBrowsableItem[0])
+ 		{
++			loading_count = 0;
+ 		}
+ 
+-		public UriCollection (System.IO.FileInfo [] files) : this ()
++		public UriCollection (System.IO.FileInfo[] files) : this()
+ 		{
++			loading_count = 0;
++			LoaderStarting ();
+ 			LoadItems (files);
+ 		}
+ 
+-		public UriCollection (Uri [] uri) : this ()
++		public UriCollection (Uri[] uri) : this()
+ 		{
++			loading_count = 0;
+ 			LoadItems (uri);
+ 		}
+ 
+ 		public void Add (Uri uri)
+ 		{
++			LoaderStarting ();
+ 			if (FSpot.ImageFile.HasLoader (uri)) {
+ 				//Console.WriteLine ("using image loader {0}", uri.ToString ());
+ 				Add (new FileBrowsableItem (uri));
++				LoaderComplete ();
+ 			} else {
+ 				GLib.FileInfo info = FileFactory.NewForUri (uri).QueryInfo ("standard::type,standard::content-type", FileQueryInfoFlags.None, null);
+ 
+@@ -57,13 +63,37 @@
+ 			}
+ 		}
+ 
+-		public void LoadItems (Uri [] uris)
++		public void LoadItems (Uri[] uris)
+ 		{
++			// Prime the count with 1, so that if everything is synchronous the LoadingCount-- at the end
++			// will trigger LoadingComplete.
++			LoaderStarting ();
+ 			foreach (Uri uri in uris) {
+ 				Add (uri);
+ 			}
++			LoaderComplete ();
+ 		}
+ 
++		public event EventHandler LoadingComplete;
++		
++		int loading_count;
++		void LoaderStarting ()
++		{
++			System.Threading.Interlocked.Increment (ref loading_count);
++		}
++		
++		void LoaderComplete ()
++		{
++			if (System.Threading.Interlocked.Decrement (ref loading_count) == 0) {
++				FSpot.Utils.Log.Debug ("Final loader finished, triggering LoadingComplete signal");
++				var handler = LoadingComplete;
++				if (handler != null) {
++					handler (this, EventArgs.Empty);
++				}
++			}
++		}
++		
++		
+ 		private class RssLoader
+ 		{
+ 			public RssLoader (UriCollection collection, System.Uri uri)
+@@ -74,15 +104,15 @@
+ 				ns.AddNamespace ("media", "http://search.yahoo.com/mrss/");
+ 				ns.AddNamespace ("pheed", "http://www.pheed.com/pheed/");
+ 				ns.AddNamespace ("apple", "http://www.apple.com/ilife/wallpapers");
+-
++				
+ 				ArrayList items = new ArrayList ();
+ 				XmlNodeList list = doc.SelectNodes ("/rss/channel/item/media:content", ns);
+ 				foreach (XmlNode item in list) {
+-					Uri image_uri = new Uri (item.Attributes ["url"].Value);
++					Uri image_uri = new Uri (item.Attributes["url"].Value);
+ 					System.Console.WriteLine ("flickr uri = {0}", image_uri.ToString ());
+ 					items.Add (new FileBrowsableItem (image_uri));
+ 				}
+-
++				
+ 				if (list.Count < 1) {
+ 					list = doc.SelectNodes ("/rss/channel/item/pheed:imgsrc", ns);
+ 					foreach (XmlNode item in list) {
+@@ -91,7 +121,7 @@
+ 						items.Add (new FileBrowsableItem (image_uri));
+ 					}
+ 				}
+-
++				
+ 				if (list.Count < 1) {
+ 					list = doc.SelectNodes ("/rss/channel/item/apple:image", ns);
+ 					foreach (XmlNode item in list) {
+@@ -100,7 +130,8 @@
+ 						items.Add (new FileBrowsableItem (image_uri));
+ 					}
+ 				}
+-				collection.Add (items.ToArray (typeof (FileBrowsableItem)) as FileBrowsableItem []);
++				collection.Add (items.ToArray (typeof(FileBrowsableItem)) as FileBrowsableItem[]);
++				collection.LoaderComplete ();
+ 			}
+ 		}
+ 
+@@ -120,7 +151,6 @@
+ 							     500,
+ 							     null,
+ 							     InfoLoaded);
+-										    
+ 			}
+ 
+ 			void InfoLoaded (GLib.Object o, GLib.AsyncResult res)
+@@ -132,13 +162,14 @@
+ 					if (FSpot.ImageFile.HasLoader (i))
+ 						items.Add (new FileBrowsableItem (i));
+ 				}
+-				Gtk.Application.Invoke (items, System.EventArgs.Empty, delegate (object sender, EventArgs args) {
++				Gtk.Application.Invoke (items, System.EventArgs.Empty, delegate(object sender, EventArgs args) {
+ 					collection.Add (items.ToArray ());
++					collection.LoaderComplete ();
+ 				});
+ 			}
+ 		}
+ 
+-		protected void LoadItems (System.IO.FileInfo [] files)
++		protected void LoadItems (System.IO.FileInfo[] files)
+ 		{
+ 			List<IBrowsableItem> items = new List<IBrowsableItem> ();
+ 			foreach (var f in files) {
+@@ -149,6 +180,7 @@
+ 			}
+ 
+ 			list = items;
++			LoaderComplete ();
+ 			this.Reload ();
+ 		}
+ 	}
diff --git a/debian/patches/ubuntu_add_save_and_undo.patch b/debian/patches/ubuntu_add_save_and_undo.patch
new file mode 100644
index 0000000..9421c94
--- /dev/null
+++ b/debian/patches/ubuntu_add_save_and_undo.patch
@@ -0,0 +1,921 @@
+Description: Add Save & Undo to editing in View mode
+Author: Christopher Halse Rogers <christopher.halse.rogers at canonical.com>
+Forwarded: No
+
+=== modified file 'src/Editors/Editor.cs'
+Index: f-spot.git/src/Editors/Editor.cs
+===================================================================
+--- f-spot.git.orig/src/Editors/Editor.cs	2010-05-19 14:28:58.077959584 +0100
++++ f-spot.git/src/Editors/Editor.cs	2010-05-19 14:28:58.117959302 +0100
+@@ -18,7 +18,6 @@
+ using Mono.Unix;
+ 
+ using System;
+-using System.IO;
+ 
+ namespace FSpot.Editors {
+ 	[ExtensionNode ("Editor")]
+@@ -50,6 +49,9 @@
+ 		public bool InBrowseMode {
+ 			get { return PhotoImageView == null; }
+ 		}
++		
++		// Undo history
++		public UndoCache History;
+ 	}
+ 
+ 	// This is the base class from which all editors inherit.
+@@ -100,11 +102,31 @@
+ 		}
+ 
+ 
+-		protected void LoadPhoto (IBrowsableItem photo, out Pixbuf photo_pixbuf, out Cms.Profile photo_profile) {
+-			// FIXME: We might get this value from the PhotoImageView.
+-			using (ImageFile img = ImageFile.Create (photo.DefaultVersionUri)) {
+-				photo_pixbuf = img.Load ();
+-				photo_profile = img.GetProfile ();
++		protected void LoadPhoto (IBrowsableItem photo, out Pixbuf photo_pixbuf, out Cms.Profile photo_profile)
++		{
++			if (State.History != null) {
++				if (State.History.ContainsImageFor (photo.DefaultVersionUri)) {
++					photo_pixbuf = State.History.Pixbuf (photo.DefaultVersionUri);
++					photo_profile = State.History.Profile (photo.DefaultVersionUri);
++					return;
++				}
++				// FIXME: We might get this value from the PhotoImageView.
++				PixbufOrientation orientation;
++				using (ImageFile img = ImageFile.Create (photo.DefaultVersionUri)) {
++					photo_pixbuf = img.Load ();
++					orientation = img.Orientation;
++					var temp = FSpot.Utils.PixbufUtils.TransformOrientation (photo_pixbuf, FSpot.Utils.PixbufUtils.ReverseTransformation (orientation));
++					photo_pixbuf.Dispose ();
++					photo_pixbuf = temp;
++					photo_profile = img.GetProfile ();
++				}
++				State.History.InitialiseImage (photo.DefaultVersionUri, photo_pixbuf, orientation, photo_profile);
++			} else {
++				// FIXME: We might get this value from the PhotoImageView.
++				using (ImageFile img = ImageFile.Create (photo.DefaultVersionUri)) {
++					photo_pixbuf = img.Load ();
++					photo_profile = img.GetProfile ();
++				}
+ 			}
+ 		}
+ 
+@@ -141,7 +163,8 @@
+ 			}
+ 		}
+ 
+-		private void TryApply () {
++		private void TryApply ()
++		{
+ 			if (NeedsSelection && !State.HasSelection) {
+ 				throw new Exception ("Cannot apply without selection!");
+ 			}
+@@ -152,9 +175,27 @@
+ 				Cms.Profile input_profile;
+ 				LoadPhoto (item, out input, out input_profile);
+ 
++				if (State.History != null) {
++					// If we're loading from history, then we've got the pixbuf in the original orientation
++					// and are transforming it on display.  We need to transform it before editing.
++					var transformed_input = Utils.PixbufUtils.TransformOrientation (input, State.History.Orientation (item.DefaultVersionUri));
++					input.Dispose ();
++					input = transformed_input;
++				}
++				
+ 				Pixbuf edited = Process (input, input_profile);
+ 				input.Dispose ();
+-
++				
++				
++				if (State.History != null) {
++					// And now we need to reverse the transformation, so that the pixbuf remains in the
++					// original orientation.
++					var transformed_edited = Utils.PixbufUtils.TransformOrientation (edited, 
++						Utils.PixbufUtils.ReverseTransformation (State.History.Orientation (item.DefaultVersionUri)));
++					edited.Dispose ();
++					edited = transformed_edited;
++				}
++				
+ 				if (item is Photo) {
+ 					var photo = item as Photo;
+ 					bool create_version = photo.DefaultVersion.IsProtected;
+@@ -162,13 +203,10 @@
+ 					photo.Changes.DataChanged = true;
+ 					App.Instance.Database.Photos.Commit (photo);
+ 				} else {
+-					var pb = edited.Copy ();
+-					using (ImageFile img = ImageFile.Create (item.DefaultVersionUri)) {
+-						using (Stream stream = System.IO.File.OpenWrite (item.DefaultVersionUri.LocalPath)) {
+-							img.Save (edited, stream);
+-						}
+-					}
+-					State.PhotoImageView.Pixbuf = pb; 
++					State.History.PushEdit (item.DefaultVersionUri, edited);
++					Pixbuf prev = State.PhotoImageView.Pixbuf;
++					State.PhotoImageView.Pixbuf = edited;
++					prev.Dispose ();
+ 				}
+ 
+ 				done++;
+Index: f-spot.git/src/Makefile.am
+===================================================================
+--- f-spot.git.orig/src/Makefile.am	2010-05-19 14:27:22.516709468 +0100
++++ f-spot.git/src/Makefile.am	2010-05-19 14:28:58.127960001 +0100
+@@ -59,6 +59,7 @@
+ 	$(srcdir)/Utils/Log.cs			\
+ 	$(srcdir)/Utils/PixbufOrientation.cs	\
+ 	$(srcdir)/Utils/PixbufUtils.cs		\
++	$(srcdir)/Utils/UndoCache.cs		\
+ 	$(srcdir)/Utils/Unix.cs			\
+ 	$(srcdir)/Utils/UriExtensions.cs			\
+ 	$(srcdir)/Utils/UriUtils.cs
+@@ -343,6 +344,7 @@
+ 	-pkg:gnome-sharp-2.0			\
+ 	-r:Mono.Posix				\
+ 	-r:Mono.Cairo				\
++    -r:Cms.dll                  \
+ 	$(GCONF_PKG)
+ 
+ JOBSCHEDULER_ASSEMBLIES =			\
+@@ -502,7 +504,7 @@
+ 
+ FSpot.Utils.dll.mdb: FSpot.Utils.dll
+ 
+-FSpot.Utils.dll: $(UTILS_CSFILES)
++FSpot.Utils.dll: $(UTILS_CSFILES) Cms.dll
+ 	@echo -e "\n*** Compiling $@"
+ 	$(CSC_LIB) -out:$@ $(EXTRAFLAGS) $(UTILS_CSFILES) $(UTILS_ASSEMBLIES)
+ 
+Index: f-spot.git/src/PhotoImageView.cs
+===================================================================
+--- f-spot.git.orig/src/PhotoImageView.cs	2010-05-19 14:27:22.536710442 +0100
++++ f-spot.git/src/PhotoImageView.cs	2010-05-19 14:30:51.287958810 +0100
+@@ -20,9 +20,14 @@
+ namespace FSpot.Widgets {
+ 	public class PhotoImageView : ImageView {
+ #region public API
+-		public PhotoImageView (IBrowsableCollection query) : this (new BrowsablePointer (query, -1))
++		public PhotoImageView (IBrowsableCollection query) : this(new BrowsablePointer (query, -1))
+ 		{
+ 		}
++		
++		public PhotoImageView (IBrowsableCollection query, UndoCache history) : this(query)
++		{
++			history_cache = history;
++		}
+ 
+ 		public PhotoImageView (BrowsablePointer item) : base ()
+ 		{
+@@ -166,8 +171,25 @@
+ #region loader		
+ 		uint timer;
+ 		IImageLoader loader;
++		UndoCache history_cache;
+ 		void Load (Uri uri)
+ 		{
++			if (history_cache != null && history_cache.ContainsImageFor (uri)) {
++				// We can get the pixmap faster out of the cache.  Just load up the orientation from the file
++				using (ImageFile img = ImageFile.Create (uri)) {
++					PixbufOrientation = Accelerometer.GetViewOrientation (img.Orientation);
++				}
++				Pixbuf prev = Pixbuf;
++				Pixbuf = history_cache.Pixbuf (uri);
++				if (prev != null) {
++					prev.Dispose ();
++				}
++				ZoomFit ();
++				Gdk.Rectangle area = ImageCoordsToWindow (new Gdk.Rectangle (0, 0, Pixbuf.Width, Pixbuf.Height));
++				QueueDrawArea (area.X, area.Y, area.Width, area.Height);
++				return;
++			}
++			
+ 			timer = Log.DebugTimerStart ();
+ 			if (loader != null)
+ 				loader.Dispose ();
+Index: f-spot.git/src/SingleView.cs
+===================================================================
+--- f-spot.git.orig/src/SingleView.cs	2010-05-19 14:28:58.077959584 +0100
++++ f-spot.git/src/SingleView.cs	2010-05-19 14:30:51.267959290 +0100
+@@ -1,6 +1,8 @@
+ using Gtk;
+ using Gdk;
+ using System;
++using System.IO;
++using System.Linq;
+ using System.Collections.Generic;
+ 
+ using Mono.Addins;
+@@ -38,6 +40,7 @@
+ 		[Glade.Widget] ImageMenuItem rotate_right;
+ 
+ 		ToolButton rr_button, rl_button;
++		ToolButton undo;
+ 
+ 		Sidebar sidebar;
+ 
+@@ -48,19 +51,20 @@
+ 		private Uri uri;
+ 		
+ 		UriCollection collection;
++		UndoCache undo_history;
+ 		
+ 		FullScreenView fsview;
+ 
+ 		private static Gtk.Tooltips toolTips = new Gtk.Tooltips ();
+ 
+-		public SingleView (Uri [] uris) 
++		public SingleView (Uri[] uris)
+ 		{
+ 			string glade_name = "single_view";
+-			this.uri = uris [0];
++			this.uri = uris[0];
+ 			
+ 			xml = new Glade.XML (null, "f-spot.glade", glade_name, "f-spot");
+ 			xml.Autoconnect (this);
+-			window = (Gtk.Window) xml.GetWidget (glade_name);
++			window = (Gtk.Window)xml.GetWidget (glade_name);
+ 		
+ 			LoadPreference (Preferences.VIEWER_WIDTH);
+ 			LoadPreference (Preferences.VIEWER_MAXIMIZED);
+@@ -80,6 +84,17 @@
+ 
+ 			toolbar.Insert (new SeparatorToolItem (), -1);
+ 
++			StockItem undo_info = Stock.Lookup (Stock.Undo);
++			undo = new ToolButton (undo_info.StockId);
++			undo.Label = undo_info.Label;
++			undo.IsImportant = true;
++			undo.UseUnderline = true;
++			undo.Clicked += HandleUndoClicked;
++			toolbar.Insert (undo, -1);
++			
++			toolbar.Insert (new SeparatorToolItem (), -1);
++
++			
+ 			ToolButton fs_button = GtkUtil.ToolButtonFromTheme ("view-fullscreen", Catalog.GetString ("Fullscreen"), true);
+ 			fs_button.Clicked += HandleViewFullscreen;
+ 			fs_button.SetTooltip (toolTips, Catalog.GetString ("View photos fullscreen"), null);
+@@ -91,6 +106,8 @@
+ 			toolbar.Insert (ss_button, -1);
+ 
+ 			collection = new UriCollection (uris);
++			undo_history = new UndoCache ();
++			undo_history.UndoStateChanged += HandleUndoStateChanged;
+ 
+ 			TargetEntry [] dest_table = {
+ 				FSpot.DragDropTargets.UriListEntry,
+@@ -120,7 +137,7 @@
+ 
+ 			ThumbnailGenerator.Default.OnPixbufLoaded += delegate { directory_view.QueueDraw (); };
+ 
+-			image_view = new PhotoImageView (collection);
++			image_view = new PhotoImageView (collection, undo_history);
+ 			GtkUtil.ModifyColors (image_view);
+ 			GtkUtil.ModifyColors (image_scrolled);
+ 			image_view.ZoomChanged += HandleZoomChanged;
+@@ -176,6 +193,23 @@
+ 			export.Activated += HandleExportActivated ;
+ 		}
+ 
++		void HandleUndoStateChanged (object sender, UndoStateChangedArgs e)
++		{
++			if (e.ChangedFor == image_view.Item.Current.DefaultVersionUri) {
++				undo.Sensitive = undo_history.ContainsUndoInformationFor (e.ChangedFor);
++				UpdateTitle ();
++			}
++		}
++
++		void HandleUndoClicked (object sender, EventArgs e)
++		{
++			undo_history.PopEdit (image_view.Item.Current.DefaultVersionUri);
++			Pixbuf prev = image_view.Pixbuf;
++			image_view.Pixbuf = undo_history.Pixbuf (image_view.Item.Current.DefaultVersionUri);
++			prev.Dispose ();
++			undo.Sensitive = undo_history.ContainsUndoInformationFor (image_view.Item.Current.DefaultVersionUri);
++		}
++
+ 		private void OnSidebarExtensionChanged (object s, ExtensionNodeEventArgs args) {
+ 			// FIXME: No sidebar page removal yet!
+ 			if (args.Change == ExtensionChange.Add) {
+@@ -184,6 +218,11 @@
+ 				page.PhotoImageView = image_view;
+ 				page.ParentWindow = window;
+ 				sidebar.AppendPage (page); 
++				if (page is EditorPage) {
++					// Pass undo history to Editors
++					((EditorPage)page).History = undo_history;
++					sidebar.SwitchTo (page.Label);
++				}
+ 			}
+ 		}
+ 
+@@ -255,23 +294,41 @@
+ 				collection.MarkChanged (image_view.Item.Index, FullInvalidate.Instance);
+ 		}		
+ 
+-		private void HandleSelectionChanged (FSpot.IBrowsableCollection selection) 
++		private void HandleSelectionChanged (FSpot.IBrowsableCollection selection)
+ 		{
+ 			
+ 			if (selection.Count > 0) {
+ 				image_view.Item.Index = ((FSpot.Widgets.IconView.SelectionCollection)selection).Ids[0];
+-
++				
+ 				zoom_scale.Value = image_view.NormalizedZoom;
+ 			}
++			if (selection.Count == 1) {
++				undo.Sensitive = undo_history.ContainsUndoInformationFor (image_view.Item.Current.DefaultVersionUri);
++			} else {
++				undo.Sensitive = false;
++			}
+ 			UpdateStatusLabel ();
+ 		}
+ 
++		void UpdateTitle ()
++		{
++			if (image_view.Item.IsValid) {
++				Window.Title = String.Format ("{0}{1} - F-Spot View",
++					image_view.Item.Current.Name,
++					undo_history.ContainsUndoInformationFor (image_view.Item.Current.DefaultVersionUri) ? "*" : "");
++			} else {
++				Window.Title = "F-Spot View";
++			}
++		}
++
+ 		private void HandleItemChanged (object sender, BrowsablePointerChangedEventArgs old)
+ 		{
+ 			BrowsablePointer pointer = sender as BrowsablePointer;
+ 			if (pointer == null)
+ 				return;
+ 
++			UpdateTitle ();
++
+ 			directory_view.FocusCell = pointer.Index;
+ 			directory_view.Selection.Clear ();
+ 			if (collection.Count > 0) {
+@@ -343,6 +400,115 @@
+ 			Open (FileChooserAction.Open);
+ 		}
+ 
++		void SafeSaveFile (Uri original, Uri saveTo, Pixbuf image, PixbufOrientation orientation)
++		{
++			//Get file permissions... (mkstemp does not keep permissions or ownership)
++			Mono.Unix.Native.Stat stat;
++			int stat_err = Mono.Unix.Native.Syscall.stat (original.LocalPath, out stat);
++			
++			string temp_path = saveTo.LocalPath;
++			using (ImageFile img = ImageFile.Create (original)) {
++				using (Stream stream = FSpot.Utils.Unix.MakeSafeTemp (ref temp_path)) {
++					img.Save (image, stream);
++				}
++				if (img is JpegFile) {
++					((JpegFile)img).SetOrientation (orientation);
++					Log.Debug ("Saving orientation {0}", orientation);
++					((JpegFile)img).SaveMetaData (temp_path);
++				}
++			}
++			File.SetAttributes (temp_path, File.GetAttributes (original.LocalPath));
++			
++			if (FSpot.Utils.Unix.Rename (temp_path, saveTo.LocalPath) < 0) {
++				System.IO.File.Delete (temp_path);
++				throw new System.Exception (System.String.Format ("Unable to rename {0} to {1}",
++						temp_path, saveTo.LocalPath));
++			}
++			
++			//Set file permissions and gid...
++			if (stat_err == 0) {
++				try {
++					Mono.Unix.Native.Syscall.chmod (saveTo.LocalPath, stat.st_mode |
++						Mono.Unix.Native.FilePermissions.S_IRUSR | 
++						Mono.Unix.Native.FilePermissions.S_IWUSR);
++					Mono.Unix.Native.Syscall.chown (saveTo.LocalPath, Mono.Unix.Native.Syscall.getuid (), stat.st_gid);
++				} catch (Exception) {
++				}
++			}
++		}
++		
++		private void HandleSave (object sender, System.EventArgs args)
++		{
++			IBrowsableItem current_item = image_view.Item.Current;
++			if (!undo_history.ChangedUris ().Contains (current_item.DefaultVersionUri)) {
++				Log.Debug ("Not saving unchanged file: {0}", current_item.DefaultVersionUri.AbsoluteUri);
++				return;
++			}
++			Pixbuf current_image = undo_history.Pixbuf (current_item.DefaultVersionUri);
++			PixbufOrientation current_orientation = undo_history.Orientation (current_item.DefaultVersionUri);
++			
++			SafeSaveFile (current_item.DefaultVersionUri, current_item.DefaultVersionUri, current_image, current_orientation);
++			
++			Log.Debug ("Saved changes to {0}", current_item.DefaultVersionUri.LocalPath);
++			undo_history.MarkImageSaved (current_item.DefaultVersionUri);
++			directory_view.UpdateThumbnail (collection.IndexOf (current_item));
++		}
++		
++		private void HandleSaveAs (object sender, System.EventArgs args)
++		{
++			IBrowsableItem current_item = image_view.Item.Current;
++			Pixbuf current_image;
++			PixbufOrientation current_orientation;
++			if (!undo_history.ContainsImageFor (current_item.DefaultVersionUri)) {
++				//TODO: This should probably load from the image file, instead.
++				//TODO: Or possibly just do a filesystem copy?
++				current_image = image_view.Pixbuf;
++				current_orientation = image_view.PixbufOrientation;
++			} else {
++				current_image = undo_history.Pixbuf (current_item.DefaultVersionUri);
++				current_orientation = undo_history.Orientation (current_item.DefaultVersionUri);
++			}
++			FileChooserDialog chooser = new FileChooserDialog (Catalog.GetString ("Save As..."),
++				window,
++				FileChooserAction.Save);
++			
++			chooser.AddButton (Stock.Cancel, ResponseType.Cancel);
++			chooser.AddButton (Stock.Save, ResponseType.Ok);
++			
++			// TODO: In an ideal world, this would be using GIO and we wouldn't have the "local only" constraint.
++			chooser.LocalOnly = true;
++			chooser.DoOverwriteConfirmation = true;
++			
++			chooser.SelectUri (current_item.DefaultVersionUri.AbsoluteUri);
++			int response = chooser.Run ();
++			
++			if ((ResponseType)response == ResponseType.Ok) {
++				Uri saveTo = new Uri (chooser.Uri);
++				SafeSaveFile (current_item.DefaultVersionUri, saveTo, current_image, current_orientation);
++				
++				Log.Debug ("Saved changes to {0} to {1}", current_item.DefaultVersionUri.AbsoluteUri, chooser.Filename);
++				
++				if (current_item.DefaultVersionUri != saveTo) {
++					// We're saving to a new file name
++					if (undo_history.ContainsUndoInformationFor (current_item.DefaultVersionUri)) {
++						undo_history.Clear (current_item.DefaultVersionUri);
++					}
++					
++					Uri new_item_uri = new Uri (chooser.Uri);
++					collection.Add (new_item_uri);
++					// We know that chooser.Uri points to a local file, so collection.Add will be synchronous
++					var new_item = collection.Items.FirstOrDefault ((item) => item.DefaultVersionUri == new_item_uri);
++					directory_view.Selection.Clear ();
++					directory_view.Selection.Add (new_item);
++				} else if (undo_history.ContainsUndoInformationFor (saveTo)) {
++					// We've decided to save to the original file.
++					undo_history.MarkImageSaved (saveTo);
++				}
++			}
++			
++			chooser.Destroy ();
++		}
++		
+ 		private void Open (FileChooserAction action)
+ 		{
+ 			string title = Catalog.GetString ("Open");
+@@ -437,10 +603,78 @@
+ 			popup_menu.Popup (null, null, null, 0, Gtk.Global.CurrentEventTime);
+ 		}
+ 
++
++		bool PresentUnsavedChangesDialog ()
++		{
++			var changed_uris = undo_history.ChangedUris ().ToList ();
++			if (changed_uris.Count == 0) {
++				// We've got no changed files, so we can just allow the quit to proceed.
++				return true;
++			}
++
++			System.Text.StringBuilder changed_filenames = new System.Text.StringBuilder (GLib.Markup.EscapeText (Uri.UnescapeDataString (changed_uris[0].GetFilename ())));
++			for (int i = 1; i < changed_uris.Count; ++i) {
++				changed_filenames.Append ("\n");
++				changed_filenames.Append (GLib.Markup.EscapeText (Uri.UnescapeDataString (changed_uris[i].GetFilename ())));
++			}
++			string message;
++			message = Catalog.GetPluralString ("Save changes to image: {0} before closing?", "Save changes to images:\n{0}\nbefore closing?", changed_uris.Count);
++			var save_dialogue = new HigMessageDialog (Window,
++				DialogFlags.Modal,
++				MessageType.Warning,
++				ButtonsType.None,
++				String.Format (message, changed_filenames),
++				Catalog.GetPluralString ("If you don't save, the changes you have made to this image will be permanently lost",
++					"If you don't save, the changes you have made to these images will be permanently lost",
++					changed_uris.Count));
++			
++			Gtk.Button button = new Gtk.Button ();
++			button.Label = Catalog.GetString ("Discard changes");
++			button.CanDefault = true;
++			button.Show ();
++			save_dialogue.AddActionWidget (button, ResponseType.Close);
++			
++			button = new Gtk.Button ();
++			button.Label = Catalog.GetString ("Continue editing");
++			button.CanDefault = true;
++			button.Show ();
++			save_dialogue.AddActionWidget (button, ResponseType.Cancel);
++			
++			button = new Gtk.Button ();
++			button.Label = Catalog.GetPluralString ("Save", "Save all", changed_uris.Count);
++			button.CanDefault = true;
++			button.Show ();
++			save_dialogue.AddActionWidget (button, ResponseType.Accept);
++			save_dialogue.Default = button;
++			save_dialogue.DefaultResponse = ResponseType.Accept;
++			button.GrabDefault ();
++			
++			ResponseType response = (ResponseType)save_dialogue.Run ();
++			save_dialogue.Destroy ();
++			switch (response) {
++			case ResponseType.Close:
++				return true;
++			case ResponseType.Cancel:
++				return false;
++			case ResponseType.Accept:
++				foreach (var uri in changed_uris) {
++					Pixbuf current_image = undo_history.Pixbuf (uri);
++					PixbufOrientation current_orientation = undo_history.Orientation (uri);
++					SafeSaveFile (uri, uri, current_image, current_orientation);
++					Log.Debug ("Saved changes to {0}", uri.AbsoluteUri);
++				}
++				break;
++			}
++			return true;
++		}
++		
+ 		void HandleDeleteEvent (object sender, DeleteEventArgs args)
+ 		{
+-			SavePreferences ();
+-			this.Window.Destroy ();
++			if (PresentUnsavedChangesDialog ()) {
++				SavePreferences ();
++				undo_history.Dispose ();
++				this.Window.Destroy ();
++			}
+ 			args.RetVal = true;
+ 		}
+ 
+@@ -477,8 +711,11 @@
+ 
+ 		private void HandleFileClose (object sender, System.EventArgs args)
+ 		{
+-			SavePreferences ();
+-			this.Window.Destroy ();
++			if (PresentUnsavedChangesDialog ()) {
++				SavePreferences ();
++				undo_history.Dispose ();
++				this.Window.Destroy ();
++			}
+ 		}
+ 
+ 		private void SavePreferences  ()
+Index: f-spot.git/src/Utils/UndoCache.cs
+===================================================================
+--- /dev/null	1970-01-01 00:00:00.000000000 +0000
++++ f-spot.git/src/Utils/UndoCache.cs	2010-05-19 14:28:58.127960001 +0100
+@@ -0,0 +1,244 @@
++// 
++//  UndoCache.cs
++//  
++//  Author:
++//       Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
++// 
++//  Copyright © 2010 Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
++// 
++//  This program is free software; you can redistribute it and/or modify
++//  it under the terms of the GNU General Public License as published by
++//  the Free Software Foundation; either version 2 of the License, or
++//  (at your option) any later version.
++// 
++//  This program is distributed in the hope that it will be useful,
++//  but WITHOUT ANY WARRANTY; without even the implied warranty of
++//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++//  GNU General Public License for more details.
++//  
++//  You should have received a copy of the GNU General Public License
++//  along with this program; if not, write to the Free Software
++//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++// 
++
++using System;
++using System.Collections.Generic;
++using System.Threading;
++using System.IO;
++
++using Gdk;
++
++using Cms;
++
++namespace FSpot.Utils
++{
++	public class UndoStateChangedArgs : EventArgs
++	{
++		public UndoStateChangedArgs (Uri changedFor)
++		{
++			ChangedFor = changedFor;
++		}
++		
++		public Uri ChangedFor { get; private set; }
++	}
++	
++	public class UndoCache : IDisposable
++	{
++		class CachedPixbuf : IDisposable
++		{
++			Pixbuf mem_cache;
++			FileStream file_cache;
++			ManualResetEvent write_done;
++			
++			public CachedPixbuf (Gdk.Pixbuf image)
++			{
++				// TODO: We can be smarter than always disc caching or always mem caching
++				// Making the cache memory-backed with eviction to disc should be investigated.
++				mem_cache = image.Copy ();
++				
++				write_done = new ManualResetEvent (false);
++				ThreadPool.QueueUserWorkItem ((state) => {
++					uint id = Log.DebugTimerStart ("Saving undo information");
++					// PNG is lossless, and can handle all the bitdepths we're likely to encounter
++					byte[] buffer = mem_cache.SaveToBuffer ("png");
++					
++					file_cache = File.Create (Path.GetTempFileName (), 4096, FileOptions.DeleteOnClose);
++					
++					file_cache.Write (buffer, 0, buffer.Length);
++					Log.DebugTimerPrint (id, "Saving undo information took: {0}");
++					lock (mem_cache) {
++						mem_cache.Dispose ();
++					}
++					write_done.Set ();
++				});
++			}
++			
++			public Pixbuf GetPixbuf ()
++			{
++				lock (mem_cache) {
++					if (mem_cache.Handle != IntPtr.Zero) {
++						// Pixbuf.Dispose sets the handle to IntPtr.Zero
++						return mem_cache.Copy ();
++					}
++				}
++				write_done.WaitOne ();
++				file_cache.Seek (0, SeekOrigin.Begin);
++				return new Pixbuf (file_cache);
++			}
++			
++			public void Dispose ()
++			{
++				if (write_done.WaitOne (0)) {
++					//We've written to disc, which means we've already disposed mem_cache.
++					file_cache.Dispose ();
++				} else {
++					//We're still writing out to disc.  Wait for for it to finish, then dispose.
++					//Do this on the threadpool, as it might take some time.
++					//TODO: Use a cancellable write method.
++					ThreadPool.QueueUserWorkItem ((state) => {
++						write_done.WaitOne ();
++						file_cache.Dispose ();
++					});
++				}
++			}
++		}
++		
++		class PixbufProfilePair
++		{
++			public PixbufProfilePair ()
++			{
++				pixbuf_history = new Stack<CachedPixbuf> ();
++			}
++			public Stack<CachedPixbuf> pixbuf_history;
++			public Profile profile;
++			public PixbufOrientation orientation;
++			public bool on_disc;
++		}
++	
++		Dictionary<Uri, UndoCache.PixbufProfilePair> history;
++		public UndoCache ()
++		{
++			history = new Dictionary<Uri, UndoCache.PixbufProfilePair> ();
++		}
++		
++		public void InitialiseImage (Uri photoUri, Pixbuf baseImage, PixbufOrientation orientation, Profile profile)
++		{
++			if (history.ContainsKey (photoUri)) {
++				throw new ApplicationException (String.Format ("Attempted to initialise the undo cache twice for image {0}", photoUri.AbsolutePath));
++			}
++			history[photoUri] = new PixbufProfilePair ();
++			history[photoUri].profile = profile;
++			history[photoUri].orientation = orientation;
++			history[photoUri].pixbuf_history.Push (new CachedPixbuf (baseImage));
++			history[photoUri].on_disc = true;
++		}
++		
++		public void MarkImageSaved (Uri photoUri)
++		{
++			history[photoUri].on_disc = true;
++			
++			var handler = UndoStateChanged;
++			if (handler != null) {
++				handler (this, new UndoStateChangedArgs (photoUri));
++			}
++		}
++		
++		public void PushEdit (Uri photoUri, Pixbuf image)
++		{
++			if (!history.ContainsKey (photoUri)) {
++				throw new ApplicationException ("Must initialise the undo cache before adding a new edit");
++			}
++			history[photoUri].pixbuf_history.Push (new CachedPixbuf (image));
++			history[photoUri].on_disc = false;
++
++			var handler = UndoStateChanged;
++			if (handler != null) {
++				handler (this, new UndoStateChangedArgs (photoUri));
++			}
++		}
++		
++		public bool ContainsImageFor (Uri uri)
++		{
++			return history.ContainsKey (uri);
++		}
++		
++		public bool ContainsUndoInformationFor (Uri uri)
++		{
++			return ContainsImageFor (uri) && history[uri].pixbuf_history.Count > 1;
++		}
++		
++		public IEnumerable<Uri> ChangedUris ()
++		{
++			foreach (var uri in history.Keys) {
++				if (ContainsUndoInformationFor (uri) && !history[uri].on_disc) {
++					yield return uri;
++				}
++			}
++			yield break;
++		}
++		
++		public Profile Profile (Uri uri)
++		{
++			if (!history.ContainsKey (uri)) {
++				throw new KeyNotFoundException (String.Format ("Undo cache has not yet been initialised for {0}", uri.AbsolutePath));
++			}
++			return history[uri].profile;
++		}
++		
++		public Pixbuf Pixbuf (Uri uri)
++		{
++			if (!history.ContainsKey (uri)) {
++				throw new KeyNotFoundException (String.Format ("Undo cache has not yet been initialised for {0}", uri.AbsolutePath));
++			}
++			return history[uri].pixbuf_history.Peek ().GetPixbuf ();
++		}
++		
++		public PixbufOrientation Orientation (Uri uri)
++		{
++			if (!history.ContainsKey (uri)) {
++				throw new KeyNotFoundException (String.Format ("Undo cache has not yet been initialised for {0}", uri.AbsolutePath));
++			}
++			return history[uri].orientation;
++		}
++		
++		public void PopEdit (Uri photoUri)
++		{
++			if (!ContainsUndoInformationFor (photoUri)) {
++				throw new KeyNotFoundException (String.Format ("No Undo info for image {0}!", photoUri.AbsolutePath));
++			}
++			history[photoUri].pixbuf_history.Pop ().Dispose ();
++			history[photoUri].on_disc = false;
++			
++			var handler = UndoStateChanged;
++			if (handler != null) {
++				handler (this, new UndoStateChangedArgs (photoUri));
++			}
++		}
++		
++		public void Clear (Uri uri)
++		{
++			if (history.ContainsKey (uri)) {
++				foreach (var pixbuf in history[uri].pixbuf_history) {
++					pixbuf.Dispose ();
++				}
++				history.Remove (uri);
++				
++				var handler = UndoStateChanged;
++				if (handler != null) {
++					handler (this, new UndoStateChangedArgs (uri));
++				}
++			}
++		}
++		
++		public void Dispose ()
++		{
++			foreach (var pair in history.Values) {
++				foreach (var pixbuf in pair.pixbuf_history) {
++					pixbuf.Dispose ();
++				}
++			}
++		}
++		
++		public event EventHandler<UndoStateChangedArgs> UndoStateChanged;
++	}
++}
+Index: f-spot.git/src/Widgets/EditorPage.cs
+===================================================================
+--- f-spot.git.orig/src/Widgets/EditorPage.cs	2010-05-19 14:28:58.077959584 +0100
++++ f-spot.git/src/Widgets/EditorPage.cs	2010-05-19 14:30:15.747958503 +0100
+@@ -42,7 +42,12 @@
+ 
+ 		public override Gtk.Window ParentWindow {
+ 			get { return EditorPageWidget.ParentWindow; }
+-			set { EditorPageWidget.ParentWindow = value ;}
++			set { EditorPageWidget.ParentWindow = value; }
++		}
++		
++		public UndoCache History {
++			get { return EditorPageWidget.History; }
++			set { EditorPageWidget.History = value; } 
+ 		}
+ 
+ 		protected override void AddedToSidebar () {
+@@ -52,7 +57,7 @@
+ 
+ 		private void HandleContextChanged (object sender, EventArgs args)
+ 		{
+-            InPhotoView = (Sidebar.Context == ViewContext.Edit) || (Sidebar.Context == ViewContext.Single);
++            InPhotoView = ((Sidebar as Sidebar).Context == ViewContext.Edit) || ((Sidebar as Sidebar).Context == ViewContext.Single);
+ 			EditorPageWidget.ChangeButtonVisibility ();
+ 		}
+ 	}
+@@ -65,7 +70,8 @@
+ 		List<Editor> editors;
+ 		Editor current_editor;
+ 
+-        public PhotoImageView PhotoImageView { get; set;}
++        public PhotoImageView PhotoImageView { get; set; }
++        public UndoCache History { get; set; }
+         public Gtk.Window ParentWindow { get; set; }
+ 
+ 		// Used to make buttons insensitive when selecting multiple images.
+@@ -184,7 +190,8 @@
+ 				Apply (editor); // Instant apply
+ 		}
+ 
+-		private bool SetupEditor (Editor editor) {
++		private bool SetupEditor (Editor editor)
++		{
+ 			EditorState state = editor.CreateState ();
+ 
+ 			PhotoImageView photo_view = PhotoImageView;
+@@ -199,6 +206,8 @@
+ 				return false;
+ 			state.Items = (Page.Sidebar as Sidebar).Selection.Items;
+ 
++            state.History = History;
++
+ 			editor.Initialize (state);
+ 			return true;
+ 		}
+Index: f-spot.git/src/f-spot.glade
+===================================================================
+--- f-spot.git.orig/src/f-spot.glade	2010-05-19 14:28:57.987959630 +0100
++++ f-spot.git/src/f-spot.glade	2010-05-19 14:30:51.307958832 +0100
+@@ -2440,6 +2440,31 @@
+                       </widget>
+                     </child>
+                     <child>
++                      <widget class="GtkImageMenuItem" id="save">
++                        <property name="label">gtk-save</property>
++                        <property name="visible">True</property>
++                        <property name="use_underline">True</property>
++                        <property name="use_stock">True</property>
++                        <signal name="activate" handler="HandleSave"/>
++                        <accelerator key="N" signal="activate" modifiers="GDK_CONTROL_MASK"/>
++                      </widget>
++                    </child>
++                    <child>
++                      <widget class="GtkImageMenuItem" id="save_as">
++                        <property name="label">gtk-save-as</property>
++                        <property name="visible">True</property>
++                        <property name="use_underline">True</property>
++                        <property name="use_stock">True</property>
++                        <signal name="activate" handler="HandleSaveAs"/>
++                        <accelerator key="N" signal="activate" modifiers="GDK_CONTROL_MASK"/>
++                      </widget>
++                    </child>
++                    <child>
++                      <widget class="GtkSeparatorMenuItem" id="separator1">
++                        <property name="visible">True</property>
++                      </widget>
++                    </child>
++                    <child>
+                       <widget class="GtkMenuItem" id="export">
+                         <property name="visible">True</property>
+                         <property name="label" translatable="yes">_Export</property>
+Index: f-spot.git/src/UI.Dialog/HigMessageDialog.cs
+===================================================================
+--- f-spot.git.orig/src/UI.Dialog/HigMessageDialog.cs	2010-05-19 14:27:22.496709303 +0100
++++ f-spot.git/src/UI.Dialog/HigMessageDialog.cs	2010-05-19 14:28:58.127960001 +0100
+@@ -77,6 +77,7 @@
+ 			label.Justify = Gtk.Justification.Left;
+ 			label.LineWrap = true;
+ 			label.SetAlignment (0.0f, 0.5f);
++			label.UseUnderline = false;
+ 			label.Show ();
+ 			label_vbox.PackStart (label, false, false, 0);
+ 	
diff --git a/debian/patches/ubuntu_clear_selection_on_crop.patch b/debian/patches/ubuntu_clear_selection_on_crop.patch
new file mode 100644
index 0000000..0549cda
--- /dev/null
+++ b/debian/patches/ubuntu_clear_selection_on_crop.patch
@@ -0,0 +1,30 @@
+Description: Clear the selection after crop
+Author: Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
+Bug-LP: https://bugs.edge.launchpad.net/ubuntu/+source/f-spot/+bug/535186
+Forwarded: No
+
+------------------------------------------------------------
+revno: 99
+committer: Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
+branch nick: edit-fixes
+timestamp: Thu 2010-03-11 15:08:26 +1100
+message:
+  Clear selection after crop
+modified:
+  src/Editors/CropEditor.cs
+diff:
+=== modified file 'src/Editors/CropEditor.cs'
+Index: f-spot-0.6.1.5/src/Editors/CropEditor.cs
+===================================================================
+--- f-spot-0.6.1.5.orig/src/Editors/CropEditor.cs	2010-03-18 10:17:40.334269406 +1100
++++ f-spot-0.6.1.5/src/Editors/CropEditor.cs	2010-03-18 10:24:25.564260496 +1100
+@@ -178,7 +178,8 @@
+ 						 selection.Width, selection.Height);
+ 
+ 			input.CopyArea (selection.X, selection.Y,
+-					selection.Width, selection.Height, edited, 0, 0);
++				selection.Width, selection.Height, edited, 0, 0);
++			State.PhotoImageView.Selection = Rectangle.Zero;
+ 			return edited;
+ 		}
+ 	}
diff --git a/debian/patches/ubuntu_default_view_size.patch b/debian/patches/ubuntu_default_view_size.patch
new file mode 100644
index 0000000..25cd672
--- /dev/null
+++ b/debian/patches/ubuntu_default_view_size.patch
@@ -0,0 +1,27 @@
+------------------------------------------------------------
+revno: 112
+committer: Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
+branch nick: edit-fixes
+timestamp: Wed 2010-03-31 17:23:34 +1100
+message:
+  Set a more reasonable default size for View mode
+modified:
+  src/Preferences.cs
+diff:
+=== modified file 'src/Preferences.cs'
+--- a/src/Preferences.cs	2009-09-01 22:12:30 +0000
++++ b/src/Preferences.cs	2010-03-31 06:23:34 +0000
+@@ -110,7 +110,12 @@
+ 			case IMPORT_WINDOW_PANE_POSITION:
+ 			case FILMSTRIP_ORIENTATION:
+ 				return 0;
+-					
++
++			case VIEWER_WIDTH:
++				return 700;
++			case VIEWER_HEIGHT:
++				return 550;
++
+ 			case METADATA_EMBED_IN_IMAGE:
+ 			case MAIN_WINDOW_MAXIMIZED:
+ 			case GROUP_ADAPTOR_ORDER_ASC:
diff --git a/debian/patches/ubuntu_fix_disposed_pixbuf_errors_in_adjustment.patch b/debian/patches/ubuntu_fix_disposed_pixbuf_errors_in_adjustment.patch
new file mode 100644
index 0000000..0445826
--- /dev/null
+++ b/debian/patches/ubuntu_fix_disposed_pixbuf_errors_in_adjustment.patch
@@ -0,0 +1,36 @@
+Description: Fix disposed pixmap bug in Adjust Colours tool
+ The code paths in Adjustment.Adjust () for the alpha- and non- alpha channel
+ cases have an important difference - the alpha channel codepath modifies
+ Input and returns it, the non-alpha channel path returns a modified copy of 
+ Input.
+ .
+ In the alpha case, returning Input eventually results in the preview pixmap
+ being disposed, causing debug spew and (in View mode) the window to display
+ garbage.
+Author: Christopher Halse Rogers <christopher.halse.rogers at canonical.com>
+Bug: https://bugzilla.gnome.org/show_bug.cgi?id=611773
+
+=== modified file 'src/ColorAdjustment/Adjustment.cs'
+--- a/src/ColorAdjustment/Adjustment.cs	2008-10-06 23:11:41 +0000
++++ b/src/ColorAdjustment/Adjustment.cs	2010-03-04 04:24:09 +0000
+@@ -64,16 +64,17 @@
+ 			Cms.Profile [] list = GenerateAdjustments ().ToArray ();
+ 			
+ 			if (Input.HasAlpha) {
++				Gdk.Pixbuf input_copy = (Gdk.Pixbuf)Input.Clone ();
+ 				Pixbuf alpha = PixbufUtils.Flatten (Input);
+ 				Transform transform = new Transform (list,
+ 								     PixbufUtils.PixbufCmsFormat (alpha),
+ 								     PixbufUtils.PixbufCmsFormat (final),
+ 								     intent, 0x0000);
+ 				PixbufUtils.ColorAdjust (alpha, final, transform);
+-				PixbufUtils.ReplaceColor (final, Input);
++				PixbufUtils.ReplaceColor (final, input_copy);
+ 				alpha.Dispose ();
+ 				final.Dispose ();
+-				final = Input;
++				final = input_copy;
+ 			} else {
+ 				Cms.Transform transform = new Cms.Transform (list,
+ 									     PixbufUtils.PixbufCmsFormat (Input),
+
diff --git a/debian/patches/ubuntu_fix_folder_export_hang.patch b/debian/patches/ubuntu_fix_folder_export_hang.patch
new file mode 100644
index 0000000..fe0c091
--- /dev/null
+++ b/debian/patches/ubuntu_fix_folder_export_hang.patch
@@ -0,0 +1,18 @@
+Description: Fix hang when pressing the “export” button in the folder exporter
+Author: Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
+Bug-LP: https://bugs.edge.launchpad.net/ubuntu/+source/f-spot/+bug/533435
+Forwarded: No
+
+Index: f-spot.git/extensions/Exporters/FolderExport/FolderExport.cs
+===================================================================
+--- f-spot.git.orig/extensions/Exporters/FolderExport/FolderExport.cs	2010-05-19 13:49:44.416709675 +0100
++++ f-spot.git/extensions/Exporters/FolderExport/FolderExport.cs	2010-05-19 14:28:58.257959751 +0100
+@@ -186,7 +186,7 @@
+ 			Gnome.Vfs.Result result = Gnome.Vfs.Result.Ok;
+ 
+ 			try {
+-				Dialog.Hide ();
++				Gtk.Application.Invoke (delegate {Dialog.Hide ();});
+ 
+ 				Gnome.Vfs.Uri source = new Gnome.Vfs.Uri (Path.Combine (gallery_path, gallery_name));
+ 				Gnome.Vfs.Uri target = dest.Clone();
diff --git a/debian/patches/ubuntu_handle_broken_uris_to_view_mode.patch b/debian/patches/ubuntu_handle_broken_uris_to_view_mode.patch
new file mode 100644
index 0000000..b0edfe5
--- /dev/null
+++ b/debian/patches/ubuntu_handle_broken_uris_to_view_mode.patch
@@ -0,0 +1,38 @@
+------------------------------------------------------------
+revno: 109
+committer: Christopher James Halse Rogers <christopher.halse.rogers at canonical.com>
+branch nick: edit-fixes
+timestamp: Thu 2010-03-18 10:13:37 +1100
+message:
+  Catch exceptions in creating the URI list
+modified:
+  src/Util.cs
+diff:
+=== modified file 'src/Util.cs'
+Index: f-spot-0.6.1.5/src/Util.cs
+===================================================================
+--- f-spot-0.6.1.5.orig/src/Util.cs	2010-04-01 13:13:49.305538478 +1100
++++ f-spot-0.6.1.5/src/Util.cs	2010-04-01 13:13:55.915539697 +1100
+@@ -84,12 +84,16 @@
+ 		{
+ 			Uri uri;
+ 			
+-			if (File.Exists (unknown) || Directory.Exists (unknown))
+-				uri = UriUtils.PathToFileUri (unknown);
+-			else 
+-				uri = new Uri (unknown);
+-			
+-			Add (uri);
++			try {
++				if (File.Exists (unknown) || Directory.Exists (unknown))
++					uri = UriUtils.PathToFileUri (unknown);
++				else
++					uri = new Uri (unknown);
++				
++				Add (uri);
++			} catch (Exception e) {
++				Log.Exception ("Error handling file or directory to view", e);
++			}
+ 		}
+ 	
+ 		public UriList (string data) 
diff --git a/debian/patches/ubuntu_only_update_timestamp_of_unprotected_versions.patch b/debian/patches/ubuntu_only_update_timestamp_of_unprotected_versions.patch
new file mode 100644
index 0000000..4acdfb4
--- /dev/null
+++ b/debian/patches/ubuntu_only_update_timestamp_of_unprotected_versions.patch
@@ -0,0 +1,17 @@
+Index: f-spot.git/src/Jobs/SyncMetadataJob.cs
+===================================================================
+--- f-spot.git.orig/src/Jobs/SyncMetadataJob.cs	2010-05-18 17:30:51.000000000 +0100
++++ f-spot.git/src/Jobs/SyncMetadataJob.cs	2010-05-19 12:07:32.506709827 +0100
+@@ -53,7 +53,11 @@
+ 					FSpot.JpegFile jimg = img as FSpot.JpegFile;
+ 				
+ 					jimg.SetDescription (photo.Description);
+-					jimg.SetDateTimeOriginal (photo.Time);
++					if (!photo.DefaultVersion.IsProtected) {
++						// If the version is protected we should be able to assume that the data hasn't changed.
++						// Thus, the DateTime shouldn't change, either.
++						jimg.SetDateTimeOriginal (photo.Time);
++					}
+ 					jimg.SetXmp (UpdateXmp (photo, jimg.Header.GetXmp ()));
+ 	
+ 					jimg.SaveMetaData (path);
diff --git a/debian/patches/ubuntu_slider_animation_tweaking.patch b/debian/patches/ubuntu_slider_animation_tweaking.patch
new file mode 100644
index 0000000..a8ef22b
--- /dev/null
+++ b/debian/patches/ubuntu_slider_animation_tweaking.patch
@@ -0,0 +1,28 @@
+# Description: tweak the slider animation value
+# Ubuntu: https://bugs.launchpad.net/hundredpapercuts/+bug/513004
+# Upstream: https://bugzilla.gnome.org/show_bug.cgi?id=608443
+From 77800058c950021f2c26bd07a1aef149fb42d753 Mon Sep 17 00:00:00 2001
+From: Alex Launi <alex at jolicloud.org>
+Date: Sat, 20 Feb 2010 23:48:51 -0500
+Subject: [PATCH] Decreases the animation time for the filmstrip slider from 4 seconds to 1.5
+
+---
+ src/Widgets/Filmstrip.cs |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/src/Widgets/Filmstrip.cs b/src/Widgets/Filmstrip.cs
+index fbd6745..d6a47bd 100644
+--- a/src/Widgets/Filmstrip.cs
++++ b/src/Widgets/Filmstrip.cs
+@@ -308,7 +308,7 @@ namespace FSpot.Widgets
+ 			thumb_cache = new DisposableCache<Uri, Pixbuf> (30);
+ 			ThumbnailGenerator.Default.OnPixbufLoaded += HandlePixbufLoaded;
+ 
+-			animation = new DoubleAnimation (0, 0, TimeSpan.FromSeconds (4), SetPositionCore, new CubicEase (EasingMode.EaseOut));
++			animation = new DoubleAnimation (0, 0, TimeSpan.FromSeconds (1.5), SetPositionCore, new CubicEase (EasingMode.EaseOut));
+ 		}
+ 	
+ 		int min_length = 400;
+-- 
+1.6.3.3
+

-- 
f-spot



More information about the Pkg-cli-apps-commits mailing list