From 86e670bc98b45c8d62d311345d298b2a7e70c4ee Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 27 May 2026 20:59:48 +0800 Subject: [PATCH 01/19] wip --- TeXmacs/progs/generic/search-widgets.scm | 87 ++++++++- devel/1042.md | 54 ++++++ src/Plugins/Qt/qt_chat_controller.hpp | 3 + src/Plugins/Qt/qt_chat_tab_widget.hpp | 3 + src/Plugins/Qt/qt_floating_search_bar.cpp | 216 ++++++++++++++++++++++ src/Plugins/Qt/qt_floating_search_bar.hpp | 62 +++++++ src/Scheme/L5/glue_widget.lua | 8 + src/Scheme/L5/init_glue_l5.cpp | 1 + 8 files changed, 424 insertions(+), 10 deletions(-) create mode 100644 devel/1042.md create mode 100644 src/Plugins/Qt/qt_floating_search_bar.cpp create mode 100644 src/Plugins/Qt/qt_floating_search_bar.hpp diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 0c281c7a28..ab71f7842e 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -1112,6 +1112,42 @@ ) ;when ) ;tm-define +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Chat tab search (floating search bar) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define chat-tab-search-target #f) + +(define (chat-tab-search-init target-buf) + (set! chat-tab-search-target target-buf) + (let* ((aux (search-buffer))) + (buffer-set-master aux target-buf) + (set-search-window-state #t #t) + (with-buffer target-buf + (set-search-reference (cursor-path))) + (set-search-filter) + (set! search-filter-out? #f))) + +(tm-define (chat-tab-set-query text) + (when (and chat-tab-search-target (buffer-exists? (search-buffer))) + (buffer-set-body (search-buffer) `(document ,text)) + (with-buffer chat-tab-search-target + (perform-search*)))) + +(tm-define (chat-tab-search-next forward?) + (when chat-tab-search-target + (with-buffer chat-tab-search-target + (search-next-match forward? chat-tab-search-target)))) + +(tm-define (chat-tab-search-close) + (when chat-tab-search-target + (search-show-all) + (set! search-serial (+ search-serial 1)) + (with-buffer chat-tab-search-target + (cancel-alt-selection "alternate")) + (set-search-window-state #f #f) + (set! chat-tab-search-target #f))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Search and replace widget ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1626,19 +1662,50 @@ (define-preferences ("toolbar search" "on" noop) ("toolbar replace" "on" noop)) +(define (chat-message-buffer? buf) + (string-starts? (url->system buf) "tmfs://chat-message-")) + +(define (chat-input-buffer? buf) + (string-starts? (url->system buf) "tmfs://chat-input-")) + +(define (chat-buffer-session-id buf) + (with s (url->system buf) + (cond ((chat-message-buffer? buf) + (substring s (string-length "tmfs://chat-message-"))) + ((chat-input-buffer? buf) + (substring s (string-length "tmfs://chat-input-"))) + (else #f)))) + +(define (chat-message-buffer-has-content? msg-buf) + (and (buffer-exists? msg-buf) + (with body (buffer-get-body msg-buf) + (not (and (tm-func? body 'document 1) + (tree-empty? (tm-ref body 0))))))) + (tm-define (interactive-search) (:interactive #t) - (unless (string-starts? (url->system (current-buffer)) "tmfs:") - (set! search-replace-text - (cond ((in-math?) "Only search in math mode") - ((in-prog?) "Only search in Program mode") - ((in-graphics?) "Graphics mode cannot search") - (else "Only search in text mode") + (with buf (current-buffer) + (with sid (chat-buffer-session-id buf) + (cond + ((and sid + (chat-message-buffer-has-content? + (string->url (string-append "tmfs://chat-message-" sid)))) + (with msg-buf (string->url (string-append "tmfs://chat-message-" sid)) + (chat-tab-search-init msg-buf) + (qt-floating-search "true"))) + ((not (string-starts? (url->system buf) "tmfs:")) + (set! search-replace-text + (cond ((in-math?) "Only search in math mode") + ((in-prog?) "Only search in Program mode") + ((in-graphics?) "Graphics mode cannot search") + (else "Only search in text mode") + ) ;cond + ) ;set! + (set-boolean-preference "search-and-replace" #f) + (open-search)) ) ;cond - ) ;set! - (set-boolean-preference "search-and-replace" #f) - (open-search) - ) ;unless + ) ;with + ) ;with ) ;tm-define (tm-define (interactive-replace) diff --git a/devel/1042.md b/devel/1042.md new file mode 100644 index 0000000000..32cd479fa8 --- /dev/null +++ b/devel/1042.md @@ -0,0 +1,54 @@ +# [1042] Chat Tab 搜索功能 + +## 1 相关文档 +- [dddd.md](dddd.md) - 任务文档模板 +- [0228.md](0228.md) - 禁用 Chat Tab 搜索(本任务修复并替代) + +## 2 任务相关的代码文件 +- `TeXmacs/progs/generic/search-widgets.scm` - 搜索/替换入口函数 +- `src/Plugins/Qt/qt_chat_tab_widget.hpp` - Chat tab widget 头文件 +- `src/Plugins/Qt/qt_chat_tab_widget.cpp` - Chat tab widget 实现 + +## 3 如何测试 + +### 3.1 确定性测试(单元测试) +无单元测试,需手动验证。 + +### 3.2 非确定性测试(文档验证) +``` +1. 打开 Chat Tab,在 message buffer 中按 Cmd+F → 右上角出现悬浮搜索框 +2. 输入文本 → 高亮匹配项,显示匹配计数 +3. 点击上一个/下一个按钮 → 跳转到对应匹配 +4. 按 Esc 或点击关闭 → 搜索框消失,高亮清除 +5. 在 Chat Tab 的 input buffer 中按 Cmd+F → 自动定位到 message buffer 搜索 +6. 在 Chat Tab 中按 Cmd+H(替换) → 无反应 +7. 普通文档 Tab 中按 Cmd+F → 侧边栏搜索正常工作 +``` + +## 4 如何提交 + +提交前执行以下最少步骤: + +```bash +xmake b stem +gf fmt --changed-since=main +``` + +## 5 What +Chat Tab 的 message buffer 是嵌入式 TeXmacs widget,没有标准 `tm_window`,无法使用主窗口的 auxiliary-widget 侧边栏搜索机制。需要为 Chat Tab 实现独立的搜索功能。 + +1. 在 QTChatTabWidget 右上角实现 VSCode 风格的悬浮搜索栏(公共组件类) +2. 搜索栏包含:输入框、上一个/下一个按钮、关闭按钮、匹配计数 +3. Scheme 端新增 chat-tab 专用搜索初始化和导航函数 +4. 修改 `interactive-search` 在嵌入式 chat buffer 时使用新的搜索 UI +5. 修改 `interactive-replace` 在 chat tab 中禁用替换 + +## 6 Why +commit [0228] 通过 blanket `tmfs:` 检查禁用了所有 tmfs:// 缓冲区的搜索来避免 crash。用户需要在 Chat Tab 的消息输出框中搜索对话内容。Chat Tab 的嵌入式 buffer 不支持 auxiliary-widget 机制,需要独立的搜索 UI。 + +## 7 How +1. 实现一个可复用的悬浮搜索栏 Qt 组件(`QTMFloatingSearchBar`),包含输入框、导航按钮、关闭按钮、匹配计数标签 +2. 在 QTChatTabWidget 的 content 区域右上角放置该组件,初始隐藏 +3. Scheme 端新增 `chat-tab-search-init`、`chat-tab-perform-search`、`chat-tab-search-next`、`chat-tab-search-close` 函数 +4. C++ 按钮回调通过 `eval_scheme` 调用 Scheme 搜索函数 +5. `interactive-search` 检测到嵌入式 chat buffer 时调 C++ glue 显示搜索栏并初始化搜索 diff --git a/src/Plugins/Qt/qt_chat_controller.hpp b/src/Plugins/Qt/qt_chat_controller.hpp index 06d51c6482..66aa110500 100644 --- a/src/Plugins/Qt/qt_chat_controller.hpp +++ b/src/Plugins/Qt/qt_chat_controller.hpp @@ -133,6 +133,9 @@ class ChatController : public QObject { */ void destroyView (); +public: + QTChatTabWidget* view () const { return view_; } + private: QTChatTabWidget* view_= nullptr; ///< View 指针,由 createView 创建 ChatSessionManager sessionManager_; ///< 会话管理器 diff --git a/src/Plugins/Qt/qt_chat_tab_widget.hpp b/src/Plugins/Qt/qt_chat_tab_widget.hpp index 41aee1cc66..81791f6645 100644 --- a/src/Plugins/Qt/qt_chat_tab_widget.hpp +++ b/src/Plugins/Qt/qt_chat_tab_widget.hpp @@ -351,6 +351,9 @@ class QTChatTabWidget : public QWidget { void setSidebarVisible (bool visible); void setCloseSidebarButtonVisible (bool visible); + // ---- 供外部组件访问 ---- + QWidget* contentWidget () const { return contentWidget_; } + signals: void cancelRequested (const string& sessionId); void newChatRequested (); diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp new file mode 100644 index 0000000000..fbfac47923 --- /dev/null +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -0,0 +1,216 @@ + +/****************************************************************************** + * MODULE : qt_floating_search_bar.cpp + * DESCRIPTION: A VSCode-style floating search bar widget + * COPYRIGHT : (C) 2026 Mogan STEM + ****************************************************************************** + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#include "qt_floating_search_bar.hpp" +#include "qt_chat_controller.hpp" +#include "qt_chat_tab_widget.hpp" +#include "qt_dpi_utils.hpp" +#include "qt_utilities.hpp" + +#include "s7_tm.hpp" + +#include +#include +#include +#include + +/****************************************************************************** + * QTMFloatingSearchBar + ******************************************************************************/ + +QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) + : QWidget (parent), edit_ (nullptr), infoLbl_ (nullptr) { + setObjectName ("floating_search_bar"); + setWindowFlags (Qt::Widget); + setAttribute (Qt::WA_StyledBackground); + setFixedHeight (DpiUtils::scaled (32)); + + QHBoxLayout* lay= new QHBoxLayout (this); + lay->setContentsMargins (DpiUtils::scaled (4), 0, DpiUtils::scaled (4), 0); + lay->setSpacing (DpiUtils::scaled (2)); + + // 搜索输入框 + edit_= new QLineEdit (this); + edit_->setPlaceholderText (tr ("Search")); + edit_->setFixedHeight (DpiUtils::scaled (24)); + lay->addWidget (edit_, 1); + + // 上一个按钮 + QPushButton* prevBtn= new QPushButton (this); + prevBtn->setIcon (style ()->standardIcon (QStyle::SP_ArrowUp)); + prevBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); + prevBtn->setToolTip (tr ("Previous (Shift+Enter)")); + lay->addWidget (prevBtn); + + // 下一个按钮 + QPushButton* nextBtn= new QPushButton (this); + nextBtn->setIcon (style ()->standardIcon (QStyle::SP_ArrowDown)); + nextBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); + nextBtn->setToolTip (tr ("Next (Enter)")); + lay->addWidget (nextBtn); + + // 关闭按钮 + QPushButton* closeBtn= new QPushButton (this); + closeBtn->setIcon (style ()->standardIcon (QStyle::SP_DialogCloseButton)); + closeBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); + closeBtn->setToolTip (tr ("Close (Esc)")); + lay->addWidget (closeBtn); + + // 匹配计数标签 + infoLbl_= new QLabel (this); + infoLbl_->setFixedHeight (DpiUtils::scaled (24)); + infoLbl_->setMinimumWidth (DpiUtils::scaled (40)); + lay->addWidget (infoLbl_); + + // 信号连接 + QObject::connect (edit_, &QLineEdit::textChanged, this, + &QTMFloatingSearchBar::queryChanged); + QObject::connect (nextBtn, &QPushButton::clicked, this, + &QTMFloatingSearchBar::findNextRequested); + QObject::connect (prevBtn, &QPushButton::clicked, this, + &QTMFloatingSearchBar::findPreviousRequested); + QObject::connect (closeBtn, &QPushButton::clicked, this, + &QTMFloatingSearchBar::closeRequested); + + hide (); +} + +void +QTMFloatingSearchBar::activate () { + show (); + raise (); + edit_->clear (); + infoLbl_->clear (); + edit_->setFocus (); +} + +QString +QTMFloatingSearchBar::queryText () const { + return edit_->text (); +} + +void +QTMFloatingSearchBar::setMatchInfo (const QString& info) { + infoLbl_->setText (info); +} + +void +QTMFloatingSearchBar::keyPressEvent (QKeyEvent* event) { + if (event->key () == Qt::Key_Escape) { + emit closeRequested (); + event->accept (); + } + else if (event->key () == Qt::Key_Return || event->key () == Qt::Key_Enter) { + if (event->modifiers () & Qt::ShiftModifier) emit findPreviousRequested (); + else emit findNextRequested (); + event->accept (); + } + else { + QWidget::keyPressEvent (event); + } +} + +/****************************************************************************** + * 浮动搜索栏管理(独立于 QTChatTabWidget) + ******************************************************************************/ + +static QTMFloatingSearchBar* g_search_bar = nullptr; +static QWidget* g_search_bar_parent= nullptr; + +// 事件过滤器:parent resize 时重新定位搜索栏 +class SearchBarResizeFilter : public QObject { +public: + SearchBarResizeFilter (QObject* parent) : QObject (parent) {} + +protected: + bool eventFilter (QObject* watched, QEvent* event) override { + if (event->type () == QEvent::Resize && g_search_bar && + g_search_bar->isVisible ()) { + QWidget* w= qobject_cast (watched); + if (w) { + int x= w->width () - g_search_bar->width () - DpiUtils::scaled (8); + int y= DpiUtils::scaled (4); + g_search_bar->move (x, y); + } + } + return QObject::eventFilter (watched, event); + } +}; + +static SearchBarResizeFilter* g_resize_filter= nullptr; + +static void +position_search_bar (QWidget* content) { + int x= content->width () - g_search_bar->width () - DpiUtils::scaled (8); + int y= DpiUtils::scaled (4); + g_search_bar->move (x, y); +} + +static void +connect_search_bar_signals (QTMFloatingSearchBar* bar) { + QObject::connect (bar, &QTMFloatingSearchBar::queryChanged, bar, + [bar] (const QString& text) { + eval_scheme ("(chat-tab-set-query " * + qt_scheme_quote (text) * ")"); + }); + QObject::connect (bar, &QTMFloatingSearchBar::findNextRequested, bar, + [] () { eval_scheme ("(chat-tab-search-next #t)"); }); + QObject::connect (bar, &QTMFloatingSearchBar::findPreviousRequested, bar, + [] () { eval_scheme ("(chat-tab-search-next #f)"); }); + QObject::connect (bar, &QTMFloatingSearchBar::closeRequested, bar, [] () { + eval_scheme ("(chat-tab-search-close)"); + if (g_search_bar) g_search_bar->hide (); + }); +} + +/****************************************************************************** + * Scheme 胶水函数 + ******************************************************************************/ + +void +qt_floating_search (string flag) { + ChatController* ctrl= get_chat_controller (); + if (!ctrl) return; + QTChatTabWidget* view= ctrl->view (); + if (!view) return; + QWidget* content= view->contentWidget (); + if (!content) return; + + if (flag == "true" || flag == "#t") { + // 按需创建搜索栏 + if (!g_search_bar || g_search_bar_parent != content) { + // 删除旧实例(parent 变了) + if (g_search_bar) { + delete g_search_bar; + g_search_bar= nullptr; + } + if (g_resize_filter) { + if (g_search_bar_parent) + g_search_bar_parent->removeEventFilter (g_resize_filter); + delete g_resize_filter; + g_resize_filter= nullptr; + } + + g_search_bar = new QTMFloatingSearchBar (content); + g_search_bar_parent= content; + g_resize_filter = new SearchBarResizeFilter (content); + content->installEventFilter (g_resize_filter); + + g_search_bar->setFixedWidth (DpiUtils::scaled (320)); + connect_search_bar_signals (g_search_bar); + } + g_search_bar->activate (); + position_search_bar (content); + } + else { + if (g_search_bar) g_search_bar->hide (); + } +} diff --git a/src/Plugins/Qt/qt_floating_search_bar.hpp b/src/Plugins/Qt/qt_floating_search_bar.hpp new file mode 100644 index 0000000000..ee28bd9f70 --- /dev/null +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -0,0 +1,62 @@ + +/****************************************************************************** + * MODULE : qt_floating_search_bar.hpp + * DESCRIPTION: A VSCode-style floating search bar widget for TeXmacs + * COPYRIGHT : (C) 2026 Mogan STEM + ****************************************************************************** + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#ifndef QT_FLOATING_SEARCH_BAR_HPP +#define QT_FLOATING_SEARCH_BAR_HPP + +#include +#include +#include +#include + +#include "string.hpp" + +/** + * 轻量级悬浮搜索栏,布局类似 VSCode 的 Ctrl+F 搜索框。 + * + * 布局: [搜索输入框] [上一个] [下一个] [关闭] [匹配计数] + * 通过信号通知宿主执行搜索操作。 + */ +class QTMFloatingSearchBar : public QWidget { + Q_OBJECT + +public: + QTMFloatingSearchBar (QWidget* parent= nullptr); + + /// 显示搜索栏,清空输入并聚焦。 + void activate (); + /// 获取当前搜索文本。 + QString queryText () const; + /// 设置匹配信息标签(如 "3/12" 或 "无结果")。 + void setMatchInfo (const QString& info); + +signals: + /// 搜索文本变化时触发(实时搜索)。 + void queryChanged (const QString& text); + /// 用户点击下一个或按 Enter 时触发。 + void findNextRequested (); + /// 用户点击上一个或按 Shift+Enter 时触发。 + void findPreviousRequested (); + /// 用户关闭搜索栏时触发。 + void closeRequested (); + +protected: + void keyPressEvent (QKeyEvent* event) override; + +private: + QLineEdit* edit_; ///< 搜索输入框 + QLabel* infoLbl_; ///< 匹配计数标签 +}; + +/// Scheme 胶水函数:显示 ("true"/"#t") 或隐藏悬浮搜索栏。 +void qt_floating_search (string flag); + +#endif // QT_FLOATING_SEARCH_BAR_HPP diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua index 30cc2501e3..9231c149ef 100644 --- a/src/Scheme/L5/glue_widget.lua +++ b/src/Scheme/L5/glue_widget.lua @@ -526,6 +526,14 @@ function main() "int", "string" } + }, + { + scm_name = "qt-floating-search", + cpp_name = "qt_floating_search", + ret_type = "void", + arg_list = { + "string" + } } } } diff --git a/src/Scheme/L5/init_glue_l5.cpp b/src/Scheme/L5/init_glue_l5.cpp index 4bd3a48a7c..aae47442c5 100644 --- a/src/Scheme/L5/init_glue_l5.cpp +++ b/src/Scheme/L5/init_glue_l5.cpp @@ -29,6 +29,7 @@ #include "preferences.hpp" #include "promise.hpp" #include "qt_chat_controller.hpp" +#include "qt_floating_search_bar.hpp" #include "tm_debug.hpp" #include "tm_locale.hpp" #include "tree_observer.hpp" From 93cadf8e82d8d8fe6a6c017ed3d84858b5bd6474 Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 27 May 2026 22:21:05 +0800 Subject: [PATCH 02/19] wip --- TeXmacs/misc/images/floating-search/down.svg | 12 ++ TeXmacs/misc/images/floating-search/up.svg | 12 ++ TeXmacs/misc/images/images.qrc | 3 + TeXmacs/progs/generic/search-widgets.scm | 32 ++-- src/Plugins/Qt/qt_floating_search_bar.cpp | 184 +++++++++++++------ src/Plugins/Qt/qt_floating_search_bar.hpp | 36 ++-- src/Scheme/L5/glue_widget.lua | 8 + 7 files changed, 197 insertions(+), 90 deletions(-) create mode 100644 TeXmacs/misc/images/floating-search/down.svg create mode 100644 TeXmacs/misc/images/floating-search/up.svg diff --git a/TeXmacs/misc/images/floating-search/down.svg b/TeXmacs/misc/images/floating-search/down.svg new file mode 100644 index 0000000000..c6115207a6 --- /dev/null +++ b/TeXmacs/misc/images/floating-search/down.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/floating-search/up.svg b/TeXmacs/misc/images/floating-search/up.svg new file mode 100644 index 0000000000..95ef9e9fc9 --- /dev/null +++ b/TeXmacs/misc/images/floating-search/up.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/images.qrc b/TeXmacs/misc/images/images.qrc index 1669da501a..3276c7b7e1 100644 --- a/TeXmacs/misc/images/images.qrc +++ b/TeXmacs/misc/images/images.qrc @@ -25,6 +25,9 @@ llm-chat/thinking.svg llm-chat/thinking-white.svg + floating-search/down.svg + floating-search/up.svg + ocr-button/left-align-white.svg ocr-button/middle-align-white.svg diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index ab71f7842e..f509cfc693 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -1117,25 +1117,35 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define chat-tab-search-target #f) +(define chat-tab-search-aux #f) (define (chat-tab-search-init target-buf) (set! chat-tab-search-target target-buf) - (let* ((aux (search-buffer))) + (let ((aux (search-buffer))) + (set! chat-tab-search-aux aux) (buffer-set-master aux target-buf) (set-search-window-state #t #t) (with-buffer target-buf (set-search-reference (cursor-path))) (set-search-filter) - (set! search-filter-out? #f))) + (set! search-filter-out? #f) + (qt-floating-search-init (url->string aux)) + (qt-floating-search "true") + (with-buffer target-buf + (perform-search*)))) -(tm-define (chat-tab-set-query text) - (when (and chat-tab-search-target (buffer-exists? (search-buffer))) - (buffer-set-body (search-buffer) `(document ,text)) +(define (chat-tab-perform-search) + (when (and chat-tab-search-target chat-tab-search-aux + (buffer-exists? chat-tab-search-aux)) (with-buffer chat-tab-search-target - (perform-search*)))) + (set-search-reference (cursor-path))) + (set-search-filter) + (with-buffer chat-tab-search-target + (perform-search)))) (tm-define (chat-tab-search-next forward?) - (when chat-tab-search-target + (when (and chat-tab-search-target chat-tab-search-aux) + (chat-tab-perform-search) (with-buffer chat-tab-search-target (search-next-match forward? chat-tab-search-target)))) @@ -1146,7 +1156,8 @@ (with-buffer chat-tab-search-target (cancel-alt-selection "alternate")) (set-search-window-state #f #f) - (set! chat-tab-search-target #f))) + (set! chat-tab-search-target #f) + (set! chat-tab-search-aux #f))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Search and replace widget @@ -1690,9 +1701,8 @@ ((and sid (chat-message-buffer-has-content? (string->url (string-append "tmfs://chat-message-" sid)))) - (with msg-buf (string->url (string-append "tmfs://chat-message-" sid)) - (chat-tab-search-init msg-buf) - (qt-floating-search "true"))) + (chat-tab-search-init + (string->url (string-append "tmfs://chat-message-" sid)))) ((not (string-starts? (url->system buf) "tmfs:")) (set! search-replace-text (cond ((in-math?) "Only search in math mode") diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index fbfac47923..3d392cdf46 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -14,65 +14,97 @@ #include "qt_chat_tab_widget.hpp" #include "qt_dpi_utils.hpp" #include "qt_utilities.hpp" +#include "qt_widget.hpp" #include "s7_tm.hpp" +#include "tm_window.hpp" +#include "widget.hpp" +#include + +#include #include -#include #include #include +using namespace moebius; + /****************************************************************************** * QTMFloatingSearchBar ******************************************************************************/ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) - : QWidget (parent), edit_ (nullptr), infoLbl_ (nullptr) { + : QWidget (parent) { setObjectName ("floating_search_bar"); setWindowFlags (Qt::Widget); setAttribute (Qt::WA_StyledBackground); - setFixedHeight (DpiUtils::scaled (32)); + setMinimumHeight (DpiUtils::scaled (44)); + + // 圆角、去边框、悬浮阴影 + QString btnStyle= QString ("QPushButton {" + " border: none;" + " border-radius: %1px;" + " background: transparent;" + "}" + "QPushButton:hover {" + " background: rgba(0,0,0,30);" + "}" + "QPushButton:pressed {" + " background: rgba(0,0,0,50);" + "}") + .arg (DpiUtils::scaled (12)); + setStyleSheet (QString ("#floating_search_bar {" + " background: #f3f3f3;" + " border: none;" + " border-radius: %1px;" + "}") + .arg (DpiUtils::scaled (4))); + QGraphicsDropShadowEffect* shadow= new QGraphicsDropShadowEffect (this); + shadow->setBlurRadius (DpiUtils::scaled (8)); + shadow->setOffset (0, DpiUtils::scaled (1)); + shadow->setColor (QColor (0, 0, 0, 30)); + setGraphicsEffect (shadow); - QHBoxLayout* lay= new QHBoxLayout (this); - lay->setContentsMargins (DpiUtils::scaled (4), 0, DpiUtils::scaled (4), 0); - lay->setSpacing (DpiUtils::scaled (2)); + layout_= new QHBoxLayout (this); + layout_->setContentsMargins (DpiUtils::scaled (6), DpiUtils::scaled (6), + DpiUtils::scaled (6), DpiUtils::scaled (6)); + layout_->setSpacing (DpiUtils::scaled (4)); - // 搜索输入框 - edit_= new QLineEdit (this); - edit_->setPlaceholderText (tr ("Search")); - edit_->setFixedHeight (DpiUtils::scaled (24)); - lay->addWidget (edit_, 1); + // 布局顺序: [输入区] [匹配计数] [上一个] [下一个] [关闭] + // 输入区由 setSearchInput 动态插入 index 0 + + // 匹配计数标签 + infoLbl_= new QLabel (this); + infoLbl_->setFixedHeight (DpiUtils::scaled (24)); + infoLbl_->setMinimumWidth (DpiUtils::scaled (80)); + infoLbl_->setAlignment (Qt::AlignCenter); + infoLbl_->setText (QString::fromUtf8 ("无匹配")); + layout_->addWidget (infoLbl_); // 上一个按钮 QPushButton* prevBtn= new QPushButton (this); - prevBtn->setIcon (style ()->standardIcon (QStyle::SP_ArrowUp)); + prevBtn->setIcon (QIcon (":floating-search/up.svg")); prevBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); prevBtn->setToolTip (tr ("Previous (Shift+Enter)")); - lay->addWidget (prevBtn); + prevBtn->setStyleSheet (btnStyle); + layout_->addWidget (prevBtn); // 下一个按钮 QPushButton* nextBtn= new QPushButton (this); - nextBtn->setIcon (style ()->standardIcon (QStyle::SP_ArrowDown)); + nextBtn->setIcon (QIcon (":floating-search/down.svg")); nextBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); nextBtn->setToolTip (tr ("Next (Enter)")); - lay->addWidget (nextBtn); + nextBtn->setStyleSheet (btnStyle); + layout_->addWidget (nextBtn); // 关闭按钮 QPushButton* closeBtn= new QPushButton (this); - closeBtn->setIcon (style ()->standardIcon (QStyle::SP_DialogCloseButton)); + closeBtn->setIcon (QIcon (":tabpage/close.svg")); closeBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); closeBtn->setToolTip (tr ("Close (Esc)")); - lay->addWidget (closeBtn); - - // 匹配计数标签 - infoLbl_= new QLabel (this); - infoLbl_->setFixedHeight (DpiUtils::scaled (24)); - infoLbl_->setMinimumWidth (DpiUtils::scaled (40)); - lay->addWidget (infoLbl_); + closeBtn->setStyleSheet (btnStyle); + layout_->addWidget (closeBtn); - // 信号连接 - QObject::connect (edit_, &QLineEdit::textChanged, this, - &QTMFloatingSearchBar::queryChanged); QObject::connect (nextBtn, &QPushButton::clicked, this, &QTMFloatingSearchBar::findNextRequested); QObject::connect (prevBtn, &QPushButton::clicked, this, @@ -84,38 +116,26 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) } void -QTMFloatingSearchBar::activate () { - show (); - raise (); - edit_->clear (); - infoLbl_->clear (); - edit_->setFocus (); -} - -QString -QTMFloatingSearchBar::queryText () const { - return edit_->text (); +QTMFloatingSearchBar::setSearchInput (QWidget* input) { + if (inputQW_) { + layout_->removeWidget (inputQW_); + delete inputQW_; + } + inputQW_= input; + layout_->insertWidget (0, input, 1); } void -QTMFloatingSearchBar::setMatchInfo (const QString& info) { - infoLbl_->setText (info); +QTMFloatingSearchBar::activate () { + show (); + raise (); + if (inputQW_) inputQW_->setFocus (); } void -QTMFloatingSearchBar::keyPressEvent (QKeyEvent* event) { - if (event->key () == Qt::Key_Escape) { - emit closeRequested (); - event->accept (); - } - else if (event->key () == Qt::Key_Return || event->key () == Qt::Key_Enter) { - if (event->modifiers () & Qt::ShiftModifier) emit findPreviousRequested (); - else emit findNextRequested (); - event->accept (); - } - else { - QWidget::keyPressEvent (event); - } +QTMFloatingSearchBar::setMatchInfo (int current, int total) { + if (total == 0) infoLbl_->setText (tr ("No matches")); + else infoLbl_->setText (tr ("%1 of %2").arg (current).arg (total)); } /****************************************************************************** @@ -156,11 +176,6 @@ position_search_bar (QWidget* content) { static void connect_search_bar_signals (QTMFloatingSearchBar* bar) { - QObject::connect (bar, &QTMFloatingSearchBar::queryChanged, bar, - [bar] (const QString& text) { - eval_scheme ("(chat-tab-set-query " * - qt_scheme_quote (text) * ")"); - }); QObject::connect (bar, &QTMFloatingSearchBar::findNextRequested, bar, [] () { eval_scheme ("(chat-tab-search-next #t)"); }); QObject::connect (bar, &QTMFloatingSearchBar::findPreviousRequested, bar, @@ -185,9 +200,8 @@ qt_floating_search (string flag) { if (!content) return; if (flag == "true" || flag == "#t") { - // 按需创建搜索栏 + // 按需创建浮动栏容器 if (!g_search_bar || g_search_bar_parent != content) { - // 删除旧实例(parent 变了) if (g_search_bar) { delete g_search_bar; g_search_bar= nullptr; @@ -204,9 +218,10 @@ qt_floating_search (string flag) { g_resize_filter = new SearchBarResizeFilter (content); content->installEventFilter (g_resize_filter); - g_search_bar->setFixedWidth (DpiUtils::scaled (320)); + g_search_bar->setFixedWidth (DpiUtils::scaled (360)); connect_search_bar_signals (g_search_bar); } + g_search_bar->activate (); position_search_bar (content); } @@ -214,3 +229,52 @@ qt_floating_search (string flag) { if (g_search_bar) g_search_bar->hide (); } } + +void +qt_floating_search_init (string aux_url_str) { + ChatController* ctrl= get_chat_controller (); + if (!ctrl) return; + QTChatTabWidget* view= ctrl->view (); + if (!view) return; + QWidget* content= view->contentWidget (); + if (!content) return; + + // 确保浮动栏容器存在 + if (!g_search_bar || g_search_bar_parent != content) { + if (g_search_bar) { + delete g_search_bar; + g_search_bar= nullptr; + } + if (g_resize_filter) { + if (g_search_bar_parent) + g_search_bar_parent->removeEventFilter (g_resize_filter); + delete g_resize_filter; + g_resize_filter= nullptr; + } + + g_search_bar = new QTMFloatingSearchBar (content); + g_search_bar_parent= content; + g_resize_filter = new SearchBarResizeFilter (content); + content->installEventFilter (g_resize_filter); + + g_search_bar->setFixedWidth (DpiUtils::scaled (420)); + connect_search_bar_signals (g_search_bar); + } + + // 创建 texmacs_input_widget 绑定到 search-buffer + url aux_url= url_system (aux_url_str); + tree doc (DOCUMENT, ""); + tree sty = compound ("style", tree (TUPLE, "generic")); + widget tw = texmacs_input_widget (doc, sty, aux_url); + QWidget* inputW= concrete (tw)->as_qwidget (); + if (inputW) { + inputW->setStyleSheet ("QWidget {" + " background: white;" + " border: 1px solid #d0d0d0;" + "}" + "QWidget:focus {" + " border: 1px solid #215a6a;" + "}"); + g_search_bar->setSearchInput (inputW); + } +} diff --git a/src/Plugins/Qt/qt_floating_search_bar.hpp b/src/Plugins/Qt/qt_floating_search_bar.hpp index ee28bd9f70..d7231cb3f0 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.hpp +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -12,18 +12,19 @@ #ifndef QT_FLOATING_SEARCH_BAR_HPP #define QT_FLOATING_SEARCH_BAR_HPP +#include #include -#include #include #include #include "string.hpp" /** - * 轻量级悬浮搜索栏,布局类似 VSCode 的 Ctrl+F 搜索框。 + * 悬浮搜索栏容器。 * - * 布局: [搜索输入框] [上一个] [下一个] [关闭] [匹配计数] - * 通过信号通知宿主执行搜索操作。 + * 布局: [TeXmacs 输入区] [上一个] [下一个] [关闭] [匹配计数] + * 输入区是嵌入的 texmacs_input_widget,绑定到 search-buffer, + * 搜索逻辑与底部搜索面板完全一致。 */ class QTMFloatingSearchBar : public QWidget { Q_OBJECT @@ -31,32 +32,29 @@ class QTMFloatingSearchBar : public QWidget { public: QTMFloatingSearchBar (QWidget* parent= nullptr); - /// 显示搜索栏,清空输入并聚焦。 + /// 设置嵌入的 TeXmacs 搜索输入 widget。 + void setSearchInput (QWidget* input); + /// 显示搜索栏并聚焦输入区。 void activate (); - /// 获取当前搜索文本。 - QString queryText () const; - /// 设置匹配信息标签(如 "3/12" 或 "无结果")。 - void setMatchInfo (const QString& info); + /// 设置匹配信息(current=0, total=0 显示"无匹配")。 + void setMatchInfo (int current, int total); signals: - /// 搜索文本变化时触发(实时搜索)。 - void queryChanged (const QString& text); - /// 用户点击下一个或按 Enter 时触发。 void findNextRequested (); - /// 用户点击上一个或按 Shift+Enter 时触发。 void findPreviousRequested (); - /// 用户关闭搜索栏时触发。 void closeRequested (); -protected: - void keyPressEvent (QKeyEvent* event) override; - private: - QLineEdit* edit_; ///< 搜索输入框 - QLabel* infoLbl_; ///< 匹配计数标签 + QHBoxLayout* layout_ = nullptr; + QWidget* inputQW_= nullptr; ///< 嵌入的 TeXmacs 输入 QWidget + QLabel* infoLbl_= nullptr; ///< 匹配计数标签 }; /// Scheme 胶水函数:显示 ("true"/"#t") 或隐藏悬浮搜索栏。 void qt_floating_search (string flag); +/// Scheme 胶水函数:传入 search-buffer URL,创建 texmacs-input +/// 并嵌入浮动搜索栏。 +void qt_floating_search_init (string aux_url_str); + #endif // QT_FLOATING_SEARCH_BAR_HPP diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua index 9231c149ef..5999d68cc9 100644 --- a/src/Scheme/L5/glue_widget.lua +++ b/src/Scheme/L5/glue_widget.lua @@ -534,6 +534,14 @@ function main() arg_list = { "string" } + }, + { + scm_name = "qt-floating-search-init", + cpp_name = "qt_floating_search_init", + ret_type = "void", + arg_list = { + "string" + } } } } From 79e1543a5098ba589188884fd10dc3dcaa9f1437 Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 27 May 2026 22:31:37 +0800 Subject: [PATCH 03/19] wip --- src/Plugins/Qt/qt_floating_search_bar.cpp | 217 ++++++++++++---------- src/Plugins/Qt/qt_floating_search_bar.hpp | 10 +- 2 files changed, 126 insertions(+), 101 deletions(-) diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 3d392cdf46..b0de9b28be 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -18,17 +18,36 @@ #include "s7_tm.hpp" #include "tm_window.hpp" -#include "widget.hpp" #include #include #include #include -#include +#include using namespace moebius; +// ---- 尺寸常量(单位:逻辑像素,经 DpiUtils::scaled 缩放) ---- // +constexpr int kBarMinHeight= 64; +constexpr int kBarWidth = 420; +constexpr int kBarRadius = 4; +constexpr int kBarMargin = 6; +constexpr int kBarSpacing = 4; + +constexpr int kBtnSize = 24; +constexpr int kBtnRadius= 12; + +constexpr int kInfoHeight = 24; +constexpr int kInfoMinWidth= 80; + +constexpr int kShadowBlur = 8; +constexpr int kShadowOffsetY= 1; +constexpr int kShadowAlpha = 30; + +constexpr int kPosRightPad= 8; +constexpr int kPosTopPad = 4; + /****************************************************************************** * QTMFloatingSearchBar ******************************************************************************/ @@ -38,72 +57,89 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) setObjectName ("floating_search_bar"); setWindowFlags (Qt::Widget); setAttribute (Qt::WA_StyledBackground); - setMinimumHeight (DpiUtils::scaled (44)); - - // 圆角、去边框、悬浮阴影 - QString btnStyle= QString ("QPushButton {" - " border: none;" - " border-radius: %1px;" - " background: transparent;" - "}" - "QPushButton:hover {" - " background: rgba(0,0,0,30);" - "}" - "QPushButton:pressed {" - " background: rgba(0,0,0,50);" - "}") - .arg (DpiUtils::scaled (12)); + setMinimumHeight (DpiUtils::scaled (kBarMinHeight)); + setStyleSheet (QString ("#floating_search_bar {" " background: #f3f3f3;" " border: none;" " border-radius: %1px;" "}") - .arg (DpiUtils::scaled (4))); + .arg (DpiUtils::scaled (kBarRadius))); + QGraphicsDropShadowEffect* shadow= new QGraphicsDropShadowEffect (this); - shadow->setBlurRadius (DpiUtils::scaled (8)); - shadow->setOffset (0, DpiUtils::scaled (1)); - shadow->setColor (QColor (0, 0, 0, 30)); + shadow->setBlurRadius (DpiUtils::scaled (kShadowBlur)); + shadow->setOffset (0, DpiUtils::scaled (kShadowOffsetY)); + shadow->setColor (QColor (0, 0, 0, kShadowAlpha)); setGraphicsEffect (shadow); - layout_= new QHBoxLayout (this); - layout_->setContentsMargins (DpiUtils::scaled (6), DpiUtils::scaled (6), - DpiUtils::scaled (6), DpiUtils::scaled (6)); - layout_->setSpacing (DpiUtils::scaled (4)); - - // 布局顺序: [输入区] [匹配计数] [上一个] [下一个] [关闭] - // 输入区由 setSearchInput 动态插入 index 0 + const QString btnStyle= QString ("QPushButton {" + " border: none;" + " border-radius: %1px;" + " background: transparent;" + "}" + "QPushButton:hover {" + " background: rgba(0,0,0,30);" + "}" + "QPushButton:pressed {" + " background: rgba(0,0,0,50);" + "}") + .arg (DpiUtils::scaled (kBtnRadius)); + + // 外层水平布局:左边 [输入区],右边 [按钮 + 匹配信息] + QHBoxLayout* mainLayout= new QHBoxLayout (this); + mainLayout->setContentsMargins ( + DpiUtils::scaled (kBarMargin), DpiUtils::scaled (kBarMargin), + DpiUtils::scaled (kBarMargin), DpiUtils::scaled (kBarMargin)); + mainLayout->setSpacing (DpiUtils::scaled (kBarSpacing)); + + // 左侧:输入区(由 setSearchInput 动态插入,占满左侧) + rowLayout_= new QHBoxLayout (); + rowLayout_->setSpacing (0); + + // 右侧:垂直布局 [按钮行] + [匹配信息] + QVBoxLayout* rightLayout= new QVBoxLayout (); + rightLayout->setSpacing (DpiUtils::scaled (4)); + + // 右侧上层:按钮行 + QHBoxLayout* btnRow= new QHBoxLayout (); + btnRow->setSpacing (DpiUtils::scaled (4)); - // 匹配计数标签 - infoLbl_= new QLabel (this); - infoLbl_->setFixedHeight (DpiUtils::scaled (24)); - infoLbl_->setMinimumWidth (DpiUtils::scaled (80)); - infoLbl_->setAlignment (Qt::AlignCenter); - infoLbl_->setText (QString::fromUtf8 ("无匹配")); - layout_->addWidget (infoLbl_); - - // 上一个按钮 QPushButton* prevBtn= new QPushButton (this); prevBtn->setIcon (QIcon (":floating-search/up.svg")); - prevBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); + prevBtn->setFixedSize (DpiUtils::scaled (kBtnSize), + DpiUtils::scaled (kBtnSize)); prevBtn->setToolTip (tr ("Previous (Shift+Enter)")); prevBtn->setStyleSheet (btnStyle); - layout_->addWidget (prevBtn); + btnRow->addWidget (prevBtn); - // 下一个按钮 QPushButton* nextBtn= new QPushButton (this); nextBtn->setIcon (QIcon (":floating-search/down.svg")); - nextBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); + nextBtn->setFixedSize (DpiUtils::scaled (kBtnSize), + DpiUtils::scaled (kBtnSize)); nextBtn->setToolTip (tr ("Next (Enter)")); nextBtn->setStyleSheet (btnStyle); - layout_->addWidget (nextBtn); + btnRow->addWidget (nextBtn); - // 关闭按钮 QPushButton* closeBtn= new QPushButton (this); closeBtn->setIcon (QIcon (":tabpage/close.svg")); - closeBtn->setFixedSize (DpiUtils::scaled (24), DpiUtils::scaled (24)); + closeBtn->setFixedSize (DpiUtils::scaled (kBtnSize), + DpiUtils::scaled (kBtnSize)); closeBtn->setToolTip (tr ("Close (Esc)")); closeBtn->setStyleSheet (btnStyle); - layout_->addWidget (closeBtn); + btnRow->addWidget (closeBtn); + + rightLayout->addLayout (btnRow); + + // 右侧下层:匹配信息 + infoLbl_= new QLabel (this); + infoLbl_->setFixedHeight (DpiUtils::scaled (kInfoHeight)); + infoLbl_->setAlignment (Qt::AlignCenter); + infoLbl_->setText (QString::fromUtf8 ("无匹配")); + rightLayout->addWidget (infoLbl_); + + // 组装:左输入(stretch=1) + 右面板 + mainLayout->addLayout (rowLayout_, 1); + mainLayout->addLayout (rightLayout); QObject::connect (nextBtn, &QPushButton::clicked, this, &QTMFloatingSearchBar::findNextRequested); @@ -118,11 +154,11 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) void QTMFloatingSearchBar::setSearchInput (QWidget* input) { if (inputQW_) { - layout_->removeWidget (inputQW_); + rowLayout_->removeWidget (inputQW_); delete inputQW_; } inputQW_= input; - layout_->insertWidget (0, input, 1); + rowLayout_->insertWidget (0, input, 1); } void @@ -139,7 +175,7 @@ QTMFloatingSearchBar::setMatchInfo (int current, int total) { } /****************************************************************************** - * 浮动搜索栏管理(独立于 QTChatTabWidget) + * 浮动搜索栏管理 ******************************************************************************/ static QTMFloatingSearchBar* g_search_bar = nullptr; @@ -156,8 +192,9 @@ class SearchBarResizeFilter : public QObject { g_search_bar->isVisible ()) { QWidget* w= qobject_cast (watched); if (w) { - int x= w->width () - g_search_bar->width () - DpiUtils::scaled (8); - int y= DpiUtils::scaled (4); + int x= w->width () - g_search_bar->width () - + DpiUtils::scaled (kPosRightPad); + int y= DpiUtils::scaled (kPosTopPad); g_search_bar->move (x, y); } } @@ -168,9 +205,11 @@ class SearchBarResizeFilter : public QObject { static SearchBarResizeFilter* g_resize_filter= nullptr; static void -position_search_bar (QWidget* content) { - int x= content->width () - g_search_bar->width () - DpiUtils::scaled (8); - int y= DpiUtils::scaled (4); +position_search_bar () { + if (!g_search_bar || !g_search_bar_parent) return; + int x= g_search_bar_parent->width () - g_search_bar->width () - + DpiUtils::scaled (kPosRightPad); + int y= DpiUtils::scaled (kPosTopPad); g_search_bar->move (x, y); } @@ -186,6 +225,31 @@ connect_search_bar_signals (QTMFloatingSearchBar* bar) { }); } +/// 创建或重建浮动栏容器,确保 attach 到 content。 +static void +ensure_search_bar (QWidget* content) { + if (g_search_bar && g_search_bar_parent == content) return; + + if (g_search_bar) { + delete g_search_bar; + g_search_bar= nullptr; + } + if (g_resize_filter) { + if (g_search_bar_parent) + g_search_bar_parent->removeEventFilter (g_resize_filter); + delete g_resize_filter; + g_resize_filter= nullptr; + } + + g_search_bar = new QTMFloatingSearchBar (content); + g_search_bar_parent= content; + g_resize_filter = new SearchBarResizeFilter (content); + content->installEventFilter (g_resize_filter); + + g_search_bar->setFixedWidth (DpiUtils::scaled (kBarWidth)); + connect_search_bar_signals (g_search_bar); +} + /****************************************************************************** * Scheme 胶水函数 ******************************************************************************/ @@ -200,30 +264,9 @@ qt_floating_search (string flag) { if (!content) return; if (flag == "true" || flag == "#t") { - // 按需创建浮动栏容器 - if (!g_search_bar || g_search_bar_parent != content) { - if (g_search_bar) { - delete g_search_bar; - g_search_bar= nullptr; - } - if (g_resize_filter) { - if (g_search_bar_parent) - g_search_bar_parent->removeEventFilter (g_resize_filter); - delete g_resize_filter; - g_resize_filter= nullptr; - } - - g_search_bar = new QTMFloatingSearchBar (content); - g_search_bar_parent= content; - g_resize_filter = new SearchBarResizeFilter (content); - content->installEventFilter (g_resize_filter); - - g_search_bar->setFixedWidth (DpiUtils::scaled (360)); - connect_search_bar_signals (g_search_bar); - } - + ensure_search_bar (content); g_search_bar->activate (); - position_search_bar (content); + position_search_bar (); } else { if (g_search_bar) g_search_bar->hide (); @@ -239,27 +282,7 @@ qt_floating_search_init (string aux_url_str) { QWidget* content= view->contentWidget (); if (!content) return; - // 确保浮动栏容器存在 - if (!g_search_bar || g_search_bar_parent != content) { - if (g_search_bar) { - delete g_search_bar; - g_search_bar= nullptr; - } - if (g_resize_filter) { - if (g_search_bar_parent) - g_search_bar_parent->removeEventFilter (g_resize_filter); - delete g_resize_filter; - g_resize_filter= nullptr; - } - - g_search_bar = new QTMFloatingSearchBar (content); - g_search_bar_parent= content; - g_resize_filter = new SearchBarResizeFilter (content); - content->installEventFilter (g_resize_filter); - - g_search_bar->setFixedWidth (DpiUtils::scaled (420)); - connect_search_bar_signals (g_search_bar); - } + ensure_search_bar (content); // 创建 texmacs_input_widget 绑定到 search-buffer url aux_url= url_system (aux_url_str); diff --git a/src/Plugins/Qt/qt_floating_search_bar.hpp b/src/Plugins/Qt/qt_floating_search_bar.hpp index d7231cb3f0..6805bceb8f 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.hpp +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -22,7 +22,9 @@ /** * 悬浮搜索栏容器。 * - * 布局: [TeXmacs 输入区] [上一个] [下一个] [关闭] [匹配计数] + * 布局: + * 上层: [TeXmacs 输入区] [上一个] [下一个] [关闭] + * 下层: [TeXmacs 输入区] [匹配计数] * 输入区是嵌入的 texmacs_input_widget,绑定到 search-buffer, * 搜索逻辑与底部搜索面板完全一致。 */ @@ -45,9 +47,9 @@ class QTMFloatingSearchBar : public QWidget { void closeRequested (); private: - QHBoxLayout* layout_ = nullptr; - QWidget* inputQW_= nullptr; ///< 嵌入的 TeXmacs 输入 QWidget - QLabel* infoLbl_= nullptr; ///< 匹配计数标签 + QHBoxLayout* rowLayout_= nullptr; ///< 上层水平布局(输入+按钮) + QWidget* inputQW_ = nullptr; ///< 嵌入的 TeXmacs 输入 QWidget + QLabel* infoLbl_ = nullptr; ///< 匹配计数标签 }; /// Scheme 胶水函数:显示 ("true"/"#t") 或隐藏悬浮搜索栏。 From 6758bc696c82a4a428b0d8349ca061c028c82391 Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 27 May 2026 22:43:25 +0800 Subject: [PATCH 04/19] wip --- TeXmacs/progs/generic/search-widgets.scm | 10 +++++----- src/Plugins/Qt/qt_floating_search_bar.cpp | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index f509cfc693..ae90a6df54 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -1130,9 +1130,7 @@ (set-search-filter) (set! search-filter-out? #f) (qt-floating-search-init (url->string aux)) - (qt-floating-search "true") - (with-buffer target-buf - (perform-search*)))) + (qt-floating-search "true"))) (define (chat-tab-perform-search) (when (and chat-tab-search-target chat-tab-search-aux @@ -1145,7 +1143,6 @@ (tm-define (chat-tab-search-next forward?) (when (and chat-tab-search-target chat-tab-search-aux) - (chat-tab-perform-search) (with-buffer chat-tab-search-target (search-next-match forward? chat-tab-search-target)))) @@ -1703,7 +1700,10 @@ (string->url (string-append "tmfs://chat-message-" sid)))) (chat-tab-search-init (string->url (string-append "tmfs://chat-message-" sid)))) - ((not (string-starts? (url->system buf) "tmfs:")) + ((string-starts? (url->system buf) "tmfs:") + ;; 其他 tmfs:// 缓冲区不支持搜索 + (noop)) + (else (set! search-replace-text (cond ((in-math?) "Only search in math mode") ((in-prog?) "Only search in Program mode") diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index b0de9b28be..cb9a8ef38f 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -246,6 +246,13 @@ ensure_search_bar (QWidget* content) { g_resize_filter = new SearchBarResizeFilter (content); content->installEventFilter (g_resize_filter); + // content 被 Qt 销毁时自动清理全局指针,避免悬空 + QObject::connect (content, &QObject::destroyed, [] () { + g_search_bar = nullptr; + g_search_bar_parent= nullptr; + g_resize_filter = nullptr; + }); + g_search_bar->setFixedWidth (DpiUtils::scaled (kBarWidth)); connect_search_bar_signals (g_search_bar); } @@ -285,10 +292,11 @@ qt_floating_search_init (string aux_url_str) { ensure_search_bar (content); // 创建 texmacs_input_widget 绑定到 search-buffer - url aux_url= url_system (aux_url_str); - tree doc (DOCUMENT, ""); - tree sty = compound ("style", tree (TUPLE, "generic")); - widget tw = texmacs_input_widget (doc, sty, aux_url); + url aux_url= url_system (aux_url_str); + tree doc (DOCUMENT, ""); + tree sty= compound ("style", tree (TUPLE, "generic")); + widget tw = texmacs_input_widget (doc, sty, aux_url); + if (is_nil (tw)) return; QWidget* inputW= concrete (tw)->as_qwidget (); if (inputW) { inputW->setStyleSheet ("QWidget {" From e2dd5c400dae7cb9679735ce04af84366e26944e Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 27 May 2026 23:03:03 +0800 Subject: [PATCH 05/19] wip --- TeXmacs/progs/generic/search-widgets.scm | 113 +++++++++++++--------- src/Plugins/Qt/qt_floating_search_bar.cpp | 5 + src/Plugins/Qt/qt_floating_search_bar.hpp | 3 + src/Scheme/L5/glue_widget.lua | 9 ++ 4 files changed, 86 insertions(+), 44 deletions(-) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index ae90a6df54..b5fc7a06ad 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -111,6 +111,15 @@ (when isreplace? (set! isreplace? #f) ) ;when + ;; 更新浮动搜索栏的匹配计数 + (when chat-tab-search-target + (if (== index-str "") + (qt-floating-search-set-match-info 0 0) + (let* ((parts (string-split index-str #\/)) + (cur (string->number (car parts))) + (tot (string->number (cadr parts)))) + (when (and cur tot) + (qt-floating-search-set-match-info cur tot))))) (if (== index-str "") (set-auxiliary-widget-title (translate search-replace-text)) (set-auxiliary-widget-title (string-append (translate search-replace-text) " (" index-str ")") @@ -158,20 +167,24 @@ ;; ---- ;; 此函数用于管理搜索辅助缓冲区的生命周期,确保每个主文档视图有唯一的搜索缓冲区。 (tm-define (search-buffer) - (with u - (current-buffer) - (if (and (url-rooted-tmfs? u) - (== (url-head (url-head u)) (string->url "tmfs://aux/search")) - ) ;and - u - (string->url (string-append "tmfs://aux/search/" - (md5 (url->string (current-view-url))) - "/" - (url->string (url-tail (current-window))) - ) ;string-append - ) ;string->url - ) ;if - ) ;with + ;; chat tab 搜索激活期间,直接返回保存的 aux buffer + (if chat-tab-search-active? + chat-tab-search-aux + (with u + (current-buffer) + (if (and (url-rooted-tmfs? u) + (== (url-head (url-head u)) (string->url "tmfs://aux/search")) + ) ;and + u + (string->url (string-append "tmfs://aux/search/" + (md5 (url->string (current-view-url))) + "/" + (url->string (url-tail (current-window))) + ) ;string-append + ) ;string->url + ) ;if + ) ;with + ) ;if ) ;tm-define ;; replace-buffer @@ -329,29 +342,36 @@ ) ;tm-define (tm-define (master-buffer) - (and (buffer-exists? (search-buffer)) - (with mas - (buffer-get-master (search-buffer)) - (cond ((nnull? (buffer->windows mas)) mas) - ((in? search-window (window-list)) - (buffer-set-master (search-buffer) (window->buffer search-window)) - (with-buffer (buffer-get-master (search-buffer)) - (set-search-reference (cursor-path)) - (set-search-filter) - ) ;with-buffer - (master-buffer) - ) ; - ((nnull? (window-list)) - (set! search-window (car (window-list))) - (master-buffer) - ) ; - (else #f) - ) ;cond - ) ;with - ) ;and + ;; chat tab 搜索激活期间,直接返回保存的 target buffer + (if chat-tab-search-active? + chat-tab-search-target + (and (buffer-exists? (search-buffer)) + (with mas + (buffer-get-master (search-buffer)) + (cond ((nnull? (buffer->windows mas)) mas) + ((in? search-window (window-list)) + (buffer-set-master (search-buffer) (window->buffer search-window)) + (with-buffer (buffer-get-master (search-buffer)) + (set-search-reference (cursor-path)) + (set-search-filter) + ) ;with-buffer + (master-buffer) + ) ; + ((nnull? (window-list)) + (set! search-window (car (window-list))) + (master-buffer) + ) ; + (else #f) + ) ;cond + ) ;with + ) ;and + ) ;if ) ;tm-define -(tm-define (inside-search-buffer?) (== (current-buffer) (search-buffer))) +(tm-define (inside-search-buffer?) + (if chat-tab-search-active? + (== (current-buffer) chat-tab-search-aux) + (== (current-buffer) (search-buffer)))) (tm-define (inside-replace-buffer?) (== (current-buffer) (replace-buffer))) @@ -418,15 +438,17 @@ (define (accept-search-result? p) (or (== (get-init "mode") "src") - (let* ((buf (buffer-tree)) - (rel (path-strip (cDr p) (tree->path buf))) - (initial (cons 'attr (get-main-attrs get-init))) - (old-env (get-search-filter)) - (new-env (tree-descendant-env* buf rel initial)) - ) ; - ;; (display* p " ~> " new-env "\n") - (check-same? (tm-children new-env) (tm-children old-env)) - ) ;let* + (catch #t + (lambda () + (let* ((buf (buffer-tree)) + (rel (path-strip (cDr p) (tree->path buf)))) + (if (not rel) #t + (let* ((initial (cons 'attr (get-main-attrs get-init))) + (old-env (get-search-filter)) + (new-env (tree-descendant-env* buf rel initial))) + (if (not new-env) #t + (check-same? (tm-children new-env) (tm-children old-env))))))) + (lambda (key msg . rest) #t)) ) ;or ) ;define @@ -1118,11 +1140,13 @@ (define chat-tab-search-target #f) (define chat-tab-search-aux #f) +(define chat-tab-search-active? #f) (define (chat-tab-search-init target-buf) (set! chat-tab-search-target target-buf) (let ((aux (search-buffer))) (set! chat-tab-search-aux aux) + (set! chat-tab-search-active? #t) (buffer-set-master aux target-buf) (set-search-window-state #t #t) (with-buffer target-buf @@ -1153,6 +1177,7 @@ (with-buffer chat-tab-search-target (cancel-alt-selection "alternate")) (set-search-window-state #f #f) + (set! chat-tab-search-active? #f) (set! chat-tab-search-target #f) (set! chat-tab-search-aux #f))) diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index cb9a8ef38f..f3691b9726 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -309,3 +309,8 @@ qt_floating_search_init (string aux_url_str) { g_search_bar->setSearchInput (inputW); } } + +void +qt_floating_search_set_match_info (int current, int total) { + if (g_search_bar) g_search_bar->setMatchInfo (current, total); +} diff --git a/src/Plugins/Qt/qt_floating_search_bar.hpp b/src/Plugins/Qt/qt_floating_search_bar.hpp index 6805bceb8f..939a8ff256 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.hpp +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -59,4 +59,7 @@ void qt_floating_search (string flag); /// 并嵌入浮动搜索栏。 void qt_floating_search_init (string aux_url_str); +/// Scheme 胶水函数:更新浮动搜索栏的匹配计数显示。 +void qt_floating_search_set_match_info (int current, int total); + #endif // QT_FLOATING_SEARCH_BAR_HPP diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua index 5999d68cc9..fe6b3735a4 100644 --- a/src/Scheme/L5/glue_widget.lua +++ b/src/Scheme/L5/glue_widget.lua @@ -542,6 +542,15 @@ function main() arg_list = { "string" } + }, + { + scm_name = "qt-floating-search-set-match-info", + cpp_name = "qt_floating_search_set_match_info", + ret_type = "void", + arg_list = { + "int", + "int" + } } } } From 9d0ed89adb837f9e795d82ecc88124d499f4feae Mon Sep 17 00:00:00 2001 From: Yuki Date: Thu, 28 May 2026 16:30:31 +0800 Subject: [PATCH 06/19] fix --- .../images/floating-search/down-white.svg | 12 ++ .../misc/images/floating-search/up-white.svg | 12 ++ TeXmacs/misc/images/images.qrc | 3 + TeXmacs/progs/generic/search-widgets.scm | 2 + devel/1042.md | 124 +++++++++++++++++- src/Plugins/Qt/qt_chat_controller.cpp | 7 + src/Plugins/Qt/qt_chat_controller.hpp | 3 - src/Scheme/L5/glue_widget.lua | 10 ++ 8 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 TeXmacs/misc/images/floating-search/down-white.svg create mode 100644 TeXmacs/misc/images/floating-search/up-white.svg diff --git a/TeXmacs/misc/images/floating-search/down-white.svg b/TeXmacs/misc/images/floating-search/down-white.svg new file mode 100644 index 0000000000..a8bc6f93eb --- /dev/null +++ b/TeXmacs/misc/images/floating-search/down-white.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/floating-search/up-white.svg b/TeXmacs/misc/images/floating-search/up-white.svg new file mode 100644 index 0000000000..74ec95b068 --- /dev/null +++ b/TeXmacs/misc/images/floating-search/up-white.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/images.qrc b/TeXmacs/misc/images/images.qrc index 3276c7b7e1..d3215e4865 100644 --- a/TeXmacs/misc/images/images.qrc +++ b/TeXmacs/misc/images/images.qrc @@ -46,6 +46,9 @@ pdf-reader/open-white.svg pdf-reader/screenshot-white.svg + floating-search/down-white.svg + floating-search/up-white.svg + tutorial/ocr-tutorial.gif tutorial/magic-paste-tutorial.gif diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index b5fc7a06ad..71909ffbf2 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -1153,6 +1153,8 @@ (set-search-reference (cursor-path))) (set-search-filter) (set! search-filter-out? #f) + (qt-floating-search-set-callbacks + "(chat-tab-search-next #t)" "(chat-tab-search-next #f)" "(chat-tab-search-close)") (qt-floating-search-init (url->string aux)) (qt-floating-search "true"))) diff --git a/devel/1042.md b/devel/1042.md index 32cd479fa8..ccf5539938 100644 --- a/devel/1042.md +++ b/devel/1042.md @@ -6,8 +6,11 @@ ## 2 任务相关的代码文件 - `TeXmacs/progs/generic/search-widgets.scm` - 搜索/替换入口函数 +- `src/Plugins/Qt/qt_floating_search_bar.hpp` - 悬浮搜索栏通用组件 +- `src/Plugins/Qt/qt_floating_search_bar.cpp` - 悬浮搜索栏实现与管理器 +- `src/Plugins/Qt/qt_chat_controller.cpp` - Chat controller(注册 parent provider) - `src/Plugins/Qt/qt_chat_tab_widget.hpp` - Chat tab widget 头文件 -- `src/Plugins/Qt/qt_chat_tab_widget.cpp` - Chat tab widget 实现 +- `src/Scheme/L5/glue_widget.lua` - Scheme 胶水函数声明 ## 3 如何测试 @@ -37,7 +40,7 @@ gf fmt --changed-since=main ## 5 What Chat Tab 的 message buffer 是嵌入式 TeXmacs widget,没有标准 `tm_window`,无法使用主窗口的 auxiliary-widget 侧边栏搜索机制。需要为 Chat Tab 实现独立的搜索功能。 -1. 在 QTChatTabWidget 右上角实现 VSCode 风格的悬浮搜索栏(公共组件类) +1. 实现可复用的悬浮搜索栏 Qt 组件 `QTMFloatingSearchBar`,支持任意 QWidget parent 2. 搜索栏包含:输入框、上一个/下一个按钮、关闭按钮、匹配计数 3. Scheme 端新增 chat-tab 专用搜索初始化和导航函数 4. 修改 `interactive-search` 在嵌入式 chat buffer 时使用新的搜索 UI @@ -47,8 +50,115 @@ Chat Tab 的 message buffer 是嵌入式 TeXmacs widget,没有标准 `tm_windo commit [0228] 通过 blanket `tmfs:` 检查禁用了所有 tmfs:// 缓冲区的搜索来避免 crash。用户需要在 Chat Tab 的消息输出框中搜索对话内容。Chat Tab 的嵌入式 buffer 不支持 auxiliary-widget 机制,需要独立的搜索 UI。 ## 7 How -1. 实现一个可复用的悬浮搜索栏 Qt 组件(`QTMFloatingSearchBar`),包含输入框、导航按钮、关闭按钮、匹配计数标签 -2. 在 QTChatTabWidget 的 content 区域右上角放置该组件,初始隐藏 -3. Scheme 端新增 `chat-tab-search-init`、`chat-tab-perform-search`、`chat-tab-search-next`、`chat-tab-search-close` 函数 -4. C++ 按钮回调通过 `eval_scheme` 调用 Scheme 搜索函数 -5. `interactive-search` 检测到嵌入式 chat buffer 时调 C++ glue 显示搜索栏并初始化搜索 +1. 实现通用悬浮搜索栏 Qt 组件 `QTMFloatingSearchBar`,包含输入框、导航按钮、关闭按钮、匹配计数标签 +2. 通过 `QHash` 按 parent widget 管理实例,parent 销毁时自动清理 +3. `QTMFloatingSearchBar` 自身安装 eventFilter 处理 parent resize,自动重新定位到右上角 +4. Scheme 端新增 `chat-tab-search-init`、`chat-tab-perform-search`、`chat-tab-search-next`、`chat-tab-search-close` 函数 +5. C++ 按钮回调通过 `eval_scheme` 调用 Scheme 搜索函数,回调命令由 `qt-floating-search-set-callbacks` 注入,不再硬编码 +6. `interactive-search` 检测到嵌入式 chat buffer 时调 C++ glue 显示搜索栏并初始化搜索 + +--- + +## 8 组件复用方法(供后续开发者参考) + +`QTMFloatingSearchBar` 已设计为**通用组件**,不依赖 ChatController,可 attach 到任意 `QWidget`。 + +### 8.1 复用架构 + +``` +┌─────────────────────────────────────┐ +│ Scheme 侧 (search-widgets.scm) │ +│ ── (qt-floating-search-set-callbacks ...) +│ ── (qt-floating-search-init aux-url) +│ ── (qt-floating-search "true") +└─────────────┬───────────────────────┘ + │ glue_widget.lua +┌─────────────▼───────────────────────┐ +│ C++ 兼容层 (qt_floating_search_bar.cpp) +│ ── 通过注册的 parent provider 获取默认 parent +└─────────────┬───────────────────────┘ + │ 底层通用 API +┌─────────────▼───────────────────────┐ +│ QTMFloatingSearchBar 管理器 │ +│ ── QHash 管理生命周期 │ +└─────────────────────────────────────┘ +``` + +### 8.2 在新页面中使用(以 XXX Tab 为例) + +#### 步骤 1:C++ 侧注册 parent provider + +在负责该页面的 Controller 初始化时(如 `createView` 中),注册一个返回 content widget 的 provider: + +```cpp +#include "qt_floating_search_bar.hpp" + +void +XXXController::createView (QWidget* parent) { + view_ = new XXXTabWidget (...); + // ... 其他初始化 ... + + // 注册浮动搜索栏的 parent provider + qt_floating_search_set_parent_provider ([this] () -> QWidget* { + if (!view_) return nullptr; + return view_->contentWidget (); // 或任意目标 QWidget + }); +} +``` + +> **注意**:同一时间只能有一个活跃的 provider。如果多个页面都想使用浮动搜索栏,需要确保切换页面时重新注册 provider,或直接调用底层通用 API(见 8.3)。 + +#### 步骤 2:Scheme 侧调用胶水函数 + +```scheme +;; 设置按钮对应的 Scheme 回调命令 +(qt-floating-search-set-callbacks + "(my-page-search-next #t)" + "(my-page-search-next #f)" + "(my-page-search-close)") + +;; 初始化:创建 texmacs_input_widget 并嵌入搜索栏 +(qt-floating-search-init (url->string aux-buffer-url)) + +;; 显示搜索栏 +(qt-floating-search "true") + +;; 隐藏搜索栏 +(qt-floating-search "false") + +;; 更新匹配计数(通常在搜索回调中调用) +(qt-floating-search-set-match-info 3 10) +``` + +#### 步骤 3:实现对应的 Scheme 搜索函数 + +参考 `search-widgets.scm` 中的 `chat-tab-search-init` / `chat-tab-search-next` / `chat-tab-search-close` 实现即可。核心逻辑: +- `init`:绑定 search-buffer 到目标 buffer,设置搜索起点 +- `next`:调用 `search-next-match` 在目标 buffer 中跳转 +- `close`:清除高亮、重置搜索状态、隐藏搜索栏 + +### 8.3 直接使用底层 C++ API(不走 Scheme 胶水层) + +如果场景更适合纯 C++ 控制(如非 TeXmacs buffer 的纯 Qt 页面),可以直接调用: + +```cpp +#include "qt_floating_search_bar.hpp" + +// 在某个 QWidget* content 上创建/显示搜索栏 +qt_floating_search_bar_show (content, true); + +// 初始化输入框(需要 aux_url 作为 texmacs_input_widget 的绑定) +bool ok = qt_floating_search_bar_init (content, "tmfs://aux/search/..."); + +// 设置匹配计数 +qt_floating_search_bar_set_match_info (content, 3, 10); + +// 设置 Scheme 回调(可选,如果不需要 Scheme 交互可跳过) +qt_floating_search_bar_set_callbacks ( + content, "(my-next)", "(my-prev)", "(my-close)"); + +// 手动销毁(parent 销毁时会自动清理,通常不需要手动调用) +qt_floating_search_bar_destroy (content); +``` + +底层 API 的 parent 参数可以是任意 `QWidget*`,组件会自动安装 eventFilter 处理 resize 并定位到 parent 的右上角。 diff --git a/src/Plugins/Qt/qt_chat_controller.cpp b/src/Plugins/Qt/qt_chat_controller.cpp index 9cc701de62..64ac343820 100644 --- a/src/Plugins/Qt/qt_chat_controller.cpp +++ b/src/Plugins/Qt/qt_chat_controller.cpp @@ -11,6 +11,7 @@ #include "qt_chat_controller.hpp" #include "qt_chat_tab_widget.hpp" +#include "qt_floating_search_bar.hpp" #include "new_buffer.hpp" #include "s7_tm.hpp" @@ -139,6 +140,12 @@ ChatController::createView (QWidget* parent, qt_tm_widget_rep* tm) { } } + // 6. 注册浮动搜索栏的 parent provider + qt_floating_search_set_parent_provider ([this] () -> QWidget* { + if (!view_) return nullptr; + return view_->contentWidget (); + }); + return view_; } diff --git a/src/Plugins/Qt/qt_chat_controller.hpp b/src/Plugins/Qt/qt_chat_controller.hpp index 66aa110500..06d51c6482 100644 --- a/src/Plugins/Qt/qt_chat_controller.hpp +++ b/src/Plugins/Qt/qt_chat_controller.hpp @@ -133,9 +133,6 @@ class ChatController : public QObject { */ void destroyView (); -public: - QTChatTabWidget* view () const { return view_; } - private: QTChatTabWidget* view_= nullptr; ///< View 指针,由 createView 创建 ChatSessionManager sessionManager_; ///< 会话管理器 diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua index fe6b3735a4..4e1756ecd9 100644 --- a/src/Scheme/L5/glue_widget.lua +++ b/src/Scheme/L5/glue_widget.lua @@ -551,6 +551,16 @@ function main() "int", "int" } + }, + { + scm_name = "qt-floating-search-set-callbacks", + cpp_name = "qt_floating_search_set_callbacks", + ret_type = "void", + arg_list = { + "string", + "string", + "string" + } } } } From ab0d9895798a642b10c54f5d0f835db4283b03e4 Mon Sep 17 00:00:00 2001 From: Yuki Date: Thu, 28 May 2026 16:43:04 +0800 Subject: [PATCH 07/19] css --- TeXmacs/misc/themes/liii-night.css | 47 +++ TeXmacs/misc/themes/liii.css | 46 +++ TeXmacs/plugins/lang/dic/en_US/zh_CN.scm | 1 + src/Plugins/Qt/qt_floating_search_bar.cpp | 365 +++++++++++++--------- src/Plugins/Qt/qt_floating_search_bar.hpp | 84 ++++- 5 files changed, 376 insertions(+), 167 deletions(-) diff --git a/TeXmacs/misc/themes/liii-night.css b/TeXmacs/misc/themes/liii-night.css index b1e75cbd4a..68ffa62261 100644 --- a/TeXmacs/misc/themes/liii-night.css +++ b/TeXmacs/misc/themes/liii-night.css @@ -1491,3 +1491,50 @@ QPushButton#chat-tab-conversation-btn:checked { QWidget#centralWidget QPushButton#chat-tab-conversation-btn:checked { background-color: #1a3a5a; } + +/**************************************************************************** +* 悬浮搜索栏样式 +****************************************************************************/ + +QWidget#centralWidget QWidget#floating_search_bar { + background: #2d2d2d; + border: none; +} + +QWidget#floating_search_bar QToolButton { + border: none; + background: transparent; +} + +QWidget#floating_search_bar QToolButton:hover { + background: rgba(255, 255, 255, 30); +} + +QWidget#floating_search_bar QToolButton:pressed { + background: rgba(255, 255, 255, 50); +} + +QWidget#floating_search_bar QToolButton#floating-search-prev { + qproperty-icon: url(":/floating-search/up-white.svg"); +} + +QWidget#floating_search_bar QToolButton#floating-search-next { + qproperty-icon: url(":/floating-search/down-white.svg"); +} + +QWidget#floating_search_bar QToolButton#floating-search-close { + qproperty-icon: url(":/tabpage/close-white.svg"); +} + +QWidget#floating_search_bar QWidget#floating-search-input { + border: 1px solid #555555; +} + +QWidget#floating_search_bar QWidget#floating-search-input:focus { + border: 1px solid #215a6a; +} + +QWidget#floating_search_bar QLabel#floating-search-info { + background: transparent; + color: #ffffff; +} diff --git a/TeXmacs/misc/themes/liii.css b/TeXmacs/misc/themes/liii.css index 0a9fd19a7d..9640c6f3fa 100644 --- a/TeXmacs/misc/themes/liii.css +++ b/TeXmacs/misc/themes/liii.css @@ -1410,3 +1410,49 @@ QPushButton#chat-tab-conversation-btn:checked { font-weight: 600; background-color: #e8eefc; } + +/**************************************************************************** +* 悬浮搜索栏样式 +****************************************************************************/ +QWidget#floating_search_bar { + background: #f3f3f3; + border: none; +} + +QWidget#floating_search_bar QToolButton { + border: none; + background: transparent; +} + +QWidget#floating_search_bar QToolButton:hover { + background: rgba(0, 0, 0, 30); +} + +QWidget#floating_search_bar QToolButton:pressed { + background: rgba(0, 0, 0, 50); +} + +QWidget#floating_search_bar QToolButton#floating-search-prev { + qproperty-icon: url(":/floating-search/up.svg"); +} + +QWidget#floating_search_bar QToolButton#floating-search-next { + qproperty-icon: url(":/floating-search/down.svg"); +} + +QWidget#floating_search_bar QToolButton#floating-search-close { + qproperty-icon: url(":/tabpage/close.svg"); +} + +QWidget#floating_search_bar QWidget#floating-search-input { + border: 1px solid #d0d0d0; +} + +QWidget#floating_search_bar QWidget#floating-search-input:focus { + border: 1px solid #215a6a; +} + +QWidget#floating_search_bar QLabel#floating-search-info { + background: transparent; + color: #2c2c2c; +} diff --git a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm index c18a0c313c..50ca02eefd 100644 --- a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm +++ b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm @@ -1608,6 +1608,7 @@ ("no line break" "禁止换行") ("no matches found for" "") ("no matches found" "") +("No matches" "无匹配") ("no more matches for" "") ("no more redo information available" "") ("no more undo information available" "") diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index f3691b9726..50dbdbaaeb 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -10,8 +10,6 @@ ******************************************************************************/ #include "qt_floating_search_bar.hpp" -#include "qt_chat_controller.hpp" -#include "qt_chat_tab_widget.hpp" #include "qt_dpi_utils.hpp" #include "qt_utilities.hpp" #include "qt_widget.hpp" @@ -23,12 +21,14 @@ #include #include +#include #include +#include #include using namespace moebius; -// ---- 尺寸常量(单位:逻辑像素,经 DpiUtils::scaled 缩放) ---- // +// ---- 尺寸常量(逻辑像素,经 DpiUtils 缩放) ---- constexpr int kBarMinHeight= 64; constexpr int kBarWidth = 420; constexpr int kBarRadius = 4; @@ -38,8 +38,7 @@ constexpr int kBarSpacing = 4; constexpr int kBtnSize = 24; constexpr int kBtnRadius= 12; -constexpr int kInfoHeight = 24; -constexpr int kInfoMinWidth= 80; +constexpr int kInfoHeight= 24; constexpr int kShadowBlur = 8; constexpr int kShadowOffsetY= 1; @@ -49,7 +48,7 @@ constexpr int kPosRightPad= 8; constexpr int kPosTopPad = 4; /****************************************************************************** - * QTMFloatingSearchBar + * QTMFloatingSearchBar 实现 ******************************************************************************/ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) @@ -59,34 +58,20 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) setAttribute (Qt::WA_StyledBackground); setMinimumHeight (DpiUtils::scaled (kBarMinHeight)); + // border-radius 由代码动态计算,支持 DPI 缩放 setStyleSheet (QString ("#floating_search_bar {" - " background: #f3f3f3;" - " border: none;" " border-radius: %1px;" "}") .arg (DpiUtils::scaled (kBarRadius))); - QGraphicsDropShadowEffect* shadow= new QGraphicsDropShadowEffect (this); + auto* shadow= new QGraphicsDropShadowEffect (this); shadow->setBlurRadius (DpiUtils::scaled (kShadowBlur)); shadow->setOffset (0, DpiUtils::scaled (kShadowOffsetY)); shadow->setColor (QColor (0, 0, 0, kShadowAlpha)); setGraphicsEffect (shadow); - const QString btnStyle= QString ("QPushButton {" - " border: none;" - " border-radius: %1px;" - " background: transparent;" - "}" - "QPushButton:hover {" - " background: rgba(0,0,0,30);" - "}" - "QPushButton:pressed {" - " background: rgba(0,0,0,50);" - "}") - .arg (DpiUtils::scaled (kBtnRadius)); - // 外层水平布局:左边 [输入区],右边 [按钮 + 匹配信息] - QHBoxLayout* mainLayout= new QHBoxLayout (this); + auto* mainLayout= new QHBoxLayout (this); mainLayout->setContentsMargins ( DpiUtils::scaled (kBarMargin), DpiUtils::scaled (kBarMargin), DpiUtils::scaled (kBarMargin), DpiUtils::scaled (kBarMargin)); @@ -97,68 +82,83 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) rowLayout_->setSpacing (0); // 右侧:垂直布局 [按钮行] + [匹配信息] - QVBoxLayout* rightLayout= new QVBoxLayout (); + auto* rightLayout= new QVBoxLayout (); rightLayout->setSpacing (DpiUtils::scaled (4)); // 右侧上层:按钮行 - QHBoxLayout* btnRow= new QHBoxLayout (); + auto* btnRow= new QHBoxLayout (); btnRow->setSpacing (DpiUtils::scaled (4)); - QPushButton* prevBtn= new QPushButton (this); - prevBtn->setIcon (QIcon (":floating-search/up.svg")); + const QString btnRadiusStyle= + QString ( + "QToolButton { border-radius: %1px; padding: 0px; margin: 0px; }") + .arg (DpiUtils::scaled (kBtnRadius)); + + auto* prevBtn= new QToolButton (this); + prevBtn->setObjectName ("floating-search-prev"); prevBtn->setFixedSize (DpiUtils::scaled (kBtnSize), DpiUtils::scaled (kBtnSize)); - prevBtn->setToolTip (tr ("Previous (Shift+Enter)")); - prevBtn->setStyleSheet (btnStyle); + prevBtn->setToolTip (qt_translate ("Previous (Shift+Enter)")); + prevBtn->setStyleSheet (btnRadiusStyle); btnRow->addWidget (prevBtn); - QPushButton* nextBtn= new QPushButton (this); - nextBtn->setIcon (QIcon (":floating-search/down.svg")); + auto* nextBtn= new QToolButton (this); + nextBtn->setObjectName ("floating-search-next"); nextBtn->setFixedSize (DpiUtils::scaled (kBtnSize), DpiUtils::scaled (kBtnSize)); - nextBtn->setToolTip (tr ("Next (Enter)")); - nextBtn->setStyleSheet (btnStyle); + nextBtn->setToolTip (qt_translate ("Next (Enter)")); + nextBtn->setStyleSheet (btnRadiusStyle); btnRow->addWidget (nextBtn); - QPushButton* closeBtn= new QPushButton (this); - closeBtn->setIcon (QIcon (":tabpage/close.svg")); + auto* closeBtn= new QToolButton (this); + closeBtn->setObjectName ("floating-search-close"); closeBtn->setFixedSize (DpiUtils::scaled (kBtnSize), DpiUtils::scaled (kBtnSize)); - closeBtn->setToolTip (tr ("Close (Esc)")); - closeBtn->setStyleSheet (btnStyle); + closeBtn->setToolTip (qt_translate ("Close (Esc)")); + closeBtn->setStyleSheet (btnRadiusStyle); btnRow->addWidget (closeBtn); rightLayout->addLayout (btnRow); // 右侧下层:匹配信息 infoLbl_= new QLabel (this); + infoLbl_->setObjectName ("floating-search-info"); infoLbl_->setFixedHeight (DpiUtils::scaled (kInfoHeight)); infoLbl_->setAlignment (Qt::AlignCenter); - infoLbl_->setText (QString::fromUtf8 ("无匹配")); + infoLbl_->setText (qt_translate ("No matches")); rightLayout->addWidget (infoLbl_); // 组装:左输入(stretch=1) + 右面板 mainLayout->addLayout (rowLayout_, 1); mainLayout->addLayout (rightLayout); - QObject::connect (nextBtn, &QPushButton::clicked, this, - &QTMFloatingSearchBar::findNextRequested); - QObject::connect (prevBtn, &QPushButton::clicked, this, - &QTMFloatingSearchBar::findPreviousRequested); - QObject::connect (closeBtn, &QPushButton::clicked, this, - &QTMFloatingSearchBar::closeRequested); + connect (nextBtn, &QToolButton::clicked, this, + &QTMFloatingSearchBar::findNextRequested); + connect (prevBtn, &QToolButton::clicked, this, + &QTMFloatingSearchBar::findPreviousRequested); + connect (closeBtn, &QToolButton::clicked, this, + &QTMFloatingSearchBar::closeRequested); + + if (parent) parent->installEventFilter (this); hide (); } +QTMFloatingSearchBar::~QTMFloatingSearchBar () { + if (parent ()) parent ()->removeEventFilter (this); +} + void QTMFloatingSearchBar::setSearchInput (QWidget* input) { if (inputQW_) { rowLayout_->removeWidget (inputQW_); - delete inputQW_; + inputQW_->deleteLater (); } inputQW_= input; - rowLayout_->insertWidget (0, input, 1); + if (input) { + input->setObjectName ("floating-search-input"); + rowLayout_->insertWidget (0, input, 1); + } } void @@ -170,147 +170,206 @@ QTMFloatingSearchBar::activate () { void QTMFloatingSearchBar::setMatchInfo (int current, int total) { - if (total == 0) infoLbl_->setText (tr ("No matches")); - else infoLbl_->setText (tr ("%1 of %2").arg (current).arg (total)); + if (total == 0) infoLbl_->setText (qt_translate ("No matches")); + else infoLbl_->setText (qt_translate ("%1 of %2").arg (current).arg (total)); } -/****************************************************************************** - * 浮动搜索栏管理 - ******************************************************************************/ +void +QTMFloatingSearchBar::setSchemeCallbacks (const string& next_cmd, + const string& prev_cmd, + const string& close_cmd) { + next_cmd_ = next_cmd; + prev_cmd_ = prev_cmd; + close_cmd_= close_cmd; + if (!callbacksConnected_) { + connectSignals (); + callbacksConnected_= true; + } +} -static QTMFloatingSearchBar* g_search_bar = nullptr; -static QWidget* g_search_bar_parent= nullptr; - -// 事件过滤器:parent resize 时重新定位搜索栏 -class SearchBarResizeFilter : public QObject { -public: - SearchBarResizeFilter (QObject* parent) : QObject (parent) {} - -protected: - bool eventFilter (QObject* watched, QEvent* event) override { - if (event->type () == QEvent::Resize && g_search_bar && - g_search_bar->isVisible ()) { - QWidget* w= qobject_cast (watched); - if (w) { - int x= w->width () - g_search_bar->width () - - DpiUtils::scaled (kPosRightPad); - int y= DpiUtils::scaled (kPosTopPad); - g_search_bar->move (x, y); - } - } - return QObject::eventFilter (watched, event); +void +QTMFloatingSearchBar::connectSignals () { + if (!is_empty (next_cmd_)) { + connect (this, &QTMFloatingSearchBar::findNextRequested, this, + [this] () { eval_scheme (next_cmd_); }); } -}; + if (!is_empty (prev_cmd_)) { + connect (this, &QTMFloatingSearchBar::findPreviousRequested, this, + [this] () { eval_scheme (prev_cmd_); }); + } + if (!is_empty (close_cmd_)) { + connect (this, &QTMFloatingSearchBar::closeRequested, this, [this] () { + eval_scheme (close_cmd_); + hide (); + }); + } +} -static SearchBarResizeFilter* g_resize_filter= nullptr; +bool +QTMFloatingSearchBar::eventFilter (QObject* watched, QEvent* event) { + if (event->type () == QEvent::Resize && watched == parent () && + isVisible ()) { + reposition (); + } + return QWidget::eventFilter (watched, event); +} + +void +QTMFloatingSearchBar::showEvent (QShowEvent* event) { + QWidget::showEvent (event); + reposition (); +} -static void -position_search_bar () { - if (!g_search_bar || !g_search_bar_parent) return; - int x= g_search_bar_parent->width () - g_search_bar->width () - - DpiUtils::scaled (kPosRightPad); +void +QTMFloatingSearchBar::reposition () { + QWidget* p= qobject_cast (parent ()); + if (!p) return; + int x= p->width () - width () - DpiUtils::scaled (kPosRightPad); int y= DpiUtils::scaled (kPosTopPad); - g_search_bar->move (x, y); + move (x, y); } -static void -connect_search_bar_signals (QTMFloatingSearchBar* bar) { - QObject::connect (bar, &QTMFloatingSearchBar::findNextRequested, bar, - [] () { eval_scheme ("(chat-tab-search-next #t)"); }); - QObject::connect (bar, &QTMFloatingSearchBar::findPreviousRequested, bar, - [] () { eval_scheme ("(chat-tab-search-next #f)"); }); - QObject::connect (bar, &QTMFloatingSearchBar::closeRequested, bar, [] () { - eval_scheme ("(chat-tab-search-close)"); - if (g_search_bar) g_search_bar->hide (); +/****************************************************************************** + * 搜索栏管理器 + ******************************************************************************/ + +static QHash& +searchBars () { + static QHash bars; + return bars; +} + +static QTMFloatingSearchBar* +get_or_create_bar (QWidget* parent) { + if (!parent) return nullptr; + auto& bars= searchBars (); + auto it = bars.find (parent); + if (it != bars.end () && *it) return *it; + + auto* bar = new QTMFloatingSearchBar (parent); + bars[parent]= bar; + + QObject::connect (parent, &QObject::destroyed, [parent] () { + auto& b= searchBars (); + auto i= b.find (parent); + if (i != b.end ()) b.erase (i); }); + + bar->setFixedWidth (DpiUtils::scaled (kBarWidth)); + return bar; +} + +void +qt_floating_search_bar_show (QWidget* parent, bool show) { + if (!parent) return; + auto* bar= get_or_create_bar (parent); + if (!bar) return; + if (show) bar->show (); + else bar->hide (); } -/// 创建或重建浮动栏容器,确保 attach 到 content。 -static void -ensure_search_bar (QWidget* content) { - if (g_search_bar && g_search_bar_parent == content) return; +bool +qt_floating_search_bar_init (QWidget* parent, const string& aux_url_str) { + if (!parent) return false; + auto* bar= get_or_create_bar (parent); - if (g_search_bar) { - delete g_search_bar; - g_search_bar= nullptr; + url aux_url= url_system (aux_url_str); + tree doc (DOCUMENT, ""); + tree sty= compound ("style", tree (TUPLE, "generic")); + widget tw = texmacs_input_widget (doc, sty, aux_url); + if (is_nil (tw)) { + bar->hide (); + return false; } - if (g_resize_filter) { - if (g_search_bar_parent) - g_search_bar_parent->removeEventFilter (g_resize_filter); - delete g_resize_filter; - g_resize_filter= nullptr; + QWidget* inputW= concrete (tw)->as_qwidget (); + if (!inputW) { + bar->hide (); + return false; } + bar->setSearchInput (inputW); + return true; +} - g_search_bar = new QTMFloatingSearchBar (content); - g_search_bar_parent= content; - g_resize_filter = new SearchBarResizeFilter (content); - content->installEventFilter (g_resize_filter); +void +qt_floating_search_bar_set_match_info (QWidget* parent, int current, + int total) { + if (!parent) return; + auto* bar= searchBars ().value (parent); + if (bar) bar->setMatchInfo (current, total); +} - // content 被 Qt 销毁时自动清理全局指针,避免悬空 - QObject::connect (content, &QObject::destroyed, [] () { - g_search_bar = nullptr; - g_search_bar_parent= nullptr; - g_resize_filter = nullptr; - }); +void +qt_floating_search_bar_set_callbacks (QWidget* parent, const string& next_cmd, + const string& prev_cmd, + const string& close_cmd) { + if (!parent) return; + auto* bar= get_or_create_bar (parent); + bar->setSchemeCallbacks (next_cmd, prev_cmd, close_cmd); +} - g_search_bar->setFixedWidth (DpiUtils::scaled (kBarWidth)); - connect_search_bar_signals (g_search_bar); +void +qt_floating_search_bar_destroy (QWidget* parent) { + if (!parent) return; + auto& bars= searchBars (); + auto it = bars.find (parent); + if (it != bars.end ()) { + (*it)->setParent (nullptr); + delete *it; + bars.erase (it); + } } /****************************************************************************** - * Scheme 胶水函数 + * 兼容层胶水函数(通过 provider 代理) ******************************************************************************/ +static qt_floating_search_parent_provider g_parent_provider; + +void +qt_floating_search_set_parent_provider ( + qt_floating_search_parent_provider provider) { + g_parent_provider= provider; +} + +static QWidget* +get_provider_parent () { + return g_parent_provider ? g_parent_provider () : nullptr; +} + void qt_floating_search (string flag) { - ChatController* ctrl= get_chat_controller (); - if (!ctrl) return; - QTChatTabWidget* view= ctrl->view (); - if (!view) return; - QWidget* content= view->contentWidget (); - if (!content) return; - - if (flag == "true" || flag == "#t") { - ensure_search_bar (content); - g_search_bar->activate (); - position_search_bar (); + QWidget* parent= get_provider_parent (); + if (!parent) return; + bool show= (flag == "true" || flag == "#t"); + if (show) { + auto* bar= get_or_create_bar (parent); + if (bar) bar->activate (); } else { - if (g_search_bar) g_search_bar->hide (); + qt_floating_search_bar_show (parent, false); } } void qt_floating_search_init (string aux_url_str) { - ChatController* ctrl= get_chat_controller (); - if (!ctrl) return; - QTChatTabWidget* view= ctrl->view (); - if (!view) return; - QWidget* content= view->contentWidget (); - if (!content) return; - - ensure_search_bar (content); - - // 创建 texmacs_input_widget 绑定到 search-buffer - url aux_url= url_system (aux_url_str); - tree doc (DOCUMENT, ""); - tree sty= compound ("style", tree (TUPLE, "generic")); - widget tw = texmacs_input_widget (doc, sty, aux_url); - if (is_nil (tw)) return; - QWidget* inputW= concrete (tw)->as_qwidget (); - if (inputW) { - inputW->setStyleSheet ("QWidget {" - " background: white;" - " border: 1px solid #d0d0d0;" - "}" - "QWidget:focus {" - " border: 1px solid #215a6a;" - "}"); - g_search_bar->setSearchInput (inputW); - } + QWidget* parent= get_provider_parent (); + if (!parent) return; + if (!qt_floating_search_bar_init (parent, aux_url_str)) return; + auto* bar= get_or_create_bar (parent); + if (bar) bar->activate (); } void qt_floating_search_set_match_info (int current, int total) { - if (g_search_bar) g_search_bar->setMatchInfo (current, total); + QWidget* parent= get_provider_parent (); + if (!parent) return; + qt_floating_search_bar_set_match_info (parent, current, total); +} + +void +qt_floating_search_set_callbacks (string next_cmd, string prev_cmd, + string close_cmd) { + QWidget* parent= get_provider_parent (); + if (!parent) return; + qt_floating_search_bar_set_callbacks (parent, next_cmd, prev_cmd, close_cmd); } diff --git a/src/Plugins/Qt/qt_floating_search_bar.hpp b/src/Plugins/Qt/qt_floating_search_bar.hpp index 939a8ff256..aaef43d6f0 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.hpp +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -14,44 +14,96 @@ #include #include -#include #include #include "string.hpp" +#include + /** - * 悬浮搜索栏容器。 + * VSCode 风格的悬浮搜索栏组件。 * - * 布局: - * 上层: [TeXmacs 输入区] [上一个] [下一个] [关闭] - * 下层: [TeXmacs 输入区] [匹配计数] - * 输入区是嵌入的 texmacs_input_widget,绑定到 search-buffer, - * 搜索逻辑与底部搜索面板完全一致。 + * 布局: + * 左侧:嵌入的输入框(如 texmacs_input_widget) + * 右侧:[上一个] [下一个] [关闭] 按钮 + 匹配计数 */ class QTMFloatingSearchBar : public QWidget { Q_OBJECT public: - QTMFloatingSearchBar (QWidget* parent= nullptr); + explicit QTMFloatingSearchBar (QWidget* parent= nullptr); + ~QTMFloatingSearchBar (); - /// 设置嵌入的 TeXmacs 搜索输入 widget。 + /// 设置嵌入的搜索输入框。旧的输入框(如有)会被移除并 deleteLater。 void setSearchInput (QWidget* input); - /// 显示搜索栏并聚焦输入区。 + /// 显示搜索栏并聚焦输入框。 void activate (); - /// 设置匹配信息(current=0, total=0 显示"无匹配")。 + /// 设置匹配信息(current=0, total=0 时显示"无匹配")。 void setMatchInfo (int current, int total); + /// 配置按钮点击时求值的 Scheme 命令。 + void setSchemeCallbacks (const string& next_cmd, const string& prev_cmd, + const string& close_cmd); + signals: void findNextRequested (); void findPreviousRequested (); void closeRequested (); +protected: + bool eventFilter (QObject* watched, QEvent* event) override; + void showEvent (QShowEvent* event) override; + private: - QHBoxLayout* rowLayout_= nullptr; ///< 上层水平布局(输入+按钮) - QWidget* inputQW_ = nullptr; ///< 嵌入的 TeXmacs 输入 QWidget - QLabel* infoLbl_ = nullptr; ///< 匹配计数标签 + void reposition (); + void connectSignals (); + + QHBoxLayout* rowLayout_= nullptr; + QWidget* inputQW_ = nullptr; + QLabel* infoLbl_ = nullptr; + + string next_cmd_; + string prev_cmd_; + string close_cmd_; + bool callbacksConnected_= false; }; +/****************************************************************************** + * 通用管理 API(基于 parent widget,不依赖 ChatController) + ******************************************************************************/ + +/// 显示或隐藏 attach 到 \a parent 的悬浮搜索栏。 +void qt_floating_search_bar_show (QWidget* parent, bool show); + +/// 为 \a parent 创建/attach 搜索栏,并用绑定到 \a aux_url_str 的 +/// texmacs 输入框初始化。失败时返回 false。 +bool qt_floating_search_bar_init (QWidget* parent, const string& aux_url_str); + +/// 更新 attach 到 \a parent 的搜索栏的匹配计数。 +void qt_floating_search_bar_set_match_info (QWidget* parent, int current, + int total); + +/// 为 attach 到 \a parent 的搜索栏设置 Scheme 回调。 +void qt_floating_search_bar_set_callbacks (QWidget* parent, + const string& next_cmd, + const string& prev_cmd, + const string& close_cmd); + +/// 销毁 attach 到 \a parent 的搜索栏。 +void qt_floating_search_bar_destroy (QWidget* parent); + +/****************************************************************************** + * 兼容层胶水函数(保留向后兼容)。 + * 通过注册的 parent provider 代理到上面的通用 API。 + ******************************************************************************/ + +using qt_floating_search_parent_provider= std::function; + +/// 注册一个返回默认 parent widget 的函数,供兼容层胶水函数使用。 +/// 通常在 chat controller 初始化时调用。 +void qt_floating_search_set_parent_provider ( + qt_floating_search_parent_provider provider); + /// Scheme 胶水函数:显示 ("true"/"#t") 或隐藏悬浮搜索栏。 void qt_floating_search (string flag); @@ -62,4 +114,8 @@ void qt_floating_search_init (string aux_url_str); /// Scheme 胶水函数:更新浮动搜索栏的匹配计数显示。 void qt_floating_search_set_match_info (int current, int total); +/// Scheme 胶水函数:设置搜索栏按钮点击时求值的 Scheme 回调命令。 +void qt_floating_search_set_callbacks (string next_cmd, string prev_cmd, + string close_cmd); + #endif // QT_FLOATING_SEARCH_BAR_HPP From beeb280d116adaa315daf518202533e2067954ab Mon Sep 17 00:00:00 2001 From: Yuki Date: Thu, 28 May 2026 17:14:23 +0800 Subject: [PATCH 08/19] translate --- TeXmacs/plugins/lang/dic/en_US/zh_CN.scm | 4 ++++ src/Plugins/Qt/qt_floating_search_bar.cpp | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm index 50ca02eefd..3bd041b001 100644 --- a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm +++ b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm @@ -465,6 +465,7 @@ ("close replace tool (Esc)" "关闭替换窗 (Esc)") ("close window" "关闭窗口") ("close" "关闭") +("Close (Esc)" "关闭 (Esc)") ("closed bezier" "闭合贝塞尔曲线") ("closed curve" "闭合曲线") ("closed smooth" "闭合平滑曲线") @@ -1600,6 +1601,7 @@ ("next screen" "前一屏") ("next similar" "相似的(后一个)") ("next" "后一个") +("Next (Enter)" "下一个 (Enter)") ("no changes need to be saved" "没有任何更改需要保存") ("no dictionary for" "") ("no first indentation" "") @@ -1609,6 +1611,7 @@ ("no matches found for" "") ("no matches found" "") ("No matches" "无匹配") +("%1 of %2" "%1 / %2") ("no more matches for" "") ("no more redo information available" "") ("no more undo information available" "") @@ -1871,6 +1874,7 @@ ("previous screen" "后一屏") ("previous similar" "相似的(前一个)") ("previous" "前一个") +("Previous (Shift+Enter)" "上一个 (Shift+Enter)") ("primary" "") ("prime" "") ("print all to file" "全部打印为文件") diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 50dbdbaaeb..d94781baac 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -44,8 +44,9 @@ constexpr int kShadowBlur = 8; constexpr int kShadowOffsetY= 1; constexpr int kShadowAlpha = 30; -constexpr int kPosRightPad= 8; -constexpr int kPosTopPad = 4; +constexpr int kPosRightPad = 8; +constexpr int kPosTopPad = 4; +constexpr int kInnerSpacing= 4; /****************************************************************************** * QTMFloatingSearchBar 实现 @@ -83,11 +84,11 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) // 右侧:垂直布局 [按钮行] + [匹配信息] auto* rightLayout= new QVBoxLayout (); - rightLayout->setSpacing (DpiUtils::scaled (4)); + rightLayout->setSpacing (DpiUtils::scaled (kInnerSpacing)); // 右侧上层:按钮行 auto* btnRow= new QHBoxLayout (); - btnRow->setSpacing (DpiUtils::scaled (4)); + btnRow->setSpacing (DpiUtils::scaled (kInnerSpacing)); const QString btnRadiusStyle= QString ( From 091e2a701c7b97c95d93bf5716371b42bc090311 Mon Sep 17 00:00:00 2001 From: Yuki Date: Thu, 28 May 2026 17:38:53 +0800 Subject: [PATCH 09/19] next --- TeXmacs/progs/generic/search-widgets.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 71909ffbf2..2fdff4c2e2 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -1170,7 +1170,7 @@ (tm-define (chat-tab-search-next forward?) (when (and chat-tab-search-target chat-tab-search-aux) (with-buffer chat-tab-search-target - (search-next-match forward? chat-tab-search-target)))) + (search-rotate-match forward?)))) (tm-define (chat-tab-search-close) (when chat-tab-search-target From 633ee8b6f485775ac3444f03c7b45acee96f9568 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 14:11:14 +0800 Subject: [PATCH 10/19] =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Plugins/Qt/qt_floating_search_bar.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index d94781baac..04051960a8 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -275,9 +275,12 @@ qt_floating_search_bar_init (QWidget* parent, const string& aux_url_str) { auto* bar= get_or_create_bar (parent); url aux_url= url_system (aux_url_str); - tree doc (DOCUMENT, ""); + qreal searchZoom= DpiUtils::scaled (100) / 100.0; + tree doc (WITH, "font", "sys-chinese", "zoom-factor", as_string (searchZoom), + tree (DOCUMENT, "")); tree sty= compound ("style", tree (TUPLE, "generic")); widget tw = texmacs_input_widget (doc, sty, aux_url); + set_zoom_factor (tw, searchZoom); if (is_nil (tw)) { bar->hide (); return false; From ff19a5750337ab59c99c3c566a502a830b4b8cd2 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 14:23:39 +0800 Subject: [PATCH 11/19] viewport --- src/Plugins/Qt/qt_floating_search_bar.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 04051960a8..5e0c25e30e 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -19,9 +19,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -158,6 +160,10 @@ QTMFloatingSearchBar::setSearchInput (QWidget* input) { inputQW_= input; if (input) { input->setObjectName ("floating-search-input"); + QAbstractScrollArea* scrollArea= input->findChild (); + if (scrollArea) { + scrollArea->viewport ()->setBackgroundRole (QPalette::Base); + } rowLayout_->insertWidget (0, input, 1); } } @@ -274,12 +280,12 @@ qt_floating_search_bar_init (QWidget* parent, const string& aux_url_str) { if (!parent) return false; auto* bar= get_or_create_bar (parent); - url aux_url= url_system (aux_url_str); - qreal searchZoom= DpiUtils::scaled (100) / 100.0; - tree doc (WITH, "font", "sys-chinese", "zoom-factor", as_string (searchZoom), - tree (DOCUMENT, "")); - tree sty= compound ("style", tree (TUPLE, "generic")); - widget tw = texmacs_input_widget (doc, sty, aux_url); + url aux_url = url_system (aux_url_str); + qreal searchZoom= DpiUtils::scaled (100) / 100.0; + tree doc (WITH, "font", "sys-chinese", "zoom-factor", as_string (searchZoom), + tree (DOCUMENT, "")); + tree sty= compound ("style", tree (TUPLE, "generic")); + widget tw= texmacs_input_widget (doc, sty, aux_url); set_zoom_factor (tw, searchZoom); if (is_nil (tw)) { bar->hide (); From c903cb741cb054a51db17c29cfbbf603cbcbbc09 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 15:00:39 +0800 Subject: [PATCH 12/19] =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86=E9=80=82?= =?UTF-8?q?=E9=85=8D=E9=BB=91=E5=A4=9C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TeXmacs/progs/generic/search-widgets.scm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 2fdff4c2e2..09d1b57838 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -1156,6 +1156,11 @@ (qt-floating-search-set-callbacks "(chat-tab-search-next #t)" "(chat-tab-search-next #f)" "(chat-tab-search-close)") (qt-floating-search-init (url->string aux)) + ;; 同步暗色样式到搜索缓冲区 + (when (== (get-preference "gui theme") "liii-night") + (with-buffer aux + (when (not (has-style-package? "dark")) + (add-style-package "dark")))) (qt-floating-search "true"))) (define (chat-tab-perform-search) From a943bd89d49c7db3fc3c98b58603cf9e53400c86 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 15:09:56 +0800 Subject: [PATCH 13/19] devel --- devel/1042.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devel/1042.md b/devel/1042.md index ccf5539938..9fa87c1dd1 100644 --- a/devel/1042.md +++ b/devel/1042.md @@ -45,6 +45,7 @@ Chat Tab 的 message buffer 是嵌入式 TeXmacs widget,没有标准 `tm_windo 3. Scheme 端新增 chat-tab 专用搜索初始化和导航函数 4. 修改 `interactive-search` 在嵌入式 chat buffer 时使用新的搜索 UI 5. 修改 `interactive-replace` 在 chat tab 中禁用替换 +6. 搜索框 DPI 缩放、系统字体、暗色主题跟随 ## 6 Why commit [0228] 通过 blanket `tmfs:` 检查禁用了所有 tmfs:// 缓冲区的搜索来避免 crash。用户需要在 Chat Tab 的消息输出框中搜索对话内容。Chat Tab 的嵌入式 buffer 不支持 auxiliary-widget 机制,需要独立的搜索 UI。 @@ -56,6 +57,8 @@ commit [0228] 通过 blanket `tmfs:` 检查禁用了所有 tmfs:// 缓冲区的 4. Scheme 端新增 `chat-tab-search-init`、`chat-tab-perform-search`、`chat-tab-search-next`、`chat-tab-search-close` 函数 5. C++ 按钮回调通过 `eval_scheme` 调用 Scheme 搜索函数,回调命令由 `qt-floating-search-set-callbacks` 注入,不再硬编码 6. `interactive-search` 检测到嵌入式 chat buffer 时调 C++ glue 显示搜索栏并初始化搜索 +7. 搜索框的 `texmacs_input_widget` 参照 chat 组件,应用 `DpiUtils::scaled` 缩放、`"font" "sys-chinese"` 字体、viewport 背景色 `QPalette::Base` +8. Scheme 端在 `chat-tab-search-init` 中检测 `liii-night` 主题,自动给搜索缓冲区添加 `"dark"` 样式包 --- From bd45a2e449bc947db33bc5b2af2f2f9ebb46371c Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 15:31:57 +0800 Subject: [PATCH 14/19] fix --- TeXmacs/progs/generic/search-widgets.scm | 17 +++++------------ src/Plugins/Qt/qt_floating_search_bar.cpp | 6 ++---- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 09d1b57838..c55e2c371c 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -168,7 +168,7 @@ ;; 此函数用于管理搜索辅助缓冲区的生命周期,确保每个主文档视图有唯一的搜索缓冲区。 (tm-define (search-buffer) ;; chat tab 搜索激活期间,直接返回保存的 aux buffer - (if chat-tab-search-active? + (if (and chat-tab-search-active? chat-tab-search-aux) chat-tab-search-aux (with u (current-buffer) @@ -343,7 +343,7 @@ (tm-define (master-buffer) ;; chat tab 搜索激活期间,直接返回保存的 target buffer - (if chat-tab-search-active? + (if (and chat-tab-search-active? chat-tab-search-target) chat-tab-search-target (and (buffer-exists? (search-buffer)) (with mas @@ -448,7 +448,9 @@ (new-env (tree-descendant-env* buf rel initial))) (if (not new-env) #t (check-same? (tm-children new-env) (tm-children old-env))))))) - (lambda (key msg . rest) #t)) + (lambda (key msg . rest) + (display* "Warning: accept-search-result? error: " msg "\n") + #t)) ) ;or ) ;define @@ -1163,15 +1165,6 @@ (add-style-package "dark")))) (qt-floating-search "true"))) -(define (chat-tab-perform-search) - (when (and chat-tab-search-target chat-tab-search-aux - (buffer-exists? chat-tab-search-aux)) - (with-buffer chat-tab-search-target - (set-search-reference (cursor-path))) - (set-search-filter) - (with-buffer chat-tab-search-target - (perform-search)))) - (tm-define (chat-tab-search-next forward?) (when (and chat-tab-search-target chat-tab-search-aux) (with-buffer chat-tab-search-target diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 5e0c25e30e..15939a88c5 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -256,10 +256,8 @@ get_or_create_bar (QWidget* parent) { auto* bar = new QTMFloatingSearchBar (parent); bars[parent]= bar; - QObject::connect (parent, &QObject::destroyed, [parent] () { - auto& b= searchBars (); - auto i= b.find (parent); - if (i != b.end ()) b.erase (i); + QObject::connect (parent, &QObject::destroyed, [] (QObject* obj) { + searchBars ().remove (static_cast (obj)); }); bar->setFixedWidth (DpiUtils::scaled (kBarWidth)); From 47d4d7da8982ae3eca83116923329bf3e2f8c8f3 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 16:05:15 +0800 Subject: [PATCH 15/19] =?UTF-8?q?kbd=E6=96=87=E6=9C=AC=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TeXmacs/plugins/lang/dic/en_US/zh_CN.scm | 3 ++- src/Plugins/Qt/qt_floating_search_bar.cpp | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm index 3bd041b001..b8691fcec1 100644 --- a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm +++ b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm @@ -1874,7 +1874,8 @@ ("previous screen" "后一屏") ("previous similar" "相似的(前一个)") ("previous" "前一个") -("Previous (Shift+Enter)" "上一个 (Shift+Enter)") +("Previous (Ctrl+Enter)" "上一个 (Ctrl+Enter)") +("Previous (Cmd+Enter)" "上一个 (Cmd+Enter)") ("primary" "") ("prime" "") ("print all to file" "全部打印为文件") diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 15939a88c5..2c39e0a64a 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -101,7 +101,11 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) prevBtn->setObjectName ("floating-search-prev"); prevBtn->setFixedSize (DpiUtils::scaled (kBtnSize), DpiUtils::scaled (kBtnSize)); - prevBtn->setToolTip (qt_translate ("Previous (Shift+Enter)")); +#ifdef Q_OS_MAC + prevBtn->setToolTip (qt_translate ("Previous (Cmd+Enter)")); +#else + prevBtn->setToolTip (qt_translate ("Previous (Ctrl+Enter)")); +#endif prevBtn->setStyleSheet (btnRadiusStyle); btnRow->addWidget (prevBtn); From da3fb345c0f839f35f94b791d7a96a5f21d9c22a Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 17:00:06 +0800 Subject: [PATCH 16/19] fix --- TeXmacs/progs/generic/search-widgets.scm | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index c55e2c371c..21dd6be756 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -167,8 +167,10 @@ ;; ---- ;; 此函数用于管理搜索辅助缓冲区的生命周期,确保每个主文档视图有唯一的搜索缓冲区。 (tm-define (search-buffer) - ;; chat tab 搜索激活期间,直接返回保存的 aux buffer - (if (and chat-tab-search-active? chat-tab-search-aux) + ;; chat tab 搜索激活且当前在 chat tab 上下文中时,返回保存的 aux buffer + (if (and chat-tab-search-active? chat-tab-search-aux + (or (== (current-buffer) chat-tab-search-aux) + (== (current-buffer) chat-tab-search-target))) chat-tab-search-aux (with u (current-buffer) @@ -342,8 +344,10 @@ ) ;tm-define (tm-define (master-buffer) - ;; chat tab 搜索激活期间,直接返回保存的 target buffer - (if (and chat-tab-search-active? chat-tab-search-target) + ;; chat tab 搜索激活且当前在 chat tab 上下文中时,返回保存的 target buffer + (if (and chat-tab-search-active? chat-tab-search-target + (or (== (current-buffer) chat-tab-search-aux) + (== (current-buffer) chat-tab-search-target))) chat-tab-search-target (and (buffer-exists? (search-buffer)) (with mas @@ -369,7 +373,9 @@ ) ;tm-define (tm-define (inside-search-buffer?) - (if chat-tab-search-active? + (if (and chat-tab-search-active? + (or (== (current-buffer) chat-tab-search-aux) + (== (current-buffer) chat-tab-search-target))) (== (current-buffer) chat-tab-search-aux) (== (current-buffer) (search-buffer)))) @@ -661,7 +667,7 @@ (set! search-serial (+ search-serial 1)) (with-buffer (master-buffer) (cancel-alt-selection "alternate")) (set-search-window-state #f #f) - (buffer-focus u #t) + (when u (buffer-focus u #t)) ) ;tm-define ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1462,6 +1468,7 @@ ) ;tm-define (tm-define (toolbar-search-end) + (when chat-tab-search-active? (chat-tab-search-close)) (cancel-alt-selection "alternate") (search-show-all) (set! search-filter-out? #f) From 3e040c333afd31f1641d1037a5925c3559cad45b Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 29 May 2026 21:03:51 +0800 Subject: [PATCH 17/19] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TeXmacs/progs/generic/search-widgets.scm | 27 ++++++++++++++------ src/Plugins/Qt/qt_chat_controller.cpp | 16 ++++++++++++ src/Plugins/Qt/qt_chat_controller.hpp | 4 +++ src/Plugins/Qt/qt_floating_search_bar.cpp | 30 +++++++++++++++++------ src/Scheme/L5/glue_widget.lua | 7 ++++++ 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 21dd6be756..25c6784507 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -456,7 +456,7 @@ (check-same? (tm-children new-env) (tm-children old-env))))))) (lambda (key msg . rest) (display* "Warning: accept-search-result? error: " msg "\n") - #t)) + (if chat-tab-search-active? #t #f))) ) ;or ) ;define @@ -1183,6 +1183,14 @@ (with-buffer chat-tab-search-target (cancel-alt-selection "alternate")) (set-search-window-state #f #f) + (let* ((msg-url (url->system chat-tab-search-target)) + (in-url (if (string-starts? msg-url "tmfs://chat-message-") + (string-append "tmfs://chat-input-" + (substring msg-url + (string-length "tmfs://chat-message-"))) + ""))) + (when (not (== in-url "")) + (buffer-focus (string->url in-url) #t))) (set! chat-tab-search-active? #f) (set! chat-tab-search-target #f) (set! chat-tab-search-aux #f))) @@ -1727,13 +1735,18 @@ (with buf (current-buffer) (with sid (chat-buffer-session-id buf) (cond - ((and sid - (chat-message-buffer-has-content? - (string->url (string-append "tmfs://chat-message-" sid)))) - (chat-tab-search-init - (string->url (string-append "tmfs://chat-message-" sid)))) + ((string-starts? (url->system buf) "tmfs://chat-") + ;; chat tab 任何缓冲区:通过 sid 或胶水函数找到消息缓冲区 + (let* ((msg-url (if sid + (string-append "tmfs://chat-message-" sid) + (qt-chat-tab-active-message-buffer-url))) + (msg-u (and msg-url (not (== msg-url "")) + (string->url msg-url)))) + (if (and msg-u (chat-message-buffer-has-content? msg-u)) + (chat-tab-search-init msg-u) + (noop)))) ((string-starts? (url->system buf) "tmfs:") - ;; 其他 tmfs:// 缓冲区不支持搜索 + ;; 其他 tmfs:// 缓冲区保持禁用搜索 (noop)) (else (set! search-replace-text diff --git a/src/Plugins/Qt/qt_chat_controller.cpp b/src/Plugins/Qt/qt_chat_controller.cpp index 64ac343820..9652d2ab34 100644 --- a/src/Plugins/Qt/qt_chat_controller.cpp +++ b/src/Plugins/Qt/qt_chat_controller.cpp @@ -712,3 +712,19 @@ qt_chat_tab_restore_session (string sessionId, string title, string model, get_chat_controller ()->restoreSessionMeta ( sessionId, title, model, isArchived, createdAt, expandCount, isThinking); } + +string +ChatController::activeSessionMessageBufferUrl () const { + if (!view_) return ""; + ChatSidebar* sidebar= view_->sidebar (); + if (!sidebar) return ""; + string activeId= sidebar->activeSessionId (); + if (is_empty (activeId)) return ""; + url msgBufUrl= ChatSessionManager::messageBufferUrl (activeId); + return as_string (msgBufUrl); +} + +string +qt_chat_tab_active_message_buffer_url () { + return get_chat_controller ()->activeSessionMessageBufferUrl (); +} diff --git a/src/Plugins/Qt/qt_chat_controller.hpp b/src/Plugins/Qt/qt_chat_controller.hpp index 06d51c6482..651263f7eb 100644 --- a/src/Plugins/Qt/qt_chat_controller.hpp +++ b/src/Plugins/Qt/qt_chat_controller.hpp @@ -133,6 +133,8 @@ class ChatController : public QObject { */ void destroyView (); + string activeSessionMessageBufferUrl () const; + private: QTChatTabWidget* view_= nullptr; ///< View 指针,由 createView 创建 ChatSessionManager sessionManager_; ///< 会话管理器 @@ -234,4 +236,6 @@ void qt_chat_tab_restore_session (string sessionId, string title, string model, string archived, string createdAt, int defaultExpandCount, string thinking); +string qt_chat_tab_active_message_buffer_url (); + #endif // QT_CHAT_CONTROLLER_HPP diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 2c39e0a64a..38a90a7349 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -158,6 +159,8 @@ QTMFloatingSearchBar::~QTMFloatingSearchBar () { void QTMFloatingSearchBar::setSearchInput (QWidget* input) { if (inputQW_) { + QAbstractScrollArea* oldArea= inputQW_->findChild (); + if (oldArea) oldArea->removeEventFilter (this); rowLayout_->removeWidget (inputQW_); inputQW_->deleteLater (); } @@ -167,6 +170,7 @@ QTMFloatingSearchBar::setSearchInput (QWidget* input) { QAbstractScrollArea* scrollArea= input->findChild (); if (scrollArea) { scrollArea->viewport ()->setBackgroundRole (QPalette::Base); + scrollArea->installEventFilter (this); } rowLayout_->insertWidget (0, input, 1); } @@ -176,7 +180,11 @@ void QTMFloatingSearchBar::activate () { show (); raise (); - if (inputQW_) inputQW_->setFocus (); + if (inputQW_) { + QAbstractScrollArea* sa= inputQW_->findChild (); + if (sa) sa->setFocus (); + else inputQW_->setFocus (); + } } void @@ -222,6 +230,16 @@ QTMFloatingSearchBar::eventFilter (QObject* watched, QEvent* event) { isVisible ()) { reposition (); } + if (event->type () == QEvent::KeyPress && isVisible () && inputQW_) { + auto* ke= static_cast (event); + if (ke->key () == Qt::Key_Escape) { + QAbstractScrollArea* sa= inputQW_->findChild (); + if (sa && watched == sa) { + emit closeRequested (); + return true; + } + } + } return QWidget::eventFilter (watched, event); } @@ -260,9 +278,9 @@ get_or_create_bar (QWidget* parent) { auto* bar = new QTMFloatingSearchBar (parent); bars[parent]= bar; - QObject::connect (parent, &QObject::destroyed, [] (QObject* obj) { - searchBars ().remove (static_cast (obj)); - }); + QWidget* pw= parent; + QObject::connect (parent, &QObject::destroyed, + [pw] () { searchBars ().remove (pw); }); bar->setFixedWidth (DpiUtils::scaled (kBarWidth)); return bar; @@ -366,9 +384,7 @@ void qt_floating_search_init (string aux_url_str) { QWidget* parent= get_provider_parent (); if (!parent) return; - if (!qt_floating_search_bar_init (parent, aux_url_str)) return; - auto* bar= get_or_create_bar (parent); - if (bar) bar->activate (); + qt_floating_search_bar_init (parent, aux_url_str); } void diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua index 4e1756ecd9..ba9da6918b 100644 --- a/src/Scheme/L5/glue_widget.lua +++ b/src/Scheme/L5/glue_widget.lua @@ -527,6 +527,13 @@ function main() "string" } }, + { + scm_name = "qt-chat-tab-active-message-buffer-url", + cpp_name = "qt_chat_tab_active_message_buffer_url", + ret_type = "string", + arg_list = {} + }, + { scm_name = "qt-floating-search", cpp_name = "qt_floating_search", From 3ab930e97df08ca7be8f6332b03355504e8d8029 Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 30 May 2026 01:08:05 +0800 Subject: [PATCH 18/19] img --- .../misc/images/floating-search/math-mode-white.svg | 9 +++++++++ TeXmacs/misc/images/floating-search/math-mode.svg | 9 +++++++++ .../misc/images/floating-search/text-mode-white.svg | 10 ++++++++++ TeXmacs/misc/images/floating-search/text-mode.svg | 10 ++++++++++ TeXmacs/misc/images/images.qrc | 4 ++++ 5 files changed, 42 insertions(+) create mode 100644 TeXmacs/misc/images/floating-search/math-mode-white.svg create mode 100644 TeXmacs/misc/images/floating-search/math-mode.svg create mode 100644 TeXmacs/misc/images/floating-search/text-mode-white.svg create mode 100644 TeXmacs/misc/images/floating-search/text-mode.svg diff --git a/TeXmacs/misc/images/floating-search/math-mode-white.svg b/TeXmacs/misc/images/floating-search/math-mode-white.svg new file mode 100644 index 0000000000..c88232ab2b --- /dev/null +++ b/TeXmacs/misc/images/floating-search/math-mode-white.svg @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/floating-search/math-mode.svg b/TeXmacs/misc/images/floating-search/math-mode.svg new file mode 100644 index 0000000000..d78999d32e --- /dev/null +++ b/TeXmacs/misc/images/floating-search/math-mode.svg @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/floating-search/text-mode-white.svg b/TeXmacs/misc/images/floating-search/text-mode-white.svg new file mode 100644 index 0000000000..476050b613 --- /dev/null +++ b/TeXmacs/misc/images/floating-search/text-mode-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/floating-search/text-mode.svg b/TeXmacs/misc/images/floating-search/text-mode.svg new file mode 100644 index 0000000000..7ff9c9e988 --- /dev/null +++ b/TeXmacs/misc/images/floating-search/text-mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/TeXmacs/misc/images/images.qrc b/TeXmacs/misc/images/images.qrc index d3215e4865..c08353c8d1 100644 --- a/TeXmacs/misc/images/images.qrc +++ b/TeXmacs/misc/images/images.qrc @@ -27,6 +27,8 @@ floating-search/down.svg floating-search/up.svg + floating-search/text-mode.svg + floating-search/math-mode.svg ocr-button/left-align-white.svg @@ -48,6 +50,8 @@ floating-search/down-white.svg floating-search/up-white.svg + floating-search/text-mode-white.svg + floating-search/math-mode-white.svg tutorial/ocr-tutorial.gif From ef0ad65a0ffea6ecfe19a4aa677ab112ef67c8c5 Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 30 May 2026 01:12:14 +0800 Subject: [PATCH 19/19] =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E4=B8=B4=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TeXmacs/misc/themes/liii-night.css | 8 ++++++ TeXmacs/misc/themes/liii.css | 8 ++++++ TeXmacs/progs/generic/search-widgets.scm | 33 ++++++++++++++++++++--- src/Plugins/Qt/qt_floating_search_bar.cpp | 24 +++++++++++++++++ src/Plugins/Qt/qt_floating_search_bar.hpp | 6 ++++- 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/TeXmacs/misc/themes/liii-night.css b/TeXmacs/misc/themes/liii-night.css index 68ffa62261..e2bbc1f0cf 100644 --- a/TeXmacs/misc/themes/liii-night.css +++ b/TeXmacs/misc/themes/liii-night.css @@ -1526,6 +1526,14 @@ QWidget#floating_search_bar QToolButton#floating-search-close { qproperty-icon: url(":/tabpage/close-white.svg"); } +QWidget#floating_search_bar QToolButton#floating-search-mode-text { + qproperty-icon: url(":/floating-search/text-mode-white.svg"); +} + +QWidget#floating_search_bar QToolButton#floating-search-mode-math { + qproperty-icon: url(":/floating-search/math-mode-white.svg"); +} + QWidget#floating_search_bar QWidget#floating-search-input { border: 1px solid #555555; } diff --git a/TeXmacs/misc/themes/liii.css b/TeXmacs/misc/themes/liii.css index 9640c6f3fa..26a3b513f4 100644 --- a/TeXmacs/misc/themes/liii.css +++ b/TeXmacs/misc/themes/liii.css @@ -1444,6 +1444,14 @@ QWidget#floating_search_bar QToolButton#floating-search-close { qproperty-icon: url(":/tabpage/close.svg"); } +QWidget#floating_search_bar QToolButton#floating-search-mode-text { + qproperty-icon: url(":/floating-search/text-mode.svg"); +} + +QWidget#floating_search_bar QToolButton#floating-search-mode-math { + qproperty-icon: url(":/floating-search/math-mode.svg"); +} + QWidget#floating_search_bar QWidget#floating-search-input { border: 1px solid #d0d0d0; } diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 25c6784507..2a4047aadf 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -443,7 +443,10 @@ ) ;define (define (accept-search-result? p) - (or (== (get-init "mode") "src") + (or (and chat-tab-search-active? + (== chat-tab-search-mode "math") + (search-path-inside-math? p)) + (== (get-init "mode") "src") (catch #t (lambda () (let* ((buf (buffer-tree)) @@ -481,8 +484,11 @@ (define (tree-perform-search t what p limit) (let* ((source-mode 2) (old-mode (get-access-mode)) - (new-mode (if (== (get-init "mode") "src") source-mode old-mode)) - ) ; + (new-mode (if (or (== (get-init "mode") "src") + (and chat-tab-search-active? + (== chat-tab-search-mode "math"))) + source-mode old-mode)) + ) ; (set-access-mode new-mode) (let* ((cp (cDr (cursor-path))) (pos (if (list-starts? cp p) (list-tail cp (length p)) (list))) @@ -1149,6 +1155,27 @@ (define chat-tab-search-target #f) (define chat-tab-search-aux #f) (define chat-tab-search-active? #f) +(define chat-tab-search-mode "text") + +(define (chat-tab-search-toggle-mode) + (set! chat-tab-search-mode + (if (== chat-tab-search-mode "text") "math" "text")) + (with-buffer (master-buffer) + (if (== chat-tab-search-mode "math") + (begin (set-access-mode 2) (init-env "mode" "math")) + (begin (set-access-mode 0) (init-default "mode")))) + (perform-search*)) + +(define (search-path-inside-math? p) + (with-buffer (master-buffer) + (let* ((buf (buffer-tree)) + (dr (cDr p)) + (len (length dr))) + (let loop ((i (- len 1))) + (if (< i 0) #f + (let ((node (subtree buf (sublist dr 0 (- i 1))))) + (if (tm-func? node 'math) #t + (loop (- i 1))))))))) (define (chat-tab-search-init target-buf) (set! chat-tab-search-target target-buf) diff --git a/src/Plugins/Qt/qt_floating_search_bar.cpp b/src/Plugins/Qt/qt_floating_search_bar.cpp index 38a90a7349..df281c077e 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.cpp +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -98,6 +99,14 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) "QToolButton { border-radius: %1px; padding: 0px; margin: 0px; }") .arg (DpiUtils::scaled (kBtnRadius)); + modeBtn_= new QToolButton (this); + modeBtn_->setObjectName ("floating-search-mode-text"); + modeBtn_->setFixedSize (DpiUtils::scaled (kBtnSize), + DpiUtils::scaled (kBtnSize)); + modeBtn_->setToolTip (qt_translate ("Toggle search mode (text/math)")); + modeBtn_->setStyleSheet (btnRadiusStyle); + btnRow->addWidget (modeBtn_); + auto* prevBtn= new QToolButton (this); prevBtn->setObjectName ("floating-search-prev"); prevBtn->setFixedSize (DpiUtils::scaled (kBtnSize), @@ -146,6 +155,12 @@ QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent) &QTMFloatingSearchBar::findPreviousRequested); connect (closeBtn, &QToolButton::clicked, this, &QTMFloatingSearchBar::closeRequested); + connect (modeBtn_, &QToolButton::clicked, this, [this] () { + bool isMath= (modeBtn_->objectName () == + QStringLiteral ("floating-search-mode-text")); + setModeIcon (isMath); + eval_scheme ("(chat-tab-search-toggle-mode)"); + }); if (parent) parent->installEventFilter (this); @@ -224,6 +239,15 @@ QTMFloatingSearchBar::connectSignals () { } } +void +QTMFloatingSearchBar::setModeIcon (bool mathMode) { + if (!modeBtn_) return; + modeBtn_->setObjectName (mathMode ? "floating-search-mode-math" + : "floating-search-mode-text"); + modeBtn_->style ()->unpolish (modeBtn_); + modeBtn_->style ()->polish (modeBtn_); +} + bool QTMFloatingSearchBar::eventFilter (QObject* watched, QEvent* event) { if (event->type () == QEvent::Resize && watched == parent () && diff --git a/src/Plugins/Qt/qt_floating_search_bar.hpp b/src/Plugins/Qt/qt_floating_search_bar.hpp index aaef43d6f0..024865274d 100644 --- a/src/Plugins/Qt/qt_floating_search_bar.hpp +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -32,7 +32,7 @@ class QTMFloatingSearchBar : public QWidget { public: explicit QTMFloatingSearchBar (QWidget* parent= nullptr); - ~QTMFloatingSearchBar (); + ~QTMFloatingSearchBar () override; /// 设置嵌入的搜索输入框。旧的输入框(如有)会被移除并 deleteLater。 void setSearchInput (QWidget* input); @@ -44,11 +44,13 @@ class QTMFloatingSearchBar : public QWidget { /// 配置按钮点击时求值的 Scheme 命令。 void setSchemeCallbacks (const string& next_cmd, const string& prev_cmd, const string& close_cmd); + void setModeIcon (bool mathMode); signals: void findNextRequested (); void findPreviousRequested (); void closeRequested (); + void modeToggled (); protected: bool eventFilter (QObject* watched, QEvent* event) override; @@ -61,10 +63,12 @@ class QTMFloatingSearchBar : public QWidget { QHBoxLayout* rowLayout_= nullptr; QWidget* inputQW_ = nullptr; QLabel* infoLbl_ = nullptr; + QToolButton* modeBtn_ = nullptr; string next_cmd_; string prev_cmd_; string close_cmd_; + string mode_cmd_; bool callbacksConnected_= false; };