| | |
| | | // Copyright (C) 2023-2024 Stdware Collections (https://www.github.com/stdware) |
| | | // Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) |
| | | // SPDX-License-Identifier: Apache-2.0 |
| | | |
| | | #include "cocoawindowcontext_p.h" |
| | | |
| | | #include <objc/runtime.h> |
| | |
| | | |
| | | #include "qwkglobal_p.h" |
| | | #include "systemwindow_p.h" |
| | | |
| | | // https://forgetsou.github.io/2020/11/06/macos%E5%BC%80%E5%8F%91-%E5%85%B3%E9%97%AD-%E6%9C%80%E5%B0%8F%E5%8C%96-%E5%85%A8%E5%B1%8F%E5%B1%85%E4%B8%AD%E5%A4%84%E7%90%86(%E4%BB%BFMac%20QQ)/ |
| | | // https://nyrra33.com/2019/03/26/changing-titlebars-height/ |
| | | |
| | | namespace QWK { |
| | | |
| | |
| | | |
| | | struct QWK_NSWindowDelegate { |
| | | public: |
| | | enum NSEventType { |
| | | WillEnterFullScreen, |
| | | DidEnterFullScreen, |
| | | WillExitFullScreen, |
| | | DidExitFullScreen, |
| | | DidResize, |
| | | }; |
| | | |
| | | virtual ~QWK_NSWindowDelegate() = default; |
| | | virtual void windowWillEnterFullScreen(){}; |
| | | virtual void windowDidEnterFullScreen(){}; |
| | | virtual void windowWillExitFullScreen(){}; |
| | | virtual void windowDidExitFullScreen(){}; |
| | | virtual void windowDidResize(){}; |
| | | virtual void windowEvent(NSEventType eventType) = 0; |
| | | }; |
| | | |
| | | // |
| | |
| | | - (void)windowWillEnterFullScreen:(NSNotification *)notification { |
| | | auto nswindow = reinterpret_cast<NSWindow *>(notification.object); |
| | | if (auto proxy = QWK::g_proxyIndexes->value(nswindow)) { |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowWillEnterFullScreen(); |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowEvent( |
| | | QWK_NSWindowDelegate::WillEnterFullScreen); |
| | | } |
| | | } |
| | | |
| | | - (void)windowDidEnterFullScreen:(NSNotification *)notification { |
| | | auto nswindow = reinterpret_cast<NSWindow *>(notification.object); |
| | | if (auto proxy = QWK::g_proxyIndexes->value(nswindow)) { |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowDidEnterFullScreen(); |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowEvent( |
| | | QWK_NSWindowDelegate::DidEnterFullScreen); |
| | | } |
| | | } |
| | | |
| | | - (void)windowWillExitFullScreen:(NSNotification *)notification { |
| | | auto nswindow = reinterpret_cast<NSWindow *>(notification.object); |
| | | if (auto proxy = QWK::g_proxyIndexes->value(nswindow)) { |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowWillExitFullScreen(); |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowEvent( |
| | | QWK_NSWindowDelegate::WillExitFullScreen); |
| | | } |
| | | } |
| | | |
| | | - (void)windowDidExitFullScreen:(NSNotification *)notification { |
| | | auto nswindow = reinterpret_cast<NSWindow *>(notification.object); |
| | | if (auto proxy = QWK::g_proxyIndexes->value(nswindow)) { |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowDidExitFullScreen(); |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowEvent( |
| | | QWK_NSWindowDelegate::DidExitFullScreen); |
| | | } |
| | | } |
| | | |
| | | - (void)windowDidResize:(NSNotification *)notification { |
| | | auto nswindow = reinterpret_cast<NSWindow *>(notification.object); |
| | | if (auto proxy = QWK::g_proxyIndexes->value(nswindow)) { |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowDidResize(); |
| | | reinterpret_cast<QWK_NSWindowDelegate *>(proxy)->windowEvent( |
| | | QWK_NSWindowDelegate::DidResize); |
| | | } |
| | | } |
| | | |
| | |
| | | namespace QWK { |
| | | |
| | | struct NSWindowProxy : public QWK_NSWindowDelegate { |
| | | enum class BlurMode { |
| | | Dark, |
| | | Light, |
| | | None, |
| | | }; |
| | | |
| | | NSWindowProxy(NSWindow *macWindow) { |
| | | nswindow = macWindow; |
| | | g_proxyIndexes->insert(nswindow, this); |
| | |
| | | g_proxyIndexes->remove(nswindow); |
| | | } |
| | | |
| | | void windowWillEnterFullScreen() override { |
| | | } |
| | | // Delegate |
| | | void windowEvent(NSEventType eventType) override { |
| | | switch (eventType) { |
| | | case WillExitFullScreen: { |
| | | if (!screenRectCallback || !systemButtonVisible) |
| | | return; |
| | | |
| | | void windowDidEnterFullScreen() override { |
| | | } |
| | | // The system buttons will stuck at their default positions when the |
| | | // exit-fullscreen animation is running, we need to hide them until the |
| | | // animation finishes |
| | | for (const auto &button : systemButtons()) { |
| | | button.hidden = true; |
| | | } |
| | | break; |
| | | } |
| | | |
| | | void windowWillExitFullScreen() override { |
| | | if (systemButtonRect.isEmpty() || !systemButtonVisible) |
| | | return; |
| | | case DidExitFullScreen: { |
| | | if (!screenRectCallback || !systemButtonVisible) |
| | | return; |
| | | |
| | | // The system buttons will stuck at their default positions when the exit-fullscreen |
| | | // animation is running, we need to hide them until the animation finishes |
| | | for (const auto &button : systemButtons()) { |
| | | button.hidden = true; |
| | | for (const auto &button : systemButtons()) { |
| | | button.hidden = false; |
| | | } |
| | | updateSystemButtonRect(); |
| | | break; |
| | | } |
| | | |
| | | case DidResize: { |
| | | if (!screenRectCallback || !systemButtonVisible) { |
| | | return; |
| | | } |
| | | updateSystemButtonRect(); |
| | | break; |
| | | } |
| | | |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | |
| | | void windowDidExitFullScreen() override { |
| | | if (systemButtonRect.isEmpty() || !systemButtonVisible) |
| | | return; |
| | | |
| | | for (const auto &button : systemButtons()) { |
| | | button.hidden = false; |
| | | } |
| | | updateSystemButtonRect(); |
| | | } |
| | | |
| | | void windowDidResize() override { |
| | | if (systemButtonRect.isEmpty() || !systemButtonVisible) { |
| | | return; |
| | | } |
| | | updateSystemButtonRect(); |
| | | } |
| | | |
| | | // System buttons visibility |
| | | void setSystemButtonVisible(bool visible) { |
| | | systemButtonVisible = visible; |
| | | for (const auto &button : systemButtons()) { |
| | | button.hidden = !visible; |
| | | } |
| | | |
| | | if (systemButtonRect.isEmpty() || !visible) { |
| | | if (!screenRectCallback || !visible) { |
| | | return; |
| | | } |
| | | updateSystemButtonRect(); |
| | | } |
| | | |
| | | void setSystemButtonRect(const QRect &rect) { |
| | | systemButtonRect = rect; |
| | | // System buttons area |
| | | void setScreenRectCallback(const ScreenRectCallback &callback) { |
| | | screenRectCallback = callback; |
| | | |
| | | if (rect.isEmpty() || !systemButtonVisible) { |
| | | if (!callback || !systemButtonVisible) { |
| | | return; |
| | | } |
| | | updateSystemButtonRect(); |
| | | } |
| | | |
| | | void updateSystemButtonRect() { |
| | | // https://forgetsou.github.io/2020/11/06/macos%E5%BC%80%E5%8F%91-%E5%85%B3%E9%97%AD-%E6%9C%80%E5%B0%8F%E5%8C%96-%E5%85%A8%E5%B1%8F%E5%B1%85%E4%B8%AD%E5%A4%84%E7%90%86(%E4%BB%BFMac%20QQ)/ |
| | | |
| | | const auto &buttons = systemButtons(); |
| | | const auto &leftButton = buttons[0]; |
| | | const auto &midButton = buttons[1]; |
| | | const auto &rightButton = buttons[2]; |
| | | |
| | | auto titlebar = rightButton.superview; |
| | | int titlebarHeight = titlebar.frame.size.height; |
| | | |
| | | auto spacing = midButton.frame.origin.x - leftButton.frame.origin.x; |
| | | auto width = midButton.frame.size.width; |
| | | auto height = midButton.frame.size.height; |
| | | |
| | | QPoint center = systemButtonRect.center(); |
| | | auto viewSize = |
| | | nswindow.contentView ? nswindow.contentView.frame.size : nswindow.frame.size; |
| | | QPoint center = screenRectCallback(QSize(viewSize.width, titlebarHeight)).center(); |
| | | |
| | | // The origin of the NSWindow coordinate system is in the lower left corner, we |
| | | // do the necessary transformations |
| | | center.ry() = titlebarHeight - center.y(); |
| | | |
| | | // Mid button |
| | | NSPoint centerOrigin = { |
| | |
| | | return {closeBtn, minimizeBtn, zoomBtn}; |
| | | } |
| | | |
| | | inline int titleBarHeight() const { |
| | | NSButton *closeBtn = [nswindow standardWindowButton:NSWindowCloseButton]; |
| | | return closeBtn.superview.frame.size.height; |
| | | } |
| | | |
| | | // Blur effect |
| | | bool setBlurEffect(BlurMode mode) { |
| | | static Class visualEffectViewClass = NSClassFromString(@"NSVisualEffectView"); |
| | | if (!visualEffectViewClass) |
| | | return false; |
| | | |
| | | NSVisualEffectView *effectView = nil; |
| | | NSView *const view = [nswindow contentView]; |
| | | for (NSView *subview in [[view superview] subviews]) { |
| | | if ([subview isKindOfClass:visualEffectViewClass]) { |
| | | effectView = reinterpret_cast<NSVisualEffectView *>(subview); |
| | | } |
| | | } |
| | | if (effectView == nil) { |
| | | return false; |
| | | } |
| | | |
| | | static const auto originalMaterial = effectView.material; |
| | | static const auto originalBlendingMode = effectView.blendingMode; |
| | | static const auto originalState = effectView.state; |
| | | |
| | | if (mode == BlurMode::None) { |
| | | effectView.material = originalMaterial; |
| | | effectView.blendingMode = originalBlendingMode; |
| | | effectView.state = originalState; |
| | | effectView.appearance = nil; |
| | | } else { |
| | | effectView.material = NSVisualEffectMaterialUnderWindowBackground; |
| | | effectView.blendingMode = NSVisualEffectBlendingModeBehindWindow; |
| | | effectView.state = NSVisualEffectStateFollowsWindowActiveState; |
| | | |
| | | if (mode == BlurMode::Dark) { |
| | | effectView.appearance = |
| | | [NSAppearance appearanceNamed:@"NSAppearanceNameVibrantDark"]; |
| | | } else { |
| | | effectView.appearance = |
| | | [NSAppearance appearanceNamed:@"NSAppearanceNameVibrantLight"]; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | // System title bar |
| | | void setSystemTitleBarVisible(const bool visible) { |
| | | NSView *nsview = [nswindow contentView]; |
| | | if (!nsview) { |
| | |
| | | nswindow.titlebarAppearsTransparent = (visible ? NO : YES); |
| | | nswindow.titleVisibility = (visible ? NSWindowTitleVisible : NSWindowTitleHidden); |
| | | nswindow.hasShadow = YES; |
| | | nswindow.showsToolbarButton = NO; |
| | | // nswindow.showsToolbarButton = NO; |
| | | nswindow.movableByWindowBackground = NO; |
| | | nswindow.movable = NO; // This line causes the window in the wrong position when |
| | | // become fullscreen. |
| | | // For some unknown reason, we don't need the following hack in Qt versions below or |
| | | // equal to 6.2.4. |
| | | #if (QT_VERSION > QT_VERSION_CHECK(6, 2, 4)) |
| | | [nswindow standardWindowButton:NSWindowCloseButton].hidden = (visible ? NO : YES); |
| | | [nswindow standardWindowButton:NSWindowMiniaturizeButton].hidden = (visible ? NO : YES); |
| | | [nswindow standardWindowButton:NSWindowZoomButton].hidden = (visible ? NO : YES); |
| | | #endif |
| | | [nswindow standardWindowButton:NSWindowCloseButton].hidden = NO; |
| | | [nswindow standardWindowButton:NSWindowMiniaturizeButton].hidden = NO; |
| | | [nswindow standardWindowButton:NSWindowZoomButton].hidden = NO; |
| | | } |
| | | |
| | | static void replaceImplementations() { |
| | |
| | | [windowObserver release]; |
| | | windowObserver = nil; |
| | | } |
| | | |
| | | static inline const Class windowClass = [NSWindow class]; |
| | | |
| | | protected: |
| | | static BOOL canBecomeKeyWindow(id obj, SEL sel) { |
| | |
| | | #endif |
| | | } |
| | | |
| | | static inline const Class windowClass = [NSWindow class]; |
| | | |
| | | private: |
| | | Q_DISABLE_COPY(NSWindowProxy) |
| | | |
| | | NSWindow *nswindow = nil; |
| | | |
| | | bool systemButtonVisible = true; |
| | | QRect systemButtonRect; |
| | | ScreenRectCallback screenRectCallback; |
| | | |
| | | static inline QWK_NSWindowObserver *windowObserver = nil; |
| | | |
| | |
| | | |
| | | static inline void releaseWindowProxy(const WId windowId) { |
| | | if (auto proxy = g_proxyList->take(windowId)) { |
| | | proxy->setSystemTitleBarVisible(true); |
| | | // TODO: Determine if the window is valid |
| | | |
| | | // The window has been destroyed |
| | | // proxy->setSystemTitleBarVisible(true); |
| | | delete proxy; |
| | | } else { |
| | | return; |
| | |
| | | } |
| | | |
| | | case QEvent::MouseButtonDblClick: { |
| | | if (me->button() == Qt::LeftButton && inTitleBar && |
| | | !delegate->isHostSizeFixed(host)) { |
| | | if (me->button() == Qt::LeftButton && inTitleBar && !m_context->isHostSizeFixed()) { |
| | | Qt::WindowFlags windowFlags = delegate->getWindowFlags(host); |
| | | Qt::WindowStates windowState = delegate->getWindowState(host); |
| | | if (!(windowState & Qt::WindowFullScreen)) { |
| | | if ((windowFlags & Qt::WindowMaximizeButtonHint) && |
| | | !(windowState & Qt::WindowFullScreen)) { |
| | | if (windowState & Qt::WindowMaximized) { |
| | | delegate->setWindowState(host, windowState & ~Qt::WindowMaximized); |
| | | } else { |
| | |
| | | } |
| | | |
| | | CocoaWindowContext::~CocoaWindowContext() { |
| | | releaseWindowProxy(windowId); |
| | | releaseWindowProxy(m_windowId); |
| | | } |
| | | |
| | | QString CocoaWindowContext::key() const { |
| | |
| | | void CocoaWindowContext::virtual_hook(int id, void *data) { |
| | | switch (id) { |
| | | case SystemButtonAreaChangedHook: { |
| | | ensureWindowProxy(windowId)->setSystemButtonRect(m_systemButtonArea); |
| | | ensureWindowProxy(m_windowId)->setScreenRectCallback(m_systemButtonAreaCallback); |
| | | return; |
| | | } |
| | | |
| | |
| | | AbstractWindowContext::virtual_hook(id, data); |
| | | } |
| | | |
| | | void CocoaWindowContext::winIdChanged() { |
| | | QVariant CocoaWindowContext::windowAttribute(const QString &key) const { |
| | | if (key == QStringLiteral("title-bar-height")) { |
| | | if (!m_windowId) |
| | | return {}; |
| | | return ensureWindowProxy(m_windowId)->titleBarHeight(); |
| | | } |
| | | return AbstractWindowContext::windowAttribute(key); |
| | | } |
| | | |
| | | void CocoaWindowContext::winIdChanged(WId winId, WId oldWinId) { |
| | | // If the original window id is valid, remove all resources related |
| | | if (windowId) { |
| | | releaseWindowProxy(windowId); |
| | | windowId = 0; |
| | | if (oldWinId) { |
| | | releaseWindowProxy(oldWinId); |
| | | } |
| | | |
| | | if (!m_windowHandle) { |
| | | if (!winId) { |
| | | return; |
| | | } |
| | | |
| | | // Allocate new resources |
| | | windowId = m_windowHandle->winId(); |
| | | ensureWindowProxy(windowId)->setSystemTitleBarVisible(false); |
| | | ensureWindowProxy(winId)->setSystemTitleBarVisible(false); |
| | | } |
| | | |
| | | bool CocoaWindowContext::windowAttributeChanged(const QString &key, const QVariant &attribute, |
| | | const QVariant &oldAttribute) { |
| | | Q_UNUSED(oldAttribute) |
| | | |
| | | Q_ASSERT(m_windowId); |
| | | |
| | | if (key == QStringLiteral("no-system-buttons")) { |
| | | if (attribute.type() != QVariant::Bool) |
| | | return false; |
| | | ensureWindowProxy(windowId)->setSystemButtonVisible(!attribute.toBool()); |
| | | ensureWindowProxy(m_windowId)->setSystemButtonVisible(!attribute.toBool()); |
| | | return true; |
| | | } |
| | | |
| | | if (key == QStringLiteral("blur-effect")) { |
| | | auto mode = NSWindowProxy::BlurMode::None; |
| | | if (attribute.type() == QVariant::Bool) { |
| | | if (attribute.toBool()) { |
| | | NSString *osxMode = |
| | | [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; |
| | | mode = [osxMode isEqualToString:@"Dark"] ? NSWindowProxy::BlurMode::Dark |
| | | : NSWindowProxy::BlurMode::Light; |
| | | } |
| | | } else if (attribute.type() == QVariant::String) { |
| | | auto value = attribute.toString(); |
| | | if (value == QStringLiteral("dark")) { |
| | | mode = NSWindowProxy::BlurMode::Dark; |
| | | } else if (value == QStringLiteral("light")) { |
| | | mode = NSWindowProxy::BlurMode::Light; |
| | | } else if (value == QStringLiteral("none")) { |
| | | // ... |
| | | } else { |
| | | return false; |
| | | } |
| | | } else { |
| | | return false; |
| | | } |
| | | return ensureWindowProxy(m_windowId)->setBlurEffect(mode); |
| | | } |
| | | return false; |
| | | } |
| | | |