Files
eveo/Eve-O-Preview/UI/Implementation/ThumbnailView.cs

524 lines
14 KiB
C#

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace EveOPreview.UI
{
public partial class ThumbnailView : Form, IThumbnailView
{
#region Private fields
private readonly ThumbnailOverlay _overlay;
// Part of the logic (namely current size / position management)
// was moved to the view due to the performance reasons
private bool _isThumbnailSetUp;
private bool _isOverlayVisible;
private bool _isTopMost;
private bool _isPositionChanged;
private bool _isSizeChanged;
private bool _isCustomMouseModeActive;
private DateTime _suppressResizeEventsTimestamp;
private DWM_THUMBNAIL_PROPERTIES _thumbnail;
private IntPtr _thumbnailHandle;
private Size _baseZoomSize;
private Point _baseZoomLocation;
private Point _baseMousePosition;
private Size _baseZoomMaximumSize;
private HotkeyHandler _hotkeyHandler;
#endregion
public ThumbnailView()
{
this.IsEnabled = true;
this.IsActive = false;
this.IsOverlayEnabled = false;
this._isThumbnailSetUp = false;
this._isOverlayVisible = false;
this._isTopMost = false;
this._isPositionChanged = true;
this._isSizeChanged = true;
this._isCustomMouseModeActive = false;
this._suppressResizeEventsTimestamp = DateTime.UtcNow;
InitializeComponent();
this._overlay = new ThumbnailOverlay(this, this.MouseDown_Handler);
}
public IntPtr Id { get; set; }
public string Title
{
get
{
return this.Text;
}
set
{
this.Text = value;
this._overlay.SetOverlayLabel(value);
}
}
public bool IsEnabled { get; set; }
public bool IsActive { get; set; }
public bool IsOverlayEnabled { get; set; }
public Point ThumbnailLocation
{
get
{
return this.Location;
}
set
{
if ((value.X > 0) || (value.Y > 0))
{
this.StartPosition = FormStartPosition.Manual;
}
this.Location = value;
}
}
public Size ThumbnailSize
{
get
{
return this.ClientSize;
}
set
{
this.ClientSize = value;
}
}
public Action<IntPtr> ThumbnailResized { get; set; }
public Action<IntPtr> ThumbnailMoved { get; set; }
public Action<IntPtr> ThumbnailFocused { get; set; }
public Action<IntPtr> ThumbnailLostFocus { get; set; }
public Action<IntPtr> ThumbnailActivated { get; set; }
public new void Show()
{
base.Show();
this._isPositionChanged = true;
this._isSizeChanged = true;
this._isOverlayVisible = false;
// Thumbnail will be properly registered during the Manager's Refresh cycle
this.Refresh();
this.IsActive = true;
}
public new void Hide()
{
this.IsActive = false;
this._overlay.Hide();
base.Hide();
}
public new void Close()
{
this.IsActive = false;
this.UnregisterThumbnail(this._thumbnailHandle);
this._overlay.Close();
base.Close();
}
// This method is used to determine if the provided Handle is related to client or its thumbnail
public bool IsKnownHandle(IntPtr handle)
{
return (this.Id == handle) || (this.Handle == handle) || (this._overlay.Handle == handle);
}
public void SetSizeLimitations(Size minimumSize, Size maximumSize)
{
this.MinimumSize = minimumSize;
this.MaximumSize = maximumSize;
}
public void SetOpacity(double opacity)
{
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 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;
}
public void SetFrames(bool enable)
{
FormBorderStyle style = enable ? FormBorderStyle.SizableToolWindow : FormBorderStyle.None;
// No need to change the borders style if it is ALREADY correct
if (this.FormBorderStyle == style)
{
return;
}
// Fix 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(450);
this.FormBorderStyle = style;
// Notify about possible contents position change
this._isSizeChanged = true;
}
public void SetTopMost(bool enableTopmost)
{
// IMO WinForms could check this too
if (this._isTopMost == enableTopmost)
{
return;
}
this.TopMost = enableTopmost;
this._overlay.TopMost = enableTopmost;
this._isTopMost = enableTopmost;
}
public void SetHighlight(bool enabled, Color color)
{
this._overlay.EnableHighlight(enabled, color);
}
public void ZoomIn(ViewZoomAnchor anchor, int zoomFactor)
{
int oldWidth = this._baseZoomSize.Width;
int oldHeight = this._baseZoomSize.Height;
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);
// 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 -> ...
this.MaximumSize = new Size(0, 0);
this.Size = new Size(newWidth, newHeight);
switch (anchor)
{
case ViewZoomAnchor.NW:
break;
case ViewZoomAnchor.N:
this.Location = new Point(locationX - newWidth / 2 + oldWidth / 2, locationY);
break;
case ViewZoomAnchor.NE:
this.Location = new Point(locationX - newWidth + oldWidth, locationY);
break;
case ViewZoomAnchor.W:
this.Location = new Point(locationX, locationY - newHeight / 2 + oldHeight / 2);
break;
case ViewZoomAnchor.C:
this.Location = new Point(locationX - newWidth / 2 + oldWidth / 2, locationY - newHeight / 2 + oldHeight / 2);
break;
case ViewZoomAnchor.E:
this.Location = new Point(locationX - newWidth + oldWidth, locationY - newHeight / 2 + oldHeight / 2);
break;
case ViewZoomAnchor.SW:
this.Location = new Point(locationX, locationY - newHeight + this._baseZoomSize.Height);
break;
case ViewZoomAnchor.S:
this.Location = new Point(locationX - newWidth / 2 + oldWidth / 2, locationY - newHeight + oldHeight);
break;
case ViewZoomAnchor.SE:
this.Location = new Point(locationX - newWidth + oldWidth, locationY - newHeight + oldHeight);
break;
}
}
public void ZoomOut()
{
this.RestoreWindowSizeAndLocation();
}
public void RegisterHotkey(Keys hotkey)
{
if (this._hotkeyHandler != null)
{
this.UnregisterHotkey();
}
if (hotkey == Keys.None)
{
return;
}
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
}
}
public void UnregisterHotkey()
{
if (this._hotkeyHandler == null)
{
return;
}
this._hotkeyHandler.Unregister();
this._hotkeyHandler.Pressed -= HotkeyPressed_Handler;
this._hotkeyHandler.Dispose();
this._hotkeyHandler = null;
}
public void Refresh(bool forceRefresh)
{
// To prevent flickering the old broken thumbnail is removed AFTER the new shiny one is created
IntPtr obsoleteThumbnailHanlde = forceRefresh ? this._thumbnailHandle : IntPtr.Zero;
if ((this._isThumbnailSetUp == false) || forceRefresh)
{
this.RegisterThumbnail();
}
bool sizeChanged = this._isSizeChanged || forceRefresh;
bool locationChanged = this._isPositionChanged || forceRefresh;
if (sizeChanged)
{
this._thumbnail.rcDestination = new RECT(0, 0, this.ClientSize.Width, this.ClientSize.Height);
try
{
WindowManagerNativeMethods.DwmUpdateThumbnailProperties(this._thumbnailHandle, this._thumbnail);
}
catch (ArgumentException)
{
//This exception will be thrown if the EVE client disappears while this method is running
}
this._isSizeChanged = false;
}
if (obsoleteThumbnailHanlde != IntPtr.Zero)
{
this.UnregisterThumbnail(obsoleteThumbnailHanlde);
}
this._overlay.EnableOverlayLabel(this.IsOverlayEnabled);
if (!this._isOverlayVisible)
{
// One-time action to show the Overlay before it is set up
// Otherwise its position won't be set
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;
overlayLocation.X += (this.Size.Width - this.ClientSize.Width) / 2;
overlayLocation.Y += (this.Size.Height - this.ClientSize.Height) - (this.Size.Width - this.ClientSize.Width) / 2;
this._isPositionChanged = false;
this._overlay.Size = overlaySize;
this._overlay.Location = overlayLocation;
this._overlay.Refresh();
}
#region GUI events
protected override CreateParams CreateParams
{
get
{
var Params = base.CreateParams;
Params.ExStyle |= (int)WindowManagerNativeMethods.WS_EX_TOOLWINDOW;
return Params;
}
}
private void Move_Handler(object sender, EventArgs e)
{
this._isPositionChanged = true;
this.ThumbnailMoved?.Invoke(this.Id);
}
private void Resize_Handler(object sender, EventArgs e)
{
if (DateTime.UtcNow < this._suppressResizeEventsTimestamp)
{
return;
}
this._isSizeChanged = true;
this.ThumbnailResized?.Invoke(this.Id);
}
private void MouseEnter_Handler(object sender, EventArgs e)
{
this.ExitCustomMouseMode();
this.SaveWindowSizeAndLocation();
this.ThumbnailFocused?.Invoke(this.Id);
}
private void MouseLeave_Handler(object sender, EventArgs e)
{
this.ThumbnailLostFocus?.Invoke(this.Id);
}
private void MouseDown_Handler(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
this.ThumbnailActivated?.Invoke(this.Id);
}
if ((e.Button == MouseButtons.Right) || (e.Button == (MouseButtons.Left | MouseButtons.Right)))
{
this.EnterCustomMouseMode();
}
}
private void MouseMove_Handler(object sender, MouseEventArgs e)
{
if (this._isCustomMouseModeActive)
{
this.ProcessCustomMouseMode(e.Button.HasFlag(MouseButtons.Left), e.Button.HasFlag(MouseButtons.Right));
}
}
private void MouseUp_Handler(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
this.ExitCustomMouseMode();
}
}
private void HotkeyPressed_Handler(object sender, HandledEventArgs e)
{
this.ThumbnailActivated?.Invoke(this.Id);
e.Handled = true;
}
#endregion
#region Thumbnail management
private void RegisterThumbnail()
{
this._thumbnailHandle = WindowManagerNativeMethods.DwmRegisterThumbnail(this.Handle, this.Id);
this._thumbnail = new DWM_THUMBNAIL_PROPERTIES();
this._thumbnail.dwFlags = DWM_TNP_CONSTANTS.DWM_TNP_VISIBLE
+ DWM_TNP_CONSTANTS.DWM_TNP_OPACITY
+ DWM_TNP_CONSTANTS.DWM_TNP_RECTDESTINATION
+ DWM_TNP_CONSTANTS.DWM_TNP_SOURCECLIENTAREAONLY;
this._thumbnail.opacity = 255;
this._thumbnail.fVisible = true;
this._thumbnail.fSourceClientAreaOnly = true;
this._isThumbnailSetUp = true;
}
private void UnregisterThumbnail(IntPtr thumbnailHandle)
{
try
{
WindowManagerNativeMethods.DwmUnregisterThumbnail(thumbnailHandle);
}
catch (ArgumentException)
{
}
}
#endregion
#region Custom Mouse mode
// This pair of methods saves/restores certain window propeties
// 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
// the code that responds to the mouse events like movement
// seems like a huge overkill
private void SaveWindowSizeAndLocation()
{
this._baseZoomSize = this.Size;
this._baseZoomLocation = this.Location;
this._baseZoomMaximumSize = this.MaximumSize;
}
private void RestoreWindowSizeAndLocation()
{
this.Size = this._baseZoomSize;
this.MaximumSize = this._baseZoomMaximumSize;
this.Location = this._baseZoomLocation;
}
private void EnterCustomMouseMode()
{
this.RestoreWindowSizeAndLocation();
this._isCustomMouseModeActive = true;
this._baseMousePosition = Control.MousePosition;
}
private void ProcessCustomMouseMode(bool leftButton, bool rightButton)
{
Point mousePosition = Control.MousePosition;
int offsetX = mousePosition.X - this._baseMousePosition.X;
int offsetY = mousePosition.Y - this._baseMousePosition.Y;
this._baseMousePosition = mousePosition;
// Left + Right buttons trigger thumbnail resize
// Right button only trigger thumbnail movement
if (leftButton && rightButton)
{
this.Size = new Size(this.Size.Width + offsetX, this.Size.Height + offsetY);
this._baseZoomSize = this.Size;
}
else
{
this.Location = new Point(this.Location.X + offsetX, this.Location.Y + offsetY);
this._baseZoomLocation = this.Location;
}
}
private void ExitCustomMouseMode()
{
this._isCustomMouseModeActive = false;
}
#endregion
}
}