From 25c9226fe78dfd39da6d54404bfe494af2cb23ee Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 24 Oct 2026 23:47:24 +0700 Subject: [PATCH 1/3] test(linux): check the app at display scales and fix what the pass found --- linux/ROADMAP.md | 1 + linux/crates/app/meson.build | 24 +++++++++ linux/crates/app/src/application/imp.rs | 3 ++ .../app/src/ui/editor/completion/tests.rs | 43 ++++++++++++++++ linux/crates/app/src/ui/editor/mod.rs | 4 ++ .../crates/app/src/ui/editor_font_provider.rs | 12 ++++- .../app/src/ui/workspace/workspace_tabs.rs | 51 ++++++++++++++++++- .../actions/statement-pending-symbolic.svg | 6 +-- .../actions/table-relation-symbolic.svg | 7 +-- linux/docs/README.md | 2 +- linux/docs/testing.md | 30 +++++++++++ 11 files changed, 172 insertions(+), 11 deletions(-) diff --git a/linux/ROADMAP.md b/linux/ROADMAP.md index 916d8b31c9..82160a03a8 100644 --- a/linux/ROADMAP.md +++ b/linux/ROADMAP.md @@ -165,6 +165,7 @@ Not built yet, in no order. - An AUR PKGBUILD, kept by the community. - More languages: French, Korean, Turkish, and Simplified and Traditional Chinese, which the macOS app ships. - A manual pass with Orca, the keyboard alone, High Contrast, 1.5x text scaling and a right-to-left locale. +- The [display scale pass](docs/testing.md#display-scales) again on a machine with a GPU and a HiDPI screen: the 2026-10-24 one ran in a container, on a software renderer. - Install and use it on Ubuntu 26.04 and Fedora 44 by hand, and under KDE Plasma with the Adwaita style. - A Linux user guide on docs.tablepro.app, a page on the marketing site, and a support channel. diff --git a/linux/crates/app/meson.build b/linux/crates/app/meson.build index b3db68b011..c661e31e45 100644 --- a/linux/crates/app/meson.build +++ b/linux/crates/app/meson.build @@ -101,3 +101,27 @@ test( is_parallel: false, timeout: 3600, ) + +# The code that measures and places things, again at the scale a 200% +# display gives GDK, so a size worked out in device pixels rather than +# logical ones fails here and not only on a HiDPI screen. +gtk_scale_env = gtk_env +gtk_scale_env.set('GDK_SCALE', '2') + +test( + 'app-scale-2', + gtk_program, + args: gtk_args + [ + '--lib', + '--', + 'ui::grid::', + 'ui::diagram::', + 'ui::editor::editor_view::', + 'ui::window_geometry::', + ], + env: gtk_scale_env, + depends: [compiled_schemas, metainfo_file], + suite: 'gtk', + is_parallel: false, + timeout: 3600, +) diff --git a/linux/crates/app/src/application/imp.rs b/linux/crates/app/src/application/imp.rs index d50f8fa63c..16e33e1515 100644 --- a/linux/crates/app/src/application/imp.rs +++ b/linux/crates/app/src/application/imp.rs @@ -81,6 +81,9 @@ impl ObjectImpl for TableProApplication { impl ApplicationImpl for TableProApplication { fn startup(&self) { self.parent_startup(); + // GtkSourceView's stylesheet, icon path and translations need the + // display, which GTK opens in the startup above. + sourceview5::init(); let app = self.obj(); install_app_actions(&app); install_application_accelerators(&*app); diff --git a/linux/crates/app/src/ui/editor/completion/tests.rs b/linux/crates/app/src/ui/editor/completion/tests.rs index 4c2809bbaf..3e0cfe3e1e 100644 --- a/linux/crates/app/src/ui/editor/completion/tests.rs +++ b/linux/crates/app/src/ui/editor/completion/tests.rs @@ -601,6 +601,49 @@ fn typing_a_dot_after_an_alias_lists_its_columns_with_types() { screen.wait_for("integer").expect("the column's type beside it"); } +/// The app starts GtkSourceView, whose stylesheet gives the list its +/// width while its rows are hidden. Without it GDK refused the list after +/// a word with no match, and it never came back. +#[gtk4::test] +fn the_list_comes_back_after_a_word_with_no_match() { + let test = crate::ui::session_window::tests::TestWindow::connected("CompletionComesBack"); + WidgetExt::activate_action(&test.window, "win.new-editor-tab", None).expect("win.new-editor-tab"); + let editors = || crate::ui::session_window::tests::editors(&test.window); + wait_until(WAIT, || editors().len() == 1).expect("an editor tab"); + let view: sourceview5::View = editors()[0].imp().source_view.get().upcast(); + wait_until(WAIT, || view.is_mapped() && view.has_focus()).expect("the editor's text with the focus"); + assert!(comes_back(&view), "the list stayed off screen"); +} + +/// Types as keys do, one character at a time, since GtkSourceView starts a +/// completion only for a single character typed. +fn comes_back(view: &sourceview5::View) -> bool { + let buffer = view.buffer(); + // The document's words complete whatever the catalog holds. + buffer.set_text("SELECT unit_price, units_sold FROM sales;\n"); + buffer.place_cursor(&buffer.end_iter()); + let list = || { + descendants(view) + .into_iter() + .filter_map(|widget| widget.downcast::().ok()) + .find(|popover| popover.type_().name() == "GtkSourceCompletionList") + }; + let shown = || list().is_some_and(|list| list.is_mapped()); + let type_text = |text: &str| { + for character in text.chars() { + buffer.insert_interactive_at_cursor(&character.to_string(), true); + crate::test_support::drain_main_context(); + } + }; + + type_text("un"); + wait_until(WAIT, shown).expect("the list on screen"); + type_text("zq"); + wait_until(WAIT, || list().is_none_or(|list| !list.is_visible())).expect("no match hides the list"); + type_text(" un"); + wait_until(WAIT, shown).is_ok() +} + /// A provider on a session to the engine `driver`, speaking `dialect`, /// with its snippets from `folder`. fn on_engine( diff --git a/linux/crates/app/src/ui/editor/mod.rs b/linux/crates/app/src/ui/editor/mod.rs index ae06127769..e4162689bb 100644 --- a/linux/crates/app/src/ui/editor/mod.rs +++ b/linux/crates/app/src/ui/editor/mod.rs @@ -199,6 +199,10 @@ impl SqlEditor { self.emit_by_name::<()>("undoable-toast", &[&crate::i18n::gettext("Query replaced")]); } + pub fn focus_text(&self) { + self.imp().source_view.grab_focus(); + } + /// Take back the last edit, for a toast's Undo button. pub fn undo(&self) { let buffer = self.buffer(); diff --git a/linux/crates/app/src/ui/editor_font_provider.rs b/linux/crates/app/src/ui/editor_font_provider.rs index 933362adc1..64609f55f9 100644 --- a/linux/crates/app/src/ui/editor_font_provider.rs +++ b/linux/crates/app/src/ui/editor_font_provider.rs @@ -46,7 +46,10 @@ fn editor_font_css(font: &pango::FontDescription) -> String { let size = font.size(); if size > 0 { let unit = if font.is_size_absolute() { "px" } else { "pt" }; - declarations.push(format!("font-size: {}{unit};", size / pango::SCALE)); + declarations.push(format!( + "font-size: {}{unit};", + f64::from(size) / f64::from(pango::SCALE) + )); } if font.weight() != pango::Weight::Normal { declarations.push(format!("font-weight: {};", font.weight().into_glib())); @@ -126,6 +129,13 @@ mod tests { assert!(css.contains("font-family: \"Ev\\\"il\\\\Font\";"), "{css}"); } + #[test] + fn editor_font_css_keeps_a_fractional_size() { + let css = editor_font_css(&pango::FontDescription::from_string("Monospace 10.5")); + + assert!(css.contains("font-size: 10.5pt;"), "{css}"); + } + #[test] fn editor_font_css_uses_px_for_an_absolute_size() { let mut font = pango::FontDescription::from_string("Monospace"); diff --git a/linux/crates/app/src/ui/workspace/workspace_tabs.rs b/linux/crates/app/src/ui/workspace/workspace_tabs.rs index eb235744ed..e915bb261f 100644 --- a/linux/crates/app/src/ui/workspace/workspace_tabs.rs +++ b/linux/crates/app/src/ui/workspace/workspace_tabs.rs @@ -177,13 +177,27 @@ impl WorkspaceModel { } pub(super) fn open_editor_tab(&mut self, initial_query: Option) { - self.open_tab(TabSpec::Editor { + self.open_editor(TabSpec::Editor { initial_query, draft: None, file: None, }); } + // AdwTabView moves the focus to the page it selects only from the page + // that held it, so an editor opened from the sidebar, a header button + // or a key pressed there would leave the typing where it was. + fn open_editor(&mut self, spec: TabSpec) { + if self.open_tab(spec).is_none() { + return; + } + if let Some(id) = self.selected_workspace_tab_id() + && let Some(WorkspaceTab::Editor(slot)) = self.workspace_tabs.borrow().get(&id) + { + slot.editor.focus_text(); + } + } + /// The directory this session's editor drafts live in. fn draft_scope(&self) -> DraftScope { WorkspaceFileName::for_key(&self.key).draft_scope() @@ -673,7 +687,7 @@ impl WorkspaceModel { match descriptor { ClosedTabDescriptor::Editor { query, file } => { let initial = if query.is_empty() { None } else { Some(query) }; - self.open_tab(TabSpec::Editor { + self.open_editor(TabSpec::Editor { initial_query: initial, draft: None, file, @@ -807,6 +821,39 @@ mod tests { wait_until(WAIT, || !banner.is_revealed()).expect("the banner goes once the file is moved aside"); } + #[gtk4::test] + fn a_new_editor_tab_takes_the_focus_from_outside_the_tabs() { + let test = TestWindow::connected("NewEditorFocus"); + let button = test.workspace().imp().new_tab_button.get(); + button.grab_focus(); + assert!(button.is_focus(), "the header button holds the focus"); + + button.emit_clicked(); + + wait_until(WAIT, || editors(&test.window).len() == 1).expect("an editor tab"); + let text = crate::test_support::first_descendant_of_type::(&editors(&test.window)[0]) + .expect("the editor's text"); + wait_until(WAIT, || text.is_focus()).expect("the new editor's text takes the focus"); + } + + #[gtk4::test] + fn a_reopened_editor_tab_takes_the_focus() { + let test = TestWindow::connected("ReopenedEditorFocus"); + WidgetExt::activate_action(&test.window, "win.new-editor-tab", None).expect("win.new-editor-tab"); + wait_until(WAIT, || editors(&test.window).len() == 1).expect("an editor tab"); + WidgetExt::activate_action(&test.window, "win.close-current", None).expect("win.close-current"); + wait_until(WAIT, || editors(&test.window).is_empty()).expect("the tab closes"); + let button = test.workspace().imp().new_tab_button.get(); + button.grab_focus(); + + WidgetExt::activate_action(&test.window, "win.reopen-closed-tab", None).expect("win.reopen-closed-tab"); + + wait_until(WAIT, || editors(&test.window).len() == 1).expect("the tab comes back"); + let text = crate::test_support::first_descendant_of_type::(&editors(&test.window)[0]) + .expect("the editor's text"); + wait_until(WAIT, || text.is_focus()).expect("the reopened editor's text takes the focus"); + } + #[gtk4::test] fn tab_view_freed_after_teardown() { let test = TestWindow::connected("TabViewFreed"); diff --git a/linux/data/resources/icons/scalable/actions/statement-pending-symbolic.svg b/linux/data/resources/icons/scalable/actions/statement-pending-symbolic.svg index 0870e983b7..0719f8e78d 100644 --- a/linux/data/resources/icons/scalable/actions/statement-pending-symbolic.svg +++ b/linux/data/resources/icons/scalable/actions/statement-pending-symbolic.svg @@ -1,9 +1,7 @@ - - - - + + diff --git a/linux/data/resources/icons/scalable/actions/table-relation-symbolic.svg b/linux/data/resources/icons/scalable/actions/table-relation-symbolic.svg index e8ad0946e4..1f26c2b723 100644 --- a/linux/data/resources/icons/scalable/actions/table-relation-symbolic.svg +++ b/linux/data/resources/icons/scalable/actions/table-relation-symbolic.svg @@ -1,7 +1,8 @@ - - - + + + + diff --git a/linux/docs/README.md b/linux/docs/README.md index d98cd0076f..078c09b8ba 100644 --- a/linux/docs/README.md +++ b/linux/docs/README.md @@ -10,7 +10,7 @@ The reference for working on the Linux app. Start with [ARCHITECTURE.md](../ARCH | [error-handling.md](error-handling.md) | The error types each layer returns, their categories, and the text and logs the app makes of them | | [state-management.md](state-management.md) | Where the application, window, workspace and tab state lives, and the rules every view follows | | [storage.md](storage.md) | Which store holds which data: files, GSettings, the Secret Service and SQLite | -| [testing.md](testing.md) | The test suites, the conformance and Docker tests, and the CI workflows | +| [testing.md](testing.md) | The test suites, the conformance and Docker tests, the manual passes and the CI workflows | | [troubleshooting.md](troubleshooting.md) | For users: saved passwords, SSH, Kerberos, logs, crash reports, bug reports and downgrades | | [regression-tests.md](regression-tests.md) | The tests that guard each critical audit finding | | [releasing.md](releasing.md) | The setup before the first release, a release, a packaging fix and a fix release | diff --git a/linux/docs/testing.md b/linux/docs/testing.md index a8e482589e..b23c342403 100644 --- a/linux/docs/testing.md +++ b/linux/docs/testing.md @@ -371,6 +371,8 @@ Helpers live in `crates/app/src/test_support`, compiled only for tests: The gtk suite sets `GTK_A11Y=test`, so results never depend on an AT-SPI bus. With `GTK_A11Y=none`, GTK records no accessible properties and `assert_labelled` returns `TestBackendMissing` instead of a wrong verdict. +The suite's `app-scale-2` test runs the grid, ER diagram, editor view and window geometry tests again with `GDK_SCALE=2`, the scale a 200% display gives GDK, so a size worked out in device pixels rather than logical ones fails on CI and not only on a HiDPI screen. Fractional scales are left to the [display scale pass](#display-scales), since Xvfb has none. + Two harnesses build the app's real objects over fakes: - `TestWindow` (`crates/app/src/ui/session_window/tests/mod.rs`) starts a `TableProApplication` with one window on the picker, a saved connection in its list and a `FakeConnector`. `TestWindow::connected` opens that connection and waits for its workspace. Each test registers an application id of its own, so it never meets another test, or a TablePro the developer has open, on the session bus. `test.hold` is the application's `CountingHold`. @@ -466,6 +468,34 @@ The tests in `crates/app/src/ui/editor/script_runner.rs` drive `run_batches` und `crates/app/src/workspace/tests/structure_review.rs` saves a structure tab through `WorkspaceHandle::save` on the `workspace::test_workspace::Workspace` harness and answers the review with `workspace::answer`. A dropped column shows the review with its SQL, Cancel sends nothing, Apply runs one write batch, an additive save on an engine that applies DDL in one transaction starts without a review, a draft changed during the review is not saved, and a second save during the review opens no second one. The close guard's own tests check that its Save reviews too, and that a declined review keeps the window open with no alert and starts none of the saves after it. +## Manual passes + +What a person checks on a desktop because no suite can, each with the date it last ran, where, and what it found. A problem a pass finds is fixed or filed, and the pass runs again when what it covers changes. + +### Display scales + +The app at 100, 125, 150, 175 and 200% on GNOME Wayland, at 200% on X11, on two monitors at different scales with the window moved from one to the other while a table, an editor and an ER diagram tab are open, on KDE Plasma Wayland at 125 and 150% with the Adwaita style, and at 150% with Large Text. At each scale: grid text, row height, header, pager and saved column widths; the focused cell ring, the selection and the pending insert, edit and delete tints; the SQL editor's text, gutter and completion list; the sidebar, the connect dialog, Preferences, the save alert, the main menu and About; the app icon and the three symbolic icons in `data/resources/icons`. + +#### 2026-10-24 + +Ran in the `tablepro-linux-dev:26.04` container on an Apple silicon Mac, without a GPU: Mesa's llvmpipe drew everything in software. GTK 4.22.4, libadwaita 1.9.1, GtkSourceView 5.18.0, Adwaita Sans 11 and DejaVu Sans Mono 11, since Ubuntu ships no Adwaita Mono. + +| Configuration | How | Result | +|---|---|---| +| GNOME Wayland 100, 125, 150, 175, 200% | GNOME Shell 50.1 `--headless` with one virtual monitor, 2560×1600 (2880×1800 at 150%, 2800×1750 at 175%, where Mutter offers those scales), the scale set through Mutter's `DisplayConfig` | Drawn at the monitor's scale, never stretched. Grid text, rows, header, pager, tints and the 2 px focus ring hold at every scale; the saved widths of `status` (72) and `note` (96) show the same text at each. Dialogs, alerts and menus lay out as at 100%. The problems below | +| X11 200% | Xvfb, with xsettingsd handing GTK `Gdk/WindowScalingFactor 2` as GNOME's settings daemon does on X11; GNOME 50 has no X11 session | As Wayland at 200% | +| Two monitors | 1920×1080 at 100% and 2560×1600 at 200%; the window, with a table, an editor and an ER diagram tab, moved to the second and back | Each move redraws at the new monitor's scale; layout, column widths and the diagram's cards are the same on both | +| KDE Plasma Wayland 125, 150% | KDE_RESULT_PLACEHOLDER | KDE_RESULT_PLACEHOLDER | +| 150% with Large Text | GNOME at 150% with `text-scaling-factor` 1.25, read through the settings portal | Text a quarter larger and rows taller with it, nothing clipped; the connect dialog's title shortens to "Connect to PostgreS…" beside Test and Connect, as a header bar does | + +Problems it found, each fixed with it: + +- The app never called `gtk_source_init`, so GtkSourceView's stylesheet was missing: the completion list had no background and no gap between a name and its type, and with its rows hidden it asked for no width. GDK refuses such a popup, so once a word had no match the list never came back, at any scale and on X11 too. The application's startup now calls `sourceview5::init`. +- `table-relation-symbolic` (Show ER Diagram) drew its rows on half pixels and `statement-pending-symbolic` (Not Run) had a 1.5 px ring and round dots, so both were soft at 100%; both are redrawn on the 16 px grid. +- A new editor tab opened from the header button, the sidebar or Ctrl+T with the focus outside the tabs left the keyboard focus where it was, so typed SQL went to the sidebar. + +Left for a machine with a GPU and a HiDPI screen: the GL and Vulkan renderers on a real driver, text on a physical panel, a touchpad's scrolling and zoom, and an X11 desktop that composites. + ## CI `.github/workflows/build-linux.yml` builds and tests the app, `.github/workflows/linux-packages.yml` builds, installs and starts the .deb and .rpm, and `.github/workflows/release-linux.yml` publishes a `linux-v*` tag. Each workflow's `on:` and each job's `if:` say when it runs; its steps say what it runs. To run the same checks locally: the [fast-job commands](../CONTRIBUTING.md#fast-job-commands) and `build-aux/packaging/build-packages.sh`. From 3a66a5c17dfe1937929b8b699e6d5bae0f771526 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 24 Oct 2026 23:56:12 +0700 Subject: [PATCH 2/3] docs(linux): record the KDE runs of the display scale pass --- linux/docs/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/docs/testing.md b/linux/docs/testing.md index 4637f9f8be..1bf1b7bddb 100644 --- a/linux/docs/testing.md +++ b/linux/docs/testing.md @@ -500,7 +500,7 @@ Ran in the `tablepro-linux-dev:26.04` container on an Apple silicon Mac, without | GNOME Wayland 100, 125, 150, 175, 200% | GNOME Shell 50.1 `--headless` with one virtual monitor, 2560×1600 (2880×1800 at 150%, 2800×1750 at 175%, where Mutter offers those scales), the scale set through Mutter's `DisplayConfig` | Drawn at the monitor's scale, never stretched. Grid text, rows, header, pager, tints and the 2 px focus ring hold at every scale; the saved widths of `status` (72) and `note` (96) show the same text at each. Dialogs, alerts and menus lay out as at 100%. The problems below | | X11 200% | Xvfb, with xsettingsd handing GTK `Gdk/WindowScalingFactor 2` as GNOME's settings daemon does on X11; GNOME 50 has no X11 session | As Wayland at 200% | | Two monitors | 1920×1080 at 100% and 2560×1600 at 200%; the window, with a table, an editor and an ER diagram tab, moved to the second and back | Each move redraws at the new monitor's scale; layout, column widths and the diagram's cards are the same on both | -| KDE Plasma Wayland 125, 150% | KDE_RESULT_PLACEHOLDER | KDE_RESULT_PLACEHOLDER | +| KDE Plasma Wayland 125, 150% | KWin 6.6.6 `kwin_wayland --scale`, nested in Xvfb since its virtual backend takes no screenshots, without the Plasma shell; GTK draws its own header bar and the Adwaita style | As GNOME at the same scale: the grid, tints and focus ring, the editor and its completion list, the dialogs, the main menu and the ER diagram lay out the same | | 150% with Large Text | GNOME at 150% with `text-scaling-factor` 1.25, read through the settings portal | Text a quarter larger and rows taller with it, nothing clipped; the connect dialog's title shortens to "Connect to PostgreS…" beside Test and Connect, as a header bar does | Problems it found, each fixed with it: From 265e5012574757a9627759b3ea5d7c0d28ad8bd3 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 24 Oct 2026 23:56:54 +0700 Subject: [PATCH 3/3] docs(linux): add the changelog fragments for #3826 --- linux/changelog.d/3826.fixed.1.md | 1 + linux/changelog.d/3826.fixed.2.md | 1 + linux/changelog.d/3826.fixed.3.md | 1 + linux/changelog.d/3826.fixed.4.md | 1 + linux/changelog.d/3826.fixed.md | 1 + 5 files changed, 5 insertions(+) create mode 100644 linux/changelog.d/3826.fixed.1.md create mode 100644 linux/changelog.d/3826.fixed.2.md create mode 100644 linux/changelog.d/3826.fixed.3.md create mode 100644 linux/changelog.d/3826.fixed.4.md create mode 100644 linux/changelog.d/3826.fixed.md diff --git a/linux/changelog.d/3826.fixed.1.md b/linux/changelog.d/3826.fixed.1.md new file mode 100644 index 0000000000..14ba416d4b --- /dev/null +++ b/linux/changelog.d/3826.fixed.1.md @@ -0,0 +1 @@ +A new SQL editor tab opened from the header button or Ctrl+T did not take the keyboard focus. diff --git a/linux/changelog.d/3826.fixed.2.md b/linux/changelog.d/3826.fixed.2.md new file mode 100644 index 0000000000..fc9608ecdb --- /dev/null +++ b/linux/changelog.d/3826.fixed.2.md @@ -0,0 +1 @@ +The Show ER Diagram and Not Run icons looked blurry at 100% scale. diff --git a/linux/changelog.d/3826.fixed.3.md b/linux/changelog.d/3826.fixed.3.md new file mode 100644 index 0000000000..26ff376142 --- /dev/null +++ b/linux/changelog.d/3826.fixed.3.md @@ -0,0 +1 @@ +A column's type in the completion list ran into the longest column name. diff --git a/linux/changelog.d/3826.fixed.4.md b/linux/changelog.d/3826.fixed.4.md new file mode 100644 index 0000000000..56e41637bb --- /dev/null +++ b/linux/changelog.d/3826.fixed.4.md @@ -0,0 +1 @@ +An editor font with a fractional size, such as 10.5, was shown at the whole size below it. diff --git a/linux/changelog.d/3826.fixed.md b/linux/changelog.d/3826.fixed.md new file mode 100644 index 0000000000..c006eaf665 --- /dev/null +++ b/linux/changelog.d/3826.fixed.md @@ -0,0 +1 @@ +The SQL editor's completion list stopped appearing once a word had no match.