// This file is part of Desktop App Toolkit, // a set of libraries for developing nice desktop applications. // // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // #include "ui/platform/mac/ui_window_mac.h" #include "ui/platform/mac/ui_window_title_mac.h" #include "ui/widgets/rp_window.h" #include "base/qt/qt_common_adapters.h" #include "base/platform/base_platform_info.h" #include "styles/palette.h" #include #include #include #include #include #include @interface WindowObserver : NSObject { } - (id) initWithToggle:(Fn)toggleCustomTitleVisibility enforce:(Fn)enforceCorrectStyle; - (void) windowWillEnterFullScreen:(NSNotification *)aNotification; - (void) windowWillExitFullScreen:(NSNotification *)aNotification; - (void) windowDidExitFullScreen:(NSNotification *)aNotification; @end // @interface WindowObserver @implementation WindowObserver { Fn _toggleCustomTitleVisibility; Fn _enforceCorrectStyle; } - (id) initWithToggle:(Fn)toggleCustomTitleVisibility enforce:(Fn)enforceCorrectStyle { if (self = [super init]) { _toggleCustomTitleVisibility = toggleCustomTitleVisibility; _enforceCorrectStyle = enforceCorrectStyle; } return self; } - (void) windowWillEnterFullScreen:(NSNotification *)aNotification { _toggleCustomTitleVisibility(false); } - (void) windowWillExitFullScreen:(NSNotification *)aNotification { _enforceCorrectStyle(); _toggleCustomTitleVisibility(true); } - (void) windowDidExitFullScreen:(NSNotification *)aNotification { _enforceCorrectStyle(); } @end // @implementation MainWindowObserver namespace Ui { namespace Platform { namespace { class LayerCreationChecker : public QObject { public: LayerCreationChecker(NSView * __weak view, Fn callback) : _weakView(view) , _callback(std::move(callback)) { QCoreApplication::instance()->installEventFilter(this); } protected: bool eventFilter(QObject *object, QEvent *event) override { if (!_weakView || [_weakView layer] != nullptr) { _callback(); } return QObject::eventFilter(object, event); } private: NSView * __weak _weakView = nil; Fn _callback; }; class EventFilter : public QObject, public QAbstractNativeEventFilter { public: EventFilter( not_null parent, Fn checkStartDrag, Fn checkPerformDrag) : QObject(parent) , _checkStartDrag(std::move(checkStartDrag)) , _checkPerformDrag(std::move(checkPerformDrag)) { Expects(_checkPerformDrag != nullptr); Expects(_checkStartDrag != nullptr); } bool nativeEventFilter( const QByteArray &eventType, void *message, base::NativeEventResult *result) { if (NSEvent *e = static_cast(message)) { if ([e type] == NSEventTypeLeftMouseDown) { _dragStarted = _checkStartDrag(); } else if (([e type] == NSEventTypeLeftMouseDragged) && _dragStarted) { if (_checkPerformDrag([e window])) { return true; } _dragStarted = false; } } return false; } private: bool _dragStarted = false; Fn _checkStartDrag; Fn _checkPerformDrag; }; } // namespace class WindowHelper::Private final { public: explicit Private(not_null owner); ~Private(); [[nodiscard]] int customTitleHeight() const; [[nodiscard]] QRect controlsRect() const; [[nodiscard]] bool checkNativeMove(void *nswindow) const; void activateBeforeNativeMove(); void setStaysOnTop(bool enabled); void setNativeTitleVisibility(bool visible); void close(); private: void init(); void initOpenGL(); void resolveWeakPointers(); void revalidateWeakPointers() const; void initCustomTitle(); [[nodiscard]] Fn toggleCustomTitleCallback(); [[nodiscard]] Fn enforceStyleCallback(); void enforceStyle(); const not_null _owner; const WindowObserver *_observer = nullptr; NSWindow * __weak _nativeWindow = nil; NSView * __weak _nativeView = nil; bool _hadNativeValues = false; std::unique_ptr _layerCreationChecker; int _customTitleHeight = 0; }; WindowHelper::Private::Private(not_null owner) : _owner(owner) { init(); } WindowHelper::Private::~Private() { if (_observer) { [_observer release]; } } int WindowHelper::Private::customTitleHeight() const { return _customTitleHeight; } QRect WindowHelper::Private::controlsRect() const { revalidateWeakPointers(); const auto button = [&](NSWindowButton type) { auto view = [_nativeWindow standardWindowButton:type]; if (!view) { return QRect(); } auto result = [view frame]; for (auto parent = [view superview]; parent != nil; parent = [parent superview]) { const auto origin = [parent frame].origin; result.origin.x += origin.x; result.origin.y += origin.y; } return QRect(result.origin.x, result.origin.y, result.size.width, result.size.height); }; auto result = QRect(); const auto buttons = { NSWindowCloseButton, NSWindowMiniaturizeButton, NSWindowZoomButton, }; for (const auto type : buttons) { result = result.united(button(type)); } return QRect( result.x(), [_nativeWindow frame].size.height - result.y() - result.height(), result.width(), result.height()); } bool WindowHelper::Private::checkNativeMove(void *nswindow) const { revalidateWeakPointers(); if (_nativeWindow != nswindow || ([_nativeWindow styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { return false; } const auto cgReal = [NSEvent mouseLocation]; const auto real = QPointF(cgReal.x, cgReal.y); const auto cgFrame = [_nativeWindow frame]; const auto frame = QRectF(cgFrame.origin.x, cgFrame.origin.y, cgFrame.size.width, cgFrame.size.height); const auto border = QMarginsF{ 3., 3., 3., 3. }; return frame.marginsRemoved(border).contains(real); } void WindowHelper::Private::activateBeforeNativeMove() { revalidateWeakPointers(); [_nativeWindow makeKeyAndOrderFront:_nativeWindow]; } void WindowHelper::Private::setStaysOnTop(bool enabled) { _owner->BasicWindowHelper::setStaysOnTop(enabled); resolveWeakPointers(); initCustomTitle(); _owner->updateCustomTitleVisibility(true); } void WindowHelper::Private::setNativeTitleVisibility(bool visible) { revalidateWeakPointers(); if (!_nativeWindow) { return; } const auto value = visible ? NSWindowTitleVisible : NSWindowTitleHidden; [_nativeWindow setTitleVisibility:value]; } void WindowHelper::Private::close() { const auto weak = Ui::MakeWeak(_owner->window()); QCloseEvent e; qApp->sendEvent(_owner->window(), &e); if (!e.isAccepted() || !weak) { return; } revalidateWeakPointers(); if (_nativeWindow) { [_nativeWindow close]; } } Fn WindowHelper::Private::toggleCustomTitleCallback() { return crl::guard(_owner->window(), [=](bool visible) { _owner->_titleVisible = visible; _owner->updateCustomTitleVisibility(true); }); } Fn WindowHelper::Private::enforceStyleCallback() { return crl::guard(_owner->window(), [=] { enforceStyle(); }); } void WindowHelper::Private::enforceStyle() { revalidateWeakPointers(); if (_nativeWindow && _customTitleHeight > 0) { [_nativeWindow setStyleMask:[_nativeWindow styleMask] | NSWindowStyleMaskFullSizeContentView]; } } void WindowHelper::Private::initOpenGL() { #if QT_VERSION < QT_VERSION_CHECK(6, 4, 0) auto forceOpenGL = std::make_unique(_owner->window()); #endif // Qt < 6.4.0 } void WindowHelper::Private::resolveWeakPointers() { if (!_owner->window()->winId()) { _owner->window()->createWinId(); } _nativeView = reinterpret_cast(_owner->window()->winId()); _nativeWindow = _nativeView ? [_nativeView window] : nullptr; _hadNativeValues = true; Ensures(_nativeWindow != nullptr); } void WindowHelper::Private::revalidateWeakPointers() const { if (_nativeWindow || !_hadNativeValues) { return; } const_cast(this)->resolveWeakPointers(); } void WindowHelper::Private::initCustomTitle() { if (![_nativeWindow respondsToSelector:@selector(contentLayoutRect)] || ![_nativeWindow respondsToSelector:@selector(setTitlebarAppearsTransparent:)]) { return; } [_nativeWindow setTitlebarAppearsTransparent:YES]; if (_observer) { [_observer release]; } _observer = [[WindowObserver alloc] initWithToggle:toggleCustomTitleCallback() enforce:enforceStyleCallback()]; [[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowWillEnterFullScreen:) name:NSWindowWillEnterFullScreenNotification object:_nativeWindow]; [[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowWillExitFullScreen:) name:NSWindowWillExitFullScreenNotification object:_nativeWindow]; [[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:_nativeWindow]; // Qt has bug with layer-backed widgets containing QOpenGLWidgets. // See https://bugreports.qt.io/browse/QTBUG-64494 // Emulate custom title instead (code below). // // Tried to backport a fix, testing. [_nativeWindow setStyleMask:[_nativeWindow styleMask] | NSWindowStyleMaskFullSizeContentView]; auto inner = [_nativeWindow contentLayoutRect]; auto full = [_nativeView frame]; _customTitleHeight = qMax(qRound(full.size.height - inner.size.height), 0); // Qt still has some bug with layer-backed widgets containing QOpenGLWidgets. // See https://github.com/telegramdesktop/tdesktop/issues/4150 // Tried to workaround it by catching the first moment we have CALayer created // and explicitly setting contentsScale to window->backingScaleFactor there. _layerCreationChecker = std::make_unique(_nativeView, [=] { if (_nativeView && _nativeWindow) { if (CALayer *layer = [_nativeView layer]) { [layer setContentsScale: [_nativeWindow backingScaleFactor]]; _layerCreationChecker = nullptr; } } else { _layerCreationChecker = nullptr; } }); } void WindowHelper::Private::init() { initOpenGL(); resolveWeakPointers(); initCustomTitle(); } WindowHelper::WindowHelper(not_null window) : BasicWindowHelper(window) , _private(std::make_unique(this)) , _title(Ui::CreateChild( window.get(), _private->customTitleHeight())) , _body(Ui::CreateChild(window.get())) { init(); _title->setControlsRect(_private->controlsRect()); } WindowHelper::~WindowHelper() { } not_null WindowHelper::body() { return _body; } QMargins WindowHelper::frameMargins() { const auto titleHeight = !_title->isHidden() ? _title->height() : 0; return QMargins{ 0, titleHeight, 0, 0 }; } void WindowHelper::setTitle(const QString &title) { _title->setText(title); window()->setWindowTitle(title); } void WindowHelper::setTitleStyle(const style::WindowTitle &st) { _title->setStyle(st); updateCustomTitleVisibility(); } void WindowHelper::updateCustomTitleVisibility(bool force) { const auto visible = !_title->shouldBeHidden() && _titleVisible; if (!force && _title->isHidden() != visible) { return; } _title->setVisible(visible); _private->setNativeTitleVisibility(!_titleVisible); } void WindowHelper::setMinimumSize(QSize size) { window()->setMinimumSize(size.width(), frameMargins().top() + size.height()); } void WindowHelper::setFixedSize(QSize size) { window()->setFixedSize(size.width(), frameMargins().top() + size.height()); } void WindowHelper::setStaysOnTop(bool enabled) { _private->setStaysOnTop(enabled); } void WindowHelper::setGeometry(QRect rect) { window()->setGeometry(rect.marginsAdded(frameMargins())); } void WindowHelper::setupBodyTitleAreaEvents() { const auto controls = _private->controlsRect(); qApp->installNativeEventFilter(new EventFilter(window(), [=] { const auto point = body()->mapFromGlobal(QCursor::pos()); return (bodyTitleAreaHit(point) & WindowTitleHitTestFlag::Move); }, [=](void *nswindow) { const auto point = body()->mapFromGlobal(QCursor::pos()); if (_private->checkNativeMove(nswindow) && !controls.contains(point) && (bodyTitleAreaHit(point) & WindowTitleHitTestFlag::Move)) { _private->activateBeforeNativeMove(); window()->windowHandle()->startSystemMove(); return true; } return false; })); } void WindowHelper::close() { _private->close(); } const style::TextStyle &WindowHelper::titleTextStyle() const { return _title->textStyle(); } void WindowHelper::init() { updateCustomTitleVisibility(true); style::PaletteChanged( ) | rpl::start_with_next([=] { Ui::ForceFullRepaint(window()); }, window()->lifetime()); rpl::combine( window()->sizeValue(), _title->heightValue(), _title->shownValue() ) | rpl::start_with_next([=](QSize size, int titleHeight, bool shown) { if (!shown) { titleHeight = 0; } _body->setGeometry( 0, titleHeight, size.width(), size.height() - titleHeight); }, _body->lifetime()); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) setBodyTitleArea([](QPoint widgetPoint) { using Flag = Ui::WindowTitleHitTestFlag; return (widgetPoint.y() < 0) ? (Flag::Move | Flag::Maximize) : Flag::None; }); #endif // Qt >= 6.0.0 } std::unique_ptr CreateSpecialWindowHelper( not_null window) { return std::make_unique(window); } bool NativeWindowFrameSupported() { return false; } } // namespace Platform } // namespace Ui