diff --git a/README.md b/README.md index 1add853..24b8fb6 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ You can join our [Discord channel](https://discord.gg/grrM4Tmesy) to communicate ## Requiredments -- Compiler: a modern compiler which supports C++17 at least. Tested on MSVC 2022 (Windows), GCC 11 (Linux) and Clang 14 (macOS). +- Compiler: a modern compiler which supports C++17 at least. Tested on MSVC 2022 (Windows), GCC 11 (Linux) and Clang 13 (macOS). - Qt version: using the latest stable version of Qt is highly recommended, the minimum supported version is Qt 5.6. However, if you are using some old Qt versions (such as older than 5.12), some features may not be available. - Qt modules: QtCore and QtGui for the core module; QtWidgets for the widgets module; QtQuick, QtQuickControls2 and QtQuickTemplates2 for the quick module. - CMake & ninja: the newer, the better. Other build systems are not tested. @@ -66,7 +66,7 @@ You can join our [Discord channel](https://discord.gg/grrM4Tmesy) to communicate - Windows: Windows 7, Windows 8, Windows 8.1, Windows 10, Windows 11 - Linux: any modern Linux distros should work, but only tested on Ubuntu 20.04 and Ubuntu 22.04 -- macOS: only tested on macOS 12 due to lack of Apple devices +- macOS: only tested on macOS 12.3 due to lack of Apple devices There are some additional restrictions for each platform, please refer to the _Platform notes_ section below. @@ -86,12 +86,12 @@ cmake --build . --config Release --target all --parallel ### Qt Widgets -To customize the window frame of a QWidget, you need to instantiate a `FramelessWidgetsHelper` object and then attach it to the widget's top level widget, and then `FramelessWidgetsHelper` will do all the rest work for you: the window frame will be removed automatically once it has been attached to the top level widget successfully. In theory you can instantiate multiple `FramelessWidgetsHelper` instances for a same widget, in this case there will be only one instance that keeps functional, all other instances will become a wrapper of that one. But to make sure everything goes smoothly and normally, you should not do that in any case. The simplest way to instantiate a `FramelessWidgetsHelper` -instance is to call the static method `FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *)`. It will return the handle of the previously instantiated instance if any, or it will instantiate a new instance if it can't find one. It's safe to call this method multiple times for a same widget, it won't instantiate any new instances if there is one already. It also does not matter when and where you call that function as long as the top level widget is the same. The internally created instance will always be parented to the top level widget. Once you get the handle of the `FramelessWidgetsHelper` instance, you can call `void FramelessWidgetsHelper::extendsContentIntoTitleBar()` to let it hide the default title bar provided by the operating system. In order to make sure `FramelessWidgetsHelper` can find the correct top level widget, you should call the `FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *)` function on a widget which has a complete parent-chain whose root parent is the top level widget. To make the frameless window draggable, you should provide a homemade title bar widget yourself, the title bar widget doesn't need to be in rectangular shape, it also doesn't need to be placed on the first row of the window. Call `void FramelessWidgetsHelper::setTitleBarWidget(QWidget *)` to let `FramelessHelper` know what's your title bar widget. By default, all the widgets in the title bar area won't be responsible to any mouse and keyboard events due to they have been intercepted by FramelessHelper. To make them recover the responsible state, you should make them visible to hit test. Call `void FramelessWidgetsHelper::setHitTestVisible(QWidget* )` to do that. You can of course call it on a widget that is not inside the title bar at all, it won't have any effect though. Due to Qt's own limitations, you need to make sure your widget has a complete parent-chain whose root parent is the top level widget. Do not ever try to delete the `FramelessWidgetsHelper` instance, it may still be monitoring and controlling your widget, and Qt will delete it for you automatically. No need to worry about memory leaks. +To customize the window frame of a QWidget, you need to instantiate a `FramelessWidgetsHelper` object and then attach it to the widget's top level widget, and then `FramelessWidgetsHelper` will do all the rest work for you: the window frame will be removed automatically once it has been attached to the top level widget successfully. In theory you can instantiate multiple `FramelessWidgetsHelper` objects for a same widget, in this case there will be only one object that keeps functional, all other objects will become a wrapper of that one. But to make sure everything goes smoothly and normally, you should not do that in any case. The simplest way to instantiate a `FramelessWidgetsHelper` +object is to call the static method `FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *)`. It will return the handle of the previously instantiated object if any, or it will instantiate a new object if it can't find one. It's safe to call this method multiple times for a same widget, it won't instantiate any new objects if there is one already. It also does not matter when and where you call that function as long as the top level widget is the same. The internally created objects will always be parented to the top level widget. Once you get the handle of the `FramelessWidgetsHelper` object, you can call `void FramelessWidgetsHelper::extendsContentIntoTitleBar()` to let it hide the default title bar provided by the operating system. In order to make sure `FramelessWidgetsHelper` can find the correct top level widget, you should call the `FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *)` function on a widget which has a complete parent-chain whose root parent is the top level widget. To make the frameless window draggable, you should provide a homemade title bar widget yourself, the title bar widget doesn't need to be in rectangular shape, it also doesn't need to be placed on the first row of the window. Call `void FramelessWidgetsHelper::setTitleBarWidget(QWidget *)` to let `FramelessHelper` know what's your title bar widget. By default, all the widgets in the title bar area won't be responsible to any mouse and keyboard events due to they have been intercepted by FramelessHelper. To make them recover the responsible state, you should make them visible to hit test. Call `void FramelessWidgetsHelper::setHitTestVisible(QWidget* )` to do that. You can of course call it on a widget that is not inside the title bar at all, it won't have any effect though. Due to Qt's own limitations, you need to make sure your widget has a complete parent-chain whose root parent is the top level widget. Do not ever try to delete the `FramelessWidgetsHelper` object, it may still be monitoring and controlling your widget, and Qt will delete it for you automatically. No need to worry about memory leaks. There are also two classes called `FramelessWidget` and `FramelessMainWindow`, they are only simple wrappers of `FramelessWidgetsHelper`, which just saves the call of the `void FramelessWidgetsHelper::extendsContentIntoTitleBar()` function for you. You can absolutely use plain `QWidget` instead. -#### Code +#### Code snippet First of all, call `void FramelessHelper::Core::initialize()` in your `main` function in a very early stage: @@ -140,7 +140,7 @@ void MyWidget::myFunction2() ### Qt Quick -#### Code +#### Code snippet First of all, you should call `void FramelessHelper::Core::initialize()` in your `main` function in a very early stage: @@ -212,10 +212,14 @@ Window { } ``` -In theory it's possible to instantiate multiple `FramelessHelper` instances for a same `Window`, in this case only one of them will keep functional, all other instances will become a wrapper of it, but doing so is not recommended and may cause unexpected behavior or bugs, so please avoid trying to do that in any case. +In theory it's possible to instantiate multiple `FramelessHelper` objects for a same `Window`, in this case only one of them will keep functional, all other objects will become a wrapper of it, but doing so is not recommended and may cause unexpected behavior or bugs, so please avoid trying to do that in any case. + +If you find any of `FramelessHelper` functions have no effect after calling, the most possible reason is by the time you call the function/change the property of `FramelessHelper`, the root window has not finished its initialization process and thus `FramelessHelper` can't get the handle of it, so any action from the user will be ignored until the root window finished initialization. There's also a QML type called `FramelessWindow`, it's only a simple wrapper of `FramelessHelper`, you can absolutely use plain `Window` instead. +### More + Please refer to the demo projects to see more detailed usages: [examples](./examples/) ### Title bar design guidance @@ -255,6 +259,7 @@ Please refer to the demo projects to see more detailed usages: [examples](./exam - The frameless windows will appear in square corners instead of round corners. - The resize area is inside of the window. +- Some users reported that the window is not resizable on some old macOS versions. ## FAQs @@ -264,11 +269,11 @@ Please refer to the demo projects to see more detailed usages: [examples](./exam ### `When running on Wayland, dragging the title bar causes crash?` -You need to force Qt to use the **XCB** QPA backend when running on Wayland. Try setting the environment variable `QT_QPA_PLATFORM` to `xcb` before instantiating any `Q(Gui)Application` instances. Or just call `void FramelessHelper::Core::initialize()` in your `main` function, this function will take care of it for you. +You need to force Qt to use the **XCB** QPA when running on Wayland. Try setting the environment variable `QT_QPA_PLATFORM` to `xcb` before instantiating any `Q(Gui)Application` instances. Or just call `void FramelessHelper::Core::initialize()` in your `main` function, this function will take care of it for you. ### `I can see the black background during window resizing?` -First of all, it's a Qt issue, not caused by FramelessHelper. And it should not be possible for Qt Widgets applications. It's a common issue for Qt Quick applications. Most of the time it's caused by D3D11/Vulkan/Metal because they are not good at dealing with texture resizing operations. If you really want to fix this issue, you can try to change Qt's RHI backend to **OpenGL** or **Software**. And please keep in mind that this issue is not fixable from outside of Qt. +First of all, it's a Qt issue, not caused by FramelessHelper. And it should not be possible for Qt Widgets applications. It's a common issue for Qt Quick applications. Most of the time it's caused by D3D11/Vulkan/Metal because they are not good at dealing with texture resizing operations. If you really want to fix this issue, you can try to change Qt's RHI backend to **OpenGL** (be careful of the bug of your graphics card driver) or **Software** (if you don't care about performance). And please keep in mind that this issue is not fixable from outside of Qt. ## License diff --git a/include/FramelessHelper/Quick/private/quickstandardclosebutton_p.h b/include/FramelessHelper/Quick/private/quickstandardclosebutton_p.h index f315578..017aef1 100644 --- a/include/FramelessHelper/Quick/private/quickstandardclosebutton_p.h +++ b/include/FramelessHelper/Quick/private/quickstandardclosebutton_p.h @@ -26,7 +26,6 @@ #include "framelesshelperquick_global.h" #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) -#include #include QT_BEGIN_NAMESPACE @@ -51,7 +50,6 @@ public: private Q_SLOTS: void updateForeground(); void updateBackground(); - void updateToolTip(); private: void initialize(); @@ -60,7 +58,6 @@ private: QScopedPointer m_contentItem; QScopedPointer m_image; QScopedPointer m_backgroundItem; - QPointer m_tooltip = nullptr; }; FRAMELESSHELPER_END_NAMESPACE diff --git a/include/FramelessHelper/Quick/private/quickstandardmaximizebutton_p.h b/include/FramelessHelper/Quick/private/quickstandardmaximizebutton_p.h index aee2abb..e2824f8 100644 --- a/include/FramelessHelper/Quick/private/quickstandardmaximizebutton_p.h +++ b/include/FramelessHelper/Quick/private/quickstandardmaximizebutton_p.h @@ -27,7 +27,6 @@ #include "framelesshelperquick_global.h" #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include -#include QT_BEGIN_NAMESPACE class QQuickImage; @@ -55,7 +54,6 @@ public: private Q_SLOTS: void updateForeground(); void updateBackground(); - void updateToolTip(); Q_SIGNALS: void maximizedChanged(); @@ -68,7 +66,6 @@ private: QScopedPointer m_contentItem; QScopedPointer m_image; QScopedPointer m_backgroundItem; - QPointer m_tooltip = nullptr; }; FRAMELESSHELPER_END_NAMESPACE diff --git a/include/FramelessHelper/Quick/private/quickstandardminimizebutton_p.h b/include/FramelessHelper/Quick/private/quickstandardminimizebutton_p.h index 49ea059..761744d 100644 --- a/include/FramelessHelper/Quick/private/quickstandardminimizebutton_p.h +++ b/include/FramelessHelper/Quick/private/quickstandardminimizebutton_p.h @@ -27,7 +27,6 @@ #include "framelesshelperquick_global.h" #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include -#include QT_BEGIN_NAMESPACE class QQuickImage; @@ -51,7 +50,6 @@ public: private Q_SLOTS: void updateForeground(); void updateBackground(); - void updateToolTip(); private: void initialize(); @@ -60,7 +58,6 @@ private: QScopedPointer m_contentItem; QScopedPointer m_image; QScopedPointer m_backgroundItem; - QPointer m_tooltip = nullptr; }; FRAMELESSHELPER_END_NAMESPACE diff --git a/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h b/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h index bd4c651..53b8397 100644 --- a/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h +++ b/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h @@ -27,9 +27,9 @@ #include "framelesshelperquick_global.h" #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include +#include QT_BEGIN_NAMESPACE -class QQuickLabel; class QQuickRow; QT_END_NAMESPACE @@ -47,6 +47,7 @@ class FRAMELESSHELPER_QUICK_API QuickStandardTitleBar : public QQuickRectangle #endif Q_DISABLE_COPY_MOVE(QuickStandardTitleBar) Q_PROPERTY(Qt::Alignment titleLabelAlignment READ titleLabelAlignment WRITE setTitleLabelAlignment NOTIFY titleLabelAlignmentChanged FINAL) + Q_PROPERTY(QQuickLabel* titleLabel READ titleLabel CONSTANT FINAL) Q_PROPERTY(QuickStandardMinimizeButton* minimizeButton READ minimizeButton CONSTANT FINAL) Q_PROPERTY(QuickStandardMaximizeButton* maximizeButton READ maximizeButton CONSTANT FINAL) Q_PROPERTY(QuickStandardCloseButton* closeButton READ closeButton CONSTANT FINAL) @@ -59,6 +60,7 @@ public: Q_NODISCARD Qt::Alignment titleLabelAlignment() const; void setTitleLabelAlignment(const Qt::Alignment value); + Q_NODISCARD QQuickLabel *titleLabel() const; Q_NODISCARD QuickStandardMinimizeButton *minimizeButton() const; Q_NODISCARD QuickStandardMaximizeButton *maximizeButton() const; Q_NODISCARD QuickStandardCloseButton *closeButton() const; @@ -68,6 +70,7 @@ public: protected: void itemChange(const ItemChange change, const ItemChangeData &value) override; + Q_NODISCARD bool eventFilter(QObject *object, QEvent *event) override; private Q_SLOTS: void updateMaximizeButton(); @@ -76,6 +79,7 @@ private Q_SLOTS: void clickMinimizeButton(); void clickMaximizeButton(); void clickCloseButton(); + void retranslateUi(); Q_SIGNALS: void titleLabelAlignmentChanged(); @@ -87,11 +91,11 @@ private: private: Qt::Alignment m_labelAlignment = {}; - QScopedPointer m_label; - QScopedPointer m_row; - QScopedPointer m_minBtn; - QScopedPointer m_maxBtn; - QScopedPointer m_closeBtn; + QScopedPointer m_windowTitleLabel; + QScopedPointer m_systemButtonsRow; + QScopedPointer m_minimizeButton; + QScopedPointer m_maximizeButton; + QScopedPointer m_closeButton; QMetaObject::Connection m_windowStateChangeConnection = {}; QMetaObject::Connection m_windowActiveChangeConnection = {}; QMetaObject::Connection m_windowTitleChangeConnection = {}; diff --git a/include/FramelessHelper/Widgets/private/standardtitlebar_p.h b/include/FramelessHelper/Widgets/private/standardtitlebar_p.h index 06aff03..2ec7ec1 100644 --- a/include/FramelessHelper/Widgets/private/standardtitlebar_p.h +++ b/include/FramelessHelper/Widgets/private/standardtitlebar_p.h @@ -30,6 +30,7 @@ QT_BEGIN_NAMESPACE class QLabel; +class QSpacerItem; QT_END_NAMESPACE FRAMELESSHELPER_BEGIN_NAMESPACE @@ -50,12 +51,16 @@ public: Q_NODISCARD static StandardTitleBarPrivate *get(StandardTitleBar *pub); Q_NODISCARD static const StandardTitleBarPrivate *get(const StandardTitleBar *pub); + Q_NODISCARD Qt::Alignment titleLabelAlignment() const; + void setTitleLabelAlignment(const Qt::Alignment value); + Q_NODISCARD bool isExtended() const; void setExtended(const bool value); public Q_SLOTS: void updateMaximizeButton(); void updateTitleBarStyleSheet(); + void retranslateUi(); protected: Q_NODISCARD bool eventFilter(QObject *object, QEvent *event) override; @@ -71,6 +76,9 @@ private: QScopedPointer m_closeButton; QPointer m_window = nullptr; bool m_extended = false; + Qt::Alignment m_labelAlignment = {}; + QSpacerItem *m_labelLeftStretch = nullptr; + QSpacerItem *m_labelRightStretch = nullptr; }; FRAMELESSHELPER_END_NAMESPACE diff --git a/include/FramelessHelper/Widgets/standardtitlebar.h b/include/FramelessHelper/Widgets/standardtitlebar.h index 4b65464..0cd3287 100644 --- a/include/FramelessHelper/Widgets/standardtitlebar.h +++ b/include/FramelessHelper/Widgets/standardtitlebar.h @@ -26,6 +26,7 @@ #include "framelesshelperwidgets_global.h" #include +#include FRAMELESSHELPER_BEGIN_NAMESPACE @@ -37,6 +38,8 @@ class FRAMELESSHELPER_WIDGETS_API StandardTitleBar : public QWidget Q_OBJECT Q_DECLARE_PRIVATE(StandardTitleBar) Q_DISABLE_COPY_MOVE(StandardTitleBar) + Q_PROPERTY(Qt::Alignment titleLabelAlignment READ titleLabelAlignment WRITE setTitleLabelAlignment NOTIFY titleLabelAlignmentChanged FINAL) + Q_PROPERTY(QLabel* titleLabel READ titleLabel CONSTANT FINAL) Q_PROPERTY(StandardSystemButton* minimizeButton READ minimizeButton CONSTANT FINAL) Q_PROPERTY(StandardSystemButton* maximizeButton READ maximizeButton CONSTANT FINAL) Q_PROPERTY(StandardSystemButton* closeButton READ closeButton CONSTANT FINAL) @@ -46,6 +49,10 @@ public: explicit StandardTitleBar(QWidget *parent = nullptr); ~StandardTitleBar() override; + Q_NODISCARD Qt::Alignment titleLabelAlignment() const; + void setTitleLabelAlignment(const Qt::Alignment value); + + Q_NODISCARD QLabel *titleLabel() const; Q_NODISCARD StandardSystemButton *minimizeButton() const; Q_NODISCARD StandardSystemButton *maximizeButton() const; Q_NODISCARD StandardSystemButton *closeButton() const; @@ -58,6 +65,7 @@ protected: Q_SIGNALS: void extendedChanged(); + void titleLabelAlignmentChanged(); private: QScopedPointer d_ptr; diff --git a/src/core/framelesshelper_win.cpp b/src/core/framelesshelper_win.cpp index 396ca71..06c2b07 100644 --- a/src/core/framelesshelper_win.cpp +++ b/src/core/framelesshelper_win.cpp @@ -103,31 +103,60 @@ FRAMELESSHELPER_STRING_CONSTANT(FindWindowW) const Win32HelperData data = g_win32Helper()->data.value(parentWindowId); g_win32Helper()->mutex.unlock(); const auto parentWindowHandle = reinterpret_cast(parentWindowId); + // All mouse events: client area mouse events + non-client area mouse events. + // Hit-testing event should not be considered as a mouse event. + const bool isMouseEvent = (((uMsg >= WM_MOUSEFIRST) && (uMsg <= WM_MOUSELAST)) || + ((uMsg >= WM_NCMOUSEMOVE) && (uMsg <= WM_NCXBUTTONDBLCLK))); // We only use this drag bar window to activate the snap layouts feature, if the parent // window is not resizable, the snap layouts feature should also be disabled at the same time, // hence forward everything to the parent window, we don't need to handle anything here. if (data.params.isWindowFixedSize()) { - return SendMessageW(parentWindowHandle, uMsg, wParam, lParam); + // Ask the parent window for the hit test result and returns it here, to + // let our homemade title bar still draggable. + if (uMsg == WM_NCHITTEST) { + return SendMessageW(parentWindowHandle, uMsg, wParam, lParam); + } + // Forward all mouse events to the parent window to let the controls inside + // our homemade title bar still continue to work normally. But ignore these + // events in this drag bar window due to there are no controls in it. + if (isMouseEvent) { + SendMessageW(parentWindowHandle, uMsg, wParam, lParam); + return 0; + } + // For all other events just use the default handling. + return DefWindowProcW(hWnd, uMsg, wParam, lParam); } - const auto releaseButtons = [&data]() -> void { + const auto releaseButtons = [&data](const std::optional exclude) -> void { static constexpr const auto defaultButtonState = ButtonState::Unspecified; - data.params.setSystemButtonState(SystemButtonType::WindowIcon, defaultButtonState); - data.params.setSystemButtonState(SystemButtonType::Help, defaultButtonState); - data.params.setSystemButtonState(SystemButtonType::Minimize, defaultButtonState); - data.params.setSystemButtonState(SystemButtonType::Maximize, defaultButtonState); - data.params.setSystemButtonState(SystemButtonType::Restore, defaultButtonState); - data.params.setSystemButtonState(SystemButtonType::Close, defaultButtonState); + if (!exclude.has_value() || (exclude.value() != SystemButtonType::WindowIcon)) { + data.params.setSystemButtonState(SystemButtonType::WindowIcon, defaultButtonState); + } + if (!exclude.has_value() || (exclude.value() != SystemButtonType::Help)) { + data.params.setSystemButtonState(SystemButtonType::Help, defaultButtonState); + } + if (!exclude.has_value() || (exclude.value() != SystemButtonType::Minimize)) { + data.params.setSystemButtonState(SystemButtonType::Minimize, defaultButtonState); + } + if (!exclude.has_value() || (exclude.value() != SystemButtonType::Maximize)) { + data.params.setSystemButtonState(SystemButtonType::Maximize, defaultButtonState); + } + if (!exclude.has_value() || (exclude.value() != SystemButtonType::Restore)) { + data.params.setSystemButtonState(SystemButtonType::Restore, defaultButtonState); + } + if (!exclude.has_value() || (exclude.value() != SystemButtonType::Close)) { + data.params.setSystemButtonState(SystemButtonType::Close, defaultButtonState); + } }; const auto hoverButton = [&releaseButtons, &data](const SystemButtonType button) -> void { - releaseButtons(); + releaseButtons(button); data.params.setSystemButtonState(button, ButtonState::Hovered); }; const auto pressButton = [&releaseButtons, &data](const SystemButtonType button) -> void { - releaseButtons(); + releaseButtons(button); data.params.setSystemButtonState(button, ButtonState::Pressed); }; const auto clickButton = [&releaseButtons, &data](const SystemButtonType button) -> void { - releaseButtons(); + releaseButtons(button); data.params.setSystemButtonState(button, ButtonState::Clicked); }; switch (uMsg) { @@ -177,7 +206,7 @@ FRAMELESSHELPER_STRING_CONSTANT(FindWindowW) switch (wParam) { case HTTOP: case HTCAPTION: { - releaseButtons(); + releaseButtons(std::nullopt); // Pass caption-related nonclient messages to the parent window. // Make sure to do this for the HTTOP, which is the top resize // border, so we can resize the window on the top. @@ -199,7 +228,7 @@ FRAMELESSHELPER_STRING_CONSTANT(FindWindowW) hoverButton(SystemButtonType::Close); break; default: - releaseButtons(); + releaseButtons(std::nullopt); break; } // If we haven't previously asked for mouse tracking, request mouse @@ -230,7 +259,7 @@ FRAMELESSHELPER_STRING_CONSTANT(FindWindowW) case WM_NCMOUSELEAVE: case WM_MOUSELEAVE: { // When the mouse leaves the drag rect, make sure to dismiss any hover. - releaseButtons(); + releaseButtons(std::nullopt); QMutexLocker locker(&g_win32Helper()->mutex); g_win32Helper()->data[parentWindowId].trackingMouse = false; } break; @@ -314,11 +343,10 @@ FRAMELESSHELPER_STRING_CONSTANT(FindWindowW) default: break; } - // Forward all the mouse events we don't handled here to the parent window, + // Forward all the mouse events we don't handle here to the parent window, // this is a necessary step to make sure the child widgets/quick items can still // receive mouse events from our homemade title bar. - if (((uMsg >= WM_MOUSEFIRST) && (uMsg <= WM_MOUSELAST)) || - ((uMsg >= WM_NCMOUSEMOVE) && (uMsg <= WM_NCXBUTTONDBLCLK))) { + if (isMouseEvent) { SendMessageW(parentWindowHandle, uMsg, wParam, lParam); } return DefWindowProcW(hWnd, uMsg, wParam, lParam); diff --git a/src/quick/quickstandardclosebutton.cpp b/src/quick/quickstandardclosebutton.cpp index 0b116c8..0ecbe65 100644 --- a/src/quick/quickstandardclosebutton.cpp +++ b/src/quick/quickstandardclosebutton.cpp @@ -29,6 +29,7 @@ #include #include #include +#include static inline void initResource() { @@ -60,15 +61,11 @@ void QuickStandardCloseButton::updateForeground() void QuickStandardCloseButton::updateBackground() { static constexpr const auto button = SystemButtonType::Close; - const ButtonState state = (isPressed() ? ButtonState::Pressed : ButtonState::Hovered); - const bool visible = (isHovered() || isPressed()); - m_backgroundItem->setColor(Utils::calculateSystemButtonBackgroundColor(button, state)); - m_backgroundItem->setVisible(visible); -} - -void QuickStandardCloseButton::updateToolTip() -{ - m_tooltip->setVisible(isHovered() && !isPressed()); + const bool hover = isHovered(); + const bool press = isPressed(); + m_backgroundItem->setColor(Utils::calculateSystemButtonBackgroundColor(button, (press ? ButtonState::Pressed : ButtonState::Hovered))); + m_backgroundItem->setVisible(hover || press); + qobject_cast(qmlAttachedPropertiesObject(this))->setVisible(hover); } void QuickStandardCloseButton::initialize() @@ -93,15 +90,8 @@ void QuickStandardCloseButton::initialize() connect(this, &QuickStandardCloseButton::hoveredChanged, this, &QuickStandardCloseButton::updateBackground); connect(this, &QuickStandardCloseButton::pressedChanged, this, &QuickStandardCloseButton::updateBackground); - m_tooltip = qobject_cast(qmlAttachedPropertiesObject(this)); - m_tooltip->setText(tr("Close")); - m_tooltip->setDelay(0); - connect(this, &QuickStandardCloseButton::hoveredChanged, this, &QuickStandardCloseButton::updateToolTip); - connect(this, &QuickStandardCloseButton::pressedChanged, this, &QuickStandardCloseButton::updateToolTip); - updateBackground(); updateForeground(); - updateToolTip(); setContentItem(m_contentItem.data()); setBackground(m_backgroundItem.data()); diff --git a/src/quick/quickstandardmaximizebutton.cpp b/src/quick/quickstandardmaximizebutton.cpp index 64dbce8..c788a76 100644 --- a/src/quick/quickstandardmaximizebutton.cpp +++ b/src/quick/quickstandardmaximizebutton.cpp @@ -29,6 +29,7 @@ #include #include #include +#include static inline void initResource() { @@ -76,16 +77,11 @@ void QuickStandardMaximizeButton::updateForeground() void QuickStandardMaximizeButton::updateBackground() { const SystemButtonType button = (m_max ? SystemButtonType::Restore : SystemButtonType::Maximize); - const ButtonState state = (isPressed() ? ButtonState::Pressed : ButtonState::Hovered); - const bool visible = (isHovered() || isPressed()); - m_backgroundItem->setColor(Utils::calculateSystemButtonBackgroundColor(button, state)); - m_backgroundItem->setVisible(visible); -} - -void QuickStandardMaximizeButton::updateToolTip() -{ - m_tooltip->setVisible(isHovered() && !isPressed()); - m_tooltip->setText(m_max ? tr("Restore") : tr("Maximize")); + const bool hover = isHovered(); + const bool press = isPressed(); + m_backgroundItem->setColor(Utils::calculateSystemButtonBackgroundColor(button, (press ? ButtonState::Pressed : ButtonState::Hovered))); + m_backgroundItem->setVisible(hover || press); + qobject_cast(qmlAttachedPropertiesObject(this))->setVisible(hover); } void QuickStandardMaximizeButton::initialize() @@ -109,15 +105,8 @@ void QuickStandardMaximizeButton::initialize() connect(this, &QuickStandardMaximizeButton::hoveredChanged, this, &QuickStandardMaximizeButton::updateBackground); connect(this, &QuickStandardMaximizeButton::pressedChanged, this, &QuickStandardMaximizeButton::updateBackground); - m_tooltip = qobject_cast(qmlAttachedPropertiesObject(this)); - m_tooltip->setDelay(0); - connect(this, &QuickStandardMaximizeButton::hoveredChanged, this, &QuickStandardMaximizeButton::updateToolTip); - connect(this, &QuickStandardMaximizeButton::pressedChanged, this, &QuickStandardMaximizeButton::updateToolTip); - connect(this, &QuickStandardMaximizeButton::maximizedChanged, this, &QuickStandardMaximizeButton::updateToolTip); - updateBackground(); updateForeground(); - updateToolTip(); setContentItem(m_contentItem.data()); setBackground(m_backgroundItem.data()); diff --git a/src/quick/quickstandardminimizebutton.cpp b/src/quick/quickstandardminimizebutton.cpp index ed38af4..b0e3a40 100644 --- a/src/quick/quickstandardminimizebutton.cpp +++ b/src/quick/quickstandardminimizebutton.cpp @@ -29,6 +29,7 @@ #include #include #include +#include static inline void initResource() { @@ -60,15 +61,11 @@ void QuickStandardMinimizeButton::updateForeground() void QuickStandardMinimizeButton::updateBackground() { static constexpr const auto button = SystemButtonType::Minimize; - const ButtonState state = (isPressed() ? ButtonState::Pressed : ButtonState::Hovered); - const bool visible = (isHovered() || isPressed()); - m_backgroundItem->setColor(Utils::calculateSystemButtonBackgroundColor(button, state)); - m_backgroundItem->setVisible(visible); -} - -void QuickStandardMinimizeButton::updateToolTip() -{ - m_tooltip->setVisible(isHovered() && !isPressed()); + const bool hover = isHovered(); + const bool press = isPressed(); + m_backgroundItem->setColor(Utils::calculateSystemButtonBackgroundColor(button, (press ? ButtonState::Pressed : ButtonState::Hovered))); + m_backgroundItem->setVisible(hover || press); + qobject_cast(qmlAttachedPropertiesObject(this))->setVisible(hover); } void QuickStandardMinimizeButton::initialize() @@ -91,15 +88,8 @@ void QuickStandardMinimizeButton::initialize() connect(this, &QuickStandardMinimizeButton::hoveredChanged, this, &QuickStandardMinimizeButton::updateBackground); connect(this, &QuickStandardMinimizeButton::pressedChanged, this, &QuickStandardMinimizeButton::updateBackground); - m_tooltip = qobject_cast(qmlAttachedPropertiesObject(this)); - m_tooltip->setText(tr("Minimize")); - m_tooltip->setDelay(0); - connect(this, &QuickStandardMinimizeButton::hoveredChanged, this, &QuickStandardMinimizeButton::updateToolTip); - connect(this, &QuickStandardMinimizeButton::pressedChanged, this, &QuickStandardMinimizeButton::updateToolTip); - updateBackground(); updateForeground(); - updateToolTip(); setContentItem(m_contentItem.data()); setBackground(m_backgroundItem.data()); diff --git a/src/quick/quickstandardtitlebar.cpp b/src/quick/quickstandardtitlebar.cpp index 29ee0cd..aab8433 100644 --- a/src/quick/quickstandardtitlebar.cpp +++ b/src/quick/quickstandardtitlebar.cpp @@ -33,7 +33,7 @@ #include #include #include -#include +#include FRAMELESSHELPER_BEGIN_NAMESPACE @@ -57,7 +57,7 @@ void QuickStandardTitleBar::setTitleLabelAlignment(const Qt::Alignment value) return; } m_labelAlignment = value; - QQuickAnchors * const labelAnchors = QQuickItemPrivate::get(m_label.data())->anchors(); + QQuickAnchors * const labelAnchors = QQuickItemPrivate::get(m_windowTitleLabel.data())->anchors(); const QQuickItemPrivate * const titleBarPriv = QQuickItemPrivate::get(this); if (m_labelAlignment & Qt::AlignTop) { labelAnchors->setTop(titleBarPriv->top()); @@ -72,7 +72,7 @@ void QuickStandardTitleBar::setTitleLabelAlignment(const Qt::Alignment value) labelAnchors->setLeftMargin(kDefaultTitleBarContentsMargin); } if (m_labelAlignment & Qt::AlignRight) { - labelAnchors->setRight(QQuickItemPrivate::get(m_row.data())->left()); + labelAnchors->setRight(QQuickItemPrivate::get(m_systemButtonsRow.data())->left()); labelAnchors->setRightMargin(kDefaultTitleBarContentsMargin); } if (m_labelAlignment & Qt::AlignVCenter) { @@ -88,19 +88,24 @@ void QuickStandardTitleBar::setTitleLabelAlignment(const Qt::Alignment value) Q_EMIT titleLabelAlignmentChanged(); } +QQuickLabel *QuickStandardTitleBar::titleLabel() const +{ + return m_windowTitleLabel.data(); +} + QuickStandardMinimizeButton *QuickStandardTitleBar::minimizeButton() const { - return m_minBtn.data(); + return m_minimizeButton.data(); } QuickStandardMaximizeButton *QuickStandardTitleBar::maximizeButton() const { - return m_maxBtn.data(); + return m_maximizeButton.data(); } QuickStandardCloseButton *QuickStandardTitleBar::closeButton() const { - return m_closeBtn.data(); + return m_closeButton.data(); } bool QuickStandardTitleBar::isExtended() const @@ -124,7 +129,15 @@ void QuickStandardTitleBar::updateMaximizeButton() if (!w) { return; } - m_maxBtn->setMaximized(w->visibility() == QQuickWindow::Maximized); + m_maximizeButton->setMaximized(w->visibility() == QQuickWindow::Maximized); + qobject_cast(qmlAttachedPropertiesObject(m_maximizeButton.data()))->setText([this]() -> QString { + if (const QQuickWindow * const w = window()) { + if (w->visibility() == QQuickWindow::Maximized) { + return tr("Restore"); + } + } + return tr("Maximize"); + }()); } void QuickStandardTitleBar::updateTitleLabelText() @@ -133,7 +146,7 @@ void QuickStandardTitleBar::updateTitleLabelText() if (!w) { return; } - m_label->setText(w->title()); + m_windowTitleLabel->setText(w->title()); } void QuickStandardTitleBar::updateTitleBarColor() @@ -174,7 +187,7 @@ void QuickStandardTitleBar::updateTitleBarColor() foregroundColor = kDefaultDarkGrayColor; } setColor(backgroundColor); - m_label->setColor(foregroundColor); + m_windowTitleLabel->setColor(foregroundColor); } void QuickStandardTitleBar::clickMinimizeButton() @@ -208,6 +221,20 @@ void QuickStandardTitleBar::clickCloseButton() w->close(); } +void QuickStandardTitleBar::retranslateUi() +{ + qobject_cast(qmlAttachedPropertiesObject(m_minimizeButton.data()))->setText(tr("Minimize")); + qobject_cast(qmlAttachedPropertiesObject(m_maximizeButton.data()))->setText([this]() -> QString { + if (const QQuickWindow * const w = window()) { + if (w->visibility() == QQuickWindow::Maximized) { + return tr("Restore"); + } + } + return tr("Maximize"); + }()); + qobject_cast(qmlAttachedPropertiesObject(m_closeButton.data()))->setText(tr("Close")); +} + void QuickStandardTitleBar::initialize() { QQuickPen * const b = border(); @@ -215,27 +242,27 @@ void QuickStandardTitleBar::initialize() b->setColor(kDefaultTransparentColor); setHeight(kDefaultTitleBarHeight); - m_label.reset(new QQuickLabel(this)); - QFont f = m_label->font(); + m_windowTitleLabel.reset(new QQuickLabel(this)); + QFont f = m_windowTitleLabel->font(); f.setPointSize(kDefaultTitleBarFontPointSize); - m_label->setFont(f); + m_windowTitleLabel->setFont(f); - m_row.reset(new QQuickRow(this)); - QQuickAnchors * const rowAnchors = QQuickItemPrivate::get(m_row.data())->anchors(); + m_systemButtonsRow.reset(new QQuickRow(this)); + QQuickAnchors * const rowAnchors = QQuickItemPrivate::get(m_systemButtonsRow.data())->anchors(); const QQuickItemPrivate * const thisPriv = QQuickItemPrivate::get(this); rowAnchors->setTop(thisPriv->top()); rowAnchors->setRight(thisPriv->right()); - m_minBtn.reset(new QuickStandardMinimizeButton(m_row.data())); - connect(m_minBtn.data(), &QuickStandardMinimizeButton::clicked, this, &QuickStandardTitleBar::clickMinimizeButton); - m_maxBtn.reset(new QuickStandardMaximizeButton(m_row.data())); - connect(m_maxBtn.data(), &QuickStandardMaximizeButton::clicked, this, &QuickStandardTitleBar::clickMaximizeButton); - m_closeBtn.reset(new QuickStandardCloseButton(m_row.data())); - connect(m_closeBtn.data(), &QuickStandardCloseButton::clicked, this, &QuickStandardTitleBar::clickCloseButton); + m_minimizeButton.reset(new QuickStandardMinimizeButton(m_systemButtonsRow.data())); + connect(m_minimizeButton.data(), &QuickStandardMinimizeButton::clicked, this, &QuickStandardTitleBar::clickMinimizeButton); + m_maximizeButton.reset(new QuickStandardMaximizeButton(m_systemButtonsRow.data())); + connect(m_maximizeButton.data(), &QuickStandardMaximizeButton::clicked, this, &QuickStandardTitleBar::clickMaximizeButton); + m_closeButton.reset(new QuickStandardCloseButton(m_systemButtonsRow.data())); + connect(m_closeButton.data(), &QuickStandardCloseButton::clicked, this, &QuickStandardTitleBar::clickCloseButton); connect(FramelessManager::instance(), &FramelessManager::systemThemeChanged, this, &QuickStandardTitleBar::updateTitleBarColor); setTitleLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); - + retranslateUi(); updateAll(); } @@ -245,20 +272,32 @@ void QuickStandardTitleBar::itemChange(const ItemChange change, const ItemChange if ((change == ItemSceneChange) && value.window) { if (m_windowStateChangeConnection) { disconnect(m_windowStateChangeConnection); + m_windowStateChangeConnection = {}; } if (m_windowActiveChangeConnection) { disconnect(m_windowActiveChangeConnection); + m_windowActiveChangeConnection = {}; } if (m_windowTitleChangeConnection) { disconnect(m_windowTitleChangeConnection); + m_windowTitleChangeConnection = {}; } m_windowStateChangeConnection = connect(value.window, &QQuickWindow::visibilityChanged, this, &QuickStandardTitleBar::updateMaximizeButton); m_windowActiveChangeConnection = connect(value.window, &QQuickWindow::activeChanged, this, &QuickStandardTitleBar::updateTitleBarColor); m_windowTitleChangeConnection = connect(value.window, &QQuickWindow::windowTitleChanged, this, &QuickStandardTitleBar::updateTitleLabelText); updateAll(); + value.window->installEventFilter(this); } } +bool QuickStandardTitleBar::eventFilter(QObject *object, QEvent *event) +{ + if (event && (event->type() == QEvent::LanguageChange)) { + retranslateUi(); + } + return QQuickRectangle::eventFilter(object, event); +} + void QuickStandardTitleBar::updateAll() { updateMaximizeButton(); diff --git a/src/widgets/standardsystembutton.cpp b/src/widgets/standardsystembutton.cpp index aac5922..12897b7 100644 --- a/src/widgets/standardsystembutton.cpp +++ b/src/widgets/standardsystembutton.cpp @@ -26,6 +26,7 @@ #include "standardsystembutton_p.h" #include #include +#include #include #include @@ -200,6 +201,16 @@ void StandardSystemButtonPrivate::setHovered(const bool value) m_hovered = value; Q_Q(StandardSystemButton); q->update(); + if (m_hovered) { + const QString toolTip = q->toolTip(); + if (!toolTip.isEmpty() && !QToolTip::isVisible()) { + QToolTip::showText(q->mapToGlobal(QPoint(0, -(qRound(qreal(q->height()) * 1.3)))), toolTip, q, q->geometry()); + } + } else { + if (QToolTip::isVisible()) { + QToolTip::hideText(); + } + } Q_EMIT q->hoveredChanged(); } diff --git a/src/widgets/standardtitlebar.cpp b/src/widgets/standardtitlebar.cpp index 8fe2b0d..cbb0d8e 100644 --- a/src/widgets/standardtitlebar.cpp +++ b/src/widgets/standardtitlebar.cpp @@ -71,6 +71,40 @@ const StandardTitleBarPrivate *StandardTitleBarPrivate::get(const StandardTitleB return pub->d_func(); } +Qt::Alignment StandardTitleBarPrivate::titleLabelAlignment() const +{ + return m_labelAlignment; +} + +void StandardTitleBarPrivate::setTitleLabelAlignment(const Qt::Alignment value) +{ + if (m_labelAlignment == value) { + return; + } + m_labelAlignment = value; + bool needsInvalidate = false; + if (m_labelAlignment & Qt::AlignLeft) { + m_labelLeftStretch->changeSize(0, 0); + m_labelRightStretch->changeSize(0, 0, QSizePolicy::Expanding); + needsInvalidate = true; + } + if (m_labelAlignment & Qt::AlignRight) { + m_labelLeftStretch->changeSize(0, 0, QSizePolicy::Expanding); + m_labelRightStretch->changeSize(0, 0); + needsInvalidate = true; + } + if (m_labelAlignment & Qt::AlignHCenter) { + m_labelLeftStretch->changeSize(0, 0, QSizePolicy::Expanding); + m_labelRightStretch->changeSize(0, 0, QSizePolicy::Expanding); + needsInvalidate = true; + } + Q_Q(StandardTitleBar); + if (needsInvalidate) { + q->layout()->invalidate(); + } + Q_EMIT q->titleLabelAlignmentChanged(); +} + bool StandardTitleBarPrivate::isExtended() const { return m_extended; @@ -89,9 +123,9 @@ void StandardTitleBarPrivate::setExtended(const bool value) void StandardTitleBarPrivate::updateMaximizeButton() { - const bool zoomed = (m_window->isMaximized() || m_window->isFullScreen()); - m_maximizeButton->setToolTip(zoomed ? tr("Restore") : tr("Maximize")); - m_maximizeButton->setButtonType(zoomed ? SystemButtonType::Restore : SystemButtonType::Maximize); + const bool max = m_window->isMaximized(); + m_maximizeButton->setButtonType(max ? SystemButtonType::Restore : SystemButtonType::Maximize); + m_maximizeButton->setToolTip(max ? tr("Restore") : tr("Maximize")); } void StandardTitleBarPrivate::updateTitleBarStyleSheet() @@ -133,6 +167,13 @@ void StandardTitleBarPrivate::updateTitleBarStyleSheet() q->update(); } +void StandardTitleBarPrivate::retranslateUi() +{ + m_minimizeButton->setToolTip(tr("Minimize")); + m_maximizeButton->setToolTip(m_window->isMaximized() ? tr("Restore") : tr("Maximize")); + m_closeButton->setToolTip(tr("Close")); +} + bool StandardTitleBarPrivate::eventFilter(QObject *object, QEvent *event) { Q_ASSERT(object); @@ -154,6 +195,9 @@ bool StandardTitleBarPrivate::eventFilter(QObject *object, QEvent *event) case QEvent::ActivationChange: updateTitleBarStyleSheet(); break; + case QEvent::LanguageChange: + retranslateUi(); + break; default: break; } @@ -174,7 +218,6 @@ void StandardTitleBarPrivate::initialize() m_windowTitleLabel->setText(m_window->windowTitle()); connect(m_window, &QWidget::windowTitleChanged, m_windowTitleLabel.data(), &QLabel::setText); m_minimizeButton.reset(new StandardSystemButton(SystemButtonType::Minimize, q)); - m_minimizeButton->setToolTip(tr("Minimize")); connect(m_minimizeButton.data(), &StandardSystemButton::clicked, m_window, &QWidget::showMinimized); m_maximizeButton.reset(new StandardSystemButton(SystemButtonType::Maximize, q)); updateMaximizeButton(); @@ -186,8 +229,15 @@ void StandardTitleBarPrivate::initialize() } }); m_closeButton.reset(new StandardSystemButton(SystemButtonType::Close, q)); - m_closeButton->setToolTip(tr("Close")); connect(m_closeButton.data(), &StandardSystemButton::clicked, m_window, &QWidget::close); + m_labelLeftStretch = new QSpacerItem(0, 0); + m_labelRightStretch = new QSpacerItem(0, 0); + const auto titleLabelLayout = new QHBoxLayout; + titleLabelLayout->setSpacing(0); + titleLabelLayout->setContentsMargins(0, 0, 0, 0); + titleLabelLayout->addSpacerItem(m_labelLeftStretch); + titleLabelLayout->addWidget(m_windowTitleLabel.data()); + titleLabelLayout->addSpacerItem(m_labelRightStretch); // According to the title bar design guidance, the system buttons should always be // placed on the top-right corner of the window, so we need the following additional // layouts to ensure this. @@ -205,11 +255,12 @@ void StandardTitleBarPrivate::initialize() const auto titleBarLayout = new QHBoxLayout(q); titleBarLayout->setContentsMargins(0, 0, 0, 0); titleBarLayout->setSpacing(0); - titleBarLayout->addSpacerItem(new QSpacerItem(kDefaultTitleBarContentsMargin, kDefaultTitleBarContentsMargin)); - titleBarLayout->addWidget(m_windowTitleLabel.data()); - titleBarLayout->addStretch(); + titleBarLayout->addSpacerItem(new QSpacerItem(kDefaultTitleBarContentsMargin, 0, QSizePolicy::Fixed, QSizePolicy::Fixed)); + titleBarLayout->addLayout(titleLabelLayout); titleBarLayout->addLayout(systemButtonsOuterLayout); q->setLayout(titleBarLayout); + setTitleLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); + retranslateUi(); updateTitleBarStyleSheet(); connect(FramelessManager::instance(), &FramelessManager::systemThemeChanged, this, &StandardTitleBarPrivate::updateTitleBarStyleSheet); @@ -223,6 +274,24 @@ StandardTitleBar::StandardTitleBar(QWidget *parent) StandardTitleBar::~StandardTitleBar() = default; +Qt::Alignment StandardTitleBar::titleLabelAlignment() const +{ + Q_D(const StandardTitleBar); + return d->titleLabelAlignment(); +} + +void StandardTitleBar::setTitleLabelAlignment(const Qt::Alignment value) +{ + Q_D(StandardTitleBar); + d->setTitleLabelAlignment(value); +} + +QLabel *StandardTitleBar::titleLabel() const +{ + Q_D(const StandardTitleBar); + return d->m_windowTitleLabel.data(); +} + StandardSystemButton *StandardTitleBar::minimizeButton() const { Q_D(const StandardTitleBar);