diff --git a/.docker/desktop-apps.bake.Dockerfile b/.docker/desktop-apps.bake.Dockerfile index 8ab80540c..1b9ef6901 100644 --- a/.docker/desktop-apps.bake.Dockerfile +++ b/.docker/desktop-apps.bake.Dockerfile @@ -31,6 +31,7 @@ FROM core-base AS desktop-linux bison \ libnotify-dev \ libcups2-dev \ + libwayland-dev \ libdbus-1-dev \ libxcb-util0-dev \ libxcb-xkb-dev \ @@ -102,7 +103,7 @@ FROM core-base AS desktop-linux -DVCPKG_MANIFEST_FEATURES="desktop-editors" \ -DABOUT_PAGE_APP_NAME="${ABOUT_PAGE_APP_NAME}" \ /desktop-apps/win-linux/ && \ - cmake --build . && \ + cmake --build . -- -j4 && \ cmake --install . && \ ccache --show-stats && \ cp -a desktopeditors /desktopeditors diff --git a/win-linux/CMakeLists.txt b/win-linux/CMakeLists.txt index b256bacdf..3264a2348 100644 --- a/win-linux/CMakeLists.txt +++ b/win-linux/CMakeLists.txt @@ -21,7 +21,6 @@ set(BUILD_DESKTOP TRUE CACHE INTERNAL "tell common to prepare third party for de include(${CORE_ROOT_DIR}/common.cmake) -# 2. Find Qt Packages if(UNIX) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS DBus) add_definitions(-DLINUX -D_LINUX -D_WAYLAND) @@ -115,6 +114,8 @@ if(NOT TARGET allthemesgen) add_subdirectory( "${CORE_ROOT_DIR}/DesktopEditor/allthemesgen" allthemesgen ) endif() + + # 3. Definitions & Global Config add_definitions(-D__DONT_WRITE_IN_APP_TITLE) add_definitions(-DAPP_ICON_PATH="./res/icons/desktopeditors-eo.ico") @@ -279,6 +280,9 @@ elseif(UNIX AND NOT APPLE) src/platform_linux/gtkprintdialog.cpp src/platform_linux/gtkutils.cpp src/platform_linux/linux_window_utils.cpp + src/platform_linux/x11backend.cpp + src/platform_linux/waylandbackend.cpp + src/platform_linux/platformbackendfactory.cpp extras/update-daemon/src/classes/csocket.cpp ) list(APPEND COMMON_HEADERS @@ -294,11 +298,20 @@ elseif(UNIX AND NOT APPLE) src/platform_linux/gtkprintdialog.h src/platform_linux/gtkutils.h src/platform_linux/linux_window_utils.h + src/platform_linux/iplatformbackend.h + src/platform_linux/x11backend.h + src/platform_linux/waylandbackend.h extras/update-daemon/src/classes/csocket.h ) find_package(PkgConfig REQUIRED) - pkg_check_modules(GTK3 REQUIRED glib-2.0 gtk+-3.0 atk gtk+-unix-print-3.0 xcb xext libnotify) + pkg_check_modules(GTK3 REQUIRED glib-2.0 gtk+-3.0 atk gtk+-unix-print-3.0 libnotify) + pkg_check_modules(XCB_LIBS xcb xext) + pkg_check_modules(X11_LIBS x11 x11-xcb) + if(XCB_LIBS_FOUND AND X11_LIBS_FOUND) + add_definitions(-DHAVE_X11) + endif() + add_definitions(-DHAVE_WAYLAND) # Custom command for glib-compile-resources set(GRESOURCE_XML ${CMAKE_CURRENT_SOURCE_DIR}/res/gresource.xml) @@ -310,7 +323,10 @@ elseif(UNIX AND NOT APPLE) ) list(APPEND COMMON_SOURCES ${GRESOURCE_C}) - set(PLATFORM_LIBS ${GTK3_LIBRARIES} X11 X11-xcb cups) + set(PLATFORM_LIBS ${GTK3_LIBRARIES} cups) + if(XCB_LIBS_FOUND AND X11_LIBS_FOUND) + list(APPEND PLATFORM_LIBS ${X11_LIBS_LIBRARIES} ${XCB_LIBS_LIBRARIES}) + endif() endif() # 7. Create Executable @@ -362,8 +378,9 @@ target_link_libraries(DesktopEditors PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::PrintSupport - Qt${QT_VERSION_MAJOR}::Multimedia + Qt${QT_VERSION_MAJOR}::Multimedia Qt${QT_VERSION_MAJOR}::MultimediaWidgets + Qt${QT_VERSION_MAJOR}::OpenGLWidgets ${PLATFORM_LIBS} ${DBUS_LIBRARIES} diff --git a/win-linux/res/icons/logo-dark-eo.svg b/win-linux/res/icons/logo-dark-eo.svg index a0fac4c36..2d2195fe1 100644 --- a/win-linux/res/icons/logo-dark-eo.svg +++ b/win-linux/res/icons/logo-dark-eo.svg @@ -1,6 +1,6 @@ - + - + Euro-Office diff --git a/win-linux/res/icons/logo-light-eo.svg b/win-linux/res/icons/logo-light-eo.svg index b9b4921a2..750a33ebe 100644 --- a/win-linux/res/icons/logo-light-eo.svg +++ b/win-linux/res/icons/logo-light-eo.svg @@ -1,6 +1,6 @@ - + - + Euro-Office diff --git a/win-linux/res/icons/message_warn.svg b/win-linux/res/icons/message_warn.svg index edfb2af43..e236e4e8e 100644 --- a/win-linux/res/icons/message_warn.svg +++ b/win-linux/res/icons/message_warn.svg @@ -36,7 +36,7 @@ inkscape:current-layer="svg4" showguides="true" />QToolButton {max-height: 28px;} Tab {min-width: 41px; max-width: 135px; width: 135px; height: 28px;} Tab #tabIcon {padding: 0px; min-width: 15px; min-height: 15px; max-width: 15px; max-height: 15px;} -Tab #tabText {font-size: 11px;} +/* 12px, not 11px, to match the editor ribbon's own tab labels + (web-apps' .ribtab uses @font-size-medium: 12px). The two sit + directly above one another, so a 1px difference is visible as the + title bar looking undersized next to the ribbon. */ +Tab #tabText {font-size: 12px;} Tab #tabButton {width: 16px; max-width: 16px; max-height: 16px; border-width: 2px;} #mainPanel[rtl-font=true][unix=true] Tab #tabText {font-size: 12px;} #mainPanel[win10=true] CTabBar {margin-top: 5px;} diff --git a/win-linux/src/cascapplicationmanagerwrapper_private.h b/win-linux/src/cascapplicationmanagerwrapper_private.h index 07f6bd6bd..97814b7ae 100644 --- a/win-linux/src/cascapplicationmanagerwrapper_private.h +++ b/win-linux/src/cascapplicationmanagerwrapper_private.h @@ -46,6 +46,8 @@ #else # include "platform_linux/linux_window_utils.h" # include +# include +# include # define APP_LAUNCH_NAME "/DesktopEditors" # define RESTART_BATCH "/apprestart.sh" #endif @@ -116,7 +118,12 @@ class CAscApplicationManagerWrapper_Private ts << "del \"%~f0\"&exit\n"; #else ts << "#!/bin/bash\n"; - ts << "\"" << QString::fromStdWString(NSFile::GetProcessDirectory()) << APP_LAUNCH_NAME << "\" &\n"; + ts << "sleep 1\n"; + QString appDir = QString::fromStdWString(NSFile::GetProcessDirectory()); + ts << "cd \"" << appDir << "\"\n"; + ts << "LD_LIBRARY_PATH=\"$PWD:$PWD/converter:$LD_LIBRARY_PATH\""; + ts << " LD_PRELOAD=libcef.so"; + ts << " \"" << appDir << APP_LAUNCH_NAME << "\" &\n"; ts << "rm -- \"$0\"\n"; #endif if (!f.flush()) { @@ -143,8 +150,17 @@ class CAscApplicationManagerWrapper_Private CloseHandle(pi.hProcess); CloseHandle(pi.hThread); #else - if (!QProcess::startDetached("/bin/sh", QStringList{fileName})) + // Use fork/exec instead of QProcess::startDetached because + // this runs from a destructor after Qt's event loop has stopped, + // where QProcess may not work reliably. + pid_t pid = fork(); + if (pid == 0) { + setsid(); + execlp("/bin/sh", "/bin/sh", fileName.toLocal8Bit().constData(), (char *)nullptr); + _exit(1); + } else if (pid < 0) { CLogger::log("An error occurred while restarting the app!"); + } #endif } @@ -452,8 +468,20 @@ class CAscApplicationManagerWrapper_Private do { WId wid = (WId)hWnd; #else - std::vector winStack; + std::vector winStack; LinuxWindowUtils::getWindowStack(winStack); + if (winStack.empty()) { + // Wayland fallback: use our tracked editors list + for (auto it = m_appmanager.m_vecEditors.rbegin(); it != m_appmanager.m_vecEditors.rend(); it++) { + CEditorWindow *editor = reinterpret_cast(*it); + if (editor && editor->editorType() == etype) { + rc = editor->normalGeometry(); + rc.adjust(50, 50, 50, 50); + isMaximized = editor->windowState().testFlag(Qt::WindowMaximized); + return; + } + } + } for (auto it = winStack.rbegin(); it != winStack.rend(); it++) { WId wid = (WId)(*it); #endif @@ -470,8 +498,10 @@ class CAscApplicationManagerWrapper_Private } #ifdef _WIN32 } while ((hWnd = GetWindow(hWnd, GW_HWNDNEXT)) != nullptr); -#endif } +#else + } +#endif } GET_REGISTRY_USER(reg_user); diff --git a/win-linux/src/components/cdownloadwidget.cpp b/win-linux/src/components/cdownloadwidget.cpp index d75676ce9..aaf46bdda 100644 --- a/win-linux/src/components/cdownloadwidget.cpp +++ b/win-linux/src/components/cdownloadwidget.cpp @@ -69,6 +69,12 @@ struct CDownloadWidget::CDownloadItem static bool isCompositingEnabled() { #ifdef __linux__ + // On Wayland the compositor renders shadows natively. Qt's + // QGraphicsDropShadowEffect uses its own off-screen painter, and a + // concurrent Wayland compositor flush during window close causes + // "painted by one painter at a time" crashes. Skip it entirely. + if (QGuiApplication::platformName() == QLatin1String("wayland")) + return false; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) return QX11Info::isCompositingManagerRunning(); #else diff --git a/win-linux/src/components/cmenu.cpp b/win-linux/src/components/cmenu.cpp index 9f9aa046e..016d66783 100644 --- a/win-linux/src/components/cmenu.cpp +++ b/win-linux/src/components/cmenu.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #ifdef __linux__ @@ -50,6 +51,12 @@ static bool isCompositingEnabled() { #ifdef __linux__ + // On Wayland the compositor renders shadows natively. Qt's + // QGraphicsDropShadowEffect uses its own off-screen painter, and a + // concurrent Wayland compositor flush during window close causes + // "painted by one painter at a time" crashes. Skip it entirely. + if (QGuiApplication::platformName() == QLatin1String("wayland")) + return false; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) return QX11Info::isCompositingManagerRunning(); #else @@ -132,7 +139,22 @@ class CMenuWidget : public QWidget Q_OBJECT public: explicit CMenuWidget(QWidget * parent = nullptr) : - QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint) + // Qt::Popup (not Qt::Tool) for Wayland: Qt defines + // Tool = Popup | Dialog, so "Qt::Tool | Qt::Popup" is a no-op -- + // Popup's bit was already set, the resolved window type is still + // exactly Tool. Using Qt::Popup alone changes the actual resolved + // type, which is what Qt's Wayland QPA plugin needs to map this to + // the xdg_popup protocol (positioner-based, anchored to the parent) + // instead of a plain xdg_toplevel with set_parent() (which has no + // positioner, so the compositor defaults to centering it over its + // parent regardless of move() -- this widget was rendering dead + // center of the main window instead of at the click position). + // Dismiss-on-outside-click/Escape is already handled explicitly + // below (WindowDeactivate/MouseButtonPress/KeyRelease); Qt::Popup's + // own implicit pointer grab changes how those events are delivered + // (grabbed globally rather than only within this widget), so watch + // for that logic if dismiss behavior regresses. + QWidget(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint) { m_dpiRatio = CScalingWrapper::parentScalingFactor(topLevelWidget()); if (isCompositingEnabled()) { diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 3a4e5ad6b..218fb2137 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -97,8 +97,8 @@ #define TEXT_SAVEANDINS toCharPtr(BTN_TEXT_SAVEANDINS) #define TEXT_DOWNLOAD toCharPtr(BTN_TEXT_DOWNLOAD) -#define MSG_ICON_WIDTH 35 -#define MSG_ICON_HEIGHT 35 +#define MSG_ICON_WIDTH 44 +#define MSG_ICON_HEIGHT 44 #define DLG_PADDING 7 #define BTN_SPACING 5 @@ -150,6 +150,10 @@ class QtMsg : public QDialog void setContent(const QString&); void setCheckBox(const QString &chekBoxText, bool checkBoxState); bool getCheckStatus(); + // Applies every dpiRatio-dependent size/margin/stylesheet, once, from the + // parent-derived m_priv->dpiRatio. See the call site for why it must not + // be re-run off this dialog's own (Wayland-racing) devicePixelRatio. + void applyScaling(); QWidget *m_boxButtons = nullptr, *m_centralWidget = nullptr; @@ -163,11 +167,37 @@ class QtMsg : public QDialog std::unique_ptr m_priv; }; +namespace { +// Returns a DPI ratio taken from a window whose devicePixelRatio has already +// settled. A freshly-created top-level dialog must NOT use its own ratio: on +// Wayland a brand-new surface reports the rounded default (e.g. 2.0) until the +// compositor answers with the real fractional scale, so reading it here would +// size the whole dialog for the wrong factor (and, with no later correction, +// leave it that way). The parent window has been mapped long enough to carry +// the correct value. +double stableDpiRatioForDialog(QWidget * parent) +{ + QWidget * ref = parent ? parent->window() : nullptr; + if (!ref) + ref = QApplication::activeWindow(); + if (!ref) { + const QWidgetList tops = QApplication::topLevelWidgets(); + for (QWidget * w : tops) { + if (w->isWindow() && w->isVisible() && !w->windowFlags().testFlag(Qt::ToolTip)) { + ref = w; + break; + } + } + } + return ref ? Utils::getScreenDpiRatioByWidget(ref) : 1.0; +} +} + class QtMsg::QtMsgPrivateIntf { public: - explicit QtMsgPrivateIntf(QtMsg * parent) - : m_mess(parent) - , dpiRatio(Utils::getScreenDpiRatioByWidget(parent)) + QtMsgPrivateIntf(QtMsg * dlg, QWidget * parentWindow) + : m_mess(dlg) + , dpiRatio(stableDpiRatioForDialog(parentWindow)) {} auto addButton(QPushButton * b) -> void { @@ -197,6 +227,11 @@ class QtMsg::QtMsgPrivateIntf { double dpiRatio = 1; QMetaObject::Connection focusConnection; bool isWindowActive = false; + // Kept around so applyScaling() can re-set their margins later; every + // other dpiRatio-dependent widget it touches is already a QtMsg member. + QVBoxLayout * cLayout = nullptr; + QFormLayout * fLayout = nullptr; + QWidget * bodyWidget = nullptr; }; int QtMsg::m_modalresult(MODAL_RESULT_CANCEL); @@ -207,7 +242,7 @@ QtMsg::QtMsg(QWidget * p) , m_message(new QLabel) , m_content(new QLabel) , m_typeIcon(new QLabel) - , m_priv(new QtMsgPrivateIntf(this)) + , m_priv(new QtMsgPrivateIntf(this, p)) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); //setWindowTitle(APP_TITLE); @@ -220,45 +255,50 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->setObjectName("messageBody"); m_centralWidget->setProperty("uitheme", QString::fromStdWString(GetCurrentTheme().originalId())); - QVBoxLayout * _c_layout = new QVBoxLayout; - QHBoxLayout * _h_layout2 = new QHBoxLayout; - QHBoxLayout * _h_layout1 = new QHBoxLayout; - _c_layout->addLayout(_h_layout2, 1); - _c_layout->addLayout(_h_layout1, 0); + m_priv->cLayout = new QVBoxLayout; - const int _body_margin = int(12 * m_priv->dpiRatio); - _c_layout->setContentsMargins(_body_margin,_body_margin,_body_margin,_body_margin); + // bodyWidget groups the content row (icon + text) and the buttons row so + // they share the same left/right extents. The whole block is then centered + // within cLayout, giving symmetric margins and aligning the icon's left + // edge with the first button's left edge. + m_priv->bodyWidget = new QWidget; + QVBoxLayout * bodyLayout = new QVBoxLayout(m_priv->bodyWidget); + bodyLayout->setContentsMargins(0, 0, 0, 0); + bodyLayout->setSpacing(0); + QHBoxLayout * contentRow = new QHBoxLayout; + contentRow->setContentsMargins(0, 0, 0, 0); m_typeIcon->setProperty("class", "msg-icon"); - m_typeIcon->setFixedSize(int(round(MSG_ICON_WIDTH*m_priv->dpiRatio - 0.25)), - int(round(MSG_ICON_HEIGHT*m_priv->dpiRatio - 0.25))); - _h_layout2->addWidget(m_typeIcon, 0, Qt::AlignTop); + contentRow->addWidget(m_typeIcon, 0, Qt::AlignTop); // m_message->setWordWrap(true); m_message->setProperty("class", "msg-report"); - m_message->setStyleSheet(QString("margin-bottom: %1px;").arg(int(8*m_priv->dpiRatio))); - m_message->setTextFormat(Qt::PlainText); + m_message->setTextFormat(Qt::RichText); + m_message->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_content->setProperty("class", "msg-report"); - m_content->setStyleSheet(QString("margin-bottom: %1px;").arg(int(8*m_priv->dpiRatio))); m_content->setTextFormat(Qt::RichText); + m_content->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_content->setOpenExternalLinks(true); - QFormLayout * _f_layout = new QFormLayout; - _f_layout->addWidget(m_message); - _f_layout->addWidget(m_content); - _f_layout->setSpacing(0); - _f_layout->setContentsMargins(int(10*m_priv->dpiRatio),0,int(5*m_priv->dpiRatio),0); - _h_layout2->addLayout(_f_layout, 1); - _h_layout2->setContentsMargins(0,0,0,0); + m_priv->fLayout = new QFormLayout; + m_priv->fLayout->addWidget(m_message); + m_priv->fLayout->addWidget(m_content); + m_priv->fLayout->setSpacing(0); + contentRow->addLayout(m_priv->fLayout, 1); + bodyLayout->addLayout(contentRow, 1); QPushButton * btn_ok = new QPushButton("&" + QObject::tr("OK")); btn_ok->setAutoDefault(true); m_boxButtons->setLayout(new QHBoxLayout); m_boxButtons->layout()->addWidget(btn_ok); - m_boxButtons->layout()->setContentsMargins(0,int(10*m_priv->dpiRatio),0,0); - m_boxButtons->layout()->setSpacing(int(8*m_priv->dpiRatio)); - _h_layout1->addWidget(m_boxButtons, 0, Qt::AlignCenter); + + QHBoxLayout * buttonsRow = new QHBoxLayout; + buttonsRow->setContentsMargins(0, 0, 0, 0); + buttonsRow->addWidget(m_boxButtons, 0, Qt::AlignCenter); + bodyLayout->addLayout(buttonsRow, 0); + + m_priv->cLayout->addWidget(m_priv->bodyWidget, 0, Qt::AlignCenter); m_priv->addButton(btn_ok); @@ -269,16 +309,13 @@ QtMsg::QtMsg(QWidget * p) } ); - m_centralWidget->setLayout(_c_layout); - m_centralWidget->setMinimumWidth(int(350*m_priv->dpiRatio)); + m_centralWidget->setLayout(m_priv->cLayout); m_centralWidget->move(0, 0); - QString _styles(Utils::readStylesheets(":/styles/message.qss")); - _styles.append(QString("QPushButton{min-width:%1px;}").arg(int(40*m_priv->dpiRatio))); - m_centralWidget->setStyleSheet( _styles ); - - QString zoom = QString::number(m_priv->dpiRatio) + "x"; - m_centralWidget->setProperty("scaling", zoom); + // Scale once, from the stable parent-window ratio (see + // stableDpiRatioForDialog). No watcher/re-derivation is needed because + // that ratio doesn't change under this dialog. + applyScaling(); m_priv->focusConnection = QObject::connect(qApp, &QApplication::focusChanged, this, [&] (QWidget * from, QWidget *to){ @@ -294,6 +331,34 @@ QtMsg::QtMsg(QWidget * p) }); } +void QtMsg::applyScaling() +{ + const int _body_margin = int(12 * m_priv->dpiRatio); + m_priv->cLayout->setContentsMargins(_body_margin,_body_margin,_body_margin,_body_margin); + + m_typeIcon->setFixedSize(int(round(MSG_ICON_WIDTH*m_priv->dpiRatio - 0.25)), + int(round(MSG_ICON_HEIGHT*m_priv->dpiRatio - 0.25))); + + // Wider gap under the primary line, matching the GTK message dialog's + // spacing between its bold primary and regular secondary text. + m_message->setStyleSheet(QString("margin-bottom: %1px;").arg(int(12*m_priv->dpiRatio))); + m_content->setStyleSheet(QString("margin-bottom: %1px;").arg(int(8*m_priv->dpiRatio))); + + m_priv->fLayout->setContentsMargins(int(10*m_priv->dpiRatio),0,int(5*m_priv->dpiRatio),0); + + m_boxButtons->layout()->setContentsMargins(0,int(10*m_priv->dpiRatio),0,0); + m_boxButtons->layout()->setSpacing(int(8*m_priv->dpiRatio)); + + m_centralWidget->setMinimumWidth(m_priv->buttons.size() > 2 ? 400 : 350); + + QString _styles(Utils::readStylesheets(":/styles/message.qss")); + _styles.append(QString("QPushButton{min-width:%1px;}").arg(int(40*m_priv->dpiRatio))); + m_centralWidget->setStyleSheet( _styles ); + + QString zoom = QString::number(m_priv->dpiRatio) + "x"; + m_centralWidget->setProperty("scaling", zoom); +} + QtMsg::~QtMsg() { QObject::disconnect(m_priv->focusConnection); @@ -356,7 +421,7 @@ void QtMsg::setButtons(std::initializer_list btns) } if (_btn_num > 2) - m_centralWidget->setMinimumWidth(int(400*m_priv->dpiRatio)); + m_centralWidget->setMinimumWidth(400); } void QtMsg::setButtons(MsgBtns btns) @@ -414,13 +479,27 @@ int QtMsg::showMessage(QWidget *parent, dlg.setLayoutDirection(Qt::RightToLeft); #endif } - dlg.setText(QTextDocumentFragment::fromHtml(msg).toPlainText()); + // Split the primary (first) line from any following secondary lines, like + // the GTK message dialog: the primary is bold/larger, the rest regular. + const QString plain = QTextDocumentFragment::fromHtml(msg).toPlainText(); + QString primary = plain, secondary; + const int nl = plain.indexOf('\n'); + if (nl != -1) { + primary = plain.left(nl); + secondary = plain.mid(nl + 1); + } + dlg.setText(primary); QString content = opts.contentText; if (!content.isEmpty() && !opts.linkText.isEmpty()) { content.append("\n"); content.replace("\n", "
"); } content.append(opts.linkText); + if (!secondary.isEmpty()) { + QString sec = secondary.toHtmlEscaped(); + sec.replace("\n", "
"); + content = content.isEmpty() ? sec : (sec + "
" + content); + } if (!content.isEmpty()) dlg.setContent(content); dlg.setIcon(msgType); @@ -448,7 +527,12 @@ void QtMsg::setIcon(MsgType msgType) void QtMsg::setText( const QString& t) { - m_message->setText(t); + // Primary line, styled like the GTK message dialog: bold and a little + // larger than the secondary text. showMessage() splits the primary line + // off and routes the rest to the (regular) content label. + const int px = int(13.5 * m_priv->dpiRatio + 0.5); + m_message->setText(QString("%2") + .arg(px).arg(t.toHtmlEscaped())); } void QtMsg::setContent( const QString& t) @@ -931,8 +1015,12 @@ int CMessage::showMessage(QWidget *parent, return WinMsg::showMessage(parent, msg, msgType, msgBtns, opts); # endif #else - WindowHelper::CParentDisable oDisabler(parent); - return GtkMsg::showMessage(parent, msg, msgType, msgBtns, opts); + // On Wayland, use Qt dialogs to avoid GTK falling back to Xwayland + // (which causes wrong scaling and misaligned click areas). + if (QGuiApplication::platformName() != "wayland") { + WindowHelper::CParentDisable oDisabler(parent); + return GtkMsg::showMessage(parent, msg, msgType, msgBtns, opts); + } #endif } return QtMsg::showMessage(parent, msg, msgType, msgBtns, opts); diff --git a/win-linux/src/components/csvgpushbutton.cpp b/win-linux/src/components/csvgpushbutton.cpp index dc754f10d..010bd4d7f 100644 --- a/win-linux/src/components/csvgpushbutton.cpp +++ b/win-linux/src/components/csvgpushbutton.cpp @@ -100,9 +100,13 @@ void CSVGPushButton::setFillDark(bool dark) void CSVGPushButton::updateIcon() { if ( !m_svglayout.isEmpty() ) { - QImage img(iconSize(), QImage::Format_ARGB32); + qreal dpr = devicePixelRatioF(); + QSize physicalSize = iconSize() * dpr; + QImage img(physicalSize, QImage::Format_ARGB32); + img.setDevicePixelRatio(dpr); img.fill(Qt::transparent); QPixmap pixmap = QPixmap::fromImage(img, Qt::NoFormatConversion); + pixmap.setDevicePixelRatio(dpr); QPainter painter(&pixmap); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); diff --git a/win-linux/src/components/ctabbar.cpp b/win-linux/src/components/ctabbar.cpp index a88234468..0edecf85c 100644 --- a/win-linux/src/components/ctabbar.cpp +++ b/win-linux/src/components/ctabbar.cpp @@ -1283,7 +1283,12 @@ bool CTabBar::eventFilter(QObject *watched, QEvent *event) QContextMenuEvent* cm_event = static_cast(event); for (int i = 0; i < d->tabList.size(); i++) { if (d->_tabRect(i).contains(cm_event->pos())) { - QPoint pos = d->tabArea->mapToGlobal(cm_event->pos()); + // mapToGlobal() is unreliable on Wayland (clients aren't + // told their absolute window position), which was + // rendering this menu at a bogus/centered location. The + // event's own global position is computed correctly by + // Qt's input pipeline when the event was generated. + QPoint pos = cm_event->globalPos(); SKIP_EVENTS_QUEUE([=]() { emit tabMenuRequested(i, pos); }); diff --git a/win-linux/src/components/ctabpanel.cpp b/win-linux/src/components/ctabpanel.cpp index 35dfc43ee..8d304e413 100644 --- a/win-linux/src/components/ctabpanel.cpp +++ b/win-linux/src/components/ctabpanel.cpp @@ -4,6 +4,7 @@ #include "defines.h" #include "cefview.h" #include +#include "cmessage.h" using namespace NSEditorApi; diff --git a/win-linux/src/components/ctooltip.cpp b/win-linux/src/components/ctooltip.cpp index 9af469722..b128884d8 100644 --- a/win-linux/src/components/ctooltip.cpp +++ b/win-linux/src/components/ctooltip.cpp @@ -44,6 +44,12 @@ static bool isCompositingEnabled() { #ifdef __linux__ + // On Wayland the compositor renders shadows natively. Qt's + // QGraphicsDropShadowEffect uses its own off-screen painter, and a + // concurrent Wayland compositor flush during window close causes + // "painted by one painter at a time" crashes. Skip it entirely. + if (QGuiApplication::platformName() == QLatin1String("wayland")) + return false; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) return QX11Info::isCompositingManagerRunning(); #else diff --git a/win-linux/src/main.cpp b/win-linux/src/main.cpp index 271f9e08c..32c8d2ade 100644 --- a/win-linux/src/main.cpp +++ b/win-linux/src/main.cpp @@ -40,10 +40,17 @@ #include "chelp.h" #include "common/File.h" #include +#include +#include +#include int main( int argc, char *argv[] ) { + bool isWayland = false; + int new_argc = argc; + char** new_argv = argv; + std::vector dynamic_argv; #ifdef _WIN32 Core_SetProcessDpiAwareness(); Utils::setAppUserModelId(); @@ -53,9 +60,30 @@ int main( int argc, char *argv[] ) return 0; } #else - qputenv("QT_QPA_PLATFORM", "xcb"); - qputenv("GDK_BACKEND", "x11"); - InputArgs::init(argc, argv); + dynamic_argv.assign(argv, argv + argc); + QByteArray platform = qgetenv("QT_QPA_PLATFORM"); + if (platform.isEmpty()) { + QByteArray sessionType = qgetenv("XDG_SESSION_TYPE"); + if (sessionType == "wayland") { + platform = "wayland"; + } else { + platform = "xcb"; + } + qputenv("QT_QPA_PLATFORM", platform); + } + isWayland = (platform == "wayland"); + + if (isWayland) { + qputenv("GDK_BACKEND", "wayland"); + dynamic_argv.push_back(const_cast("--ozone-platform=wayland")); + } else { + qputenv("GDK_BACKEND", "x11"); + } + dynamic_argv.push_back(nullptr); + new_argc = dynamic_argv.size() - 1; + new_argv = dynamic_argv.data(); + + InputArgs::init(new_argc, new_argv); if (geteuid() == 0) { CMessage::warning(nullptr, WARNING_LAUNCH_WITH_ADMIN_RIGHTS); return 0; @@ -65,12 +93,47 @@ int main( int argc, char *argv[] ) return 0; } #endif +#ifdef __linux + char* qpaPlatform = getenv("QT_QPA_PLATFORM"); + char* xdgSessionType = getenv("XDG_SESSION_TYPE"); + if ((qpaPlatform && strcmp(qpaPlatform, "wayland") == 0) || + (xdgSessionType && strcmp(xdgSessionType, "wayland") == 0)) { + isWayland = true; + } +#endif + + if (!isWayland) { + // Plasma and other environments export QT_SCREEN_SCALE_FACTORS / + // QT_SCALE_FACTOR on X11. In Qt 6 these activate high-DPI scaling + // even with QT_ENABLE_HIGHDPI_SCALING=0, which breaks this app: + // widgets become logical-pixel sized while native CEF child windows + // (SetWindowSize/XConfigureWindow) and _NET_WM_MOVERESIZE coordinates + // remain in device pixels. The app does its own DPI scaling on X11, + // so neutralize Qt's completely. + qunsetenv("QT_SCREEN_SCALE_FACTORS"); + qunsetenv("QT_SCALE_FACTOR"); + qunsetenv("QT_AUTO_SCREEN_SCALE_FACTOR"); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); + qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); #else - QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); +#endif + } else { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif - QCoreApplication::setAttribute(Qt::AA_Use96Dpi); + // Without this, Qt's Wayland platform plugin disables fractional- + // scale support and permanently rounds devicePixelRatio() to the + // nearest integer for ordinary widgets (e.g. a real 1.25 scale + // reports as 2) -- not a startup race, the default, permanent + // behavior regardless of how long you wait. Must be set before + // QGuiApplication is constructed. + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + } + // On Wayland, skip AA_Use96Dpi to let the compositor's native DPI + // take effect — otherwise Qt overrides DPI, causing fuzzy text. + if (!isWayland) + QCoreApplication::setAttribute(Qt::AA_Use96Dpi); #ifdef _WIN32 QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); // avoid Qt's ANGLE colliding with CEF's libEGL/libGLESv2 #endif @@ -139,7 +202,7 @@ int main( int argc, char *argv[] ) reg_user.remove("lockPortals"); } - SingleApplication app(argc, argv); + SingleApplication app(new_argc, new_argv); if ( !app.isPrimary() ) { QString _out_args; @@ -166,12 +229,16 @@ int main( int argc, char *argv[] ) /* the order is important */ #ifdef __linux - gtk_init(&argc, &argv); + gtk_disable_setlocale(); + gtk_init(&new_argc, &new_argv); #endif - CApplicationCEF::Prepare(argc, argv); + CApplicationCEF::Prepare(new_argc, new_argv); + if (QGuiApplication::platformName() == "wayland") { + qputenv("GDK_BACKEND", "wayland"); + } CApplicationCEF* application_cef = new CApplicationCEF(); setup_paths(&AscAppManager::getInstance()); - application_cef->Init_CEF(&AscAppManager::getInstance(), argc, argv); + application_cef->Init_CEF(&AscAppManager::getInstance(), new_argc, new_argv); /* ********************** */ // GET_REGISTRY_SYSTEM(reg_system) diff --git a/win-linux/src/platform_linux/gtkfilechooser.cpp b/win-linux/src/platform_linux/gtkfilechooser.cpp index df3b372e0..85dc5dd6a 100755 --- a/win-linux/src/platform_linux/gtkfilechooser.cpp +++ b/win-linux/src/platform_linux/gtkfilechooser.cpp @@ -4,7 +4,9 @@ #include "gtkutils.h" #include "gtkfilechooser.h" #include "cascapplicationmanagerwrapper.h" +#ifdef HAVE_X11 #include +#endif //static void find_children(GList **list, GtkWidget *wgt, const gchar *name) @@ -53,7 +55,7 @@ static GSList* parseString(const char *str, const char *delim) return list; } -static void nativeFileDialog(const Window &parent_xid, +static void nativeFileDialog(ulong parent_xid, Gtk::Mode mode, char*** filenames, int* files_count, @@ -181,7 +183,7 @@ QStringList Gtk::openGtkFileChooser(QWidget *parent, char **filenames = nullptr; char *_sel_filter = (sel_filter) ? strdup(sel_filter->toLocal8Bit().data()) : nullptr; int files_count = 0; - Window parent_xid = (parent) ? (Window)parent->winId() : 0L; + ulong parent_xid = (parent) ? (ulong)parent->winId() : 0; nativeFileDialog(parent_xid, mode, &filenames, diff --git a/win-linux/src/platform_linux/gtkprintdialog.cpp b/win-linux/src/platform_linux/gtkprintdialog.cpp index e48665f09..7d3a7614e 100644 --- a/win-linux/src/platform_linux/gtkprintdialog.cpp +++ b/win-linux/src/platform_linux/gtkprintdialog.cpp @@ -6,7 +6,9 @@ #include "cascapplicationmanagerwrapper.h" #include #include +#ifdef HAVE_X11 #include +#endif #include #include "components/cmessage.h" @@ -183,7 +185,7 @@ void GtkPrintDialog::setPrintRange(PrintRange print_range) QDialog::DialogCode GtkPrintDialog::exec() { QDialog::DialogCode exit_code = QDialog::DialogCode::Rejected; - Window parent_xid = (m_parent) ? (Window)m_parent->winId() : 0L; + ulong parent_xid = (m_parent) ? (ulong)m_parent->winId() : 0; auto qt_printer_name = m_printer->printerName(); auto qt_resolution = m_printer->resolution(); diff --git a/win-linux/src/platform_linux/gtkutils.cpp b/win-linux/src/platform_linux/gtkutils.cpp index e4b0b4f7d..d129a97a1 100644 --- a/win-linux/src/platform_linux/gtkutils.cpp +++ b/win-linux/src/platform_linux/gtkutils.cpp @@ -24,45 +24,59 @@ */ #include "gtkutils.h" -#include #include #include "platform_linux/linux_window_utils.h" +#ifdef HAVE_X11 +#include +#endif gboolean set_focus(GtkWidget *dialog) { - GdkWindow *gdk_dialog = gtk_widget_get_window(dialog); - if (gdk_dialog) { - xcb_window_t wnd = (xcb_window_t)gdk_x11_window_get_xid(gdk_dialog); - LinuxWindowUtils::setNativeFocusTo(wnd); +#ifdef HAVE_X11 + GdkDisplay *gdk_display = gdk_display_get_default(); + if (gdk_display && GDK_IS_X11_DISPLAY(gdk_display)) { + GdkWindow *gdk_dialog = gtk_widget_get_window(dialog); + if (gdk_dialog && GDK_IS_X11_WINDOW(gdk_dialog)) { + WId wnd = (WId)gdk_x11_window_get_xid(gdk_dialog); + LinuxWindowUtils::setNativeFocusTo(wnd); + } } +#endif return FALSE; } gboolean focus_out(gpointer data) { - if (data) { +#ifdef HAVE_X11 + GdkDisplay *gdk_display = gdk_display_get_default(); + if (data && gdk_display && GDK_IS_X11_DISPLAY(gdk_display)) { DialogTag *tag = (DialogTag*)data; GtkWidget *dialog = tag->dialog; - xcb_window_t parent_xid = (xcb_window_t)tag->parent_xid; - if (dialog && LinuxWindowUtils::isNativeFocus(parent_xid)) + WId parent_xid = (WId)tag->parent_xid; + if (dialog && parent_xid != 0 && LinuxWindowUtils::isNativeFocus(parent_xid)) set_focus(dialog); } +#endif return FALSE; } void set_parent(GtkWidget *dialog, gpointer data) { +#ifdef HAVE_X11 if (dialog && data) { GdkDisplay *gdk_display = gdk_display_get_default(); - Window parent_xid = *(Window*)data; - if (gdk_display && parent_xid != None) { - GdkWindow *gdk_dialog = gtk_widget_get_window(dialog); - GdkWindow *gdk_qtparent = gdk_x11_window_foreign_new_for_display(gdk_display, parent_xid); - if (gdk_dialog && gdk_qtparent ) - gdk_window_set_transient_for(gdk_dialog, gdk_qtparent); + if (gdk_display && GDK_IS_X11_DISPLAY(gdk_display)) { + Window parent_xid = *(Window*)data; + if (parent_xid != None) { + GdkWindow *gdk_dialog = gtk_widget_get_window(dialog); + GdkWindow *gdk_qtparent = gdk_x11_window_foreign_new_for_display(gdk_display, parent_xid); + if (gdk_dialog && gdk_qtparent) + gdk_window_set_transient_for(gdk_dialog, gdk_qtparent); + } } } +#endif } void add_to_recent(const gchar *uri) diff --git a/win-linux/src/platform_linux/iplatformbackend.h b/win-linux/src/platform_linux/iplatformbackend.h new file mode 100644 index 000000000..6037cb413 --- /dev/null +++ b/win-linux/src/platform_linux/iplatformbackend.h @@ -0,0 +1,71 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#ifndef IPLATFORMBACKEND_H +#define IPLATFORMBACKEND_H + +#include +#include +#include + +class IPlatformBackend { +public: + virtual ~IPlatformBackend() = default; + + // Window management + virtual void moveWindow(WId window, int x, int y) = 0; + virtual bool isNativeFocus(WId window) = 0; + virtual void setNativeFocusTo(WId window) = 0; + virtual void setInputEnabled(WId window, bool enabled) = 0; + virtual void getWindowStack(std::vector &winStack) = 0; + virtual void findWindowAsync(const char *name, void *data, + uint timeout_ms, + void(*cb)(WId, void*)) = 0; + + // Decoration / WM interaction + virtual void raiseWindow(QWidget *window) = 0; + virtual void minimizeWindow(QWidget *window) = 0; + virtual void setCursorPos(int x, int y) = 0; + virtual void sendButtonRelease(QWidget *window) = 0; + virtual void startInteractiveMove(QWidget *window, const QPoint &globalPos) = 0; + virtual void startInteractiveResize(QWidget *window, Qt::Edges edges, const QPoint &globalPos) = 0; + + // Cursor management + virtual void setCursor(WId window, int cursorShape) = 0; + virtual void resetCursor(WId window) = 0; + + // Queries + virtual bool isCompositingAvailable() = 0; + virtual bool checkButtonState(Qt::MouseButton b) = 0; + + // XDG Portal parent window handle + virtual QString portalParentHandle(QWidget *parent) = 0; + + // Factory + static IPlatformBackend* create(); + static IPlatformBackend* instance(); +}; + +#endif // IPLATFORMBACKEND_H diff --git a/win-linux/src/platform_linux/linux_window_utils.cpp b/win-linux/src/platform_linux/linux_window_utils.cpp index 7d3e5edc6..c20b67cab 100644 --- a/win-linux/src/platform_linux/linux_window_utils.cpp +++ b/win-linux/src/platform_linux/linux_window_utils.cpp @@ -24,236 +24,36 @@ */ #include "linux_window_utils.h" -#include -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include -#endif -#include -#include -#include -#include -#include -#include -#include +#include "iplatformbackend.h" - -void LinuxWindowUtils::moveWindow(xcb_window_t window, int x, int y) -{ -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - xcb_connection_t *conn = QX11Info::connection(); - if (conn && window != XCB_WINDOW_NONE) { - uint32_t val[2]; - val[0] = x; - val[1] = y; - xcb_configure_window(conn, window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, val); - xcb_flush(conn); - } -#else - Display *disp = XOpenDisplay(NULL); - if (disp) { - xcb_connection_t *conn = XGetXCBConnection(disp); - if (conn && window != XCB_WINDOW_NONE) { - uint32_t val[2]; - val[0] = x; - val[1] = y; - xcb_configure_window(conn, window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, val); - xcb_flush(conn); - } - XCloseDisplay(disp); - } -#endif -} - -bool LinuxWindowUtils::isNativeFocus(xcb_window_t window) +void LinuxWindowUtils::moveWindow(WId window, int x, int y) { - xcb_window_t win = 0; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - xcb_connection_t *conn = QX11Info::connection(); - if (conn) { - xcb_get_input_focus_cookie_t cookie; - xcb_get_input_focus_reply_t *reply; - cookie = xcb_get_input_focus(conn); - reply = xcb_get_input_focus_reply(conn, cookie, NULL); - if (reply) { - win = reply->focus; - free(reply); - } - xcb_flush(conn); - } -#else - Display *disp = XOpenDisplay(NULL); - if (disp) { - xcb_connection_t *conn = XGetXCBConnection(disp); - if (conn) { - xcb_get_input_focus_cookie_t cookie; - xcb_get_input_focus_reply_t *reply; - cookie = xcb_get_input_focus(conn); - reply = xcb_get_input_focus_reply(conn, cookie, NULL); - if (reply) { - win = reply->focus; - free(reply); - } - xcb_flush(conn); - } - XCloseDisplay(disp); - } -#endif - return window == win; + IPlatformBackend::instance()->moveWindow(window, x, y); } -void LinuxWindowUtils::setNativeFocusTo(xcb_window_t window) +bool LinuxWindowUtils::isNativeFocus(WId window) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - xcb_connection_t *conn = QX11Info::connection(); - if (conn && window != XCB_WINDOW_NONE) { - xcb_void_cookie_t cookie; - cookie = xcb_set_input_focus(conn, XCB_INPUT_FOCUS_PARENT, - window, XCB_CURRENT_TIME); - xcb_flush(conn); - } -#else - Display *disp = XOpenDisplay(NULL); - if (disp) { - xcb_connection_t *conn = XGetXCBConnection(disp); - if (conn && window != XCB_WINDOW_NONE) { - xcb_void_cookie_t cookie; - cookie = xcb_set_input_focus(conn, XCB_INPUT_FOCUS_PARENT, - window, XCB_CURRENT_TIME); - xcb_flush(conn); - } - XCloseDisplay(disp); - } -#endif -} - -static void SetSkipTaskbar(Display* disp, Window win) -{ - Atom wm_state = XInternAtom(disp, "_NET_WM_STATE", True); - Atom wm_state_skip_taskbar = XInternAtom(disp, "_NET_WM_STATE_SKIP_TASKBAR", True); - if (wm_state != None && wm_state_skip_taskbar != None) - XChangeProperty(disp, win, wm_state, XA_ATOM, 32, PropModeReplace, (const unsigned char*)&wm_state_skip_taskbar, 1); -} - -static void GetWindowName(Display* disp, Window win, char **name) { - XClassHint* class_hint = NULL; - class_hint = XAllocClassHint(); - if (class_hint) { - Status s = XGetClassHint(disp, win, class_hint); - if (s == 1) - *name = strdup(class_hint->res_name); - XFree(class_hint); - } -} - -static void GetWindowList(Display *disp, Window **list, unsigned long *len) { - int form; - unsigned long remain; - unsigned char *win_list; - Atom type; - Atom prop = XInternAtom(disp, "_NET_CLIENT_LIST_STACKING", true); - Window root = XDefaultRootWindow(disp); - int res = XGetWindowProperty(disp, root, prop, 0, 1024, false, XA_WINDOW, - &type, &form, len, &remain, &win_list); - if (res == Success) - *list = (Window*)win_list; + return IPlatformBackend::instance()->isNativeFocus(window); } -static bool IsVisible(Display *disp, Window wnd) +void LinuxWindowUtils::setNativeFocusTo(WId window) { - xcb_connection_t *conn = XGetXCBConnection(disp); - if (conn) { - xcb_get_window_attributes_cookie_t cookie; - xcb_get_window_attributes_reply_t *reply; - cookie = xcb_get_window_attributes(conn, wnd); - reply = xcb_get_window_attributes_reply(conn, cookie, NULL); - if (reply) { - uint8_t state = reply->map_state; - free(reply); - if (state == XCB_MAP_STATE_VIEWABLE) - return true; - } - } - return false; + IPlatformBackend::instance()->setNativeFocusTo(window); } void LinuxWindowUtils::findWindowAsync(const char *window_name, void *user_data, uint timeout_ms, - void(*callback)(xcb_window_t, void*)) + void(*callback)(WId, void*)) { - QtConcurrent::run([=]() { - Display *disp = XOpenDisplay(NULL); - if (!disp) - return; - int DELAY_MS = 50; - int RETRIES = (int)((float)timeout_ms / DELAY_MS); - Window win_found = None; - do { - std::this_thread::sleep_for(std::chrono::milliseconds(DELAY_MS)); - Window *win_list = NULL; - unsigned long win_list_size = 0; - GetWindowList(disp, &win_list, &win_list_size); - for (int i = 0; i < (int)win_list_size; i++) { - char *name = NULL; - GetWindowName(disp, win_list[i], &name); - if (name) { - if (strstr(name, window_name) != NULL) { - if (IsVisible(disp, win_list[i])) { - win_found = win_list[i]; - SetSkipTaskbar(disp, win_found); - callback((xcb_window_t)win_found, user_data); - } - free(name); - break; - } - free(name); - } - } - if (win_list) - XFree(win_list); - } while (--RETRIES > 0 && win_found == None); - XCloseDisplay(disp); - }); + IPlatformBackend::instance()->findWindowAsync(window_name, user_data, timeout_ms, callback); } -void LinuxWindowUtils::getWindowStack(std::vector &winStack) +void LinuxWindowUtils::getWindowStack(std::vector &winStack) { - Display *disp = XOpenDisplay(NULL); - if (!disp) - return; - Window *win_list = NULL; - unsigned long win_list_size = 0; - GetWindowList(disp, &win_list, &win_list_size); - if (win_list) { - for (int i = 0; i < (int)win_list_size; i++) - winStack.push_back((xcb_window_t)win_list[i]); - XFree(win_list); - } + IPlatformBackend::instance()->getWindowStack(winStack); } -void LinuxWindowUtils::setInputEnabled(xcb_window_t window, bool enabled) +void LinuxWindowUtils::setInputEnabled(WId window, bool enabled) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - Display* disp = QX11Info::display(); - Window wnd = window; - if (enabled) { - XShapeCombineMask(disp, wnd, ShapeInput, 0, 0, None, ShapeSet); - } else { - XRectangle rc = {0, 0, 0, 0}; - XShapeCombineRectangles(disp, wnd, ShapeInput, 0, 0, &rc, 1, ShapeSet, YXBanded); - } - XFlush(disp); -#else - Display* disp = XOpenDisplay(NULL); - if (disp) { - Window wnd = window; - if (enabled) { - XShapeCombineMask(disp, wnd, ShapeInput, 0, 0, None, ShapeSet); - } else { - XRectangle rc = {0, 0, 0, 0}; - XShapeCombineRectangles(disp, wnd, ShapeInput, 0, 0, &rc, 1, ShapeSet, YXBanded); - } - XFlush(disp); - XCloseDisplay(disp); - } -#endif + IPlatformBackend::instance()->setInputEnabled(window, enabled); } diff --git a/win-linux/src/platform_linux/linux_window_utils.h b/win-linux/src/platform_linux/linux_window_utils.h index 4ac70c8f5..93336a414 100644 --- a/win-linux/src/platform_linux/linux_window_utils.h +++ b/win-linux/src/platform_linux/linux_window_utils.h @@ -26,20 +26,20 @@ #ifndef LINUX_WINDOW_UTILS_H #define LINUX_WINDOW_UTILS_H -#include +#include #include namespace LinuxWindowUtils { -void moveWindow(xcb_window_t window, int x, int y); -void setNativeFocusTo(xcb_window_t window); -bool isNativeFocus(xcb_window_t window); +void moveWindow(WId window, int x, int y); +void setNativeFocusTo(WId window); +bool isNativeFocus(WId window); void findWindowAsync(const char *window_name, void *user_data, uint timeout_ms, - void(*callback)(xcb_window_t, void*)); -void getWindowStack(std::vector &winStack); -void setInputEnabled(xcb_window_t window, bool enabled); + void(*callback)(WId, void*)); +void getWindowStack(std::vector &winStack); +void setInputEnabled(WId window, bool enabled); } #endif // LINUX_WINDOW_UTILS_H diff --git a/win-linux/src/platform_linux/platformbackendfactory.cpp b/win-linux/src/platform_linux/platformbackendfactory.cpp new file mode 100644 index 000000000..dc96c8743 --- /dev/null +++ b/win-linux/src/platform_linux/platformbackendfactory.cpp @@ -0,0 +1,45 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#include "iplatformbackend.h" +#include "x11backend.h" +#include "waylandbackend.h" +#include + +static IPlatformBackend* s_instance = nullptr; + +IPlatformBackend* IPlatformBackend::create() +{ + if (QGuiApplication::platformName() == "wayland") + return new WaylandBackend(); + return new X11Backend(); +} + +IPlatformBackend* IPlatformBackend::instance() +{ + if (!s_instance) + s_instance = create(); + return s_instance; +} diff --git a/win-linux/src/platform_linux/waylandbackend.cpp b/win-linux/src/platform_linux/waylandbackend.cpp new file mode 100644 index 000000000..5a4781750 --- /dev/null +++ b/win-linux/src/platform_linux/waylandbackend.cpp @@ -0,0 +1,170 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#include "waylandbackend.h" +#include +#include + + +WaylandBackend::WaylandBackend() +{ +} + +WaylandBackend::~WaylandBackend() +{ +} + +void WaylandBackend::moveWindow(WId window, int x, int y) +{ + // Wayland does not allow clients to position their own windows. + Q_UNUSED(window); + Q_UNUSED(x); + Q_UNUSED(y); +} + +bool WaylandBackend::isNativeFocus(WId window) +{ + QWindow *focusWindow = QGuiApplication::focusWindow(); + if (!focusWindow) + return false; + return focusWindow->winId() == window; +} + +void WaylandBackend::setNativeFocusTo(WId window) +{ + QWidget *widget = QWidget::find(window); + if (widget && widget->windowHandle()) + widget->windowHandle()->requestActivate(); +} + +void WaylandBackend::setInputEnabled(WId window, bool enabled) +{ + // XShape is not available on Wayland; use Qt's setEnabled as a simplified fallback. + QWidget *widget = QWidget::find(window); + if (widget) + widget->setEnabled(enabled); +} + +void WaylandBackend::getWindowStack(std::vector &winStack) +{ + // Window stacking order is not available on Wayland. + // Consumers should use internal tracking instead. + Q_UNUSED(winStack); +} + +void WaylandBackend::findWindowAsync(const char *name, void *data, + uint timeout_ms, + void(*cb)(WId, void*)) +{ + // Enumerating foreign windows is not possible on Wayland. + Q_UNUSED(name); + Q_UNUSED(data); + Q_UNUSED(timeout_ms); + Q_UNUSED(cb); +} + +void WaylandBackend::raiseWindow(QWidget *window) +{ + // Best-effort: raise and request activation via Qt APIs. + window->raise(); + window->activateWindow(); +} + +void WaylandBackend::minimizeWindow(QWidget *window) +{ + window->showMinimized(); +} + +void WaylandBackend::setCursorPos(int x, int y) +{ + // Warping the cursor is not allowed on Wayland. + Q_UNUSED(x); + Q_UNUSED(y); +} + +void WaylandBackend::sendButtonRelease(QWidget *window) +{ + // Synthetic input events are not possible on Wayland. + Q_UNUSED(window); +} + +void WaylandBackend::startInteractiveMove(QWidget *window, const QPoint &globalPos) +{ + Q_UNUSED(globalPos); + if (window && window->windowHandle()) + window->windowHandle()->startSystemMove(); +} + +void WaylandBackend::startInteractiveResize(QWidget *window, Qt::Edges edges, const QPoint &globalPos) +{ + Q_UNUSED(globalPos); + if (window && window->windowHandle()) + window->windowHandle()->startSystemResize(edges); +} + +void WaylandBackend::setCursor(WId window, int cursorShape) +{ + Qt::CursorShape qt_shape = Qt::ArrowCursor; + switch (cursorShape) { + case 0: qt_shape = Qt::SizeFDiagCursor; break; // Top-left + case 1: qt_shape = Qt::SizeVerCursor; break; // Top + case 2: qt_shape = Qt::SizeBDiagCursor; break; // Top-right + case 3: qt_shape = Qt::SizeHorCursor; break; // Right + case 4: qt_shape = Qt::SizeFDiagCursor; break; // Bottom-right + case 5: qt_shape = Qt::SizeVerCursor; break; // Bottom + case 6: qt_shape = Qt::SizeBDiagCursor; break; // Bottom-left + case 7: qt_shape = Qt::SizeHorCursor; break; // Left + default: qt_shape = Qt::CursorShape(cursorShape); break; // fallback + } + QWidget *widget = QWidget::find(window); + if (widget) + widget->setCursor(qt_shape); +} + +void WaylandBackend::resetCursor(WId window) +{ + QWidget *widget = QWidget::find(window); + if (widget) + widget->unsetCursor(); +} + +bool WaylandBackend::isCompositingAvailable() +{ + // Wayland is inherently composited. + return true; +} + +bool WaylandBackend::checkButtonState(Qt::MouseButton b) +{ + return QGuiApplication::mouseButtons().testFlag(b); +} + +QString WaylandBackend::portalParentHandle(QWidget *parent) +{ + // XDG Desktop Portal works without a parent window handle on Wayland. + // Full xdg_foreign support can be added later if needed. + Q_UNUSED(parent); + return QString(); +} diff --git a/win-linux/src/platform_linux/waylandbackend.h b/win-linux/src/platform_linux/waylandbackend.h new file mode 100644 index 000000000..53166add2 --- /dev/null +++ b/win-linux/src/platform_linux/waylandbackend.h @@ -0,0 +1,66 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#ifndef WAYLANDBACKEND_H +#define WAYLANDBACKEND_H + +#include "iplatformbackend.h" + +class WaylandBackend : public IPlatformBackend { +public: + WaylandBackend(); + ~WaylandBackend() override; + + // Window management + void moveWindow(WId window, int x, int y) override; + bool isNativeFocus(WId window) override; + void setNativeFocusTo(WId window) override; + void setInputEnabled(WId window, bool enabled) override; + void getWindowStack(std::vector &winStack) override; + void findWindowAsync(const char *name, void *data, + uint timeout_ms, + void(*cb)(WId, void*)) override; + + // Decoration / WM interaction + void raiseWindow(QWidget *window) override; + void minimizeWindow(QWidget *window) override; + void setCursorPos(int x, int y) override; + void sendButtonRelease(QWidget *window) override; + void startInteractiveMove(QWidget *window, const QPoint &globalPos) override; + void startInteractiveResize(QWidget *window, Qt::Edges edges, const QPoint &globalPos) override; + + // Cursor management + void setCursor(WId window, int cursorShape) override; + void resetCursor(WId window) override; + + // Queries + bool isCompositingAvailable() override; + bool checkButtonState(Qt::MouseButton b) override; + + // XDG Portal parent window handle + QString portalParentHandle(QWidget *parent) override; +}; + +#endif // WAYLANDBACKEND_H diff --git a/win-linux/src/platform_linux/x11backend.cpp b/win-linux/src/platform_linux/x11backend.cpp new file mode 100644 index 000000000..89dcaf99d --- /dev/null +++ b/win-linux/src/platform_linux/x11backend.cpp @@ -0,0 +1,459 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#include "x11backend.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +namespace { + +auto getXDisplay() -> Display * { +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + if (auto *x11 = qGuiApp ? qGuiApp->nativeInterface() : nullptr) + return x11->display(); + return nullptr; +#else + return QX11Info::display(); +#endif +} + +void SetSkipTaskbar(Display* disp, Window win) +{ + Atom wm_state = XInternAtom(disp, "_NET_WM_STATE", True); + Atom wm_state_skip_taskbar = XInternAtom(disp, "_NET_WM_STATE_SKIP_TASKBAR", True); + if (wm_state != None && wm_state_skip_taskbar != None) + XChangeProperty(disp, win, wm_state, XA_ATOM, 32, PropModeReplace, (const unsigned char*)&wm_state_skip_taskbar, 1); +} + +void GetWindowName(Display* disp, Window win, char **name) { + XClassHint* class_hint = NULL; + class_hint = XAllocClassHint(); + if (class_hint) { + Status s = XGetClassHint(disp, win, class_hint); + if (s == 1) + *name = strdup(class_hint->res_name); + XFree(class_hint); + } +} + +void GetWindowList(Display *disp, Window **list, unsigned long *len) { + int form; + unsigned long remain; + unsigned char *win_list; + Atom type; + Atom prop = XInternAtom(disp, "_NET_CLIENT_LIST_STACKING", true); + Window root = XDefaultRootWindow(disp); + int res = XGetWindowProperty(disp, root, prop, 0, 1024, false, XA_WINDOW, + &type, &form, len, &remain, &win_list); + if (res == Success) + *list = (Window*)win_list; +} + +bool IsVisible(Display *disp, Window wnd) +{ + xcb_connection_t *conn = XGetXCBConnection(disp); + if (conn) { + xcb_get_window_attributes_cookie_t cookie; + xcb_get_window_attributes_reply_t *reply; + cookie = xcb_get_window_attributes(conn, wnd); + reply = xcb_get_window_attributes_reply(conn, cookie, NULL); + if (reply) { + uint8_t state = reply->map_state; + free(reply); + if (state == XCB_MAP_STATE_VIEWABLE) + return true; + } + } + return false; +} + +int edgesToMoveResizeDirection(Qt::Edges edges) +{ + // _NET_WM_MOVERESIZE direction constants + const int SIZE_TOPLEFT = 0; + const int SIZE_TOP = 1; + const int SIZE_TOPRIGHT = 2; + const int SIZE_RIGHT = 3; + const int SIZE_BOTTOMRIGHT = 4; + const int SIZE_BOTTOM = 5; + const int SIZE_BOTTOMLEFT = 6; + const int SIZE_LEFT = 7; + + if (edges == (Qt::TopEdge | Qt::LeftEdge)) return SIZE_TOPLEFT; + if (edges == Qt::TopEdge) return SIZE_TOP; + if (edges == (Qt::TopEdge | Qt::RightEdge)) return SIZE_TOPRIGHT; + if (edges == Qt::RightEdge) return SIZE_RIGHT; + if (edges == (Qt::BottomEdge | Qt::RightEdge)) return SIZE_BOTTOMRIGHT; + if (edges == Qt::BottomEdge) return SIZE_BOTTOM; + if (edges == (Qt::BottomEdge | Qt::LeftEdge)) return SIZE_BOTTOMLEFT; + if (edges == Qt::LeftEdge) return SIZE_LEFT; + + return -1; +} + +} // anonymous namespace + + +X11Backend::X11Backend() +{ +} + +X11Backend::~X11Backend() +{ +} + +void X11Backend::moveWindow(WId window, int x, int y) +{ + Display *disp = getXDisplay(); + if (disp) { + xcb_connection_t *conn = XGetXCBConnection(disp); + if (conn && (xcb_window_t)window != XCB_WINDOW_NONE) { + uint32_t val[2]; + val[0] = x; + val[1] = y; + xcb_configure_window(conn, (xcb_window_t)window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, val); + xcb_flush(conn); + } + } +} + +bool X11Backend::isNativeFocus(WId window) +{ + xcb_window_t win = 0; + Display *disp = getXDisplay(); + if (disp) { + xcb_connection_t *conn = XGetXCBConnection(disp); + if (conn) { + xcb_get_input_focus_cookie_t cookie; + xcb_get_input_focus_reply_t *reply; + cookie = xcb_get_input_focus(conn); + reply = xcb_get_input_focus_reply(conn, cookie, NULL); + if (reply) { + win = reply->focus; + free(reply); + } + xcb_flush(conn); + } + } + return (xcb_window_t)window == win; +} + +void X11Backend::setNativeFocusTo(WId window) +{ + Display *disp = getXDisplay(); + if (disp) { + xcb_connection_t *conn = XGetXCBConnection(disp); + if (conn && (xcb_window_t)window != XCB_WINDOW_NONE) { + xcb_void_cookie_t cookie; + cookie = xcb_set_input_focus(conn, XCB_INPUT_FOCUS_PARENT, + (xcb_window_t)window, XCB_CURRENT_TIME); + xcb_flush(conn); + } + } +} + +void X11Backend::setInputEnabled(WId window, bool enabled) +{ + Display* disp = getXDisplay(); + if (disp) { + Window wnd = (Window)window; + if (enabled) { + XShapeCombineMask(disp, wnd, ShapeInput, 0, 0, None, ShapeSet); + } else { + XRectangle rc = {0, 0, 0, 0}; + XShapeCombineRectangles(disp, wnd, ShapeInput, 0, 0, &rc, 1, ShapeSet, YXBanded); + } + XFlush(disp); + } +} + +void X11Backend::getWindowStack(std::vector &winStack) +{ + Display *disp = XOpenDisplay(NULL); + if (!disp) + return; + Window *win_list = NULL; + unsigned long win_list_size = 0; + GetWindowList(disp, &win_list, &win_list_size); + if (win_list) { + for (int i = 0; i < (int)win_list_size; i++) + winStack.push_back((WId)win_list[i]); + XFree(win_list); + } + XCloseDisplay(disp); +} + +void X11Backend::findWindowAsync(const char *name, void *data, + uint timeout_ms, + void(*cb)(WId, void*)) +{ + QtConcurrent::run([=]() { + Display *disp = XOpenDisplay(NULL); + if (!disp) + return; + int DELAY_MS = 50; + int RETRIES = (int)((float)timeout_ms / DELAY_MS); + Window win_found = None; + do { + std::this_thread::sleep_for(std::chrono::milliseconds(DELAY_MS)); + Window *win_list = NULL; + unsigned long win_list_size = 0; + GetWindowList(disp, &win_list, &win_list_size); + for (int i = 0; i < (int)win_list_size; i++) { + char *wname = NULL; + GetWindowName(disp, win_list[i], &wname); + if (wname) { + if (strstr(wname, name) != NULL) { + if (IsVisible(disp, win_list[i])) { + win_found = win_list[i]; + SetSkipTaskbar(disp, win_found); + cb((WId)win_found, data); + } + free(wname); + break; + } + free(wname); + } + } + if (win_list) + XFree(win_list); + } while (--RETRIES > 0 && win_found == None); + XCloseDisplay(disp); + }); +} + +void X11Backend::raiseWindow(QWidget *window) +{ + Display *disp = getXDisplay(); + Atom atom_active_wnd = XInternAtom(disp, "_NET_ACTIVE_WINDOW", False); + if (atom_active_wnd == None) + return; + Window wnd = (Window)window->winId(); + Window root = DefaultRootWindow(disp); + XEvent event; + memset(&event, 0, sizeof(XEvent)); + event.xclient.type = ClientMessage; + event.xclient.serial = 0; + event.xclient.send_event = True; + event.xclient.message_type = atom_active_wnd; + event.xclient.window = wnd; + event.xclient.format = 32; + XSendEvent(disp, root, False, SubstructureRedirectMask | SubstructureNotifyMask, &event); + XMapRaised(disp, wnd); + XFlush(disp); +} + +void X11Backend::minimizeWindow(QWidget *window) +{ + Display * xdisplay_ = getXDisplay(); + Atom wm_change_state = XInternAtom(xdisplay_, "WM_CHANGE_STATE", False); + + XClientMessageEvent ev; + ev.type = ClientMessage; + ev.window = window->winId(); + ev.message_type = wm_change_state; + ev.format = 32; + ev.data.l[0] = IconicState; + + XSendEvent(xdisplay_, RootWindow(xdisplay_, DefaultScreen(xdisplay_)), False, + SubstructureRedirectMask|SubstructureNotifyMask, (XEvent *)&ev); + + // Also send a button release to avoid stuck states + sendButtonRelease(window); +} + +void X11Backend::setCursorPos(int x, int y) +{ + Display *xdisplay_ = getXDisplay(); + Window root_window = DefaultRootWindow(xdisplay_); + XSelectInput(xdisplay_, root_window, KeyReleaseMask); + XWarpPointer(xdisplay_, None, root_window, 0, 0, 0, 0, x, y); + XFlush(xdisplay_); +} + +void X11Backend::sendButtonRelease(QWidget *window) +{ + Display * xdisplay_ = getXDisplay(); + Window x_root_window_ = (Window)window->effectiveWinId(); + + XEvent event; + memset(&event, 0, sizeof(XEvent)); + + event.type = ButtonRelease; + event.xbutton.button = Button1; + event.xbutton.same_screen = True; + + XQueryPointer(xdisplay_, x_root_window_, &event.xbutton.root, &event.xbutton.window, + &event.xbutton.x_root, &event.xbutton.y_root, &event.xbutton.x, &event.xbutton.y, &event.xbutton.state); + XSendEvent(xdisplay_, PointerWindow, True, ButtonReleaseMask, &event); + XFlush(xdisplay_); +} + +void X11Backend::startInteractiveMove(QWidget *window, const QPoint &globalPos) +{ + Q_UNUSED(globalPos); + const int MOVE = 8; + + Display * xdisplay_ = getXDisplay(); + Window x_root_window_ = DefaultRootWindow(xdisplay_); + + XUngrabPointer(xdisplay_, CurrentTime); + + Window root_ret, child_ret; // <-- new: query native coords + int rx = 0, ry = 0, wx, wy; + unsigned int mask; + XQueryPointer(xdisplay_, x_root_window_, &root_ret, &child_ret, + &rx, &ry, &wx, &wy, &mask); + + XEvent event; + memset(&event, 0, sizeof(event)); + event.xclient.type = ClientMessage; + event.xclient.display = xdisplay_; + event.xclient.window = window->winId(); + event.xclient.message_type = XInternAtom(xdisplay_, "_NET_WM_MOVERESIZE", False); + event.xclient.format = 32; + event.xclient.data.l[0] = rx; + event.xclient.data.l[1] = ry; + event.xclient.data.l[2] = MOVE; + event.xclient.data.l[3] = Button1; + event.xclient.data.l[4] = 0; + + XSendEvent(xdisplay_, x_root_window_, False, SubstructureRedirectMask | SubstructureNotifyMask, &event); + XFlush(xdisplay_); +} + +void X11Backend::startInteractiveResize(QWidget *window, Qt::Edges edges, const QPoint &globalPos) +{ + Q_UNUSED(globalPos); + int direction = edgesToMoveResizeDirection(edges); + if (direction < 0) + return; + + Display * xdisplay_ = getXDisplay(); + Window x_root_window_ = DefaultRootWindow(xdisplay_); + + XUngrabPointer(xdisplay_, CurrentTime); + + Window root_ret, child_ret; // <-- new: query native coords + int rx = 0, ry = 0, wx, wy; + unsigned int mask; + XQueryPointer(xdisplay_, x_root_window_, &root_ret, &child_ret, + &rx, &ry, &wx, &wy, &mask); + + XEvent event; + memset(&event, 0, sizeof(event)); + event.xclient.type = ClientMessage; + event.xclient.display = xdisplay_; + event.xclient.window = window->winId(); + event.xclient.message_type = XInternAtom(xdisplay_, "_NET_WM_MOVERESIZE", False); + event.xclient.format = 32; + event.xclient.data.l[0] = rx; + event.xclient.data.l[1] = ry; + event.xclient.data.l[2] = direction; + event.xclient.data.l[3] = Button1; + event.xclient.data.l[4] = 0; + + XSendEvent(xdisplay_, x_root_window_, False, SubstructureRedirectMask | SubstructureNotifyMask, &event); + XFlush(xdisplay_); +} + +void X11Backend::setCursor(WId window, int cursorShape) +{ + unsigned int x11_shape = 0; + switch (cursorShape) { + case 0: x11_shape = 134; break; // XC_top_left_corner + case 1: x11_shape = 138; break; // XC_top_side + case 2: x11_shape = 136; break; // XC_top_right_corner + case 3: x11_shape = 96; break; // XC_right_side + case 4: x11_shape = 14; break; // XC_bottom_right_corner + case 5: x11_shape = 16; break; // XC_bottom_side + case 6: x11_shape = 12; break; // XC_bottom_left_corner + case 7: x11_shape = 70; break; // XC_left_side + default: x11_shape = cursorShape; break; // fallback + } + Display * _display = getXDisplay(); + if (_display) { + Cursor cursor = XCreateFontCursor(_display, x11_shape); + XDefineCursor(_display, (Window)window, cursor); + XFlush(_display); + XFreeCursor(_display, cursor); + } +} + +void X11Backend::resetCursor(WId window) +{ + Display * _display = getXDisplay(); + XUndefineCursor(_display, (Window)window); + XFlush(_display); +} + +bool X11Backend::isCompositingAvailable() +{ + static const bool active = [] { + Display *d = getXDisplay(); + if (!d) return true; + char name[32]; + snprintf(name, sizeof(name), "_NET_WM_CM_S%d", DefaultScreen(d)); + Atom a = XInternAtom(d, name, False); + return XGetSelectionOwner(d, a) != None; + }(); + return active; +} + +bool X11Backend::checkButtonState(Qt::MouseButton b) +{ + Display * xdisplay_ = getXDisplay(); + Window x_root_window_ = DefaultRootWindow(xdisplay_); + + Window root_, child_; + int root_x, root_y, child_x, child_y; + uint mask; + + Bool res = XQueryPointer(xdisplay_, x_root_window_, &root_, &child_, + &root_x, &root_y, &child_x, &child_y, &mask); + + if ( res ) { + if ( b == Qt::LeftButton) + return mask & Button1MotionMask; + } + + return false; +} + +QString X11Backend::portalParentHandle(QWidget *parent) +{ + return "x11:" + QString::number((long)parent->winId(), 16); +} diff --git a/win-linux/src/platform_linux/x11backend.h b/win-linux/src/platform_linux/x11backend.h new file mode 100644 index 000000000..98cad2adc --- /dev/null +++ b/win-linux/src/platform_linux/x11backend.h @@ -0,0 +1,66 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#ifndef X11BACKEND_H +#define X11BACKEND_H + +#include "iplatformbackend.h" + +class X11Backend : public IPlatformBackend { +public: + X11Backend(); + ~X11Backend() override; + + // Window management + void moveWindow(WId window, int x, int y) override; + bool isNativeFocus(WId window) override; + void setNativeFocusTo(WId window) override; + void setInputEnabled(WId window, bool enabled) override; + void getWindowStack(std::vector &winStack) override; + void findWindowAsync(const char *name, void *data, + uint timeout_ms, + void(*cb)(WId, void*)) override; + + // Decoration / WM interaction + void raiseWindow(QWidget *window) override; + void minimizeWindow(QWidget *window) override; + void setCursorPos(int x, int y) override; + void sendButtonRelease(QWidget *window) override; + void startInteractiveMove(QWidget *window, const QPoint &globalPos) override; + void startInteractiveResize(QWidget *window, Qt::Edges edges, const QPoint &globalPos) override; + + // Cursor management + void setCursor(WId window, int cursorShape) override; + void resetCursor(WId window) override; + + // Queries + bool isCompositingAvailable() override; + bool checkButtonState(Qt::MouseButton b) override; + + // XDG Portal parent window handle + QString portalParentHandle(QWidget *parent) override; +}; + +#endif // X11BACKEND_H diff --git a/win-linux/src/platform_linux/xdgdesktopportal.cpp b/win-linux/src/platform_linux/xdgdesktopportal.cpp index 1a1941745..0c0962c3b 100644 --- a/win-linux/src/platform_linux/xdgdesktopportal.cpp +++ b/win-linux/src/platform_linux/xdgdesktopportal.cpp @@ -2,6 +2,7 @@ #include "xdgdesktopportal.h" #include "components/cmessage.h" #include "platform_linux/linux_window_utils.h" +#include "iplatformbackend.h" #include #include #include @@ -13,7 +14,9 @@ #include #include #include +#ifdef HAVE_X11 #include +#endif #if defined(__x86_64__) # define GETRANDOM_NR 318 @@ -804,7 +807,7 @@ Result allocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, char* } #endif -Result callXdgPortal(Window parent, Xdg::Mode mode, const char* title, +Result callXdgPortal(QWidget* parent, Xdg::Mode mode, const char* title, DBusMessage* &outMsg, const FilterItem* filterList, uint filterCount, @@ -831,8 +834,9 @@ Result callXdgPortal(Window parent, Xdg::Mode mode, const char* title, DBusMessageIter iter; dbus_message_iter_init_append(methd, &iter); - QString parent_window_qstr = "x11:" + QString::number((long)parent, 16); - char* parent_window = parent_window_qstr.toUtf8().data(); + QString parent_window_qstr = parent ? IPlatformBackend::instance()->portalParentHandle(parent) : QString(); + QByteArray parent_window_ba = parent_window_qstr.toUtf8(); + char* parent_window = parent_window_ba.data(); __dbusAppend(&iter, DBUS_TYPE_STRING, &parent_window); __dbusAppend(&iter, DBUS_TYPE_STRING, &title); @@ -936,7 +940,7 @@ void freePath(char* filePath) { Free(filePath); } -Result openDialog(Window parent, Xdg::Mode mode, const char* title, +Result openDialog(QWidget* parent, Xdg::Mode mode, const char* title, char** outPaths, const FilterItem* filterList, uint filterCount, @@ -1076,7 +1080,7 @@ void Free(void* p) { } } -void onWindowFound(xcb_window_t w, void *user_data) +void onWindowFound(WId w, void *user_data) { if (QWidget *p = (QWidget*)user_data) LinuxWindowUtils::moveWindow(w, p->x() + 20, p->y() + 80); @@ -1107,7 +1111,6 @@ QStringList Xdg::openXdgPortal(QWidget *parent, bool sel_multiple) { initDBus(); - Window parentWid = (parent) ? (Window)parent->winId() : 0L; const int pos = file_name.lastIndexOf('/'); const QString _file_name = (pos != -1) ? file_name.mid(pos + 1) : file_name; const QString _path = (path.isEmpty() && pos != -1) ? file_name.mid(0, pos) : path; @@ -1135,7 +1138,7 @@ QStringList Xdg::openXdgPortal(QWidget *parent, char* outPaths; LinuxWindowUtils::findWindowAsync("xdg-desktop-portal", (void*)parent, 3000, onWindowFound); Result result; - result = openDialog(parentWid, mode, title.toUtf8().data(), + result = openDialog(parent, mode, title.toUtf8().data(), &outPaths, filterItem, filterSize, diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index b085c854c..135bb4810 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -33,11 +33,15 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include #include #include #include @@ -156,6 +160,13 @@ namespace EditorJSVariables { vars_object["os"] = "winxp"; #elif defined(__linux__) vars_object["os"] = "linux"; + // Lets JS distinguish Wayland's OSR-mode CEF from windowed CEF + // (X11, Windows), without inferring it from the mere presence of + // window.AscDesktopEditor -- that exists on every desktop platform. + // Needed wherever behavior differs specifically because CEF has no + // native window to work with, e.g. sdkjs's native clipboard bridge + // and canvas device-scale correction. + vars_object["isWayland"] = (QGuiApplication::platformName() == QLatin1String("wayland")); #endif if ( InputArgs::contains(L"--help-url") ) vars_object["helpUrl"] = QUrl(QString::fromStdWString(InputArgs::argument_value(L"--help-url"))).toString(); @@ -550,12 +561,32 @@ inline double choose_scaling(double s) s > 1 ? 1.25 : 1; } -double Utils::getScreenDpiRatio(int scrnum) +namespace { +// The app sizes its custom chrome by multiplying base constants by the +// ratio these helpers return. Qt then renders those logical sizes at +// devicePixelRatio physical pixels, so whatever Qt already applies must be +// divided out here or the two compound: on a 1.25-scaled display the +// manual 1.25 and Qt's 1.25 produced chrome at 1.5625x, 25% larger than +// the display scale, while the editor content (scaled once, by CEF page +// zoom off the same devicePixelRatio) sat correctly at 1.25x -- the top +// bar looking oversized next to the document. +// +// This applies on X11 as well as Wayland: Qt 6 scales on xcb too +// (measured devicePixelRatio 1.25 with Xft.dpi 120), because main.cpp's +// attempt to disable it uses QT_ENABLE_HIGHDPI_SCALING, removed in Qt 6, +// and AA_DisableHighDpiScaling, guarded to Qt 5. On Windows, where the +// disabling does work, devicePixelRatio is 1 and this divides by 1. +// +// Returns what is left for the app to apply itself: 1.0 when Qt's scaling +// already covers the display scale, >1 only where the detected DPI ratio +// genuinely exceeds it. +double residualManualScale(double detectedRatio, double qtAutoScale) { - unsigned int _dpi_x = 0; - unsigned int _dpi_y = 0; - double nScale = AscAppManager::getInstance().GetMonitorScaleByIndex(scrnum, _dpi_x, _dpi_y); - return choose_scaling(nScale); + if (qtAutoScale <= 0) + return detectedRatio; + const double residual = detectedRatio / qtAutoScale; + return residual < 1.0 ? 1.0 : residual; +} } double Utils::getScreenDpiRatio(const QPoint& pt) @@ -575,7 +606,15 @@ double Utils::getScreenDpiRatioByHWND(int hwnd) unsigned int _dpi_x = 0; unsigned int _dpi_y = 0; double nScale = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)hwnd, _dpi_x, _dpi_y); - return choose_scaling(nScale); + // See residualManualScale. On Windows Qt's own scaling really is + // disabled, so devicePixelRatio is 1 and there is nothing to divide + // out; only Linux needs the lookup (WId is an integer type there). + double qtAutoScale = 1.0; +#ifdef Q_OS_LINUX + if (QWidget * wid = QWidget::find((WId)hwnd)) + qtAutoScale = wid->devicePixelRatio(); +#endif + return residualManualScale(choose_scaling(nScale), qtAutoScale); } double Utils::getScreenDpiRatioByWidget(QWidget* wid) @@ -594,11 +633,136 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif + // See residualManualScale: divide out the scaling Qt performs itself, + // so the caller's manual multiplication doesn't compound with it. if ( dpiApp >= 0 ) { - return choose_scaling(dpiApp); + return residualManualScale(choose_scaling(dpiApp), wid->devicePixelRatio()); } - return wid->devicePixelRatio(); + return 1.0; +} + +namespace { +class CDpiChangeWatcher : public QObject +{ +public: + CDpiChangeWatcher(QWidget* w, std::function onChange) + : QObject(w), m_widget(w), m_onChange(std::move(onChange)) + { + // Collapse a burst of triggers (screenChanged + a DPR change + the + // new screen's geometry settling all arrive together when a window + // is dragged across a monitor boundary) into a single rescale, and + // keep the resulting setGeometry() out of the window manager's own + // move/resize handling by deferring it to the next event loop pass. + m_debounce.setSingleShot(true); + m_debounce.setInterval(80); + connect(&m_debounce, &QTimer::timeout, this, [this]() { + // updateScaling() resizes and re-centers the window, which can + // itself emit geometryChanged/screenChanged. Don't let that feed + // back in; the caller's own "did the ratio actually change?" + // check is what decides whether a further pass is needed. + if (m_inChange) + return; + m_inChange = true; + m_onChange(); + m_inChange = false; + }); + + w->installEventFilter(this); + attachToWindow(); + } + +protected: + bool eventFilter(QObject* watched, QEvent* event) override + { + switch (event->type()) { + // Wayland: the compositor reports a per-surface (fractional) scale, + // so a real scale change always surfaces as a DPR change. + case QEvent::DevicePixelRatioChange: + schedule(); + break; + + // The QWindow only exists once the widget acquires a native handle, + // which happens after this watcher is constructed. Re-attach on every + // event that can create or replace it. + case QEvent::Show: + case QEvent::WinIdChange: + case QEvent::PlatformSurface: + attachToWindow(); + break; + + default: + break; + } + return QObject::eventFilter(watched, event); + } + +private: + void attachToWindow() + { + QWindow* wnd = m_widget ? m_widget->windowHandle() : nullptr; + if (!wnd || wnd == m_window) + return; + + if (m_window) + m_window->disconnect(this); + m_window = wnd; + + // xcb: Qt's own scaling is deliberately neutralized on X11 (see + // main.cpp -- QT_ENABLE_HIGHDPI_SCALING=0 + AA_Use96Dpi), so + // devicePixelRatio() is pinned at 1.0 on every screen and + // DevicePixelRatioChange never fires. The app derives its scale + // per-screen itself (QDpiChecker::GetMonitorDpi), so the value does + // change when the window moves -- it just needs a trigger to be + // re-read. screenChanged is that trigger, and it works on Wayland + // too, so it is not gated by platform. + connect(wnd, &QWindow::screenChanged, this, [this](QScreen*) { + attachToScreen(); + schedule(); + }); + + attachToScreen(); + } + + void attachToScreen() + { + QScreen* scr = m_window ? m_window->screen() : nullptr; + if (!scr || scr == m_screen) + return; + + if (m_screen) + m_screen->disconnect(this); + m_screen = scr; + + // The window can also stay put while the monitor under it changes + // resolution or DPI (xrandr, display settings, docking). None of + // these produce a DPR change on X11 either. + auto rescale = [this]() { schedule(); }; + connect(scr, &QScreen::physicalDotsPerInchChanged, this, rescale); + connect(scr, &QScreen::logicalDotsPerInchChanged, this, rescale); + connect(scr, &QScreen::geometryChanged, this, rescale); + } + + void schedule() + { + if (!m_inChange) + m_debounce.start(); + } + + QPointer m_widget; + QPointer m_window; + QPointer m_screen; + QTimer m_debounce; + bool m_inChange = false; + std::function m_onChange; +}; +} + +void Utils::WatchForDpiChange(QWidget* w, std::function onChange) +{ + if (!w) + return; + new CDpiChangeWatcher(w, std::move(onChange)); } QScreen * Utils::screenAt(const QPoint& pt) @@ -980,6 +1144,11 @@ namespace WindowHelper { } auto useGtkDialog() -> bool { + // On Wayland, prefer XDG Desktop Portal over GTK to avoid + // GTK dialogs falling back to Xwayland (wrong scale / click offset). + if (QGuiApplication::platformName() == "wayland") + return false; + GET_REGISTRY_USER(reg_user) bool use_gtk_dialog = true; bool saved_flag = reg_user.value("--xdg-desktop-portal", false).toBool(); diff --git a/win-linux/src/utils.h b/win-linux/src/utils.h index 523e8ce55..6247879a1 100644 --- a/win-linux/src/utils.h +++ b/win-linux/src/utils.h @@ -29,6 +29,7 @@ #include #include #include +#include #ifdef Q_OS_WIN # include #endif @@ -90,10 +91,27 @@ class Utils { static void openUrl(const QString&); static void openFileLocation(const QString&); static QString getPortalName(const QString&); - static double getScreenDpiRatio(int); static double getScreenDpiRatio(const QPoint&); static double getScreenDpiRatioByHWND(int); static double getScreenDpiRatioByWidget(QWidget*); + + // Installs a watcher (child of w, cleaned up automatically with it) that + // calls onChange whenever the display scale w should be drawn at may + // have changed. Triggers on: + // * QEvent::DevicePixelRatioChange -- Wayland's per-surface scale, + // including the async compositor answer arriving after the widget's + // initial DPI-dependent sizing was computed from a transient reading; + // * QWindow::screenChanged -- the window moved to another monitor. + // This is the only trigger on X11, where Qt's own scaling is + // deliberately neutralized (main.cpp) so devicePixelRatio() is a + // constant 1.0 and never reports a change, even though the app's + // own per-screen scale (QDpiChecker::GetMonitorDpi) does differ; + // * the current screen's DPI/geometry changing under a stationary + // window (xrandr, display settings, docking). + // Calls are debounced and non-reentrant, so onChange may itself resize + // or move w. Safe to call multiple times on the same widget; each call + // adds an independent watcher. + static void WatchForDpiChange(QWidget* w, std::function onChange); static QScreen * screenAt(const QPoint&); static QString replaceBackslash(const QString&); static std::wstring normalizeAppProtocolUrl(const std::wstring &url); diff --git a/win-linux/src/windows/ceditorwindow.cpp b/win-linux/src/windows/ceditorwindow.cpp index 9e8628d74..5348ec5ee 100644 --- a/win-linux/src/windows/ceditorwindow.cpp +++ b/win-linux/src/windows/ceditorwindow.cpp @@ -28,6 +28,7 @@ #include "iconfactory.h" #include "defines.h" #include +#include #include #define CAPTURED_WINDOW_OFFSET_X (6*TOOLBTN_WIDTH + 10) * m_dpiRatio @@ -176,7 +177,9 @@ void CEditorWindow::undock(bool maximized) if (isCustomWindowStyle()) { m_restoreMaximized = false; CWindowPlatform::show(false); - captureMouse(); + if (QGuiApplication::platformName() != "wayland") { + captureMouse(); + } } else { CWindowPlatform::show(false); } diff --git a/win-linux/src/windows/cmainwindow.cpp b/win-linux/src/windows/cmainwindow.cpp index c2dc25786..b0a4dad57 100644 --- a/win-linux/src/windows/cmainwindow.cpp +++ b/win-linux/src/windows/cmainwindow.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/win-linux/src/windows/cpresenterwindow.cpp b/win-linux/src/windows/cpresenterwindow.cpp index 15c9f33fa..6fe2ec533 100644 --- a/win-linux/src/windows/cpresenterwindow.cpp +++ b/win-linux/src/windows/cpresenterwindow.cpp @@ -29,6 +29,7 @@ #include "defines.h" #include "utils.h" #include +#include #include #include diff --git a/win-linux/src/windows/cwindowbase.cpp b/win-linux/src/windows/cwindowbase.cpp index 8d125efe6..021270698 100644 --- a/win-linux/src/windows/cwindowbase.cpp +++ b/win-linux/src/windows/cwindowbase.cpp @@ -33,6 +33,7 @@ # include "windows/platform_win/caption.h" #endif #include +#include #include #include #include @@ -67,9 +68,16 @@ CWindowBase::CWindowBase(const QRect& rect) { setWindowIcon(Utils::appIcon()); m_window_rect = startRect(rect, m_dpiRatio); + setMinimumSize(WINDOW_MIN_WIDTH * m_dpiRatio, WINDOW_MIN_HEIGHT * m_dpiRatio); #ifdef __linux__ setGeometry(m_window_rect); // for Windows is set in CWindowPlatform + + // m_dpiRatio above may have been computed from a transiently-wrong + // devicePixelRatio() (e.g. the startup Wayland compositor round-trip + // hasn't completed yet) -- re-derive and re-apply once Qt tells us + // this window's own ratio actually changed. + Utils::WatchForDpiChange(this, [this]() { updateScaling(); }); #endif } @@ -190,7 +198,13 @@ void CWindowBase::saveWindowState(const QString &baseKey) { if (!windowState().testFlag(Qt::WindowFullScreen)) { GET_REGISTRY_USER(reg_user) - reg_user.setValue(baseKey + "position", normalGeometry()); + if (QGuiApplication::platformName() == "wayland") { + QRect rect = normalGeometry(); + rect.moveTo(0, 0); + reg_user.setValue(baseKey + "position", rect); + } else { + reg_user.setValue(baseKey + "position", normalGeometry()); + } if (windowState().testFlag(Qt::WindowMaximized)) { reg_user.setValue(baseKey + "maximized", true); } else { diff --git a/win-linux/src/windows/platform_linux/cplatformdecoration.cpp b/win-linux/src/windows/platform_linux/cplatformdecoration.cpp index 57938ec58..861a190c8 100644 --- a/win-linux/src/windows/platform_linux/cplatformdecoration.cpp +++ b/win-linux/src/windows/platform_linux/cplatformdecoration.cpp @@ -32,10 +32,13 @@ #endif #include #include +#ifdef HAVE_X11 #include "X11/Xlib.h" #include "X11/cursorfont.h" #include +#endif #include "platform_linux/linux_window_utils.h" +#include "platform_linux/iplatformbackend.h" #define CUSTOM_BORDER_WIDTH MAIN_WINDOW_BORDER_WIDTH #define MOTION_TIMER_MS 250 @@ -50,6 +53,7 @@ const int k_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT = 6; const int k_NET_WM_MOVERESIZE_SIZE_LEFT = 7; const int k_NET_WM_MOVERESIZE_MOVE = 8; +#ifdef HAVE_X11 #define MWM_HINTS_DECORATIONS 2 typedef struct { unsigned long flags; @@ -209,7 +213,7 @@ namespace { return false; } - auto supports_ewmh() -> bool { + bool supports_ewmh() { static bool supports_ewmh = false; static bool supports_ewmh_cached = false; if (!supports_ewmh_cached) { @@ -229,7 +233,7 @@ namespace { return supports_ewmh; } - auto get_window_manager_name(std::string* wm_name) -> bool { + bool get_window_manager_name(std::string* wm_name) { if ( supports_ewmh() ) { int wm_window = 0; if (get_int_property(getRootWindow(), "_NET_SUPPORTING_WM_CHECK", &wm_window)) { @@ -240,7 +244,7 @@ namespace { return false; } - auto guess_window_manager() -> Platform_WindowManagerName { + Platform_WindowManagerName guess_window_manager() { std::string name; if (!get_window_manager_name(&name)) return WM_UNNAMED; if (name == "awesome") return WM_AWESOME; @@ -267,27 +271,12 @@ namespace { if (name == "xmonad") return WM_XMONAD; return Platform_WindowManagerName::WM_OTHER; } - } +#endif namespace WindowHelper { auto check_button_state(Qt::MouseButton b) -> bool { - Display * xdisplay_ = getXDisplay(); - Window x_root_window_ = DefaultRootWindow(xdisplay_); - - Window root_, child_; - int root_x, root_y, child_x, child_y; - uint mask; - - Bool res = XQueryPointer(xdisplay_, x_root_window_, &root_, &child_, - &root_x, &root_y, &child_x, &child_y, &mask); - - if ( res ) { - if ( b == Qt::LeftButton) - return mask & Button1MotionMask; - } - - return false; + return IPlatformBackend::instance()->checkButtonState(b); } } @@ -295,23 +284,25 @@ CPlatformDecoration::CPlatformDecoration(QWidget * w) : m_window(w) , m_title(NULL) , m_motionTimer(nullptr) - , m_currentCursor(0) + , m_currentCursor(-1) , m_decoration(true) , m_nBorderSize(CUSTOM_BORDER_WIDTH) , m_bIsMaximized(false) , m_startSize(QSize()) { - createCursors(); m_nDirection = -1; - need_to_check_motion = guess_window_manager() == WM_KWIN; +#ifdef HAVE_X11 + need_to_check_motion = (QGuiApplication::platformName() == "xcb") && (guess_window_manager() == WM_KWIN); +#else + need_to_check_motion = false; +#endif dpi_ratio = Utils::getScreenDpiRatioByWidget(w); m_nBorderSize = CUSTOM_BORDER_WIDTH * dpi_ratio; } CPlatformDecoration::~CPlatformDecoration() { - freeCursors(); if ( m_motionTimer ) { m_motionTimer->stop(); m_motionTimer->deleteLater(); @@ -325,29 +316,6 @@ void CPlatformDecoration::setTitleWidget(QWidget * w) m_title->setMouseTracking(true); } -void CPlatformDecoration::createCursors() -{ - m_cursors[k_NET_WM_MOVERESIZE_SIZE_TOPLEFT] = XCreateFontCursor(getXDisplay(), XC_top_left_corner); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_TOP] = XCreateFontCursor(getXDisplay(), XC_top_side); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_TOPRIGHT] = XCreateFontCursor(getXDisplay(), XC_top_right_corner); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_RIGHT] = XCreateFontCursor(getXDisplay(), XC_right_side); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT] = XCreateFontCursor(getXDisplay(), XC_bottom_right_corner); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_BOTTOM] = XCreateFontCursor(getXDisplay(), XC_bottom_side); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT] = XCreateFontCursor(getXDisplay(), XC_bottom_left_corner); - m_cursors[k_NET_WM_MOVERESIZE_SIZE_LEFT] = XCreateFontCursor(getXDisplay(), XC_left_side); -} - -void CPlatformDecoration::freeCursors() -{ - Display * _display = getXDisplay(); - std::for_each(m_cursors.begin(), m_cursors.end(), - [_display](std::pair i) { - if (_display) - XFreeCursor(_display, i.second); - } - ); -} - int CPlatformDecoration::hitTest(int x, int y) const { if (m_bIsMaximized) @@ -399,25 +367,16 @@ void CPlatformDecoration::checkCursor(QPoint & p) { int _hit_test = hitTest(p.x(), p.y()); - Cursor _cursor = 0; - if (!(_hit_test < 0)) { - _cursor = m_cursors[_hit_test]; - } - - Display * _display = getXDisplay(); - if (_cursor) { - if (m_currentCursor == 0 || m_currentCursor != _cursor) { - m_currentCursor = _cursor; - XDefineCursor(_display, m_window->winId(), _cursor); - - XFlush(_display); + if (_hit_test >= 0) { + if (m_currentCursor != _hit_test) { + m_currentCursor = _hit_test; + IPlatformBackend::instance()->setCursor(m_window->winId(), _hit_test); + } + } else { + if (m_currentCursor != -1) { + m_currentCursor = -1; + IPlatformBackend::instance()->resetCursor(m_window->winId()); } - } else - if (m_currentCursor) { - m_currentCursor = 0; - XUndefineCursor(_display, m_window->winId()); - - XFlush(_display); } } @@ -451,46 +410,34 @@ void CPlatformDecoration::dispatchMouseMove(QMouseEvent *e) } } else { m_motionTimer->stop(); - sendButtonRelease(); + IPlatformBackend::instance()->sendButtonRelease(m_window); QApplication::postEvent(m_window, new QEvent(static_cast(UM_ENDMOVE))); m_window->activateWindow(); -// QTimer::singleShot(25, [=]() { -// if (m_window->size() == m_startSize) -// QApplication::postEvent(m_window, new QEvent(QEvent::User)); -// }); } }); } if (m_nDirection >= 0 && e->buttons() == Qt::LeftButton) { - Display * xdisplay_ = getXDisplay(); - Window x_root_window_ = DefaultRootWindow(xdisplay_); - - XUngrabPointer(xdisplay_, CurrentTime); if ( !m_motionTimer->isActive() ) m_motionTimer->start(MOTION_TIMER_MS); - XEvent event; - memset(&event, 0, sizeof(event)); - event.xclient.type = ClientMessage; - event.xclient.display = xdisplay_; - event.xclient.window = m_window->winId(); -// event.xclient.message_type = XInternAtom(xdisplay_, "_NET_WM_MOVERESIZE", false); - event.xclient.message_type = GetAtom("_NET_WM_MOVERESIZE"); - event.xclient.format = 32; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - event.xclient.data.l[0] = e->globalPosition().toPoint().x(); - event.xclient.data.l[1] = e->globalPosition().toPoint().y(); -#else - event.xclient.data.l[0] = e->globalPos().x(); - event.xclient.data.l[1] = e->globalPos().y(); -#endif - event.xclient.data.l[2] = m_nDirection; - event.xclient.data.l[3] = Button1; - event.xclient.data.l[4] = 0; - - XSendEvent(xdisplay_, x_root_window_, False, SubstructureRedirectMask | SubstructureNotifyMask, &event); - XFlush(xdisplay_); + QPoint globalPos = e->globalPosition().toPoint(); + if (m_nDirection == k_NET_WM_MOVERESIZE_MOVE) { + IPlatformBackend::instance()->startInteractiveMove(m_window, globalPos); + } else { + Qt::Edges edges = Qt::Edges(); + switch (m_nDirection) { + case k_NET_WM_MOVERESIZE_SIZE_TOPLEFT: edges = Qt::TopEdge | Qt::LeftEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_TOP: edges = Qt::TopEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_TOPRIGHT: edges = Qt::TopEdge | Qt::RightEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_RIGHT: edges = Qt::RightEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT: edges = Qt::BottomEdge | Qt::RightEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_BOTTOM: edges = Qt::BottomEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT: edges = Qt::BottomEdge | Qt::LeftEdge; break; + case k_NET_WM_MOVERESIZE_SIZE_LEFT: edges = Qt::LeftEdge; break; + } + IPlatformBackend::instance()->startInteractiveResize(m_window, edges, globalPos); + } m_startSize = m_window->size(); m_nDirection = -1; } @@ -526,6 +473,7 @@ void CPlatformDecoration::turnOff() m_decoration = false; } +#ifdef HAVE_X11 void CPlatformDecoration::switchDecoration(bool on) { if (m_decoration != on) { @@ -548,6 +496,11 @@ void CPlatformDecoration::switchDecoration(bool on) } } } +#else +void CPlatformDecoration::switchDecoration(bool) +{ +} +#endif bool CPlatformDecoration::isDecorated() { @@ -577,66 +530,20 @@ int CPlatformDecoration::customWindowBorderWith() void CPlatformDecoration::raiseWindow() { - Display *disp = getXDisplay(); - Atom atom_active_wnd = XInternAtom(disp, "_NET_ACTIVE_WINDOW", False); - if (atom_active_wnd == None) - return; - Window wnd = (Window)m_window->winId(); - Window root = DefaultRootWindow(disp); - XEvent event; - memset(&event, 0, sizeof(XEvent)); - event.xclient.type = ClientMessage; - event.xclient.serial = 0; - event.xclient.send_event = True; - event.xclient.message_type = atom_active_wnd; - event.xclient.window = wnd; - event.xclient.format = 32; - XSendEvent(disp, root, False, SubstructureRedirectMask | SubstructureNotifyMask, &event); - XMapRaised(disp, wnd); - XFlush(disp); + IPlatformBackend::instance()->raiseWindow(m_window); } void CPlatformDecoration::sendButtonRelease() { - Display * xdisplay_ = getXDisplay(); - Window x_root_window_ = (Window)m_window->effectiveWinId(); - - XEvent event; - memset(&event, 0, sizeof(XEvent)); - - event.type = ButtonRelease; - event.xbutton.button = Button1; - event.xbutton.same_screen = True; - -// event.xbutton.root = x_root_window_; -// event.xbutton.window = m_window->winId(); - - XQueryPointer(xdisplay_, x_root_window_, &event.xbutton.root, &event.xbutton.window, - &event.xbutton.x_root, &event.xbutton.y_root, &event.xbutton.x, &event.xbutton.y, &event.xbutton.state); - XSendEvent(xdisplay_, PointerWindow, True, ButtonReleaseMask, &event); - XFlush(xdisplay_); + IPlatformBackend::instance()->sendButtonRelease(m_window); } void CPlatformDecoration::setCursorPos(int x, int y) { - Display *xdisplay_= getXDisplay(); - Window root_window = DefaultRootWindow(xdisplay_); - XSelectInput(xdisplay_, root_window, KeyReleaseMask); - XWarpPointer(xdisplay_, None, root_window, 0, 0, 0, 0, x, y); - XFlush(xdisplay_); + IPlatformBackend::instance()->setCursorPos(x, y); } void CPlatformDecoration::setMinimized() { - XClientMessageEvent ev; - ev.type = ClientMessage; - ev.window = m_window->winId(); - ev.message_type = GetAtom("WM_CHANGE_STATE"); - ev.format = 32; - ev.data.l[0] = IconicState; - - Display * xdisplay_ = getXDisplay(); - XSendEvent(xdisplay_, RootWindow(xdisplay_, DefaultScreen(xdisplay_)), False, - SubstructureRedirectMask|SubstructureNotifyMask, (XEvent *)&ev); - sendButtonRelease(); + IPlatformBackend::instance()->minimizeWindow(m_window); } diff --git a/win-linux/src/windows/platform_linux/cplatformdecoration.h b/win-linux/src/windows/platform_linux/cplatformdecoration.h index 61f3dc409..f9a510a56 100644 --- a/win-linux/src/windows/platform_linux/cplatformdecoration.h +++ b/win-linux/src/windows/platform_linux/cplatformdecoration.h @@ -66,17 +66,13 @@ class CPlatformDecoration QWidget * m_window; QWidget * m_title; QTimer * m_motionTimer; - ulong m_currentCursor; + int m_currentCursor; bool m_decoration; int m_nBorderSize; bool m_bIsMaximized; bool need_to_check_motion = false; QSize m_startSize; - std::map m_cursors; - - void createCursors(); - void freeCursors(); int hitTest(int x, int y) const; void checkCursor(QPoint & p); void switchDecoration(bool); diff --git a/win-linux/src/windows/platform_linux/cwindowplatform.cpp b/win-linux/src/windows/platform_linux/cwindowplatform.cpp index 9ea49d040..171588061 100644 --- a/win-linux/src/windows/platform_linux/cwindowplatform.cpp +++ b/win-linux/src/windows/platform_linux/cwindowplatform.cpp @@ -33,7 +33,11 @@ #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #endif +#ifdef HAVE_X11 #include +#endif +#include +#include "platform_linux/iplatformbackend.h" #ifdef DOCUMENTSCORE_OPENSSL_SUPPORT # include "platform_linux/cdialogopenssl.h" @@ -48,11 +52,7 @@ CWindowPlatform::CWindowPlatform(const QRect &rect) : if (AscAppManager::isRtlEnabled()) setLayoutDirection(Qt::RightToLeft); if (isCustomWindowStyle()) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - if (QX11Info::isCompositingManagerRunning()) -#else - if (true) -#endif + if (IPlatformBackend::instance()->isCompositingAvailable()) setAttribute(Qt::WA_TranslucentBackground); CPlatformDecoration::turnOff(); } @@ -129,7 +129,7 @@ bool CWindowPlatform::event(QEvent * event) adjustGeometry(); } else if (event->type() == QEvent::HoverLeave) { - if (m_boxTitleBtns) + if (m_boxTitleBtns && QGuiApplication::platformName() == QLatin1String("wayland")) m_boxTitleBtns->setCursor(QCursor(Qt::ArrowCursor)); } else if (event->type() == QEvent::LayoutDirectionChange) { @@ -140,6 +140,13 @@ bool CWindowPlatform::event(QEvent * event) } else if (event->type() == QEvent::WindowActivate) { onWindowActivate(true); + if (QGuiApplication::platformName() == "wayland") { + focus(); + m_propertyTimer->stop(); + if (property("stabilized").toBool()) + setProperty("stabilized", false); + m_propertyTimer->start(); + } } else if (event->type() == QEvent::WindowDeactivate) { @@ -154,6 +161,7 @@ bool CWindowPlatform::nativeEvent(const QByteArray &ev_type, void *msg, qintptr bool CWindowPlatform::nativeEvent(const QByteArray &ev_type, void *msg, long *res) #endif { +#ifdef HAVE_X11 if (ev_type == "xcb_generic_event_t") { xcb_generic_event_t *ev = static_cast(msg); switch (ev->response_type & ~0x80) { @@ -170,6 +178,7 @@ bool CWindowPlatform::nativeEvent(const QByteArray &ev_type, void *msg, long *re break; } } +#endif return CWindowBase::nativeEvent(ev_type, msg, res); } @@ -181,11 +190,7 @@ void CWindowPlatform::setScreenScalingFactor(double factor, bool resize) void CWindowPlatform::paintEvent(QPaintEvent *event) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - if (!QX11Info::isCompositingManagerRunning()) { -#else - if (false) { -#endif + if (!IPlatformBackend::instance()->isCompositingAvailable()) { CWindowBase::paintEvent(event); return; }