Wayland support - #11
Open
pplupo wants to merge 49 commits into
Open
Conversation
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Enable Qt input method handling for dead key sequences (ã, é, ü, ç, etc.): 1. Enable WA_InputMethodEnabled on the QCefView widget for Wayland 2. Suppress raw dead key events (Qt::Key_Dead_Grave..Key_Dead_Greek) in keyPressEvent/keyReleaseEvent so Qt's IM can compose them 3. Add inputMethodEvent handler that receives the composed character and sends it to CEF via the full KEYDOWN->CHAR->KEYUP sequence 4. Add inputMethodQuery to support Qt's IM state queries Applied cleanly on top of acc5477b (074a568 in desktop-sdk) with no changes to OnPaint or any other rendering code. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
CEF's off-screen render buffer reached Qt through OnPaint calling repaint()/update() from inside CEF's callback stack. On Wayland the wl_surface_commit / frame-callback handshake never completed at a top-of-loop point, so the app looked frozen until a physical input event forced a synchronous flush. Iterations tried before landing on the fix: - Fixed the 16ms Wayland heartbeat detection (QGuiApplication::platformName() instead of an env var check that could miss Qt6's default Wayland backend). - Swapped repaint()/sendEvent(UpdateRequest) for a plain update() call in OnPaint. - Moved the repaint trigger out of OnPaint entirely: OnPaint now only stages the frame and sets a dirty flag; a top-of-loop poller drove update() and QWindow::requestUpdate(). This didn't fully clear the stall on this compositor -- rendering still only committed on user input, effectively back to the original symptom. - Added a guarded, input-excluded processEvents() fallback in the poller. This regressed badly: since content animates continuously, the poller spun the loop ~60x/sec while excluding user input, starving mouse and keyboard. Removed. Final fix: present the CEF buffer through a QOpenGLWidget overlay (QCefGLWidget) instead of the raster QWidget backing store. On Wayland the GL swap is itself the wl_surface_commit and is throttled by the compositor's frame callback natively, so presentation no longer depends on a top-of-loop nudge or a physical input event to complete. OnPaint stages the frame and pushes it to the overlay; the poller only nudges a repaint, with no event-loop spin. Also pulls in the OpenGLWidgets Qt module locally in qt_wrapper's CMakeLists.txt and .pro file, since the build only resets desktop-sdk and never touches core/common.cmake. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
CEF's own clipboard integration silently no-ops in off-screen-rendering mode under Wayland: the OS clipboard protocol needs a real wl_surface and input-event serial to claim ownership, and OSR mode never has one (Qt owns the only real window). Copy writes never leave the app and paste never sees anything written outside it. Route copy/paste through the platform widget's real clipboard instead: a fire-and-forget CefProcessMessage for writes (clipboard_write) and a CefMessageRouter query/callback round trip for reads (clipboard_read), bridged on the Qt side via QClipboard in QCefView::SetClipboardData/ GetClipboardData. sdkjs mirrors every copy write into the bridge and services paste through it when available, covering both the legacy and new (navigator.clipboard) copy paths. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…rray size) - CefProcessMessage sends go through CefFrame::SendProcessMessage in this CEF version, not CefBrowser (which has no such member). - X11/X.h #defines Success to 0, colliding with CefMessageRouterBrowserSide::Callback::Success; #undef it locally. - EXTEND_METHODS_COUNT needed bumping to 198 for the added nativeClipboardWrite entry (fixed-size const char* array). Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
message_router_ (base client::ClientHandler) is only constructed lazily inside OnAfterCreated(), once a real browser exists. The previous registration point, right after CAscClientHandler's own construction in CCefView::load(), ran before that -- message_router_ was still a null CefRefPtr there, so AddHandler() dereferenced null and crashed on every launch. Move registration into OnAfterCreated(), right after the base class call that creates message_router_, guarded so it only runs once even if OnAfterCreated() fires again for popups. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…e reverted) Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Diagnostic logging (now removed) confirmed the actual bug: CCefView's GetScreenPoint mixed physical-pixel and DIP units. widgetScreenX/Y (from GetWidgetScreenPosition) is already in physical/device pixels; viewX/viewY are DIPs (the convention used throughout for GetViewRect/mouse events). Multiplying viewX/viewY by the device scale factor on top of that -- following CEF's own doc comment for this callback, "provide screen device (pixel) coordinates" -- doubles the scaling for the view-relative portion. Chromium exposes this callback's result to JS as MouseEvent.screenX/screenY, which per the DOM spec must be in CSS pixels (DIPs), not physical pixels. With a fractional device scale factor (e.g. 1.15), the excess grew proportionally with distance from the origin -- confirmed against logged click coordinates, which showed an exact match to click * (scale - 1). sdkjs's own context-menu edge-avoidance logic, positioned using this inflated screenX/screenY, pushed the menu off-screen for clicks near the edge of large (especially maximized) windows. Normalize both terms to DIPs instead of physical pixels. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
… (to be reverted) Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
GetScreenPoint is confirmed correct after the previous fix (screenX/Y now track viewX/Y properly, no more device-scale inflation), but the document context menu offset persists regardless -- confirming GetScreenPoint isn't the code path responsible for that menu's positioning. Removing the temporary logging; the fix to GetScreenPoint itself is kept since it's a genuine correctness improvement (matches the DOM MouseEvent.screenX/screenY spec), even though it didn't turn out to be the cause of the reported bug. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…pping Alternative to the JS-level menu-position compensation: instead of correcting for CEF OSR applying device_scale_factor as an internal CSS zoom, eliminate the zoom at the source by always reporting a scale factor of 1.0 to CEF and feeding it a physical-pixel view rect/buffer size directly. GetViewRect and GetScreenInfo now report physical pixels instead of DIPs; GetScreenPoint no longer needs a DIP/physical conversion since both terms are physical; mouse events sent to CEF are scaled up from Qt's DIP coordinates to match. Expected side effect: window.innerWidth/innerHeight, devicePixelRatio, and all CSS-pixel-based UI sizing in sdkjs/web-apps will change (more CSS pixels reported for the same physical screen area), which may shrink or misalign toolbars/icons/text across the whole app. This is exploratory -- keep isolated on this branch until confirmed safe. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
… double-click) Three related native bugs in the Qt/CEF Wayland input path, found via an iterative build-test-log cycle against the spreadsheet editor: 1. focusOutEvent unconditionally returned before telling CEF it lost focus. On Wayland, keyboard events are forwarded to CEF manually regardless of Qt's own focus state, so this let CEF's internal focus bookkeeping drift out of sync with Qt's. Propagate focus-out to CEF as intended. 2. Tab never reached keyPressEvent at all: Qt's own QWidget::focusNextPrevChild() intercepts Tab/Shift+Tab for widget-to-widget focus traversal before keyPressEvent sees the key. On Wayland nothing reliably reclaimed focus afterward, producing a freeze where no key (including Alt-menu accelerators) worked until a mouse click forced Qt to resolve focus. Override focusNextPrevChild() to refuse the traversal on Wayland so Tab falls through to keyPressEvent() and is forwarded to CEF like any other key. 3. Double-click silently did nothing: Qt's Wayland backend delivers an ordinary mousePressEvent for BOTH clicks of a double-click (not just the first), so the naive fix of forwarding a synthetic click from mouseDoubleClickEvent sent a THIRD, conflicting click-down event to CEF for two physical clicks -- corrupting Blink's click-count tracking, which sdkjs's double-click-to-edit relies on via MouseEvent.detail === 2. Track click count/timing natively in mousePressEvent (mirroring Qt's own multi-click detection), forward the same count on the matching release, and stop mouseDoubleClickEvent from talking to CEF at all -- it now only keeps Qt's own bookkeeping intact. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com> Assisted-by: Claude Code:Opus 4.8
…n Wayland OSR mode Windowed CEF gets a real native window and sets the OS cursor itself when the page requests a shape change. OSR mode (used for Wayland) has no CEF-owned window, so OnCursorChange was never wired up and nothing ever changed the cursor there. Override CefDisplayHandler::OnCursorChange in CAscClientHandler and bridge the requested cef_cursor_type_t to the platform widget via a new CCefViewWidgetImpl::SetCursorType, implemented in QCefView with a mapping to the closest Qt::CursorShape. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
…er under Wayland OSR OnCursorChange previously only forwarded the named cef_cursor_type_t value, which covers CSS cursor keywords like text/move/ew-resize but not `cursor: url(...)` custom-image cursors (used throughout sdkjs, e.g. the spreadsheet's column/row resize-divider hover cursor and table-select cursors registered via g_oHtmlCursor.register). CEF reports those as CT_CUSTOM with the actual bitmap in custom_cursor_info instead of a named type, so they silently fell back to the plain arrow. Add CCefViewWidgetImpl::SetCursorCustom, taking the premultiplied BGRA buffer/size/hotspot CEF provides, and build a real QCursor from it in QCefView -- same buffer layout OnPaint already assumes for the OSR frame. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…dsf-1.0-osr Add CCefViewWidgetImpl::GetUIScalePercentage(), overridden in QCefView to bucket Qt's logicalDotsPerInch() into 100/150/200/250% the same way LibreOffice's CountDPIScaleFactor() does off a 96 DPI baseline. This is deliberately independent of GetDeviceScaleFactor()/devicePixelRatio(), which dsf-1.0-osr forces to always report 1:1 for CEF's OSR coordinate mapping -- reusing that value here would lose the real DPI signal on HiDPI displays. CCefView::UpdateUIScalePercentage() pushes the computed factor into the page: it sets --pixel-ratio-factor, --x-small-btn-size, and --x-small-btn-icon-size (CSS custom properties already consumed by web-apps' LESS but never previously set by anything), and corrects window.devicePixelRatio before re-running sdkjs's own AscBrowser.checkZoom(), since sdkjs's canvas rendering reads devicePixelRatio directly rather than any of the LESS-side variables. Called on main-frame load, and again on widget move (covers the common case of dragging the window to a different-DPI monitor without adding new Qt screen-changed signal plumbing). Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
The rebuilt binary confirmed the fix code is present (via the devicePixelRatio marker string), but toolbar icon size still shows no visible change. Add temporary diagnostics to find out why: - CCefView::UpdateUIScalePercentage() now logs to /tmp/uiscale_debug.log (bypassing the app's --log-severity=disable, which would otherwise silently drop LOG()-style messages) confirming it's actually called, with what percentage/factor, and against which frame URL. - The injected JS now wraps each step (devicePixelRatio override, CSS custom property setProperty calls, checkZoom() re-run) in its own try/catch with console.log/console.error, since a single throw (e.g. if devicePixelRatio is non-configurable in this Chromium build) could have silently aborted the rest of the injected script before the CSS variables were ever set. - CAscApplicationManager now honors EURO_OFFICE_DEBUG=1 to call the existing (but previously never-invoked) SetDebugInfoSupport(true), so F1 opens real DevTools and the above console output is actually visible instead of guessing blind. All temporary; remove once the root cause is confirmed and fixed. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…on scale Two real bugs found via live CDP inspection of the built binary: 1. The actual editor UI (ribbon, AscCommon) loads in a nested iframe (documenteditor/main/index.html), not the main frame. OnLoadEnd's frame->IsMain() check meant UpdateUIScalePercentage() never injected into that iframe -- only into the outer shell pages, which have no ribbon at all. Fixed by enumerating every frame via CefBrowser::GetFrameIdentifiers()/GetFrame() and injecting into each. 2. Setting --x-small-btn-size via document.documentElement.style even directly in the real iframe (confirmed via devtools) had zero effect on .btn-toolbar's rendered height -- verified empirically: an inline !important override on the button DID work and rendered correctly (screenshot-confirmed via CDP Page.captureScreenshot), but the CSS custom property route does not reach this rule for a reason not pinned down (not an @Property registration, not an inline style, not a media query -- the matched-styles cascade looked correct but the value never took effect). Rather than keep chasing that, inject a <style id="ui-scale-override-style"> with !important declarations for .btn-toolbar/.btn-toolbar .icon directly, which is guaranteed to win regardless of whatever is defeating the variable indirection. Screenshot-confirmed working: toolbar icons visibly shrink to match LibreOffice's density. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Two corrections based on user feedback after the icon-size fix landed: 1. Wrong scale signal. GetUIScalePercentage() bucketed off logicalDotsPerInch() the way LibreOffice's CountDPIScaleFactor() does (X11-era, 96 DPI baseline) -- but on this machine's actual Wayland compositor (KDE, configured display scale 1.15 via kscreen-doctor), that bucketing returned a flat 100%, silently discarding the real configured scale. Wayland sets an explicit output scale directly rather than relying on physical-DPI heuristics, and Qt's own devicePixelRatio() already tracks that scale live -- this is a separate value from what dsf-1.0-osr forces CEF's own device_scale_factor to (for OSR coordinate-mapping only); Qt keeps reporting the real ratio locally regardless. Use it directly. 2. Scope was too narrow. The previous fix only touched .btn-toolbar (small Home-tab icons), matched to LibreOffice's compact 16px as a reference baseline. But shrinking only those buttons made the whole toolbar row look collapsed by comparison (unchanged font/ribbon/input sizing around now-tiny buttons), which read as "everything got smaller." Broadened the !important override to also cover: ribbon-tab (x-huge) buttons and their icons, form controls (dropdowns/inputs), and document/UI font-size classes (.font-size-tiny through .font-size-very-huge) -- each scaled from Euro-Office's own existing default as its 1.0x reference (not LibreOffice's, which has no equivalent ribbon UI to reference). Only the small toolbar buttons use LibreOffice's 16px as the reference, for lack of a better baseline. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…ice's own 20px The previous commit already reframed every other element (ribbon buttons, form controls, fonts) to use Euro-Office's own existing defaults as the 1.0x reference, since the actual bug is display-scale tracking, not LibreOffice size-matching -- but left the small toolbar buttons on LibreOffice's 16px by oversight. That undermined the fix: 16px * 1.15 (this machine's real display scale) ~= 18px, still smaller than the original 20px baseline the user is comparing against. Revert to 20px so scaling now correctly starts from Euro-Office's own size (20px * 1.15 ~= 23px) rather than an unrelated reference point. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…ad-hoc overrides The previous approach hand-picked selectors to !important-override (.btn-toolbar, ribbon x-huge buttons, form controls, font-size classes) after finding the plain CSS-custom-property route didn't visibly affect .btn-toolbar's height. That patchwork was necessarily incomplete: it missed color-swatch decoration elements (.btn-color-value-line etc, absolutely positioned with fixed px offsets), causing visible overlap once the desktop scale was actually changed and the ribbon container itself never resized around the now-larger buttons, clipping dropdowns. Root cause of both the incompleteness and the earlier CSS-variable mystery: this app already has its own comprehensive HiDPI scaling system. Common.Utils.checkSize() (web-apps apps/common/main/lib/util/utils.js) computes the real device pixel ratio (preferring window.AscCommon.checkDeviceScale(), which reads window.devicePixelRatio -- the same value this code already overrides) and adds a pixel-ratio__1_25/1_5/1_75/2/2_5 class to document.body. Nearly every scale-aware CSS rule in this codebase -- buttons, dropdowns, color swatches, ribbon spacing -- is keyed off that class, not off --x-small-btn-size in isolation. checkSize() runs on load and on window resize, and is also exposed directly as Common.Utils.checkSize, so it can be re-run here instead of faking a resize event. Verified live via CDP: after overriding devicePixelRatio and calling checkSize(), document.body correctly gains e.g. "pixel-ratio__1_5", and the Bold toolbar button's rendered height changes accordingly (20px -> 23px) via the app's own CSS, with no per-selector patching needed. Removed the entire !important stylesheet-injection block in favor of this single checkSize() call. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Two bugs found via live CDP console monitoring during an actual OS-level display scale change (1.15 -> 1.5 -> 1.15): 1. checkSize()/checkZoom() never actually ran in production. OnLoadEnd fires before the app's own JS bootstrap finishes -- Common.Utils and AscCommon were confirmed always undefined at first injection (logged "not present at injection time" on every load). The fix only ever appeared to work in manual CDP testing because those tests ran well after the page had settled. Now polls every 200ms (up to ~10s) until both are available, instead of trying once and giving up silently. 2. No re-injection happened at all during the actual scale change -- confirmed via a 90-second live console monitor spanning the whole test, which captured zero additional [UIScale] log lines. Neither moveEvent nor resizeEvent fire on a pure OS-level scale change (ruling out the moveEvent-based race theorized earlier); no Qt/CEF signal for this was found. Added a 1s QTimer in QCefView that polls GetUIScalePercentage() and only re-injects when the value actually changes, which is the mechanism that will actually pick up a live scale change now. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
User reports the enlarge still reverts to the small size even without changing the scale back -- but the last monitoring session only watched for ~13s and saw no revert in that window. Need to see whether Qt's own devicePixelRatio() is itself unstable/flip-flopping some time after a real display-scale change (independent of whether this code reacts to it correctly), over a longer window. Logs every 1s poll tick's raw GetUIScalePercentage() value to /tmp/uiscale_debug.log regardless of whether it triggered a re-injection. Temporary; remove alongside the rest of the debug logging once resolved. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…ePixelRatio
User confirmed the revert happens almost instantly -- icons/text flash
to the correct larger size for what looks like a single frame, then
snap back. That's far faster than anything a 1s poll or CDP console
logging could observe reliably, and points to a race with Chromium
itself rather than anything in this code's own timing.
The renderer runs with --force-device-scale-factor=1
(client_renderer_wrapper / cef command line). window.devicePixelRatio
is a browser-native, reactive WebIDL property; a real OS-level
display-scale change plausibly lets Chromium reassert that forced
value through its own internal layout/DPI-change handling faster than
the JS-level Object.defineProperty() override set here can be trusted
to stick -- especially once the app's own native $(window).on('resize',
checkSize) handler fires (which is presumably what is actually racing
here, given it's wired to real resize events this app's own code
already relies on elsewhere).
window.AscCommon.checkDeviceScale() -- the actual function both
checkSize() (CSS pixel-ratio__N class) and AscBrowser.checkZoom()
(canvas retinaPixelRatio) call to read the scale -- is a plain
app-defined JS function, not a browser-native property. Nothing in
Chromium can silently reassert it the way it can devicePixelRatio.
Monkeypatch it directly to return this code's computed factor instead
of reading window.devicePixelRatio, closing the race regardless of
what order the native resize handler and this code's own poll fire in.
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Per explicit request: apply the display-scale correction as a single uniform constant to the whole UI via CEF's own page zoom, rather than continuing to patch individual CSS classes/JS functions -- guarantees proportional scaling everywhere in one place, and (per this reasoning) should not need any mouse-coordinate compensation, since Chromium's own input pipeline already translates physical-pixel events into zoomed-layout coordinates as core functionality, independent of whatever this app's own C++ does for view-rect sizing. This codebase already has a CheckZoom()/SetZoomLevel() mechanism (further down in this file) that is currently dead on Linux (GetDpiChecker() is WIN32-only) and explicitly skips zoom on Wayland, with a comment warning that GetScreenInfo's device_scale_factor and SetZoomLevel's CSS zoom compound into double-scaling if both are non-neutral simultaneously. That comment predates dsf-1.0-osr, which now unconditionally forces device_scale_factor to 1.0 (neutral) -- GetScreenInfo no longer contributes any real scale to compound with, so SetZoomLevel can safely be the sole scaling mechanism now. Removes the CSS custom property / pixel-ratio__N body-class / stylesheet injection entirely. Keeps only the checkDeviceScale() monkeypatch + AscBrowser.checkZoom() trigger, needed for the separate case of the document/page <canvas> element, whose backing-buffer resolution is set explicitly by sdkjs's own JS independent of page zoom -- zoom stretches whatever resolution the canvas already has, so it still needs the real display scale fed to it directly to avoid blurriness. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
GetUIScalePercentage() unconditionally used devicePixelRatio(), which still reflects Xft.dpi under xcb despite AA_Use96Dpi and the HiDPI env overrides. That value was fed into CEF's page zoom on top of the Xft.dpi-based geometry scale already applied via QDpiChecker::GetMonitorDpi()/Core_GetMonitorScale, double-scaling content relative to the window frame and leaving an unpainted strip. Restrict the devicePixelRatio()-based zoom to Wayland, where it is the sole source of scale. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Init()'s XCreateSimpleWindow and SetWindowSize() (used by UpdateSize()) size the embedded CEF child window directly from QWidget::width()/ height(), which are device-independent pixels. X11 geometry is physical pixels, and the outer Qt window ends up sized in physical pixels regardless, since devicePixelRatio() isn't reliably neutralized by AA_Use96Dpi under XWayland. That mismatch left the CEF child window undersized relative to the real window frame, leaving an unpainted region. Scale both by devicePixelRatio() to match. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Fixing the CEF-zoom double-application (previous commit) and the CEF child window's undersized X11 geometry (commit before that) surfaced a third gap: CEF content and native Qt chrome (tab bar, etc.) each got their scale from a different, platform-exclusive source, and each source only worked on one platform: - Native chrome uses QDpiChecker::GetMonitorDpi(), which is Xft.dpi- based and correct on xcb but inert on Wayland (getX11SessionDpi() returns 0 there), falling back to a flat, unscaled DPI regardless of the compositor's real output scale. - CEF content uses QCefView::GetUIScalePercentage(), gated to Wayland- only after the previous fix and returning a flat 100% (no zoom) on xcb, since the double-application it used to have there is gone but nothing replaced it with the single correct scale. Result: xcb had correctly-scaled chrome but unscaled (too small) CEF content; Wayland had correctly-scaled CEF content but unscaled (too small) chrome. Fix both sides to read from the same source per platform: - GetUIScalePercentage() on xcb now derives its percentage from QDpiChecker::GetMonitorDpi()/GetScale(), the same Xft.dpi-based value native chrome already uses, instead of a flat 100%. - GetMonitorDpi() on Wayland now derives an equivalent DPI from devicePixelRatio() when getX11SessionDpi() is inert, instead of falling back to a flat, unscaled physicalDotsPerInchX()/Y(). Also documents the undocumented small-screen (<=1600x900) 96-DPI clamp's history (Ascensio/ONLYOFFICE tracker bugs 50621/50711, flipped between 192 and 96 DPI three days apart with no rationale recorded in either commit) and carves out a Wayland exception, since it would otherwise silently override the new Wayland devicePixelRatio()-derived DPI on small outputs. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Wayland shows two distinct GetUIScalePercentage() values across the two QCefView types (cvwtSimple shell vs cvwtEditor document content), 125% and 200%, and the tab bar appears rendered at its unscaled base size rather than either. Need type + geometry alongside the existing percentage/zoom values to identify which view is which and why the tab bar doesn't match. Remove once the underlying issue is found. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
The poll timer and moveEvent() each independently read devicePixelRatio()-derived scale and applied it immediately on any change. During startup, window placement fires several real move/ resize events in quick succession, and devicePixelRatio() can transiently misreport (observed: 200% then 125% then 200% again, repeating, before settling permanently) on individual reads taken around those events -- each misread was applied immediately since nothing checked whether a reading was actually stable. Extract the shared logic into MaybeUpdateUIScale() and only commit (call UpdateUIScalePercentage()) once the same value is read on two consecutive checks, from either trigger. moveEvent() now goes through this same debounced path instead of committing synchronously, so a single bad reading just becomes the pending value and gets overwritten by the next check rather than being acted on. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
The previous debounce lived in QCefView, guarding only the poll timer and moveEvent() -- but OnLoadEnd() (cefview.cpp) calls CCefView::UpdateUIScalePercentage() directly once per frame, including the nested iframe the actual editor UI loads in, completely bypassing that guard. Since multiple frames finish loading in quick succession during startup, each transiently-misread devicePixelRatio() value was still being committed via this uncovered path. Move the debounce into UpdateUIScalePercentage() itself, since it's the single choke point all three triggers (poll timer, moveEvent, OnLoadEnd) converge on already reading GetUIScalePercentage() internally. Unlike the QCefView version, this can't just skip its own body when a reading isn't yet confirmed -- the per-frame JS injection below still needs to run on every call regardless of whether the scale changed, since each frame is a separate JS context -- so instead of skipping, it falls back to the last confirmed value until the new one is seen twice in a row. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
QDpiChecker::GetWidgetDpi() delegated to GetMonitorDpi(), which reads QScreen::devicePixelRatio() -- but native widgets (window decoration, title-bar buttons, dialogs) were still reading a too-high value (2.0 instead of 1.25) well after startup, even once CEF's UI scale (which reads QWidget::devicePixelRatio() instead, already proven correct/self-correcting) had settled. QScreen's value appears to stay pinned to the initial integer-rounded buffer scale and never receive the same fractional-scale correction. Read the widget's own devicePixelRatio() directly instead of going through the screen-level lookup for the Wayland case. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
That change assumed QWidget::devicePixelRatio() was more reliable than QScreen::devicePixelRatio() on Wayland, but empirical testing showed both report the identical wrong value for the same widget -- the real cause was Qt's Wayland platform plugin permanently rounding devicePixelRatio() to the nearest integer unless the app opts into Qt::HighDpiScaleFactorRoundingPolicy::PassThrough (now set in desktop-apps' main.cpp), which fixes both equally. This switch provided no benefit once that's set, so restore the simpler screen-based lookup. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Lets the CEF log be correlated by time against desktop-apps' native- widget DPI debug log, to confirm whether native widgets hit the same startup devicePixelRatio() race already proven for CEF. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
QScreen::devicePixelRatio() can stay stuck at the rounded integer value after PassThrough is set, while a widget's own devicePixelRatio() correctly receives the compositor's async fractional-scale answer. Query the widget directly on Wayland instead of going through the screen-based lookup. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Investigation is concluded; the underlying fix is confirmed working. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
A freshly created Wayland surface reports the compositor's rounded integer scale rather than the real fractional one until the fractional-scale protocol replies -- measured at 2.0 for roughly 90ms on a 1.25 output. The first UI-scale read caught that value, so CEF page zoom was set to 200% and then revised to 125% once it corrected. That revision is the visible jump in the editor and its ribbon during startup. The native chrome does not move, because its manual factor is a residual that divides devicePixelRatio out and so reads the same in both states. Report a negative percentage while the surface is younger than the settle window, and skip applying the scale on a negative reading. Because the debounce state starts at -1, the first reading after the window is applied directly, so the correct zoom is the first one ever set rather than the second. A one-shot timer fires just after the window closes so the scale is applied promptly; the existing 1s poll would otherwise leave room for the page to paint unscaled first. A compositor slower than the settle window corrects on the next poll tick, exactly as it does today. Verified with a spreadsheet and a document open: every reading 125%, no 200% reading at all. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
CCefView::UpdateUIScalePercentage() required a new reading to appear on two consecutive calls before committing to it, guarding against devicePixelRatio() transiently misreporting during startup window placement. That race is already fully handled upstream: QCefView::GetUIScalePercentage() withholds a value entirely (returns negative, skipped before reaching this debounce) while the surface is younger than its settle window. Traced with per-call logging across repeated monitor-to-monitor moves (drag and instant keyboard-shortcut crossings, screens at 1.25x and 1.5x): the raw reading was correct on the very first call after every single crossing, with devicePixelRatio() reflecting the new output before window()->screen() even caught up to it. The debounce was therefore not guarding against anything on a real scale change -- it was unconditionally delaying every one by two calls, holding the old, wrong-for-the-new-monitor zoom in between. That delay is what was visible as the editor and ribbon rescaling twice on every monitor crossing. Apply GetUIScalePercentage()'s value directly and drop the now-fully- orphaned m_dPendingUIScalePercentage/m_dLastKnownUIScalePercentage debounce state. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
OnCursorChange was added to bridge cursor-shape changes to the OS on Wayland, where OSR mode gives CEF no native window to set the cursor on itself. It was written unconditionally, though, with no OSR/Wayland check, and always returned true -- the signal that tells CEF the app already set the cursor and CEF should not. In windowed mode (X11/xcb, Windows), CEF owns a real native child window and sets the OS cursor on it directly when this handler returns false. Returning true unconditionally told CEF's windowed-mode cursor handling to stand down on every platform, not just Wayland OSR. The app's own fallback, SetCursorType(), then calls setCursor() on the Qt QCefView ancestor widget -- which has no visible effect, since the pointer sits over CEF's own separate native child window, not that ancestor. Net result on any windowed platform: the cursor froze at whatever CEF's child window had by default (a plain arrow) everywhere in the editor content -- no I-beam over text, no resize handles, no row/column select arrows, no move cursor. Gate the interception to GetWidgetImpl()->IsWayland(), matching the comment already documenting the OSR-only rationale. Windowed platforms now return false and let CEF handle its own cursor as designed. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
UpdateUIScalePercentage() applied SetZoomLevel + WasResized + the per-frame checkDeviceScale/checkZoom JS injection unconditionally on every call, including the 1s poll timer tick, regardless of whether the scale had actually changed since the last apply. Cache the last-applied percentage and skip SetZoomLevel/WasResized when it matches -- those are whole-browser, genuinely idempotent operations, safe to skip. The per-frame JS injection loop is deliberately NOT gated by the same check: this function is also OnLoadEnd()'s trigger for a newly loaded frame (including nested iframes), which has never received the checkDeviceScale monkeypatch even when the global scale itself hasn't moved since the last apply -- skipping it there would silently break the injection for any document opened at an already- seen display scale. Also drops the two [UIScale] console.log calls (the checkZoom re-run trace and the give-up-waiting message); the console.error failure traces stay. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…r image paste CClipboardQueryHandler (the paste/'clipboard_read' side of the bridge) was registered unconditionally in RegisterClipboardQueryHandler(), on every platform. The bridge exists solely to work around CEF's own OS clipboard integration being unusable in Wayland OSR mode -- windowed platforms (X11, Windows) already have working native clipboard integration via CEF's own real window, and registering this handler there too meant it activated alongside CEF's own clipboard.write() path, racing for the same OS clipboard. Only register it when GetWidgetImpl()->IsWayland(). The write side (nativeClipboardWrite) is addressed on the JS side instead (sdkjs commit gating _isNativeClipboardAvailable() on window.RendererProcessVariable. isWayland), since the binding itself sits in a large fixed-size platform-independent method table not worth restructuring for this. Separately: QCefView::GetClipboardData() only ever checked QMimeData::hasImage() (raw pixel data, e.g. a screenshot tool's 'copy image'), never hasUrls() -- which is what a file manager's 'Copy' actually populates (Qt normalizes text/uri-list, CF_HDROP, etc into this one portable API). So copying an image file in the file manager and pasting it into a document silently did nothing under Wayland: no recognized clipboard format ever reached the JS side to match against. Now checks hasUrls() when hasImage() didn't already find something, and loads+encodes a single local image-file URL as PNG through the same image/png path the hasImage() branch already used, so it flows through the existing NativePaste() image handling in clipboard_base.js unchanged. Pasting a non-image file remains a no-op, consistent with every other document editor. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Traced with real diagnostics: the document opened unscaled under Wayland, but manually toggling the display scale afterward corrected it. window.AscCommon.AscBrowser.retinaPixelRatio (what drives canvas backing-store resolution and CSS scale throughout sdkjs) *was* already correcting itself shortly after page load, via this function's own existing monkeypatch-then-poll mechanism -- confirmed via a console diagnostic showing applicationPixelRatio jump from 1 to the real value (e.g. 1.25) about 180ms after the page's own devicePixelRatio-based first read. The gap: AscBrowser.checkZoom() (what the poll calls) only updates that internal state: it does not itself trigger a repaint at the corrected scale. That only happens through each editor's own asc_Resize()-equivalent, which every editor's web-apps Viewport.js already wires to a genuine window 'resize' DOM event -- exactly what manually toggling the display scale fires naturally, which is why that already fixed it. The poll's correction landing asynchronously after the document has already opened does not fire that event, so retinaPixelRatio became correct internally while nothing ever redrew to match it. Dispatch a synthetic 'resize' after checkZoom() succeeds, so the same already-correct, editor-agnostic resize pipeline picks up the correction. Deduped per frame against the last f actually dispatched for: this per-frame injection re-runs on every call including the 1s poll tick even when the scale is unchanged (by design, see the existing comment above this string -- a newly loaded frame still needs the monkeypatch), so without the dedup this would fire a resize/ redraw every second forever instead of once per real change. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…s own window
The prior fix dispatched 'resize' on window -- but window here is the
canvas frame's own window, a nested iframe and separate browsing
context from the outer shell page (see the existing comment on the
injection loop below: 'a separate browsing context from the outer
shell page'). The $(window).on('resize', ...) listener that actually
triggers each editor's asc_Resize()-equivalent redraw lives on the
outer shell's window, not the iframe's -- a resize event dispatched
inside a child iframe does not propagate to window.top, so the
previous fix corrected AscBrowser.retinaPixelRatio's internal state
(confirmed via diagnostic) but the redraw listener it was meant to
reach never received anything and the document stayed visibly
unscaled, exactly as before.
Target window.top explicitly, falling back to window if already at
the top (harmless no-op difference). Logs which target it used and for
which factor, to confirm on the next test.
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
The window.top dispatch was confirmed firing with no error (logged),
yet the document stayed visibly unscaled at open -- only a much later,
real (user-triggered) scale change actually redrew it. The two only
differ in timing: a real scale change necessarily happens well after
the app has fully loaded, while this poll's dispatch typically fires
under 1s after launch, plausibly before the outer shell page's own UI
framework has finished initializing and attached its
$(window).on('resize', ...) listener (Viewport.js) -- an early
dispatch would then land on nothing.
Retry a few times over the next several seconds instead of dispatching
once, so at least one attempt lands after the outer shell is
definitely ready. This is a timing-robustness fix based on the
available evidence (dispatch confirmed firing; only a late dispatch
observed to work), not a confirmed root cause for why the very first
attempt specifically goes unheard -- if this doesn't resolve it, that
needs its own diagnostic (e.g. checking whether the listener is even
registered yet at dispatch time).
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Reverts 94ba7b4, 2a8a0d3 and ddb1ec4. All three were built on a misdiagnosis: that the document stayed unscaled because sdkjs's retinaPixelRatio correction landed without anything triggering a redraw. Diagnostics disproved it -- the final version dispatched 'resize' on window.top four times across three seconds, every dispatch logged with no error, and the document still rendered unscaled. The actual cause is elsewhere (fixed separately): CEF page zoom being dropped by navigation and never re-applied. These commits add a synthetic-resize burst that fixes nothing, so drop them rather than leave unrelated churn on the branch. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Regression from the SetZoomLevel dedup added for the PR review (bfdfdd5): that cached the last-applied percentage and skipped re-applying SetZoomLevel/WasResized when it matched, but nothing ever invalidated the cache. CEF's page zoom is per-load -- navigating drops it -- so the sequence was: first UpdateUIScalePercentage() applies the zoom and caches 125, the editor then navigates to the actual document, CEF resets the zoom for that load, and every subsequent poll sees an unchanged 125, skips, and never restores it. The document rendered unscaled indefinitely. That matches the reported symptom exactly, including why it was self-healing: any real display-scale change makes the percentage differ from the cached value, so the zoom gets applied again and stays correct (and toggling back likewise). It also explains why the earlier resize-dispatch attempts had no effect -- sdkjs's own retinaPixelRatio correction was working the whole time; it was CEF page zoom that was missing. Reset the cache in OnLoadStart, alongside the m_dDeviceScale reset already there for the same per-load reason, so the next call after any navigation re-applies the zoom. Keeps the dedup's benefit for the 1s poll tick, which is what the review asked for. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
The review asked for the [UIScale] console.log calls to go; these two console.error calls are the same debug instrumentation in the same injected snippet, introduced by the same commit, and equally have no place running in every frame of a shipped build. Keep the try/catch in both cases -- this script is injected into every frame, including ones with no AscCommon at all, so a throw would stop the poll below from ever being installed. Neither site has any recovery to attempt, so the catches are intentionally empty, with a comment saying why rather than leaving a bare block for the next reader to wonder about. Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reopened from feature/wayland-migration (closed #10, which was opened from release/wayland-db-support and carried unrelated work — the CDP gateway bridge, int64_t churn — that doesn't belong here). Same Wayland migration work as desktop-apps#30, sdkjs#64, core#113, and DesktopEditors#42.