Fill handle: Ctrl+drag swaps copying and extending a series (#264)
A plain drag copies a lone number and extends every other series; Ctrl+drag (Cmd on macOS) now does the opposite, as in Excel: a lone number counts up by 1, and numbers, dates, lists and `Item 1`-style text are copied. edit.autoFill gains mode "alternate" for this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f896c1249
commit
1b9751ed82
|
|
@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Ctrl+drag on the fill handle swaps copying and extending a series (#264) |
|
||||
| 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed |
|
||||
| 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 |
|
||||
| 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows |
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"AutoFill",
|
||||
[],
|
||||
None,
|
||||
"{source: \"A1:A2\", target: \"A1:A10\", mode?: series|copy|formats|values}: the fill-handle drag",
|
||||
"{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)",
|
||||
has_doc,
|
||||
auto_fill
|
||||
),
|
||||
|
|
@ -1139,6 +1139,7 @@ fn auto_fill(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
let Some(target) = str_param(p, "target").and_then(RangeRef::parse) else { return Err(bad("edit.autoFill", "missing `target`")) };
|
||||
let mode = match str_param(p, "mode").unwrap_or("series") {
|
||||
"copy" => crate::fill::FillMode::Copy,
|
||||
"alternate" => crate::fill::FillMode::Alternate,
|
||||
"formats" => crate::fill::FillMode::Formats,
|
||||
"values" => crate::fill::FillMode::ValuesOnly,
|
||||
_ => crate::fill::FillMode::Series,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ use crate::cmd::Ctx;
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FillMode {
|
||||
Series,
|
||||
/// The Ctrl+drag fill: the opposite of a plain drag. A lone number fills a
|
||||
/// series stepping by 1; anything a plain drag would extend is copied.
|
||||
Alternate,
|
||||
Copy,
|
||||
Formats,
|
||||
ValuesOnly,
|
||||
|
|
@ -62,13 +65,14 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
|
|||
if forward { (src.end.col + 1..=target.end.col).collect() } else { (target.start.col..src.start.col).rev().collect() };
|
||||
cols.into_iter().map(|c| CellRef::new(lane, c)).collect()
|
||||
};
|
||||
let pattern = if mode == FillMode::Series {
|
||||
Pattern::detect(
|
||||
let pattern = if matches!(mode, FillMode::Series | FillMode::Alternate) {
|
||||
let detected = Pattern::detect(
|
||||
&src_cells,
|
||||
&custom,
|
||||
cx.wb.date_system,
|
||||
cx.wb.styles.get(src_cells.first().and_then(|c| c.1.as_ref()).map(|c| c.style).unwrap_or_default()).num_fmt.as_str(),
|
||||
)
|
||||
);
|
||||
if mode == FillMode::Alternate { detected.alternate(&src_cells) } else { detected }
|
||||
} else {
|
||||
Pattern::Copy
|
||||
};
|
||||
|
|
@ -84,7 +88,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::Copy => {
|
||||
FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy => {
|
||||
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() {
|
||||
|
|
@ -231,6 +235,19 @@ impl Pattern {
|
|||
Pattern::Copy
|
||||
}
|
||||
|
||||
/// The pattern of a Ctrl+drag, which swaps copying and extending: a single
|
||||
/// number (which a plain drag copies) counts up by 1, and every series a
|
||||
/// plain drag would extend (numbers, dates, lists, `Item 1`) is copied.
|
||||
fn alternate(self, cells: &[(CellRef, Option<Cell>)]) -> Pattern {
|
||||
match (self, cells) {
|
||||
(Pattern::Copy, [(_, Some(cell))]) if cell.formula.is_none() => match cell.value.as_f64() {
|
||||
Some(n) if cell.value.is_number() => Pattern::Linear { last: n, first: n, step: 1.0 },
|
||||
_ => Pattern::Copy,
|
||||
},
|
||||
_ => Pattern::Copy,
|
||||
}
|
||||
}
|
||||
|
||||
/// The value `k` steps beyond the source (forward) or before it (backward).
|
||||
fn value(&self, k: i64, forward: bool, _len: usize) -> Option<Value> {
|
||||
let sign = if forward { 1 } else { -1 };
|
||||
|
|
|
|||
|
|
@ -452,6 +452,39 @@ fn fill_series() {
|
|||
assert_eq!(v(&s, "C2"), Value::from("Item 10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_drag_fill_swaps_copy_and_series() {
|
||||
// Issue #264: Ctrl+drag fills a lone number as a series, and copies what a plain drag extends.
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "7"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "A1", "target": "A1:A4"})).unwrap();
|
||||
assert_eq!(v(&s, "A4"), Value::Number(7.0), "a plain drag copies a lone number");
|
||||
s.execute("edit.autoFill", json!({"source": "A1", "target": "A1:A4", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "A2"), Value::Number(8.0));
|
||||
assert_eq!(v(&s, "A4"), Value::Number(10.0));
|
||||
// Upward too.
|
||||
s.execute("cell.set", json!({"cell": "B5", "input": "7"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "B5", "target": "B3:B5", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "B3"), Value::Number(5.0));
|
||||
// A two-number series, a list entry and text with a number are copied.
|
||||
s.execute("range.setValues", json!({"range": "C1", "values": [[1], [3]]})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "C1:C2", "target": "C1:C4", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "C3"), Value::Number(1.0));
|
||||
assert_eq!(v(&s, "C4"), Value::Number(3.0));
|
||||
s.execute("cell.set", json!({"cell": "D1", "input": "Mon"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "D1", "target": "D1:D3", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "D3"), Value::from("Mon"));
|
||||
s.execute("cell.set", json!({"cell": "E1", "input": "Item 9"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "E1", "target": "E1:E2", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "E2"), Value::from("Item 9"));
|
||||
// Plain text copies either way; an empty source clears.
|
||||
s.execute("cell.set", json!({"cell": "F1", "input": "abc"})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "F1", "target": "F1:F2", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "F2"), Value::from("abc"));
|
||||
s.execute("edit.autoFill", json!({"source": "G1", "target": "G1:G3", "mode": "alternate"})).unwrap();
|
||||
assert_eq!(v(&s, "G3"), Value::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_cells_adjust() {
|
||||
let formula = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["formula"].clone();
|
||||
|
|
|
|||
|
|
@ -1421,7 +1421,8 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
Drag::Fill { target: Some(t) } => {
|
||||
let src = sel.current();
|
||||
if t != src {
|
||||
let mode = if mods.command { "copy" } else { "series" };
|
||||
// Ctrl (Cmd on macOS) swaps copying and extending, as in Excel.
|
||||
let mode = if mods.command { "alternate" } else { "series" };
|
||||
if let Err(e) = app.run("edit.autoFill", json!({"source": src.a1(), "target": t.a1(), "mode": mode})) {
|
||||
app.message = Some(("GridCraft".into(), crate::clean_error(&e)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
|||
| Behaviour | Status |
|
||||
|---|---|
|
||||
| Click/drag/Shift/Ctrl selection, multi-area selection | rectangular drag works in one direction only in some cases (#28) |
|
||||
| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done; right-drag menu missing |
|
||||
| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series); right-drag menu missing |
|
||||
| Drag-move and drag-copy cells (border drag, Ctrl/Option, Shift to insert) | done (M3) |
|
||||
| In-cell editing, Enter/Tab movement, Alt/Option+Enter line breaks | done |
|
||||
| Point mode (click/drag/arrow references), reference colouring, F4 | done |
|
||||
|
|
|
|||
Loading…
Reference in New Issue