/// /// 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.Diagnostics; using JetBrains.Omea.Base; using JetBrains.Omea.OpenAPI; using System.Collections; using JetBrains.Omea.ResourceStore; using JetBrains.DataStructures; using JetBrains.Omea.Containers; namespace JetBrains.Omea.ResourceTools { public interface IResourceTabProvider { string GetDefaultTab(); string GetResourceTab( IResource res ); IResourceList GetTabFilterList( string tabId ); } /** * A specific state of unread resources. */ public class UnreadState { private readonly UnreadManager _unreadManager; private readonly string _tab; private readonly IResource _workspace; private readonly IntHashTableOfInt _unreadCountBuffer = new IntHashTableOfInt(); private readonly IntHashTableOfInt _unreadCountersValid = new IntHashTableOfInt(); internal UnreadState( UnreadManager mgr, string tab, IResource workspace ) { _unreadManager = mgr; _unreadCountBuffer.MissingKeyValue = 0; _tab = tab; _workspace = workspace; } internal string Tab { get { return _tab; } } internal IResource Workspace { get { return _workspace; } } internal bool IsPersistent { get { return _tab == null; } } internal int GetCountFromBuffer( IResource res ) { lock( _unreadCountBuffer ) { return _unreadCountBuffer [res.Id]; } } internal void ResetCounters() { lock( _unreadCountersValid ) { _unreadCountersValid.Clear(); } } internal bool IsCounterValid( IResource res ) { lock( _unreadCountersValid ) { return _unreadCountersValid [res.Id] == 1; } } internal void InvalidateCounter( IResource res ) { lock( _unreadCountersValid ) { _unreadCountersValid.Remove( res.Id ); } } public int GetUnreadCount( IResource res ) { return _unreadManager.GetCountForState( this, res ); } internal void UpdateUnreadCounter( IResource res, int count ) { bool countChanged = false; lock( _unreadCountBuffer ) { if ( _unreadCountBuffer [res.Id] != count ) { _unreadCountBuffer [res.Id] = count; countChanged = true; } lock( _unreadCountersValid ) { _unreadCountersValid [res.Id] = 1; } } if ( countChanged ) { OnUnreadCountChanged( res ); } } internal void OnUnreadCountChanged( IResource res ) { if ( UnreadCountChanged != null ) { UnreadCountChanged( this, new ResourceEventArgs( res ) ); } } /* public override string ToString() { return "UnreadState(" + ((_key == null ) ? "default" : _key.ToString() ) + ")"; } */ public event ResourceEventHandler UnreadCountChanged; } /** * Monitors resources that become read or unread and adjusts the unread * count of related containers accordingly. */ public class UnreadManager : IUnreadManager { private readonly IResourceStore _store; private readonly IResourceTabProvider _tabProvider; private readonly WorkspaceManager _workspaceManager; private readonly int _propUnreadCount; private readonly ICoreProps _coreProps; private readonly Hashtable _unreadStateTabMap = new Hashtable(); // tab ID -> IntHashTable private readonly UnreadState _defaultUnreadState; private UnreadState _curUnreadState; private readonly IntHashSet _unreadCountersChanged = new IntHashSet(); private readonly HashMap _unreadCountProviders = new HashMap(); // resource type -> IUnreadCountProvider private bool _isEnabled = false; private readonly bool _traceUnreadCounters; public UnreadManager( WorkspaceManager workspaceManager, IResourceTabProvider tabProvider, ISettingStore settingStore, ICoreProps coreProps ) { _tabProvider = tabProvider; _workspaceManager = workspaceManager; _coreProps = coreProps; _defaultUnreadState = new UnreadState( this, null, null ); _curUnreadState = _defaultUnreadState; _store = Core.ResourceStore; _propUnreadCount = _store.PropTypes.Register( "UnreadCount", PropDataType.Int, PropTypeFlags.Internal ); _traceUnreadCounters = settingStore.ReadBool( "UnreadCounters", "TraceUnreadCounters", false ); Core.ResourceAP.JobFinished += environment_ResourceOperationFinished; Enabled = true; } public UnreadState CurrentUnreadState { get { return _curUnreadState; } } public bool Enabled { get { return _isEnabled; } set { if ( _isEnabled != value ) { _isEnabled = value; if ( _isEnabled ) { MyPalStorage.Storage.ResourceSaved += OnResourceSaved; MyPalStorage.Storage.ResourceDeleting += OnResourceDeleting; } else { MyPalStorage.Storage.ResourceSaved -= OnResourceSaved; MyPalStorage.Storage.ResourceDeleting -= OnResourceDeleting; } } } } public void RegisterUnreadCountProvider( string resType, IUnreadCountProvider provider ) { _unreadCountProviders [resType] = provider; } /** * Switches the state of unread counters to the state associated with the * specified key and filter list, and creates a new state if necessary. */ public UnreadState SetUnreadState( string activeTab, IResource activeWorkspace ) { if ( activeTab == _tabProvider.GetDefaultTab() && activeWorkspace == null ) { _curUnreadState = _defaultUnreadState; } else { IntHashTable tabHash = (IntHashTable) _unreadStateTabMap [activeTab]; if ( tabHash == null ) { tabHash = new IntHashTable(); _unreadStateTabMap [activeTab] = tabHash; } int wsId = (activeWorkspace == null) ? 0 : activeWorkspace.Id; UnreadState state = (UnreadState) tabHash [wsId]; if ( state == null ) { state = new UnreadState( this, activeTab, activeWorkspace ); tabHash [wsId] = state; } _curUnreadState = state; } return _curUnreadState; } /** * Returns the unread counter to be displayed for the specified resource * (if an in-memory unread counter was specified, it overrides the persistent * unread counter). */ public int GetUnreadCount( IResource res ) { return GetCountForState( _curUnreadState, res ); } /** * Sets the in-memory unread counter for the specified resource. */ public void SetInMemoryUnreadCount( IResource res, int count ) { if ( _traceUnreadCounters ) { Trace.WriteLine( "Count for " + res + " on " + _curUnreadState + " is " + count ); } _curUnreadState.UpdateUnreadCounter( res, count ); } /** * Returns the persistent unread counter for the specified resource * (the counter which is not view-specific and which is saved in the * resource store). */ public int GetPersistentUnreadCount( IResource res ) { if ( _defaultUnreadState.IsCounterValid( res ) ) { return _defaultUnreadState.GetUnreadCount( res ); } int count = res.GetIntProp( _propUnreadCount ); _defaultUnreadState.UpdateUnreadCounter( res, count ); return count; } /** * Returns the unread count for the specified state. */ internal int GetCountForState( UnreadState state, IResource res ) { if ( state.IsCounterValid( res ) || ( state != _curUnreadState && !state.IsPersistent) ) { return state.GetCountFromBuffer( res ); } int count; IUnreadCountProvider provider = (IUnreadCountProvider) _unreadCountProviders [res.Type]; if ( provider == null ) { count = state.IsPersistent ? res.GetIntProp( _propUnreadCount ) : GetUnreadCountFromLinks( res, state ); } else { count = GetProviderUnreadCount( res, state, provider ); } state.UpdateUnreadCounter( res, count ); return count; } private int GetUnreadCountFromLinks( IResource res, UnreadState state ) { int count; int persistentCount = GetPersistentUnreadCount( res ); if ( persistentCount == 0 ) { count = 0; } else { int unfilteredCount; IResourceList links = GetUnreadCountedLinks( res, out unfilteredCount ); if ( links == null ) { count = 0; } else { links = links.Intersect( _tabProvider.GetTabFilterList( state.Tab ), true ); count = CountUnreadResources( links ); // HACK: Cleanup for out-of-sync UnreadCount values if ( links.Count == unfilteredCount && count != persistentCount ) { SetPersistentUnreadCount( res, count ); MarkUnreadCounterChanged( res ); } } } return count; } /** * Returns the count of unread resources returned by the specified provider and * filtered by the workspace. */ private int GetProviderUnreadCount( IResource res, UnreadState state, IUnreadCountProvider provider ) { IResourceList unreadList = provider.GetResourcesForView( res ); if ( unreadList == Core.ResourceStore.EmptyResourceList ) { return 0; } if ( state != _defaultUnreadState ) { unreadList = unreadList.Intersect( _tabProvider.GetTabFilterList( state.Tab ) ); if ( state.Workspace != null ) { unreadList = unreadList.Intersect( _workspaceManager.GetFilterList( state.Workspace ) ); } } return unreadList.Count; } /** * Sets the persistent unread counter for the specified resource. * (The counter will be actually flushed to the resource store * when the resource operation is finished.) */ public void SetPersistentUnreadCount( IResource res, int count ) { _defaultUnreadState.UpdateUnreadCounter( res, count ); } public void InvalidateUnreadCounter( IResource res ) { _defaultUnreadState.InvalidateCounter( res ); foreach( DictionaryEntry de in _unreadStateTabMap ) { IntHashTable ht = (IntHashTable) de.Value; foreach ( IntHashTable.Entry entry in ht ) { UnreadState state = (UnreadState) entry.Value; state.InvalidateCounter( res ); } } _curUnreadState.OnUnreadCountChanged( res ); } private void OnResourceSaved( object sender, ResourcePropEventArgs e ) { if ( e.ChangeSet.IsPropertyChanged( _coreProps.IsUnread ) || e.ChangeSet.IsPropertyChanged( _coreProps.IsDeleted ) ) { bool wasUnread = e.ChangeSet.GetOldValue( _coreProps.IsUnread ) != null || (!e.ChangeSet.IsPropertyChanged( _coreProps.IsUnread ) && e.Resource.HasProp( _coreProps.IsUnread ) ); bool wasDeleted = e.ChangeSet.GetOldValue( _coreProps.IsDeleted ) != null || (!e.ChangeSet.IsPropertyChanged( _coreProps.IsDeleted ) && e.Resource.HasProp( _coreProps.IsDeleted ) ); bool isUnread = e.Resource.HasProp( _coreProps.IsUnread ); bool isDeleted = e.Resource.HasProp( _coreProps.IsDeleted ); int oldUnreadStatus = (wasUnread && !wasDeleted) ? 1 : 0; int newUnreadStatus = (isUnread && !isDeleted) ? 1 : 0; if ( oldUnreadStatus != newUnreadStatus ) { ProcessResourceUnreadChange( e.Resource, newUnreadStatus - oldUnreadStatus, (newUnreadStatus > oldUnreadStatus) ? null : e.ChangeSet ); } } else if ( e.Resource.HasProp( _coreProps.IsUnread ) && !e.Resource.HasProp( _coreProps.IsDeleted ) ) { ProcessUnreadCountedLinksChange( e.Resource, e.ChangeSet ); if ( e.ChangeSet.IsPropertyChanged( _workspaceManager.Props.WorkspaceVisible ) ) { ProcessWorkspaceChange( e.Resource, e.ChangeSet ); } } } private void OnResourceDeleting( object sender, EventArgs e ) { IResource res = (IResource) sender; if ( res.HasProp( _coreProps.IsUnread ) && !res.HasProp( _coreProps.IsDeleted ) ) { ProcessResourceUnreadChange( res, -1, null ); } } /** * Increments the unread counters of resources linked with unread-counted links * to the specified resource by 'delta'. If cs is not null, the affected resources * are not the currently linked ones, but rather the ones linked before the changes * described by cs happened. */ private void ProcessResourceUnreadChange( IResource res, int delta, IPropertyChangeSet cs ) { UnreadState[] unreadStates = GetResourceUnreadStates( res ); IntArrayList linkTypeIDs = GetAllLinkTypes( res, cs ); for( int i=0; i /// When a resource matching a view enters or leaves a workspace, update unread counter for the /// view in that workspace. /// /// The resource entering or leaving the workspace. /// The view for which the counter should be updated. /// The ID of the workspace which the resource enters or leaves. /// The value by which the counter is changed (1 or -1). internal void AdjustViewWorkspaceCounter( IResource res, IResource viewResource, int workspaceId, int delta ) { IntHashTable defaultTabMap = (IntHashTable) _unreadStateTabMap [_tabProvider.GetDefaultTab()]; AdjustCounterInState( defaultTabMap, workspaceId, viewResource, delta ); string resourceTab = _tabProvider.GetResourceTab( res ); if ( resourceTab != null ) { IntHashTable specificTabMap = (IntHashTable) _unreadStateTabMap [resourceTab]; AdjustCounterInState( specificTabMap, workspaceId, viewResource, delta ); } } private void AdjustCounterInState( IntHashTable tabMap, int workspaceId, IResource resource, int delta ) { if ( tabMap != null ) { UnreadState tabState = (UnreadState) tabMap [workspaceId]; if ( tabState != null ) { AdjustUnreadCount( resource, delta, tabState ); } } } /** * Returns the list of all link types for the given resource. If the resource * was changed, the list also includes the link types which were present on * the resource before the change. */ private IntArrayList GetAllLinkTypes( IResource res, IPropertyChangeSet cs ) { IntArrayList linkTypeIDs = new IntArrayList( res.GetLinkTypeIds() ); if ( cs != null ) { int[] changedPropIDs = cs.GetChangedProperties(); for( int i=0; i /// Returns the array of existing unread states in which the specified /// resource is visible. /// private UnreadState[] GetResourceUnreadStates( IResource res ) { ArrayList states = ArrayListPool.Alloc(); try { IResourceList workspaces = _workspaceManager.GetResourceWorkspaces( res ); FillStatesForTab( states, workspaces, _tabProvider.GetDefaultTab() ); string tab = _tabProvider.GetResourceTab( res ); if ( tab != null ) { FillStatesForTab( states, workspaces, tab ); } return (UnreadState[]) states.ToArray( typeof (UnreadState) ); } finally { ArrayListPool.Dispose( states ); } } /** * Adds the existing unread states for the specified tab and list of workspaces * to the specified array list. */ private void FillStatesForTab( IList states, IResourceList workspaces, string tab ) { if ( tab == _tabProvider.GetDefaultTab() ) { states.Add( _defaultUnreadState ); } IntHashTable tabHash = (IntHashTable) _unreadStateTabMap [tab]; if ( tabHash != null ) { if ( tab != _tabProvider.GetDefaultTab() ) { UnreadState state = (UnreadState) tabHash [0]; if ( state != null ) { states.Add( state ); } } foreach( IResource ws in workspaces ) { UnreadState state = (UnreadState) tabHash [ws.Id]; if ( state != null ) { states.Add( state ); } } } } /// /// Adjusts the unread count for the specified resource stored in the specified /// count map by the specified delta value. /// private void AdjustUnreadCount( IResource res, int delta, UnreadState state ) { if ( !state.IsPersistent && !state.IsCounterValid( res ) ) return; int count; if ( state.IsCounterValid( res ) || ( state.IsPersistent && !_unreadCountProviders.Contains( res.Type ) ) ) { count = state.GetUnreadCount( res ) + delta; } else { // this will initiate a new count calculation which will already take into account // the new unread state of the resource count = state.GetUnreadCount( res ); } if ( count >= 0 ) { state.UpdateUnreadCounter( res, count ); if ( state.IsPersistent && !_unreadCountProviders.Contains( res.Type ) ) { MarkUnreadCounterChanged( res ); } } } private void MarkUnreadCounterChanged( IResource res ) { lock ( _unreadCountersChanged ) { _unreadCountersChanged.Add( res.Id ); } } private bool IsUnreadCountedLink( int propID ) { return _store.PropTypes [propID].HasFlag( PropTypeFlags.CountUnread ); } /** * When a resource operation is finished, flushes the persistent unread * counters to the resource store. */ private void environment_ResourceOperationFinished( object sender, EventArgs e ) { int[] countersToFlush = null; lock ( _unreadCountersChanged ) { if ( _unreadCountersChanged.Count > 0 ) { countersToFlush = new int [_unreadCountersChanged.Count]; int i = 0; foreach( IntHashSet.Entry hse in _unreadCountersChanged ) { countersToFlush [i++] = hse.Key; } _unreadCountersChanged.Clear(); } } if ( countersToFlush != null ) { for( int i = 0; i < countersToFlush.Length; ++i ) { IResource res = _store.TryLoadResource( countersToFlush[i] ); if ( res != null ) { int count = _defaultUnreadState.GetUnreadCount( res ); Trace.WriteLineIf( _traceUnreadCounters, "Flushing unread count " + count + " for resource " + res ); res.SetProp( _propUnreadCount, count ); } } } } /** * Refreshes the unread counters on all resources. Assumes to be invoked from the * resource thread. */ public void RefreshUnreadCounters() { IResourceList unreadCountedResources = _store.FindResourcesWithProp( null, _propUnreadCount ); foreach( IResource unreadCountedRes in unreadCountedResources ) { int linkCount; IResourceList unreadCountedLinks = GetUnreadCountedLinks( unreadCountedRes, out linkCount ); unreadCountedRes.SetProp( _propUnreadCount, CountUnreadResources( unreadCountedLinks ) ); } _defaultUnreadState.ResetCounters(); foreach( DictionaryEntry de in _unreadStateTabMap ) { IntHashTable tabHash = (IntHashTable) de.Value; foreach( IntHashTable.Entry ie in tabHash ) { UnreadState state = (UnreadState) ie.Value; state.ResetCounters(); } } } /// /// Returns the list of all resources linked to the specified resource /// with links marked as CountUnread. /// private static IResourceList GetUnreadCountedLinks( IResource res, out int count ) { IResourceList unreadCountedLinks = null; count = 0; foreach( IPropType propType in Core.ResourceStore.PropTypes ) { if ( propType.HasFlag( PropTypeFlags.CountUnread ) ) { bool hasLink = res.HasProp( propType.Id ); if ( !hasLink && propType.HasFlag( PropTypeFlags.DirectedLink ) ) { hasLink = res.HasProp( -propType.Id ); } if ( hasLink ) { IResourceList linkList = res.GetLinksOfType( null, propType.Id ); count += linkList.Count; unreadCountedLinks = linkList.Union( unreadCountedLinks, true ); } } } return unreadCountedLinks; } /// /// Returns the count of resources in the specified list that have the /// Unread flag set. /// private int CountUnreadResources( IResourceList list ) { return ( list == null ) ? 0 : list.Intersect( Core.ResourceStore.FindResourcesWithProp( null, _coreProps.IsUnread ), true ).Minus( Core.ResourceStore.FindResourcesWithProp( null, _coreProps.IsDeleted ) ).Count; } } }