ElyTable: under LOK an AutoFilter range keeps its rows through list, apply, toggle, undo and reopen (R2.9)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 03:20:07 -04:00
1 parent f4c1a1665b
commit 0ce468a3ad
5 files changed
+141 -120

No files matched your search

+1 -1
View File
@@ -263,4 +263,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-09(用户定:“打开方式”里不得出现 LibreOffice):系统会登记磁盘上能找到的 .app、挂载镜像里的 .app 与运行过的包,构建目录里的 `LibreOffice.app` 与测试挂载的官方镜像曾因此成为 xlsx 等的默认程序。现构建后由 `scripts/inert-lo.sh` 去掉其全部文件与 URL 声明、快速查看与聚焦插件,注销它,并在 `engines/` 放 `.metadata_never_index`;Ely 只取其 Frameworks 与 Resources。任何脚本与测试都不挂载 LibreOffice 镜像:LOK 探测用本构建的内核,单测所需的官方内核由 `scripts/extract-official-lo.sh` 以 7zz 解成无包的文件夹。`tests/scripts/lo-unregistered.sh` 以 NSWorkspace 核对 docx、xlsx、pptx、odt、csv 的“打开方式”里没有 LibreOffice。
- 2026-10-09(用户报:ElyTable 点格跳到别处、拖不出选区):Calc 在 `sc_print_twips_msgs` 下以打印 twips(行高原值)报光标、选区与 `.uno:SheetGeometryData`,图块与鼠标却在图块 twips(每行按 `ScViewData::ToPixel` 截成整像素),两者随行号漂移(5000 行处约 140 行)。视图对外一律打印 twips;`LokView::set_geometry` 收下工作表几何后,`sheet::Axis::tile`、`print` 照内核的截断换算(每像素 twips 取图块的像素与 twips 之比),绘制、显示、跳转与冻结都先换成图块 twips,鼠标以图块 twips 发出(`tile_point`),拖动句柄仍为打印 twips;`.uno:` 读回与应用(表头、填充柄)照旧用打印 twips,表头的起点取 `LokView::scrolled_start`。内核读鼠标用的比例原取最后一块图块,worker 现于每次 `setClientVisibleArea` 前以 `setClientZoom` 告知图块比例,点击不再依赖先画过图块。
- 2026-10-09(用户报:ElyPDF 改字报“这种字体画不出 V”):文档自带的子集字体常无可用的 Unicode 表(LibreOffice 导出的子集表为占位,0x00 起逐一对应字形号),按 Unicode 找字形一律失败。现以“文档自带字体”写字时直接用页面上的那个字体对象(`shim/ely_pen.c`;`pdf_load_font` 按对象缓存,与文字层的字体同一对象),以它自己的编码写:按文字提取的读法(ToUnicode 或 cid_to_ucs)倒出 Unicode 到编码的表,排版取它的宽度(`pdf_lookup_hmtx`),原有的 ToUnicode 与宽度不动,两个字共用一个字形时各写各的编码(原先按字形号写、重建 ToUnicode,共用字形的字被改写,且 CMap 漏写条目数,PDFKit 读不出字)。删去原文字块后页面资源里可能已无该字体,故按对象号以新名字重新引用。只支持编码即 CID 的字体(简单字体与 Identity-H),其余拒绝并说明;页面上没有的字仍报缺字。排版按同一套宽度量出的框,比较时留 0.01 点的余量(原块宽度即这些宽度之和,浮点误差曾使末字折行)。
- 2026-10-09:ElyTable 的自动筛选(R2.9)。内核每次打开筛选列表都把未命名的筛选区向下扩到紧邻的数据行(为接上后来追加的行),分不出选区时就在下方的合计行,合计因此进了列表、随数据一起被隐藏。补丁 `0040-lok-calc-autofilter-keeps-a-marked-block.patch`:LOK 下对跨行的选区开自动筛选时,筛选区记下“选定的行”(不存入文件),此后不再扩展;单格开筛选照旧取整个数据区。已知限制:存后重开的文件不带此记号,其筛选区照内核原样扩展。
- 2026-10-09:ElyTable 的自动筛选(R2.9)。内核会重新推算未命名的自动筛选区:每次打开筛选列表时向下扩到紧邻的数据行(`ExtendDataArea`、`ExtendBackColorArea`,为接上后来追加的行),命令从单格找到它时按连续数据区重算(`ScDocShell::GetDBData`:应用条件、关闭筛选及其撤销);选区下方紧邻的合计行因此进了列表、随数据一起被隐藏,存后重开的筛选区同样会扩。补丁 `0040-lok-calc-autofilter-keeps-its-rows.patch`:LOK 下已有的自动筛选区保持设定时的行(同 Excel),不再隐式扩展;单格开筛选时仍取整个数据区。代价:紧贴在筛选区下方追加的行不自动并入,需重新选定后再开筛选。
+68 -9
View File
@@ -1,4 +1,4 @@
//! An AutoFilter keeps to the marked block (TABLE-08).
//! An AutoFilter range keeps its rows (TABLE-08).
use super::sheet::open_book;
use super::*;
@@ -18,14 +18,33 @@ const BLOCK: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
</table:table></office:spreadsheet></office:body>
</office:document>"#;
/// The filter entries after an AutoFilter on `marked`.
fn entries_after(marked: &str) -> Vec<String> {
let (_dir, mut worker, doc) = open_book(BLOCK);
/// Column A's filter entries.
fn entries(worker: &mut Worker, doc: &Doc) -> Vec<String> {
let query = sheet::cell_query(sheet::FILTER, 0, 0);
let json = worker.values(doc, &query).expect("the entries");
sheet::filter_values(&json).expect("parses").entries.into_iter().map(|e| e.text).collect()
}
/// A book with an AutoFilter set on `marked`.
fn filtered(marked: &str) -> (tempfile::TempDir, Worker, Doc) {
let (dir, mut worker, doc) = open_book(BLOCK);
worker.run(&doc, "cell.goto", &[marked]).expect("marks the cells");
worker.run(&doc, "data.autofilter", &[]).expect("filters");
let query = sheet::cell_query(sheet::FILTER, 0, 0);
let json = worker.values(&doc, &query).expect("the entries");
sheet::filter_values(&json).expect("parses").entries.into_iter().map(|e| e.text).collect()
(dir, worker, doc)
}
/// Keeps 1: the saved range, and whether the total hides.
fn keep_one(worker: &mut Worker, doc: &Doc, dir: &std::path::Path) -> (String, bool) {
worker.run(doc, "cell.goto", &["A1"]).expect("A1");
let one = r#"{"column":0,"conditions":[{"op":"values","values":["1"]}]}"#;
worker.run(doc, "data.filter", &[one]).expect("keeps 1");
let out = dir.join("kept.fods");
doc.save_as(worker, &crate::file_url(&out), "fods").expect("saves");
let xml = std::fs::read_to_string(&out).expect("the saved book");
let range = xml.split("table:target-range-address=\"").nth(1).expect("a filter range");
let total = &xml[..xml.find("office:value=\"6\"").expect("the total")];
let row = &total[total.rfind("<table:table-row").expect("its row")..];
(range[..range.find('"').expect("closed")].to_string(), row.contains("table:visibility=\"filter\""))
}
#[test]
@@ -34,7 +53,9 @@ fn an_autofilter_on_a_marked_block_leaves_the_row_below_out() {
eprintln!("SKIP: no pinned LOK build");
return;
}
assert_eq!(entries_after("A1:A4"), ["1", "2", "3"], "the total below stays out");
let (dir, mut worker, doc) = filtered("A1:A4");
assert_eq!(entries(&mut worker, &doc), ["1", "2", "3"], "the total below stays out");
assert_eq!(keep_one(&mut worker, &doc, dir.path()), ("S.A1:S.A4".into(), false), "applied from one cell");
}
#[test]
@@ -43,5 +64,43 @@ fn an_autofilter_on_one_cell_takes_the_data_area() {
eprintln!("SKIP: no pinned LOK build");
return;
}
assert_eq!(entries_after("A1"), ["1", "2", "3", "6"], "the whole column of data");
let (_dir, mut worker, doc) = filtered("A1");
assert_eq!(entries(&mut worker, &doc), ["1", "2", "3", "6"], "the whole column of data");
}
#[test]
fn off_and_undone_or_set_on_a_filtered_block_the_rows_stay() {
if install().is_none() {
eprintln!("SKIP: no pinned LOK build");
return;
}
let (_dir, mut worker, doc) = filtered("A1:A4");
worker.run(&doc, "cell.goto", &["A1"]).expect("A1");
worker.run(&doc, "data.autofilter", &[]).expect("off");
worker.run(&doc, "edit.undo", &[]).expect("back on");
assert_eq!(entries(&mut worker, &doc), ["1", "2", "3"], "undo keeps the rows");
let (_dir, mut worker, doc) = open_book(BLOCK);
worker.run(&doc, "cell.goto", &["A1:A4"]).expect("marks the block");
let one = r#"{"conditions":[{"field":0,"op":"equals","value":"1"}]}"#;
worker.run(&doc, "data.filter", &[one]).expect("a standard filter");
worker.run(&doc, "cell.goto", &["A1:A4"]).expect("marks it again");
worker.run(&doc, "data.autofilter", &[]).expect("filters");
assert_eq!(entries(&mut worker, &doc), ["1", "2", "3"], "a filtered block keeps its rows");
}
#[test]
fn a_saved_autofilter_range_keeps_its_rows_when_reopened() {
if install().is_none() {
eprintln!("SKIP: no pinned LOK build");
return;
}
for format in ["xlsx", "ods"] {
let (dir, mut worker, doc) = filtered("A1:A4");
let path = dir.path().join(format!("book.{format}"));
doc.save_as(&mut worker, &crate::file_url(&path), format).expect("saves");
worker.close_doc(doc).expect("closes");
let doc = worker.load(&crate::file_url(&path)).expect("reopens");
assert_eq!(entries(&mut worker, &doc), ["1", "2", "3"], "{format}");
assert_eq!(keep_one(&mut worker, &doc, dir.path()), ("S.A1:S.A4".into(), false), "{format}");
}
}
@@ -1,110 +0,0 @@
LOK Calc: an AutoFilter on marked rows keeps to those rows
Each time the filter list opens, GetFilterEntries grows an unnamed
filter range to the data rows right under it (ExtendDataArea and
ExtendBackColorArea), so appended rows join. It cannot tell them from
a totals row that was there when the user marked the block: the
total joined the filter, showed in its list and hid with the data.
Under LOK, an AutoFilter set on a marked block that spans rows keeps
that block: ScDBData remembers it (not saved) and never extends.
diff --git a/sc/inc/dbdata.hxx b/sc/inc/dbdata.hxx
index 6d80216b4..54515b138 100644
--- a/sc/inc/dbdata.hxx
+++ b/sc/inc/dbdata.hxx
@@ -119,6 +119,7 @@ private:
sal_uInt16 nIndex; ///< unique index formulas
bool bAutoFilter; ///< AutoFilter? (not saved)
+ bool bMarkedRows; ///< rows were marked; never extend (not saved)
bool bModified; ///< is set/cleared for/by(?) UpdateReference
::std::vector< OUString > maTableColumnNames; ///< names of table columns
@@ -244,6 +245,9 @@ public:
bool HasAutoFilter() const { return bAutoFilter; }
void SetAutoFilter(bool bSet) { bAutoFilter = bSet; }
+ bool HasMarkedRows() const { return bMarkedRows; }
+ void SetMarkedRows(bool bSet) { bMarkedRows = bSet; }
+
bool IsModified() const { return bModified; }
void SetModified(bool bMod) { bModified = bMod; }
diff --git a/sc/source/core/tool/dbdata.cxx b/sc/source/core/tool/dbdata.cxx
index b1d714c55..e7c5a16c1 100644
--- a/sc/source/core/tool/dbdata.cxx
+++ b/sc/source/core/tool/dbdata.cxx
@@ -75,6 +75,7 @@ ScDBData::ScDBData( const OUString& rName,
bDBSelection(false),
nIndex (0),
bAutoFilter (false),
+ bMarkedRows (false),
bModified (false),
mbTableColumnNamesDirty(true),
nFilteredRowCount(SCSIZE_MAX)
@@ -110,6 +111,7 @@ ScDBData::ScDBData( const ScDBData& rData ) :
bDBSelection (rData.bDBSelection),
nIndex (rData.nIndex),
bAutoFilter (rData.bAutoFilter),
+ bMarkedRows (rData.bMarkedRows),
bModified (rData.bModified),
maTableColumnNames (rData.maTableColumnNames),
maTableColumnAttributes(rData.maTableColumnAttributes),
@@ -146,6 +148,7 @@ ScDBData::ScDBData( const OUString& rName, const ScDBData& rData ) :
bDBSelection (rData.bDBSelection),
nIndex (rData.nIndex),
bAutoFilter (rData.bAutoFilter),
+ bMarkedRows (rData.bMarkedRows),
bModified (rData.bModified),
maTableColumnNames (rData.maTableColumnNames),
maTableColumnAttributes(rData.maTableColumnAttributes),
@@ -192,6 +195,7 @@ ScDBData& ScDBData::operator= (const ScDBData& rData)
bDBSelection = rData.bDBSelection;
nIndex = rData.nIndex;
bAutoFilter = rData.bAutoFilter;
+ bMarkedRows = rData.bMarkedRows;
nFilteredRowCount = rData.nFilteredRowCount;
if (bHeaderRangeDiffers)
@@ -646,6 +650,8 @@ bool ScDBData::UpdateReference(const ScDocument& rDoc, UpdateRefMode eUpdateRefM
void ScDBData::ExtendDataArea(const ScDocument& rDoc)
{
+ if (bMarkedRows)
+ return;
// Extend the DB area to include data rows immediately below.
SCCOL nOldCol1 = nStartCol, nOldCol2 = nEndCol;
SCROW nOldEndRow = nEndRow;
@@ -665,6 +671,8 @@ void ScDBData::ExtendDataArea(const ScDocument& rDoc)
void ScDBData::ExtendBackColorArea(const ScDocument& rDoc)
{
+ if (bMarkedRows)
+ return;
// Extend the DB area to include data rows immediately below.
SCCOL nOldCol1 = nStartCol, nOldCol2 = nEndCol;
SCROW nOldEndRow = nEndRow;
diff --git a/sc/source/ui/view/dbfunc.cxx b/sc/source/ui/view/dbfunc.cxx
index db3d23d86..8140ce4d0 100644
--- a/sc/source/ui/view/dbfunc.cxx
+++ b/sc/source/ui/view/dbfunc.cxx
@@ -18,6 +18,7 @@
*/
#include <scitems.hxx>
+#include <comphelper/lok.hxx>
#include <sfx2/bindings.hxx>
#include <vcl/svapp.hxx>
#include <vcl/weld.hxx>
@@ -286,6 +287,11 @@ void ScDBFunc::ToggleAutoFilter()
return;
pDBData->SetByRow( true ); //! undo, retrieve beforehand ??
+ // LOK: marked rows are the range, as in Excel.
+ ScRange aMarked;
+ pDBData->SetMarkedRows(comphelper::LibreOfficeKit::isActive()
+ && rViewData.GetSimpleArea(aMarked) == SC_MARK_SIMPLE
+ && aMarked.aStart.Row() != aMarked.aEnd.Row());
pDBData->GetQueryParam( aParam );
SCCOL nCol;
@@ -0,0 +1,65 @@
LOK Calc: an AutoFilter range keeps its rows
Calc regrows an unnamed AutoFilter range from the data around it: each
time the filter list opens (ExtendDataArea, ExtendBackColorArea, so
appended rows join) and whenever a command finds it from a single cell
(ScDocShell::GetDBData, so applying a condition, toggling it off, and
its undo). A totals row right under a marked block joined the filter,
showed in its list and hid with the data; a saved range reopened grew
the same way. Under LOK the range keeps the rows it was set on, as in
Excel. A single cell still takes the data area when the filter is set.
diff --git a/sc/source/core/tool/dbdata.cxx b/sc/source/core/tool/dbdata.cxx
index b1d714c55..9a01847ee 100644
--- a/sc/source/core/tool/dbdata.cxx
+++ b/sc/source/core/tool/dbdata.cxx
@@ -19,6 +19,7 @@
#include <sal/config.h>
#include <sal/log.hxx>
+#include <comphelper/lok.hxx>
#include <o3tl/safeint.hxx>
#include <unotools/transliterationwrapper.hxx>
@@ -646,6 +647,9 @@ bool ScDBData::UpdateReference(const ScDocument& rDoc, UpdateRefMode eUpdateRefM
void ScDBData::ExtendDataArea(const ScDocument& rDoc)
{
+ // LOK: an AutoFilter range keeps its rows, as in Excel.
+ if (comphelper::LibreOfficeKit::isActive() && bAutoFilter)
+ return;
// Extend the DB area to include data rows immediately below.
SCCOL nOldCol1 = nStartCol, nOldCol2 = nEndCol;
SCROW nOldEndRow = nEndRow;
@@ -665,6 +669,8 @@ void ScDBData::ExtendDataArea(const ScDocument& rDoc)
void ScDBData::ExtendBackColorArea(const ScDocument& rDoc)
{
+ if (comphelper::LibreOfficeKit::isActive() && bAutoFilter)
+ return;
// Extend the DB area to include data rows immediately below.
SCCOL nOldCol1 = nStartCol, nOldCol2 = nEndCol;
SCROW nOldEndRow = nEndRow;
diff --git a/sc/source/ui/docshell/docsh5.cxx b/sc/source/ui/docshell/docsh5.cxx
index d154597c3..54a2344af 100644
--- a/sc/source/ui/docshell/docsh5.cxx
+++ b/sc/source/ui/docshell/docsh5.cxx
@@ -19,6 +19,7 @@
#include <memory>
#include <sal/config.h>
+#include <comphelper/lok.hxx>
#include <cassert>
@@ -147,7 +148,9 @@ ScDBData* ScDocShell::GetDBData( const ScRange& rMarked, ScGetDBMode eMode, ScGe
if (!bSelected)
{
bUseThis = true;
- if ( bIsNoName && (eMode == SC_DB_MAKE || eMode == SC_DB_AUTOFILTER) )
+ // LOK: an AutoFilter range keeps its rows, as in Excel.
+ const bool bKeep = comphelper::LibreOfficeKit::isActive() && pData->HasAutoFilter();
+ if ( bIsNoName && !bKeep && (eMode == SC_DB_MAKE || eMode == SC_DB_AUTOFILTER) )
{
// If nothing marked or only one row marked, adapt
// "unnamed" to contiguous area.
+7
View File
@@ -0,0 +1,7 @@
FAIL
patches/libreoffice/0040-lok-calc-autofilter-keeps-a-marked-block.patch:73:固定选区只拦截 ExtendDataArea / ExtendBackColorArea,应用筛选条件仍可通过 ScDocShell::GetDBData 扩区。复现:使用 filter.rs 的 BLOCK,选择 A1:A4 开自动筛选,再点选 A1;读取 AutoFilterValues 得到 ["1", "2", "3"],随后执行 data.filter,参数为 {"column":0,"conditions":[{"op":"values","values":["1"]}]}。导出 ODS 的 database-range 已为 S.A1:S.A5,值为 6 的合计行带 table:visibility="filter"。GetDBData 在 sc/source/ui/docshell/docsh5.cxx:150 对单格重新计算连续数据区,172 行直接 SetArea,绕过新增守卫。同样,单格处关闭自动筛选再撤销,也恢复为包含合计行的范围。修复方向:所有重算已有筛选区的入口都遵守固定边界,切换和撤销保留原范围及其固定状态;补充单格处应用条件、关闭再撤销后的实际行可见性回归。独立 cargo build -p ely-lok --bins --locked、两个 client::tests::filter 测试及八个 client::tests::rules 测试通过,现有测试仅在初始选区仍存在时读取列表,漏掉此失败路径。证据:/tmp/ely-r29-cursor.log、/tmp/ely-r29-confirm.log。
patches/libreoffice/0040-lok-calc-autofilter-keeps-a-marked-block.patch:106:选区判定漏掉 SC_MARK_SIMPLE_FILTERED,已有条件筛选的连续选区无法获得固定边界。复现:同一 BLOCK,选择 A1:A4,执行 data.filter,参数为 {"conditions":[{"field":0,"op":"equals","value":"1"}]};再次选择 A1:A4 并执行 data.autofilter。AutoFilterValues 随即返回 ["1", "2", "3", "6"];应用仅保留 1 的条件后,导出的筛选区为 S.A1:S.A5,合计行 6 被隐藏。GetDBData 已支持 SC_MARK_SIMPLE_FILTERED,新增标记判定却只接受 SC_MARK_SIMPLE。修复方向:对跨行的连续已筛选选区同样记录固定边界,并增加先条件筛选、再开启自动筛选的回归。该场景以独立 worker 重跑两次结果一致,证据:/tmp/ely-r29-confirm.log、/tmp/ely-r29-cursor.log。
patches/libreoffice/0040-lok-calc-autofilter-keeps-a-marked-block.patch:19:固定范围依赖未持久化的 bMarkedRows,XLSX / ODS 保存重开后恢复原缺陷。复现:选择同一 BLOCK 的 A1:A4 开自动筛选,保存并关闭;独立读取存档确认 XLSX 的 autoFilter ref="A1:A4"、ODS 的 target-range-address="S.A1:S.A4"。重开后首次读取筛选列表返回 ["1", "2", "3", "6"];仅保留 1 后导出,范围变成 S.A1:S.A5,合计行带 table:visibility="filter"。两种格式均复现,违反 R2.9 的选区边界及 TABLE-01 / TABLE-13 的保存使用路径;agents.md:266 记录了此限制。修复方向:保存重开后保留或从已保存的筛选范围恢复固定边界,覆盖 XLSX、ODS 往返后的列表和实际隐藏行。证据:/tmp/ely-r29-confirm.log。