From c64b6b94fcc2d9cd7a6ee0531e5f878cd6704fc3 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Fri, 11 Sep 2026 23:12:58 +0800 Subject: [PATCH 1/3] =?UTF-8?q?[1297]=20=E4=BB=BB=E5=8A=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=9A=E5=8F=A6=E5=AD=98=E4=B8=BA=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86=20stem/tmu=20=E4=BA=92=E7=9B=B8=E5=8F=AF=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- devel/1297.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 devel/1297.md diff --git a/devel/1297.md b/devel/1297.md new file mode 100644 index 0000000000..688913da8d --- /dev/null +++ b/devel/1297.md @@ -0,0 +1,55 @@ +# [1297] 另存为对话框:stem/tmu 互相可选 + +## 1 相关文档 +- [1279.md](1279.md) - 另存为引导到继任格式(.ts 默认 .stem,.tm 默认 .tmu),本任务在其过滤器基础上扩展 +- [1131.md](1131.md) - stem 格式的引入 +- CLAUDE.md - 开发规范 + +## 2 任务相关的代码文件 +- src/Plugins/Qt/qt_chooser_widget.cpp - 另存为对话框过滤器列表(本次修改) +- src/Plugins/Qt/qt_chooser_widget.hpp - 另存为决策纯函数声明(本次修改) +- src/Plugins/ImGui/im_chooser_widget.cpp - ImGui 选择器 action_save_as 后缀列表(本次修改) +- tests/Plugins/Qt/qt_chooser_filters_test.cpp - 决策纯函数单元测试(本次修改) + +## 3 需求 + +1279 之后另存为对话框按当前 buffer 格式收窄过滤器: + +- stem buffer(或 .ts 文件)另存为:只有 `STEM files (*.stem)` 与 `TS files (*.ts)`; +- tmu/tm buffer 另存为:只有 `TMU files (*.tmu)` 与 `TM files (*.tm)`。 + +两个方向互不可达。本任务要求: + +- stem 格式的 save_as 还需要有 tmu 格式; +- tmu 格式的 save_as 还需要有 stem 格式。 + +## 4 方案 + +- stem 方向:`chooser_stem_save_as_filters()`(原 `chooser_style_filters` 改名) + 追加第三项 `TMU files (*.tmu)`。默认仍是首个过滤器 stem;用户切到 tmu + 过滤器时,选中后 `defaultSuffix` 由所选过滤器解析为 `tmu`, + `chooser_normalize_suffix` 把最终文件名规范为 `.tmu`(沿用 1279 的既有路径)。 +- tmu 方向:`set_type` 的 `action_save_as` 分支在 `TM files (*.tm)` 后追加 + `STEM files (*.stem)`。默认后缀仍是 `tmu`;用户切到 stem 过滤器并输入无 + 后缀文件名时按 QTBUG-59401 的补全逻辑追加 `.stem`。 +- ImGui 选择器 `action_save_as` 的 extensions 追加 `stem`(桌面端为 stub、 + WASM 另存为不走过滤器,仅保持与 Qt 一致)。 + +保存路径本身无需改动:scheme 侧 `has-faithful-format?` 已同时包含 +`stem`/`tmu`,`save-buffer-as` 按目标文件后缀落盘对应格式。 + +## 5 如何测试 + +### 5.1 单元测试 + +```bash +xmake b qt_chooser_filters_test +TEXMACS_PATH=$(pwd)/TeXmacs xmake r qt_chooser_filters_test +``` + +### 5.2 手动测试 + +1. 打开 `.stem` 文件,File -> Save as:过滤器为 STEM/TS/TMU 三项,默认 STEM; + 切到 TMU 过滤器保存,文件名落为 `.tmu`。 +2. 打开 `.tmu` 文件,File -> Save as:过滤器含 `STEM files (*.stem)`; + 切到 STEM 过滤器输入无后缀文件名保存,落为 `.stem`。 From b2721b9d48eff89c19bee1e36aa610f6c5d25633 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Fri, 11 Sep 2026 23:14:17 +0800 Subject: [PATCH 2/3] =?UTF-8?q?[1297]=20=E5=8F=A6=E5=AD=98=E4=B8=BA?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=20stem/tmu=20=E4=BA=92=E7=9B=B8?= =?UTF-8?q?=E5=8F=AF=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stem 方向:chooser_stem_save_as_filters(原 chooser_style_filters 改名) 追加 TMU files (*.tmu) 过滤器 - tmu 方向:action_save_as 过滤器列表追加 STEM files (*.stem) - ImGui 选择器 action_save_as extensions 追加 stem,与 Qt 保持一致 Co-Authored-By: Claude --- src/Plugins/ImGui/im_chooser_widget.cpp | 2 +- src/Plugins/Qt/qt_chooser_widget.cpp | 18 +++++++++++------- src/Plugins/Qt/qt_chooser_widget.hpp | 7 ++++--- tests/Plugins/Qt/qt_chooser_filters_test.cpp | 9 +++++---- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Plugins/ImGui/im_chooser_widget.cpp b/src/Plugins/ImGui/im_chooser_widget.cpp index fadd0d782e..aa80693d7f 100644 --- a/src/Plugins/ImGui/im_chooser_widget.cpp +++ b/src/Plugins/ImGui/im_chooser_widget.cpp @@ -194,7 +194,7 @@ im_chooser_widget_rep::set_type (const string& _type) { if (_type == "action_save_as") { type= _type; - extensions << string ("tmu"); + extensions << string ("tmu") << string ("stem"); def_ext= "tmu"; return true; } diff --git a/src/Plugins/Qt/qt_chooser_widget.cpp b/src/Plugins/Qt/qt_chooser_widget.cpp index 7e5e67c695..2684cd68d0 100644 --- a/src/Plugins/Qt/qt_chooser_widget.cpp +++ b/src/Plugins/Qt/qt_chooser_widget.cpp @@ -249,6 +249,7 @@ qt_chooser_widget_rep::set_type (const string& _type) { mainNameFilter+= " (*.tmu)"; nameFilters << mainNameFilter; nameFilters << to_qstring (translate ("TM files") * " (*.tm)"); + nameFilters << to_qstring (translate ("STEM files") * " (*.stem)"); defaultSuffix= "tmu"; } else if (_type == "action_include") { @@ -301,9 +302,10 @@ chooser_save_as_target (string& file) { } QStringList -chooser_style_filters () { +chooser_stem_save_as_filters () { return QStringList () << to_qstring (translate ("STEM files") * " (*.stem)") - << to_qstring (translate ("TS files") * " (*.ts)"); + << to_qstring (translate ("TS files") * " (*.ts)") + << to_qstring (translate ("TMU files") * " (*.tmu)"); } QString @@ -322,8 +324,9 @@ chooser_normalize_suffix (const QString& path, const QString& suffix) { */ void qt_chooser_widget_rep::perform_dialog () { - // 另存为引导到继任格式:.ts 默认 .stem(仅提供 stem/ts,任务 1131/1279), - // .tm 默认 .tmu(过滤器保持 tmu/tm);建议文件名同步改为默认后缀,让默认 + // 另存为引导到继任格式:.ts 默认 .stem(提供 stem/ts/tmu,任务 + // 1131/1279/1297), .tm 默认 .tmu(过滤器保持 + // tmu/tm);建议文件名同步改为默认后缀,让默认 // 目标在弹窗时即可见;普通保存仍写回原文件 string save_as_target; // 非空时最终文件名规范为所选过滤器的后缀 if (type == "action_save_as") { @@ -368,10 +371,11 @@ qt_chooser_widget_rep::perform_dialog () { #if (QT_VERSION >= 0x040400) if (type != "directory") { - // .ts/.stem 另存为仅提供 stem/ts 两项,默认 .stem;其余情形(.tm 另存为 - // tmu/tm 默认 tmu;.tmu/草稿保持首项 TMU files (*.tmu)) + // .ts/.stem 另存为提供 stem/ts/tmu 三项,默认 .stem(任务 1297 追加 tmu + // 互转);其余情形(.tm 另存为 tmu/tm/stem 默认 tmu;.tmu/草稿保持首项 + // TMU files (*.tmu)) if (save_as_target == "stem") { - nameFilters = chooser_style_filters (); + nameFilters = chooser_stem_save_as_filters (); defaultSuffix= "stem"; } dialog->setNameFilters (nameFilters); diff --git a/src/Plugins/Qt/qt_chooser_widget.hpp b/src/Plugins/Qt/qt_chooser_widget.hpp index 67776bdc13..1c2bb4d58c 100644 --- a/src/Plugins/Qt/qt_chooser_widget.hpp +++ b/src/Plugins/Qt/qt_chooser_widget.hpp @@ -53,15 +53,16 @@ class qt_chooser_widget_rep : public qt_widget_rep { * @param file 建议文件名,命中 .ts/.tm 时被就地改写为默认后缀 * @return 目标默认后缀("stem"/"tmu"),空串表示非引导场景;非空时最终文件名 * 需按所选过滤器后缀规范(chooser_normalize_suffix),为 "stem" 时 - * 过滤器收窄为 stem/ts 两项(chooser_style_filters) + * 过滤器收窄为 stem/ts/tmu 三项(chooser_stem_save_as_filters) * @note 纯函数,供单测;不触发 scheme 调用 */ string chooser_save_as_target (string& file); -/*! @brief .ts/.stem 样式另存的成对过滤器:stem(默认)在前、ts(兼容)在后 +/*! @brief .ts/.stem 另存的过滤器:stem(默认)在前、ts(兼容)居中、 + * tmu(互转,任务 1297)在后 * @note 纯函数,供单测;translate 在未加载字典时返回原文,不依赖 scheme */ -QStringList chooser_style_filters (); +QStringList chooser_stem_save_as_filters (); /*! @brief 从名称过滤器的括号内容解析首个后缀(如 "TS files (*.ts)" → "ts"), * 无括号或无 `*.xxx` 模式时返回空串 diff --git a/tests/Plugins/Qt/qt_chooser_filters_test.cpp b/tests/Plugins/Qt/qt_chooser_filters_test.cpp index a2abdbc79b..3a4d1e1b8d 100644 --- a/tests/Plugins/Qt/qt_chooser_filters_test.cpp +++ b/tests/Plugins/Qt/qt_chooser_filters_test.cpp @@ -59,14 +59,15 @@ private slots: QVERIFY (chooser_save_as_target (tp) == ""); } - // 样式另存的成对过滤器:stem(默认)在前,ts 在后,无其他格式 + // stem 另存的过滤器:stem(默认)在前,ts 居中,tmu(1297 互转)在后 void test_style_filters () { - QStringList filters= chooser_style_filters (); - QCOMPARE (filters.size (), 2); + QStringList filters= chooser_stem_save_as_filters (); + QCOMPARE (filters.size (), 3); QVERIFY (filters[0].contains ("*.stem")); QVERIFY (filters[0].contains ("STEM")); QVERIFY (!filters[0].contains ("*.tmu")); - QVERIFY (filters[1].contains ("*.ts")); + QVERIFY (filters[1].contains ("TS files")); + QVERIFY (filters[2].contains ("*.tmu")); } // 从过滤器括号内容解析首个后缀;非法形状返回空串 From 69ebda3ab1f4cac40fa0312b667b60b529a84ed9 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Fri, 11 Sep 2026 23:22:18 +0800 Subject: [PATCH 3/3] =?UTF-8?q?[1297]=20simplify=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=EF=BC=9Atmu=20=E6=96=B9=E5=90=91=E5=AF=B9=E7=A7=B0=E8=B5=B0=20?= =?UTF-8?q?normalize=20=E9=92=A9=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chooser_save_as_target 覆盖 .tmu(返回 tmu 不改写建议名),tmu buffer 切 STEM/TM 过滤器时经 chooser_normalize_suffix 规范最终文件名, 消除切过滤器不改名静默存原格式及 foo.tmu.tm 双后缀 - 修正 perform_dialog 头部陈旧注释(clang-format 断行 + 仍写 tmu/tm) - test_style_filters 改名 test_stem_save_as_filters,新增 test_rewrite_tmu - ImGui extensions 加注释说明 parity 性质(暂无消费者) Co-Authored-By: Claude --- devel/1297.md | 11 +++++++---- src/Plugins/ImGui/im_chooser_widget.cpp | 2 ++ src/Plugins/Qt/qt_chooser_widget.cpp | 15 +++++++++------ src/Plugins/Qt/qt_chooser_widget.hpp | 7 ++++--- tests/Plugins/Qt/qt_chooser_filters_test.cpp | 13 +++++++++---- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/devel/1297.md b/devel/1297.md index 688913da8d..f0b6ae50e8 100644 --- a/devel/1297.md +++ b/devel/1297.md @@ -30,8 +30,11 @@ 过滤器时,选中后 `defaultSuffix` 由所选过滤器解析为 `tmu`, `chooser_normalize_suffix` 把最终文件名规范为 `.tmu`(沿用 1279 的既有路径)。 - tmu 方向:`set_type` 的 `action_save_as` 分支在 `TM files (*.tm)` 后追加 - `STEM files (*.stem)`。默认后缀仍是 `tmu`;用户切到 stem 过滤器并输入无 - 后缀文件名时按 QTBUG-59401 的补全逻辑追加 `.stem`。 + `STEM files (*.stem)`;`chooser_save_as_target` 覆盖 `.tmu`(返回 "tmu", + 不改写建议名),与 stem 方向对称地经 `chooser_normalize_suffix` 把最终 + 文件名规范为所选过滤器的后缀——切到 STEM 过滤器即使不改建议名也落 + `.stem`,切到 TM 过滤器落 `.tm`(顺带消除原有的 `foo.tmu.tm` 双后缀)。 + 默认后缀仍是 `tmu`。 - ImGui 选择器 `action_save_as` 的 extensions 追加 `stem`(桌面端为 stub、 WASM 另存为不走过滤器,仅保持与 Qt 一致)。 @@ -51,5 +54,5 @@ TEXMACS_PATH=$(pwd)/TeXmacs xmake r qt_chooser_filters_test 1. 打开 `.stem` 文件,File -> Save as:过滤器为 STEM/TS/TMU 三项,默认 STEM; 切到 TMU 过滤器保存,文件名落为 `.tmu`。 -2. 打开 `.tmu` 文件,File -> Save as:过滤器含 `STEM files (*.stem)`; - 切到 STEM 过滤器输入无后缀文件名保存,落为 `.stem`。 +2. 打开 `.tmu` 文件,File -> Save as:过滤器为 TMU/TM/STEM 三项,默认 TMU; + 切到 STEM 过滤器保存(不改建议文件名),落为 `.stem`。 diff --git a/src/Plugins/ImGui/im_chooser_widget.cpp b/src/Plugins/ImGui/im_chooser_widget.cpp index aa80693d7f..72133a3d2e 100644 --- a/src/Plugins/ImGui/im_chooser_widget.cpp +++ b/src/Plugins/ImGui/im_chooser_widget.cpp @@ -194,6 +194,8 @@ im_chooser_widget_rep::set_type (const string& _type) { if (_type == "action_save_as") { type= _type; + // 与 Qt 的过滤器列表保持 parity;桌面端对话框为 stub、WASM 另存为不读 + // extensions,该列表暂无消费者 extensions << string ("tmu") << string ("stem"); def_ext= "tmu"; return true; diff --git a/src/Plugins/Qt/qt_chooser_widget.cpp b/src/Plugins/Qt/qt_chooser_widget.cpp index 2684cd68d0..2246944d7c 100644 --- a/src/Plugins/Qt/qt_chooser_widget.cpp +++ b/src/Plugins/Qt/qt_chooser_widget.cpp @@ -298,6 +298,7 @@ chooser_save_as_target (string& file) { file= remove_suffix (file, ".tm") * ".tmu"; return "tmu"; } + if (ends (file, ".tmu")) return "tmu"; return ""; } @@ -324,17 +325,19 @@ chooser_normalize_suffix (const QString& path, const QString& suffix) { */ void qt_chooser_widget_rep::perform_dialog () { - // 另存为引导到继任格式:.ts 默认 .stem(提供 stem/ts/tmu,任务 - // 1131/1279/1297), .tm 默认 .tmu(过滤器保持 - // tmu/tm);建议文件名同步改为默认后缀,让默认 - // 目标在弹窗时即可见;普通保存仍写回原文件 + // 另存为引导到继任格式:.ts 默认 .stem(过滤器 stem/ts/tmu),.tm 默认 + // .tmu(过滤器 tmu/tm/stem)(任务 1131/1279/1297);建议文件名同步改为 + // 默认后缀,让默认目标在弹窗时即可见;普通保存仍写回原文件 string save_as_target; // 非空时最终文件名规范为所选过滤器的后缀 if (type == "action_save_as") { save_as_target= chooser_save_as_target (file); if (save_as_target == "") { url cur_buf= get_current_buffer_safe (); - if (!is_none (cur_buf) && suffix (cur_buf) == "stem") { - save_as_target= "stem"; + if (!is_none (cur_buf)) { + string buf_suffix= suffix (cur_buf); + if (buf_suffix == "stem" || buf_suffix == "tmu") { + save_as_target= buf_suffix; + } } } } diff --git a/src/Plugins/Qt/qt_chooser_widget.hpp b/src/Plugins/Qt/qt_chooser_widget.hpp index 1c2bb4d58c..24d1d412de 100644 --- a/src/Plugins/Qt/qt_chooser_widget.hpp +++ b/src/Plugins/Qt/qt_chooser_widget.hpp @@ -49,11 +49,12 @@ class qt_chooser_widget_rep : public qt_widget_rep { }; /*! @brief 另存为建议名改写与目标格式判定:.ts → .stem,.tm → .tmu - * (继任格式引导,任务 1279) + * (继任格式引导,任务 1279);.stem/.tmu 原样保留仅返回目标(1297) * @param file 建议文件名,命中 .ts/.tm 时被就地改写为默认后缀 * @return 目标默认后缀("stem"/"tmu"),空串表示非引导场景;非空时最终文件名 - * 需按所选过滤器后缀规范(chooser_normalize_suffix),为 "stem" 时 - * 过滤器收窄为 stem/ts/tmu 三项(chooser_stem_save_as_filters) + * 需按所选过滤器后缀规范(chooser_normalize_suffix);为 "stem" 时 + * 过滤器替换为 stem/ts/tmu 三项(chooser_stem_save_as_filters), + * 为 "tmu" 时保持 set_type 的 tmu/tm/stem 列表 * @note 纯函数,供单测;不触发 scheme 调用 */ string chooser_save_as_target (string& file); diff --git a/tests/Plugins/Qt/qt_chooser_filters_test.cpp b/tests/Plugins/Qt/qt_chooser_filters_test.cpp index 3a4d1e1b8d..66a118325f 100644 --- a/tests/Plugins/Qt/qt_chooser_filters_test.cpp +++ b/tests/Plugins/Qt/qt_chooser_filters_test.cpp @@ -35,7 +35,7 @@ private slots: QVERIFY (f == "foo.stem"); } - // .tm 另存为改写为 .tmu;.stm/.tmu 不得误伤 + // .tm 另存为改写为 .tmu;.stm 不得误伤 void test_rewrite_tm () { string f= "foo.tm"; QVERIFY (chooser_save_as_target (f) == "tmu"); @@ -43,8 +43,13 @@ private slots: string g= "foo.stm"; QVERIFY (chooser_save_as_target (g) == ""); QVERIFY (g == "foo.stm"); - string h= "foo.tmu"; - QVERIFY (chooser_save_as_target (h) == ""); + } + + // .tmu 本就是默认格式:保持不变但属于引导场景(与 stem 方向对称,1297) + void test_rewrite_tmu () { + string f= "foo.tmu"; + QVERIFY (chooser_save_as_target (f) == "tmu"); + QVERIFY (f == "foo.tmu"); } // 其余后缀与无后缀(草稿)不触发改写 @@ -60,7 +65,7 @@ private slots: } // stem 另存的过滤器:stem(默认)在前,ts 居中,tmu(1297 互转)在后 - void test_style_filters () { + void test_stem_save_as_filters () { QStringList filters= chooser_stem_save_as_filters (); QCOMPARE (filters.size (), 3); QVERIFY (filters[0].contains ("*.stem"));