summaryrefslogtreecommitdiffstats
path: root/libgimpwidgets/gimppickbutton-win32.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--libgimpwidgets/gimppickbutton-win32.c1135
1 files changed, 1135 insertions, 0 deletions
diff --git a/libgimpwidgets/gimppickbutton-win32.c b/libgimpwidgets/gimppickbutton-win32.c
new file mode 100644
index 0000000..d5ca8b7
--- /dev/null
+++ b/libgimpwidgets/gimppickbutton-win32.c
@@ -0,0 +1,1135 @@
+/* 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-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;
+}