15

I have a Window subclass in my project, and at runtime the instance is created and shown entirely on the QML side. I know that I can prevent the window from being minimized by not including the WindowMinimizeButtonHint in the flags:, but I actually need to have the minimize button present and enabled but be able to intercept the minimize button click, cancel the actual minimizing, and do something else (FYI my client is requiring this non-standard windowing behavior, not me).

So far, the only thing I've been able to achieve is to handle the onWindowStateChanged: event, check if windowState === Qt.WindowStateMinimized and call show() from a timer (calling it inside the event handler directly does nothing). This results in the window moving down to the system tray and then suddenly coming back up to normal.

Is there any way to do this, something like an OnMinimized event that can be cancelled?

Edit: based on Benjamin T's answer, I'm at least part way to a solution for OSX:

#import <AppKit/AppKit.h>

bool NativeFilter::nativeEventFilter(const QByteArray &eventType, 
    void *message, long *result)
{
    if (eventType == "mac_generic_NSEvent") {
        NSEvent *event = static_cast<NSEvent *>(message);
        if ([event type] == NSKeyDown) {
            return true;
        }
    }
    return false;
}

In this example I'm able to intercept and cancel all NSKeyDown events (while leaving other events like mouse clicks etc. still working). The remaining problem is that I still don't know to intercept a minimize event - NSEvent.h doesn't seem to have anything that covers that. Perhaps I need to cast to a different type of event?

Edit 2 - working solution:

I was not able to find any way to intercept the minimize event proper and cancel it, so my workaround is to instead intercept the click on the window, determine if the click is over the minimize button (or the close or zoom buttons) and cancel the event if so (and send a notification to my qml window that the click occurred). I also handle the case of double-clicking the titlebar to zoom the window, and using the Command-M keys to minimize the window.

First step is to implement a QAbstractNativeEventFilter. In your header:

#include <QAbstractNativeEventFilter>

class NativeFilter : public QAbstractNativeEventFilter {
public:
    bool nativeEventFilter(const QByteArray &eventType, void *message, 
        long *result);
};

The implementation:

#import <AppKit/AppKit.h>
#import <AppKit/NSWindow.h>
#import <AppKit/NSButton.h>

bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void 
    *message, long *result)
{
    if (eventType == "mac_generic_NSEvent") {

        NSEvent *event = static_cast<NSEvent *>(message);
        NSWindow *win = [event window];

        // TODO: determine whether or not this is a window whose
        // events you want to intercept. I did this by checking
        // [win title] but you may want to find and use the 
        // window's id instead.

        // Detect a double-click on the titlebar. If the zoom button 
        // is enabled, send the full-screen message to the window
        if ([event type] == NSLeftMouseUp) {
            if ([event clickCount] > 1) {
                NSPoint pt = [event locationInWindow];
                CGRect rect = [win frame];
                // event coordinates have y going in the opposite direction from frame coordinates, very annoying
                CGFloat yInverted = rect.size.height - pt.y;
                if (yInverted <= 20) {
                    // TODO: need the proper metrics for the height of the title bar

                    NSButton *btn = [win standardWindowButton:NSWindowZoomButton];
                    if (btn.enabled) {

                        // notify qml of zoom button click

                    }

                    return true;
                }
            }
        }

        if ([event type] == NSKeyDown) {

            // detect command-M (for minimize app)
            if ([event modifierFlags] & NSCommandKeyMask) {

                // M key
                if ([event keyCode] == 46) {
                    // notify qml of miniaturize button click
                    return true;
                }

            }

            // TODO: we may be requested to handle keyboard actions for close and zoom buttons. e.g. ctrl-cmd-F is zoom, I think,
            // and Command-H is hide.

        }


        if ([event type] == NSLeftMouseDown) {

            NSPoint pt = [event locationInWindow];
            CGRect rect = [win frame];

            // event coordinates have y going in the opposite direction from frame coordinates, very annoying
            CGFloat yInverted = rect.size.height - pt.y;

            NSButton *btn = [win standardWindowButton:NSWindowMiniaturizeButton];
            CGRect rectButton = [btn frame];
            if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
                if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {

                    // notify .qml of miniaturize button click

                    return true;
                }
            }

            btn = [win standardWindowButton:NSWindowZoomButton];
            rectButton = [btn frame];

            if (btn.enabled) {
                if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
                    if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {

                        // notify qml of zoom button click

                        return true;
                    }
                }
            }

            btn = [win standardWindowButton:NSWindowCloseButton];
            rectButton = [btn frame];
            if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
                if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {

                    // notify qml of close button click

                    return true;
                }
            }

        }

        return false;

    }

    return false;
}

Then in main.cpp:

Application app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • 2
    This is a very weird behaviour. What is it you need to do instead of the OS minimize ? Can't you put this feature on a control inside your application ? (because window management is OS job). The only way I know so far is to implement your own title-bar and hide the one from the OS, then you can have any behaviour you like (but it breaks the style). – ymoreau Jul 26 '18 at 11:15
  • @ymoreau my client wants this second window to basically minimize into the app's main window instead of into the title bar. We've actually already implemented a custom title bar that does what we want, but for OSX (only) they want a native titlebar. I completely agree that this is weird behavior, but it's not my call. – MusiGenesis Jul 26 '18 at 13:21
  • 2
    Correction: instead of into the *system tray – MusiGenesis Jul 26 '18 at 13:30
  • Your need sounds a lot like a dock-widget instead of a second OS-window, but I don't see a minimize feature in [`QDockWidget`](http://doc.qt.io/qt-5/qdockwidget.html#details), and no QML Item to do this. – ymoreau Jul 26 '18 at 13:48
  • @ymoreau I'm going to do a quick experiment with a QDockWidget and see if it will do what I need. This might be too-drastic surgery for my deadline eight hours from now. – MusiGenesis Jul 26 '18 at 22:32
  • AFAIK and that I've could see in the source code, when a QWidget is minimized there is nothing prepared to cancel it (state is changed, event is sent, but nothing to abort the action) – cbuchart Jul 27 '18 at 06:50

1 Answers1

5

Generally speaking, you should use the event system ans not signal/slots to intercept events and changes.

The easiest way to do so is either to subclass the object you use and reimplement the appropriate event handler, or to use an event filter.

Since you are using QML, subclassing might be difficult as you don't have access to all Qt internal classes.

Here is what the code would look like when using event filtering.

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);


    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    if (engine.rootObjects().isEmpty())
        return -1;

    auto root = engine.rootObjects().first();
    root->installEventFilter(new EventFilter());

    return app.exec();
}

class EventFilter : public QObject
{
    Q_OBJECT
public:
    explicit EventFilter(QObject *parent = nullptr);
    bool eventFilter(QObject *watched, QEvent *event) override;
};

bool EventFilter::eventFilter(QObject *watched, QEvent *event)
{
    if (event->type() == QEvent::WindowStateChange) {
        auto e = static_cast<QWindowStateChangeEvent *>(event);
        auto window = static_cast<QWindow *>(watched);

        if (window->windowStates().testFlag(Qt::WindowMinimized)
                && ! e->oldState().testFlag(Qt::WindowMinimized))
        {
            // Restore old state
            window->setWindowStates(e->oldState());
            return true;
        }
    }

    // Do not filter event
    return false;
}

However, you will quickly run into the same issue that when using the signal/slot mechanism: Qt only notify you when the window has already been minimized. Meaning that restoring the window at this point will make a hide/show effect.

So you need to go deeper and you a native event filter.

The following code works on Windows, you should adapt it for macOS:

class NativeFilter : public QAbstractNativeEventFilter {
public:
    bool nativeEventFilter(const QByteArray &eventType, void *message, long *result);
};

bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void *message, long *result)
{
/* On Windows we interceot the click in the title bar. */
/* If we wait for the minimize event, it is already too late. */
#ifdef Q_OS_WIN
    auto msg = static_cast<MSG *>(message);
    // Filter out the event when the minimize button is pressed.
    if (msg->message == WM_NCLBUTTONDOWN && msg->wParam == HTREDUCE)
        return true;
#endif

/* Example macOS code from Qt doc, adapt to your need */
#ifdef Q_OS_MACOS
    if (eventType == "mac_generic_NSEvent") {
        NSEvent *event = static_cast<NSEvent *>(message);
        if ([event type] == NSKeyDown) {
            // Handle key event
            qDebug() << QString::fromNSString([event characters]);
        }
}
#endif

    return false;
}

In your main():

QGuiApplication app(argc, argv);
app.installNativeEventFilter(new NativeFilter());

For more info, you can read the Qt documentation about QAbstractNativeEventFilter.

You may need to use QWindow::winId() to check to which window the native events are targeted.

As I am not a macOS developer, I do not know what you can do with NSEvent. Also it seems the NSWindowDelegate class could be useful to you: https://developer.apple.com/documentation/appkit/nswindowdelegate If you can retrieve a NSWindow from QWindow::winId(), you should be able to use it.

cbuchart
  • 10,847
  • 9
  • 53
  • 93
Benjamin T
  • 8,120
  • 20
  • 37
  • So this looks promising, but I have no idea how to adapt your code for OSX. Where/how would I look for how to do that? – MusiGenesis Jul 27 '18 at 11:16
  • I found this: http://doc.qt.io/qt-5/qabstractnativeeventfilter.html and it's mostly working - for example, I can use this to cancel any key down events (but not cancel mouse clicks etc.). My problem is that this code casts the event as `NSEvent` and there doesn't seem to be any case for NSEvent of a "minimize" event that can be intercepted and cancelled. There does seem to be a system event so I'm going to try that. – MusiGenesis Jul 27 '18 at 11:53
  • 1
    @MusiGenesis It seems you have found your way around the Qt documentation. I have added some details in my answer, notably about `NSWindowDelegate`. I do not know enough about development on macOS to help you more than this. – Benjamin T Jul 27 '18 at 19:14
  • You might be the only person who can appreciate this: after getting all this to work, my client just now decided they didn't want this non-standard behavior after all. – MusiGenesis Aug 06 '18 at 15:00
  • Hey, since you're a Windows guy, I was wondering if you could take a look at this question: https://stackoverflow.com/questions/51757438/how-can-i-get-a-borderless-child-window-to-re-scale-to-current-screen-in-multi-m – MusiGenesis Aug 09 '18 at 04:45