diff --git a/.gitignore b/.gitignore index 556f8ab..5fccbfe 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,5 @@ Thumbs.db .qmake.conf *.res -.vscode/ \ No newline at end of file +.vscode/ +*/.DS_Store \ No newline at end of file diff --git a/examples/mainwindow/TitleBar.ui b/examples/mainwindow/TitleBar.ui index 29686ec..25a5b01 100644 --- a/examples/mainwindow/TitleBar.ui +++ b/examples/mainwindow/TitleBar.ui @@ -126,7 +126,7 @@ - Segoe UI + Arial 9 @@ -283,6 +283,8 @@ + + diff --git a/examples/mainwindow/mainwindow.cpp b/examples/mainwindow/mainwindow.cpp index ea4461f..49d2c0c 100644 --- a/examples/mainwindow/mainwindow.cpp +++ b/examples/mainwindow/mainwindow.cpp @@ -106,7 +106,7 @@ void MainWindow::showEvent(QShowEvent *event) titleBarWidget->minimizeButton->hide(); titleBarWidget->maximizeButton->hide(); titleBarWidget->closeButton->hide(); - Utilities::showMacWindowButton(windowHandle()); + Utilities::setStandardWindowButtonsVisibility(windowHandle(), true); #endif // Q_OS_MAC inited = true; } @@ -172,4 +172,4 @@ void MainWindow::paintEvent(QPaintEvent *event) painter.restore(); } } -#endif // Q_OS_MAC \ No newline at end of file +#endif // Q_OS_MAC diff --git a/examples/minimal/flwindow.cpp b/examples/minimal/flwindow.cpp index a32beb3..7d1c997 100644 --- a/examples/minimal/flwindow.cpp +++ b/examples/minimal/flwindow.cpp @@ -36,7 +36,10 @@ void FLWindow::initFramelessWindow() m_minimizeButton->hide(); m_maximizeButton->hide(); m_closeButton->hide(); - Utilities::showMacWindowButton(windowHandle()); + Utilities::setStandardWindowButtonsVisibility(windowHandle(), true); + auto btnGroupSize = Utilities::standardWindowButtonsSize(windowHandle()); + Utilities::setStandardWindowButtonsPosition(windowHandle(), + QPoint(12, (m_titleBarWidget->height() - btnGroupSize.height())/2)); #endif } diff --git a/examples/minimal/flwindow.h b/examples/minimal/flwindow.h index 5d1bb19..2fbb5ed 100644 --- a/examples/minimal/flwindow.h +++ b/examples/minimal/flwindow.h @@ -28,4 +28,4 @@ private: QPushButton *m_minimizeButton = nullptr; QPushButton *m_maximizeButton = nullptr; QPushButton *m_closeButton = nullptr; -}; \ No newline at end of file +}; diff --git a/examples/widget/widget.cpp b/examples/widget/widget.cpp index 7710e17..819833c 100644 --- a/examples/widget/widget.cpp +++ b/examples/widget/widget.cpp @@ -113,7 +113,7 @@ void Widget::showEvent(QShowEvent *event) m_minimizeButton->hide(); m_maximizeButton->hide(); m_closeButton->hide(); - Utilities::showMacWindowButton(windowHandle()); + Utilities::setStandardWindowButtonsVisibility(windowHandle(), true); #endif // Q_OS_MAC } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a66547e..2ac9f2d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,7 +24,14 @@ if(WIN32) ) else() if(APPLE) - list(APPEND SOURCES core/utilities_macos.mm) + list(APPEND SOURCES + core/utilities_macos.mm + core/nswindow_proxy.h + core/nswindow_proxy.mm + core/window_buttons_proxy.h + core/window_buttons_proxy.mm + core/scoped_nsobject.h + ) else() find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED) list(APPEND SOURCES core/utilities_linux.cpp) diff --git a/src/core/framelesshelper.cpp b/src/core/framelesshelper.cpp index 60dd0ba..586da14 100644 --- a/src/core/framelesshelper.cpp +++ b/src/core/framelesshelper.cpp @@ -528,7 +528,6 @@ bool FramelessHelper::eventFilter(QObject *object, QEvent *event) resizeWindow(re->size()); break; } - case QEvent::NonClientAreaMouseMove: case QEvent::MouseMove: { diff --git a/src/core/nswindow_proxy.h b/src/core/nswindow_proxy.h new file mode 100644 index 0000000..e8c742d --- /dev/null +++ b/src/core/nswindow_proxy.h @@ -0,0 +1,65 @@ +#ifndef NSWINDOWPROXY_H +#define NSWINDOWPROXY_H + +#include +#include +#include +#include + +#include + +#include "framelesshelper.h" +#include "scoped_nsobject.h" +#include "window_buttons_proxy.h" + +@class NSWindowProxyDelegate; + +class NSWindowProxy +{ +private: + NSWindow* m_window; + scoped_nsobject m_buttonProxy; + scoped_nsobject m_windowDelegate; + bool m_windowButtonVisibility; + QPoint m_trafficLightPosition; + +public: + NSWindowProxy(NSWindow *window); + ~NSWindowProxy(); + + NSWindow* window() { return m_window; } + + QPoint trafficLightPosition() { return m_trafficLightPosition; } + void setTrafficLightPosition(const QPoint &pos); + + bool windowButtonVisibility() { return m_windowButtonVisibility; } + void setWindowButtonVisibility(bool visible); + + void redrawTrafficLights(); + + bool isFullscreen() const; + void setTitle(const QString& title); + + void notifyWindowEnterFullScreen(); + void notifyWindowLeaveFullScreen(); + void notifyWindowWillEnterFullScreen(); + void notifyWindowWillLeaveFullScreen(); + +}; + +@interface NSWindowProxyDelegate : NSObject { + @private + NSWindowProxy* m_windowProxy; + bool m_isZooming; + int m_level; + bool m_isResizable; + + // Only valid during a live resize. + // Used to keep track of whether a resize is happening horizontally or + // vertically, even if physically the user is resizing in both directions. + bool m_resizingHorizontally; +} +- (id)initWithWindowProxy:(NSWindowProxy*)proxy; +@end + +#endif // NSWINDOWPROXY_H diff --git a/src/core/nswindow_proxy.mm b/src/core/nswindow_proxy.mm new file mode 100644 index 0000000..398a68a --- /dev/null +++ b/src/core/nswindow_proxy.mm @@ -0,0 +1,291 @@ +#include "nswindow_proxy.h" + +#include + +static QList gFlsWindows; +static bool gNSWindowOverrode = false; + +typedef void (*setStyleMaskType)(id, SEL, NSWindowStyleMask); +static setStyleMaskType gOrigSetStyleMask = nullptr; +static void __setStyleMask(id obj, SEL sel, NSWindowStyleMask styleMask) +{ + if (gFlsWindows.contains(reinterpret_cast(obj))) + { + styleMask = styleMask | NSWindowStyleMaskFullSizeContentView; + } + + if (gOrigSetStyleMask != nullptr) + gOrigSetStyleMask(obj, sel, styleMask); +} + +typedef void (*setTitlebarAppearsTransparentType)(id, SEL, BOOL); +static setTitlebarAppearsTransparentType gOrigSetTitlebarAppearsTransparent = nullptr; +static void __setTitlebarAppearsTransparent(id obj, SEL sel, BOOL transparent) +{ + if (gFlsWindows.contains(reinterpret_cast(obj))) + transparent = true; + + if (gOrigSetTitlebarAppearsTransparent != nullptr) + gOrigSetTitlebarAppearsTransparent(obj, sel, transparent); +} + +typedef BOOL (*canBecomeKeyWindowType)(id, SEL); +static canBecomeKeyWindowType gOrigCanBecomeKeyWindow = nullptr; +static BOOL __canBecomeKeyWindow(id obj, SEL sel) +{ + if (gFlsWindows.contains(reinterpret_cast(obj))) + { + return true; + } + + if (gOrigCanBecomeKeyWindow != nullptr) + return gOrigCanBecomeKeyWindow(obj, sel); + + return true; +} + +typedef BOOL (*canBecomeMainWindowType)(id, SEL); +static canBecomeMainWindowType gOrigCanBecomeMainWindow = nullptr; +static BOOL __canBecomeMainWindow(id obj, SEL sel) +{ + if (gFlsWindows.contains(reinterpret_cast(obj))) + { + return true; + } + + if (gOrigCanBecomeMainWindow != nullptr) + return gOrigCanBecomeMainWindow(obj, sel); + return true; +} + +typedef void (*sendEventType)(id, SEL, NSEvent*); +static sendEventType gOrigSendEvent = nullptr; +static void __sendEvent(id obj, SEL sel, NSEvent* event) +{ + if (gOrigSendEvent != nullptr) + gOrigSendEvent(obj, sel, event); + + + if (!gFlsWindows.contains(reinterpret_cast(obj))) + return; + + if (event.type == NSEventTypeLeftMouseDown) + QGuiApplication::processEvents(); +} + +typedef BOOL (*isFlippedType)(id, SEL); +static isFlippedType gOrigIsFlipped = nullptr; +static BOOL __isFlipped(id obj, SEL sel) +{ + if (!gFlsWindows.contains(reinterpret_cast(obj))) + return true; + + if (gOrigIsFlipped != nullptr) + return gOrigIsFlipped(obj, sel); + + return false; +} + +/*! + Replace origin method \a origSEL of class \a cls with new one \a newIMP , + then return old method as function pointer. + */ +static void* replaceMethod(Class cls, SEL origSEL, IMP newIMP) +{ + Method origMethod = class_getInstanceMethod(cls, origSEL); + void *funcPtr = (void *)method_getImplementation(origMethod); + if (!class_addMethod(cls, origSEL, newIMP, method_getTypeEncoding(origMethod))) { + method_setImplementation(origMethod, newIMP); + } + + return funcPtr; +} + +static void restoreMethod(Class cls, SEL origSEL, IMP oldIMP) +{ + Method method = class_getInstanceMethod(cls, origSEL); + method_setImplementation(method, oldIMP); +} + +static void overrideNSWindowMethods(NSWindow* window) +{ + if (!gNSWindowOverrode) { + Class cls = [window class]; + + gOrigSetStyleMask = (setStyleMaskType) replaceMethod( + cls, @selector(setStyleMask:), (IMP) __setStyleMask); + gOrigSetTitlebarAppearsTransparent = (setTitlebarAppearsTransparentType) replaceMethod( + cls, @selector(setTitlebarAppearsTransparent:), (IMP) __setTitlebarAppearsTransparent); + gOrigCanBecomeKeyWindow = (canBecomeKeyWindowType) replaceMethod( + cls, @selector(canBecomeKeyWindow), (IMP) __canBecomeKeyWindow); + gOrigCanBecomeMainWindow = (canBecomeMainWindowType) replaceMethod( + cls, @selector(canBecomeMainWindow), (IMP) __canBecomeMainWindow); + gOrigSendEvent = (sendEventType) replaceMethod( + cls, @selector(sendEvent:), (IMP) __sendEvent); + //gOrigIsFlipped = (isFlippedType) replaceMethod( + // cls, @selector (isFlipped), (IMP) __isFlipped); + + gNSWindowOverrode = true; + } + + gFlsWindows.append(window); +} + +static void restoreNSWindowMethods(NSWindow* window) +{ + gFlsWindows.removeAll(window); + if (gFlsWindows.size() == 0) { + Class cls = [window class]; + + restoreMethod(cls, @selector(setStyleMask:), (IMP) gOrigSetStyleMask); + gOrigSetStyleMask = nullptr; + + restoreMethod(cls, @selector(setTitlebarAppearsTransparent:), (IMP) gOrigSetTitlebarAppearsTransparent); + gOrigSetTitlebarAppearsTransparent = nullptr; + + restoreMethod(cls, @selector(canBecomeKeyWindow), (IMP) gOrigCanBecomeKeyWindow); + gOrigCanBecomeKeyWindow = nullptr; + + restoreMethod(cls, @selector(canBecomeMainWindow), (IMP) gOrigCanBecomeMainWindow); + gOrigCanBecomeMainWindow = nullptr; + + restoreMethod(cls, @selector(sendEvent:), (IMP) gOrigSendEvent); + gOrigSendEvent = nullptr; + + //restoreMethod(cls, @selector(isFlipped), (IMP) gOrigIsFlipped); + //gOrigIsFlipped = nullptr; + + gNSWindowOverrode = false; + } + +} + +NSWindowProxy::NSWindowProxy(NSWindow *window) + : m_windowButtonVisibility(false) + , m_buttonProxy(nullptr) + , m_window(window) +{ + overrideNSWindowMethods(window); + m_buttonProxy.reset([[WindowButtonsProxy alloc] initWithWindow:window]); + m_windowDelegate.reset([[NSWindowProxyDelegate alloc] initWithWindowProxy:this]); + [m_window setDelegate:m_windowDelegate.get()]; +} + +NSWindowProxy::~NSWindowProxy() +{ + restoreNSWindowMethods(m_window); + [m_buttonProxy release]; +} + +void NSWindowProxy::setTrafficLightPosition(const QPoint &pos) { + m_trafficLightPosition = pos; + if (m_buttonProxy) { + [m_buttonProxy setMargin:m_trafficLightPosition]; + } +} + +void NSWindowProxy::setWindowButtonVisibility(bool visible) { + m_windowButtonVisibility = visible; + // The visibility of window buttons are managed by |buttons_proxy_| if the + // style is customButtonsOnHover. + if (false /*title_bar_style_ == TitleBarStyle::kCustomButtonsOnHover*/) + [m_buttonProxy setVisible:visible]; + else { + [[m_window standardWindowButton:NSWindowCloseButton] setHidden:!visible]; + [[m_window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!visible]; + [[m_window standardWindowButton:NSWindowZoomButton] setHidden:!visible]; + } +} + +bool NSWindowProxy::isFullscreen() const { + return [m_window styleMask] & NSWindowStyleMaskFullScreen; +} + +void NSWindowProxy::redrawTrafficLights() { + if (m_buttonProxy && !isFullscreen()) + [m_buttonProxy redraw]; +} + +void NSWindowProxy::setTitle(const QString& title) { + [m_window setTitle:title.toNSString()]; + if (m_buttonProxy) + [m_buttonProxy redraw]; +} + +void NSWindowProxy::notifyWindowEnterFullScreen() { + // Restore the window title under fullscreen mode. + if (m_buttonProxy) { + [m_window setTitleVisibility:NSWindowTitleVisible]; + } +} + +void NSWindowProxy::notifyWindowLeaveFullScreen() { + // Restore window buttons. + if (m_buttonProxy && m_windowButtonVisibility) { + [m_buttonProxy redraw]; + [m_buttonProxy setVisible:YES]; + } +} + +void NSWindowProxy::notifyWindowWillEnterFullScreen() { + +} + +void NSWindowProxy::notifyWindowWillLeaveFullScreen() { + if (m_buttonProxy) { + // Hide window title when leaving fullscreen. + [m_window setTitleVisibility:NSWindowTitleHidden]; + // Hide the container otherwise traffic light buttons jump. + [m_buttonProxy setVisible:NO]; + } +} + +@implementation NSWindowProxyDelegate +- (id)initWithWindowProxy:(NSWindowProxy*)proxy { + m_windowProxy = proxy; + return self; +} + +- (void)windowDidBecomeMain:(NSNotification*)notification { + m_windowProxy->redrawTrafficLights(); +} + +- (void)windowDidResignMain:(NSNotification*)notification { + m_windowProxy->redrawTrafficLights(); +} + +- (void)windowDidBecomeKey:(NSNotification*)notification { + m_windowProxy->redrawTrafficLights(); +} + +- (void)windowDidResignKey:(NSNotification*)notification { + // If our app is still active and we're still the key window, ignore this + // message, since it just means that a menu extra (on the "system status bar") + // was activated; we'll get another |-windowDidResignKey| if we ever really + // lose key window status. + if ([NSApp isActive] && ([NSApp keyWindow] == [notification object])) + return; + + m_windowProxy->redrawTrafficLights(); +} + +- (void)windowDidResize:(NSNotification*)notification { + m_windowProxy->redrawTrafficLights(); +} + +- (void)windowWillEnterFullScreen:(NSNotification*)notification { + m_windowProxy->notifyWindowWillEnterFullScreen(); +} + +- (void)windowDidEnterFullScreen:(NSNotification*)notification { + m_windowProxy->notifyWindowEnterFullScreen(); +} + +- (void)windowWillExitFullScreen:(NSNotification*)notification { + m_windowProxy->notifyWindowWillLeaveFullScreen(); +} + +- (void)windowDidExitFullScreen:(NSNotification*)notification { + m_windowProxy->notifyWindowLeaveFullScreen(); +} +@end diff --git a/src/core/scoped_nsobject.h b/src/core/scoped_nsobject.h new file mode 100644 index 0000000..de506a5 --- /dev/null +++ b/src/core/scoped_nsobject.h @@ -0,0 +1,203 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SCOPED_NSOBJECT_H +#define SCOPED_NSOBJECT_H + +// Include NSObject.h directly because Foundation.h pulls in many dependencies. +// (Approx 100k lines of code versus 1.5k for NSObject.h). scoped_nsobject gets +// singled out because it is most typically included from other header files. +#import + +#if !defined(__GNUC__) && !defined(__clang__) && !defined(_MSC_VER) +#error Unsupported compiler. +#endif + +// Annotate a variable indicating it's ok if the variable is not used. +// (Typically used to silence a compiler warning when the assignment +// is important for some other reason.) +// Use like: +// int x = ...; +// FML_ALLOW_UNUSED_LOCAL(x); +#define FML_ALLOW_UNUSED_LOCAL(x) false ? (void)x : (void)0 + +// Annotate a typedef or function indicating it's ok if it's not used. +// Use like: +// typedef Foo Bar ALLOW_UNUSED_TYPE; +#if defined(__GNUC__) || defined(__clang__) +#define FML_ALLOW_UNUSED_TYPE __attribute__((unused)) +#else +#define FML_ALLOW_UNUSED_TYPE +#endif + +#ifndef FML_USED_ON_EMBEDDER + +#define FML_EMBEDDER_ONLY [[deprecated]] + +#else // FML_USED_ON_EMBEDDER + +#define FML_EMBEDDER_ONLY + +#endif // FML_USED_ON_EMBEDDER + +#define FML_DISALLOW_COPY(TypeName) TypeName(const TypeName&) = delete + +#define FML_DISALLOW_ASSIGN(TypeName) \ + TypeName& operator=(const TypeName&) = delete + +#define FML_DISALLOW_MOVE(TypeName) \ + TypeName(TypeName&&) = delete; \ + TypeName& operator=(TypeName&&) = delete + +#define FML_DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&) = delete; \ + TypeName& operator=(const TypeName&) = delete + +#define FML_DISALLOW_COPY_ASSIGN_AND_MOVE(TypeName) \ + TypeName(const TypeName&) = delete; \ + TypeName(TypeName&&) = delete; \ + TypeName& operator=(const TypeName&) = delete; \ + TypeName& operator=(TypeName&&) = delete + +#define FML_DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \ + TypeName() = delete; \ + FML_DISALLOW_COPY_ASSIGN_AND_MOVE(TypeName) + +@class NSAutoreleasePool; + +// scoped_nsobject<> is patterned after scoped_ptr<>, but maintains ownership +// of an NSObject subclass object. Style deviations here are solely for +// compatibility with scoped_ptr<>'s interface, with which everyone is already +// familiar. +// +// scoped_nsobject<> takes ownership of an object (in the constructor or in +// reset()) by taking over the caller's existing ownership claim. The caller +// must own the object it gives to scoped_nsobject<>, and relinquishes an +// ownership claim to that object. scoped_nsobject<> does not call -retain, +// callers have to call this manually if appropriate. +// +// scoped_nsprotocol<> has the same behavior as scoped_nsobject, but can be used +// with protocols. +// +// scoped_nsobject<> is not to be used for NSAutoreleasePools. For +// NSAutoreleasePools use ScopedNSAutoreleasePool from +// scoped_nsautorelease_pool.h instead. +// We check for bad uses of scoped_nsobject and NSAutoreleasePool at compile +// time with a template specialization (see below). + +template +class scoped_nsprotocol { + public: + explicit scoped_nsprotocol(NST object = nil) : object_(object) {} + + scoped_nsprotocol(const scoped_nsprotocol& that) : object_([that.object_ retain]) {} + + template + scoped_nsprotocol(const scoped_nsprotocol& that) : object_([that.get() retain]) {} + + ~scoped_nsprotocol() { [object_ release]; } + + scoped_nsprotocol& operator=(const scoped_nsprotocol& that) { + reset([that.get() retain]); + return *this; + } + + void reset(NST object = nil) { + // We intentionally do not check that object != object_ as the caller must + // either already have an ownership claim over whatever it passes to this + // method, or call it with the |RETAIN| policy which will have ensured that + // the object is retained once more when reaching this point. + [object_ release]; + object_ = object; + } + + bool operator==(NST that) const { return object_ == that; } + bool operator!=(NST that) const { return object_ != that; } + + operator NST() const { return object_; } + + NST get() const { return object_; } + + void swap(scoped_nsprotocol& that) { + NST temp = that.object_; + that.object_ = object_; + object_ = temp; + } + + // Shift reference to the autorelease pool to be released later. + NST autorelease() { return [release() autorelease]; } + + private: + NST object_; + + // scoped_nsprotocol<>::release() is like scoped_ptr<>::release. It is NOT a + // wrapper for [object_ release]. To force a scoped_nsprotocol<> to call + // [object_ release], use scoped_nsprotocol<>::reset(). + [[nodiscard]] NST release() { + NST temp = object_; + object_ = nil; + return temp; + } +}; + +// Free functions +template +void swap(scoped_nsprotocol& p1, scoped_nsprotocol& p2) { + p1.swap(p2); +} + +template +bool operator==(C p1, const scoped_nsprotocol& p2) { + return p1 == p2.get(); +} + +template +bool operator!=(C p1, const scoped_nsprotocol& p2) { + return p1 != p2.get(); +} + +template +class scoped_nsobject : public scoped_nsprotocol { + public: + explicit scoped_nsobject(NST* object = nil) : scoped_nsprotocol(object) {} + + scoped_nsobject(const scoped_nsobject& that) : scoped_nsprotocol(that) {} + + template + scoped_nsobject(const scoped_nsobject& that) : scoped_nsprotocol(that) {} + + scoped_nsobject& operator=(const scoped_nsobject& that) { + scoped_nsprotocol::operator=(that); + return *this; + } +}; + +// Specialization to make scoped_nsobject work. +template <> +class scoped_nsobject : public scoped_nsprotocol { + public: + explicit scoped_nsobject(id object = nil) : scoped_nsprotocol(object) {} + + scoped_nsobject(const scoped_nsobject& that) : scoped_nsprotocol(that) {} + + template + scoped_nsobject(const scoped_nsobject& that) : scoped_nsprotocol(that) {} + + scoped_nsobject& operator=(const scoped_nsobject& that) { + scoped_nsprotocol::operator=(that); + return *this; + } +}; + +// Do not use scoped_nsobject for NSAutoreleasePools, use +// ScopedNSAutoreleasePool instead. This is a compile time check. See details +// at top of header. +template <> +class scoped_nsobject { + private: + explicit scoped_nsobject(NSAutoreleasePool* object = nil); + FML_DISALLOW_COPY_AND_ASSIGN(scoped_nsobject); +}; + +#endif // SCOPED_NSOBJECT_H diff --git a/src/core/utilities.h b/src/core/utilities.h index 05b264e..7c3cf30 100644 --- a/src/core/utilities.h +++ b/src/core/utilities.h @@ -26,6 +26,7 @@ #include "framelesshelper_global.h" #include +#include FRAMELESSHELPER_BEGIN_NAMESPACE @@ -75,7 +76,9 @@ FRAMELESSHELPER_API bool setMacWindowFrameless(QWindow* w); FRAMELESSHELPER_API bool unsetMacWindowFrameless(QWindow* w); FRAMELESSHELPER_API bool startMacDrag(QWindow* w, const QPoint& pos); FRAMELESSHELPER_API Qt::MouseButtons getMacMouseButtons(); -FRAMELESSHELPER_API bool showMacWindowButton(QWindow *w); +FRAMELESSHELPER_API bool setStandardWindowButtonsVisibility(QWindow *w, bool visible); +FRAMELESSHELPER_API bool setStandardWindowButtonsPosition(QWindow *w, const QPoint &pos); +FRAMELESSHELPER_API QSize standardWindowButtonsSize(QWindow *w); #endif // Q_OS_MAC } diff --git a/src/core/utilities_macos.mm b/src/core/utilities_macos.mm index 6141aad..c11b1af 100644 --- a/src/core/utilities_macos.mm +++ b/src/core/utilities_macos.mm @@ -24,12 +24,11 @@ #include "utilities.h" -#include -#include -#include - #include #include +#include + +#include "nswindow_proxy.h" FRAMELESSHELPER_BEGIN_NAMESPACE @@ -120,154 +119,22 @@ bool showSystemMenu(const WId winId, const QPointF &pos) return false; } -static QList gFlsWindows; -static bool gNSWindowOverrode = false; - -typedef void (*setStyleMaskType)(id, SEL, NSWindowStyleMask); -static setStyleMaskType gOrigSetStyleMask = nullptr; -static void __setStyleMask(id obj, SEL sel, NSWindowStyleMask styleMask) -{ - if (gFlsWindows.contains(reinterpret_cast(obj))) - { - styleMask = styleMask | NSWindowStyleMaskFullSizeContentView; - } - - if (gOrigSetStyleMask != nullptr) - gOrigSetStyleMask(obj, sel, styleMask); -} - -typedef void (*setTitlebarAppearsTransparentType)(id, SEL, BOOL); -static setTitlebarAppearsTransparentType gOrigSetTitlebarAppearsTransparent = nullptr; -static void __setTitlebarAppearsTransparent(id obj, SEL sel, BOOL transparent) -{ - if (gFlsWindows.contains(reinterpret_cast(obj))) - transparent = true; - - if (gOrigSetTitlebarAppearsTransparent != nullptr) - gOrigSetTitlebarAppearsTransparent(obj, sel, transparent); -} - -typedef BOOL (*canBecomeKeyWindowType)(id, SEL); -static canBecomeKeyWindowType gOrigCanBecomeKeyWindow = nullptr; -static BOOL __canBecomeKeyWindow(id obj, SEL sel) -{ - if (gFlsWindows.contains(reinterpret_cast(obj))) - { - return true; - } - - if (gOrigCanBecomeKeyWindow != nullptr) - return gOrigCanBecomeKeyWindow(obj, sel); - - return true; -} - -typedef BOOL (*canBecomeMainWindowType)(id, SEL); -static canBecomeMainWindowType gOrigCanBecomeMainWindow = nullptr; -static BOOL __canBecomeMainWindow(id obj, SEL sel) -{ - if (gFlsWindows.contains(reinterpret_cast(obj))) - { - return true; - } - - if (gOrigCanBecomeMainWindow != nullptr) - return gOrigCanBecomeMainWindow(obj, sel); - return true; -} - -typedef void (*sendEventType)(id, SEL, NSEvent*); -static sendEventType gOrigSendEvent = nullptr; -static void __sendEvent(id obj, SEL sel, NSEvent* event) -{ - if (gOrigSendEvent != nullptr) - gOrigSendEvent(obj, sel, event); - - - if (!gFlsWindows.contains(reinterpret_cast(obj))) - return; - - if (event.type == NSEventTypeLeftMouseDown) - QGuiApplication::processEvents(); -} - -/*! - Replace origin method \a origSEL of class \a cls with new one \a newIMP , - then return old method as function pointer. - */ -static void* replaceMethod(Class cls, SEL origSEL, IMP newIMP) -{ - Method origMethod = class_getInstanceMethod(cls, origSEL); - void *funcPtr = (void *)method_getImplementation(origMethod); - if (!class_addMethod(cls, origSEL, newIMP, method_getTypeEncoding(origMethod))) { - method_setImplementation(origMethod, newIMP); - } - - return funcPtr; -} - -static void restoreMethod(Class cls, SEL origSEL, IMP oldIMP) -{ - Method method = class_getInstanceMethod(cls, origSEL); - method_setImplementation(method, oldIMP); -} - -static void overrideNSWindowMethods(NSWindow* window) -{ - if (!gNSWindowOverrode) { - Class cls = [window class]; - - gOrigSetStyleMask = (setStyleMaskType) replaceMethod( - cls, @selector(setStyleMask:), (IMP) __setStyleMask); - gOrigSetTitlebarAppearsTransparent = (setTitlebarAppearsTransparentType) replaceMethod( - cls, @selector(setTitlebarAppearsTransparent:), (IMP) __setTitlebarAppearsTransparent); - gOrigCanBecomeKeyWindow = (canBecomeKeyWindowType) replaceMethod( - cls, @selector(canBecomeKeyWindow), (IMP) __canBecomeKeyWindow); - gOrigCanBecomeMainWindow = (canBecomeMainWindowType) replaceMethod( - cls, @selector(canBecomeMainWindow), (IMP) __canBecomeMainWindow); - gOrigSendEvent = (sendEventType) replaceMethod( - cls, @selector(sendEvent:), (IMP) __sendEvent); - - gNSWindowOverrode = true; - } - - gFlsWindows.append(window); -} - -static void restoreNSWindowMethods(NSWindow* window) -{ - gFlsWindows.removeAll(window); - if (gFlsWindows.size() == 0) { - Class cls = [window class]; - - restoreMethod(cls, @selector(setStyleMask:), (IMP) gOrigSetStyleMask); - gOrigSetStyleMask = nullptr; - - restoreMethod(cls, @selector(setTitlebarAppearsTransparent:), (IMP) gOrigSetTitlebarAppearsTransparent); - gOrigSetTitlebarAppearsTransparent = nullptr; - - restoreMethod(cls, @selector(canBecomeKeyWindow), (IMP) gOrigCanBecomeKeyWindow); - gOrigCanBecomeKeyWindow = nullptr; - - restoreMethod(cls, @selector(canBecomeMainWindow), (IMP) gOrigCanBecomeMainWindow); - gOrigCanBecomeMainWindow = nullptr; - - restoreMethod(cls, @selector(sendEvent:), (IMP) gOrigSendEvent); - gOrigSendEvent = nullptr; - - gNSWindowOverrode = false; - } - -} - -static QHash gQWindowToNSWindow; +static QHash gQWindowToNSWindow; static NSWindow* getNSWindow(QWindow* w) { NSView* view = reinterpret_cast(w->winId()); - if (view == nullptr) + if (view == nullptr) { + qWarning() << "Unable to get NSView."; return nullptr; - return [view window]; + } + NSWindow* nswindow = [view window]; + if (nswindow == nullptr) { + qWarning() << "Unable to get NSWindow."; + return nullptr; + } + + return nswindow; } bool setMacWindowHook(QWindow* w) @@ -276,8 +143,8 @@ bool setMacWindowHook(QWindow* w) if (nswindow == nullptr) return false; - gQWindowToNSWindow.insert(w, nswindow); - overrideNSWindowMethods(nswindow); + NSWindowProxy *obj = new NSWindowProxy(nswindow); + gQWindowToNSWindow.insert(w, obj); return true; } @@ -286,9 +153,10 @@ bool unsetMacWindowHook(QWindow* w) { if (!gQWindowToNSWindow.contains(w)) return false; - NSWindow* obj = gQWindowToNSWindow[w]; + + NSWindowProxy* obj = gQWindowToNSWindow[w]; gQWindowToNSWindow.remove(w); - restoreNSWindowMethods(obj); + delete obj; return true; } @@ -348,29 +216,9 @@ bool unsetMacWindowFrameless(QWindow* w) return true; } -bool showMacWindowButton(QWindow *w) -{ - NSView* view = reinterpret_cast(w->winId()); - if (view == nullptr) - return false; - NSWindow* nswindow = [view window]; - if (nswindow == nullptr) - return false; - - nswindow.showsToolbarButton = true; - [nswindow standardWindowButton:NSWindowCloseButton].hidden = false; - [nswindow standardWindowButton:NSWindowMiniaturizeButton].hidden = false; - [nswindow standardWindowButton:NSWindowZoomButton].hidden = false; - - return true; -} - bool startMacDrag(QWindow* w, const QPoint& pos) { - NSView* view = reinterpret_cast(w->winId()); - if (view == nullptr) - return false; - NSWindow* nswindow = [view window]; + NSWindow* nswindow = getNSWindow(w); if (nswindow == nullptr) return false; @@ -387,6 +235,35 @@ Qt::MouseButtons getMacMouseButtons() return static_cast((uint)(NSEvent.pressedMouseButtons & Qt::MouseButtonMask)); } +bool setStandardWindowButtonsVisibility(QWindow *w, bool visible) +{ + NSWindowProxy* obj = gQWindowToNSWindow[w]; + obj->setWindowButtonVisibility(visible); + return true; +} + +/*! The origin of \a pos is top-left of window. */ +bool setStandardWindowButtonsPosition(QWindow *w, const QPoint &pos) +{ + NSWindowProxy* obj = gQWindowToNSWindow[w]; + obj->setWindowButtonVisibility(true); + obj->setTrafficLightPosition(pos); + return true; +} + +QSize standardWindowButtonsSize(QWindow *w) +{ + NSWindow* nswindow = getNSWindow(w); + if (nswindow == nullptr) + return QSize(); + + NSButton* left = [nswindow standardWindowButton:NSWindowCloseButton]; + NSButton* right = [nswindow standardWindowButton:NSWindowZoomButton]; + float height = NSHeight(left.frame); + float width = NSMaxX(right.frame) - NSMinX(left.frame); + return QSize(width, height); +} + } // namespace Utilities FRAMELESSHELPER_END_NAMESPACE diff --git a/src/core/window_buttons_proxy.h b/src/core/window_buttons_proxy.h new file mode 100644 index 0000000..67aebfd --- /dev/null +++ b/src/core/window_buttons_proxy.h @@ -0,0 +1,59 @@ +// Copyright (c) 2021 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_UI_COCOA_WINDOW_BUTTONS_PROXY_H_ +#define SHELL_BROWSER_UI_COCOA_WINDOW_BUTTONS_PROXY_H_ + +#include +#import +#include + +#include "scoped_nsobject.h" + +@class WindowButtonsProxy; + +// A helper view that floats above the window buttons. +@interface ButtonsAreaHoverView : NSView { + @private + WindowButtonsProxy* proxy_; +} +- (id)initWithProxy:(WindowButtonsProxy*)proxy; +@end + +// Manipulating the window buttons. +@interface WindowButtonsProxy : NSObject { + @private + NSWindow* window_; + + // Current left-top margin of buttons. + QPoint margin_; + // The default left-top margin. + QPoint default_margin_; + + // Track mouse moves above window buttons. + BOOL show_on_hover_; + BOOL mouse_inside_; + scoped_nsobject tracking_area_; + scoped_nsobject hover_view_; +} + +- (id)initWithWindow:(NSWindow*)window; + +- (void)setVisible:(BOOL)visible; +- (BOOL)isVisible; + +// Only show window buttons when mouse moves above them. +- (void)setShowOnHover:(BOOL)yes; + +// Set left-top margin of the window buttons.. +- (void)setMargin:(const QPoint&)margin; + +// Return the bounds of all 3 buttons, with margin on all sides. +- (NSRect)getButtonsContainerBounds; + +- (void)redraw; +- (void)updateTrackingAreas; +@end + +#endif // SHELL_BROWSER_UI_COCOA_WINDOW_BUTTONS_PROXY_H_ diff --git a/src/core/window_buttons_proxy.mm b/src/core/window_buttons_proxy.mm new file mode 100644 index 0000000..ed517f4 --- /dev/null +++ b/src/core/window_buttons_proxy.mm @@ -0,0 +1,220 @@ +// Copyright (c) 2021 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "window_buttons_proxy.h" + +@implementation ButtonsAreaHoverView : NSView + +- (id)initWithProxy:(WindowButtonsProxy*)proxy { + if ((self = [super init])) { + proxy_ = proxy; + } + return self; +} + +// Ignore all mouse events. +- (NSView*)hitTest:(NSPoint)aPoint { + return nil; +} + +- (void)updateTrackingAreas { + [proxy_ updateTrackingAreas]; +} + +@end + +@implementation WindowButtonsProxy + +- (id)initWithWindow:(NSWindow*)window { + window_ = window; + show_on_hover_ = NO; + mouse_inside_ = NO; + + // Remember the default margin. + margin_ = default_margin_ = [self getCurrentMargin]; + + return self; +} + +- (void)dealloc { + if (hover_view_) + [hover_view_ removeFromSuperview]; + [super dealloc]; +} + +- (void)setVisible:(BOOL)visible { + NSView* titleBarContainer = [self titleBarContainer]; + if (!titleBarContainer) + return; + [titleBarContainer setHidden:!visible]; +} + +- (BOOL)isVisible { + NSView* titleBarContainer = [self titleBarContainer]; + if (!titleBarContainer) + return YES; + return ![titleBarContainer isHidden]; +} + +- (void)setShowOnHover:(BOOL)yes { + NSView* titleBarContainer = [self titleBarContainer]; + if (!titleBarContainer) + return; + show_on_hover_ = yes; + // Put a transparent view above the window buttons so we can track mouse + // events when mouse enter/leave the window buttons. + if (show_on_hover_) { + hover_view_.reset([[ButtonsAreaHoverView alloc] initWithProxy:self]); + [hover_view_ setFrame:[self getButtonsBounds]]; + [titleBarContainer addSubview:hover_view_.get()]; + } else { + [hover_view_ removeFromSuperview]; + hover_view_.reset(); + } + [self updateButtonsVisibility]; +} + +- (void)setMargin:(const QPoint&)margin { + if (!margin.isNull()) + margin_ = margin; + else + margin_ = default_margin_; + [self redraw]; +} + +- (NSRect)getButtonsContainerBounds { + return NSInsetRect([self getButtonsBounds], -margin_.x(), -margin_.y()); +} + +- (void)redraw { + NSView* titleBarContainer = [self titleBarContainer]; + if (!titleBarContainer) + return; + + NSView* left = [self leftButton]; + NSView* middle = [self middleButton]; + NSView* right = [self rightButton]; + + float button_width = NSWidth(left.frame); + float button_height = NSHeight(left.frame); + float padding = NSMinX(middle.frame) - NSMaxX(left.frame); + float start; + if (false /*base::i18n::IsRTL()*/) + start = + NSWidth(window_.frame) - 3 * button_width - 2 * padding - margin_.x(); + else + start = margin_.x(); + + NSRect cbounds = titleBarContainer.frame; + cbounds.size.height = button_height + 2 * margin_.y(); + cbounds.origin.y = NSHeight(window_.frame) - NSHeight(cbounds); + [titleBarContainer setFrame:cbounds]; + + [left setFrameOrigin:NSMakePoint(start, margin_.y())]; + start += button_width + padding; + [middle setFrameOrigin:NSMakePoint(start, margin_.y())]; + start += button_width + padding; + [right setFrameOrigin:NSMakePoint(start, margin_.y())]; + + if (hover_view_) + [hover_view_ setFrame:[self getButtonsBounds]]; +} + +- (void)updateTrackingAreas { + if (tracking_area_) + [hover_view_ removeTrackingArea:tracking_area_.get()]; + tracking_area_.reset([[NSTrackingArea alloc] + initWithRect:NSZeroRect + options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | + NSTrackingInVisibleRect + owner:self + userInfo:nil]); + [hover_view_ addTrackingArea:tracking_area_.get()]; +} + +- (void)mouseEntered:(NSEvent*)event { + mouse_inside_ = YES; + [self updateButtonsVisibility]; +} + +- (void)mouseExited:(NSEvent*)event { + mouse_inside_ = NO; + [self updateButtonsVisibility]; +} + +- (void)updateButtonsVisibility { + NSArray* buttons = @[ + [window_ standardWindowButton:NSWindowCloseButton], + [window_ standardWindowButton:NSWindowMiniaturizeButton], + [window_ standardWindowButton:NSWindowZoomButton], + ]; + // Show buttons when mouse hovers above them. + BOOL hidden = show_on_hover_ && !mouse_inside_; + // Always show buttons under fullscreen. + if ([window_ styleMask] & NSWindowStyleMaskFullScreen) + hidden = NO; + for (NSView* button in buttons) { + [button setHidden:hidden]; + [button setNeedsDisplay:YES]; + } +} + +// Return the bounds of all 3 buttons. +- (NSRect)getButtonsBounds { + NSView* left = [self leftButton]; + NSView* right = [self rightButton]; + return NSMakeRect(NSMinX(left.frame), NSMinY(left.frame), + NSMaxX(right.frame) - NSMinX(left.frame), + NSHeight(left.frame)); +} + +// Compute margin from position of current buttons. +- (QPoint)getCurrentMargin { + QPoint result; + NSView* titleBarContainer = [self titleBarContainer]; + if (!titleBarContainer) + return result; + + NSView* left = [self leftButton]; + NSView* right = [self rightButton]; + + result.setX((NSHeight(titleBarContainer.frame) - NSHeight(left.frame)) / 2); + + if (false /*base::i18n::IsRTL()*/) + result.setX(NSWidth(window_.frame) - NSMaxX(right.frame)); + else + result.setX(NSMinX(left.frame)); + return result; +} + +// Receive the titlebar container, which might be nil if the window does not +// have the NSWindowStyleMaskTitled style. +- (NSView*)titleBarContainer { + NSView* left = [self leftButton]; + if (!left.superview) + return nil; + return left.superview.superview; +} + +// Receive the window buttons, note that the buttons might be removed and +// re-added on the fly so we should not cache them. +- (NSButton*)leftButton { + if (false /*base::i18n::IsRTL()*/) + return [window_ standardWindowButton:NSWindowZoomButton]; + else + return [window_ standardWindowButton:NSWindowCloseButton]; +} + +- (NSButton*)middleButton { + return [window_ standardWindowButton:NSWindowMiniaturizeButton]; +} + +- (NSButton*)rightButton { + if (false /*base::i18n::IsRTL()*/) + return [window_ standardWindowButton:NSWindowCloseButton]; + else + return [window_ standardWindowButton:NSWindowZoomButton]; +} + +@end