dialog.cpp (12061B)
1 /* ReaPack: Package manager for REAPER 2 * Copyright (C) 2015-2025 Christian Fillion 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU Lesser General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "dialog.hpp" 19 20 #include "control.hpp" 21 #include "win32.hpp" 22 23 #include <algorithm> 24 #include <boost/algorithm/string/join.hpp> 25 #include <reaper_plugin_functions.h> 26 27 #ifdef _WIN32 28 # include <windowsx.h> 29 #endif 30 31 WDL_DLGRET Dialog::Proc(HWND handle, UINT msg, WPARAM wParam, LPARAM lParam) 32 { 33 // On Windows WM_DESTROY is emitted in place of WM_INITDIALOG 34 // if the dialog resource is invalid (eg. because of an unloaded dll). 35 // 36 // When this happens neither lParam nor GWLP_USERDATA will contain 37 // a pointer to the Dialog instance, so there is nothing we can do. 38 39 Dialog *dlg = reinterpret_cast<Dialog *>( 40 msg == WM_INITDIALOG ? lParam : GetWindowLongPtr(handle, GWLP_USERDATA) 41 ); 42 43 if(!dlg) 44 return false; 45 46 switch(msg) { 47 case WM_INITDIALOG: 48 SetWindowLongPtr(handle, GWLP_USERDATA, lParam); 49 dlg->m_handle = handle; 50 dlg->onInit(); 51 return 1; 52 case WM_TIMER: 53 dlg->onTimer(static_cast<int>(wParam)); 54 return 0; 55 case WM_COMMAND: 56 dlg->onCommand(LOWORD(wParam), HIWORD(wParam)); 57 return 0; 58 case WM_NOTIFY: 59 dlg->onNotify(reinterpret_cast<LPNMHDR>(lParam), lParam); 60 return 0; 61 case WM_CONTEXTMENU: 62 dlg->onContextMenu(reinterpret_cast<HWND>(wParam), 63 GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); 64 return 0; 65 case WM_GETMINMAXINFO: { 66 MINMAXINFO *mmi = reinterpret_cast<MINMAXINFO *>(lParam); 67 mmi->ptMinTrackSize.x = dlg->m_minimumSize.x; 68 mmi->ptMinTrackSize.y = dlg->m_minimumSize.y; 69 return 0; 70 } 71 case WM_SIZE: 72 if(wParam != SIZE_MINIMIZED) 73 dlg->onResize(); 74 return 0; 75 #ifdef __APPLE__ 76 // This stops SWELL_SendMouseMessageImpl from continuously resetting the 77 // mouse cursor allowing NSTextViews to change it on mouse hover. 78 case WM_SETCURSOR: 79 return dlg->isTextEditUnderMouse(); 80 #endif 81 case WM_DESTROY: 82 dlg->onClose(); 83 // Disabling processing after the dialog instance has been destroyed 84 SetWindowLongPtr(handle, GWLP_USERDATA, 0); 85 return 0; 86 default: 87 return 0; 88 }; 89 } 90 91 int Dialog::HandleKey(MSG *msg, accelerator_register_t *accel) 92 { 93 Dialog *dialog = reinterpret_cast<Dialog *>(accel->user); 94 if(!dialog || !dialog->hasFocus()) 95 return 0; // not our window 96 97 const int key = static_cast<int>(msg->wParam); 98 int modifiers = 0; 99 100 if(GetAsyncKeyState(VK_MENU) & 0x8000) 101 modifiers |= AltModifier; 102 if(GetAsyncKeyState(VK_CONTROL) & 0x8000) 103 modifiers |= CtrlModifier; 104 if(GetAsyncKeyState(VK_SHIFT) & 0x8000) 105 modifiers |= ShiftModifier; 106 107 if(msg->message == WM_KEYDOWN && dialog->onKeyDown(key, modifiers)) 108 return 1; 109 else 110 return -1; 111 } 112 113 Dialog::Dialog(const int templateId) 114 : m_template(templateId), m_instance(nullptr), m_parent(nullptr), m_handle(nullptr) 115 { 116 m_accel.translateAccel = HandleKey; 117 m_accel.isLocal = true; 118 m_accel.user = this; 119 plugin_register("accelerator", &m_accel); 120 121 // don't call reimplemented virtual methods here during object construction 122 } 123 124 Dialog::~Dialog() 125 { 126 plugin_register("-accelerator", &m_accel); 127 128 for(const int id : m_timers) 129 KillTimer(m_handle, id); // not using stopTimer to avoid modifying m_timers 130 131 if(m_mode == Modeless) { 132 // Unregistering the instance pointer right now before DestroyWindow 133 // prevents WM_DESTROY from calling the default implementation of onClose 134 // (because we're in the destructor – no polymorphism allowed here). 135 // Instead, the right onClose has been called directly by close() for 136 // modeless dialogs, or by the OS/SWELL for modal dialogs. 137 SetWindowLongPtr(m_handle, GWLP_USERDATA, 0); 138 DestroyWindow(m_handle); 139 } 140 } 141 142 INT_PTR Dialog::init(REAPER_PLUGIN_HINSTANCE inst, HWND parent, Modality mode) 143 { 144 m_instance = inst; 145 m_parent = parent; 146 m_mode = mode; 147 148 switch(mode) { 149 case Modeless: 150 CreateDialogParam(inst, MAKEINTRESOURCE(m_template), 151 m_parent, Proc, reinterpret_cast<LPARAM>(this)); 152 return true; 153 case Modal: 154 return DialogBoxParam(inst, MAKEINTRESOURCE(m_template), 155 m_parent, Proc, reinterpret_cast<LPARAM>(this)); 156 } 157 158 return false; // makes MSVC happy. 159 } 160 161 void Dialog::setVisible(const bool visible, HWND handle) 162 { 163 ShowWindow(handle ? handle : m_handle, visible ? SW_SHOW : SW_HIDE); 164 } 165 166 bool Dialog::isVisible() const 167 { 168 return IsWindowVisible(m_handle); 169 } 170 171 void Dialog::close(const INT_PTR result) 172 { 173 switch(m_mode) { 174 case Modal: 175 EndDialog(m_handle, result); 176 break; 177 case Modeless: 178 onClose(); 179 if(m_closeHandler) 180 m_closeHandler(result); 181 break; 182 } 183 } 184 185 void Dialog::center() 186 { 187 using std::min, std::max; 188 189 RECT dialogRect, parentRect; 190 191 GetWindowRect(m_handle, &dialogRect); 192 GetWindowRect(m_parent, &parentRect); 193 194 #ifdef _WIN32 195 HMONITOR monitor = MonitorFromWindow(m_parent, MONITOR_DEFAULTTONEAREST); 196 MONITORINFO minfo{sizeof(minfo)}; 197 GetMonitorInfo(monitor, &minfo); 198 RECT &screenRect = minfo.rcWork; 199 #else 200 RECT screenRect; 201 SWELL_GetViewPort(&screenRect, &dialogRect, false); 202 #endif 203 204 #ifndef __linux__ // SWELL_GetViewPort only gives the default monitor 205 // limit the centering to the monitor containing most of the parent window 206 parentRect.left = max(parentRect.left, screenRect.left); 207 parentRect.top = max(parentRect.top, screenRect.top); 208 parentRect.right = min(parentRect.right, screenRect.right); 209 parentRect.bottom = min(parentRect.bottom, screenRect.bottom); 210 #endif 211 212 const int parentWidth = parentRect.right - parentRect.left; 213 const int dialogWidth = dialogRect.right - dialogRect.left; 214 int left = (parentWidth - dialogWidth) / 2; 215 left += parentRect.left; 216 217 const int parentHeight = parentRect.bottom - parentRect.top; 218 const int dialogHeight = dialogRect.bottom - dialogRect.top; 219 int top = (parentHeight - dialogHeight) / 2; 220 top += parentRect.top; 221 222 const double verticalBias = (top - parentRect.top) * 0.3; 223 224 #ifdef __APPLE__ 225 top += verticalBias; // y starts from the bottom on macOS 226 #else 227 top -= static_cast<int>(verticalBias); 228 #endif 229 230 boundedMove(left, top); 231 } 232 233 void Dialog::boundedMove(const int x, const int y) 234 { 235 RECT rect; 236 GetWindowRect(m_handle, &rect); 237 238 const int deltaX = x - rect.left, 239 deltaY = y - rect.top; 240 rect.left += deltaX; 241 rect.top += deltaY; 242 rect.right += deltaX; 243 rect.bottom += deltaY; 244 245 EnsureNotCompletelyOffscreen(&rect); 246 247 SetWindowPos(m_handle, nullptr, rect.left, rect.top, 248 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE); 249 } 250 251 bool Dialog::hasFocus() const 252 { 253 const HWND focused = GetFocus(); 254 return focused == m_handle || IsChild(m_handle, focused); 255 } 256 257 void Dialog::setFocus() 258 { 259 show(); // hack to unminimize the window on macOS 260 SetFocus(m_handle); 261 } 262 263 void Dialog::setEnabled(const bool enabled, HWND handle) 264 { 265 EnableWindow(handle, enabled); 266 } 267 268 bool Dialog::isChecked(HWND handle) const 269 { 270 return SendMessage(handle, BM_GETCHECK, 0, 0) == BST_CHECKED; 271 } 272 273 void Dialog::setChecked(const bool checked, HWND handle) 274 { 275 SendMessage(handle, BM_SETCHECK, checked ? BST_CHECKED : BST_UNCHECKED, 0); 276 } 277 278 int Dialog::startTimer(const int ms, int id, const bool replace) 279 { 280 if(id == 0) { 281 if(m_timers.empty()) 282 id = 1; 283 else 284 id = *m_timers.rbegin() + 1; 285 } 286 else if(!replace && m_timers.count(id)) 287 return 0; 288 289 m_timers.insert(id); 290 SetTimer(m_handle, id, ms, nullptr); 291 292 return id; 293 } 294 295 void Dialog::stopTimer(int id) 296 { 297 KillTimer(m_handle, id); 298 m_timers.erase(id); 299 } 300 301 void Dialog::setClipboard(const std::string &text) 302 { 303 const HANDLE mem = Win32::globalCopy(text); 304 305 OpenClipboard(m_handle); 306 EmptyClipboard(); 307 #ifdef _WIN32 308 SetClipboardData(CF_UNICODETEXT, mem); 309 #else 310 // using RegisterClipboardFormat instead of CF_TEXT for compatibility with REAPER v5 311 // (prior to WDL commit 0f77b72adf1cdbe98fd56feb41eb097a8fac5681) 312 const unsigned int fmt = RegisterClipboardFormat("SWELL__CF_TEXT"); 313 SetClipboardData(fmt, mem); 314 #endif 315 CloseClipboard(); 316 } 317 318 void Dialog::setClipboard(const std::vector<std::string> &values) 319 { 320 #ifdef _WIN32 321 constexpr const char *nl = "\r\n"; 322 #else 323 constexpr const char *nl = "\n"; 324 #endif 325 326 if(!values.empty()) 327 setClipboard(boost::algorithm::join(values, nl)); 328 } 329 330 HWND Dialog::getControl(const int idc) 331 { 332 return GetDlgItem(m_handle, idc); 333 } 334 335 void Dialog::setAnchor(HWND handle, const int flags) 336 { 337 m_resizer.init_itemhwnd(handle, 338 static_cast<float>(flags & AnchorLeft), 339 static_cast<float>(flags & AnchorTop), 340 static_cast<float>(flags & AnchorRight), 341 static_cast<float>(flags & AnchorBottom)); 342 } 343 344 void Dialog::setAnchorPos(HWND handle, 345 const LONG *left, const LONG *top, const LONG *right, const LONG *bottom) 346 { 347 auto *item = m_resizer.get_itembywnd(handle); 348 349 if(!item) 350 return; 351 352 RECT *rect = &item->orig; 353 354 if(left) 355 rect->left = *left; 356 if(top) 357 rect->top = *top; 358 if(right) 359 rect->right = *right; 360 if(bottom) 361 rect->bottom = *bottom; 362 } 363 364 void Dialog::restoreState(Serializer::Data &data) 365 { 366 if(data.size() < 2) 367 return; 368 369 auto it = data.begin(); 370 const auto &[x, y] = *it++; 371 const auto &[width, height] = *it++; 372 373 #ifdef _WIN32 374 // Move to the target screen first so the new size is applied with the 375 // correct DPI in Per-Monitor v2 mode. 376 // Then boundedMove will correct the position if necessary. 377 SetWindowPos(m_handle, nullptr, x, y, 0, 0, 378 SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE); 379 #endif 380 381 SetWindowPos(m_handle, nullptr, 0, 0, width, height, 382 SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); 383 onResize(); 384 385 boundedMove(x, y); 386 387 data.erase(data.begin(), it); 388 } 389 390 void Dialog::saveState(Serializer::Data &data) const 391 { 392 RECT rect; 393 GetWindowRect(m_handle, &rect); 394 395 data.push_back({rect.left, rect.top}); 396 data.push_back({rect.right - rect.left, rect.bottom - rect.top}); 397 } 398 399 void Dialog::onInit() 400 { 401 RECT rect; 402 GetWindowRect(m_handle, &rect); 403 m_minimumSize = {rect.right - rect.left, rect.bottom - rect.top}; 404 405 center(); 406 407 m_resizer.init(m_handle); 408 } 409 410 void Dialog::onTimer(int) 411 { 412 } 413 414 void Dialog::onCommand(const int id, int) 415 { 416 switch(id) { 417 case IDOK: 418 close(1); 419 break; 420 case IDCANCEL: 421 close(0); 422 break; 423 } 424 } 425 426 void Dialog::onNotify(LPNMHDR info, LPARAM lParam) 427 { 428 const auto &it = m_controls.find(static_cast<int>(info->idFrom)); 429 430 if(it != m_controls.end()) 431 it->second->onNotify(info, lParam); 432 } 433 434 void Dialog::onContextMenu(HWND target, const int x, const int y) 435 { 436 for(const auto &[id, ctrl] : m_controls) { 437 (void)id; 438 439 if(!IsWindowVisible(ctrl->handle())) 440 continue; 441 442 // target HWND is not always accurate: 443 // on macOS it does not match the listview when hovering the column header 444 445 RECT rect; 446 GetWindowRect(ctrl->handle(), &rect); 447 448 #ifdef __APPLE__ 449 // special treatment for SWELL on macOS 450 std::swap(rect.top, rect.bottom); 451 #endif 452 453 const POINT point{x, y}; 454 if(target == ctrl->handle() || PtInRect(&rect, point)) { 455 if(ctrl->onContextMenu(m_handle, x, y)) 456 return; 457 } 458 } 459 } 460 461 bool Dialog::onKeyDown(int, int) 462 { 463 return false; 464 } 465 466 void Dialog::onResize() 467 { 468 m_resizer.onResize(); 469 470 #ifdef __APPLE__ 471 // Fix for wrong control positions after a sudden change of window size 472 // SWELL has code to do this when some mysterious isOpaque property is set. 473 // See https://forum.cockos.com/showthread.php?t=187585. 474 InvalidateRect(m_handle, nullptr, false); 475 #endif 476 } 477 478 void Dialog::onClose() 479 { 480 }