reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

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 }