lib_ui/ui/platform/mac/ui_window_mac.mm
2023-07-20 09:59:54 +04:00

475 lines
13 KiB
Text

// 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 <QtCore/QCoreApplication>
#include <QtCore/QAbstractNativeEventFilter>
#include <QtGui/QWindow>
#include <QtGui/QtEvents>
#include <QOpenGLWidget>
#include <Cocoa/Cocoa.h>
@interface WindowObserver : NSObject {
}
- (id) initWithToggle:(Fn<void(bool)>)toggleCustomTitleVisibility enforce:(Fn<void()>)enforceCorrectStyle;
- (void) windowWillEnterFullScreen:(NSNotification *)aNotification;
- (void) windowWillExitFullScreen:(NSNotification *)aNotification;
- (void) windowDidExitFullScreen:(NSNotification *)aNotification;
@end // @interface WindowObserver
@implementation WindowObserver {
Fn<void(bool)> _toggleCustomTitleVisibility;
Fn<void()> _enforceCorrectStyle;
}
- (id) initWithToggle:(Fn<void(bool)>)toggleCustomTitleVisibility enforce:(Fn<void()>)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<void()> 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<void()> _callback;
};
class EventFilter : public QObject, public QAbstractNativeEventFilter {
public:
EventFilter(
not_null<QObject*> parent,
Fn<bool()> checkStartDrag,
Fn<bool(void*)> 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<NSEvent*>(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<bool()> _checkStartDrag;
Fn<bool(void*)> _checkPerformDrag;
};
} // namespace
class WindowHelper::Private final {
public:
explicit Private(not_null<WindowHelper*> 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<void(bool)> toggleCustomTitleCallback();
[[nodiscard]] Fn<void()> enforceStyleCallback();
void enforceStyle();
const not_null<WindowHelper*> _owner;
const WindowObserver *_observer = nullptr;
NSWindow * __weak _nativeWindow = nil;
NSView * __weak _nativeView = nil;
bool _hadNativeValues = false;
std::unique_ptr<LayerCreationChecker> _layerCreationChecker;
int _customTitleHeight = 0;
};
WindowHelper::Private::Private(not_null<WindowHelper*> 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<void(bool)> WindowHelper::Private::toggleCustomTitleCallback() {
return crl::guard(_owner->window(), [=](bool visible) {
_owner->_titleVisible = visible;
_owner->updateCustomTitleVisibility(true);
});
}
Fn<void()> 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<QOpenGLWidget>(_owner->window());
#endif // Qt < 6.4.0
}
void WindowHelper::Private::resolveWeakPointers() {
if (!_owner->window()->winId()) {
_owner->window()->createWinId();
}
_nativeView = reinterpret_cast<NSView*>(_owner->window()->winId());
_nativeWindow = _nativeView ? [_nativeView window] : nullptr;
_hadNativeValues = true;
Ensures(_nativeWindow != nullptr);
}
void WindowHelper::Private::revalidateWeakPointers() const {
if (_nativeWindow || !_hadNativeValues) {
return;
}
const_cast<Private*>(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<LayerCreationChecker>(_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<RpWidget*> window)
: BasicWindowHelper(window)
, _private(std::make_unique<Private>(this))
, _title(Ui::CreateChild<TitleWidget>(
window.get(),
_private->customTitleHeight()))
, _body(Ui::CreateChild<RpWidget>(window.get())) {
init();
_title->setControlsRect(_private->controlsRect());
}
WindowHelper::~WindowHelper() {
}
not_null<RpWidget*> 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<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window) {
return std::make_unique<WindowHelper>(window);
}
bool NativeWindowFrameSupported() {
return false;
}
} // namespace Platform
} // namespace Ui