4
using System.Collections.Generic;
5
using System.Threading;
11
public delegate void NotesChangedHandler (object sender, Note changed);
13
public class NoteManager
18
AddinManager addin_mgr;
19
TrieController trie_controller;
21
public static string NoteTemplateTitle = Catalog.GetString ("New Note Template");
23
static string start_note_uri = String.Empty;
25
static void OnSettingChanged (object sender, NotifyEventArgs args)
28
case Preferences.START_NOTE_URI:
29
start_note_uri = args.Value as string;
34
public NoteManager (string directory) :
35
this (directory, Path.Combine (directory, "Backup"))
39
public NoteManager (string directory, string backup_directory)
41
Logger.Debug ("NoteManager created with note path \"{0}\".", directory);
43
notes_dir = directory;
44
backup_dir = backup_directory;
47
public bool Initialized {
51
public TomboyCommandLine CommandLine {
55
// TODO: Decide if/how to enforce
56
public bool ReadOnly {
61
/// Use Gtk.Application.Invoke to invoke the Action delegate
62
/// once this NoteManager is initialized. If this NoteManager
63
/// is already initialized, Gtk.Application.Invoke is *not*
64
/// used (for performance reasons). In other words, this method
65
/// should only be called from the GTK+ main thread.
67
public void GtkInvoke (Action a)
73
Gtk.Application.Invoke ((o, e) => a ());
79
public void Invoke (Action a)
91
public void Initialize ()
93
notes = new List<Note> ();
95
string conf_dir = Services.NativeApplication.ConfigurationDirectory;
97
string old_notes_dir = null;
98
bool migration_needed = false;
100
bool first_run = FirstRun ();
102
old_notes_dir = Services.NativeApplication.PreOneDotZeroNoteDirectory;
103
migration_needed = DirectoryExists (old_notes_dir);
106
if (!Directory.Exists (conf_dir))
107
Directory.CreateDirectory (conf_dir);
109
if (migration_needed) {
111
foreach (string noteFile in Directory.GetFiles (old_notes_dir, "*.note"))
112
File.Copy (noteFile, Path.Combine (notes_dir, Path.GetFileName (noteFile)));
114
// Copy deleted notes
115
string old_backup = Path.Combine (old_notes_dir, "Backup");
116
if (DirectoryExists (old_backup)) {
117
Directory.CreateDirectory (backup_dir);
118
foreach (string noteFile in Directory.GetFiles (old_backup, "*.note"))
119
File.Copy (noteFile, Path.Combine (backup_dir, Path.GetFileName (noteFile)));
122
// Copy configuration data
123
// NOTE: Add-in configuration data copied by AddinManager
124
string sync_manifest_name = "manifest.xml";
125
string old_sync_manifest_path =
126
Path.Combine (old_notes_dir, sync_manifest_name);
127
if (File.Exists (old_sync_manifest_path))
128
File.Copy (old_sync_manifest_path,
129
Path.Combine (conf_dir, sync_manifest_name));
132
// NOTE: Not copying cached data
137
trie_controller = CreateTrieController ();
138
addin_mgr = new AddinManager (conf_dir,
139
migration_needed ? old_notes_dir : null);
142
// First run. Create "Start Here" notes.
148
if (migration_needed) {
149
// Create migration notification note
150
// Translators: The title of the data migration note
151
string base_migration_note_title = Catalog.GetString ("Your Notes Have Moved!");
152
string migration_note_title = base_migration_note_title;
154
// NOTE: Uncomment to generate a title suitable for
155
// putting in the note collection
157
// while (Find (migration_note_title) != null) {
158
// migration_note_title = base_migration_note_title +
159
// string.Format (" ({0})", ++count);
162
string migration_note_content_template = Catalog.GetString (
163
// Translators: The contents (not including the title) of the data migration note. {0}, {1}, {2}, {3}, and {4} are replaced by directory paths and should not be changed
164
@"In the latest version of Tomboy, your note files have moved. You have probably never cared where your notes are stored, and if you still don't care, please go ahead and <bold>delete this note</bold>. :-)
166
Your old note directory is still safe and sound at <link:url>{0}</link:url> . If you go back to an older version of Tomboy, it will look for notes there.
168
But we've copied your notes and configuration info into new directories, which will be used from now on:
170
<list><list-item dir=""ltr"">Notes can now be found at <link:url>{1}</link:url>
171
</list-item><list-item dir=""ltr"">Configuration is at <link:url>{2}</link:url>
172
</list-item><list-item dir=""ltr"">You can install add-ins at <link:url>{3}</link:url>
173
</list-item><list-item dir=""ltr"">Log files can be found at <link:url>{4}</link:url></list-item></list>
176
string migration_note_content =
177
"<note-content version=\"1.0\">" +
178
migration_note_title + "\n\n" +
179
string.Format (migration_note_content_template,
180
old_notes_dir + Path.DirectorySeparatorChar,
181
notes_dir + Path.DirectorySeparatorChar,
182
conf_dir + Path.DirectorySeparatorChar,
183
Path.Combine (conf_dir, "addins") + Path.DirectorySeparatorChar,
184
Services.NativeApplication.LogDirectory + Path.DirectorySeparatorChar) +
187
// NOTE: Uncomment to create a Tomboy note and
188
// show it to the user
189
// Note migration_note = Create (migration_note_title,
190
// migration_note_content);
191
// migration_note.QueueSave (ChangeType.ContentChanged);
192
// migration_note.Window.Show ();
194
// Place file announcing migration in old note directory
196
using (var writer = File.CreateText (Path.Combine (old_notes_dir,
197
migration_note_title.ToUpper ().Replace (" ", "_"))))
198
writer.Write (migration_note_content);
202
Tomboy.ExitingEvent += OnExitingEvent;
207
// Create the TrieController. For overriding in test methods.
208
protected virtual TrieController CreateTrieController ()
210
return new TrieController (this);
213
// For overriding in test methods.
214
protected virtual bool DirectoryExists (string directory)
216
return Directory.Exists (directory);
219
// For overriding in test methods.
220
protected virtual DirectoryInfo CreateDirectory (string directory)
222
return Directory.CreateDirectory (directory);
225
protected virtual bool FirstRun ()
227
return !DirectoryExists (notes_dir);
230
// Create the notes directory if it doesn't exist yet.
231
void CreateNotesDir ()
233
if (!DirectoryExists (notes_dir)) {
234
// First run. Create storage directory.
235
CreateDirectory (notes_dir);
239
void OnNoteRename (Note note, string old_title)
241
if (NoteRenamed != null)
242
NoteRenamed (note, old_title);
243
this.notes.Sort (new CompareDates ());
246
void OnNoteSave (Note note)
248
if (NoteSaved != null)
250
this.notes.Sort (new CompareDates ());
253
void OnBufferChanged (Note note)
255
if (NoteBufferChanged != null)
256
NoteBufferChanged (note);
259
protected virtual void CreateStartNotes ()
261
// FIXME: Delay the creation of the start notes so the panel/tray
262
// icon has enough time to appear so that Tomboy.TrayIconShowing
263
// is valid. Then, we'll be able to instruct the user where to
264
// find the Tomboy icon.
265
//string icon_str = Tomboy.TrayIconShowing ?
266
// Catalog.GetString ("System Tray Icon area") :
267
// Catalog.GetString ("GNOME Panel");
268
string start_note_content =
269
Catalog.GetString ("<note-content>" +
271
"<bold>Welcome to Tomboy!</bold>\n\n" +
272
"Use this \"Start Here\" note to begin organizing " +
273
"your ideas and thoughts.\n\n" +
274
"You can create new notes to hold your ideas by " +
275
"selecting the \"Create New Note\" item from the " +
276
"Tomboy Notes menu in your GNOME Panel. " +
277
"Your note will be saved automatically.\n\n" +
278
"Then organize the notes you create by linking " +
279
"related notes and ideas together!\n\n" +
280
"We've created a note called " +
281
"<link:internal>Using Links in Tomboy</link:internal>. " +
282
"Notice how each time we type <link:internal>Using " +
283
"Links in Tomboy</link:internal> it automatically " +
284
"gets underlined? Click on the link to open the note." +
287
string links_note_content =
288
Catalog.GetString ("<note-content>" +
289
"Using Links in Tomboy\n\n" +
290
"Notes in Tomboy can be linked together by " +
291
"highlighting text in the current note and clicking" +
292
" the <bold>Link</bold> button above in the toolbar. " +
293
"Doing so will create a new note and also underline " +
294
"the note's title in the current note.\n\n" +
295
"Changing the title of a note will update links " +
296
"present in other notes. This prevents broken links " +
297
"from occurring when a note is renamed.\n\n" +
298
"Also, if you type the name of another note in your " +
299
"current note, it will automatically be linked for you." +
303
Note start_note = Create (Catalog.GetString ("Start Here"),
305
start_note.QueueSave (ChangeType.ContentChanged);
306
Preferences.Set (Preferences.START_NOTE_URI, start_note.Uri);
308
Note links_note = Create (Catalog.GetString ("Using Links in Tomboy"),
310
links_note.QueueSave (ChangeType.ContentChanged);
312
if (!Tomboy.IsPanelApplet)
313
start_note.Window.Show ();
314
} catch (Exception e) {
315
Logger.Warn ("Error creating start notes: {0}\n{1}",
316
e.Message, e.StackTrace);
320
protected virtual void LoadNotes ()
322
Logger.Debug ("Loading notes");
323
string [] files = Directory.GetFiles (notes_dir, "*.note");
325
foreach (string file_path in files) {
327
Note note = Note.Load (file_path, this);
329
note.Renamed += OnNoteRename;
330
note.Saved += OnNoteSave;
331
note.BufferChanged += OnBufferChanged;
334
} catch (System.Xml.XmlException e) {
335
Logger.Error ("Error parsing note XML, skipping \"{0}\": {1}",
338
} catch (System.IO.IOException e) {
339
Logger.Error ("Note {0} can not be loaded - file corrupted?: {1}",
342
Gtk.MessageDialog md =
343
new Gtk.MessageDialog(null,Gtk.DialogFlags.DestroyWithParent,
344
Gtk.MessageType.Error,
345
Gtk.ButtonsType.Close,
346
"Skipping a note.\n {0} can not be loaded - Error loading file!",
351
} catch (System.UnauthorizedAccessException e) {
352
Logger.Error ("Note {0} can not be loaded - access denied: {1}",
355
Gtk.MessageDialog md =
356
new Gtk.MessageDialog(null,Gtk.DialogFlags.DestroyWithParent,
357
Gtk.MessageType.Error,
358
Gtk.ButtonsType.Close,
359
"Skipping a note.\n {0} can not be loaded - Access denied!",
367
notes.Sort (new CompareDates ());
369
// Update the trie so addins can access it, if they want.
370
trie_controller.Update ();
372
if (NotesLoaded != null)
373
NotesLoaded (this, EventArgs.Empty);
375
bool startup_notes_enabled = (bool)
376
Preferences.Get (Preferences.ENABLE_STARTUP_NOTES);
378
// Load all the addins for our notes.
379
// Iterating through copy of notes list, because list may be
380
// changed when loading addins.
381
List<Note> notesCopy = new List<Note> (notes);
382
foreach (Note note in notesCopy) {
383
addin_mgr.LoadAddinsForNote (note);
385
// Show all notes that were visible when tomboy was shut down
386
if (note.IsOpenOnStartup) {
387
if (startup_notes_enabled)
390
note.QueueSave (ChangeType.NoChange);
394
// Make sure that a Start Note Uri is set in the preferences, and
395
// make sure that the Uri is valid to prevent bug #508982. This
396
// has to be done here for long-time Tomboy users who won't go
397
// through the CreateStartNotes () process.
398
if (StartNoteUri == String.Empty ||
399
FindByUri(StartNoteUri) == null) {
400
// Attempt to find an existing Start Here note
401
Note start_note = Find (Catalog.GetString ("Start Here"));
402
if (start_note != null)
403
Preferences.Set (Preferences.START_NOTE_URI, start_note.Uri);
406
if (NotesLoaded != null)
407
NotesLoaded (this, EventArgs.Empty);
410
void OnExitingEvent (object sender, EventArgs args)
412
// Call ApplicationAddin.Shutdown () on all the known ApplicationAddins
413
foreach (ApplicationAddin addin in addin_mgr.GetApplicationAddins ()) {
416
} catch (Exception e) {
417
Logger.Warn ("Error calling {0}.Shutdown (): {1}",
418
addin.GetType ().ToString (), e.Message);
422
Logger.Debug ("Saving unsaved notes...");
424
// Use a copy of the notes to prevent bug #510442 (crash on exit
425
// when iterating the notes to save them.
426
List<Note> notesCopy = new List<Note> (notes);
427
foreach (Note note in notesCopy) {
428
// If the note is visible, it will be shown automatically on
430
if (note.HasWindow && note.Window.Visible)
431
note.IsOpenOnStartup = true;
433
note.IsOpenOnStartup = false; /* bgo #672482 */
439
public void Delete (Note note)
441
if (File.Exists (note.FilePath)) {
442
if (backup_dir != null) {
443
if (!Directory.Exists (backup_dir))
444
Directory.CreateDirectory (backup_dir);
447
Path.Combine (backup_dir,
448
Path.GetFileName (note.FilePath));
449
if (File.Exists (backup_path))
450
File.Delete (backup_path);
452
File.Move (note.FilePath, backup_path);
454
File.Delete (note.FilePath);
460
Logger.Debug ("Deleting note '{0}'.", note.Title);
462
if (NoteDeleted != null)
463
NoteDeleted (this, note);
466
string MakeNewFileName ()
468
return MakeNewFileName (Guid.NewGuid ().ToString ());
471
string MakeNewFileName (string guid)
473
return Path.Combine (notes_dir, guid + ".note");
476
// Create a new note with a generated title
477
public Note Create ()
479
int new_num = notes.Count;
483
temp_title = String.Format (Catalog.GetString ("New Note {0}"),
485
if (Find (temp_title) == null)
489
return Create (temp_title);
492
public static string SplitTitleFromContent (string title, out string body)
499
title = title.Trim();
500
if (title == string.Empty)
503
string[] lines = title.Split (new char[] { '\n', '\r' }, 2);
504
if (lines.Length > 0) {
506
title = title.Trim ();
507
title = title.TrimEnd ('.', ',', ';');
508
if (title == string.Empty)
512
if (lines.Length > 1)
518
public Note Create (string title)
520
return CreateNewNote (title, null);
523
public Note Create (string title, string xml_content)
525
return CreateNewNote (title, xml_content, null);
529
/// Creates a new note with GUID.
532
/// Empty note with specified title and GUID.
534
/// <param name='title'>
537
/// <param name='guid'>
540
public Note CreateWithGuid (string title, string guid)
542
return CreateNewNote (title, "", guid);
545
// Create a new note with the specified title from the default
546
// template note. Optionally the body can be overridden by appending
548
private Note CreateNewNote (string title, string guid)
552
title = SplitTitleFromContent (title, out body);
556
Note template_note = GetOrCreateTemplateNote ();
558
if (String.IsNullOrEmpty (body))
559
return CreateNoteFromTemplate (title, template_note, guid);
561
// Use a simple "Describe..." body and highlight
562
// it so it can be easily overwritten
563
body = Catalog.GetString ("Describe your new note here.");
565
string header = title + "\n\n";
567
String.Format ("<note-content>{0}{1}</note-content>",
568
XmlEncoder.Encode (header),
569
XmlEncoder.Encode (body));
571
Note new_note = CreateNewNote (title, content, guid);
573
// Select the inital text so typing will overwrite the body text
574
NoteBuffer buffer = new_note.Buffer;
575
Gtk.TextIter iter = buffer.GetIterAtOffset (header.Length);
576
buffer.MoveMark (buffer.SelectionBound, iter);
577
buffer.MoveMark (buffer.InsertMark, buffer.EndIter);
582
// Create a new note with the specified Xml content
583
private Note CreateNewNote (string title, string xml_content, string guid)
585
if (title == null || title == string.Empty)
586
throw new Exception ("Invalid title");
588
if (Find (title) != null)
589
throw new Exception ("A note with this title already exists: " + title);
593
filename = MakeNewFileName (guid);
595
filename = MakeNewFileName ();
597
Note new_note = Note.CreateNewNote (title, filename, this);
598
new_note.XmlContent = xml_content;
599
new_note.Renamed += OnNoteRename;
600
new_note.Saved += OnNoteSave;
601
new_note.BufferChanged += OnBufferChanged;
603
notes.Add (new_note);
605
// Load all the addins for the new note
606
addin_mgr.LoadAddinsForNote (new_note);
608
if (NoteAdded != null)
609
NoteAdded (this, new_note);
615
/// Get the existing template note or create a new one
616
/// if it doesn't already exist.
619
/// A <see cref="Note"/>
621
public Note GetOrCreateTemplateNote ()
623
// The default template note will have the system template tag and
624
// will belong to zero notebooks. We find this by searching all
625
// notes with the TemplateNoteSystemTag and check that it's
627
Note template_note = null;
628
Tag template_tag = TagManager.GetOrCreateSystemTag (TagManager.TemplateNoteSystemTag);
629
foreach (Note note in template_tag.Notes) {
630
if (Notebooks.NotebookManager.GetNotebookFromNote (note) == null) {
631
template_note = note;
636
if (template_note == null) {
638
Create (NoteTemplateTitle,
639
GetNoteTemplateContent (NoteTemplateTitle));
641
// Select the initial text
642
NoteBuffer buffer = template_note.Buffer;
643
Gtk.TextIter iter = buffer.GetIterAtLineOffset (2, 0);
644
buffer.MoveMark (buffer.SelectionBound, iter);
645
buffer.MoveMark (buffer.InsertMark, buffer.EndIter);
647
// Flag this as a template note
648
template_note.AddTag (template_tag);
650
template_note.QueueSave (ChangeType.ContentChanged);
653
return template_note;
656
public static string GetNoteTemplateContent (string title)
658
const string base_xml =
660
"<note-title>{0}</note-title>\n\n" +
664
return string.Format (base_xml,
665
XmlEncoder.Encode (title),
666
Catalog.GetString ("Describe your new note here."));
669
public Note Find (string linked_title)
671
foreach (Note note in notes) {
672
if (note.Title.ToLower () == linked_title.ToLower ())
678
public Note FindByUri (string uri)
680
foreach (Note note in notes) {
687
// Removes any trailing whitespace on the title line
688
public static string SanitizeXmlContent (string xml_content)
690
int i = String.IsNullOrEmpty (xml_content) ? -1 : xml_content.IndexOf ('\n');
692
if (xml_content [i].Equals ('\r'))
695
if (Char.IsWhiteSpace (xml_content [i]))
696
xml_content = xml_content.Remove (i,1);
705
/// Creates a new note with the given titel based on the template note.
707
/// <param name="title">
708
/// A <see cref="System.String"/>
710
/// <param name="template_note">
711
/// A <see cref="Note"/>
714
/// A <see cref="Note"/>
716
public Note CreateNoteFromTemplate (string title, Note template_note)
718
return CreateNoteFromTemplate (title, template_note, null);
721
// Creates a new note with the given title and guid with body based on
722
// the template note.
723
private Note CreateNoteFromTemplate (string title, Note template_note, string guid)
725
Tag template_save_title = TagManager.GetOrCreateSystemTag (TagManager.TemplateNoteSaveTitleSystemTag);
726
if (template_note.ContainsTag (template_save_title))
727
title = GetUniqueName (template_note.Title, notes.Count);
729
// Use the body from the template note
731
template_note.XmlContent.Replace (XmlEncoder.Encode (template_note.Title),
732
XmlEncoder.Encode (title));
733
xml_content = SanitizeXmlContent (xml_content);
735
Note new_note = CreateNewNote (title, xml_content, guid);
737
// Copy template note's properties
738
Tag template_save_size = TagManager.GetOrCreateSystemTag (TagManager.TemplateNoteSaveSizeSystemTag);
739
if (template_note.Data.HasExtent () && template_note.ContainsTag (template_save_size)) {
740
new_note.Data.Height = template_note.Data.Height;
741
new_note.Data.Width = template_note.Data.Width;
744
Tag template_save_selection = TagManager.GetOrCreateSystemTag (TagManager.TemplateNoteSaveSelectionSystemTag);
745
if (template_note.Data.CursorPosition > 0 && template_note.ContainsTag (template_save_selection)) {
746
Gtk.TextBuffer buffer = new_note.Buffer;
749
// Because the titles will be different between template and
750
// new note, we can't just drop the cursor at template's
751
// CursorPosition. Whitespace after the title makes this more
752
// complicated so let's just start counting from the line after the title.
753
int title_offset_difference = buffer.GetIterAtLine (1).Offset - template_note.Buffer.GetIterAtLine (1).Offset;
755
iter = buffer.GetIterAtOffset (template_note.Data.CursorPosition + title_offset_difference);
756
buffer.PlaceCursor(iter);
758
iter = buffer.GetIterAtOffset (template_note.Data.SelectionBoundPosition + title_offset_difference);
759
buffer.MoveMark (buffer.SelectionBound.Name, iter);
765
// Find a title that does not exist using basename and id as
767
public string GetUniqueName (string basename, int id)
771
title = String.Concat (basename, " ", id++);
772
if (Find (title) == null)
779
class CompareDates : IComparer<Note>
781
public int Compare (Note a, Note b)
783
// Sort in reverse chrono order...
784
if (a == null || b == null)
787
return DateTime.Compare (b.ChangeDate,
792
public static string StartNoteUri
795
if (String.IsNullOrEmpty (start_note_uri)) {
796
// Watch the START_NOTE_URI setting and update it so that the
797
// StartNoteUri property doesn't generate a call to
798
// Preferences.Get () each time it's accessed.
800
Preferences.Get (Preferences.START_NOTE_URI) as string;
801
Preferences.SettingChanged -= OnSettingChanged;
802
Preferences.SettingChanged += OnSettingChanged;
804
return start_note_uri;
808
public List<Note> Notes
811
// FIXME: Only sort on change by listening to
812
// Note.Saved or Note.Buffer.Changed
813
//notes.Sort (new CompareDates ());
818
public TrieTree TitleTrie
821
return trie_controller.TitleTrie;
825
public AddinManager AddinManager
832
public string NoteDirectoryPath
839
public event NotesChangedHandler NoteDeleted;
840
public event NotesChangedHandler NoteAdded;
841
public event NoteRenameHandler NoteRenamed;
842
public event NoteSavedHandler NoteSaved;
843
public event Action<Note> NoteBufferChanged;
844
public event EventHandler NotesLoaded;
847
public class TrieController
852
public TrieController (NoteManager manager)
854
this.manager = manager;
855
manager.NoteDeleted += OnNoteDeleted;
856
manager.NoteAdded += OnNoteAdded;
857
manager.NoteRenamed += OnNoteRenamed;
862
void OnNoteAdded (object sender, Note added)
867
void OnNoteDeleted (object sender, Note deleted)
872
void OnNoteRenamed (Note renamed, string old_title)
877
public void Update ()
879
title_trie = new TrieTree (false /* !case_sensitive */);
881
foreach (Note note in manager.Notes) {
882
title_trie.AddKeyword (note.Title, note);
885
title_trie.ComputeFailureGraph ();
888
public TrieTree TitleTrie