diff DPF-Prymula-audioplugins/dpf/dgl/src/WindowPrivateData.cpp @ 3:84e66ea83026

DPF-Prymula-audioplugins-0.231015-2
author prymula <prymula76@outlook.com>
date Mon, 16 Oct 2023 21:53:34 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/DPF-Prymula-audioplugins/dpf/dgl/src/WindowPrivateData.cpp	Mon Oct 16 21:53:34 2023 +0200
@@ -0,0 +1,1191 @@
+/*
+ * DISTRHO Plugin Framework (DPF)
+ * Copyright (C) 2012-2022 Filipe Coelho <falktx@falktx.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any purpose with
+ * or without fee is hereby granted, provided that the above copyright notice and this
+ * permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
+ * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
+ * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
+ * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "WindowPrivateData.hpp"
+#include "TopLevelWidgetPrivateData.hpp"
+
+#include "pugl.hpp"
+
+// #define DGL_DEBUG_EVENTS
+
+#if defined(DEBUG) && defined(DGL_DEBUG_EVENTS)
+# ifdef DISTRHO_PROPER_CPP11_SUPPORT
+#  include <cinttypes>
+# else
+#  include <inttypes.h>
+# endif
+#endif
+
+START_NAMESPACE_DGL
+
+#ifdef DGL_DEBUG_EVENTS
+# define DGL_DBG(msg)  std::fprintf(stderr, "%s", msg);
+# define DGL_DBGp(...) std::fprintf(stderr, __VA_ARGS__);
+# define DGL_DBGF      std::fflush(stderr);
+#else
+# define DGL_DBG(msg)
+# define DGL_DBGp(...)
+# define DGL_DBGF
+#endif
+
+#define DEFAULT_WIDTH 640
+#define DEFAULT_HEIGHT 480
+
+#define FOR_EACH_TOP_LEVEL_WIDGET(it) \
+  for (std::list<TopLevelWidget*>::iterator it = topLevelWidgets.begin(); it != topLevelWidgets.end(); ++it)
+
+#define FOR_EACH_TOP_LEVEL_WIDGET_INV(rit) \
+  for (std::list<TopLevelWidget*>::reverse_iterator rit = topLevelWidgets.rbegin(); rit != topLevelWidgets.rend(); ++rit)
+
+// -----------------------------------------------------------------------
+
+static double getScaleFactorFromParent(const PuglView* const view)
+{
+    // allow custom scale for testing
+    if (const char* const scale = getenv("DPF_SCALE_FACTOR"))
+        return std::max(1.0, std::atof(scale));
+
+    if (view != nullptr)
+        return puglGetScaleFactorFromParent(view);
+
+    return 1.0;
+}
+
+static PuglView* puglNewViewWithTransientParent(PuglWorld* const world, PuglView* const transientParentView)
+{
+    DISTRHO_SAFE_ASSERT_RETURN(world != nullptr, nullptr);
+
+    if (PuglView* const view = puglNewView(world))
+    {
+        puglSetTransientParent(view, puglGetNativeView(transientParentView));
+        return view;
+    }
+
+    return nullptr;
+}
+
+static PuglView* puglNewViewWithParentWindow(PuglWorld* const world, const uintptr_t parentWindowHandle)
+{
+    DISTRHO_SAFE_ASSERT_RETURN(world != nullptr, nullptr);
+
+    if (PuglView* const view = puglNewView(world))
+    {
+        puglSetParentWindow(view, parentWindowHandle);
+        return view;
+    }
+
+    return nullptr;
+}
+
+// -----------------------------------------------------------------------
+
+Window::PrivateData::PrivateData(Application& a, Window* const s)
+    : app(a),
+      appData(a.pData),
+      self(s),
+      view(appData->world != nullptr ? puglNewView(appData->world) : nullptr),
+      topLevelWidgets(),
+      isClosed(true),
+      isVisible(false),
+      isEmbed(false),
+      usesSizeRequest(false),
+      scaleFactor(getScaleFactorFromParent(view)),
+      autoScaling(false),
+      autoScaleFactor(1.0),
+      minWidth(0),
+      minHeight(0),
+      keepAspectRatio(false),
+      ignoreIdleCallbacks(false),
+      waitingForClipboardData(false),
+      waitingForClipboardEvents(false),
+      clipboardTypeId(0),
+      filenameToRenderInto(nullptr),
+#ifndef DGL_FILE_BROWSER_DISABLED
+      fileBrowserHandle(nullptr),
+#endif
+      modal()
+{
+    initPre(DEFAULT_WIDTH, DEFAULT_HEIGHT, false);
+}
+
+Window::PrivateData::PrivateData(Application& a, Window* const s, PrivateData* const ppData)
+    : app(a),
+      appData(a.pData),
+      self(s),
+      view(puglNewViewWithTransientParent(appData->world, ppData->view)),
+      topLevelWidgets(),
+      isClosed(true),
+      isVisible(false),
+      isEmbed(false),
+      usesSizeRequest(false),
+      scaleFactor(ppData->scaleFactor),
+      autoScaling(false),
+      autoScaleFactor(1.0),
+      minWidth(0),
+      minHeight(0),
+      keepAspectRatio(false),
+      ignoreIdleCallbacks(false),
+      waitingForClipboardData(false),
+      waitingForClipboardEvents(false),
+      clipboardTypeId(0),
+      filenameToRenderInto(nullptr),
+#ifndef DGL_FILE_BROWSER_DISABLED
+      fileBrowserHandle(nullptr),
+#endif
+      modal(ppData)
+{
+    initPre(DEFAULT_WIDTH, DEFAULT_HEIGHT, false);
+}
+
+Window::PrivateData::PrivateData(Application& a, Window* const s,
+                                 const uintptr_t parentWindowHandle,
+                                 const double scale, const bool resizable)
+    : app(a),
+      appData(a.pData),
+      self(s),
+      view(puglNewViewWithParentWindow(appData->world, parentWindowHandle)),
+      topLevelWidgets(),
+      isClosed(parentWindowHandle == 0),
+      isVisible(parentWindowHandle != 0),
+      isEmbed(parentWindowHandle != 0),
+      usesSizeRequest(false),
+      scaleFactor(scale != 0.0 ? scale : getScaleFactorFromParent(view)),
+      autoScaling(false),
+      autoScaleFactor(1.0),
+      minWidth(0),
+      minHeight(0),
+      keepAspectRatio(false),
+      ignoreIdleCallbacks(false),
+      waitingForClipboardData(false),
+      waitingForClipboardEvents(false),
+      clipboardTypeId(0),
+      filenameToRenderInto(nullptr),
+#ifndef DGL_FILE_BROWSER_DISABLED
+      fileBrowserHandle(nullptr),
+#endif
+      modal()
+{
+    initPre(DEFAULT_WIDTH, DEFAULT_HEIGHT, resizable);
+}
+
+Window::PrivateData::PrivateData(Application& a, Window* const s,
+                                 const uintptr_t parentWindowHandle,
+                                 const uint width, const uint height,
+                                 const double scale, const bool resizable, const bool usesSizeRequest_)
+    : app(a),
+      appData(a.pData),
+      self(s),
+      view(puglNewViewWithParentWindow(appData->world, parentWindowHandle)),
+      topLevelWidgets(),
+      isClosed(parentWindowHandle == 0),
+      isVisible(parentWindowHandle != 0 && view != nullptr),
+      isEmbed(parentWindowHandle != 0),
+      usesSizeRequest(usesSizeRequest_),
+      scaleFactor(scale != 0.0 ? scale : getScaleFactorFromParent(view)),
+      autoScaling(false),
+      autoScaleFactor(1.0),
+      minWidth(0),
+      minHeight(0),
+      keepAspectRatio(false),
+      ignoreIdleCallbacks(false),
+      waitingForClipboardData(false),
+      waitingForClipboardEvents(false),
+      clipboardTypeId(0),
+      filenameToRenderInto(nullptr),
+#ifndef DGL_FILE_BROWSER_DISABLED
+      fileBrowserHandle(nullptr),
+#endif
+      modal()
+{
+    if (isEmbed)
+        puglSetParentWindow(view, parentWindowHandle);
+
+    initPre(width != 0 ? width : DEFAULT_WIDTH, height != 0 ? height : DEFAULT_HEIGHT, resizable);
+}
+
+Window::PrivateData::~PrivateData()
+{
+    appData->idleCallbacks.remove(this);
+    appData->windows.remove(self);
+    std::free(filenameToRenderInto);
+
+    if (view == nullptr)
+        return;
+
+    if (isEmbed)
+    {
+#ifndef DGL_FILE_BROWSER_DISABLED
+        if (fileBrowserHandle != nullptr)
+            fileBrowserClose(fileBrowserHandle);
+#endif
+        puglHide(view);
+        appData->oneWindowClosed();
+        isClosed = true;
+        isVisible = false;
+    }
+
+    puglFreeView(view);
+}
+
+// -----------------------------------------------------------------------
+
+void Window::PrivateData::initPre(const uint width, const uint height, const bool resizable)
+{
+    appData->windows.push_back(self);
+    appData->idleCallbacks.push_back(this);
+    memset(graphicsContext, 0, sizeof(graphicsContext));
+
+    if (view == nullptr)
+    {
+        d_stderr2("Failed to create Pugl view, everything will fail!");
+        return;
+    }
+
+    puglSetMatchingBackendForCurrentBuild(view);
+    puglSetHandle(view, this);
+
+    puglSetViewHint(view, PUGL_RESIZABLE, resizable ? PUGL_TRUE : PUGL_FALSE);
+    puglSetViewHint(view, PUGL_IGNORE_KEY_REPEAT, PUGL_FALSE);
+#if DGL_USE_RGBA
+    puglSetViewHint(view, PUGL_DEPTH_BITS, 24);
+#else
+    puglSetViewHint(view, PUGL_DEPTH_BITS, 16);
+#endif
+    puglSetViewHint(view, PUGL_STENCIL_BITS, 8);
+
+    // PUGL_SAMPLES ??
+    puglSetEventFunc(view, puglEventCallback);
+
+    // setting default size triggers system-level calls, do it last
+    puglSetSizeHint(view, PUGL_DEFAULT_SIZE, width, height);
+}
+
+bool Window::PrivateData::initPost()
+{
+    if (view == nullptr)
+        return false;
+
+    // create view now, as a few methods we allow devs to use require it
+    if (puglRealize(view) != PUGL_SUCCESS)
+    {
+        view = nullptr;
+        d_stderr2("Failed to realize Pugl view, everything will fail!");
+        return false;
+    }
+
+    if (isEmbed)
+    {
+        appData->oneWindowShown();
+        puglShow(view);
+    }
+
+    return true;
+}
+
+// -----------------------------------------------------------------------
+
+void Window::PrivateData::close()
+{
+    DGL_DBG("Window close\n");
+    // DGL_DBGp("Window close DBG %i %i %p\n", isEmbed, isClosed, appData);
+
+    if (isEmbed || isClosed)
+        return;
+
+    isClosed = true;
+    hide();
+    appData->oneWindowClosed();
+}
+
+// -----------------------------------------------------------------------
+
+void Window::PrivateData::show()
+{
+    if (isVisible)
+    {
+        DGL_DBG("Window show matches current visible state, ignoring request\n");
+        return;
+    }
+    if (isEmbed)
+    {
+        DGL_DBG("Window show cannot be called when embedded\n");
+        return;
+    }
+
+    DGL_DBG("Window show called\n");
+
+    if (view == nullptr)
+        return;
+
+    if (isClosed)
+    {
+        isClosed = false;
+        appData->oneWindowShown();
+
+        // FIXME
+//         PuglRect rect = puglGetFrame(view);
+//         puglSetWindowSize(view, static_cast<uint>(rect.width), static_cast<uint>(rect.height));
+
+#if defined(DISTRHO_OS_WINDOWS)
+        puglWin32ShowCentered(view);
+#elif defined(DISTRHO_OS_MAC)
+        puglMacOSShowCentered(view);
+#else
+        puglShow(view);
+#endif
+    }
+    else
+    {
+#ifdef DISTRHO_OS_WINDOWS
+        puglWin32RestoreWindow(view);
+#else
+        puglShow(view);
+#endif
+    }
+
+    isVisible = true;
+}
+
+void Window::PrivateData::hide()
+{
+    if (isEmbed)
+    {
+        DGL_DBG("Window hide cannot be called when embedded\n");
+        return;
+    }
+    if (! isVisible)
+    {
+        DGL_DBG("Window hide matches current visible state, ignoring request\n");
+        return;
+    }
+
+    DGL_DBG("Window hide called\n");
+
+    if (modal.enabled)
+        stopModal();
+
+#ifndef DGL_FILE_BROWSER_DISABLED
+    if (fileBrowserHandle != nullptr)
+    {
+        fileBrowserClose(fileBrowserHandle);
+        fileBrowserHandle = nullptr;
+    }
+#endif
+
+    puglHide(view);
+
+    isVisible = false;
+}
+
+// -----------------------------------------------------------------------
+
+void Window::PrivateData::focus()
+{
+    if (view == nullptr)
+        return;
+
+    if (! isEmbed)
+        puglRaiseWindow(view);
+
+    puglGrabFocus(view);
+}
+
+// -----------------------------------------------------------------------
+
+void Window::PrivateData::setResizable(const bool resizable)
+{
+    DISTRHO_SAFE_ASSERT_RETURN(! isEmbed,);
+
+    DGL_DBG("Window setResizable called\n");
+
+    puglSetResizable(view, resizable);
+}
+
+// -----------------------------------------------------------------------
+
+void Window::PrivateData::idleCallback()
+{
+#ifndef DGL_FILE_BROWSER_DISABLED
+    if (fileBrowserHandle != nullptr && fileBrowserIdle(fileBrowserHandle))
+    {
+        self->onFileSelected(fileBrowserGetPath(fileBrowserHandle));
+        fileBrowserClose(fileBrowserHandle);
+        fileBrowserHandle = nullptr;
+    }
+#endif
+}
+
+// -----------------------------------------------------------------------
+// idle callback stuff
+
+bool Window::PrivateData::addIdleCallback(IdleCallback* const callback, const uint timerFrequencyInMs)
+{
+    if (ignoreIdleCallbacks)
+        return false;
+
+    if (timerFrequencyInMs == 0)
+    {
+        appData->idleCallbacks.push_back(callback);
+        return true;
+    }
+
+    return puglStartTimer(view, (uintptr_t)callback, static_cast<double>(timerFrequencyInMs) / 1000.0) == PUGL_SUCCESS;
+}
+
+bool Window::PrivateData::removeIdleCallback(IdleCallback* const callback)
+{
+    if (ignoreIdleCallbacks)
+        return false;
+
+    if (std::find(appData->idleCallbacks.begin(),
+                  appData->idleCallbacks.end(), callback) != appData->idleCallbacks.end())
+    {
+        appData->idleCallbacks.remove(callback);
+        return true;
+    }
+
+    return puglStopTimer(view, (uintptr_t)callback) == PUGL_SUCCESS;
+}
+
+#ifndef DGL_FILE_BROWSER_DISABLED
+// -----------------------------------------------------------------------
+// file handling
+
+bool Window::PrivateData::openFileBrowser(const FileBrowserOptions& options)
+{
+    if (fileBrowserHandle != nullptr)
+        fileBrowserClose(fileBrowserHandle);
+
+    FileBrowserOptions options2 = options;
+
+    if (options2.title == nullptr)
+        options2.title = puglGetWindowTitle(view);
+
+    fileBrowserHandle = fileBrowserCreate(isEmbed,
+                                          puglGetNativeView(view),
+                                          autoScaling ? autoScaleFactor : scaleFactor,
+                                          options2);
+
+    return fileBrowserHandle != nullptr;
+}
+#endif // ! DGL_FILE_BROWSER_DISABLED
+
+// -----------------------------------------------------------------------
+// modal handling
+
+void Window::PrivateData::startModal()
+{
+    DGL_DBG("Window modal loop starting..."); DGL_DBGF;
+    DISTRHO_SAFE_ASSERT_RETURN(modal.parent != nullptr, show());
+
+    // activate modal mode for this window
+    modal.enabled = true;
+
+    // make parent give focus to us
+    modal.parent->modal.child = this;
+
+    // make sure both parent and ourselves are visible
+    modal.parent->show();
+    show();
+
+#ifdef DISTRHO_OS_MAC
+    puglMacOSAddChildWindow(modal.parent->view, view);
+#endif
+
+    DGL_DBG("Ok\n");
+}
+
+void Window::PrivateData::stopModal()
+{
+    DGL_DBG("Window modal loop stopping..."); DGL_DBGF;
+
+    // deactivate modal mode
+    modal.enabled = false;
+
+    // safety checks, make sure we have a parent and we are currently active as the child to give focus to
+    if (modal.parent == nullptr)
+        return;
+    if (modal.parent->modal.child != this)
+        return;
+
+#ifdef DISTRHO_OS_MAC
+    puglMacOSRemoveChildWindow(modal.parent->view, view);
+#endif
+
+    // stop parent from giving focus to us, so it behaves like normal
+    modal.parent->modal.child = nullptr;
+
+    // refocus main window after closing child
+    if (! modal.parent->isClosed)
+    {
+        const Widget::MotionEvent ev;
+        modal.parent->onPuglMotion(ev);
+        modal.parent->focus();
+    }
+
+    DGL_DBG("Ok\n");
+}
+
+void Window::PrivateData::runAsModal(const bool blockWait)
+{
+    DGL_DBGp("Window::PrivateData::runAsModal %i\n", blockWait);
+    startModal();
+
+    if (blockWait)
+    {
+        DISTRHO_SAFE_ASSERT_RETURN(appData->isStandalone,);
+
+        while (isVisible && modal.enabled)
+            appData->idle(10);
+
+        stopModal();
+    }
+    else
+    {
+        appData->idle(0);
+    }
+}
+
+// -----------------------------------------------------------------------
+// pugl events
+
+void Window::PrivateData::onPuglConfigure(const double width, const double height)
+{
+    DISTRHO_SAFE_ASSERT_INT2_RETURN(width > 1 && height > 1, width, height,);
+
+    DGL_DBGp("PUGL: onReshape : %f %f\n", width, height);
+
+    if (autoScaling)
+    {
+        const double scaleHorizontal = width  / static_cast<double>(minWidth);
+        const double scaleVertical   = height / static_cast<double>(minHeight);
+        autoScaleFactor = scaleHorizontal < scaleVertical ? scaleHorizontal : scaleVertical;
+    }
+
+    const uint uwidth = static_cast<uint>(width + 0.5);
+    const uint uheight = static_cast<uint>(height + 0.5);
+
+    self->onReshape(uwidth, uheight);
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET(it)
+    {
+        TopLevelWidget* const widget(*it);
+
+        /* Some special care here, we call Widget::setSize instead of the TopLevelWidget one.
+         * This is because we want TopLevelWidget::setSize to handle both window and widget size,
+         * but we dont want to change window size here, because we are the window..
+         * So we just call the Widget specific method manually.
+         *
+         * Alternatively, we could expose a resize function on the pData, like done with the display function.
+         * But there is nothing extra we need to do in there, so this works fine.
+         */
+        ((Widget*)widget)->setSize(uwidth, uheight);
+    }
+#endif
+
+    // always repaint after a resize
+    puglPostRedisplay(view);
+}
+
+void Window::PrivateData::onPuglExpose()
+{
+    // DGL_DBG("PUGL: onPuglExpose\n");
+
+    puglOnDisplayPrepare(view);
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET(it)
+    {
+        TopLevelWidget* const widget(*it);
+
+        if (widget->isVisible())
+            widget->pData->display();
+    }
+
+    if (char* const filename = filenameToRenderInto)
+    {
+        const PuglRect rect = puglGetFrame(view);
+        filenameToRenderInto = nullptr;
+        renderToPicture(filename, getGraphicsContext(), static_cast<uint>(rect.width), static_cast<uint>(rect.height));
+        std::free(filename);
+    }
+#endif
+}
+
+void Window::PrivateData::onPuglClose()
+{
+    DGL_DBG("PUGL: onClose\n");
+
+#ifndef DISTRHO_OS_MAC
+    // if we are running as standalone we can prevent closing in certain conditions
+    if (appData->isStandalone)
+    {
+        // a child window is active, gives focus to it
+        if (modal.child != nullptr)
+            return modal.child->focus();
+
+        // ask window if we should close
+        if (! self->onClose())
+            return;
+    }
+#endif
+
+    if (modal.enabled)
+        stopModal();
+
+    if (modal.child != nullptr)
+    {
+        modal.child->close();
+        modal.child = nullptr;
+    }
+
+    close();
+}
+
+void Window::PrivateData::onPuglFocus(const bool focus, const CrossingMode mode)
+{
+    DGL_DBGp("onPuglFocus : %i %i | %i\n", focus, mode, isClosed);
+
+    if (isClosed)
+        return;
+
+    if (modal.child != nullptr)
+        return modal.child->focus();
+
+    self->onFocus(focus, mode);
+}
+
+void Window::PrivateData::onPuglKey(const Widget::KeyboardEvent& ev)
+{
+    DGL_DBGp("onPuglKey : %i %u %u\n", ev.press, ev.key, ev.keycode);
+
+    if (modal.child != nullptr)
+        return modal.child->focus();
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET_INV(rit)
+    {
+        TopLevelWidget* const widget(*rit);
+
+        if (widget->isVisible() && widget->onKeyboard(ev))
+            break;
+    }
+#endif
+}
+
+void Window::PrivateData::onPuglText(const Widget::CharacterInputEvent& ev)
+{
+    DGL_DBGp("onPuglText : %u %u %s\n", ev.keycode, ev.character, ev.string);
+
+    if (modal.child != nullptr)
+        return modal.child->focus();
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET_INV(rit)
+    {
+        TopLevelWidget* const widget(*rit);
+
+        if (widget->isVisible() && widget->onCharacterInput(ev))
+            break;
+    }
+#endif
+}
+
+void Window::PrivateData::onPuglMouse(const Widget::MouseEvent& ev)
+{
+    DGL_DBGp("onPuglMouse : %i %i %f %f\n", ev.button, ev.press, ev.pos.getX(), ev.pos.getY());
+
+    if (modal.child != nullptr)
+        return modal.child->focus();
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET_INV(rit)
+    {
+        TopLevelWidget* const widget(*rit);
+
+        if (widget->isVisible() && widget->onMouse(ev))
+            break;
+    }
+#endif
+}
+
+void Window::PrivateData::onPuglMotion(const Widget::MotionEvent& ev)
+{
+    DGL_DBGp("onPuglMotion : %f %f\n", ev.pos.getX(), ev.pos.getY());
+
+    if (modal.child != nullptr)
+        return modal.child->focus();
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET_INV(rit)
+    {
+        TopLevelWidget* const widget(*rit);
+
+        if (widget->isVisible() && widget->onMotion(ev))
+            break;
+    }
+#endif
+}
+
+void Window::PrivateData::onPuglScroll(const Widget::ScrollEvent& ev)
+{
+    DGL_DBGp("onPuglScroll : %f %f %f %f\n", ev.pos.getX(), ev.pos.getY(), ev.delta.getX(), ev.delta.getY());
+
+    if (modal.child != nullptr)
+        return modal.child->focus();
+
+#ifndef DPF_TEST_WINDOW_CPP
+    FOR_EACH_TOP_LEVEL_WIDGET_INV(rit)
+    {
+        TopLevelWidget* const widget(*rit);
+
+        if (widget->isVisible() && widget->onScroll(ev))
+            break;
+    }
+#endif
+}
+
+const void* Window::PrivateData::getClipboard(size_t& dataSize)
+{
+    clipboardTypeId = 0;
+    waitingForClipboardData = true,
+    waitingForClipboardEvents = true;
+
+    // begin clipboard dance here
+    if (puglPaste(view) != PUGL_SUCCESS)
+    {
+        dataSize = 0;
+        waitingForClipboardEvents = false;
+        return nullptr;
+    }
+
+   #ifdef DGL_USING_X11
+    // wait for type request, clipboardTypeId must be != 0 to be valid
+    int retry = static_cast<int>(2 / 0.03);
+    while (clipboardTypeId == 0 && waitingForClipboardData && --retry >= 0)
+    {
+        if (puglX11UpdateWithoutExposures(appData->world) != PUGL_SUCCESS)
+            break;
+    }
+   #endif
+
+    if (clipboardTypeId == 0)
+    {
+        dataSize = 0;
+        waitingForClipboardEvents = false;
+        return nullptr;
+    }
+
+   #ifdef DGL_USING_X11
+    // wait for actual data (assumes offer was accepted)
+    retry = static_cast<int>(2 / 0.03);
+    while (waitingForClipboardData && --retry >= 0)
+    {
+        if (puglX11UpdateWithoutExposures(appData->world) != PUGL_SUCCESS)
+            break;
+    }
+   #endif
+
+    if (clipboardTypeId == 0)
+    {
+        dataSize = 0;
+        waitingForClipboardEvents = false;
+        return nullptr;
+    }
+
+    waitingForClipboardEvents = false;
+    return puglGetClipboard(view, clipboardTypeId - 1, &dataSize);
+}
+
+uint32_t Window::PrivateData::onClipboardDataOffer()
+{
+    DGL_DBG("onClipboardDataOffer\n");
+
+    if ((clipboardTypeId = self->onClipboardDataOffer()) != 0)
+        return clipboardTypeId;
+
+    // stop waiting for data, it was rejected
+    waitingForClipboardData = false;
+    return 0;
+}
+
+void Window::PrivateData::onClipboardData(const uint32_t typeId)
+{
+    if (clipboardTypeId != typeId)
+        clipboardTypeId = 0;
+
+    waitingForClipboardData = false;
+}
+
+#if defined(DEBUG) && defined(DGL_DEBUG_EVENTS)
+static int printEvent(const PuglEvent* event, const char* prefix, const bool verbose);
+#endif
+
+PuglStatus Window::PrivateData::puglEventCallback(PuglView* const view, const PuglEvent* const event)
+{
+    Window::PrivateData* const pData = (Window::PrivateData*)puglGetHandle(view);
+#if defined(DEBUG) && defined(DGL_DEBUG_EVENTS)
+    if (event->type != PUGL_TIMER) {
+        printEvent(event, "pugl event: ", true);
+    }
+#endif
+
+    if (pData->waitingForClipboardEvents)
+    {
+        switch (event->type)
+        {
+        case PUGL_UPDATE:
+        case PUGL_EXPOSE:
+        case PUGL_FOCUS_IN:
+        case PUGL_FOCUS_OUT:
+        case PUGL_KEY_PRESS:
+        case PUGL_KEY_RELEASE:
+        case PUGL_TEXT:
+        case PUGL_POINTER_IN:
+        case PUGL_POINTER_OUT:
+        case PUGL_BUTTON_PRESS:
+        case PUGL_BUTTON_RELEASE:
+        case PUGL_MOTION:
+        case PUGL_SCROLL:
+        case PUGL_TIMER:
+        case PUGL_LOOP_ENTER:
+        case PUGL_LOOP_LEAVE:
+            return PUGL_SUCCESS;
+        case PUGL_DATA_OFFER:
+        case PUGL_DATA:
+            break;
+        default:
+            d_stdout("Got event %d while waitingForClipboardEvents", event->type);
+            break;
+        }
+    }
+
+    switch (event->type)
+    {
+    ///< No event
+    case PUGL_NOTHING:
+        break;
+
+    ///< View created, a #PuglEventCreate
+    case PUGL_CREATE:
+       #ifdef DGL_USING_X11
+        if (! pData->isEmbed)
+            puglX11SetWindowTypeAndPID(view, pData->appData->isStandalone);
+       #endif
+        break;
+
+    ///< View destroyed, a #PuglEventDestroy
+    case PUGL_DESTROY:
+        break;
+
+    ///< View moved/resized, a #PuglEventConfigure
+    case PUGL_CONFIGURE:
+        // unused x, y (double)
+        pData->onPuglConfigure(event->configure.width, event->configure.height);
+        break;
+
+    ///< View made visible, a #PuglEventMap
+    case PUGL_MAP:
+        break;
+
+    ///< View made invisible, a #PuglEventUnmap
+    case PUGL_UNMAP:
+        break;
+
+    ///< View ready to draw, a #PuglEventUpdate
+    case PUGL_UPDATE:
+        break;
+
+    ///< View must be drawn, a #PuglEventExpose
+    case PUGL_EXPOSE:
+        // unused x, y, width, height (double)
+        pData->onPuglExpose();
+        break;
+
+    ///< View will be closed, a #PuglEventClose
+    case PUGL_CLOSE:
+        pData->onPuglClose();
+        break;
+
+    ///< Keyboard focus entered view, a #PuglEventFocus
+    case PUGL_FOCUS_IN:
+    ///< Keyboard focus left view, a #PuglEventFocus
+    case PUGL_FOCUS_OUT:
+        pData->onPuglFocus(event->type == PUGL_FOCUS_IN,
+                           static_cast<CrossingMode>(event->focus.mode));
+        break;
+
+    ///< Key pressed, a #PuglEventKey
+    case PUGL_KEY_PRESS:
+    ///< Key released, a #PuglEventKey
+    case PUGL_KEY_RELEASE:
+    {
+        // unused x, y, xRoot, yRoot (double)
+        Widget::KeyboardEvent ev;
+        ev.mod     = event->key.state;
+        ev.flags   = event->key.flags;
+        ev.time    = static_cast<uint>(event->key.time * 1000.0 + 0.5);
+        ev.press   = event->type == PUGL_KEY_PRESS;
+        ev.key     = event->key.key;
+        ev.keycode = event->key.keycode;
+
+        // keyboard events must always be lowercase
+        if (ev.key >= 'A' && ev.key <= 'Z')
+        {
+            ev.key += 'a' - 'A'; // A-Z -> a-z
+            ev.mod |= kModifierShift;
+        }
+
+        pData->onPuglKey(ev);
+        break;
+    }
+
+    ///< Character entered, a #PuglEventText
+    case PUGL_TEXT:
+    {
+        // unused x, y, xRoot, yRoot (double)
+        Widget::CharacterInputEvent ev;
+        ev.mod       = event->text.state;
+        ev.flags     = event->text.flags;
+        ev.time      = static_cast<uint>(event->text.time * 1000.0 + 0.5);
+        ev.keycode   = event->text.keycode;
+        ev.character = event->text.character;
+        std::strncpy(ev.string, event->text.string, sizeof(ev.string));
+        pData->onPuglText(ev);
+        break;
+    }
+
+    ///< Pointer entered view, a #PuglEventCrossing
+    case PUGL_POINTER_IN:
+        break;
+    ///< Pointer left view, a #PuglEventCrossing
+    case PUGL_POINTER_OUT:
+        break;
+
+    ///< Mouse button pressed, a #PuglEventButton
+    case PUGL_BUTTON_PRESS:
+    ///< Mouse button released, a #PuglEventButton
+    case PUGL_BUTTON_RELEASE:
+    {
+        Widget::MouseEvent ev;
+        ev.mod    = event->button.state;
+        ev.flags  = event->button.flags;
+        ev.time   = static_cast<uint>(event->button.time * 1000.0 + 0.5);
+        ev.button = event->button.button + 1;
+        ev.press  = event->type == PUGL_BUTTON_PRESS;
+        ev.pos    = Point<double>(event->button.x, event->button.y);
+        ev.absolutePos = ev.pos;
+        pData->onPuglMouse(ev);
+        break;
+    }
+
+    ///< Pointer moved, a #PuglEventMotion
+    case PUGL_MOTION:
+    {
+        Widget::MotionEvent ev;
+        ev.mod   = event->motion.state;
+        ev.flags = event->motion.flags;
+        ev.time  = static_cast<uint>(event->motion.time * 1000.0 + 0.5);
+        ev.pos   = Point<double>(event->motion.x, event->motion.y);
+        ev.absolutePos = ev.pos;
+        pData->onPuglMotion(ev);
+        break;
+    }
+
+    ///< Scrolled, a #PuglEventScroll
+    case PUGL_SCROLL:
+    {
+        Widget::ScrollEvent ev;
+        ev.mod       = event->scroll.state;
+        ev.flags     = event->scroll.flags;
+        ev.time      = static_cast<uint>(event->scroll.time * 1000.0 + 0.5);
+        ev.pos       = Point<double>(event->scroll.x, event->scroll.y);
+        ev.delta     = Point<double>(event->scroll.dx, event->scroll.dy);
+        ev.direction = static_cast<ScrollDirection>(event->scroll.direction);
+        ev.absolutePos = ev.pos;
+        pData->onPuglScroll(ev);
+        break;
+    }
+
+    ///< Custom client message, a #PuglEventClient
+    case PUGL_CLIENT:
+        break;
+
+    ///< Timer triggered, a #PuglEventTimer
+    case PUGL_TIMER:
+        if (IdleCallback* const idleCallback = reinterpret_cast<IdleCallback*>(event->timer.id))
+            idleCallback->idleCallback();
+        break;
+
+    ///< Recursive loop entered, a #PuglEventLoopEnter
+    case PUGL_LOOP_ENTER:
+        break;
+
+    ///< Recursive loop left, a #PuglEventLoopLeave
+    case PUGL_LOOP_LEAVE:
+        break;
+
+    ///< Data offered from clipboard, a #PuglDataOfferEvent
+    case PUGL_DATA_OFFER:
+        if (const uint32_t offerTypeId = pData->onClipboardDataOffer())
+            puglAcceptOffer(view, &event->offer, offerTypeId - 1);
+        break;
+
+    ///< Data available from clipboard, a #PuglDataEvent
+    case PUGL_DATA:
+        pData->onClipboardData(event->data.typeIndex + 1);
+        break;
+    }
+
+    return PUGL_SUCCESS;
+}
+
+// -----------------------------------------------------------------------
+
+#if defined(DEBUG) && defined(DGL_DEBUG_EVENTS)
+static int printModifiers(const uint32_t mods)
+{
+	return fprintf(stderr, "Modifiers:%s%s%s%s\n",
+	               (mods & PUGL_MOD_SHIFT) ? " Shift"   : "",
+	               (mods & PUGL_MOD_CTRL)  ? " Ctrl"    : "",
+	               (mods & PUGL_MOD_ALT)   ? " Alt"     : "",
+	               (mods & PUGL_MOD_SUPER) ? " Super" : "");
+}
+
+static int printEvent(const PuglEvent* event, const char* prefix, const bool verbose)
+{
+#define FFMT            "%6.1f"
+#define PFMT            FFMT " " FFMT
+#define PRINT(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
+
+	switch (event->type) {
+	case PUGL_NOTHING:
+		return 0;
+	case PUGL_KEY_PRESS:
+		return PRINT("%sKey press   code %3u key  U+%04X\n",
+		             prefix,
+		             event->key.keycode,
+		             event->key.key);
+	case PUGL_KEY_RELEASE:
+		return PRINT("%sKey release code %3u key  U+%04X\n",
+		             prefix,
+		             event->key.keycode,
+		             event->key.key);
+	case PUGL_TEXT:
+		return PRINT("%sText entry  code %3u char U+%04X (%s)\n",
+		             prefix,
+		             event->text.keycode,
+		             event->text.character,
+		             event->text.string);
+	case PUGL_BUTTON_PRESS:
+	case PUGL_BUTTON_RELEASE:
+		return (PRINT("%sMouse %u %s at " PFMT " ",
+		              prefix,
+		              event->button.button,
+		              (event->type == PUGL_BUTTON_PRESS) ? "down" : "up  ",
+		              event->button.x,
+		              event->button.y) +
+		        printModifiers(event->scroll.state));
+	case PUGL_SCROLL:
+		return (PRINT("%sScroll %5.1f %5.1f at " PFMT " ",
+		              prefix,
+		              event->scroll.dx,
+		              event->scroll.dy,
+		              event->scroll.x,
+		              event->scroll.y) +
+		        printModifiers(event->scroll.state));
+	case PUGL_POINTER_IN:
+		return PRINT("%sMouse enter  at " PFMT "\n",
+		             prefix,
+		             event->crossing.x,
+		             event->crossing.y);
+	case PUGL_POINTER_OUT:
+		return PRINT("%sMouse leave  at " PFMT "\n",
+		             prefix,
+		             event->crossing.x,
+		             event->crossing.y);
+	case PUGL_FOCUS_IN:
+		return PRINT("%sFocus in %i\n",
+		             prefix,
+		             event->focus.mode);
+	case PUGL_FOCUS_OUT:
+		return PRINT("%sFocus out %i\n",
+		             prefix,
+		             event->focus.mode);
+	case PUGL_CLIENT:
+		return PRINT("%sClient %" PRIXPTR " %" PRIXPTR "\n",
+		             prefix,
+		             event->client.data1,
+		             event->client.data2);
+	case PUGL_TIMER:
+		return PRINT("%sTimer %" PRIuPTR "\n", prefix, event->timer.id);
+	default:
+		break;
+	}
+
+	if (verbose) {
+		switch (event->type) {
+		case PUGL_CREATE:
+			return fprintf(stderr, "%sCreate\n", prefix);
+		case PUGL_DESTROY:
+			return fprintf(stderr, "%sDestroy\n", prefix);
+		case PUGL_MAP:
+			return fprintf(stderr, "%sMap\n", prefix);
+		case PUGL_UNMAP:
+			return fprintf(stderr, "%sUnmap\n", prefix);
+		case PUGL_UPDATE:
+			return 0; // fprintf(stderr, "%sUpdate\n", prefix);
+		case PUGL_CONFIGURE:
+			return PRINT("%sConfigure " PFMT " " PFMT "\n",
+			             prefix,
+			             event->configure.x,
+			             event->configure.y,
+			             event->configure.width,
+			             event->configure.height);
+		case PUGL_EXPOSE:
+			return PRINT("%sExpose    " PFMT " " PFMT "\n",
+			             prefix,
+			             event->expose.x,
+			             event->expose.y,
+			             event->expose.width,
+			             event->expose.height);
+		case PUGL_CLOSE:
+			return PRINT("%sClose\n", prefix);
+		case PUGL_MOTION:
+			return PRINT("%sMouse motion at " PFMT "\n",
+			             prefix,
+			             event->motion.x,
+			             event->motion.y);
+		default:
+			return PRINT("%sUnknown event type %d\n", prefix, (int)event->type);
+		}
+	}
+
+#undef PRINT
+#undef PFMT
+#undef FFMT
+
+	return 0;
+}
+#endif
+
+#undef DGL_DBG
+#undef DGL_DBGF
+
+// -----------------------------------------------------------------------
+
+END_NAMESPACE_DGL