using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Windows.Threading; using EveOPreview.Configuration; namespace EveOPreview.UI { public class ThumbnailManager : IThumbnailManager { #region Private constants private const string ClientProcessName = "ExeFile"; #endregion #region Private fields private readonly IApplicationConfiguration _configuration; private readonly IConfigurationStorage _configurationStorage; private readonly DispatcherTimer _thumbnailUpdateTimer; private readonly IThumbnailViewFactory _thumbnailViewFactory; private readonly Dictionary _thumbnailViews; private IntPtr _activeClientHandle; private string _activeClientTitle; private bool _ignoreViewEvents; private bool _isHoverEffectActive; #endregion public ThumbnailManager(IApplicationConfiguration configuration, IConfigurationStorage configurationStorage, IThumbnailViewFactory factory) { this._configuration = configuration; this._configurationStorage = configurationStorage; this._thumbnailViewFactory = factory; this._activeClientHandle = (IntPtr)0; this._activeClientTitle = "EVE"; this.EnableViewEvents(); this._isHoverEffectActive = false; this._thumbnailViews = new Dictionary(); // DispatcherTimer setup this._thumbnailUpdateTimer = new DispatcherTimer(); this._thumbnailUpdateTimer.Tick += ThumbnailUpdateTimerTick; this._thumbnailUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, configuration.ThumbnailRefreshPeriod); } public Action> ThumbnailsAdded { get; set; } public Action> ThumbnailsUpdated { get; set; } public Action> ThumbnailsRemoved { get; set; } public Action ThumbnailSizeChanged { get; set; } public void Activate() { this._thumbnailUpdateTimer.Start(); this.RefreshThumbnails(); } public void Deactivate() { this._thumbnailUpdateTimer.Stop(); } public void SetThumbnailState(IntPtr thumbnailId, bool hideAlways) { IThumbnailView thumbnail; if (!this._thumbnailViews.TryGetValue(thumbnailId, out thumbnail)) { return; } thumbnail.IsEnabled = !hideAlways; } public void SetThumbnailsSize(Size size) { this.DisableViewEvents(); foreach (KeyValuePair entry in this._thumbnailViews) { entry.Value.Size = size; entry.Value.Refresh(false); } this.ThumbnailSizeChanged?.Invoke(size); this.EnableViewEvents(); } public void RefreshThumbnails() { IntPtr foregroundWindowHandle = DwmApiNativeMethods.GetForegroundWindow(); Boolean hideAllThumbnails = (this._configuration.HideThumbnailsOnLostFocus && this.IsNonClientWindowActive(foregroundWindowHandle)) || !DwmApiNativeMethods.DwmIsCompositionEnabled(); this.DisableViewEvents(); // Hide, show, resize and move foreach (KeyValuePair entry in this._thumbnailViews) { IThumbnailView view = entry.Value; if (hideAllThumbnails || !view.IsEnabled) { if (view.IsActive) { view.Hide(); } continue; } if (this._configuration.HideActiveClientThumbnail && (view.Id == this._activeClientHandle)) { if (view.IsActive) { view.Hide(); } continue; } if (!this._isHoverEffectActive) { // No need to move Thumbnails while one of them is highlighted view.Location = this._configuration.GetThumbnailLocation(view.Title, this._activeClientTitle, view.Location); view.SetOpacity(this._configuration.ThumbnailsOpacity); view.SetTopMost(this._configuration.ShowThumbnailsAlwaysOnTop); } view.IsOverlayEnabled = this._configuration.ShowThumbnailOverlays; if (!view.IsActive) { view.Show(); } else { view.Refresh(false); } } this.EnableViewEvents(); } public void SetupThumbnailFrames() { this.DisableViewEvents(); foreach (KeyValuePair entry in this._thumbnailViews) { entry.Value.SetWindowFrames(this._configuration.ShowThumbnailFrames); } this.EnableViewEvents(); } private void ThumbnailUpdateTimerTick(object sender, EventArgs e) { this.UpdateThumbnailsList(); this.RefreshThumbnails(); } private void EnableViewEvents() { this._ignoreViewEvents = false; } private void DisableViewEvents() { 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); IntPtr foregroundWindowHandle = DwmApiNativeMethods.GetForegroundWindow(); List viewsAdded = new List(); List viewsUpdated = new List(); List viewsRemoved = new List(); foreach (Process process in clientProcesses) { IntPtr processHandle = process.MainWindowHandle; string processTitle = process.MainWindowTitle; processHandles.Add(processHandle); IThumbnailView view; this._thumbnailViews.TryGetValue(processHandle, out view); if ((view == null) && (processTitle != "")) { view = this._thumbnailViewFactory.Create(processHandle, processTitle, this._configuration.ThumbnailSize); view.IsEnabled = true; view.IsOverlayEnabled = this._configuration.ShowThumbnailOverlays; view.SetSizeLimitations(this._configuration.ThumbnailMinimumSize, this._configuration.ThumbnailMaximumSize); view.SetTopMost(this._configuration.ShowThumbnailsAlwaysOnTop); view.SetWindowFrames(this._configuration.ShowThumbnailFrames); view.Location = this._configuration.GetThumbnailLocation(processTitle, this._activeClientTitle, view.Location); view.ThumbnailResized = this.ThumbnailViewResized; view.ThumbnailMoved = this.ThumbnailViewMoved; view.ThumbnailFocused = this.ThumbnailViewFocused; view.ThumbnailLostFocus = this.ThumbnailViewLostFocus; view.ThumbnailActivated = this.ThumbnailActivated; view.RegisterHotkey(this._configuration.GetClientHotkey(processTitle)); this._thumbnailViews.Add(processHandle, view); this.ApplyClientLayout(processHandle, processTitle); viewsAdded.Add(view); } else if ((view != null) && (processTitle != view.Title)) // update thumbnail title { view.Title = processTitle; view.RegisterHotkey(this._configuration.GetClientHotkey(processTitle)); this.ApplyClientLayout(processHandle, processTitle); viewsUpdated.Add(view); } if (process.MainWindowHandle == foregroundWindowHandle) { this._activeClientHandle = process.MainWindowHandle; this._activeClientTitle = process.MainWindowTitle; } } // Cleanup IList obsoleteThumbnails = new List(); foreach (IntPtr processHandle in this._thumbnailViews.Keys) { if (!processHandles.Contains(processHandle)) { obsoleteThumbnails.Add(processHandle); } } foreach (IntPtr processHandle in obsoleteThumbnails) { IThumbnailView view = this._thumbnailViews[processHandle]; this._thumbnailViews.Remove(processHandle); view.UnregisterHotkey(); view.ThumbnailResized = null; view.ThumbnailMoved = null; view.ThumbnailFocused = null; view.ThumbnailLostFocus = null; view.ThumbnailActivated = null; view.Close(); viewsRemoved.Add(view); } this.ThumbnailsAdded?.Invoke(viewsAdded); this.ThumbnailsUpdated?.Invoke(viewsUpdated); this.ThumbnailsRemoved?.Invoke(viewsRemoved); } private void ThumbnailViewFocused(IntPtr id) { if (this._isHoverEffectActive) { return; } this._isHoverEffectActive = true; IThumbnailView view = this._thumbnailViews[id]; view.SetTopMost(true); view.SetOpacity(1.0); if (this._configuration.EnableThumbnailZoom) { this.ThumbnailZoomIn(view); } } private void ThumbnailViewLostFocus(IntPtr id) { if (!this._isHoverEffectActive) { return; } IThumbnailView view = this._thumbnailViews[id]; if (this._configuration.EnableThumbnailZoom) { this.ThumbnailZoomOut(view); } view.SetOpacity(this._configuration.ThumbnailsOpacity); this._isHoverEffectActive = false; } private void ThumbnailActivated(IntPtr id) { DwmApiNativeMethods.SetForegroundWindow(id); int style = DwmApiNativeMethods.GetWindowLong(id, DwmApiNativeMethods.GWL_STYLE); // If the window was minimized then its thumbnail should be reset if ((style & DwmApiNativeMethods.WS_MINIMIZE) == DwmApiNativeMethods.WS_MINIMIZE) { DwmApiNativeMethods.ShowWindowAsync(id, DwmApiNativeMethods.SW_SHOWNORMAL); IThumbnailView view; if (this._thumbnailViews.TryGetValue(id, out view)) { view.Refresh(true); } } if (this._configuration.EnableClientLayoutTracking) { this.UpdateClientLayouts(); } this.RefreshThumbnails(); this._configurationStorage.Save(); } private void ThumbnailViewResized(IntPtr id) { if (this._ignoreViewEvents) { return; } IThumbnailView view = this._thumbnailViews[id]; this.SetThumbnailsSize(view.Size); view.Refresh(false); } private void ThumbnailViewMoved(IntPtr id) { if (this._ignoreViewEvents) { return; } IThumbnailView view = this._thumbnailViews[id]; this._configuration.SetThumbnailLocation(view.Title, this._activeClientTitle, view.Location); view.Refresh(false); } private bool IsNonClientWindowActive(IntPtr windowHandle) { // We just don't know ATM if (windowHandle == IntPtr.Zero) { return false; } foreach (KeyValuePair entry in this._thumbnailViews) { IThumbnailView view = entry.Value; if (view.IsKnownHandle(windowHandle)) { return false; } } return true; } private void ThumbnailZoomIn(IThumbnailView view) { this.DisableViewEvents(); view.ZoomIn(ViewZoomAnchorConverter.Convert(this._configuration.ThumbnailZoomAnchor), this._configuration.ThumbnailZoomFactor); view.Refresh(false); this.EnableViewEvents(); } private void ThumbnailZoomOut(IThumbnailView view) { this.DisableViewEvents(); view.ZoomOut(); view.Refresh(false); this.EnableViewEvents(); } private void ApplyClientLayout(IntPtr clientHandle, string clientTitle) { ClientLayout clientLayout = this._configuration.GetClientLayout(clientTitle); if (clientLayout == null) { return; } DwmApiNativeMethods.MoveWindow(clientHandle, clientLayout.X, clientLayout.Y, clientLayout.Width, clientLayout.Height, true); } private void UpdateClientLayouts() { Process[] clientProcesses = ThumbnailManager.GetClientProcesses(); foreach (Process process in clientProcesses) { RECT rect; DwmApiNativeMethods.GetWindowRect(process.MainWindowHandle, out rect); int left = Math.Abs(rect.Left); int right = Math.Abs(rect.Right); int clientWidth = Math.Abs(left - right); int top = Math.Abs(rect.Top); int bottom = Math.Abs(rect.Bottom); int clientHeight = Math.Abs(top - bottom); ClientLayout clientLayout = new ClientLayout(); clientLayout.X = rect.Left; clientLayout.Y = rect.Top; clientLayout.Width = clientWidth; clientLayout.Height = clientHeight; this._configuration.SetClientLayout(process.MainWindowTitle, clientLayout); } } } }