[tomboy] Allow user to decide if links are updated when renaming a note (bug #574485)



commit 179c8cd200754f77575f0aab5e476cdcdb980d54
Author: Sandy Armstrong <sanfordarmstrong gmail com>
Date:   Sat Feb 6 22:47:45 2010 -0800

    Allow user to decide if links are updated when renaming a note (bug #574485)
    
    By default, when renaming a note to which other notes have links, display
    a dialog explaining the situation, showing a list of affected notes, and
    letting the user pick which notes (if any) will be updated.  Notes that
    are not updated will have the link removed (not just broken).
    
    The dialog also allows the user to set a preference to always show the
    dialog, or always update link text to reference the new note name, or
    always leave the text alone and remove the link. This preference has
    also been added to the Editing tab of the Prefs dialog.

 Tomboy.mdp                  |    1 +
 Tomboy/Makefile.am          |    1 +
 Tomboy/Note.cs              |  107 +++++++++++++++++++++--
 Tomboy/NoteRenameDialog.cs  |  205 +++++++++++++++++++++++++++++++++++++++++++
 Tomboy/Preferences.cs       |    5 +
 Tomboy/PreferencesDialog.cs |   49 +++++++++--
 Tomboy/Watchers.cs          |   26 +-----
 data/tomboy.schemas.in      |   20 ++++
 8 files changed, 375 insertions(+), 39 deletions(-)
---
diff --git a/Tomboy.mdp b/Tomboy.mdp
index 8ab265b..f628354 100644
--- a/Tomboy.mdp
+++ b/Tomboy.mdp
@@ -150,6 +150,7 @@
     <File name="Tomboy/Addins/ExportToHtml/ExportToHtml.xsl" subtype="Code" buildaction="Nothing" />
     <File name="Tomboy/Addins/NoteDirectoryWatcher/NoteDirectoryWatcher.addin.xml" subtype="Code" buildaction="Nothing" />
     <File name="Tomboy/Addins/NoteDirectoryWatcher/NoteDirectoryWatcherApplicationAddin.cs" subtype="Code" buildaction="Compile" />
+    <File name="Tomboy/NoteRenameDialog.cs" subtype="Code" buildaction="Compile" />
     <File name="Tomboy/Addins/WebSyncService" subtype="Directory" buildaction="Compile" />
     <File name="Tomboy/Addins/WebSyncService/WebSyncServiceAddin.cs" subtype="Code" buildaction="Compile" />
     <File name="Tomboy/Addins/WebSyncService/WebSyncServer.cs" subtype="Code" buildaction="Compile" />
diff --git a/Tomboy/Makefile.am b/Tomboy/Makefile.am
index ff19c6c..d6bdefc 100644
--- a/Tomboy/Makefile.am
+++ b/Tomboy/Makefile.am
@@ -106,6 +106,7 @@ CSFILES = 					\
 	$(srcdir)/NoteManager.cs 		\
 	$(srcdir)/NoteWindow.cs 		\
 	$(srcdir)/NoteBuffer.cs 		\
+	$(srcdir)/NoteRenameDialog.cs 		\
 	$(srcdir)/NoteTag.cs 			\
 	$(srcdir)/PlatformFactory.cs		\
 	$(srcdir)/Preferences.cs		\
diff --git a/Tomboy/Note.cs b/Tomboy/Note.cs
index e936c2f..ef7c7cb 100644
--- a/Tomboy/Note.cs
+++ b/Tomboy/Note.cs
@@ -732,17 +732,108 @@ namespace Tomboy
 				return data.Data.Title;
 			}
 			set {
-				if (data.Data.Title != value) {
-					if (window != null)
-						window.Title = value;
+				SetTitle (value, false);
+			}
+		}
+
+		public void SetTitle (string new_title, bool from_user_action)
+		{
+			if (data.Data.Title != new_title) {
+				if (window != null)
+					window.Title = new_title;
+
+				string old_title = data.Data.Title;
+				data.Data.Title = new_title;
+
+				if (from_user_action)
+					ProcessRenameLinkUpdate (old_title);
+
+				if (Renamed != null)
+					Renamed (this, old_title);
 
-					string old_title = data.Data.Title;
-					data.Data.Title = value;
+				QueueSave (ChangeType.ContentChanged); // TODO: Right place for this?
+			}
+		}
 
-					if (Renamed != null)
-						Renamed (this, old_title);
+		private void ProcessRenameLinkUpdate (string old_title)
+		{
+			List<Note> linkingNotes = new List<Note> ();
+			foreach (Note note in manager.Notes) {
+				// Technically, containing text does not imply linking,
+				// but this is less work
+				if (note != this && note.ContainsText (old_title))
+					linkingNotes.Add (note);
+			}
 
-					QueueSave (ChangeType.ContentChanged); // TODO: Right place for this?
+			if (linkingNotes.Count > 0) {
+				NoteRenameBehavior behavior = (NoteRenameBehavior)
+					Preferences.Get (Preferences.NOTE_RENAME_BEHAVIOR);
+				if (behavior == NoteRenameBehavior.AlwaysShowDialog) {
+					var dlg = new NoteRenameDialog (linkingNotes, old_title, this);
+					Gtk.ResponseType response = (Gtk.ResponseType) dlg.Run ();
+					if (response != Gtk.ResponseType.Cancel &&
+					    dlg.SelectedBehavior != NoteRenameBehavior.AlwaysShowDialog)
+						Preferences.Set (Preferences.NOTE_RENAME_BEHAVIOR, (int) dlg.SelectedBehavior);
+					foreach (var pair in dlg.Notes) {
+						if (pair.Value && response == Gtk.ResponseType.Yes) // Rename
+							pair.Key.RenameLinks (old_title, this);
+						else
+							pair.Key.RemoveLinks (old_title, this);
+					}
+					dlg.Destroy ();
+				} else if (behavior == NoteRenameBehavior.AlwaysRemoveLinks)
+					foreach (var note in linkingNotes)
+						note.RemoveLinks (old_title, this);
+				else if (behavior == NoteRenameBehavior.AlwaysRenameLinks)
+					foreach (var note in linkingNotes)
+						note.RenameLinks (old_title, this);
+			}
+		}
+
+		private bool ContainsText (string text)
+		{
+			return TextContent.IndexOf (text, StringComparison.InvariantCultureIgnoreCase) > -1;
+		}
+
+		private void RenameLinks (string old_title, Note renamed)
+		{
+			HandleLinkRename (old_title, renamed, true);
+		}
+
+		private void RemoveLinks (string old_title, Note renamed)
+		{
+			HandleLinkRename (old_title, renamed, false);
+		}
+
+		private void HandleLinkRename (string old_title, Note renamed, bool rename_links)
+		{
+			// Check again, things may have changed
+			if (!ContainsText (old_title))
+				return;
+
+			string old_title_lower = old_title.ToLower ();
+
+			NoteTag link_tag = TagTable.LinkTag;
+
+			// Replace existing links with the new title.
+			TextTagEnumerator enumerator = new TextTagEnumerator (Buffer, link_tag);
+			foreach (TextRange range in enumerator) {
+				if (range.Text.ToLower () != old_title_lower)
+					continue;
+
+				if (!rename_links) {
+					Logger.Debug ("Removing link tag from text '{0}'",
+					              range.Text);
+					Buffer.RemoveTag (link_tag, range.Start, range.End);
+				} else {
+					Logger.Debug ("Replacing '{0}' with '{1}'",
+					              range.Text,
+					              renamed.Title);
+					Gtk.TextIter start_iter = range.Start;
+					Gtk.TextIter end_iter = range.End;
+					Buffer.Delete (ref start_iter, ref end_iter);
+					start_iter = range.Start;
+					Buffer.InsertWithTags (ref start_iter, renamed.Title, link_tag);
 				}
 			}
 		}
diff --git a/Tomboy/NoteRenameDialog.cs b/Tomboy/NoteRenameDialog.cs
new file mode 100644
index 0000000..cf97e63
--- /dev/null
+++ b/Tomboy/NoteRenameDialog.cs
@@ -0,0 +1,205 @@
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//  
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//  
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+// 
+// Copyright (c) 2010 Sandy Armstrong <sanfordarmstrong gmail com>
+// 
+// Authors:
+//      Sandy Armstrong <sanfordarmstrong gmail com>
+//
+
+using System;
+using System.Collections.Generic;
+using Mono.Unix;
+
+using Gtk;
+
+namespace Tomboy
+{
+	public enum NoteRenameBehavior
+	{
+		AlwaysShowDialog = 0,
+		AlwaysRemoveLinks = 1,
+		AlwaysRenameLinks = 2
+	}
+
+	public class NoteRenameDialog : Gtk.Dialog
+	{
+		private IList<Note> notes;
+		private TreeStore notesModel;
+		private RadioButton alwaysShowDlgRadio;
+		private RadioButton neverRenameRadio;
+		private RadioButton alwaysRenameRadio;
+
+		public NoteRenameDialog (IList<Note> notes, string oldTitle, Note renamedNote) :
+			base (Catalog.GetString ("Rename Note Links?"), renamedNote.Window, DialogFlags.NoSeparator)
+		{
+			this.DefaultResponse = ResponseType.Cancel;
+			this.BorderWidth = 10;
+
+			var renameButton = (Button)
+				AddButton (Catalog.GetString ("_Rename Links"),
+				           ResponseType.Yes);
+			var dontRenameButton = (Button)
+				AddButton (Catalog.GetString ("_Don't Rename Links"),
+				           ResponseType.No);
+
+			this.notes = notes;
+			notesModel = new Gtk.TreeStore (typeof (bool), typeof (string), typeof (Note));
+			foreach (var note in notes)
+				notesModel.AppendValues (true, note.Title, note);
+
+			var labelText = Catalog.GetString ("Rename links in other notes from \"<span underline=\"single\">{0}</span>\" " +
+			                                   "to \"<span underline=\"single\">{1}</span>\"?\n\n" +
+			                                   "If you do not rename the links, " +
+			                                   "they will no longer link to anything.");
+			var label = new Label ();
+			label.UseMarkup = true;
+			label.Markup = String.Format (labelText, oldTitle, renamedNote.Title);
+			label.LineWrap = true;
+			VBox.PackStart (label, false, true, 5);
+
+			var notesView = new TreeView (notesModel);
+			notesView.SetSizeRequest (-1, 200);
+			var toggleCell = new CellRendererToggle ();
+			toggleCell.Activatable = true;
+			var column = new TreeViewColumn (Catalog.GetString ("Rename Links"),
+			                                 toggleCell, "active", 0);
+			column.SortColumnId = 0;
+			column.Resizable = true;
+			notesView.AppendColumn (column);
+			toggleCell.Toggled += (o, args) => {
+				TreeIter iter;
+				if (!notesModel.GetIterFromString (out iter, args.Path))
+					return;
+				bool val = (bool) notesModel.GetValue (iter, 0);
+				notesModel.SetValue (iter, 0, !val);
+			};
+			column = new TreeViewColumn (Catalog.GetString ("Note Title"),
+			                             new CellRendererText (), "text", 1);
+			column.SortColumnId = 1;
+			column.Resizable = true;
+			notesView.AppendColumn (column);
+
+			notesView.RowActivated += (o, args) => {
+				TreeIter iter;
+				if (!notesModel.GetIter (out iter, args.Path))
+					return;
+				Note note = (Note) notesModel.GetValue (iter, 2);
+				if (note != null) {
+					note.Window.Present ();
+					NoteFindBar find = note.Window.Find;
+					find.ShowAll ();
+					find.Visible = true;
+					find.SearchText = "\"" + oldTitle + "\"";
+				}
+			};
+
+			var notesBox = new VBox (false, 5);
+			var selectAllButton = new Button ();
+			selectAllButton.Label = Catalog.GetString ("Select All");
+			selectAllButton.Clicked += (o, e) => {
+				notesModel.Foreach ((model, path, iter) => {
+					notesModel.SetValue (iter, 0, true);
+					return false;
+				});
+			};
+			var selectNoneButton = new Button ();
+			selectNoneButton.Label = Catalog.GetString ("Select None");
+			selectNoneButton.Clicked += (o, e) => {
+				notesModel.Foreach ((model, path, iter) => {
+					notesModel.SetValue (iter, 0, false);
+					return false;
+				});
+			};
+			var notesButtonBox = new HButtonBox ();
+			notesButtonBox.Add (selectNoneButton);
+			notesButtonBox.Add (selectAllButton);
+			notesButtonBox.Spacing = 5;
+			notesButtonBox.LayoutStyle = ButtonBoxStyle.End;
+			var notesScroll = new ScrolledWindow ();
+			notesScroll.Add (notesView);
+			notesBox.PackStart (notesScroll);
+			notesBox.PackStart (notesButtonBox, false, true, 0);
+
+			var advancedExpander = new Expander (Catalog.GetString ("Ad_vanced"));
+			var expandBox = new VBox ();
+			expandBox.PackStart (notesBox);
+			alwaysShowDlgRadio = new RadioButton (Catalog.GetString ("Always show this _window"));
+			alwaysShowDlgRadio.Clicked += (o, e) => {
+				selectAllButton.Click ();
+				notesBox.Sensitive = true;
+				renameButton.Sensitive = true;
+				dontRenameButton.Sensitive = true;
+			};
+			neverRenameRadio = new RadioButton (alwaysShowDlgRadio,
+			                                    Catalog.GetString ("Never rename _links"));
+			neverRenameRadio.Clicked += (o, e) => {
+				selectNoneButton.Click ();
+				notesBox.Sensitive = false;
+				renameButton.Sensitive = false;
+				dontRenameButton.Sensitive = true;
+			};
+			alwaysRenameRadio = new RadioButton (alwaysShowDlgRadio,
+			                                     Catalog.GetString ("Alwa_ys rename links"));
+			alwaysRenameRadio.Clicked += (o, e) => {
+				selectAllButton.Click ();
+				notesBox.Sensitive = false;
+				renameButton.Sensitive = true;
+				dontRenameButton.Sensitive = false;
+			};
+			expandBox.PackStart (alwaysShowDlgRadio, false, true, 0);
+			expandBox.PackStart (neverRenameRadio, false, true, 0);
+			expandBox.PackStart (alwaysRenameRadio, false, true, 0);
+			advancedExpander.Add (expandBox);
+			VBox.PackStart (advancedExpander, true, true, 5);
+
+			advancedExpander.Activated += (o, e) =>
+				this.Resizable = advancedExpander.Expanded;
+
+			this.Focus = dontRenameButton;
+			VBox.ShowAll ();
+		}
+
+		public Dictionary<Note, bool> Notes
+		{
+			get {
+				var notes = new Dictionary<Note, bool> ();
+				notesModel.Foreach ((model, path, iter) => {
+					Note note = (Note) notesModel.GetValue (iter, 2);
+					bool rename = (bool) notesModel.GetValue (iter, 0);
+					notes [note] = rename;
+					return false;
+				});
+				return notes;
+			}
+		}
+
+		public NoteRenameBehavior SelectedBehavior
+		{
+			get {
+				if (neverRenameRadio.Active)
+					return NoteRenameBehavior.AlwaysRemoveLinks;
+				else if (alwaysRenameRadio.Active)
+					return NoteRenameBehavior.AlwaysRenameLinks;
+				else
+					return NoteRenameBehavior.AlwaysShowDialog;
+			}
+		}
+	}
+}
diff --git a/Tomboy/Preferences.cs b/Tomboy/Preferences.cs
index bba7738..6268015 100644
--- a/Tomboy/Preferences.cs
+++ b/Tomboy/Preferences.cs
@@ -39,6 +39,8 @@ namespace Tomboy
 		public const string SYNC_SELECTED_SERVICE_ADDIN = "/apps/tomboy/sync/sync_selected_service_addin";
 		public const string SYNC_CONFIGURED_CONFLICT_BEHAVIOR = "/apps/tomboy/sync/sync_conflict_behavior";
 
+		public const string NOTE_RENAME_BEHAVIOR = "/apps/tomboy/note_rename_behavior";
+
 		public const string INSERT_TIMESTAMP_FORMAT = "/apps/tomboy/insert_timestamp/format";
 		
 		public const string SEARCH_WINDOW_X_POS = "/apps/tomboy/search_window_x_pos";
@@ -133,6 +135,9 @@ namespace Tomboy
 			case SYNC_CONFIGURED_CONFLICT_BEHAVIOR:
 				return 0;
 
+			case NOTE_RENAME_BEHAVIOR:
+				return 0;
+
 			case INSERT_TIMESTAMP_FORMAT:
 				return Catalog.GetString ("dddd, MMMM d, h:mm tt");
 			}
diff --git a/Tomboy/PreferencesDialog.cs b/Tomboy/PreferencesDialog.cs
index e2bd3a0..94a8806 100644
--- a/Tomboy/PreferencesDialog.cs
+++ b/Tomboy/PreferencesDialog.cs
@@ -18,6 +18,7 @@ namespace Tomboy
 		Gtk.Widget syncAddinPrefsWidget;
 		Gtk.Button resetSyncAddinButton;
 		Gtk.Button saveSyncAddinButton;
+		Gtk.ComboBox rename_behavior_combo;
 		readonly AddinManager addin_manager;
 		
 		Gtk.Button font_button;
@@ -120,6 +121,21 @@ namespace Tomboy
 
 			AddActionWidget (button, Gtk.ResponseType.Close);
 			DefaultResponse = Gtk.ResponseType.Close;
+
+			Preferences.SettingChanged += HandlePreferencesSettingChanged;
+		}
+
+		void HandlePreferencesSettingChanged (object sender, NotifyEventArgs args)
+		{
+			if (args.Key == Preferences.NOTE_RENAME_BEHAVIOR) {
+				int rename_behavior = (int) args.Value;
+				if (rename_behavior < 0 || rename_behavior > 2) {
+					rename_behavior = 0;
+					Preferences.Set (Preferences.NOTE_RENAME_BEHAVIOR, rename_behavior);
+				}
+				if (rename_behavior_combo.Active != rename_behavior)
+					rename_behavior_combo.Active = rename_behavior;
+			}
 		}
 		
 		// Page 1
@@ -184,24 +200,43 @@ namespace Tomboy
 			SetupPropertyEditor (bullet_peditor);
 
 			// Custom font...
-
+			Gtk.HBox font_box = new Gtk.HBox (false, 0);
 			check = MakeCheckButton (Catalog.GetString ("Use custom _font"));
-			options_list.PackStart (check, false, false, 0);
+			font_box.PackStart (check);
 
 			font_peditor =
 			        Services.Factory.CreatePropertyEditorToggleButton (Preferences.ENABLE_CUSTOM_FONT,
 			                                        check);
 			SetupPropertyEditor (font_peditor);
 
-			align = new Gtk.Alignment (0.5f, 0.5f, 0.4f, 1.0f);
-			align.Show ();
-			options_list.PackStart (align, false, false, 0);
-
 			font_button = MakeFontButton ();
 			font_button.Sensitive = check.Active;
-			align.Add (font_button);
+			font_box.PackStart (font_button);
+			font_box.ShowAll ();
+			options_list.PackStart (font_box, false, false, 0);
 
 			font_peditor.AddGuard (font_button);
+
+			// Note renaming bahvior
+			Gtk.HBox rename_behavior_box = new Gtk.HBox (false, 0);
+			label = MakeLabel (Catalog.GetString ("When renaming a linked note: "));
+			rename_behavior_box.PackStart (label);
+			rename_behavior_combo = new Gtk.ComboBox (new string [] {
+				Catalog.GetString ("Ask me what to do"),
+				Catalog.GetString ("Never rename links"),
+				Catalog.GetString ("Always rename links")});
+			int rename_behavior = (int) Preferences.Get (Preferences.NOTE_RENAME_BEHAVIOR);
+			if (rename_behavior < 0 || rename_behavior > 2) {
+				rename_behavior = 0;
+				Preferences.Set (Preferences.NOTE_RENAME_BEHAVIOR, rename_behavior);
+			}
+			rename_behavior_combo.Active = rename_behavior;
+			rename_behavior_combo.Changed += (o, e) =>
+				Preferences.Set (Preferences.NOTE_RENAME_BEHAVIOR,
+				                 rename_behavior_combo.Active);
+			rename_behavior_box.PackStart (rename_behavior_combo);
+			rename_behavior_box.ShowAll ();
+			options_list.PackStart (rename_behavior_box, false, false, 0);
 			
 			// New Note Template
 			// Translators: This is 'New Note' Template, not New 'Note Template'
diff --git a/Tomboy/Watchers.cs b/Tomboy/Watchers.cs
index adfdfa3..b77f767 100644
--- a/Tomboy/Watchers.cs
+++ b/Tomboy/Watchers.cs
@@ -174,7 +174,8 @@ namespace Tomboy
 			}
 
 			Logger.Debug ("Renaming note from {0} to {1}", Note.Title, title);
-			Note.Title = title;
+			Note.SetTitle (title, true);
+
 			return true;
 		}
 
@@ -678,29 +679,6 @@ namespace Tomboy
 			// Highlight previously unlinked text
 			if (ContainsText (renamed.Title))
 				HighlightNoteInBlock (renamed, Buffer.StartIter, Buffer.EndIter);
-
-			if (!ContainsText (old_title))
-				return;
-
-			string old_title_lower = old_title.ToLower ();
-
-			// Replace existing links with the new title.
-			NoteTag link_tag = Note.TagTable.LinkTag;
-			TextTagEnumerator enumerator = new TextTagEnumerator (Buffer, link_tag);
-			foreach (TextRange range in enumerator) {
-				if (range.Text.ToLower () != old_title_lower)
-					continue;
-
-				Logger.Log ("Replacing '{0}' with '{1}'",
-				            range.Text,
-				            renamed.Title);
-
-				Gtk.TextIter start_iter = range.Start;
-				Gtk.TextIter end_iter = range.End;
-				Buffer.Delete (ref start_iter, ref end_iter);
-				start_iter = range.Start;
-				Buffer.InsertWithTags (ref start_iter, renamed.Title, link_tag);
-			}
 		}
 
 		void DoHighlight (TrieHit hit, Gtk.TextIter start, Gtk.TextIter end)
diff --git a/data/tomboy.schemas.in b/data/tomboy.schemas.in
index 423b2f2..d403fe3 100644
--- a/data/tomboy.schemas.in
+++ b/data/tomboy.schemas.in
@@ -549,6 +549,26 @@
     </schema>
 
     <schema>
+      <key>/schemas/apps/tomboy/note_rename_behavior</key>
+      <applyto>/apps/tomboy/note_rename_behavior</applyto>
+      <owner>tomboy</owner>
+      <type>int</type>
+      <default>0</default>
+      <locale name="C">
+         <short>Link Updating Behavior on Note Rename</short>
+         <long>
+	   Integer value indicating if there is a preference to always perform
+	   a specific link updating behavior when a note is renamed, instead of prompting
+	   the user.  The values map to an internal enumeration.  0 indicates
+	   that the user wishes to be prompted when renaming a note may impact links
+	   that exist in other notes. 1 indicates that links should automatically
+	   be removed. 2 indicates that link text should be updated to the new note name
+	   so that it will continue linking to the renamed note.
+         </long>
+      </locale>
+    </schema>
+
+    <schema>
       <key>/schemas/apps/tomboy/insert_timestamp/format</key>
       <applyto>/apps/tomboy/insert_timestamp/format</applyto>
       <owner>tomboy</owner>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]