From f4dad1d269d42e281d4228dd7df9bca0c457197f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 06:47:31 -0400 Subject: [PATCH] ElyTable: a new chart goes in empty cells beside its data, off the view if need be, and scrolls into view (R2.10) --- agents.md | 2 +- crates/ely-lok/src/client/tests/analysis.rs | 63 ++++++++++++--- crates/ely-lok/src/view.rs | 1 + crates/ely-lok/src/view/scroll.rs | 7 ++ crates/ely-lok/src/view/tests/large.rs | 24 ++++++ .../0041-lok-calc-chart-beside-its-data.patch | 78 +++++++++++++++++-- 6 files changed, 156 insertions(+), 19 deletions(-) diff --git a/agents.md b/agents.md index 0f4d690..f9757aa 100644 --- a/agents.md +++ b/agents.md @@ -268,6 +268,6 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-09:ElyTable 的自动筛选(R2.9)。内核会重新推算未命名的自动筛选区:每次打开筛选列表时向下扩到紧邻的数据行(`ExtendDataArea`、`ExtendBackColorArea`,为接上后来追加的行),命令从单格找到它时按连续数据区重算(`ScDocShell::GetDBData`:应用条件、关闭筛选及其撤销);选区下方紧邻的合计行因此进了列表、随数据一起被隐藏,存后重开的筛选区同样会扩。补丁 `0040-lok-calc-autofilter-keeps-its-rows.patch`:LOK 下已有的自动筛选区保持设定时的行(同 Excel),不再隐式扩展(光标在区内时;光标在同表另一块数据里即按那一块);单格开筛选时仍取整个数据区。代价:紧贴在筛选区下方追加的行不自动并入,需重新选定后再开筛选。 - 2026-10-09:ElyTree 的差异按文件类型着色(R2.12)。组件库 7f2ac3e 与 5caf5aa:`DiffViewer` 按路径的扩展名判定散文与纯文本(md、markdown、txt、rst、adoc、org、csv、tsv、log 等,无扩展名同,不着色),代码按语言的行注释记号着色(`#`:py、toml、sh、yaml 等与 Makefile;`--`:sql、lua;`;`:ini、lisp;`%`:tex、erl;其余 `//`),关键字表各语言共用(原先一律套 `//` 的通用词法,Markdown 的 `#`、Python 的注释都被当作代码上色)。同批带入上游的块编辑器跨块选择、浮层滚轮、输入法候选框位置、插入点按设备像素对齐与格式修正。 - 2026-10-09:ElyMark 的表格(R2.13)。表格的空格与分隔行写法有许多种(`|a|b|`、`|---|`、对齐空格),逐字重现做不到,原先凡写不回原样的表格都成了原文块。现在一块的序列化与原文在把其中每张表格化成规范形式(各列对齐方式加各格的原文,`tables.js` 的 `canon`)后相同,即为可视块(`preserve.js` 的 `kept`),引用与列表里的表格同样;格内有 Milkdown 会改写的写法(`[[…]]`、引用式链接等)即不同,整块仍为原文块,与段落的规则一致。Milkdown 的列表项必须以段落开头,表格作列表项的第一块时无法放进编辑器,照原文块保留。未改的表格照旧逐字写回,改过的表格按文档多数表格的写法重写(`style.js` 按表格的行节点投票:单元格两侧有无空格、是否按列对齐,经 remark-gfm 的 `tableCellPadding` 与 `tablePipeAlign`;原先投票的解析器不认 GFM,表格从未计票),分隔行随之重写(如 `|---|` 成 `| - |`)。 -- 2026-10-09:ElyTable 新插图表的位置(R2.10)。内核按“定位区域”把图表放在其旁的空处;带参数的请求(Ely 经 `FN_PARAM_5` 给区域)原把定位区域取为光标格,图表紧贴光标、压在数据上。补丁 `0041-lok-calc-chart-beside-its-data.patch`:定位区域取请求里的数据区域(多段取并集),同对话框路径取所选单元格。 +- 2026-10-09:ElyTable 新插图表的位置(R2.10)。内核按“定位区域”把图表放在其旁、并挤回当前可见区;带参数的请求(Ely 经 `FN_PARAM_5` 给区域)原把定位区域取为光标格,图表压在数据上或旁边的另一块数据上。补丁 `0041-lok-calc-chart-beside-its-data.patch`:LOK 下有请求区域时,图表放进数据区右侧(隔一列)第一块够大的空单元格(逐列向右找,跨过旁边的数据块),右侧没有即放在数据下方,不受可见区限制;`LokView` 在新选中对象(原先无选中)时把它滚入视口,拖动中的对象不滚动。 - 2026-10-09:ElyPDF 编辑与缩放不闪白(R2.6)。旧图块不再随编辑或比例变化即刻丢弃:编辑落地时全部当前图块、比例变化时离开视图的同页图块,连同当时的页面尺寸进入“底图”(`apps/ely-pdf/src/backdrop.rs`),按其在页面中的比例位置画在该页新图块之下,直到该页在视口里的新图块到齐、或页面尺寸与旋转变了才释放;每页画什么由 `PdfView::pieces` 决定,绘制与测试同用它。只有改内容的编辑保留底图并沿用已测的页面尺寸(前 100 页以外不再被占位尺寸覆盖);增删、移动、旋转、裁剪页面与撤销重做(`PdfView::edit_pages`)清空底图,因同一序号可能已是另一页。关闭标签时底图随图块交给窗口释放。原先比较整份尺寸表(超过 100 页的混合纸张文档尺寸表后段为占位,比较失败即全部清空)且只认同比例的图块(编辑后右栏收起、比例变化即清空),都会闪白。 - 2026-10-09:文档的缩放锚点(R2.7)。窄于视口的文档居中,缩放原按视口坐标求锚点、漏了居中的偏移,锚点随缩放漂移;现锚点先减去缩放前的偏移,缩放后按新的偏移(`LokView::slack`,绘制同用)换回。按键与菜单的缩放(无指针)另保持首页左缘在视口内;指针与捏合的缩放保持指针下的点不动(文档边界以外即止)。 diff --git a/crates/ely-lok/src/client/tests/analysis.rs b/crates/ely-lok/src/client/tests/analysis.rs index 03a97d0..230eb1d 100644 --- a/crates/ely-lok/src/client/tests/analysis.rs +++ b/crates/ely-lok/src/client/tests/analysis.rs @@ -334,20 +334,61 @@ fn undoing_a_new_kind_brings_the_whole_chart_back() { ); } +/// A sheet of ones filling each `(columns, rows)` block. +fn blocks(filled: &[(std::ops::RangeInclusive, std::ops::RangeInclusive)]) -> String { + let rows: String = (1..=60) + .map(|r| { + let cells: String = (0..20) + .map(|c| match filled.iter().any(|(cols, rows)| cols.contains(&c) && rows.contains(&r)) { + true => r#"1"#, + false => "", + }) + .collect(); + format!("{cells}") + }) + .collect(); + format!( + r#" + + {rows} +"# + ) +} + +/// Charts `source` and checks it clears each data block. +fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&str, &str)]) { + let (_dir, mut worker, doc) = open_book(sheet); + doc.visible_area(&mut worker, visible, (256, 3840)).expect("the area"); + let mut seen = poll_until(&mut worker, &doc, &Events::default(), SETTLE, |_| false); + let mut cell = |worker: &mut Worker, name: &str| { + worker.run(&doc, "cell.goto", &[name]).expect("the cell"); + let before = seen.cell.cursor; + seen = poll_until(worker, &doc, &seen, SETTLE, |now| now.cell.cursor.is_some() && now.cell.cursor != before); + seen.cell.cursor.expect("the cell's rect") + }; + let spans: Vec<_> = data.iter().map(|(from, to)| (cell(&mut worker, from), cell(&mut worker, to))).collect(); + let all = written(&mut worker, &doc, source); + worker.run(&doc, "cell.goto", &["A1"]).expect("A1"); + worker.run(&doc, "chart.insert", &[&all, "false", &Look { kind: "column".into(), ..Look::default() }.to_json()]).expect("inserts"); + let framed = poll_until(&mut worker, &doc, &seen, SETTLE, |now| now.graphic.is_some()); + let (x, y, w, h) = framed.graphic.expect("the chart is selected"); + for ((a, b), (from, to)) in spans.iter().zip(data) { + let clear = x >= b.0 + b.2 || x + w <= a.0 || y >= b.1 + b.3 || y + h <= a.1; + assert!(clear, "the chart {:?} covers {from}:{to}", (x, y, w, h)); + } +} + #[test] fn a_new_chart_lands_beside_its_data() { if skip() { return; } - let (_dir, mut worker, doc) = open_book(&data()); - let all = written(&mut worker, &doc, "A1:C5"); - let base = poll_until(&mut worker, &doc, &Events::default(), SETTLE, |_| false); - worker.run(&doc, "cell.goto", &["C5"]).expect("C5"); - let corner = poll_until(&mut worker, &doc, &base, SETTLE, |seen| seen.cell.cursor.is_some() && seen.cell.cursor != base.cell.cursor); - let (x, y, w, h) = corner.cell.cursor.expect("the last data cell"); - worker.run(&doc, "cell.goto", &["A1"]).expect("A1"); - worker.run(&doc, "chart.insert", &[&all, "false", &Look { kind: "column".into(), ..Look::default() }.to_json()]).expect("inserts"); - let framed = poll_until(&mut worker, &doc, &corner, SETTLE, |seen| seen.graphic.is_some()); - let (left, top, _, _) = framed.graphic.expect("the chart is selected"); - assert!(left >= x + w || top >= y + h, "the chart at {left},{top} covers data ending {},{}", x + w, y + h); + // Its own data, a neighbour past two empty columns. + charted(&blocks(&[(0..=2, 1..=5), (5..=15, 1..=20)]), (0, 0, 15000, 10000), "A1:C5", &[("A1", "C5"), ("F1", "P20")]); + // Data wider and taller than the view. + charted(&blocks(&[(0..=15, 1..=50)]), (0, 0, 15000, 10000), "A1:P50", &[("A1", "P50")]); + // A view narrower than the chart. + charted(&blocks(&[(0..=2, 1..=5)]), (0, 0, 6000, 3500), "A1:C5", &[("A1", "C5")]); } diff --git a/crates/ely-lok/src/view.rs b/crates/ely-lok/src/view.rs index 5c8053e..e3bc9f7 100644 --- a/crates/ely-lok/src/view.rs +++ b/crates/ely-lok/src/view.rs @@ -389,6 +389,7 @@ impl LokView { { self.show(cell, cx); } + self.follow_graphic(batch.graphic, cx); self.cursor = batch.cursor; self.cursor_visible = batch.cursor_visible; self.selection = batch.selection; diff --git a/crates/ely-lok/src/view/scroll.rs b/crates/ely-lok/src/view/scroll.rs index 91de12f..ca0e9de 100644 --- a/crates/ely-lok/src/view/scroll.rs +++ b/crates/ely-lok/src/view/scroll.rs @@ -41,6 +41,13 @@ impl LokView { self.scroll_by(dx, dy, cx); } + /// A newly selected object, such as a chart, shows. + pub(super) fn follow_graphic(&mut self, frame: Option, cx: &mut Context) { + if let (None, Some(frame), true) = (self.graphic, frame, self.viewport.1 > 0.0) { + self.show(frame, cx); + } + } + /// Scrolls print-twips `rect` into view; frozen parts always show. pub fn show(&mut self, rect: Rect, cx: &mut Context) { let (x, y, w, h) = self.tile_rect(rect); diff --git a/crates/ely-lok/src/view/tests/large.rs b/crates/ely-lok/src/view/tests/large.rs index 1eb58e5..5ddc0a0 100644 --- a/crates/ely-lok/src/view/tests/large.rs +++ b/crates/ely-lok/src/view/tests/large.rs @@ -215,3 +215,27 @@ fn a_sheet_brings_far_cut_and_frozen_cells_into_view() { }); }); } + +#[test] +fn a_newly_selected_object_comes_into_view_a_moved_one_stays() { + let Some(install) = crate::client::tests::install() else { + eprintln!("SKIP: no pinned LOK build"); + return; + }; + let cx = gpui::TestAppContext::single(); + cx.update(|app| { + let (view, _) = live_view_of(app, &install, "tall.fods", TALL); + view.update(app, |view, cx| { + let bounds = Bounds { origin: point(px(0.0), px(0.0)), size: size(px(800.0), px(600.0)) }; + view.prepare(bounds); + let far = (60_000, 40_000, 9_000, 5_000); + let chart = |graphic| crate::Events { graphic, ..crate::Events::default() }; + view.apply_batch(chart(Some(far)), None, cx); + view.prepare(bounds); + let (x, y) = (view.scroll.0, view.scroll.1); + assert!(x > 0 && y > 0, "the new chart is shown: scroll {x},{y}"); + view.apply_batch(chart(Some((61_000, 40_000, 9_000, 5_000))), None, cx); + assert_eq!((view.scroll.0, view.scroll.1), (x, y), "dragging it does not scroll"); + }); + }); +} diff --git a/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch b/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch index bae0776..d0d0710 100644 --- a/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch +++ b/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch @@ -1,16 +1,68 @@ LOK Calc: a new chart lands beside its data -FuInsertChart positions a chart away from aPositionRange. With request -arguments (a kit client gives the range in FN_PARAM_5) that range was -the cursor cell alone, so the chart went right next to the cursor and -over the data it charts. Position it from the requested data range, -as the dialog path does with the marked cells. +FuInsertChart placed a chart away from aPositionRange, which for +request arguments (a kit client gives the range in FN_PARAM_5) was the +cursor cell alone; GetChartInsertPos then pulled the chart back into +the visible area. The chart covered the data it charts, or a block +next to it. Under LOK, with a requested range, the chart goes in the +first empty cells right of its data large enough for it, else below +the data, whatever the visible area; the view scrolls to it. diff --git a/sc/source/ui/drawfunc/fuins2.cxx b/sc/source/ui/drawfunc/fuins2.cxx -index b0169d0..0a4e3dc 100644 +index b0169d0..fd65434 100644 --- a/sc/source/ui/drawfunc/fuins2.cxx +++ b/sc/source/ui/drawfunc/fuins2.cxx -@@ -452,6 +452,14 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV +@@ -47,6 +47,7 @@ + #include + + #include ++#include + #include + #include + #include +@@ -215,6 +216,41 @@ void lcl_ChartInit(const uno::Reference & xObj, ScViewDa + // don't create chart listener here (range may be modified in chart dialog) + } + ++ ++/// Cells from (nCol, nRow) wide and tall enough for rSize. ++ScRange lcl_Room(const ScDocument& rDoc, SCTAB nTab, SCCOL nCol, SCROW nRow, const Size& rSize) ++{ ++ SCCOL nRight = nCol; ++ while (nRight < rDoc.MaxCol() && rDoc.GetMMRect(nCol, nRow, nRight, nRow, nTab).GetWidth() < rSize.Width()) ++ ++nRight; ++ SCROW nBottom = nRow; ++ while (nBottom < rDoc.MaxRow() && rDoc.GetMMRect(nCol, nRow, nCol, nBottom, nTab).GetHeight() < rSize.Height()) ++ ++nBottom; ++ return ScRange(nCol, nRow, nTab, nRight, nBottom, nTab); ++} ++ ++/// LOK: the first empty cells right of rData, else below it. ++Point lcl_EmptyBeside(const ScDocument& rDoc, const ScRange& rData, const Size& rSize) ++{ ++ const SCTAB nTab = rData.aStart.Tab(); ++ const auto empty = [&](const ScRange& r) { ++ return rDoc.IsBlockEmpty(r.aStart.Col(), r.aStart.Row(), r.aEnd.Col(), r.aEnd.Row(), nTab); ++ }; ++ for (SCCOL nCol = rData.aEnd.Col() + 2; nCol <= rDoc.MaxCol(); ++nCol) ++ { ++ const ScRange aRoom = lcl_Room(rDoc, nTab, nCol, rData.aStart.Row(), rSize); ++ if (aRoom.aEnd.Col() < rDoc.MaxCol() && empty(aRoom)) ++ return rDoc.GetMMRect(nCol, aRoom.aStart.Row(), nCol, aRoom.aStart.Row(), nTab).TopLeft(); ++ } ++ for (SCROW nRow = rData.aEnd.Row() + 2; nRow <= rDoc.MaxRow(); ++nRow) ++ { ++ const ScRange aRoom = lcl_Room(rDoc, nTab, rData.aStart.Col(), nRow, rSize); ++ if (empty(aRoom)) ++ return rDoc.GetMMRect(aRoom.aStart.Col(), nRow, aRoom.aStart.Col(), nRow, nTab).TopLeft(); ++ } ++ throw std::runtime_error("no empty cells hold the chart"); ++} ++ + } + + FuInsertOLE::FuInsertOLE(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawView* pViewP, +@@ -452,6 +488,14 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV aRangeString = static_cast(pItem)->GetValue(); aPositionRange = rViewSh.GetViewData().GetCurPos(); @@ -25,3 +77,15 @@ index b0169d0..0a4e3dc 100644 } else { +@@ -612,7 +656,10 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV + // object position + + // get chart position (from window size and data range) +- Point aStart = rViewSh.GetChartInsertPos( aSize, aPositionRange ); ++ // LOK: beside the data, never over it nor squeezed into view. ++ Point aStart = pRange && comphelper::LibreOfficeKit::isActive() ++ ? lcl_EmptyBeside(rData.GetDocument(), aPositionRange, aSize) ++ : rViewSh.GetChartInsertPos( aSize, aPositionRange ); + + tools::Rectangle aRect (aStart, aSize); + rtl::Reference pObj = new SdrOle2Obj(