From 3d7576e06255c92de54465f7eadc631292d350a8 Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Sun, 24 Apr 2022 21:03:46 +0800 Subject: [PATCH] macOS: remove the system buttons, fix implementation Signed-off-by: Yuhang Zhao <2546789017@qq.com> --- README.md | 13 ++- src/core/CMakeLists.txt | 7 -- src/core/framelesshelper_qt.cpp | 2 - src/core/utils_mac.mm | 171 ++++++++++++++++++++++++++++++-- 4 files changed, 175 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9a95f9b1..5e6960d4 100644 --- a/README.md +++ b/README.md @@ -70,16 +70,23 @@ Please refer to the demo applications to see more detailed usages: [examples](./ ### Windows -- If DWM composition is disabled in some very rare cases (only possible on Windows 7), the top-left corner and top-right corner will appear in round shape. +- If DWM composition is disabled in some very rare cases (only possible on Windows 7), the top-left corner and top-right corner will appear in round shape. The round corners can be restored to square again if you re-enable DWM composition. +- There's an OpenGL driver bug which will cause some frameless windows have a strange black bar right on top of your homemade title bar, and it also makes the controls in your windows shifted to the bottom-right corner for some pixels. It's a bug of your graphics card driver, specifically, your OpenGL driver, not FramelessHelper. There are some solutions provided by our users but some of them may not work in all conditions, you can pick one from them: + - Upgrade your graphics card driver to the latest version. + - Change your system theme to "Basic". + - If you have multiple graphics cards, try to use another one instead. + - Force your application use the ANGLE backend instead of the Desktop OpenGL. + - Force your application use pure software rendering instead of rendering through OpenGL. + - Or just don't use OpenGL at all, try to use Direct3D/Vulkan/Metal instead. ### Linux - FramelessHelper will force your application to use the _XCB_ platform plugin when running on Wayland. -- Currently lacks runtime theme switching support +- Currently lacks runtime theme switching support. ### macOS -- The three system buttons on the title bar can't be made hidden for Qt Widgets applications, for some unknown reason. +- The frameless windows will appear in square corners instead of round corners. ## License diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b346820b..ed822b26 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -126,13 +126,6 @@ if(UNIX AND NOT APPLE) ) endif() -if(APPLE) - target_link_libraries(${SUB_PROJ_NAME} PRIVATE - "-framework Cocoa" - "-framework Carbon" - ) -endif() - target_link_libraries(${SUB_PROJ_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::CorePrivate Qt${QT_VERSION_MAJOR}::GuiPrivate diff --git a/src/core/framelesshelper_qt.cpp b/src/core/framelesshelper_qt.cpp index 021d2cb7..6c3e24dc 100644 --- a/src/core/framelesshelper_qt.cpp +++ b/src/core/framelesshelper_qt.cpp @@ -74,9 +74,7 @@ void FramelessHelperQt::addWindow(const UserSettings &settings, const SystemPara data.eventFilter = new FramelessHelperQt(window); g_qtHelper()->data.insert(windowId, data); g_qtHelper()->mutex.unlock(); -#ifndef Q_OS_MACOS params.setWindowFlags(params.getWindowFlags() | Qt::FramelessWindowHint); -#endif window->installEventFilter(data.eventFilter); #ifdef Q_OS_MACOS Utils::setSystemTitleBarVisible(windowId, false); diff --git a/src/core/utils_mac.mm b/src/core/utils_mac.mm index a49c16c9..45c6c203 100644 --- a/src/core/utils_mac.mm +++ b/src/core/utils_mac.mm @@ -25,9 +25,10 @@ #include "utils.h" #include #include +#include #include #include -#include +#include QT_BEGIN_NAMESPACE [[nodiscard]] Q_GUI_EXPORT QColor qt_mac_toQColor(const NSColor *color); @@ -45,17 +46,29 @@ explicit NSWindowProxy(NSWindow *window) { Q_ASSERT(window); - if (!window) { + Q_ASSERT(!instances.contains(window)); + if (!window || instances.contains(window)) { return; } nswindow = window; + instances.insert(nswindow, this); saveState(); + if (!windowClass) { + windowClass = [nswindow class]; + Q_ASSERT(windowClass); + replaceImplementations(); + } } ~NSWindowProxy() { + instances.remove(nswindow); + if (instances.count() <= 0) { + restoreImplementations(); + windowClass = nil; + } restoreState(); - nswindow = nullptr; + nswindow = nil; } void saveState() @@ -86,6 +99,62 @@ void restoreState() [nswindow standardWindowButton:NSWindowZoomButton].hidden = !oldZoomButtonVisible; } + void replaceImplementations() + { + Method method = class_getInstanceMethod(windowClass, @selector(setStyleMask:)); + Q_ASSERT(method); + oldSetStyleMask = reinterpret_cast(method_setImplementation(method, reinterpret_cast(setStyleMask))); + Q_ASSERT(oldSetStyleMask); + + method = class_getInstanceMethod(windowClass, @selector(setTitlebarAppearsTransparent:)); + Q_ASSERT(method); + oldSetTitlebarAppearsTransparent = reinterpret_cast(method_setImplementation(method, reinterpret_cast(setTitlebarAppearsTransparent))); + Q_ASSERT(oldSetTitlebarAppearsTransparent); + + method = class_getInstanceMethod(windowClass, @selector(canBecomeKeyWindow)); + Q_ASSERT(method); + oldCanBecomeKeyWindow = reinterpret_cast(method_setImplementation(method, reinterpret_cast(canBecomeKeyWindow))); + Q_ASSERT(oldCanBecomeKeyWindow); + + method = class_getInstanceMethod(windowClass, @selector(canBecomeMainWindow)); + Q_ASSERT(method); + oldCanBecomeMainWindow = reinterpret_cast(method_setImplementation(method, reinterpret_cast(canBecomeMainWindow))); + Q_ASSERT(oldCanBecomeMainWindow); + + method = class_getInstanceMethod(windowClass, @selector(sendEvent:)); + Q_ASSERT(method); + oldSendEvent = reinterpret_cast(method_setImplementation(method, reinterpret_cast(sendEvent))); + Q_ASSERT(oldSendEvent); + } + + void restoreImplementations() + { + Method method = class_getInstanceMethod(windowClass, @selector(setStyleMask:)); + Q_ASSERT(method); + method_setImplementation(method, reinterpret_cast(oldSetStyleMask)); + oldSetStyleMask = nil; + + method = class_getInstanceMethod(windowClass, @selector(setTitlebarAppearsTransparent:)); + Q_ASSERT(method); + method_setImplementation(method, reinterpret_cast(oldSetTitlebarAppearsTransparent)); + oldSetTitlebarAppearsTransparent = nil; + + method = class_getInstanceMethod(windowClass, @selector(canBecomeKeyWindow)); + Q_ASSERT(method); + method_setImplementation(method, reinterpret_cast(oldCanBecomeKeyWindow)); + oldCanBecomeKeyWindow = nil; + + method = class_getInstanceMethod(windowClass, @selector(canBecomeMainWindow)); + Q_ASSERT(method); + method_setImplementation(method, reinterpret_cast(oldCanBecomeMainWindow)); + oldCanBecomeMainWindow = nil; + + method = class_getInstanceMethod(windowClass, @selector(sendEvent:)); + Q_ASSERT(method); + method_setImplementation(method, reinterpret_cast(oldSendEvent)); + oldSendEvent = nil; + } + void setSystemTitleBarVisible(const bool visible) { NSView * const nsview = [nswindow contentView]; @@ -108,10 +177,81 @@ void setSystemTitleBarVisible(const bool visible) [nswindow standardWindowButton:NSWindowCloseButton].hidden = (visible ? NO : YES); [nswindow standardWindowButton:NSWindowMiniaturizeButton].hidden = (visible ? NO : YES); [nswindow standardWindowButton:NSWindowZoomButton].hidden = (visible ? NO : YES); + [nswindow makeKeyWindow]; + } + +private: + static BOOL canBecomeKeyWindow(id obj, SEL sel) + { + if (instances.contains(reinterpret_cast(obj))) { + return YES; + } + + if (oldCanBecomeKeyWindow) { + return oldCanBecomeKeyWindow(obj, sel); + } + + return YES; + } + + static BOOL canBecomeMainWindow(id obj, SEL sel) + { + if (instances.contains(reinterpret_cast(obj))) { + return YES; + } + + if (oldCanBecomeMainWindow) { + return oldCanBecomeMainWindow(obj, sel); + } + + return YES; + } + + static void setStyleMask(id obj, SEL sel, NSWindowStyleMask styleMask) + { + if (instances.contains(reinterpret_cast(obj))) { + styleMask |= NSWindowStyleMaskFullSizeContentView; + } + + if (oldSetStyleMask) { + oldSetStyleMask(obj, sel, styleMask); + } + } + + static void setTitlebarAppearsTransparent(id obj, SEL sel, BOOL transparent) + { + if (instances.contains(reinterpret_cast(obj))) { + transparent = YES; + } + + if (oldSetTitlebarAppearsTransparent) { + oldSetTitlebarAppearsTransparent(obj, sel, transparent); + } + } + + static void sendEvent(id obj, SEL sel, NSEvent *event) + { + if (oldSendEvent) { + oldSendEvent(obj, sel, event); + } + + const auto nswindow = reinterpret_cast(obj); + if (!instances.contains(nswindow)) { + return; + } + + NSWindowProxy * const proxy = instances[nswindow]; + if (event.type == NSEventTypeLeftMouseDown) { + proxy->lastMouseDownEvent = event; + QCoreApplication::processEvents(); + proxy->lastMouseDownEvent = nil; + } } private: - NSWindow *nswindow = nullptr; + NSWindow *nswindow = nil; + NSEvent *lastMouseDownEvent = nil; + NSWindowStyleMask oldStyleMask = 0; BOOL oldTitlebarAppearsTransparent = NO; BOOL oldHasShadow = NO; @@ -122,6 +262,25 @@ void setSystemTitleBarVisible(const bool visible) BOOL oldMiniaturizeButtonVisible = NO; BOOL oldZoomButtonVisible = NO; NSWindowTitleVisibility oldTitleVisibility = NSWindowTitleVisible; + + static inline QHash instances = {}; + + static inline Class windowClass = nil; + + using setStyleMaskPtr = void(*)(id, SEL, NSWindowStyleMask); + static inline setStyleMaskPtr oldSetStyleMask = nil; + + using setTitlebarAppearsTransparentPtr = void(*)(id, SEL, BOOL); + static inline setTitlebarAppearsTransparentPtr oldSetTitlebarAppearsTransparent = nil; + + using canBecomeKeyWindowPtr = BOOL(*)(id, SEL); + static inline canBecomeKeyWindowPtr oldCanBecomeKeyWindow = nil; + + using canBecomeMainWindowPtr = BOOL(*)(id, SEL); + static inline canBecomeMainWindowPtr oldCanBecomeMainWindow = nil; + + using sendEventPtr = void(*)(id, SEL, NSEvent *); + static inline sendEventPtr oldSendEvent = nil; }; using NSWindowProxyHash = QHash; @@ -131,12 +290,12 @@ void setSystemTitleBarVisible(const bool visible) { Q_ASSERT(windowId); if (!windowId) { - return nullptr; + return nil; } const auto nsview = reinterpret_cast(windowId); Q_ASSERT(nsview); if (!nsview) { - return nullptr; + return nil; } return [nsview window]; }