1// This file is part of Desktop App Toolkit, 2// a set of libraries for developing nice desktop applications. 3// 4// For license and copyright information please follow this link: 5// https://github.com/desktop-app/legal/blob/master/LEGAL 6// 7#include "ui/platform/mac/ui_window_mac.h" 8 9#include "ui/platform/mac/ui_window_title_mac.h" 10#include "ui/widgets/rp_window.h" 11#include "base/qt_adapters.h" 12#include "base/platform/base_platform_info.h" 13#include "styles/palette.h" 14 15#include <QtCore/QCoreApplication> 16#include <QtCore/QAbstractNativeEventFilter> 17#include <QtGui/QWindow> 18#include <QtGui/QtEvents> 19#include <QOpenGLWidget> 20#include <Cocoa/Cocoa.h> 21 22@interface WindowObserver : NSObject { 23} 24 25- (id) initWithToggle:(Fn<void(bool)>)toggleCustomTitleVisibility enforce:(Fn<void()>)enforceCorrectStyle; 26- (void) windowWillEnterFullScreen:(NSNotification *)aNotification; 27- (void) windowWillExitFullScreen:(NSNotification *)aNotification; 28- (void) windowDidExitFullScreen:(NSNotification *)aNotification; 29 30@end // @interface WindowObserver 31 32@implementation WindowObserver { 33 Fn<void(bool)> _toggleCustomTitleVisibility; 34 Fn<void()> _enforceCorrectStyle; 35} 36 37- (id) initWithToggle:(Fn<void(bool)>)toggleCustomTitleVisibility enforce:(Fn<void()>)enforceCorrectStyle { 38 if (self = [super init]) { 39 _toggleCustomTitleVisibility = toggleCustomTitleVisibility; 40 _enforceCorrectStyle = enforceCorrectStyle; 41 } 42 return self; 43} 44 45- (void) windowWillEnterFullScreen:(NSNotification *)aNotification { 46 _toggleCustomTitleVisibility(false); 47} 48 49- (void) windowWillExitFullScreen:(NSNotification *)aNotification { 50 _enforceCorrectStyle(); 51 _toggleCustomTitleVisibility(true); 52} 53 54- (void) windowDidExitFullScreen:(NSNotification *)aNotification { 55 _enforceCorrectStyle(); 56} 57 58@end // @implementation MainWindowObserver 59 60namespace Ui { 61namespace Platform { 62namespace { 63 64class LayerCreationChecker : public QObject { 65public: 66 LayerCreationChecker(NSView * __weak view, Fn<void()> callback) 67 : _weakView(view) 68 , _callback(std::move(callback)) { 69 QCoreApplication::instance()->installEventFilter(this); 70 } 71 72protected: 73 bool eventFilter(QObject *object, QEvent *event) override { 74 if (!_weakView || [_weakView layer] != nullptr) { 75 _callback(); 76 } 77 return QObject::eventFilter(object, event); 78 } 79 80private: 81 NSView * __weak _weakView = nil; 82 Fn<void()> _callback; 83 84}; 85 86class EventFilter : public QObject, public QAbstractNativeEventFilter { 87public: 88 EventFilter( 89 not_null<QObject*> parent, 90 Fn<bool()> checkStartDrag, 91 Fn<bool(void*)> checkPerformDrag) 92 : QObject(parent) 93 , _checkStartDrag(std::move(checkStartDrag)) 94 , _checkPerformDrag(std::move(checkPerformDrag)) { 95 Expects(_checkPerformDrag != nullptr); 96 Expects(_checkStartDrag != nullptr); 97 } 98 99 bool nativeEventFilter( 100 const QByteArray &eventType, 101 void *message, 102 base::NativeEventResult *result) { 103 if (NSEvent *e = static_cast<NSEvent*>(message)) { 104 if ([e type] == NSEventTypeLeftMouseDown) { 105 _dragStarted = _checkStartDrag(); 106 } else if (([e type] == NSEventTypeLeftMouseDragged) 107 && _dragStarted) { 108 return _checkPerformDrag([e window]); 109 } 110 } 111 return false; 112 } 113 114private: 115 bool _dragStarted = false; 116 Fn<bool()> _checkStartDrag; 117 Fn<bool(void*)> _checkPerformDrag; 118 119}; 120 121} // namespace 122 123class WindowHelper::Private final { 124public: 125 explicit Private(not_null<WindowHelper*> owner); 126 ~Private(); 127 128 [[nodiscard]] int customTitleHeight() const; 129 [[nodiscard]] QRect controlsRect() const; 130 [[nodiscard]] bool checkNativeMove(void *nswindow) const; 131 void activateBeforeNativeMove(); 132 void setStaysOnTop(bool enabled); 133 void close(); 134 135private: 136 void init(); 137 void initOpenGL(); 138 void resolveWeakPointers(); 139 void initCustomTitle(); 140 141 [[nodiscard]] Fn<void(bool)> toggleCustomTitleCallback(); 142 [[nodiscard]] Fn<void()> enforceStyleCallback(); 143 void enforceStyle(); 144 145 const not_null<WindowHelper*> _owner; 146 const WindowObserver *_observer = nullptr; 147 148 NSWindow * __weak _nativeWindow = nil; 149 NSView * __weak _nativeView = nil; 150 151 std::unique_ptr<LayerCreationChecker> _layerCreationChecker; 152 153 int _customTitleHeight = 0; 154 155}; 156 157WindowHelper::Private::Private(not_null<WindowHelper*> owner) 158: _owner(owner) { 159 init(); 160} 161 162WindowHelper::Private::~Private() { 163 if (_observer) { 164 [_observer release]; 165 } 166} 167 168int WindowHelper::Private::customTitleHeight() const { 169 return _customTitleHeight; 170} 171 172QRect WindowHelper::Private::controlsRect() const { 173 const auto button = [&](NSWindowButton type) { 174 auto view = [_nativeWindow standardWindowButton:type]; 175 if (!view) { 176 return QRect(); 177 } 178 auto result = [view frame]; 179 for (auto parent = [view superview]; parent != nil; parent = [parent superview]) { 180 const auto origin = [parent frame].origin; 181 result.origin.x += origin.x; 182 result.origin.y += origin.y; 183 } 184 return QRect(result.origin.x, result.origin.y, result.size.width, result.size.height); 185 }; 186 auto result = QRect(); 187 const auto buttons = { 188 NSWindowCloseButton, 189 NSWindowMiniaturizeButton, 190 NSWindowZoomButton, 191 }; 192 for (const auto type : buttons) { 193 result = result.united(button(type)); 194 } 195 return QRect( 196 result.x(), 197 [_nativeWindow frame].size.height - result.y() - result.height(), 198 result.width(), 199 result.height()); 200} 201 202bool WindowHelper::Private::checkNativeMove(void *nswindow) const { 203 if (_nativeWindow != nswindow 204 || ([_nativeWindow styleMask] & NSFullScreenWindowMask) == NSFullScreenWindowMask) { 205 return false; 206 } 207 const auto cgReal = [NSEvent mouseLocation]; 208 const auto real = QPointF(cgReal.x, cgReal.y); 209 const auto cgFrame = [_nativeWindow frame]; 210 const auto frame = QRectF(cgFrame.origin.x, cgFrame.origin.y, cgFrame.size.width, cgFrame.size.height); 211 const auto border = QMarginsF{ 3., 3., 3., 3. }; 212 return frame.marginsRemoved(border).contains(real); 213} 214 215void WindowHelper::Private::activateBeforeNativeMove() { 216 [_nativeWindow makeKeyAndOrderFront:_nativeWindow]; 217} 218 219void WindowHelper::Private::setStaysOnTop(bool enabled) { 220 _owner->BasicWindowHelper::setStaysOnTop(enabled); 221 resolveWeakPointers(); 222 initCustomTitle(); 223} 224 225void WindowHelper::Private::close() { 226 const auto weak = Ui::MakeWeak(_owner->window()); 227 QCloseEvent e; 228 qApp->sendEvent(_owner->window(), &e); 229 if (e.isAccepted() && weak && _nativeWindow) { 230 [_nativeWindow close]; 231 } 232} 233 234Fn<void(bool)> WindowHelper::Private::toggleCustomTitleCallback() { 235 return crl::guard(_owner->window(), [=](bool visible) { 236 _owner->_titleVisible = visible; 237 _owner->updateCustomTitleVisibility(true); 238 }); 239} 240 241Fn<void()> WindowHelper::Private::enforceStyleCallback() { 242 return crl::guard(_owner->window(), [=] { enforceStyle(); }); 243} 244 245void WindowHelper::Private::enforceStyle() { 246 if (_nativeWindow && _customTitleHeight > 0) { 247 [_nativeWindow setStyleMask:[_nativeWindow styleMask] | NSFullSizeContentViewWindowMask]; 248 } 249} 250 251void WindowHelper::Private::initOpenGL() { 252 auto forceOpenGL = std::make_unique<QOpenGLWidget>(_owner->window()); 253} 254 255void WindowHelper::Private::resolveWeakPointers() { 256 _owner->window()->createWinId(); 257 258 _nativeView = reinterpret_cast<NSView*>(_owner->window()->winId()); 259 _nativeWindow = _nativeView ? [_nativeView window] : nullptr; 260 261 Ensures(_nativeWindow != nullptr); 262} 263 264void WindowHelper::Private::initCustomTitle() { 265 if (![_nativeWindow respondsToSelector:@selector(contentLayoutRect)] 266 || ![_nativeWindow respondsToSelector:@selector(setTitlebarAppearsTransparent:)]) { 267 return; 268 } 269 270 [_nativeWindow setTitlebarAppearsTransparent:YES]; 271 272 if (_observer) { 273 [_observer release]; 274 } 275 _observer = [[WindowObserver alloc] initWithToggle:toggleCustomTitleCallback() enforce:enforceStyleCallback()]; 276 [[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowWillEnterFullScreen:) name:NSWindowWillEnterFullScreenNotification object:_nativeWindow]; 277 [[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowWillExitFullScreen:) name:NSWindowWillExitFullScreenNotification object:_nativeWindow]; 278 [[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:_nativeWindow]; 279 280 // Qt has bug with layer-backed widgets containing QOpenGLWidgets. 281 // See https://bugreports.qt.io/browse/QTBUG-64494 282 // Emulate custom title instead (code below). 283 // 284 // Tried to backport a fix, testing. 285 [_nativeWindow setStyleMask:[_nativeWindow styleMask] | NSFullSizeContentViewWindowMask]; 286 auto inner = [_nativeWindow contentLayoutRect]; 287 auto full = [_nativeView frame]; 288 _customTitleHeight = qMax(qRound(full.size.height - inner.size.height), 0); 289 290 // Qt still has some bug with layer-backed widgets containing QOpenGLWidgets. 291 // See https://github.com/telegramdesktop/tdesktop/issues/4150 292 // Tried to workaround it by catching the first moment we have CALayer created 293 // and explicitly setting contentsScale to window->backingScaleFactor there. 294 _layerCreationChecker = std::make_unique<LayerCreationChecker>(_nativeView, [=] { 295 if (_nativeView && _nativeWindow) { 296 if (CALayer *layer = [_nativeView layer]) { 297 [layer setContentsScale: [_nativeWindow backingScaleFactor]]; 298 _layerCreationChecker = nullptr; 299 } 300 } else { 301 _layerCreationChecker = nullptr; 302 } 303 }); 304} 305 306void WindowHelper::Private::init() { 307 initOpenGL(); 308 resolveWeakPointers(); 309 initCustomTitle(); 310} 311 312WindowHelper::WindowHelper(not_null<RpWidget*> window) 313: BasicWindowHelper(window) 314, _private(std::make_unique<Private>(this)) 315, _title(Ui::CreateChild<TitleWidget>( 316 window.get(), 317 _private->customTitleHeight())) 318, _body(Ui::CreateChild<RpWidget>(window.get())) { 319 if (_title->shouldBeHidden()) { 320 updateCustomTitleVisibility(); 321 } 322 init(); 323} 324 325WindowHelper::~WindowHelper() { 326} 327 328not_null<RpWidget*> WindowHelper::body() { 329 return _body; 330} 331 332QMargins WindowHelper::frameMargins() { 333 const auto titleHeight = !_title->isHidden() ? _title->height() : 0; 334 return QMargins{ 0, titleHeight, 0, 0 }; 335} 336 337void WindowHelper::setTitle(const QString &title) { 338 _title->setText(title); 339 window()->setWindowTitle(_titleVisible ? QString() : title); 340} 341 342void WindowHelper::setTitleStyle(const style::WindowTitle &st) { 343 _title->setStyle(st); 344 if (_title->shouldBeHidden()) { 345 updateCustomTitleVisibility(); 346 } 347} 348 349void WindowHelper::updateCustomTitleVisibility(bool force) { 350 const auto visible = !_title->shouldBeHidden() && _titleVisible; 351 if (!force && _title->isHidden() != visible) { 352 return; 353 } 354 _title->setVisible(visible); 355 window()->setWindowTitle(_titleVisible ? QString() : _title->text()); 356} 357 358void WindowHelper::setMinimumSize(QSize size) { 359 window()->setMinimumSize(size.width(), _title->height() + size.height()); 360} 361 362void WindowHelper::setFixedSize(QSize size) { 363 window()->setFixedSize(size.width(), _title->height() + size.height()); 364} 365 366void WindowHelper::setStaysOnTop(bool enabled) { 367 _private->setStaysOnTop(enabled); 368} 369 370void WindowHelper::setGeometry(QRect rect) { 371 window()->setGeometry(rect.marginsAdded({ 0, _title->height(), 0, 0 })); 372} 373 374void WindowHelper::setupBodyTitleAreaEvents() { 375 const auto controls = _private->controlsRect(); 376 qApp->installNativeEventFilter(new EventFilter(window(), [=] { 377 const auto point = body()->mapFromGlobal(QCursor::pos()); 378 return (bodyTitleAreaHit(point) & WindowTitleHitTestFlag::Move); 379 }, [=](void *nswindow) { 380 const auto point = body()->mapFromGlobal(QCursor::pos()); 381 if (_private->checkNativeMove(nswindow) 382 && !controls.contains(point) 383 && (bodyTitleAreaHit(point) & WindowTitleHitTestFlag::Move)) { 384 _private->activateBeforeNativeMove(); 385 window()->windowHandle()->startSystemMove(); 386 return true; 387 } 388 return false; 389 })); 390} 391 392void WindowHelper::close() { 393 _private->close(); 394} 395 396void WindowHelper::init() { 397 style::PaletteChanged( 398 ) | rpl::start_with_next([=] { 399 Ui::ForceFullRepaint(window()); 400 }, window()->lifetime()); 401 402 rpl::combine( 403 window()->sizeValue(), 404 _title->heightValue(), 405 _title->shownValue() 406 ) | rpl::start_with_next([=](QSize size, int titleHeight, bool shown) { 407 if (!shown) { 408 titleHeight = 0; 409 } 410 _body->setGeometry( 411 0, 412 titleHeight, 413 size.width(), 414 size.height() - titleHeight); 415 }, _body->lifetime()); 416 417#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 418 setBodyTitleArea([](QPoint widgetPoint) { 419 using Flag = Ui::WindowTitleHitTestFlag; 420 return (widgetPoint.y() < 0) 421 ? (Flag::Move | Flag::Maximize) 422 : Flag::None; 423 }); 424#endif // Qt >= 6.0.0 425} 426 427std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper( 428 not_null<RpWidget*> window) { 429 return std::make_unique<WindowHelper>(window); 430} 431 432bool NativeWindowFrameSupported() { 433 return false; 434} 435 436} // namespace Platform 437} // namespace Ui 438