diff --git a/Eve-O-Preview/Eve-O-Preview.csproj b/Eve-O-Preview/Eve-O-Preview.csproj index 1532924..ecad15d 100644 --- a/Eve-O-Preview/Eve-O-Preview.csproj +++ b/Eve-O-Preview/Eve-O-Preview.csproj @@ -110,6 +110,10 @@ + + + + diff --git a/Eve-O-Preview/Presentation/ThumbnailManager.cs b/Eve-O-Preview/Presentation/ThumbnailManager.cs index 0f8cb9a..ca47111 100644 --- a/Eve-O-Preview/Presentation/ThumbnailManager.cs +++ b/Eve-O-Preview/Presentation/ThumbnailManager.cs @@ -1,24 +1,25 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Drawing; using System.Windows.Threading; using EveOPreview.Configuration; +using EveOPreview.Mediator; +using EveOPreview.Services; using EveOPreview.WindowManager; namespace EveOPreview.UI { - public class ThumbnailManager : IThumbnailManager + class ThumbnailManager : IThumbnailManager { #region Private constants private const int WindowPositionThreshold = -5000; private const int WindowSizeThreshold = -5000; - private const string ClientProcessName = "ExeFile"; private const string DefaultClientTitle = "EVE"; #endregion #region Private fields + private readonly IProcessMonitor _processMonitor; private readonly IWindowManager _windowManager; private readonly IThumbnailConfiguration _configuration; private readonly DispatcherTimer _thumbnailUpdateTimer; @@ -32,8 +33,9 @@ namespace EveOPreview.UI private bool _isHoverEffectActive; #endregion - public ThumbnailManager(IWindowManager windowManager, IThumbnailConfiguration configuration, IThumbnailViewFactory factory) + public ThumbnailManager(IMediator mediator, IThumbnailConfiguration configuration, IProcessMonitor processMonitor, IWindowManager windowManager, IThumbnailViewFactory factory) { + this._processMonitor = processMonitor; this._windowManager = windowManager; this._configuration = configuration; this._thumbnailViewFactory = factory; @@ -189,15 +191,9 @@ namespace EveOPreview.UI this._ignoreViewEvents = true; } - private static Process[] GetClientProcesses() - { - return Process.GetProcessesByName(ThumbnailManager.ClientProcessName); - } - private void UpdateThumbnailsList() { - Process[] clientProcesses = ThumbnailManager.GetClientProcesses(); - List processHandles = new List(clientProcesses.Length); + this._processMonitor.GetUpdatedProcesses(out ICollection addedProcesses, out ICollection updatedProcesses, out ICollection removedProcesses); IntPtr foregroundWindowHandle = this._windowManager.GetForegroundWindowHandle(); @@ -205,78 +201,74 @@ namespace EveOPreview.UI List viewsUpdated = new List(); List viewsRemoved = new List(); - foreach (Process process in clientProcesses) + foreach (IProcessInfo process in addedProcesses) { - IntPtr processHandle = process.MainWindowHandle; - string processTitle = process.MainWindowTitle; - processHandles.Add(processHandle); + IThumbnailView view = this._thumbnailViewFactory.Create(process.Handle, process.Title, this._configuration.ThumbnailSize); + view.IsEnabled = true; + view.IsOverlayEnabled = this._configuration.ShowThumbnailOverlays; + view.SetFrames(this._configuration.ShowThumbnailFrames); + // Max/Min size limitations should be set AFTER the frames are disabled + // Otherwise thumbnail window will be unnecessary resized + view.SetSizeLimitations(this._configuration.ThumbnailMinimumSize, this._configuration.ThumbnailMaximumSize); + view.SetTopMost(this._configuration.ShowThumbnailsAlwaysOnTop); - IThumbnailView view; - this._thumbnailViews.TryGetValue(processHandle, out view); + view.ThumbnailLocation = this.IsManageableThumbnail(view) + ? this._configuration.GetThumbnailLocation(process.Title, this._activeClientTitle, view.ThumbnailLocation) + : this._configuration.GetDefaultThumbnailLocation(); - if ((view == null) && (processTitle != "")) + this._thumbnailViews.Add(process.Handle, view); + + view.ThumbnailResized = this.ThumbnailViewResized; + view.ThumbnailMoved = this.ThumbnailViewMoved; + view.ThumbnailFocused = this.ThumbnailViewFocused; + view.ThumbnailLostFocus = this.ThumbnailViewLostFocus; + view.ThumbnailActivated = this.ThumbnailActivated; + view.ThumbnailDeactivated = this.ThumbnailDeactivated; + + view.RegisterHotkey(this._configuration.GetClientHotkey(process.Title)); + + this.ApplyClientLayout(process.Handle, process.Title); + + viewsAdded.Add(view); + + if (process.Handle == foregroundWindowHandle) { - view = this._thumbnailViewFactory.Create(processHandle, processTitle, this._configuration.ThumbnailSize); - view.IsEnabled = true; - view.IsOverlayEnabled = this._configuration.ShowThumbnailOverlays; - view.SetFrames(this._configuration.ShowThumbnailFrames); - // Max/Min size limitations should be set AFTER the frames are disabled - // Otherwise thumbnail window will be unnecessary resized - view.SetSizeLimitations(this._configuration.ThumbnailMinimumSize, this._configuration.ThumbnailMaximumSize); - view.SetTopMost(this._configuration.ShowThumbnailsAlwaysOnTop); - - view.ThumbnailLocation = this.IsManageableThumbnail(view) - ? this._configuration.GetThumbnailLocation(processTitle, this._activeClientTitle, view.ThumbnailLocation) - : this._configuration.GetDefaultThumbnailLocation(); - - this._thumbnailViews.Add(processHandle, view); - - view.ThumbnailResized = this.ThumbnailViewResized; - view.ThumbnailMoved = this.ThumbnailViewMoved; - view.ThumbnailFocused = this.ThumbnailViewFocused; - view.ThumbnailLostFocus = this.ThumbnailViewLostFocus; - view.ThumbnailActivated = this.ThumbnailActivated; - view.ThumbnailDeactivated = this.ThumbnailDeactivated; - - view.RegisterHotkey(this._configuration.GetClientHotkey(processTitle)); - - this.ApplyClientLayout(processHandle, processTitle); - - viewsAdded.Add(view); + this._activeClientHandle = process.Handle; + this._activeClientTitle = process.Title; } - else if ((view != null) && (processTitle != view.Title)) // update thumbnail title + } + + foreach (IProcessInfo process in updatedProcesses) + { + this._thumbnailViews.TryGetValue(process.Handle, out IThumbnailView view); + + if (view == null) { - view.Title = processTitle; - view.RegisterHotkey(this._configuration.GetClientHotkey(processTitle)); + // Something went terribly wrong + continue; + } - this.ApplyClientLayout(processHandle, processTitle); + if (process.Title != view.Title) // update thumbnail title + { + view.Title = process.Title; + view.RegisterHotkey(this._configuration.GetClientHotkey(process.Title)); + this.ApplyClientLayout(process.Handle, process.Title); viewsUpdated.Add(view); } - if (process.MainWindowHandle == foregroundWindowHandle) + if (process.Handle == foregroundWindowHandle) { - this._activeClientHandle = process.MainWindowHandle; - this._activeClientTitle = process.MainWindowTitle; + this._activeClientHandle = process.Handle; + this._activeClientTitle = process.Title; } } - // Cleanup - IList obsoleteThumbnails = new List(); - - foreach (IntPtr processHandle in this._thumbnailViews.Keys) + foreach (IProcessInfo process in removedProcesses) { - if (!processHandles.Contains(processHandle)) - { - obsoleteThumbnails.Add(processHandle); - } - } + IThumbnailView view = this._thumbnailViews[process.Handle]; - foreach (IntPtr processHandle in obsoleteThumbnails) - { - IThumbnailView view = this._thumbnailViews[processHandle]; - - this._thumbnailViews.Remove(processHandle); + this._thumbnailViews.Remove(process.Handle); view.UnregisterHotkey(); @@ -453,11 +445,11 @@ namespace EveOPreview.UI private void UpdateClientLayouts() { - Process[] clientProcesses = ThumbnailManager.GetClientProcesses(); + ICollection processes = this._processMonitor.GetAllProcesses(); - foreach (Process process in clientProcesses) + foreach (IProcessInfo process in processes) { - this._windowManager.GetWindowCoordinates(process.MainWindowHandle, out int left, out int top, out int right, out int bottom); + this._windowManager.GetWindowCoordinates(process.Handle, out int left, out int top, out int right, out int bottom); int width = Math.Abs(right - left); int height = Math.Abs(bottom - top); @@ -467,7 +459,7 @@ namespace EveOPreview.UI continue; } - this._configuration.SetClientLayout(process.MainWindowTitle, new ClientLayout(left, top, width, height)); + this._configuration.SetClientLayout(process.Title, new ClientLayout(left, top, width, height)); } } diff --git a/Eve-O-Preview/Program.cs b/Eve-O-Preview/Program.cs index 1ab2408..a439198 100644 --- a/Eve-O-Preview/Program.cs +++ b/Eve-O-Preview/Program.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Windows.Forms; using EveOPreview.Configuration; using EveOPreview.Mediator; +using EveOPreview.Services; using EveOPreview.UI; using EveOPreview.WindowManager; @@ -41,13 +42,13 @@ namespace EveOPreview { // The code might look overcomplicated here for a single Mutex operation // Yet we had already experienced a Windows-level issue - // where .NET finalizer theread was literally paralyzed by + // where .NET finalizer thread was literally paralyzed by // a failed Mutex operation. That did lead to weird OutOfMemory // exceptions later try { - Mutex mutex = Mutex.OpenExisting(Program.MutexName); - // if that didn't fail then anotherinstance is already running + Mutex.OpenExisting(Program.MutexName); + // if that didn't fail then another instance is already running return null; } catch (UnauthorizedAccessException) @@ -75,6 +76,7 @@ namespace EveOPreview // Low-level services container.Register(); container.Register(); + container.Register(); // Configuration services container.Register(); diff --git a/Eve-O-Preview/Services/Implementation/ProcessInfo.cs b/Eve-O-Preview/Services/Implementation/ProcessInfo.cs new file mode 100644 index 0000000..7021c3f --- /dev/null +++ b/Eve-O-Preview/Services/Implementation/ProcessInfo.cs @@ -0,0 +1,16 @@ +using System; + +namespace EveOPreview.Services.Implementation +{ + sealed class ProcessInfo : IProcessInfo + { + public ProcessInfo(IntPtr handle, string title) + { + this.Handle = handle; + this.Title = title; + } + + public IntPtr Handle { get; } + public string Title { get; } + } +} \ No newline at end of file diff --git a/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs b/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs new file mode 100644 index 0000000..3f53e87 --- /dev/null +++ b/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace EveOPreview.Services.Implementation +{ + sealed class ProcessMonitor : IProcessMonitor + { + #region Private constants + private const string DefaultProcessName = "ExeFile"; + #endregion + + #region Private fields + private readonly IDictionary _processCache; + #endregion + + public ProcessMonitor() + { + this._processCache = new Dictionary(512); + } + + private bool IsMonitoredProcess(string processName) + { + // This is a possible extension point + return String.Equals(processName, ProcessMonitor.DefaultProcessName, StringComparison.OrdinalIgnoreCase); + } + + public void GetUpdatedProcesses(out ICollection addedProcesses, out ICollection updatedProcesses, out ICollection removedProcesses) + { + addedProcesses = new List(16); + updatedProcesses = new List(16); + removedProcesses = new List(16); + + IList knownProcesses = new List(this._processCache.Keys); + foreach (Process process in Process.GetProcesses()) + { + string processName = process.ProcessName; + + if (!this.IsMonitoredProcess(processName)) + { + continue; + } + + IntPtr mainWindowHandle = process.MainWindowHandle; + if (mainWindowHandle == IntPtr.Zero) + { + continue; // No need to monitor non-visual processes + } + + string mainWindowTitle = process.MainWindowTitle; + this._processCache.TryGetValue(mainWindowHandle, out string cachedTitle); + + if (cachedTitle == null) + { + // This is a new process in the list + this._processCache.Add(mainWindowHandle, mainWindowTitle); + addedProcesses.Add(new ProcessInfo(mainWindowHandle, mainWindowTitle)); + } + else + { + // This is an already known process + if (cachedTitle != mainWindowTitle) + { + this._processCache[mainWindowHandle] = mainWindowTitle; + updatedProcesses.Add((IProcessInfo)new ProcessInfo(mainWindowHandle, mainWindowTitle)); + } + + knownProcesses.Remove(mainWindowHandle); + } + } + + foreach (IntPtr index in knownProcesses) + { + string title = this._processCache[index]; + removedProcesses.Add(new ProcessInfo(index, title)); + this._processCache.Remove(index); + } + } + + public ICollection GetAllProcesses() + { + ICollection result = new List(this._processCache.Count); + + // TODO Lock list here just in case + foreach (KeyValuePair entry in this._processCache) + { + result.Add(new ProcessInfo(entry.Key, entry.Value)); + } + + return result; + } + } +} diff --git a/Eve-O-Preview/Services/Interface/IProcessInfo.cs b/Eve-O-Preview/Services/Interface/IProcessInfo.cs new file mode 100644 index 0000000..d86e9ac --- /dev/null +++ b/Eve-O-Preview/Services/Interface/IProcessInfo.cs @@ -0,0 +1,10 @@ +using System; + +namespace EveOPreview.Services +{ + public interface IProcessInfo + { + IntPtr Handle { get; } + string Title { get; } + } +} \ No newline at end of file diff --git a/Eve-O-Preview/Services/Interface/IProcessMonitor.cs b/Eve-O-Preview/Services/Interface/IProcessMonitor.cs new file mode 100644 index 0000000..1e123bb --- /dev/null +++ b/Eve-O-Preview/Services/Interface/IProcessMonitor.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace EveOPreview.Services +{ + public interface IProcessMonitor + { + ICollection GetAllProcesses(); + void GetUpdatedProcesses(out ICollection addedProcesses, out ICollection updatedProcesses, out ICollection removedProcesses); + } +} \ No newline at end of file