DPF

DISTRHO Plugin Framework
Log | Files | Refs | Submodules | README | LICENSE

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