ElyTable: a new chart goes in empty cells beside its data, off the view if need be, and scrolls into view (R2.10)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 06:47:31 -04:00
1 parent 409805e863
commit f4dad1d269
6 files changed
+156 -19

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 与 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`,绘制同用)换回。按键与菜单的缩放(无指针)另保持首页左缘在视口内;指针与捏合的缩放保持指针下的点不动(文档边界以外即止)。
+52 -11
View File
@@ -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<usize>, std::ops::RangeInclusive<usize>)]) -> 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#"<table:table-cell office:value-type="float" office:value="1"><text:p>1</text:p></table:table-cell>"#,
false => "<table:table-cell/>",
})
.collect();
format!("<table:table-row>{cells}</table:table-row>")
})
.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:document>"#
)
}
/// 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")]);
}
+1
View File
@@ -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;
+7
View File
@@ -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<Rect>, cx: &mut Context<Self>) {
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<Self>) {
let (x, y, w, h) = self.tile_rect(rect);
+24
View File
@@ -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");
});
});
}
@@ -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 <vcl/weldutils.hxx>
#include <comphelper/lok.hxx>
+#include <stdexcept>
#include <comphelper/storagehelper.hxx>
#include <comphelper/propertysequence.hxx>
#include <com/sun/star/embed/EmbedVerbs.hpp>
@@ -215,6 +216,41 @@ void lcl_ChartInit(const uno::Reference <embed::XEmbeddedObject>& 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<const SfxStringItem*>(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<SdrOle2Obj> pObj = new SdrOle2Obj(