macOS: widgets: preserve the native title bar elements

Quick part to be done.
This commit is contained in:
Yuhang Zhao 2023-01-14 18:32:47 +08:00
parent 9e975e02e0
commit 819ffb79fb
14 changed files with 128 additions and 58 deletions

View File

@ -52,7 +52,9 @@ void Dialog::setupUi()
titleBar = new StandardTitleBar(this);
titleBar->setWindowIconVisible(true);
#ifndef Q_OS_MACOS
titleBar->maximizeButton()->hide();
#endif // Q_OS_MACOS
label = new QLabel(tr("Find &what:"));
lineEdit = new QLineEdit;
@ -124,9 +126,11 @@ void Dialog::setupUi()
FramelessWidgetsHelper *helper = FramelessWidgetsHelper::get(this);
helper->setTitleBarWidget(titleBar);
#ifndef Q_OS_MACOS
helper->setSystemButton(titleBar->minimizeButton(), SystemButtonType::Minimize);
helper->setSystemButton(titleBar->maximizeButton(), SystemButtonType::Maximize);
helper->setSystemButton(titleBar->closeButton(), SystemButtonType::Close);
#endif // Q_OS_MACOS
// Special hack to disable the overriding of the mouse cursor, it's totally different
// with making the window un-resizable: we still want the window be able to resize
// programatically, but we also want the user not able to resize the window manually.

View File

@ -101,9 +101,11 @@ QMenuBar::item:pressed {
FramelessWidgetsHelper *helper = FramelessWidgetsHelper::get(this);
helper->setTitleBarWidget(m_titleBar);
#ifndef Q_OS_MACOS
helper->setSystemButton(m_titleBar->minimizeButton(), SystemButtonType::Minimize);
helper->setSystemButton(m_titleBar->maximizeButton(), SystemButtonType::Maximize);
helper->setSystemButton(m_titleBar->closeButton(), SystemButtonType::Close);
#endif // Q_OS_MACOS
helper->setHitTestVisible(mb); // IMPORTANT!
connect(helper, &FramelessWidgetsHelper::ready, this, [this, helper](){
const auto savedGeometry = Settings::get<QRect>({}, kGeometry);

View File

@ -77,9 +77,11 @@ void MainWindow::initialize()
FramelessWidgetsHelper *helper = FramelessWidgetsHelper::get(this);
helper->setTitleBarWidget(m_titleBar);
#ifndef Q_OS_MACOS
helper->setSystemButton(m_titleBar->minimizeButton(), SystemButtonType::Minimize);
helper->setSystemButton(m_titleBar->maximizeButton(), SystemButtonType::Maximize);
helper->setSystemButton(m_titleBar->closeButton(), SystemButtonType::Close);
#endif // Q_OS_MACOS
connect(helper, &FramelessWidgetsHelper::ready, this, [this, helper](){
const auto savedGeometry = Settings::get<QRect>({}, kGeometry);
if (savedGeometry.isValid() && !parent()) {

View File

@ -128,9 +128,11 @@ void Widget::initialize()
FramelessWidgetsHelper *helper = FramelessWidgetsHelper::get(this);
helper->setTitleBarWidget(m_titleBar);
#ifndef Q_OS_MACOS
helper->setSystemButton(m_titleBar->minimizeButton(), SystemButtonType::Minimize);
helper->setSystemButton(m_titleBar->maximizeButton(), SystemButtonType::Maximize);
helper->setSystemButton(m_titleBar->closeButton(), SystemButtonType::Close);
#endif // Q_OS_MACOS
connect(helper, &FramelessWidgetsHelper::ready, this, [this, helper](){
const QString objName = objectName();
const auto savedGeometry = Settings::get<QRect>(objName, kGeometry);

View File

@ -206,6 +206,7 @@ Q_NAMESPACE_EXPORT(FRAMELESSHELPER_CORE_API)
[[maybe_unused]] inline constexpr const int kDefaultWindowFrameBorderThickness = 1;
[[maybe_unused]] inline constexpr const int kDefaultTitleBarFontPointSize = 11;
[[maybe_unused]] inline constexpr const int kDefaultTitleBarContentsMargin = 10;
[[maybe_unused]] inline constexpr const int kMacOSChromeButtonAreaWidth = 60;
[[maybe_unused]] inline constexpr const QSize kDefaultWindowIconSize = {16, 16};
// We have to use "qRound()" here because "std::round()" is not constexpr, yet.
[[maybe_unused]] inline constexpr const QSize kDefaultSystemButtonSize = {qRound(qreal(kDefaultTitleBarHeight) * 1.5), kDefaultTitleBarHeight};

View File

@ -25,9 +25,11 @@
#pragma once
#include "framelesshelperwidgets_global.h"
#include <QtGui/qfont.h>
QT_BEGIN_NAMESPACE
class QPaintEvent;
class QMouseEvent;
QT_END_NAMESPACE
FRAMELESSHELPER_BEGIN_NAMESPACE
@ -43,6 +45,13 @@ class FRAMELESSHELPER_WIDGETS_API StandardTitleBarPrivate : public QObject
Q_DISABLE_COPY_MOVE(StandardTitleBarPrivate)
public:
struct FontMetrics
{
int width = 0;
int height = 0;
int baseline = 0;
};
explicit StandardTitleBarPrivate(StandardTitleBar *q);
~StandardTitleBarPrivate() override;
@ -80,6 +89,9 @@ public:
Q_NODISCARD bool windowIconVisible_real() const;
Q_NODISCARD bool isInTitleBarIconArea(const QPoint &pos) const;
Q_NODISCARD QFont defaultFont() const;
Q_NODISCARD FontMetrics titleLabelSize() const;
public Q_SLOTS:
void updateMaximizeButton();
void updateTitleBarColor();
@ -94,9 +106,11 @@ private:
private:
StandardTitleBar *q_ptr = nullptr;
#ifndef Q_OS_MACOS
StandardSystemButton *m_minimizeButton = nullptr;
StandardSystemButton *m_maximizeButton = nullptr;
StandardSystemButton *m_closeButton = nullptr;
#endif // Q_OS_MACOS
QPointer<QWidget> m_window = nullptr;
bool m_extended = false;
Qt::Alignment m_labelAlignment = {};

View File

@ -41,9 +41,11 @@ class FRAMELESSHELPER_WIDGETS_API StandardTitleBar : public QWidget
Q_DECLARE_PRIVATE(StandardTitleBar)
Q_DISABLE_COPY_MOVE(StandardTitleBar)
Q_PROPERTY(Qt::Alignment titleLabelAlignment READ titleLabelAlignment WRITE setTitleLabelAlignment NOTIFY titleLabelAlignmentChanged FINAL)
#ifndef Q_OS_MACOS
Q_PROPERTY(StandardSystemButton* minimizeButton READ minimizeButton CONSTANT FINAL)
Q_PROPERTY(StandardSystemButton* maximizeButton READ maximizeButton CONSTANT FINAL)
Q_PROPERTY(StandardSystemButton* closeButton READ closeButton CONSTANT FINAL)
#endif // Q_OS_MACOS
Q_PROPERTY(bool extended READ isExtended WRITE setExtended NOTIFY extendedChanged FINAL)
Q_PROPERTY(bool hideWhenClose READ isHideWhenClose WRITE setHideWhenClose NOTIFY hideWhenCloseChanged FINAL)
Q_PROPERTY(ChromePalette* chromePalette READ chromePalette CONSTANT FINAL)
@ -59,9 +61,11 @@ public:
Q_NODISCARD Qt::Alignment titleLabelAlignment() const;
void setTitleLabelAlignment(const Qt::Alignment value);
#ifndef Q_OS_MACOS
Q_NODISCARD StandardSystemButton *minimizeButton() const;
Q_NODISCARD StandardSystemButton *maximizeButton() const;
Q_NODISCARD StandardSystemButton *closeButton() const;
#endif // Q_OS_MACOS
Q_NODISCARD bool isExtended() const;
void setExtended(const bool value);

View File

@ -88,15 +88,12 @@ void FramelessHelperQt::addWindow(const SystemParameters &params)
data.eventFilter = new FramelessHelperQt(window);
g_qtHelper()->data.insert(windowId, data);
g_qtHelper()->mutex.unlock();
const auto shouldApplyFramelessFlag = [&params]() -> bool {
const auto shouldApplyFramelessFlag = []() -> bool {
#ifdef Q_OS_MACOS
const auto widget = params.getWidgetHandle();
return (widget && widget->isWidgetType());
return false;
#elif defined(Q_OS_LINUX)
Q_UNUSED(params);
return !Utils::isCustomDecorationSupported();
#else // Windows
Q_UNUSED(params);
return true;
#endif // Q_OS_MACOS
}();

View File

@ -197,10 +197,6 @@ void initialize()
Utils::fixupDialogsDpiScaling();
#endif
// This attribute is known to be __NOT__ compatible with QGLWidget.
// Please consider migrating to the recommended QOpenGLWidget instead.
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
// Enable high DPI scaling by default, but only for Qt5 applications,
// because this has become the default setting since Qt6 and it can't

View File

@ -292,6 +292,7 @@ public Q_SLOTS:
oldSetTitlebarAppearsTransparent = reinterpret_cast<setTitlebarAppearsTransparentPtr>(method_setImplementation(method, reinterpret_cast<IMP>(setTitlebarAppearsTransparent)));
Q_ASSERT(oldSetTitlebarAppearsTransparent);
#if 0
method = class_getInstanceMethod(windowClass, @selector(canBecomeKeyWindow));
Q_ASSERT(method);
oldCanBecomeKeyWindow = reinterpret_cast<canBecomeKeyWindowPtr>(method_setImplementation(method, reinterpret_cast<IMP>(canBecomeKeyWindow)));
@ -301,6 +302,7 @@ public Q_SLOTS:
Q_ASSERT(method);
oldCanBecomeMainWindow = reinterpret_cast<canBecomeMainWindowPtr>(method_setImplementation(method, reinterpret_cast<IMP>(canBecomeMainWindow)));
Q_ASSERT(oldCanBecomeMainWindow);
#endif
method = class_getInstanceMethod(windowClass, @selector(sendEvent:));
Q_ASSERT(method);
@ -320,6 +322,7 @@ public Q_SLOTS:
method_setImplementation(method, reinterpret_cast<IMP>(oldSetTitlebarAppearsTransparent));
oldSetTitlebarAppearsTransparent = nil;
#if 0
method = class_getInstanceMethod(windowClass, @selector(canBecomeKeyWindow));
Q_ASSERT(method);
method_setImplementation(method, reinterpret_cast<IMP>(oldCanBecomeKeyWindow));
@ -329,6 +332,7 @@ public Q_SLOTS:
Q_ASSERT(method);
method_setImplementation(method, reinterpret_cast<IMP>(oldCanBecomeMainWindow));
oldCanBecomeMainWindow = nil;
#endif
method = class_getInstanceMethod(windowClass, @selector(sendEvent:));
Q_ASSERT(method);
@ -487,6 +491,7 @@ private:
oldSendEvent(obj, sel, event);
}
#if 0
const auto nswindow = reinterpret_cast<NSWindow *>(obj);
if (!instances.contains(nswindow)) {
return;
@ -498,12 +503,13 @@ private:
QCoreApplication::processEvents();
proxy->lastMouseDownEvent = nil;
}
#endif
}
private:
QWindow *qwindow = nil;
NSWindow *nswindow = nil;
NSEvent *lastMouseDownEvent = nil;
//NSEvent *lastMouseDownEvent = nil;
NSView *blurEffect = nil;
NSWindowStyleMask oldStyleMask = 0;

View File

@ -79,12 +79,6 @@ const FramelessDialogPrivate *FramelessDialogPrivate::get(const FramelessDialog
void FramelessDialogPrivate::initialize()
{
Q_Q(FramelessDialog);
// Without this flag, Qt will always create an invisible native parent window
// for any native widgets which will intercept some win32 messages and confuse
// our own native event filter, so to prevent some weired bugs from happening,
// just disable this feature.
q->setAttribute(Qt::WA_DontCreateNativeAncestors);
q->setAttribute(Qt::WA_NativeWindow);
FramelessWidgetsHelper::get(q)->extendsContentIntoTitleBar();
m_sharedHelper = new WidgetsSharedHelper(this);
m_sharedHelper->setup(q);

View File

@ -79,12 +79,6 @@ const FramelessMainWindowPrivate *FramelessMainWindowPrivate::get(const Frameles
void FramelessMainWindowPrivate::initialize()
{
Q_Q(FramelessMainWindow);
// Without this flag, Qt will always create an invisible native parent window
// for any native widgets which will intercept some win32 messages and confuse
// our own native event filter, so to prevent some weired bugs from happening,
// just disable this feature.
q->setAttribute(Qt::WA_DontCreateNativeAncestors);
q->setAttribute(Qt::WA_NativeWindow);
FramelessWidgetsHelper::get(q)->extendsContentIntoTitleBar();
m_sharedHelper = new WidgetsSharedHelper(this);
m_sharedHelper->setup(q);

View File

@ -79,12 +79,6 @@ const FramelessWidgetPrivate *FramelessWidgetPrivate::get(const FramelessWidget
void FramelessWidgetPrivate::initialize()
{
Q_Q(FramelessWidget);
// Without this flag, Qt will always create an invisible native parent window
// for any native widgets which will intercept some win32 messages and confuse
// our own native event filter, so to prevent some weired bugs from happening,
// just disable this feature.
q->setAttribute(Qt::WA_DontCreateNativeAncestors);
q->setAttribute(Qt::WA_NativeWindow);
FramelessWidgetsHelper::get(q)->extendsContentIntoTitleBar();
m_sharedHelper = new WidgetsSharedHelper(this);
m_sharedHelper->setup(q);

View File

@ -30,6 +30,7 @@
#include <QtCore/qtimer.h>
#include <QtGui/qpainter.h>
#include <QtGui/qevent.h>
#include <QtGui/qfontmetrics.h>
#include <QtWidgets/qboxlayout.h>
FRAMELESSHELPER_BEGIN_NAMESPACE
@ -132,6 +133,32 @@ ChromePalette *StandardTitleBarPrivate::chromePalette() const
return m_chromePalette;
}
QFont StandardTitleBarPrivate::defaultFont() const
{
Q_Q(const StandardTitleBar);
QFont font = q->font();
font.setPointSize(kDefaultTitleBarFontPointSize);
return font;
}
StandardTitleBarPrivate::FontMetrics StandardTitleBarPrivate::titleLabelSize() const
{
if (!m_window) {
return {};
}
const QString text = m_window->windowTitle();
if (text.isEmpty()) {
return {};
}
const QFont font = m_titleFont.value_or(defaultFont());
const QFontMetrics fontMetrics(font);
return {
fontMetrics.horizontalAdvance(text),
fontMetrics.height(),
fontMetrics.ascent()
};
}
void StandardTitleBarPrivate::paintTitleBar(QPaintEvent *event)
{
Q_ASSERT(event);
@ -154,41 +181,35 @@ void StandardTitleBarPrivate::paintTitleBar(QPaintEvent *event)
painter.setRenderHints(QPainter::Antialiasing |
QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform);
painter.fillRect(QRect(QPoint(0, 0), q->size()), backgroundColor);
int titleLabelLeftOffset = 0;
if (m_windowIconVisible) {
const QIcon icon = m_window->windowIcon();
if (!icon.isNull()) {
const QRect rect = windowIconRect();
titleLabelLeftOffset = (rect.left() + rect.width());
icon.paint(&painter, rect);
icon.paint(&painter, windowIconRect());
}
}
if (m_titleLabelVisible) {
const QString text = m_window->windowTitle();
if (!text.isEmpty()) {
painter.setPen(foregroundColor);
painter.setFont(m_titleFont.value_or([q]() -> QFont {
QFont f = q->font();
f.setPointSize(kDefaultTitleBarFontPointSize);
return f;
}()));
const auto rect = [this, q, titleLabelLeftOffset]() -> QRect {
const int w = q->width();
int leftMargin = 0;
int rightMargin = 0;
painter.setFont(m_titleFont.value_or(defaultFont()));
const auto pos = [this, q]() -> QPoint {
const FontMetrics labelSize = titleLabelSize();
const int titleBarWidth = q->width();
int x = 0;
if (m_labelAlignment & Qt::AlignLeft) {
leftMargin = (kDefaultTitleBarContentsMargin + titleLabelLeftOffset);
x = (windowIconRect().right() + kDefaultTitleBarContentsMargin);
} else if (m_labelAlignment & Qt::AlignRight) {
x = (titleBarWidth - kDefaultTitleBarContentsMargin - labelSize.width);
#ifndef Q_OS_MACOS
x -= m_minimizeButton->x();
#endif // Q_OS_MACOS
} else {
x = std::round(qreal(titleBarWidth - labelSize.width) / qreal(2));
}
if (m_labelAlignment & Qt::AlignRight) {
rightMargin = (w - m_minimizeButton->x() + kDefaultTitleBarContentsMargin);
}
const int x = leftMargin;
const int y = 0;
const int width = (w - leftMargin - rightMargin);
const int height = q->height();
return {QPoint(x, y), QSize(width, height)};
const int y = std::round((qreal(q->height() - labelSize.height) / qreal(2)) + qreal(labelSize.baseline));
return {x, y};
}();
painter.drawText(rect, int(m_labelAlignment), text);
painter.drawText(pos, text);
}
}
painter.restore();
@ -242,6 +263,10 @@ void StandardTitleBarPrivate::setWindowIconVisible(const bool value)
return;
}
m_windowIconVisible = value;
Q_Q(StandardTitleBar);
q->update();
Q_EMIT q->windowIconVisibleChanged();
#ifndef Q_OS_MACOS
// Ideally we should use FramelessWidgetsHelper::get(this) everywhere, but sadly when
// we call it here, it may be too early that FramelessWidgetsHelper has not attached
// to the top level widget yet, and thus it will trigger an assert error (the assert
@ -250,9 +275,7 @@ void StandardTitleBarPrivate::setWindowIconVisible(const bool value)
// NOTE: In your own code, you should always use FramelessWidgetsHelper::get(this)
// if possible.
FramelessWidgetsHelper::get(m_window)->setHitTestVisible(windowIconRect(), windowIconVisible_real());
Q_Q(StandardTitleBar);
q->update();
Q_EMIT q->windowIconVisibleChanged();
#endif // Q_OS_MACOS
}
QFont StandardTitleBarPrivate::titleFont() const
@ -273,17 +296,21 @@ void StandardTitleBarPrivate::setTitleFont(const QFont &value)
bool StandardTitleBarPrivate::mouseEventHandler(QMouseEvent *event)
{
#ifdef Q_OS_MACOS
Q_UNUSED(event);
return false;
#else // !Q_OS_MACOS
Q_ASSERT(event);
if (!event) {
return false;
}
Q_Q(const StandardTitleBar);
const Qt::MouseButton button = event->button();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
const QPoint scenePos = event->scenePosition().toPoint();
#else
# else // (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
const QPoint scenePos = event->windowPos().toPoint();
#endif
# endif // (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
const bool interestArea = isInTitleBarIconArea(scenePos);
switch (event->type()) {
case QEvent::MouseButtonRelease:
@ -336,14 +363,31 @@ bool StandardTitleBarPrivate::mouseEventHandler(QMouseEvent *event)
break;
}
return false;
#endif // Q_OS_MACOS
}
QRect StandardTitleBarPrivate::windowIconRect() const
{
Q_Q(const StandardTitleBar);
const QSize size = windowIconSize();
#ifdef Q_OS_MACOS
const auto x = [this, q, &size]() -> int {
if (m_labelAlignment & Qt::AlignLeft) {
return (kMacOSChromeButtonAreaWidth + kDefaultTitleBarContentsMargin);
}
const int titleBarWidth = q->width();
const int labelWidth = titleLabelSize().width;
if (m_labelAlignment & Qt::AlignRight) {
return (titleBarWidth - labelWidth - kDefaultTitleBarContentsMargin - size.width());
}
const int centeredX = std::round(qreal(titleBarWidth - labelWidth) / qreal(2));
return (centeredX - kDefaultTitleBarContentsMargin - size.width());
}();
#else // !Q_OS_MACOS
const int x = kDefaultTitleBarContentsMargin;
#endif // Q_OS_MACOS
const int y = std::round(qreal(q->height() - size.height()) / qreal(2));
return {QPoint(kDefaultTitleBarContentsMargin, y), size};
return {QPoint(x, y), size};
}
bool StandardTitleBarPrivate::windowIconVisible_real() const
@ -361,9 +405,11 @@ bool StandardTitleBarPrivate::isInTitleBarIconArea(const QPoint &pos) const
void StandardTitleBarPrivate::updateMaximizeButton()
{
#ifndef Q_OS_MACOS
const bool max = m_window->isMaximized();
m_maximizeButton->setButtonType(max ? SystemButtonType::Restore : SystemButtonType::Maximize);
m_maximizeButton->setToolTip(max ? tr("Restore") : tr("Maximize"));
#endif // Q_OS_MACOS
}
void StandardTitleBarPrivate::updateTitleBarColor()
@ -374,6 +420,7 @@ void StandardTitleBarPrivate::updateTitleBarColor()
void StandardTitleBarPrivate::updateChromeButtonColor()
{
#ifndef Q_OS_MACOS
const bool active = m_window->isActiveWindow();
const QColor activeForeground = m_chromePalette->titleBarActiveForegroundColor();
const QColor inactiveForeground = m_chromePalette->titleBarInactiveForegroundColor();
@ -398,13 +445,16 @@ void StandardTitleBarPrivate::updateChromeButtonColor()
m_closeButton->setHoverColor(m_chromePalette->closeButtonHoverColor());
m_closeButton->setPressColor(m_chromePalette->closeButtonPressColor());
m_closeButton->setActive(active);
#endif // Q_OS_MACOS
}
void StandardTitleBarPrivate::retranslateUi()
{
#ifndef Q_OS_MACOS
m_minimizeButton->setToolTip(tr("Minimize"));
m_maximizeButton->setToolTip(m_window->isMaximized() ? tr("Restore") : tr("Maximize"));
m_closeButton->setToolTip(tr("Close"));
#endif // Q_OS_MACOS
}
bool StandardTitleBarPrivate::eventFilter(QObject *object, QEvent *event)
@ -457,6 +507,13 @@ void StandardTitleBarPrivate::initialize()
Q_UNUSED(title);
q->update();
});
#ifdef Q_OS_MACOS
const auto titleBarLayout = new QHBoxLayout(q);
titleBarLayout->setSpacing(0);
titleBarLayout->setContentsMargins(0, 0, 0, 0);
q->setLayout(titleBarLayout);
setTitleLabelAlignment(Qt::AlignCenter);
#else // !Q_OS_MACOS
m_minimizeButton = new StandardSystemButton(SystemButtonType::Minimize, q);
connect(m_minimizeButton, &StandardSystemButton::clicked, m_window, &QWidget::showMinimized);
m_maximizeButton = new StandardSystemButton(SystemButtonType::Maximize, q);
@ -497,6 +554,7 @@ void StandardTitleBarPrivate::initialize()
titleBarLayout->addLayout(systemButtonsOuterLayout);
q->setLayout(titleBarLayout);
setTitleLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter);
#endif // Q_OS_MACOS
retranslateUi();
updateTitleBarColor();
updateChromeButtonColor();
@ -522,6 +580,7 @@ void StandardTitleBar::setTitleLabelAlignment(const Qt::Alignment value)
d->setTitleLabelAlignment(value);
}
#ifndef Q_OS_MACOS
StandardSystemButton *StandardTitleBar::minimizeButton() const
{
Q_D(const StandardTitleBar);
@ -539,6 +598,7 @@ StandardSystemButton *StandardTitleBar::closeButton() const
Q_D(const StandardTitleBar);
return d->m_closeButton;
}
#endif // Q_OS_MACOS
bool StandardTitleBar::isExtended() const
{