1137 lines
31 KiB
C
1137 lines
31 KiB
C
|
/* LIBPIKA - The PIKA Library
|
||
|
* Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
|
||
|
*
|
||
|
* pikapickbutton-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 "libpikacolor/pikacolor.h"
|
||
|
|
||
|
#include "pikawidgetstypes.h"
|
||
|
#include "pikapickbutton.h"
|
||
|
#include "pikapickbutton-private.h"
|
||
|
#include "pikapickbutton-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);
|
||
|
|
||
|
/* PikaPickButtonWin32 */
|
||
|
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 PikaRGB pick_color_with_gdi (POINT physical_point);
|
||
|
static void user_picked (MonitorData *monitor,
|
||
|
POINT physical_point);
|
||
|
void _pika_pick_button_win32_pick (PikaPickButton *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 ("/technology.heckin/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 = SetWindowsHookExW (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 = SetWindowsHookExW (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"PikaPickButtonInputWindowClass";
|
||
|
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"Pika 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"PikaPickButtonNotifWindowClass";
|
||
|
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"Pika 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 ();
|
||
|
}
|
||
|
|
||
|
/* }}}
|
||
|
* {{{ PikaPickButtonWin32 */
|
||
|
|
||
|
/* 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 PikaRGB
|
||
|
pick_color_with_gdi (POINT physical_point)
|
||
|
{
|
||
|
PikaRGB 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);
|
||
|
|
||
|
pika_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)
|
||
|
{
|
||
|
PikaRGB rgb;
|
||
|
GList *l;
|
||
|
|
||
|
/* Currently unused */
|
||
|
(void) monitor;
|
||
|
|
||
|
rgb = pick_color_with_gdi (physical_point);
|
||
|
|
||
|
for (l = pickers; l != NULL; l = l->next)
|
||
|
{
|
||
|
PikaPickButton *button = PIKA_PICK_BUTTON (l->data);
|
||
|
g_signal_emit_by_name (button, "color-picked", &rgb);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* entry point to this file, called from pikapickbutton.c */
|
||
|
void
|
||
|
_pika_pick_button_win32_pick (PikaPickButton *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;
|
||
|
}
|