Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions TeXmacs/progs/doc/docgrep.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion TeXmacs/progs/texmacs/menus/edit-menu.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
150 changes: 147 additions & 3 deletions ai-docs/qml/qml-dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -994,6 +1064,7 @@ <h1>QML 组件模拟器</h1>
<button class="nav-btn" data-target="version">Version.qml</button>
<button class="nav-btn" data-target="preferences">Preferences.qml</button>
<button class="nav-btn" data-target="confirmRestart">ConfirmRestart.qml</button>
<button class="nav-btn" data-target="recentSearch">SearchRecentDocuments.qml</button>
<div class="sidebar-foot">模拟目标: DialogShell / DialogButtons / EnumCombo / SelectableList / PreviewPane 组合逻辑。</div>
</aside>

Expand Down Expand Up @@ -1596,6 +1667,36 @@ <h3>切换界面语言</h3>
</p>
</section>

<section class="panel" id="panel-recent-search">
<article class="recent-search-shell">
<h3>Search recent documents</h3>
<input class="recent-search-input" id="recent-search-input" type="text" placeholder="Search">
<ul class="recent-results" id="recent-results">
<li class="recent-result" data-name="National Natural Science Foundation proposal" data-path="C:\\Users\\Mogan\\Documents\\National Natural Science Foundation proposal.tmu">
<strong>National Natural Science Foundation proposal</strong>
<span>C:\\Users\\Mogan\\Documents\\National Natural Science Foundation proposal.tmu</span>
</li>
<li class="recent-result" data-name="Research notes" data-path="C:\\Users\\Mogan\\Documents\\Research notes.tm">
<strong>Research notes</strong>
<span>C:\\Users\\Mogan\\Documents\\Research notes.tm</span>
</li>
<li class="recent-result" data-name="Thesis outline" data-path="D:\\Writing\\Thesis outline.tmu">
<strong>Thesis outline</strong>
<span>D:\\Writing\\Thesis outline.tmu</span>
</li>
<li class="recent-empty" id="recent-empty">No matching recent documents</li>
</ul>
<div class="btn-row" style="justify-content: flex-end; margin-top: 14px;">
<button class="btn primary" id="recent-open">Open</button>
<button class="btn" data-log="SearchRecentDocuments: cancel()">Cancel</button>
</div>
</article>
<p class="legend">
QML: <code>recentDocuments + fuzzyScore()</code>
<code>onTextChanged -&gt; updateMatches()</code>
<code>Open -&gt; recentSearchBridge.open(path)</code>
</p>
</section>

</main>
</div>
Expand All @@ -1611,7 +1712,8 @@ <h3>切换界面语言</h3>
statistics: document.getElementById('panel-statistics'),
version: document.getElementById('panel-version'),
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 = {
Expand All @@ -1622,7 +1724,8 @@ <h3>切换界面语言</h3>
paragraph: 'ParagraphFormat.qml 模拟',
statistics: 'Statistics.qml 模拟',
preferences: 'Preferences.qml 模拟',
confirmRestart: 'ConfirmRestart.qml 模拟'
confirmRestart: 'ConfirmRestart.qml 模拟',
recentSearch: 'SearchRecentDocuments.qml 模拟'
};

const codeMap = {
Expand All @@ -1633,7 +1736,8 @@ <h3>切换界面语言</h3>
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() + recentSearchBridge.open(path)'
};

const viewTitle = document.getElementById('view-title');
Expand Down Expand Up @@ -1668,6 +1772,46 @@ <h3>切换界面语言</h3>
});
});

const recentSearchInput = document.getElementById('recent-search-input');
const recentResults = document.querySelectorAll('.recent-result');
const recentEmpty = document.getElementById('recent-empty');
let recentSelected = null;

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';
recentSelected = null;
recentResults.forEach((row) => row.classList.remove('active'));
});

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: open("' + recentSelected.dataset.path + '")');
});

document.getElementById('form-submit').addEventListener('click', () => {
const values = {
language: document.getElementById('field-language').value,
Expand Down
79 changes: 79 additions & 0 deletions devel/2085.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# [2085] 编辑菜单中的最近文档搜索

## 相关文档
- [Issue #4143](https://github.com/MoganLab/mogan/issues/4143) - 采用 QML 重构并修复最近文档搜索。

## 2026/08/04 对齐 QML bridge 约定

### What
- 最近文档 bridge 负责读取 Scheme 数据、Cork 到 UTF-8 转码、规范化去重和已翻译文案。
- QML 仅维护模糊筛选与显式选择状态,支持上下键选择和 Enter 打开。
- 按钮文案改由通用 `dialogButtons` context property 注入。

### Why
此前 bridge 只承载已在对话框入口解析的数据,导致编码与去重规则分散;设计稿仍默认选中首项,也与实际交互不一致。

### How
- Windows 下对规范化路径做大小写折叠后去重,保留最近记录中的首次出现。
- 筛选后始终清空选择,只有点击或键盘移动后才可打开候选项。
- 设计稿、QML 加载测试和开发文档同步验证 bridge/context 契约。

## 任务相关的代码文件
- `TeXmacs/progs/doc/docgrep.scm`
- `TeXmacs/progs/texmacs/menus/edit-menu.scm`
- `src/Plugins/Qt/QTMQmlDialog.cpp`
- `src/Plugins/Qt/RecentDocumentsSearchBridge.hpp`
- `src/Plugins/Qt/RecentDocumentsSearchBridge.cpp`
- `src/Plugins/Qt/qml/SearchRecentDocuments.qml`
- `tests/Plugins/Qt/qml_dialog_test.cpp`
- `tests/Plugins/Qt/qml_load_test.cpp`
- `ai-docs/qml/qml-dialog.html`

## 如何测试
```powershell
xmake build stem
xmake install stem
& '.\build\packages\stem\data\bin\MoganSTEM.exe'
```

在“编辑 -> 搜索最近打开的文档”中输入文件名的一部分,确认结果按模糊匹配显示。选中条目并点击“打开”,确认文件被加载。

## 2026/08/04 Bridge 收敛与路径编码

### What
- 删除冗余的 `RecentDocumentsSearchDialog.cpp`,将对话框 glue 入口收回
`QTMQmlDialog.cpp`。
- 将最近文档 bridge 收敛为独立 `QObject`:它提供只读模型并完成确认回流;通用
`QmlDialogBridge` 只处理取消、Esc 与窗口拖动。
- 最近文档读取和返回均使用 UTF-8 系统路径,保留中文文件名。

### Why
专属业务动作不应继承通用 form bridge;同时 `system->url` 只接受 UTF-8 系统路径,
将路径转换为 Cork 会导致中文路径在回传后无法打开。

### How
- `url->system` 的 Scheme 返回值经 `QString::fromUtf8` 进入 QML;QML 确认后通过
`from_qstring_utf8` 回传给 `system->url`。
- QML 继续使用 `DialogShell`、`Theme` 和独立的 `recentSearchBridge`,不在筛选中
修改最近记录。

## 2026/08/03 采用 QML 重构最近文档搜索

### What
- 用 QML 对话框替换旧的交互式全文 grep 输入。
- 显示最近文档的文件名和路径,并支持实时模糊筛选与显式选择。
- 对最近文件路径去重;初始和筛选后均不默认选中,避免重复项同时高亮或误打开。
- 补充 QML 加载与桥接测试。
- 在 `ai-docs/qml/qml-dialog.html` 中补充该对话框的交互设计稿。

### Why
旧交互式输入在中文输入法确认候选字时会提前执行,且其全文搜索语义不符合按最近文件选择文档的预期。

### How
- Scheme 侧提供最近文件列表,并在确认选择后加载对应路径。
- C++ 将最近文件模型注入 QML,保持 UTF-8 路径编码。
- C++ 以规范化路径去重,保留最近记录中的第一次出现。
- QML 按文件名优先、完整路径兜底执行子序列模糊匹配;只有显式选中并点击“打开”才加载选定文件。
- 最近文档搜索的路径和模型逻辑独立于 QML 通用对话框底座,复用其内部模态引擎和 context 注入接口。
- 专属 bridge 向 QML 暴露候选模型和已翻译文案,并在确认时回传所选路径;通用
`QmlDialogBridge` 仅负责取消、Esc 和窗口关闭。
33 changes: 31 additions & 2 deletions src/Plugins/Qt/QTMQmlDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "PreferencesBridge.hpp"
#include "QTMQmlDialogBridge.hpp"
#include "QTMQmlDialogInternal.hpp"
#include "RecentDocumentsSearchBridge.hpp"
#include "VersionDialogBridge.hpp"

#include "analyze.hpp" // occurs
Expand Down Expand Up @@ -156,7 +157,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<void (QQuickWidget*, QDialog&)> inject_context,
int logic_w, int logic_h) {
Expand Down Expand Up @@ -580,8 +581,36 @@ cpp_version_dialog (string title, string message) {
return choice == 1;
}

string
cpp_search_recent_documents_dialog () {
string preset= get_env ("MOGAN_TEST_SEARCH_RECENT_DOCUMENTS");
if (preset != "") return preset == "cancel" ? string ("") : preset;

array<string> buttons= {string ("Open"), string ("Cancel")};
QmlDialogBridge* closeBridge = nullptr;
RecentDocumentsSearchBridge* searchBridge= nullptr;
run_qml_dialog (
"qrc:/qml/SearchRecentDocuments.qml", "SearchRecentDocuments.qml",
[&] (QQuickWidget* qw, QDialog& host) {
closeBridge = inject_common_context (qw, host);
searchBridge= new RecentDocumentsSearchBridge (&host);
qw->rootContext ()->setContextProperty ("recentSearchBridge",
searchBridge);
qw->rootContext ()->setContextProperty ("dialogButtons",
translate_buttons (buttons));
},
520, 450);

QString path;
if (searchBridge) path= searchBridge->selectedPath ();
delete closeBridge;
delete searchBridge;
return from_qstring_utf8 (path);
}

/**
* @brief 首选项 QML 对话框的 glue 入口(声明/语义见 QTMQmlDialog.hpp)。
* @brief 首选项 QML 对话框的 glue 入口(声明/语义见
* QTMQmlDialog.hpp)。
*
* @details 走 run_qml_dialog(exec 阻塞模态,同
* FormDialog——首选项是一次性提交, 无需 live
Expand Down
5 changes: 5 additions & 0 deletions src/Plugins/Qt/QTMQmlDialog.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,9 @@ bool cpp_version_dialog (string title, string message);
*/
tree cpp_preferences_dialog ();

/**
* @brief 打开最近文档搜索 QML 对话框。
*/
string cpp_search_recent_documents_dialog ();

#endif // defined QTM_QML_DIALOG_H
14 changes: 14 additions & 0 deletions src/Plugins/Qt/QTMQmlDialogInternal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <QStringList>
#include <QVariantMap>
#include <functional>

class QDialog;
class QQuickWidget;
class QmlDialogBridge;

// 字段节点下标协议(见 QTMQmlDialog.hpp @par 数据协议):
// (<type> <label> <key> (<options>...) <value> <live?>)
Expand Down Expand Up @@ -57,4 +64,11 @@ QVariantMap field_tree_to_qml (tree f);
*/
QStringList translate_buttons (array<string> buttons);

QmlDialogBridge* inject_common_context (QQuickWidget* qw, QDialog& host);

int
run_qml_dialog (const string& qml_url, const char* debug_tag,
std::function<void (QQuickWidget*, QDialog&)> inject_context,
int logic_w, int logic_h);

#endif // defined QTM_QML_DIALOG_INTERNAL_H
Loading
Loading