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