using System; using System.Collections; using System.IO; using System.Xml; namespace Tomboy { // Provides the concept of active tags, which are applied on text // insert. Exposes the UndoManager for this buffer. And adds a // InsertTextWithTags event which is fired after inserted text has all // the active tags applied. public class NoteBuffer : Gtk.TextBuffer { UndoManager undo_manager; char[] indent_bullets = {'\u2022', '\u2218', '\u2023'}; // GODDAMN Gtk.TextBuffer. I hate you. Hate Hate Hate. struct ImageInsertData { public bool adding; public Gtk.TextBuffer buffer; public Gtk.TextMark position; public Gdk.Pixbuf image; public NoteTag tag; }; ArrayList imageQueue; uint imageQueueTimeout; // HATE. // list of Gtk.TextTags to apply on insert ArrayList active_tags; public NoteBuffer (Gtk.TextTagTable tags) : base (tags) { active_tags = new ArrayList (); undo_manager = new UndoManager (this); InsertText += TextInsertedEvent; DeleteRange += RangeDeletedEvent; MarkSet += MarkSetEvent; TagApplied += OnTagApplied; tags.TagChanged += OnTagChanged; imageQueue = new ArrayList(); imageQueueTimeout = 0; } // Signal that text has been inserted, and any active tags have // been applied to the text. This allows undo to pull any // active tags from the inserted text. public event Gtk.InsertTextHandler InsertTextWithTags; public event ChangeDepthHandler ChangeTextDepth; public event NewBulletHandler NewBulletInserted; public void ToggleActiveTag (string tag_name) { Logger.Log ("ToggleTag called for '{0}'", tag_name); Gtk.TextTag tag = TagTable.Lookup (tag_name); Gtk.TextIter select_start, select_end; if (GetSelectionBounds (out select_start, out select_end)) { // Ignore the bullet character if (FindDepthTag (select_start) != null) select_start.LineOffset = 2; if (select_start.BeginsTag (tag) || select_start.HasTag (tag)) RemoveTag (tag, select_start, select_end); else ApplyTag (tag, select_start, select_end); } else { if (active_tags.Contains (tag)) active_tags.Remove (tag); else active_tags.Add (tag); } } public void SetActiveTag (string tag_name) { Logger.Log ("SetTag called for '{0}'", tag_name); Gtk.TextTag tag = TagTable.Lookup (tag_name); Gtk.TextIter select_start, select_end; if (GetSelectionBounds (out select_start, out select_end)) { ApplyTag (tag, select_start, select_end); } else { active_tags.Add (tag); } } public void RemoveActiveTag (string tag_name) { Logger.Log ("RemoveTag called for '{0}'", tag_name); Gtk.TextTag tag = TagTable.Lookup (tag_name); Gtk.TextIter select_start, select_end; if (GetSelectionBounds (out select_start, out select_end)) { RemoveTag (tag, select_start, select_end); } else { active_tags.Remove (tag); } } /// /// Returns the specified DynamicNoteTag if one exists on the TextIter /// or null if none was found. /// public DynamicNoteTag GetDynamicTag (string tag_name, Gtk.TextIter iter) { Gtk.TextTag [] tags = iter.Tags; foreach (Gtk.TextTag tag in iter.Tags) { DynamicNoteTag dynamic_tag = tag as DynamicNoteTag; if (dynamic_tag != null && dynamic_tag.ElementName.CompareTo (tag_name) == 0) return dynamic_tag; } return null; } public void OnTagApplied (object o, Gtk.TagAppliedArgs args) { if (!(args.Tag is DepthNoteTag)) { // Remove the tag from any bullets in the selection Undoer.FreezeUndo (); Gtk.TextIter iter; for (int i = args.StartChar.Line; i <= args.EndChar.Line; i++) { iter = GetIterAtLine(i); if (FindDepthTag (iter) != null) { Gtk.TextIter next = iter; next.ForwardChars (2); RemoveTag (args.Tag, iter, next); } } Undoer.ThawUndo (); } else { // Remove any existing tags when a depth tag is applied Undoer.FreezeUndo (); foreach (Gtk.TextTag tag in args.StartChar.Tags) { if (!(tag is DepthNoteTag)) { RemoveTag (tag, args.StartChar, args.EndChar); } } Undoer.ThawUndo (); } } public bool IsActiveTag (string tag_name) { Gtk.TextTag tag = TagTable.Lookup (tag_name); Gtk.TextIter iter, select_end; if (GetSelectionBounds (out iter, out select_end)) { // Ignore the bullet character and look at the // first character of the list item if (FindDepthTag (iter) != null) iter.ForwardChars (2); return iter.BeginsTag (tag) || iter.HasTag (tag); } else { return active_tags.Contains (tag); } } // Returns true if the cursor is inside of a bulleted list public bool IsBulletedListActive () { Gtk.TextMark insert_mark = InsertMark; Gtk.TextIter iter = GetIterAtMark (insert_mark); iter.LineOffset = 0; DepthNoteTag depth = FindDepthTag (iter); if (depth == null) return false; return true; } // Returns true if the cursor is at a position that can // be made into a bulleted list public bool CanMakeBulletedList () { Gtk.TextMark insert_mark = InsertMark; Gtk.TextIter iter = GetIterAtMark (insert_mark); if (iter.Line == 0) return false; return true; } // Apply active_tags to inserted text void TextInsertedEvent (object sender, Gtk.InsertTextArgs args) { // Only apply active tags when typing, not on paste. if (args.Text.Length == 1) { Gtk.TextIter insert_start = args.Pos; insert_start.BackwardChars (args.Text.Length); Undoer.FreezeUndo (); foreach (Gtk.TextTag tag in insert_start.Tags) { RemoveTag (tag, insert_start, args.Pos); } foreach (Gtk.TextTag tag in active_tags) { ApplyTag (tag, insert_start, args.Pos); } Undoer.ThawUndo (); } // See if we want to change the direction of the bullet Gtk.TextIter line_start = args.Pos; line_start.LineOffset = 0; if (args.Pos.LineOffset - args.Text.Length == 2 && FindDepthTag (line_start) != null) { Pango.Direction direction = Pango.Direction.Ltr; if (args.Text.Length > 0) direction = Pango.Global.UnicharDirection (args.Text[0]); ChangeBulletDirection (args.Pos, direction); } if (InsertTextWithTags != null) InsertTextWithTags (sender, args); } // Change the direction of a bulleted line to match the new // first character after the previous character is deleted. void RangeDeletedEvent (object sender, Gtk.DeleteRangeArgs args) { Gtk.TextIter[] iters = {args.Start, args.End}; foreach (Gtk.TextIter iter in iters) { Gtk.TextIter line_start = iter; line_start.LineOffset = 0; if ((iter.LineOffset == 3 || iter.LineOffset == 2) && FindDepthTag (line_start) != null) { Gtk.TextIter first_char = iter; first_char.LineOffset = 2; Pango.Direction direction = Pango.Direction.Ltr; if (first_char.Char.Length > 0) direction = Pango.Global.UnicharDirection (first_char.Char[0]); ChangeBulletDirection (first_char, direction); } } } public bool AddNewline() { if (!CanMakeBulletedList()) return false; Gtk.TextMark insert_mark = InsertMark; Gtk.TextIter iter = GetIterAtMark (insert_mark); iter.LineOffset = 0; DepthNoteTag prev_depth = FindDepthTag (iter); // If the previous line has a bullet point on it we add a bullet // to the new line, unless the previous line was blank (apart from // the bullet), in which case we clear the bullet/indent from the // previous line. if (prev_depth != null) { iter.ForwardChar (); Gtk.TextIter insert = GetIterAtMark (insert_mark); // See if the line was left contentless and remove the bullet // if so. if (iter.EndsLine () || insert.LineOffset < 3 ) { Gtk.TextIter start = GetIterAtLine (iter.Line); Gtk.TextIter end = start; end.ForwardToLineEnd (); if (end.LineOffset < 2) { end = start; } else { end = GetIterAtLineOffset (iter.Line, 2); } Delete (ref start, ref end); iter = GetIterAtMark (insert_mark); Insert (ref iter, "\n"); } else { Undoer.FreezeUndo (); iter = GetIterAtMark (insert_mark); int offset = iter.Offset; Insert (ref iter, "\n"); iter = GetIterAtMark (insert_mark); Gtk.TextIter start = GetIterAtLine (iter.Line); // Set the direction of the bullet to be the same // as the first character on the new line Pango.Direction direction = prev_depth.Direction; if (iter.Char != "\n" && iter.Char.Length > 0) direction = Pango.Global.UnicharDirection (iter.Char[0]); InsertBullet (ref start, prev_depth.Depth, direction); Undoer.ThawUndo (); NewBulletInserted (this, new InsertBulletEventArgs (offset, prev_depth.Depth, direction)); } return true; } // Replace lines starting with '*' or '-' with bullets else if (iter.Char.Equals ("*") || iter.Char.Equals ("-")) { Gtk.TextIter start = GetIterAtLineOffset (iter.Line, 0); Gtk.TextIter end = GetIterAtLineOffset (iter.Line, 1); // Remove the '*' character and any leading white space if (end.Char == " ") end.ForwardChar(); // Set the direction of the bullet to be the same as // the first character after the '*' or '-' Pango.Direction direction = Pango.Direction.Ltr; if (end.Char.Length > 0) direction = Pango.Global.UnicharDirection (end.Char[0]); Delete (ref start, ref end); if (end.EndsLine ()) { IncreaseDepth (ref start); } else { IncreaseDepth (ref start); iter = GetIterAtMark (insert_mark); int offset = iter.Offset; Insert (ref iter, "\n"); iter = GetIterAtMark (insert_mark); iter.LineOffset = 0; Undoer.FreezeUndo (); InsertBullet (ref iter, 0, direction); Undoer.ThawUndo (); NewBulletInserted (this, new InsertBulletEventArgs (offset, 0, direction)); } return true; } return false; } // Returns true if the depth of the line was increased public bool AddTab () { Gtk.TextMark insert_mark = InsertMark; Gtk.TextIter iter = GetIterAtMark (insert_mark); iter.LineOffset = 0; DepthNoteTag depth = FindDepthTag (iter); // If the cursor is at a line with a depth and a tab has been // inserted then we increase the indent depth of that line. if (depth != null) { IncreaseDepth (ref iter); return true; } return false; } // Returns true if the depth of the line was decreased public bool RemoveTab () { Gtk.TextMark insert_mark = InsertMark; Gtk.TextIter iter = GetIterAtMark (insert_mark); iter.LineOffset = 0; DepthNoteTag depth = FindDepthTag (iter); // If the cursor is at a line with depth and a tab has been // inserted, then we decrease the depth of that line. if (depth != null) { DecreaseDepth (ref iter); return true; } return false; } // Returns true if a bullet had to be removed // This is for the Delete key not Backspace public bool DeleteKeyHandler () { // See if there is a selection Gtk.TextIter start; Gtk.TextIter end; bool selection = GetSelectionBounds (out start, out end); if (selection) { AugmentSelection (ref start, ref end); Delete (ref start, ref end); return true; } else if (start.EndsLine () && start.Line < LineCount) { Gtk.TextIter next = GetIterAtLine (start.Line + 1); end = start; end.ForwardChars (3); DepthNoteTag depth = FindDepthTag (next); if (depth != null) { Delete (ref start, ref end); return true; } } else { Gtk.TextIter next = start; if (next.LineOffset != 0) next.ForwardChar (); DepthNoteTag depth = FindDepthTag (start); DepthNoteTag nextDepth = FindDepthTag (next); if (depth != null || nextDepth != null) { DecreaseDepth (ref start); return true; } } return false; } public bool BackspaceKeyHandler () { Gtk.TextIter start; Gtk.TextIter end; bool selection = GetSelectionBounds (out start, out end); DepthNoteTag depth = FindDepthTag (start); if (selection) { AugmentSelection (ref start, ref end); Delete (ref start, ref end); return true; } else { // See if the cursor is inside or just after a bullet region // ie. // |* lorum ipsum // ^^^ // and decrease the depth if it is. Gtk.TextIter prev = start; if (prev.LineOffset != 0) prev.BackwardChars (1); DepthNoteTag prev_depth = FindDepthTag (prev); if (depth != null || prev_depth != null) { DecreaseDepth (ref start); return true; } } return false; } // On an InsertEvent we change the selection (if there is one) // so that it doesn't slice through bullets. [GLib.ConnectBefore] public void CheckSelection () { Gtk.TextIter start; Gtk.TextIter end; bool selection = GetSelectionBounds (out start, out end); if (selection) { AugmentSelection (ref start, ref end); } else { // If the cursor is at the start of a bulleted line // move it so it is after the bullet. if ((start.LineOffset == 0 || start.LineOffset == 1) && FindDepthTag (start) != null) { start.LineOffset = 2; SelectRange (start, start); } } } // Change the selection on the buffer taking into account any // bullets that are in or near the seletion void AugmentSelection (ref Gtk.TextIter start, ref Gtk.TextIter end) { DepthNoteTag start_depth = FindDepthTag (start); DepthNoteTag end_depth = FindDepthTag (end); Gtk.TextIter inside_end = end; inside_end.BackwardChar (); DepthNoteTag inside_end_depth = FindDepthTag (inside_end); // Start inside bullet region if (start_depth != null) { start.LineOffset = 2; SelectRange (start, end); } // End inside another bullet if (inside_end_depth != null) { end.LineOffset = 2; SelectRange (start, end); } // Check if the End is right before start of bullet if (end_depth != null) { end.LineOffset = 2; SelectRange (start, end); } } // Clear active tags, and add any tags which should be applied: // - Avoid the having tags grow frontwords by not adding tags // which start on the next character. // - Add tags ending on the prior character, to avoid needing to // constantly toggle tags. void MarkSetEvent (object sender, Gtk.MarkSetArgs args) { if (args.Mark != InsertMark) return; active_tags.Clear (); Gtk.TextIter iter = GetIterAtMark (args.Mark); // Add any growable tags not starting on the next character... foreach (Gtk.TextTag tag in iter.Tags) { if (!iter.BeginsTag (tag) && NoteTagTable.TagIsGrowable (tag)) { active_tags.Add (tag); } } // Add any growable tags not ending on the prior character... foreach (Gtk.TextTag tag in iter.GetToggledTags (false)) { if (!iter.EndsTag (tag) && NoteTagTable.TagIsGrowable (tag)) { active_tags.Add (tag); } } } void ImageSwap (NoteTag tag, Gtk.TextIter start, Gtk.TextIter end, bool adding) { if (tag.Image == null) return; Gtk.TextIter prev = start; prev.BackwardChar (); if ((adding == true && tag.Image != prev.Pixbuf) || (adding == false && tag.Image == prev.Pixbuf)) { ImageInsertData data = new ImageInsertData (); data.buffer = start.Buffer; data.tag = tag; data.image = tag.Image; data.adding = adding; if (adding) { data.position = start.Buffer.CreateMark (null, start, true); } else { data.position = tag.ImageLocation; } imageQueue.Add(data); if (imageQueueTimeout == 0) { imageQueueTimeout = GLib.Idle.Add(RunImageQueue); } } } public bool RunImageQueue () { foreach (ImageInsertData data in imageQueue) { NoteBuffer buffer = data.buffer as NoteBuffer; Gtk.TextIter iter = buffer.GetIterAtMark (data.position); Gtk.TextMark location = data.position; // Prevent the image from being inserted before a bullet if (FindDepthTag (iter) != null) { iter.LineOffset = 2; location = CreateMark(data.position.Name, iter, data.position.LeftGravity); } buffer.Undoer.FreezeUndo(); if (data.adding && data.tag.ImageLocation == null) { buffer.InsertPixbuf (ref iter, data.image); data.tag.ImageLocation = location; } else if (!data.adding && data.tag.ImageLocation != null) { Gtk.TextIter end = iter; end.ForwardChar(); buffer.Delete (ref iter, ref end); buffer.DeleteMark (location); data.tag.ImageLocation = null; } buffer.Undoer.ThawUndo (); } imageQueue.Clear (); imageQueueTimeout = 0; return false; } void OnTagChanged (object sender, Gtk.TagChangedArgs args) { NoteTag note_tag = args.Tag as NoteTag; if (note_tag != null) { TextTagEnumerator enumerator = new TextTagEnumerator (this, note_tag); foreach (TextRange range in enumerator) { ImageSwap (note_tag, range.Start, range.End, true); } } } protected override void OnTagApplied (Gtk.TextTag tag, Gtk.TextIter start, Gtk.TextIter end) { base.OnTagApplied (tag, start, end); NoteTag note_tag = tag as NoteTag; if (note_tag != null) { ImageSwap (note_tag, start, end, true); } } protected override void OnTagRemoved (Gtk.TextTag tag, Gtk.TextIter start, Gtk.TextIter end) { NoteTag note_tag = tag as NoteTag; if (note_tag != null) { ImageSwap (note_tag, start, end, false); } base.OnTagRemoved (tag, start, end); } public UndoManager Undoer { get { return undo_manager; } } public string Selection { get { Gtk.TextIter select_start, select_end; if (GetSelectionBounds (out select_start, out select_end)) { string text = GetText (select_start, select_end, false); if (text.Length > 0) return text; } return null; } } public static void GetBlockExtents (ref Gtk.TextIter start, ref Gtk.TextIter end, int threshold, Gtk.TextTag avoid_tag) { // Move start and end to the beginning or end of their // respective paragraphs, bounded by some threshold. start.LineOffset = Math.Max (0, start.LineOffset - threshold); // FIXME: Sometimes I need to access this before it // returns real values. int bug = end.CharsInLine; if (end.CharsInLine - end.LineOffset > threshold + 1 /* newline */) end.LineOffset += threshold; else end.ForwardToLineEnd (); if (avoid_tag != null) { if (start.HasTag (avoid_tag)) start.BackwardToTagToggle (avoid_tag); if (end.HasTag (avoid_tag)) end.ForwardToTagToggle (avoid_tag); } } // Toggle the lines in the selection to have bullets or not public void ToggleSelectionBullets () { Gtk.TextIter start; Gtk.TextIter end; GetSelectionBounds (out start, out end); start = GetIterAtLineOffset (start.Line, 0); bool toggle_on = true; if (FindDepthTag (start) != null) { Gtk.TextIter bullet_end = GetIterAtLineOffset (start.Line, 2); toggle_on = false; } int start_line = start.Line; int end_line = end.Line; for (int i = start_line; i <= end_line; i++) { Gtk.TextIter curr_line = GetIterAtLine(i); if (toggle_on && FindDepthTag (curr_line) == null) { IncreaseDepth (ref curr_line); } else if (!toggle_on && FindDepthTag (curr_line) != null) { Gtk.TextIter bullet_end = GetIterAtLineOffset (curr_line.Line, 2); Delete (ref curr_line, ref bullet_end); } } } public void IncreaseCursorDepth () { ChangeCursorDepth (true); } public void DecreaseCursorDepth () { ChangeCursorDepth (false); } // Increase or decrease the depth of the line at the // cursor depending on wheather it is RTL or LTR public void ChangeCursorDepthDirectional (bool right) { Gtk.TextIter start; Gtk.TextIter end; GetSelectionBounds (out start, out end); // If we are moving right then: // RTL => decrease depth // LTR => increase depth // We choose to increase or decrease the depth // based on the fist line in the selection. bool increase = right; start.LineOffset = 0; DepthNoteTag start_depth = FindDepthTag (start); bool rtl_depth = start_depth != null && start_depth.Direction == Pango.Direction.Rtl; bool first_char_rtl = start.Char.Length > 0 && (Pango.Global.UnicharDirection (start.Char[0]) == Pango.Direction.Rtl); Gtk.TextIter next = start; if (start_depth != null) { next.ForwardChars (2); } else { // Look for the first non-space character on the line // and use that to determine what direction we should go next.ForwardSentenceEnd (); next.BackwardSentenceStart (); first_char_rtl = next.Char.Length > 0 && (Pango.Global.UnicharDirection (next.Char[0]) == Pango.Direction.Rtl); } if ((rtl_depth || first_char_rtl) && ((next.Line == start.Line) && !next.EndsLine ())) { increase = !right; } ChangeCursorDepth(increase); } void ChangeCursorDepth(bool increase) { Gtk.TextIter start; Gtk.TextIter end; GetSelectionBounds (out start, out end); Gtk.TextIter curr_line; int start_line = start.Line; int end_line = end.Line; for (int i = start_line; i <= end_line; i++) { curr_line = GetIterAtLine(i); if (increase) IncreaseDepth (ref curr_line); else DecreaseDepth (ref curr_line); } } // Change the writing direction (ie. RTL or LTR) of a bullet. // This makes the bulleted line use the correct indent public void ChangeBulletDirection (Gtk.TextIter iter, Pango.Direction direction) { iter.LineOffset = 0; DepthNoteTag tag = FindDepthTag (iter); if (tag != null) { if (tag.Direction != direction && direction != Pango.Direction.Neutral) { NoteTagTable note_table = TagTable as NoteTagTable; // Get the depth tag for the given direction Gtk.TextTag new_tag = note_table.GetDepthTag (tag.Depth, direction); Gtk.TextIter next = iter; next.ForwardChar (); // Replace the old depth tag with the new one RemoveAllTags (iter, next); ApplyTag (new_tag, iter, next); } } } public void InsertBullet (ref Gtk.TextIter iter, int depth, Pango.Direction direction) { NoteTagTable note_table = TagTable as NoteTagTable; DepthNoteTag tag = note_table.GetDepthTag (depth, direction); string bullet = indent_bullets [depth % indent_bullets.Length] + " "; InsertWithTags (ref iter, bullet, tag); } public void RemoveBullet (ref Gtk.TextIter iter) { Gtk.TextIter end; Gtk.TextIter line_end = iter; line_end.ForwardToLineEnd (); if (line_end.LineOffset < 2) { end = GetIterAtLineOffset (iter.Line, 1); } else { end = GetIterAtLineOffset (iter.Line, 2); } // Go back one more character to delete the \n as well iter = GetIterAtLine (iter.Line - 1); iter.ForwardToLineEnd (); Delete(ref iter, ref end); } public void IncreaseDepth (ref Gtk.TextIter start) { if (!CanMakeBulletedList()) return; Gtk.TextIter end; start = GetIterAtLineOffset (start.Line, 0); Gtk.TextIter line_end = GetIterAtLine (start.Line); line_end.ForwardToLineEnd (); end = start; end.ForwardChars (2); DepthNoteTag curr_depth = FindDepthTag (start); Undoer.FreezeUndo (); if (curr_depth == null) { // Insert a brand new bullet Gtk.TextIter next = start; next.ForwardSentenceEnd (); next.BackwardSentenceStart (); // Insert the bullet using the same direction // as the text on the line Pango.Direction direction = Pango.Direction.Ltr; if (next.Char.Length > 0 && next.Line == start.Line) direction = Pango.Global.UnicharDirection (next.Char[0]); InsertBullet (ref start, 0, direction); } else { // Remove the previous indent Delete (ref start, ref end); // Insert the indent at the new depth int nextDepth = curr_depth.Depth + 1; InsertBullet (ref start, nextDepth, curr_depth.Direction); } Undoer.ThawUndo (); ChangeTextDepth (this, new ChangeDepthEventArgs (start.Line, true)); } public void DecreaseDepth (ref Gtk.TextIter start) { if (!CanMakeBulletedList()) return; Gtk.TextIter end; start = GetIterAtLineOffset (start.Line, 0); Gtk.TextIter line_end = start; line_end.ForwardToLineEnd (); if (line_end.LineOffset < 2 || start.EndsLine()) { end = start; } else { end = GetIterAtLineOffset (start.Line, 2); } DepthNoteTag curr_depth = FindDepthTag (start); Undoer.FreezeUndo (); if (curr_depth != null) { // Remove the previous indent Delete (ref start, ref end); // Insert the indent at the new depth int nextDepth = curr_depth.Depth - 1; if (nextDepth != -1) { InsertBullet (ref start, nextDepth, curr_depth.Direction); } } Undoer.ThawUndo (); ChangeTextDepth (this, new ChangeDepthEventArgs (start.Line, false)); } public DepthNoteTag FindDepthTag (Gtk.TextIter iter) { DepthNoteTag depth_tag = null; foreach (Gtk.TextTag tag in iter.Tags) { if (NoteTagTable.TagHasDepth (tag)) { depth_tag = (DepthNoteTag) tag; break; } } return depth_tag; } } public class ChangeDepthEventArgs : EventArgs { int line; bool direction; public int Line { get {return line; } } public bool Direction { get {return direction; } } public ChangeDepthEventArgs (int line, bool direction) { this.line = line; this.direction = direction; } } public delegate void ChangeDepthHandler (object o, ChangeDepthEventArgs args); public class InsertBulletEventArgs : EventArgs { int offset; int line; int depth; Pango.Direction direction; public int Offset { get {return offset; } } public int Line { get {return line; } } public int Depth { get {return depth; } } public Pango.Direction Direction { get {return direction; } } public InsertBulletEventArgs (int offset, int depth, Pango.Direction direction) { this.offset = offset; this.depth = depth; this.direction = direction; } } public delegate void NewBulletHandler (object o, InsertBulletEventArgs args); public class NoteBufferArchiver { public static string Serialize (Gtk.TextBuffer buffer) { return Serialize (buffer, buffer.StartIter, buffer.EndIter); } public static string Serialize (Gtk.TextBuffer buffer, Gtk.TextIter start, Gtk.TextIter end) { StringWriter stream = new StringWriter (); XmlTextWriter xml = new XmlTextWriter (stream); Serialize (buffer, start, end, xml); xml.Close (); return stream.ToString (); } static void WriteTag (Gtk.TextTag tag, XmlTextWriter xml, bool start) { NoteTag note_tag = tag as NoteTag; if (note_tag != null) { note_tag.Write (xml, start); } else if (NoteTagTable.TagIsSerializable (tag)) { if (start) xml.WriteStartElement (null, tag.Name, null); else xml.WriteEndElement (); } } static bool TagEndsHere (Gtk.TextTag tag, Gtk.TextIter iter, Gtk.TextIter next_iter) { return (iter.HasTag (tag) && !next_iter.HasTag (tag)) || next_iter.IsEnd; } // This is taken almost directly from GAIM. There must be a // better way to do this... public static void Serialize (Gtk.TextBuffer buffer, Gtk.TextIter start, Gtk.TextIter end, XmlTextWriter xml) { Stack tag_stack = new Stack (); Stack replay_stack = new Stack (); Stack continue_stack = new Stack (); Gtk.TextIter iter = start; Gtk.TextIter next_iter = start; next_iter.ForwardChar (); bool line_has_depth = false; int prev_depth_line = -1; int prev_depth = -1; xml.WriteStartElement (null, "note-content", null); xml.WriteAttributeString ("version", "0.1"); // Insert any active tags at start into tag_stack... foreach (Gtk.TextTag start_tag in start.Tags) { if (!start.TogglesTag (start_tag)) { tag_stack.Push (start_tag); WriteTag (start_tag, xml, true); } } while (!iter.Equal (end) && iter.Char != null) { bool new_list = false; DepthNoteTag depth_tag = ((NoteBuffer)buffer).FindDepthTag (iter); // If we are at a character with a depth tag we are at the // start of a bulleted line if (depth_tag != null && iter.StartsLine()) { line_has_depth = true; if (iter.Line == prev_depth_line + 1) { // Line part of existing list if (depth_tag.Depth == prev_depth) { // Line same depth as previous // Close previous xml.WriteEndElement (); } else if (depth_tag.Depth > prev_depth) { // Line of greater depth xml.WriteStartElement (null, "list", null); for (int i = prev_depth + 2; i <= depth_tag.Depth; i++) { // Start a new nested list xml.WriteStartElement (null, "list-item", null); xml.WriteStartElement (null, "list", null); } } else { // Line of lesser depth // Close previous // and nested s xml.WriteEndElement (); for (int i = prev_depth; i > depth_tag.Depth; i--) { // Close nested xml.WriteEndElement (); // Close xml.WriteEndElement (); } } } else { // Start of new list xml.WriteStartElement (null, "list", null); for (int i = 1; i <= depth_tag.Depth; i++) { xml.WriteStartElement (null, "list-item", null); xml.WriteStartElement (null, "list", null); } new_list = true; } prev_depth = depth_tag.Depth; // Start a new WriteTag (depth_tag, xml, true); } // Output any tags that begin at the current position foreach (Gtk.TextTag tag in iter.Tags) { if (iter.BeginsTag (tag)) { if (!(tag is DepthNoteTag) && NoteTagTable.TagIsSerializable(tag)) { WriteTag (tag, xml, true); tag_stack.Push (tag); } } } // Reopen tags that continued across indented lines // or into or out of lines with a depth while (continue_stack.Count > 0 && ((depth_tag == null && iter.StartsLine ()) || iter.LineOffset == 1)) { Gtk.TextTag continue_tag = (Gtk.TextTag) continue_stack.Pop(); if (!TagEndsHere (continue_tag, iter, next_iter) && iter.HasTag (continue_tag)) { WriteTag (continue_tag, xml, true); tag_stack.Push (continue_tag); } } // Hidden character representing an anchor if (iter.Char[0] == (char) 0xFFFC) { Logger.Log ("Got child anchor!!!"); if (iter.ChildAnchor != null) { string serialize = (string) iter.ChildAnchor.Data ["serialize"]; if (serialize != null) xml.WriteRaw (serialize); } } else if (depth_tag == null) { xml.WriteString (iter.Char); } bool end_of_depth_line = line_has_depth && next_iter.EndsLine (); bool next_line_has_depth = false; if (iter.Line < buffer.LineCount - 1) { Gtk.TextIter next_line = buffer.GetIterAtLine(iter.Line+1); next_line_has_depth = ((NoteBuffer)buffer).FindDepthTag (next_line) != null; } bool at_empty_line = iter.EndsLine () && iter.StartsLine (); if (end_of_depth_line || (next_line_has_depth && (next_iter.EndsLine () || at_empty_line))) { // Close all tags in the tag_stack while (tag_stack.Count > 0) { Gtk.TextTag existing_tag = tag_stack.Pop () as Gtk.TextTag; // Any tags which continue across the indented // line are added to the continue_stack to be // reopened at the start of the next if (!TagEndsHere (existing_tag, iter, next_iter)) { continue_stack.Push (existing_tag); } WriteTag (existing_tag, xml, false); } } else { foreach (Gtk.TextTag tag in iter.Tags) { if (TagEndsHere (tag, iter, next_iter) && NoteTagTable.TagIsSerializable(tag) && !(tag is DepthNoteTag)) { while (tag_stack.Count > 0) { Gtk.TextTag existing_tag = tag_stack.Pop () as Gtk.TextTag; if (!TagEndsHere (existing_tag, iter, next_iter)) { replay_stack.Push (existing_tag); } WriteTag (existing_tag, xml, false); } // Replay the replay queue. // Restart any tags that // overlapped with the ended // tag... while (replay_stack.Count > 0) { Gtk.TextTag replay_tag = replay_stack.Pop () as Gtk.TextTag; tag_stack.Push (replay_tag); WriteTag (replay_tag, xml, true); } } } } // At the end of the line record that it // was the last line encountered with a depth if (end_of_depth_line) { line_has_depth = false; prev_depth_line = iter.Line; } // If we are at the end of a line with a depth and the // next line does not have a depth line close all // and tags that remain open if (end_of_depth_line && !next_line_has_depth) { for (int i = prev_depth; i > -1; i--) { // Close xml.WriteFullEndElement (); // Close xml.WriteFullEndElement (); } prev_depth = -1; } iter.ForwardChar (); next_iter.ForwardChar (); } // Empty any trailing tags left in tag_stack.. while (tag_stack.Count > 0) { Gtk.TextTag tail_tag = (Gtk.TextTag) tag_stack.Pop (); WriteTag (tail_tag, xml, false); } xml.WriteEndElement (); // } class TagStart { public int Start; public Gtk.TextTag Tag; } public static void Deserialize (Gtk.TextBuffer buffer, string content) { Deserialize (buffer, buffer.StartIter, content); } public static void Deserialize (Gtk.TextBuffer buffer, Gtk.TextIter start, string content) { StringReader reader = new StringReader (content); XmlTextReader xml = new XmlTextReader (reader); xml.Namespaces = false; Deserialize (buffer, buffer.StartIter, xml); } public static void Deserialize (Gtk.TextBuffer buffer, Gtk.TextIter start, XmlTextReader xml) { int offset = start.Offset; Stack stack = new Stack (); TagStart tag_start; NoteTagTable note_table = buffer.TagTable as NoteTagTable; int curr_depth = -1; // A stack of boolean values which mark if a // list-item contains content other than another list Stack list_stack = new Stack(); while (xml.Read ()) { switch (xml.NodeType) { case XmlNodeType.Element: if (xml.Name == "note-content") break; tag_start = new TagStart (); tag_start.Start = offset; if (note_table != null && note_table.IsDynamicTagRegistered (xml.Name)) { tag_start.Tag = note_table.CreateDynamicTag (xml.Name); } else if (xml.Name == "list") { curr_depth++; break; } else if (xml.Name == "list-item") { if (curr_depth >= 0) { if (xml.GetAttribute ("dir") == "rtl") { tag_start.Tag = note_table.GetDepthTag (curr_depth, Pango.Direction.Rtl); } else { tag_start.Tag = note_table.GetDepthTag (curr_depth, Pango.Direction.Ltr); } list_stack.Push (false); } else { Logger.Error(" tag mismatch"); } } else { tag_start.Tag = buffer.TagTable.Lookup (xml.Name); } if (tag_start.Tag is NoteTag) { ((NoteTag) tag_start.Tag).Read (xml, true); } stack.Push (tag_start); break; case XmlNodeType.Text: case XmlNodeType.Whitespace: case XmlNodeType.SignificantWhitespace: Gtk.TextIter insert_at = buffer.GetIterAtOffset (offset); buffer.Insert (ref insert_at, xml.Value); offset += xml.Value.Length; // If we are inside a mark off // that we have encountered some content if (list_stack.Count > 0) { list_stack.Pop (); list_stack.Push (true); } break; case XmlNodeType.EndElement: if (xml.Name == "note-content") break; if (xml.Name == "list") { curr_depth--; break; } tag_start = (TagStart) stack.Pop (); if (tag_start.Tag == null) break; Gtk.TextIter apply_start, apply_end; apply_start = buffer.GetIterAtOffset (tag_start.Start); apply_end = buffer.GetIterAtOffset (offset); if (tag_start.Tag is NoteTag) { ((NoteTag) tag_start.Tag).Read (xml, false); } // Insert a bullet if we have reached a closing // tag, but only if the // had content. DepthNoteTag depth_tag = tag_start.Tag as DepthNoteTag; if (depth_tag != null && (bool) list_stack.Pop ()) { ((NoteBuffer) buffer).InsertBullet (ref apply_start, depth_tag.Depth, depth_tag.Direction); buffer.RemoveAllTags (apply_start, apply_start); offset += 2; } else if(depth_tag == null) { buffer.ApplyTag (tag_start.Tag, apply_start, apply_end); } break; default: Logger.Log ("Unhandled element {0}. Value: '{1}'", xml.NodeType, xml.Value); break; } } } } }