Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions linux/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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.

Expand Down
1 change: 1 addition & 0 deletions linux/changelog.d/3826.fixed.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A new SQL editor tab opened from the header button or Ctrl+T did not take the keyboard focus.
1 change: 1 addition & 0 deletions linux/changelog.d/3826.fixed.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Show ER Diagram and Not Run icons looked blurry at 100% scale.
1 change: 1 addition & 0 deletions linux/changelog.d/3826.fixed.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A column's type in the completion list ran into the longest column name.
1 change: 1 addition & 0 deletions linux/changelog.d/3826.fixed.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An editor font with a fractional size, such as 10.5, was shown at the whole size below it.
1 change: 1 addition & 0 deletions linux/changelog.d/3826.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The SQL editor's completion list stopped appearing once a word had no match.
24 changes: 24 additions & 0 deletions linux/crates/app/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
3 changes: 3 additions & 0 deletions linux/crates/app/src/application/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
43 changes: 43 additions & 0 deletions linux/crates/app/src/ui/editor/completion/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<gtk4::Popover>().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(
Expand Down
4 changes: 4 additions & 0 deletions linux/crates/app/src/ui/editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,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();
Expand Down
12 changes: 11 additions & 1 deletion linux/crates/app/src/ui/editor_font_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down Expand Up @@ -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");
Expand Down
51 changes: 49 additions & 2 deletions linux/crates/app/src/ui/workspace/workspace_tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,27 @@ impl WorkspaceModel {
}

pub(super) fn open_editor_tab(&mut self, initial_query: Option<String>) {
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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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::<sourceview5::View>(&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::<sourceview5::View>(&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");
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion linux/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
30 changes: 30 additions & 0 deletions linux/docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,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`.
Expand Down Expand Up @@ -481,6 +483,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% | 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:

- 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`.
Expand Down
Loading