Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion QuickLook.Common/Helpers/DisplayDeviceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,46 @@ public static ScaleFactor GetScaleFactorFromWindow(Window window)

public static ScaleFactor GetCurrentScaleFactor()
{
return GetScaleFactorFromWindow(GetForegroundWindow());
// Use the cursor position rather than the foreground window to determine the monitor. The
// foreground window is the desktop (Progman) when a file sits on the desktop, and that always
// resolves to the primary monitor, which would place the preview on the wrong screen.
GetCursorPos(out var pt);
return GetScaleFactorFromPoint(pt);
}

public static ScaleFactor GetScaleFactorFromPoint(POINT point)
{
var dpiX = DefaultDpi;
var dpiY = DefaultDpi;

try
{
if (Environment.OSVersion.Version > new Version(6, 2)) // Windows 8.1 = 6.3.9200
{
var hMonitor = MonitorFromPoint(point, MonitorDefaults.TONEAREST);
GetDpiForMonitor(hMonitor, MonitorDpiType.EFFECTIVE_DPI, out dpiX, out dpiY);
}
else
{
using var g = Graphics.FromHwnd(IntPtr.Zero);
var desktop = g.GetHdc();
try
{
dpiX = GetDeviceCaps(desktop, DeviceCap.LOGPIXELSX);
dpiY = GetDeviceCaps(desktop, DeviceCap.LOGPIXELSY);
}
finally
{
g.ReleaseHdc(desktop);
}
}
}
catch (Exception e)
{
ProcessHelper.WriteLog(e.ToString());
}

return new ScaleFactor { Horizontal = (float)dpiX / DefaultDpi, Vertical = (float)dpiY / DefaultDpi };
}

public static ScaleFactor GetScaleFactorFromWindow(nint hwnd)
Expand Down
41 changes: 37 additions & 4 deletions QuickLook.Common/Helpers/WindowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ public static Size GetCurrentDesktopSize()

public static Rect GetCurrentDesktopRectInPixel()
{
return GetDesktopRectFromWindowInPixel(User32.GetForegroundWindow());
// Use the cursor position to pick the monitor. The foreground window resolves to the
// desktop (Progman) when a file sits on the desktop, which always maps to the primary
// monitor; that would place the preview on the wrong screen.
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var area = screen.WorkingArea;
return new Rect(new Point(area.X, area.Y), new Size(area.Width, area.Height));
}

public static Rect GetDesktopRectFromWindowInPixel(Window window)
Expand Down Expand Up @@ -88,7 +93,35 @@ public static void MoveWindow(this Window window,
out var pxWidth, out var pxHeight);

// Use absolute location and relative size. WPF will scale the size to the target display
User32.MoveWindow(handle, (int)Math.Round(pxLeft), (int)Math.Round(pxTop), pxWidth, pxHeight, true);
//
// Guard against arithmetic overflow in WindowChromeWorker.HandleNCHitTest (net462).
// When the window is on a per-monitor DPI display the values here are physical pixels
// that may be NaN or outside the int32 range (e.g. a window straddling a negative-
// coordinate monitor). Feeding such a rect to User32/WPF lets the (int) casts inside
// Win32.MoveWindow and WindowChrome hit-testing throw OverflowException, so clamp them.
var x = ToInt32Clamped(pxLeft);
var y = ToInt32Clamped(pxTop);
// Keep the physical window rect strictly larger than the invisible resize border and
// caption. If it ever shrinks to zero/smaller, WindowChromeWorker._HandleNCHitTest
// (net462) computes a degenerate rect that throws OverflowException on WM_NCHITTEST.
var w = Math.Max(ToInt32Clamped(pxWidth), 24);
var h = Math.Max(ToInt32Clamped(pxHeight), 56);

User32.MoveWindow(handle, x, y, w, h, true);
}

private static int ToInt32Clamped(double value)
{
// Math.Round on NaN returns NaN; (int)NaN in a checked context throws OverflowException.
if (double.IsNaN(value))
return 0;

if (value <= int.MinValue)
return int.MinValue;
if (value >= int.MaxValue)
return int.MaxValue;

return (int)Math.Round(value);
}

public static Rect GetWindowRectInPixel(this Window window)
Expand Down Expand Up @@ -116,8 +149,8 @@ private static void TransformToPixels(this Visual visual,
matrix = src.CompositionTarget.TransformToDevice;
}

pixelX = (int)Math.Round(matrix.M11 * unitX);
pixelY = (int)Math.Round(matrix.M22 * unitY);
pixelX = ToInt32Clamped(matrix.M11 * unitX);
pixelY = ToInt32Clamped(matrix.M22 * unitY);
}

public static bool IsForegroundWindowBelongToSelf()
Expand Down
13 changes: 13 additions & 0 deletions QuickLook.Common/NativeMethods/User32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public static extern int SetWindowCompositionAttribute(nint hwnd,
[DllImport("user32.dll")]
public static extern nint MonitorFromWindow(nint hWnd, MonitorDefaults dwFlags);

[DllImport("user32.dll")]
public static extern nint MonitorFromPoint(POINT pt, MonitorDefaults dwFlags);

[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);

[DllImport("user32.dll")]
public extern static bool GetMonitorInfo(nint hMonitor, ref MONITORINFOEX lpmi);

Expand Down Expand Up @@ -145,6 +151,13 @@ public struct RECT
public int Bottom;
}

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}

[StructLayout(LayoutKind.Sequential)]
public struct MONITORINFOEX
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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 System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace QuickLook.Plugin.MediaInfoViewer;

/// <summary>
/// Renders MediaInfo output as a two-column table (field label | value) instead of a fixed-width
/// text block. The label column width is shared across all rows (Grid SharedSizeGroup), so every
/// colon-like separator lines up pixel-perfectly regardless of CJK/ASCII/full-width mixing, which
/// padding with spaces can never achieve in a proportional/font-fallback situation.
/// </summary>
public class MediaInfoTablePanel : UserControl
{
private const string ColumnSeparator = " : ";

private readonly StackPanel _stack;
private string _plainText = string.Empty;

public string Text
{
get => _plainText;
set
{
_plainText = value ?? string.Empty;
Rebuild();
}
}

public MediaInfoTablePanel(string text, ContextObject context)
{
_ = context;

_stack = new StackPanel();
Grid.SetIsSharedSizeScope(_stack, true);

var scroll = new ScrollViewer
{
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
PanningMode = PanningMode.VerticalFirst,
Content = _stack,
};

// UserControl does not render its Background by default; wrap it in a Border that mirrors it.
var border = new Border();
border.SetBinding(Border.BackgroundProperty, new Binding(nameof(Background)) { Source = this });
border.Child = scroll;
Content = border;

Margin = new Thickness(8d, 0d, 0d, 0d);
FontSize = 14d;
AllowDrop = true;

FontFamily = new FontFamily("Consolas, " + TranslationHelper.Get("Editor_FontFamily",
domain: "QuickLook.Plugin.TextViewer"));

var copy = new MenuItem
{
Header = TranslationHelper.Get("Editor_Copy", domain: "QuickLook.Plugin.TextViewer"),
};
copy.Click += (_, _) =>
{
try
{
Clipboard.SetText(_plainText);
}
catch
{
// Clipboard can be locked by another process; ignore.
}
};
ContextMenu = new ContextMenu();
ContextMenu.Items.Add(copy);

Text = text;
}

private void Rebuild()
{
_stack.Children.Clear();

foreach (var raw in _plainText.Replace("\r\n", "\n").Split('\n'))
{
var line = raw.TrimEnd();
if (line.Length == 0)
continue;

var sep = line.IndexOf(ColumnSeparator, StringComparison.Ordinal);
if (sep > 0)
_stack.Children.Add(BuildFieldRow(
line.Substring(0, sep).Trim(),
line.Substring(sep + ColumnSeparator.Length)));
else
_stack.Children.Add(BuildSectionHeader(line));
}
}

private static TextBlock BuildSectionHeader(string text) => new()
{
Text = text,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 8, 0, 2),
};

private static FrameworkElement BuildFieldRow(string label, string value)
{
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "Label" });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1d, GridUnitType.Star) });

var labelText = new TextBlock
{
Text = label,
TextAlignment = TextAlignment.Left,
Margin = new Thickness(0, 0, 6, 0),
};
var colonText = new TextBlock
{
Text = ":",
Margin = new Thickness(0, 0, 6, 0),
};
var valueText = new TextBlock
{
Text = value,
TextWrapping = TextWrapping.Wrap,
};

Grid.SetColumn(labelText, 0);
Grid.SetColumn(colonText, 1);
Grid.SetColumn(valueText, 2);
grid.Children.Add(labelText);
grid.Children.Add(colonText);
grid.Children.Add(valueText);

return grid;
}
}
Loading