mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-01 22:56:54 -06:00
Merge PR #1652: Fill handle: right-drag opens the fill menu (Copy Cells, Fill Series, Fill Formatting Only, Fill Without Formatting, Series…)
crates/ui-egui/src/grid.rs: kept both GridState fields (context_menu_frame, fill_menu); previous_popups tuple has four elements inside the touch_pan branch; release match has Drag::Rows|Cols, then the two Drag::Fill arms docs/ui-parity.md: kept HEAD header line, both change rows
This commit is contained in:
@@ -79,7 +79,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"AutoFill",
|
||||
[],
|
||||
None,
|
||||
"{source: \"A1:A2\", target: \"A1:A10\", mode?: series|alternate|copy|formats|values}: the fill-handle drag (alternate = Ctrl+drag: a lone number counts up, any other series copies)",
|
||||
"{source: \"A1:A2\", target: \"A1:A10\", mode?: series|alternate|copy|formats|values|extend}: the fill-handle drag (alternate = Ctrl+drag: a lone number counts up, any other series copies; values = Fill Without Formatting: the series, targets keep their formats; extend = Fill Series: the series, and a lone number counts up)",
|
||||
has_doc,
|
||||
auto_fill
|
||||
),
|
||||
@@ -1403,6 +1403,7 @@ fn auto_fill(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
"alternate" => crate::fill::FillMode::Alternate,
|
||||
"formats" => crate::fill::FillMode::Formats,
|
||||
"values" => crate::fill::FillMode::ValuesOnly,
|
||||
"extend" => crate::fill::FillMode::Extend,
|
||||
_ => crate::fill::FillMode::Series,
|
||||
};
|
||||
if target.count() > 5_000_000 || src.count() > 5_000_000 {
|
||||
|
||||
@@ -19,7 +19,17 @@ pub enum FillMode {
|
||||
Alternate,
|
||||
Copy,
|
||||
Formats,
|
||||
/// Fill Without Formatting: extends like a plain drag, the targets keep their formats.
|
||||
ValuesOnly,
|
||||
/// Fill Series from the right-drag menu: extends like a plain drag, and a lone number counts up.
|
||||
Extend,
|
||||
}
|
||||
|
||||
impl FillMode {
|
||||
/// The modes that continue the source's series rather than copy it.
|
||||
fn extends_series(self) -> bool {
|
||||
matches!(self, FillMode::Series | FillMode::Extend | FillMode::ValuesOnly)
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in custom lists.
|
||||
@@ -67,15 +77,18 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
|
||||
cols.into_iter().map(|c| CellRef::new(lane, c)).collect()
|
||||
};
|
||||
let fmt = |c: &(CellRef, Option<Cell>)| cx.wb.styles.get(c.1.as_ref().map(|c| c.style).unwrap_or_default()).num_fmt.as_str();
|
||||
let pattern = if matches!(mode, FillMode::Series | FillMode::Alternate) {
|
||||
let pattern = if mode.extends_series() || mode == FillMode::Alternate {
|
||||
let detected = Pattern::detect(&src_cells, &custom, cx.wb.date_system, src_cells.first().map(fmt).unwrap_or_default());
|
||||
if mode == FillMode::Alternate { detected.alternate(&src_cells) } else { detected }
|
||||
match (mode, detected) {
|
||||
(FillMode::Alternate, p) | (FillMode::Extend, p @ Pattern::Copy) => p.alternate(&src_cells),
|
||||
(_, p) => p,
|
||||
}
|
||||
} else {
|
||||
Pattern::Copy
|
||||
};
|
||||
// A source with no common pattern (`Product 1`, `On backorder`): each cell continues its
|
||||
// own series, one step per repeat of the source.
|
||||
let per_cell: Vec<Pattern> = if mode == FillMode::Series && matches!(pattern, Pattern::Copy) && src_cells.len() > 1 {
|
||||
let per_cell: Vec<Pattern> = if mode.extends_series() && matches!(pattern, Pattern::Copy) && src_cells.len() > 1 {
|
||||
src_cells.iter().map(|c| Pattern::detect(std::slice::from_ref(c), &custom, cx.wb.date_system, fmt(c))).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -93,7 +106,7 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
|
||||
writes.push((*t, Some(existing)));
|
||||
continue;
|
||||
}
|
||||
FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy => {
|
||||
FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy | FillMode::Extend => {
|
||||
let style = if mode == FillMode::ValuesOnly { sh.cell(*t).map(|c| c.style).unwrap_or_default() } else { cell.style };
|
||||
if let Some(f) = &cell.formula {
|
||||
if let Some(e) = f.expr() {
|
||||
|
||||
@@ -783,6 +783,43 @@ fn ctrl_drag_fill_swaps_copy_and_series() {
|
||||
assert_eq!(v(&s, "G3"), Value::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_series_from_the_right_drag_menu_counts_a_lone_number() {
|
||||
// Excel's right-drag Fill Series: a lone number counts up, everything else extends as a plain drag.
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "7"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "A1", "target": "A1:A3", "mode": "extend"})).unwrap();
|
||||
assert_eq!([v(&s, "A2"), v(&s, "A3")], [Value::Number(8.0), Value::Number(9.0)]);
|
||||
s.execute("range.setValues", json!({"range": "B1", "values": [[1], [3]]})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "B1:B2", "target": "B1:B4", "mode": "extend"})).unwrap();
|
||||
assert_eq!([v(&s, "B3"), v(&s, "B4")], [Value::Number(5.0), Value::Number(7.0)]);
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": "Item 9"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "C1", "target": "C1:C2", "mode": "extend"})).unwrap();
|
||||
assert_eq!(v(&s, "C2"), Value::from("Item 10"));
|
||||
// A source with no common pattern continues each cell's own series, as a plain drag does.
|
||||
s.execute("range.setValues", json!({"range": "D1", "values": [["Product 1"], ["On backorder"]]})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "D1:D2", "target": "D1:D4", "mode": "extend"})).unwrap();
|
||||
assert_eq!([v(&s, "D3"), v(&s, "D4")], [Value::from("Product 2"), Value::from("On backorder")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_without_formatting_extends_the_series_and_keeps_the_target_formats() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [null], [9]]})).unwrap();
|
||||
s.execute("home.bold", json!({"range": "A1:A2", "on": true})).unwrap();
|
||||
s.execute("home.italic", json!({"range": "A4", "on": true})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "A1:A2", "target": "A1:A4", "mode": "values"})).unwrap();
|
||||
assert_eq!([v(&s, "A3"), v(&s, "A4")], [Value::Number(3.0), Value::Number(4.0)]);
|
||||
let d = s.doc().unwrap();
|
||||
let sh = d.wb.active().unwrap();
|
||||
let a4 = d.wb.styles.get(sh.style_id(CellRef::parse("A4").unwrap()));
|
||||
assert!(a4.font.italic && !a4.font.bold, "A4 keeps its own formats");
|
||||
assert!(!d.wb.styles.get(sh.style_id(CellRef::parse("A3").unwrap())).font.bold);
|
||||
s.execute("range.setValues", json!({"range": "B1", "values": [["Product 1"], ["On backorder"]]})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "B1:B2", "target": "B1:B4", "mode": "values"})).unwrap();
|
||||
assert_eq!([v(&s, "B3"), v(&s, "B4")], [Value::from("Product 2"), Value::from("On backorder")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_cells_adjust() {
|
||||
let formula = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["formula"].clone();
|
||||
|
||||
Reference in New Issue
Block a user