// // ExtensibleTreeView.cs // // Author: // Lluis Sanchez Gual // Mike Krüger // // Copyright (C) 2005-2008 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // //#define TREE_VERIFY_INTEGRITY using System; using System.IO; using System.ComponentModel; using System.Drawing; using System.Diagnostics; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Xml; using System.Resources; using System.Text; using Mono.Addins; using MonoDevelop.Core; using MonoDevelop.Components; using MonoDevelop.Ide.Commands; using MonoDevelop.Components.Commands; using MonoDevelop.Ide.Gui.Pads; using MonoDevelop.Projects.Extensions; using Mono.TextEditor; using System.Linq; using MonoDevelop.Ide.Tasks; namespace MonoDevelop.Ide.Gui.Components { public partial class ExtensibleTreeView : CompactScrolledWindow { internal const int NodeInfoColumn = 0; internal const int DataItemColumn = 1; internal const int BuilderChainColumn = 2; internal const int FilledColumn = 3; internal const int ShowPopupColumn = 4; NodeBuilder[] builders; Dictionary builderChains = new Dictionary (); NodeHashtable nodeHash = new NodeHashtable (); ExtensibleTreeViewTree tree; Gtk.TreeStore store; Gtk.TreeViewColumn complete_column; ZoomableCellRendererPixbuf pix_render; CustomCellRendererText text_render; TreeBuilderContext builderContext; Hashtable callbacks = new Hashtable (); bool editingText = false; bool showSelectionPopupButton; Gtk.TreeIter? lastPopupButtonIter; TreePadOption[] options; TreeOptions globalOptions; TreeNodeNavigator workNode; TreeNodeNavigator compareNode1; TreeNodeNavigator compareNode2; internal bool sorting; object[] copyObjects; DragOperation currentTransferOperation; TransactedNodeStore transactionStore; int updateLockCount; string contextMenuPath; IDictionary contextMenuTypeNameAliases; public IDictionary ContextMenuTypeNameAliases { get { return contextMenuTypeNameAliases; } set { contextMenuTypeNameAliases = value; } } public Gtk.TreeStore Store { get { return this.store; } } public Gtk.TreeView Tree { get { return tree; } } public string Id { get; set; } public ExtensibleTreeView () { tree = new ExtensibleTreeViewTree (this); } public ExtensibleTreeView (NodeBuilder[] builders, TreePadOption[] options) : this () { Initialize (builders, options); } void CustomFontPropertyChanged (object sender, EventArgs a) { UpdateFont (); } void UpdateFont () { text_render.CustomFont = IdeApp.Preferences.CustomPadFont ?? tree.Style.FontDescription; tree.ColumnsAutosize (); } protected override void OnStyleSet (Gtk.Style previous_style) { base.OnStyleSet (previous_style); UpdateFont (); } public void Initialize (NodeBuilder[] builders, TreePadOption[] options) { Initialize (builders, options, null); } public virtual void Initialize (NodeBuilder[] builders, TreePadOption[] options, string contextMenuPath) { this.contextMenuPath = contextMenuPath; builderContext = new TreeBuilderContext (this); SetBuilders (builders, options); store = new Gtk.TreeStore (typeof(NodeInfo), typeof(object), typeof(object), typeof(bool), typeof(bool)); tree.Model = store; tree.Selection.Mode = Gtk.SelectionMode.Multiple; store.DefaultSortFunc = new Gtk.TreeIterCompareFunc (CompareNodes); store.SetSortColumnId (/* GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID */ -1, Gtk.SortType.Ascending); tree.HeadersVisible = false; tree.SearchColumn = 0; tree.EnableSearch = true; complete_column = new Gtk.TreeViewColumn (); complete_column.Title = "column"; pix_render = new ZoomableCellRendererPixbuf (); pix_render.Xpad = 0; complete_column.PackStart (pix_render, false); text_render = new CustomCellRendererText (this); text_render.Ypad = 0; IdeApp.Preferences.CustomPadFontChanged += CustomFontPropertyChanged;; text_render.EditingStarted += HandleEditingStarted; text_render.Edited += HandleOnEdit; text_render.EditingCanceled += HandleOnEditCancelled; complete_column.PackStart (text_render, true); complete_column.SetCellDataFunc (pix_render, SetIconCellData); complete_column.SetCellDataFunc (text_render, SetTextCellData); tree.AppendColumn (complete_column); tree.TestExpandRow += OnTestExpandRow; tree.RowActivated += OnNodeActivated; tree.DoPopupMenu += ShowPopup; workNode = new TreeNodeNavigator (this); compareNode1 = new TreeNodeNavigator (this); compareNode2 = new TreeNodeNavigator (this); tree.CursorChanged += OnSelectionChanged; tree.KeyPressEvent += OnKeyPress; tree.ButtonPressEvent += HandleButtonPressEvent; tree.MotionNotifyEvent += HandleMotionNotifyEvent; tree.LeaveNotifyEvent += HandleLeaveNotifyEvent; for (int n=3; n<16; n++) { Gtk.Rc.ParseString ("style \"MonoDevelop.ExtensibleTreeView_" + n + "\" {\n GtkTreeView::expander-size = " + n + "\n }\n"); Gtk.Rc.ParseString ("widget \"*.MonoDevelop.ExtensibleTreeView_" + n + "\" style \"MonoDevelop.ExtensibleTreeView_" + n + "\"\n"); } this.Add (tree); this.ShowAll (); #if TREE_VERIFY_INTEGRITY GLib.Timeout.Add (3000, Checker); #endif } #if TREE_VERIFY_INTEGRITY // Verifies the consistency of the tree view. Disabled by default HashSet ochecked = new HashSet (); bool Checker () { int nodes = 0; foreach (DictionaryEntry e in nodeHash) { if (e.Value is Gtk.TreeIter) { nodes++; if (!store.IterIsValid ((Gtk.TreeIter)e.Value) && ochecked.Add (e.Key)) { Console.WriteLine ("Found invalid iter in tree pad - Object: " + e.Key); MessageService.ShowError ("Found invalid iter in tree pad", "Object: " + e.Key); } } else { Gtk.TreeIter[] iters = (Gtk.TreeIter[]) e.Value; for (int n=0; n"; else cell.TextMarkup = info.Label; cell.StatusIcon = info.StatusIconInternal; cell.ShowPopupButton = (bool)model.GetValue (it, ShowPopupColumn); } public void UpdateBuilders (NodeBuilder[] builders, TreePadOption[] options) { // Save the current state ITreeNavigator root = GetRootNode (); NodeState state = root != null ? root.SaveState () : null; object obj = root != null ? root.DataItem : null; Clear (); // Clean cached builder chains builderChains.Clear (); // Update the builders SetBuilders (builders, options); // Restore the this if (obj != null) LoadTree (obj); root = GetRootNode (); if (root != null && state != null) root.RestoreState (state); } void SetBuilders (NodeBuilder[] buildersArray, TreePadOption[] options) { // Create default options List builders = new List (); foreach (NodeBuilder nb in buildersArray) { if (!(nb is TreeViewItemBuilder)) builders.Add (nb); } builders.Add (new TreeViewItemBuilder ()); this.options = options; globalOptions = new TreeOptions (); foreach (TreePadOption op in options) globalOptions [op.Id] = op.DefaultValue; globalOptions.Pad = this; // Check that there is only one TypeNodeBuilder per type Hashtable bc = new Hashtable (); foreach (NodeBuilder nb in builders) { TypeNodeBuilder tnb = nb as TypeNodeBuilder; if (tnb != null) { if (tnb.UseReferenceEquality) nodeHash.RegisterByRefType (tnb.NodeDataType); TypeNodeBuilder other = (TypeNodeBuilder) bc [tnb.NodeDataType]; if (other != null) throw new ApplicationException (string.Format ("The type node builder {0} can't be used in this context because the type {1} is already handled by {2}", nb.GetType(), tnb.NodeDataType, other.GetType())); bc [tnb.NodeDataType] = tnb; } else if (!(nb is NodeBuilderExtension)) throw new InvalidOperationException (string.Format ("Invalid NodeBuilder type: {0}. NodeBuilders must inherit either from TypeNodeBuilder or NodeBuilderExtension", nb.GetType())); } NodeBuilders = builders.ToArray (); foreach (NodeBuilder nb in builders) nb.SetContext (builderContext); } public void EnableDragUriSource (Func nodeToUri) { tree.EnableDragUriSource (nodeToUri); } object[] GetDragObjects (out Xwt.Drawing.Image icon) { ITreeNavigator[] navs = GetSelectedNodes (); if (navs.Length == 0) { icon = null; return null; } var dragObjects = new object [navs.Length]; for (int n=0; n= rect.X && cx <= rect.Right) { ShowStatusMessage (it, rect, info); popupShown = true; } } } } if (!popupShown) HideStatusMessage (); } bool statusMessageVisible; Gtk.TreeIter statusIconIter; TooltipPopoverWindow statusPopover; void ShowStatusMessage (Gtk.TreeIter it, Gdk.Rectangle rect, NodeInfo info) { if (statusMessageVisible && store.GetPath (it).Equals (store.GetPath (statusIconIter))) return; if (statusPopover != null) statusPopover.Destroy (); statusMessageVisible = true; statusIconIter = it; statusPopover = new TooltipPopoverWindow { ShowArrow = true, Text = info.StatusMessage, Severity = info.StatusSeverity }; rect.Y += 2; statusPopover.ShowPopup (this, rect, PopupPosition.Bottom); } void HideStatusMessage () { if (statusMessageVisible) { statusMessageVisible = false; statusPopover.Destroy (); statusPopover = null; } } [GLib.ConnectBefore] void HandleLeaveNotifyEvent (object o, Gtk.LeaveNotifyEventArgs args) { HideStatusMessage (); } void HandleMenuHidden (object sender, EventArgs e) { ((Gtk.Menu)sender).Hidden -= HandleMenuHidden; text_render.Pushed = false; QueueDraw (); } internal void LockUpdates () { if (++updateLockCount == 1) transactionStore = new TransactedNodeStore (this); } internal void UnlockUpdates () { if (--updateLockCount == 0) { TransactedNodeStore store = transactionStore; transactionStore = null; store.CommitChanges (); } } internal ITreeBuilder CreateBuilder () { return CreateBuilder (Gtk.TreeIter.Zero); } internal ITreeBuilder CreateBuilder (Gtk.TreeIter it) { if (transactionStore != null) return new TransactedTreeBuilder (this, transactionStore, it); else return new TreeBuilder (this, it); } protected NodeBuilder[] NodeBuilders { get { return builders; } set { builders = value; } } public Gtk.TreeViewColumn CompleteColumn { get { return complete_column; } } NodeHashtable NodeHash { get { return nodeHash; } } public ITreeBuilderContext BuilderContext { get { return builderContext; } } public object[] CopyObjects { get { return copyObjects; } set { copyObjects = value; } } public DragOperation CurrentTransferOperation { get { return currentTransferOperation; } } public ITreeBuilder LoadTree (object nodeObject) { Clear (); TreeBuilder builder = new TreeBuilder (this); builder.AddChild (nodeObject, true); builder.Expanded = true; InitialSelection (); return builder; } public ITreeBuilder AddChild (object nodeObject) { TreeBuilder builder = new TreeBuilder (this); builder.AddChild (nodeObject, true); builder.Expanded = true; InitialSelection (); return builder; } public void RemoveChild (object nodeObject) { TreeBuilder builder = new TreeBuilder (this); if (builder.MoveToObject (nodeObject)) { builder.Remove (); InitialSelection (); } } void InitialSelection () { if (tree.Selection.CountSelectedRows () == 0) { Gtk.TreeIter it; if (store.GetIterFirst (out it)) { tree.Selection.SelectIter (it); tree.SetCursor (store.GetPath (it), tree.Columns [0], false); } } } public void Clear () { copyObjects = tree.dragObjects = null; object[] obs = new object [nodeHash.Count]; nodeHash.Keys.CopyTo (obs, 0); foreach (object dataObject in obs) NotifyNodeRemoved (dataObject, null); nodeHash = new NodeHashtable (); store.Clear (); } public ITreeNavigator GetSelectedNode () { Gtk.TreePath[] sel = tree.Selection.GetSelectedRows (); if (sel.Length == 0) return null; Gtk.TreeIter iter; if (store.GetIter (out iter, sel[0])) return new TreeNodeNavigator (this, iter); else return null; } class SelectionGroup { public NodeBuilder[] BuilderChain; public List Nodes; public Gtk.TreeStore store; NodePosition[] savedPos; object[] dataItems; public object[] DataItems { get { if (dataItems == null) { dataItems = new object [Nodes.Count]; for (int n=0; n GetSelectedNodesGrouped () { Gtk.TreePath[] paths = tree.Selection.GetSelectedRows (); if (paths.Length == 0) { return new SelectionGroup [0]; } if (paths.Length == 1) { Gtk.TreeIter it; store.GetIter (out it, paths [0]); SelectionGroup grp = new SelectionGroup (); TreeNodeNavigator nav = new TreeNodeNavigator (this, it); grp.BuilderChain = nav.BuilderChain; grp.Nodes = new List (); grp.Nodes.Add (nav); grp.store = store; return new SelectionGroup [] { grp }; } Dictionary dict = new Dictionary (); for (int n=0; n (); grp.store = store; dict [nav.BuilderChain] = grp; } grp.Nodes.Add (nav); } return dict.Values; } public bool MultipleNodesSelected () { return tree.Selection.GetSelectedRows ().Length > 1; } public ITreeNavigator[] GetSelectedNodes () { Gtk.TreePath[] paths = tree.Selection.GetSelectedRows (); ITreeNavigator [] navs = new ITreeNavigator [paths.Length]; for (int n=0; n 0) { ITreeNavigator[] nodes = grp.Nodes.ToArray (); NodeCommandTargetChain targetChain = null; NodeCommandTargetChain lastNode = null; foreach (NodeBuilder nb in chain) { NodeCommandTargetChain newNode = new NodeCommandTargetChain (nb.CommandHandler, nodes); if (lastNode == null) targetChain = lastNode = newNode; else { lastNode.Next = newNode; lastNode = newNode; } } if (targetChain != null) targets.Add (targetChain); } } if (targets.Count == 1) return targets[0]; else if (targets.Count > 1) return new MulticastNodeRouter (targets); else return null; } void ExpandCurrentItem () { try { LockUpdates (); IEnumerable nodeGroups = GetSelectedNodesGrouped (); if (nodeGroups.Count () == 1) { SelectionGroup grp = nodeGroups.First (); if (grp.Nodes.Count () == 1) { ITreeNavigator node = grp.Nodes.First (); if (node.Expanded) { grp.SavePositions (); node.Selected = false; if (node.MoveToFirstChild ()) node.Selected = true; // This exit statement is so that it doesn't do 2 actions at a time. // As in, navigate, then expand. return; } } } foreach (SelectionGroup grp in nodeGroups) { grp.SavePositions (); foreach (var node in grp.Nodes) { node.Expanded = true; } } } finally { UnlockUpdates (); } } void CollapseCurrentItem () { try { LockUpdates (); IEnumerable nodeGroups = GetSelectedNodesGrouped (); if (nodeGroups.Count () == 1) { SelectionGroup grp = nodeGroups.First (); if (grp.Nodes.Count () == 1) { ITreeNavigator node = grp.Nodes.First (); if (!node.HasChildren () || !node.Expanded) { grp.SavePositions (); node.Selected = false; if (node.MoveToParent ()) node.Selected = true; // This exit statement is so that it doesn't do 2 actions at a time. // As in, navigate, then collapse. return; } } } foreach (SelectionGroup grp in nodeGroups) { grp.SavePositions (); foreach (var node in grp.Nodes) { node.Expanded = false; } } } finally { UnlockUpdates (); } } [CommandHandler (ViewCommands.Open)] public virtual void ActivateCurrentItem () { try { LockUpdates (); foreach (SelectionGroup grp in GetSelectedNodesGrouped ()) { grp.SavePositions (); foreach (NodeBuilder b in grp.BuilderChain) { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNodes (grp.Nodes.ToArray ()); handler.ActivateMultipleItems (); if (!grp.RestorePositions ()) break; } } OnCurrentItemActivated (EventArgs.Empty); } finally { UnlockUpdates (); } } public virtual void DeleteCurrentItem () { try { LockUpdates (); foreach (SelectionGroup grp in GetSelectedNodesGrouped ()) { NodeBuilder[] chain = grp.BuilderChain; grp.SavePositions (); foreach (NodeBuilder b in chain) { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNodes (grp.Nodes.ToArray ()); if (handler.CanDeleteMultipleItems ()) { if (!grp.RestorePositions ()) return; handler.DeleteMultipleItems (); // FIXME: fixes bug #396566, but it is not 100% correct // It can only be fully fixed if updates to the tree are delayed break; } if (!grp.RestorePositions ()) return; } } } finally { UnlockUpdates (); } } protected virtual bool CanDeleteCurrentItem () { foreach (SelectionGroup grp in GetSelectedNodesGrouped ()) { NodeBuilder[] chain = grp.BuilderChain; grp.SavePositions (); foreach (NodeBuilder b in chain) { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNodes (grp.Nodes.ToArray ()); if (handler.CanDeleteMultipleItems ()) return true; if (!grp.RestorePositions ()) return false; } } return false; } [CommandHandler (ViewCommands.RefreshTree)] public virtual void RefreshCurrentItem () { try { LockUpdates (); foreach (SelectionGroup grp in GetSelectedNodesGrouped ()) { NodeBuilder[] chain = grp.BuilderChain; grp.SavePositions (); foreach (NodeBuilder b in chain) { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNodes (grp.Nodes.ToArray ()); if (!grp.RestorePositions ()) return; handler.RefreshMultipleItems (); if (!grp.RestorePositions ()) return; } } } finally { UnlockUpdates (); } RefreshTree (); } protected virtual void OnCurrentItemActivated (EventArgs args) { if (CurrentItemActivated != null) CurrentItemActivated (this, args); } public event EventHandler CurrentItemActivated; [Obsolete ("Not supported anymore")] public double Zoom { get { return 1d; } set { } } [CommandHandler (EditCommands.Copy)] public void CopyCurrentItem () { CancelTransfer (); TransferCurrentItem (DragOperation.Copy); } [CommandHandler (EditCommands.Cut)] public void CutCurrentItem () { CancelTransfer (); TransferCurrentItem (DragOperation.Move); if (copyObjects != null) { foreach (object ob in copyObjects) { ITreeBuilder tb = CreateBuilder (); if (tb.MoveToObject (ob)) tb.Update (); } } } [CommandUpdateHandler (EditCommands.Copy)] protected void UpdateCopyCurrentItem (CommandInfo info) { if (editingText) { info.Bypass = true; return; } info.Enabled = CanTransferCurrentItem (DragOperation.Copy); } [CommandUpdateHandler (EditCommands.Cut)] protected void UpdateCutCurrentItem (CommandInfo info) { if (editingText) { info.Bypass = true; return; } info.Enabled = CanTransferCurrentItem (DragOperation.Move); } void TransferCurrentItem (DragOperation oper) { foreach (SelectionGroup grp in GetSelectedNodesGrouped ()) { NodeBuilder[] chain = grp.BuilderChain; grp.SavePositions (); foreach (NodeBuilder b in chain) { try { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNodes (grp.Nodes.ToArray ()); if ((handler.CanDragNode () & oper) != 0) { grp.RestorePositions (); copyObjects = grp.DataItems; currentTransferOperation = oper; break; } } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } grp.RestorePositions (); } } } bool CanTransferCurrentItem (DragOperation oper) { TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); if (node != null) { NodeBuilder[] chain = node.NodeBuilderChain; NodePosition pos = node.CurrentPosition; foreach (NodeBuilder b in chain) { try { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNode (node); if ((handler.CanDragNode () & oper) != 0) return true; } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } node.MoveToPosition (pos); } } return false; } [CommandHandler (EditCommands.Paste)] public void PasteToCurrentItem () { if (copyObjects == null) return; try { LockUpdates (); TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); if (node != null) { NodeBuilder[] chain = node.NodeBuilderChain; NodePosition pos = node.CurrentPosition; foreach (NodeBuilder b in chain) { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNode (node); if (handler.CanDropMultipleNodes (copyObjects, currentTransferOperation, DropPosition.Into)) { node.MoveToPosition (pos); handler.OnMultipleNodeDrop (copyObjects, currentTransferOperation, DropPosition.Into); } node.MoveToPosition (pos); } } if (currentTransferOperation == DragOperation.Move) CancelTransfer (); } finally { UnlockUpdates (); } } [CommandUpdateHandler (EditCommands.Paste)] protected void UpdatePasteToCurrentItem (CommandInfo info) { if (editingText) { info.Bypass = true; return; } if (copyObjects != null) { TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); if (node != null) { NodeBuilder[] chain = node.NodeBuilderChain; NodePosition pos = node.CurrentPosition; foreach (NodeBuilder b in chain) { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNode (node); if (handler.CanDropMultipleNodes (copyObjects, currentTransferOperation, DropPosition.Into)) { info.Enabled = true; return; } node.MoveToPosition (pos); } } } info.Enabled = false; } void CancelTransfer () { if (copyObjects != null) { object[] oldCopyObjects = copyObjects; copyObjects = null; if (currentTransferOperation == DragOperation.Move) { foreach (object ob in oldCopyObjects) { ITreeBuilder tb = CreateBuilder (); if (tb.MoveToObject (ob)) tb.Update (); } } } } NodeInfo GetNodeInfo (Gtk.TreeIter it) { return (NodeInfo)store.GetValue (it, NodeInfoColumn); } public void StartLabelEditInternal() { if (editingText) return; TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); if (node == null) return; Gtk.TreeIter iter = node.CurrentPosition._iter; object dataObject = node.DataItem; NodeAttributes attributes = NodeAttributes.None; ITreeNavigator parentNode = node.Clone (); parentNode.MoveToParent (); NodePosition pos = parentNode.CurrentPosition; foreach (NodeBuilder b in node.NodeBuilderChain) { try { b.GetNodeAttributes (parentNode, dataObject, ref attributes); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } parentNode.MoveToPosition (pos); } if ((attributes & NodeAttributes.AllowRename) == 0) return; node.ExpandToNode (); //make sure the parent of the node that is being edited is expanded string nodeName = node.NodeName; GetNodeInfo (iter).Label = nodeName; store.EmitRowChanged (store.GetPath (iter), iter); // Get and validate the initial text selection int nameLength = nodeName != null ? nodeName.Length : 0, selectionStart = 0, selectionLength = nameLength; foreach (NodeBuilder b in node.NodeBuilderChain) { try { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNode(node); handler.OnRenameStarting(ref selectionStart, ref selectionLength); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } } if (selectionStart < 0 || selectionStart >= nameLength) selectionStart = 0; if (selectionStart + selectionLength > nameLength) selectionLength = nameLength - selectionStart; // This will apply the selection as soon as possible GLib.Idle.Add (() => { var editable = currentLabelEditable; if (editable == null) return false; editable.SelectRegion (selectionStart, selectionStart + selectionLength); return false; }); // Ensure we set all our state variables before calling SetCursor // as this may directly invoke HandleOnEditCancelled text_render.Editable = true; editingText = true; tree.SetCursor (store.GetPath (iter), complete_column, true); } Gtk.Editable currentLabelEditable; void HandleEditingStarted (object o, Gtk.EditingStartedArgs e) { currentLabelEditable = e.Editable as Gtk.Entry; } void HandleOnEdit (object o, Gtk.EditedArgs e) { try { editingText = false; text_render.Editable = false; currentLabelEditable = null; Gtk.TreeIter iter; if (!store.GetIterFromString (out iter, e.Path)) throw new Exception("Error calculating iter for path " + e.Path); if (e.NewText != null && e.NewText.Length > 0) { ITreeNavigator nav = new TreeNodeNavigator (this, iter); NodePosition pos = nav.CurrentPosition; try { LockUpdates (); NodeBuilder[] chain = (NodeBuilder[]) store.GetValue (iter, BuilderChainColumn); foreach (NodeBuilder b in chain) { try { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNode (nav); handler.RenameItem (e.NewText); } catch (Exception ex) { LoggingService.LogInternalError (ex); } nav.MoveToPosition (pos); } } finally { UnlockUpdates (); } } // Get the iter again since the this node may have been replaced. if (!store.GetIterFromString (out iter, e.Path)) return; ITreeBuilder builder = CreateBuilder (iter); builder.Update (); } catch (Exception ex) { LoggingService.LogInternalError ("The item could not be renamed", ex); } } void HandleOnEditCancelled (object s, EventArgs args) { editingText = false; text_render.Editable = false; currentLabelEditable = null; TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); if (node == null) return; // Restore the original node label Gtk.TreeIter iter = node.CurrentPosition._iter; ITreeBuilder builder = CreateBuilder (iter); builder.Update (); } public NodeState SaveTreeState () { ITreeNavigator root = GetRootNode (); if (root == null) return null; var rootState = NodeState.CreateRoot (); List children = new List (); rootState.ChildrenState = children; var s = new Dictionary (); foreach (TreePadOption opt in options) { bool val; if (globalOptions.TryGetValue (opt.Id, out val) && val != opt.DefaultValue) s [opt.Id] = val; } if (s.Count != 0) rootState.Options = s; do { rootState.ChildrenState.Add (root.SaveState ()); } while (root.MoveNext ()); return rootState; } public void RestoreTreeState (NodeState state) { if (state == null) return; ITreeNavigator nav = GetRootNode (); if (nav == null) return; if (state.IsRoot) { if (state.ChildrenState != null) { var pos = nav.CurrentPosition; foreach (NodeState ces in state.ChildrenState) { do { if (nav.NodeName == ces.NodeName) { nav.RestoreState (ces); break; } } while (nav.MoveNext ()); nav.MoveToPosition (pos); } } } else nav.RestoreState (state); globalOptions = new TreeOptions (); foreach (TreePadOption opt in options) { bool val = false; if (state.Options == null || !state.Options.TryGetValue (opt.Id, out val)) val = opt.DefaultValue; globalOptions[opt.Id] = val; } globalOptions.Pad = this; RefreshTree (); } TypeNodeBuilder GetTypeNodeBuilder (Type type) { NodeBuilder[] chain = GetBuilderChain (type); if (chain == null) return null; return (TypeNodeBuilder) chain [0]; } public NodeBuilder[] GetBuilderChain (Type type) { NodeBuilder[] chain; builderChains.TryGetValue (type, out chain); if (chain == null) { List list = new List (); // Find the most specific node builder type. TypeNodeBuilder bestTypeNodeBuilder = null; Type bestNodeType = null; foreach (NodeBuilder nb in builders) { if (nb is TypeNodeBuilder) { TypeNodeBuilder tnb = (TypeNodeBuilder) nb; if (tnb.NodeDataType.IsAssignableFrom (type)) { if (bestNodeType == null || bestNodeType.IsAssignableFrom (tnb.NodeDataType)) { bestNodeType = tnb.NodeDataType; bestTypeNodeBuilder = tnb; } } } else { try { if (((NodeBuilderExtension)nb).CanBuildNode (type)) list.Add (nb); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } } } if (bestTypeNodeBuilder != null) { list.Insert (0, bestTypeNodeBuilder); chain = list.ToArray (); } else chain = null; builderChains [type] = chain; } return chain; } TypeNodeBuilder GetTypeNodeBuilder (Gtk.TreeIter iter) { NodeBuilder[] chain = (NodeBuilder[]) store.GetValue (iter, ExtensibleTreeView.BuilderChainColumn); if (chain != null && chain.Length > 0) return chain[0] as TypeNodeBuilder; return null; } internal int CompareNodes (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) { sorting = true; try { NodeBuilder[] chain1 = (NodeBuilder[]) store.GetValue (a, BuilderChainColumn); if (chain1 == null) return -1; compareNode1.MoveToIter (a); compareNode2.MoveToIter (b); int sort = CompareObjects (chain1, compareNode1, compareNode2); if (sort != TypeNodeBuilder.DefaultSort) return sort; NodeBuilder[] chain2 = (NodeBuilder[]) store.GetValue (b, BuilderChainColumn); if (chain2 == null) return 1; if (chain1 != chain2) { sort = CompareObjects (chain2, compareNode2, compareNode1); if (sort != TypeNodeBuilder.DefaultSort) return sort * -1; } TypeNodeBuilder tb1 = (TypeNodeBuilder) chain1[0]; TypeNodeBuilder tb2 = (TypeNodeBuilder) chain2[0]; object o1 = store.GetValue (a, DataItemColumn); object o2 = store.GetValue (b, DataItemColumn); return string.Compare (tb1.GetNodeName (compareNode1, o1), tb2.GetNodeName (compareNode2, o2), true); } finally { sorting = false; compareNode1.MoveToIter (Gtk.TreeIter.Zero); compareNode2.MoveToIter (Gtk.TreeIter.Zero); } } int CompareObjects (NodeBuilder[] chain, ITreeNavigator thisNode, ITreeNavigator otherNode) { int result = NodeBuilder.DefaultSort; for (int n=0; n 0) copyObjects = list.ToArray (); else copyObjects = null; } } // Remove object from drag list if (tree.dragObjects != null) { int i = Array.IndexOf (tree.dragObjects, dataObject); if (i != -1) { ArrayList list = new ArrayList (tree.dragObjects); list.RemoveAt (i); if (list.Count > 0) tree.dragObjects = list.ToArray (); else tree.dragObjects = null; } } object currentIt; nodeHash.TryGetValue (dataObject, out currentIt); if (currentIt is Gtk.TreeIter[]) { Gtk.TreeIter[] arr = (Gtk.TreeIter[]) currentIt; Gtk.TreePath path = null; List iters = new List (); if (store.IterIsValid (iter)) path = store.GetPath (iter); // Iters can't be directly compared (TreeIter.Equals is broken), so we have // to compare paths. foreach (Gtk.TreeIter it in arr) { if (store.IterIsValid (it) && (path == null || !path.Equals (store.GetPath (it)))) iters.Add (it); } if (iters.Count > 1) nodeHash [dataObject] = iters.ToArray (); else if (iters.Count == 1) nodeHash [dataObject] = iters[0]; else nodeHash.Remove (dataObject); } else { nodeHash.Remove (dataObject); if (fireRemovedEvent) NotifyNodeRemoved (dataObject, chain); } } internal void RemoveChildren (Gtk.TreeIter it) { Gtk.TreeIter child; while (store.IterChildren (out child, it)) { RemoveChildren (child); object childData = store.GetValue (child, ExtensibleTreeView.DataItemColumn); if (childData != null) UnregisterNode (childData, child, null, true); store.Remove (ref child); } } void NotifyNodeRemoved (object dataObject, NodeBuilder[] chain) { if (chain == null) chain = GetBuilderChain (dataObject.GetType()); foreach (NodeBuilder nb in chain) { try { nb.OnNodeRemoved (dataObject); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } } } internal bool IsRegistered (object dataObject) { return nodeHash.ContainsKey (dataObject); } public void NotifyInserted (Gtk.TreeIter it, object dataObject) { if (callbacks.Count > 0) { ArrayList list = callbacks [dataObject] as ArrayList; if (list != null) { ITreeNavigator nav = new TreeNodeNavigator (this, it); NodePosition pos = nav.CurrentPosition; foreach (TreeNodeCallback callback in list) { callback (nav); nav.MoveToPosition (pos); } callbacks.Remove (dataObject); } } } internal string GetNamePathFromIter (Gtk.TreeIter iter) { workNode.MoveToIter (iter); StringBuilder sb = new StringBuilder (); do { string name = workNode.NodeName; if (sb.Length > 0) sb.Insert (0, '/'); name = name.Replace ("%","%%"); name = name.Replace ("/","_%_"); sb.Insert (0, name); } while (workNode.MoveToParent ()); workNode.MoveToIter (Gtk.TreeIter.Zero); return sb.ToString (); } public void RefreshNode (Gtk.TreeIter iter) { ITreeBuilder builder = CreateBuilder (iter); builder.UpdateAll (); } public void RefreshNode (ITreeNavigator nav) { RefreshNode (nav.CurrentPosition._iter); } internal void ResetState (ITreeNavigator nav) { if (nav is TreeBuilder) ((TreeBuilder)nav).ResetState (); else if (nav is TransactedTreeBuilder) ((TransactedTreeBuilder)nav).ResetState (); else { ITreeBuilder builder = CreateBuilder (nav.CurrentPosition._iter); ResetState (builder); } } internal bool GetIterFromNamePath (string path, out Gtk.TreeIter iter) { if (!store.GetIterFirst (out iter)) return false; TreeNodeNavigator nav = new TreeNodeNavigator (this, iter); string[] names = path.Split ('/'); int n = 0; bool more; do { string name = names [n].Replace ("_%_","/"); name = name.Replace ("%%","%"); if (nav.NodeName == name) { iter = nav.CurrentPosition._iter; if (++n == names.Length) return true; more = nav.MoveToFirstChild (); } else more = nav.MoveNext (); } while (more); return false; } /// /// If you want to edit a node label. Select the node you want to edit and then /// call this method, instead of using the LabelEdit Property and the BeginEdit /// Method directly. /// [CommandHandler (EditCommands.Rename)] public void StartLabelEdit () { GLib.Timeout.Add (20, new GLib.TimeoutHandler (wantFocus)); } [CommandUpdateHandler (EditCommands.Rename)] public void UpdateStartLabelEdit (CommandInfo info) { if (editingText || GetSelectedNodes ().Length != 1) { info.Visible = false; return; } TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); NodeAttributes attributes = GetNodeAttributes (node); if ((attributes & NodeAttributes.AllowRename) == 0) { info.Visible = false; return; } } NodeAttributes GetNodeAttributes (TreeNodeNavigator node) { object dataObject = node.DataItem; NodeAttributes attributes = NodeAttributes.None; ITreeNavigator parentNode = node.Clone (); parentNode.MoveToParent (); NodePosition pos = parentNode.CurrentPosition; foreach (NodeBuilder b in node.NodeBuilderChain) { try { b.GetNodeAttributes (parentNode, dataObject, ref attributes); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } parentNode.MoveToPosition (pos); } return attributes; } bool wantFocus () { tree.GrabFocus (); StartLabelEditInternal (); return false; } private void OnTestExpandRow (object sender, Gtk.TestExpandRowArgs args) { bool filled = (bool) store.GetValue (args.Iter, FilledColumn); if (!filled) { TreeBuilder nb = new TreeBuilder (this, args.Iter); args.RetVal = !nb.FillNode (); } else args.RetVal = false; } void ShowPopup (Gdk.EventButton evt) { var entryset = BuildEntrySet () ?? new CommandEntrySet (); IdeApp.CommandService.ShowContextMenu (this, evt, entryset, this); } protected CommandEntrySet BuildEntrySet () { ITreeNavigator tnav = GetSelectedNode (); if (tnav == null) return null; TypeNodeBuilder nb = GetTypeNodeBuilder (tnav.CurrentPosition._iter); string menuPath = nb != null && nb.ContextMenuAddinPath != null ? nb.ContextMenuAddinPath : contextMenuPath; if (menuPath == null) { if (options.Length > 0) { CommandEntrySet opset = new CommandEntrySet (); opset.AddItem (ViewCommands.TreeDisplayOptionList); opset.AddItem (Command.Separator); opset.AddItem (ViewCommands.ResetTreeDisplayOptions); return opset; } return null; } else { ExtensionContext ctx = AddinManager.CreateExtensionContext (); ctx.RegisterCondition ("ItemType", new ItemTypeCondition (tnav.DataItem.GetType (), contextMenuTypeNameAliases)); CommandEntrySet eset = IdeApp.CommandService.CreateCommandEntrySet (ctx, menuPath); eset.AddItem (Command.Separator); if (!tnav.Clone ().MoveToParent ()) { CommandEntrySet opset = eset.AddItemSet (GettextCatalog.GetString ("Display Options")); opset.AddItem (ViewCommands.TreeDisplayOptionList); opset.AddItem (Command.Separator); opset.AddItem (ViewCommands.ResetTreeDisplayOptions); // opset.AddItem (ViewCommands.CollapseAllTreeNodes); } eset.AddItem (ViewCommands.RefreshTree); return eset; } } [CommandUpdateHandler (ViewCommands.TreeDisplayOptionList)] protected void BuildTreeOptionsMenu (CommandArrayInfo info) { foreach (TreePadOption op in options) { CommandInfo ci = new CommandInfo (op.Label); ci.Checked = globalOptions [op.Id]; info.Add (ci, op.Id); } } [CommandHandler (ViewCommands.TreeDisplayOptionList)] protected void OptionToggled (string optionId) { globalOptions [optionId] = !globalOptions [optionId]; RefreshRoots (); } [CommandHandler (ViewCommands.ResetTreeDisplayOptions)] protected void ResetOptions () { foreach (TreePadOption op in options) globalOptions [op.Id] = op.DefaultValue; RefreshRoots (); } void RefreshRoots () { Gtk.TreeIter it; if (!store.GetIterFirst (out it)) return; do { ITreeBuilder tb = CreateBuilder (it); tb.UpdateAll (); } while (store.IterNext (ref it)); } protected void RefreshTree () { foreach (TreeNodeNavigator node in GetSelectedNodes ()) { Gtk.TreeIter it = node.CurrentPosition._iter; if (store.IterIsValid (it)) { ITreeBuilder tb = CreateBuilder (it); tb.UpdateAll (); } } } [CommandHandler (ViewCommands.CollapseAllTreeNodes)] protected void CollapseTree () { tree.CollapseAll(); } public bool ShowSelectionPopupButton { get { return showSelectionPopupButton; } set { showSelectionPopupButton = value; UpdateSelectionPopupButton (); } } [GLib.ConnectBefore] void OnKeyPress (object o, Gtk.KeyPressEventArgs args) { if (args.Event.Key == Gdk.Key.Delete || args.Event.Key == Gdk.Key.KP_Delete) { DeleteCurrentItem (); args.RetVal = true; return; } //HACK: to work around "Bug 377810 - Many errors when expanding MonoDevelop treeviews with keyboard" // The shift-right combo recursively expands all child nodes but the OnTestExpandRow callback // modifies tree and successive calls get passed an invalid iter. Using the path to regenerate the iter // causes a Gtk-Fatal. bool shift = (args.Event.State & Gdk.ModifierType.ShiftMask) != 0; if (args.Event.Key == Gdk.Key.asterisk || args.Event.Key == Gdk.Key.KP_Multiply || (shift && (args.Event.Key == Gdk.Key.Right || args.Event.Key == Gdk.Key.KP_Right || args.Event.Key == Gdk.Key.plus || args.Event.Key == Gdk.Key.KP_Add))) { Gtk.TreeIter iter; foreach (Gtk.TreePath path in tree.Selection.GetSelectedRows ()) { store.GetIter (out iter, path); Expand (iter); } args.RetVal = true; return; } if (args.Event.Key == Gdk.Key.Right || args.Event.Key == Gdk.Key.KP_Right) { ExpandCurrentItem (); args.RetVal = true; return; } if (args.Event.Key == Gdk.Key.Left || args.Event.Key == Gdk.Key.KP_Left) { CollapseCurrentItem (); args.RetVal = true; return; } if (args.Event.Key == Gdk.Key.Return || args.Event.Key == Gdk.Key.KP_Enter || args.Event.Key == Gdk.Key.ISO_Enter) { ActivateCurrentItem (); args.RetVal = true; return; } } void Expand (Gtk.TreeIter it) { tree.ExpandRow (store.GetPath (it), false); Gtk.TreeIter ci; if (store.IterChildren (out ci, it)) { do { Expand (ci); } while (store.IterNext (ref ci)); } } protected virtual void OnNodeActivated (object sender, Gtk.RowActivatedArgs args) { ActivateCurrentItem (); } void UpdateSelectionPopupButton () { if (editingText) return; if (lastPopupButtonIter != null) { if (store.IterIsValid (lastPopupButtonIter.Value)) tree.Model.SetValue (lastPopupButtonIter.Value, ShowPopupColumn, false); lastPopupButtonIter = null; } if (showSelectionPopupButton) { var sel = Tree.Selection.GetSelectedRows (); if (sel.Length > 0) { Gtk.TreeIter it; if (store.GetIter (out it, sel[0])) { lastPopupButtonIter = it; tree.Model.SetValue (it, ShowPopupColumn, true); } } } } protected virtual void OnSelectionChanged (object sender, EventArgs args) { UpdateSelectionPopupButton (); TreeNodeNavigator node = (TreeNodeNavigator) GetSelectedNode (); if (node != null) { NodeBuilder[] chain = node.NodeBuilderChain; NodePosition pos = node.CurrentPosition; foreach (NodeBuilder b in chain) { try { NodeCommandHandler handler = b.CommandHandler; handler.SetCurrentNode (node); handler.OnItemSelected (); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } node.MoveToPosition (pos); } } } protected override void OnDestroyed () { IdeApp.Preferences.CustomPadFontChanged -= CustomFontPropertyChanged;; if (pix_render != null) { pix_render.Destroy (); pix_render = null; } if (complete_column != null) { complete_column.Destroy (); complete_column = null; } if (text_render != null) { text_render.Destroy (); text_render = null; } if (store != null) { Clear (); store.Dispose (); store = null; } if (builders != null) { foreach (NodeBuilder nb in builders) { try { nb.Dispose (); } catch (Exception ex) { LoggingService.LogError (ex.ToString ()); } } builders = null; } builderChains.Clear (); base.OnDestroyed (); } class PopupButton: Gtk.EventBox { public event EventHandler Clicked; public PopupButton () { Gtk.Button b = new Gtk.Button ("..."); b.CanFocus = false; Add (b); b.Clicked += delegate { if (Clicked != null) Clicked (this, EventArgs.Empty); }; ShowAll (); } } internal class PadCheckMenuItem: Gtk.CheckMenuItem { internal string Id; public PadCheckMenuItem (string label, string id): base (label) { Id = id; } } internal class TreeBuilderContext: ITreeBuilderContext { ExtensibleTreeView pad; Hashtable icons = new Hashtable (); Hashtable composedIcons = new Hashtable (); internal TreeBuilderContext (ExtensibleTreeView pad) { this.pad = pad; } public ITreeBuilder GetTreeBuilder () { Gtk.TreeIter iter; if (!pad.store.GetIterFirst (out iter)) return pad.CreateBuilder (Gtk.TreeIter.Zero); else return pad.CreateBuilder (iter); } public ITreeBuilder GetTreeBuilder (object dataObject) { ITreeBuilder tb = pad.CreateBuilder (); if (tb.MoveToObject (dataObject)) return tb; else return null; } public ITreeBuilder GetTreeBuilder (ITreeNavigator navigator) { return pad.CreateBuilder (navigator.CurrentPosition._iter); } public Xwt.Drawing.Image GetIcon (string id) { Xwt.Drawing.Image icon = icons [id] as Xwt.Drawing.Image; if (icon == null) { icon = ImageService.GetIcon (id).WithSize (Gtk.IconSize.Menu); icons [id] = icon; } return icon; } public Xwt.Drawing.Image GetComposedIcon (Xwt.Drawing.Image baseIcon, object compositionKey) { Hashtable itable = composedIcons [baseIcon] as Hashtable; if (itable == null) return null; return itable [compositionKey] as Xwt.Drawing.Image; } public Xwt.Drawing.Image CacheComposedIcon (Xwt.Drawing.Image baseIcon, object compositionKey, Xwt.Drawing.Image composedIcon) { Hashtable itable = composedIcons [baseIcon] as Hashtable; if (itable == null) { itable = new Hashtable (); composedIcons [baseIcon] = itable; } itable [compositionKey] = composedIcon; return composedIcon; } public ITreeNavigator GetTreeNavigator (object dataObject) { Gtk.TreeIter iter; if (!pad.GetFirstNode (dataObject, out iter)) return null; return new TreeNodeNavigator (pad, iter); } public ExtensibleTreeView Tree { get { return pad; } } } class ExtensibleTreeViewTree : ContextMenuTreeView { ExtensibleTreeView tv; public ExtensibleTreeViewTree (ExtensibleTreeView tv) { this.tv = tv; EnableModelDragDest (targetTable, Gdk.DragAction.Copy | Gdk.DragAction.Move); Gtk.Drag.SourceSet (this, Gdk.ModifierType.Button1Mask, targetTable, Gdk.DragAction.Copy | Gdk.DragAction.Move); } static Gtk.TargetEntry [] targetTable = new Gtk.TargetEntry [] { new Gtk.TargetEntry ("text/uri-list", 0, 11 ), new Gtk.TargetEntry ("text/plain", 0, 22), new Gtk.TargetEntry ("application/x-rootwindow-drop", 0, 33) }; public object[] dragObjects = null; bool dropping = false; Func nodeToUri; public void EnableDragUriSource (Func nodeToUri) { this.nodeToUri = nodeToUri; } protected override void OnDragBegin (Gdk.DragContext context) { Xwt.Drawing.Image dragIcon; dragObjects = tv.GetDragObjects (out dragIcon); Gtk.Drag.SetIconPixbuf (context, dragIcon != null ? dragIcon.ToPixbuf (Gtk.IconSize.Menu) : null, -10, -10); base.OnDragBegin (context); } protected override void OnDragEnd (Gdk.DragContext context) { dragObjects = null; base.OnDragEnd (context); } protected override bool OnDragMotion (Gdk.DragContext context, int x, int y, uint time) { //OnDragDataReceived callback loses x/y values, so stash them this.x = x; this.y = y; if (dragObjects == null) { //it's a drag from outside, need to retrieve the data. This will cause OnDragDataReceived to be called. Gdk.Atom atom = Gtk.Drag.DestFindTarget (this, context, null); Gtk.Drag.GetData (this, context, atom, time); } else { //it's from inside, can call OnDragDataReceived directly OnDragDataReceived (context, x, y, null, 0, time); } return true; } int x, y; protected override void OnDragDataReceived (Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { x = this.x; y = this.y; object[] data = dragObjects ?? new object[] { selection_data }; bool canDrop = tv.CheckAndDrop (x, y, dropping, context, data); if (dropping) { dropping = false; SetDragDestRow (null, 0); Gtk.Drag.Finish (context, canDrop, true, time); return; } //let default handler handle hover-to-expand, autoscrolling, etc base.OnDragMotion (context, x, y, time); //if we can't handle it, flag as not droppable and remove the drop marker if (!canDrop) { Gdk.Drag.Status (context, (Gdk.DragAction)0, time); SetDragDestRow (null, 0); } } protected override bool OnDragDrop (Gdk.DragContext context, int x, int y, uint time_) { dropping = true; return base.OnDragDrop (context, x, y, time_); } protected override void OnDragDataGet (Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time_) { if (dragObjects == null || nodeToUri == null) return; uint uriListTarget = targetTable [0].Info; if (info == uriListTarget) { var sb = new StringBuilder (); foreach (var dobj in dragObjects) { var val = nodeToUri (dobj); if (val != null) { sb.AppendLine (val); } } selection_data.Set (selection_data.Target, selection_data.Format, Encoding.UTF8.GetBytes (sb.ToString ())); } } } class CustomCellRendererText: Gtk.CellRendererText { Pango.Layout layout; Pango.FontDescription scaledFont, customFont; static Xwt.Drawing.Image popupIcon; static Xwt.Drawing.Image popupIconDown; static Xwt.Drawing.Image popupIconHover; bool bound; ExtensibleTreeView parent; Gdk.Rectangle buttonScreenRect; Gdk.Rectangle buttonAllocation; string markup; const int StatusIconSpacing = 4; public bool Pushed { get; set; } //using this instead of FontDesc property, FontDesc seems to be broken public Pango.FontDescription CustomFont { get { return customFont; } set { if (scaledFont != null) { scaledFont.Dispose (); scaledFont = null; } customFont = value; } } static CustomCellRendererText () { popupIcon = Xwt.Drawing.Image.FromResource ("tree-popup-button.png"); popupIconDown = Xwt.Drawing.Image.FromResource ("tree-popup-button-down.png"); popupIconHover = Xwt.Drawing.Image.FromResource ("tree-popup-button-hover.png"); } [GLib.Property ("text-markup")] public string TextMarkup { get { return markup; } set { Markup = markup = value; } } [GLib.Property ("show-popup-button")] public bool ShowPopupButton { get; set; } [GLib.Property ("status-icon")] public Xwt.Drawing.Image StatusIcon { get; set; } public CustomCellRendererText (ExtensibleTreeView parent) { this.parent = parent; } void SetupLayout (Gtk.Widget widget) { if (scaledFont == null) { if (scaledFont != null) scaledFont.Dispose (); scaledFont = (customFont ?? parent.Style.FontDesc).Copy (); scaledFont.Size = customFont.Size; if (layout != null) layout.FontDescription = scaledFont; } if (layout == null || layout.Context != widget.PangoContext) { if (layout != null) layout.Dispose (); layout = new Pango.Layout (widget.PangoContext); layout.FontDescription = scaledFont; } layout.SetMarkup (TextMarkup); } const int iconMode = 0; protected override void Render (Gdk.Drawable window, Gtk.Widget widget, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gdk.Rectangle expose_area, Gtk.CellRendererState flags) { Gtk.StateType st = Gtk.StateType.Normal; if ((flags & Gtk.CellRendererState.Prelit) != 0) st = Gtk.StateType.Prelight; if ((flags & Gtk.CellRendererState.Focused) != 0) st = Gtk.StateType.Normal; if ((flags & Gtk.CellRendererState.Insensitive) != 0) st = Gtk.StateType.Insensitive; if ((flags & Gtk.CellRendererState.Selected) != 0) st = widget.HasFocus ? Gtk.StateType.Selected : Gtk.StateType.Active; SetupLayout (widget); int w, h; layout.GetPixelSize (out w, out h); int tx = cell_area.X + (int)Xpad; int ty = cell_area.Y + (cell_area.Height - h) / 2; bool hasStatusIcon = StatusIcon != CellRendererImage.NullImage && StatusIcon != null; if (hasStatusIcon && iconMode != 2) { var x = iconMode == 1 ? tx : tx + w + StatusIconSpacing; using (var ctx = Gdk.CairoHelper.Create (window)) { ctx.DrawImage (widget, StatusIcon, x, cell_area.Y + (cell_area.Height - StatusIcon.Height) / 2); } if (iconMode == 1) tx += (int)StatusIcon.Width + StatusIconSpacing; } window.DrawLayout (widget.Style.TextGC (st), tx, ty, layout); hasStatusIcon &= iconMode == 2; if (ShowPopupButton || hasStatusIcon) { if (!bound) { bound = true; ((Gtk.ScrolledWindow)widget.Parent).Hadjustment.ValueChanged += delegate { foreach (var r in parent.Tree.Selection.GetSelectedRows ()) { var rect = parent.Tree.GetCellArea (r, parent.Tree.Columns [0]); parent.Tree.QueueDrawArea (rect.X, rect.Y, rect.Width, rect.Height); } }; } var selected = (flags & Gtk.CellRendererState.Selected) != 0; if (selected || hasStatusIcon) { var icon = Pushed ? popupIconDown : popupIcon; var dy = (cell_area.Height - (int)icon.Height) / 2 - 1; var y = cell_area.Y + dy; var x = cell_area.X + cell_area.Width - (int)icon.Width - dy; var sw = (Gtk.ScrolledWindow)widget.Parent; int ox, oy, ow, oh; sw.GdkWindow.GetOrigin (out ox, out oy); sw.GdkWindow.GetSize (out ow, out oh); ox += sw.Allocation.X; oy += sw.Allocation.Y; if (sw.VScrollbar.Visible) ow -= sw.VScrollbar.Allocation.Width; int cx, cy, cw, ch; ((Gdk.Window)window).GetOrigin (out cx, out cy); ((Gdk.Window)window).GetSize (out cw, out ch); cx += widget.Allocation.X; cy += widget.Allocation.Y; int rp = ox + ow; int diff = rp - (cx + cw); if (diff < 0) { x += diff; if (x < cell_area.X + 20) x = cell_area.X + 20; } buttonScreenRect = new Gdk.Rectangle (cx + x, cy + y, (int)popupIcon.Width, (int)popupIcon.Height); buttonAllocation = new Gdk.Rectangle (x, y, (int)popupIcon.Width, (int)popupIcon.Height); buttonAllocation = GtkUtil.ToScreenCoordinates (widget, ((Gdk.Window)window), buttonAllocation); buttonAllocation = GtkUtil.ToWindowCoordinates (widget, widget.GdkWindow, buttonAllocation); bool mouseOver = (flags & Gtk.CellRendererState.Prelit) != 0 && buttonScreenRect.Contains (PointerPosition); if (mouseOver && !Pushed) icon = popupIconHover; using (var ctx = Gdk.CairoHelper.Create (window)) { if (ShowPopupButton && selected) { if (hasStatusIcon) x -= (int) icon.Width + StatusIconSpacing; ctx.DrawImage (widget, icon, x, y); if (hasStatusIcon) x += (int) icon.Width + StatusIconSpacing; } if (hasStatusIcon) { ctx.DrawImage (widget, StatusIcon, x, y); } } } } } public Gdk.Rectangle GetStatusIconArea (Gtk.Widget widget, Gdk.Rectangle cell_area) { SetupLayout (widget); int w, h; layout.GetPixelSize (out w, out h); int tx = cell_area.X + (int)Xpad; if (iconMode == 0) { var x = tx + w + StatusIconSpacing; return new Gdk.Rectangle (x, cell_area.Y, (int) StatusIcon.Width, (int) cell_area.Height); } else if (iconMode == 1) { var x = tx; return new Gdk.Rectangle (x, cell_area.Y, (int) StatusIcon.Width, (int) cell_area.Height); } else { return new Gdk.Rectangle (cell_area.Width - (int) StatusIcon.Width, cell_area.Y, (int) StatusIcon.Width, (int) cell_area.Height); } } public override void GetSize (Gtk.Widget widget, ref Gdk.Rectangle cell_area, out int x_offset, out int y_offset, out int width, out int height) { base.GetSize (widget, ref cell_area, out x_offset, out y_offset, out width, out height); if (StatusIcon != CellRendererImage.NullImage && StatusIcon != null) width += (int) StatusIcon.Width + StatusIconSpacing; } public bool PointerInButton (int px, int py) { return buttonScreenRect.Contains (px, py); } public Gdk.Point PointerPosition { get; set; } public Gdk.Rectangle PopupAllocation { get { return buttonAllocation; } } protected override void OnDestroyed () { base.OnDestroyed (); if (scaledFont != null) scaledFont.Dispose (); if (layout != null) layout.Dispose (); } } } class NodeCommandTargetChain: ICommandDelegatorRouter { NodeCommandHandler target; ITreeNavigator[] nodes; internal NodeCommandTargetChain Next; public NodeCommandTargetChain (NodeCommandHandler target, ITreeNavigator[] nodes) { this.nodes = nodes; this.target = target; } public object GetNextCommandTarget () { target.SetCurrentNodes (null); return Next; } public object GetDelegatedCommandTarget () { target.SetCurrentNodes (nodes); return target; } } class IterComparer: IEqualityComparer { Gtk.TreeStore store; public IterComparer (Gtk.TreeStore store) { this.store = store; } public bool Equals (Gtk.TreeIter x, Gtk.TreeIter y) { if (!store.IterIsValid (x) || !store.IterIsValid (y)) return false; Gtk.TreePath px = store.GetPath (x); Gtk.TreePath py = store.GetPath (y); if (px == null || py == null) return false; return px.Equals (py); } public int GetHashCode (Gtk.TreeIter obj) { if (!store.IterIsValid (obj)) return 0; Gtk.TreePath p = store.GetPath (obj); if (p == null) return 0; else return p.ToString ().GetHashCode (); } } class ZoomableCellRendererPixbuf: CellRendererImage { Dictionary resizedCache = new Dictionary (); Xwt.Drawing.Image overlayBottomLeft; Xwt.Drawing.Image overlayBottomRight; Xwt.Drawing.Image overlayTopLeft; Xwt.Drawing.Image overlayTopRight; public override Xwt.Drawing.Image Image { get { return base.Image; } set { base.Image = GetResized (value); } } public override Xwt.Drawing.Image ImageExpanderOpen { get { return base.ImageExpanderOpen; } set { base.ImageExpanderOpen = GetResized (value); } } public override Xwt.Drawing.Image ImageExpanderClosed { get { return base.ImageExpanderClosed; } set { base.ImageExpanderClosed = GetResized (value); } } [GLib.Property ("overlay-image-top-left")] public Xwt.Drawing.Image OverlayTopLeft { get { return overlayTopLeft; } set { overlayTopLeft = GetResized (value); } } [GLib.Property ("overlay-image-top-right")] public Xwt.Drawing.Image OverlayTopRight { get { return overlayTopRight; } set { overlayTopRight = GetResized (value); } } [GLib.Property ("overlay-image-bottom-left")] public Xwt.Drawing.Image OverlayBottomLeft { get { return overlayBottomLeft; } set { overlayBottomLeft = GetResized (value); } } [GLib.Property ("overlay-image-bottom-right")] public Xwt.Drawing.Image OverlayBottomRight { get { return overlayBottomRight; } set { overlayBottomRight = GetResized (value); } } Xwt.Drawing.Image GetResized (Xwt.Drawing.Image value) { //this can happen during solution deserialization if the project is unrecognized //because a line is added into the treeview with no icon if (value == null || value == CellRendererImage.NullImage) return null; return value; } public override void GetSize (Gtk.Widget widget, ref Gdk.Rectangle cell_area, out int x_offset, out int y_offset, out int width, out int height) { base.GetSize (widget, ref cell_area, out x_offset, out y_offset, out width, out height); /* if (overlayBottomLeft != null || overlayBottomRight != null) height += overlayOverflow; if (overlayTopLeft != null || overlayTopRight != null) height += overlayOverflow; if (overlayBottomRight != null || overlayTopRight != null) width += overlayOverflow;*/ } const int overlayOverflow = 2; protected override void Render (Gdk.Drawable window, Gtk.Widget widget, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gdk.Rectangle expose_area, Gtk.CellRendererState flags) { base.Render (window, widget, background_area, cell_area, expose_area, flags); if (overlayBottomLeft != null || overlayBottomRight != null || overlayTopLeft != null || overlayTopRight != null) { int x, y; Xwt.Drawing.Image image; GetImageInfo (cell_area, out image, out x, out y); if (image == null) return; using (var ctx = Gdk.CairoHelper.Create (window)) { if (overlayBottomLeft != null && overlayBottomLeft != NullImage) ctx.DrawImage (widget, overlayBottomLeft, x - overlayOverflow, y + image.Height - overlayBottomLeft.Height + overlayOverflow); if (overlayBottomRight != null && overlayBottomRight != NullImage) ctx.DrawImage (widget, overlayBottomRight, x + image.Width - overlayBottomRight.Width + overlayOverflow, y + image.Height - overlayBottomRight.Height + overlayOverflow); if (overlayTopLeft != null && overlayTopLeft != NullImage) ctx.DrawImage (widget, overlayTopLeft, x - overlayOverflow, y - overlayOverflow); if (overlayTopRight != null && overlayTopRight != NullImage) ctx.DrawImage (widget, overlayTopRight, x + image.Width - overlayTopRight.Width + overlayOverflow, y - overlayOverflow); } } } } class NodeHashtable: Dictionary { // This dictionary can be configured to use object reference equality // instead of regular object equality for a specific set of types NodeComparer nodeComparer; public NodeHashtable (): base (new NodeComparer ()) { nodeComparer = (NodeComparer)Comparer; } /// /// Sets that the objects of the specified type have to be compared /// using object reference equality /// public void RegisterByRefType (Type type) { nodeComparer.byRefTypes.Add (type); } class NodeComparer: IEqualityComparer { public HashSet byRefTypes = new HashSet (); public Dictionary typeData = new Dictionary (); bool IEqualityComparer.Equals (object x, object y) { if (CompareByRef (x.GetType ())) return x == y; else return x.Equals (y); } int IEqualityComparer.GetHashCode (object obj) { if (CompareByRef (obj.GetType ())) return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode (obj); else return obj.GetHashCode (); } bool CompareByRef (Type type) { if (byRefTypes.Count == 0) return false; bool compareRef; if (!typeData.TryGetValue (type, out compareRef)) { compareRef = false; var t = type; while (t != null) { if (byRefTypes.Contains (t)) { compareRef = true; break; } t = t.BaseType; } typeData [type] = compareRef; } return compareRef; } } } }