ElyTable: the chart search uses its own sheet, scans the sheet as a last resort, and fails cleanly with no room (R2.10)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 09:10:28 -04:00
1 parent b3fc6c9c82
commit eee3b9e288
5 files changed
+71 -25

No files matched your search

+1 -1
View File
@@ -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 至 b0dbfd4:`DiffViewer`(建立时按路径定下读法) 按路径的扩展名判定散文与纯文本(md、markdown、txt、rst、adoc、org、csv、tsv、log 等,无扩展名同,不着色),代码按语言着色(组件库 b0dbfd4 的 `forms::syntax`:Rust、JavaScript/TypeScript、类 C、Python、Ruby、Shell、YAML、INI、SQL、Lua、Haskell、Lisp、Erlang、TeX 与其他 `#` 语言各有行注释记号、关键字与常量;Shell 与 YAML 的 `#` 只在词首起注释;Makefile、Dockerfile、CMakeLists.txt 先按文件名认),不是完整的语法分析器(字符串插值、块注释、多行字符串照通用词法)(原先一律套 `//` 的通用词法,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`:LOK 下有请求区域时,图表放进数据区右侧(隔一列)第一块够大、无合并格的空单元格(逐列向右找,跨过旁边的数据块),右侧没有依次找下方、左侧、上方(尺寸从候选格起累加,表尾也快),不受可见区限制;四周都没有空处时照内核原样;`LokView` 在新选中对象(原先无选中)时把它滚入视口,拖动中的对象不滚动。
- 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`,绘制同用)换回。按键与菜单的缩放(无指针)另保持首页左缘在视口内;指针与捏合的缩放保持指针下的点不动(文档边界以外即止)。
+34 -14
View File
@@ -336,7 +336,13 @@ 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<usize>, std::ops::RangeInclusive<usize>)]) -> String {
let rows: String = (1..=60)
sheet(&rows_of(filled))
}
/// Rows of ones filling each `(columns, rows)` block.
fn rows_of(filled: &[(std::ops::RangeInclusive<usize>, std::ops::RangeInclusive<usize>)]) -> String {
let height = filled.iter().map(|(_, rows)| *rows.end()).max().unwrap_or(0);
(1..=height)
.map(|r| {
let cells: String = (0..20)
.map(|c| match filled.iter().any(|(cols, rows)| cols.contains(&c) && rows.contains(&r)) {
@@ -346,12 +352,11 @@ fn blocks(filled: &[(std::ops::RangeInclusive<usize>, std::ops::RangeInclusive<u
.collect();
format!("<table:table-row>{cells}</table:table-row>")
})
.collect();
sheet(&rows)
.collect()
}
/// Charts `source` and checks it clears each data block.
fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&str, &str)]) {
fn charted(sheet: &str, visible: (i32, i32, i32, i32), (source, stand): (&str, &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);
@@ -363,7 +368,7 @@ fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&s
};
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, "cell.goto", &[stand]).expect("where the chart goes");
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");
@@ -375,12 +380,18 @@ fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&s
/// A sheet body as flat XML.
fn sheet(rows: &str) -> String {
sheets(&[("S", rows)])
}
/// Named sheets as one flat XML book.
fn sheets(tables: &[(&str, &str)]) -> String {
let tables: String = tables.iter().map(|(name, rows)| format!(r#"<table:table table:name="{name}">{rows}</table:table>"#)).collect();
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
<office:body><office:spreadsheet><table:table table:name="S">{rows}</table:table></office:spreadsheet></office:body>
<office:body><office:spreadsheet>{tables}</office:spreadsheet></office:body>
</office:document>"#
)
}
@@ -393,19 +404,28 @@ fn a_new_chart_lands_beside_its_data() {
return;
}
// 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")]);
charted(&blocks(&[(0..=2, 1..=5), (5..=15, 1..=20)]), (0, 0, 15000, 10000), ("A1:C5", "A1"), &[("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")]);
charted(&blocks(&[(0..=15, 1..=50)]), (0, 0, 15000, 10000), ("A1:P50", "A1"), &[("A1", "P50")]);
// A view narrower than the chart.
charted(&blocks(&[(0..=2, 1..=5)]), (0, 0, 6000, 3500), "A1:C5", &[("A1", "C5")]);
charted(&blocks(&[(0..=2, 1..=5)]), (0, 0, 6000, 3500), ("A1:C5", "A1"), &[("A1", "C5")]);
// A merged block beside the data is taken, not free.
let merged = format!(
r#"<table:table-row>{ONE}{ONE}{ONE}<table:table-cell table:number-columns-spanned="13" table:number-rows-spanned="20"><text:p>MERGED</text:p></table:table-cell></table:table-row><table:table-row table:number-rows-repeated="4">{ONE}{ONE}{ONE}</table:table-row>"#
);
charted(&sheet(&merged), (0, 0, 15000, 10000), "A1:C5", &[("A1", "C5"), ("D1", "D1")]);
charted(&sheet(&merged), (0, 0, 15000, 10000), ("A1:C5", "A1"), &[("A1", "C5"), ("D1", "D1")]);
// Data in the last cells: left or above.
let corner = format!(
r#"<table:table-row table:number-rows-repeated="1048574"><table:table-cell table:number-columns-repeated="16384"/></table:table-row><table:table-row table:number-rows-repeated="2"><table:table-cell table:number-columns-repeated="16382"/>{ONE}{ONE}</table:table-row>"#
);
charted(&sheet(&corner), (0, 0, 15000, 10000), "XFC1048575:XFD1048576", &[("XFC1048575", "XFD1048576")]);
let corner = |above: usize| {
let empty = 1048574 - above;
format!(
r#"<table:table-row table:number-rows-repeated="{empty}"><table:table-cell table:number-columns-repeated="16384"/></table:table-row><table:table-row table:number-rows-repeated="2"><table:table-cell table:number-columns-repeated="16382"/>{ONE}{ONE}</table:table-row>"#
)
};
charted(&sheet(&corner(0)), (0, 0, 15000, 10000), ("XFC1048575:XFD1048576", "A1"), &[("XFC1048575", "XFD1048576")]);
// Corner data, a block at A1: room elsewhere.
let full = format!("{}{}", rows_of(&[(0..=15, 1..=20)]), corner(20));
charted(&sheet(&full), (0, 0, 15000, 10000), ("XFC1048575:XFD1048576", "A1"), &[("A1", "P20"), ("XFC1048575", "XFD1048576")]);
// Data on S, chart on T: T's cells stay clear.
let two = sheets(&[("S", &rows_of(&[(0..=2, 1..=5)])), ("T", &rows_of(&[(0..=15, 1..=20)]))]);
charted(&two, (0, 0, 15000, 10000), ("S!A1:C5", "T!A1"), &[("T!A1", "T!P20")]);
}
@@ -5,12 +5,14 @@ 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, unmerged cells right of its data large enough for it,
else below, left or above, whatever the visible area. Sizes are summed
from the candidate cell, so a search near the sheet's end stays quick.
first empty, unmerged cells of its own sheet right of its data large
enough for it, else below, left or above, else the first such block
anywhere, whatever the visible area. Data on another sheet starts the
search at the cursor. With no room on the sheet the insert fails and
nothing lands. Sizes are summed from each candidate cell.
diff --git a/sc/source/ui/drawfunc/fuins2.cxx b/sc/source/ui/drawfunc/fuins2.cxx
index b0169d0..2a6a2e9 100644
index b0169d0..121500d 100644
--- a/sc/source/ui/drawfunc/fuins2.cxx
+++ b/sc/source/ui/drawfunc/fuins2.cxx
@@ -47,6 +47,8 @@
@@ -22,7 +24,7 @@ index b0169d0..2a6a2e9 100644
#include <comphelper/storagehelper.hxx>
#include <comphelper/propertysequence.hxx>
#include <com/sun/star/embed/EmbedVerbs.hpp>
@@ -215,6 +217,64 @@ void lcl_ChartInit(const uno::Reference <embed::XEmbeddedObject>& xObj, ScViewDa
@@ -215,6 +217,69 @@ void lcl_ChartInit(const uno::Reference <embed::XEmbeddedObject>& xObj, ScViewDa
// don't create chart listener here (range may be modified in chart dialog)
}
@@ -81,13 +83,18 @@ index b0169d0..2a6a2e9 100644
+ for (SCROW nRow = nTop - 2; oRight && nRow >= 0; --nRow)
+ if (const auto oEnd = lcl_Down(rDoc, nTab, nRow, nHeight); oEnd && *oEnd < nTop - 1 && free(nLeft, nRow, *oRight, *oEnd))
+ return at(nLeft, nRow);
+ // Last, any chart-sized empty block, row bands first.
+ for (SCROW nRow = 0; const auto oBottom = lcl_Down(rDoc, nTab, nRow, nHeight); nRow = *oBottom + 1)
+ for (SCCOL nCol = 0; const auto oEnd = lcl_Across(rDoc, nTab, nCol, nWidth); nCol = *oEnd + 1)
+ if (free(nCol, nRow, *oEnd, *oBottom))
+ return at(nCol, nRow);
+ return std::nullopt;
+}
+
}
FuInsertOLE::FuInsertOLE(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawView* pViewP,
@@ -452,6 +512,14 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV
@@ -452,6 +517,14 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV
aRangeString = static_cast<const SfxStringItem*>(pItem)->GetValue();
aPositionRange = rViewSh.GetViewData().GetCurPos();
@@ -102,7 +109,7 @@ index b0169d0..2a6a2e9 100644
}
else
{
@@ -612,7 +680,12 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV
@@ -612,7 +685,26 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV
// object position
// get chart position (from window size and data range)
@@ -110,8 +117,22 @@ index b0169d0..2a6a2e9 100644
+ // LOK: beside the data, never over it, view or not.
+ std::optional<Point> oBeside;
+ if (pRange && comphelper::LibreOfficeKit::isActive())
+ oBeside = lcl_EmptyBeside(rData.GetDocument(), aPositionRange, aSize);
+ // A sheet with no room around the data: as before.
+ {
+ // The chart's own sheet; data elsewhere starts at the cursor.
+ const SCTAB nTab = rData.CurrentTabForData();
+ ScRange aNear = aPositionRange.aStart.Tab() == nTab ? aPositionRange : ScRange(rData.GetCurPos());
+ aNear.aStart.SetTab(nTab);
+ aNear.aEnd.SetTab(nTab);
+ oBeside = lcl_EmptyBeside(rData.GetDocument(), aNear, aSize);
+ if (!oBeside)
+ {
+ SAL_WARN("sc.lok", "no empty cells hold the chart");
+ if (xChartModel.is())
+ xChartModel->unlockControllers();
+ rViewSh.GetObjectShell()->GetEmbeddedObjectContainer().RemoveEmbeddedObject(aName);
+ return;
+ }
+ }
+ Point aStart = oBeside ? *oBeside : rViewSh.GetChartInsertPos( aSize, aPositionRange );
tools::Rectangle aRect (aStart, aSize);
+1 -1
View File
@@ -187,7 +187,7 @@
- [x] R2.7 ElyDoc:页面水平位置每次一致;缩放以视口中的页面为锚,左页边不滚出视口 — R1 锚点漏居中偏移;R2 同帧连续缩放用旧偏移;R3 PASS
- [ ] R2.8 ElyTable:状态栏统计按单元格格式显示数值(内核按光标格的数字格式写出,Ely 不改写;常规格式即内核的常规写法) — R3 FAIL(格式字面量里的分号被当作分隔)后改为按字段名切分,三轮上限,未再审
- [x] R2.9 ElyTable:对选中区域开自动筛选不扩到区域外紧邻的合计行 — R1 只标记于内存,应用、撤销、重开仍扩;R2 光标在别的数据块也沿用原区;R3 PASS
- [ ] R2.10 ElyTable:新插入的图表放在数据区旁的空白处,不压住数据
- [ ] R2.10 ElyTable:新插入的图表放在数据区旁的空白处,不压住数据 — R1 只避开源区、被挤回可见区;R2 抛出异常使 worker 退出、合并格;R3 FAIL(跨表按源表查、四周无处时照旧压住数据)后改为按所在表查、全表找空处、无处即失败,三轮上限,未再审
- [x] R2.11 ElyTree:改动数的单复数(“1 file changed”) — R1 测试只核键名;R2 PASS
- [ ] R2.12 ElyTree:差异按文件类型着色,Markdown 不按代码高亮 — R3 FAIL(Rust 与 Ruby 词表、Haskell 与 Erlang 注释、TOML 与 CMake 紧接注释)后按语言分表(组件库 b0dbfd4),三轮上限,未再审
- [ ] R2.13 ElyMark:紧凑写法的 GFM 表格(无对齐空格)在可视模式显示为表格
+5
View File
@@ -0,0 +1,5 @@
FAIL
patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch:53 — 上一轮的跨表定位缺陷仍存在:空白检测与坐标计算取数据源工作表,图表插入当前工作表。具体场景:S 表 A1:C5 填入数值,T 表 A1:P20 填满数值;在 T 表以 `S!$A$1:$C$5` 插入柱图。两次独立运行均生成矩形 (5120,0,9070,5102),覆盖 T 表已有数值的 F1 (6400,0,1279,255);保存后的 FODS 确认图表属于 T 表并保留该位置。修复方向:按实际插入工作表的几何与占用计算候选位置,补充跨表数据源且目标表已有数据的回归断言。证据:/tmp/ely-r210-r3-probes.log。
patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch:115 — 搜索失败静默调用 GetChartInsertPos,重新引入视口内覆盖数据的行为。具体场景:同一表的 A1:P20 与 XFC1048575:XFD1048576 填入数值,可见区为 (0,0,15000,10000) twips,在 A1 以右下角四格插入柱图。搜索固定横向候选的起始行、纵向候选的起始列,右下角两个方向均容纳不了图表,大片空白也搜索不到;随后生成图表 (5872,4840,9071,5102),覆盖 F20 (6400,4864,1279,255)。两次独立运行及第三次带同 worker 另一文档的复验一致,FODS 保留该位置;另一文档仍可编辑,确认旧崩溃已消除。现有角落测试只核对源数据区,漏掉被覆盖的另一块数据。修复方向:在工作表边界同时调整候选行列以找到完整空白区域,搜索耗尽通过受控的命令失败上报,并让角落回归核对全部已有数据块。证据:/tmp/ely-r210-r3-probes.log、/tmp/ely-r210-r3-confirm.log。