1 /* 2 Cockos WDL License 3 4 Copyright (C) 2005 - 2015 Cockos Incorporated 5 Copyright (C) 2015 - 2016 Auburn Sounds 6 7 Portions copyright other contributors, see each source file for more information 8 9 This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. 10 11 Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 14 1. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 15 1. This notice may not be removed or altered from any source distribution. 16 */ 17 /** 18 Win32 window implementation. 19 */ 20 module dplug.window.win32window; 21 22 import dplug.math.vector; 23 import dplug.math.box; 24 25 import dplug.core.runtime; 26 import dplug.core.nogc; 27 import dplug.core.vec; 28 29 import dplug.graphics.image; 30 31 import dplug.window.window; 32 33 34 version(legacyMouseCursor) 35 { 36 version = noCursors; // FUTURE: tell to replace with Dplug_NoMouseCursor 37 } 38 else version(Dplug_NoMouseCursor) 39 { 40 version = noCursors; 41 } 42 43 44 // Hook the mouse callback so that you can use mouse wheel while dragging outside of the plugin 45 // window boundaries. This is most useful for eg. EQ plugins where you would want to use mouse 46 // wheel while dragging a point out of the plugin window. 47 // This option is Windows-only, it is ignored on Linux and macOS, where such a thing always works. 48 // Dragging + mouse wheel inside the plugin window doesn't need that option. 49 // See: https://github.com/AuburnSounds/Dplug/wiki/More-Options 50 version(Dplug_MouseWheelOutsideWindow) 51 { 52 // Note: can be annoying while debugging, as it is a global mouse hook. 53 // But the only way to drag outside of window with mouse wheel working. 54 // It is amazing that Windows even allow that. 55 debug 56 { 57 } 58 else 59 { 60 version = hookMouseForWheelEvents; 61 } 62 } 63 64 nothrow: 65 @nogc: 66 67 68 // Disabled, remaining issues in #536 69 // - very few hosts actually give the needed DPI 70 // - resizing that window is in some cases a challenge 71 // - using "mixed" DPI support to force Hi-DPI looks like a bad idea 72 // - not always easy to resize the child window 73 // - need testing in Windows 8 or something where some calls are missing 74 enum DPISupportWin32 = false; 75 76 version(Windows) 77 { 78 import dplug.core.random; 79 80 import core.sys.windows.windef; 81 import core.sys.windows.winuser; 82 import core.sys.windows.winbase; 83 import core.sys.windows.wingdi; 84 85 HINSTANCE getModuleHandle() nothrow @nogc 86 { 87 return GetModuleHandleA(null); 88 } 89 90 final class Win32Window : IWindow 91 { 92 public: 93 nothrow: 94 @nogc: 95 96 this(HWND parentWindow, IWindowListener listener, int width, int height) 97 { 98 _wndClass.style = CS_DBLCLKS | CS_OWNDC | CS_VREDRAW | CS_HREDRAW; 99 100 _wndClass.lpfnWndProc = &windowProcCallback; 101 102 _wndClass.cbClsExtra = 0; 103 _wndClass.cbWndExtra = 0; 104 _wndClass.hInstance = getModuleHandle(); 105 _wndClass.hIcon = null; 106 _wndClass.hCursor = LoadCursor(null, IDC_ARROW); 107 _wndClass.hbrBackground = null; 108 _wndClass.lpszMenuName = null; 109 110 // Generates an unique class name 111 generateClassName(); 112 _wndClass.lpszClassName = _className.ptr; 113 114 if (!RegisterClassW(&_wndClass)) 115 { 116 assert(false, "Couldn't register Win32 class"); 117 } 118 119 DWORD flags = 0; 120 if (parentWindow != null) 121 flags |= WS_CHILD; 122 else 123 parentWindow = GetDesktopWindow(); 124 125 _hwnd = CreateWindowW(_className.ptr, null, flags, CW_USEDEFAULT, CW_USEDEFAULT, 126 width, height, 127 parentWindow, null, 128 getModuleHandle(), 129 cast(void*)this); 130 131 if (_hwnd is null) 132 { 133 assert(false, "Couldn't create a Win32 window"); 134 } 135 136 // Create update region 137 _updateRegion = CreateRectRgn(0, 0, 0, 0); 138 _clipRegion = CreateRectRgn(0, 0, 0, 0); 139 _updateRgbBuf = makeVec!ubyte(); 140 _updateRects = makeVec!box2i(); 141 142 _listener = listener; 143 // Sets this as user data 144 SetWindowLongPtrA(_hwnd, GWLP_USERDATA, cast(LONG_PTR)( cast(void*)this )); 145 146 if (_listener !is null) // we are interested in custom behavior 147 { 148 149 int mSec = 15; // refresh at 60 hz if possible 150 SetTimer(_hwnd, TIMER_ID, mSec, null); 151 } 152 153 // Resize if DPI is supported. 154 if (DPISupportWin32) 155 { 156 _dpiSupport.initialize(); 157 _currentDPI = _dpiSupport.GetDpiForWindow(_hwnd) / 96.0f; 158 //MAYDO resize for DPI 159 //resizeWindowForDPI(); 160 } 161 162 ShowWindow(_hwnd, SW_SHOW); 163 SetFocus(_hwnd); 164 165 // Get performance counter frequency 166 LARGE_INTEGER performanceFrequency; 167 BOOL res = QueryPerformanceFrequency(&performanceFrequency); 168 assert(res != 0); // since XP it is always supported 169 _performanceCounterDivider = performanceFrequency.QuadPart; 170 171 // Get reference time 172 _timeAtCreationInMs = getTimeMs(); 173 _lastMeasturedTimeInMs = _timeAtCreationInMs; 174 175 // Do we need the FLStudio bridge work-around? 176 // Detect if we are under FLStudio's bridge. 177 _useFLStudioBridgeWorkaround = false; 178 HMODULE hmodule = GetModuleHandle(NULL); 179 if (hmodule !is NULL) 180 { 181 char[256] path; 182 int len = GetModuleFileNameA(hmodule, path.ptr, 256); 183 if (len >= 12) 184 { 185 _useFLStudioBridgeWorkaround = path[len - 12 .. len] == "ilbridge.exe"; 186 } 187 } 188 version(hookMouseForWheelEvents) 189 installMouseHook(); 190 } 191 192 ~this() 193 { 194 version(hookMouseForWheelEvents) 195 removeMouseHook(); 196 197 if (_hwnd != null) 198 { 199 DestroyWindow(_hwnd); 200 _hwnd = null; 201 202 // Unregister the window class, which was unique 203 UnregisterClassW(_wndClass.lpszClassName, getModuleHandle()); 204 } 205 206 if (_updateRegion != null) 207 { 208 DeleteObject(_updateRegion); 209 _updateRegion = null; 210 } 211 212 if (_clipRegion != null) 213 { 214 DeleteObject(_clipRegion); 215 _clipRegion = null; 216 } 217 } 218 219 /// Returns: true if window size changed. 220 /// Update internal buffers, if needed. 221 bool updateInternalSize() 222 { 223 RECT winsize; 224 BOOL res = GetClientRect(_hwnd, &winsize); 225 if (res == 0) 226 { 227 assert(false, "GetClientRect failed"); 228 } 229 230 int newWidth = winsize.right - winsize.left; 231 int newHeight = winsize.bottom - winsize.top; 232 233 // only do something if the client size has changed 234 if (newWidth != _width || newHeight != _height) 235 { 236 _width = newWidth; 237 _height = newHeight; 238 239 _sizeSet = true; 240 _wfb = _listener.onResized(_width, _height); 241 return true; 242 } 243 else 244 return false; 245 } 246 247 override bool requestResize(int widthLogicalPixels, int heightLogicalPixels, bool alsoResizeParentWindow) 248 { 249 UINT flags = SWP_NOACTIVATE 250 | SWP_NOZORDER 251 | SWP_NOOWNERZORDER 252 | SWP_NOMOVE 253 | SWP_NOCOPYBITS; // discard entire content of the client area 254 255 RECT formerClient; 256 GetClientRect(_hwnd, &formerClient); 257 258 int dw = widthLogicalPixels - (formerClient.right - formerClient.left); 259 int dh = heightLogicalPixels - (formerClient.bottom - formerClient.top); 260 261 HWND parent = null; 262 RECT parentClient; 263 HWND gparent = null; 264 RECT gparentClient; 265 if (alsoResizeParentWindow) 266 { 267 if (IsChildWindow(_hwnd)) 268 { 269 parent = GetParent(_hwnd); 270 GetClientRect(parent, &parentClient); 271 if (IsChildWindow(parent)) 272 { 273 gparent = GetParent(parent); 274 GetClientRect(gparent, &gparentClient); 275 } 276 } 277 } 278 279 BOOL r = SetWindowPos(_hwnd, null, 280 0, 0, 0 + widthLogicalPixels, 0 + heightLogicalPixels, 281 flags); 282 bool result = r != 0; 283 284 if (parent !is null) 285 { 286 int parentWidth = (parentClient.right - parentClient.left) + dw; 287 int parentHeight = (parentClient.bottom - parentClient.top) + dh; 288 SetWindowPos(parent, null, 289 0, 0, 0 + parentWidth, 0 + parentHeight, 290 SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); 291 } 292 if (gparent !is null) 293 { 294 int gparentWidth = (gparentClient.right - gparentClient.left) + dw; 295 int gparentHeight = (gparentClient.bottom - gparentClient.top) + dh; 296 SetWindowPos(gparent, null, 297 0, 0, 0 + gparentWidth, 0 + gparentHeight, 298 SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); 299 } 300 return result; 301 } 302 303 LRESULT windowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 304 { 305 // because DispatchMessage is called by host, we don't know which thread comes here 306 ScopedForeignCallback!(true, true) scopedCallback; 307 scopedCallback.enter(); 308 309 if (_listener is null) 310 return DefWindowProc(hwnd, uMsg, wParam, lParam); 311 312 switch (uMsg) 313 { 314 case WM_KEYDOWN: 315 case WM_KEYUP: 316 { 317 bool handled = false; 318 319 shiftPressed = GetKeyState(VK_SHIFT) < 0; 320 321 Key key = vkToKey(wParam, shiftPressed); 322 if (uMsg == WM_KEYDOWN) 323 { 324 if (_listener.onKeyDown(key)) 325 { 326 handled = true; 327 } 328 } 329 else 330 { 331 if (_listener.onKeyUp(key)) 332 { 333 handled = true; 334 } 335 } 336 337 if (!handled) 338 { 339 // TODO: looks fishy, why GA_ROOT instead of GA_PARENT? 340 // key is passed to the most-parent window 341 HWND rootHWnd = GetAncestor(hwnd, GA_ROOT); 342 if (rootHWnd != hwnd) // send message up the chain if not self (not hosting) 343 { 344 SendMessage(rootHWnd, uMsg, wParam, lParam); 345 return DefWindowProc(hwnd, uMsg, wParam, lParam); 346 } 347 else 348 return DefWindowProc(hwnd, uMsg, wParam, lParam); 349 } 350 else 351 return 0; 352 } 353 354 355 case WM_SETCURSOR: 356 { 357 // In case the cursor was modified by another window, restore the cursor. 358 // This is done differently depending on whether we want a modifed cursor ourselves. 359 // Easily tested in REAPER by hovering over the left border of 360 // the plugin: this turn the custor into a resize arrow. 361 version(noCursors) 362 { 363 goto default; 364 } 365 else 366 return setMouseCursor(true); 367 } 368 369 case WM_MOUSEMOVE: 370 { 371 // See Dplug #378, important to sign-extend for multiple monitors 372 int newMouseX = cast(int)(lParam << 16) >> 16; 373 int newMouseY = ( cast(int)lParam ) >> 16; 374 int dx = newMouseX - _mouseX; 375 int dy = newMouseY - _mouseY; 376 _listener.onMouseMove(newMouseX, newMouseY, dx, dy, getMouseState(wParam)); 377 _mouseX = newMouseX; 378 _mouseY = newMouseY; 379 setMouseCursor(false); 380 381 // Track when mouse exit the window 382 if (!_mouseTracking) 383 { 384 // Enable mouse tracking. 385 TRACKMOUSEEVENT tme; 386 tme.cbSize = tme.sizeof; 387 tme.hwndTrack = hwnd; 388 tme.dwFlags = TME_LEAVE; 389 tme.dwHoverTime = HOVER_DEFAULT; 390 TrackMouseEvent(&tme); 391 _mouseTracking = true; 392 } 393 394 return 0; 395 } 396 397 case WM_SETFOCUS: 398 { 399 return 0; 400 } 401 402 case WM_KILLFOCUS: 403 { 404 return 0; 405 } 406 407 case WM_MOUSEWHEEL: 408 { 409 // See Dplug #378, important to sign-extend for multiple monitors 410 int mouseX = cast(int)(lParam << 16) >> 16; 411 int mouseY = ( cast(int)lParam ) >> 16; 412 413 // Mouse positions we are getting are not relative to the client area 414 RECT r; 415 GetWindowRect(hwnd, &r); 416 mouseX -= r.left; 417 mouseY -= r.top; 418 419 int wheelDeltaY = cast(short)((wParam & 0xffff0000) >> 16) / WHEEL_DELTA; 420 421 if (_listener.onMouseWheel(mouseX, mouseY, 0, wheelDeltaY, getMouseState(wParam))) 422 { 423 return 0; // handled 424 } 425 goto default; 426 } 427 428 case WM_RBUTTONDOWN: 429 case WM_RBUTTONDBLCLK: 430 { 431 if (mouseClick(_mouseX, _mouseY, MouseButton.right, uMsg == WM_RBUTTONDBLCLK, wParam)) 432 return 0; // handled 433 goto default; 434 } 435 436 case WM_LBUTTONDOWN: 437 case WM_LBUTTONDBLCLK: 438 { 439 if (mouseClick(_mouseX, _mouseY, MouseButton.left, uMsg == WM_LBUTTONDBLCLK, wParam)) 440 return 0; // handled 441 goto default; 442 } 443 444 case WM_MBUTTONDOWN: 445 case WM_MBUTTONDBLCLK: 446 { 447 if (mouseClick(_mouseX, _mouseY, MouseButton.middle, uMsg == WM_MBUTTONDBLCLK, wParam)) 448 return 0; // handled 449 goto default; 450 } 451 452 // X1/X2 buttons 453 case WM_XBUTTONDOWN: 454 case WM_XBUTTONDBLCLK: 455 { 456 auto mb = (wParam >> 16) == 1 ? MouseButton.x1 : MouseButton.x2; 457 if (mouseClick(_mouseX, _mouseY, mb, uMsg == WM_XBUTTONDBLCLK, wParam)) 458 return 0; 459 goto default; 460 } 461 462 case WM_RBUTTONUP: 463 if (mouseRelease(_mouseX, _mouseY, MouseButton.right, wParam)) 464 return 0; 465 goto default; 466 467 case WM_LBUTTONUP: 468 if (mouseRelease(_mouseX, _mouseY, MouseButton.left, wParam)) 469 return 0; 470 goto default; 471 case WM_MBUTTONUP: 472 if (mouseRelease(_mouseX, _mouseY, MouseButton.middle, wParam)) 473 return 0; 474 goto default; 475 476 case WM_XBUTTONUP: 477 { 478 auto mb = (wParam >> 16) == 1 ? MouseButton.x1 : MouseButton.x2; 479 if (mouseRelease(_mouseX, _mouseY, mb, wParam)) 480 return 0; 481 goto default; 482 } 483 484 case WM_CAPTURECHANGED: 485 _listener.onMouseCaptureCancelled(); 486 setMouseCursor(true); 487 goto default; 488 489 case WM_MOUSELEAVE: 490 _listener.onMouseExitedWindow(); 491 _mouseTracking = false; 492 return 0; 493 494 case WM_SIZE: 495 case WM_SHOWWINDOW: 496 { 497 // This get the size of the client area, and then tells the listener about the new size. 498 updateInternalSize(); 499 goto default; 500 } 501 502 case WM_PAINT: 503 { 504 // Internal size must have been set. 505 assert(_sizeSet); 506 507 // Same thing that under Cocoa and Linux, the first WM_PAINT could happen before WM_TIMER is ever called. 508 // In this case, call `recomputeDirtyAreas()` so that onDraw draw something and uninitialized pixels are not displayed. 509 // See Issue #523 and #572. 510 if (_dirtyAreasAreNotYetComputed) 511 { 512 _dirtyAreasAreNotYetComputed = false; 513 _listener.recomputeDirtyAreas(); 514 } 515 516 // Renders UI. 517 // FUTURE: This could be done in a separate thread? 518 // For efficiency purpose, render in BGRA for Windows 519 // We do it here, but note that redrawing has nothing to do with WM_PAINT specifically, 520 // we just need to wait for it here. 521 _listener.onDraw(WindowPixelFormat.BGRA8); 522 523 // Get the update region 524 int type = GetUpdateRgn(hwnd, _updateRegion, FALSE); 525 assert (type != ERROR); 526 527 // Begin painting 528 PAINTSTRUCT paintStruct; 529 HDC hdc = BeginPaint(_hwnd, &paintStruct); 530 531 HRGN regionToUpdate = _updateRegion; 532 533 // FLStudio compatibility 534 // Try to get the DC's clipping region, which may be larger in the case of FLStudio's bridge. 535 if (_useFLStudioBridgeWorkaround) 536 { 537 if ( GetClipRgn(hdc, _clipRegion) == 1) 538 regionToUpdate = _clipRegion; 539 } 540 541 // Get needed number of bytes 542 DWORD bytes = GetRegionData(regionToUpdate, 0, null); 543 _updateRgbBuf.resize(bytes); 544 545 if (bytes == GetRegionData(regionToUpdate, bytes, cast(RGNDATA*)(_updateRgbBuf.ptr))) 546 { 547 // Get rectangles to update visually from the update region 548 ubyte* buf = _updateRgbBuf.ptr; 549 RGNDATAHEADER* header = cast(RGNDATAHEADER*)buf; 550 assert(header.iType == RDH_RECTANGLES); 551 _updateRects.clearContents(); 552 RECT* pRect = cast(RECT*)(buf + RGNDATAHEADER.sizeof); 553 554 alias wfb = _wfb; 555 for (int r = 0; r < header.nCount; ++r) 556 { 557 int left = pRect[r].left; 558 int top = pRect[r].top; 559 int right = pRect[r].right; 560 int bottom = pRect[r].bottom; 561 _updateRects.pushBack(box2i(left, top, right, bottom)); 562 } 563 564 static int byteStride(int width) pure nothrow @nogc @safe 565 { 566 enum scanLineAlignment = 4; 567 int widthInBytes = width * 4; 568 return (widthInBytes + (scanLineAlignment - 1)) & ~(scanLineAlignment-1); 569 } 570 // Note: the pitch of the displayed image _has_ to follow `byteStride`. 571 // SetDIBitsToDevice doesn't seem to take arbitrary pitch 572 assert(wfb.pitch == byteStride(wfb.w)); 573 574 BITMAPINFOHEADER bmi = BITMAPINFOHEADER.init; // fill with zeroes 575 with (bmi) 576 { 577 biSize = BITMAPINFOHEADER.sizeof; 578 biWidth = wfb.w; 579 biHeight = -wfb.h; 580 biPlanes = 1; 581 biCompression = BI_RGB; 582 biXPelsPerMeter = 72; 583 biYPelsPerMeter = 72; 584 biBitCount = 32; 585 biSizeImage = cast(int)(wfb.pitch) * wfb.h; 586 } 587 588 foreach(box2i area; _updateRects) 589 { 590 if (area.width() <= 0 || area.height() <= 0) 591 continue; // nothing to update 592 593 SetDIBitsToDevice(hdc, area.min.x, area.min.y, area.width, area.height, 594 area.min.x, -area.min.y - area.height + wfb.h, 595 0, wfb.h, wfb.pixels, cast(BITMAPINFO *)&bmi, DIB_RGB_COLORS); 596 } 597 } 598 else 599 assert(false); 600 601 EndPaint(_hwnd, &paintStruct); 602 return 0; 603 } 604 605 case WM_ERASEBKGND: 606 { 607 // This fails, so cause this window's WM_PAINT to be responsible for erasing background, 608 // hence saving a bit of performance. 609 return 1; 610 } 611 612 case WM_CLOSE: 613 { 614 this.destroyNoGC(); 615 return 0; 616 } 617 618 case WM_TIMER: 619 { 620 if (wParam == TIMER_ID) 621 { 622 uint now = getTimeMs(); 623 double dt = (now - _lastMeasturedTimeInMs) * 0.001; 624 double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days 625 _lastMeasturedTimeInMs = now; 626 _listener.onAnimate(dt, time); 627 628 _listener.recomputeDirtyAreas(); 629 _dirtyAreasAreNotYetComputed = false; 630 box2i dirtyRect = _listener.getDirtyRectangle(); 631 if (!dirtyRect.empty()) 632 { 633 RECT r = RECT(dirtyRect.min.x, dirtyRect.min.y, dirtyRect.max.x, dirtyRect.max.y); 634 InvalidateRect(_hwnd, &r, FALSE); // FUTURE: invalidate rects one by one 635 636 // See issue #432 and #269 637 // To avoid blocking WM_TIMER with expensive WM_PAINT, it's important NOT to enqueue manually a 638 // WM_PAINT here. Let Windows do its job of sending WM_PAINT when needed. 639 } 640 } 641 return 0; 642 } 643 644 default: 645 return DefWindowProcA(hwnd, uMsg, wParam, lParam); 646 } 647 } 648 649 // Implements IWindow 650 override void waitEventAndDispatch() 651 { 652 MSG msg; 653 int ret = GetMessageW(&msg, _hwnd, 0, 0); // no range filtering 654 if (ret == -1) 655 assert(false, "Error while in GetMessage"); 656 TranslateMessage(&msg); 657 DispatchMessageW(&msg); 658 } 659 660 override bool terminated() 661 { 662 return _terminated; 663 } 664 665 override uint getTimeMs() 666 { 667 LARGE_INTEGER perfCounter; 668 BOOL err = QueryPerformanceCounter(&perfCounter); 669 assert(err != 0); // always supported since XP 670 double time = (perfCounter.QuadPart * 1000 + (_performanceCounterDivider >> 1)) / cast(double)_performanceCounterDivider; 671 return cast(uint)(time); 672 } 673 674 override void* systemHandle() 675 { 676 return cast(void*)( cast(size_t)_hwnd ); 677 } 678 679 private: 680 enum TIMER_ID = 144; 681 682 HWND _hwnd; 683 684 WNDCLASSW _wndClass; 685 686 HRGN _updateRegion; 687 HRGN _clipRegion; 688 bool _useFLStudioBridgeWorkaround; 689 690 Vec!ubyte _updateRgbBuf; 691 Vec!box2i _updateRects; 692 693 long _performanceCounterDivider; 694 uint _timeAtCreationInMs; 695 uint _lastMeasturedTimeInMs; 696 697 bool _sizeSet = false; 698 IWindowListener _listener; // contract: _listener must only be used in the message callback 699 700 ImageRef!RGBA _wfb; // framebuffer reference 701 702 bool _terminated = false; 703 int _width = 0; 704 int _height = 0; 705 706 int _mouseX = 0; 707 int _mouseY = 0; 708 bool _mouseTracking = false; 709 710 bool shiftPressed = false; 711 bool _dirtyAreasAreNotYetComputed = true; 712 713 // Helper for getting DPI information. 714 DPIHelper _dpiSupport; 715 716 // Current UI scale, 1.0f means default DPI (96). 717 // See_also: https://www.enlyze.com/blog/writing-win32-apps-like-its-2020/part-3/ 718 float _currentDPI = 1.0f; 719 720 // Last MouseCursor used. This is to avoid updating the cursor 721 // more often than necessary 722 // Default value of pointer 723 MouseCursor _lastMouseCursor = MouseCursor.pointer; 724 725 /// Propagates mouse events. 726 /// Returns: true if event handled. 727 bool mouseClick(int mouseX, int mouseY, MouseButton mb, bool isDoubleClick, WPARAM wParam) 728 { 729 SetFocus(_hwnd); // get keyboard focus 730 731 // Start mouse capture, if not already captures (See Issue #694) 732 if (GetCapture() != _hwnd) 733 SetCapture(_hwnd); 734 735 bool consumed = _listener.onMouseClick(mouseX, mouseY, mb, isDoubleClick, getMouseState(wParam)); 736 return consumed; 737 } 738 739 /// ditto 740 bool mouseRelease(int mouseX, int mouseY, MouseButton mb, WPARAM wParam) 741 { 742 // Note: the first button release from mouse interrupts the dragging operation. 743 // This need not be the case, but it makes things easier. 744 // Not sure how cross platform we are on that specific point. 745 if (GetCapture() == _hwnd) 746 ReleaseCapture(); 747 748 bool consumed = _listener.onMouseRelease(mouseX, mouseY, mb, getMouseState(wParam)); 749 return consumed; 750 } 751 752 wchar[43] _className; // Zero-terminated class name 753 754 void generateClassName() nothrow @nogc 755 { 756 generateNullTerminatedRandomUUID!wchar(_className, "dplug_"w); 757 } 758 759 int setMouseCursor(bool forceUpdate) 760 { 761 version(noCursors) 762 {} 763 else 764 { 765 MouseCursor cursor = _listener.getMouseCursor(); 766 767 if((cursor != _lastMouseCursor) || forceUpdate ) 768 { 769 CURSORINFO pci; 770 pci.cbSize = CURSORINFO.sizeof; 771 GetCursorInfo(&pci); 772 773 // If the cursor we want to display is "hidden" and the cursor is being shown 774 // then we will hide the cursor. 775 // If the cursor we want to display is anything other than "hidden" and the 776 // cursor is being hidden already, we will set it to show 777 // (this triggers a WM_SETCURSOR which will call this to set the cursor) 778 // lastly if the above conditions are false then we will set the cursor 779 if(cursor == MouseCursor.hidden && pci.flags == CURSOR_SHOWING) 780 { 781 ShowCursor(false); 782 } 783 else if(cursor != MouseCursor.hidden && pci.flags == 0) 784 { 785 ShowCursor(true); 786 } 787 else 788 { 789 auto cursorId = mouseCursorToCursorId(cursor); 790 HCURSOR hc = LoadCursorA(NULL, cast(const(char)*)cursorId); 791 SetCursor(hc); 792 } 793 _lastMouseCursor = cursor; 794 return 1; 795 } 796 } 797 798 return 0; 799 } 800 801 version(hookMouseForWheelEvents) 802 { 803 804 // This prop says that this is a Dplug window, that has hooked the mouse. 805 // useful to identiy things when you just have a HWND. 806 // having does NOT mean that you can cast to Win32Window or anything, just that 807 // you expect the mouse wheel events to come to you in a drag outside your bounds. 808 enum string HOOK_PROPERTY = "i-am-a-dplug-window-and-have-hooked-the-mouse"; 809 810 // true if installed 811 bool installMouseHook() 812 { 813 _mouseHook = SetWindowsHookExA(WH_MOUSE_LL, &LowLevelMouseProc, null, 0); 814 815 if (_mouseHook) 816 { 817 BOOL res = SetPropA(_hwnd, HOOK_PROPERTY.ptr, _mouseHook); 818 if (res == 0) 819 { 820 removeMouseHook(); 821 return false; 822 } 823 } 824 return (_mouseHook !is null); 825 } 826 827 // true if removed 828 void removeMouseHook() 829 { 830 // Remove property, if any 831 if (null != GetPropA(_hwnd, HOOK_PROPERTY.ptr)) 832 { 833 RemovePropA(_hwnd, HOOK_PROPERTY.ptr); 834 } 835 836 // Note: nothing prevent the hook to be currently called there. 837 if (_mouseHook) 838 { 839 UnhookWindowsHookEx(_mouseHook); 840 _mouseHook = null; 841 } 842 } 843 844 HHOOK _mouseHook = null; 845 } 846 } 847 848 849 extern(Windows) nothrow 850 { 851 LRESULT windowProcCallback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 852 { 853 Win32Window window = cast(Win32Window)( cast(void*)(GetWindowLongPtrA(hwnd, GWLP_USERDATA)) ); 854 if (window !is null) 855 return window.windowProc(hwnd, uMsg, wParam, lParam); 856 else 857 return DefWindowProcA(hwnd, uMsg, wParam, lParam); 858 } 859 } 860 861 Key vkToKey(WPARAM vk, bool shiftPressed)pure nothrow @nogc 862 { 863 switch (vk) 864 { 865 case VK_SPACE: return Key.space; 866 867 case VK_UP: return Key.upArrow; 868 case VK_DOWN: return Key.downArrow; 869 case VK_LEFT: return Key.leftArrow; 870 case VK_RIGHT: return Key.rightArrow; 871 872 case VK_NUMPAD0: return Key.digit0; 873 case VK_NUMPAD1: return Key.digit1; 874 case VK_NUMPAD2: return Key.digit2; 875 case VK_NUMPAD3: return Key.digit3; 876 case VK_NUMPAD4: return Key.digit4; 877 case VK_NUMPAD5: return Key.digit5; 878 case VK_NUMPAD6: return Key.digit6; 879 case VK_NUMPAD7: return Key.digit7; 880 case VK_NUMPAD8: return Key.digit8; 881 case VK_NUMPAD9: return Key.digit9; 882 case 0x30: return Key.digit0; 883 case 0x31: return Key.digit1; 884 case 0x32: return Key.digit2; 885 case 0x33: return Key.digit3; 886 case 0x34: return Key.digit4; 887 case 0x35: return Key.digit5; 888 case 0x36: return Key.digit6; 889 case 0x37: return Key.digit7; 890 case 0x38: return Key.digit8; 891 case 0x39: return Key.digit9; 892 case 0x41: return shiftPressed ? Key.A : Key.a; 893 case 0x42: return shiftPressed ? Key.B : Key.b; 894 case 0x43: return shiftPressed ? Key.C : Key.c; 895 case 0x44: return shiftPressed ? Key.D : Key.d; 896 case 0x45: return shiftPressed ? Key.E : Key.e; 897 case 0x46: return shiftPressed ? Key.F : Key.f; 898 case 0x47: return shiftPressed ? Key.G : Key.g; 899 case 0x48: return shiftPressed ? Key.H : Key.h; 900 case 0x49: return shiftPressed ? Key.I : Key.i; 901 case 0x4A: return shiftPressed ? Key.J : Key.j; 902 case 0x4B: return shiftPressed ? Key.K : Key.k; 903 case 0x4C: return shiftPressed ? Key.L : Key.l; 904 case 0x4D: return shiftPressed ? Key.M : Key.m; 905 case 0x4E: return shiftPressed ? Key.N : Key.n; 906 case 0x4F: return shiftPressed ? Key.O : Key.o; 907 case 0x50: return shiftPressed ? Key.P : Key.p; 908 case 0x51: return shiftPressed ? Key.Q : Key.q; 909 case 0x52: return shiftPressed ? Key.R : Key.r; 910 case 0x53: return shiftPressed ? Key.S : Key.s; 911 case 0x54: return shiftPressed ? Key.T : Key.t; 912 case 0x55: return shiftPressed ? Key.U : Key.u; 913 case 0x56: return shiftPressed ? Key.V : Key.v; 914 case 0x57: return shiftPressed ? Key.W : Key.w; 915 case 0x58: return shiftPressed ? Key.X : Key.x; 916 case 0x59: return shiftPressed ? Key.Y : Key.y; 917 case 0x5A: return shiftPressed ? Key.Z : Key.z; 918 case VK_BACK: return Key.backspace; 919 case VK_RETURN: return Key.enter; 920 case VK_DELETE: return Key.suppr; 921 case VK_ESCAPE: return Key.escape; 922 default: return Key.unsupported; 923 } 924 } 925 926 SHORT keyState(int vk) 927 { 928 version(AAX) 929 return GetAsyncKeyState(VK_MENU); 930 else 931 return GetKeyState(VK_MENU); 932 } 933 934 static MouseState getMouseState(WPARAM wParam) 935 { 936 return MouseState( (wParam & MK_LBUTTON) != 0, 937 (wParam & MK_RBUTTON) != 0, 938 (wParam & MK_MBUTTON) != 0, 939 (wParam & MK_XBUTTON1) != 0, 940 (wParam & MK_XBUTTON2) != 0, 941 (wParam & MK_CONTROL) != 0, 942 (wParam & MK_SHIFT) != 0, 943 keyState(VK_MENU) < 0 ); 944 } 945 946 HCURSOR mouseCursorToCursorId(MouseCursor cursor) 947 { 948 switch(cursor) 949 { 950 case cursor.linkSelect: 951 return IDC_HAND; 952 953 case cursor.drag: 954 return IDC_CROSS; 955 956 case cursor.move: 957 return IDC_HAND; 958 959 case cursor.horizontalResize: 960 return IDC_SIZEWE; 961 962 case cursor.verticalResize: 963 return IDC_SIZENS; 964 965 case cursor.diagonalResize: 966 return IDC_SIZENWSE; 967 968 case cursor.pointer: 969 970 default: 971 return IDC_ARROW; 972 } 973 } 974 975 976 // Copyright 2013 The Flutter Authors. All rights reserved. 977 // 978 // Redistribution and use in source and binary forms, with or without modification, 979 // are permitted provided that the following conditions are met: 980 // 981 // * Redistributions of source code must retain the above copyright 982 // notice, this list of conditions and the following disclaimer. 983 // * Redistributions in binary form must reproduce the above 984 // copyright notice, this list of conditions and the following 985 // disclaimer in the documentation and/or other materials provided 986 // with the distribution. 987 // * Neither the name of Google Inc. nor the names of its 988 // contributors may be used to endorse or promote products derived 989 // from this software without specific prior written permission. 990 // 991 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 992 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 993 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 994 // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 995 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 996 // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 997 // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 998 // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 999 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 1000 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1001 1002 /// A helper class for abstracting various Windows DPI related functions across 1003 /// Windows OS versions. 1004 private struct DPIHelper 1005 { 1006 public: 1007 nothrow: 1008 @nogc: 1009 void initialize() 1010 { 1011 if ((user32_module_ = LoadLibraryA("User32.dll")) != null) 1012 { 1013 get_dpi_for_window_ = cast(GetDpiForWindow_t) GetProcAddress(user32_module_, "GetDpiForWindow".ptr); 1014 dpi_for_window_supported_ = get_dpi_for_window_ !is null; 1015 1016 set_thread_dpi_awareness_context = cast(SetThreadDpiAwarenessContext_t) GetProcAddress(user32_module_, "SetThreadDpiAwarenessContext".ptr); 1017 set_thread_dpi_hosting_behavior = cast(SetThreadDpiHostingBehavior_t) GetProcAddress(user32_module_, "SetThreadDpiHostingBehavior".ptr); 1018 } 1019 if ((shlib_module_ = LoadLibraryA("Shcore.dll")) != null) 1020 { 1021 get_dpi_for_monitor_ = cast(GetDpiForMonitor_t) GetProcAddress(shlib_module_, "GetDpiForMonitor".ptr); 1022 dpi_for_monitor_supported_ = get_dpi_for_monitor_ !is null; 1023 } 1024 } 1025 1026 ~this() 1027 { 1028 if (user32_module_ != null) 1029 { 1030 FreeLibrary(user32_module_); 1031 user32_module_ = null; 1032 } 1033 if (shlib_module_ != null) 1034 { 1035 FreeLibrary(shlib_module_); 1036 shlib_module_ = null; 1037 } 1038 } 1039 1040 private: 1041 extern(Windows) nothrow @nogc 1042 { 1043 alias GetDpiForWindow_t = UINT function(HWND); 1044 alias GetDpiForMonitor_t = HRESULT function(HMONITOR hmonitor, 1045 UINT dpiType, 1046 UINT* dpiX, 1047 UINT* dpiY); 1048 alias EnableNonClientDpiScaling_t = BOOL function(HWND); 1049 1050 alias SetThreadDpiAwarenessContext_t = DPI_AWARENESS_CONTEXT function(DPI_AWARENESS_CONTEXT); 1051 alias SetThreadDpiHostingBehavior_t = DPI_HOSTING_BEHAVIOR function(DPI_HOSTING_BEHAVIOR); 1052 } 1053 1054 GetDpiForWindow_t get_dpi_for_window_; 1055 GetDpiForMonitor_t get_dpi_for_monitor_; 1056 1057 SetThreadDpiAwarenessContext_t set_thread_dpi_awareness_context; 1058 SetThreadDpiHostingBehavior_t set_thread_dpi_hosting_behavior; 1059 1060 EnableNonClientDpiScaling_t enable_non_client_dpi_scaling_; 1061 HMODULE user32_module_ = null; 1062 HMODULE shlib_module_ = null; 1063 bool dpi_for_window_supported_ = false; 1064 bool dpi_for_monitor_supported_ = false; 1065 1066 1067 /// Returns the DPI for |hwnd|. Supports all DPI awareness modes, and is 1068 /// backward compatible down to Windows Vista. If |hwnd| is nullptr, returns 1069 /// the DPI for the primary monitor. If Per-Monitor DPI awareness is not 1070 /// available, returns the system's DPI. 1071 UINT GetDpiForWindow(HWND hwnd) 1072 { 1073 // GetDpiForWindow returns the DPI for any awareness mode. If not available, 1074 // or no |hwnd| is provided, fallback to a per monitor, system, or default 1075 // DPI. 1076 if (dpi_for_window_supported_ && hwnd != null) { 1077 return get_dpi_for_window_(hwnd); 1078 } 1079 1080 if (dpi_for_monitor_supported_) 1081 { 1082 HMONITOR monitor = null; 1083 if (hwnd != null) { 1084 monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); 1085 } 1086 return GetDpiForMonitor(monitor); 1087 } 1088 HDC hdc = GetDC(hwnd); 1089 UINT dpi = GetDeviceCaps(hdc, LOGPIXELSX); 1090 ReleaseDC(hwnd, hdc); 1091 return dpi; 1092 } 1093 1094 enum kDefaultDpi = 96; 1095 1096 /// Returns the DPI of a given monitor. Defaults to 96 if the API is not 1097 /// available. 1098 UINT GetDpiForMonitor(HMONITOR monitor) 1099 { 1100 if (dpi_for_monitor_supported_) 1101 { 1102 if (monitor == null) 1103 { 1104 POINT target_point; 1105 target_point.x = 0; 1106 target_point.y = 0; 1107 monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTOPRIMARY); 1108 } 1109 UINT dpi_x = 0, 1110 dpi_y = 0; 1111 1112 HRESULT result = get_dpi_for_monitor_(monitor, 0 /* kEffectiveDpiMonitorType */, &dpi_x, &dpi_y); 1113 if (SUCCEEDED(result)) 1114 { 1115 return dpi_x; 1116 } 1117 } 1118 return kDefaultDpi; 1119 } 1120 1121 DPI_AWARENESS_CONTEXT SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context) 1122 { 1123 // Do not set DPI to unsupported if asked so. 1124 if (context == DPI_AWARENESS_CONTEXT_UNSUPPORTED) 1125 return context; 1126 1127 if (set_thread_dpi_awareness_context !is null) 1128 { 1129 return set_thread_dpi_awareness_context(context); 1130 } 1131 else 1132 return DPI_AWARENESS_CONTEXT_UNSUPPORTED; 1133 } 1134 1135 DPI_HOSTING_BEHAVIOR SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR value) 1136 { 1137 if (value == DPI_HOSTING_UNSUPPORTED) 1138 return value; 1139 1140 if (set_thread_dpi_hosting_behavior !is null) 1141 return set_thread_dpi_hosting_behavior(value); 1142 else 1143 return value; // do nothing but pretend it went OK 1144 } 1145 } 1146 1147 alias DPI_AWARENESS_CONTEXT = void*; 1148 enum : DPI_AWARENESS_CONTEXT 1149 { 1150 DPI_AWARENESS_CONTEXT_UNSUPPORTED = cast(void*)0, // This API not supported 1151 DPI_AWARENESS_CONTEXT_UNAWARE = cast(void*)(-1), 1152 DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = cast(void*)(-2), 1153 DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = cast(void*)(-3), 1154 DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = cast(void*)(-4), 1155 DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = cast(void*)(-5) // since Windows 10 1156 } 1157 1158 alias DPI_HOSTING_BEHAVIOR = int; 1159 enum : DPI_HOSTING_BEHAVIOR 1160 { 1161 DPI_HOSTING_UNSUPPORTED = -2, 1162 DPI_HOSTING_BEHAVIOR_INVALID = -1, 1163 DPI_HOSTING_BEHAVIOR_DEFAULT = 0, 1164 DPI_HOSTING_BEHAVIOR_MIXED = 1 1165 } 1166 1167 static bool IsChildWindow(HWND pWnd) 1168 { 1169 if (pWnd) 1170 { 1171 int style = GetWindowLong(pWnd, GWL_STYLE); 1172 int exStyle = GetWindowLong(pWnd, GWL_EXSTYLE); 1173 return ((style & WS_CHILD) && !(exStyle & WS_EX_MDICHILD)); 1174 } 1175 return false; 1176 } 1177 1178 1179 version(hookMouseForWheelEvents) 1180 { 1181 extern(Windows) LRESULT LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) 1182 { 1183 bool processed = false; 1184 1185 if (nCode == 0) 1186 { 1187 if (wParam == WM_MOUSEWHEEL) // this is about a global mouse wheel event 1188 { 1189 // What window has the mouse capture, if any? 1190 HWND hwnd = GetCapture(); 1191 1192 if (hwnd != null) 1193 { 1194 // We need to make sure this is destined to a Dplug-window. 1195 // It is one such window, one that can hook the mouse. Send it the mouse wheel event. 1196 // Indeed, casting to Win32Window would not be completely safe. 1197 // Actual mouse data is there. 1198 if (null != GetPropA(hwnd, Win32Window.HOOK_PROPERTY.ptr)) 1199 { 1200 MSLLHOOKSTRUCT* hookData = cast(MSLLHOOKSTRUCT*) lParam; 1201 DWORD lParamMsg = cast(short)(hookData.pt.x) | (cast(short)(hookData.pt.y) << 16); 1202 1203 BOOL res = PostMessageA(hwnd, 1204 WM_MOUSEWHEEL, 1205 hookData.mouseData & 0xffff0000, // Note: no support for horizontal mouse wheel 1206 lParamMsg); 1207 processed = (res != 0); // posted = processed 1208 } 1209 } 1210 } 1211 } 1212 1213 if (!processed) 1214 return CallNextHookEx(null, nCode, wParam, lParam); // never consume the message 1215 else 1216 { 1217 /// If the hook procedure processed the message, it may return a nonzero value to 1218 /// prevent the system from passing the message to the rest of the hook chain or 1219 /// the target window procedure. 1220 return 1; 1221 } 1222 } 1223 } 1224 }