diff --git a/CMakeLists.txt b/CMakeLists.txt index e66e8c5..faf6194 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ else() if(MACOS) list(APPEND SOURCES utilities_macos.mm) else() + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED) list(APPEND SOURCES utilities_linux.cpp) endif() endif() @@ -87,6 +88,14 @@ if(WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE dwmapi ) +else() + if(MACOS) + #TODO + else() + target_link_libraries(${PROJECT_NAME} PRIVATE + Qt${QT_VERSION_MAJOR}::X11Extras + ) + endif() endif() target_link_libraries(${PROJECT_NAME} PRIVATE diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d106427..db13084 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,6 +4,7 @@ find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets) if(TARGET Qt${QT_VERSION_MAJOR}::Widgets) add_subdirectory(widget) add_subdirectory(mainwindow) + add_subdirectory(minimal) endif() if(TARGET Qt${QT_VERSION_MAJOR}::Quick) diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt new file mode 100644 index 0000000..4e846d3 --- /dev/null +++ b/examples/minimal/CMakeLists.txt @@ -0,0 +1,39 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) + +set(SOURCES + ../images.qrc + main.cpp + flwindow.h + flwindow.cpp +) + +if(WIN32) + enable_language(RC) + list(APPEND SOURCES ../example.rc ../example.manifest) +endif() + +add_executable(minimal WIN32 ${SOURCES}) + +target_link_libraries(minimal PRIVATE + Qt${QT_VERSION_MAJOR}::Widgets + wangwenx190::FramelessHelper +) + +target_compile_definitions(minimal PRIVATE + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QT_NO_KEYWORDS + QT_DEPRECATED_WARNINGS + QT_DISABLE_DEPRECATED_BEFORE=0x060100 +) + +if(WIN32) + target_link_libraries(minimal PRIVATE dwmapi) +endif() diff --git a/examples/minimal/flwindow.cpp b/examples/minimal/flwindow.cpp new file mode 100644 index 0000000..0a998d4 --- /dev/null +++ b/examples/minimal/flwindow.cpp @@ -0,0 +1,92 @@ +#include "flwindow.h" +#include "../../framelesshelper.h" + +#include +#include +#include + +FRAMELESSHELPER_USE_NAMESPACE + +FLWindow::FLWindow(QWidget *parent) : QWidget(parent) +{ + setWindowFlags(Qt::FramelessWindowHint); + setupUi(); + + move(screen()->geometry().center() - frameGeometry().center()); +} + +FLWindow::~FLWindow() +{ + +} + +void FLWindow::initFramelessWindow() +{ + FramelessHelper* helper = new FramelessHelper(windowHandle()); + helper->setResizeBorderThickness(4); + helper->setTitleBarHeight(m_titleBarWidget->height()); + helper->setResizable(true); + helper->setHitTestVisible(m_minimizeButton); + helper->setHitTestVisible(m_maximizeButton); + helper->setHitTestVisible(m_closeButton); + helper->install(); +} + +void FLWindow::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + + static bool inited = false; + if (!inited) { + inited = true; + initFramelessWindow(); + } +} + +void FLWindow::setupUi() +{ + resize(800, 600); + + m_titleBarWidget = new QWidget(this); + m_titleBarWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_titleBarWidget->setFixedHeight(40); + m_titleBarWidget->setStyleSheet(QString::fromLatin1("background:grey")); + + m_minimizeButton = new QPushButton(m_titleBarWidget); + m_minimizeButton->setText(QStringLiteral("Min")); + m_minimizeButton->setObjectName(QStringLiteral("MinimizeButton")); + connect(m_minimizeButton, &QPushButton::clicked, this, &QWidget::showMinimized); + + m_maximizeButton = new QPushButton(m_titleBarWidget); + m_maximizeButton->setText(QStringLiteral("Max")); + m_maximizeButton->setObjectName(QStringLiteral("MaximizeButton")); + connect(m_maximizeButton, &QPushButton::clicked, this, [this](){ + if (isMaximized() || isFullScreen()) { + showNormal(); + } else { + showMaximized(); + } + }); + + m_closeButton = new QPushButton(m_titleBarWidget); + m_closeButton->setText(QStringLiteral("Close")); + m_closeButton->setObjectName(QStringLiteral("CloseButton")); + connect(m_closeButton, &QPushButton::clicked, this, &QWidget::close); + + const auto titleBarLayout = new QHBoxLayout(m_titleBarWidget); + titleBarLayout->setContentsMargins(0, 0, 0, 0); + titleBarLayout->setSpacing(10); + titleBarLayout->addStretch(); + titleBarLayout->addWidget(m_minimizeButton); + titleBarLayout->addWidget(m_maximizeButton); + titleBarLayout->addWidget(m_closeButton); + titleBarLayout->addStretch(); + m_titleBarWidget->setLayout(titleBarLayout); + + const auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + mainLayout->addWidget(m_titleBarWidget); + mainLayout->addStretch(); + setLayout(mainLayout); +} \ No newline at end of file diff --git a/examples/minimal/flwindow.h b/examples/minimal/flwindow.h new file mode 100644 index 0000000..e4507a8 --- /dev/null +++ b/examples/minimal/flwindow.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +class QPushButton; + +class FLWindow : public QWidget +{ + Q_OBJECT +public: + explicit FLWindow(QWidget *parent = nullptr); + ~FLWindow(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void initFramelessWindow(); + void setupUi(); + +private: + QWidget *m_titleBarWidget = nullptr; + QPushButton *m_minimizeButton = nullptr; + QPushButton *m_maximizeButton = nullptr; + QPushButton *m_closeButton = nullptr; +}; \ No newline at end of file diff --git a/examples/minimal/main.cpp b/examples/minimal/main.cpp new file mode 100644 index 0000000..dc6d197 --- /dev/null +++ b/examples/minimal/main.cpp @@ -0,0 +1,22 @@ +#include +#include "flwindow.h" + +int main(int argc, char *argv[]) +{ +#if 1 + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif +#endif +#endif + QApplication application(argc, argv); + + FLWindow win; + win.show(); + + return QApplication::exec(); +} diff --git a/examples/widget/widget.cpp b/examples/widget/widget.cpp index 4bca9da..9718982 100644 --- a/examples/widget/widget.cpp +++ b/examples/widget/widget.cpp @@ -31,6 +31,7 @@ #include #include "../../utilities.h" #include "../../framelesswindowsmanager.h" +#include "../../framelesshelper.h" FRAMELESSHELPER_USE_NAMESPACE diff --git a/framelesshelper.cpp b/framelesshelper.cpp index dca059b..99ab882 100644 --- a/framelesshelper.cpp +++ b/framelesshelper.cpp @@ -29,182 +29,460 @@ #include #include #include +#include #include "framelesswindowsmanager.h" #include "utilities.h" FRAMELESSHELPER_BEGIN_NAMESPACE -FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) {} - -void FramelessHelper::removeWindowFrame(QWindow *window) +FramelessHelper::FramelessHelper(QWindow *window) + : QObject(window) + , m_window(window) + , m_hoveredFrameSection(Qt::NoSection) + , m_clickedFrameSection(Qt::NoSection) { - Q_ASSERT(window); - if (!window) { - return; - } - window->setFlags(window->flags() | Qt::FramelessWindowHint); - window->installEventFilter(this); - window->setProperty(Constants::kFramelessModeFlag, true); + Q_ASSERT(window != nullptr && window->isTopLevel()); } -void FramelessHelper::bringBackWindowFrame(QWindow *window) +/*! + Setup the window, make it frameless. + */ +void FramelessHelper::install() { - Q_ASSERT(window); - if (!window) { + QRect origRect = m_window->geometry(); + m_origWindowFlags = m_window->flags(); + +#ifdef Q_OS_MAC + m_window->setFlags(Qt::Window); +#else + m_window->setFlags(m_origWindowFlags | Qt::FramelessWindowHint); +#endif + + m_window->setGeometry(origRect); + resizeWindow(origRect.size()); + + m_window->installEventFilter(this); +} + +/*! + Restore the window to its original state + */ +void FramelessHelper::uninstall() +{ + m_window->setFlags(m_origWindowFlags); + m_origWindowFlags = Qt::WindowFlags(); + resizeWindow(QSize()); + + m_window->removeEventFilter(this); +} + +/*! + Resize non-client area + */ +void FramelessHelper::resizeWindow(const QSize& windowSize) +{ + if (windowSize == this->windowSize()) return; + + setWindowSize(windowSize); +} + +QRect FramelessHelper::titleBarRect() +{ + return QRect(0, 0, windowSize().width(), titleBarHeight()); +} + + +QRegion FramelessHelper::titleBarRegion() +{ + QRegion region(titleBarRect()); + + for (const auto obj : m_HTVObjects) { + if (!obj || !(obj->isWidgetType() || obj->inherits("QQuickItem"))) { + continue; + } + + if (!obj->property("visible").toBool()) { + continue; + } + + region -= getHTVObjectRect(obj); } - window->removeEventFilter(this); - window->setFlags(window->flags() & ~Qt::FramelessWindowHint); - window->setProperty(Constants::kFramelessModeFlag, false); + + return region; +} + +QRect FramelessHelper::clientRect() +{ + QRect rect(0, 0, windowSize().width(), windowSize().height()); + rect = rect.adjusted( + resizeBorderThickness(), titleBarHeight(), + -resizeBorderThickness(), -resizeBorderThickness() + ); + return rect; +} + +QRegion FramelessHelper::nonClientRegion() +{ + QRegion region(QRect(QPoint(0, 0), windowSize())); + region -= clientRect(); + + for (const auto obj : m_HTVObjects) { + if (!obj || !(obj->isWidgetType() || obj->inherits("QQuickItem"))) { + continue; + } + + if (!obj->property("visible").toBool()) { + continue; + } + + region -= getHTVObjectRect(obj); + } + + return region; +} + +bool FramelessHelper::isInTitlebarArea(const QPoint& pos) +{ + return titleBarRegion().contains(pos); +} + +const int kCornerFactor = 2; + +/*! + \brief Determine window frame section by coordinates. + + Returns the window frame section at position \a pos, or \c Qt::NoSection + if there is no window frame section at this position. + + */ +Qt::WindowFrameSection FramelessHelper::mapPosToFrameSection(const QPoint& pos) +{ + int border = 0; + + // TODO: get system default resize border + const int sysBorder = Utilities::getSystemMetric(m_window, SystemMetric::ResizeBorderThickness, false); + + Qt::WindowStates states = m_window->windowState(); + // Resizing is disabled when WindowMaximized or WindowFullScreen + if (!(states & Qt::WindowMaximized) && !(states & Qt::WindowFullScreen)) + { + border = resizeBorderThickness(); + border = qMin(border, sysBorder); + } + + QRect windowRect(0, 0, windowSize().width(), windowSize().height()); + + if (windowRect.contains(pos)) + { + QPoint mappedPos = pos - windowRect.topLeft(); + + // The corner is kCornerFactor times the size of the border + if (QRect(0, 0, border * kCornerFactor, border * kCornerFactor).contains(mappedPos)) + return Qt::TopLeftSection; + + if (QRect(border * kCornerFactor, 0, windowRect.width() - border * 2 * kCornerFactor, border).contains(mappedPos)) + return Qt::TopSection; + + if (QRect(windowRect.width() - border * kCornerFactor, 0, border * kCornerFactor, border * kCornerFactor).contains(mappedPos)) + return Qt::TopRightSection; + + if (QRect(windowRect.width() - border, border * kCornerFactor, border, windowRect.height() - border * 2 * kCornerFactor).contains(mappedPos)) + return Qt::RightSection; + + if (QRect(windowRect.width() - border * kCornerFactor, windowRect.height() - border * kCornerFactor, border * kCornerFactor, border * kCornerFactor).contains(mappedPos)) + return Qt::BottomRightSection; + + if (QRect(border * kCornerFactor, windowRect.height() - border, windowRect.width() - border * 2 * kCornerFactor, border).contains(mappedPos)) + return Qt::BottomSection; + + if (QRect(0, windowRect.height() - border * kCornerFactor, border * kCornerFactor, border * kCornerFactor).contains(mappedPos)) + return Qt::BottomLeftSection; + + if (QRect(0, border * kCornerFactor, border, windowRect.height() - border * 2 * kCornerFactor).contains(mappedPos)) + return Qt::LeftSection; + + // Determining window frame secion is the highest priority, + // so the determination of the title bar area can be simpler. + if (isInTitlebarArea(pos)) + return Qt::TitleBarArea; + } + + return Qt::NoSection; +} + +bool FramelessHelper::isHoverResizeHandler() +{ + return m_hoveredFrameSection == Qt::LeftSection || + m_hoveredFrameSection == Qt::RightSection || + m_hoveredFrameSection == Qt::TopSection || + m_hoveredFrameSection == Qt::BottomSection || + m_hoveredFrameSection == Qt::TopLeftSection || + m_hoveredFrameSection == Qt::TopRightSection || + m_hoveredFrameSection == Qt::BottomLeftSection || + m_hoveredFrameSection == Qt::BottomRightSection; +} + +bool FramelessHelper::isClickResizeHandler() +{ + return m_clickedFrameSection == Qt::LeftSection || + m_clickedFrameSection == Qt::RightSection || + m_clickedFrameSection == Qt::TopSection || + m_clickedFrameSection == Qt::BottomSection || + m_clickedFrameSection == Qt::TopLeftSection || + m_clickedFrameSection == Qt::TopRightSection || + m_clickedFrameSection == Qt::BottomLeftSection || + m_clickedFrameSection == Qt::BottomRightSection; +} + +QCursor FramelessHelper::cursorForFrameSection(Qt::WindowFrameSection frameSection) +{ + Qt::CursorShape cursor = Qt::ArrowCursor; + + switch (frameSection) + { + case Qt::LeftSection: + case Qt::RightSection: + cursor = Qt::SizeHorCursor; + break; + case Qt::BottomSection: + case Qt::TopSection: + cursor = Qt::SizeVerCursor; + break; + case Qt::TopLeftSection: + case Qt::BottomRightSection: + cursor = Qt::SizeFDiagCursor; + break; + case Qt::TopRightSection: + case Qt::BottomLeftSection: + cursor = Qt::SizeBDiagCursor; + break; + case Qt::TitleBarArea: + cursor = Qt::ArrowCursor; + break; + default: + break; + } + + return QCursor(cursor); +} + +void FramelessHelper::setCursor(const QCursor& cursor) +{ + m_window->setCursor(cursor); + m_cursorChanged = true; +} + +void FramelessHelper::unsetCursor() +{ + if (!m_cursorChanged) + return; + + m_window->unsetCursor(); + m_cursorChanged = false; +} + +void FramelessHelper::updateCursor() +{ +#ifdef Q_OS_LINUX + if (isHoverResizeHandler()) { + Utilities::setX11CursorShape(m_window, + Utilities::getX11CursorForFrameSection(m_hoveredFrameSection)); + m_cursorChanged = true; + } else { + if (!m_cursorChanged) + return; + Utilities::resetX1CursorShape(m_window); + m_cursorChanged = false; + } +#else + if (isHoverResizeHandler()) { + setCursor(cursorForFrameSection(m_hoveredFrameSection)); + } else { + unsetCursor(); + } +#endif +} + +void FramelessHelper::updateMouse(const QPoint& pos) +{ + updateHoverStates(pos); + updateCursor(); +} + +void FramelessHelper::updateHoverStates(const QPoint& pos) +{ + m_hoveredFrameSection = mapPosToFrameSection(pos); +} + +void FramelessHelper::startMove(const QPoint &globalPos) +{ +#ifdef Q_OS_LINUX + // On HiDPI screen, X11 ButtonRelease is likely to trigger + // a QEvent::MouseMove, so we reset m_clickedFrameSection in advance. + m_clickedFrameSection = Qt::NoSection; + Utilities::sendX11ButtonReleaseEvent(m_window, globalPos); + Utilities::startX11Moving(m_window, globalPos); +#endif +} + +void FramelessHelper::startResize(const QPoint &globalPos, Qt::WindowFrameSection frameSection) +{ +#ifdef Q_OS_LINUX + // On HiDPI screen, X11 ButtonRelease is likely to trigger + // a QEvent::MouseMove, so we reset m_clickedFrameSection in advance. + m_clickedFrameSection = Qt::NoSection; + Utilities::sendX11ButtonReleaseEvent(m_window, globalPos); + Utilities::startX11Resizing(m_window, globalPos, frameSection); +#endif +} + +void FramelessHelper::setHitTestVisible(QObject *obj) +{ + m_HTVObjects.push_back(obj); +} + +bool FramelessHelper::isHitTestVisible(QObject *obj) +{ + return m_HTVObjects.contains(obj); +} + +QRect FramelessHelper::getHTVObjectRect(QObject *obj) +{ + const QPoint originPoint = m_window->mapFromGlobal( + Utilities::mapOriginPointToWindow(obj).toPoint()); + const int width = obj->property("width").toInt(); + const int height = obj->property("height").toInt(); + + return QRect(originPoint, QSize(width, height)); } bool FramelessHelper::eventFilter(QObject *object, QEvent *event) { - Q_ASSERT(object); - Q_ASSERT(event); - if (!object || !event) { - return false; - } - // Only monitor window events. - if (!object->isWindowType()) { - return false; - } - const QEvent::Type type = event->type(); - // We are only interested in mouse events. - if ((type != QEvent::MouseButtonDblClick) && (type != QEvent::MouseButtonPress) - && (type != QEvent::MouseMove)) { - return false; - } - const auto window = qobject_cast(object); - const int resizeBorderThickness = FramelessWindowsManager::getResizeBorderThickness(window); - const int titleBarHeight = FramelessWindowsManager::getTitleBarHeight(window); - const bool resizable = FramelessWindowsManager::getResizable(window); - const int windowWidth = window->width(); - const auto mouseEvent = static_cast(event); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - const QPoint localMousePosition = mouseEvent->position().toPoint(); -#else - const QPoint localMousePosition = mouseEvent->windowPos().toPoint(); -#endif - const Qt::Edges edges = [window, resizeBorderThickness, windowWidth, &localMousePosition] { - const int windowHeight = window->height(); - if (localMousePosition.y() <= resizeBorderThickness) { - if (localMousePosition.x() <= resizeBorderThickness) { - return Qt::TopEdge | Qt::LeftEdge; - } - if (localMousePosition.x() >= (windowWidth - resizeBorderThickness)) { - return Qt::TopEdge | Qt::RightEdge; - } - return Qt::Edges{Qt::TopEdge}; - } - if (localMousePosition.y() >= (windowHeight - resizeBorderThickness)) { - if (localMousePosition.x() <= resizeBorderThickness) { - return Qt::BottomEdge | Qt::LeftEdge; - } - if (localMousePosition.x() >= (windowWidth - resizeBorderThickness)) { - return Qt::BottomEdge | Qt::RightEdge; - } - return Qt::Edges{Qt::BottomEdge}; - } - if (localMousePosition.x() <= resizeBorderThickness) { - return Qt::Edges{Qt::LeftEdge}; - } - if (localMousePosition.x() >= (windowWidth - resizeBorderThickness)) { - return Qt::Edges{Qt::RightEdge}; - } - return Qt::Edges{}; - } (); - const bool hitTestVisible = Utilities::isHitTestVisibleInChrome(window); - bool isInTitlebarArea = false; - if ((window->windowState() == Qt::WindowMaximized) - || (window->windowState() == Qt::WindowFullScreen)) { - isInTitlebarArea = (localMousePosition.y() >= 0) - && (localMousePosition.y() <= titleBarHeight) - && (localMousePosition.x() >= 0) - && (localMousePosition.x() <= windowWidth) - && !hitTestVisible; - } - if (window->windowState() == Qt::WindowNoState) { - isInTitlebarArea = (localMousePosition.y() > resizeBorderThickness) - && (localMousePosition.y() <= titleBarHeight) - && (localMousePosition.x() > resizeBorderThickness) - && (localMousePosition.x() < (windowWidth - resizeBorderThickness)) - && !hitTestVisible; - } + bool filterOut = false; - // Determine if the mouse click occurred in the title bar - static bool titlebarClicked = false; - if (type == QEvent::MouseButtonPress) { - if (isInTitlebarArea) - titlebarClicked = true; - else - titlebarClicked = false; - } - - if (type == QEvent::MouseButtonDblClick) { - if (mouseEvent->button() != Qt::MouseButton::LeftButton) { - return false; - } - if (isInTitlebarArea) { - if (window->windowState() == Qt::WindowState::WindowFullScreen) { - return false; - } - if (window->windowState() == Qt::WindowState::WindowMaximized) { - window->showNormal(); - } else { - window->showMaximized(); - } - window->setCursor(Qt::ArrowCursor); - } - } else if (type == QEvent::MouseMove) { - // Display resize indicators - static bool cursorChanged = false; - if ((window->windowState() == Qt::WindowState::WindowNoState) && resizable) { - if (((edges & Qt::TopEdge) && (edges & Qt::LeftEdge)) - || ((edges & Qt::BottomEdge) && (edges & Qt::RightEdge))) { - window->setCursor(Qt::SizeFDiagCursor); - cursorChanged = true; - } else if (((edges & Qt::TopEdge) && (edges & Qt::RightEdge)) - || ((edges & Qt::BottomEdge) && (edges & Qt::LeftEdge))) { - window->setCursor(Qt::SizeBDiagCursor); - cursorChanged = true; - } else if ((edges & Qt::TopEdge) || (edges & Qt::BottomEdge)) { - window->setCursor(Qt::SizeVerCursor); - cursorChanged = true; - } else if ((edges & Qt::LeftEdge) || (edges & Qt::RightEdge)) { - window->setCursor(Qt::SizeHorCursor); - cursorChanged = true; - } else { - if (cursorChanged) { - window->setCursor(Qt::ArrowCursor); - cursorChanged = false; - } - } + if (object == m_window) { + switch (event->type()) + { + case QEvent::Resize: + { + QResizeEvent* re = static_cast(event); + resizeWindow(re->size()); + break; } - if ((mouseEvent->buttons() & Qt::LeftButton) && titlebarClicked) { - if (edges == Qt::Edges{}) { - if (isInTitlebarArea) { - if (!window->startSystemMove()) { - // ### FIXME: TO BE IMPLEMENTED! - qWarning() << "Current OS doesn't support QWindow::startSystemMove()."; - } - } + case QEvent::NonClientAreaMouseMove: + case QEvent::MouseMove: + { + auto ev = static_cast(event); + updateMouse(ev->pos()); + + if (m_clickedFrameSection == Qt::TitleBarArea + && isInTitlebarArea(ev->pos())) { + // Start system move + startMove(ev->globalPos()); + ev->accept(); + filterOut = true; + } else if (isClickResizeHandler() && isHoverResizeHandler()) { + // Start system resize + startResize(ev->globalPos(), m_hoveredFrameSection); + ev->accept(); + filterOut = true; } + + // This case takes into account that the mouse moves outside the window boundary + QRect windowRect(0, 0, windowSize().width(), windowSize().height()); + if (isClickResizeHandler() && !windowRect.contains(ev->pos())) { + startResize(ev->globalPos(), m_clickedFrameSection); + ev->accept(); + filterOut = true; + } + + break; + } + case QEvent::Leave: + { + updateMouse(m_window->mapFromGlobal(QCursor::pos())); + break; + } + case QEvent::NonClientAreaMouseButtonPress: + case QEvent::MouseButtonPress: + { + auto ev = static_cast(event); + + if (ev->button() == Qt::LeftButton) + m_clickedFrameSection = m_hoveredFrameSection; + + break; } - } else if (type == QEvent::MouseButtonPress) { - if (edges != Qt::Edges{}) { - if ((window->windowState() == Qt::WindowState::WindowNoState) && !hitTestVisible && resizable) { - if (!window->startSystemResize(edges)) { - // ### FIXME: TO BE IMPLEMENTED! - qWarning() << "Current OS doesn't support QWindow::startSystemResize()."; - } + case QEvent::NonClientAreaMouseButtonRelease: + case QEvent::MouseButtonRelease: + { + m_clickedFrameSection = Qt::NoSection; + break; + } + + case QEvent::NonClientAreaMouseButtonDblClick: + case QEvent::MouseButtonDblClick: + { + auto ev = static_cast(event); + if (isHoverResizeHandler() && ev->button() == Qt::LeftButton) { + // double click resize handler + handleResizeHandlerDblClicked(); + } else if (isInTitlebarArea(ev->pos()) && ev->button() == Qt::LeftButton) { + Qt::WindowStates states = m_window->windowState(); + if (states & Qt::WindowMaximized) + m_window->showNormal(); + else + m_window->showMaximized(); } + + break; + } + + default: + break; } } - return false; + return filterOut; +} + +void FramelessHelper::handleResizeHandlerDblClicked() +{ + QRect screenRect = m_window->screen()->availableGeometry(); + QRect winRect = m_window->geometry(); + + switch (m_clickedFrameSection) + { + case Qt::TopSection: + m_window->setGeometry(winRect.x(), 0, winRect.width(), winRect.height() + winRect.y()); + break; + case Qt::BottomSection: + m_window->setGeometry(winRect.x(), winRect.y(), winRect.width(), screenRect.height() - winRect.y()); + break; + case Qt::LeftSection: + m_window->setGeometry(0, winRect.y(), winRect.x() + winRect.width(), winRect.height()); + break; + case Qt::RightSection: + m_window->setGeometry(winRect.x(), winRect.y(), screenRect.width() - winRect.x(), winRect.height()); + break; + case Qt::TopLeftSection: + m_window->setGeometry(0, 0, winRect.x() + winRect.width(), winRect.y() + winRect.height()); + break; + case Qt::TopRightSection: + m_window->setGeometry(winRect.x(), 0, screenRect.width() - winRect.x(), winRect.y() + winRect.height()); + break; + case Qt::BottomLeftSection: + m_window->setGeometry(0, winRect.y(), winRect.x() + winRect.width(), screenRect.height() - winRect.y()); + break; + case Qt::BottomRightSection: + m_window->setGeometry(winRect.x(), winRect.y(), screenRect.width() - winRect.x(), screenRect.height() - winRect.y()); + break; + default: + break; + } } FRAMELESSHELPER_END_NAMESPACE diff --git a/framelesshelper.h b/framelesshelper.h index 5b7dd86..81ff7af 100644 --- a/framelesshelper.h +++ b/framelesshelper.h @@ -29,9 +29,11 @@ #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) #include +#include QT_BEGIN_NAMESPACE QT_FORWARD_DECLARE_CLASS(QWindow) +QT_FORWARD_DECLARE_CLASS(QMouseEvent) QT_END_NAMESPACE FRAMELESSHELPER_BEGIN_NAMESPACE @@ -42,14 +44,68 @@ class FRAMELESSHELPER_API FramelessHelper : public QObject Q_DISABLE_COPY_MOVE(FramelessHelper) public: - explicit FramelessHelper(QObject *parent = nullptr); + explicit FramelessHelper(QWindow *window); ~FramelessHelper() override = default; - void removeWindowFrame(QWindow *window); - void bringBackWindowFrame(QWindow *window); + void install(); + void uninstall(); + + QWindow *window() { return m_window; } + + QSize windowSize() { return m_windowSize; } + void setWindowSize(const QSize& size) { m_windowSize = size; } + void resizeWindow(const QSize& windowSize); + + int titleBarHeight() { return m_titleBarHeight; } + int setTitleBarHeight(int height) { m_titleBarHeight = height; } + QRect titleBarRect(); + QRegion titleBarRegion(); + + int resizeBorderThickness() { return m_resizeBorderThickness; } + void setResizeBorderThickness(int thickness) { m_resizeBorderThickness = thickness; } + + bool resizable() { return m_resizable; } + void setResizable(bool resizable) { m_resizable = resizable; } + + QRect clientRect(); + QRegion nonClientRegion(); + + bool isInTitlebarArea(const QPoint& pos); + Qt::WindowFrameSection mapPosToFrameSection(const QPoint& pos); + + bool isHoverResizeHandler(); + bool isClickResizeHandler(); + + QCursor cursorForFrameSection(Qt::WindowFrameSection frameSection); + void setCursor(const QCursor& cursor); + void unsetCursor(); + void updateCursor(); + + void updateMouse(const QPoint& pos); + void updateHoverStates(const QPoint& pos); + + void startMove(const QPoint &globalPos); + void startResize(const QPoint &globalPos, Qt::WindowFrameSection frameSection); + + void setHitTestVisible(QObject *obj); + bool isHitTestVisible(QObject *obj); + QRect getHTVObjectRect(QObject *obj); protected: bool eventFilter(QObject *object, QEvent *event) override; + void handleResizeHandlerDblClicked(); + +private: + QWindow *m_window; + QSize m_windowSize; + int m_titleBarHeight; + int m_resizeBorderThickness; + bool m_resizable; + Qt::WindowFlags m_origWindowFlags; + bool m_cursorChanged; + Qt::WindowFrameSection m_hoveredFrameSection; + Qt::WindowFrameSection m_clickedFrameSection; + QList m_HTVObjects; }; FRAMELESSHELPER_END_NAMESPACE diff --git a/framelesswindowsmanager.cpp b/framelesswindowsmanager.cpp index 093e8f7..c2874b2 100644 --- a/framelesswindowsmanager.cpp +++ b/framelesswindowsmanager.cpp @@ -38,7 +38,7 @@ FRAMELESSHELPER_BEGIN_NAMESPACE #ifdef FRAMELESSHELPER_USE_UNIX_VERSION -Q_GLOBAL_STATIC(FramelessHelper, framelessHelperUnix) +//Q_GLOBAL_STATIC(FramelessHelper, framelessHelperUnix) #endif void FramelessWindowsManager::addWindow(QWindow *window) @@ -51,7 +51,7 @@ void FramelessWindowsManager::addWindow(QWindow *window) QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); } #ifdef FRAMELESSHELPER_USE_UNIX_VERSION - framelessHelperUnix()->removeWindowFrame(window); + //framelessHelperUnix()->removeWindowFrame(window); #else FramelessHelperWin::addFramelessWindow(window); // Work-around a Win32 multi-monitor bug. @@ -165,7 +165,7 @@ void FramelessWindowsManager::removeWindow(QWindow *window) return; } #ifdef FRAMELESSHELPER_USE_UNIX_VERSION - framelessHelperUnix()->bringBackWindowFrame(window); + //framelessHelperUnix()->bringBackWindowFrame(window); #else FramelessHelperWin::removeFramelessWindow(window); #endif diff --git a/utilities.h b/utilities.h index f7be5c2..222f0aa 100644 --- a/utilities.h +++ b/utilities.h @@ -58,6 +58,16 @@ FRAMELESSHELPER_API void updateQtFrameMargins(QWindow *window, const bool enable [[nodiscard]] FRAMELESSHELPER_API QString getSystemErrorMessage(const QString &function); #endif +#ifdef Q_OS_LINUX +FRAMELESSHELPER_API void sendX11ButtonReleaseEvent(QWindow *w, const QPoint &globalPos); +FRAMELESSHELPER_API void sendX11MoveResizeEvent(QWindow *w, const QPoint &globalPos, int section); +FRAMELESSHELPER_API void startX11Moving(QWindow *w, const QPoint &globalPos); +FRAMELESSHELPER_API void startX11Resizing(QWindow *w, const QPoint &globalPos, Qt::WindowFrameSection frameSection); +FRAMELESSHELPER_API void setX11CursorShape(QWindow *w, int cursorId); +FRAMELESSHELPER_API void resetX1CursorShape(QWindow *w); +FRAMELESSHELPER_API unsigned int getX11CursorForFrameSection(Qt::WindowFrameSection frameSection); +#endif + } FRAMELESSHELPER_END_NAMESPACE diff --git a/utilities_linux.cpp b/utilities_linux.cpp index 5aad9f7..41216c0 100644 --- a/utilities_linux.cpp +++ b/utilities_linux.cpp @@ -25,9 +25,26 @@ #include "utilities.h" #include +#include +#include +#include +#include FRAMELESSHELPER_BEGIN_NAMESPACE +#define _NET_WM_MOVERESIZE_SIZE_TOPLEFT 0 +#define _NET_WM_MOVERESIZE_SIZE_TOP 1 +#define _NET_WM_MOVERESIZE_SIZE_TOPRIGHT 2 +#define _NET_WM_MOVERESIZE_SIZE_RIGHT 3 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT 4 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOM 5 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT 6 +#define _NET_WM_MOVERESIZE_SIZE_LEFT 7 +#define _NET_WM_MOVERESIZE_MOVE 8 /* movement only */ +#define _NET_WM_MOVERESIZE_SIZE_KEYBOARD 9 /* size via keyboard */ +#define _NET_WM_MOVERESIZE_MOVE_KEYBOARD 10 /* move via keyboard */ +#define _NET_WM_MOVERESIZE_CANCEL 11 /* cancel operation */ + static constexpr int kDefaultResizeBorderThickness = 8; static constexpr int kDefaultCaptionHeight = 23; @@ -106,7 +123,8 @@ bool Utilities::shouldAppsUseDarkMode() ColorizationArea Utilities::getColorizationArea() { // ### TO BE IMPLEMENTED - return ColorizationArea::None; + //return ColorizationArea::None; // ‘None’ has been defined as a macro in X11 headers. + return ColorizationArea::All; } bool Utilities::isThemeChanged(const void *data) @@ -132,4 +150,178 @@ bool Utilities::showSystemMenu(const WId winId, const QPointF &pos) return false; } -FRAMELESSHELPER_END_NAMESPACE \ No newline at end of file +void Utilities::sendX11ButtonReleaseEvent(QWindow *w, const QPoint &globalPos) +{ + const QPoint pos = w->mapFromGlobal(globalPos); + const auto display = QX11Info::display(); + const auto screen = QX11Info::appScreen(); + + XEvent xevent; + memset(&xevent, 0, sizeof(XEvent)); + + xevent.type = ButtonRelease; + xevent.xbutton.time = CurrentTime; + xevent.xbutton.button = 0; + xevent.xbutton.same_screen = True; + xevent.xbutton.send_event = True; + xevent.xbutton.window = w->winId(); + xevent.xbutton.root = QX11Info::appRootWindow(screen); + xevent.xbutton.x = pos.x() * w->screen()->devicePixelRatio(); + xevent.xbutton.y = pos.y() * w->screen()->devicePixelRatio(); + xevent.xbutton.x_root = globalPos.x() * w->screen()->devicePixelRatio(); + xevent.xbutton.y_root = globalPos.y() * w->screen()->devicePixelRatio(); + xevent.xbutton.display = display; + + if (XSendEvent(display, w->winId(), True, ButtonReleaseMask, &xevent) == 0) + qWarning() << "Failed to send ButtonRelease event."; + XFlush(display); +} + +void Utilities::sendX11MoveResizeEvent(QWindow *w, const QPoint &globalPos, int section) +{ + const auto display = QX11Info::display(); + const auto winId = w->winId(); + const auto screen = QX11Info::appScreen(); + + XUngrabPointer(display, CurrentTime); + + XEvent xev; + memset(&xev, 0x00, sizeof(xev)); + const Atom netMoveResize = XInternAtom(display, "_NET_WM_MOVERESIZE", False); + xev.xclient.type = ClientMessage; + xev.xclient.message_type = netMoveResize; + xev.xclient.serial = 0; + xev.xclient.display = display; + xev.xclient.send_event = True; + xev.xclient.window = winId; + xev.xclient.format = 32; + + xev.xclient.data.l[0] = globalPos.x() * w->screen()->devicePixelRatio(); + xev.xclient.data.l[1] = globalPos.y() * w->screen()->devicePixelRatio(); + xev.xclient.data.l[2] = section; + xev.xclient.data.l[3] = Button1; + xev.xclient.data.l[4] = 0; + + if(XSendEvent(display, QX11Info::appRootWindow(screen), + False, SubstructureRedirectMask | SubstructureNotifyMask, &xev) == 0) + qWarning("Failed to send Move or Resize event."); + XFlush(display); +} + +void Utilities::startX11Moving(QWindow *w, const QPoint &pos) +{ + sendX11MoveResizeEvent(w, pos, _NET_WM_MOVERESIZE_MOVE); +} + +void Utilities::startX11Resizing(QWindow *w, const QPoint &pos, Qt::WindowFrameSection frameSection) +{ + int section = -1; + + switch (frameSection) + { + case Qt::LeftSection: + section = _NET_WM_MOVERESIZE_SIZE_LEFT; + break; + case Qt::TopLeftSection: + section = _NET_WM_MOVERESIZE_SIZE_TOPLEFT; + break; + case Qt::TopSection: + section = _NET_WM_MOVERESIZE_SIZE_TOP; + break; + case Qt::TopRightSection: + section = _NET_WM_MOVERESIZE_SIZE_TOPRIGHT; + break; + case Qt::RightSection: + section = _NET_WM_MOVERESIZE_SIZE_RIGHT; + break; + case Qt::BottomRightSection: + section = _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT; + break; + case Qt::BottomSection: + section = _NET_WM_MOVERESIZE_SIZE_BOTTOM; + break; + case Qt::BottomLeftSection: + section = _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT; + break; + default: + break; + } + + if (section != -1) + sendX11MoveResizeEvent(w, pos, section); +} + +enum class X11CursorType +{ + kArrow = 2, + kTop = 138, + kTopRight = 136, + kRight = 96, + kBottomRight = 14, + kBottom = 16, + kBottomLeft = 12, + kLeft = 70, + kTopLeft = 134, +}; + +void Utilities::setX11CursorShape(QWindow *w, int cursorId) +{ + const auto display = QX11Info::display(); + const WId window_id = w->winId(); + const Cursor cursor = XCreateFontCursor(display, cursorId); + if (!cursor) { + qWarning() << "Failed to set cursor."; + } + XDefineCursor(display, window_id, cursor); + XFlush(display); +} + +void Utilities::resetX1CursorShape(QWindow *w) +{ + const auto display = QX11Info::display(); + const WId window_id = w->winId(); + XUndefineCursor(display, window_id); + XFlush(display); +} + +unsigned int Utilities::getX11CursorForFrameSection(Qt::WindowFrameSection frameSection) +{ + X11CursorType cursor = X11CursorType::kArrow; + + switch (frameSection) + { + case Qt::LeftSection: + cursor = X11CursorType::kLeft; + break; + case Qt::RightSection: + cursor = X11CursorType::kRight; + break; + case Qt::BottomSection: + cursor = X11CursorType::kBottom; + break; + case Qt::TopSection: + cursor = X11CursorType::kTop; + break; + case Qt::TopLeftSection: + cursor = X11CursorType::kTopLeft; + break; + case Qt::BottomRightSection: + cursor = X11CursorType::kBottomRight; + break; + case Qt::TopRightSection: + cursor = X11CursorType::kTopRight; + break; + case Qt::BottomLeftSection: + cursor = X11CursorType::kBottomLeft; + break; + case Qt::TitleBarArea: + cursor = X11CursorType::kArrow; + break; + default: + break; + } + + return (unsigned int)cursor; +} + +FRAMELESSHELPER_END_NAMESPACE