Skip to content

Configurable Default Save Format (ODF / OOXML) - #36

Open
pplupo wants to merge 34 commits into
Euro-Office:mainfrom
pplupo:feature/default-save-format
Open

Configurable Default Save Format (ODF / OOXML)#36
pplupo wants to merge 34 commits into
Euro-Office:mainfrom
pplupo:feature/default-save-format

Conversation

@pplupo

@pplupo pplupo commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Implementation of Euro-Office/DesktopEditors#60

Summary

  • New documents default to OOXML out of the box, resolved at creation time from a tiered configuration cascade (compiled-in baseline → system-scope admin default/lock → user-scope choice) built on the existing QSettings registry group, reusing the same mechanism that already governs CheckForUpdates.
  • Adds two registry keys, DefaultSaveFormat (ODF|OOXML) and EnforceDefaultFormat, readable via standard OS deployment tooling (/etc/xdg/euro-office/DesktopEditors.conf on Linux, HKLM/HKCU on Windows) with no new config subsystem.
  • Start page Settings gains a "Default save format" combo, wired through the existing settings:init/settings:apply round-trip; disabled with an explanatory message when an administrator has locked the format, and the lock is enforced server-side (not just hidden in the UI), so it can't be bypassed from the renderer.
  • A one-time onboarding dialog lets unmanaged users pick a format before creating their first document; skipped when a policy exists, or the user has already chosen, and never reappears once dismissed.

Test plan

  • Cascade resolution verified against real QSettings files across 15 cases: all four tiers, the hard lock, malformed/lowercase values, and the dismissed-onboarding path
  • New-document naming confirmed for word/cell/slide/form via the running app (CDP-driven), both the create:new and --new: CLI paths
  • Settings-panel round trip confirmed end-to-end: UI change → settings:apply → registry write → next new document reflects it, in both directions (ODF→OOXML→ODF)
  • Admin baseline (unlocked): onboarding suppressed, new docs follow the baseline, user can still override
  • Admin hard lock: combo disabled with managed message, resolution ignores the user tier entirely, forged settings:apply calls bypassing the disabled UI are rejected server-side
  • Onboarding dialog: both choice paths and the dismiss-without-choosing path, confirmed via manual verification of the native dialog.

ATTENTION: This is to go on top of the wayland-migration branch.

pplupo and others added 24 commits July 3, 2026 17:53
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
…on() in this Qt version

Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
… 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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
Assisted-by: Claude Code:Opus 4.8
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
Assisted-by: Claude Code:Opus 4.8
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
…gantic 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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
Investigation is concluded; the underlying fix is confirmed working.

Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
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 <pplupo@gmail.com>
(cherry picked from commit 4273100)
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 <pplupo@gmail.com>
(cherry picked from commit 4dff132)
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 <pplupo@gmail.com>
(cherry picked from commit 3ed1c04)
pplupo added 5 commits July 30, 2026 23:43
This reverts commit 3ed1c04.

(cherry picked from commit 2e68c94)
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
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 <pplupo@gmail.com>
(cherry picked from commit d99f644)
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 <pplupo@gmail.com>
(cherry picked from commit e155cbf)
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 <pplupo@gmail.com>
(cherry picked from commit b7fab27)
- 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 <pplupo@gmail.com>
(cherry picked from commit 768387d)
pplupo added 5 commits July 30, 2026 23:43
…lign 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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
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 <pplupo@gmail.com>
Separates the closing 'You can change this anytime in Settings.' line
from the ODF description paragraph above it.

Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
The start page's document-creation tile grid hardcoded DOCX/XLSX/PPTX
badge text and never reflected the resolved DefaultSaveFormat, so a user
who set ODF as their default still saw OOXML extensions on the tiles
they'd click to create a new document. The PDF/form tile is unaffected --
forms always stay DOCXF/PDF regardless of this setting, matching
newFileFormat()'s existing behavior.

The underlying "-big" icon glyphs (common-svg/docx-big.svg etc.) have no
format-specific detail baked in -- plain page/grid/slide shapes -- so
they're reused as-is for ODT/ODS/ODP; only the badge text changes.

panelrecent.js now reads the resolved format from two places: directly
from settings:init (registered at genuine app startup, independent of
whether the Settings panel has ever been opened -- its own settings:init
parsing only activates once that panel is constructed), and from a new
defaultformat:changed event fired by panelsettings.js both at its own
settings:init parse and right after a successful Settings-Apply, so the
tiles update immediately without waiting for a fresh native round-trip.

DocumentCreationGrid.render() now clears its own previous output before
appending, so it can be safely re-rendered with a new format each time
the resolved default changes.

Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
@pplupo
pplupo force-pushed the feature/default-save-format branch from 35b216a to cd7eacb Compare July 31, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants