iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
ReaperExtBase.cpp
1// Helper to stringify macro
2#define IPLUG_STRINGIFY_HELPER(x) #x
3#define IPLUG_STRINGIFY(x) IPLUG_STRINGIFY_HELPER(x)
4
5ReaperExtBase::ReaperExtBase(reaper_plugin_info_t* pRec)
6: EDITOR_DELEGATE_CLASS(0) // zero params
7, mRec(pRec)
8{
9 mTimer = std::unique_ptr<Timer>(Timer::Create(std::bind(&ReaperExtBase::OnTimer, this, std::placeholders::_1), IDLE_TIMER_RATE));
10 mDockId.Set(IPLUG_STRINGIFY(PLUG_CLASS_NAME));
11 mMenuName.Set(IPLUG_STRINGIFY(PLUG_CLASS_NAME));
12 memset(&mDockState, 0, sizeof(ReaperExtDockState));
13 // Note: LoadDockState() is called lazily via EnsureStateLoaded() after API imports
14}
15
16ReaperExtBase::~ReaperExtBase()
17{
18 mTimer->Stop();
19 if (gHWND)
20 {
21 mSaveStateOnDestroy = false;
22 DestroyWindow(gHWND);
23 }
24}
25
26void ReaperExtBase::OnTimer(Timer& t)
27{
28 OnIdle();
29}
30
31auto ClientResize = [](HWND hWnd, int nWidth, int nHeight) {
32 RECT rcClient, rcWindow;
33 POINT ptDiff;
34 int screenwidth, screenheight;
35 int x, y;
36
37 screenwidth = GetSystemMetrics(SM_CXSCREEN);
38 screenheight = GetSystemMetrics(SM_CYSCREEN);
39 x = (screenwidth / 2) - (nWidth / 2);
40 y = (screenheight / 2) - (nHeight / 2);
41
42 GetClientRect(hWnd, &rcClient);
43 GetWindowRect(hWnd, &rcWindow);
44 ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right;
45 ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom;
46
47 SetWindowPos(hWnd, 0, x, y, nWidth + ptDiff.x, nHeight + ptDiff.y, 0);
48};
49
50bool ReaperExtBase::EditorResizeFromUI(int viewWidth, int viewHeight, bool needsPlatformResize)
51{
52 if (viewWidth != GetEditorWidth() || viewHeight != GetEditorHeight())
53 {
54 // Don't resize the window when docked — REAPER controls the dock size
55 if (!IsDocked() && needsPlatformResize)
56 {
57#ifdef OS_MAC
58#define TITLEBAR_BODGE 22 //TODO: sort this out
59 RECT r;
60 GetWindowRect(gHWND, &r);
61 SetWindowPos(gHWND, 0, r.left, r.bottom - viewHeight - TITLEBAR_BODGE, viewWidth, viewHeight + TITLEBAR_BODGE, 0);
62#endif
63 }
64
65 return true;
66 }
67
68 return false;
69}
70
73void ReaperExtBase::EnsureStateLoaded()
74{
75 if (mStateLoaded)
76 return;
77
78 LoadDockState();
79 mStateLoaded = true;
80 UpdateToggleStates();
81}
82
83void ReaperExtBase::CreateMainWindow()
84{
85 if (gHWND != NULL)
86 return;
87
88 EnsureStateLoaded();
89
90 gHWND = CreateDialog(gHINSTANCE, MAKEINTRESOURCE(IDD_DIALOG_MAIN), gParent, ReaperExtBase::MainDlgProc);
91
92 UpdateToggleStates();
93}
94
98void ReaperExtBase::UpdateToggleStates()
99{
100 mWindowToggle = (gHWND != NULL) ? 1 : 0;
101 mDockToggle = IsDocked() ? 1 : 0;
102}
103
104void ReaperExtBase::DestroyMainWindow()
105{
106 if (gHWND == NULL)
107 return;
108
109 SaveDockState();
110 gPlug->CloseWindow();
111 DockWindowRemove(gHWND);
112 DestroyWindow(gHWND);
113 gHWND = NULL;
114
115 UpdateToggleStates();
116}
117
119{
120 if (gHWND == NULL)
121 {
122 CreateMainWindow();
123 if (IsDocked())
124 DockWindowActivate(gHWND);
125 }
126 else
127 {
128 DestroyMainWindow();
129 }
130}
131
133{
134 EnsureStateLoaded();
135
136 // With no window open, just flip the persisted preference so the next open honours it
137 if (gHWND == NULL)
138 {
139 mDockState.state ^= 2;
140 SaveDockState();
141 UpdateToggleStates();
142 return;
143 }
144
145 // Save floating position before toggling
146 if (!IsDocked())
147 GetWindowRect(gHWND, &mDockState.r);
148
149 // Destroy and recreate - this is the SWS pattern for reliable dock toggling
150 mSaveStateOnDestroy = false;
151 gPlug->CloseWindow();
152 DockWindowRemove(gHWND);
153 DestroyWindow(gHWND);
154 gHWND = NULL;
155
156 // Toggle docked bit
157 mDockState.state ^= 2;
158 mSaveStateOnDestroy = true;
159
160 // Recreate window with new state
161 CreateMainWindow();
162 if (IsDocked())
163 DockWindowActivate(gHWND);
164}
165
166void ReaperExtBase::SaveDockState()
167{
168 const char* iniFile = get_ini_file();
169 if (!iniFile)
170 return;
171
172 if (gHWND != NULL)
173 {
174 int dockIdx = DockIsChildOfDock(gHWND, NULL);
175 if (dockIdx >= 0)
176 mDockState.whichdock = dockIdx;
177 else
178 GetWindowRect(gHWND, &mDockState.r);
179 }
180
181 // Set visible bit based on window state
182 if (gHWND != NULL && IsWindowVisible(gHWND))
183 mDockState.state |= 1;
184 else
185 mDockState.state &= ~1;
186
187 // Convert to little-endian for cross-platform compatibility
188 ReaperExtDockState stateLE;
189 memcpy(&stateLE, &mDockState, sizeof(ReaperExtDockState));
190 for (int i = 0; i < (int)(sizeof(ReaperExtDockState) / sizeof(int)); i++)
191 REAPER_MAKELEINTMEM(&((int*)&stateLE)[i]);
192
193 WritePrivateProfileStruct("iPlug2", mDockId.Get(), &stateLE, sizeof(ReaperExtDockState), iniFile);
194}
195
196void ReaperExtBase::LoadDockState()
197{
198 const char* iniFile = get_ini_file();
199 if (!iniFile)
200 return;
201
202 ReaperExtDockState stateLE;
203 if (GetPrivateProfileStruct("iPlug2", mDockId.Get(), &stateLE, sizeof(ReaperExtDockState), iniFile))
204 {
205 // Convert from little-endian
206 for (int i = 0; i < (int)(sizeof(ReaperExtDockState) / sizeof(int)); i++)
207 REAPER_MAKELEINTMEM(&((int*)&stateLE)[i]);
208 memcpy(&mDockState, &stateLE, sizeof(ReaperExtDockState));
209 }
210}
211
212void ReaperExtBase::RegisterAction(const char* actionName, std::function<void()> func, bool addMenuItem, int* pToggle, const char* contextMenuId, const char* menuLabel/*, IKeyPress keyCmd*/)
213{
214 ReaperAction action;
215
216 int commandID = mRec->Register("command_id", (void*) actionName /* ?? */);
217
218 assert(commandID);
219
220 action.func = func;
221 action.accel.accel.cmd = commandID;
222 action.accel.desc = actionName;
223 action.addMenuItem = addMenuItem;
224 action.pToggle = pToggle;
225
226 action.contextMenuId = contextMenuId;
227 action.menuLabel = menuLabel ? menuLabel : actionName;
228 gActions.push_back(action);
229
230 mRec->Register("gaccel", (void*) &gActions.back().accel);
231}
232
238static bool CheckActionMenuItem(HMENU hMenu, int commandId, bool checked)
239{
240 const int nItems = GetMenuItemCount(hMenu);
241
242 for (int i = 0; i < nItems; i++)
243 {
244 MENUITEMINFO mi = { sizeof(MENUITEMINFO), };
245 mi.fMask = MIIM_ID | MIIM_SUBMENU;
246
247 if (!GetMenuItemInfo(hMenu, i, TRUE, &mi))
248 continue;
249
250 if (mi.hSubMenu)
251 {
252 if (CheckActionMenuItem(mi.hSubMenu, commandId, checked))
253 return true;
254 }
255 else if (static_cast<int>(mi.wID) == commandId)
256 {
257 CheckMenuItem(hMenu, i, MF_BYPOSITION | (checked ? MF_CHECKED : MF_UNCHECKED));
258 return true;
259 }
260 }
261
262 return false;
263}
264
265static void AppendActionMenuItem(HMENU hMenu, const ReaperAction& action)
266{
267 MENUITEMINFO mi = { sizeof(MENUITEMINFO), };
268 mi.fMask = MIIM_TYPE | MIIM_ID;
269 mi.fType = MFT_STRING;
270 mi.dwTypeData = LPSTR(action.menuLabel);
271 mi.wID = action.accel.accel.cmd;
272 // Append to the end of the menu (works regardless of user customization)
273 InsertMenuItem(hMenu, GetMenuItemCount(hMenu), TRUE, &mi);
274}
275
276//static
277void ReaperExtBase::MenuHook(const char* menuidstr, void* menu, int flag)
278{
279 if (menuidstr == nullptr || menu == nullptr)
280 return;
281
282 HMENU hMenu = (HMENU) menu;
283
284 const bool isExtensionsMenu = strcmp(menuidstr, "Main extensions") == 0;
285
286 // flag==1: the menu is about to be shown. Per the SDK this - not flag==0 - is where
287 // check/grayed states are set, so toggle actions get a tick when they're on.
288 if (flag == 1)
289 {
290 for (auto& action : gActions)
291 {
292 const bool inThisMenu = (isExtensionsMenu && action.addMenuItem) ||
293 (action.contextMenuId && strcmp(action.contextMenuId, menuidstr) == 0);
294
295 if (!inThisMenu || action.pToggle == nullptr)
296 continue;
297
298 CheckActionMenuItem(hMenu, action.accel.accel.cmd, *action.pToggle != 0);
299 }
300
301 return;
302 }
303
304 // flag==0: the default menu is being initialized; this is when we may add items.
305 if (flag != 0)
306 return;
307
308 // The main Extensions menu, added by AddExtensionsMainMenu(). Give this extension its
309 // own submenu rather than adding items directly, so several extensions can coexist.
310 if (isExtensionsMenu)
311 {
312 HMENU hSubMenu = CreatePopupMenu();
313
314 for (auto& action : gActions)
315 {
316 if (action.addMenuItem)
317 AppendActionMenuItem(hSubMenu, action);
318 }
319
320 if (GetMenuItemCount(hSubMenu) == 0)
321 {
322 DestroyMenu(hSubMenu);
323 return;
324 }
325
326 MENUITEMINFO mi = { sizeof(MENUITEMINFO), };
327 mi.fMask = MIIM_TYPE | MIIM_SUBMENU;
328 mi.fType = MFT_STRING;
329 mi.dwTypeData = LPSTR(gPlug->mMenuName.Get());
330 mi.hSubMenu = hSubMenu; // owned by hMenu from here on; don't retain the handle
331 InsertMenuItem(hMenu, GetMenuItemCount(hMenu), TRUE, &mi);
332
333 return;
334 }
335
336 for (auto& action : gActions)
337 {
338 if (action.contextMenuId && strcmp(action.contextMenuId, menuidstr) == 0)
339 AppendActionMenuItem(hMenu, action);
340 }
341}
342
343//static
344void ReaperExtBase::PostCommandProc(int command, int flag)
345{
346 if (gPlug)
347 gPlug->OnActionRun(command, flag);
348}
349
350//static
351void ReaperExtBase::BeginLoadProjectState(bool isUndo, project_config_extension_t* reg)
352{
353 if (gPlug)
354 gPlug->OnBeginLoadProjectState(isUndo);
355}
356
357//static
358bool ReaperExtBase::ProcessExtensionLine(const char* line, ProjectStateContext* ctx, bool isUndo, project_config_extension_t* reg)
359{
360 return gPlug ? gPlug->LoadProjectStateLine(line) : false;
361}
362
363//static
364void ReaperExtBase::SaveExtensionConfig(ProjectStateContext* ctx, bool isUndo, project_config_extension_t* reg)
365{
366 if (gPlug)
367 gPlug->SaveProjectState(ctx);
368}
369
370//static
371bool ReaperExtBase::HookCommandProc(int command, int flag)
372{
373 auto it = std::find_if (gActions.begin(), gActions.end(), [&](const auto& e) { return e.accel.accel.cmd == command; });
374
375 if (it == gActions.end())
376 return false; // not ours - let REAPER pass it to the next hook
377
378 it->func();
379
380 return true; // handled; stop further hooks and the default action from running
381}
382
383//static
384int ReaperExtBase::ToggleActionCallback(int command)
385{
386 auto it = std::find_if (gActions.begin(), gActions.end(), [&](const auto& e) { return e.accel.accel.cmd == command; });
387
388 // -1 means "not this extension's action, or it doesn't toggle". Returning 0 here would
389 // claim every other extension's commands and report them as off, since REAPER walks the
390 // registered toggleaction hooks until one of them answers.
391 if (it == gActions.end() || it->pToggle == nullptr)
392 return -1;
393
394 return *it->pToggle;
395}
396
397//static
398WDL_DLGRET ReaperExtBase::MainDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
399{
400 extern float GetScaleForHWND(HWND hWnd);
401
402 switch (uMsg)
403 {
404 case WM_INITDIALOG:
405 {
406 auto scale = GetScaleForHWND(hwnd);
407
408 if (gPlug->IsDocked())
409 {
410 // Docked: register with dock system
411 DockWindowAddEx(hwnd, (char*)gPlug->mDockId.Get(), gPlug->mDockId.Get(), true);
412 }
413 else
414 {
415 // Floating: restore position and show
416 if (gPlug->mDockState.r.left || gPlug->mDockState.r.top ||
417 gPlug->mDockState.r.right || gPlug->mDockState.r.bottom)
418 {
419 EnsureNotCompletelyOffscreen(&gPlug->mDockState.r);
420 SetWindowPos(hwnd, NULL,
421 gPlug->mDockState.r.left, gPlug->mDockState.r.top,
422 gPlug->mDockState.r.right - gPlug->mDockState.r.left,
423 gPlug->mDockState.r.bottom - gPlug->mDockState.r.top,
424 SWP_NOZORDER);
425 }
426 else
427 {
428 ClientResize(hwnd, static_cast<int>(PLUG_WIDTH * scale), static_cast<int>(PLUG_HEIGHT * scale));
429 }
430 AttachWindowTopmostButton(hwnd);
431 ShowWindow(hwnd, SW_SHOW);
432 }
433
434 gPlug->OpenWindow(hwnd);
435
436 // Trigger initial resize now that IGraphics exists
437 // (WM_SIZE during SetWindowPos/DockWindowAddEx above fires before OpenWindow)
438 {
439 RECT r;
440 GetClientRect(hwnd, &r);
441 int w = r.right - r.left;
442 int h = r.bottom - r.top;
443#ifdef WEBVIEW_EDITOR_DELEGATE
444 // GetClientRect returns physical pixels. The WebView delegate expects logical
445 // (DPI-independent) dimensions and scales to physical internally, so convert here.
446 // (IGraphics, by contrast, wants physical and divides internally.) On a DPI-aware
447 // host like REAPER at 200%, skipping this would double-scale and clip the WebView.
448 w = static_cast<int>(w / scale);
449 h = static_cast<int>(h / scale);
450#endif
451 if (w > 0 && h > 0)
452 gPlug->OnParentWindowResize(w, h);
453 }
454
455 GetWindowRect(hwnd, &gPrevBounds);
456
457 return 0;
458 }
459 case WM_DESTROY:
460 {
461 if (gPlug->mSaveStateOnDestroy)
462 gPlug->SaveDockState();
463
464 DockWindowRemove(hwnd);
465 gHWND = NULL;
466 gPlug->UpdateToggleStates(); // also covers the user closing the window directly
467 return 0;
468 }
469 case WM_SIZE:
470 {
471#ifndef NO_IGRAPHICS
472 if (gPlug->GetUI())
473#endif
474 {
475 RECT r;
476 GetClientRect(hwnd, &r);
477 int w = r.right - r.left;
478 int h = r.bottom - r.top;
479#ifdef WEBVIEW_EDITOR_DELEGATE
480 // See WM_INITDIALOG: convert physical client size to logical for the WebView delegate.
481 const float scale = GetScaleForHWND(hwnd);
482 w = static_cast<int>(w / scale);
483 h = static_cast<int>(h / scale);
484#endif
485 if (w > 0 && h > 0)
486 gPlug->OnParentWindowResize(w, h);
487 }
488 return 0;
489 }
490 case WM_CLOSE:
491 gPlug->CloseWindow();
492 DestroyWindow(hwnd);
493 return 0;
494 }
495 return 0;
496}
State structure for dock window persistence - matches SWS pattern.
Definition: ReaperExtBase.h:32
void RegisterAction(const char *actionName, std::function< void()> func, bool addMenuItem=false, int *pToggle=nullptr, const char *contextMenuId=nullptr, const char *menuLabel=nullptr)
Registers an action with the REAPER extension system.
void ShowHideMainWindow()
Toggles the visibility of the main extension window.
bool IsDocked() const
Returns true if the window is currently docked.
Definition: ReaperExtBase.h:99
void ToggleDocking()
Toggles between docked and floating state.
virtual void OnIdle()
Called during idle processing - override to perform periodic tasks.
Definition: ReaperExtBase.h:55
Helper struct for registering Reaper Actions.
Base class for timer.
Definition: IPlugTimer.h:40