Files
QuickLook/QuickLook/ViewerWindow.Actions.cs
2026-02-13 04:05:17 +00:00

450 lines
15 KiB
C#

// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using QuickLook.Common.Helpers;
using QuickLook.Common.Plugin;
using QuickLook.Common.Plugin.MoreMenu;
using QuickLook.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Wpf.Ui.Controls;
using MenuItem = System.Windows.Controls.MenuItem;
namespace QuickLook;
public partial class ViewerWindow
{
internal void Run()
{
if (string.IsNullOrEmpty(_path))
return;
try
{
using var _ = Process.Start(new ProcessStartInfo(_path)
{
WorkingDirectory = Path.GetDirectoryName(_path)
});
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
}
}
internal void RunAndClose()
{
Run();
Close();
}
internal void ToggleFullscreen()
{
if (_isFullscreen)
{
// Exit fullscreen
_isFullscreen = false;
// Restore window properties
WindowState = _preFullscreenWindowState;
WindowStyle = _preFullscreenWindowStyle;
ResizeMode = _preFullscreenResizeMode;
// Restore position and size
Left = _preFullscreenBounds.Left;
Top = _preFullscreenBounds.Top;
Width = _preFullscreenBounds.Width;
Height = _preFullscreenBounds.Height;
}
else
{
// Enter fullscreen
_isFullscreen = true;
// Save current window properties
_preFullscreenWindowState = WindowState;
_preFullscreenWindowStyle = WindowStyle;
_preFullscreenResizeMode = ResizeMode;
_preFullscreenBounds = new Rect(Left, Top, Width, Height);
// Set to normal state first to get proper bounds
WindowState = WindowState.Normal;
// Hide window chrome for true fullscreen
WindowStyle = WindowStyle.None;
ResizeMode = ResizeMode.NoResize;
// Get the screen bounds where the window is currently located
var screen = System.Windows.Forms.Screen.FromHandle(new System.Windows.Interop.WindowInteropHelper(this).Handle);
// Set window to cover the entire screen
Left = screen.Bounds.Left;
Top = screen.Bounds.Top;
Width = screen.Bounds.Width;
Height = screen.Bounds.Height;
// Ensure the window is on top
WindowState = WindowState.Maximized;
}
}
private void PositionWindow(Size size)
{
// If the window is now maximized, do not move it
if (WindowState == WindowState.Maximized)
return;
size = new Size(Math.Max(MinWidth, size.Width), Math.Max(MinHeight, size.Height));
var newRect = IsLoaded ? ResizeAndCentreExistingWindow(size) : ResizeAndCentreNewWindow(size);
this.MoveWindow(newRect.Left, newRect.Top, newRect.Width, newRect.Height);
}
private Rect ResizeAndCentreExistingWindow(Size size)
{
// Align window just like in macOS ...
//
// |10%| 80% |10%|
// |---|-----------|---|---
// |TL | T |TR |10%
// |---|-----------|---|---
// | | | |
// |L | C | R |80%
// | | | |
// |---|-----------|---|---
// |LB | B |RB |10%
// |---|-----------|---|---
var scale = DisplayDeviceHelper.GetScaleFactorFromWindow(this);
var limitPercentX = 0.1 * scale.Horizontal;
var limitPercentY = 0.1 * scale.Vertical;
// Use absolute pixels for calculation
var pxSize = new Size(scale.Horizontal * size.Width, scale.Vertical * size.Height);
var pxOldRect = this.GetWindowRectInPixel();
// Scale to new size, maintain centre
var pxNewRect = Rect.Inflate(pxOldRect,
(pxSize.Width - pxOldRect.Width) / 2,
(pxSize.Height - pxOldRect.Height) / 2);
var desktopRect = WindowHelper.GetDesktopRectFromWindowInPixel(this);
var leftLimit = desktopRect.Left + desktopRect.Width * limitPercentX;
var rightLimit = desktopRect.Right - desktopRect.Width * limitPercentX;
var topLimit = desktopRect.Top + desktopRect.Height * limitPercentY;
var bottomLimit = desktopRect.Bottom - desktopRect.Height * limitPercentY;
if (pxOldRect.Left < leftLimit && pxOldRect.Right < rightLimit) // L
pxNewRect.Location = new Point(Math.Max(pxOldRect.Left, desktopRect.Left), pxNewRect.Top);
else if (pxOldRect.Left > leftLimit && pxOldRect.Right > rightLimit) // R
pxNewRect.Location = new Point(Math.Min(pxOldRect.Right, desktopRect.Right) - pxNewRect.Width, pxNewRect.Top);
else // C, fix window boundary
pxNewRect.Offset(
Math.Max(0, desktopRect.Left - pxNewRect.Left) + Math.Min(0, desktopRect.Right - pxNewRect.Right), 0);
if (pxOldRect.Top < topLimit && pxOldRect.Bottom < bottomLimit) // T
pxNewRect.Location = new Point(pxNewRect.Left, Math.Max(pxOldRect.Top, desktopRect.Top));
else if (pxOldRect.Top > topLimit && pxOldRect.Bottom > bottomLimit) // B
pxNewRect.Location = new Point(pxNewRect.Left,
Math.Min(pxOldRect.Bottom, desktopRect.Bottom) - pxNewRect.Height);
else // C, fix window boundary
pxNewRect.Offset(0,
Math.Max(0, desktopRect.Top - pxNewRect.Top) + Math.Min(0, desktopRect.Bottom - pxNewRect.Bottom));
// Return absolute location and relative size
return new Rect(pxNewRect.Location, size);
}
private Rect ResizeAndCentreNewWindow(Size size)
{
var desktopRect = WindowHelper.GetCurrentDesktopRectInPixel();
var scale = DisplayDeviceHelper.GetCurrentScaleFactor();
var pxSize = new Size(scale.Horizontal * size.Width, scale.Vertical * size.Height);
var pxLocation = new Point(
desktopRect.X + (desktopRect.Width - pxSize.Width) / 2,
desktopRect.Y + (desktopRect.Height - pxSize.Height) / 2);
// Return absolute location and relative size
return new Rect(pxLocation, size);
}
internal void UnloadPlugin()
{
// The focused element will not processed by GC: https://stackoverflow.com/questions/30848939/memory-leak-due-to-window-efectivevalues-retention
FocusManager.SetFocusedElement(this, null);
Keyboard.DefaultRestoreFocusMode =
RestoreFocusMode.None; // WPF will put the focused item into a "_restoreFocus" list ... omg
Keyboard.ClearFocus();
_canOldPluginResize = ContextObject.CanResize;
ContextObject.Reset();
try
{
Plugin?.Cleanup();
}
catch (Exception e)
{
Debug.WriteLine(e);
}
if (_autoReloadWatcher != null)
{
_autoReloadWatcher.EnableRaisingEvents = false;
_autoReloadWatcher.Dispose();
_autoReloadWatcher = null;
}
Plugin = null;
_path = string.Empty;
}
internal void BeginShow(IViewer matchedPlugin, string path,
Action<string, ExceptionDispatchInfo> exceptionHandler)
{
_path = path;
Plugin = matchedPlugin;
ContextObject.Reset();
// Assign monitor color profile
ContextObject.ColorProfileName = DisplayDeviceHelper.GetMonitorColorProfileFromWindow(this);
// Get window size before showing it
try
{
Plugin.Prepare(path, ContextObject);
}
catch (Exception e)
{
exceptionHandler(path, ExceptionDispatchInfo.Capture(e));
return;
}
if (ContextObject.IsBlocked)
{
ContextObject.ViewerContent = new System.Windows.Controls.TextBlock
{
Text = TranslationHelper.Get("MW_FileBlocked", failsafe: "This file type is blocked."),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 14,
};
ContextObject.IsBusy = false;
return;
}
SetOpenWithButtonAndPath();
// Revert UI changes
ContextObject.IsBusy = true;
var newHeight = ContextObject.PreferredSize.Height + BorderThickness.Top + BorderThickness.Bottom +
(ContextObject.TitlebarOverlap ? 0 : windowCaptionContainer.Height);
var newWidth = ContextObject.PreferredSize.Width + BorderThickness.Left + BorderThickness.Right;
var newSize = new Size(newWidth, newHeight);
// If use has adjusted the window size, keep it
if (_customWindowSize != Size.Empty)
newSize = _customWindowSize;
else
_ignoreNextWindowSizeChange = true;
PositionWindow(newSize);
if (!IsVisible)
{
Dispatcher.BeginInvoke(new Action(() => this.BringToFront(Topmost)), DispatcherPriority.Render);
Show();
}
if (_autoReload && File.Exists(path))
{
_autoReloadWatcher?.Dispose();
_autoReloadWatcher = new FileSystemWatcher(Path.GetDirectoryName(path), Path.GetFileName(path))
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
};
_autoReloadWatcher.Changed += (_, _) =>
// Executed asynchronously to avoid deadlock
Dispatcher.BeginInvoke(() => ViewWindowManager.GetInstance().ReloadPreview());
_autoReloadWatcher.EnableRaisingEvents = true;
}
// Load plugin, do not block UI
Dispatcher.BeginInvoke(() =>
{
try
{
Plugin.View(path, ContextObject);
// Initial the more menu
ClearMoreMenuUnpin();
foreach (var plugin in
PluginManager.GetInstance().LoadedPlugins
.GroupBy(x => x.ToString()).Select(g => g.First()) // DistinctBy plugin name
.OrderBy(p => p.Priority)) // OrderBy plugin priority
{
if (plugin.ToString() == Plugin.ToString())
{
if (Plugin is IMoreMenu moreMenu && moreMenu.MenuItems is not null)
{
InsertMoreMenu(moreMenu.MenuItems);
}
continue;
}
else
{
if (plugin is IMoreMenuExtended moreMenu && moreMenu.MenuItems is not null)
{
InsertMoreMenu(moreMenu.MenuItems);
}
}
}
}
catch (Exception e)
{
exceptionHandler(path, ExceptionDispatchInfo.Capture(e));
}
}, DispatcherPriority.Input);
}
private void ClearMoreMenuUnpin()
{
var toRemove = buttonMore.ContextMenu.Items
.OfType<FrameworkElement>() // MenuItem and Separator
.Where(item => item.Tag is not "PinMenu")
.ToArray();
foreach (var item in toRemove)
{
buttonMore.ContextMenu.Items.Remove(item);
}
}
private void InsertMoreMenu(IEnumerable<IMenuItem> moreMenu)
{
int count = 0;
foreach (IMenuItem item in moreMenu)
{
if (item is null) continue;
if (!item.IsSeparator)
{
MenuItem menuItem = new()
{
Header = item.Header,
Icon = ResolveIcon(item.Icon),
Visibility = item.IsVisible ? Visibility.Visible : Visibility.Collapsed,
Command = item.Command,
};
buttonMore.ContextMenu.Items.Insert(count++, menuItem);
}
else
{
buttonMore.ContextMenu.Items.Insert(count++, new Separator());
}
}
if (moreMenu.Any())
{
buttonMore.ContextMenu.Items.Insert(count++, new Separator());
}
}
private object ResolveIcon(object icon)
{
if (icon is string glyph)
{
return new FontIcon()
{
FontFamily = (FontFamily)Application.Current.Resources["SymbolThemeFontFamily"],
Glyph = glyph,
};
}
else if (icon is UIElement)
{
return icon;
}
else
{
// Not supported yet
}
return null;
}
private void SetOpenWithButtonAndPath()
{
// Share icon
buttonShare.Visibility = ShareHelper.IsShareSupported(_path) ? Visibility.Visible : Visibility.Collapsed;
// Open icon
if (Directory.Exists(_path))
{
buttonOpen.ToolTip = string.Format(TranslationHelper.Get("MW_BrowseFolder"), Path.GetFileName(_path));
return;
}
var isExe = FileHelper.IsExecutable(_path, out var appFriendlyName);
if (isExe)
{
buttonOpen.ToolTip = string.Format(TranslationHelper.Get("MW_Run"), appFriendlyName);
return;
}
// Not an exe
var found = FileHelper.GetAssocApplication(_path, out appFriendlyName);
if (found)
{
buttonOpen.ToolTip = string.Format(TranslationHelper.Get("MW_OpenWith"), appFriendlyName);
return;
}
// Assoc not found
buttonOpen.ToolTip = string.Format(TranslationHelper.Get("MW_Open"), Path.GetFileName(_path));
}
protected override void OnClosing(CancelEventArgs e)
{
UnloadPlugin();
busyDecorator.Dispose();
base.OnClosing(e);
ProcessHelper.PerformAggressiveGC();
}
}