aseprite/src/she/win/window.cpp

1377 lines
38 KiB
C++

// SHE library
// Copyright (C) 2012-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "she/win/window.h"
#include <windowsx.h>
#include <commctrl.h>
#include <shellapi.h>
#include <sstream>
#include "base/base.h"
#include "base/debug.h"
#include "base/log.h"
#include "gfx/size.h"
#include "she/event.h"
#include "she/native_cursor.h"
#include "she/win/system.h"
#include "she/win/vk.h"
#include "she/win/window_dde.h"
#define SHE_WND_CLASS_NAME L"Aseprite.Window"
#define MOUSE_TRACE(...)
// Gets the window client are in absolute/screen coordinates
#define ABS_CLIENT_RC(rc) \
RECT rc; \
GetClientRect(m_hwnd, &rc); \
MapWindowPoints(m_hwnd, NULL, (POINT*)&rc, 2)
// Not yet ready because if we start receiving WM_POINTERDOWN messages
// instead of WM_LBUTTONDBLCLK we lost the automatic double-click
// messages.
#define USE_EnableMouseInPointer 0
#ifndef INTERACTION_CONTEXT_PROPERTY_MEASUREMENT_UNITS_SCREEN
#define INTERACTION_CONTEXT_PROPERTY_MEASUREMENT_UNITS_SCREEN 1
#endif
namespace she {
WinWindow::WinWindow(int width, int height, int scale)
: m_hwnd(nullptr)
, m_hcursor(nullptr)
, m_clientSize(1, 1)
, m_restoredSize(0, 0)
, m_scale(scale)
, m_isCreated(false)
, m_translateDeadKeys(false)
, m_hasMouse(false)
, m_captureMouse(false)
, m_customHcursor(false)
, m_usePointerApi(false)
, m_ignoreMouseMessages(false)
, m_lastPointerId(0)
, m_ictx(nullptr)
, m_hpenctx(nullptr)
, m_pointerType(PointerType::Unknown)
, m_pressure(0.0)
{
auto& winApi = system()->winApi();
if (winApi.EnableMouseInPointer &&
winApi.IsMouseInPointerEnabled &&
winApi.GetPointerInfo &&
winApi.GetPointerPenInfo) {
#if USE_EnableMouseInPointer == 1
if (!winApi.IsMouseInPointerEnabled()) {
// Prefer pointer messages (WM_POINTER*) since Windows 8 instead
// of mouse messages (WM_MOUSE*)
winApi.EnableMouseInPointer(TRUE);
m_ignoreMouseMessages = (winApi.IsMouseInPointerEnabled() ? true: false);
}
#endif
// Initialize a Interaction Context to convert WM_POINTER messages
// into gestures processed by handleInteractionContextOutput().
if (winApi.CreateInteractionContext &&
winApi.RegisterOutputCallbackInteractionContext &&
winApi.SetInteractionConfigurationInteractionContext) {
HRESULT hr = winApi.CreateInteractionContext(&m_ictx);
if (SUCCEEDED(hr)) {
hr = winApi.RegisterOutputCallbackInteractionContext(
m_ictx, &WinWindow::staticInteractionContextCallback, this);
}
if (SUCCEEDED(hr)) {
INTERACTION_CONTEXT_CONFIGURATION cfg[] = {
{ INTERACTION_ID_MANIPULATION,
INTERACTION_CONFIGURATION_FLAG_MANIPULATION |
INTERACTION_CONFIGURATION_FLAG_MANIPULATION_TRANSLATION_X |
INTERACTION_CONFIGURATION_FLAG_MANIPULATION_TRANSLATION_Y |
INTERACTION_CONFIGURATION_FLAG_MANIPULATION_SCALING |
INTERACTION_CONFIGURATION_FLAG_MANIPULATION_TRANSLATION_INERTIA |
INTERACTION_CONFIGURATION_FLAG_MANIPULATION_SCALING_INERTIA },
{ INTERACTION_ID_TAP,
INTERACTION_CONFIGURATION_FLAG_TAP |
INTERACTION_CONFIGURATION_FLAG_TAP_DOUBLE },
{ INTERACTION_ID_SECONDARY_TAP,
INTERACTION_CONFIGURATION_FLAG_SECONDARY_TAP },
{ INTERACTION_ID_HOLD,
INTERACTION_CONFIGURATION_FLAG_NONE },
{ INTERACTION_ID_DRAG,
INTERACTION_CONFIGURATION_FLAG_NONE },
{ INTERACTION_ID_CROSS_SLIDE,
INTERACTION_CONFIGURATION_FLAG_NONE }
};
hr = winApi.SetInteractionConfigurationInteractionContext(
m_ictx, sizeof(cfg) / sizeof(INTERACTION_CONTEXT_CONFIGURATION), cfg);
}
if (SUCCEEDED(hr)) {
hr = winApi.SetPropertyInteractionContext(
m_ictx,
INTERACTION_CONTEXT_PROPERTY_MEASUREMENT_UNITS,
INTERACTION_CONTEXT_PROPERTY_MEASUREMENT_UNITS_SCREEN);
}
}
m_usePointerApi = true;
}
registerClass();
// The HWND returned by CreateWindowEx() is different than the
// HWND used in WM_CREATE message.
m_hwnd = createHwnd(this, width, height);
if (!m_hwnd)
throw std::runtime_error("Error creating window");
SetWindowLongPtr(m_hwnd, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(this));
// This flag is used to avoid calling T::resizeImpl() when we
// add the scrollbars to the window. (As the T type could not be
// fully initialized yet.)
m_isCreated = true;
}
WinWindow::~WinWindow()
{
auto& winApi = system()->winApi();
if (m_ictx && winApi.DestroyInteractionContext)
winApi.DestroyInteractionContext(m_ictx);
if (m_hwnd)
DestroyWindow(m_hwnd);
}
void WinWindow::queueEvent(Event& ev)
{
onQueueEvent(ev);
}
void WinWindow::setScale(int scale)
{
m_scale = scale;
onResize(m_clientSize);
}
void WinWindow::setVisible(bool visible)
{
if (visible) {
ShowWindow(m_hwnd, SW_SHOWNORMAL);
UpdateWindow(m_hwnd);
DrawMenuBar(m_hwnd);
}
else
ShowWindow(m_hwnd, SW_HIDE);
}
void WinWindow::maximize()
{
ShowWindow(m_hwnd, SW_MAXIMIZE);
}
bool WinWindow::isMaximized() const
{
return (IsZoomed(m_hwnd) ? true: false);
}
bool WinWindow::isMinimized() const
{
return (GetWindowLong(m_hwnd, GWL_STYLE) & WS_MINIMIZE ? true: false);
}
gfx::Size WinWindow::clientSize() const
{
return m_clientSize;
}
gfx::Size WinWindow::restoredSize() const
{
return m_restoredSize;
}
void WinWindow::setTitle(const std::string& title)
{
SetWindowText(m_hwnd, base::from_utf8(title).c_str());
}
void WinWindow::captureMouse()
{
m_captureMouse = true;
if (GetCapture() != m_hwnd) {
MOUSE_TRACE("SetCapture\n");
SetCapture(m_hwnd);
}
}
void WinWindow::releaseMouse()
{
m_captureMouse = false;
if (GetCapture() == m_hwnd) {
MOUSE_TRACE("ReleaseCapture\n");
ReleaseCapture();
}
}
void WinWindow::setMousePosition(const gfx::Point& position)
{
POINT pos = { position.x * m_scale,
position.y * m_scale };
ClientToScreen(m_hwnd, &pos);
SetCursorPos(pos.x, pos.y);
}
bool WinWindow::setNativeMouseCursor(NativeCursor cursor)
{
HCURSOR hcursor = NULL;
switch (cursor) {
case kNoCursor:
// Do nothing, just set to null
break;
case kArrowCursor:
hcursor = LoadCursor(NULL, IDC_ARROW);
break;
case kCrosshairCursor:
hcursor = LoadCursor(NULL, IDC_CROSS);
break;
case kIBeamCursor:
hcursor = LoadCursor(NULL, IDC_IBEAM);
break;
case kWaitCursor:
hcursor = LoadCursor(NULL, IDC_WAIT);
break;
case kLinkCursor:
hcursor = LoadCursor(NULL, IDC_HAND);
break;
case kHelpCursor:
hcursor = LoadCursor(NULL, IDC_HELP);
break;
case kForbiddenCursor:
hcursor = LoadCursor(NULL, IDC_NO);
break;
case kMoveCursor:
hcursor = LoadCursor(NULL, IDC_SIZEALL);
break;
case kSizeNCursor:
case kSizeNSCursor:
case kSizeSCursor:
hcursor = LoadCursor(NULL, IDC_SIZENS);
break;
case kSizeECursor:
case kSizeWCursor:
case kSizeWECursor:
hcursor = LoadCursor(NULL, IDC_SIZEWE);
break;
case kSizeNWCursor:
case kSizeSECursor:
hcursor = LoadCursor(NULL, IDC_SIZENWSE);
break;
case kSizeNECursor:
case kSizeSWCursor:
hcursor = LoadCursor(NULL, IDC_SIZENESW);
break;
}
return setCursor(hcursor, false);
}
bool WinWindow::setNativeMouseCursor(const she::Surface* surface,
const gfx::Point& focus,
const int scale)
{
ASSERT(surface);
SurfaceFormatData format;
surface->getFormat(&format);
// Only for 32bpp surfaces
if (format.bitsPerPixel != 32)
return false;
// Based on the following article "How To Create an Alpha
// Blended Cursor or Icon in Windows XP":
// https://support.microsoft.com/en-us/kb/318876
int w = scale*surface->width();
int h = scale*surface->height();
BITMAPV5HEADER bi;
ZeroMemory(&bi, sizeof(BITMAPV5HEADER));
bi.bV5Size = sizeof(BITMAPV5HEADER);
bi.bV5Width = w;
bi.bV5Height = h;
bi.bV5Planes = 1;
bi.bV5BitCount = 32;
bi.bV5Compression = BI_BITFIELDS;
bi.bV5RedMask = 0x00ff0000;
bi.bV5GreenMask = 0x0000ff00;
bi.bV5BlueMask = 0x000000ff;
bi.bV5AlphaMask = 0xff000000;
uint32_t* bits;
HDC hdc = GetDC(nullptr);
HBITMAP hbmp = CreateDIBSection(
hdc, (BITMAPINFO*)&bi, DIB_RGB_COLORS,
(void**)&bits, NULL, (DWORD)0);
ReleaseDC(nullptr, hdc);
if (!hbmp)
return false;
for (int y=0; y<h; ++y) {
const uint32_t* ptr = (const uint32_t*)surface->getData(0, (h-1-y)/scale);
for (int x=0, u=0; x<w; ++x, ++bits) {
uint32_t c = *ptr;
*bits =
(((c & format.alphaMask) >> format.alphaShift) << 24) |
(((c & format.redMask ) >> format.redShift ) << 16) |
(((c & format.greenMask) >> format.greenShift) << 8) |
(((c & format.blueMask ) >> format.blueShift ));
if (++u == scale) {
u = 0;
++ptr;
}
}
}
// Create an empty mask bitmap.
HBITMAP hmonobmp = CreateBitmap(w, h, 1, 1, nullptr);
if (!hmonobmp) {
DeleteObject(hbmp);
return false;
}
ICONINFO ii;
ii.fIcon = FALSE;
ii.xHotspot = scale*focus.x + scale/2;
ii.yHotspot = scale*focus.y + scale/2;
ii.hbmMask = hmonobmp;
ii.hbmColor = hbmp;
HCURSOR hcursor = CreateIconIndirect(&ii);
DeleteObject(hbmp);
DeleteObject(hmonobmp);
return setCursor(hcursor, true);
}
void WinWindow::updateWindow(const gfx::Rect& bounds)
{
RECT rc = { bounds.x*m_scale,
bounds.y*m_scale,
bounds.x*m_scale+bounds.w*m_scale,
bounds.y*m_scale+bounds.h*m_scale };
InvalidateRect(m_hwnd, &rc, FALSE);
UpdateWindow(m_hwnd);
}
std::string WinWindow::getLayout()
{
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
if (GetWindowPlacement(m_hwnd, &wp)) {
std::ostringstream s;
s << 1 << ' '
<< wp.flags << ' '
<< wp.showCmd << ' '
<< wp.ptMinPosition.x << ' '
<< wp.ptMinPosition.y << ' '
<< wp.ptMaxPosition.x << ' '
<< wp.ptMaxPosition.y << ' '
<< wp.rcNormalPosition.left << ' '
<< wp.rcNormalPosition.top << ' '
<< wp.rcNormalPosition.right << ' '
<< wp.rcNormalPosition.bottom;
return s.str();
}
return "";
}
void WinWindow::setLayout(const std::string& layout)
{
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
std::istringstream s(layout);
int ver;
s >> ver;
if (ver == 1) {
s >> wp.flags
>> wp.showCmd
>> wp.ptMinPosition.x
>> wp.ptMinPosition.y
>> wp.ptMaxPosition.x
>> wp.ptMaxPosition.y
>> wp.rcNormalPosition.left
>> wp.rcNormalPosition.top
>> wp.rcNormalPosition.right
>> wp.rcNormalPosition.bottom;
}
else
return;
if (SetWindowPlacement(m_hwnd, &wp)) {
// TODO use the return value
}
}
void WinWindow::setTranslateDeadKeys(bool state)
{
m_translateDeadKeys = state;
// Here we clear dead keys so we don't get those keys in the new
// "translate dead keys" state. E.g. If we focus a text entry
// field and the translation of dead keys is enabled, we don't
// want to get previous dead keys. The same in case we leave the
// text field with a pending dead key, that dead key must be
// discarded.
VkToUnicode tu;
if (tu) {
tu.toUnicode(VK_SPACE, 0);
if (tu.size() != 0)
tu.toUnicode(VK_SPACE, 0);
}
}
bool WinWindow::setCursor(HCURSOR hcursor, bool custom)
{
SetCursor(hcursor);
if (m_hcursor && m_customHcursor)
DestroyIcon(m_hcursor);
m_hcursor = hcursor;
m_customHcursor = custom;
return (hcursor ? true: false);
}
LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
{
switch (msg) {
case WM_CREATE:
LOG("WIN: Creating window %p\n", m_hwnd);
if (system()->useWintabAPI()) {
// Attach Wacom context
m_hpenctx = system()->penApi().open(m_hwnd);
}
break;
case WM_DESTROY:
LOG("WIN: Destroying window %p (pen context %p)\n",
m_hwnd, m_hpenctx);
if (m_hpenctx) {
system()->penApi().close(m_hpenctx);
m_hpenctx = nullptr;
}
break;
case WM_SETCURSOR:
if (LOWORD(lparam) == HTCLIENT) {
SetCursor(m_hcursor);
return TRUE;
}
break;
case WM_CLOSE: {
Event ev;
ev.setType(Event::CloseDisplay);
queueEvent(ev);
// Don't close the window, it must be closed manually after
// the CloseDisplay event is processed.
return 0;
}
case WM_PAINT:
if (m_isCreated) {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_hwnd, &ps);
onPaint(hdc);
EndPaint(m_hwnd, &ps);
return true;
}
break;
case WM_SIZE:
if (m_isCreated) {
int w = GET_X_LPARAM(lparam);
int h = GET_Y_LPARAM(lparam);
if (w > 0 && h > 0) {
m_clientSize.w = w;
m_clientSize.h = h;
onResize(m_clientSize);
}
WINDOWPLACEMENT pl;
pl.length = sizeof(pl);
if (GetWindowPlacement(m_hwnd, &pl)) {
m_restoredSize = gfx::Size(
pl.rcNormalPosition.right - pl.rcNormalPosition.left,
pl.rcNormalPosition.bottom - pl.rcNormalPosition.top);
}
}
break;
// Mouse and Trackpad Messages
case WM_MOUSEMOVE: {
// If the pointer API is enable, we use WM_POINTERUPDATE instead
// of WM_MOUSEMOVE. This check is here because Windows keeps
// sending us WM_MOUSEMOVE messages even when we call
// EnableMouseInPointer() (mainly when we use Alt+stylus we
// receive WM_MOUSEMOVE with the position of the mouse/trackpad
// + WM_POINTERUPDATE with the position of the pen)
if (m_ignoreMouseMessages)
break;
Event ev;
mouseEvent(lparam, ev);
MOUSE_TRACE("MOUSEMOVE xy=%d,%d\n",
ev.position().x, ev.position().y);
if (!m_hasMouse) {
m_hasMouse = true;
ev.setType(Event::MouseEnter);
queueEvent(ev);
MOUSE_TRACE("-> Event::MouseEnter\n");
// Track mouse to receive WM_MOUSELEAVE message.
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(TRACKMOUSEEVENT);
tme.dwFlags = TME_LEAVE;
tme.hwndTrack = m_hwnd;
_TrackMouseEvent(&tme);
}
if (m_pointerType != PointerType::Unknown) {
ev.setPointerType(m_pointerType);
ev.setPressure(m_pressure);
}
ev.setType(Event::MouseMove);
queueEvent(ev);
break;
}
case WM_NCMOUSEMOVE:
case WM_MOUSELEAVE:
if (m_hasMouse) {
m_hasMouse = false;
Event ev;
ev.setType(Event::MouseLeave);
ev.setModifiers(get_modifiers_from_last_win32_message());
queueEvent(ev);
MOUSE_TRACE("-> Event::MouseLeave\n");
}
break;
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_XBUTTONDOWN: {
Event ev;
mouseEvent(lparam, ev);
ev.setType(Event::MouseDown);
ev.setButton(
msg == WM_LBUTTONDOWN ? Event::LeftButton:
msg == WM_RBUTTONDOWN ? Event::RightButton:
msg == WM_MBUTTONDOWN ? Event::MiddleButton:
msg == WM_XBUTTONDOWN && GET_XBUTTON_WPARAM(wparam) == 1 ? Event::X1Button:
msg == WM_XBUTTONDOWN && GET_XBUTTON_WPARAM(wparam) == 2 ? Event::X2Button:
Event::NoneButton);
if (m_pointerType != PointerType::Unknown) {
ev.setPointerType(m_pointerType);
ev.setPressure(m_pressure);
}
queueEvent(ev);
MOUSE_TRACE("BUTTONDOWN xy=%d,%d button=%d\n",
ev.position().x, ev.position().y,
ev.button());
break;
}
case WM_LBUTTONUP:
case WM_RBUTTONUP:
case WM_MBUTTONUP:
case WM_XBUTTONUP: {
Event ev;
mouseEvent(lparam, ev);
ev.setType(Event::MouseUp);
ev.setButton(
msg == WM_LBUTTONUP ? Event::LeftButton:
msg == WM_RBUTTONUP ? Event::RightButton:
msg == WM_MBUTTONUP ? Event::MiddleButton:
msg == WM_XBUTTONUP && GET_XBUTTON_WPARAM(wparam) == 1 ? Event::X1Button:
msg == WM_XBUTTONUP && GET_XBUTTON_WPARAM(wparam) == 2 ? Event::X2Button:
Event::NoneButton);
if (m_pointerType != PointerType::Unknown) {
ev.setPointerType(m_pointerType);
ev.setPressure(m_pressure);
}
queueEvent(ev);
MOUSE_TRACE("BUTTONUP xy=%d,%d button=%d\n",
ev.position().x, ev.position().y,
ev.button());
// Avoid popup menu for scrollbars
if (msg == WM_RBUTTONUP)
return 0;
break;
}
case WM_LBUTTONDBLCLK:
case WM_MBUTTONDBLCLK:
case WM_RBUTTONDBLCLK:
case WM_XBUTTONDBLCLK: {
Event ev;
mouseEvent(lparam, ev);
ev.setType(Event::MouseDoubleClick);
ev.setButton(
msg == WM_LBUTTONDBLCLK ? Event::LeftButton:
msg == WM_RBUTTONDBLCLK ? Event::RightButton:
msg == WM_MBUTTONDBLCLK ? Event::MiddleButton:
msg == WM_XBUTTONDBLCLK && GET_XBUTTON_WPARAM(wparam) == 1 ? Event::X1Button:
msg == WM_XBUTTONDBLCLK && GET_XBUTTON_WPARAM(wparam) == 2 ? Event::X2Button:
Event::NoneButton);
if (m_pointerType != PointerType::Unknown) {
ev.setPointerType(m_pointerType);
ev.setPressure(m_pressure);
}
queueEvent(ev);
MOUSE_TRACE("BUTTONDBLCLK xy=%d,%d button=%d\n",
ev.position().x, ev.position().y,
ev.button());
break;
}
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL: {
POINT pos = { GET_X_LPARAM(lparam),
GET_Y_LPARAM(lparam) };
ScreenToClient(m_hwnd, &pos);
Event ev;
ev.setType(Event::MouseWheel);
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setPosition(gfx::Point(pos.x, pos.y) / m_scale);
int z = GET_WHEEL_DELTA_WPARAM(wparam);
if (ABS(z) >= WHEEL_DELTA)
z /= WHEEL_DELTA;
else {
// TODO use floating point numbers or something similar
// (so we could use: z /= double(WHEEL_DELTA))
z = SGN(z);
}
gfx::Point delta(
(msg == WM_MOUSEHWHEEL ? z: 0),
(msg == WM_MOUSEWHEEL ? -z: 0));
ev.setWheelDelta(delta);
queueEvent(ev);
MOUSE_TRACE("MOUSEWHEEL xy=%d,%d delta=%d,%d\n",
ev.position().x, ev.position().y,
ev.wheelDelta().x, ev.wheelDelta().y);
break;
}
case WM_HSCROLL:
case WM_VSCROLL: {
POINT pos;
GetCursorPos(&pos);
ScreenToClient(m_hwnd, &pos);
Event ev;
ev.setType(Event::MouseWheel);
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setPosition(gfx::Point(pos.x, pos.y) / m_scale);
int bar = (msg == WM_HSCROLL ? SB_HORZ: SB_VERT);
int z = GetScrollPos(m_hwnd, bar);
switch (LOWORD(wparam)) {
case SB_LEFT:
case SB_LINELEFT:
--z;
break;
case SB_PAGELEFT:
z -= 2;
break;
case SB_RIGHT:
case SB_LINERIGHT:
++z;
break;
case SB_PAGERIGHT:
z += 2;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK:
case SB_ENDSCROLL:
// Do nothing
break;
}
gfx::Point delta(
(msg == WM_HSCROLL ? (z-50): 0),
(msg == WM_VSCROLL ? (z-50): 0));
ev.setWheelDelta(delta);
SetScrollPos(m_hwnd, bar, 50, FALSE);
queueEvent(ev);
MOUSE_TRACE("HVSCROLL xy=%d,%d delta=%d,%d\n",
ev.position().x, ev.position().y,
ev.wheelDelta().x, ev.wheelDelta().y);
break;
}
// Pointer API (since Windows 8.0)
case WM_POINTERCAPTURECHANGED: {
MOUSE_TRACE("POINTERCAPTURECHANGED\n");
releaseMouse();
break;
}
case WM_POINTERENTER: {
POINTER_INFO pi;
Event ev;
if (!pointerEvent(wparam, ev, pi))
break;
MOUSE_TRACE("POINTERENTER id=%d xy=%d,%d\n",
pi.pointerId, ev.position().x, ev.position().y);
#if USE_EnableMouseInPointer == 0
// This is necessary to avoid receiving random WM_MOUSEMOVE from
// the mouse position when we use Alt+pen tip.
// TODO Remove this line when we enable EnableMouseInPointer(TRUE);
m_ignoreMouseMessages = true;
#endif
if (pi.pointerType == PT_TOUCH) {
auto& winApi = system()->winApi();
if (m_ictx && winApi.AddPointerInteractionContext)
winApi.AddPointerInteractionContext(m_ictx, pi.pointerId);
}
if (!m_hasMouse) {
m_hasMouse = true;
ev.setType(Event::MouseEnter);
queueEvent(ev);
MOUSE_TRACE("-> Event::MouseEnter\n");
}
return 0;
}
case WM_POINTERLEAVE: {
POINTER_INFO pi;
Event ev;
if (!pointerEvent(wparam, ev, pi))
break;
MOUSE_TRACE("POINTERLEAVE id=%d\n", pi.pointerId);
#if USE_EnableMouseInPointer == 0
m_ignoreMouseMessages = false;
#endif
if (pi.pointerType == PT_TOUCH) {
auto& winApi = system()->winApi();
if (m_ictx && winApi.RemovePointerInteractionContext)
winApi.RemovePointerInteractionContext(m_ictx, pi.pointerId);
}
#if 0 // Don't generate MouseLeave from pen/touch messages
// TODO we should generate this message, but after this touch
// messages don't work anymore, so we have to fix that problem.
if (m_hasMouse) {
m_hasMouse = false;
ev.setType(Event::MouseLeave);
queueEvent(ev);
MOUSE_TRACE("-> Event::MouseLeave\n");
return 0;
}
#endif
break;
}
case WM_POINTERDOWN: {
POINTER_INFO pi;
Event ev;
if (!pointerEvent(wparam, ev, pi))
break;
if (pi.pointerType == PT_TOUCH) {
auto& winApi = system()->winApi();
if (m_ictx && winApi.ProcessPointerFramesInteractionContext) {
winApi.ProcessPointerFramesInteractionContext(m_ictx, 1, 1, &pi);
return 0;
}
}
ev.setType(Event::MouseDown);
ev.setButton(
pi.ButtonChangeType == POINTER_CHANGE_FIRSTBUTTON_DOWN ? Event::LeftButton:
pi.ButtonChangeType == POINTER_CHANGE_SECONDBUTTON_DOWN ? Event::RightButton:
pi.ButtonChangeType == POINTER_CHANGE_THIRDBUTTON_DOWN ? Event::MiddleButton:
pi.ButtonChangeType == POINTER_CHANGE_FOURTHBUTTON_DOWN ? Event::X1Button:
pi.ButtonChangeType == POINTER_CHANGE_FIFTHBUTTON_DOWN ? Event::X2Button:
Event::NoneButton);
queueEvent(ev);
MOUSE_TRACE("POINTERDOWN id=%d xy=%d,%d button=%d\n",
pi.pointerId, ev.position().x, ev.position().y,
ev.button());
return 0;
}
case WM_POINTERUP: {
POINTER_INFO pi;
Event ev;
if (!pointerEvent(wparam, ev, pi))
break;
if (pi.pointerType == PT_TOUCH) {
auto& winApi = system()->winApi();
if (m_ictx && winApi.ProcessPointerFramesInteractionContext) {
winApi.ProcessPointerFramesInteractionContext(m_ictx, 1, 1, &pi);
return 0;
}
}
ev.setType(Event::MouseUp);
ev.setButton(
pi.ButtonChangeType == POINTER_CHANGE_FIRSTBUTTON_UP ? Event::LeftButton:
pi.ButtonChangeType == POINTER_CHANGE_SECONDBUTTON_UP ? Event::RightButton:
pi.ButtonChangeType == POINTER_CHANGE_THIRDBUTTON_UP ? Event::MiddleButton:
pi.ButtonChangeType == POINTER_CHANGE_FOURTHBUTTON_UP ? Event::X1Button:
pi.ButtonChangeType == POINTER_CHANGE_FIFTHBUTTON_UP ? Event::X2Button:
Event::NoneButton);
queueEvent(ev);
MOUSE_TRACE("POINTERUP id=%d xy=%d,%d button=%d\n",
pi.pointerId, ev.position().x, ev.position().y,
ev.button());
return 0;
}
case WM_POINTERUPDATE: {
POINTER_INFO pi;
Event ev;
if (!pointerEvent(wparam, ev, pi))
break;
if (pi.pointerType == PT_TOUCH) {
auto& winApi = system()->winApi();
if (m_ictx && winApi.ProcessPointerFramesInteractionContext) {
winApi.ProcessPointerFramesInteractionContext(m_ictx, 1, 1, &pi);
return 0;
}
}
if (!m_hasMouse) {
m_hasMouse = true;
ev.setType(Event::MouseEnter);
queueEvent(ev);
MOUSE_TRACE("-> Event::MouseEnter\n");
}
ev.setType(Event::MouseMove);
queueEvent(ev);
MOUSE_TRACE("POINTERUPDATE id=%d xy=%d,%d\n",
pi.pointerId, ev.position().x, ev.position().y);
return 0;
}
case WM_POINTERWHEEL:
case WM_POINTERHWHEEL: {
POINTER_INFO pi;
Event ev;
if (!pointerEvent(wparam, ev, pi))
break;
ev.setType(Event::MouseWheel);
int z = GET_WHEEL_DELTA_WPARAM(wparam);
if (ABS(z) >= WHEEL_DELTA)
z /= WHEEL_DELTA;
else {
// TODO use floating point numbers or something similar
// (so we could use: z /= double(WHEEL_DELTA))
z = SGN(z);
}
gfx::Point delta(
(msg == WM_POINTERHWHEEL ? z: 0),
(msg == WM_POINTERWHEEL ? -z: 0));
ev.setWheelDelta(delta);
queueEvent(ev);
MOUSE_TRACE("POINTERWHEEL xy=%d,%d delta=%d,%d\n",
ev.position().x, ev.position().y,
ev.wheelDelta().x, ev.wheelDelta().y);
return 0;
}
// Keyboard Messages
case WM_SYSKEYDOWN:
case WM_KEYDOWN: {
int vk = wparam;
int scancode = (lparam >> 16) & 0xff;
bool sendMsg = true;
Event ev;
ev.setType(Event::KeyDown);
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setScancode(win32vk_to_scancode(vk));
ev.setUnicodeChar(0);
ev.setRepeat(MAX(0, (lparam & 0xffff)-1));
{
VkToUnicode tu;
if (tu) {
tu.toUnicode(vk, scancode);
if (tu.isDeadKey()) {
ev.setDeadKey(true);
ev.setUnicodeChar(tu[0]);
if (!m_translateDeadKeys)
tu.toUnicode(vk, scancode); // Call again to remove dead-key
}
else if (tu.size() > 0) {
sendMsg = false;
for (int chr : tu) {
ev.setUnicodeChar(chr);
queueEvent(ev);
}
}
}
}
if (sendMsg)
queueEvent(ev);
return 0;
}
case WM_SYSKEYUP:
case WM_KEYUP: {
Event ev;
ev.setType(Event::KeyUp);
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setScancode(win32vk_to_scancode(wparam));
ev.setUnicodeChar(0);
ev.setRepeat(MAX(0, (lparam & 0xffff)-1));
queueEvent(ev);
// TODO If we use native menus, this message should be given
// to the DefWindowProc() in some cases (e.g. F10 or Alt keys)
return 0;
}
case WM_MENUCHAR:
// Avoid playing a sound when Alt+key is pressed and it's not in a native menu
return MAKELONG(0, MNC_CLOSE);
case WM_DROPFILES: {
HDROP hdrop = (HDROP)(wparam);
Event::Files files;
int count = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
for (int index=0; index<count; ++index) {
int length = DragQueryFile(hdrop, index, NULL, 0);
if (length > 0) {
std::vector<TCHAR> str(length+1);
DragQueryFile(hdrop, index, &str[0], str.size());
files.push_back(base::to_utf8(&str[0]));
}
}
DragFinish(hdrop);
Event ev;
ev.setType(Event::DropFiles);
ev.setFiles(files);
queueEvent(ev);
break;
}
case WM_NCCALCSIZE: {
if (wparam) {
// Scrollbars must be enabled and visible to get trackpad
// events of old drivers. So we cannot use ShowScrollBar() to
// hide them. This is a simple (maybe not so elegant)
// solution: Expand the client area to we overlap the
// scrollbars. In this way they are not visible, but we still
// get their messages.
NCCALCSIZE_PARAMS* cs = reinterpret_cast<NCCALCSIZE_PARAMS*>(lparam);
cs->rgrc[0].right += GetSystemMetrics(SM_CYVSCROLL);
cs->rgrc[0].bottom += GetSystemMetrics(SM_CYHSCROLL);
}
break;
}
case WM_NCHITTEST: {
LRESULT result = CallWindowProc(DefWindowProc, m_hwnd, msg, wparam, lparam);
gfx::Point pt(GET_X_LPARAM(lparam),
GET_Y_LPARAM(lparam));
ABS_CLIENT_RC(rc);
gfx::Rect area(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top);
//LOG("NCHITTEST: %d %d - %d %d %d %d - %s\n", pt.x, pt.y, area.x, area.y, area.w, area.h, area.contains(pt) ? "true": "false");
// We ignore scrollbars so if the mouse is above them, we return
// as it's in the window client or resize area. (Remember that
// we have scroll bars are enabled and visible to receive
// trackpad messages only.)
if (result == HTHSCROLL) {
result = (area.contains(pt) ? HTCLIENT: HTBOTTOM);
}
else if (result == HTVSCROLL) {
result = (area.contains(pt) ? HTCLIENT: HTRIGHT);
}
return result;
}
// Wintab API Messages
case WT_PROXIMITY: {
bool entering_ctx = (LOWORD(lparam) ? true: false);
if (!entering_ctx)
m_pointerType = PointerType::Unknown;
break;
}
case WT_CSRCHANGE: { // From Wintab 1.1
auto& api = system()->penApi();
UINT serial = wparam;
HCTX ctx = (HCTX)lparam;
PACKET packet;
if (api.packet(ctx, serial, &packet)) {
switch (packet.pkCursor) {
case 0:
case 3:
m_pointerType = PointerType::Cursor;
break;
case 1:
case 4:
m_pointerType = PointerType::Pen;
break;
case 2:
case 5:
m_pointerType = PointerType::Eraser;
break;
default:
m_pointerType = PointerType::Unknown;
break;
}
}
else
m_pointerType = PointerType::Unknown;
}
case WT_PACKET: {
auto& api = system()->penApi();
UINT serial = wparam;
HCTX ctx = (HCTX)lparam;
PACKET packet;
if (api.packet(ctx, serial, &packet)) {
m_pressure = packet.pkNormalPressure / 1000.0; // TODO get the maximum value
if (packet.pkCursor == 2 || packet.pkCursor == 5)
m_pointerType = PointerType::Eraser;
else
m_pointerType = PointerType::Pen;
}
else
m_pointerType = PointerType::Unknown;
break;
}
}
LRESULT result = FALSE;
if (handle_dde_messages(m_hwnd, msg, wparam, lparam, result))
return result;
return DefWindowProc(m_hwnd, msg, wparam, lparam);
}
void WinWindow::mouseEvent(LPARAM lparam, Event& ev)
{
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setPosition(gfx::Point(
GET_X_LPARAM(lparam) / m_scale,
GET_Y_LPARAM(lparam) / m_scale));
}
bool WinWindow::pointerEvent(WPARAM wparam, Event& ev, POINTER_INFO& pi)
{
if (!m_usePointerApi)
return false;
auto& winApi = system()->winApi();
if (!winApi.GetPointerInfo(GET_POINTERID_WPARAM(wparam), &pi))
return false;
ABS_CLIENT_RC(rc);
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setPosition(gfx::Point((pi.ptPixelLocation.x - rc.left) / m_scale,
(pi.ptPixelLocation.y - rc.top) / m_scale));
switch (pi.pointerType) {
case PT_MOUSE: {
ev.setPointerType(PointerType::Mouse);
break;
}
case PT_TOUCH: {
ev.setPointerType(PointerType::Touch);
break;
}
case PT_TOUCHPAD: {
ev.setPointerType(PointerType::Touchpad);
break;
}
case PT_PEN: {
ev.setPointerType(PointerType::Pen);
POINTER_PEN_INFO ppi;
if (winApi.GetPointerPenInfo(pi.pointerId, &ppi)) {
if (ppi.penFlags & PEN_FLAG_ERASER)
ev.setPointerType(PointerType::Eraser);
}
break;
}
}
m_lastPointerId = pi.pointerId;
return true;
}
void WinWindow::handleInteractionContextOutput(
const INTERACTION_CONTEXT_OUTPUT* output)
{
MOUSE_TRACE("%s (%d) xy=%.16g %.16g flags=%d type=%d\n",
output->interactionId == INTERACTION_ID_MANIPULATION ? "INTERACTION_ID_MANIPULATION":
output->interactionId == INTERACTION_ID_TAP ? "INTERACTION_ID_TAP":
output->interactionId == INTERACTION_ID_SECONDARY_TAP ? "INTERACTION_ID_SECONDARY_TAP":
output->interactionId == INTERACTION_ID_HOLD ? "INTERACTION_ID_HOLD": "INTERACTION_ID_???",
output->interactionId,
output->x, output->y,
output->interactionFlags,
output->inputType);
// We use the InteractionContext to interpret touch gestures only.
if (output->inputType == PT_TOUCH) {
ABS_CLIENT_RC(rc);
gfx::Point pos(int((output->x - rc.left) / m_scale),
int((output->y - rc.top) / m_scale));
Event ev;
ev.setModifiers(get_modifiers_from_last_win32_message());
ev.setPosition(pos);
switch (output->interactionId) {
case INTERACTION_ID_MANIPULATION: {
MOUSE_TRACE(" - delta xy=%.16g %.16g scale=%.16g expansion=%.16g rotation=%.16g\n",
output->arguments.manipulation.delta.translationX,
output->arguments.manipulation.delta.translationY,
output->arguments.manipulation.delta.scale,
output->arguments.manipulation.delta.expansion,
output->arguments.manipulation.delta.rotation);
gfx::Point delta(-int(output->arguments.manipulation.delta.translationX) / m_scale,
-int(output->arguments.manipulation.delta.translationY) / m_scale);
ev.setType(Event::MouseWheel);
ev.setWheelDelta(delta);
ev.setPreciseWheel(true);
queueEvent(ev);
ev.setType(Event::TouchMagnify);
ev.setMagnification(output->arguments.manipulation.delta.scale - 1.0);
queueEvent(ev);
break;
}
case INTERACTION_ID_TAP:
MOUSE_TRACE(" - count=%d\n", output->arguments.tap.count);
ev.setButton(Event::LeftButton);
if (output->arguments.tap.count == 2) {
ev.setType(Event::MouseDoubleClick); queueEvent(ev);
}
else {
ev.setType(Event::MouseDown); queueEvent(ev);
ev.setType(Event::MouseUp); queueEvent(ev);
}
break;
case INTERACTION_ID_SECONDARY_TAP:
case INTERACTION_ID_HOLD:
ev.setButton(Event::RightButton);
ev.setType(Event::MouseDown); queueEvent(ev);
ev.setType(Event::MouseUp); queueEvent(ev);
break;
}
}
}
//static
void WinWindow::registerClass()
{
HMODULE instance = GetModuleHandle(nullptr);
WNDCLASSEX wcex;
if (GetClassInfoEx(instance, SHE_WND_CLASS_NAME, &wcex))
return; // Already registered
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_DBLCLKS;
wcex.lpfnWndProc = &WinWindow::staticWndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = instance;
wcex.hIcon = LoadIcon(instance, L"0");
wcex.hCursor = NULL;
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = nullptr;
wcex.lpszClassName = SHE_WND_CLASS_NAME;
wcex.hIconSm = nullptr;
if (RegisterClassEx(&wcex) == 0)
throw std::runtime_error("Error registering window class");
}
//static
HWND WinWindow::createHwnd(WinWindow* self, int width, int height)
{
HWND hwnd = CreateWindowEx(
WS_EX_APPWINDOW | WS_EX_ACCEPTFILES,
SHE_WND_CLASS_NAME,
L"",
WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
width, height,
nullptr,
nullptr,
GetModuleHandle(nullptr),
reinterpret_cast<LPVOID>(self));
if (!hwnd)
return nullptr;
// Center the window
RECT workarea;
if (SystemParametersInfo(SPI_GETWORKAREA, 0, (PVOID)&workarea, 0)) {
SetWindowPos(hwnd, nullptr,
(workarea.right-workarea.left)/2-width/2,
(workarea.bottom-workarea.top)/2-height/2, 0, 0,
SWP_NOSIZE |
SWP_NOSENDCHANGING |
SWP_NOOWNERZORDER |
SWP_NOZORDER |
SWP_NOREDRAW);
}
// Set scroll info to receive WM_HSCROLL/VSCROLL events (events
// generated by some trackpad drivers).
SCROLLINFO si;
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nPos = 50;
si.nMax = 100;
si.nPage = 10;
SetScrollInfo(hwnd, SB_HORZ, &si, FALSE);
SetScrollInfo(hwnd, SB_VERT, &si, FALSE);
return hwnd;
}
//static
LRESULT CALLBACK WinWindow::staticWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
WinWindow* wnd = nullptr;
if (msg == WM_CREATE) {
wnd =
reinterpret_cast<WinWindow*>(
reinterpret_cast<LPCREATESTRUCT>(lparam)->lpCreateParams);
if (wnd && wnd->m_hwnd == nullptr)
wnd->m_hwnd = hwnd;
}
else {
wnd = reinterpret_cast<WinWindow*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
// Check that the user data makes sense
if (wnd && wnd->m_hwnd != hwnd)
wnd = nullptr;
}
if (wnd) {
ASSERT(wnd->m_hwnd == hwnd);
return wnd->wndProc(msg, wparam, lparam);
}
else {
return DefWindowProc(hwnd, msg, wparam, lparam);
}
}
//static
void CALLBACK WinWindow::staticInteractionContextCallback(
void* clientData,
const INTERACTION_CONTEXT_OUTPUT* output)
{
WinWindow* self = reinterpret_cast<WinWindow*>(clientData);
self->handleInteractionContextOutput(output);
}
// static
WindowSystem* WinWindow::system()
{
return static_cast<WindowSystem*>(she::instance());
}
} // namespace she