From e1804536832a6d6b1802a91941de1f3035b98eac Mon Sep 17 00:00:00 2001 From: pigmagicfly <2831850183@qq.com> Date: Mon, 3 Aug 2026 15:38:13 +0800 Subject: [PATCH 1/8] [2085] add recent document search development notes --- devel/2085.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 devel/2085.md diff --git a/devel/2085.md b/devel/2085.md new file mode 100644 index 0000000000..949e67c9a5 --- /dev/null +++ b/devel/2085.md @@ -0,0 +1,37 @@ +# [2085] 编辑菜单中的最近文档搜索 + +## 相关文档 +- [Issue #4143](https://github.com/MoganLab/mogan/issues/4143) - 采用 QML 重构并修复最近文档搜索。 + +## 任务相关的代码文件 +- `TeXmacs/progs/doc/docgrep.scm` +- `TeXmacs/progs/texmacs/menus/edit-menu.scm` +- `src/Plugins/Qt/QTMQmlDialog.cpp` +- `src/Plugins/Qt/qml/SearchRecentDocuments.qml` + +## 如何测试 +```powershell +xmake build stem +xmake install stem +& '.\build\packages\stem\data\bin\MoganSTEM.exe' +``` + +在“编辑 -> 搜索最近打开的文档”中输入文件名的一部分,确认结果按模糊匹配显示。选中条目并点击“打开”,确认文件被加载。 + +## 2026/08/03 采用 QML 重构最近文档搜索 + +### What +- 用 QML 对话框替换旧的交互式全文 grep 输入。 +- 显示最近文档的文件名和路径,并支持实时模糊筛选与显式选择。 +- 对最近文件路径去重;初始和筛选后均不默认选中,避免重复项同时高亮或误打开。 +- 补充 QML 加载与桥接测试。 +- 在 `ai-docs/qml/qml-dialog.html` 中补充该对话框的交互设计稿。 + +### Why +旧交互式输入在中文输入法确认候选字时会提前执行,且其全文搜索语义不符合按最近文件选择文档的预期。 + +### How +- Scheme 侧提供最近文件列表,并在确认选择后加载对应路径。 +- C++ 将最近文件模型注入 QML,保持 UTF-8 路径编码。 +- C++ 以规范化路径去重,保留最近记录中的第一次出现。 +- QML 按文件名优先、完整路径兜底执行子序列模糊匹配;只有显式选中并点击“打开”才加载选定文件。 From 58b73b970631cff7825b43bef64bb53f3828a889 Mon Sep 17 00:00:00 2001 From: pigmagicfly <2831850183@qq.com> Date: Mon, 3 Aug 2026 15:41:19 +0800 Subject: [PATCH 2/8] [2085] refactor recent document search with QML --- TeXmacs/progs/doc/docgrep.scm | 12 ++ TeXmacs/progs/texmacs/menus/edit-menu.scm | 2 +- ai-docs/qml/qml-dialog.html | 155 ++++++++++++++- src/Plugins/Qt/QTMQmlDialog.cpp | 52 +++++ src/Plugins/Qt/QTMQmlDialog.hpp | 9 + src/Plugins/Qt/moganqml.qrc | 1 + src/Plugins/Qt/qml/SearchRecentDocuments.qml | 194 +++++++++++++++++++ src/Plugins/Qt/qml/qmldir | 1 + src/Scheme/L5/glue_qt.lua | 8 +- tests/Plugins/Qt/qml_dialog_test.cpp | 14 ++ tests/Plugins/Qt/qml_load_test.cpp | 31 +++ 11 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 src/Plugins/Qt/qml/SearchRecentDocuments.qml diff --git a/TeXmacs/progs/doc/docgrep.scm b/TeXmacs/progs/doc/docgrep.scm index 6c568b96ce..076e3186b8 100644 --- a/TeXmacs/progs/doc/docgrep.scm +++ b/TeXmacs/progs/doc/docgrep.scm @@ -247,3 +247,15 @@ (load-document (string-append "tmfs://grep/" query)) ) ;with ) ;tm-define + +(tm-define (recent-documents-for-qml) (map url->system (recent-file-list 25))) + +(tm-define (open-search-recent-documents) + (:interactive #t) + (with path + (cpp-search-recent-documents-dialog) + (when (!= path "") + (load-document (system->url path)) + ) ;when + ) ;with +) ;tm-define diff --git a/TeXmacs/progs/texmacs/menus/edit-menu.scm b/TeXmacs/progs/texmacs/menus/edit-menu.scm index dd7314b5da..1490ba42fe 100644 --- a/TeXmacs/progs/texmacs/menus/edit-menu.scm +++ b/TeXmacs/progs/texmacs/menus/edit-menu.scm @@ -99,7 +99,7 @@ ) ;-> ) ;if --- - ("Search recent documents" (interactive docgrep-in-recent)) + ("Search recent documents" (open-search-recent-documents)) --- (if (use-menus?) (-> "Preferences" (link preferences-menu))) (if (use-popups?) ("Preferences" (open-preferences))) diff --git a/ai-docs/qml/qml-dialog.html b/ai-docs/qml/qml-dialog.html index 926a5d9ec1..bce953ef51 100644 --- a/ai-docs/qml/qml-dialog.html +++ b/ai-docs/qml/qml-dialog.html @@ -299,6 +299,76 @@ box-shadow: var(--shadow); } + .recent-search-shell { + width: min(620px, 100%); + background: #ffffff; + border: 1px solid #d6dfdf; + border-radius: var(--radius); + padding: 22px; + box-shadow: var(--shadow); + } + + .recent-search-shell h3 { + text-align: center; + font-size: 18px; + margin-bottom: 18px; + } + + .recent-search-input { + width: 100%; + height: 38px; + border: 1px solid #9fcac7; + border-radius: 10px; + padding: 0 12px; + background: #f9fcfc; + color: #25353a; + font: inherit; + } + + .recent-results { + list-style: none; + margin-top: 14px; + border: 1px solid #cad7d7; + border-radius: 10px; + overflow: hidden; + } + + .recent-result { + padding: 10px 12px; + border-bottom: 1px solid #e4eeee; + cursor: pointer; + } + + .recent-result:last-child { + border-bottom: 0; + } + + .recent-result.active { + background: #dff3f1; + color: #194f53; + } + + .recent-result strong, + .recent-result span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .recent-result span { + margin-top: 3px; + color: #60787e; + font-size: 12px; + } + + .recent-empty { + display: none; + padding: 18px; + color: var(--muted); + text-align: center; + } + .field { display: grid; grid-template-columns: 120px 1fr; @@ -993,6 +1063,7 @@

QML 组件模拟器

+ @@ -1577,6 +1648,36 @@

切换界面语言

+ @@ -1591,7 +1692,8 @@

切换界面语言

paragraph: document.getElementById('panel-paragraph'), statistics: document.getElementById('panel-statistics'), preferences: document.getElementById('panel-preferences'), - confirmRestart: document.getElementById('panel-confirm-restart') + confirmRestart: document.getElementById('panel-confirm-restart'), + recentSearch: document.getElementById('panel-recent-search') }; const titleMap = { @@ -1601,7 +1703,8 @@

切换界面语言

paragraph: 'ParagraphFormat.qml 模拟', statistics: 'Statistics.qml 模拟', preferences: 'Preferences.qml 模拟', - confirmRestart: 'ConfirmRestart.qml 模拟' + confirmRestart: 'ConfirmRestart.qml 模拟', + recentSearch: 'SearchRecentDocuments.qml 模拟' }; const codeMap = { @@ -1611,7 +1714,8 @@

切换界面语言

paragraph: 'EnumCombo x N + tabs(基础/高级) + make-multi-line-with 写回', statistics: 'statsModel: [{label, value}, ...] + closeBridge.choose(0) 关闭', preferences: 'prefTabs(5) + subTabs(6) + EnumCombo + Toggle + 纯 OK 提交', - confirmRestart: 'message + buttonLabels + closeBridge.choose(index)' + confirmRestart: 'message + buttonLabels + closeBridge.choose(index)', + recentSearch: 'recentDocuments + fuzzyScore() + closeBridge.submit({ path })' }; const viewTitle = document.getElementById('view-title'); @@ -1646,6 +1750,49 @@

切换界面语言

}); }); + const recentSearchInput = document.getElementById('recent-search-input'); + const recentResults = document.querySelectorAll('.recent-result'); + const recentEmpty = document.getElementById('recent-empty'); + let recentSelected = document.querySelector('.recent-result.active'); + + function fuzzyMatch(text, query) { + let cursor = 0; + const haystack = text.toLowerCase(); + for (const character of query.toLowerCase()) { + cursor = haystack.indexOf(character, cursor); + if (cursor < 0) return false; + cursor += 1; + } + return true; + } + + recentSearchInput.addEventListener('input', () => { + const query = recentSearchInput.value.trim(); + let visibleCount = 0; + recentResults.forEach((row) => { + const matched = !query || fuzzyMatch(row.dataset.name, query) || fuzzyMatch(row.dataset.path, query); + row.style.display = matched ? '' : 'none'; + if (matched) visibleCount += 1; + }); + recentEmpty.style.display = visibleCount ? 'none' : 'block'; + const selectedVisible = recentSelected && recentSelected.style.display !== 'none'; + if (!selectedVisible) { + recentSelected = Array.from(recentResults).find((row) => row.style.display !== 'none') || null; + recentResults.forEach((row) => row.classList.toggle('active', row === recentSelected)); + } + }); + + recentResults.forEach((row) => { + row.addEventListener('click', () => { + recentSelected = row; + recentResults.forEach((item) => item.classList.toggle('active', item === row)); + }); + }); + + document.getElementById('recent-open').addEventListener('click', () => { + if (recentSelected) log('SearchRecentDocuments: submit({ path: "' + recentSelected.dataset.path + '" })'); + }); + document.getElementById('form-submit').addEventListener('click', () => { const values = { language: document.getElementById('field-language').value, @@ -1871,4 +2018,4 @@

切换界面语言

})(); - \ No newline at end of file + diff --git a/src/Plugins/Qt/QTMQmlDialog.cpp b/src/Plugins/Qt/QTMQmlDialog.cpp index 566280f30c..d5dd2fc49e 100644 --- a/src/Plugins/Qt/QTMQmlDialog.cpp +++ b/src/Plugins/Qt/QTMQmlDialog.cpp @@ -26,9 +26,12 @@ using moebius::data::scm_unquote; using moebius::data::tree_to_scheme_tree; #include +#include +#include #include #include #include +#include #include #include #include @@ -599,3 +602,52 @@ cpp_preferences_dialog () { delete bridge; return tree (TUPLE); } + +// ---- Search recent documents ------------------------------------------------ + +string +cpp_search_recent_documents_dialog () { + string preset= get_env ("MOGAN_TEST_SEARCH_RECENT_DOCUMENTS"); + if (preset != "") return preset == "cancel" ? string ("") : preset; + + QVariantList recentDocuments; + QSet seenPaths; + tmscm recentPaths= eval_scheme ("(recent-documents-for-qml)"); + for (tmscm cur= recentPaths; !tmscm_is_null (cur); cur= tmscm_cdr (cur)) { + tmscm item= tmscm_car (cur); + if (!tmscm_is_string (item)) continue; + QString path= QString::fromUtf8 (as_charp (tmscm_to_string (item))); + if (path.isEmpty ()) continue; + QString normalizedPath= QDir::cleanPath (QDir::fromNativeSeparators (path)); + if (seenPaths.contains (normalizedPath)) continue; + seenPaths.insert (normalizedPath); + QVariantMap document; + document["path"]= path; + document["name"]= QFileInfo (path).fileName (); + recentDocuments << document; + } + + array buttons= {string ("Open"), string ("Cancel")}; + QmlDialogBridge* bridge = nullptr; + run_qml_dialog ( + "qrc:/qml/SearchRecentDocuments.qml", "SearchRecentDocuments.qml", + [&] (QQuickWidget* qw, QDialog& host) { + bridge= inject_common_context (qw, host); + qw->rootContext ()->setContextProperty ( + "searchDialogTitle", qt_translate ("Search recent documents")); + qw->rootContext ()->setContextProperty ("searchPlaceholder", + qt_translate ("Search")); + qw->rootContext ()->setContextProperty ( + "searchEmptyText", qt_translate ("No matching recent documents")); + qw->rootContext ()->setContextProperty ("recentDocuments", + recentDocuments); + qw->rootContext ()->setContextProperty ("dialogButtons", + translate_buttons (buttons)); + }, + 520, 450); + + QString path; + if (bridge) path= bridge->results ().value ("path").toString (); + delete bridge; + return from_qstring (path); +} diff --git a/src/Plugins/Qt/QTMQmlDialog.hpp b/src/Plugins/Qt/QTMQmlDialog.hpp index 6ac1cda09f..a2b4f8d41f 100644 --- a/src/Plugins/Qt/QTMQmlDialog.hpp +++ b/src/Plugins/Qt/QTMQmlDialog.hpp @@ -204,4 +204,13 @@ void cpp_statistics_dialog (string title, tree items); */ tree cpp_preferences_dialog (); +/** + * @brief Open the QML search dialog for recently opened documents. + * + * @return The selected document path, or an empty string after Cancel, Esc, + * + * window close, or QML loading failure. + */ +string cpp_search_recent_documents_dialog (); + #endif // defined QTM_QML_DIALOG_H diff --git a/src/Plugins/Qt/moganqml.qrc b/src/Plugins/Qt/moganqml.qrc index 6955c83c19..19f1c659a4 100644 --- a/src/Plugins/Qt/moganqml.qrc +++ b/src/Plugins/Qt/moganqml.qrc @@ -21,5 +21,6 @@ qml/ParagraphFormat.qml qml/Statistics.qml qml/Preferences.qml + qml/SearchRecentDocuments.qml diff --git a/src/Plugins/Qt/qml/SearchRecentDocuments.qml b/src/Plugins/Qt/qml/SearchRecentDocuments.qml new file mode 100644 index 0000000000..29218f1ea5 --- /dev/null +++ b/src/Plugins/Qt/qml/SearchRecentDocuments.qml @@ -0,0 +1,194 @@ +import QtQuick +import "atoms" + +DialogShell { + id: root + implicitWidth: 520 + implicitHeight: 450 + implicitMargins: Theme.margin + + property string title: typeof searchDialogTitle !== "undefined" ? searchDialogTitle : "Search recent documents" + property string placeholder: typeof searchPlaceholder !== "undefined" ? searchPlaceholder : "Search" + property string emptyText: typeof searchEmptyText !== "undefined" ? searchEmptyText : "No matching recent documents" + property var documents: typeof recentDocuments !== "undefined" ? recentDocuments : [] + property var buttonLabels: typeof dialogButtons !== "undefined" ? dialogButtons : ["Open", "Cancel"] + property var matches: [] + property int selectedIndex: -1 + + function fuzzyScore(text, query) { + var haystack = text.toLocaleLowerCase() + var needle = query.toLocaleLowerCase() + var cursor = 0 + var score = 0 + for (var i = 0; i < needle.length; ++i) { + var index = haystack.indexOf(needle.charAt(i), cursor) + if (index < 0) + return -1 + score += index - cursor + cursor = index + 1 + } + return score + } + + function updateMatches() { + var query = searchInput.text.trim() + var next = [] + for (var i = 0; i < documents.length; ++i) { + var document = documents[i] + var score = query.length === 0 ? i : fuzzyScore(document.name, query) + if (score < 0 && query.length > 0) + score = fuzzyScore(document.path, query) + if (score >= 0) + next.push({ "name": document.name, "path": document.path, "score": score }) + } + next.sort(function(a, b) { return a.score - b.score }) + matches = next + selectedIndex = -1 + } + + function openSelection() { + if (selectedIndex >= 0 && selectedIndex < matches.length) + closeBridge.submit({ "path": matches[selectedIndex].path }) + } + + Component.onCompleted: updateMatches() + onDocumentsChanged: updateMatches() + + content: Column { + spacing: Theme.gapM + + Text { + width: parent.width + height: Theme.titleH + text: root.title + color: Theme.fg + font.pixelSize: 18 * Theme.scaleFactor + font.weight: Font.Bold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Rectangle { + width: parent.width + height: Theme.rowH + radius: Theme.radius + color: Theme.fieldBg + border.width: Theme.borderW + border.color: searchInput.activeFocus ? Theme.dropdownBorder : Theme.borderClr + + Text { + anchors.fill: parent + anchors.leftMargin: Theme.comboPad + anchors.rightMargin: Theme.comboPad + text: root.placeholder + color: Theme.muted + font.pixelSize: Theme.fontBody + verticalAlignment: Text.AlignVCenter + visible: searchInput.text.length === 0 && !searchInput.activeFocus + elide: Text.ElideRight + } + + TextInput { + id: searchInput + anchors.fill: parent + anchors.leftMargin: Theme.comboPad + anchors.rightMargin: Theme.comboPad + color: Theme.fg + font.pixelSize: Theme.fontBody + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + clip: true + focus: true + onTextChanged: root.updateMatches() + Component.onCompleted: forceActiveFocus() + } + } + + Rectangle { + width: parent.width + height: 230 * Theme.scaleFactor + radius: Theme.radius + color: Theme.listBg + border.width: Theme.borderW + border.color: Theme.borderClr + + Rectangle { + anchors.fill: parent + anchors.margins: parent.border.width + radius: Math.max(0, parent.radius - parent.border.width) + color: parent.color + clip: true + + ListView { + id: resultList + anchors.fill: parent + anchors.margins: Theme.padS + clip: true + boundsBehavior: Flickable.StopAtBounds + model: root.matches + + delegate: Rectangle { + width: resultList.width + height: 48 * Theme.scaleFactor + radius: Theme.radius + color: root.selectedIndex === index ? Theme.selectBg : (resultMouse.containsMouse ? Theme.fieldBgHover : "transparent") + border.width: root.selectedIndex === index ? Theme.borderW : 0 + border.color: Theme.selectBorder + + Column { + anchors.fill: parent + anchors.leftMargin: Theme.comboPad + anchors.rightMargin: Theme.comboPad + spacing: Theme.padS + + Text { + width: parent.width + text: modelData.name + color: root.selectedIndex === index ? Theme.selectFg : Theme.fg + font.pixelSize: Theme.fontBody + font.weight: Font.DemiBold + elide: Text.ElideRight + } + + Text { + width: parent.width + text: modelData.path + color: Theme.muted + font.pixelSize: Theme.fontTiny + elide: Text.ElideMiddle + } + } + + MouseArea { + id: resultMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.selectedIndex = index + } + } + } + + Text { + anchors.centerIn: parent + visible: root.matches.length === 0 + text: root.emptyText + color: Theme.muted + font.pixelSize: Theme.fontBody + } + } + } + + DialogButtons { + anchors.horizontalCenter: parent.horizontalCenter + buttonLabels: root.buttonLabels + onClicked: function(index) { + if (index === 0) + root.openSelection() + else + closeBridge.cancel() + } + } + } +} diff --git a/src/Plugins/Qt/qml/qmldir b/src/Plugins/Qt/qml/qmldir index cbd79391d7..aa1fcc92bb 100644 --- a/src/Plugins/Qt/qml/qmldir +++ b/src/Plugins/Qt/qml/qmldir @@ -5,3 +5,4 @@ FontSelector 1.0 FontSelector.qml ParagraphFormat 1.0 ParagraphFormat.qml Statistics 1.0 Statistics.qml Preferences 1.0 Preferences.qml +SearchRecentDocuments 1.0 SearchRecentDocuments.qml diff --git a/src/Scheme/L5/glue_qt.lua b/src/Scheme/L5/glue_qt.lua index 278e06a0fe..094c7b5119 100644 --- a/src/Scheme/L5/glue_qt.lua +++ b/src/Scheme/L5/glue_qt.lua @@ -89,6 +89,12 @@ function main() ret_type = "tree", arg_list = {} }, + { + scm_name = "cpp-search-recent-documents-dialog", + cpp_name = "cpp_search_recent_documents_dialog", + ret_type = "string", + arg_list = {} + }, { scm_name = "cpp-rasterize-widget", cpp_name = "cpp_rasterize_widget", @@ -197,4 +203,4 @@ function main() } } } -end \ No newline at end of file +end diff --git a/tests/Plugins/Qt/qml_dialog_test.cpp b/tests/Plugins/Qt/qml_dialog_test.cpp index f5e2a98677..e7cc8bb853 100644 --- a/tests/Plugins/Qt/qml_dialog_test.cpp +++ b/tests/Plugins/Qt/qml_dialog_test.cpp @@ -72,6 +72,7 @@ private slots: void test_translate_buttons (); void test_confirm_close_hook (); void test_form_dialog_hook (); + void test_search_recent_documents_hook (); }; // 字段下标协议常量与协议文档一致。 @@ -245,6 +246,19 @@ TestQmlDialog::test_form_dialog_hook () { } } +void +TestQmlDialog::test_search_recent_documents_hook () { + { + EnvHook hook ("MOGAN_TEST_SEARCH_RECENT_DOCUMENTS", "C:/docs/needle.tm"); + QCOMPARE (cpp_search_recent_documents_dialog (), + string ("C:/docs/needle.tm")); + } + { + EnvHook hook ("MOGAN_TEST_SEARCH_RECENT_DOCUMENTS", "cancel"); + QCOMPARE (cpp_search_recent_documents_dialog (), string ("")); + } +} + #ifdef QTTEXMACS QTEST_MAIN (TestQmlDialog) #else diff --git a/tests/Plugins/Qt/qml_load_test.cpp b/tests/Plugins/Qt/qml_load_test.cpp index e2c8533464..78fa105479 100644 --- a/tests/Plugins/Qt/qml_load_test.cpp +++ b/tests/Plugins/Qt/qml_load_test.cpp @@ -185,6 +185,7 @@ private slots: void test_paragraph_format_loads (); void test_preferences_loads (); void test_statistics_loads (); + void test_search_recent_documents_loads (); }; // 共用:构造带 closeBridge/dpScale/isDark 的 QQuickWidget,加载给定 qrc url。 @@ -334,6 +335,36 @@ TestQmlLoad::test_statistics_loads () { QCOMPARE (qw->status (), QQuickWidget::Ready); } +void +TestQmlLoad::test_search_recent_documents_loads () { + QDialog host; + QQuickWidget* qw= new QQuickWidget (&host); + qw->setResizeMode (QQuickWidget::SizeRootObjectToView); + StubBridge* bridge= new StubBridge (qw); + qw->rootContext ()->setContextProperty ("closeBridge", bridge); + qw->rootContext ()->setContextProperty ("dpScale", 1.0); + qw->rootContext ()->setContextProperty ("isDark", false); + qw->rootContext ()->setContextProperty ("searchDialogTitle", + QString ("Search recent documents")); + qw->rootContext ()->setContextProperty ("searchPlaceholder", + QString ("Search")); + qw->rootContext ()->setContextProperty ( + "searchEmptyText", QString ("No matching recent documents")); + QVariantMap document; + document["name"]= QString ("notes.tm"); + document["path"]= QString ("C:/docs/notes.tm"); + QVariantList documents; + documents << document; + qw->rootContext ()->setContextProperty ("recentDocuments", documents); + QStringList buttons; + buttons << "Open" + << "Cancel"; + qw->rootContext ()->setContextProperty ("dialogButtons", buttons); + qw->setSource (QUrl ("qrc:/qml/SearchRecentDocuments.qml")); + QCOMPARE (qw->status (), QQuickWidget::Ready); + QCOMPARE (qw->rootObject ()->property ("selectedIndex").toInt (), -1); +} + #ifdef QTTEXMACS QTEST_MAIN (TestQmlLoad) #else From 5203290875d2a8380d8b98e1a8076a75f50d1c35 Mon Sep 17 00:00:00 2001 From: pigmagicfly <2831850183@qq.com> Date: Mon, 3 Aug 2026 16:36:37 +0800 Subject: [PATCH 3/8] [2085] isolate recent document search dialog --- devel/2085.md | 4 +- src/Plugins/Qt/QTMQmlDialog.cpp | 56 +------------- src/Plugins/Qt/QTMQmlDialog.hpp | 10 +-- src/Plugins/Qt/QTMQmlDialogInternal.hpp | 14 ++++ .../Qt/RecentDocumentsSearchDialog.cpp | 73 +++++++++++++++++++ .../Qt/RecentDocumentsSearchDialog.hpp | 23 ++++++ 6 files changed, 116 insertions(+), 64 deletions(-) create mode 100644 src/Plugins/Qt/RecentDocumentsSearchDialog.cpp create mode 100644 src/Plugins/Qt/RecentDocumentsSearchDialog.hpp diff --git a/devel/2085.md b/devel/2085.md index 949e67c9a5..067d4b21fa 100644 --- a/devel/2085.md +++ b/devel/2085.md @@ -6,7 +6,8 @@ ## 任务相关的代码文件 - `TeXmacs/progs/doc/docgrep.scm` - `TeXmacs/progs/texmacs/menus/edit-menu.scm` -- `src/Plugins/Qt/QTMQmlDialog.cpp` +- `src/Plugins/Qt/RecentDocumentsSearchDialog.hpp` +- `src/Plugins/Qt/RecentDocumentsSearchDialog.cpp` - `src/Plugins/Qt/qml/SearchRecentDocuments.qml` ## 如何测试 @@ -35,3 +36,4 @@ xmake install stem - C++ 将最近文件模型注入 QML,保持 UTF-8 路径编码。 - C++ 以规范化路径去重,保留最近记录中的第一次出现。 - QML 按文件名优先、完整路径兜底执行子序列模糊匹配;只有显式选中并点击“打开”才加载选定文件。 +- 最近文档搜索的路径和模型逻辑独立于 QML 通用对话框底座,复用其内部模态引擎和 context 注入接口。 diff --git a/src/Plugins/Qt/QTMQmlDialog.cpp b/src/Plugins/Qt/QTMQmlDialog.cpp index 4109f05654..ed7c67f6a3 100644 --- a/src/Plugins/Qt/QTMQmlDialog.cpp +++ b/src/Plugins/Qt/QTMQmlDialog.cpp @@ -27,12 +27,9 @@ using moebius::data::scm_unquote; using moebius::data::tree_to_scheme_tree; #include -#include -#include #include #include #include -#include #include #include #include @@ -126,7 +123,7 @@ setup_frameless_qml_host (QDialog& d) { * QObject parent,不会被宿主 QDialog 析构带走,以便 form 型 exec 后取 * results())。 */ -static QmlDialogBridge* +QmlDialogBridge* inject_common_context (QQuickWidget* qw, QDialog& host) { bool isDark= occurs ("dark", tm_style_sheet) || occurs ("liii-night", tm_style_sheet); @@ -172,7 +169,7 @@ lock_fixed_size (QQuickWidget* qw, QVBoxLayout* vl, QDialog& d, int logic_w, * QML 加载失败返回 -1。调用方据此映射结果(确认型 → 按钮下标;form 型 * → Accepted/Rejected,表单值另行从 bridge->results() 取)。 */ -static int +int run_qml_dialog (const string& qml_url, const char* debug_tag, std::function inject_context, int logic_w, int logic_h) { @@ -641,52 +638,3 @@ cpp_preferences_dialog () { delete bridge; return tree (TUPLE); } - -// ---- Search recent documents ------------------------------------------------ - -string -cpp_search_recent_documents_dialog () { - string preset= get_env ("MOGAN_TEST_SEARCH_RECENT_DOCUMENTS"); - if (preset != "") return preset == "cancel" ? string ("") : preset; - - QVariantList recentDocuments; - QSet seenPaths; - tmscm recentPaths= eval_scheme ("(recent-documents-for-qml)"); - for (tmscm cur= recentPaths; !tmscm_is_null (cur); cur= tmscm_cdr (cur)) { - tmscm item= tmscm_car (cur); - if (!tmscm_is_string (item)) continue; - QString path= QString::fromUtf8 (as_charp (tmscm_to_string (item))); - if (path.isEmpty ()) continue; - QString normalizedPath= QDir::cleanPath (QDir::fromNativeSeparators (path)); - if (seenPaths.contains (normalizedPath)) continue; - seenPaths.insert (normalizedPath); - QVariantMap document; - document["path"]= path; - document["name"]= QFileInfo (path).fileName (); - recentDocuments << document; - } - - array buttons= {string ("Open"), string ("Cancel")}; - QmlDialogBridge* bridge = nullptr; - run_qml_dialog ( - "qrc:/qml/SearchRecentDocuments.qml", "SearchRecentDocuments.qml", - [&] (QQuickWidget* qw, QDialog& host) { - bridge= inject_common_context (qw, host); - qw->rootContext ()->setContextProperty ( - "searchDialogTitle", qt_translate ("Search recent documents")); - qw->rootContext ()->setContextProperty ("searchPlaceholder", - qt_translate ("Search")); - qw->rootContext ()->setContextProperty ( - "searchEmptyText", qt_translate ("No matching recent documents")); - qw->rootContext ()->setContextProperty ("recentDocuments", - recentDocuments); - qw->rootContext ()->setContextProperty ("dialogButtons", - translate_buttons (buttons)); - }, - 520, 450); - - QString path; - if (bridge) path= bridge->results ().value ("path").toString (); - delete bridge; - return from_qstring (path); -} diff --git a/src/Plugins/Qt/QTMQmlDialog.hpp b/src/Plugins/Qt/QTMQmlDialog.hpp index 831bbd92c7..308dad4d56 100644 --- a/src/Plugins/Qt/QTMQmlDialog.hpp +++ b/src/Plugins/Qt/QTMQmlDialog.hpp @@ -92,6 +92,7 @@ #define QTM_QML_DIALOG_H #include "array.hpp" +#include "RecentDocumentsSearchDialog.hpp" #include "string.hpp" #include "tree.hpp" @@ -214,13 +215,4 @@ bool cpp_version_dialog (string title, string message); */ tree cpp_preferences_dialog (); -/** - * @brief Open the QML search dialog for recently opened documents. - * - * @return The selected document path, or an empty string after Cancel, Esc, - * - * window close, or QML loading failure. - */ -string cpp_search_recent_documents_dialog (); - #endif // defined QTM_QML_DIALOG_H diff --git a/src/Plugins/Qt/QTMQmlDialogInternal.hpp b/src/Plugins/Qt/QTMQmlDialogInternal.hpp index 4c4fb5b16d..80681757c9 100644 --- a/src/Plugins/Qt/QTMQmlDialogInternal.hpp +++ b/src/Plugins/Qt/QTMQmlDialogInternal.hpp @@ -10,10 +10,17 @@ #ifndef QTM_QML_DIALOG_INTERNAL_H #define QTM_QML_DIALOG_INTERNAL_H +#include "array.hpp" +#include "string.hpp" #include "tree.hpp" #include #include +#include + +class QDialog; +class QQuickWidget; +class QmlDialogBridge; // 字段节点下标协议(见 QTMQmlDialog.hpp @par 数据协议): // (