// 
// Hiero
// Copyright (c) 2015  Barry Block 
// 
// 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 3 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, see <http://www.gnu.org/licenses/>. 
//

using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Xml;
using System.IO;
using Gtk;
using Gdk;

namespace Hiero
{
   public partial class MainWindow: Gtk.Window
   {
      static Dictionary<string, Hiero.Document> docLookup;
         // Dictionary to look-up the active document from the current notebook tab.
      static string appAuthor = "Barry W. Block";
         // The applications author.
      string appVers = "1.2"; // TODO: Correct version?
         // The applications version.
      string appCopyright = "\u00a9 2015 " + appAuthor;
         // The applications copyright.
      string appSlogan = "An outliner for Linux";
         // The applications slogan.
      string appUserGuide = "userguide.hro";
         // The applications user guide file.
      string documentExt = "hro";
         // The application's documents are saved with this extension.
      string indentedTextFileExt = "itf";
         // File extension for tabbed files (indented text format).
      string xmlExt = "xml";
         // File extension for XML files.
      string sessionFile = MainClass.appFolder + "/Session.txt";
         // The file containing the session data.
      string configFile = MainClass.appFolder + "/" + MainClass.appName + "Conf.xml";
         // The app's configuration file.
      string defaultTextFont = "FreeSans 10";
         // The default text font used for the text editor.
      string editTextFont = "FreeSans 10";
         // The font used for the text editor.
      string textEditorHighlightColor = "#FFFF00";
         // The background color used to display search results in the text editor.
      bool raiseEditorOnAddNode;
         // Determines whether editor dialog is raised when child or sibling node is added.
      int textEditorDialogWidth = 500;
         // Width of the text editor dialog window.
      int textEditorDialogHeight = 100;
         // Height of the text editor dialog window.
      bool hintsEnabled;
         // Determines whether UI hints are enabled.
      Gtk.Window mainWin;
         // Reference to the main window constructed by Gtk.Builder.
      Hiero.Clipboard clipboard;
         // The app's clipboard instance.
      Pixbuf dragPixbuf;
         // This is the icon for the node being dragged.
      ToolItem searchToolbarItem;
         // Needed so that a toolbar can contain an entry widget.
      Entry searchEntry;
         // The search entry.
      ToolButton searchButton;
         // The button for initiating a search.
      string lastFolder = string.Empty;
         // The last folder where a file was opened or saved.
      TargetEntry[] notebookAcceptedDropTypes = new TargetEntry[] { new TargetEntry("text/uri-list", 0, 0) };
         // The only media type allowed for dropping onto the notebook widget is an outline document file. 
      string keystrokeQueue;
         // Accumulates keystrokes typed on an outline.
      string assyFolder;
         // The folder that the app's executable is running from.
      Logger appLog;
         // The app's logger instance.
      Label statusbarStatLabel;
         // The status bar's statistics label.
      Label statusbarMesgLabel;
         // The status bar's text message label.
      Session session;
         // Object for managing list of open documents.

      // This constructor is called from Program.cs.
      public MainWindow(string[] args) :this() 
      {
         for (int i=0; i < args.Length; i++)
         {
            if (args[i].Substring(0, 2) != "--")
            {
               // It's an outline file to be loaded.

               FileOpen(args[i], string.Empty, false);
            }
            else
            {
               // It's a switch argument.

               if ((args[i].Length > 11) && (args[i].ToUpper().Substring(0, 11) == "--TESTUNDO=")) 
               {
                  // Testing "undo" logic. Get the command file:
   
                  string cmdFile = args[i].Substring(11);
                  Console.WriteLine("Cmd File: " + System.IO.Path.GetFileName(cmdFile));

                  // Get the command file's absolute path:

                  cmdFile = System.IO.Path.GetFullPath(cmdFile);

                  // Run the command file on the most recent loaded document:

                  Document theDoc = ActiveDocument;
                  if (theDoc != null)
                  {
                     // Load and execute the commands:

                     theDoc.RunCommandsFromFile(cmdFile, false);
                  }

                  Console.WriteLine("Undoing commands... ");

                  // Undo all of the edits made:

                  while (theDoc.UndoCount > 0) theDoc.Undo(false);

                  // Set the resulting outline's file name based on the original file name
                  // and then remove the old path from the document look up:

                  string newPathname = System.IO.Path.GetFileNameWithoutExtension(cmdFile) + "." + documentExt;
                  docLookup.Remove(theDoc.Pathname);
   
                  // Update the document itself:
   
                  theDoc.Pathname = newPathname; 
                  theDoc.ReadOnly = false;
   
                  // Add the new "tab to document" look-up key:
   
                  Gtk.Box theBox = (Gtk.Box) docNotebook.GetNthPage(docNotebook.CurrentPage);  
                  ((Gtk.Label) theBox.Children[1]).Text = theDoc.Pathname;
                  ActiveDocument = theDoc;
   
                  // Save the outline to the new file:
   
                  theDoc.Save();
   
                  // Update the log:
   
                  appLog.Write("Outline saved as: " + TabLabelText);

                  // Close the outline:

                  OnFileCloseAction(new object(), new EventArgs());

                  // Update the GUI:
   
                  UpdateControls();
               }
            }
         }
      }
   
      // The default constructor.
      public MainWindow() : base(Gtk.WindowType.Toplevel)
      {
         Build();

         // Set up the status bar:

         theStatusbar.Remove(theStatusbar.Children[0]);
         statusbarStatLabel = new Label();
         statusbarMesgLabel = new Label();
         theStatusbar.PackStart(statusbarStatLabel, false, false, 2);
         theStatusbar.Add(statusbarMesgLabel);
         theStatusbar.ShowAll();

         // Add the search components to the toolbar:

         searchToolbarItem = new ToolItem();
         searchEntry = new Entry();
         searchEntry.KeyReleaseEvent += OnSearchEntryKeyReleased;
         searchEntry.Changed += OnSearchEntryChanged;
         searchToolbarItem.Add(searchEntry);

         searchButton = new ToolButton(Stock.Find);
         searchButton.Clicked += OnSearchButtonClicked;

         theToolbar.Add(searchToolbarItem);
         theToolbar.Add(searchButton);
         theToolbar.ShowAll();

         mainWin = this;

         // Set up needed folders, etc.:

         assyFolder = System.IO.Path.GetDirectoryName(typeof(MainWindow).Assembly.Location);

         // Create the logger:

         appLog = new Logger(MainClass.logFolder);

         // Load the app's configuration:

         LoadConfiguration();
      
         // Write start-up message to log:

         appLog.Write(MainClass.appName + " start up");

         // Create the document "look-up":

         docLookup = new Dictionary<string, Hiero.Document>();

         // Create the internal clipboard:

         clipboard = new Hiero.Clipboard();

         // Load the drag icon resource:

         dragPixbuf = new Pixbuf(Assembly.GetAssembly(typeof(Hiero.MainClass)),"Hiero.dragicon.png");

         // Set up notebook for dropping of files:

         Gtk.Drag.DestSet(docNotebook, DestDefaults.All, notebookAcceptedDropTypes, Gdk.DragAction.Copy);

         // Connect up the notebook event handlers:

         docNotebook.SwitchPage += OnDocNotebookSwitchPage;
         docNotebook.DragDataReceived += OnNotebookDragDataReceived;
         
         // Create session admin object:
         
         session = new Session(sessionFile); 
         
         if (File.Exists(session.FilePath))
         {
            // Load (and possibly recover) any outlines that were previously opened:
            
            string[] paths = File.ReadAllLines(session.FilePath);
            foreach (var path in paths)
            {
               if (File.Exists(path))
               {
                  // The file exists.
                  
                  appLog.Write("Loading outline from prior session: " + path);
            
                  if (path.Substring(0, MainClass.newOutlinesFolder.Length) == MainClass.newOutlinesFolder)
                  {
                     // It's the log for a "new" outline. Recover the outline:
      
                     Document theDoc = ConstructTab(string.Empty);
                     theDoc.New();
                     theDoc.CmdLog.Path = path;
                     theDoc.RunCommandsFromFile(path, false);
                     session.Save(SessionData);
                     SaveCrashFiles(path, string.Empty);
                     appLog.Write("New outline recovered.");
                  }
                  else
                  {
                     // It's an "open" outline. Open it:
                     
                     FileOpen(path, string.Empty, true);
                     Document theDoc = GetDocumentFromKey(path);
                     if (theDoc.CmdLog.Exists)
                     {
                        // A log exists for this outline. Recover the outline:
                        
                        string logFile = Log.GetLogFromPath(path);
                        theDoc.RunCommandsFromFile(logFile, false);
                        SaveCrashFiles(logFile, path);
                        appLog.Write("Open outline recovered.");
                     }
                  }
               }
               else
               {
                  // Oops!
               
                  appLog.Write("Path in session list not found: \"" + path + "\".");
                  if (path.IndexOf(MainClass.newOutlinesFolder) != 0)
                  {
                     // The missing file is an "open" outline. Ensure that it has no log
                     // otherwise it could cause problems later if an outline of the same
                     // name and location was created and subsequently underwent recovery:
                     
                     string tempPath = Log.GetLogFromPath(path);
                     if (File.Exists(tempPath)) 
                     {
                        // Delete the log:
                        
                        File.Delete(tempPath);
                        appLog.Write("Orphaned log deleted: " + tempPath);
                     }
                  }
               }
            }
         }
         
         if (MainClass.firstRun)
         {
            DispMesg("Welcome and thanks for trying out " + MainClass.appName + "! I hope you enjoy " +
            "using it. Let me just pop in here to help you get started using the application. First, you'll find access to " +
            MainClass.appName + "'s user guide right up there on the Help menu. As the user guide loads the same as any other " + MainClass.appName + 
            " outline though, it might help to know the basics of how to navigate a " + MainClass.appName + " outline. Well, it's " +
            "really quite simple as you only need to use the arrow keys to get around: \n\n" +
            "LEFT\t- positions to the selected node's parent\nRIGHT\t- expands the selected node's immediate descendants\n" +
            "UP\t\t- positions to the prior row\nDOWN\t- positions to the next row\n\n" + 
            "For the full scoop on navigating outlines see the section, \"Keyboard Shortcuts\" in the user guide.");

            // Load the user guide:

            FileOpen(assyFolder + "/" + appUserGuide, string.Empty, false);
         }

         // Update the GUI:

         UpdateControls();
      }

      // Saves crash files to the application's "Crashes" folder for later review.
      private void SaveCrashFiles(string logPath, string docPath)
      {
         string dstPath;
         string timeStr = System.DateTime.Now.ToString("yyyyMMddHHmmss");
         
         bool crashFolderExists = System.IO.Directory.Exists(MainClass.crashFolder);
         if (!crashFolderExists)
         {
            // Create the crash folder:
            
            System.IO.Directory.CreateDirectory(MainClass.crashFolder);
         }
         
         bool logExists = System.IO.File.Exists(logPath);
         if (logExists)
         {
            // Copy the log file:
            
            dstPath = MainClass.crashFolder + "/" +
            System.IO.Path.GetFileNameWithoutExtension(logPath) + 
            "_" + timeStr + ".log";
            File.Copy(logPath, dstPath);
            appLog.Write("Saved crash file: " + dstPath);
         }
   
         bool docExists = System.IO.File.Exists(docPath);
         if (docExists)
         {      
            // Copy the document file:
            
            dstPath = MainClass.crashFolder + "/" +
            System.IO.Path.GetFileNameWithoutExtension(docPath) + 
            "_" + timeStr + "." + documentExt;
            File.Copy(docPath, dstPath);
            appLog.Write("Saved crash file: " + dstPath);
         }
      }

      // Executes the commands that are stored in a text file. WARNING: This method
      // is only intended to be called from MainWindow.OnOutlineKeyPressEvent(). 
      // Unlike all other calls made elsewhere to RunCommandsFromFile(), this method
      // passes "true" for the "logging" parameter.
      private void RunCommands() 
      {
         // Display a file open dialog:

         FileChooserDialog FileOpenDlg = new FileChooserDialog(
         "Choose file to open", mainWin, FileChooserAction.Open,
         "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);

         FileFilter filter = new FileFilter();
         filter.Name = "Log files (*.log)";
         filter.AddMimeType("text/log");
         filter.AddPattern("*.log");
         FileOpenDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileOpenDlg.AddFilter(filter);

         if (lastFolder != string.Empty)
         {
            FileOpenDlg.SetCurrentFolder(lastFolder);
         }

         try
         {
            if (FileOpenDlg.Run() == (int) ResponseType.Accept)
            {
               if (FileOpenDlg.Filename != null)
               {
                  Document theDoc = ActiveDocument;
                  if (theDoc != null)
                  {
                     // Load and execute the commands:

                     theDoc.RunCommandsFromFile(FileOpenDlg.Filename, true);

                     // Record the folder where the file was opened:
            
                     if (FileOpenDlg.CurrentFolder != string.Empty)
                     {
                        lastFolder = FileOpenDlg.CurrentFolder;
                     }
                  }
               }
               else
                  DispMesg("No file was chosen.");
            }
         }

         finally
         {
            FileOpenDlg.Destroy();
         }
      }

      // Gets/sets whether hints are enabled in the user interface.
      private bool HintsEnabled
      {
         get 
         { 
            return hintsEnabled; 
         }

         set 
         {
            hintsEnabled = value;
            if (hintsEnabled)
            {
               newAction.Tooltip = "creates a new outline (Ctrl+N)";
               openAction.Tooltip = "opens an existing outline (Ctrl+O)";
               saveAction.Tooltip = "saves changes made to outline (Ctrl+S)";
               saveAsAction.Tooltip = "saves changes as another file";
               FromIndentedTextFileAction.Tooltip = "imports data from " + MainClass.appName + " indented text formatted file";
               FromXML_FileAction.Tooltip = "imports data from " + MainClass.appName + " XML file";
               ToFlatTextFileAction.Tooltip = "exports data to non-indented text file";
               ToIndentedTextFileAction.Tooltip = "exports data to " + MainClass.appName + " indented text formatted file";
               ToXMLFileAction.Tooltip = "exports data to " + MainClass.appName + " XML file";
               PropertiesAction.Tooltip = "allows you to edit outline properties";
               closeAction.Tooltip = "closes the outline/tab";
               quitAction.Tooltip = "exits the application";
               UndoAction.Tooltip = "undoes the last action (Ctrl+Z)";
               RedoAction.Tooltip = "re-does the last undone action (Ctrl+Y)";
               CutAction.Tooltip = "cuts the node";
               CopyAction.Tooltip = "copies the node";
               PasteAction.Tooltip = "pastes the previously cut/copied node";
               DeleteAction.Tooltip = "deletes the node";
               CrossmarkNodeAction.Tooltip = "adds crossmark to node's text";
               CheckmarkNodeAction.Tooltip = "adds checkmark to node's text";
               ReduceNewlinesAction.Tooltip = "replaces multiple newlines with single ones";
               NormalizeNewlinesAction.Tooltip = "reduces newlines and then doubles them";
               NewlinesToDoubleNewlinesAction.Tooltip = "replaces newlines with double newlines";
               NewlinesToSpacesAction.Tooltip = "replaces newlines with spaces";
               AddChildNodeAction.Tooltip = "adds new child node (F3)";
               AddSiblingNodeAction.Tooltip = "adds new sibling node (F2)";
               ExtractNodeAction.Tooltip = "extracts the node (Ctrl+E)";
               ReparentNodeAction.Tooltip = "inserts new parent node";
               JoinNodeAction.Tooltip = "joins node to prior sibling";
               SplitNodeAction.Tooltip = "splits the node by paragraphs";
               MoveNodeUpAction.Tooltip = "moves the node up (Ctrl+Up)";
               MoveNodeDownAction.Tooltip = "moves the node down (Ctrl+Down)";
               PromoteNodeAction.Tooltip = "promotes the node (Ctrl+Left)";
               DemoteNodeAction.Tooltip = "Demotes the node (Ctrl+Right)";
               EditTextAction.Tooltip = "allows editing of the node's text (Return)";
               PreferencesAction.Tooltip = "allows editing of user preferences";
               ExpandNodeAction.Tooltip = "makes the node's children visible";
               CollapseNodeAction.Tooltip = "hides the node's children";
               ExpandAllAction.Tooltip = "fully expands the outline";
               CollapseAllAction.Tooltip = "fully collapses the outline";
               FindAction.Tooltip = "allows searching for text";
               FindNextAction.Tooltip = "locates next search match";
               AboutAction.Tooltip = "displays info about application";
               searchEntry.TooltipText = "text to find in outline";
               searchButton.TooltipText = "finds the specified text (Ctrl+F/G)";
               statusbarStatLabel.TooltipText = "[char count, word count]";
            }
            else
            {
               ActionGroup group = this.UIManager.ActionGroups[0];
               Gtk.Action[] actions = group.ListActions();

               for (int i = 0; i < actions.Length; i++)
               {
                  actions[i].Tooltip = string.Empty;
               }

               searchEntry.TooltipText = string.Empty;
               searchButton.TooltipText = string.Empty;
               statusbarStatLabel.TooltipText = string.Empty;
            }
         }
      }

      // Gets/sets the active document.
      private Document ActiveDocument
      {
         get
         {
            if (docNotebook.NPages > 0)
            {
               Document theDoc;
               Gtk.Box theBox = (Gtk.Box) docNotebook.GetNthPage(docNotebook.CurrentPage); 
               string theKey = ((Gtk.Label) theBox.Children[1]).Text;

               if (docLookup.TryGetValue(theKey, out theDoc))
                  return theDoc;
               else 
                  return null;
            }
            else
               return null;
         }

         set
         {
            Gtk.Box theBox = (Gtk.Box) docNotebook.GetNthPage(docNotebook.CurrentPage);  
            string theKey = ((Gtk.Label) theBox.Children[1]).Text;
            docLookup.Add(theKey, value);
         }
      }

      // Gets the current notebook tab's label text.
      private string TabLabelText
      {
         get
         {
            int i = docNotebook.CurrentPage;
            Gtk.Box theBox = (Gtk.Box) docNotebook.GetNthPage(i);
            string tabLabelText = docNotebook.GetTabLabelText(theBox);
            return tabLabelText;
         }
      }

      // Deletes the currently selected node if the action is allowed by user.
      private void DeleteSelectedNode()
      {
         MessageDialog WarningMesgDlg = new MessageDialog (this, 
         DialogFlags.DestroyWithParent, MessageType.Warning, 
         ButtonsType.YesNo, "Ok to delete node?");

         try
         {
            if (WarningMesgDlg.Run() == (int) ResponseType.Yes)
            {
               Document theDoc = ActiveDocument;
               TreeIter selectedNode;
               theDoc.Tree.Selection.GetSelected(out selectedNode);
               theDoc.Do(new DeleteNodeCommand(theDoc, selectedNode), true);
               UpdateControls();
            }
         }
         
         finally
         {
            WarningMesgDlg.Destroy();
         }
      }

      // Returns true if the given node is the first one in the sibling order.
      private bool FirstChild(TreeIter theNode, OutlineView theTree)
      {
         TreeStore model = (TreeStore) theTree.Model;
         TreePath thePath = model.GetPath(theNode);
         return !thePath.Prev();
      }

      // Returns true if the given node is the last one in the sibling order.
      private bool LastChild(TreeIter theNode, OutlineView theTree)
      {
         TreeIter nextNode = theNode;
         TreeStore model = (TreeStore) theTree.Model;
         return !model.IterNext(ref nextNode);
      }

      // Updates the GUI's controls.
      private void UpdateControls()
      {
         Document theDoc = ActiveDocument;
         TreeStore model = null;
         TreeIter selectedNode = TreeIter.Zero;

         bool docExists = theDoc != null;

         if (docExists)
         {
            model = (TreeStore) theDoc.Tree.Model;
            theDoc.Tree.Selection.GetSelected(out selectedNode);
         }

         bool nodeSelected = docExists && (theDoc.Tree.Selection.CountSelectedRows() == 1);
         bool treeHasFocus = docExists && theDoc.Tree.HasFocus;
         bool docExistsAndIsntReadOnly = docExists && !theDoc.ReadOnly;

         saveAction.Sensitive = docExistsAndIsntReadOnly; 
         saveAsAction.Sensitive = docExists;
         ImportAction.Sensitive = nodeSelected;
         exportAction.Sensitive = nodeSelected;
         PropertiesAction.Sensitive = docExists;
         closeAction.Sensitive = docNotebook.NPages > 0; 

         UndoAction.Sensitive = docExistsAndIsntReadOnly && (theDoc.UndoCount > 0);
         RedoAction.Sensitive = docExistsAndIsntReadOnly && (theDoc.RedoCount > 0);

         bool canEditNode = treeHasFocus && nodeSelected && docExistsAndIsntReadOnly;

         CutAction.Sensitive = canEditNode && !Command.IsRoot(model, selectedNode);
         CopyAction.Sensitive = canEditNode;
         PasteAction.Sensitive = canEditNode && (clipboard.operation != CutCopyType.NONE);
         DeleteAction.Sensitive = canEditNode && !Command.IsRoot(model, selectedNode);

         AddChildNodeAction.Sensitive = canEditNode;
         AddSiblingNodeAction.Sensitive = canEditNode && !Command.IsRoot(model, selectedNode);

         ExtractNodeAction.Sensitive = treeHasFocus && nodeSelected && ((!Command.IsRoot(model, selectedNode) && 
         (model.IterNChildren(selectedNode) > 0)) || (Command.IsRoot(model, selectedNode) && 
         (model.IterNChildren(selectedNode) == 1))) && docExistsAndIsntReadOnly;

         ReparentNodeAction.Sensitive = canEditNode;
         JoinNodeAction.Sensitive = canEditNode && !FirstChild(selectedNode, theDoc.Tree);
         SplitNodeAction.Sensitive = canEditNode && (!Command.IsRoot(model, selectedNode));

         MoveNodeUpAction.Sensitive = canEditNode && !FirstChild(selectedNode, theDoc.Tree);
         MoveNodeDownAction.Sensitive = canEditNode && !LastChild(selectedNode, theDoc.Tree);
         PromoteNodeAction.Sensitive = canEditNode && (model.IterDepth(selectedNode) > 1);
         DemoteNodeAction.Sensitive = canEditNode && !FirstChild(selectedNode, theDoc.Tree);

         SortLevelAction.Sensitive = canEditNode; 

         FormatAction.Sensitive = canEditNode;
         CheckmarkNodeAction.Sensitive = FormatAction.Sensitive;
         CrossmarkNodeAction.Sensitive = FormatAction.Sensitive;
         NewlinesToDoubleNewlinesAction.Sensitive = FormatAction.Sensitive;
         ReduceNewlinesAction.Sensitive = FormatAction.Sensitive;
         NewlinesToSpacesAction.Sensitive = FormatAction.Sensitive;

         EditTextAction.Sensitive = canEditNode;

         searchEntry.Sensitive = docExists; 
         searchButton.Sensitive = docExists && (searchEntry.Text != string.Empty);
         FindAction.Sensitive = docExists;
         FindNextAction.Sensitive = docExists;
         CollapseNodeAction.Sensitive = treeHasFocus && nodeSelected;
         ExpandNodeAction.Sensitive = treeHasFocus && nodeSelected;
         CollapseAllAction.Sensitive = treeHasFocus;
         ExpandAllAction.Sensitive = treeHasFocus;

         if (docNotebook.NPages > 0)
         {
            // Update all tab labels as well as the window's title: 
   
            for (int i = 0; i < docNotebook.NPages; i++)
            {
               Gtk.Box theBox;
               theDoc = GetDocumentFromTabIndex(i, out theBox);
               if (theDoc != null)
               {
                  // Update the tab label:   
   
                  string fileName;                                                
                  if (theDoc.Pathname != string.Empty)
                  {
                     // The document has an associated file.

                     fileName = System.IO.Path.GetFileName(theDoc.Pathname);
                  }
                  else
                  {
                     // The document doesn't yet have an associated file.

                     fileName = docNotebook.GetTabLabelText(theBox);
                     if (fileName[0] == '*') fileName = fileName.Substring(1);
                  }
   
                  if (theDoc.Modified) fileName = '*' + fileName;
                  docNotebook.SetTabLabelText(theBox, fileName);
   
                  // Update the window's title:
         
                  if (i == docNotebook.CurrentPage)
                  {
                     string roTag;
                     roTag = (theDoc.ReadOnly) ? " (read only)" : string.Empty;
                     mainWin.Title = fileName + roTag + " - " + MainClass.appName; 
                  }
               }
            }
         }
         else
         {
            // Update the window's title:

            mainWin.Title = MainClass.appName; 
         }         
      }

      // Returns list of documents currently open in Hiero.
      private string SessionData 
      {
         get 
         {
            string tempStr = string.Empty;
            
            for (int i = 0; i < docNotebook.NPages; i++)
            {
               Gtk.Box theBox;
               Document theDoc = GetDocumentFromTabIndex(i, out theBox);
   
               if ((theDoc.Pathname == String.Empty) && File.Exists(theDoc.CmdLog.Path))
               {
                  // Outline is a "new", unsaved one.
                  
                  tempStr += theDoc.CmdLog.Path + System.Environment.NewLine;
               }
               else
               {
                  // Outline is an "open", previously saved one.
                  
                  tempStr += theDoc.Pathname + System.Environment.NewLine;
               }
            }
            
            return tempStr;
         }
      }

      // Returns a tab's document (and its top-level widget) given the tab's index.
      public Document GetDocumentFromTabIndex(int tabIndex, out Gtk.Box theBox)
      {
         Document theDoc;
         theBox = (Gtk.Box) docNotebook.GetNthPage(tabIndex); 
         string theKey = ((Gtk.Label) theBox.Children[1]).Text;

         if (docLookup.TryGetValue(theKey, out theDoc))
            return theDoc;
         else 
            return null;
      }

      // Generates first available "new file".
      private string NewFileName()
      {
         int i = 1;
         string theKeyBase = "New File ";
         while (docLookup.ContainsKey(theKeyBase + i.ToString()))
         { 
            i++;
         }

         return theKeyBase + i.ToString();
      }

      // Builds a new notebook tab and returns its associated outline object.
      private Document ConstructTab(string filePath)
      {
         // Create and populate new outline document object:

         Document theDoc = new Document(appLog, MainClass.newOutlinesFolder);
         theDoc.Pathname = filePath;

         // Configure the document's Outline widget:

         theDoc.Tree.Selection.Changed += OnOutlineSelectionChanged;
         theDoc.Tree.RowActivated += OnOutlineRowActivated;
         theDoc.Tree.DragBegin += OnOutlineDragBegin;
         theDoc.Tree.DragDataGet += OnOutlineDragDataGet;
         theDoc.Tree.DragDataReceived += OnOutlineDragDataReceived;
         theDoc.Tree.FocusInEvent += OnOutlineFocusIn;
         theDoc.Tree.FocusOutEvent += OnOutlineFocusOut;
         theDoc.Tree.KeyPressEvent += OnOutlineKeyPressEvent;

         theDoc.Tree.Model.RowsReordered += OnRowsReordered;

         // Create and configure the scrolled window widget that will host the 
         // document's Outline widget:

         Gtk.ScrolledWindow theScrolledWindow = new Gtk.ScrolledWindow();
         theScrolledWindow.Visible = true;
         theScrolledWindow.CanFocus = true;
         theScrolledWindow.HscrollbarPolicy = PolicyType.Never;      
         theScrolledWindow.ShadowType = ShadowType.In;
         theScrolledWindow.Add(theDoc.Tree);
         theScrolledWindow.SizeAllocated += OnScrolledWindowAllocated;

         // Create and configure the (to be hidden) document key label:

         string tabLabelText;
         if (filePath != string.Empty)
            tabLabelText = System.IO.Path.GetFileName(filePath);
         else
            tabLabelText = NewFileName();

         Gtk.Label docKeyLabel = new Gtk.Label();   
         if (filePath != string.Empty)
            docKeyLabel.Text = filePath;
         else
            docKeyLabel.Text = tabLabelText;

         // Create the VBox to hold the scrolled window and document key label:

         VBox tabBox = new VBox();  
         tabBox.PackStart(theScrolledWindow, true, true, 0);   
         tabBox.PackStart(docKeyLabel, false, true, 0);

         // Add the new tab to the notebook and display it:

         docNotebook.SwitchPage -= OnDocNotebookSwitchPage;
         Label theLabel = new Label(tabLabelText);
         int pageIndex = docNotebook.AppendPage(tabBox, theLabel);
         docNotebook.SetTabReorderable(tabBox, true);
         tabBox.ShowAll();                                   // Must do this ...
         docKeyLabel.Hide();

         // Select the new page:

         docNotebook.CurrentPage = pageIndex;                // ... before this.
         docNotebook.SwitchPage += OnDocNotebookSwitchPage;

         // Attach the document to the tab:

         ActiveDocument = theDoc;
         return theDoc;
      }

      // Returns true if the specified outline is already open.
      private bool OutlineIsAlreadyOpen(string filePath, out int docIndex)
      {
         docIndex = -1;

         for (int i = 0; i < docNotebook.NPages; i++)
         {
            Gtk.Box theBox = (Gtk.Box) docNotebook.Children[i];
            Gtk.Label docKeyLabel = (Gtk.Label) theBox.Children[1];

            if (docKeyLabel.Text == filePath)
            {
               docIndex = i;
               return true;
            }
         }
         return false;
      }

      private void DispMesg(string mesg)
      {
         MessageDialog notifyMesgDlg = new MessageDialog (mainWin, DialogFlags.DestroyWithParent, 
         MessageType.Info, ButtonsType.Ok, mesg);

         try
         {
            notifyMesgDlg.Run();
         }

         finally
         {
            notifyMesgDlg.Destroy();
         }
      }

      // Given an outline widget, this method returns its associated document. 
      // Note: This lookup is well suited for use in Outline event handlers 
      // as it isn't dependent on a call to ActiveDocument().
      private static bool GetDocumentFromTree(OutlineView theTree, out Document theDoc)
      {
         ScrolledWindow sw = (ScrolledWindow) theTree.Parent;
         Box theBox = (VBox) sw.Parent;
         Label theLabel = (Label) theBox.Children[1];
         string theKey = theLabel.Text;
         return docLookup.TryGetValue(theKey, out theDoc);
      }

      // Returns a document's key given an outline widget.
      private static string GetKeyFromTree(OutlineView theTree)
      {
         ScrolledWindow sw = (ScrolledWindow) theTree.Parent;
         Box theBox = (VBox) sw.Parent;
         Label theLabel = (Label) theBox.Children[1];
         return theLabel.Text;
      }

      // Returns a document given its key.
      private static Document GetDocumentFromKey(string theKey)
      {
         Document theDoc;
         docLookup.TryGetValue(theKey, out theDoc);
         return theDoc;
      }

      // Warns user about unsaved changes to active document.
      private ResponseType WarnAboutUnsavedChanges()
      {
         // Get the document key:

         Gtk.Box theBox = (Gtk.Box) docNotebook.GetNthPage(docNotebook.CurrentPage); 
         string theKey = ((Gtk.Label) theBox.Children[1]).Text;
         
         Document theDoc;  
         docLookup.TryGetValue(theKey, out theDoc);

         string fileName;                                                
         if (theDoc.Pathname != string.Empty)
            fileName = System.IO.Path.GetFileName(theDoc.Pathname);
         else
            fileName = docNotebook.GetTabLabelText(theBox).Substring(1);

         MessageDialog WarningMesgDlg = new MessageDialog (this, 
         DialogFlags.DestroyWithParent, MessageType.Warning, 
         ButtonsType.None, "Save changes to \"" + fileName + "\"?");
         WarningMesgDlg.AddButton("Cancel", ResponseType.Cancel);
         WarningMesgDlg.AddButton("No", ResponseType.No);
         WarningMesgDlg.AddButton("Yes", ResponseType.Yes);
         WarningMesgDlg.DefaultResponse = ResponseType.Yes;
         ResponseType response = ResponseType.Yes;

         try
         {
            if (theDoc.Modified && ((response = (ResponseType) WarningMesgDlg.Run()) == ResponseType.Yes))
            {
               // Save it:
   
               OnFileSaveAction(new object(), new EventArgs());
            }
   
            if (response != ResponseType.Cancel)
            {
               // Update the log:

               appLog.Write("Outline closed: " + TabLabelText);

               // Closing the outline. Remove the page from the Notebook widget:
      
               docNotebook.RemovePage(docNotebook.CurrentPage);
      
               // Delete the document key from the look-up:
      
               docLookup.Remove(theKey);
               
               // Delete the command log associated with this outline:
               
               theDoc.CmdLog.Delete();
      
               // Update the session:
               
               session.Save(SessionData);

               // Update the GUI:
      
               UpdateControls();
            }
         }
         
         finally
         {
            WarningMesgDlg.Destroy();
         }

         return response;
      }

      private void cutCopyNode(Hiero.CutCopyType operation)
      {
         Document theDoc = ActiveDocument;
         TreeStore model = (TreeStore) theDoc.Tree.Model;
         
         // Get the selected node:

         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Save the information onto the clipboard:

         clipboard.srcDoc = theDoc;
         clipboard.srcNode = selectedNode;
         clipboard.contents = Command.GetSubtreeText(model, selectedNode);
         clipboard.operation = operation;

         // Update the GUI:

         UpdateControls();
      }

      // Copies the subtree of the outline rooted at "srcParent" to the destination denoted by "dstParent". Note that 
      // the source is copied as a CHILD beneath dstParent and therefore when this function is called in the process
      // of node promotion, the root of the resulting subtree (dstChild) is an "uncle" of the original subtree root 
      // (srcParent). In this application then, the function would be called by supplying the selected node as srcParent 
      // and that node's grandparent as dstParent:
      //
      //    CopyNode(srcDoc, dstDoc, selectedNode, grandParent, out newRoot);
      //
      // Note that a subtree can be copied across documents by specifying two distinct document instances. In order to
      // perform a subtree copy in a single document, both instances should be specified the same.
      //
      private static void CopyNode(Document srcDoc, Document dstDoc,
      TreeIter srcParent, TreeIter dstParent, out TreeIter dstChild)    
      {
         TreeStore srcStore = (TreeStore) srcDoc.Tree.Model;
         TreeStore dstStore = (TreeStore) dstDoc.Tree.Model;

         // Process the node:

         string nodeText = (string) srcStore.GetValue(srcParent, 0);
         if (srcDoc == dstDoc)
         {
            // Copying / moving in same outline document.

            bool nodeMatched = (bool) srcStore.GetValue(srcParent, 1);
            dstChild = dstStore.AppendValues(dstParent, nodeText, nodeMatched);
         }
         else
            dstChild = dstStore.AppendValues(dstParent, nodeText, false);

         // Process the child nodes:

         int NoOfChildren = srcStore.IterNChildren(srcParent);
         for (int i = 0; i < NoOfChildren; i++)
         {
            TreeIter srcChild;
            TreeIter dummy;
            srcStore.IterNthChild(out srcChild, srcParent, i);
            CopyNode(srcDoc, dstDoc, srcChild, dstChild, out dummy); 
         }
      }

      // Returns true if the selected node is the sibling following the reference node.
      private bool IsNextSibling(TreeStore model, TreeIter selectedNode, TreeIter refNode)
      {
         TreeIter nextNode = refNode;
         bool Ok = model.IterNext(ref nextNode); 
         return (Ok && selectedNode.Equals(nextNode));
      }

      //----------------------------------------------------------------------------//
      //                             MAINWINDOW EVENTS                              //
      //----------------------------------------------------------------------------//

      protected void OnDeleteEvent(object sender, DeleteEventArgs a)
      {
         while ((docNotebook.NPages > 0) && WarnAboutUnsavedChanges() != ResponseType.Cancel) {}

         if (docNotebook.NPages == 0) 
         {
            appLog.Write(MainClass.appName + " shutdown");
            Application.Quit();
            a.RetVal = false;
         }
         else
            a.RetVal = true;
      }

      private void OnSearchEntryKeyReleased(object o, Gtk.KeyReleaseEventArgs args)
      {
         Gdk.Key theKey = args.Event.Key;
         char theChar = (char) args.Event.KeyValue;
         //Console.WriteLine("KeyValue: " + args.Event.KeyValue);

         bool controlPressed = (args.Event.State & Gdk.ModifierType.ControlMask) != 0;   
         bool altPressed = (args.Event.State & Gdk.ModifierType.Mod1Mask) != 0;            
         bool shiftPressed = (args.Event.State & Gdk.ModifierType.ShiftMask) != 0;
         
         Document theDoc = ActiveDocument;

         if (theDoc != null) 
         {
            if (!altPressed && !controlPressed && !shiftPressed && (theKey == Gdk.Key.Return))
            {
               // An ENTER was pressed. "Click" the search button:

               theDoc.ResetSearch();
               OnSearchButtonClicked(o, args);
            }

            searchButton.Sensitive = searchEntry.Text != string.Empty;
         }
      }

      private void OnSearchEntryChanged(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;

         if (theDoc != null) 
         {
            theDoc.ResetSearch();
         }
      }

      private void OnSearchButtonClicked(object sender, EventArgs args)
      {
         Document theDoc = ActiveDocument;
         TreeStore model = (TreeStore) theDoc.Tree.Model;
         
         TreeIter resultNode;
         bool matched = theDoc.Find(searchEntry.Text, out resultNode);

         if (matched)
         {
            // A node was matched. Get it's parent:

            TreeIter parentNode;
            if (model.IterParent(out parentNode, resultNode))
            {
               // Expand the node's parent:
   
               TreePath parentPath = model.GetPath(parentNode);
               theDoc.Tree.ExpandToPath(parentPath);
            }

            // Set focus to the "result" node:

            TreePath resultPath = model.GetPath(resultNode);
            theDoc.Tree.SetCursor(resultPath, theDoc.Tree.Columns[0], false);
            theDoc.Tree.GrabFocus();
         }
         else
         {
            // There wasn't a match.

            DispMesg("No more matches found.");
            theDoc.ResetSearch();
         }
      }

      private string LoadFont(string fontElemName, XmlElement RootElem)
      {
         XmlElement Elem = RootElem[fontElemName];

         if (Elem != null)
            return Elem.InnerText;
         else
            return defaultTextFont;
      }

      // Loads the app's configuration from disk.
      private void LoadConfiguration() 
      {
         try
         {
            if (!File.Exists(configFile))
            {
               // Configuration file doesn't exist. Create it:
               
               string configContents = 
               "<Config>\n" + 
               "  <HintsEnabled>True</HintsEnabled>\n" + 
               "  <LoggingEnabled>True</LoggingEnabled>\n" + 
               "  <TextEditorFont>FreeSans 10</TextEditorFont>\n" + 
               "  <TextEditorDialogWidth>600</TextEditorDialogWidth>\n" + 
               "  <TextEditorDialogHeight>300</TextEditorDialogHeight>\n" + 
               "</Config>\n";
      
               File.WriteAllText(configFile, configContents);
            }
         
            // Load the configuration file:
            
            XmlDocument XmlDoc = new XmlDocument();
            XmlDoc.Load(configFile);
            XmlElement RootElem = XmlDoc["Config"];
         
            XmlElement Elem;
            Elem = RootElem["HintsEnabled"];
            HintsEnabled = (Elem != null) && (Elem.InnerText.ToUpper() == "TRUE"); 
            
            Elem = RootElem["LoggingEnabled"];
            appLog.Enabled = (Elem != null) && (Elem.InnerText.ToUpper() == "TRUE"); 

            Elem = RootElem["RaiseEditorOnAddNode"];
            raiseEditorOnAddNode = (Elem != null) && (Elem.InnerText.ToUpper() == "TRUE"); 
            
            editTextFont = LoadFont("TextEditorFont", RootElem);

            Elem = RootElem["TextEditorHighlightColor"];
            if (Elem != null) 
            {
               textEditorHighlightColor = Elem.InnerText;
            }

            Elem = RootElem["TextEditorDialogWidth"];
            if (Elem != null)
            {
               textEditorDialogWidth = Convert.ToInt32(Elem.InnerText);
            }

            Elem = RootElem["TextEditorDialogHeight"];
            if (Elem != null)
            {
               textEditorDialogHeight = Convert.ToInt32(Elem.InnerText);
            }
         }
         
         catch (Exception)
         {
            throw new Exception("Could not create/load app. configuration (" + configFile + ").");
         }
      }
   
      // Saves the app's configuration to disk.
      private void SaveConfiguration()
      {
         XmlElement Elem;
   
         XmlDocument XmlDoc = new XmlDocument();
         XmlElement RootElem = XmlDoc.CreateElement("Config");
   
         Elem = XmlDoc.CreateElement("HintsEnabled");
         Elem.InnerText = HintsEnabled.ToString(); 
         RootElem.AppendChild(Elem);
         
         Elem = XmlDoc.CreateElement("LoggingEnabled");
         Elem.InnerText = appLog.Enabled.ToString(); 
         RootElem.AppendChild(Elem);
         
         Elem = XmlDoc.CreateElement("RaiseEditorOnAddNode");
         Elem.InnerText = raiseEditorOnAddNode.ToString(); 
         RootElem.AppendChild(Elem);
         
         Elem = XmlDoc.CreateElement("TextEditorFont");
         Elem.InnerText = editTextFont;
         RootElem.AppendChild(Elem);

         Elem = XmlDoc.CreateElement("TextEditorHighlightColor");
         Elem.InnerText = textEditorHighlightColor;
         RootElem.AppendChild(Elem);
   
         Elem = XmlDoc.CreateElement("TextEditorDialogWidth");
         Elem.InnerText = textEditorDialogWidth.ToString();
         RootElem.AppendChild(Elem);
   
         Elem = XmlDoc.CreateElement("TextEditorDialogHeight");
         Elem.InnerText = textEditorDialogHeight.ToString();
         RootElem.AppendChild(Elem);
   
         XmlDoc.AppendChild(RootElem);
         XmlDoc.Save(configFile);
      }

      // Performs actual opening of the outline document.
      private void FileOpen(string fileName, string currentFolder, bool recovering)
      {
         if (File.Exists(fileName))
         {
            int docIndex;
            if (!OutlineIsAlreadyOpen(fileName, out docIndex))
            {
               // Outline isn't already opened. Build the tab:
   
               Document theDoc = ConstructTab(fileName);
               TreeStore model = (TreeStore) theDoc.Tree.Model;
   
               if (currentFolder != string.Empty)
               {
                  // Record the folder where the file was opened:
      
                  lastFolder = currentFolder;
               }

               if (!recovering)
               {
                  // The outline is NOT being opened as part of recovery/crash handling.
                  
                  string theLog = Log.GetLogFromPath(fileName);
                  if (File.Exists(theLog))
                  {
                     // The outline's log already exists. Rename it and alert the user:
                     
                     string newLog = System.IO.Path.GetDirectoryName(fileName) + "/" +
                     System.IO.Path.GetFileNameWithoutExtension(fileName) + "_" + 
                     System.DateTime.Now.ToString("yyyyMMddHHmmss") + ".log";
                     File.Move(theLog, newLog);
                     DispMesg("The outline being opened has an associated log file which must be " +
                     "removed before proceeding. It has been renamed to:\n\n" +
                     newLog + "\n\nYou might want to examine this file in a text editor in case it " +
                     "contains useful information worth keeping.");
                  }
               }

               // Unsubscribe to some events prior to loading of outline:
   
               model.RowsReordered -= OnRowsReordered;
   
               // Load the contents of the file into the treeview control:
   
               theDoc.Open();
   
               // Re-subscribe to the previously un-subscribed events:
   
               model.RowsReordered += OnRowsReordered;
   
               // Get the root node:
   
               TreeIter theRoot;
               bool Ok = model.GetIterFirst(out theRoot);
               if (Ok)
               {
                  // Set focus to the root node:
   
                  TreePath thePath = model.GetPath(theRoot);
                  theDoc.Tree.SetCursor(thePath, theDoc.Tree.Columns[0], false);
                  theDoc.Tree.GrabFocus();
               }
   
               // Update the session:
               
               session.Save(SessionData);

               // Update the log:
   
               appLog.Write("Outline opened: " + TabLabelText);
            }
            else
            {
               // Outline is already opened! Select it:
   
               if (docIndex > -1)
                  docNotebook.Page = docIndex;
               else
                  DispMesg("Outline should be open but it can't be found!");
            }
   
            // Update GUI:
   
            UpdateControls();
         }
      }

      // Parses the data sent during a drag and drop operation. Used by OnOutlineDragDataReceived(). 
      private void ParseData(string data, out string key, out string pathStr, out string  subtree)
      {
         int delPos1 = data.IndexOf('\n');
         key = data.Substring(0, delPos1);
         int delPos2 = data.IndexOf('\n', delPos1 + 1);
         pathStr = data.Substring(delPos1 + 1, delPos2-delPos1 - 1);
         subtree = data.Substring(delPos2 + 1);
      }

      //----------------------------------------------------------------------------//
      //                                ACTION EVENTS                               //
      //----------------------------------------------------------------------------//

      // FILE MENU //

      private void OnFileNewAction(object o, EventArgs args)
      {
         Document theDoc = ConstructTab(string.Empty);
         theDoc.New();

         // Assign and init the document's command log:

         theDoc.CmdLog.Assign(theDoc.Pathname);
         File.AppendAllText(theDoc.CmdLog.Path, string.Empty);
         
         // Update session:
         
         session.Save(SessionData);

         // Update the log:

         appLog.Write("Outline created: " + TabLabelText);

         // Update the GUI:

         UpdateControls();
      }

      private void OnFileOpenAction(object o, EventArgs args)
      {
         // Display a file open dialog:

         FileChooserDialog FileOpenDlg = new FileChooserDialog(
         "Choose file to open", mainWin, FileChooserAction.Open,
         "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);

         FileFilter filter = new FileFilter();
         filter.Name = MainClass.appName + " files (*." + documentExt + ")";
         filter.AddMimeType("text/" + documentExt);
         filter.AddPattern("*." + documentExt);
         FileOpenDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileOpenDlg.AddFilter(filter);

         if (lastFolder != string.Empty)
         {
            FileOpenDlg.SetCurrentFolder(lastFolder);
         }

         try
         {
            if (FileOpenDlg.Run() == (int) ResponseType.Accept)
            {
               if (FileOpenDlg.Filename != null)
               {
                  FileOpen(FileOpenDlg.Filename, FileOpenDlg.CurrentFolder, false);
               }
               else
                  DispMesg("No file was chosen.");
            }
         }

         finally
         {
            FileOpenDlg.Destroy();
         }
      }

      private void OnFileSaveAction(object o, EventArgs args)
      {
         // Retrieve the outline document:

         Document theDoc = ActiveDocument;

         // Save it:

         if (theDoc.Pathname == string.Empty)
         {
            // The outline hasn't been saved yet and therefore it has no associated 
            // file. Perform a "Save As" to save it under a new name:

            OnFileSaveAsAction(o, args);
         }
         else
         {
            // The outline has an associated file. Save to it:

            theDoc.Save();

            // Update the GUI:

            UpdateControls();

            // Update the session:
            
            session.Save(SessionData);

            // Update the log:

            appLog.Write("Outline saved: " + TabLabelText);
         }
      }

      private void OnFileSaveAsAction(object o, EventArgs args)
      {
         // Retrieve the outline document:

         Document theDoc = ActiveDocument;

         // Create/configure a file save dialog:

         FileChooserDialog  FileSaveDlg = new FileChooserDialog(
         "Choose file to save outline to", mainWin, FileChooserAction.Save,
         "Cancel", ResponseType.Cancel, "Save", ResponseType.Accept);

         FileSaveDlg.DoOverwriteConfirmation = true;

         FileFilter filter = new FileFilter();
         filter.Name = MainClass.appName + " files (*." + documentExt + ")";
         filter.AddMimeType("text/" + documentExt);
         filter.AddPattern("*." + documentExt);
         FileSaveDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileSaveDlg.AddFilter(filter);

         if (lastFolder != string.Empty)
         {
            FileSaveDlg.SetCurrentFolder(lastFolder);
         }

         try
         {
            // Display the file save dialog:

            if (FileSaveDlg.Run() == (int) ResponseType.Accept)
            {
               // Record where the file was saved:

               lastFolder = FileSaveDlg.CurrentFolder;

               // Remove the old "tab to document" look-up key:

               docLookup.Remove(theDoc.Pathname);

               // Update the document itself:

               theDoc.Pathname = FileSaveDlg.Filename; 
               theDoc.ReadOnly = false;

               // Add the new "tab to document" look-up key:

               Gtk.Box theBox = (Gtk.Box) docNotebook.GetNthPage(docNotebook.CurrentPage);  
               ((Gtk.Label) theBox.Children[1]).Text = theDoc.Pathname;
               ActiveDocument = theDoc;

               // Save the outline to the new file:

               theDoc.Save();
               theDoc.CmdLog.Assign(theDoc.Pathname);

               // Update the GUI:

               UpdateControls();

               // Update the session:
            
               session.Save(SessionData);

               // Update the log:

               appLog.Write("Outline saved as: " + TabLabelText);
            }
         }

         finally
         {
            FileSaveDlg.Destroy();
         }
      }

      private void OnFileImportFromIndentedTextFile(object o, EventArgs args)
      {
         // Display a file open dialog:

         FileChooserDialog FileOpenDlg = new FileChooserDialog(
         "Choose file to import from", mainWin, FileChooserAction.Open,
         "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);

         FileFilter filter = new FileFilter();
         filter.Name = MainClass.appName + " files (*." + indentedTextFileExt + ")";
         filter.AddMimeType("text/" + indentedTextFileExt);
         filter.AddPattern("*." + indentedTextFileExt);
         FileOpenDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileOpenDlg.AddFilter(filter);

         if (lastFolder != string.Empty)
         {
            FileOpenDlg.SetCurrentFolder(lastFolder);
         }

         try
         {
            if (FileOpenDlg.Run() == (int) ResponseType.Accept)
            {
               if (FileOpenDlg.Filename != null)
               {
                  string theText = File.ReadAllText(FileOpenDlg.Filename);

                  Document theDoc = ActiveDocument;
                  TreeIter selectedNode;
                  theDoc.Tree.Selection.GetSelected(out selectedNode);

                  TreeIter theRoot;
                  theDoc.AttachSubtree(selectedNode, theText, out theRoot);

                  theDoc.Modified = true;
                  UpdateControls();
               }
               else
                  DispMesg("No file was chosen.");
            }
         }

         finally
         {
            FileOpenDlg.Destroy();
         }
      }

      private void OnFileImportFromXML_File(object o, EventArgs args)
      {
         // Display a file open dialog:

         FileChooserDialog FileOpenDlg = new FileChooserDialog(
         "Choose file to import from", mainWin, FileChooserAction.Open,
         "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);

         FileFilter filter = new FileFilter();
         filter.Name = MainClass.appName + " files (*." + xmlExt + ")";
         filter.AddMimeType("text/" + xmlExt);
         filter.AddPattern("*." + xmlExt);
         FileOpenDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileOpenDlg.AddFilter(filter);

         if (lastFolder != string.Empty)
         {
            FileOpenDlg.SetCurrentFolder(lastFolder);
         }

         try
         {
            if (FileOpenDlg.Run() == (int) ResponseType.Accept)
            {
               if (FileOpenDlg.Filename != null)
               {
                  Document theDoc = ActiveDocument;
                  TreeIter selectedNode;
                  theDoc.Tree.Selection.GetSelected(out selectedNode);
                  theDoc.ImportXML(selectedNode, FileOpenDlg.Filename);
                  theDoc.Modified = true;
                  UpdateControls();
               }
               else
                  DispMesg("No file was chosen.");
            }
         }

         finally
         {
            FileOpenDlg.Destroy();
         }
      }

      private void OnFileExportToFlatTextFile(object o, EventArgs args)
      {
         // Retrieve the outline document:

         Document theDoc = ActiveDocument;

         // Create dialog to query whether pango markup should be included:

         bool includeMarkup;
         MessageDialog theDialog = new MessageDialog(this, 
         DialogFlags.DestroyWithParent, MessageType.Warning, 
         ButtonsType.YesNo, "Include Pango markup?");

         try
         {
            includeMarkup = theDialog.Run() == (int) ResponseType.Yes;

            // Create a file export dialog:
   
            FileChooserDialog  FileExportDlg = new FileChooserDialog(
            "Choose file to export outline to", mainWin, FileChooserAction.Save,
            "Cancel", ResponseType.Cancel, "Save", ResponseType.Accept);
   
            FileExportDlg.DoOverwriteConfirmation = true;
   
            FileFilter filter = new FileFilter();
            filter.Name = "Text files (*.txt)";
            filter.AddMimeType("text/txt");
            filter.AddPattern("*.txt");
            FileExportDlg.AddFilter(filter);
   
            filter = new FileFilter();
            filter.Name = "All files (*.*)";
            filter.AddPattern("*.*");
            FileExportDlg.AddFilter(filter);
   
            try
            {
               // Display the file export dialog:
   
               if (FileExportDlg.Run() == (int) ResponseType.Accept)
               {
                  // Get selected node:
         
                  TreeIter selectedNode;
                  theDoc.Tree.Selection.GetSelected(out selectedNode);
   
                  // Export the node as a flat text file:
   
                  theDoc.Export(selectedNode, FileExportDlg.Filename, false, includeMarkup);
               }
            }
   
            finally
            {
               FileExportDlg.Destroy();
            }
         }
         
         finally
         {
            theDialog.Destroy();
         }
      }

      private void OnFileExportToIndentedTextFile(object o, EventArgs args)
      {
         // Retrieve the outline document:

         Document theDoc = ActiveDocument;

         // Create/configure a file export dialog:

         FileChooserDialog  FileExportDlg = new FileChooserDialog(
         "Choose file to export outline to", mainWin, FileChooserAction.Save,
         "Cancel", ResponseType.Cancel, "Save", ResponseType.Accept);

         FileExportDlg.DoOverwriteConfirmation = true;

         FileFilter filter = new FileFilter();
         filter.Name = MainClass.appName + " files (*." + indentedTextFileExt + ")";
         filter.AddMimeType("text/" + indentedTextFileExt);
         filter.AddPattern("*." + indentedTextFileExt);
         FileExportDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileExportDlg.AddFilter(filter);

         try
         {
            // Display the file export dialog:

            if (FileExportDlg.Run() == (int) ResponseType.Accept)
            {
               // Get selected node:
      
               TreeIter selectedNode;
               theDoc.Tree.Selection.GetSelected(out selectedNode);

               // Export the outline as an indented text formatted file:

               theDoc.Export(selectedNode, FileExportDlg.Filename, true, false);
            }
         }

         finally
         {
            FileExportDlg.Destroy();
         }
      }

      private void OnFileExportToXML_File(object o, EventArgs args)
      {
         // Retrieve the outline document:

         Document theDoc = ActiveDocument;

         // Create/configure a file export dialog:

         FileChooserDialog  FileExportDlg = new FileChooserDialog(
         "Choose file to export outline to", mainWin, FileChooserAction.Save,
         "Cancel", ResponseType.Cancel, "Save", ResponseType.Accept);

         FileExportDlg.DoOverwriteConfirmation = true;

         FileFilter filter = new FileFilter();
         filter.Name = "XML files (*." + xmlExt + ")";
         filter.AddMimeType("text/" + xmlExt);
         filter.AddPattern("*." + xmlExt);
         FileExportDlg.AddFilter(filter);

         filter = new FileFilter();
         filter.Name = "All files (*.*)";
         filter.AddPattern("*.*");
         FileExportDlg.AddFilter(filter);

         try
         {
            // Display the file export dialog:

            if (FileExportDlg.Run() == (int) ResponseType.Accept)
            {
               // Get selected node:
      
               TreeIter selectedNode;
               theDoc.Tree.Selection.GetSelected(out selectedNode);

               // Export the outline as an indented text formatted file:

               theDoc.ExportAsXML(selectedNode, FileExportDlg.Filename);
            }
         }

         finally
         {
            FileExportDlg.Destroy();
         }
      }

      private void OnFileProperties(object o, EventArgs args)
      {
         PropertiesDialog dialog = new PropertiesDialog(mainWin);
         ResponseType response = ResponseType.None;
         Document theDoc = ActiveDocument;

         dialog.OutlineFont = theDoc.Font;
         dialog.TreeLinesEnabled = theDoc.Tree.EnableTreeLines; 
         dialog.PangoEnabled = theDoc.PangoEnabled;
         dialog.ColorBarsEnabled = theDoc.Tree.RulesHint;
         dialog.WrapMargin = theDoc.WrapMargin;

         try
         {
            response = (ResponseType) dialog.Run();

            if (response == ResponseType.Ok)
            {
               // Update the outline properties:

               theDoc.Font = dialog.OutlineFont;
               theDoc.Tree.EnableTreeLines = dialog.TreeLinesEnabled;
               theDoc.PangoEnabled = dialog.PangoEnabled;
               theDoc.Tree.RulesHint = dialog.ColorBarsEnabled;
               theDoc.WrapMargin = dialog.WrapMargin;
               //theDoc.Modified = true;
               theDoc.SaveProperties();
            }
         }

         finally
         {
            dialog.Destroy();
         }
      }

      private void OnFileCloseAction(object o, EventArgs args)
      {
         WarnAboutUnsavedChanges();
      }

      private void OnFileQuitAction(object o, EventArgs args)
      {
         while ((docNotebook.NPages > 0) && WarnAboutUnsavedChanges() != ResponseType.Cancel) {}
         if (docNotebook.NPages == 0) 
         {
            appLog.Write(MainClass.appName + " shutdown");
            Application.Quit();
         }
      }

      // EDIT MENU //

      private void OnUndoAction(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;
         theDoc.Undo(true);
         UpdateControls();
      }

      private void OnRedoAction(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;
         theDoc.Redo(true);
         UpdateControls();
      }

      private void OnCutAction(object o, EventArgs args)
      {
         cutCopyNode(CutCopyType.CUT);
      }

      private void OnCopyAction(object o, EventArgs args)
      {
         cutCopyNode(CutCopyType.COPY);
      }

      private void OnPasteAction(object o, EventArgs args)
      {
         if (clipboard.operation != CutCopyType.NONE)
         {
            // Performing a CUT or COPY operation. Get the selected node:
   
            Document theDoc = ActiveDocument;
            TreeIter selectedNode;
            theDoc.Tree.Selection.GetSelected(out selectedNode);

            // Get the source document:

            Document srcDoc = clipboard.srcDoc;

            // Get the source node:

            TreeIter srcNode = clipboard.srcNode;

            TreeStore srcModel = (TreeStore) srcDoc.Tree.Model; 
            if ((theDoc != srcDoc) || (clipboard.operation != CutCopyType.CUT) || !srcModel.IsAncestor(srcNode, selectedNode))
            {
               // The operation is legitimate. Perform the copy part:
   
               theDoc.Do(new CopyNodeCommand(theDoc, selectedNode, clipboard.contents), true);
   
               if (clipboard.operation == CutCopyType.CUT)  
               {
                  // Delete the source node:
   
                  srcDoc.Do(new DeleteNodeCommand(srcDoc, srcNode), true);

                  // Clear the clipboard:
      
                  clipboard.operation = CutCopyType.NONE;
               }
      
               // Update the GUI:
   
               UpdateControls();
            }
            else
               DispMesg("Can't move a node into its own subtree!");
         }
         else
            DispMesg("There's nothing on the clipboard to copy!");
      }

      private void OnDeleteAction(object o, EventArgs args)
      {
         DeleteSelectedNode();
      }

      private void OnAddChildAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Add the child node:

         theDoc.Do(new AddChildNodeCommand(theDoc, selectedNode), true);

         if (raiseEditorOnAddNode)
         {
            // Edit the new node:
   
            OnEditTextAction(new object(), new EventArgs());
         }

         UpdateControls();
      }

      private void OnAddSiblingAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Add the sibling node:

         theDoc.Do(new AddSiblingNodeCommand(theDoc, selectedNode), true);

         if (raiseEditorOnAddNode)
         {
            // Edit the new node:
   
            OnEditTextAction(new object(), new EventArgs());
         }

         UpdateControls();
      }

      private void OnExtractNodeAction(object o, EventArgs args)
      {
         MessageDialog WarningMesgDlg = new MessageDialog (this, 
         DialogFlags.DestroyWithParent, MessageType.Warning, 
         ButtonsType.YesNo, "Ok to extract node?");

         try
         {
            if (WarningMesgDlg.Run() == (int) ResponseType.Yes)
            {
               // Get selected node:
      
               Document theDoc = ActiveDocument;
               TreeIter selectedNode;
               theDoc.Tree.Selection.GetSelected(out selectedNode);
      
               // Extract the node:
      
               theDoc.Do(new ExtractNodeCommand(theDoc, selectedNode), true);
               UpdateControls();
            }
         }
         
         finally
         {
            WarningMesgDlg.Destroy();
         }
      }

      private void OnReParentAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Re-parent the node:

         theDoc.Do(new ReparentNodeCommand(theDoc, selectedNode), true);

         if (raiseEditorOnAddNode)
         {
            // Edit the new node:
   
            OnEditTextAction(new object(), new EventArgs());
         }

         UpdateControls();
      }

      private void OnJoinNodeAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Split the node:

         theDoc.Do(new JoinNodeCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnSplitNodeAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Split the node:

         theDoc.Do(new SplitNodeCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnMoveUpAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Move the node up:

         theDoc.Do(new MoveNodeUpCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnMoveDownAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Move the node down:

         theDoc.Do(new MoveNodeDownCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnPromoteAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Promote the node:

         theDoc.Do(new PromoteNodeCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnDemoteAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Promote the node:

         theDoc.Do(new DemoteNodeCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnCheckmarkNodeAction(object o, EventArgs args) 
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Checkmark the node:

         theDoc.Do(new CheckmarkNodeCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnCrossmarkNodeAction(object o, EventArgs args) 
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Crossmark the node:

         theDoc.Do(new CrossmarkNodeCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnNewlinesToDoubleNewlinesAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Reduce newlines:

         theDoc.Do(new DoubleNewlinesCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnReduceNewlinesAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Reduce newlines:

         theDoc.Do(new ReduceNewlinesCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnNormalizeNewlinesAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Normalize newlines:

         theDoc.Do(new NormalizeNewlinesCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnNewlinesToSpacesAction(object o, EventArgs args)
      {
         // Get selected node:

         Document theDoc = ActiveDocument;
         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Normalize newlines:

         theDoc.Do(new NewlinesToSpacesCommand(theDoc, selectedNode), true);
         UpdateControls();
      }

      private void OnEditTextAction(object o, EventArgs args)
      {
         ResponseType response = ResponseType.None;

         // Create a "warning" dialog:

         MessageDialog warningMesgDlg = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Warning, 
         ButtonsType.YesNo, "Save changes?");

         // Get the active document:

         Document theDoc = ActiveDocument;

         // Get the selected node:

         TreeIter selectedNode;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Get the node's text:

         TreeStore model = (TreeStore) theDoc.Tree.Model;
         string text = (string) model.GetValue(selectedNode, 0); 

         // Create the dialog:

         EditDialog dialog = new EditDialog(mainWin, theDoc, textEditorDialogWidth, 
         textEditorDialogHeight, editTextFont, text, searchEntry.Text, textEditorHighlightColor);

         try
         {
            response = (ResponseType) dialog.Run();

            if (dialog.TextModified) 
            {
               // The node's text was changed.

               if ((response == ResponseType.Ok) || ((response == ResponseType.DeleteEvent) && 
               (warningMesgDlg.Run() == (int) ResponseType.Yes))) 
               {
                  theDoc.Do(new EditTextCommand(theDoc, selectedNode, dialog.Text), true);

                  // Update the character and word counts on the status bar:

                  int charCount = 0;
                  int wordCount = 0;
                  Document.GetNodeStats(model, selectedNode, ref charCount, ref wordCount);
                  statusbarStatLabel.Text = "[" + charCount.ToString() + ", " + wordCount.ToString() + "]";

                  // Update the GUI:

                  UpdateControls();
               }
            }
         }

         finally
         {
            warningMesgDlg.Destroy();
            dialog.Destroy();
         }
      }

      // Converts given Gdk color to string form for writing to disk.
      private string Color2String(Gdk.Color color)
      {
         return "#" + 
         ((int) (color.Red / 256.0)).ToString("X2") + 
         ((int) (color.Green / 256.0)).ToString("X2") + 
         ((int) (color.Blue / 256.0)).ToString("X2");
      }

      private void OnPreferencesAction(object o, EventArgs args)
      {
         PreferenceDialog dialog = new PreferenceDialog(mainWin);
         ResponseType response = ResponseType.None;

         dialog.HintsEnabled = HintsEnabled;
         dialog.LoggingEnabled = appLog.Enabled;
         dialog.RaiseEditorOnAddNode = raiseEditorOnAddNode;
         dialog.TextEditorFont = editTextFont;
         Gdk.Color tempColor = new Gdk.Color();
         Gdk.Color.Parse(textEditorHighlightColor, ref tempColor);
         dialog.TextEditorHighlightColor = tempColor;

         dialog.TextEditorDialogWidth = textEditorDialogWidth;
         dialog.TextEditorDialogHeight = textEditorDialogHeight;

         try
         {
            response = (ResponseType) dialog.Run();

            if (response == ResponseType.Ok)
            {
               // Update the general preferences:

               HintsEnabled = dialog.HintsEnabled;
               appLog.Enabled = dialog.LoggingEnabled;

               // Update the text editor preferences:

               raiseEditorOnAddNode = dialog.RaiseEditorOnAddNode;
               editTextFont = dialog.TextEditorFont;
               textEditorHighlightColor = Color2String(dialog.TextEditorHighlightColor);
               textEditorDialogWidth = dialog.TextEditorDialogWidth;
               textEditorDialogHeight = dialog.TextEditorDialogHeight;

               // Save the configuration changes:

               SaveConfiguration();
            }
         }

         finally
         {
            dialog.Destroy();
         }
      }

      // VIEW MENU //

      private void OnCollapseNodeAction(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;

         // Get selected node:

         TreeIter selectedNode;
         TreeStore model = (TreeStore) theDoc.Tree.Model;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Expand the node's children:

         theDoc.Tree.CollapseRow(model.GetPath(selectedNode));
      }

      private void OnCollapseAllAction(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;
         theDoc.Tree.CollapseAll();
      }

      private void OnExpandNodeAction(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;

         // Get selected node:

         TreeIter selectedNode;
         TreeStore model = (TreeStore) theDoc.Tree.Model;
         theDoc.Tree.Selection.GetSelected(out selectedNode);

         // Expand the node's children:

         theDoc.Tree.ExpandRow(model.GetPath(selectedNode), false);
      }

      private void OnExpandAllAction(object o, EventArgs args)
      {
         Document theDoc = ActiveDocument;
         theDoc.Tree.ExpandAll();
      }

      private void OnFindAction(object sender, EventArgs args)
      {
         searchEntry.GrabFocus();
      }

      private void OnFindNextAction(object sender, EventArgs args)
      {
         OnSearchButtonClicked(sender, args);
      }

      // HELP MENU //

      private void OnUserGuideAction(object o, EventArgs args)
      {
         FileOpen(assyFolder + "/" + appUserGuide, string.Empty, false);
      }

      private void OnAboutAction(object o, EventArgs args)
      {
         AboutDialog dialog = new AboutDialog ();
         
         try
         {
            dialog.WindowPosition = WindowPosition.Center;
            dialog.ProgramName = MainClass.appName;
            dialog.Version = appVers;
            dialog.Comments = appSlogan;
            dialog.Copyright = appCopyright;
            dialog.Authors = new string [] { appAuthor };
            //dialog.Website = "???";
            //dialog.Logo = new Gdk.Pixbuf("about.png");

            dialog.Run();
         }

         finally
         {
            dialog.Destroy();
         }
      }

      //-------------------------------------------------------------------------------//
      //                            SCROLLEDWINDOW EVENTS                              //
      //-------------------------------------------------------------------------------//

      private void OnScrolledWindowAllocated(object o, EventArgs args)
      {
         ScrolledWindow sw = (ScrolledWindow) o;
         OutlineView theTree = (OutlineView) sw.Child;
         Document theDoc;
         if (GetDocumentFromTree(theTree, out theDoc))
         {
            CellRendererText theCellRenderer = (CellRendererText) theTree.Columns[0].Cells[0];
            if (theTree.Columns[0].Width > theDoc.WrapMargin)
            {
               theCellRenderer.WrapWidth = theTree.Columns[0].Width - theDoc.WrapMargin;
            }
         }
      }

      //-------------------------------------------------------------------------------//
      //                              OUTLINEVIEW EVENTS                               //
      //-------------------------------------------------------------------------------//

      private void OnOutlineRowActivated(object o, Gtk.RowActivatedArgs args)
      {
         Document theDoc = ActiveDocument;
         if (!theDoc.ReadOnly) OnEditTextAction(o, new EventArgs());
      }

      private void OnOutlineFocusIn(object o, FocusInEventArgs args)
      {
         UpdateControls();
      }

      private void OnOutlineFocusOut(object o, FocusOutEventArgs args)
      {
         UpdateControls();
      }

      private void OnOutlineDragBegin(object o, DragBeginArgs args)
      {
         Gtk.Drag.SetIconPixbuf(args.Context, dragPixbuf, 0, 0);
      }

      private void OnOutlineDragDataGet(object o, DragDataGetArgs args)
      {
         OutlineView theTree = (OutlineView) o;
         TreeStore model = (TreeStore) theTree.Model;
         TreeIter srcNode;
         bool Ok = theTree.Selection.GetSelected(out srcNode);

         if (Ok)
         {
            string theKey = GetKeyFromTree(theTree);
            string srcPathStr = model.GetStringFromIter(srcNode);
            string subtree = Command.GetSubtreeText(model, srcNode);
            string data = theKey + '\n' + srcPathStr + '\n' + subtree;
            Atom[] targets = args.Context.Targets;
            args.SelectionData.Set(targets[0], 8, System.Text.Encoding.UTF8.GetBytes(data)); 
         }
      }

      private void OnOutlineDragDataReceived(object o, DragDataReceivedArgs args)
      {
         string data = System.Text.Encoding.UTF8.GetString(args.SelectionData.Data);

         string theKey;
         string srcPathStr;
         string subtree;
         ParseData(data, out theKey, out srcPathStr, out subtree);

         Document theDoc = ActiveDocument;
         TreeStore model = (TreeStore) theDoc.Tree.Model;
         TreePath dstPath;
         TreeViewDropPosition thePos;
         bool Ok = theDoc.Tree.GetDestRowAtPos(args.X, args.Y, out dstPath, out thePos);

         if (Ok)
         {
            TreeIter dstNode;
            Ok = model.GetIter(out dstNode, dstPath);

            if (Ok)
            {
               Document srcDoc = GetDocumentFromKey(theKey);

               TreeIter srcNode;
               srcDoc.Tree.Model.GetIterFromString(out srcNode, srcPathStr);

               TreeIter parentNode;
               srcDoc.Tree.Model.IterParent(out parentNode, srcNode);

               bool isMove = args.Context.Action == DragAction.Move;
               bool srcNodeIsRoot = Command.IsRoot((TreeStore) srcDoc.Tree.Model, srcNode);
               bool isSameDocument = srcDoc == theDoc;
               bool isAncestorMove = isMove && isSameDocument && model.IsAncestor(srcNode, dstNode);
               bool okToDelete = isMove && !srcDoc.ReadOnly && !srcNodeIsRoot && !isAncestorMove;
   
               if (!isMove || okToDelete)
               {
                  // Perform the copy:
   
                  theDoc.Do(new CopyNodeCommand(theDoc, dstNode, subtree), true);
   
                  if (isMove) 
                  {
                     // Perform the delete:

                     srcDoc.Do(new DeleteNodeCommand(srcDoc, srcNode), true);
                  }
               }
               else
                  DispMesg("Moving a root node isn't allowed and nor is moving a node into its own subtree.");

               // Close the drag/drop operation and if performing a MOVE, signal 
               // the source widget to delete its copy of the subtree:

               Gtk.Drag.Finish(args.Context, true, false, args.Time);

               // Update the GUI:
      
               UpdateControls();
            }
         }
      }
     
      [GLib.ConnectBefore]
      private void OnOutlineKeyPressEvent(object o, Gtk.KeyPressEventArgs e)
      {
         Gdk.Key theKey = e.Event.Key;
         char theChar = (char) e.Event.KeyValue;

         bool controlPressed = (e.Event.State & Gdk.ModifierType.ControlMask) != 0;   
         bool altPressed = (e.Event.State & Gdk.ModifierType.Mod1Mask) != 0;            
         bool shiftPressed = (e.Event.State & Gdk.ModifierType.ShiftMask) != 0;

         bool justALetter = char.IsLetter(theChar) && ((theKey != Gdk.Key.Home) && 
         (theKey != Gdk.Key.Left) && (theKey != Gdk.Key.Right) && 
         (theKey != Gdk.Key.Up) && (theKey != Gdk.Key.Down) && 
         (theKey != Gdk.Key.Return) && (theKey != Gdk.Key.Delete) && 
         (theKey != Gdk.Key.BackSpace) && (theKey != Gdk.Key.End)) && 
         (!controlPressed) && (!altPressed);

         // 'A'..'Z': 
         if (justALetter || (theKey == Gdk.Key.numbersign))
         {
            if (theKey == Gdk.Key.numbersign)
            {
               keystrokeQueue = string.Empty;
            }
            else
            {
               keystrokeQueue += theChar;
               if (keystrokeQueue == "shallowhalwantsagal")
               {
                  // Load and execute outline editing commands from a file:

                  RunCommands();
               }
            }
         }

         // RETURN
         if (!altPressed && !controlPressed && !shiftPressed && (theKey == Gdk.Key.Return))
         {
            Document theDoc = ActiveDocument;
            if ((theDoc != null) && !theDoc.ReadOnly) 
            {
               // Get the selected node:
      
               TreeIter selectedNode;
               TreeStore model = (TreeStore) theDoc.Tree.Model;
               theDoc.Tree.Selection.GetSelected(out selectedNode);

               if (Command.IsRoot(model, selectedNode))
               {
                  // Root node is selected.

                  if (AddChildNodeAction.Sensitive) 
                  {
                     // Add new node as a child:

                     OnAddChildAction(o, new EventArgs());
                  }
               }
               else
               {
                  // Root node isn't selected.

                  if (AddSiblingNodeAction.Sensitive) 
                  {
                     // Add new node as a sibling:

                     OnAddSiblingAction(o, new EventArgs());
                  }
               }
            }
         }
      }         

      //----------------------------------------------------------------------------//
      //                              TREESTORE EVENTS                              //
      //----------------------------------------------------------------------------//

      private void OnRowsReordered(object o, RowsReorderedArgs args)
      {
         UpdateControls();
      }

      //----------------------------------------------------------------------------//
      //                            TREESELECTION EVENTS                            //
      //----------------------------------------------------------------------------//

      private void OnOutlineSelectionChanged(object o, EventArgs args)
      {
         // Make the cursor "track" the selection:

         TreeSelection treeSelection = (TreeSelection) o;
         TreeView theTree = treeSelection.TreeView;
         TreeIter theIter;

         if (theTree.Selection.GetSelected(out theIter))
         {
            TreePath thePath = theTree.Model.GetPath(theIter);
            theTree.SetCursor(thePath, theTree.Columns[0], false);
      
            // Update the character and word counts on the status bar:

            int charCount = 0;
            int wordCount = 0;
            Document.GetNodeStats((TreeStore) theTree.Model, theIter, ref charCount, ref wordCount);
            statusbarStatLabel.Text = "[" + charCount.ToString() + ", " + wordCount.ToString() + "]";
         }         

         // Update the GUI:

         UpdateControls();
      }

      //----------------------------------------------------------------------------//
      //                               NOTEBOOK EVENTS                              //
      //----------------------------------------------------------------------------//

      private void OnDocNotebookSwitchPage(object o, SwitchPageArgs args)  
      {
         appLog.Write("Outline selected: " + TabLabelText);
         UpdateControls();
      }

      private void OnNotebookDragDataReceived(object o, DragDataReceivedArgs args)
      {
         // One or more files are being dropped.

         string fileList = System.Text.Encoding.UTF8.GetString(args.SelectionData.Data);
         string[] files = fileList.Split(new char[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);

         foreach (string file in files) 
         {
            if (file.Length > 0)
            {
               Uri theURI = new Uri(file);
               string localPath = theURI.LocalPath;

               if (file.Substring(0, 7).ToLower() == "file://")
               {
                  // Open the file:

                  FileOpen(theURI.LocalPath, string.Empty, false);
               }
               else
                  DispMesg("Drag and drop opening of non-local files isn't supported.");
            }
         }

         Gtk.Drag.Finish(args.Context, true, false, args.Time);
      }
   }
}