diff --git a/Eve-O-Preview/ApplicationBase/ExceptionHandler.cs b/Eve-O-Preview/ApplicationBase/ExceptionHandler.cs index 461370e..f1669b6 100644 --- a/Eve-O-Preview/ApplicationBase/ExceptionHandler.cs +++ b/Eve-O-Preview/ApplicationBase/ExceptionHandler.cs @@ -12,8 +12,8 @@ namespace EveOPreview // So this dumb and non elegant approach is used sealed class ExceptionHandler { - private const string ExceptionDumpFileName = "EVE-O Preview.log"; - private const string ExceptionMessage = "EVE-O Preview has encountered a problem and needs to close. Additional information has been saved in the crash log file."; + private const string EXCEPTION_DUMP_FILE_NAME = "EVE-O Preview.log"; + private const string EXCEPTION_MESSAGE = "EVE-O Preview has encountered a problem and needs to close. Additional information has been saved in the crash log file."; public void SetupExceptionHandlers() { @@ -39,9 +39,9 @@ namespace EveOPreview try { String exceptionMessage = exception.ToString(); - File.WriteAllText(ExceptionHandler.ExceptionDumpFileName, exceptionMessage); + File.WriteAllText(ExceptionHandler.EXCEPTION_DUMP_FILE_NAME, exceptionMessage); - MessageBox.Show(ExceptionHandler.ExceptionMessage, @"EVE-O Preview", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show(ExceptionHandler.EXCEPTION_MESSAGE, @"EVE-O Preview", MessageBoxButtons.OK, MessageBoxIcon.Error); } catch { diff --git a/Eve-O-Preview/Configuration/Implementation/ConfigurationStorage.cs b/Eve-O-Preview/Configuration/Implementation/ConfigurationStorage.cs index e07ec8f..3419b74 100644 --- a/Eve-O-Preview/Configuration/Implementation/ConfigurationStorage.cs +++ b/Eve-O-Preview/Configuration/Implementation/ConfigurationStorage.cs @@ -5,7 +5,7 @@ namespace EveOPreview.Configuration.Implementation { class ConfigurationStorage : IConfigurationStorage { - private const string ConfigurationFileName = "EVE-O Preview.json"; + private const string CONFIGURATION_FILE_NAME = "EVE-O Preview.json"; private readonly IAppConfig _appConfig; private readonly IThumbnailConfiguration _thumbnailConfiguration; @@ -50,7 +50,7 @@ namespace EveOPreview.Configuration.Implementation private string GetConfigFileName() { - return string.IsNullOrEmpty(this._appConfig.ConfigFileName) ? ConfigurationStorage.ConfigurationFileName : this._appConfig.ConfigFileName; + return string.IsNullOrEmpty(this._appConfig.ConfigFileName) ? ConfigurationStorage.CONFIGURATION_FILE_NAME : this._appConfig.ConfigFileName; } } } \ No newline at end of file diff --git a/Eve-O-Preview/Configuration/Implementation/ThumbnailConfiguration.cs b/Eve-O-Preview/Configuration/Implementation/ThumbnailConfiguration.cs index eac17e9..779f509 100644 --- a/Eve-O-Preview/Configuration/Implementation/ThumbnailConfiguration.cs +++ b/Eve-O-Preview/Configuration/Implementation/ThumbnailConfiguration.cs @@ -24,6 +24,8 @@ namespace EveOPreview.Configuration.Implementation this.MinimizeToTray = false; this.ThumbnailRefreshPeriod = 500; + this.EnableCompatibilityMode = false; + this.ThumbnailOpacity = 0.5; this.EnableClientLayoutTracking = false; @@ -54,6 +56,9 @@ namespace EveOPreview.Configuration.Implementation public bool MinimizeToTray { get; set; } public int ThumbnailRefreshPeriod { get; set; } + [JsonProperty("CompatibilityMode")] + public bool EnableCompatibilityMode { get; set; } + [JsonProperty("ThumbnailsOpacity")] public double ThumbnailOpacity { get; set; } diff --git a/Eve-O-Preview/Configuration/Interface/ClientLayout.cs b/Eve-O-Preview/Configuration/Interface/ClientLayout.cs index 02266c4..c158401 100644 --- a/Eve-O-Preview/Configuration/Interface/ClientLayout.cs +++ b/Eve-O-Preview/Configuration/Interface/ClientLayout.cs @@ -6,18 +6,23 @@ namespace EveOPreview.Configuration { } - public ClientLayout(int x, int y, int width, int height) + public ClientLayout(int x, int y, int width, int height, bool maximized) { this.X = x; this.Y = y; this.Width = width; this.Height = height; + this.IsMaximized = maximized; } public int X { get; set; } + public int Y { get; set; } public int Width { get; set; } + public int Height { get; set; } + + public bool IsMaximized { get; set; } } } \ No newline at end of file diff --git a/Eve-O-Preview/Configuration/Interface/IThumbnailConfiguration.cs b/Eve-O-Preview/Configuration/Interface/IThumbnailConfiguration.cs index 565e395..3cb1158 100644 --- a/Eve-O-Preview/Configuration/Interface/IThumbnailConfiguration.cs +++ b/Eve-O-Preview/Configuration/Interface/IThumbnailConfiguration.cs @@ -8,6 +8,8 @@ namespace EveOPreview.Configuration bool MinimizeToTray { get; set; } int ThumbnailRefreshPeriod { get; set; } + bool EnableCompatibilityMode { get; set; } + double ThumbnailOpacity { get; set; } bool EnableClientLayoutTracking { get; set; } diff --git a/Eve-O-Preview/Eve-O-Preview.csproj b/Eve-O-Preview/Eve-O-Preview.csproj index abdecee..375b7a6 100644 --- a/Eve-O-Preview/Eve-O-Preview.csproj +++ b/Eve-O-Preview/Eve-O-Preview.csproj @@ -1,5 +1,6 @@  + Debug AnyCPU @@ -9,7 +10,7 @@ WinExe Properties EveOPreview - Eve-O Preview + EVE-O Preview v4.7 @@ -75,24 +76,29 @@ app.manifest - - ..\packages\Costura.Fody.1.6.2\lib\dotnet\Costura.dll - False + + ..\packages\Costura.Fody.3.3.2\lib\net40\Costura.dll - - ..\packages\LightInject.5.1.2\lib\net452\LightInject.dll - True + + ..\packages\LightInject.5.4.0\lib\net46\LightInject.dll - - ..\packages\MediatR.4.0.1\lib\net45\MediatR.dll + + ..\packages\MediatR.6.0.0\lib\net461\MediatR.dll - - ..\packages\Newtonsoft.Json.11.0.1\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + @@ -141,6 +147,7 @@ + @@ -151,6 +158,15 @@ + + Form + + + Component + + + Form + @@ -206,7 +222,7 @@ ThumbnailView.cs - + @@ -214,7 +230,6 @@ Designer - @@ -222,15 +237,14 @@ + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + - - + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Eve-O-Preview/Hotkeys/HotkeyHandler.cs b/Eve-O-Preview/Hotkeys/HotkeyHandler.cs index 86c705a..bb95186 100644 --- a/Eve-O-Preview/Hotkeys/HotkeyHandler.cs +++ b/Eve-O-Preview/Hotkeys/HotkeyHandler.cs @@ -7,7 +7,7 @@ namespace EveOPreview.UI.Hotkeys class HotkeyHandler : IMessageFilter, IDisposable { private static int _currentId; - private const int MaxId = 0xBFFF; + private const int MAX_ID = 0xBFFF; #region Private fields private readonly int _hotkeyId; @@ -17,7 +17,7 @@ namespace EveOPreview.UI.Hotkeys public HotkeyHandler(IntPtr target, Keys hotkey) { this._hotkeyId = HotkeyHandler._currentId; - HotkeyHandler._currentId = (HotkeyHandler._currentId + 1) & HotkeyHandler.MaxId; + HotkeyHandler._currentId = (HotkeyHandler._currentId + 1) & HotkeyHandler.MAX_ID; this._hotkeyTarget = target; @@ -29,28 +29,14 @@ namespace EveOPreview.UI.Hotkeys public void Dispose() { - if (this.IsRegistered) - { - this.Unregister(); - } - + this.Unregister(); GC.SuppressFinalize(this); } ~HotkeyHandler() { // Unregister the hotkey if necessary - if (this.IsRegistered) - { - try - { - this.Unregister(); - } - catch (Exception) - { - // Please no exceptions in the finalizer thread - } - } + this.Unregister(); } public bool IsRegistered { get; private set; } @@ -61,22 +47,12 @@ namespace EveOPreview.UI.Hotkeys public bool CanRegister() { - // Any exception means "no, you can't register" - try - { - // Attempt to register - if (this.Register()) - { - // Unregister and say we managed it - this.Unregister(); - return true; - } - } - catch (Win32Exception) - { - } - catch (NotSupportedException) + // Attempt to register + if (this.Register()) { + // Unregister and say we managed it + this.Unregister(); + return true; } return false; @@ -87,12 +63,12 @@ namespace EveOPreview.UI.Hotkeys // Check that we have not registered if (this.IsRegistered) { - throw new NotSupportedException("This hotkey is already registered"); + return false; } if (this.KeyCode == Keys.None) { - throw new NotSupportedException("Cannot register an empty hotkey"); + return false; } // Remove all modifiers from the 'main' hotkey @@ -122,18 +98,15 @@ namespace EveOPreview.UI.Hotkeys // Check that we have registered if (!this.IsRegistered) { - throw new NotSupportedException("This hotkey was not registered"); + return; } + this.IsRegistered = false; + Application.RemoveMessageFilter(this); // Clean up after ourselves - if (!HotkeyHandlerNativeMethods.UnregisterHotKey(this._hotkeyTarget, this._hotkeyId)) - { - throw new Win32Exception(); - } - - this.IsRegistered = false; + HotkeyHandlerNativeMethods.UnregisterHotKey(this._hotkeyTarget, this._hotkeyId); } #region IMessageFilter diff --git a/Eve-O-Preview/Mediator/Handlers/Configuration/SaveConfigurationHandler.cs b/Eve-O-Preview/Mediator/Handlers/Configuration/SaveConfigurationHandler.cs index c870f7a..f1b2db2 100644 --- a/Eve-O-Preview/Mediator/Handlers/Configuration/SaveConfigurationHandler.cs +++ b/Eve-O-Preview/Mediator/Handlers/Configuration/SaveConfigurationHandler.cs @@ -15,11 +15,11 @@ namespace EveOPreview.Mediator.Handlers.Configuration this._storage = storage; } - public Task Handle(SaveConfiguration message, CancellationToken cancellationToken) + public Task Handle(SaveConfiguration message, CancellationToken cancellationToken) { this._storage.Save(); - return Task.CompletedTask; + return Unit.Task; } } } \ No newline at end of file diff --git a/Eve-O-Preview/Mediator/Handlers/Services/StartStopServiceHandler.cs b/Eve-O-Preview/Mediator/Handlers/Services/StartStopServiceHandler.cs index 2b34cdb..896ba68 100644 --- a/Eve-O-Preview/Mediator/Handlers/Services/StartStopServiceHandler.cs +++ b/Eve-O-Preview/Mediator/Handlers/Services/StartStopServiceHandler.cs @@ -15,18 +15,18 @@ namespace EveOPreview.Mediator.Handlers.Services this._manager = manager; } - public Task Handle(StartService message, CancellationToken cancellationToken) + public Task Handle(StartService message, CancellationToken cancellationToken) { this._manager.Start(); - return Task.CompletedTask; + return Unit.Task; } - public Task Handle(StopService message, CancellationToken cancellationToken) + public Task Handle(StopService message, CancellationToken cancellationToken) { this._manager.Stop(); - return Task.CompletedTask; + return Unit.Task; } } } \ No newline at end of file diff --git a/Eve-O-Preview/Presenters/Implementation/MainFormPresenter.cs b/Eve-O-Preview/Presenters/Implementation/MainFormPresenter.cs index 8d35104..57b82ea 100644 --- a/Eve-O-Preview/Presenters/Implementation/MainFormPresenter.cs +++ b/Eve-O-Preview/Presenters/Implementation/MainFormPresenter.cs @@ -12,7 +12,7 @@ namespace EveOPreview.Presenters public class MainFormPresenter : Presenter, IMainFormPresenter { #region Private constants - private const string ForumUrl = @"https://forum.eveonline.com/t/4202"; + private const string FORUM_URL = @"https://forum.eveonline.com/t/4202"; #endregion #region Private fields @@ -50,7 +50,7 @@ namespace EveOPreview.Presenters private void Activate() { this.LoadApplicationSettings(); - this.View.SetDocumentationUrl(MainFormPresenter.ForumUrl); + this.View.SetDocumentationUrl(MainFormPresenter.FORUM_URL); this.View.SetVersionInfo(this.GetApplicationVersion()); if (this._configuration.MinimizeToTray) { @@ -225,7 +225,7 @@ namespace EveOPreview.Presenters private void OpenDocumentationLink() { // TODO Move out to a separate service / presenter / message handler - ProcessStartInfo processStartInfo = new ProcessStartInfo(new Uri(MainFormPresenter.ForumUrl).AbsoluteUri); + ProcessStartInfo processStartInfo = new ProcessStartInfo(new Uri(MainFormPresenter.FORUM_URL).AbsoluteUri); Process.Start(processStartInfo); } diff --git a/Eve-O-Preview/Program.cs b/Eve-O-Preview/Program.cs index eb723f5..dd29e98 100644 --- a/Eve-O-Preview/Program.cs +++ b/Eve-O-Preview/Program.cs @@ -4,7 +4,6 @@ using System.Windows.Forms; using EveOPreview.Configuration; using EveOPreview.Presenters; using EveOPreview.Services; -using EveOPreview.UI; using EveOPreview.View; using MediatR; @@ -12,7 +11,9 @@ namespace EveOPreview { static class Program { - private static string MutexName = "EVE-O Preview Single Instance Mutex"; + private static string MUTEX_NAME = "EVE-O Preview Single Instance Mutex"; + + private static Mutex _singleInstanceMutex; /// The main entry point for the application. [STAThread] @@ -21,11 +22,11 @@ namespace EveOPreview // The very usual Mutex-based single-instance screening // 'token' variable is used to store reference to the instance Mutex // during the app lifetime - object token = Program.GetInstanceToken(); + Program._singleInstanceMutex = Program.GetInstanceToken(); - // If it was not possible to aquire the app token then another app instance is already running + // If it was not possible to acquire the app token then another app instance is already running // Nothing to do here - if (token == null) + if (Program._singleInstanceMutex == null) { return; } @@ -39,7 +40,7 @@ namespace EveOPreview controller.Run(); } - private static object GetInstanceToken() + private static Mutex GetInstanceToken() { // The code might look overcomplicated here for a single Mutex operation // Yet we had already experienced a Windows-level issue @@ -48,7 +49,7 @@ namespace EveOPreview // exceptions later try { - Mutex.OpenExisting(Program.MutexName); + Mutex.OpenExisting(Program.MUTEX_NAME); // if that didn't fail then another instance is already running return null; } @@ -58,7 +59,7 @@ namespace EveOPreview } catch (Exception) { - Mutex token = new Mutex(true, Program.MutexName, out var result); + Mutex token = new Mutex(true, Program.MUTEX_NAME, out var result); return result ? token : null; } } @@ -80,8 +81,7 @@ namespace EveOPreview // MediatR container.Register(); - container.RegisterInstance(t => container.Resolve(t)); - container.RegisterInstance(t => container.ResolveAll(t)); + container.RegisterInstance(t => container.Resolve(t)); container.Register(typeof(INotificationHandler<>), typeof(Program).Assembly); container.Register(typeof(IRequestHandler<>), typeof(Program).Assembly); container.Register(typeof(IRequestHandler<,>), typeof(Program).Assembly); @@ -99,9 +99,11 @@ namespace EveOPreview IApplicationController controller = new ApplicationController(container); // UI classes - controller.RegisterView() - .RegisterView() - .RegisterInstance(new ApplicationContext()); + controller.RegisterView(); + controller.RegisterView(); + + controller.RegisterView(); + controller.RegisterInstance(new ApplicationContext()); return controller; } diff --git a/Eve-O-Preview/Properties/AssemblyInfo.cs b/Eve-O-Preview/Properties/AssemblyInfo.cs index 363b1d9..701f66a 100644 --- a/Eve-O-Preview/Properties/AssemblyInfo.cs +++ b/Eve-O-Preview/Properties/AssemblyInfo.cs @@ -12,7 +12,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] [assembly: Guid("04f08f8d-9e98-423b-acdb-4effb31c0d35")] -[assembly: AssemblyVersion("4.0.1.0")] -[assembly: AssemblyFileVersion("4.0.1.0")] +[assembly: AssemblyVersion("5.0.0.0")] +[assembly: AssemblyFileVersion("5.0.0.0")] [assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/Eve-O-Preview/Services/Implementation/DwmThumbnail.cs b/Eve-O-Preview/Services/Implementation/DwmThumbnail.cs index e1b8fa6..7562929 100644 --- a/Eve-O-Preview/Services/Implementation/DwmThumbnail.cs +++ b/Eve-O-Preview/Services/Implementation/DwmThumbnail.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using EveOPreview.Services.Interop; namespace EveOPreview.Services.Implementation @@ -35,16 +36,22 @@ namespace EveOPreview.Services.Implementation try { - this._handle = DwmApiNativeMethods.DwmRegisterThumbnail(destination, source); + this._handle = DwmNativeMethods.DwmRegisterThumbnail(destination, source); } catch (ArgumentException) { // This exception is raised if the source client is already closed // Can happen on a really slow CPU's that the window is still being - // lised in the process list yet it already cannot be used as + // listed in the process list yet it already cannot be used as // a thumbnail source this._handle = IntPtr.Zero; } + catch (COMException) + { + // This exception is raised if DWM is suddenly not available + // (f.e. when switching between Windows user accounts) + this._handle = IntPtr.Zero; + } } public void Unregister() @@ -56,11 +63,15 @@ namespace EveOPreview.Services.Implementation try { - DwmApiNativeMethods.DwmUnregisterThumbnail(this._handle); + DwmNativeMethods.DwmUnregisterThumbnail(this._handle); } catch (ArgumentException) { } + catch (COMException) + { + // This exception is raised when DWM is not available for some reason + } } public void Move(int left, int top, int right, int bottom) @@ -77,11 +88,15 @@ namespace EveOPreview.Services.Implementation try { - DwmApiNativeMethods.DwmUpdateThumbnailProperties(this._handle, this._properties); + DwmNativeMethods.DwmUpdateThumbnailProperties(this._handle, this._properties); } catch (ArgumentException) { - //This exception will be thrown if the EVE client disappears while this method is running + // This exception will be thrown if the EVE client disappears while this method is running + } + catch (COMException) + { + // This exception is raised when DWM is not available for some reason } } } diff --git a/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs b/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs index 3f53e87..5c5e057 100644 --- a/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs +++ b/Eve-O-Preview/Services/Implementation/ProcessMonitor.cs @@ -7,7 +7,7 @@ namespace EveOPreview.Services.Implementation sealed class ProcessMonitor : IProcessMonitor { #region Private constants - private const string DefaultProcessName = "ExeFile"; + private const string DEFAULT_PROCESS_NAME = "ExeFile"; #endregion #region Private fields @@ -22,7 +22,7 @@ namespace EveOPreview.Services.Implementation private bool IsMonitoredProcess(string processName) { // This is a possible extension point - return String.Equals(processName, ProcessMonitor.DefaultProcessName, StringComparison.OrdinalIgnoreCase); + return String.Equals(processName, ProcessMonitor.DEFAULT_PROCESS_NAME, StringComparison.OrdinalIgnoreCase); } public void GetUpdatedProcesses(out ICollection addedProcesses, out ICollection updatedProcesses, out ICollection removedProcesses) @@ -62,7 +62,7 @@ namespace EveOPreview.Services.Implementation if (cachedTitle != mainWindowTitle) { this._processCache[mainWindowHandle] = mainWindowTitle; - updatedProcesses.Add((IProcessInfo)new ProcessInfo(mainWindowHandle, mainWindowTitle)); + updatedProcesses.Add(new ProcessInfo(mainWindowHandle, mainWindowTitle)); } knownProcesses.Remove(mainWindowHandle); diff --git a/Eve-O-Preview/Services/Implementation/ThumbnailManager.cs b/Eve-O-Preview/Services/Implementation/ThumbnailManager.cs index 693e12d..ea0904f 100644 --- a/Eve-O-Preview/Services/Implementation/ThumbnailManager.cs +++ b/Eve-O-Preview/Services/Implementation/ThumbnailManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows.Threading; using EveOPreview.Configuration; @@ -11,16 +10,16 @@ using MediatR; namespace EveOPreview.Services { - class ThumbnailManager : IThumbnailManager + sealed class ThumbnailManager : IThumbnailManager { #region Private constants - private const int WindowPositionThresholdLow = -10_000; - private const int WindowPositionThresholdHigh = 31_000; - private const int WindowSizeThreshold = 10; - private const int ForcedRefreshCycleThreshold = 2; - private const int DefaultLocationChangeNotificationDelay = 2; + private const int WINDOW_POSITION_THRESHOLD_LOW = -10_000; + private const int WINDOW_POSITION_THRESHOLD_HIGH = 31_000; + private const int WINDOW_SIZE_THRESHOLD = 10; + private const int FORCED_REFRESH_CYCLE_THRESHOLD = 2; + private const int DEFAULT_LOCATION_CHANGE_NOTIFICATION_DELAY = 2; - private const string DefaultClientTitle = "EVE"; + private const string DEFAULT_CLIENT_TITLE = "EVE"; #endregion #region Private fields @@ -33,6 +32,7 @@ namespace EveOPreview.Services private readonly Dictionary _thumbnailViews; private (IntPtr Handle, string Title) _activeClient; + private IntPtr _externalApplication; private readonly object _locationChangeNotificationSyncRoot; private (IntPtr Handle, string Title, string ActiveClient, Point Location, int Delay) _enqueuedLocationChangeNotification; @@ -51,7 +51,7 @@ namespace EveOPreview.Services this._configuration = configuration; this._thumbnailViewFactory = factory; - this._activeClient = (IntPtr.Zero, ThumbnailManager.DefaultClientTitle); + this._activeClient = (IntPtr.Zero, ThumbnailManager.DEFAULT_CLIENT_TITLE); this.EnableViewEvents(); this._isHoverEffectActive = false; @@ -121,7 +121,7 @@ namespace EveOPreview.Services this.ApplyClientLayout(view.Id, view.Title); // TODO Add extension filter here later - if (view.Title != ThumbnailManager.DefaultClientTitle) + if (view.Title != ThumbnailManager.DEFAULT_CLIENT_TITLE) { viewsAdded.Add(view.Title); } @@ -154,7 +154,7 @@ namespace EveOPreview.Services IThumbnailView view = this._thumbnailViews[process.Handle]; this._thumbnailViews.Remove(view.Id); - if (view.Title != ThumbnailManager.DefaultClientTitle) + if (view.Title != ThumbnailManager.DEFAULT_CLIENT_TITLE) { viewsRemoved.Add(view.Title); } @@ -182,6 +182,9 @@ namespace EveOPreview.Services IntPtr foregroundWindowHandle = this._windowManager.GetForegroundWindowHandle(); string foregroundWindowTitle = null; + // Check if the foreground window handle is one of the known handles for client windows or their thumbnails + bool isClientWindow = this.IsClientWindowActive(foregroundWindowHandle); + if (foregroundWindowHandle == this._activeClient.Handle) { foregroundWindowTitle = this._activeClient.Title; @@ -191,6 +194,15 @@ namespace EveOPreview.Services // This code will work only on Alt+Tab switch between clients foregroundWindowTitle = foregroundView.Title; } + else if (!isClientWindow) + { + // Under some circumstances Foreground WindowHandle can be zero + // (f.e. when Thumbnail is silently stealing focus from the currently open app) + if (foregroundWindowHandle != IntPtr.Zero) + { + this._externalApplication = foregroundWindowHandle; + } + } // No need to minimize EVE clients when switching out to non-EVE window (like thumbnail) if (!string.IsNullOrEmpty(foregroundWindowTitle)) @@ -198,12 +210,12 @@ namespace EveOPreview.Services this.SwitchActiveClient(foregroundWindowHandle, foregroundWindowTitle); } - bool hideAllThumbnails = this._configuration.HideThumbnailsOnLostFocus && !this.IsClientWindowActive(foregroundWindowHandle); + bool hideAllThumbnails = this._configuration.HideThumbnailsOnLostFocus && !isClientWindow; this._refreshCycleCount++; bool forceRefresh; - if (this._refreshCycleCount >= ThumbnailManager.ForcedRefreshCycleThreshold) + if (this._refreshCycleCount >= ThumbnailManager.FORCED_REFRESH_CYCLE_THRESHOLD) { this._refreshCycleCount = 0; forceRefresh = true; @@ -223,11 +235,11 @@ namespace EveOPreview.Services { this.SnapThumbnailView(view); - this.RaiseThumbnailLocationUpdatedNotification(view.Title, this._activeClient.Title, view.ThumbnailLocation); + this.RaiseThumbnailLocationUpdatedNotification(view.Title); } else { - this.RaiseThumbnailLocationUpdatedNotification(locationChange.Title, locationChange.ActiveClient, locationChange.Location); + this.RaiseThumbnailLocationUpdatedNotification(locationChange.Title); } } @@ -325,10 +337,10 @@ namespace EveOPreview.Services this._ignoreViewEvents = true; } - private void SwitchActiveClient(IntPtr foregroungClientHandle, string foregroundClientTitle) + private void SwitchActiveClient(IntPtr foregroundClientHandle, string foregroundClientTitle) { // Check if any actions are needed - if (this._activeClient.Handle == foregroungClientHandle) + if (this._activeClient.Handle == foregroundClientHandle) { return; } @@ -339,7 +351,7 @@ namespace EveOPreview.Services this._windowManager.MinimizeWindow(this._activeClient.Handle, false); } - this._activeClient = (foregroungClientHandle, foregroundClientTitle); + this._activeClient = (foregroundClientHandle, foregroundClientTitle); } private void ThumbnailViewFocused(IntPtr id) @@ -389,27 +401,31 @@ namespace EveOPreview.Services { this._windowManager.ActivateWindow(view.Id); }) - .ConfigureAwait(true) - .GetAwaiter() - .OnCompleted(() => + .ContinueWith((task) => { + // This code should be executed on UI thread this.SwitchActiveClient(view.Id, view.Title); this.UpdateClientLayouts(); this.RefreshThumbnails(); - }); - - this.UpdateClientLayouts(); + }, TaskScheduler.FromCurrentSynchronizationContext()); } - private void ThumbnailDeactivated(IntPtr id) + private void ThumbnailDeactivated(IntPtr id, bool switchOut) { - if (!this._thumbnailViews.TryGetValue(id, out IThumbnailView view)) + if (switchOut) { - return; + this._windowManager.ActivateWindow(this._externalApplication); } + else + { + if (!this._thumbnailViews.TryGetValue(id, out IThumbnailView view)) + { + return; + } - this._windowManager.MinimizeWindow(view.Id, true); - this.RefreshThumbnails(); + this._windowManager.MinimizeWindow(view.Id, true); + this.RefreshThumbnails(); + } } private async void ThumbnailViewResized(IntPtr id) @@ -562,6 +578,11 @@ namespace EveOPreview.Services private void ApplyClientLayout(IntPtr clientHandle, string clientTitle) { + if (!this._configuration.EnableClientLayoutTracking) + { + return; + } + ClientLayout clientLayout = this._configuration.GetClientLayout(clientTitle); if (clientLayout == null) @@ -569,7 +590,14 @@ namespace EveOPreview.Services return; } - this._windowManager.MoveWindow(clientHandle, clientLayout.X, clientLayout.Y, clientLayout.Width, clientLayout.Height); + if (clientLayout.IsMaximized) + { + this._windowManager.MaximizeWindow(clientHandle); + } + else + { + this._windowManager.MoveWindow(clientHandle, clientLayout.X, clientLayout.Y, clientLayout.Width, clientLayout.Height); + } } private void UpdateClientLayouts() @@ -583,16 +611,17 @@ namespace EveOPreview.Services { IThumbnailView view = entry.Value; (int Left, int Top, int Right, int Bottom) position = this._windowManager.GetWindowPosition(view.Id); - int width = Math.Abs(position.Right - position.Left); int height = Math.Abs(position.Bottom - position.Top); - if (!this.IsValidWindowPosition(position.Left, position.Top, width, height)) + var isMaximized = this._windowManager.IsWindowMaximized(view.Id); + + if (!(isMaximized || this.IsValidWindowPosition(position.Left, position.Top, width, height))) { continue; } - this._configuration.SetClientLayout(view.Title, new ClientLayout(position.Left, position.Top, width, height)); + this._configuration.SetClientLayout(view.Title, new ClientLayout(position.Left, position.Top, width, height, isMaximized)); } } @@ -606,7 +635,7 @@ namespace EveOPreview.Services { if (this._enqueuedLocationChangeNotification.Handle == IntPtr.Zero) { - this._enqueuedLocationChangeNotification = (view.Id, view.Title, activeClientTitle, view.ThumbnailLocation, ThumbnailManager.DefaultLocationChangeNotificationDelay); + this._enqueuedLocationChangeNotification = (view.Id, view.Title, activeClientTitle, view.ThumbnailLocation, ThumbnailManager.DEFAULT_LOCATION_CHANGE_NOTIFICATION_DELAY); return; } @@ -614,12 +643,12 @@ namespace EveOPreview.Services if ((this._enqueuedLocationChangeNotification.Handle == view.Id) && (this._enqueuedLocationChangeNotification.ActiveClient == activeClientTitle)) { - this._enqueuedLocationChangeNotification.Delay = ThumbnailManager.DefaultLocationChangeNotificationDelay; + this._enqueuedLocationChangeNotification.Delay = ThumbnailManager.DEFAULT_LOCATION_CHANGE_NOTIFICATION_DELAY; return; } - this.RaiseThumbnailLocationUpdatedNotification(this._enqueuedLocationChangeNotification.Title, activeClientTitle, this._enqueuedLocationChangeNotification.Location); - this._enqueuedLocationChangeNotification = (view.Id, view.Title, activeClientTitle, view.ThumbnailLocation, ThumbnailManager.DefaultLocationChangeNotificationDelay); + this.RaiseThumbnailLocationUpdatedNotification(this._enqueuedLocationChangeNotification.Title); + this._enqueuedLocationChangeNotification = (view.Id, view.Title, activeClientTitle, view.ThumbnailLocation, ThumbnailManager.DEFAULT_LOCATION_CHANGE_NOTIFICATION_DELAY); } } @@ -648,9 +677,9 @@ namespace EveOPreview.Services } } - private async void RaiseThumbnailLocationUpdatedNotification(string title, string activeClient, Point location) + private async void RaiseThumbnailLocationUpdatedNotification(string title) { - if (string.IsNullOrEmpty(title) || (title == ThumbnailManager.DefaultClientTitle)) + if (string.IsNullOrEmpty(title) || (title == ThumbnailManager.DEFAULT_CLIENT_TITLE)) { return; } @@ -662,15 +691,15 @@ namespace EveOPreview.Services // TODO Move to a service (?) private bool IsManageableThumbnail(IThumbnailView view) { - return view.Title != ThumbnailManager.DefaultClientTitle; + return view.Title != ThumbnailManager.DEFAULT_CLIENT_TITLE; } // Quick sanity check that the window is not minimized - private bool IsValidWindowPosition(int letf, int top, int width, int height) + private bool IsValidWindowPosition(int left, int top, int width, int height) { - return (letf > ThumbnailManager.WindowPositionThresholdLow) && (letf < ThumbnailManager.WindowPositionThresholdHigh) - && (top > ThumbnailManager.WindowPositionThresholdLow) && (top < ThumbnailManager.WindowPositionThresholdHigh) - && (width > ThumbnailManager.WindowSizeThreshold) && (height > ThumbnailManager.WindowSizeThreshold); + return (left > ThumbnailManager.WINDOW_POSITION_THRESHOLD_LOW) && (left < ThumbnailManager.WINDOW_POSITION_THRESHOLD_HIGH) + && (top > ThumbnailManager.WINDOW_POSITION_THRESHOLD_LOW) && (top < ThumbnailManager.WINDOW_POSITION_THRESHOLD_HIGH) + && (width > ThumbnailManager.WINDOW_SIZE_THRESHOLD) && (height > ThumbnailManager.WINDOW_SIZE_THRESHOLD); } } } \ No newline at end of file diff --git a/Eve-O-Preview/Services/Implementation/WindowManager.cs b/Eve-O-Preview/Services/Implementation/WindowManager.cs index a2096f1..80ff23c 100644 --- a/Eve-O-Preview/Services/Implementation/WindowManager.cs +++ b/Eve-O-Preview/Services/Implementation/WindowManager.cs @@ -1,14 +1,23 @@ using System; +using System.Drawing; using System.Runtime.InteropServices; using EveOPreview.Services.Interop; namespace EveOPreview.Services.Implementation { - class WindowManager : IWindowManager + sealed class WindowManager : IWindowManager { + #region Private constants + private const int WINDOW_SIZE_THRESHOLD = 300; + #endregion + public WindowManager() { - this.IsCompositionEnabled = DwmApiNativeMethods.DwmIsCompositionEnabled(); + // Composition is always enabled for Windows 8+ + this.IsCompositionEnabled = + ((Environment.OSVersion.Version.Major == 6) && (Environment.OSVersion.Version.Minor >= 2)) // Win 8 and Win 8.1 + || (Environment.OSVersion.Version.Major >= 10) // Win 10 + || DwmNativeMethods.DwmIsCompositionEnabled(); // In case of Win 7 an API call is requiredWin 7 } public bool IsCompositionEnabled { get; } @@ -26,7 +35,7 @@ namespace EveOPreview.Services.Implementation if ((style & InteropConstants.WS_MINIMIZE) == InteropConstants.WS_MINIMIZE) { - User32NativeMethods.ShowWindowAsync(handle, InteropConstants.SW_SHOWNORMAL); + User32NativeMethods.ShowWindowAsync(handle, InteropConstants.SW_RESTORE); } } @@ -51,6 +60,11 @@ namespace EveOPreview.Services.Implementation User32NativeMethods.MoveWindow(handle, left, top, width, height, true); } + public void MaximizeWindow(IntPtr handle) + { + User32NativeMethods.ShowWindowAsync(handle, InteropConstants.SW_SHOWMAXIMIZED); + } + public (int Left, int Top, int Right, int Bottom) GetWindowPosition(IntPtr handle) { User32NativeMethods.GetWindowRect(handle, out RECT windowRectangle); @@ -58,17 +72,52 @@ namespace EveOPreview.Services.Implementation return (windowRectangle.Left, windowRectangle.Top, windowRectangle.Right, windowRectangle.Bottom); } + public bool IsWindowMaximized(IntPtr handle) + { + return User32NativeMethods.IsZoomed(handle); + } + public bool IsWindowMinimized(IntPtr handle) { return User32NativeMethods.IsIconic(handle); } - public IDwmThumbnail RegisterThumbnail(IntPtr destination, IntPtr source) + public IDwmThumbnail GetLiveThumbnail(IntPtr destination, IntPtr source) { IDwmThumbnail thumbnail = new DwmThumbnail(this); thumbnail.Register(destination, source); return thumbnail; } + + public Image GetStaticThumbnail(IntPtr source) + { + var sourceContext = User32NativeMethods.GetDC(source); + + User32NativeMethods.GetClientRect(source, out RECT windowRect); + + var width = windowRect.Right - windowRect.Left; + var height = windowRect.Bottom - windowRect.Top; + + // Check if there is anything to make thumbnail of + if ((width < WINDOW_SIZE_THRESHOLD) || (height < WINDOW_SIZE_THRESHOLD)) + { + return null; + } + + var destContext = Gdi32NativeMethods.CreateCompatibleDC(sourceContext); + var bitmap = Gdi32NativeMethods.CreateCompatibleBitmap(sourceContext, width, height); + + var oldBitmap = Gdi32NativeMethods.SelectObject(destContext, bitmap); + Gdi32NativeMethods.BitBlt(destContext, 0, 0, width, height, sourceContext, 0, 0, Gdi32NativeMethods.SRCCOPY); + Gdi32NativeMethods.SelectObject(destContext, oldBitmap); + Gdi32NativeMethods.DeleteDC(destContext); + User32NativeMethods.ReleaseDC(source, sourceContext); + + Image image = Image.FromHbitmap(bitmap); + Gdi32NativeMethods.DeleteObject(bitmap); + + return image; + } } } \ No newline at end of file diff --git a/Eve-O-Preview/Services/Interface/IWindowManager.cs b/Eve-O-Preview/Services/Interface/IWindowManager.cs index e482a16..c1ce08d 100644 --- a/Eve-O-Preview/Services/Interface/IWindowManager.cs +++ b/Eve-O-Preview/Services/Interface/IWindowManager.cs @@ -1,4 +1,5 @@ using System; +using System.Drawing; namespace EveOPreview.Services { @@ -7,14 +8,14 @@ namespace EveOPreview.Services bool IsCompositionEnabled { get; } IntPtr GetForegroundWindowHandle(); - void ActivateWindow(IntPtr handle); void MinimizeWindow(IntPtr handle, bool enableAnimation); - void MoveWindow(IntPtr handle, int left, int top, int width, int height); + void MaximizeWindow(IntPtr handle); (int Left, int Top, int Right, int Bottom) GetWindowPosition(IntPtr handle); + bool IsWindowMaximized(IntPtr handle); bool IsWindowMinimized(IntPtr handle); - - IDwmThumbnail RegisterThumbnail(IntPtr destination, IntPtr source); + IDwmThumbnail GetLiveThumbnail(IntPtr destination, IntPtr source); + Image GetStaticThumbnail(IntPtr source); } } \ No newline at end of file diff --git a/Eve-O-Preview/Services/Interface/InteropConstants.cs b/Eve-O-Preview/Services/Interface/InteropConstants.cs index e6df312..5fdd320 100644 --- a/Eve-O-Preview/Services/Interface/InteropConstants.cs +++ b/Eve-O-Preview/Services/Interface/InteropConstants.cs @@ -56,7 +56,7 @@ namespace EveOPreview.Services public const UInt32 WS_EX_OVERLAPPEDWINDOW = (WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE); public const UInt32 WS_EX_PALETTEWINDOW = (WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST); public const UInt32 WS_EX_LAYERED = 0x00080000; - public const UInt32 WS_EX_NOINHERITLAYOUT = 0x00100000; // Disable inheritence of mirroring by children + public const UInt32 WS_EX_NOINHERITLAYOUT = 0x00100000; // Disable inheritance of mirroring by children public const UInt32 WS_EX_LAYOUTRTL = 0x00400000; // Right to left mirroring public const UInt32 WS_EX_COMPOSITED = 0x02000000; public const UInt32 WS_EX_NOACTIVATE = 0x08000000; @@ -78,5 +78,6 @@ namespace EveOPreview.Services public const int SW_SHOWNORMAL = 1; public const int SW_SHOWMINIMIZED = 2; public const int SW_SHOWMAXIMIZED = 3; + public const int SW_RESTORE = 9; } } \ No newline at end of file diff --git a/Eve-O-Preview/Services/Interop/DwmApiNativeMethods.cs b/Eve-O-Preview/Services/Interop/DwmNativeMethods.cs similarity index 95% rename from Eve-O-Preview/Services/Interop/DwmApiNativeMethods.cs rename to Eve-O-Preview/Services/Interop/DwmNativeMethods.cs index 90794e0..5cb7325 100644 --- a/Eve-O-Preview/Services/Interop/DwmApiNativeMethods.cs +++ b/Eve-O-Preview/Services/Interop/DwmNativeMethods.cs @@ -4,7 +4,7 @@ using System.Drawing; namespace EveOPreview.Services.Interop { - static class DwmApiNativeMethods + static class DwmNativeMethods { [DllImport("dwmapi.dll", PreserveSig = false)] public static extern void DwmEnableBlurBehindWindow(IntPtr hWnd, DWM_BLURBEHIND pBlurBehind); diff --git a/Eve-O-Preview/Services/Interop/Gdi32NativeMethods.cs b/Eve-O-Preview/Services/Interop/Gdi32NativeMethods.cs new file mode 100644 index 0000000..f966511 --- /dev/null +++ b/Eve-O-Preview/Services/Interop/Gdi32NativeMethods.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.InteropServices; + +namespace EveOPreview.Services.Interop +{ + static class Gdi32NativeMethods + { + public const int SRCCOPY = 13369376; + + [DllImport("gdi32.dll")] + public static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + public static extern bool DeleteDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int height); + + [DllImport("gdi32.dll")] + public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject); + + [DllImport("gdi32.dll")] + public static extern bool DeleteObject(IntPtr hObject); + + [DllImport("gdi32.dll")] + public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hObjectSource, int nXSrc, int nYSrc, int dwRop); + } +} diff --git a/Eve-O-Preview/Services/Interop/User32NativeMethods.cs b/Eve-O-Preview/Services/Interop/User32NativeMethods.cs index fbde187..6adc97a 100644 --- a/Eve-O-Preview/Services/Interop/User32NativeMethods.cs +++ b/Eve-O-Preview/Services/Interop/User32NativeMethods.cs @@ -24,7 +24,10 @@ namespace EveOPreview.Services.Interop public static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] - public static extern int GetWindowRect(IntPtr hwnd, out RECT rect); + public static extern int GetWindowRect(IntPtr hWnd, out RECT rect); + + [DllImport("user32.dll")] + public static extern bool GetClientRect(IntPtr hWnd, out RECT rect); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] @@ -42,5 +45,14 @@ namespace EveOPreview.Services.Interop [DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern IntPtr GetWindowDC(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern IntPtr GetDC(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hdc); } } \ No newline at end of file diff --git a/Eve-O-Preview/View/Implementation/LiveThumbnailView.cs b/Eve-O-Preview/View/Implementation/LiveThumbnailView.cs new file mode 100644 index 0000000..487d9c1 --- /dev/null +++ b/Eve-O-Preview/View/Implementation/LiveThumbnailView.cs @@ -0,0 +1,60 @@ +using System; +using System.Drawing; +using EveOPreview.Services; + +namespace EveOPreview.View +{ + sealed class LiveThumbnailView : ThumbnailView + { + #region Private fields + private IDwmThumbnail _thumbnail; + private Point _startLocation; + private Point _endLocation; + #endregion + + public LiveThumbnailView(IWindowManager windowManager) + : base(windowManager) + { + this._startLocation = new Point(0, 0); + this._endLocation = new Point(this.ClientSize); + } + + protected override void RefreshThumbnail(bool forceRefresh) + { + // To prevent flickering the old broken thumbnail is removed AFTER the new shiny one is created + IDwmThumbnail obsoleteThumbnail = forceRefresh ? this._thumbnail : null; + + if ((this._thumbnail == null) || forceRefresh) + { + this.RegisterThumbnail(); + } + + obsoleteThumbnail?.Unregister(); + } + + protected override void ResizeThumbnail(int baseWidth, int baseHeight, int highlightWidthTop, int highlightWidthRight, int highlightWidthBottom, int highlightWidthLeft) + { + var left = 0 + highlightWidthLeft; + var top = 0 + highlightWidthTop; + var right = baseWidth - highlightWidthRight; + var bottom = baseHeight - highlightWidthBottom; + + if ((this._startLocation.X == left) && (this._startLocation.Y == top) && (this._endLocation.X == right) && (this._endLocation.Y == bottom)) + { + return; // No update required + } + this._startLocation = new Point(left, top); + this._endLocation = new Point(right, bottom); + + this._thumbnail.Move(left, top, right, bottom); + this._thumbnail.Update(); + } + + private void RegisterThumbnail() + { + this._thumbnail = this.WindowManager.GetLiveThumbnail(this.Handle, this.Id); + this._thumbnail.Move(this._startLocation.X, this._startLocation.Y, this._endLocation.X, this._endLocation.Y); + this._thumbnail.Update(); + } + } +} diff --git a/Eve-O-Preview/View/Implementation/MainForm.Designer.cs b/Eve-O-Preview/View/Implementation/MainForm.Designer.cs index e185e73..084354f 100644 --- a/Eve-O-Preview/View/Implementation/MainForm.Designer.cs +++ b/Eve-O-Preview/View/Implementation/MainForm.Designer.cs @@ -820,7 +820,7 @@ namespace EveOPreview.View DocumentationLinkLabel.Padding = new System.Windows.Forms.Padding(12, 5, 12, 5); DocumentationLinkLabel.Size = new System.Drawing.Size(336, 30); DocumentationLinkLabel.TabIndex = 6; - DocumentationLinkLabel.Text = "For more information visit our forum thread:"; + DocumentationLinkLabel.Text = "For more information visit the forum thread:"; // // DescriptionLabel // diff --git a/Eve-O-Preview/View/Implementation/MainForm.resx b/Eve-O-Preview/View/Implementation/MainForm.resx index 426d527..0537524 100644 --- a/Eve-O-Preview/View/Implementation/MainForm.resx +++ b/Eve-O-Preview/View/Implementation/MainForm.resx @@ -141,12 +141,72 @@ True + + False + + + True + + + True + + + False + + + True + + + False + + + True + + + False + + + True + + + False + + + True + False True + + False + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + True @@ -180,6 +240,39 @@ True + + False + + + True + + + False + + + True + + + False + + + True + + + True + + + True + + + True + + + False + + + True + False @@ -216,6 +309,12 @@ True + + False + + + True + False @@ -225,6 +324,54 @@ True + + False + + + True + + + True + + + True + + + False + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + True @@ -276,6 +423,27 @@ True + + False + + + True + + + True + + + True + + + True + + + True + + + True + True @@ -303,6 +471,21 @@ True + + False + + + True + + + True + + + False + + + True + True @@ -324,6 +507,36 @@ True + + False + + + True + + + False + + + True + + + False + + + True + + + True + + + False + + + True + + + True + False diff --git a/Eve-O-Preview/View/Implementation/StaticThumbnailImage.cs b/Eve-O-Preview/View/Implementation/StaticThumbnailImage.cs new file mode 100644 index 0000000..f5ec500 --- /dev/null +++ b/Eve-O-Preview/View/Implementation/StaticThumbnailImage.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows.Forms; + +namespace EveOPreview.View +{ + sealed class StaticThumbnailImage : PictureBox + { + protected override void WndProc(ref Message m) + { + const int WM_NCHITTEST = 0x0084; + const int HTTRANSPARENT = (-1); + + if (m.Msg == WM_NCHITTEST) + { + m.Result = (IntPtr)HTTRANSPARENT; + } + else + { + base.WndProc(ref m); + } + } + } +} diff --git a/Eve-O-Preview/View/Implementation/StaticThumbnailView.cs b/Eve-O-Preview/View/Implementation/StaticThumbnailView.cs new file mode 100644 index 0000000..c01ebc3 --- /dev/null +++ b/Eve-O-Preview/View/Implementation/StaticThumbnailView.cs @@ -0,0 +1,70 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using EveOPreview.Services; + +namespace EveOPreview.View +{ + sealed class StaticThumbnailView : ThumbnailView + { + #region Private fields + private readonly PictureBox _thumbnail; + #endregion + + public StaticThumbnailView(IWindowManager windowManager) + : base(windowManager) + { + this._thumbnail = new StaticThumbnailImage + { + TabStop = false, + SizeMode = PictureBoxSizeMode.StretchImage, + Location = new Point(0, 0), + Size = new Size(this.ClientSize.Width, this.ClientSize.Height) + }; + this.Controls.Add(this._thumbnail); + } + + protected override void RefreshThumbnail(bool forceRefresh) + { + if (!forceRefresh) + { + return; + } + + var thumbnail = this.WindowManager.GetStaticThumbnail(this.Id); + if (thumbnail != null) + { + var oldImage = this._thumbnail.Image; + this._thumbnail.Image = thumbnail; + oldImage?.Dispose(); + } + } + + protected override void ResizeThumbnail(int baseWidth, int baseHeight, int highlightWidthTop, int highlightWidthRight, int highlightWidthBottom, int highlightWidthLeft) + { + var left = 0 + highlightWidthLeft; + var top = 0 + highlightWidthTop; + if (this.IsLocationUpdateRequired(this._thumbnail.Location, left, top)) + { + this._thumbnail.Location = new Point(left, top); + } + + var width = baseWidth - highlightWidthLeft - highlightWidthRight; + var height = baseHeight - highlightWidthTop - highlightWidthBottom; + if (this.IsSizeUpdateRequired(this._thumbnail.Size, width, height)) + { + this._thumbnail.Size = new Size(width, height); + } + } + + private bool IsLocationUpdateRequired(Point currentLocation, int left, int top) + { + return (currentLocation.X != left) || (currentLocation.Y != top); + } + + private bool IsSizeUpdateRequired(Size currentSize, int width, int height) + { + return (currentSize.Width != width) || (currentSize.Height != height); + } + } +} \ No newline at end of file diff --git a/Eve-O-Preview/View/Implementation/ThumbnailView.Designer.cs b/Eve-O-Preview/View/Implementation/ThumbnailView.Designer.cs index c0540fd..697d424 100644 --- a/Eve-O-Preview/View/Implementation/ThumbnailView.Designer.cs +++ b/Eve-O-Preview/View/Implementation/ThumbnailView.Designer.cs @@ -19,11 +19,12 @@ namespace EveOPreview.View // ThumbnailView // this.AccessibleRole = System.Windows.Forms.AccessibleRole.None; - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; this.BackColor = System.Drawing.Color.Black; + this.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch; this.ClientSize = new System.Drawing.Size(153, 89); this.ControlBox = false; + this.DoubleBuffered = true; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.MaximizeBox = false; this.MinimizeBox = false; diff --git a/Eve-O-Preview/View/Implementation/ThumbnailView.cs b/Eve-O-Preview/View/Implementation/ThumbnailView.cs index 7199707..167b673 100644 --- a/Eve-O-Preview/View/Implementation/ThumbnailView.cs +++ b/Eve-O-Preview/View/Implementation/ThumbnailView.cs @@ -7,26 +7,32 @@ using EveOPreview.UI.Hotkeys; namespace EveOPreview.View { - public partial class ThumbnailView : Form, IThumbnailView + public abstract partial class ThumbnailView : Form, IThumbnailView { #region Private constants - private const int ResizeEventTimeout = 500; + private const int RESIZE_EVENT_TIMEOUT = 500; + private const double OPACITY_THRESHOLD = 0.9; + private const double OPACITY_EPSILON = 0.1; #endregion #region Private fields - private readonly IWindowManager _windowManager; private readonly ThumbnailOverlay _overlay; - private IDwmThumbnail _thumbnail; // Part of the logic (namely current size / position management) // was moved to the view due to the performance reasons private bool _isOverlayVisible; private bool _isTopMost; + private bool _isHighlightEnabled; + private bool _isHighlightRequested; + private int _highlightWidth; + private bool _isLocationChanged; private bool _isSizeChanged; + private bool _isCustomMouseModeActive; - private bool _isHighlightEnabled; - private int _highlightWidth; + + private double _opacity; + private DateTime _suppressResizeEventsTimestamp; private Size _baseZoomSize; private Point _baseZoomLocation; @@ -36,29 +42,34 @@ namespace EveOPreview.View private HotkeyHandler _hotkeyHandler; #endregion - public ThumbnailView(IWindowManager windowManager) + protected ThumbnailView(IWindowManager windowManager) { this.SuppressResizeEvent(); - this._windowManager = windowManager; + this.WindowManager = windowManager; this.IsActive = false; this.IsOverlayEnabled = false; this._isOverlayVisible = false; this._isTopMost = false; + this._isHighlightEnabled = false; + this._isHighlightRequested = false; this._isLocationChanged = true; this._isSizeChanged = true; + this._isCustomMouseModeActive = false; - this._isHighlightEnabled = false; + this._opacity = 0.1; InitializeComponent(); this._overlay = new ThumbnailOverlay(this, this.MouseDown_Handler); } + protected IWindowManager WindowManager { get; } + public IntPtr Id { get; set; } public string Title @@ -104,7 +115,7 @@ namespace EveOPreview.View public Action ThumbnailActivated { get; set; } - public Action ThumbnailDeactivated { get; set; } + public Action ThumbnailDeactivated { get; set; } public new void Show() { @@ -131,12 +142,11 @@ namespace EveOPreview.View base.Hide(); } - public new void Close() + public new virtual void Close() { this.SuppressResizeEvent(); this.IsActive = false; - this._thumbnail?.Unregister(); this._overlay.Close(); base.Close(); } @@ -155,13 +165,33 @@ namespace EveOPreview.View public void SetOpacity(double opacity) { - this.Opacity = opacity; + if (opacity >= OPACITY_THRESHOLD) + { + opacity = 1.0; + } - // Overlay opacity settings - // Of the thumbnail's opacity is almost full then set the overlay's one to - // full. Otherwise set it to half of the thumnail opacity - // Opacity value is stored even if the overlay is not displayed atm - this._overlay.Opacity = this.Opacity > 0.9 ? 1.0 : 1.0 - (1.0 - this.Opacity) / 2; + if (Math.Abs(opacity - this._opacity) < OPACITY_EPSILON) + { + return; + } + + try + { + this.Opacity = opacity; + + // Overlay opacity settings + // Of the thumbnail's opacity is almost full then set the overlay's one to + // full. Otherwise set it to half of the thumbnail opacity + // Opacity value is stored even if the overlay is not displayed atm + this._overlay.Opacity = opacity > 0.8 ? 1.0 : 1.0 - (1.0 - opacity) / 2; + + this._opacity = opacity; + } + catch (Win32Exception) + { + // Something went wrong in WinForms internals + // Opacity will be updated in the next cycle + } } public void SetFrames(bool enable) @@ -177,9 +207,6 @@ namespace EveOPreview.View this.SuppressResizeEvent(); this.FormBorderStyle = style; - - // Notify about possible contents position change - this._isSizeChanged = true; } public void SetTopMost(bool enableTopmost) @@ -198,20 +225,20 @@ namespace EveOPreview.View public void SetHighlight(bool enabled, Color color, int width) { - if (this._isHighlightEnabled == enabled) + if (this._isHighlightRequested == enabled) { return; } if (enabled) { - this._isHighlightEnabled = true; + this._isHighlightRequested = true; this._highlightWidth = width; this.BackColor = color; } else { - this._isHighlightEnabled = false; + this._isHighlightRequested = false; this.BackColor = SystemColors.Control; } @@ -226,12 +253,14 @@ namespace EveOPreview.View int locationX = this.Location.X; int locationY = this.Location.Y; - int newWidth = (zoomFactor * this.ClientSize.Width) + (this.Size.Width - this.ClientSize.Width); - int newHeight = (zoomFactor * this.ClientSize.Height) + (this.Size.Height - this.ClientSize.Height); + int clientSizeWidth = this.ClientSize.Width; + int clientSizeHeight = this.ClientSize.Height; + int newWidth = (zoomFactor * clientSizeWidth) + (this.Size.Width - clientSizeWidth); + int newHeight = (zoomFactor * clientSizeHeight) + (this.Size.Height - clientSizeHeight); // First change size, THEN move the window // Otherwise there is a chance to fail in a loop - // Zoom requied -> Moved the windows 1st -> Focus is lost -> Window is moved back -> Focus is back on -> Zoom required -> ... + // Zoom required -> Moved the windows 1st -> Focus is lost -> Window is moved back -> Focus is back on -> Zoom required -> ... this.MaximumSize = new Size(0, 0); this.Size = new Size(newWidth, newHeight); @@ -287,15 +316,7 @@ namespace EveOPreview.View this._hotkeyHandler = new HotkeyHandler(this.Handle, hotkey); this._hotkeyHandler.Pressed += HotkeyPressed_Handler; - try - { - this._hotkeyHandler.Register(); - } - catch (Exception) - { - // There can be a lot of possible exception reasons here - // In case of any of them the hotkey setting is silently ignored - } + this._hotkeyHandler.Register(); } public void UnregisterHotkey() @@ -313,27 +334,55 @@ namespace EveOPreview.View public void Refresh(bool forceRefresh) { - // To prevent flickering the old broken thumbnail is removed AFTER the new shiny one is created - IDwmThumbnail obsoleteThumbnail = forceRefresh ? this._thumbnail : null; + this.RefreshThumbnail(forceRefresh); + this.HighlightThumbnail(forceRefresh || this._isSizeChanged); + this.RefreshOverlay(forceRefresh || this._isSizeChanged || this._isLocationChanged); - if ((this._thumbnail == null) || forceRefresh) + this._isSizeChanged = false; + } + + protected abstract void RefreshThumbnail(bool forceRefresh); + + protected abstract void ResizeThumbnail(int baseWidth, int baseHeight, int highlightWidthTop, int highlightWidthRight, int highlightWidthBottom, int highlightWidthLeft); + + private void HighlightThumbnail(bool forceRefresh) + { + if (!forceRefresh && (this._isHighlightRequested == this._isHighlightEnabled)) { - this.RegisterThumbnail(); + // Nothing to do here + return; } - bool sizeChanged = this._isSizeChanged || forceRefresh; - bool locationChanged = this._isLocationChanged || forceRefresh; + this._isHighlightEnabled = this._isHighlightRequested; - if (sizeChanged) + int baseWidth = this.ClientSize.Width; + int baseHeight = this.ClientSize.Height; + + if (!this._isHighlightRequested) { - this.RecalculateThumbnailSize(); - - this.UpdateThumbnail(); - - this._isSizeChanged = false; + //No highlighting enabled, so no math required + this.ResizeThumbnail(baseWidth, baseHeight, 0, 0, 0, 0); + return; } - obsoleteThumbnail?.Unregister(); + double baseAspectRatio = ((double)baseWidth) / baseHeight; + + int actualHeight = baseHeight - 2 * this._highlightWidth; + double desiredWidth = actualHeight * baseAspectRatio; + int actualWidth = (int)Math.Round(desiredWidth, MidpointRounding.AwayFromZero); + int highlightWidthLeft = (baseWidth - actualWidth) / 2; + int highlightWidthRight = baseWidth - actualWidth - highlightWidthLeft; + + this.ResizeThumbnail(this.ClientSize.Width, this.ClientSize.Height, this._highlightWidth, highlightWidthRight, this._highlightWidth, highlightWidthLeft); + } + + private void RefreshOverlay(bool forceRefresh) + { + if (this._isOverlayVisible && !forceRefresh) + { + // No need to update anything. Everything is already set up + return; + } this._overlay.EnableOverlayLabel(this.IsOverlayEnabled); @@ -344,14 +393,6 @@ namespace EveOPreview.View this._overlay.Show(); this._isOverlayVisible = true; } - else - { - if (!(sizeChanged || locationChanged)) - { - // No need to adjust in the overlay location if it is already visible and properly set - return; - } - } Size overlaySize = this.ClientSize; Point overlayLocation = this.Location; @@ -366,37 +407,11 @@ namespace EveOPreview.View this._overlay.Refresh(); } - private void RecalculateThumbnailSize() - { - // This approach would work only for square-shaped thumbnail window - // To get PROPER results we have to do some crazy math - //int delta = this._isHighlightEnabled ? this._highlightWidth : 0; - //this._thumbnail.rcDestination = new RECT(0 + delta, 0 + delta, this.ClientSize.Width - delta, this.ClientSize.Height - delta); - if (!this._isHighlightEnabled) - { - //No highlighting enabled, so no odd math required - this._thumbnail.Move(0, 0, this.ClientSize.Width, this.ClientSize.Height); - return; - } - - int baseWidth = this.ClientSize.Width; - int baseHeight = this.ClientSize.Height; - double baseAspectRatio = ((double)baseWidth) / baseHeight; - - int actualHeight = baseHeight - 2 * this._highlightWidth; - double desiredWidth = actualHeight * baseAspectRatio; - int actualWidth = (int)Math.Round(desiredWidth, MidpointRounding.AwayFromZero); - int highlightWidthLeft = (baseWidth - actualWidth) / 2; - int highlightWidthRight = baseWidth - actualWidth - highlightWidthLeft; - - this._thumbnail.Move(0 + highlightWidthLeft, 0 + this._highlightWidth, baseWidth - highlightWidthRight, baseHeight - this._highlightWidth); - } - private void SuppressResizeEvent() { // Workaround for WinForms issue with the Resize event being fired with inconsistent ClientSize value // Any Resize events fired before this timestamp will be ignored - this._suppressResizeEventsTimestamp = DateTime.UtcNow.AddMilliseconds(ThumbnailView.ResizeEventTimeout); + this._suppressResizeEventsTimestamp = DateTime.UtcNow.AddMilliseconds(ThumbnailView.RESIZE_EVENT_TIMEOUT); } #region GUI events @@ -447,7 +462,12 @@ namespace EveOPreview.View { if (Control.ModifierKeys == Keys.Control) { - this.ThumbnailDeactivated?.Invoke(this.Id); + this.ThumbnailDeactivated?.Invoke(this.Id, false); + } + else + if (Control.ModifierKeys == (Keys.Control | Keys.Shift)) + { + this.ThumbnailDeactivated?.Invoke(this.Id, true); } else { @@ -485,20 +505,8 @@ namespace EveOPreview.View } #endregion - #region Thumbnail management - private void RegisterThumbnail() - { - this._thumbnail = this._windowManager.RegisterThumbnail(this.Handle, this.Id); - } - - private void UpdateThumbnail() - { - this._thumbnail.Update(); - } - #endregion - #region Custom Mouse mode - // This pair of methods saves/restores certain window propeties + // This pair of methods saves/restores certain window properties // Methods are used to remove the 'Zoom' effect (if any) when the // custom resize/move mode is activated // Methods are kept on this level because moving to the presenter diff --git a/Eve-O-Preview/View/Implementation/ThumbnailViewFactory.cs b/Eve-O-Preview/View/Implementation/ThumbnailViewFactory.cs index 22c8715..83d271e 100644 --- a/Eve-O-Preview/View/Implementation/ThumbnailViewFactory.cs +++ b/Eve-O-Preview/View/Implementation/ThumbnailViewFactory.cs @@ -1,20 +1,25 @@ using System; using System.Drawing; +using EveOPreview.Configuration; namespace EveOPreview.View { sealed class ThumbnailViewFactory : IThumbnailViewFactory { private readonly IApplicationController _controller; + private readonly bool _isCompatibilityModeEnabled; - public ThumbnailViewFactory(IApplicationController controller) + public ThumbnailViewFactory(IApplicationController controller, IThumbnailConfiguration configuration) { this._controller = controller; + this._isCompatibilityModeEnabled = configuration.EnableCompatibilityMode; } public IThumbnailView Create(IntPtr id, string title, Size size) { - IThumbnailView view = this._controller.Create(); + IThumbnailView view = this._isCompatibilityModeEnabled + ? (IThumbnailView)this._controller.Create() + : (IThumbnailView)this._controller.Create(); view.Id = id; view.Title = title; diff --git a/Eve-O-Preview/View/Interface/IMainFormView.cs b/Eve-O-Preview/View/Interface/IMainFormView.cs index 1c4ad32..cc727ef 100644 --- a/Eve-O-Preview/View/Interface/IMainFormView.cs +++ b/Eve-O-Preview/View/Interface/IMainFormView.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Drawing; -using EveOPreview.UI; namespace EveOPreview.View { diff --git a/Eve-O-Preview/View/Interface/IThumbnailView.cs b/Eve-O-Preview/View/Interface/IThumbnailView.cs index 3d7e593..00cac7d 100644 --- a/Eve-O-Preview/View/Interface/IThumbnailView.cs +++ b/Eve-O-Preview/View/Interface/IThumbnailView.cs @@ -36,6 +36,6 @@ namespace EveOPreview.View Action ThumbnailLostFocus { get; set; } Action ThumbnailActivated { get; set; } - Action ThumbnailDeactivated { get; set; } + Action ThumbnailDeactivated { get; set; } } } \ No newline at end of file diff --git a/Eve-O-Preview/app.manifest b/Eve-O-Preview/app.manifest index 5fc7044..cc914b0 100644 --- a/Eve-O-Preview/app.manifest +++ b/Eve-O-Preview/app.manifest @@ -74,8 +74,8 @@ --> - True/PM - PerMonitorV2 + True + PerMonitor diff --git a/Eve-O-Preview/packages.config b/Eve-O-Preview/packages.config index b04a818..7b4d3e0 100644 --- a/Eve-O-Preview/packages.config +++ b/Eve-O-Preview/packages.config @@ -1,8 +1,9 @@  - - - - - + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 84fbf39..b7d166a 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Mouse gestures are applied to the thumbnail window currently being hovered over. | --- | --- | | Activate the EVE Online client and bring it to front | Click the thumbnail | | Minimize the EVE Online client | Hold Control key and click the thumbnail | +| Switch to the last used application that is not an EVE Online client | Hold Control + Shift keys and click any thumbnail | | Move thumbnail to a new position | Press right mouse button and move the mouse | | Adjust thumbnail height | Press both left and right mouse buttons and move the mouse up or down | | Adjust thumbnail width | Press both left and right mouse buttons and move the mouse left or right | @@ -90,6 +91,7 @@ Some of the application options are not exposed in the GUI. They can be adjusted | Option | Description | | --- | --- | | **ActiveClientHighlightThickness** | Thickness of the border used to highlight the active client's thumbnail.
Allowed values are **1**...**6**.
The default value is **3**
For example: **"ActiveClientHighlightThickness": 3** | +| **CompatibilityMode** | Enables the alternative render mode (see below)
The default value is **false**
For example: **"CompatibilityMode": true** | | **EnableThumbnailSnap** | Allows to disable thumbnails snap feature by setting its value to **false**
The default value is **true**
For example: **"EnableThumbnailSnap": true** | | **PriorityClients** | Allows to set a list of clients that are not auto-minimized on inactivity even if the **Minimize inactive EVE clients** option is enabled. Listed clients still can be minimized using Windows hotkeys or via _Ctrl+Click_ on the corresponding thumbnail
The default value is empty list **[]**
For example: **"PriorityClients": [ "EVE - Phrynohyas Tig-Rah", "EVE - Ondatra Patrouette" ]** | | **ThumbnailMinimumSize** | Minimum thumbnail size that can be set either via GUI or by resizing a thumbnail window. Value is written in the form "width, height"
The default value is **"100, 80"**.
For example: **"ThumbnailMinimumSize": "100, 80"** | @@ -123,6 +125,13 @@ The following hotkey is described as `modifier+key` where `modifier` can be **Co **Note:** Do not set hotkeys to use the key combinations already used by EVE. It won't work as "_I set hotkey for my DPS char to F1 and when I'll press F1 it will automatically open the DPS char's window and activate guns_". Key combination will be swallowed by EVE-O Preview and NOT retranslated to EVE window. So it will be only "_it will automatically open the DPS char's window_". +## Compatibility Mode + +This setting allows to enable an alternate thumbnail render. This render doesn't use advanced DWM API to create live previews. Instead it is a screenshot-based render with the following pros and cons: +* `+` Should work even in remote desktop environments +* `-` Consumes significantly more memory. In the testing environment EVE-O Preview did consume around 180 MB to manage 3 thumbnails using this render. At the same time the primary render did consume around 50 MB when run in the same environment. +* `-` Thumbnail images are refreshed at 1 FPS rate +* `-` Possible short mouse cursor freezes ---