diff --git a/WineFix/Patches/ColorPickerWaylandPatch.cs b/WineFix/Patches/ColorPickerWaylandPatch.cs new file mode 100644 index 0000000..620b160 --- /dev/null +++ b/WineFix/Patches/ColorPickerWaylandPatch.cs @@ -0,0 +1,442 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using HarmonyLib; +using AffinityPluginLoader.Core; + +namespace WineFix.Patches +{ + /// + /// Fixes color picker zoom preview on Wayland by capturing the DocumentView/canvas instead of the full screen. + /// On Wayland, CopyFromScreen doesn't work properly, resulting in a black preview. + /// This patch captures the RenderControl's native window (HwndHost) using Win32 BitBlt. + /// + public static class ColorPickerWaylandPatch + { + private static Type _documentViewServiceType; + private static Type _screenHelperType; + private static Type _win32MethodsType; + private static Type _rectType; + private static MethodInfo _getDCMethod; + private static MethodInfo _releaseDCMethod; + private static MethodInfo _bitBltMethod; + private static MethodInfo _getWindowRectMethod; + private static bool _useExactPixelColor = false; + + public static void ApplyPatches(Harmony harmony) + { + try + { + Logger.Info($"Applying ColorPickerWayland patch..."); + + // Check environment variable for color picker mode + string pickerMode = Environment.GetEnvironmentVariable("APL_PICKER_VALUE"); + if (!string.IsNullOrEmpty(pickerMode)) + { + _useExactPixelColor = pickerMode.Equals("EXACT", StringComparison.OrdinalIgnoreCase); + Logger.Info($"APL_PICKER_VALUE={pickerMode}, using {(_useExactPixelColor ? "EXACT" : "NATIVE")} color mode"); + } + else + { + Logger.Info($"APL_PICKER_VALUE not set, using NATIVE color mode (default)"); + } + + // Find the Serif.Affinity assembly + var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); + + if (serifAssembly == null) + { + Logger.Error($"ERROR: Serif.Affinity assembly not found"); + return; + } + + // Get the ScreenHelper type + _screenHelperType = serifAssembly.GetType("Serif.Affinity.UI.Controls.ScreenHelper"); + if (_screenHelperType == null) + { + Logger.Error($"ERROR: ScreenHelper type not found"); + return; + } + + // Get Win32Methods type for Win32 API calls (in Serif.Windows assembly) + var serifWindowsAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Serif.Windows"); + if (serifWindowsAssembly == null) + { + Logger.Error($"ERROR: Serif.Windows assembly not found"); + return; + } + + _win32MethodsType = serifWindowsAssembly.GetType("Serif.Windows.Win32Methods"); + if (_win32MethodsType == null) + { + Logger.Error($"ERROR: Win32Methods type not found"); + return; + } + + _getDCMethod = _win32MethodsType.GetMethod("GetDC", BindingFlags.Public | BindingFlags.Static); + _releaseDCMethod = _win32MethodsType.GetMethod("ReleaseDC", BindingFlags.Public | BindingFlags.Static); + _bitBltMethod = _win32MethodsType.GetMethod("BitBlt", BindingFlags.Public | BindingFlags.Static); + _getWindowRectMethod = _win32MethodsType.GetMethod("GetWindowRect", BindingFlags.Public | BindingFlags.Static); + + if (_getDCMethod == null || _releaseDCMethod == null || _bitBltMethod == null || _getWindowRectMethod == null) + { + Logger.Error($"ERROR: Win32Methods API methods not found"); + return; + } + + // Get RECT type + _rectType = serifWindowsAssembly.GetType("Serif.Windows.RECT"); + if (_rectType == null) + { + Logger.Error($"ERROR: RECT type not found"); + return; + } + + // Get IDocumentViewService type + var personaAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Serif.Interop.Persona"); + if (personaAssembly != null) + { + _documentViewServiceType = personaAssembly.GetType("Serif.Interop.Persona.Services.IDocumentViewService"); + } + + // Patch SaveAllScreens(int, int, int, int) method + var saveAllScreensMethod = _screenHelperType.GetMethod("SaveAllScreens", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(int), typeof(int), typeof(int), typeof(int) }, + null); + + if (saveAllScreensMethod != null) + { + var prefix = typeof(ColorPickerWaylandPatch).GetMethod(nameof(SaveAllScreens_Prefix), + BindingFlags.Static | BindingFlags.Public); + harmony.Patch(saveAllScreensMethod, prefix: new HarmonyMethod(prefix)); + Logger.Info($"Patched ScreenHelper.SaveAllScreens to capture canvas instead of screen"); + } + else + { + Logger.Error($"ERROR: SaveAllScreens method not found"); + } + + // Patch CreateZoomImage to override color in EXACT mode + var createZoomImageMethod = _screenHelperType.GetMethod("CreateZoomImage", + BindingFlags.Public | BindingFlags.Static); + + if (createZoomImageMethod != null) + { + var postfix = typeof(ColorPickerWaylandPatch).GetMethod(nameof(CreateZoomImage_Postfix), + BindingFlags.Static | BindingFlags.Public); + harmony.Patch(createZoomImageMethod, postfix: new HarmonyMethod(postfix)); + Logger.Info($"Patched ScreenHelper.CreateZoomImage for EXACT color mode support"); + } + else + { + Logger.Warning($"WARNING: CreateZoomImage method not found"); + } + + // Patch ColourPickerMagnifier.UpdateWindowLocation to force bitmap refresh + var colourPickerMagnifierType = serifAssembly.GetType("Serif.Affinity.UI.Controls.ColourPickerMagnifier"); + if (colourPickerMagnifierType != null) + { + var updateWindowLocationMethod = colourPickerMagnifierType.GetMethod("UpdateWindowLocation", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (updateWindowLocationMethod != null) + { + var prefix = typeof(ColorPickerWaylandPatch).GetMethod(nameof(UpdateWindowLocation_Prefix), + BindingFlags.Static | BindingFlags.Public); + harmony.Patch(updateWindowLocationMethod, prefix: new HarmonyMethod(prefix)); + Logger.Info($"Patched ColourPickerMagnifier.UpdateWindowLocation to refresh bitmap"); + } + else + { + Logger.Warning($"WARNING: UpdateWindowLocation method not found"); + } + } + else + { + Logger.Warning($"WARNING: ColourPickerMagnifier type not found"); + } + } + catch (Exception ex) + { + Logger.Error("Failed to apply ColorPickerWayland patch", ex); + } + } + + private static int _frameCounter = 0; + private const int REFRESH_INTERVAL = 10; + + // Postfix to override color with exact pixel value in EXACT mode + public static void CreateZoomImage_Postfix(Bitmap bmpSrc, System.Windows.Point posIn, System.Windows.Size size, ref object col) + { + if (!_useExactPixelColor || bmpSrc == null) + { + return; // NATIVE mode or no bitmap - use original behavior + } + + try + { + // Calculate the center pixel position in the bitmap + // posIn is in screen coordinates, need to convert to bitmap coordinates + var screenLeft = SystemParameters.VirtualScreenLeft; + var screenTop = SystemParameters.VirtualScreenTop; + + // Offset by virtual screen position + int bitmapX = (int)(posIn.X - screenLeft); + int bitmapY = (int)(posIn.Y - screenTop); + + // Bounds check + if (bitmapX >= 0 && bitmapX < bmpSrc.Width && bitmapY >= 0 && bitmapY < bmpSrc.Height) + { + // Get the exact pixel color from the bitmap + System.Drawing.Color pixelColor = bmpSrc.GetPixel(bitmapX, bitmapY); + + // Convert to Colour type + // Need to find the ColourRGB type and create an instance + var personaAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Serif.Interop.Persona"); + + if (personaAssembly != null) + { + var colourRGBType = personaAssembly.GetType("Serif.Interop.Persona.Colours.ColourRGB"); + if (colourRGBType != null) + { + // Create ColourRGB(double r, double g, double b, double a) + var constructor = colourRGBType.GetConstructor(new Type[] { typeof(double), typeof(double), typeof(double), typeof(double) }); + if (constructor != null) + { + col = constructor.Invoke(new object[] { + pixelColor.R / 255.0, + pixelColor.G / 255.0, + pixelColor.B / 255.0, + pixelColor.A / 255.0 + }); + + Logger.Debug($"EXACT mode: Picked color from pixel ({bitmapX}, {bitmapY}): R={pixelColor.R}, G={pixelColor.G}, B={pixelColor.B}"); + } + } + } + } + } + catch (Exception ex) + { + Logger.Debug($"Error in EXACT color mode: {ex.Message}"); + } + } + + // Prefix to periodically refresh bitmap (not every frame for performance) + public static void UpdateWindowLocation_Prefix(object __instance) + { + try + { + _frameCounter++; + + // Only recapture every N frames to improve performance + if (_frameCounter >= REFRESH_INTERVAL) + { + _frameCounter = 0; + + // Clear the cached _screenBmp so it gets recaptured + var field = __instance.GetType().GetField("_screenBmp", BindingFlags.NonPublic | BindingFlags.Instance); + if (field != null) + { + field.SetValue(__instance, null); + } + } + } + catch (Exception ex) + { + Logger.Debug($"Error clearing cached bitmap: {ex.Message}"); + } + } + + private static bool _loggedCaptureInfo = false; + + // Prefix that replaces the screen capture with canvas/DocumentView capture + public static bool SaveAllScreens_Prefix(int screenLeft, int screenTop, int screenWidth, int screenHeight, ref Bitmap __result) + { + try + { + // Only log capture info once to reduce spam + if (!_loggedCaptureInfo) + { + Logger.Debug($"SaveAllScreens_Prefix: Capturing canvas (virtual screen: {screenLeft}, {screenTop}, {screenWidth}x{screenHeight})"); + } + + // Get the DocumentView from the application + var affinityApp = System.Windows.Application.Current; + if (affinityApp == null) + { + Logger.Warning("Application.Current is null"); + return true; + } + + // Get the DocumentViewService and current view + object documentView = null; + + try + { + // Get the service using reflection + // Look for the generic GetService() method (no parameters) + var getServiceMethod = affinityApp.GetType().GetMethods() + .FirstOrDefault(m => m.Name == "GetService" + && m.IsGenericMethod + && m.GetParameters().Length == 0); + + if (getServiceMethod != null && _documentViewServiceType != null) + { + var genericMethod = getServiceMethod.MakeGenericMethod(_documentViewServiceType); + var documentViewService = genericMethod.Invoke(affinityApp, null); + + if (documentViewService != null) + { + var viewsProperty = _documentViewServiceType.GetProperty("CurrentView"); + documentView = viewsProperty?.GetValue(documentViewService); + } + } + } + catch (Exception ex) + { + if (!_loggedCaptureInfo) + { + Logger.Debug($"Could not get DocumentView: {ex.Message}"); + } + } + + // Try to get the RenderControl's window handle + IntPtr renderControlHwnd = IntPtr.Zero; + System.Windows.Point controlPosition = new System.Windows.Point(0, 0); + double controlWidth = 0; + double controlHeight = 0; + + if (documentView != null) + { + // Try to get the RenderControl from DocumentView + var renderControlProp = documentView.GetType().GetProperty("RenderControl"); + + if (renderControlProp != null) + { + var renderControl = renderControlProp.GetValue(documentView); + + // RenderControl is an HwndHost, so we can get its Handle + if (renderControl is HwndHost hwndHost) + { + renderControlHwnd = hwndHost.Handle; + + // Get position and size using Win32 GetWindowRect for accurate positioning + try + { + // Use GetWindowRect to get the actual window rectangle + object rect = Activator.CreateInstance(_rectType); + object[] args = new object[] { renderControlHwnd, rect }; + _getWindowRectMethod.Invoke(null, args); + rect = args[1]; // GetWindowRect uses out parameter + + // Extract Left, Top, Right, Bottom from RECT + var leftField = _rectType.GetField("Left"); + var topField = _rectType.GetField("Top"); + var rightField = _rectType.GetField("Right"); + var bottomField = _rectType.GetField("Bottom"); + + int left = (int)leftField.GetValue(rect); + int top = (int)topField.GetValue(rect); + int right = (int)rightField.GetValue(rect); + int bottom = (int)bottomField.GetValue(rect); + + controlPosition = new System.Windows.Point(left, top); + controlWidth = right - left; + controlHeight = bottom - top; + + if (!_loggedCaptureInfo) + { + Logger.Debug($"RenderControl HWND: {renderControlHwnd}"); + Logger.Debug($"RenderControl RECT: left={left}, top={top}, right={right}, bottom={bottom}"); + Logger.Debug($"RenderControl position: {controlPosition}, size: {controlWidth}x{controlHeight}"); + } + } + catch (Exception ex) + { + if (!_loggedCaptureInfo) + { + Logger.Debug($"Error getting control position: {ex.Message}"); + } + } + } + } + } + + // If we didn't get a RenderControl, fall back to original implementation + if (renderControlHwnd == IntPtr.Zero) + { + Logger.Warning("Could not get RenderControl HWND, falling back"); + return true; + } + + // Capture the RenderControl window using BitBlt + Bitmap bitmap = new Bitmap(screenWidth, screenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + using (Graphics graphics = Graphics.FromImage(bitmap)) + { + // Fill with black background + graphics.Clear(System.Drawing.Color.Black); + + IntPtr hdcDest = graphics.GetHdc(); + try + { + // Get the device context for the RenderControl window + IntPtr hdcSrc = (IntPtr)_getDCMethod.Invoke(null, new object[] { renderControlHwnd }); + + // Calculate where to draw the control in the virtual screen bitmap + int destX = (int)(controlPosition.X - screenLeft); + int destY = (int)(controlPosition.Y - screenTop); + + if (!_loggedCaptureInfo) + { + Logger.Debug($"Capturing from HWND {renderControlHwnd} to position ({destX}, {destY})"); + Logger.Debug($"Virtual screen: left={screenLeft}, top={screenTop}, size={screenWidth}x{screenHeight}"); + Logger.Debug($"Control in bitmap: from ({destX},{destY}) to ({destX + (int)controlWidth},{destY + (int)controlHeight})"); + } + + // Copy window contents to bitmap at the correct position using BitBlt + // SRCCOPY = 0x00CC0020 + bool success = (bool)_bitBltMethod.Invoke(null, new object[] { + hdcDest, destX, destY, (int)controlWidth, (int)controlHeight, + hdcSrc, 0, 0, 0x00CC0020 + }); + + if (!_loggedCaptureInfo) + { + Logger.Debug($"BitBlt result: {success}"); + _loggedCaptureInfo = true; // Only log detailed info once + } + + // Release the window DC + _releaseDCMethod.Invoke(null, new object[] { renderControlHwnd, hdcSrc }); + } + finally + { + graphics.ReleaseHdc(hdcDest); + } + } + + __result = bitmap; + return false; // Skip original method + } + catch (Exception ex) + { + Logger.Error($"Error in SaveAllScreens_Prefix: {ex.Message}", ex); + return true; + } + } + + } +} diff --git a/WineFix/WineFix.csproj b/WineFix/WineFix.csproj index c64b1c6..c7ec6ef 100644 --- a/WineFix/WineFix.csproj +++ b/WineFix/WineFix.csproj @@ -24,6 +24,9 @@ + + + diff --git a/WineFix/WineFixPlugin.cs b/WineFix/WineFixPlugin.cs index 8ece872..cbc6c55 100644 --- a/WineFix/WineFixPlugin.cs +++ b/WineFix/WineFixPlugin.cs @@ -17,6 +17,7 @@ public void Initialize(Harmony harmony) // Apply Wine compatibility patches Patches.MainWindowLoadedPatch.ApplyPatches(harmony); + Patches.ColorPickerWaylandPatch.ApplyPatches(harmony); Logger.Info($"WineFix plugin initialized successfully"); }