From 05b92d985c03f713105e87cffe94688010b3a2e2 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 3 Jul 2026 17:53:09 -0400 Subject: [PATCH 01/32] feat: Wayland migration and PR fixes Signed-off-by: Peter P. Lupo --- .docker/desktop-apps.bake.Dockerfile | 4 +- win-linux/CMakeLists.txt | 40 +- win-linux/res/icons/logo-dark-eo.svg | 4 +- win-linux/res/icons/logo-light-eo.svg | 4 +- win-linux/res/styles/styles.qss | 4 +- .../cascapplicationmanagerwrapper_private.h | 38 +- win-linux/src/components/cfiledialog.cpp | 3 +- win-linux/src/components/cmenu.cpp | 1 + win-linux/src/components/cmessage.cpp | 8 +- win-linux/src/components/csvgpushbutton.cpp | 6 +- win-linux/src/components/ctabpanel.cpp | 1 + win-linux/src/main.cpp | 70 ++- .../src/platform_linux/gtkfilechooser.cpp | 6 +- .../src/platform_linux/gtkprintdialog.cpp | 4 +- win-linux/src/platform_linux/gtkutils.cpp | 42 +- .../src/platform_linux/iplatformbackend.h | 71 +++ .../src/platform_linux/linux_window_utils.cpp | 226 +-------- .../src/platform_linux/linux_window_utils.h | 14 +- .../platform_linux/platformbackendfactory.cpp | 45 ++ .../src/platform_linux/waylandbackend.cpp | 170 +++++++ win-linux/src/platform_linux/waylandbackend.h | 66 +++ win-linux/src/platform_linux/x11backend.cpp | 440 ++++++++++++++++++ win-linux/src/platform_linux/x11backend.h | 66 +++ .../src/platform_linux/xdgdesktopportal.cpp | 17 +- win-linux/src/utils.cpp | 22 + win-linux/src/windows/ceditorwindow.cpp | 8 +- win-linux/src/windows/cmainwindow.cpp | 4 + win-linux/src/windows/cpresenterwindow.cpp | 4 + win-linux/src/windows/cwindowbase.cpp | 28 +- .../platform_linux/cplatformdecoration.cpp | 197 +++----- .../platform_linux/cplatformdecoration.h | 6 +- .../platform_linux/cwindowplatform.cpp | 28 +- 32 files changed, 1209 insertions(+), 438 deletions(-) create mode 100644 win-linux/src/platform_linux/iplatformbackend.h create mode 100644 win-linux/src/platform_linux/platformbackendfactory.cpp create mode 100644 win-linux/src/platform_linux/waylandbackend.cpp create mode 100644 win-linux/src/platform_linux/waylandbackend.h create mode 100644 win-linux/src/platform_linux/x11backend.cpp create mode 100644 win-linux/src/platform_linux/x11backend.h diff --git a/.docker/desktop-apps.bake.Dockerfile b/.docker/desktop-apps.bake.Dockerfile index c1f20c428..c88bcbbb0 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 \ @@ -81,6 +82,7 @@ FROM core-base AS desktop-linux ENV ABOUT_PAGE_APP_NAME="${COMPANY_NAME} ${PRODUCT_NAME}" RUN pip3 install aqtinstall && \ aqt install-qt linux desktop 6.11.1 linux_gcc_64 -m qtmultimedia qtwebsockets qtwebchannel qtwaylandcompositor --outputdir /qt6 + ENV QT6_ROOT=/qt6/6.11.1/gcc_64 RUN --mount=type=cache,target=/build-cache-desktop,id=build-cache-desktop-${CACHE_BUST} \ --mount=type=cache,target=/nuget-cache,id=nuget-cache-${CACHE_BUST} \ @@ -100,7 +102,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..200ae8035 100644 --- a/win-linux/CMakeLists.txt +++ b/win-linux/CMakeLists.txt @@ -22,6 +22,15 @@ include(${CORE_ROOT_DIR}/common.cmake) # 2. Find Qt Packages +if(NOT DEFINED ENV{QT6_ROOT}) + message(FATAL_ERROR "QT6_ROOT environment variable must be set to the Qt installation prefix (e.g. /qt6/6.11.1/gcc_64)") +endif() +set(QT_ROOT "$ENV{QT6_ROOT}") +set(QT_DIR "${QT_ROOT}/lib/cmake/Qt6") +set(Qt6_DIR "${QT_ROOT}/lib/cmake/Qt6") +find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg LinguistTools Multimedia MultimediaWidgets) + if(UNIX) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS DBus) add_definitions(-DLINUX -D_LINUX -D_WAYLAND) @@ -115,6 +124,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 +290,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 +308,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 +333,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 @@ -386,6 +412,10 @@ target_link_libraries(DesktopEditors PRIVATE # --- System Essentials --- hunspell_wrapper ooxmlsignature + dl + pthread + rt + z ) if(WIN32) @@ -407,9 +437,9 @@ endif() if(UNIX) target_link_libraries(DesktopEditors PRIVATE Qt${QT_VERSION_MAJOR}::DBus - ${LIBICUUC} - ${LIBICUDATA} - ${LIBICUI} + ${LIBICUUC_DESKTOP} + ${LIBICUDATA_DESKTOP} + ${LIBICUI_DESKTOP} ) target_link_options(DesktopEditors PRIVATE "-Wl,-rpath,$ORIGIN" "-Wl,-rpath,$ORIGIN/converter") 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/styles/styles.qss b/win-linux/res/styles/styles.qss index 26c535b52..907e6f6e0 100644 --- a/win-linux/res/styles/styles.qss +++ b/win-linux/res/styles/styles.qss @@ -66,7 +66,7 @@ QPushButton#toolButtonMain { font-weight: bold; border-left: 0px; border-right: 1px solid %7; - qproperty-icon: url(:/logo-light-eo.png); + qproperty-icon: url(:/logo-light-eo.svg); } #mainPanel[rtl=true] QPushButton#toolButtonMain {border-right: 0px; border-left: 1px solid %7;} #mainPanel[win10=true] QPushButton#toolButtonMain {margin-top: 5px;} @@ -94,7 +94,7 @@ QPushButton#toolButtonDownload {border-left: 0px; border-right: 1px solid %6; ma #mainPanel[uithemetype=dark] QPushButton#toolButtonMaximize[class=min] {image: url(:/maximize_light.svg) center no-repeat;} #mainPanel[uithemetype=dark] QPushButton#toolButtonMinimize {image: url(:/minimize_light.svg) center no-repeat;} #mainPanel[uithemetype=dark] QPushButton#toolButtonClose {image: url(:/close_light.svg) center no-repeat;} -#mainPanel[uithemetype=dark] QPushButton#toolButtonMain {qproperty-icon: url(:/logo-dark-eo.png);} +#mainPanel[uithemetype=dark] QPushButton#toolButtonMain {qproperty-icon: url(:/logo-dark-eo.svg);} /* Menu */ 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/cfiledialog.cpp b/win-linux/src/components/cfiledialog.cpp index a88501098..8bbb8d6f9 100644 --- a/win-linux/src/components/cfiledialog.cpp +++ b/win-linux/src/components/cfiledialog.cpp @@ -257,6 +257,7 @@ QStringList CFileDialogWrapper::modalOpen(const QString& path, const QString& fi " (*.docx *.doc *.odt *.ott *.rtf *.docm *.dot *.dotx *.dotm *.fb2 *.fodt *.hml *.wps *.wpt *.xml *.pdf *.djv *.djvu *.md *.sxw *.stw *.xps *.oxps);;" + #endif tr("Spreadsheets") + " (*.xlsx *.xls *.xlsm *.xlsb *.ods *.ots *.xltx *.xltm *.xml *.fods *.et *.ett *.sxc *.numbers);;" + + tr("Databases") + " (*.sqlite *.sqlite3 *.db *.db3 *.duckdb *.parquet *.pq *.mdb *.accdb);;" + tr("Presentations") + " (*.pptx *.ppt *.odp *.odg *.otp *.ppsm *.pptm *.ppsx *.pps *.potx *.pot *.potm *.fodp *.dps *.dpt *.sxi *.key);;" + tr("Visio diagram") + " (*.vsdx *.vssx *.vstx *.vsdm *.vssm *.vstm);;" + tr("Web Page") + " (*.html *.htm *.mht *.mhtml *.epub);;" + @@ -361,7 +362,7 @@ QStringList CFileDialogWrapper::modalOpenDocuments(const QString& path, bool mul QStringList CFileDialogWrapper::modalOpenSpreadsheets(const QString& path, bool multi) { QString filter = m_mapFilters[AVS_OFFICESTUDIO_FILE_UNKNOWN]; - filter.prepend(tr("Spreadsheets") + " (*.xlsx *.xls *.ods *.ots *.csv *.tsv *.xltx *.xltm *.fods *.et *.ett);;"); + filter.prepend(tr("Spreadsheets") + " (*.xlsx *.xls *.ods *.ots *.csv *.tsv *.xltx *.xltm *.fods *.et *.ett);;" + tr("Databases") + " (*.sqlite *.sqlite3 *.db *.db3 *.duckdb *.parquet *.pq *.mdb *.accdb);;"); return modalOpen(path, filter, nullptr, multi); } diff --git a/win-linux/src/components/cmenu.cpp b/win-linux/src/components/cmenu.cpp index 9f9aa046e..f4a83c3c9 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__ diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 3a4e5ad6b..5258f67d5 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -931,8 +931,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/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/main.cpp b/win-linux/src/main.cpp index 271f9e08c..6c58a06d9 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,30 @@ 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) { #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); + } + // 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 +185,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 +212,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..db8f5eb57 --- /dev/null +++ b/win-linux/src/platform_linux/x11backend.cpp @@ -0,0 +1,440 @@ +/* + * (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 * { + static Display* display = NULL; + if ( !display ) + display = XOpenDisplay(NULL); + + return display; +} + +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 = XOpenDisplay(NULL); + 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); + } + XCloseDisplay(disp); + } +} + +bool X11Backend::isNativeFocus(WId window) +{ + xcb_window_t win = 0; + 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); + } + return (xcb_window_t)window == win; +} + +void X11Backend::setNativeFocusTo(WId window) +{ + Display *disp = XOpenDisplay(NULL); + 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); + } + XCloseDisplay(disp); + } +} + +void X11Backend::setInputEnabled(WId window, bool enabled) +{ + Display* disp = XOpenDisplay(NULL); + 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); + XCloseDisplay(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) +{ + const int MOVE = 8; + + Display * xdisplay_ = getXDisplay(); + Window x_root_window_ = DefaultRootWindow(xdisplay_); + + XUngrabPointer(xdisplay_, CurrentTime); + + 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] = globalPos.x(); + event.xclient.data.l[1] = globalPos.y(); + 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) +{ + int direction = edgesToMoveResizeDirection(edges); + if (direction < 0) + return; + + Display * xdisplay_ = getXDisplay(); + Window x_root_window_ = DefaultRootWindow(xdisplay_); + + XUngrabPointer(xdisplay_, CurrentTime); + + 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] = globalPos.x(); + event.xclient.data.l[1] = globalPos.y(); + 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() +{ + // Qt 6 does not provide QX11Info; assume compositing is available + return true; +} + +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..c0e6d2d45 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -552,6 +553,9 @@ inline double choose_scaling(double s) double Utils::getScreenDpiRatio(int scrnum) { + if (qApp && QGuiApplication::platformName() == "wayland") { + return 1.0; + } unsigned int _dpi_x = 0; unsigned int _dpi_y = 0; double nScale = AscAppManager::getInstance().GetMonitorScaleByIndex(scrnum, _dpi_x, _dpi_y); @@ -560,6 +564,9 @@ double Utils::getScreenDpiRatio(int scrnum) double Utils::getScreenDpiRatio(const QPoint& pt) { + if (qApp && QGuiApplication::platformName() == "wayland") { + return 1.0; + } QWidget _w; _w.setGeometry(QRect(pt, QSize(10,10))); @@ -572,6 +579,9 @@ double Utils::getScreenDpiRatio(const QPoint& pt) double Utils::getScreenDpiRatioByHWND(int hwnd) { + if (qApp && QGuiApplication::platformName() == "wayland") { + return 1.0; + } unsigned int _dpi_x = 0; unsigned int _dpi_y = 0; double nScale = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)hwnd, _dpi_x, _dpi_y); @@ -580,6 +590,13 @@ double Utils::getScreenDpiRatioByHWND(int hwnd) double Utils::getScreenDpiRatioByWidget(QWidget* wid) { + // On Wayland, Qt handles all widget scaling natively through + // devicePixelRatio(). The application's manual DPI scaling + // (multiplying sizes by dpiRatio) is only needed on X11. + // Returning 1.0 prevents double-scaling. + if (qApp && QGuiApplication::platformName() == "wayland") { + return 1.0; + } if (!wid) return 1; @@ -980,6 +997,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/windows/ceditorwindow.cpp b/win-linux/src/windows/ceditorwindow.cpp index 9e8628d74..71179aaeb 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); } @@ -504,6 +507,9 @@ bool CEditorWindow::event(QEvent * event) void CEditorWindow::setScreenScalingFactor(double factor, bool resize) { + if (QGuiApplication::platformName() == "wayland") { + factor = 1.0; + } CWindowPlatform::setScreenScalingFactor(factor, resize); CScalingWrapper::updateScalingFactor(factor); if (isCustomWindowStyle()) { diff --git a/win-linux/src/windows/cmainwindow.cpp b/win-linux/src/windows/cmainwindow.cpp index c2dc25786..ccd32e51c 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 @@ -1560,6 +1561,9 @@ void CMainWindow::updateScalingFactor(double dpiratio) void CMainWindow::setScreenScalingFactor(double factor, bool resize) { + if (QGuiApplication::platformName() == "wayland") { + factor = 1.0; + } CWindowPlatform::setScreenScalingFactor(factor, resize); QString css(AscAppManager::getWindowStylesheets(factor)); #ifdef __linux__ diff --git a/win-linux/src/windows/cpresenterwindow.cpp b/win-linux/src/windows/cpresenterwindow.cpp index 15c9f33fa..cc6cebbd2 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 @@ -166,6 +167,9 @@ QWidget * CPresenterWindow::createMainPanel(QWidget * parent, const QString& tit void CPresenterWindow::setScreenScalingFactor(double factor, bool resize) { + if (QGuiApplication::platformName() == "wayland") { + factor = 1.0; + } CWindowPlatform::setScreenScalingFactor(factor, resize); QString css(AscAppManager::getWindowStylesheets(factor)); #ifdef __linux__ diff --git a/win-linux/src/windows/cwindowbase.cpp b/win-linux/src/windows/cwindowbase.cpp index 8d125efe6..ad396f4f5 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 @@ -66,7 +67,15 @@ CWindowBase::CWindowBase(const QRect& rect) , m_windowActivated(false) { setWindowIcon(Utils::appIcon()); - m_window_rect = startRect(rect, m_dpiRatio); + double tempDpi = 1.0; + if (QGuiApplication::platformName() == "wayland") { + m_dpiRatio = 1.0; + m_window_rect = startRect(rect, tempDpi); + m_dpiRatio = 1.0; + } else { + 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 @@ -83,7 +92,11 @@ CWindowBase::~CWindowBase() QRect CWindowBase::startRect(const QRect &rc, double &dpi) { QRect prim_scr_rc = qApp->primaryScreen()->availableGeometry(); - dpi = Utils::getScreenDpiRatio(rc.isEmpty() ? prim_scr_rc.topLeft() : rc.topLeft()); + if (QGuiApplication::platformName() == "wayland") { + dpi = 1.0; + } else { + dpi = Utils::getScreenDpiRatio(rc.isEmpty() ? prim_scr_rc.topLeft() : rc.topLeft()); + } QSize def_size = MAIN_WINDOW_DEFAULT_SIZE * dpi; QRect def_rc = QRect(prim_scr_rc.center() - QPoint(def_size.width()/2, def_size.height()/2), def_size), out_rc = rc.isEmpty() ? def_rc : rc, @@ -190,7 +203,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 { @@ -234,6 +253,9 @@ bool CWindowBase::event(QEvent *event) void CWindowBase::setScreenScalingFactor(double factor, bool resize) { + if (QGuiApplication::platformName() == "wayland") { + factor = 1.0; + } if (resize && !isMaximized()) { setMinimumSize(WINDOW_MIN_WIDTH * factor, WINDOW_MIN_HEIGHT * factor); double change_factor = factor / m_dpiRatio; 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..e89795a5a 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(); } @@ -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,22 +178,22 @@ bool CWindowPlatform::nativeEvent(const QByteArray &ev_type, void *msg, long *re break; } } +#endif return CWindowBase::nativeEvent(ev_type, msg, res); } void CWindowPlatform::setScreenScalingFactor(double factor, bool resize) { + if (QGuiApplication::platformName() == "wayland") { + factor = 1.0; + } CPlatformDecoration::onDpiChanged(factor); CWindowBase::setScreenScalingFactor(factor, 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; } From 8f28800830bed3ced0d68f8c12625c67789bd719 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 3 Jul 2026 18:09:02 -0400 Subject: [PATCH 02/32] fix: address code review comments on dbus, qt paths and database formats Signed-off-by: Peter P. Lupo --- win-linux/CMakeLists.txt | 3 +-- win-linux/src/components/cfiledialog.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/win-linux/CMakeLists.txt b/win-linux/CMakeLists.txt index 200ae8035..16f636c18 100644 --- a/win-linux/CMakeLists.txt +++ b/win-linux/CMakeLists.txt @@ -23,7 +23,7 @@ include(${CORE_ROOT_DIR}/common.cmake) # 2. Find Qt Packages if(NOT DEFINED ENV{QT6_ROOT}) - message(FATAL_ERROR "QT6_ROOT environment variable must be set to the Qt installation prefix (e.g. /qt6/6.11.1/gcc_64)") + message(WARNING "QT6_ROOT environment variable is not set. Assuming Qt is provided by aqtinstall.") endif() set(QT_ROOT "$ENV{QT6_ROOT}") set(QT_DIR "${QT_ROOT}/lib/cmake/Qt6") @@ -350,7 +350,6 @@ add_executable(DesktopEditors ${COMMON_SOURCES} ${COMMON_HEADERS} ${RESOURCES}) set_default_options(DesktopEditors) if(NOT WIN32) - find_package(PkgConfig REQUIRED) pkg_check_modules(DBUS REQUIRED dbus-1) endif() diff --git a/win-linux/src/components/cfiledialog.cpp b/win-linux/src/components/cfiledialog.cpp index 8bbb8d6f9..a88501098 100644 --- a/win-linux/src/components/cfiledialog.cpp +++ b/win-linux/src/components/cfiledialog.cpp @@ -257,7 +257,6 @@ QStringList CFileDialogWrapper::modalOpen(const QString& path, const QString& fi " (*.docx *.doc *.odt *.ott *.rtf *.docm *.dot *.dotx *.dotm *.fb2 *.fodt *.hml *.wps *.wpt *.xml *.pdf *.djv *.djvu *.md *.sxw *.stw *.xps *.oxps);;" + #endif tr("Spreadsheets") + " (*.xlsx *.xls *.xlsm *.xlsb *.ods *.ots *.xltx *.xltm *.xml *.fods *.et *.ett *.sxc *.numbers);;" + - tr("Databases") + " (*.sqlite *.sqlite3 *.db *.db3 *.duckdb *.parquet *.pq *.mdb *.accdb);;" + tr("Presentations") + " (*.pptx *.ppt *.odp *.odg *.otp *.ppsm *.pptm *.ppsx *.pps *.potx *.pot *.potm *.fodp *.dps *.dpt *.sxi *.key);;" + tr("Visio diagram") + " (*.vsdx *.vssx *.vstx *.vsdm *.vssm *.vstm);;" + tr("Web Page") + " (*.html *.htm *.mht *.mhtml *.epub);;" + @@ -362,7 +361,7 @@ QStringList CFileDialogWrapper::modalOpenDocuments(const QString& path, bool mul QStringList CFileDialogWrapper::modalOpenSpreadsheets(const QString& path, bool multi) { QString filter = m_mapFilters[AVS_OFFICESTUDIO_FILE_UNKNOWN]; - filter.prepend(tr("Spreadsheets") + " (*.xlsx *.xls *.ods *.ots *.csv *.tsv *.xltx *.xltm *.fods *.et *.ett);;" + tr("Databases") + " (*.sqlite *.sqlite3 *.db *.db3 *.duckdb *.parquet *.pq *.mdb *.accdb);;"); + filter.prepend(tr("Spreadsheets") + " (*.xlsx *.xls *.ods *.ots *.csv *.tsv *.xltx *.xltm *.fods *.et *.ett);;"); return modalOpen(path, filter, nullptr, multi); } From acc5477b01bd456817f63d9e06dcd27c575f1e91 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 4 Jul 2026 01:00:12 -0400 Subject: [PATCH 03/32] Fix Qt discovery from common.cmake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove redundant QT_ROOT override. Note: there is still a bug with characters with stress marks, such as ã é ü, etc. Signed-off-by: Peter P. Lupo --- win-linux/CMakeLists.txt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/win-linux/CMakeLists.txt b/win-linux/CMakeLists.txt index 16f636c18..ad626af7e 100644 --- a/win-linux/CMakeLists.txt +++ b/win-linux/CMakeLists.txt @@ -22,12 +22,7 @@ include(${CORE_ROOT_DIR}/common.cmake) # 2. Find Qt Packages -if(NOT DEFINED ENV{QT6_ROOT}) - message(WARNING "QT6_ROOT environment variable is not set. Assuming Qt is provided by aqtinstall.") -endif() -set(QT_ROOT "$ENV{QT6_ROOT}") -set(QT_DIR "${QT_ROOT}/lib/cmake/Qt6") -set(Qt6_DIR "${QT_ROOT}/lib/cmake/Qt6") + find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg LinguistTools Multimedia MultimediaWidgets) From 9d4dfe29721d9df55fb00b614e223742ead61052 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Tue, 7 Jul 2026 15:46:52 -0400 Subject: [PATCH 04/32] fix: link OpenGLWidgets for the Wayland CEF GL overlay qcefview.h now includes QOpenGLWidget for the Wayland OSR presenter, and this target moc's that header directly, so it needs the OpenGLWidgets component too or automoc fails with "QOpenGLWidget: No such file". Signed-off-by: Peter P. Lupo --- win-linux/CMakeLists.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/win-linux/CMakeLists.txt b/win-linux/CMakeLists.txt index ad626af7e..a63c5f5ad 100644 --- a/win-linux/CMakeLists.txt +++ b/win-linux/CMakeLists.txt @@ -24,7 +24,7 @@ include(${CORE_ROOT_DIR}/common.cmake) # 2. Find Qt Packages find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg LinguistTools Multimedia MultimediaWidgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg LinguistTools Multimedia MultimediaWidgets OpenGLWidgets) if(UNIX) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS DBus) @@ -382,8 +382,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} From 61c0eb1839525e14063969f47dab06ae096a6926 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 10 Jul 2026 11:12:37 -0400 Subject: [PATCH 05/32] fix: tab right-click context menu renders at screen center on Wayland mapToGlobal() relies on knowing the widget's absolute screen position, which Wayland deliberately doesn't expose to clients -- it was returning a bogus point, and QMenu fell back to centering itself. QContextMenuEvent's own global position is computed correctly by Qt's input pipeline at the time the event was generated, so use that instead of mapping the local position after the fact. Signed-off-by: Peter P. Lupo --- win-linux/src/components/ctabbar.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/win-linux/src/components/ctabbar.cpp b/win-linux/src/components/ctabbar.cpp index a88234468..cdc3c5c42 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->globalPosition().toPoint(); SKIP_EVENTS_QUEUE([=]() { emit tabMenuRequested(i, pos); }); From ea201128587e58fca349c88cdb1336e68c4dcff5 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 10 Jul 2026 11:21:05 -0400 Subject: [PATCH 06/32] fix: build error, QContextMenuEvent uses globalPos() not globalPosition() in this Qt version Signed-off-by: Peter P. Lupo --- win-linux/src/components/ctabbar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win-linux/src/components/ctabbar.cpp b/win-linux/src/components/ctabbar.cpp index cdc3c5c42..0edecf85c 100644 --- a/win-linux/src/components/ctabbar.cpp +++ b/win-linux/src/components/ctabbar.cpp @@ -1288,7 +1288,7 @@ bool CTabBar::eventFilter(QObject *watched, QEvent *event) // 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->globalPosition().toPoint(); + QPoint pos = cm_event->globalPos(); SKIP_EVENTS_QUEUE([=]() { emit tabMenuRequested(i, pos); }); From 5099fd3739d902dfdb78d42e745f7b07477b61a8 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 10 Jul 2026 12:18:12 -0400 Subject: [PATCH 07/32] fix: tab context menu centers on window instead of appearing at click position CMenuWidget is a custom QWidget-based popup using Qt::Tool, which under Wayland maps to a plain xdg_toplevel with set_parent() -- there's no positioner data, so the compositor defaults to centering it over its parent regardless of any move() call, explaining why it rendered dead center of the main window rather than at the click position (a previous attempt to fix the position passed to exec() had no effect, confirming the problem isn't the position value itself). Add Qt::Popup so this maps to the xdg_popup protocol instead, which does support anchored positioning. Dismiss-on-outside-click/Escape is already handled explicitly in this class's own eventFilter (WindowDeactivate/MouseButtonPress/KeyRelease), so Qt::Popup's implicit grab/dismiss behavior is redundant with that, not conflicting. Signed-off-by: Peter P. Lupo --- win-linux/src/components/cmenu.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/win-linux/src/components/cmenu.cpp b/win-linux/src/components/cmenu.cpp index f4a83c3c9..5b7ac7765 100644 --- a/win-linux/src/components/cmenu.cpp +++ b/win-linux/src/components/cmenu.cpp @@ -133,7 +133,16 @@ class CMenuWidget : public QWidget Q_OBJECT public: explicit CMenuWidget(QWidget * parent = nullptr) : - QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint) + // Qt::Popup added for Wayland: a plain Qt::Tool top-level has no + // positioner data, so Wayland compositors default 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. + // Qt::Popup maps to the xdg_popup protocol, which does support + // anchored positioning. Dismiss-on-outside-click/Escape is already + // handled explicitly below (WindowDeactivate/MouseButtonPress/ + // KeyRelease), so Qt::Popup's own implicit grab/dismiss behavior is + // redundant with, not conflicting with, that. + QWidget(parent, Qt::Tool | Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint) { m_dpiRatio = CScalingWrapper::parentScalingFactor(topLevelWidget()); if (isCompositingEnabled()) { From 3efda65079592468a3e1eebf8bb112d0deac4f7e Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 10 Jul 2026 14:06:33 -0400 Subject: [PATCH 08/32] fix: correct context-menu Wayland fix, previous attempt was a no-op Qt defines Tool = Popup | Dialog, so the previous "Qt::Tool | Qt::Popup" change didn't actually change anything -- the Popup bit was already part of Qt::Tool's value, so the resolved window type was still exactly Tool (confirmed: menu still centered on the window after that build). Use Qt::Popup alone so the widget's actual resolved window type changes, which is what Qt's Wayland QPA plugin needs to map it to the xdg_popup protocol instead of a plain xdg_toplevel. Signed-off-by: Peter P. Lupo --- win-linux/src/components/cmenu.cpp | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/win-linux/src/components/cmenu.cpp b/win-linux/src/components/cmenu.cpp index 5b7ac7765..9bc1f6379 100644 --- a/win-linux/src/components/cmenu.cpp +++ b/win-linux/src/components/cmenu.cpp @@ -133,16 +133,22 @@ class CMenuWidget : public QWidget Q_OBJECT public: explicit CMenuWidget(QWidget * parent = nullptr) : - // Qt::Popup added for Wayland: a plain Qt::Tool top-level has no - // positioner data, so Wayland compositors default 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. - // Qt::Popup maps to the xdg_popup protocol, which does support - // anchored positioning. Dismiss-on-outside-click/Escape is already - // handled explicitly below (WindowDeactivate/MouseButtonPress/ - // KeyRelease), so Qt::Popup's own implicit grab/dismiss behavior is - // redundant with, not conflicting with, that. - QWidget(parent, Qt::Tool | Qt::Popup | 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()) { From 76db0b9317b85f167384071bd77ce17adde58eae Mon Sep 17 00:00:00 2001 From: Hendrik Leidinger Date: Fri, 17 Jul 2026 15:31:34 -0700 Subject: [PATCH 09/32] fix: qt scaling Signed-off-by: Hendrik Leidinger Assisted-by: Claude Code:Opus 4.8 --- win-linux/src/main.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/win-linux/src/main.cpp b/win-linux/src/main.cpp index 6c58a06d9..a42e0a797 100644 --- a/win-linux/src/main.cpp +++ b/win-linux/src/main.cpp @@ -103,6 +103,16 @@ int main( int argc, char *argv[] ) #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"); #else From f53dcdeae8a98c73a58fafca8c3c97f76ea4ab71 Mon Sep 17 00:00:00 2001 From: Hendrik Leidinger Date: Sat, 18 Jul 2026 14:52:36 -0700 Subject: [PATCH 10/32] fix: X11Backend must use Qt's X11 connection, not a private one Signed-off-by: Hendrik Leidinger Assisted-by: Claude Code:Opus 4.8 --- win-linux/src/platform_linux/x11backend.cpp | 57 ++++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/win-linux/src/platform_linux/x11backend.cpp b/win-linux/src/platform_linux/x11backend.cpp index db8f5eb57..89dcaf99d 100644 --- a/win-linux/src/platform_linux/x11backend.cpp +++ b/win-linux/src/platform_linux/x11backend.cpp @@ -41,11 +41,13 @@ namespace { auto getXDisplay() -> Display * { - static Display* display = NULL; - if ( !display ) - display = XOpenDisplay(NULL); - - return 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) @@ -135,7 +137,7 @@ X11Backend::~X11Backend() void X11Backend::moveWindow(WId window, int x, int y) { - Display *disp = XOpenDisplay(NULL); + Display *disp = getXDisplay(); if (disp) { xcb_connection_t *conn = XGetXCBConnection(disp); if (conn && (xcb_window_t)window != XCB_WINDOW_NONE) { @@ -145,14 +147,13 @@ void X11Backend::moveWindow(WId window, int x, int y) xcb_configure_window(conn, (xcb_window_t)window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, val); xcb_flush(conn); } - XCloseDisplay(disp); } } bool X11Backend::isNativeFocus(WId window) { xcb_window_t win = 0; - Display *disp = XOpenDisplay(NULL); + Display *disp = getXDisplay(); if (disp) { xcb_connection_t *conn = XGetXCBConnection(disp); if (conn) { @@ -166,14 +167,13 @@ bool X11Backend::isNativeFocus(WId window) } xcb_flush(conn); } - XCloseDisplay(disp); } return (xcb_window_t)window == win; } void X11Backend::setNativeFocusTo(WId window) { - Display *disp = XOpenDisplay(NULL); + Display *disp = getXDisplay(); if (disp) { xcb_connection_t *conn = XGetXCBConnection(disp); if (conn && (xcb_window_t)window != XCB_WINDOW_NONE) { @@ -182,13 +182,12 @@ void X11Backend::setNativeFocusTo(WId window) (xcb_window_t)window, XCB_CURRENT_TIME); xcb_flush(conn); } - XCloseDisplay(disp); } } void X11Backend::setInputEnabled(WId window, bool enabled) { - Display* disp = XOpenDisplay(NULL); + Display* disp = getXDisplay(); if (disp) { Window wnd = (Window)window; if (enabled) { @@ -198,7 +197,6 @@ void X11Backend::setInputEnabled(WId window, bool enabled) XShapeCombineRectangles(disp, wnd, ShapeInput, 0, 0, &rc, 1, ShapeSet, YXBanded); } XFlush(disp); - XCloseDisplay(disp); } } @@ -326,6 +324,7 @@ void X11Backend::sendButtonRelease(QWidget *window) void X11Backend::startInteractiveMove(QWidget *window, const QPoint &globalPos) { + Q_UNUSED(globalPos); const int MOVE = 8; Display * xdisplay_ = getXDisplay(); @@ -333,6 +332,12 @@ void X11Backend::startInteractiveMove(QWidget *window, const QPoint &globalPos) 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; @@ -340,8 +345,8 @@ void X11Backend::startInteractiveMove(QWidget *window, const QPoint &globalPos) event.xclient.window = window->winId(); event.xclient.message_type = XInternAtom(xdisplay_, "_NET_WM_MOVERESIZE", False); event.xclient.format = 32; - event.xclient.data.l[0] = globalPos.x(); - event.xclient.data.l[1] = globalPos.y(); + 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; @@ -352,6 +357,7 @@ void X11Backend::startInteractiveMove(QWidget *window, const QPoint &globalPos) void X11Backend::startInteractiveResize(QWidget *window, Qt::Edges edges, const QPoint &globalPos) { + Q_UNUSED(globalPos); int direction = edgesToMoveResizeDirection(edges); if (direction < 0) return; @@ -361,6 +367,12 @@ void X11Backend::startInteractiveResize(QWidget *window, Qt::Edges edges, const 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; @@ -368,8 +380,8 @@ void X11Backend::startInteractiveResize(QWidget *window, Qt::Edges edges, const event.xclient.window = window->winId(); event.xclient.message_type = XInternAtom(xdisplay_, "_NET_WM_MOVERESIZE", False); event.xclient.format = 32; - event.xclient.data.l[0] = globalPos.x(); - event.xclient.data.l[1] = globalPos.y(); + 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; @@ -410,8 +422,15 @@ void X11Backend::resetCursor(WId window) bool X11Backend::isCompositingAvailable() { - // Qt 6 does not provide QX11Info; assume compositing is available - return true; + 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) From fa05cba771363a0318f08c40aa9677e31d06b14a Mon Sep 17 00:00:00 2001 From: Hendrik Leidinger Date: Sat, 18 Jul 2026 21:57:23 -0700 Subject: [PATCH 11/32] fix: merge issues in cmake file Signed-off-by: Hendrik Leidinger --- win-linux/CMakeLists.txt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/win-linux/CMakeLists.txt b/win-linux/CMakeLists.txt index a63c5f5ad..3264a2348 100644 --- a/win-linux/CMakeLists.txt +++ b/win-linux/CMakeLists.txt @@ -21,11 +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 - -find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets PrintSupport Svg LinguistTools Multimedia MultimediaWidgets OpenGLWidgets) - if(UNIX) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS DBus) add_definitions(-DLINUX -D_LINUX -D_WAYLAND) @@ -345,6 +340,7 @@ add_executable(DesktopEditors ${COMMON_SOURCES} ${COMMON_HEADERS} ${RESOURCES}) set_default_options(DesktopEditors) if(NOT WIN32) + find_package(PkgConfig REQUIRED) pkg_check_modules(DBUS REQUIRED dbus-1) endif() @@ -407,10 +403,6 @@ target_link_libraries(DesktopEditors PRIVATE # --- System Essentials --- hunspell_wrapper ooxmlsignature - dl - pthread - rt - z ) if(WIN32) @@ -432,9 +424,9 @@ endif() if(UNIX) target_link_libraries(DesktopEditors PRIVATE Qt${QT_VERSION_MAJOR}::DBus - ${LIBICUUC_DESKTOP} - ${LIBICUDATA_DESKTOP} - ${LIBICUI_DESKTOP} + ${LIBICUUC} + ${LIBICUDATA} + ${LIBICUI} ) target_link_options(DesktopEditors PRIVATE "-Wl,-rpath,$ORIGIN" "-Wl,-rpath,$ORIGIN/converter") From fa1b1b1bc22a813573c83971984009e21867847a Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Wed, 22 Jul 2026 16:08:00 -0400 Subject: [PATCH 12/32] Remove incorrect Wayland early-return from DPI ratio helpers getScreenDpiRatio(), getScreenDpiRatioByHWND(), and getScreenDpiRatioByWidget() each hardcoded a flat 1.0 on Wayland, reasoning that Qt's automatic HiDPI backing-store scaling makes manual scaling redundant there. That conflates two different things: Qt's automatic scaling only controls how many physical pixels render a given logical (DIP) size -- it doesn't retroactively enlarge a DIP size an app explicitly computed from a hardcoded constant, e.g. cplatformdecoration.cpp's CUSTOM_BORDER_WIDTH * dpi_ratio, which sizes the native window decoration/caption region containing the tab strip. That manual computation needs a real ratio on Wayland exactly like it already does on X11, so let all three fall through to the same QDpiChecker-based path X11 already uses (now Wayland-aware after the desktop-sdk fix), instead of X11 || Wayland branching. Signed-off-by: Peter P. Lupo --- win-linux/src/utils.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index c0e6d2d45..229e27bc4 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -564,9 +564,6 @@ double Utils::getScreenDpiRatio(int scrnum) double Utils::getScreenDpiRatio(const QPoint& pt) { - if (qApp && QGuiApplication::platformName() == "wayland") { - return 1.0; - } QWidget _w; _w.setGeometry(QRect(pt, QSize(10,10))); @@ -579,9 +576,6 @@ double Utils::getScreenDpiRatio(const QPoint& pt) double Utils::getScreenDpiRatioByHWND(int hwnd) { - if (qApp && QGuiApplication::platformName() == "wayland") { - return 1.0; - } unsigned int _dpi_x = 0; unsigned int _dpi_y = 0; double nScale = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)hwnd, _dpi_x, _dpi_y); @@ -590,13 +584,14 @@ double Utils::getScreenDpiRatioByHWND(int hwnd) double Utils::getScreenDpiRatioByWidget(QWidget* wid) { - // On Wayland, Qt handles all widget scaling natively through - // devicePixelRatio(). The application's manual DPI scaling - // (multiplying sizes by dpiRatio) is only needed on X11. - // Returning 1.0 prevents double-scaling. - if (qApp && QGuiApplication::platformName() == "wayland") { - return 1.0; - } + // Manual DPI scaling here (multiplying sizes by dpiRatio) computes a + // logical/DIP size for widgets that don't rely purely on Qt's layout + // system (e.g. cplatformdecoration.cpp's CUSTOM_BORDER_WIDTH * ratio). + // That's independent of Qt's own automatic HiDPI backing-store + // scaling (which just renders whatever DIP size is chosen more + // crisply) -- it's needed on Wayland exactly the same way it's + // needed on X11, so this used to (incorrectly) skip it with a flat + // 1.0 return on Wayland, leaving those widgets sized as if unscaled. if (!wid) return 1; From b29381bdd86fc1a195ba48ad92134069d92635cf Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Wed, 22 Jul 2026 22:32:11 -0400 Subject: [PATCH 13/32] Remove Wayland hardcoded-neutral scaling factor across window classes CWindowBase's constructor, startRect(), and setScreenScalingFactor(), plus the setScreenScalingFactor() overrides in CWindowPlatform, CPresenterWindow, CEditorWindow, and CMainWindow all independently hardcoded factor/dpi to 1.0 on Wayland before it ever reached CPlatformDecoration's border sizing or m_boxTitleBtns (title bar/tab button) sizing -- silently discarding whatever real scale Utils::getScreenDpiRatio()/getScreenDpiRatioByWidget() computed (already fixed to be Wayland-aware) and leaving the tab strip sized as if unscaled regardless of the actual display scale. Same root pattern as the earlier DPI-checker fix: each of these assumed Qt's own automatic widget scaling made this redundant on Wayland, but that only covers backing-store render resolution, not layout sizes computed explicitly from a hardcoded base constant times a ratio. Signed-off-by: Peter P. Lupo --- win-linux/src/windows/ceditorwindow.cpp | 3 --- win-linux/src/windows/cmainwindow.cpp | 3 --- win-linux/src/windows/cpresenterwindow.cpp | 3 --- win-linux/src/windows/cwindowbase.cpp | 18 ++---------------- .../windows/platform_linux/cwindowplatform.cpp | 3 --- 5 files changed, 2 insertions(+), 28 deletions(-) diff --git a/win-linux/src/windows/ceditorwindow.cpp b/win-linux/src/windows/ceditorwindow.cpp index 71179aaeb..5348ec5ee 100644 --- a/win-linux/src/windows/ceditorwindow.cpp +++ b/win-linux/src/windows/ceditorwindow.cpp @@ -507,9 +507,6 @@ bool CEditorWindow::event(QEvent * event) void CEditorWindow::setScreenScalingFactor(double factor, bool resize) { - if (QGuiApplication::platformName() == "wayland") { - factor = 1.0; - } CWindowPlatform::setScreenScalingFactor(factor, resize); CScalingWrapper::updateScalingFactor(factor); if (isCustomWindowStyle()) { diff --git a/win-linux/src/windows/cmainwindow.cpp b/win-linux/src/windows/cmainwindow.cpp index ccd32e51c..b0a4dad57 100644 --- a/win-linux/src/windows/cmainwindow.cpp +++ b/win-linux/src/windows/cmainwindow.cpp @@ -1561,9 +1561,6 @@ void CMainWindow::updateScalingFactor(double dpiratio) void CMainWindow::setScreenScalingFactor(double factor, bool resize) { - if (QGuiApplication::platformName() == "wayland") { - factor = 1.0; - } CWindowPlatform::setScreenScalingFactor(factor, resize); QString css(AscAppManager::getWindowStylesheets(factor)); #ifdef __linux__ diff --git a/win-linux/src/windows/cpresenterwindow.cpp b/win-linux/src/windows/cpresenterwindow.cpp index cc6cebbd2..6fe2ec533 100644 --- a/win-linux/src/windows/cpresenterwindow.cpp +++ b/win-linux/src/windows/cpresenterwindow.cpp @@ -167,9 +167,6 @@ QWidget * CPresenterWindow::createMainPanel(QWidget * parent, const QString& tit void CPresenterWindow::setScreenScalingFactor(double factor, bool resize) { - if (QGuiApplication::platformName() == "wayland") { - factor = 1.0; - } CWindowPlatform::setScreenScalingFactor(factor, resize); QString css(AscAppManager::getWindowStylesheets(factor)); #ifdef __linux__ diff --git a/win-linux/src/windows/cwindowbase.cpp b/win-linux/src/windows/cwindowbase.cpp index ad396f4f5..64301ab15 100644 --- a/win-linux/src/windows/cwindowbase.cpp +++ b/win-linux/src/windows/cwindowbase.cpp @@ -67,14 +67,7 @@ CWindowBase::CWindowBase(const QRect& rect) , m_windowActivated(false) { setWindowIcon(Utils::appIcon()); - double tempDpi = 1.0; - if (QGuiApplication::platformName() == "wayland") { - m_dpiRatio = 1.0; - m_window_rect = startRect(rect, tempDpi); - m_dpiRatio = 1.0; - } else { - m_window_rect = startRect(rect, m_dpiRatio); - } + m_window_rect = startRect(rect, m_dpiRatio); setMinimumSize(WINDOW_MIN_WIDTH * m_dpiRatio, WINDOW_MIN_HEIGHT * m_dpiRatio); #ifdef __linux__ @@ -92,11 +85,7 @@ CWindowBase::~CWindowBase() QRect CWindowBase::startRect(const QRect &rc, double &dpi) { QRect prim_scr_rc = qApp->primaryScreen()->availableGeometry(); - if (QGuiApplication::platformName() == "wayland") { - dpi = 1.0; - } else { - dpi = Utils::getScreenDpiRatio(rc.isEmpty() ? prim_scr_rc.topLeft() : rc.topLeft()); - } + dpi = Utils::getScreenDpiRatio(rc.isEmpty() ? prim_scr_rc.topLeft() : rc.topLeft()); QSize def_size = MAIN_WINDOW_DEFAULT_SIZE * dpi; QRect def_rc = QRect(prim_scr_rc.center() - QPoint(def_size.width()/2, def_size.height()/2), def_size), out_rc = rc.isEmpty() ? def_rc : rc, @@ -253,9 +242,6 @@ bool CWindowBase::event(QEvent *event) void CWindowBase::setScreenScalingFactor(double factor, bool resize) { - if (QGuiApplication::platformName() == "wayland") { - factor = 1.0; - } if (resize && !isMaximized()) { setMinimumSize(WINDOW_MIN_WIDTH * factor, WINDOW_MIN_HEIGHT * factor); double change_factor = factor / m_dpiRatio; diff --git a/win-linux/src/windows/platform_linux/cwindowplatform.cpp b/win-linux/src/windows/platform_linux/cwindowplatform.cpp index e89795a5a..f941c1f9b 100644 --- a/win-linux/src/windows/platform_linux/cwindowplatform.cpp +++ b/win-linux/src/windows/platform_linux/cwindowplatform.cpp @@ -184,9 +184,6 @@ bool CWindowPlatform::nativeEvent(const QByteArray &ev_type, void *msg, long *re void CWindowPlatform::setScreenScalingFactor(double factor, bool resize) { - if (QGuiApplication::platformName() == "wayland") { - factor = 1.0; - } CPlatformDecoration::onDpiChanged(factor); CWindowBase::setScreenScalingFactor(factor, resize); } From 7f76fe7bcabfbd17b27727a9d9ac54de31dbaf3a Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Thu, 23 Jul 2026 00:24:57 -0400 Subject: [PATCH 14/32] TEMPORARY: log getScreenDpiRatioByWidget() readings to investigate gigantic Wayland widgets Native widgets (CWindowBase, CMessage, etc.) call this once at construction time with no debounce, unlike CEF's UpdateUIScalePercentage() which now guards against the known startup devicePixelRatio() transient-misread race. Need to confirm whether choose_scaling() is snapping such a misread to a too-high standard step for these widgets, and whether it recurs for widgets constructed well after startup (on-demand dialogs), which would rule out a pure startup-race explanation. Remove once the underlying issue is found. Signed-off-by: Peter P. Lupo --- win-linux/src/utils.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 229e27bc4..b00b19935 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -606,6 +606,23 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif + // TEMPORARY: investigating "gigantic" native widgets on Wayland -- + // confirm whether choose_scaling() is snapping a transient + // devicePixelRatio() misread (matching the same startup-window- + // placement race already found and fixed for CEF's UI scale) to a + // too-high standard step, and whether that happens even for widgets + // constructed well after startup (e.g. on-demand dialogs). + { + FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); + if (pLogFile) + { + fprintf(pLogFile, "[getScreenDpiRatioByWidget] widget=%p nDpiX=%u nDpiY=%u dpiApp=%f screenDPR=%f chosen=%f\n", + (void*)wid, nDpiX, nDpiY, dpiApp, wid->screen() ? wid->screen()->devicePixelRatio() : -1.0, + dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); + fclose(pLogFile); + } + } + if ( dpiApp >= 0 ) { return choose_scaling(dpiApp); } From f71ce83623e310fbdfee2920d0612b2ed7788b61 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Thu, 23 Jul 2026 08:36:42 -0400 Subject: [PATCH 15/32] TEMPORARY: log both widget- and screen-level devicePixelRatio() Extends the earlier debug logging to compare QWidget::devicePixelRatio() against QScreen::devicePixelRatio() for the same widget, to confirm they diverge on Wayland (screen-level appears stuck at the initial integer-rounded buffer scale) before relying on that as the basis for the desktop-sdk fix switching QDpiChecker::GetWidgetDpi() to the widget-level value. Remove once confirmed. Signed-off-by: Peter P. Lupo --- win-linux/src/utils.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index b00b19935..2727d6ae2 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -616,8 +616,9 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); if (pLogFile) { - fprintf(pLogFile, "[getScreenDpiRatioByWidget] widget=%p nDpiX=%u nDpiY=%u dpiApp=%f screenDPR=%f chosen=%f\n", + fprintf(pLogFile, "[getScreenDpiRatioByWidget] widget=%p nDpiX=%u nDpiY=%u dpiApp=%f screenDPR=%f widgetDPR=%f chosen=%f\n", (void*)wid, nDpiX, nDpiY, dpiApp, wid->screen() ? wid->screen()->devicePixelRatio() : -1.0, + wid->devicePixelRatio(), dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); fclose(pLogFile); } From 18189fbce1d03ae8faef730345e3c2d8ca26441b Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Thu, 23 Jul 2026 12:03:37 -0400 Subject: [PATCH 16/32] Set HighDpiScaleFactorRoundingPolicy::PassThrough on Wayland 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 reads as 2) unless the app explicitly opts into PassThrough -- confirmed as a known, documented Qt/Wayland behavior, not a startup timing race as earlier commits in this area assumed. This is the actual root cause of native widgets (window decoration, title-bar buttons, dialogs) rendering oversized: Utils::getScreenDpiRatioByWidget() was correctly reading devicePixelRatio(), but Qt itself was permanently misreporting it. Also removes the temporary debug logging added while investigating this, now that the underlying cause is confirmed. Signed-off-by: Peter P. Lupo --- win-linux/src/main.cpp | 7 +++++++ win-linux/src/utils.cpp | 18 ------------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/win-linux/src/main.cpp b/win-linux/src/main.cpp index a42e0a797..32c8d2ade 100644 --- a/win-linux/src/main.cpp +++ b/win-linux/src/main.cpp @@ -122,6 +122,13 @@ int main( int argc, char *argv[] ) #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif + // 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. diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 2727d6ae2..229e27bc4 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -606,24 +606,6 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif - // TEMPORARY: investigating "gigantic" native widgets on Wayland -- - // confirm whether choose_scaling() is snapping a transient - // devicePixelRatio() misread (matching the same startup-window- - // placement race already found and fixed for CEF's UI scale) to a - // too-high standard step, and whether that happens even for widgets - // constructed well after startup (e.g. on-demand dialogs). - { - FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); - if (pLogFile) - { - fprintf(pLogFile, "[getScreenDpiRatioByWidget] widget=%p nDpiX=%u nDpiY=%u dpiApp=%f screenDPR=%f widgetDPR=%f chosen=%f\n", - (void*)wid, nDpiX, nDpiY, dpiApp, wid->screen() ? wid->screen()->devicePixelRatio() : -1.0, - wid->devicePixelRatio(), - dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); - fclose(pLogFile); - } - } - if ( dpiApp >= 0 ) { return choose_scaling(dpiApp); } From 15516ab8be0fe2665140e97130f018f2f0e46258 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Thu, 23 Jul 2026 15:31:27 -0400 Subject: [PATCH 17/32] Re-apply window scaling once Qt's real devicePixelRatio() arrives Even with HighDpiScaleFactorRoundingPolicy::PassThrough set, initial sizing on Wayland can still be computed from a transiently-wrong devicePixelRatio() reading -- the same startup compositor round-trip race already proven for CEF's UI scale, just with no debounce here since these widgets read the value once, synchronously, at construction. Add Utils::WatchForDpiChange(), a reusable helper installing an event filter for QEvent::DevicePixelRatioChange (fired per-widget whenever Qt's own notion of that widget's effective ratio changes, for any reason -- the async correction arriving, or a real later change like moving to a different-scaled monitor). Wire it into CWindowBase's constructor to re-run updateScaling() when it fires, covering the main window and its subclasses (title bar, buttons, tabs). CMessage (dialogs) is not wired up yet -- its DPI-dependent sizing is spread inline across its constructor rather than a separate, re-callable method, so doing the same there needs a small refactor first rather than being folded into this change. Also re-adds (and extends, with a timestamp for correlating against the CEF debug log) the temporary debug logging removed earlier, now that PassThrough alone didn't fully resolve the issue. Signed-off-by: Peter P. Lupo --- win-linux/src/utils.cpp | 49 +++++++++++++++++++++++++++ win-linux/src/utils.h | 10 ++++++ win-linux/src/windows/cwindowbase.cpp | 6 ++++ 3 files changed, 65 insertions(+) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 229e27bc4..62a2c5aea 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -29,6 +29,7 @@ #include "utils.h" #include "defines.h" #include +#include #include #include #include @@ -606,6 +607,24 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif + // TEMPORARY: confirm whether native widgets hit the same startup + // devicePixelRatio() race already proven for CEF (raw reads 200 a + // few times before settling to 125), now that + // HighDpiScaleFactorRoundingPolicy::PassThrough is set -- if so, + // these widgets need the same kind of correction-on-change handling + // CEF's poll timer approximates, not just a data-source fix. + { + FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); + if (pLogFile) + { + fprintf(pLogFile, "[%lld] [getScreenDpiRatioByWidget] class=%s widget=%p dpiApp=%f widgetDPR=%f chosen=%f\n", + QDateTime::currentMSecsSinceEpoch(), wid->metaObject()->className(), (void*)wid, dpiApp, + wid->devicePixelRatio(), + dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); + fclose(pLogFile); + } + } + if ( dpiApp >= 0 ) { return choose_scaling(dpiApp); } @@ -613,6 +632,36 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) return wid->devicePixelRatio(); } +namespace { +class CDpiChangeWatcher : public QObject +{ +public: + CDpiChangeWatcher(QWidget* w, std::function onChange) + : QObject(w), m_onChange(std::move(onChange)) + { + w->installEventFilter(this); + } + +protected: + bool eventFilter(QObject* watched, QEvent* event) override + { + if (event->type() == QEvent::DevicePixelRatioChange) + m_onChange(); + return QObject::eventFilter(watched, event); + } + +private: + 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) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) diff --git a/win-linux/src/utils.h b/win-linux/src/utils.h index 523e8ce55..d17d1b920 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 @@ -94,6 +95,15 @@ class Utils { 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 w's own effective devicePixelRatio() changes + // (QEvent::DevicePixelRatioChange) -- e.g. once Qt receives the async + // Wayland fractional-scale compositor answer for w's surface, which can + // arrive after the widget's initial DPI-dependent sizing was already + // computed from a transiently-wrong reading. 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/cwindowbase.cpp b/win-linux/src/windows/cwindowbase.cpp index 64301ab15..021270698 100644 --- a/win-linux/src/windows/cwindowbase.cpp +++ b/win-linux/src/windows/cwindowbase.cpp @@ -72,6 +72,12 @@ CWindowBase::CWindowBase(const QRect& rect) 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 } From 3b25047f390af0b7ffe28d4b9642ccbc07bd6da9 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Thu, 23 Jul 2026 17:44:15 -0400 Subject: [PATCH 18/32] Remove unused Utils::getScreenDpiRatio(int) overload Dead code with no remaining callers; had a stale Wayland hardcode (return 1.0) inconsistent with the widget-level DPI fix applied elsewhere. Signed-off-by: Peter P. Lupo --- win-linux/src/utils.cpp | 11 ----------- win-linux/src/utils.h | 1 - 2 files changed, 12 deletions(-) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 62a2c5aea..5ee51bcc6 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -552,17 +552,6 @@ inline double choose_scaling(double s) s > 1 ? 1.25 : 1; } -double Utils::getScreenDpiRatio(int scrnum) -{ - if (qApp && QGuiApplication::platformName() == "wayland") { - return 1.0; - } - unsigned int _dpi_x = 0; - unsigned int _dpi_y = 0; - double nScale = AscAppManager::getInstance().GetMonitorScaleByIndex(scrnum, _dpi_x, _dpi_y); - return choose_scaling(nScale); -} - double Utils::getScreenDpiRatio(const QPoint& pt) { QWidget _w; diff --git a/win-linux/src/utils.h b/win-linux/src/utils.h index d17d1b920..9cebf992f 100644 --- a/win-linux/src/utils.h +++ b/win-linux/src/utils.h @@ -91,7 +91,6 @@ 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*); From 9b39dbe499bacead4b5c363c86067ef9fd3c434a Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Thu, 23 Jul 2026 19:42:24 -0400 Subject: [PATCH 19/32] Remove temporary DPI-race debug logging Investigation is concluded; the underlying fix is confirmed working. Signed-off-by: Peter P. Lupo --- win-linux/src/utils.cpp | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 5ee51bcc6..b62da52d8 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -29,7 +29,6 @@ #include "utils.h" #include "defines.h" #include -#include #include #include #include @@ -596,24 +595,6 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif - // TEMPORARY: confirm whether native widgets hit the same startup - // devicePixelRatio() race already proven for CEF (raw reads 200 a - // few times before settling to 125), now that - // HighDpiScaleFactorRoundingPolicy::PassThrough is set -- if so, - // these widgets need the same kind of correction-on-change handling - // CEF's poll timer approximates, not just a data-source fix. - { - FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); - if (pLogFile) - { - fprintf(pLogFile, "[%lld] [getScreenDpiRatioByWidget] class=%s widget=%p dpiApp=%f widgetDPR=%f chosen=%f\n", - QDateTime::currentMSecsSinceEpoch(), wid->metaObject()->className(), (void*)wid, dpiApp, - wid->devicePixelRatio(), - dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); - fclose(pLogFile); - } - } - if ( dpiApp >= 0 ) { return choose_scaling(dpiApp); } From 26bacf5e1aa6c9cabeaf74490e6210df2b503538 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Fri, 24 Jul 2026 20:49:24 -0400 Subject: [PATCH 20/32] Fix gigantic message dialogs on Wayland QtMsg (CMessage's underlying dialog) read its own devicePixelRatio() once at construction time, before Qt had received the Wayland compositor's async fractional-scale answer for the dialog's surface -- the same startup race already fixed for CWindowBase, but this dialog was never wired into the reactive correction. Extracts the dpiRatio-dependent sizing into applyScaling(), called once at construction and again via WatchForDpiChange whenever the dialog's own devicePixelRatio() settles to its real value. Signed-off-by: Peter P. Lupo (cherry picked from commit 4273100a199a598839c1ce8acdbf58c85b28749b) --- win-linux/src/components/cmessage.cpp | 82 ++++++++++++++++++--------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 5258f67d5..095f8a63e 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -150,6 +150,13 @@ class QtMsg : public QDialog void setContent(const QString&); void setCheckBox(const QString &chekBoxText, bool checkBoxState); bool getCheckStatus(); + // Re-applies every dpiRatio-dependent size/margin/stylesheet. Called once + // from the constructor and again whenever this dialog's own + // devicePixelRatio() self-corrects after the Wayland compositor's async + // fractional-scale answer arrives (the initial read in + // QtMsgPrivateIntf's constructor can catch the same startup race already + // handled for CWindowBase, but this dialog never had the reactive fix). + void applyScaling(); QWidget *m_boxButtons = nullptr, *m_centralWidget = nullptr; @@ -197,6 +204,10 @@ 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; }; int QtMsg::m_modalresult(MODAL_RESULT_CANCEL); @@ -220,44 +231,34 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->setObjectName("messageBody"); m_centralWidget->setProperty("uitheme", QString::fromStdWString(GetCurrentTheme().originalId())); - QVBoxLayout * _c_layout = new QVBoxLayout; + m_priv->cLayout = new QVBoxLayout; QHBoxLayout * _h_layout2 = new QHBoxLayout; QHBoxLayout * _h_layout1 = new QHBoxLayout; - _c_layout->addLayout(_h_layout2, 1); - _c_layout->addLayout(_h_layout1, 0); - - const int _body_margin = int(12 * m_priv->dpiRatio); - _c_layout->setContentsMargins(_body_margin,_body_margin,_body_margin,_body_margin); + m_priv->cLayout->addLayout(_h_layout2, 1); + m_priv->cLayout->addLayout(_h_layout1, 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); // 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_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->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); + m_priv->fLayout = new QFormLayout; + m_priv->fLayout->addWidget(m_message); + m_priv->fLayout->addWidget(m_content); + m_priv->fLayout->setSpacing(0); + _h_layout2->addLayout(m_priv->fLayout, 1); _h_layout2->setContentsMargins(0,0,0,0); 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); m_priv->addButton(btn_ok); @@ -269,16 +270,19 @@ 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); + applyScaling(); + // m_priv->dpiRatio above may have been read from this dialog's own + // devicePixelRatio() before Qt received the Wayland compositor's async + // fractional-scale answer for its surface -- re-derive and re-apply + // once Qt tells us it actually changed (mirrors CWindowBase's use of + // the same watcher). + Utils::WatchForDpiChange(this, [this]() { + m_priv->dpiRatio = Utils::getScreenDpiRatioByWidget(this); + applyScaling(); + }); m_priv->focusConnection = QObject::connect(qApp, &QApplication::focusChanged, this, [&] (QWidget * from, QWidget *to){ @@ -294,6 +298,32 @@ 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))); + + m_message->setStyleSheet(QString("margin-bottom: %1px;").arg(int(8*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(int((m_priv->buttons.size() > 2 ? 400 : 350)*m_priv->dpiRatio)); + + 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); From 4d019e301b1510d0864a272c880e490480f90e27 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 00:44:13 -0400 Subject: [PATCH 21/32] Force dialog resize after re-applying scaling on Wayland QLayout::SetFixedSize locks the dialog's top-level size the first time it activates. Shrinking a child's minimum width afterwards (the WatchForDpiChange-triggered re-run, once the real ratio arrives) doesn't automatically renegotiate an already-mapped Wayland surface down to the smaller size, leaving the outer window oversized even though its contents are now correctly scaled. Force the resize explicitly via layout()->invalidate() + adjustSize(). Signed-off-by: Peter P. Lupo (cherry picked from commit 4dff13224d5b1842105106f3792e4170740c4f73) --- win-linux/src/components/cmessage.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 095f8a63e..08040395c 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -322,6 +322,14 @@ void QtMsg::applyScaling() QString zoom = QString::number(m_priv->dpiRatio) + "x"; m_centralWidget->setProperty("scaling", zoom); + + // layout()'s SetFixedSize constraint locks the dialog's own top-level + // size the first time it activates. Shrinking a child's minimum width + // afterwards (the WatchForDpiChange-triggered re-run, once the real + // ratio arrives) doesn't automatically renegotiate an already-mapped + // Wayland surface down to the smaller size -- force it explicitly. + layout()->invalidate(); + adjustSize(); } QtMsg::~QtMsg() From d250db590ab2ecd05fe504fe10e9b71a7c8be2a9 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 09:48:11 -0400 Subject: [PATCH 22/32] Stop double-scaling native widgets on Wayland The app scales native Qt widgets manually (sizes/margins x dpiRatio, plus a QSS 'scaling' property that bakes the factor into pixel font-sizes) as a substitute for Qt's automatic HiDPI scaling, which is deliberately disabled on X11 so devicePixelRatio() stays 1. On Wayland, HighDpiScaleFactorRoundingPolicy::PassThrough makes Qt's devicePixelRatio report the true fractional scale and Qt already scales every widget by it. The manual scaling then applies on top, so a 1.25 display rendered native UI at ~1.56x. This was uniform across the whole native UI (only visible by comparing a dialog side-by-side with X11, since a uniform enlargement has no reference). Make Qt's PassThrough devicePixelRatio the single source of scaling on Wayland: getScreenDpiRatioByWidget/getScreenDpiRatioByHWND now return a neutral 1.0 there, so the manual factor is 1.0 and Qt does the scaling once, crisply, matching X11's physical result. Since the Wayland factor no longer depends on the late-arriving devicePixelRatio, the startup race it chased is gone: drop QtMsg's WatchForDpiChange re-derivation and the forced-resize workaround. Signed-off-by: Peter P. Lupo (cherry picked from commit 3ed1c04513c24b91587938ea07e3b32d5caaa6e7) --- win-linux/src/components/cmessage.cpp | 27 ++++-------------------- win-linux/src/utils.cpp | 30 ++++++++++++++++++++------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 08040395c..db63af3b2 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -150,12 +150,10 @@ class QtMsg : public QDialog void setContent(const QString&); void setCheckBox(const QString &chekBoxText, bool checkBoxState); bool getCheckStatus(); - // Re-applies every dpiRatio-dependent size/margin/stylesheet. Called once - // from the constructor and again whenever this dialog's own - // devicePixelRatio() self-corrects after the Wayland compositor's async - // fractional-scale answer arrives (the initial read in - // QtMsgPrivateIntf's constructor can catch the same startup race already - // handled for CWindowBase, but this dialog never had the reactive fix). + // Applies every dpiRatio-dependent size/margin/stylesheet. On Wayland + // dpiRatio is a neutral 1.0 (Qt's PassThrough devicePixelRatio does the + // scaling); on X11 it carries the real factor (Qt HiDPI is disabled + // there). See Utils::getScreenDpiRatioByWidget. void applyScaling(); QWidget *m_boxButtons = nullptr, @@ -274,15 +272,6 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->move(0, 0); applyScaling(); - // m_priv->dpiRatio above may have been read from this dialog's own - // devicePixelRatio() before Qt received the Wayland compositor's async - // fractional-scale answer for its surface -- re-derive and re-apply - // once Qt tells us it actually changed (mirrors CWindowBase's use of - // the same watcher). - Utils::WatchForDpiChange(this, [this]() { - m_priv->dpiRatio = Utils::getScreenDpiRatioByWidget(this); - applyScaling(); - }); m_priv->focusConnection = QObject::connect(qApp, &QApplication::focusChanged, this, [&] (QWidget * from, QWidget *to){ @@ -322,14 +311,6 @@ void QtMsg::applyScaling() QString zoom = QString::number(m_priv->dpiRatio) + "x"; m_centralWidget->setProperty("scaling", zoom); - - // layout()'s SetFixedSize constraint locks the dialog's own top-level - // size the first time it activates. Shrinking a child's minimum width - // afterwards (the WatchForDpiChange-triggered re-run, once the real - // ratio arrives) doesn't automatically renegotiate an already-mapped - // Wayland surface down to the smaller size -- force it explicitly. - layout()->invalidate(); - adjustSize(); } QtMsg::~QtMsg() diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index b62da52d8..6968c5343 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -565,6 +565,12 @@ double Utils::getScreenDpiRatio(const QPoint& pt) double Utils::getScreenDpiRatioByHWND(int hwnd) { + // See getScreenDpiRatioByWidget: on Wayland Qt's PassThrough + // devicePixelRatio already scales native widgets, so manual scaling + // must stay neutral to avoid double-scaling. + if (qApp && QGuiApplication::platformName() == QLatin1String("wayland")) + return 1.0; + unsigned int _dpi_x = 0; unsigned int _dpi_y = 0; double nScale = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)hwnd, _dpi_x, _dpi_y); @@ -573,14 +579,22 @@ double Utils::getScreenDpiRatioByHWND(int hwnd) double Utils::getScreenDpiRatioByWidget(QWidget* wid) { - // Manual DPI scaling here (multiplying sizes by dpiRatio) computes a - // logical/DIP size for widgets that don't rely purely on Qt's layout - // system (e.g. cplatformdecoration.cpp's CUSTOM_BORDER_WIDTH * ratio). - // That's independent of Qt's own automatic HiDPI backing-store - // scaling (which just renders whatever DIP size is chosen more - // crisply) -- it's needed on Wayland exactly the same way it's - // needed on X11, so this used to (incorrectly) skip it with a flat - // 1.0 return on Wayland, leaving those widgets sized as if unscaled. + // This returns the factor by which the app MANUALLY scales native Qt + // widgets (multiplying sizes/margins by it, plus the QSS "scaling" + // property that bakes the factor into pixel font-sizes). That manual + // scaling is the app's substitute for Qt's automatic HiDPI scaling, + // which is deliberately disabled on X11 (AA_Use96Dpi, + // QT_ENABLE_HIGHDPI_SCALING=0) so devicePixelRatio() stays 1 there. + // + // On Wayland, HighDpiScaleFactorRoundingPolicy::PassThrough is set, so + // Qt's own devicePixelRatio() reports the true fractional scale and Qt + // already scales every widget by it. Applying the app's manual scaling + // on top double-scales (a 1.25 display renders native UI at ~1.56). + // Let Qt's devicePixelRatio be the single source of scaling on Wayland + // by returning a neutral 1.0 here. + if (qApp && QGuiApplication::platformName() == QLatin1String("wayland")) + return 1.0; + if (!wid) return 1; From 104067b7cc25d5f1bd6471835e280a101028ff53 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 11:10:12 -0400 Subject: [PATCH 23/32] Revert "Stop double-scaling native widgets on Wayland" This reverts commit 3ed1c04513c24b91587938ea07e3b32d5caaa6e7. (cherry picked from commit 2e68c945d53d3a046f38d16831cd071aa687f790) Signed-off-by: Peter P. Lupo --- win-linux/src/components/cmessage.cpp | 27 ++++++++++++++++++++---- win-linux/src/utils.cpp | 30 +++++++-------------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index db63af3b2..08040395c 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -150,10 +150,12 @@ class QtMsg : public QDialog void setContent(const QString&); void setCheckBox(const QString &chekBoxText, bool checkBoxState); bool getCheckStatus(); - // Applies every dpiRatio-dependent size/margin/stylesheet. On Wayland - // dpiRatio is a neutral 1.0 (Qt's PassThrough devicePixelRatio does the - // scaling); on X11 it carries the real factor (Qt HiDPI is disabled - // there). See Utils::getScreenDpiRatioByWidget. + // Re-applies every dpiRatio-dependent size/margin/stylesheet. Called once + // from the constructor and again whenever this dialog's own + // devicePixelRatio() self-corrects after the Wayland compositor's async + // fractional-scale answer arrives (the initial read in + // QtMsgPrivateIntf's constructor can catch the same startup race already + // handled for CWindowBase, but this dialog never had the reactive fix). void applyScaling(); QWidget *m_boxButtons = nullptr, @@ -272,6 +274,15 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->move(0, 0); applyScaling(); + // m_priv->dpiRatio above may have been read from this dialog's own + // devicePixelRatio() before Qt received the Wayland compositor's async + // fractional-scale answer for its surface -- re-derive and re-apply + // once Qt tells us it actually changed (mirrors CWindowBase's use of + // the same watcher). + Utils::WatchForDpiChange(this, [this]() { + m_priv->dpiRatio = Utils::getScreenDpiRatioByWidget(this); + applyScaling(); + }); m_priv->focusConnection = QObject::connect(qApp, &QApplication::focusChanged, this, [&] (QWidget * from, QWidget *to){ @@ -311,6 +322,14 @@ void QtMsg::applyScaling() QString zoom = QString::number(m_priv->dpiRatio) + "x"; m_centralWidget->setProperty("scaling", zoom); + + // layout()'s SetFixedSize constraint locks the dialog's own top-level + // size the first time it activates. Shrinking a child's minimum width + // afterwards (the WatchForDpiChange-triggered re-run, once the real + // ratio arrives) doesn't automatically renegotiate an already-mapped + // Wayland surface down to the smaller size -- force it explicitly. + layout()->invalidate(); + adjustSize(); } QtMsg::~QtMsg() diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 6968c5343..b62da52d8 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -565,12 +565,6 @@ double Utils::getScreenDpiRatio(const QPoint& pt) double Utils::getScreenDpiRatioByHWND(int hwnd) { - // See getScreenDpiRatioByWidget: on Wayland Qt's PassThrough - // devicePixelRatio already scales native widgets, so manual scaling - // must stay neutral to avoid double-scaling. - if (qApp && QGuiApplication::platformName() == QLatin1String("wayland")) - return 1.0; - unsigned int _dpi_x = 0; unsigned int _dpi_y = 0; double nScale = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)hwnd, _dpi_x, _dpi_y); @@ -579,22 +573,14 @@ double Utils::getScreenDpiRatioByHWND(int hwnd) double Utils::getScreenDpiRatioByWidget(QWidget* wid) { - // This returns the factor by which the app MANUALLY scales native Qt - // widgets (multiplying sizes/margins by it, plus the QSS "scaling" - // property that bakes the factor into pixel font-sizes). That manual - // scaling is the app's substitute for Qt's automatic HiDPI scaling, - // which is deliberately disabled on X11 (AA_Use96Dpi, - // QT_ENABLE_HIGHDPI_SCALING=0) so devicePixelRatio() stays 1 there. - // - // On Wayland, HighDpiScaleFactorRoundingPolicy::PassThrough is set, so - // Qt's own devicePixelRatio() reports the true fractional scale and Qt - // already scales every widget by it. Applying the app's manual scaling - // on top double-scales (a 1.25 display renders native UI at ~1.56). - // Let Qt's devicePixelRatio be the single source of scaling on Wayland - // by returning a neutral 1.0 here. - if (qApp && QGuiApplication::platformName() == QLatin1String("wayland")) - return 1.0; - + // Manual DPI scaling here (multiplying sizes by dpiRatio) computes a + // logical/DIP size for widgets that don't rely purely on Qt's layout + // system (e.g. cplatformdecoration.cpp's CUSTOM_BORDER_WIDTH * ratio). + // That's independent of Qt's own automatic HiDPI backing-store + // scaling (which just renders whatever DIP size is chosen more + // crisply) -- it's needed on Wayland exactly the same way it's + // needed on X11, so this used to (incorrectly) skip it with a flat + // 1.0 return on Wayland, leaving those widgets sized as if unscaled. if (!wid) return 1; From 4c078ebf1b214669e308b9648dafbb8ea5de926f Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 11:10:54 -0400 Subject: [PATCH 24/32] TEMPORARY: log per-widget DPI factor to diagnose oversized dialog Captures platform, widget class, size, widget/screen devicePixelRatio, and the computed manual scaling factor for every native widget, so the oversized dialog's numbers can be compared against the correctly-sized main window on Wayland. To be reverted once diagnosed. Signed-off-by: Peter P. Lupo (cherry picked from commit d99f6449fe196da310922205dbef073bf483cad0) --- win-linux/src/utils.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index b62da52d8..2705380bc 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -29,6 +29,7 @@ #include "utils.h" #include "defines.h" #include +#include #include #include #include @@ -595,6 +596,28 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif + // TEMPORARY diagnostic: capture, per native widget (main window, tab + // widget, dialogs), the factor this computes vs the widget's own and + // the screen's devicePixelRatio -- to see how the oversized dialog's + // numbers differ from the correctly-sized main window on Wayland. + { + FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); + if (pLogFile) + { + double screenDpr = (qApp && QGuiApplication::primaryScreen()) + ? QGuiApplication::primaryScreen()->devicePixelRatio() : -1.0; + fprintf(pLogFile, + "[%lld] platform=%s class=%s widget=%p size=%dx%d widgetDPR=%f screenDPR=%f dpiApp=%f chosen=%f\n", + QDateTime::currentMSecsSinceEpoch(), + qApp ? QGuiApplication::platformName().toUtf8().constData() : "?", + wid->metaObject()->className(), (void*)wid, + wid->width(), wid->height(), + wid->devicePixelRatio(), screenDpr, dpiApp, + dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); + fclose(pLogFile); + } + } + if ( dpiApp >= 0 ) { return choose_scaling(dpiApp); } From 81ec7cb791e73e3256726413f0a0bf85dd2c13ac Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 15:18:52 -0400 Subject: [PATCH 25/32] Fix oversized/mis-styled message dialog on Wayland The dialog computes its scaling factor once from its (stable) parent in QtMsgPrivateIntf and applies it in the constructor -- which is already correct (1.25) and matches how X11 styles the dialog exactly once. The WatchForDpiChange re-derivation added earlier read the dialog's OWN devicePixelRatio, which on Wayland races 2.0 -> 1.25 as the compositor answers for the freshly-created surface. That fired applyScaling() with a transient 2.0 (scaling="2x": double fonts/margins) and then 1.25, leaving the dialog oversized, with the wrong icon, non-bold and non-centered text, and a massive height. X11 never sees the race (its devicePixelRatio never changes, so the watcher never fired) and looked correct. Drop the watcher and the forced-resize workaround so Wayland styles once from the parent ratio too. Also removes the temporary per-widget DPI diagnostic logging. Signed-off-by: Peter P. Lupo (cherry picked from commit e155cbf037ea014f5fd6174b691cbb0cdde8bf64) --- win-linux/src/components/cmessage.cpp | 32 ++++++++------------------- win-linux/src/utils.cpp | 23 ------------------- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 08040395c..c66eb368d 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -150,12 +150,9 @@ class QtMsg : public QDialog void setContent(const QString&); void setCheckBox(const QString &chekBoxText, bool checkBoxState); bool getCheckStatus(); - // Re-applies every dpiRatio-dependent size/margin/stylesheet. Called once - // from the constructor and again whenever this dialog's own - // devicePixelRatio() self-corrects after the Wayland compositor's async - // fractional-scale answer arrives (the initial read in - // QtMsgPrivateIntf's constructor can catch the same startup race already - // handled for CWindowBase, but this dialog never had the reactive fix). + // 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, @@ -273,16 +270,13 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->setLayout(m_priv->cLayout); m_centralWidget->move(0, 0); + // Scale once, from the (stable) parent's ratio computed in + // QtMsgPrivateIntf. Deliberately NOT re-derived from this dialog's own + // devicePixelRatio: on Wayland that value races 2.0 -> 1.25 as the + // compositor answers, and re-running applyScaling() through the transient + // 2.0 corrupts the dialog's styling and leaves it oversized. X11 never + // sees that race and styles once correctly; do the same everywhere. applyScaling(); - // m_priv->dpiRatio above may have been read from this dialog's own - // devicePixelRatio() before Qt received the Wayland compositor's async - // fractional-scale answer for its surface -- re-derive and re-apply - // once Qt tells us it actually changed (mirrors CWindowBase's use of - // the same watcher). - Utils::WatchForDpiChange(this, [this]() { - m_priv->dpiRatio = Utils::getScreenDpiRatioByWidget(this); - applyScaling(); - }); m_priv->focusConnection = QObject::connect(qApp, &QApplication::focusChanged, this, [&] (QWidget * from, QWidget *to){ @@ -322,14 +316,6 @@ void QtMsg::applyScaling() QString zoom = QString::number(m_priv->dpiRatio) + "x"; m_centralWidget->setProperty("scaling", zoom); - - // layout()'s SetFixedSize constraint locks the dialog's own top-level - // size the first time it activates. Shrinking a child's minimum width - // afterwards (the WatchForDpiChange-triggered re-run, once the real - // ratio arrives) doesn't automatically renegotiate an already-mapped - // Wayland surface down to the smaller size -- force it explicitly. - layout()->invalidate(); - adjustSize(); } QtMsg::~QtMsg() diff --git a/win-linux/src/utils.cpp b/win-linux/src/utils.cpp index 2705380bc..b62da52d8 100644 --- a/win-linux/src/utils.cpp +++ b/win-linux/src/utils.cpp @@ -29,7 +29,6 @@ #include "utils.h" #include "defines.h" #include -#include #include #include #include @@ -596,28 +595,6 @@ double Utils::getScreenDpiRatioByWidget(QWidget* wid) double dpiApp = AscAppManager::getInstance().GetMonitorScaleByWindow((WindowHandleId)wid->winId(), nDpiX, nDpiY); #endif - // TEMPORARY diagnostic: capture, per native widget (main window, tab - // widget, dialogs), the factor this computes vs the widget's own and - // the screen's devicePixelRatio -- to see how the oversized dialog's - // numbers differ from the correctly-sized main window on Wayland. - { - FILE* pLogFile = fopen("/tmp/native_dpi_debug.log", "a"); - if (pLogFile) - { - double screenDpr = (qApp && QGuiApplication::primaryScreen()) - ? QGuiApplication::primaryScreen()->devicePixelRatio() : -1.0; - fprintf(pLogFile, - "[%lld] platform=%s class=%s widget=%p size=%dx%d widgetDPR=%f screenDPR=%f dpiApp=%f chosen=%f\n", - QDateTime::currentMSecsSinceEpoch(), - qApp ? QGuiApplication::platformName().toUtf8().constData() : "?", - wid->metaObject()->className(), (void*)wid, - wid->width(), wid->height(), - wid->devicePixelRatio(), screenDpr, dpiApp, - dpiApp >= 0 ? choose_scaling(dpiApp) : wid->devicePixelRatio()); - fclose(pLogFile); - } - } - if ( dpiApp >= 0 ) { return choose_scaling(dpiApp); } From eaaaccdbebd43d94f50755f564474c1d65e9cacd Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 21:20:34 -0400 Subject: [PATCH 26/32] Fix Wayland message dialog size and style it like the GTK dialog Size (root cause): QtMsg computed its scale from getScreenDpiRatioByWidget on the DIALOG itself. A freshly-created top-level surface reports the rounded default devicePixelRatio (2.0) on Wayland until the compositor answers with the real fractional scale, so the dialog was sized for 2.0 (scaling=2x, everything doubled) and never corrected -- hence 'enormous'. Derive the ratio from the stable parent window instead (its dpr has long since settled), via stableDpiRatioForDialog(). Styling: on Wayland the app uses this custom QtMsg (X11 uses the native GTK dialog); QtMsg was a plain fallback that never matched GTK. Bring it in line with the GTK message dialog: bold the primary (first) line and keep following lines regular, centre the text, and recolour the warning icon from yellow (#f1c40f) to GTK's warning orange (#f57900). Signed-off-by: Peter P. Lupo (cherry picked from commit b7fab27c037d40551ee55aeecef65e1efe571406) --- win-linux/res/icons/message_warn.svg | 2 +- win-linux/src/components/cmessage.cpp | 61 +++++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) 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" /> 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 { @@ -215,7 +241,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); @@ -239,10 +265,12 @@ QtMsg::QtMsg(QWidget * p) // m_message->setWordWrap(true); m_message->setProperty("class", "msg-report"); - m_message->setTextFormat(Qt::PlainText); + m_message->setTextFormat(Qt::RichText); // setText() bolds the primary line + m_message->setAlignment(Qt::AlignHCenter); // match the GTK message dialog m_content->setProperty("class", "msg-report"); m_content->setTextFormat(Qt::RichText); + m_content->setAlignment(Qt::AlignHCenter); m_content->setOpenExternalLinks(true); m_priv->fLayout = new QFormLayout; @@ -270,12 +298,9 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->setLayout(m_priv->cLayout); m_centralWidget->move(0, 0); - // Scale once, from the (stable) parent's ratio computed in - // QtMsgPrivateIntf. Deliberately NOT re-derived from this dialog's own - // devicePixelRatio: on Wayland that value races 2.0 -> 1.25 as the - // compositor answers, and re-running applyScaling() through the transient - // 2.0 corrupts the dialog's styling and leaves it oversized. X11 never - // sees that race and styles once correctly; do the same everywhere. + // 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, @@ -472,7 +497,19 @@ void QtMsg::setIcon(MsgType msgType) void QtMsg::setText( const QString& t) { - m_message->setText(t); + // Mirror the GTK message dialog: the first line is the bold "primary" + // text; any following lines stay regular "secondary" text. + const QString esc = t.toHtmlEscaped(); + const int nl = esc.indexOf('\n'); + QString html; + if (nl != -1) { + QString secondary = esc.mid(nl + 1); + secondary.replace("\n", "
"); + html = "" + esc.left(nl) + "
" + secondary; + } else { + html = "" + esc + ""; + } + m_message->setText(html); } void QtMsg::setContent( const QString& t) From 44e44072d3dfeab674a700d762f930225097f55c Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sat, 25 Jul 2026 22:54:58 -0400 Subject: [PATCH 27/32] Refine Wayland message dialog styling toward the GTK look - Enlarge the primary line (bold + ~1.2x the secondary font size), split cleanly from the secondary line which stays regular. - Widen the gap between the primary and secondary lines. - Enlarge the message icon (35 -> 44) closer to GTK's dialog icon size. Signed-off-by: Peter P. Lupo (cherry picked from commit 768387db0624229ea73ff0655545de92ac9eee07) --- win-linux/src/components/cmessage.cpp | 43 ++++++++++++++++----------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index c16356dd1..72e5bbbb5 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 @@ -325,7 +325,9 @@ void QtMsg::applyScaling() m_typeIcon->setFixedSize(int(round(MSG_ICON_WIDTH*m_priv->dpiRatio - 0.25)), int(round(MSG_ICON_HEIGHT*m_priv->dpiRatio - 0.25))); - m_message->setStyleSheet(QString("margin-bottom: %1px;").arg(int(8*m_priv->dpiRatio))); + // 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); @@ -463,13 +465,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); @@ -497,19 +513,12 @@ void QtMsg::setIcon(MsgType msgType) void QtMsg::setText( const QString& t) { - // Mirror the GTK message dialog: the first line is the bold "primary" - // text; any following lines stay regular "secondary" text. - const QString esc = t.toHtmlEscaped(); - const int nl = esc.indexOf('\n'); - QString html; - if (nl != -1) { - QString secondary = esc.mid(nl + 1); - secondary.replace("\n", "
"); - html = "" + esc.left(nl) + "
" + secondary; - } else { - html = "" + esc + ""; - } - m_message->setText(html); + // 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) From 1cce5fb9792be14fb321ffda600de0d9da904007 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sun, 26 Jul 2026 11:28:38 -0400 Subject: [PATCH 28/32] Fix Wayland message dialog: remove DPI-scaling from min-width, left-align text setMinimumWidth was multiplied by dpiRatio (e.g. 1.25), making the dialog 25% wider than the X11/GTK equivalent and forcing text centering to leave a large dead gap between the icon and the primary line. Use unscaled logical-pixel values for the minimum width (Qt's layout engine already handles DPI independently) and change label alignment to AlignLeft so the text sits flush in its column, matching the GTK message dialog layout. Signed-off-by: Peter P. Lupo --- win-linux/src/components/cmessage.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 72e5bbbb5..9e3608b4e 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -265,12 +265,12 @@ QtMsg::QtMsg(QWidget * p) // m_message->setWordWrap(true); m_message->setProperty("class", "msg-report"); - m_message->setTextFormat(Qt::RichText); // setText() bolds the primary line - m_message->setAlignment(Qt::AlignHCenter); // match the GTK message dialog + m_message->setTextFormat(Qt::RichText); + m_message->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_content->setProperty("class", "msg-report"); m_content->setTextFormat(Qt::RichText); - m_content->setAlignment(Qt::AlignHCenter); + m_content->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_content->setOpenExternalLinks(true); m_priv->fLayout = new QFormLayout; @@ -335,7 +335,7 @@ void QtMsg::applyScaling() m_boxButtons->layout()->setContentsMargins(0,int(10*m_priv->dpiRatio),0,0); m_boxButtons->layout()->setSpacing(int(8*m_priv->dpiRatio)); - m_centralWidget->setMinimumWidth(int((m_priv->buttons.size() > 2 ? 400 : 350)*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))); @@ -407,7 +407,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) From 40e6edea61e9345eabb96ab470cec6afb43858f2 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Sun, 26 Jul 2026 15:43:48 -0400 Subject: [PATCH 29/32] Center icon+text block over buttons in Wayland message dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the content row (icon + text) and the buttons row together in a single bodyWidget. Add it to cLayout with AlignCenter so the whole block uses its natural width and is centered symmetrically. Previously the content row filled the entire available width while m_boxButtons was AlignCenter — so the icon's left edge was at the far left margin while the buttons started further in, creating an unbalanced appearance. Now both rows share the same horizontal extent. Signed-off-by: Peter P. Lupo --- win-linux/src/components/cmessage.cpp | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/win-linux/src/components/cmessage.cpp b/win-linux/src/components/cmessage.cpp index 9e3608b4e..218fb2137 100644 --- a/win-linux/src/components/cmessage.cpp +++ b/win-linux/src/components/cmessage.cpp @@ -231,6 +231,7 @@ class QtMsg::QtMsgPrivateIntf { // 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); @@ -255,13 +256,20 @@ QtMsg::QtMsg(QWidget * p) m_centralWidget->setProperty("uitheme", QString::fromStdWString(GetCurrentTheme().originalId())); m_priv->cLayout = new QVBoxLayout; - QHBoxLayout * _h_layout2 = new QHBoxLayout; - QHBoxLayout * _h_layout1 = new QHBoxLayout; - m_priv->cLayout->addLayout(_h_layout2, 1); - m_priv->cLayout->addLayout(_h_layout1, 0); + // 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"); - _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"); @@ -277,14 +285,20 @@ QtMsg::QtMsg(QWidget * p) m_priv->fLayout->addWidget(m_message); m_priv->fLayout->addWidget(m_content); m_priv->fLayout->setSpacing(0); - _h_layout2->addLayout(m_priv->fLayout, 1); - _h_layout2->setContentsMargins(0,0,0,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); - _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); From 51da4a34354a80ac39cf68a78e3f1c7bc7f44798 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Mon, 27 Jul 2026 23:44:09 -0400 Subject: [PATCH 30/32] feat: configurable default save format (ODF / OOXML) New documents are named and saved as OOXML by default, with the format resolved at creation time from a tiered configuration cascade built on the existing QSettings registry group -- the same one that already governs CheckForUpdates -- rather than a new configuration subsystem. Resolution order (Utils::defaultSaveFormat): 1. system scope EnforceDefaultFormat=true -> system DefaultSaveFormat, stop 2. user scope DefaultSaveFormat, if set 3. system scope DefaultSaveFormat, if set 4. compiled-in baseline, OOXML Two keys are added to the existing group: DefaultSaveFormat (ODF|OOXML) and EnforceDefaultFormat. On Linux the files are /etc/xdg/euro-office/ DesktopEditors.conf (system) and ~/.config/euro-office/DesktopEditors.conf (user); on Windows the corresponding HKLM/HKCU keys. Note the group is REG_GROUP_KEY, i.e. lowercase "euro-office" on Linux. The user scope is read with fallbacks disabled: QSettings falls back to the system scope by default, which would otherwise make an administrator's baseline indistinguishable from a preference the user set for themselves, breaking both tier 2 vs. tier 3 precedence and the onboarding trigger. Surfaces: - Start page Settings gains a "Default save format" combo, carried over the existing settings:init / settings:apply round-trip. When the format is enforced the combo is disabled and the panel explains that the setting is managed by the organization; an enforced format is also ignored server-side in applySettings, so policy cannot be bypassed from the renderer. - A one-time startup dialog lets an unmanaged user pick a format before creating their first document. It is skipped when an administrator has set or locked a format, or once the user has chosen. Dismissing it without choosing records FormatOnboardingShown so it never reappears, and resolution falls through as normal. newFileFormat() centralizes the document-type -> format mapping that was duplicated across the create:new handler, the --new: command-line path and newFileName(); forms stay DOCXF since they are only defined for OOXML. The cascade was verified against real QSettings files across 15 cases covering all four tiers, the hard lock, malformed values and the dismissed-onboarding path. Signed-off-by: Peter P. Lupo --- common/loginpage/src/locale.js | 4 + common/loginpage/src/panelsettings.js | 32 +++++++ .../src/cascapplicationmanagerwrapper.cpp | 94 ++++++++++++++++--- win-linux/src/cascapplicationmanagerwrapper.h | 1 + .../cascapplicationmanagerwrapper_private.h | 6 +- win-linux/src/components/cmessage.cpp | 25 ++++- win-linux/src/components/cmessage.h | 7 +- win-linux/src/defines.h | 6 ++ win-linux/src/prop/cmainwindowimpl.cpp | 4 + win-linux/src/utils.cpp | 52 ++++++++++ win-linux/src/utils.h | 5 + 11 files changed, 215 insertions(+), 21 deletions(-) diff --git a/common/loginpage/src/locale.js b/common/loginpage/src/locale.js index adb518edb..7e7bbf4b6 100644 --- a/common/loginpage/src/locale.js +++ b/common/loginpage/src/locale.js @@ -129,6 +129,10 @@ l10n.en = { settOptLaunchMode: 'Open file', settOptLaunchInTab: 'In its own tab', settOptLaunchInWindow: 'In its own window', + settDefaultSaveFormat: 'Default save format', + settOptFormatOoxml: 'Compatibility (OOXML)', + settOptFormatOdf: 'Sovereign (ODF)', + settDefaultSaveFormatManaged: 'Your default save format is managed by your organization.', settSpellcheckDetection: 'Spelling language detection', settOptDisabled: 'Disabled', settOptEnabled: 'Enabled', diff --git a/common/loginpage/src/panelsettings.js b/common/loginpage/src/panelsettings.js index fc3d24a24..cf4abaeb5 100644 --- a/common/loginpage/src/panelsettings.js +++ b/common/loginpage/src/panelsettings.js @@ -247,6 +247,18 @@ +