Merge pull request #84 from altairwei/2.0-linux

framelesshelper v2.0 Core API Linux 端的实现
This commit is contained in:
Altair Wei 2021-10-02 11:56:12 +08:00 committed by GitHub
commit 6a0dc5052c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 885 additions and 159 deletions

View File

@ -50,6 +50,7 @@ else()
if(MACOS) if(MACOS)
list(APPEND SOURCES utilities_macos.mm) list(APPEND SOURCES utilities_macos.mm)
else() else()
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED)
list(APPEND SOURCES utilities_linux.cpp) list(APPEND SOURCES utilities_linux.cpp)
endif() endif()
endif() endif()
@ -87,6 +88,14 @@ if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE
dwmapi dwmapi
) )
else()
if(MACOS)
#TODO
else()
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt${QT_VERSION_MAJOR}::X11Extras
)
endif()
endif() endif()
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE

View File

@ -4,6 +4,7 @@ find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets)
if(TARGET Qt${QT_VERSION_MAJOR}::Widgets) if(TARGET Qt${QT_VERSION_MAJOR}::Widgets)
add_subdirectory(widget) add_subdirectory(widget)
add_subdirectory(mainwindow) add_subdirectory(mainwindow)
add_subdirectory(minimal)
endif() endif()
if(TARGET Qt${QT_VERSION_MAJOR}::Quick) if(TARGET Qt${QT_VERSION_MAJOR}::Quick)

View File

@ -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()

View File

@ -0,0 +1,92 @@
#include "flwindow.h"
#include "../../framelesshelper.h"
#include <QScreen>
#include <QVBoxLayout>
#include <QPushButton>
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);
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <QWidget>
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;
};

22
examples/minimal/main.cpp Normal file
View File

@ -0,0 +1,22 @@
#include <QtWidgets/qapplication.h>
#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();
}

View File

@ -31,6 +31,7 @@
#include <QtWidgets/qpushbutton.h> #include <QtWidgets/qpushbutton.h>
#include "../../utilities.h" #include "../../utilities.h"
#include "../../framelesswindowsmanager.h" #include "../../framelesswindowsmanager.h"
#include "../../framelesshelper.h"
FRAMELESSHELPER_USE_NAMESPACE FRAMELESSHELPER_USE_NAMESPACE

View File

@ -29,182 +29,460 @@
#include <QtCore/qdebug.h> #include <QtCore/qdebug.h>
#include <QtGui/qevent.h> #include <QtGui/qevent.h>
#include <QtGui/qwindow.h> #include <QtGui/qwindow.h>
#include <QtGui/qscreen.h>
#include "framelesswindowsmanager.h" #include "framelesswindowsmanager.h"
#include "utilities.h" #include "utilities.h"
FRAMELESSHELPER_BEGIN_NAMESPACE FRAMELESSHELPER_BEGIN_NAMESPACE
FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) {} FramelessHelper::FramelessHelper(QWindow *window)
: QObject(window)
void FramelessHelper::removeWindowFrame(QWindow *window) , m_window(window)
, m_hoveredFrameSection(Qt::NoSection)
, m_clickedFrameSection(Qt::NoSection)
{ {
Q_ASSERT(window); Q_ASSERT(window != nullptr && window->isTopLevel());
if (!window) {
return;
}
window->setFlags(window->flags() | Qt::FramelessWindowHint);
window->installEventFilter(this);
window->setProperty(Constants::kFramelessModeFlag, true);
} }
void FramelessHelper::bringBackWindowFrame(QWindow *window) /*!
Setup the window, make it frameless.
*/
void FramelessHelper::install()
{ {
Q_ASSERT(window); QRect origRect = m_window->geometry();
if (!window) { 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; 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); return region;
window->setProperty(Constants::kFramelessModeFlag, false); }
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) bool FramelessHelper::eventFilter(QObject *object, QEvent *event)
{ {
Q_ASSERT(object); bool filterOut = false;
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<QWindow *>(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<QMouseEvent *>(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;
}
// Determine if the mouse click occurred in the title bar if (object == m_window) {
static bool titlebarClicked = false; switch (event->type())
if (type == QEvent::MouseButtonPress) { {
if (isInTitlebarArea) case QEvent::Resize:
titlebarClicked = true; {
else QResizeEvent* re = static_cast<QResizeEvent *>(event);
titlebarClicked = false; resizeWindow(re->size());
} break;
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 ((mouseEvent->buttons() & Qt::LeftButton) && titlebarClicked) { case QEvent::NonClientAreaMouseMove:
if (edges == Qt::Edges{}) { case QEvent::MouseMove:
if (isInTitlebarArea) { {
if (!window->startSystemMove()) { auto ev = static_cast<QMouseEvent *>(event);
// ### FIXME: TO BE IMPLEMENTED! updateMouse(ev->pos());
qWarning() << "Current OS doesn't support QWindow::startSystemMove().";
} 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<QMouseEvent *>(event);
if (ev->button() == Qt::LeftButton)
m_clickedFrameSection = m_hoveredFrameSection;
break;
} }
} else if (type == QEvent::MouseButtonPress) { case QEvent::NonClientAreaMouseButtonRelease:
if (edges != Qt::Edges{}) { case QEvent::MouseButtonRelease:
if ((window->windowState() == Qt::WindowState::WindowNoState) && !hitTestVisible && resizable) { {
if (!window->startSystemResize(edges)) { m_clickedFrameSection = Qt::NoSection;
// ### FIXME: TO BE IMPLEMENTED! break;
qWarning() << "Current OS doesn't support QWindow::startSystemResize()."; }
}
case QEvent::NonClientAreaMouseButtonDblClick:
case QEvent::MouseButtonDblClick:
{
auto ev = static_cast<QMouseEvent *>(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 FRAMELESSHELPER_END_NAMESPACE

View File

@ -29,9 +29,11 @@
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
#include <QtCore/qobject.h> #include <QtCore/qobject.h>
#include <QtCore/qsize.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
QT_FORWARD_DECLARE_CLASS(QWindow) QT_FORWARD_DECLARE_CLASS(QWindow)
QT_FORWARD_DECLARE_CLASS(QMouseEvent)
QT_END_NAMESPACE QT_END_NAMESPACE
FRAMELESSHELPER_BEGIN_NAMESPACE FRAMELESSHELPER_BEGIN_NAMESPACE
@ -42,14 +44,68 @@ class FRAMELESSHELPER_API FramelessHelper : public QObject
Q_DISABLE_COPY_MOVE(FramelessHelper) Q_DISABLE_COPY_MOVE(FramelessHelper)
public: public:
explicit FramelessHelper(QObject *parent = nullptr); explicit FramelessHelper(QWindow *window);
~FramelessHelper() override = default; ~FramelessHelper() override = default;
void removeWindowFrame(QWindow *window); void install();
void bringBackWindowFrame(QWindow *window); 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: protected:
bool eventFilter(QObject *object, QEvent *event) override; 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<QObject*> m_HTVObjects;
}; };
FRAMELESSHELPER_END_NAMESPACE FRAMELESSHELPER_END_NAMESPACE

View File

@ -38,7 +38,7 @@
FRAMELESSHELPER_BEGIN_NAMESPACE FRAMELESSHELPER_BEGIN_NAMESPACE
#ifdef FRAMELESSHELPER_USE_UNIX_VERSION #ifdef FRAMELESSHELPER_USE_UNIX_VERSION
Q_GLOBAL_STATIC(FramelessHelper, framelessHelperUnix) //Q_GLOBAL_STATIC(FramelessHelper, framelessHelperUnix)
#endif #endif
void FramelessWindowsManager::addWindow(QWindow *window) void FramelessWindowsManager::addWindow(QWindow *window)
@ -51,7 +51,7 @@ void FramelessWindowsManager::addWindow(QWindow *window)
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
} }
#ifdef FRAMELESSHELPER_USE_UNIX_VERSION #ifdef FRAMELESSHELPER_USE_UNIX_VERSION
framelessHelperUnix()->removeWindowFrame(window); //framelessHelperUnix()->removeWindowFrame(window);
#else #else
FramelessHelperWin::addFramelessWindow(window); FramelessHelperWin::addFramelessWindow(window);
// Work-around a Win32 multi-monitor bug. // Work-around a Win32 multi-monitor bug.
@ -165,7 +165,7 @@ void FramelessWindowsManager::removeWindow(QWindow *window)
return; return;
} }
#ifdef FRAMELESSHELPER_USE_UNIX_VERSION #ifdef FRAMELESSHELPER_USE_UNIX_VERSION
framelessHelperUnix()->bringBackWindowFrame(window); //framelessHelperUnix()->bringBackWindowFrame(window);
#else #else
FramelessHelperWin::removeFramelessWindow(window); FramelessHelperWin::removeFramelessWindow(window);
#endif #endif

View File

@ -58,6 +58,16 @@ FRAMELESSHELPER_API void updateQtFrameMargins(QWindow *window, const bool enable
[[nodiscard]] FRAMELESSHELPER_API QString getSystemErrorMessage(const QString &function); [[nodiscard]] FRAMELESSHELPER_API QString getSystemErrorMessage(const QString &function);
#endif #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 FRAMELESSHELPER_END_NAMESPACE

View File

@ -25,9 +25,26 @@
#include "utilities.h" #include "utilities.h"
#include <QtCore/qvariant.h> #include <QtCore/qvariant.h>
#include <QtCore/qdebug.h>
#include <QtGui/qscreen.h>
#include <QtX11Extras/qx11info_x11.h>
#include <X11/Xlib.h>
FRAMELESSHELPER_BEGIN_NAMESPACE 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 kDefaultResizeBorderThickness = 8;
static constexpr int kDefaultCaptionHeight = 23; static constexpr int kDefaultCaptionHeight = 23;
@ -106,7 +123,8 @@ bool Utilities::shouldAppsUseDarkMode()
ColorizationArea Utilities::getColorizationArea() ColorizationArea Utilities::getColorizationArea()
{ {
// ### TO BE IMPLEMENTED // ### 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) bool Utilities::isThemeChanged(const void *data)
@ -132,4 +150,178 @@ bool Utilities::showSystemMenu(const WId winId, const QPointF &pos)
return false; return false;
} }
FRAMELESSHELPER_END_NAMESPACE 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