/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; using JetBrains.Omea.Base; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.ResourceTools; using JetBrains.Omea.Containers; using JetBrains.UI.Components.CustomTreeView; using JetBrains.UI.Components.RichTextTreeView; using JetBrains.UI.RichText; using JetBrains.UI.Components.TreeSearchWindow; using JetBrains.UI.Interop; using JetBrains.DataStructures; using TreeViewItemFlags = JetBrains.UI.Interop.TreeViewItemFlags; using TVITEM = JetBrains.UI.Interop.TVITEM; namespace JetBrains.Omea.GUIControls { /** * A tree view displaying a resource in each of its nodes. */ public class ResourceTreeView: CustomTreeView { /// /// Required designer variable. /// private System.ComponentModel.Container components = null; private IResource _rootResource; private int _parentProperty = -1; private int _openProperty = -1; private int _checkedProperty = -1; private int _checkedSetValue = 1; private int _checkedUnsetValue = 0; private ArrayList _nodeDecorators = new ArrayList(); private ArrayList _nodeFilters = new ArrayList(); private RichTextNodePainter _nodePainter; private SearchWindow _searchWindow; private bool _showContextMenu = true; private object _menuContextInstance; private IResource _resourceToSelect = null; private TreeNode _dropHighlightNode = null; private bool _delaySaveChecked = false; private bool _dropOnEmpty = false; private bool _selectAddedItems = false; private bool _uniqueResources = true; private HashSet _expandingNodes = new HashSet(); private Timer _dragScrollTimer; private bool _dragScrollUp; private Timer _expandTimer; private bool _executeDoubleClickAction = true; private ArrayList _postponedUpdateNodeData = new ArrayList(); private IntArrayList _postponedRemoveNodeData = new IntArrayList(); private bool _showRootResource = false; private bool _expandAllRequested = false; private IResourceChildProvider _resourceChildProvider; private int _clicksAfterFocus = 0; private bool _lastKeyDownHandled = false; private long _lastTicksOfProcessingPendingUpdates = 0; private bool _inProcessPendingUpdates = false; private class ResourceNodeData: IDisposable { private ResourceTreeView _tree; private TreeNode _node; private IResourceList _childResources; private ResourceListEventQueue _eventQueue; private IntArrayList _postponedResourceAdds = new IntArrayList(); private bool _removingFromTree; internal ResourceNodeData( ResourceTreeView tree, TreeNode node, IResourceList children ) { _tree = tree; _node = node; _childResources = children; _eventQueue = new ResourceListEventQueue(); _eventQueue.Attach( _childResources ); } public void Dispose() { _eventQueue.Detach(); _childResources.Dispose(); } internal IResourceList ChildResources { get { return _childResources; } } internal bool RemovingFromTree { get { return _removingFromTree; } set { _removingFromTree = value; } } internal void ProcessPendingUpdates() { if ( _tree.IsDisposed || _removingFromTree || Core.State == CoreState.ShuttingDown ) { return; } TreeNode lastNewNode = null; bool nodesRemoved = false; while( true ) { if ( ! _eventQueue.BeginProcessEvents() ) break; ResourceListEvent ev = _eventQueue.GetNextEvent(); if ( ev == null ) { _eventQueue.EndProcessEvents(); break; } switch( ev.EventType ) { case EventType.Add: lastNewNode = ProcessNodeAdd( ev.ResourceID, true ); _eventQueue.EndProcessEvents(); break; case EventType.Change: _eventQueue.EndProcessEvents(); ProcessNodeChange( ev ); break; case EventType.Remove: // Removing a node can cause a selection change, and a lot of user // code to be called, so in order to avoid deadlocks, we need to // release the resource list lock before we process the event _eventQueue.EndProcessEvents(); ProcessNodeRemove( ev.ResourceID ); nodesRemoved = true; break; default: _eventQueue.EndProcessEvents(); break; } } // set the selected node outside of resource list lock if ( lastNewNode != null && _tree.SelectAddedItems ) { _tree.SelectedNode = lastNewNode; } if ( nodesRemoved && _tree.DoubleBuffer ) { _tree.RestartGarbageCleanupTimer(); } } /** * Processes pending adds of resources. The method is called after all the * remove events have been processed, so the check for existing nodes * will work correctly. */ internal void ProcessPostponedUpdates() { foreach( int resourceID in _postponedResourceAdds ) { ProcessNodeAdd( resourceID, false ); } _postponedResourceAdds.Clear(); } private TreeNode ProcessNodeAdd( int resourceId, bool mayPostpone ) { if ( _tree.GetResourceNode( _node, resourceId ) != null ) { // we may have a pending event to remove this resource if ( mayPostpone ) { _postponedResourceAdds.Add( resourceId ); _tree.AddPostponedUpdateNodeData( this ); } return null; } IResource res; // if the tree is filtered, we can't use the list index as the insert position // => find the previous visible node and use its index + 1 int listIndex = _childResources.IndexOf( resourceId ); int insertIndex = 0; while( listIndex > 0 ) { listIndex--; res = _childResources [listIndex]; TreeNode node = _tree.GetResourceNode( _node, res.Id ); if ( node != null ) { insertIndex = node.Index+1; break; } } try { res = Core.ResourceStore.LoadResource( resourceId ); } catch( StorageException ) { return null; } TreeNode newNode = _tree.AddResourceNode( _node, insertIndex, res ); return newNode; } private void ProcessNodeChange( ResourceListEvent ev ) { IResource res = Core.ResourceStore.LoadResource( ev.ResourceID ); if ( CheckRefreshFilter( res ) ) return; TreeNode node = _tree.GetResourceNode( _node, ev.ResourceID ); if ( node != null ) { int imgIndex = Core.ResourceIconManager.GetIconIndex( res ); if ( imgIndex != node.ImageIndex ) { node.ImageIndex = imgIndex; node.SelectedImageIndex = imgIndex; } if ( ev.ChangeSet.IsDisplayNameAffected || _tree._autoUpdateDecorators || _tree.MultiSelect ) { _tree.UpdateNodeRichText( node, false ); } if ( _tree.IsHandleCreated && node.Nodes.Count == 0 && res.GetLinksTo( null, _tree.ParentProperty ).Count > 0 ) { _tree.SetNodeChildCount( node, 1 ); } } } internal bool CheckRefreshFilter( IResource res ) { TreeNode node = _tree.GetResourceNode( _node, res.Id ); if ( node == null ) { // the node may pass the filters after the change if ( _tree.FiltersAccept( res, _node ) ) { ProcessNodeAdd( res.Id, true ); return true; } } if ( node != null && node.TreeView != null ) { if ( !_tree.FiltersAccept( res, _node ) ) { ProcessNodeRemove( res.Id ); return true; } } return false; } private void ProcessNodeRemove( int resourceID ) { TreeNode node = _tree.GetResourceNode( _node, resourceID ); // the node may have been removed by recursive remove and then added // to another child, and in this case we don't need to remove it again // - that's why we check the parent if ( node != null && node.TreeView != null && node.Parent == _node ) { ResourceNodeData childNodeData = null; if ( node.Nodes.Count > 0 ) { childNodeData = (ResourceNodeData) _tree._nodeData [resourceID]; if ( childNodeData != null ) { childNodeData.RemoveAllChildren(); childNodeData.RemovingFromTree = true; } } TreeNode parent = node.Parent; try { node.Remove(); } catch( NullReferenceException ) // #4930 { Trace.WriteLine( "Null reference exception when removing node from tree" ); } _tree.RemoveResourceNode( resourceID ); if ( _tree.IsHandleCreated && parent != null && parent.Nodes.Count == 0 ) { _tree.SetNodeChildCount( parent, 0 ); } } } private void RemoveAllChildren() { for( int i=_node.Nodes.Count-1; i >= 0; i-- ) { TreeNode node = _node.Nodes [i]; IResource res = (IResource) node.Tag; ProcessNodeRemove( res.OriginalId ); } } } private IntHashTable _nodeData = new IntHashTable(); // resource ID -> ResourceNodeData private IntHashTable _resourceToNodeMap = new IntHashTable(); // resource ID -> TreeNode private IntHashTable _resourceToNodeMapNew = new IntHashTable(); // resource ID -> TreeNode private HashSet _decorationChangedNodes = new HashSet(); internal bool _autoUpdateDecorators = false; private Timer _garbageRemoveTimer; public event EventHandler TreeCreated; public event EventHandler TreeUpdated; public event EventHandler MouseActivate; public ResourceTreeView() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); _dragScrollTimer = new Timer( components ); _dragScrollTimer.Interval = 250; _dragScrollTimer.Tick += _dragScrollTimer_Tick; _expandTimer = new Timer( components ); _expandTimer.Interval = 250; _expandTimer.Tick += _expandTimer_Tick; _nodePainter = new RichTextNodePainter(); _searchWindow = new SearchWindow(); Controls.Add( _searchWindow ); _searchWindow.Location = new Point( 4, 4 ); _garbageRemoveTimer = new Timer( components ); _garbageRemoveTimer.Interval = 300; _garbageRemoveTimer.Tick += _garbageRemoveTimer_OnTick; _searchWindow.TreeView = this; if ( ICore.Instance != null ) { if ( Core.State == CoreState.Initializing ) { Core.StateChanged += OnCoreStateChanged; } else { Initialize(); } } } private void OnCoreStateChanged( object sender, EventArgs e ) { if ( Core.State != CoreState.Initializing ) { Core.StateChanged -= OnCoreStateChanged; Initialize(); } } /// /// Clean up any resources being used. /// protected override void Dispose( bool disposing ) { if( disposing ) { ImageList = null; if ( ICore.Instance != null ) { Core.ResourceAP.JobFinished -= OnResourceOperationFinished; Core.ResourceAP.QueueGotEmpty -= ResourceAP_QueueGotEmpty; Core.UIManager.ExitMenuLoop -= UiManager_OnExitMenuLoop; } ClearNodeData(); if( components != null ) components.Dispose(); } base.Dispose( disposing ); } #region Component Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { components = new System.ComponentModel.Container(); } #endregion private void Initialize() { Core.ResourceAP.JobFinished += OnResourceOperationFinished; Core.ResourceAP.QueueGotEmpty += ResourceAP_QueueGotEmpty; ImageList = Core.ResourceIconManager.ImageList; Core.UIManager.ExitMenuLoop += UiManager_OnExitMenuLoop; } private void UiManager_OnExitMenuLoop( object sender, EventArgs e ) { // when double-buffering is used, and a menu item is selected over the tree view, // garbage remains on the place of the cursor after the menu is closed _garbageRemoveTimer.Start(); } private void _garbageRemoveTimer_OnTick( object sender, EventArgs e ) { _garbageRemoveTimer.Stop(); Invalidate(); } public IResourceChildProvider ResourceChildProvider { get { return _resourceChildProvider; } set { _resourceChildProvider = value; } } [Browsable(false), DefaultValue(null)] public IResource RootResource { get { return _rootResource; } set { if ( _rootResource != value ) { _rootResource = value; if ( IsHandleCreated && _parentProperty != -1 ) { RecreateTree(); } } } } [DefaultValue(-1)] public int ParentProperty { get { return _parentProperty; } set { if ( _parentProperty != value ) { _parentProperty = value; if ( IsHandleCreated && _rootResource != null ) { RecreateTree(); } } } } public bool ShowRootResource { get { return _showRootResource; } set { if ( _showRootResource != value ) { _showRootResource = value; if ( IsHandleCreated && _rootResource != null && _parentProperty >= 0 ) { RecreateTree(); } } } } [DefaultValue(-1)] public int OpenProperty { get { return _openProperty; } set { _openProperty = value; } } [DefaultValue(-1)] public int CheckedProperty { get { return _checkedProperty; } set { _checkedProperty = value; } } [DefaultValue(1)] public int CheckedSetValue { get { return _checkedSetValue; } set { _checkedSetValue = value; } } [DefaultValue(0)] public int CheckedUnsetValue { get { return _checkedUnsetValue; } set { _checkedUnsetValue = value; } } /** * If DelaySaveChecked is set, the Checked property of the nodes is set not * immediately after a checkbox has been toggled, but by an explicit call to * SaveCheckedState(). */ [DefaultValue(false)] public bool DelaySaveChecked { get { return _delaySaveChecked; } set { _delaySaveChecked = value; } } [DefaultValue(true)] public bool ShowContextMenu { get { return _showContextMenu; } set { _showContextMenu = value; } } [Browsable(false), DefaultValue(null)] public IResource SelectedResource { get { if ( SelectedNode == null ) return null; return (IResource) SelectedNode.Tag; } set { SelectResourceNode( value ); } } [Browsable(false), DefaultValue(null)] public IResourceList SelectedResources { get { TreeNode[] nodes = SelectedNodes; IntArrayList selectedResourceIds = new IntArrayList( nodes.Length ); for( int i=0; i /// Whether a resource can be displayed in multiple places in the tree view. /// [DefaultValue(true)] public bool UniqueResources { get { return _uniqueResources; } set { _uniqueResources = value; } } public new void ExpandAll() { if ( IsHandleCreated ) { base.ExpandAll(); } else { _expandAllRequested = true; } } public event ResourceDragEventHandler ResourceDragOver; public event ResourceDragEventHandler ResourceDrop; public event TreeViewEventHandler ResourceAdded; /** * Sets the root resource and parent property ID in a single call, * and does not update the tree. */ public void SetRootResource( IResource root, int parentProp ) { _rootResource = root; _parentProperty = parentProp; } public void AddNodeDecorator( IResourceNodeDecorator decorator ) { AddNodeDecorator( decorator, false ); } public void AddNodeDecorator( IResourceNodeDecorator decorator, bool autoUpdate ) { _nodeDecorators.Add( decorator ); decorator.DecorationChanged += OnDecorationChanged; // only when the first decorator is added, the RichTextNodePainter is enabled NodePainter = _nodePainter; if ( autoUpdate ) { _autoUpdateDecorators = true; } } protected override void OnMultiSelectChanged() { base.OnMultiSelectChanged(); if ( MultiSelect ) { NodePainter = _nodePainter; } } #region Filters public void AddNodeFilter( IResourceNodeFilter filter ) { if ( filter == null ) throw new ArgumentNullException( "filter" ); _nodeFilters.Add( filter ); } /** * Removes the specified filter from the tree. Returns true if the filter * was actually removed. */ public bool RemoveNodeFilter( IResourceNodeFilter filter ) { int index = _nodeFilters.IndexOf( filter ); if ( index >= 0 ) { _nodeFilters.RemoveAt( index ); return true; } return false; } public IResourceNodeFilter[] GetNodeFilters() { return (IResourceNodeFilter[]) _nodeFilters.ToArray( typeof(IResourceNodeFilter) ); } /** * Removes all filters from the tree. */ public void ClearNodeFilters() { _nodeFilters.Clear(); } /** * After the filtering conditions have changed, clears and refills the tree. * @param keepSelection If false, the selection will not be set to the node * which was previously selected. */ public void UpdateNodeFilter( bool keepSelection ) { // if the handle has not yet been created, the tree was not yet built, // so when the handle is created, it will be built with the correct filter if ( IsHandleCreated ) { if ( keepSelection ) _resourceToSelect = SelectedResource; else _resourceToSelect = null; RecreateTree(); } } #endregion Filters public void ClearSearchWindow() { _searchWindow.CancelSearch(); } /** * Ensures that the children of the specified node are all created. */ public void ForceCreateChildren( TreeNode node ) { IResource res = (IResource) node.Tag; if ( node.Nodes.Count == 0 ) { ExpandNode( node, res, true ); } else { foreach( TreeNode childNode in node.Nodes ) { ForceCreateChildren( childNode ); } } } protected override void OnHandleCreated( EventArgs e ) { base.OnHandleCreated( e ); if ( _parentProperty != -1 && _rootResource != null ) { ProcessHandleCreated(); } } private void ProcessHandleCreated() { if ( _inProcessPendingUpdates ) { Core.UIManager.QueueUIJob( new MethodInvoker( ProcessHandleCreated ) ); return; } if ( IsDisposed ) // OM-10748 { return; } RecreateTree(); if ( _expandAllRequested ) { _expandAllRequested = false; ExpandAll(); } } private void ClearNodeData() { foreach( IntHashTable.Entry nodeData in _nodeData ) { ((ResourceNodeData) nodeData.Value).Dispose(); } _nodeData.Clear(); _resourceToNodeMap.Clear(); _resourceToNodeMapNew.Clear(); } private void RecreateTree() { ClearNodeData(); BeginUpdate(); try { while( Nodes.Count > 0 ) { try { Nodes [Nodes.Count-1].Remove(); } catch( NullReferenceException ex ) { Trace.WriteLine( "Error removing node from tree: " + ex.ToString() ); } } if ( _rootResource != null && _parentProperty >= 0 ) { TreeNode rootNode = null; if ( _showRootResource ) { rootNode = AddResourceNode( null, 0, _rootResource ); } ExpandNode( rootNode, _rootResource, false ); if ( _resourceToSelect != null ) { SelectResourceNode( _resourceToSelect ); _resourceToSelect = null; } } FlushNodeMap(); } finally { EndUpdate(); } if ( TreeCreated != null ) { TreeCreated( this, EventArgs.Empty ); } } private void ExpandNode( TreeNode parentNode, IResource parent, bool forceCreateChildren ) { if ( parentNode != null ) { _expandingNodes.Add( parentNode ); } IResourceList children = CreateResourceNodeData( parent, parentNode ); bool hasChildren = false; for( int i=0; i 0 ) { ExpandNode( childNode, child, forceCreateChildren ); childNode.Expand(); } else if ( forceCreateChildren ) { ExpandNode( childNode, child, true ); } } } catch( OpenAPI.InvalidResourceIdException ) { // Nothing to do, just ignore. } } if ( !hasChildren && parentNode != null ) { SetNodeChildCount( parentNode, 0 ); } if ( parentNode != null ) { _expandingNodes.Remove( parentNode ); } } private IResourceList CreateResourceNodeData( IResource parent, TreeNode parentNode ) { ResourceNodeData existingNodeData = (ResourceNodeData) _nodeData [parent.Id]; if ( existingNodeData != null ) { return existingNodeData.ChildResources; } IResourceList children = null; if ( _resourceChildProvider != null ) { children = _resourceChildProvider.GetChildResources( this, parent ); } if ( children == null ) { children = parent.GetLinksToLive( null, _parentProperty ); string nodeSort = Core.ResourceTreeManager.GetResourceNodeSort( parent ); if ( nodeSort != null ) { children.Sort( nodeSort ); } } ResourceNodeData parentData = new ResourceNodeData( this, parentNode, children ); _nodeData [parent.Id] = parentData; return children; } private TreeNode AddResourceNode( TreeNode parentNode, int index, IResource child ) { if ( !FiltersAccept( child, parentNode ) ) { return null; } int iconIndex = -1; if ( ICore.Instance != null ) { iconIndex = Core.ResourceIconManager.GetIconIndex( child ); } TreeNode node = new TreeNode( "", iconIndex, iconIndex ); node.Tag = child; if ( parentNode == null ) { if ( index < 0 ) { Nodes.Add( node ); } else { Nodes.Insert( index, node ); } } else { bool firstChild = ( parentNode.Nodes.Count == 0 ); if ( index < 0 ) { parentNode.Nodes.Add( node ); } else { parentNode.Nodes.Insert( index, node ); } if ( firstChild ) { SetNodeChildCount( parentNode, 1 ); } } if ( _checkedProperty >= 0 && child.GetIntProp( _checkedProperty ) == _checkedSetValue ) { node.Checked = true; } if ( IsResourceContainer( child ) ) { if ( child.GetLinksTo( null, _parentProperty ).Count > 0 ) { SetNodeChildCount( node, 1 ); } else { // we have no children now, but may get some later CreateResourceNodeData( child, node ); } } UpdateNodeRichText( node, true ); if ( ResourceAdded != null ) { ResourceAdded( this, new TreeViewEventArgs( node ) ); } _resourceToNodeMapNew [child.Id] = node; return node; } /** * Returns the node for the specified resource. */ internal TreeNode GetResourceNode( TreeNode parent, int resourceID ) { if ( UniqueResources ) { TreeNode node = (TreeNode) _resourceToNodeMap [resourceID]; if ( node == null ) { node = (TreeNode) _resourceToNodeMapNew [resourceID]; } return node; } if ( parent != null ) { foreach( TreeNode node in parent.Nodes ) { IResource res = (IResource) node.Tag; if ( res.OriginalId == resourceID ) return node; } } else { foreach( TreeNode node in Nodes ) { IResource res = (IResource) node.Tag; if ( res.OriginalId == resourceID ) return node; } } return null; } /** * Removes the resource node for the specified resource from the map. */ internal void RemoveResourceNode( int resourceID ) { _resourceToNodeMap.Remove( resourceID ); _postponedRemoveNodeData.Add( resourceID ); } /** * Gets the level of the node (0 if node=null, 1 if node is a top-level node, and so on) */ private static int GetNodeLevel( TreeNode node ) { int level = 0; while( node != null ) { level++; node = node.Parent; } return level; } /** * Checks if the specified node, with the specified parent node, matches the * tree filter conditions. */ public bool FiltersAccept( IResource res, TreeNode parentNode ) { int level = GetNodeLevel( parentNode ); foreach( IResourceNodeFilter filter in _nodeFilters ) { if ( !filter.AcceptNode( res, level ) ) { return false; } } return true; } /** * Notifies the tree that because of a change in filtering conditions the specified * node matches or no longer matches the filtering conditions. */ public void RefreshFilterForNode( IResource res ) { if ( InvokeRequired && IsHandleCreated ) { Core.UIManager.QueueUIJob( new ResourceDelegate( RefreshFilterForNode ), new object[] { res } ); } else { IResource parent = res.GetLinkProp( _parentProperty ); if ( parent != null ) { ResourceNodeData parentData = (ResourceNodeData) _nodeData [parent.Id]; if ( parentData != null ) { parentData.CheckRefreshFilter( res ); } } } } /** * Shows or hides the [+] sign on a tree node. */ private void SetNodeChildCount( TreeNode node, int count ) { if ( node.TreeView != null ) { TVITEM item = new TVITEM(); item.mask = TreeViewItemFlags.CHILDREN | TreeViewItemFlags.HANDLE; item.hItem = node.Handle; item.cChildren = count; Win32Declarations.SendMessage( Handle, TreeViewMessage.TVM_SETITEMA, 0, ref item ); } } public int GetNodeChildCount( TreeNode node ) { TVITEM item = new TVITEM(); item.mask = TreeViewItemFlags.CHILDREN | TreeViewItemFlags.HANDLE; item.hItem = node.Handle; Win32Declarations.SendMessage( Handle, TreeViewMessage.TVM_GETITEMA, 0, ref item ); return item.cChildren; } /** * Updates the decorated rich text for all nodes. */ public void RefreshNodeRichText() { if ( _nodeDecorators.Count > 0 ) { RefreshRichTextRecursive( Nodes ); } } /** * Updates the rich text for nodes in the specified collection and their children. */ private void RefreshRichTextRecursive( TreeNodeCollection nodes ) { if ( nodes != null ) { foreach( TreeNode node in nodes ) { UpdateNodeRichText( node, false ); RefreshRichTextRecursive( node.Nodes ); } } } internal void UpdateNodeRichText( TreeNode node, bool newNode ) { Debug.Assert( node != null ); IResource res = (IResource) node.Tag; if ( _nodeDecorators.Count > 0 || MultiSelect ) { RichText text = new RichText( res.DisplayName, new RichTextParameters( this.Font ) ); foreach( IResourceNodeDecorator dec in _nodeDecorators ) { dec.DecorateNode( res, text ); } _nodePainter.Add( node, text ); } if ( newNode || node.Text != res.DisplayName ) { node.Text = res.DisplayName; } } /** * Checks if the specified resource is a resource container. */ private bool IsResourceContainer( IResource child ) { return ICore.Instance.ResourceStore.ResourceTypes [child.Type].HasFlag( ResourceTypeFlags.ResourceContainer ); } protected override void OnBeforeExpand( TreeViewCancelEventArgs e ) { if ( e.Node.Nodes.Count == 0 ) { CreateChildNodes( e.Node ); } base.OnBeforeExpand( e ); } internal void CreateChildNodes( TreeNode node ) { IResource res = (IResource) node.Tag; ExpandNode( node, res, false ); if ( node.Nodes.Count == 0 ) { SetNodeChildCount( node, 0 ); } FlushNodeMap(); } protected override void OnAfterExpand( TreeViewEventArgs e ) { base.OnAfterExpand( e ); if ( _openProperty != -1 ) { IResource res = (IResource) e.Node.Tag; new ResourceProxy( res ).SetPropAsync( _openProperty, 1 ); } } protected override void OnAfterCollapse( TreeViewEventArgs e ) { base.OnAfterCollapse( e ); if ( _openProperty != -1 ) { IResource res = (IResource) e.Node.Tag; new ResourceProxy( res ).SetPropAsync( _openProperty, 0 ); } } protected override void OnAfterCheck( TreeViewEventArgs e ) { base.OnAfterCheck( e ); if ( _checkedProperty != -1 && !_delaySaveChecked ) { IResource res = (IResource) e.Node.Tag; new ResourceProxy( res ).SetPropAsync( _checkedProperty, e.Node.Checked ? _checkedSetValue : _checkedUnsetValue ); } } private void OnResourceOperationFinished( object sender, EventArgs e ) { if ( !IsHandleCreated ) return; if( _lastTicksOfProcessingPendingUpdates + 2000000 < DateTime.Now.Ticks ) { ForceProcessingPendingUpdates(); } } private void ResourceAP_QueueGotEmpty( object sender, EventArgs e ) { ForceProcessingPendingUpdates(); } private void ForceProcessingPendingUpdates() { _lastTicksOfProcessingPendingUpdates = DateTime.Now.Ticks; Core.UIManager.QueueUIJob( new MethodInvoker( ProcessPendingUpdates ), new object[] {} ); } /** * When the decoration of an unread node changes, either queues the change * to be processed at the end of the resource operation, or processes it * immediately if it was not invoked from the resource thread. */ private void OnDecorationChanged( object sender, ResourceEventArgs e ) { if ( Core.State == CoreState.ShuttingDown ) { return; } if ( e.Resource != null ) { if ( Core.ResourceStore.IsOwnerThread() ) { lock( _decorationChangedNodes ) { _decorationChangedNodes.Add( e.Resource ); } } else if ( InvokeRequired ) { Core.UIManager.QueueUIJob( new ResourceEventHandler( OnDecorationChanged ), new object[] { sender, e } ); } else { // TODO: doesn't work for non-unique resources TreeNode node = GetResourceNode( null, e.Resource.Id ); if ( node != null ) { UpdateNodeRichText( node, false ); } } } } /** * Processes the changes in resource tree nodes that have been accumulated by operations * running in the resource thread. */ public void ProcessPendingUpdates() { if ( Core.State == CoreState.ShuttingDown ) { return; } // prevent reentering if ( _inProcessPendingUpdates ) return; _inProcessPendingUpdates = true; try { lock( _decorationChangedNodes ) { foreach (HashSet.Entry entry in _decorationChangedNodes) { IResource res = (IResource) entry.Key; TreeNode node = GetResourceNode( null, res.Id ); if ( node != null ) { UpdateNodeRichText( node, false ); } } _decorationChangedNodes.Clear(); } IResource oldSelectedResource = null; if ( SelectedNodes.Length == 1 ) { oldSelectedResource = SelectedResource; } // ResourceNodeData.ProcessPendingUpdates() can modify the hashtable, // so we cannot use foreach enumeration ArrayList dataToUpdate = ArrayListPool.Alloc(); try { foreach( IntHashTable.Entry entry in _nodeData ) { dataToUpdate.Add( entry.Value ); } foreach( ResourceNodeData resourceNodeData in dataToUpdate ) { resourceNodeData.ProcessPendingUpdates(); } } finally { ArrayListPool.Dispose( dataToUpdate ); } foreach ( ResourceNodeData resourceNodeData in _postponedUpdateNodeData ) { resourceNodeData.ProcessPostponedUpdates(); } _postponedUpdateNodeData.Clear(); foreach( int resourceId in _postponedRemoveNodeData ) { _nodeData.Remove( resourceId ); } _postponedRemoveNodeData.Clear(); FlushNodeMap(); if ( oldSelectedResource != null && !SelectAddedItems ) { TreeNode selNode = FindResourceNode( oldSelectedResource ); if ( selNode != null && !selNode.IsSelected ) { SelectedNode = selNode; if ( MultiSelect ) { SelectedNodes = new TreeNode[] { selNode }; } } } if ( TreeUpdated != null ) { TreeUpdated( this, EventArgs.Empty ); } } finally { _inProcessPendingUpdates = false; } } private void AddPostponedUpdateNodeData( ResourceNodeData nodeData ) { _postponedUpdateNodeData.Add( nodeData ); } /** * Moves all entries from the "added nodes" map to the real node map. */ private void FlushNodeMap() { foreach( IntHashTable.Entry e in _resourceToNodeMapNew ) { _resourceToNodeMap [e.Key] = e.Value; } _resourceToNodeMapNew.Clear(); } /** * Correctly selects the node when the right mouse button is clicked. */ protected override void OnMouseDown( MouseEventArgs e ) { base.OnMouseDown( e ); if ( e.Button == MouseButtons.Right ) { TreeNode node = GetNodeAt( e.X, e.Y ); if ( MultiSelect ) { if ( node == null || !node.IsSelected ) { SelectedNode = node; SelectedNodes = new TreeNode[] { node }; } } else { SelectedNode = node; } } } /** * Handles the WM_CONTEXTMENU message to show the context menu for the node. */ protected override void WndProc( ref Message m ) { if ( m.Msg == Win32Declarations.WM_LBUTTONDOWN ) { if ( !ContainsFocus ) { if ( MouseActivate != null ) { MouseActivate( this, EventArgs.Empty ); } _clicksAfterFocus = 0; } else { _clicksAfterFocus++; } } base.WndProc( ref m ); if ( m.Msg == Win32Declarations.WM_CONTEXTMENU && ICore.Instance != null && _showContextMenu && Visible && IsHandleCreated ) { Point selectedNodeTop = new Point( 0, 0 ); if ( SelectedNode != null ) { selectedNodeTop = SelectedNode.Bounds.Location; } Point pnt = new Point( m.LParam.ToInt32() ); if ( pnt.X == -1 && pnt.Y == -1 ) { pnt = selectedNodeTop; pnt.X += 4; pnt.Y += 4; } else { pnt = PointToClient( pnt ); } ActionContext context = GetActionContext( ActionContextKind.ContextMenu ); Core.ActionManager.ShowResourceContextMenu( context, this, pnt.X, pnt.Y ); } else if ( m.Msg == Win32Declarations.WM_EXITMENULOOP ) { _garbageRemoveTimer.Start(); } } /** * Returns the node tagged with the specified resource. If needed, expands the * tree branches leading to the node. */ public TreeNode FindResourceNode( IResource res ) { TreeNode node; if ( UniqueResources ) { node = GetResourceNode( null, res.Id ); if ( node != null ) { return node; } } return FindResourceNodeExpanded( res ); } internal TreeNode FindResourceNodeExpanded( IResource res ) { TreeNode node; if ( Nodes.Count == 0 ) return null; ArrayList parentStack = ArrayListPool.Alloc(); try { IResource parent = res; do { parentStack.Insert( 0, parent ); parent = parent.GetLinkProp( _parentProperty ); if ( parent == null ) return null; } while( parent != _rootResource ); node = Nodes [0]; foreach( IResource parentRes in parentStack ) { node = GetResourceNode( node, parentRes.Id ); if ( node == null ) break; if ( parentRes == res ) return node; node.Expand(); } } finally { ArrayListPool.Dispose( parentStack ); } return null; } /** * Selects the node tagged with the specified resource. Returns true if * the node was found in the tree, false otherwise. */ public bool SelectResourceNode( IResource res ) { if ( !IsHandleCreated ) { _resourceToSelect = res; return true; } if ( MultiSelect ) { SelectedNodes = new TreeNode[] {}; // clear the selection before it's changed programmatically } if ( res == null ) { SelectedNode = null; return true; } TreeNode node = FindResourceNode( res ); if ( node != null ) { SelectedNode = node; SelectedNodes = new TreeNode[] { node }; return true; } return false; } /** * Selects all the resources in the specified list. */ public void SelectResourceNodes( IResourceList resList ) { if ( !MultiSelect ) { SelectResourceNode( resList != null && resList.Count > 0 ? resList [0] : null ); return; } SelectedNodes = new TreeNode[] {}; // clear the selection before it's changed programmatically if ( resList == null ) { SelectedNode = null; return; } ArrayList treeNodes = ArrayListPool.Alloc(); try { foreach( IResource res in resList ) { TreeNode node = FindResourceNode( res ); if ( node != null ) { treeNodes.Add( node ); } } SelectedNodes = (TreeNode[]) treeNodes.ToArray( typeof (TreeNode) ); } finally { ArrayListPool.Dispose( treeNodes ); } } /** * Saves the Checked state for all nodes. */ public void SaveCheckedState() { if ( _checkedProperty < 0 ) { throw new InvalidOperationException( "CheckedProperty needs to be set before calling SaveCheckedState()" ); } SaveCheckedStateRecursive( Nodes ); } private void SaveCheckedStateRecursive( TreeNodeCollection nodes ) { foreach( TreeNode node in nodes ) { IResource res = (IResource) node.Tag; int checkedValue = node.Checked ? _checkedSetValue : _checkedUnsetValue; if ( res.GetIntProp( _checkedProperty ) != checkedValue ) { new ResourceProxy( res ).SetPropAsync( _checkedProperty, checkedValue ); } if ( node.Nodes.Count > 0 ) { SaveCheckedStateRecursive( node.Nodes ); } } } /** * When items are dragged, stores the ResourceList of the selected node * to the drag object. */ protected override void OnItemDrag( ItemDragEventArgs e ) { IResourceList resList; TreeNode dragNode = (TreeNode) e.Item; if ( dragNode.IsSelected ) { resList = SelectedResources; } else { resList = ((IResource) dragNode.Tag).ToResourceList(); if ( MultiSelect ) { SelectedNodes = new TreeNode[] { dragNode }; } } if ( resList.Count > 0 ) { Invalidate(); // to ensure selection is redrawn correctly DataObject dataObj = new DataObject(); dataObj.SetData( typeof(IResourceList), resList ); DoDragDrop( dataObj, DragDropEffects.Link | DragDropEffects.Move); } } protected override void OnDragOver( DragEventArgs drgevent ) { base.OnDragOver( drgevent ); Point pnt = PointToClient( new Point( drgevent.X, drgevent.Y ) ); if ( pnt.Y < 7 ) { _dragScrollUp = true; _dragScrollTimer.Enabled = true; } else if ( ClientSize.Height - pnt.Y < 7 ) { _dragScrollUp = false; _dragScrollTimer.Enabled = true; } else { _dragScrollTimer.Enabled = false; } TreeNode node = GetDragNodeAt( drgevent.X, drgevent.Y ); IResourceList droppedResources = (IResourceList) drgevent.Data.GetData( typeof(IResourceList) ); if ( ( node != null || _dropOnEmpty ) && droppedResources != null ) { if ( ResourceDragOver != null ) { IResource target = (node == null) ? null : (IResource) node.Tag; ResourceDragEventArgs args = new ResourceDragEventArgs( target, droppedResources ); ResourceDragOver( this, args ); drgevent.Effect = args.Effect; } } else { drgevent.Effect = DragDropEffects.None; } if ( _dropHighlightNode != node ) { _dropHighlightNode = node; SetDropHighlightNode( node ); if ( node != null && !node.IsExpanded ) { _expandTimer.Stop(); _expandTimer.Start(); } } } protected override void OnDragLeave( EventArgs e ) { base.OnDragLeave( e ); _dragScrollTimer.Enabled = false; RemoveDropHighlight(); } protected override void OnDragDrop( DragEventArgs drgevent ) { base.OnDragDrop( drgevent ); _dragScrollTimer.Enabled = false; RemoveDropHighlight(); TreeNode node = GetDragNodeAt( drgevent.X, drgevent.Y ); IResourceList droppedResources = (IResourceList) drgevent.Data.GetData( typeof(IResourceList) ); if ( ( node != null || _dropOnEmpty ) && droppedResources != null ) { try { if ( ResourceDrop != null ) { IResource target = (node == null) ? null : (IResource) node.Tag; ResourceDrop( this, new ResourceDragEventArgs( target, droppedResources ) ); } } catch( Exception ex ) { Core.ReportException( ex, false ); } } } private void RemoveDropHighlight() { if ( _dropHighlightNode != null ) { _dropHighlightNode = null; SetDropHighlightNode( null ); } } private TreeNode GetDragNodeAt( int X, int Y ) { Point pnt = PointToClient( new Point( X, Y ) ); if ( NodePainter == null ) return GetNodeAt( pnt.X, pnt.Y ); else return NodePainter.GetNodeAt( this, pnt ); } private void _dragScrollTimer_Tick( object sender, EventArgs e ) { if ( _dragScrollUp ) { Win32Declarations.SendMessage( Handle, Win32Declarations.WM_VSCROLL, new IntPtr( Win32Declarations.SB_LINEUP ), IntPtr.Zero ); } else { Win32Declarations.SendMessage( Handle, Win32Declarations.WM_VSCROLL, new IntPtr( Win32Declarations.SB_LINEDOWN ), IntPtr.Zero ); } } private void _expandTimer_Tick( object sender, EventArgs e ) { if ( _dropHighlightNode != null ) { _dropHighlightNode.Expand(); } _expandTimer.Stop(); } protected override bool IsInputKey(Keys keyData) { if (SearchWindow.Visible) return true; else return base.IsInputKey(keyData); } /** * When a key is pressed, executes the keyboard action through ActionManager. * When F2 is pressed, begins the rename of the selected node. */ protected override void OnKeyDown( KeyEventArgs e ) { base.OnKeyDown( e ); _lastKeyDownHandled = false; if ( !e.Handled && ICore.Instance != null ) { ActionContext context = GetActionContext( ActionContextKind.Keyboard ); _lastKeyDownHandled = true; // because of DoEvents() calls, we could get to OnKeyPress() before returning // from ExecuteKeyboardAction() if ( Core.ActionManager.ExecuteKeyboardAction( context, e.KeyData ) ) { e.Handled = true; } else if ( e.KeyCode == Keys.Enter && e.Modifiers == 0 && _executeDoubleClickAction ) { Core.ActionManager.ExecuteDoubleClickAction( context.SelectedResources ); } else { _lastKeyDownHandled = false; } } if ( !e.Handled && e.KeyData == Keys.F2 ) { if ( SelectedNode != null && LabelEdit ) { _clicksAfterFocus++; SelectedNode.BeginEdit(); _lastKeyDownHandled = true; } } } protected override void OnKeyPress( KeyPressEventArgs e ) { base.OnKeyPress( e ); if ( _lastKeyDownHandled ) { e.Handled = true; } } private ActionContext GetActionContext(ActionContextKind kind) { IResourceList selResList = null; if( Core.State != CoreState.ShuttingDown ) { if( MultiSelect ) { selResList = SelectedResources; } else if( SelectedResource != null && !SelectedResource.IsDeleted ) { selResList = SelectedResource.ToResourceList(); } } ActionContext context = new ActionContext( kind, _menuContextInstance, selResList ); context.SetOwnerForm( FindForm() ); return context; } protected override void OnBeforeLabelEdit( NodeLabelEditEventArgs e ) { // cancel label edit if it's too early to activate if ( _clicksAfterFocus == 0 ) { e.CancelEdit = true; return; } base.OnBeforeLabelEdit( e ); } /** * Wheh a tree node is double-clicked, executes the double-click action for it. */ protected override void OnDoubleClick( EventArgs e ) { base.OnDoubleClick( e ); if ( ICore.Instance != null && SelectedResource != null && _executeDoubleClickAction ) { Core.ActionManager.ExecuteDoubleClickAction( SelectedResource.ToResourceList() ); } } public void EditResourceLabel( IResource res ) { ProcessPendingUpdates(); TreeNode node = FindResourceNode( res ); if ( node != null ) { SelectedNodes = new TreeNode[] { node }; _clicksAfterFocus++; node.BeginEdit(); } } internal void RestartGarbageCleanupTimer() { _garbageRemoveTimer.Stop(); _garbageRemoveTimer.Start(); } } public delegate void TreeNodeDelegate( TreeNode node ); public interface IResourceNodeDecorator { event ResourceEventHandler DecorationChanged; string DecorationKey { get; } bool DecorateNode( IResource res, RichText nodeText ); } public interface IResourceChildProvider { IResourceList GetChildResources( ResourceTreeView resourceTree, IResource parent ); } }