HDT plugin: patch popups, window diagnostics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-08 10:33:41 +01:00
parent bc74760172
commit ab5468fc9e

View file

@ -12,7 +12,12 @@
// background at alpha 1/255 keeps every pixel inside the shape while staying // background at alpha 1/255 keeps every pixel inside the shape while staying
// invisible. // invisible.
// //
// Both fixes have to land before the overlay is first shown. HDT loads enabled // The same alpha-0 problem hits every other transparent HWND HDT opens: WPF
// tooltips and popups are separate layered windows (the Battlegrounds trinket
// and guide tooltips came up as large black squares). Those get a 1/255 clear
// color on their render target as soon as they appear.
//
// Both overlay fixes have to land before the overlay is first shown. HDT loads enabled
// plugins (plugins.xml) before it starts its overlay update loop, so OnLoad is // plugins (plugins.xml) before it starts its overlay update loop, so OnLoad is
// early enough; enabling the plugin mid-session needs an HDT restart. // early enough; enabling the plugin mid-session needs an HDT restart.
// //
@ -20,11 +25,15 @@
// official HDT build and its updater stay in place. // official HDT build and its updater stay in place.
using System; using System;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading;
using Hearthstone_Deck_Tracker; using Hearthstone_Deck_Tracker;
using Hearthstone_Deck_Tracker.Plugins; using Hearthstone_Deck_Tracker.Plugins;
using Hearthstone_Deck_Tracker.Utility.Logging; using Hearthstone_Deck_Tracker.Utility.Logging;
@ -40,6 +49,10 @@ namespace HdtWineOverlay
public Version Version => new Version(0, 1, 0); public Version Version => new Version(0, 1, 0);
public MenuItem MenuItem => null; public MenuItem MenuItem => null;
private static readonly Color AlmostTransparent = Color.FromArgb(1, 0, 0, 0);
private static bool _active;
private static bool _classHandlerRegistered;
private readonly ConditionalWeakTable<HwndSource, object> _patched = new ConditionalWeakTable<HwndSource, object>();
private HwndSource _hookedSource; private HwndSource _hookedSource;
private RECT _lastGameRect; private RECT _lastGameRect;
private DateTime _nextPoll = DateTime.MinValue; private DateTime _nextPoll = DateTime.MinValue;
@ -51,6 +64,15 @@ namespace HdtWineOverlay
Log.Info("Not running under Wine, nothing to do"); Log.Info("Not running under Wine, nothing to do");
return; return;
} }
_active = true;
// Tooltips: patch the moment they open so they never flash black.
// Class handlers can't be removed, hence the _active guard.
if(!_classHandlerRegistered)
{
EventManager.RegisterClassHandler(typeof(ToolTip), ToolTip.OpenedEvent, new RoutedEventHandler(OnToolTipOpened));
_classHandlerRegistered = true;
}
var overlay = Core.Overlay; var overlay = Core.Overlay;
var handle = new WindowInteropHelper(overlay).Handle; var handle = new WindowInteropHelper(overlay).Handle;
if(handle != IntPtr.Zero && overlay.IsVisible) if(handle != IntPtr.Zero && overlay.IsVisible)
@ -72,10 +94,14 @@ namespace HdtWineOverlay
overlay.SourceInitialized += OnOverlaySourceInitialized; overlay.SourceInitialized += OnOverlaySourceInitialized;
Log.Info("Applied Wine overlay workarounds"); Log.Info("Applied Wine overlay workarounds");
if(System.IO.File.Exists(System.IO.Path.Combine(Config.AppDataPath, "wine-overlay-test")))
ShowTestPopup();
} }
public void OnUnload() public void OnUnload()
{ {
_active = false;
Core.Overlay.SourceInitialized -= OnOverlaySourceInitialized; Core.Overlay.SourceInitialized -= OnOverlaySourceInitialized;
_hookedSource?.RemoveHook(WndProc); _hookedSource?.RemoveHook(WndProc);
_hookedSource = null; _hookedSource = null;
@ -88,6 +114,13 @@ namespace HdtWineOverlay
// Called by HDT every 100ms on the UI thread. // Called by HDT every 100ms on the UI thread.
public void OnUpdate() public void OnUpdate()
{ {
if(!_active)
return;
// Popups, context menus and toasts that aren't tooltips.
foreach(var source in PresentationSource.CurrentSources.OfType<HwndSource>().ToList())
PatchTransparentSource(source);
if(_hookedSource == null || DateTime.UtcNow < _nextPoll) if(_hookedSource == null || DateTime.UtcNow < _nextPoll)
return; return;
_nextPoll = DateTime.UtcNow.AddMilliseconds(250); _nextPoll = DateTime.UtcNow.AddMilliseconds(250);
@ -114,6 +147,80 @@ namespace HdtWineOverlay
overlay.UpdatePosition(); overlay.UpdatePosition();
} }
private void OnToolTipOpened(object sender, RoutedEventArgs e)
{
if(_active && PresentationSource.FromVisual((Visual)sender) is HwndSource source)
PatchTransparentSource(source);
}
// A layered (per-pixel alpha) window clears to transparent before WPF
// renders into it; clearing to 1/255 instead keeps every pixel inside
// Wine's window shape. The overlay itself is handled via Background in
// OnLoad and skipped here.
private void PatchTransparentSource(HwndSource source)
{
if(source.IsDisposed || source.CompositionTarget == null)
return;
if(_patched.TryGetValue(source, out _))
return;
_patched.Add(source, null);
// Diagnostics while the black-square report is open: every window HDT opens.
GetWindowRect(source.Handle, out var r);
Log.Info($"New window: {source.RootVisual?.GetType().Name} \"{(source.RootVisual as Window)?.Title}\" layered={source.UsesPerPixelOpacity} {r.Right - r.Left}x{r.Bottom - r.Top} at {r.Left},{r.Top}");
if(!source.UsesPerPixelOpacity)
return;
if(source.RootVisual == Core.Overlay)
return;
source.CompositionTarget.BackgroundColor = AlmostTransparent;
Log.Info($"Patched transparent window: {source.RootVisual?.GetType().Name}");
}
// Remote check without hovering anything: transparent popups whose
// edges, corners and inner ring are alpha 0. Black anywhere = not fixed.
// Left one gets the patch, right one is left alone for comparison.
// Shown at startup while %AppData%/HearthstoneDeckTracker/wine-overlay-test exists.
private void ShowTestPopup()
{
var patched = OpenTestPopup(100, "patched");
var control = OpenTestPopup(600, "unpatched");
if(PresentationSource.FromVisual(control.Child) is HwndSource source)
_patched.Add(source, null);
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) };
timer.Tick += (s, e) =>
{
patched.IsOpen = false;
control.IsOpen = false;
timer.Stop();
};
timer.Start();
Log.Info("Opened test popups for 30s");
}
private static Popup OpenTestPopup(double x, string label)
{
var popup = new Popup
{
AllowsTransparency = true,
// Owned by the overlay like HDT's own tooltips: Wine keeps owned
// popups override-redirect, unowned ones go to the compositor.
PlacementTarget = Core.Overlay,
Placement = PlacementMode.Relative,
HorizontalOffset = x,
VerticalOffset = 100,
Child = new Border
{
Width = 400,
Height = 300,
CornerRadius = new CornerRadius(60),
BorderThickness = new Thickness(40),
BorderBrush = Brushes.OrangeRed,
Child = new TextBlock { Text = label, Foreground = Brushes.White, FontSize = 28, Margin = new Thickness(20) }
}
};
popup.IsOpen = true;
return popup;
}
private void OnOverlaySourceInitialized(object sender, EventArgs e) private void OnOverlaySourceInitialized(object sender, EventArgs e)
=> HookMouseActivate(new WindowInteropHelper(Core.Overlay).Handle); => HookMouseActivate(new WindowInteropHelper(Core.Overlay).Handle);
@ -128,6 +235,9 @@ namespace HdtWineOverlay
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{ {
const int wmMouseActivate = 0x0021; const int wmMouseActivate = 0x0021;
const int wmActivate = 0x0006;
if(msg == wmActivate && wParam != IntPtr.Zero)
Log.Warn("Overlay was activated; Wine may now hand it to the compositor");
const int maNoActivate = 3; const int maNoActivate = 3;
if(msg != wmMouseActivate) if(msg != wmMouseActivate)
return IntPtr.Zero; return IntPtr.Zero;
@ -162,6 +272,9 @@ namespace HdtWineOverlay
[DllImport("user32.dll", CharSet = CharSet.Unicode)] [DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);