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/changelog.d/3566.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Widen Column and Narrow Column in the grid cell menu, on Shift+Alt+Right and Shift+Alt+Left.
6 changes: 6 additions & 0 deletions linux/crates/app/src/application/accelerators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ pub const ACCELERATORS: &[Accelerator] = &[
component(GridCell, "cell.toggle", &["space"], Listed(DataGrid)),
component(GridCell, "cell.menu", &["Menu", "<Shift>F10"], Listed(DataGrid)),
component(GridCell, "cell.set-null", &["<Primary>BackSpace"], Listed(DataGrid)),
// GtkTreeView's keys for its focused header. A column view's header
// takes no focus, so they act on the focused cell's column.
component(GridCell, "cell.widen-column", &["<Shift><Alt>Right"], Listed(DataGrid)),
component(GridCell, "cell.narrow-column", &["<Shift><Alt>Left"], Listed(DataGrid)),
stock("text.activate", &["Return"], Listed(DataGrid)),
component(CellEditor, "cell-editor.cancel", &["Escape"], Listed(DataGrid)),
component(CellEditor, "cell-editor.next", &["Tab"], Listed(DataGrid)),
Expand Down Expand Up @@ -422,6 +426,8 @@ pub(crate) fn keypad_twins(key: gdk::Key) -> &'static [gdk::Key] {
gdk::Key::Return => &[gdk::Key::ISO_Enter, gdk::Key::KP_Enter],
gdk::Key::space => &[gdk::Key::KP_Space],
gdk::Key::Delete => &[gdk::Key::KP_Delete],
gdk::Key::Left => &[gdk::Key::KP_Left],
gdk::Key::Right => &[gdk::Key::KP_Right],
_ => &[],
}
}
Expand Down
2 changes: 1 addition & 1 deletion linux/crates/app/src/ui/grid/column_figures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ fn widest<'a>(column_type: &ColumnType, values: impl Iterator<Item = &'a Value>)

/// How wide `chars` digits are in tabular figures, which libadwaita's
/// `numeric` class draws.
fn digits_width(widget: &impl IsA<gtk::Widget>, chars: u32) -> i32 {
pub(super) fn digits_width(widget: &impl IsA<gtk::Widget>, chars: u32) -> i32 {
tabular_width(widget, &"0".repeat(usize::try_from(chars).unwrap_or_default()))
}

Expand Down
221 changes: 221 additions & 0 deletions linux/crates/app/src/ui/grid/column_resize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use gtk4 as gtk;
use gtk4::prelude::*;

use super::GridCell;
use super::column_figures::digits_width;
use super::grid_menus::shown_column;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ColumnResize {
Wider,
Narrower,
}

/// A step is this many digits of the cell's font, so it stays the same
/// number of characters when the text is larger.
const STEP_DIGITS: u32 = 2;

/// Make the column `cell` is in one step wider or narrower, and save the
/// width as a header drag's is. Expand goes off, as GtkTreeView did for
/// these keys: an expanding column takes back part of the space a step
/// frees.
pub(super) fn resize_column(view: &gtk::ColumnView, cell: &GridCell, change: ColumnResize) {
let Some(column) = shown_column(view, cell) else {
return;
};
let column = column.view_column;
if !column.is_resizable() {
view.error_bell();
return;
}
let Some(width) = current_width(&column, cell) else {
return;
};
let step = digits_width(cell, STEP_DIGITS);
// Narrow stops at one step and never widens a column already below
// it: a fixed width goes below what the cells need, and the column the
// focus is in stays on screen to widen again.
let width = match change {
ColumnResize::Wider => width.saturating_add(step),
ColumnResize::Narrower => width.saturating_sub(step).max(step.min(width)),
};
column.set_expand(false);
column.set_fixed_width(width);
}

/// The width a step starts from: the one the last step set, which holds
/// even before the view has laid it out, or else the width on screen,
/// where a header drag starts too.
fn current_width(column: &gtk::ColumnViewColumn, cell: &GridCell) -> Option<i32> {
match (column.expands(), column.fixed_width()) {
(false, fixed) if fixed >= 0 => Some(fixed),
_ => column_width(cell),
}
}

/// The width of the column `cell` is in, as the view laid it out. The
/// column view's cell widget spans the column, and `width()` would give
/// only its content box, narrower by the theme's padding.
fn column_width(cell: &GridCell) -> Option<i32> {
let cell_widget = cell.parent()?;
let bounds = cell_widget.compute_bounds(&cell_widget)?;
Some(bounds.width().round() as i32)
}

#[cfg(test)]
pub(super) mod tests {
use std::time::Duration;

use gtk4::gdk;
use gtk4::prelude::*;

use super::{ColumnResize, STEP_DIGITS, column_width, resize_column};
use crate::test_support::wait_until;
use crate::ui::grid::ColumnWidthBinding;
use crate::ui::grid::column_figures::digits_width;
use crate::ui::grid::test_grid::{Grid, WAIT, two_text_columns, two_text_rows, two_text_values, type_into};

pub(in crate::ui::grid) fn shown_width(grid: &Grid, column: usize) -> i32 {
column_width(&grid.cell_of(1, column)).expect("a drawn cell")
}

pub(in crate::ui::grid) fn step(grid: &Grid) -> i32 {
digits_width(&grid.cell_of(1, 1), STEP_DIGITS)
}

/// `Grid::open` waits for the cells, not for the first layout, before
/// which no width is the one on screen.
pub(in crate::ui::grid) fn laid_out(grid: Grid) -> Grid {
wait_until(WAIT, || grid.view.width() > 0).expect("the grid to be laid out");
grid
}

#[gtk4::test]
fn a_step_wider_starts_from_the_width_on_screen_and_is_saved() {
let runtime = crate::test_support::test_runtime();
let root = tempfile::tempdir().expect("tempdir");
let widths = ColumnWidthBinding {
store: crate::persistence::ColumnWidthStore::load(root.path().join("column-widths.json"), &runtime.tasks()),
connection_id: uuid::Uuid::new_v4(),
schema: None,
table: "t".to_owned(),
};
let grid = laid_out(Grid::open_with_widths(
two_text_columns(),
two_text_values(),
widths.clone(),
));
let after = shown_width(&grid, 1) + step(&grid);

resize_column(&grid.view, &grid.cell_of(1, 1), ColumnResize::Wider);

assert_eq!(grid.column(1).fixed_width(), after);
assert!(
!grid.column(1).expands(),
"an expanding column takes back part of a step"
);
assert_eq!(widths.width("name"), Some(after));
wait_until(WAIT, || shown_width(&grid, 1) == after).expect("the column to be drawn wider");
}

#[gtk4::test]
fn a_step_narrower_shrinks_the_column_on_screen() {
let grid = laid_out(two_text_rows());
let after = shown_width(&grid, 1) - step(&grid);

resize_column(&grid.view, &grid.cell_of(1, 1), ColumnResize::Narrower);

wait_until(WAIT, || shown_width(&grid, 1) == after).expect("the column to be drawn narrower");
}

#[gtk4::test]
fn a_column_is_never_narrower_than_a_step() {
let grid = two_text_rows();
grid.column(1).set_expand(false);
grid.column(1).set_fixed_width(step(&grid) + 1);

resize_column(&grid.view, &grid.cell_of(1, 1), ColumnResize::Narrower);
resize_column(&grid.view, &grid.cell_of(1, 1), ColumnResize::Narrower);

assert_eq!(grid.column(1).fixed_width(), step(&grid));
}

#[gtk4::test]
fn narrow_leaves_a_column_already_narrower_than_a_step() {
let grid = two_text_rows();
let narrow = step(&grid) - 1;
grid.column(1).set_expand(false);
grid.column(1).set_fixed_width(narrow);

resize_column(&grid.view, &grid.cell_of(1, 1), ColumnResize::Narrower);

assert_eq!(grid.column(1).fixed_width(), narrow, "Narrow made the column wider");
}

#[gtk4::test]
fn a_column_that_is_not_resizable_keeps_its_width() {
let grid = two_text_rows();
grid.column(1).set_resizable(false);
grid.column(1).set_fixed_width(300);

resize_column(&grid.view, &grid.cell_of(1, 1), ColumnResize::Wider);

assert_eq!(grid.column(1).fixed_width(), 300);
assert!(grid.column(1).expands());
}

/// Pressed through XTEST, the keys meet the list's own arrow bindings
/// and the cell editor before the grid's controller.
mod real_keys {
use super::*;

const SHIFT_ALT: gdk::ModifierType = gdk::ModifierType::SHIFT_MASK.union(gdk::ModifierType::ALT_MASK);

fn focused_grid() -> (Grid, crate::test_support::KeyInjector) {
let grid = laid_out(two_text_rows());
crate::test_support::KeyInjector::focus_only(&grid.window);
wait_until(Duration::from_secs(2), || grid.window.is_active()).expect("the window to take the focus");
let keys = crate::test_support::KeyInjector::connect().expect("an X11 display");
(grid, keys)
}

#[gtk4::test]
fn shift_alt_right_and_left_resize_the_focused_cells_column() {
let (grid, keys) = focused_grid();
grid.focus(0, 1);
let wider = shown_width(&grid, 1) + step(&grid);

keys.press(gdk::Key::Right, SHIFT_ALT).expect("the key press");
wait_until(WAIT, || shown_width(&grid, 1) == wider).expect("Shift+Alt+Right to widen the column");
keys.press(gdk::Key::Left, SHIFT_ALT).expect("the key press");
keys.press(gdk::Key::Left, SHIFT_ALT).expect("the key press");

let narrower = wider - 2 * step(&grid);
wait_until(WAIT, || shown_width(&grid, 1) == narrower).expect("Shift+Alt+Left to narrow the column");
assert_eq!(
grid.focused_cell().map(|cell| cell.column()),
Some(1),
"the keys moved the focus"
);
assert_eq!(grid.column(0).fixed_width(), -1, "the keys resized another column");
}

#[gtk4::test]
fn an_open_editor_keeps_shift_alt_arrows() {
let (grid, keys) = focused_grid();
let cell = grid.focus(0, 1);
let editor = type_into(&cell, "typed");
wait_until(Duration::from_secs(2), || editor.has_focus()).expect("the editor to take the focus");

keys.press(gdk::Key::Right, SHIFT_ALT).expect("the key press");
crate::test_support::drain_main_context();

assert_eq!(
grid.column(1).fixed_width(),
-1,
"the key resized the column being edited"
);
assert!(cell.is_editing(), "the key ended the edit");
}
}
}
19 changes: 19 additions & 0 deletions linux/crates/app/src/ui/grid/context_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,25 @@ mod tests {
assert_eq!(widths.width("name"), None);
}

#[gtk4::test]
fn the_menu_widens_and_narrows_and_reset_lets_the_column_expand_again() {
use crate::ui::grid::column_resize::tests::{laid_out, shown_width, step};
let grid = laid_out(two_text_rows());
let before = shown_width(&grid, 1);

open_menu(&grid.cell_of(1, 1));
choose(&grid, &["cell.widen-column"]);
assert_eq!(grid.column(1).fixed_width(), before + step(&grid));
open_menu(&grid.cell_of(1, 1));
choose(&grid, &["cell.narrow-column"]);
assert_eq!(grid.column(1).fixed_width(), before);
open_menu(&grid.cell_of(1, 1));
choose(&grid, &["cell.reset-width"]);

assert_eq!(grid.column(1).fixed_width(), -1);
assert!(grid.column(1).expands(), "Reset left the column at its natural width");
}

fn shown_menu(grid: &Grid) -> Option<gtk::PopoverMenu> {
descendants(&grid.view)
.into_iter()
Expand Down
17 changes: 17 additions & 0 deletions linux/crates/app/src/ui/grid/grid_keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use gtk4::{self as gtk, gdk, glib};
use super::cell_edit::begin_edit;
use super::cell_focus::focused_cell;
use super::cell_preset::cell_presets;
use super::column_resize::{ColumnResize, resize_column};
use super::context_menu::prepare_menu;
use super::grid_menus::GridMenus;
use super::{CellPreset, GridCell, GridMsg, GridSink};
Expand Down Expand Up @@ -36,6 +37,14 @@ impl GridKeyboard {
on_focused_cell(move |widget, cell| show_menu(widget, cell, &menus)),
);
add("cell.set-null", on_focused_cell(move |_, cell| set_null(cell, &sink)));
add(
"cell.widen-column",
on_focused_cell(|widget, cell| resize(widget, cell, ColumnResize::Wider)),
);
add(
"cell.narrow-column",
on_focused_cell(|widget, cell| resize(widget, cell, ColumnResize::Narrower)),
);
column_view.add_controller(controller);
}
}
Expand Down Expand Up @@ -89,6 +98,14 @@ fn show_menu(widget: &gtk::Widget, cell: &GridCell, menus: &GridMenus) -> glib::
glib::Propagation::Stop
}

fn resize(widget: &gtk::Widget, cell: &GridCell, change: ColumnResize) -> glib::Propagation {
let Some(column_view) = widget.downcast_ref::<gtk::ColumnView>() else {
return glib::Propagation::Proceed;
};
resize_column(column_view, cell, change);
glib::Propagation::Stop
}

fn set_null(cell: &GridCell, sink: &GridSink) -> glib::Propagation {
let (None, Some(row), Some(drawing)) = (cell.lock(), cell.row(), cell.drawing()) else {
return glib::Propagation::Proceed;
Expand Down
26 changes: 23 additions & 3 deletions linux/crates/app/src/ui/grid/grid_menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tablepro_core::column::ResultColumn;
use super::cell_edit::begin_edit;
use super::cell_editor_kind::CellEditorKind;
use super::cell_preset::cell_presets;
use super::column_resize::{ColumnResize, resize_column};
use super::context_menu::{CellContext, MenuTarget};
use super::grid_columns::ShownColumn;
use super::row_json_dialog::RowJsonRequest;
Expand Down Expand Up @@ -180,6 +181,14 @@ fn build_cell_menu(shape: MenuShape) -> gio::Menu {
Some("cell.sort-descending"),
);
column_section.append(Some(&crate::i18n::gettext("Clear Sort")), Some("cell.clear-sort"));
column_section.append_item(&crate::application::menu_item(
&crate::i18n::gettext("Widen Column"),
"cell.widen-column",
));
column_section.append_item(&crate::application::menu_item(
&crate::i18n::gettext("Narrow Column"),
"cell.narrow-column",
));
column_section.append(
Some(&crate::i18n::gettext("Reset Column Width")),
Some("cell.reset-width"),
Expand Down Expand Up @@ -321,8 +330,8 @@ impl CellActionKit {
entries
}

/// Sort by the cell's column, as a header click does, or reset the
/// width a header drag set.
/// Sort by the cell's column, as a header click does, or change the
/// width a header drag sets.
fn column_entries(&self) -> Vec<CellEntry> {
let sort = |name: &str, order: gtk::SortType| {
let view = self.view.clone();
Expand All @@ -344,6 +353,14 @@ impl CellActionKit {
sink(GridMsg::SortCleared);
})
};
let resize = |name: &str, change: ColumnResize| {
let view = self.view.clone();
self.on_target(name, move |cell, _row, _rows| {
if let Some(cv) = view.upgrade() {
resize_column(&cv, cell, change);
}
})
};
let reset_width = {
let view = self.view.clone();
let tab = self.tab.clone();
Expand All @@ -353,6 +370,7 @@ impl CellActionKit {
return;
};
column.view_column.set_fixed_width(column.default_width.unwrap_or(-1));
column.view_column.set_expand(true);
if let (Some(widths), Some(info)) = (&tab.column_widths, columns.get(column.index)) {
widths.forget(&info.name);
}
Expand All @@ -362,6 +380,8 @@ impl CellActionKit {
sort("sort-ascending", gtk::SortType::Ascending),
sort("sort-descending", gtk::SortType::Descending),
clear_sort,
resize("widen-column", ColumnResize::Wider),
resize("narrow-column", ColumnResize::Narrower),
reset_width,
]
}
Expand Down Expand Up @@ -524,7 +544,7 @@ fn install_empty_space_menu(column_view: &gtk::ColumnView) -> gtk::PopoverMenu {
}

/// The view's column that `cell` sits in.
fn shown_column(view: &gtk::ColumnView, cell: &GridCell) -> Option<ShownColumn> {
pub(super) fn shown_column(view: &gtk::ColumnView, cell: &GridCell) -> Option<ShownColumn> {
cell.columns()?
.shown(view)
.into_iter()
Expand Down
1 change: 1 addition & 0 deletions linux/crates/app/src/ui/grid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod cell_tooltip;
mod column_drawing;
mod column_factory;
mod column_figures;
mod column_resize;
mod column_width_binding;
mod context_menu;
mod delete_availability;
Expand Down
Loading
Loading