diff --git a/apps/hdt-wine-overlay/WineOverlayPlugin.cs b/apps/hdt-wine-overlay/WineOverlayPlugin.cs index fa418f3..9d0693a 100644 --- a/apps/hdt-wine-overlay/WineOverlayPlugin.cs +++ b/apps/hdt-wine-overlay/WineOverlayPlugin.cs @@ -12,7 +12,12 @@ // background at alpha 1/255 keeps every pixel inside the shape while staying // 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 // early enough; enabling the plugin mid-session needs an HDT restart. // @@ -20,11 +25,15 @@ // official HDT build and its updater stay in place. using System; +using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Interop; using System.Windows.Media; +using System.Windows.Threading; using Hearthstone_Deck_Tracker; using Hearthstone_Deck_Tracker.Plugins; using Hearthstone_Deck_Tracker.Utility.Logging; @@ -40,6 +49,10 @@ namespace HdtWineOverlay public Version Version => new Version(0, 1, 0); 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 _patched = new ConditionalWeakTable(); private HwndSource _hookedSource; private RECT _lastGameRect; private DateTime _nextPoll = DateTime.MinValue; @@ -51,6 +64,15 @@ namespace HdtWineOverlay Log.Info("Not running under Wine, nothing to do"); 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 handle = new WindowInteropHelper(overlay).Handle; if(handle != IntPtr.Zero && overlay.IsVisible) @@ -72,10 +94,14 @@ namespace HdtWineOverlay overlay.SourceInitialized += OnOverlaySourceInitialized; Log.Info("Applied Wine overlay workarounds"); + + if(System.IO.File.Exists(System.IO.Path.Combine(Config.AppDataPath, "wine-overlay-test"))) + ShowTestPopup(); } public void OnUnload() { + _active = false; Core.Overlay.SourceInitialized -= OnOverlaySourceInitialized; _hookedSource?.RemoveHook(WndProc); _hookedSource = null; @@ -88,6 +114,13 @@ namespace HdtWineOverlay // Called by HDT every 100ms on the UI thread. public void OnUpdate() { + if(!_active) + return; + + // Popups, context menus and toasts that aren't tooltips. + foreach(var source in PresentationSource.CurrentSources.OfType().ToList()) + PatchTransparentSource(source); + if(_hookedSource == null || DateTime.UtcNow < _nextPoll) return; _nextPoll = DateTime.UtcNow.AddMilliseconds(250); @@ -114,6 +147,80 @@ namespace HdtWineOverlay 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) => 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) { 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; if(msg != wmMouseActivate) return IntPtr.Zero; @@ -162,6 +272,9 @@ namespace HdtWineOverlay [DllImport("user32.dll", CharSet = CharSet.Unicode)] 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")] private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);