ExternalWindow.hpp (16883B)
1 /* 2 * DISTRHO Plugin Framework (DPF) 3 * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com> 4 * 5 * Permission to use, copy, modify, and/or distribute this software for any purpose with 6 * or without fee is hereby granted, provided that the above copyright notice and this 7 * permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 10 * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN 11 * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 12 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER 13 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 14 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 #ifndef DISTRHO_EXTERNAL_WINDOW_HPP_INCLUDED 18 #define DISTRHO_EXTERNAL_WINDOW_HPP_INCLUDED 19 20 #include "String.hpp" 21 22 #ifndef DISTRHO_OS_WINDOWS 23 # include <cerrno> 24 # include <signal.h> 25 # include <sys/wait.h> 26 # include <unistd.h> 27 #endif 28 29 START_NAMESPACE_DISTRHO 30 31 // ----------------------------------------------------------------------- 32 // ExternalWindow class 33 34 /** 35 External Window class. 36 37 This is a standalone TopLevelWidget/Window-compatible class, but without any real event handling. 38 Being compatible with TopLevelWidget/Window, it allows to be used as DPF UI target. 39 40 It can be used to embed non-DPF things or to run a tool in a new process as the "UI". 41 The uiIdle() function will be called at regular intervals to keep UI running. 42 There are helper methods in place to launch external tools and keep track of its running state. 43 44 External windows can be setup to run in 3 different modes: 45 * Embed: 46 Embed into the host UI, even-loop driven by the host. 47 This is basically working as a regular plugin UI, as you typically expect them to. 48 The plugin side does not get control over showing, hiding or closing the window (as usual for plugins). 49 No restrictions on supported plugin format, everything should work. 50 Requires DISTRHO_PLUGIN_HAS_EMBED_UI to be set to 1. 51 52 * Semi-external: 53 The UI is not embed into the host, but the even-loop is still driven by it. 54 In this mode the host does not have control over the UI except for showing, hiding and setting transient parent. 55 It is possible to close the window from the plugin, the host will be notified of such case. 56 Host regularly calls isQuitting() to check if the UI got closed by the user or plugin side. 57 This mode is only possible in LV2 plugin formats, using lv2ui:showInterface extension. 58 59 * Standalone: 60 The UI is not embed into the host or uses its event-loop, basically running as standalone. 61 The host only has control over showing and hiding the window, nothing else. 62 The UI is still free to close itself at any point. 63 DPF will keep calling isRunning() to check if it should keep the event-loop running. 64 Only possible in JACK and DSSI targets, as the UIs are literally standalone applications there. 65 66 Please note that for non-embed windows, you cannot show the window yourself. 67 The plugin window is only allowed to hide or close itself, a "show" action needs to come from the host. 68 69 A few callbacks are provided so that implementations do not need to care about checking for state changes. 70 They are not called on construction, but will be everytime something changes either by the host or the window itself. 71 */ 72 class ExternalWindow 73 { 74 struct PrivateData; 75 76 public: 77 /** 78 Constructor. 79 */ 80 explicit ExternalWindow() 81 : pData() {} 82 83 /** 84 Constructor for DPF internal use. 85 */ 86 explicit ExternalWindow(const PrivateData& data) 87 : pData(data) {} 88 89 /** 90 Destructor. 91 */ 92 virtual ~ExternalWindow() 93 { 94 DISTRHO_SAFE_ASSERT(!pData.visible); 95 } 96 97 /* -------------------------------------------------------------------------------------------------------- 98 * ExternalWindow specific calls - Host side calls that you can reimplement for fine-grained funtionality */ 99 100 /** 101 Check if main-loop is running. 102 This is used under standalone mode to check whether to keep things running. 103 Returning false from this function will stop the event-loop and close the window. 104 */ 105 virtual bool isRunning() const 106 { 107 #ifndef DISTRHO_OS_WINDOWS 108 if (ext.inUse) 109 return ext.isRunning(); 110 #endif 111 return isVisible(); 112 } 113 114 /** 115 Check if we are about to close. 116 This is used when the event-loop is provided by the host to check if it should close the window. 117 It is also used in standalone mode right after isRunning() returns false to verify if window needs to be closed. 118 */ 119 virtual bool isQuitting() const 120 { 121 #ifndef DISTRHO_OS_WINDOWS 122 return ext.inUse ? ext.isQuitting : pData.isQuitting; 123 #else 124 return pData.isQuitting; 125 #endif 126 } 127 128 /** 129 Get the "native" window handle. 130 This can be reimplemented in order to pass the native window to hosts that can use such informaton. 131 132 Returned value type depends on the platform: 133 - HaikuOS: This is a pointer to a `BView`. 134 - MacOS: This is a pointer to an `NSView*`. 135 - Windows: This is a `HWND`. 136 - Everything else: This is an [X11] `Window`. 137 138 @note Only available to override if DISTRHO_PLUGIN_HAS_EMBED_UI is set to 1. 139 */ 140 virtual uintptr_t getNativeWindowHandle() const noexcept 141 { 142 return 0; 143 } 144 145 /** 146 Grab the keyboard input focus. 147 Typically you would setup OS-native methods to bring the window to front and give it focus. 148 Default implementation does nothing. 149 */ 150 virtual void focus() {} 151 152 /* -------------------------------------------------------------------------------------------------------- 153 * TopLevelWidget-like calls - Information, can be called by either host or plugin */ 154 155 #if DISTRHO_PLUGIN_HAS_EMBED_UI 156 /** 157 Whether this Window is embed into another (usually not DGL-controlled) Window. 158 */ 159 bool isEmbed() const noexcept 160 { 161 return pData.parentWindowHandle != 0; 162 } 163 #endif 164 165 /** 166 Check if this window is visible. 167 @see setVisible(bool) 168 */ 169 bool isVisible() const noexcept 170 { 171 return pData.visible; 172 } 173 174 /** 175 Whether this Window is running as standalone, that is, without being coupled to a host event-loop. 176 When in standalone mode, isRunning() is called to check if the event-loop should keep running. 177 */ 178 bool isStandalone() const noexcept 179 { 180 return pData.isStandalone; 181 } 182 183 /** 184 Get width of this window. 185 Only relevant to hosts when the UI is embedded. 186 */ 187 uint getWidth() const noexcept 188 { 189 return pData.width; 190 } 191 192 /** 193 Get height of this window. 194 Only relevant to hosts when the UI is embedded. 195 */ 196 uint getHeight() const noexcept 197 { 198 return pData.height; 199 } 200 201 /** 202 Get the scale factor requested for this window. 203 This is purely informational, and up to developers to choose what to do with it. 204 */ 205 double getScaleFactor() const noexcept 206 { 207 return pData.scaleFactor; 208 } 209 210 /** 211 Get the title of the window previously set with setTitle(). 212 This is typically displayed in the title bar or in window switchers. 213 */ 214 const char* getTitle() const noexcept 215 { 216 return pData.title; 217 } 218 219 #if DISTRHO_PLUGIN_HAS_EMBED_UI 220 /** 221 Get the "native" window handle that this window should embed itself into. 222 Returned value type depends on the platform: 223 - HaikuOS: This is a pointer to a `BView`. 224 - MacOS: This is a pointer to an `NSView*`. 225 - Windows: This is a `HWND`. 226 - Everything else: This is an [X11] `Window`. 227 */ 228 uintptr_t getParentWindowHandle() const noexcept 229 { 230 return pData.parentWindowHandle; 231 } 232 #endif 233 234 /** 235 Get the transient window that we should attach ourselves to. 236 TODO what id? also NSView* on macOS, or NSWindow? 237 */ 238 uintptr_t getTransientWindowId() const noexcept 239 { 240 return pData.transientWinId; 241 } 242 243 /* -------------------------------------------------------------------------------------------------------- 244 * TopLevelWidget-like calls - actions called by either host or plugin */ 245 246 /** 247 Hide window. 248 This is the same as calling setVisible(false). 249 Embed windows should never call this! 250 @see isVisible(), setVisible(bool) 251 */ 252 void hide() 253 { 254 setVisible(false); 255 } 256 257 /** 258 Hide the UI and gracefully terminate. 259 Embed windows should never call this! 260 */ 261 virtual void close() 262 { 263 pData.isQuitting = true; 264 hide(); 265 #ifndef DISTRHO_OS_WINDOWS 266 if (ext.inUse) 267 terminateAndWaitForExternalProcess(); 268 #endif 269 } 270 271 /** 272 Set width of this window. 273 Can trigger a sizeChanged callback. 274 Only relevant to hosts when the UI is embedded. 275 */ 276 void setWidth(uint width) 277 { 278 setSize(width, getHeight()); 279 } 280 281 /** 282 Set height of this window. 283 Can trigger a sizeChanged callback. 284 Only relevant to hosts when the UI is embedded. 285 */ 286 void setHeight(uint height) 287 { 288 setSize(getWidth(), height); 289 } 290 291 /** 292 Set size of this window using @a width and @a height values. 293 Can trigger a sizeChanged callback. 294 Only relevant to hosts when the UI is embedded. 295 */ 296 void setSize(uint width, uint height) 297 { 298 DISTRHO_SAFE_ASSERT_UINT_RETURN(width > 1, width,); 299 DISTRHO_SAFE_ASSERT_UINT_RETURN(height > 1, height,); 300 301 if (pData.width == width && pData.height == height) 302 return; 303 304 pData.width = width; 305 pData.height = height; 306 sizeChanged(width, height); 307 } 308 309 /** 310 Set the title of the window, typically displayed in the title bar or in window switchers. 311 Can trigger a titleChanged callback. 312 Only relevant to hosts when the UI is not embedded. 313 */ 314 void setTitle(const char* title) 315 { 316 if (pData.title == title) 317 return; 318 319 pData.title = title; 320 titleChanged(title); 321 } 322 323 /** 324 Set geometry constraints for the Window when resized by the user. 325 */ 326 void setGeometryConstraints(uint minimumWidth, uint minimumHeight, bool keepAspectRatio = false) 327 { 328 DISTRHO_SAFE_ASSERT_UINT_RETURN(minimumWidth > 0, minimumWidth,); 329 DISTRHO_SAFE_ASSERT_UINT_RETURN(minimumHeight > 0, minimumHeight,); 330 331 pData.minWidth = minimumWidth; 332 pData.minHeight = minimumHeight; 333 pData.keepAspectRatio = keepAspectRatio; 334 } 335 336 /* -------------------------------------------------------------------------------------------------------- 337 * TopLevelWidget-like calls - actions called by the host */ 338 339 /** 340 Show window. 341 This is the same as calling setVisible(true). 342 @see isVisible(), setVisible(bool) 343 */ 344 void show() 345 { 346 setVisible(true); 347 } 348 349 /** 350 Set window visible (or not) according to @a visible. 351 @see isVisible(), hide(), show() 352 */ 353 void setVisible(bool visible) 354 { 355 if (pData.visible == visible) 356 return; 357 358 pData.visible = visible; 359 visibilityChanged(visible); 360 } 361 362 /** 363 Called by the host to set the transient parent window that we should attach ourselves to. 364 TODO what id? also NSView* on macOS, or NSWindow? 365 */ 366 void setTransientWindowId(uintptr_t winId) 367 { 368 if (pData.transientWinId == winId) 369 return; 370 371 pData.transientWinId = winId; 372 transientParentWindowChanged(winId); 373 } 374 375 protected: 376 /* -------------------------------------------------------------------------------------------------------- 377 * ExternalWindow special calls for running externals tools */ 378 379 bool startExternalProcess(const char* args[]) 380 { 381 #ifndef DISTRHO_OS_WINDOWS 382 ext.inUse = true; 383 384 return ext.start(args); 385 #else 386 (void)args; 387 return false; // TODO 388 #endif 389 } 390 391 void terminateAndWaitForExternalProcess() 392 { 393 #ifndef DISTRHO_OS_WINDOWS 394 ext.isQuitting = true; 395 ext.terminateAndWait(); 396 #else 397 // TODO 398 #endif 399 } 400 401 /* -------------------------------------------------------------------------------------------------------- 402 * ExternalWindow specific callbacks */ 403 404 /** 405 A callback for when the window size changes. 406 @note WIP this might need to get fed back into the host somehow. 407 */ 408 virtual void sizeChanged(uint /* width */, uint /* height */) 409 { 410 // unused, meant for custom implementations 411 } 412 413 /** 414 A callback for when the window title changes. 415 @note WIP this might need to get fed back into the host somehow. 416 */ 417 virtual void titleChanged(const char* /* title */) 418 { 419 // unused, meant for custom implementations 420 } 421 422 /** 423 A callback for when the window visibility changes. 424 @note WIP this might need to get fed back into the host somehow. 425 */ 426 virtual void visibilityChanged(bool /* visible */) 427 { 428 // unused, meant for custom implementations 429 } 430 431 /** 432 A callback for when the transient parent window changes. 433 */ 434 virtual void transientParentWindowChanged(uintptr_t /* winId */) 435 { 436 // unused, meant for custom implementations 437 } 438 439 private: 440 friend class PluginWindow; 441 friend class UI; 442 443 #ifndef DISTRHO_OS_WINDOWS 444 struct ExternalProcess { 445 bool inUse; 446 bool isQuitting; 447 mutable pid_t pid; 448 449 ExternalProcess() 450 : inUse(false), 451 isQuitting(false), 452 pid(0) {} 453 454 bool isRunning() const noexcept 455 { 456 if (pid <= 0) 457 return false; 458 459 const pid_t p = ::waitpid(pid, nullptr, WNOHANG); 460 461 if (p == pid || (p == -1 && errno == ECHILD)) 462 { 463 d_stdout("NOTICE: Child process exited while idle"); 464 pid = 0; 465 return false; 466 } 467 468 return true; 469 } 470 471 bool start(const char* args[]) 472 { 473 terminateAndWait(); 474 475 pid = vfork(); 476 477 switch (pid) 478 { 479 case 0: 480 execvp(args[0], (char**)args); 481 _exit(1); 482 return false; 483 484 case -1: 485 d_stderr("Could not start external ui"); 486 return false; 487 488 default: 489 return true; 490 } 491 } 492 493 void terminateAndWait() 494 { 495 if (pid <= 0) 496 return; 497 498 d_stdout("Waiting for external process to stop,,,"); 499 500 bool sendTerm = true; 501 502 for (pid_t p;;) 503 { 504 p = ::waitpid(pid, nullptr, WNOHANG); 505 506 switch (p) 507 { 508 case 0: 509 if (sendTerm) 510 { 511 sendTerm = false; 512 ::kill(pid, SIGTERM); 513 } 514 break; 515 516 case -1: 517 if (errno == ECHILD) 518 { 519 d_stdout("Done! (no such process)"); 520 pid = 0; 521 return; 522 } 523 break; 524 525 default: 526 if (p == pid) 527 { 528 d_stdout("Done! (clean wait)"); 529 pid = 0; 530 return; 531 } 532 break; 533 } 534 535 // 5 msec 536 usleep(5*1000); 537 } 538 } 539 } ext; 540 #endif 541 542 struct PrivateData { 543 uintptr_t parentWindowHandle; 544 uintptr_t transientWinId; 545 uint width; 546 uint height; 547 double scaleFactor; 548 String title; 549 uint minWidth; 550 uint minHeight; 551 bool keepAspectRatio; 552 bool isQuitting; 553 bool isStandalone; 554 bool visible; 555 556 PrivateData() 557 : parentWindowHandle(0), 558 transientWinId(0), 559 width(1), 560 height(1), 561 scaleFactor(1.0), 562 title(), 563 minWidth(0), 564 minHeight(0), 565 keepAspectRatio(false), 566 isQuitting(false), 567 isStandalone(false), 568 visible(false) {} 569 } pData; 570 571 DISTRHO_DECLARE_NON_COPYABLE(ExternalWindow) 572 }; 573 574 // ----------------------------------------------------------------------- 575 576 END_NAMESPACE_DISTRHO 577 578 #endif // DISTRHO_EXTERNAL_WINDOW_HPP_INCLUDED