/// /// 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.Collections; using System.Diagnostics; using JetBrains.DataStructures; using JetBrains.JetListViewLibrary; using JetBrains.Omea.Base; using JetBrains.Omea.Containers; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.ResourceStore; namespace JetBrains.Omea.GUIControls { /// /// Fills ResourceListView2 with data from a threaded resource list. /// public class ConversationDataProvider: ResourceListDataProvider { private class ConversationNode { internal readonly IResource Resource; internal JetListViewNode LvNode; internal ConversationNode Parent; internal ArrayList Children; internal bool InList; public ConversationNode( IResource resource ) { Resource = resource; } internal void AddChild( ConversationNode node ) { lock( this ) { if ( Children == null ) { Children = new ArrayList(); } Debug.Assert( !HasChild( node ) ); Children.Add( node ); } node.Parent = this; } internal bool HasChild( ConversationNode node ) { lock( this ) { if ( Children != null ) { foreach( ConversationNode existingNode in Children ) { if ( existingNode.Resource.Id == node.Resource.Id ) { return true; } } } } return false; } internal bool HasUnreadReplies() { lock( this ) { if ( Children != null ) { foreach( ConversationNode node in Children ) { if ( node.InList && node.Resource.HasProp( Core.Props.IsUnread ) ) { return true; } if ( node.HasUnreadReplies() ) { return true; } } } } return false; } public void RemoveChild( ConversationNode node ) { lock( this ) { if ( Children != null ) { Children.Remove( node ); } } } } private class ConversationNodeComparer: IComparer { private readonly ResourceComparer _resourceComparer; internal ConversationNodeComparer( ResourceComparer comparer ) { _resourceComparer = comparer; } public int Compare( object x, object y ) { ConversationNode lhs = x as ConversationNode; ConversationNode rhs = y as ConversationNode; if ( lhs == null ) { return (rhs == null) ? 0 : -1; } if ( rhs == null ) { return 1; } return _resourceComparer.CompareResources( lhs.Resource, rhs.Resource ); } } private readonly IResourceThreadingHandler _threadingHandler; private readonly ArrayList _conversationRoots = new ArrayList(); // private readonly IComparer _childComparer; private IntHashTable _conversationNodeMap; // resource ID -> ConversationNode private JetListViewNode _lastExpandingNode; public ConversationDataProvider( IResourceList resourceList, IResourceThreadingHandler threadingHandler ) : base( resourceList ) { _threadingHandler = threadingHandler; _childComparer = new ResourceComparer( resourceList, new SortSettings( Core.Props.Date, true ), true ); } protected override void AddResourceNodes() { _conversationNodeMap = new IntHashTable(); foreach( IResource res in _resourceList.ValidResources ) { ConversationNode node = GetConversationNode( res ); node.InList = true; } ArrayList topLevelNodes = ArrayListPool.Alloc(); try { foreach( ConversationNode node in _conversationRoots ) { FillTopLevelNodes( topLevelNodes, node ); } if ( _lastComparer != null ) { topLevelNodes.Sort( new ConversationNodeComparer( _lastComparer ) ); } foreach( ConversationNode node in topLevelNodes ) { JetListViewNode lvNode = AddListViewNode( _listView.Nodes, node ); if ( node.Children != null || _threadingHandler.CanExpandThread( node.Resource, ThreadExpandReason.Expand ) ) { lvNode.HasChildren = true; } } } finally { ArrayListPool.Dispose( topLevelNodes ); } _listView.ChildrenRequested += HandleChildrenRequested; _listView.NodeCollection.NodeExpandChanging += HandleExpandChanging; } public override void Dispose() { if ( _listView != null ) { _listView.ChildrenRequested -= HandleChildrenRequested; _listView.NodeCollection.NodeExpandChanging -= HandleExpandChanging; } base.Dispose(); } private static JetListViewNode AddListViewNode( ChildNodeCollection nodes, ConversationNode node ) { JetListViewNode lvNode = nodes.Add( node.Resource ); node.LvNode = lvNode; return node.LvNode; } /// /// Finds or adds a conversation node for the specified resource. /// /// The resource to find the node for. /// The conversation node instance. private ConversationNode GetConversationNode( IResource res ) { lock( _conversationNodeMap ) { ConversationNode node = (ConversationNode) _conversationNodeMap [res.Id]; if ( node != null ) { return node; } node = new ConversationNode( res ); IResource parent = _threadingHandler.GetThreadParent( res ); if ( parent != null ) { ConversationNode parentNode = GetConversationNode( parent ); parentNode.AddChild( node ); } else { _conversationRoots.Add( node ); } _conversationNodeMap [res.Id] = node; return node; } } private ConversationNode FindConversationNode( int resId ) { if ( _conversationNodeMap == null ) { return null; } lock( _conversationNodeMap ) { return (ConversationNode) _conversationNodeMap [resId]; } } /// /// Adds the node (if it's present in the list) or some of its child nodes /// (if it is not) to the tree of conversations. /// /// /// private static void FillTopLevelNodes( ArrayList topLevelNodes, ConversationNode node ) { lock( node ) { if ( node.InList ) { topLevelNodes.Add( node ); } else if ( node.Children != null ) { foreach( ConversationNode child in node.Children ) { FillTopLevelNodes( topLevelNodes, child ); } } } } private void HandleExpandChanging( object sender, JetListViewNodeEventArgs e ) { if ( !e.Node.Expanded ) { _lastExpandingNode = e.Node; } } private void HandleChildrenRequested( object sender, RequestChildrenEventArgs e ) { if ( _listView == null ) { return; } IResource res = (IResource) e.Node.Data; e.Handled = _threadingHandler.HandleThreadExpand( res, (e.Reason == RequestChildrenReason.Enumerate ? ThreadExpandReason.Enumerate : ThreadExpandReason.Expand ) ); BuildConversation( res, (e.Node == _lastExpandingNode && e.Node.Level == 0 ) ); } public void ExpandConversation( IResource res ) { BuildConversation( res, true ); } private void BuildConversation( IResource res, bool expandNode ) { Guard.NullArgument( res, "res" ); Guard.NullMember( _listView, "_listView" ); ConversationNode node = FindConversationNode( res.Id ); if ( node != null ) { while( node.LvNode == null ) { if ( node.Parent == null ) { return; } node = node.Parent; } if ( node.LvNode.Nodes.Count == 0 ) { _listView.NodeCollection.SetItemComparer( node.Resource, _childComparer ); AddConversationReplies( node, node.LvNode ); } if ( expandNode ) { node.LvNode.ExpandAll(); } } } private void AddConversationReplies( ConversationNode convNode, JetListViewNode node ) { lock( convNode ) { if ( convNode.Children != null ) { foreach( ConversationNode child in convNode.Children ) { AddConversationRecursive( child, node ); } } } } private void AddConversationRecursive( ConversationNode child, JetListViewNode node ) { if ( child.LvNode != null ) { node = child.LvNode; } else if ( child.InList ) { node = AddListViewNode( node.Nodes, child ); } AddConversationReplies( child, node ); } protected override void HandleResourceAdded( object sender, ResourceIndexEventArgs e ) { if ( _disposed ) { return; } lock( _resourceList ) { ConversationNode newNode = GetConversationNode( e.Resource ); newNode.InList = true; foreach( IResource child in _threadingHandler.GetThreadChildren( e.Resource ) ) { ConversationNode childNode = FindConversationNode( child.Id ); if ( childNode != null && !newNode.HasChild( childNode ) ) { newNode.AddChild( childNode ); } } if ( _threadingHandler.GetThreadParent( e.Resource ) == null ) { OnThreadRootAdded( e.Resource, newNode ); } else { // if the parent is not in list, the node should be added as a root (#6376) if ( !OnThreadChildAdded( e.Resource, newNode ) ) { OnThreadRootAdded( e.Resource, newNode ); } } } OnResourceCountChanged(); } private void OnThreadRootAdded( IResource resource, ConversationNode node ) { JetListViewNode lvNode = null; if ( node.LvNode == null ) { lvNode = AddListViewNode( _listView.Nodes, node ); } RemoveChildRoots( resource ); if ( lvNode != null && ( node.Children != null || _threadingHandler.CanExpandThread( node.Resource, ThreadExpandReason.Expand ) ) ) { lvNode.HasChildren = true; } } /// /// If the specified root resource of a conversation was added to the list /// later than its children, the children appeared as "intermediate" roots. /// Now that we have the real root, the "intermediate" roots need to be /// removed. /// /// private void RemoveChildRoots( IResource res ) { foreach( IResource reply in _threadingHandler.GetThreadChildren( res ) ) { RemoveChildRoots( reply ); ConversationNode node = FindConversationNode( reply.Id ); if ( node != null && node.LvNode != null ) { RemoveLvNode( node ); } } } private bool OnThreadChildAdded( IResource resource, ConversationNode node ) { ConversationNode parentNode = FindParentInList( resource, true ); if ( parentNode != null && node.LvNode == null ) { JetListViewNode parentLvNode = parentNode.LvNode; if ( parentLvNode.Level == 0 && parentLvNode.Nodes.Count == 0 ) { // for a root thread which has not yet been expanded, // only mark that it has children parentLvNode.HasChildren = true; } else { AddListViewNode( parentLvNode.Nodes, node ); parentLvNode.Expanded = true; } return true; } return false; } private ConversationNode FindParentInList( IResource resource, bool needLvNode ) { IResource curRes = resource; // if some of the resources in a thread are skipped (for example, when // we're viewing only unread items and the thread goes unread->read->unread), // go up the thread until we find a node that is not skipped while( true ) { IResource parentRes = _threadingHandler.GetThreadParent( curRes ); if ( parentRes == null ) { return null; } ConversationNode parentNode = FindConversationNode( parentRes.Id ); if ( parentNode != null && parentNode.InList && ( !needLvNode || parentNode.LvNode != null ) ) { return parentNode; } curRes = parentRes; } } protected override void HandleResourceChanged( object sender, ResourcePropIndexEventArgs e ) { if ( _disposed ) { return; } lock( _resourceList ) { ConversationNode node = FindConversationNode( e.Resource.Id ); if ( _threadingHandler.IsThreadChanged( e.Resource, e.ChangeSet ) ) { if ( node != null && node.LvNode != null ) { UpdateItemThread( e.Resource, node ); } } else if ( node != null ) // if we use a live snapshot list, it's possible to get // ResourceChanged notifications for nodes which weren't included // in predicate GetMatchingResources() output (OM-8711) { _listView.UpdateItemSafe( e.Resource ); IResource lastCollapsedParent = null; node = node.Parent; while( node != null ) { if ( node.LvNode != null && !node.LvNode.Expanded ) { lastCollapsedParent = (IResource) node.LvNode.Data; } node = node.Parent; } if ( lastCollapsedParent != null ) { _listView.UpdateItemSafe( lastCollapsedParent ); } } } } private void UpdateItemThread( IResource resource, ConversationNode node ) { ConversationNode newParentNode = FindParentInList( resource, false ); if ( newParentNode != null ) { if ( node.Parent != null ) { node.Parent.RemoveChild( node ); } newParentNode.AddChild( node ); if ( newParentNode.LvNode == null ) { // the new parent belongs to a thread which wasn't expanded _listView.Nodes.Remove( resource ); node.LvNode = null; } else { CollapseState oldState = newParentNode.LvNode.CollapseState; node.LvNode.SetParent( newParentNode.LvNode ); if ( oldState == CollapseState.NoChildren ) { newParentNode.LvNode.Expanded = false; } } } else { node.LvNode.SetParent( null ); } } protected override void HandleResourceDeleting( object sender, ResourceIndexEventArgs e ) { if ( _disposed || e.Resource == null ) { return; } lock( _resourceList ) { ConversationNode node = FindConversationNode( e.Resource.Id ); if ( node != null ) { if ( node.LvNode != null ) { lock( _listView.NodeCollection ) { for( int i=node.LvNode.Nodes.Count-1; i >= 0; i-- ) { node.LvNode.Nodes [i].SetParent( node.LvNode.Parent ); } RemoveLvNode( node ); } } ConversationNode parent = node.Parent; if ( parent != null ) { if ( parent.Children != null && parent.Children.Count == 0 && !_threadingHandler.CanExpandThread( parent.Resource, ThreadExpandReason.Expand ) ) { parent.LvNode.HasChildren = false; } node.Parent.RemoveChild( node ); } lock( _conversationNodeMap ) { _conversationNodeMap.Remove( e.Resource.Id ); } } } OnResourceCountChanged(); } private static void RemoveLvNode( ConversationNode node ) { if ( node.LvNode != null ) { JetListViewNode parentLvNode = node.LvNode.Parent; if ( parentLvNode != null ) { parentLvNode.Nodes.Remove( node.Resource ); } node.LvNode = null; } } public override bool FindResourceNode( IResource res ) { if ( !base.FindResourceNode( res ) ) { return false; } ConversationNode convNode = FindConversationNode( res.Id ); if ( convNode == null ) { return false; } if ( convNode.LvNode == null ) { ConversationNode parent = convNode.Parent; while( parent != null ) { if ( parent.LvNode != null && parent.LvNode.Level == 0 ) { ExpandConversation( parent.Resource ); break; } parent = parent.Parent; } } return true; } public IResourceList ExpandSelectedResources( IResourceList selection ) { IntArrayList resourceIds = new IntArrayList(); resourceIds.AddRange( selection.ResourceIds ); for( int i=0; i