[gimp] Add GimpPickButton implementation for Windows



commit d2dc2cb9839157cdb764ee85408138584b3740b8
Author: Luca Bacci <luca bacci982 gmail com>
Date:   Thu Sep 29 14:34:55 2022 +0200

    Add GimpPickButton implementation for Windows
    
    Fixes https://gitlab.gnome.org/GNOME/gimp/-/issues/8506

 libgimpwidgets/Makefile.am            |    2 +
 libgimpwidgets/gimppickbutton-win32.c | 1136 +++++++++++++++++++++++++++++++++
 libgimpwidgets/gimppickbutton-win32.h |   23 +
 libgimpwidgets/gimppickbutton.c       |   19 +-
 libgimpwidgets/meson.build            |    4 +
 5 files changed, 1177 insertions(+), 7 deletions(-)
---
diff --git a/libgimpwidgets/Makefile.am b/libgimpwidgets/Makefile.am
index 34514e1435..6c8494cf81 100644
--- a/libgimpwidgets/Makefile.am
+++ b/libgimpwidgets/Makefile.am
@@ -152,6 +152,8 @@ EXTRA_DIST = \
 if PLATFORM_OSX_QUARTZ
 libgimpwidgets_sources += gimppickbutton-quartz.c gimppickbutton-quartz.h
 AM_CPPFLAGS += "-xobjective-c"
+else if PLATFORM_WIN32
+libgimpwidgets_sources += gimppickbutton-win32.c gimppickbutton-win32.h
 else
 libgimpwidgets_sources += \
        gimppickbutton-default.c        \
diff --git a/libgimpwidgets/gimppickbutton-win32.c b/libgimpwidgets/gimppickbutton-win32.c
new file mode 100644
index 0000000000..e87dd56e49
--- /dev/null
+++ b/libgimpwidgets/gimppickbutton-win32.c
@@ -0,0 +1,1136 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimppickbutton-win32.c
+ * Copyright (C) 2022 Luca Bacci <luca bacci outlook com>
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gegl.h>
+#include <gtk/gtk.h>
+
+#include "libgimpcolor/gimpcolor.h"
+
+#include "gimpwidgetstypes.h"
+#include "gimppickbutton.h"
+#include "gimppickbutton-private.h"
+#include "gimppickbutton-win32.h"
+
+#include <sdkddkver.h>
+#include <windows.h>
+#include <windowsx.h>
+
+#include <stdint.h>
+
+/*
+ * NOTES:
+ *
+ * This implementation is based on gtk/gtkcolorpickerwin32.c from GTK.
+ *
+ * We install a low-level mouse hook so that color picking continues
+ * even when switching active windows.
+ *
+ * Beyond that, we also create keep-above, input-only HWNDs and place
+ * them on the screen to cover each monitor. This is done to show our
+ * custom cursor and to avoid giving input to other windows: that way
+ * the desktop appears "frozen" while picking the color.
+ *
+ * Finally, we also set up a low-level keyboard hook to dismiss color-
+ * picking mode whenever the user presses <ESC>.
+ *
+ * Note that low-level hooks for mouse and keyboard do not use any DLL
+ * injection and are thus non-invasive.
+ *
+ * For GTK4: consider using an appropriate GDK surface for input-only
+ * windows. This'd enable us to also get Wintab input when CXO_SYSTEM
+ * is not set.
+ */
+
+typedef struct
+{
+  HMONITOR handle;
+  wchar_t *device;
+  HDC      hdc;
+  POINT    screen_origin;
+  int      logical_width;
+  int      logical_height;
+  int      physical_width;
+  int      physical_height;
+} MonitorData;
+
+GList  *pickers;
+HHOOK   mouse_hook;
+HHOOK   keyboard_hook;
+
+ATOM    notif_window_class;
+HWND    notif_window_handle;
+GArray *monitors;
+
+ATOM    input_window_class;
+GArray *input_window_handles;
+
+/* Utils */
+static HMODULE            this_module                      (void);
+static MonitorData *      monitors_find_for_logical_point  (POINT           logical);
+static MonitorData *      monitors_find_for_physical_point (POINT           physical);
+static POINT              logical_to_physical              (MonitorData    *data,
+                                                            POINT           logical);
+static void               destroy_window                   (gpointer        ptr);
+
+/* Mouse cursor */
+static HCURSOR            create_cursor_from_rgba32_pixbuf (GdkPixbuf      *pixbuf,
+                                                            POINT           hotspot);
+static HCURSOR            create_cursor                    (void);
+
+/* Low-level mouse hook */
+static LRESULT CALLBACK   mouse_procedure                  (int             nCode,
+                                                            WPARAM          wParam,
+                                                            LPARAM          lParam);
+static                    gboolean ensure_mouse_hook       (void);
+static void               remove_mouse_hook                (void);
+
+/* Low-level keyboard hook */
+static LRESULT CALLBACK   keyboard_procedure               (int             nCode,
+                                                            WPARAM          wParam,
+                                                            LPARAM          lParam);
+static gboolean           ensure_keyboard_hook             (void);
+static void               remove_keyboard_hook             (void);
+
+/* Input-only window */
+static LRESULT CALLBACK   input_window_procedure           (HWND            hwnd,
+                                                            UINT            uMsg,
+                                                            WPARAM          wParam,
+                                                            LPARAM          lParam);
+static gboolean           ensure_input_window_class        (void);
+static void               remove_input_window_class        (void);
+static HWND               create_input_window              (POINT           origin,
+                                                            int             width,
+                                                            int             height);
+
+/* Hidden notification window */
+static LRESULT CALLBACK   notif_window_procedure           (HWND            hwnd,
+                                                            UINT            uMsg,
+                                                            WPARAM          wParam,
+                                                            LPARAM          lParam);
+static gboolean           ensure_notif_window_class        (void);
+static void               remove_notif_window_class        (void);
+static gboolean           ensure_notif_window              (void);
+static void               remove_notif_window              (void);
+
+/* Monitor enumeration and discovery */
+static void               monitor_data_free                (gpointer        ptr);
+static BOOL CALLBACK      enum_monitor_callback            (HMONITOR        hMonitor,
+                                                            HDC             hDC,
+                                                            RECT           *pRect,
+                                                            LPARAM          lParam);
+static GArray*            enumerate_monitors               (void);
+
+/* GimpPickButtonWin32 */
+static void               ensure_input_windows             (void);
+static void               remove_input_windows             (void);
+static void               ensure_monitors                  (void);
+static void               remove_monitors                  (void);
+static void               ensure_screen_data               (void);
+static void               remove_screen_data               (void);
+static void               screen_changed                   (void);
+static void               ensure_screen_tracking           (void);
+static void               remove_screen_tracking           (void);
+static GimpRGB            pick_color_with_gdi              (POINT           physical_point);
+static void               user_picked                      (MonitorData    *monitor,
+                                                            POINT           physical_point);
+void                      _gimp_pick_button_win32_pick     (GimpPickButton *button);
+static void                stop_picking                    (void);
+
+/* {{{ Utils */
+
+/* Gets a handle to the module containing this code.
+ * Works regardless if we're building a shared or
+ * static library */
+
+static HMODULE
+this_module (void)
+{
+  extern IMAGE_DOS_HEADER __ImageBase;
+  return (HMODULE) &__ImageBase;
+}
+
+static MonitorData*
+monitors_find_for_logical_point (POINT logical)
+{
+  HMONITOR monitor_handle;
+  guint    i;
+
+  monitor_handle = MonitorFromPoint (logical, MONITOR_DEFAULTTONULL);
+  if (!monitor_handle)
+    return NULL;
+
+  ensure_monitors ();
+
+  for (i = 0; i < monitors->len; i++)
+    {
+      MonitorData *data = &g_array_index (monitors, MonitorData, i);
+
+      if (data->handle == monitor_handle)
+        return data;
+    }
+
+  return NULL;
+}
+
+static MonitorData*
+monitors_find_for_physical_point (POINT physical)
+{
+  guint i;
+
+  ensure_monitors ();
+
+  for (i = 0; i < monitors->len; i++)
+    {
+      MonitorData *data = &g_array_index (monitors, MonitorData, i);
+      RECT         physical_rect;
+
+      physical_rect.left   = data->screen_origin.x;
+      physical_rect.top    = data->screen_origin.y;
+      physical_rect.right  = physical_rect.left + data->physical_width;
+      physical_rect.bottom = physical_rect.top + data->physical_height;
+
+      /* TODO: tolerance */
+      if (PtInRect (&physical_rect, physical))
+        return data;
+    }
+
+  return NULL;
+}
+
+static POINT
+logical_to_physical (MonitorData *data,
+                     POINT        logical)
+{
+  POINT physical = logical;
+
+  if (data &&
+      (data->logical_width != data->physical_width) &&
+      data->logical_width > 0 && data->physical_width > 0)
+    {
+      double dpi_scale = (double) data->physical_width /
+                         (double) data->logical_width;
+
+      physical.x = logical.x * dpi_scale;
+      physical.y = logical.y * dpi_scale;
+    }
+
+  return physical;
+}
+
+static void
+destroy_window (gpointer ptr)
+{
+  HWND *hwnd = (HWND*) ptr;
+  DestroyWindow (*hwnd);
+}
+
+/* }}}
+ * {{{ Mouse cursor */
+
+static HCURSOR
+create_cursor_from_rgba32_pixbuf (GdkPixbuf *pixbuf,
+                                  POINT      hotspot)
+{
+  struct {
+    BITMAPV5HEADER header;
+    RGBQUAD        colors[2];
+  }                info;
+  HBITMAP          bitmap      = NULL;
+  uint8_t         *bitmap_data = NULL;
+  HBITMAP          mask        = NULL;
+  uint8_t         *mask_data   = NULL;
+  unsigned int     mask_stride;
+  HDC              hdc         = NULL;
+  int              width;
+  int              height;
+  int              size;
+  int              stride;
+  const uint8_t   *pixbuf_data = NULL;
+  int              i_offset;
+  int              j_offset;
+  int              i;
+  int              j;
+  ICONINFO         icon_info;
+  HICON            icon        = NULL;
+
+  if (gdk_pixbuf_get_n_channels (pixbuf) != 4)
+    goto cleanup;
+
+  hdc = GetDC (NULL);
+  if (!hdc)
+    goto cleanup;
+
+  width       = gdk_pixbuf_get_width (pixbuf);
+  height      = gdk_pixbuf_get_height (pixbuf);
+  stride      = gdk_pixbuf_get_rowstride (pixbuf);
+  size        = MAX (width, height);
+  pixbuf_data = gdk_pixbuf_read_pixels (pixbuf);
+
+  memset (&info, 0, sizeof (info));
+  info.header.bV5Size        = sizeof (info.header);
+  info.header.bV5Width       = size;
+  info.header.bV5Height      = size;
+  /* Since Windows XP the OS supports mouse cursors as 32bpp
+   * bitmaps with alpha channel that are laid out as BGRA32
+   * (assuming little-endian) */
+  info.header.bV5Planes      = 1;
+  info.header.bV5BitCount    = 32;
+  info.header.bV5Compression = BI_BITFIELDS;
+  info.header.bV5BlueMask    = 0x000000FF;
+  info.header.bV5GreenMask   = 0x0000FF00;
+  info.header.bV5RedMask     = 0x00FF0000;
+  info.header.bV5AlphaMask   = 0xFF000000;
+
+  bitmap = CreateDIBSection (hdc, (BITMAPINFO*) &info, DIB_RGB_COLORS,
+                             (void**) &bitmap_data, NULL, 0);
+  if (!bitmap || !bitmap_data)
+    {
+      g_warning ("CreateDIBSection failed with error code %u",
+                 (unsigned) GetLastError ());
+      goto cleanup;
+    }
+
+  /* We also need to provide a proper mask HBITMAP, otherwise
+   * CreateIconIndirect() fails with ERROR_INVALID_PARAMETER.
+   * This mask bitmap is a bitfield indicating whether the */
+  memset (&info, 0, sizeof (info));
+  info.header.bV5Size        = sizeof (info.header);
+  info.header.bV5Width       = size;
+  info.header.bV5Height      = size;
+  info.header.bV5Planes      = 1;
+  info.header.bV5BitCount    = 1;
+  info.header.bV5Compression = BI_RGB;
+  info.colors[0]             = (RGBQUAD){0x00, 0x00, 0x00};
+  info.colors[1]             = (RGBQUAD){0xFF, 0xFF, 0xFF};
+
+  mask = CreateDIBSection (hdc, (BITMAPINFO*) &info, DIB_RGB_COLORS,
+                           (void**) &mask_data, NULL, 0);
+  if (!mask || !mask_data)
+    {
+      g_warning ("CreateDIBSection failed with error code %u",
+                 (unsigned) GetLastError ());
+      goto cleanup;
+    }
+
+  /* MSDN says mask rows are aligned to LONG boundaries */
+  mask_stride = (((size + 31) & ~31) >> 3);
+
+  if (width > height)
+    {
+      i_offset = 0;
+      j_offset = (width - height) / 2;
+    }
+  else
+    {
+      i_offset = (height - width) / 2;
+      j_offset = 0;
+    }
+
+  for (j = 0; j < height; j++) /* loop over all the bitmap rows */
+    {
+      uint8_t       *bitmap_row = bitmap_data + 4 * ((j + j_offset) * size + i_offset);
+      uint8_t       *mask_byte  = mask_data + (j + j_offset) * mask_stride + i_offset / 8;
+      unsigned int   mask_bit   = (0x80 >> (i_offset % 8));
+      const uint8_t *pixbuf_row = pixbuf_data + (height - j - 1) * stride;
+
+      for (i = 0; i < width; i++) /* loop over the current bitmap row */
+        {
+          uint8_t       *bitmap_pixel = bitmap_row + 4 * i;
+          const uint8_t *pixbuf_pixel = pixbuf_row + 4 * i;
+
+          /* Assign to destination pixel from source pixel
+           * by also swapping channels as appropriate */
+          bitmap_pixel[0] = pixbuf_pixel[2];
+          bitmap_pixel[1] = pixbuf_pixel[1];
+          bitmap_pixel[2] = pixbuf_pixel[0];
+          bitmap_pixel[3] = pixbuf_pixel[3];
+
+          if (pixbuf_pixel[3] == 0)
+            mask_byte[0] |= mask_bit;    /* turn ON bit */
+          else
+            mask_byte[0] &= ~mask_bit;   /* turn OFF bit */
+
+          mask_bit >>= 1;
+          if (mask_bit == 0)
+            {
+              mask_bit = 0x80;
+              mask_byte++;
+            }
+        }
+    }
+
+  memset (&icon_info, 0, sizeof (icon_info));
+  icon_info.fIcon    = FALSE;
+  icon_info.xHotspot = hotspot.x;
+  icon_info.yHotspot = hotspot.y;
+  icon_info.hbmMask  = mask;
+  icon_info.hbmColor = bitmap;
+
+  icon = CreateIconIndirect (&icon_info);
+  if (!icon)
+    {
+      g_warning ("CreateIconIndirect failed with error code %u",
+                 (unsigned) GetLastError ());
+      goto cleanup;
+    }
+
+cleanup:
+  if (mask)
+    DeleteObject (mask);
+
+  if (bitmap)
+    DeleteObject (bitmap);
+
+  if (hdc)
+    ReleaseDC (NULL, hdc);
+
+  return (HCURSOR) icon;
+}
+
+static HCURSOR
+create_cursor (void)
+{
+  GdkPixbuf *pixbuf  = NULL;
+  GError    *error   = NULL;
+  HCURSOR    cursor  = NULL;
+
+  pixbuf = gdk_pixbuf_new_from_resource ("/org/gimp/color-picker-cursors/cursor-color-picker.png",
+                                         &error);
+  if (!pixbuf)
+    {
+      g_critical ("gdk_pixbuf_new_from_resource failed: %s",
+                  error ? error->message : "unknown error");
+      goto cleanup;
+    }
+  g_clear_error (&error);
+
+  cursor = create_cursor_from_rgba32_pixbuf (pixbuf, (POINT){ 1, 30 });
+
+cleanup:
+  g_clear_error (&error);
+  g_clear_object (&pixbuf);
+
+  return cursor;
+}
+
+/* }}}
+ * {{{ Low-level mouse hook */
+
+/* This mouse procedure can detect clicks made on any
+ * application window. Countrary to mouse capture, this
+ * method continues to work even after switching active
+ * windows. */
+
+static LRESULT CALLBACK
+mouse_procedure (int    nCode,
+                 WPARAM wParam,
+                 LPARAM lParam)
+{
+  if (nCode == HC_ACTION)
+    {
+      MSLLHOOKSTRUCT *info = (MSLLHOOKSTRUCT*) lParam;
+
+      switch (wParam)
+        {
+        case WM_LBUTTONDOWN:
+        case WM_MBUTTONDOWN:
+        case WM_RBUTTONDOWN:
+        case WM_XBUTTONDOWN:
+          {
+            POINT        physical;
+            MonitorData *data;
+
+            if (pickers == NULL)
+              break;
+
+            /* A low-level mouse hook always receives points in
+             * per-monitor DPI-aware screen coordinates, regardless of
+             * the DPI awareness setting of the application. */
+            physical = info->pt;
+
+            data = monitors_find_for_physical_point (physical);
+            if (!data)
+              g_message ("Captured point (%ld, %ld) doesn't belong to any monitor",
+                         (long) physical.x, (long) physical.y);
+
+            user_picked (data, physical);
+
+            /* It's safe to remove a hook from within its callback.
+             * Anyway this can even be called from an idle callback,
+             * as the hook does nothing if there are no pickers.
+             * (In that case also the ensure functions have to be
+             * scheduled in an idle callback) */
+            stop_picking ();
+
+            return (wParam == WM_XBUTTONDOWN) ? TRUE : 0;
+          }
+          break;
+        default:
+          break;
+        }
+    }
+
+  return CallNextHookEx (NULL, nCode, wParam, lParam);
+}
+
+static gboolean
+ensure_mouse_hook (void)
+{
+  if (!mouse_hook)
+    {
+      mouse_hook = SetWindowsHookEx (WH_MOUSE_LL, mouse_procedure, this_module (), 0);
+      if (!mouse_hook)
+        {
+          g_warning ("SetWindowsHookEx failed with error code %u",
+                     (unsigned) GetLastError ());
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+remove_mouse_hook (void)
+{
+  if (mouse_hook)
+    {
+      if (!UnhookWindowsHookEx (mouse_hook))
+        {
+          g_warning ("UnhookWindowsHookEx failed with error code %u",
+                     (unsigned) GetLastError ());
+          return;
+        }
+
+      mouse_hook = NULL;
+    }
+}
+
+/* }}}
+ * {{{ Low-level keyboard hook */
+
+/* This is used to stop picking anytime the user presses ESC */
+
+static LRESULT CALLBACK
+keyboard_procedure (int    nCode,
+                    WPARAM wParam,
+                    LPARAM lParam)
+{
+  if (nCode == HC_ACTION)
+    {
+      KBDLLHOOKSTRUCT *info = (KBDLLHOOKSTRUCT*) lParam;
+
+      switch (wParam)
+        {
+        case WM_KEYDOWN:
+        case WM_SYSKEYDOWN:
+          if (info->vkCode == VK_ESCAPE)
+            {
+              stop_picking ();
+              return 1;
+            }
+          break;
+        default:
+          break;
+        }
+    }
+
+  return CallNextHookEx(NULL, nCode, wParam, lParam);
+}
+
+static gboolean
+ensure_keyboard_hook (void)
+{
+  if (!keyboard_hook)
+    {
+      keyboard_hook = SetWindowsHookEx (WH_KEYBOARD_LL, keyboard_procedure, this_module (), 0);
+      if (!keyboard_hook)
+        {
+          g_warning ("SetWindowsHookEx failed with error code %u",
+                     (unsigned) GetLastError ());
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+remove_keyboard_hook (void)
+{
+  if (keyboard_hook)
+    {
+      if (!UnhookWindowsHookEx (keyboard_hook))
+        {
+          g_warning ("UnhookWindowsHookEx failed with error code %u",
+                     (unsigned) GetLastError ());
+          return;
+        }
+
+      keyboard_hook = NULL;
+    }
+}
+
+/* }}}
+ * {{{ Input-only windows */
+
+/* Those input-only windows are placed to cover each monitor on
+ * the screen and serve two purposes: display our custom mouse
+ * cursor and freeze the desktop by avoiding interaction of the
+ * mouse with other applications */
+
+static LRESULT CALLBACK
+input_window_procedure (HWND   hwnd,
+                        UINT   uMsg,
+                        WPARAM wParam,
+                        LPARAM lParam)
+{
+  switch (uMsg)
+    {
+    case WM_NCCREATE:
+      /* The shell automatically hides the taskbar when a window
+       * covers the entire area of a monitor (fullscreened). In
+       * order to avoid that, we can set a special property on
+       * the window
+       * See the docs for ITaskbarList2::MarkFullscreenWindow()
+       * on MSDN for more informations */
+
+      if (!SetPropW (hwnd, L"NonRudeHWND", (HANDLE) TRUE))
+        g_warning_once ("SetPropW failed with error code %u",
+                        (unsigned) GetLastError ());
+      break;
+
+    case WM_NCDESTROY:
+      /* We have to remove window properties manually before the
+       * window gets destroyed */
+
+      if (!RemovePropW (hwnd, L"NonRudeHWND"))
+        g_warning_once ("SetPropW failed with error code %u",
+                        (unsigned) GetLastError ());
+      break;
+
+    /* Avoid any drawing at all. Here we block processing of the
+     * default window procedure, although we set the NULL_BRUSH
+     * as the window class's background brush, so even the default
+     * window procedure wouldn't draw anything at all */
+
+    case WM_ERASEBKGND:
+      return 1;
+    case WM_PAINT:
+      {
+        #if 1
+        PAINTSTRUCT ps;
+        BeginPaint(hwnd, &ps);
+        EndPaint(hwnd, &ps);
+        #else
+        UINT flags = RDW_VALIDATE |
+                     RDW_NOFRAME |
+                     RDW_NOERASE;
+        RedrawWindow (hwnd, NULL, NULL, flags);
+        #endif
+      }
+      return 0;
+    #if 0
+    case WM_SYNCPAINT:
+      return 0;
+    #endif
+
+    case WM_MOUSEACTIVATE:
+      /* This can be useful in case we want to avoid
+       * using a mouse hook */
+      return MA_NOACTIVATE;
+
+    /* Anytime we detect a mouse click, pick the color. Note that
+     * this is rarely used as the mouse hook would process the
+     * clicks before that */
+
+    case WM_LBUTTONDOWN:
+    case WM_MBUTTONDOWN:
+    case WM_RBUTTONDOWN:
+    case WM_XBUTTONDOWN:
+      {
+        POINT        logical  = (POINT){GET_X_LPARAM (lParam), GET_Y_LPARAM (lParam)};
+        MonitorData *data     = monitors_find_for_logical_point (logical);
+        POINT        physical = logical_to_physical (data, logical);
+
+        if (!data)
+          g_message ("Captured point (%ld, %ld) doesn't belong to any monitor",
+                     (long) logical.x, (long) logical.y);
+
+        user_picked (data, physical);
+
+        stop_picking ();
+      }
+
+      return (uMsg == WM_XBUTTONDOWN) ? TRUE : 0;
+    default:
+      break;
+    }
+
+  return DefWindowProcW (hwnd, uMsg, wParam, lParam);
+}
+
+static gboolean
+ensure_input_window_class (void)
+{
+  if (!input_window_class)
+    {
+      WNDCLASSEXW wndclassex;
+      HCURSOR     cursor     = create_cursor ();
+
+      memset (&wndclassex, 0, sizeof (wndclassex));
+      wndclassex.cbSize        = sizeof (wndclassex);
+      wndclassex.hInstance     = this_module ();
+      wndclassex.lpszClassName = L"GimpPickButtonInputWindowClass";
+      wndclassex.lpfnWndProc   = input_window_procedure;
+      wndclassex.hbrBackground = GetStockObject (NULL_BRUSH);
+      wndclassex.hCursor       = cursor ?
+                                 cursor :
+                                 LoadImageW (NULL,
+                                             (LPCWSTR)(guintptr)IDC_CROSS,
+                                             IMAGE_CURSOR, 0, 0,
+                                             LR_DEFAULTSIZE | LR_SHARED);
+
+      input_window_class = RegisterClassExW (&wndclassex);
+      if (input_window_class == 0)
+        {
+          g_warning ("RegisterClassExW failed with error code %u",
+                     (unsigned) GetLastError ());
+
+          if (cursor)
+            DestroyCursor (cursor);
+
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+remove_input_window_class (void)
+{
+  if (input_window_class)
+    {
+      LPCWSTR name = (LPCWSTR)(guintptr)input_window_class;
+      UnregisterClassW (name, this_module ());
+      input_window_class = 0;
+    }
+}
+
+/* create_input_window expects logical screen coordinates */
+
+static HWND
+create_input_window (POINT origin,
+                     int width,
+                     int height)
+{
+  DWORD   stylex  = WS_EX_NOACTIVATE | WS_EX_TOPMOST;
+  LPCWSTR wclass;
+  LPCWSTR title   = L"Gimp Input Window";
+  DWORD   style   = WS_POPUP;
+  HWND    hwnd;
+
+  if (!ensure_input_window_class ())
+    return FALSE;
+
+  /* This must go after the ensure_input_window_class */
+  wclass = (LPCWSTR)(guintptr)input_window_class;
+
+  hwnd = CreateWindowExW (stylex, wclass, title, style,
+                          origin.x, origin.y, width, height,
+                          NULL, NULL, this_module (), NULL);
+  if (hwnd == NULL)
+    {
+      g_warning_once ("CreateWindowExW failed with error code %u",
+                      (unsigned) GetLastError ());
+      return FALSE;
+    }
+
+  ShowWindow (hwnd, SW_SHOWNOACTIVATE);
+
+  return hwnd;
+}
+
+/* }}}
+ * {{{ Hidden notification window */
+
+/* We setup a hidden window to listen for WM_DISPLAYCAHNGE
+ * messages and reposition the input-only windows on the
+ * screen */
+
+static LRESULT CALLBACK
+notif_window_procedure (HWND   hwnd,
+                        UINT   uMsg,
+                        WPARAM wParam,
+                        LPARAM lParam)
+{
+  switch (uMsg)
+    {
+    case WM_DISPLAYCHANGE:
+      screen_changed ();
+      break;
+    default:
+      break;
+    }
+
+  return DefWindowProcW (hwnd, uMsg, wParam, lParam);
+}
+
+static gboolean
+ensure_notif_window_class (void)
+{
+  if (!notif_window_class)
+    {
+      WNDCLASSEXW wndclassex;
+
+      memset (&wndclassex, 0, sizeof (wndclassex));
+      wndclassex.cbSize        = sizeof (wndclassex);
+      wndclassex.hInstance     = this_module ();
+      wndclassex.lpszClassName = L"GimpPickButtonNotifWindowClass";
+      wndclassex.lpfnWndProc   = notif_window_procedure;
+
+      notif_window_class = RegisterClassExW (&wndclassex);
+      if (notif_window_class == 0)
+        {
+          g_warning ("RegisterClassExW failed with error code %u",
+                     (unsigned) GetLastError ());
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+remove_notif_window_class (void)
+{
+  if (notif_window_class)
+    {
+      LPCWSTR name = (LPCWSTR)(guintptr)notif_window_class;
+      UnregisterClassW (name, this_module ());
+      notif_window_class = 0;
+    }
+}
+
+static gboolean
+ensure_notif_window (void)
+{
+  if (!ensure_notif_window_class ())
+    return FALSE;
+
+  if (!notif_window_handle)
+    {
+      DWORD   stylex = 0;
+      LPCWSTR wclass = (LPCWSTR)(guintptr)notif_window_class;
+      LPCWSTR title  = L"Gimp Notifications Window";
+      DWORD   style  = WS_POPUP;
+
+      notif_window_handle = CreateWindowExW (stylex, wclass,
+                                             title, style,
+                                             0, 0, 1, 1,
+                                             NULL, NULL,
+                                             this_module (),
+                                             NULL);
+      if (notif_window_handle == NULL)
+        {
+          g_warning_once ("CreateWindowExW failed with error code %u",
+                          (unsigned) GetLastError ());
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+remove_notif_window (void)
+{
+  if (notif_window_handle)
+    {
+      DestroyWindow (notif_window_handle);
+      notif_window_handle = NULL;
+    }
+
+  remove_notif_window_class ();
+}
+
+/* }}}
+ * {{{ Monitor enumeration and discovery */
+
+static void
+monitor_data_free (gpointer ptr)
+{
+  MonitorData *data = (MonitorData*) ptr;
+
+  if (data->device)
+    free (data->device);
+}
+
+static BOOL CALLBACK
+enum_monitor_callback (HMONITOR  hMonitor,
+                       HDC       hDC,
+                       RECT     *pRect,
+                       LPARAM    lParam)
+{
+  GArray         *result  = (GArray*) lParam;
+  MonitorData     data;
+  MONITORINFOEXW  info;
+  DEVMODEW        devmode;
+
+  const BOOL CALLBACK_CONTINUE = TRUE;
+  const BOOL CALLBACK_STOP = FALSE;
+
+  if (!pRect)
+    return CALLBACK_CONTINUE;
+
+  if (IsRectEmpty (pRect))
+    return CALLBACK_CONTINUE;
+
+  memset (&data, 0, sizeof (data));
+  data.handle          = hMonitor;
+  data.hdc             = hDC;
+  data.screen_origin.x = pRect->left;
+  data.screen_origin.y = pRect->top;
+  data.logical_width   = pRect->right - pRect->left;
+  data.logical_height  = pRect->bottom - pRect->top;
+
+  memset (&info, 0, sizeof (info));
+  info.cbSize = sizeof (info);
+  if (!GetMonitorInfoW (hMonitor, (MONITORINFO*) &info))
+    {
+      g_warning_once ("GetMonitorInfo failed with error code %u",
+                      (unsigned) GetLastError ());
+      return CALLBACK_CONTINUE;
+    }
+
+  data.device = _wcsdup (info.szDevice);
+
+  memset (&devmode, 0, sizeof (devmode));
+  devmode.dmSize = sizeof (devmode);
+  if (!EnumDisplaySettingsExW (info.szDevice,
+                               ENUM_CURRENT_SETTINGS,
+                               &devmode, EDS_ROTATEDMODE))
+    {
+      g_warning_once ("EnumDisplaySettingsEx failed with error code %u",
+                      (unsigned) GetLastError ());
+      return CALLBACK_CONTINUE;
+    }
+
+  if (devmode.dmPelsWidth)
+    data.physical_width = devmode.dmPelsWidth;
+
+  if (devmode.dmPelsHeight)
+    data.physical_height = devmode.dmPelsHeight;
+
+  g_array_append_val (result, data);
+
+  if (result->len >= 100)
+    return CALLBACK_STOP;
+
+  return CALLBACK_CONTINUE;
+}
+
+static GArray*
+enumerate_monitors (void)
+{
+  int     count_monitors;
+  guint   length_hint;
+  GArray *result;
+
+  count_monitors = GetSystemMetrics (SM_CMONITORS);
+
+  if (count_monitors > 0 && count_monitors < 100)
+    length_hint = (guint) count_monitors;
+  else
+    length_hint = 1;
+
+  result = g_array_sized_new (FALSE, TRUE, sizeof (MonitorData), length_hint);
+  g_array_set_clear_func (result, monitor_data_free);
+
+  if (!EnumDisplayMonitors (NULL, NULL, enum_monitor_callback, (LPARAM) result))
+    {
+      g_warning ("EnumDisplayMonitors failed with error code %u",
+                 (unsigned) GetLastError ());
+    }
+
+  return result;
+}
+
+static void
+ensure_input_windows (void)
+{
+  ensure_monitors ();
+
+  if (!input_window_handles)
+    {
+      guint i;
+
+      input_window_handles = g_array_sized_new (FALSE, TRUE,
+                                                sizeof (HWND),
+                                                monitors->len);
+      g_array_set_clear_func (input_window_handles, destroy_window);
+
+      for (i = 0; i < monitors->len; i++)
+        {
+          MonitorData *data = &g_array_index (monitors, MonitorData, i);
+          HWND         hwnd = create_input_window (data->screen_origin,
+                                                   data->logical_width,
+                                                   data->logical_height);
+
+          if (hwnd)
+            g_array_append_val (input_window_handles, hwnd);
+        }
+    }
+}
+
+static void
+remove_input_windows (void)
+{
+  if (input_window_handles)
+    {
+      g_array_free (input_window_handles, TRUE);
+      input_window_handles = NULL;
+    }
+}
+
+static void
+ensure_monitors (void)
+{
+  if (!monitors)
+    monitors = enumerate_monitors ();
+}
+
+static void
+remove_monitors (void)
+{
+  if (monitors)
+    {
+      g_array_free (monitors, TRUE);
+      monitors = NULL;
+    }
+}
+
+static void
+ensure_screen_data (void)
+{
+  ensure_monitors ();
+  ensure_input_windows ();
+}
+
+static void
+remove_screen_data (void)
+{
+  remove_input_windows ();
+  remove_monitors ();
+}
+
+static void
+screen_changed (void)
+{
+  remove_screen_data ();
+  ensure_screen_data ();
+}
+
+static void
+ensure_screen_tracking (void)
+{
+  ensure_notif_window ();
+  screen_changed ();
+}
+
+static void
+remove_screen_tracking (void)
+{
+  remove_notif_window ();
+  remove_screen_data ();
+}
+
+/* }}}
+ * {{{ GimpPickButtonWin32 */
+
+/* pick_color_with_gdi is based on the GDI GetPixel() function.
+ * Note that GDI only returns 8bit per-channel color data, but
+ * as of today there's no documented method to retrieve colors
+ * at higher bit depths. */
+
+static GimpRGB
+pick_color_with_gdi (POINT physical_point)
+{
+  GimpRGB  rgb;
+  COLORREF color;
+  HDC      hdc;
+
+  hdc = GetDC (HWND_DESKTOP);
+
+  if (!(GetDeviceCaps (hdc, RASTERCAPS) & RC_BITBLT))
+    g_warning_once ("RC_BITBLT capability missing from device context");
+
+  color = GetPixel (hdc, physical_point.x, physical_point.y);
+
+  gimp_rgba_set_uchar (&rgb, GetRValue (color), GetGValue (color), GetBValue (color), 255);
+
+  ReleaseDC (HWND_DESKTOP, hdc);
+
+  return rgb;
+}
+
+static void
+user_picked (MonitorData *monitor,
+             POINT        physical_point)
+{
+  GimpRGB rgb;
+  GList *l;
+
+  /* Currently unused */
+  (void) monitor;
+
+  rgb = pick_color_with_gdi (physical_point);
+
+  for (l = pickers; l != NULL; l = l->next)
+    {
+      GimpPickButton *button = GIMP_PICK_BUTTON (l->data);
+      g_signal_emit_by_name (button, "color-picked", &rgb);
+    }
+}
+
+/* entry point to this file, called from gimppickbutton.c */
+void
+_gimp_pick_button_win32_pick (GimpPickButton *button)
+{
+  ensure_mouse_hook ();
+  ensure_keyboard_hook ();
+  ensure_screen_tracking ();
+
+  if (g_list_find (pickers, button))
+    return;
+
+  pickers = g_list_prepend (pickers,
+                            g_object_ref (button));
+}
+
+static void
+stop_picking (void)
+{
+  remove_screen_tracking ();
+  remove_keyboard_hook ();
+  remove_mouse_hook ();
+
+  g_list_free_full (pickers, g_object_unref);
+  pickers = NULL;
+}
diff --git a/libgimpwidgets/gimppickbutton-win32.h b/libgimpwidgets/gimppickbutton-win32.h
new file mode 100644
index 0000000000..f28de815a6
--- /dev/null
+++ b/libgimpwidgets/gimppickbutton-win32.h
@@ -0,0 +1,23 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimppickbutton-win32.h
+ * Copyright (C) 2022 Luca Bacci <luca bacci outlook com>
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+/* Private header file which is not meant to be exported. */
+#ifndef __GIMP_PICK_BUTTON_WIN32_H__
+#define __GIMP_PICK_BUTTON_WIN32_H__
+
+void _gimp_pick_button_win32_pick (GimpPickButton *button);
+
+#endif /* __GIMP_PICK_BUTTON_WIN32_H__ */
diff --git a/libgimpwidgets/gimppickbutton.c b/libgimpwidgets/gimppickbutton.c
index 67dbc77e27..84cd08eb8f 100644
--- a/libgimpwidgets/gimppickbutton.c
+++ b/libgimpwidgets/gimppickbutton.c
@@ -34,17 +34,20 @@
 #include "gimphelpui.h"
 #include "gimpicons.h"
 #include "gimppickbutton.h"
-#include "gimppickbutton-default.h"
-#include "gimppickbutton-kwin.h"
-#include "gimppickbutton-xdg.h"
 #include "gimppickbutton-private.h"
 
-#ifdef GDK_WINDOWING_QUARTZ
+#include "libgimp/libgimp-intl.h"
+
+#if defined (GDK_WINDOWING_QUARTZ)
 #include "gimppickbutton-quartz.h"
+#elif defined (GDK_WINDOWING_WIN32)
+#include "gimppickbutton-win32.h"
+#else
+#include "gimppickbutton-default.h"
+#include "gimppickbutton-kwin.h"
+#include "gimppickbutton-xdg.h"
 #endif
 
-#include "libgimp/libgimp-intl.h"
-
 /**
  * SECTION: gimppickbutton
  * @title: GimpPickButton
@@ -145,8 +148,10 @@ gimp_pick_button_dispose (GObject *object)
 static void
 gimp_pick_button_clicked (GtkButton *button)
 {
-#ifdef GDK_WINDOWING_QUARTZ
+#if defined (GDK_WINDOWING_QUARTZ)
   _gimp_pick_button_quartz_pick (GIMP_PICK_BUTTON (button));
+#elif defined (GDK_WINDOWING_WIN32)
+  _gimp_pick_button_win32_pick (GIMP_PICK_BUTTON (button));
 #else
 #ifdef GDK_WINDOWING_X11
   /* It's a bit weird as we use the default pick code both in first and
diff --git a/libgimpwidgets/meson.build b/libgimpwidgets/meson.build
index ee86bce8e2..8898ad92a5 100644
--- a/libgimpwidgets/meson.build
+++ b/libgimpwidgets/meson.build
@@ -189,6 +189,10 @@ if gtk3_macos.found()
   libgimpwidgets_sources += [
     'gimppickbutton-quartz.c',
   ]
+elif platform_windows
+  libgimpwidgets_sources += [
+    'gimppickbutton-win32.c',
+  ]
 else
   libgimpwidgets_sources += [
     'gimppickbutton-default.c',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]