diff --git a/devel/1297.md b/devel/1297.md new file mode 100644 index 0000000000..f0b6ae50e8 --- /dev/null +++ b/devel/1297.md @@ -0,0 +1,58 @@ +# [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)`;`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 一致)。 + +保存路径本身无需改动: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:过滤器为 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 fadd0d782e..72133a3d2e 100644 --- a/src/Plugins/ImGui/im_chooser_widget.cpp +++ b/src/Plugins/ImGui/im_chooser_widget.cpp @@ -194,7 +194,9 @@ im_chooser_widget_rep::set_type (const string& _type) { if (_type == "action_save_as") { type= _type; - extensions << string ("tmu"); + // 与 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 7e5e67c695..2246944d7c 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") { @@ -297,13 +298,15 @@ chooser_save_as_target (string& file) { file= remove_suffix (file, ".tm") * ".tmu"; return "tmu"; } + if (ends (file, ".tmu")) return "tmu"; return ""; } 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,16 +325,19 @@ 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),.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; + } } } } @@ -368,10 +374,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..24d1d412de 100644 --- a/src/Plugins/Qt/qt_chooser_widget.hpp +++ b/src/Plugins/Qt/qt_chooser_widget.hpp @@ -49,19 +49,21 @@ 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 两项(chooser_style_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); -/*! @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..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"); } // 其余后缀与无后缀(草稿)不触发改写 @@ -59,14 +64,15 @@ private slots: QVERIFY (chooser_save_as_target (tp) == ""); } - // 样式另存的成对过滤器:stem(默认)在前,ts 在后,无其他格式 - void test_style_filters () { - QStringList filters= chooser_style_filters (); - QCOMPARE (filters.size (), 2); + // stem 另存的过滤器:stem(默认)在前,ts 居中,tmu(1297 互转)在后 + void test_stem_save_as_filters () { + 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")); } // 从过滤器括号内容解析首个后缀;非法形状返回空串