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/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/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/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/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..c08353c8d1 100644
--- a/TeXmacs/misc/images/images.qrc
+++ b/TeXmacs/misc/images/images.qrc
@@ -25,6 +25,11 @@
llm-chat/thinking.svg
llm-chat/thinking-white.svg
+ floating-search/down.svg
+ floating-search/up.svg
+ floating-search/text-mode.svg
+ floating-search/math-mode.svg
+
ocr-button/left-align-white.svg
ocr-button/middle-align-white.svg
@@ -43,6 +48,11 @@
pdf-reader/open-white.svg
pdf-reader/screenshot-white.svg
+ 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
tutorial/magic-paste-tutorial.gif
diff --git a/TeXmacs/misc/themes/liii-night.css b/TeXmacs/misc/themes/liii-night.css
index b1e75cbd4a..e2bbc1f0cf 100644
--- a/TeXmacs/misc/themes/liii-night.css
+++ b/TeXmacs/misc/themes/liii-night.css
@@ -1491,3 +1491,58 @@ 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 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;
+}
+
+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..26a3b513f4 100644
--- a/TeXmacs/misc/themes/liii.css
+++ b/TeXmacs/misc/themes/liii.css
@@ -1410,3 +1410,57 @@ 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 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;
+}
+
+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 77b467f4bf..b5249f88dc 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,9 @@
("next screen" "前一屏")
("next similar" "相似的(后一个)")
("next" "后一个")
+("math mode (Ctrl+Tab)" "数学模式 (Ctrl+Tab)")
+("math mode (Option+Tab)" "数学模式 (Option+Tab)")
+("Next (Enter)" "下一个 (Enter)")
("no changes need to be saved" "没有任何更改需要保存")
("no dictionary for" "")
("no first indentation" "")
@@ -1608,6 +1612,8 @@
("no line break" "禁止换行")
("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" "")
@@ -1870,6 +1876,8 @@
("previous screen" "后一屏")
("previous similar" "相似的(前一个)")
("previous" "前一个")
+("Previous (Ctrl+Enter)" "上一个 (Ctrl+Enter)")
+("Previous (Cmd+Enter)" "上一个 (Cmd+Enter)")
("primary" "")
("prime" "")
("print all to file" "全部打印为文件")
@@ -2396,6 +2404,8 @@
("Text for note" "用于笔记的文本")
("text height correction" "文本高度修正")
("text input" "文本输入")
+("text mode (Ctrl+Tab)" "文本模式 (Ctrl+Tab)")
+("text mode (Option+Tab)" "文本模式 (Option+Tab)")
("text mode" "文本模式")
("text width" "")
("text" "文本")
diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm
index 0c281c7a28..2967ed6756 100644
--- a/TeXmacs/progs/generic/search-widgets.scm
+++ b/TeXmacs/progs/generic/search-widgets.scm
@@ -111,6 +111,20 @@
(when isreplace?
(set! isreplace? #f)
) ;when
+ ;; 更新浮动搜索栏的匹配计数
+ (when floating-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)
+ ) ;when
+ ) ;let*
+ ) ;if
+ ) ;when
(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 +172,30 @@
;; ----
;; 此函数用于管理搜索辅助缓冲区的生命周期,确保每个主文档视图有唯一的搜索缓冲区。
(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
+ ;; 悬浮搜索激活时直接返回保存的 aux buffer
+ (if (and floating-search-active?
+ floating-search-aux
+ (buffer-exists? floating-search-aux)
+ (or (== (current-buffer) floating-search-aux)
+ (== (current-buffer) floating-search-target)
+ ) ;or
+ ) ;and
+ floating-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 +353,48 @@
) ;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
+ ;; 悬浮搜索激活时直接返回保存的 target buffer
+ (if (and floating-search-active?
+ floating-search-target
+ (buffer-exists? floating-search-target)
+ (or (== (current-buffer) floating-search-aux)
+ (== (current-buffer) floating-search-target)
+ ) ;or
+ ) ;and
+ floating-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 (and floating-search-active?
+ (or (== (current-buffer) floating-search-aux)
+ (== (current-buffer) floating-search-target)
+ ) ;or
+ ) ;and
+ (== (current-buffer) floating-search-aux)
+ (== (current-buffer) (search-buffer))
+ ) ;if
+) ;tm-define
(tm-define (inside-replace-buffer?) (== (current-buffer) (replace-buffer)))
@@ -417,17 +460,38 @@
) ;define
(define (accept-search-result? p)
- (or (== (get-init "mode") "src")
+ (if (and floating-search-active? (== floating-search-mode "math"))
+ (search-path-inside-math? p)
+ (if floating-search-active?
+ #t
+ ;; text mode: tree-perform-search 中 access=0 已限制只搜文本节点,无需额外 filter
+ (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*
+ ) ;or
+ ) ;if
+ ) ;if
+) ;define
+
+(define (search-path-inside-math? p)
+ (with-buffer (master-buffer)
(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))
+ (env (and rel (tree-descendant-env* buf rel initial)))
) ;
- ;; (display* p " ~> " new-env "\n")
- (check-same? (tm-children new-env) (tm-children old-env))
+ (and env
+ (with env-attrs (tm-children env) (check-same-sub? env-attrs "mode" "math"))
+ ) ;and
) ;let*
- ) ;or
+ ) ;with-buffer
) ;define
(define (filter-search-results sels)
@@ -450,8 +514,13 @@
(define (tree-perform-search t what p limit)
(let* ((source-mode 2)
+ (math-mode 1)
(old-mode (get-access-mode))
- (new-mode (if (== (get-init "mode") "src") source-mode old-mode))
+ (new-mode (cond ((== (get-init "mode") "src") source-mode)
+ ((and floating-search-active? (== floating-search-mode "math")) math-mode)
+ (else old-mode)
+ ) ;cond
+ ) ;new-mode
) ;
(set-access-mode new-mode)
(let* ((cp (cDr (cursor-path)))
@@ -466,7 +535,10 @@
(define (go-to* p)
(go-to p)
- (when (and (not (cursor-accessible?)) (not (in-source?)))
+ (when (and (not (cursor-accessible?))
+ (not (in-source?))
+ (not floating-search-active?)
+ ) ;and
(cursor-show-hidden)
(delayed (:pause 50)
(set! search-serial (+ search-serial 1))
@@ -637,7 +709,9 @@
(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)
+ ) ;when
) ;tm-define
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1112,6 +1186,111 @@
) ;when
) ;tm-define
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Chat tab search (floating search bar)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define floating-search-target #f)
+
+(define floating-search-aux #f)
+
+(define floating-search-active? #f)
+
+(define floating-search-mode "text")
+
+(define floating-search-last-content "")
+
+(tm-define (floating-search-toggle-mode)
+ (set! floating-search-mode (if (== floating-search-mode "text") "math" "text"))
+ ;; 更新 filter
+ (when floating-search-target
+ (with-buffer floating-search-target (set-search-filter))
+ ) ;when
+ ;; 重建 widget 以切换数学/文本输入环境,保留已输入文本
+ (when floating-search-aux
+ (let ((saved-body (buffer-get-body floating-search-aux)))
+ (qt-floating-search-init (url->string floating-search-aux) floating-search-mode)
+ (when (not (tree-empty? saved-body))
+ (buffer-set-body floating-search-aux saved-body)
+ ) ;when
+ ;; 重建 widget 后同步暗色样式
+ (sync-buffer-dark-style-with-gui-theme floating-search-aux)
+ ) ;let
+ ) ;when
+ ;; 在 floating-search-aux 上下文中搜索,确保 guards 通过
+ (when floating-search-aux
+ (with-buffer floating-search-aux (perform-search*))
+ ) ;when
+) ;tm-define
+
+(define (floating-search-init target-buf)
+ (set! floating-search-target target-buf)
+ (set! floating-search-last-content "")
+ (let ((aux (search-buffer)))
+ (set! floating-search-aux aux)
+ (set! floating-search-active? #t)
+ (buffer-set-master aux target-buf)
+ (set-search-window-state #t #t)
+ (with-buffer target-buf
+ (set-search-reference (cursor-path))
+ (set-search-filter)
+ ) ;with-buffer
+ (set! search-filter-out? #f)
+ ;; 设置搜索缓冲区的初始 init env(与侧边栏 texmacs-input 行为一致)
+ (with-buffer aux (init-env "mode" floating-search-mode))
+ (qt-floating-search-set-callbacks "(floating-search-next #t)"
+ "(floating-search-next #f)"
+ "(floating-search-close)"
+ ) ;qt-floating-search-set-callbacks
+ (qt-floating-search-init (url->string aux) floating-search-mode)
+ ;; 同步暗色样式到搜索缓冲区
+ (sync-buffer-dark-style-with-gui-theme aux)
+ (qt-floating-search "true")
+ ) ;let
+) ;define
+
+(tm-define (floating-search-next forward?)
+ (when (and floating-search-target floating-search-aux)
+ (with-buffer floating-search-target (search-rotate-match forward?))
+ ) ;when
+) ;tm-define
+
+(tm-define (floating-search-on-input)
+ (when (and floating-search-active? floating-search-aux)
+ (let ((current (tree->string (buffer-get-body floating-search-aux))))
+ (when (not (== current floating-search-last-content))
+ (set! floating-search-last-content current)
+ (with-buffer floating-search-aux (perform-search*))
+ ) ;when
+ ) ;let
+ ) ;when
+) ;tm-define
+
+(tm-define (floating-search-close)
+ (when floating-search-target
+ (search-show-all)
+ (set! search-serial (+ search-serial 1))
+ (with-buffer floating-search-target (cancel-alt-selection "alternate"))
+ (set-search-window-state #f #f)
+ (let* ((msg-url (url->system floating-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-"))
+ ) ;string-append
+ ""
+ ) ;if
+ ) ;in-url
+ ) ;
+ (when (not (== in-url ""))
+ (buffer-focus (string->url in-url) #t)
+ ) ;when
+ ) ;let*
+ (set! floating-search-active? #f)
+ (set! floating-search-target #f)
+ (set! floating-search-aux #f)
+ ) ;when
+) ;tm-define
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Search and replace widget
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1393,6 +1572,9 @@
) ;tm-define
(tm-define (toolbar-search-end)
+ (when floating-search-active?
+ (floating-search-close)
+ ) ;when
(cancel-alt-selection "alternate")
(search-show-all)
(set! search-filter-out? #f)
@@ -1626,19 +1808,73 @@
(define-preferences ("toolbar search" "on" noop) ("toolbar replace" "on" noop))
+(define (chat-message-buffer? buf)
+ (string-starts? (url->system buf) "tmfs://chat-message-")
+) ;define
+
+(define (chat-input-buffer? buf)
+ (string-starts? (url->system buf) "tmfs://chat-input-")
+) ;define
+
+(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)
+ ) ;cond
+ ) ;with
+) ;define
+
+(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))))
+ ) ;with
+ ) ;and
+) ;define
+
(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 ((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)
+ ) ;if
+ ) ;msg-url
+ (msg-u (and msg-url (not (== msg-url "")) (string->url msg-url)))
+ ) ;
+ (if (and msg-u (chat-message-buffer-has-content? msg-u))
+ (floating-search-init msg-u)
+ (noop)
+ ) ;if
+ ) ;let*
+ ) ;
+ ((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")
+ ((in-graphics?) "Graphics mode cannot search")
+ (else "Only search in text mode")
+ ) ;cond
+ ) ;set!
+ (set-boolean-preference "search-and-replace" #f)
+ (open-search)
+ ) ;else
) ;cond
- ) ;set!
- (set-boolean-preference "search-and-replace" #f)
- (open-search)
- ) ;unless
+ ) ;with
+ ) ;with
) ;tm-define
(tm-define (interactive-replace)
diff --git a/TeXmacs/progs/texmacs/texmacs/tm-files.scm b/TeXmacs/progs/texmacs/texmacs/tm-files.scm
index c885d1e4d1..5c215ddc00 100644
--- a/TeXmacs/progs/texmacs/texmacs/tm-files.scm
+++ b/TeXmacs/progs/texmacs/texmacs/tm-files.scm
@@ -118,7 +118,7 @@
(with t (tree->stree (get-style-tree))
(and (pair? t) (== (car t) 'tuple) (null? (cdr t)))))
-(define (sync-buffer-dark-style-with-gui-theme . opt-buf)
+(tm-define (sync-buffer-dark-style-with-gui-theme . opt-buf)
(with buf (if (null? opt-buf) (current-buffer) (car opt-buf))
(with-buffer buf
(if (== (get-preference "gui theme") "liii-night")
diff --git a/TeXmacs/tests/1042.scm b/TeXmacs/tests/1042.scm
new file mode 100644
index 0000000000..63b16eb94a
--- /dev/null
+++ b/TeXmacs/tests/1042.scm
@@ -0,0 +1,99 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; MODULE : 1042.scm
+;; DESCRIPTION : Unit tests for floating search on-input dedup logic
+;; COPYRIGHT : (C) 2026 Yuki Lu
+;;
+;; 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 .
+;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(import (liii check))
+
+(load "./TeXmacs/progs/generic/search-widgets.scm")
+
+(check-set-mode! 'report-failed)
+
+(define (test-floating-search-on-input-dedup)
+ (display "Testing floating-search-on-input dedup logic...\n")
+
+ ;; 创建临时 buffer,使用原子字符串作为 body(tree->string 才能返回值)
+ (let* ((buf (buffer-new)) (initial-content "hello"))
+
+ ;; 设置初始内容
+ (buffer-set-body buf initial-content)
+
+ ;; 初始化 floating search 状态
+ (set! floating-search-active? #t)
+ (set! floating-search-aux buf)
+ ;; 同步 last-content 为当前 buffer 内容
+ (set! floating-search-last-content (tree->string (buffer-get-body buf)))
+
+ ;; Test 1: 内容未变 → floating-search-last-content 保持不变
+ (let ((before floating-search-last-content))
+ (floating-search-on-input)
+ (check floating-search-last-content => before)
+ ) ;let
+
+ ;; Test 2: 内容改变 → floating-search-last-content 应更新为新内容
+ (buffer-set-body buf "hello world")
+ (floating-search-on-input)
+ (check floating-search-last-content => "hello world")
+
+ ;; Test 3: 再次用相同内容调用 → 不变
+ (let ((before floating-search-last-content))
+ (floating-search-on-input)
+ (check floating-search-last-content => before)
+ ) ;let
+
+ ;; Test 4: 清空内容 → last-content 应更新为空串
+ (buffer-set-body buf "")
+ (floating-search-on-input)
+ (check floating-search-last-content => "")
+
+ ;; Test 5: floating-search-active? 为 #f 时 → last-content 不变
+ (set! floating-search-active? #f)
+ (buffer-set-body buf "should be ignored")
+ (let ((before floating-search-last-content))
+ (floating-search-on-input)
+ (check floating-search-last-content => before)
+ ) ;let
+
+ ;; 恢复
+ (set! floating-search-active? #t)
+
+ (display "floating-search-on-input dedup tests passed!\n")
+ ) ;let*
+) ;define
+
+(define (test-floating-search-toggle-mode)
+ (display "Testing floating-search-toggle-mode...\n")
+
+ ;; 记录原始 mode
+ (let ((orig-mode floating-search-mode))
+ ;; 确保从已知状态开始
+ (set! floating-search-mode "text")
+ (check floating-search-mode => "text")
+
+ ;; 切换到 math
+ (set! floating-search-mode "math")
+ (check floating-search-mode => "math")
+
+ ;; 切回 text
+ (set! floating-search-mode "text")
+ (check floating-search-mode => "text")
+
+ ;; 恢复
+ (set! floating-search-mode orig-mode)
+ (display "floating-search-toggle-mode tests passed!\n")
+ ) ;let
+) ;define
+
+(tm-define (test_1042)
+ (display "Running test_1042...\n")
+ (test-floating-search-on-input-dedup)
+ (test-floating-search-toggle-mode)
+ (check-report)
+) ;tm-define
diff --git a/devel/1042.md b/devel/1042.md
new file mode 100644
index 0000000000..f543fd3e12
--- /dev/null
+++ b/devel/1042.md
@@ -0,0 +1,190 @@
+# [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_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/Scheme/L5/glue_widget.lua` - Scheme 胶水函数声明
+
+## 3 如何测试
+
+### 3.1 确定性测试(单元测试)
+
+**C++ 测试** (`tests/Plugins/Qt/qt_floating_search_bar_test.cpp`):
+- 构造与布局完整性(objectName、子 widget 存在性)
+- `setMatchInfo`:默认、有匹配、零匹配三种状态
+- `setModeIcon`:默认 text / 切换 math / 切回 text 三种状态
+- 运行:`xmake b qt_floating_search_bar_test && xmake r qt_floating_search_bar_test`
+
+**Scheme 测试** (`TeXmacs/tests/1042.scm`):
+- `floating-search-on-input` 去重逻辑:
+ - 内容不变 → `floating-search-last-content` 不变
+ - 内容改变 → `floating-search-last-content` 更新为新值
+ - `floating-search-active?` 为 `#f` → 不做任何操作
+- `floating-search-toggle-mode` 模式切换
+- 运行:`./bin/test_only 1042` 或 `xmake r 1042`
+
+### 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. 实现可复用的悬浮搜索栏 Qt 组件 `QTMFloatingSearchBar`,支持任意 QWidget parent
+2. 搜索栏包含:输入框、上一个/下一个按钮、关闭按钮、匹配计数
+3. Scheme 端新增 chat-tab 专用搜索初始化和导航函数
+4. 修改 `interactive-search` 在嵌入式 chat buffer 时使用新的搜索 UI
+5. 修改 `interactive-replace` 在 chat tab 中禁用替换
+6. 搜索框 DPI 缩放、系统字体、暗色主题跟随
+7. **实时搜索**:输入框内每输入/删除字符即触发 `floating-search-on-input` 自动重新搜索,无需回车
+8. **缓存 `inputScrollArea_` 指针**:避免每次按键调用 `findChild`,减少事件过滤器中的开销
+
+## 6 Why
+commit [0228] 通过 blanket `tmfs:` 检查禁用了所有 tmfs:// 缓冲区的搜索来避免 crash。用户需要在 Chat Tab 的消息输出框中搜索对话内容。Chat Tab 的嵌入式 buffer 不支持 auxiliary-widget 机制,需要独立的搜索 UI。
+
+## 7 How
+1. 实现通用悬浮搜索栏 Qt 组件 `QTMFloatingSearchBar`,包含输入框、导航按钮、关闭按钮、匹配计数标签、模式切换按钮
+2. 通过 `QHash` 按 parent widget 管理实例,parent 销毁时自动清理
+3. `QTMFloatingSearchBar` 自身安装 eventFilter 处理 parent resize,自动重新定位到右上角
+4. Scheme 端新增 `floating-search-init`、`floating-search-next`、`floating-search-close`、`floating-search-toggle-mode` 函数
+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 端在 `floating-search-init` 中检测 `liii-night` 主题,自动给搜索缓冲区添加 `"dark"` 样式包
+9. text 模式:`accept-search-result?` 直接返回 `#t`,依赖 `tree-perform-search` 中 `access=0` 引擎层过滤文本节点;math 模式:独立 `search-path-inside-math?` 检查路径是否在数学环境内
+10. `connectSignals` 中 `closeRequested` 始终连接 `hide()`,不依赖 `close_cmd_` 是否为空;callback 非空时才额外调 `eval_scheme`
+11. `QTMFloatingSearchBar` 用 `mathMode_` 成员变量追踪当前模式,`setModeIcon` 同步写入,`toggleMode` 直接读取,避免依赖 `objectName` 推断导致 C++/Scheme 端状态漂移
+12. 暗色主题 CSS 选择器 `QWidget#floating_search_bar` 与亮色主题统一,去掉多余的 `#centralWidget` 祖先约束
+13. `floating-search-toggle-mode` 切换 text/math 模式时保存并恢复 aux buffer body,保留用户已输入的搜索文本
+14. **实时搜索**:`QTMFloatingSearchBar::eventFilter` 在输入区(QAbstractScrollArea)收到非修饰键/非导航键按键时,通过 `QMetaObject::invokeMethod` + `Qt::QueuedConnection` 异步调用 `(floating-search-on-input)`,Scheme 端比较 `tree->string (buffer-get-body aux)` 与 `floating-search-last-content`,仅内容真变化时才执行 `perform-search*`,避免重复搜索
+15. **缓存 scroll area**:`inputScrollArea_` 成员在 `setSearchInput` 的延迟 lambda 中赋值、replace 旧输入时清除,`eventFilter` 和 `activate` 复用缓存指针,避免每次事件都 `findChild`
+16. **导航键过滤**:方向键、Home/End/PageUp/PageDown 不触发实时搜索,避免无内容变化的输入事件产生多余 Scheme 调用
+
+---
+
+## 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..83bc13aee3 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_;
}
@@ -450,6 +457,9 @@ void
ChatController::activateSession (const string& sessionId) {
if (!view_) return;
+ // 切换 session 时隐藏悬浮搜索栏
+ qt_floating_search_bar_show (view_->contentWidget (), false);
+
ChatConversationPanel* panel= getOrCreatePanel (sessionId);
if (!panel) return;
@@ -705,3 +715,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_chat_tab_widget.hpp b/src/Plugins/Qt/qt_chat_tab_widget.hpp
index 4a5e5ab74b..1d34b4f7a9 100644
--- a/src/Plugins/Qt/qt_chat_tab_widget.hpp
+++ b/src/Plugins/Qt/qt_chat_tab_widget.hpp
@@ -355,6 +355,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..5eaf772006
--- /dev/null
+++ b/src/Plugins/Qt/qt_floating_search_bar.cpp
@@ -0,0 +1,501 @@
+
+/******************************************************************************
+ * MODULE : qt_floating_search_bar.cpp
+ * DESCRIPTION: A VSCode-style floating search bar widget
+ * COPYRIGHT : (C) 2026 Yuki Lu
+ ******************************************************************************
+ * 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_dpi_utils.hpp"
+#include "qt_utilities.hpp"
+#include "qt_widget.hpp"
+
+#include "s7_tm.hpp"
+#include "tm_window.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace moebius;
+
+// ---- 尺寸常量(逻辑像素,经 DpiUtils 缩放) ----
+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 kShadowBlur = 8;
+constexpr int kShadowOffsetY= 1;
+constexpr int kShadowAlpha = 30;
+
+constexpr int kPosRightPad = 8;
+constexpr int kPosTopPad = 4;
+constexpr int kInnerSpacing= 4;
+
+/******************************************************************************
+ * QTMFloatingSearchBar 实现
+ ******************************************************************************/
+
+QTMFloatingSearchBar::QTMFloatingSearchBar (QWidget* parent)
+ : QWidget (parent) {
+ setObjectName ("floating_search_bar");
+ setWindowFlags (Qt::Widget);
+ setAttribute (Qt::WA_StyledBackground);
+ setMinimumHeight (DpiUtils::scaled (kBarMinHeight));
+
+ // border-radius 由代码动态计算,支持 DPI 缩放
+ setStyleSheet (QString ("#floating_search_bar {"
+ " border-radius: %1px;"
+ "}")
+ .arg (DpiUtils::scaled (kBarRadius)));
+
+ 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);
+
+ // 外层水平布局:左边 [输入区],右边 [按钮 + 匹配信息]
+ auto* 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);
+
+ // 右侧:垂直布局 [按钮行] + [匹配信息]
+ auto* rightLayout= new QVBoxLayout ();
+ rightLayout->setSpacing (DpiUtils::scaled (kInnerSpacing));
+
+ // 右侧上层:按钮行
+ auto* btnRow= new QHBoxLayout ();
+ btnRow->setSpacing (DpiUtils::scaled (kInnerSpacing));
+
+ const QString btnRadiusStyle=
+ QString (
+ "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));
+#ifdef Q_OS_MAC
+ modeBtn_->setToolTip (qt_translate ("text mode (Option+Tab)"));
+#else
+ modeBtn_->setToolTip (qt_translate ("text mode (Ctrl+Tab)"));
+#endif
+ modeBtn_->setStyleSheet (btnRadiusStyle);
+ btnRow->addWidget (modeBtn_);
+
+ auto* prevBtn= new QToolButton (this);
+ prevBtn->setObjectName ("floating-search-prev");
+ prevBtn->setFixedSize (DpiUtils::scaled (kBtnSize),
+ DpiUtils::scaled (kBtnSize));
+#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);
+
+ auto* nextBtn= new QToolButton (this);
+ nextBtn->setObjectName ("floating-search-next");
+ nextBtn->setFixedSize (DpiUtils::scaled (kBtnSize),
+ DpiUtils::scaled (kBtnSize));
+ nextBtn->setToolTip (qt_translate ("Next (Enter)"));
+ nextBtn->setStyleSheet (btnRadiusStyle);
+ btnRow->addWidget (nextBtn);
+
+ auto* closeBtn= new QToolButton (this);
+ closeBtn->setObjectName ("floating-search-close");
+ closeBtn->setFixedSize (DpiUtils::scaled (kBtnSize),
+ DpiUtils::scaled (kBtnSize));
+ 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 (qt_translate ("No matches"));
+ rightLayout->addWidget (infoLbl_);
+
+ // 组装:左输入(stretch=1) + 右面板
+ mainLayout->addLayout (rowLayout_, 1);
+ mainLayout->addLayout (rightLayout);
+
+ connect (nextBtn, &QToolButton::clicked, this,
+ &QTMFloatingSearchBar::findNextRequested);
+ connect (prevBtn, &QToolButton::clicked, this,
+ &QTMFloatingSearchBar::findPreviousRequested);
+ connect (closeBtn, &QToolButton::clicked, this,
+ &QTMFloatingSearchBar::closeRequested);
+ connect (modeBtn_, &QToolButton::clicked, this,
+ &QTMFloatingSearchBar::toggleMode);
+
+ if (parent) parent->installEventFilter (this);
+
+ hide ();
+}
+
+QTMFloatingSearchBar::~QTMFloatingSearchBar () {
+ if (parent ()) parent ()->removeEventFilter (this);
+}
+
+void
+QTMFloatingSearchBar::setSearchInput (QWidget* input) {
+ if (inputQW_) {
+ if (inputScrollArea_) {
+ inputScrollArea_->removeEventFilter (this);
+ inputScrollArea_= nullptr;
+ }
+ rowLayout_->removeWidget (inputQW_);
+ inputQW_->deleteLater ();
+ }
+ inputQW_= input;
+ if (input) {
+ input->setObjectName ("floating-search-input");
+ rowLayout_->insertWidget (0, input, 1);
+ // texmacs_input_widget 内部的 QAbstractScrollArea 可能延迟创建(show 时),
+ // 所有 scroll area 设置统一延迟到事件循环处理
+ QMetaObject::invokeMethod (
+ this,
+ [this] {
+ if (inputQW_) {
+ QAbstractScrollArea* sa=
+ inputQW_->findChild ();
+ if (sa) {
+ sa->viewport ()->setBackgroundRole (QPalette::Base);
+ sa->installEventFilter (this);
+ sa->setFocus ();
+ inputScrollArea_= sa;
+ }
+ else {
+ inputScrollArea_= nullptr;
+ inputQW_->setFocus ();
+ }
+ }
+ },
+ Qt::QueuedConnection);
+ }
+}
+
+void
+QTMFloatingSearchBar::activate () {
+ show ();
+ raise ();
+ if (inputQW_) {
+ QAbstractScrollArea* sa= inputScrollArea_;
+ if (!sa) sa= inputQW_->findChild ();
+ if (sa) sa->setFocus ();
+ else inputQW_->setFocus ();
+ }
+}
+
+void
+QTMFloatingSearchBar::setMatchInfo (int current, int 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;
+ }
+}
+
+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_); });
+ }
+ // close: 始终 hide,callback 非空时才调 eval_scheme
+ connect (this, &QTMFloatingSearchBar::closeRequested, this, [this] () {
+ if (!is_empty (close_cmd_)) eval_scheme (close_cmd_);
+ hide ();
+ });
+}
+
+void
+QTMFloatingSearchBar::setModeIcon (bool mathMode) {
+ if (!modeBtn_) return;
+ mathMode_= mathMode;
+ modeBtn_->setObjectName (mathMode ? "floating-search-mode-math"
+ : "floating-search-mode-text");
+ modeBtn_->style ()->unpolish (modeBtn_);
+ modeBtn_->style ()->polish (modeBtn_);
+#ifdef Q_OS_MAC
+ modeBtn_->setToolTip (mathMode ? qt_translate ("math mode (Option+Tab)")
+ : qt_translate ("text mode (Option+Tab)"));
+#else
+ modeBtn_->setToolTip (mathMode ? qt_translate ("math mode (Ctrl+Tab)")
+ : qt_translate ("text mode (Ctrl+Tab)"));
+#endif
+}
+
+void
+QTMFloatingSearchBar::toggleMode () {
+ bool isMath= !mathMode_;
+ setModeIcon (isMath);
+ eval_scheme ("(floating-search-toggle-mode)");
+}
+
+bool
+QTMFloatingSearchBar::eventFilter (QObject* watched, QEvent* event) {
+ if (event->type () == QEvent::Resize && watched == parent () &&
+ isVisible ()) {
+ reposition ();
+ }
+ if (event->type () == QEvent::KeyPress && isVisible () && inputQW_) {
+ auto* ke= static_cast (event);
+ QAbstractScrollArea* sa= inputScrollArea_;
+
+ if (ke->key () == Qt::Key_Escape) {
+ if (sa && watched == sa) {
+ emit closeRequested ();
+ return true;
+ }
+ }
+#ifdef Q_OS_MAC
+ if (ke->key () == Qt::Key_Tab && (ke->modifiers () & Qt::AltModifier)) {
+#else
+ if (ke->key () == Qt::Key_Tab && (ke->modifiers () & Qt::ControlModifier)) {
+#endif
+ toggleMode ();
+ return true;
+ }
+
+ // 输入区内非修饰键/非导航键按键:触发实时搜索更新
+ if (sa && watched == sa) {
+ switch (ke->key ()) {
+ case Qt::Key_Shift:
+ case Qt::Key_Control:
+ case Qt::Key_Meta:
+ case Qt::Key_Alt:
+ case Qt::Key_Left:
+ case Qt::Key_Right:
+ case Qt::Key_Up:
+ case Qt::Key_Down:
+ case Qt::Key_Home:
+ case Qt::Key_End:
+ case Qt::Key_PageUp:
+ case Qt::Key_PageDown:
+ break;
+ default:
+ QMetaObject::invokeMethod (
+ this, [this] () { eval_scheme ("(floating-search-on-input)"); },
+ Qt::QueuedConnection);
+ break;
+ }
+ }
+ }
+ return QWidget::eventFilter (watched, event);
+}
+
+void
+QTMFloatingSearchBar::showEvent (QShowEvent* event) {
+ QWidget::showEvent (event);
+ reposition ();
+}
+
+void
+QTMFloatingSearchBar::reposition () {
+ QWidget* p= qobject_cast (parent ());
+ if (!p) return;
+ int x= p->width () - width () - DpiUtils::scaled (kPosRightPad);
+ int y= DpiUtils::scaled (kPosTopPad);
+ move (x, y);
+}
+
+/******************************************************************************
+ * 搜索栏管理器
+ ******************************************************************************/
+
+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;
+
+ QWidget* pw= parent;
+ QObject::connect (parent, &QObject::destroyed,
+ [pw] () { searchBars ().remove (pw); });
+
+ 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 ();
+}
+
+bool
+qt_floating_search_bar_init (QWidget* parent, const string& aux_url_str,
+ const string& mode) {
+ 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;
+ if (mode == "math") {
+ doc= tree (WITH, "font", "sys-chinese", "zoom-factor",
+ as_string (searchZoom), "mode", "math", tree (DOCUMENT, ""));
+ }
+ else {
+ doc= tree (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;
+ }
+ QWidget* inputW= concrete (tw)->as_qwidget ();
+ if (!inputW) {
+ bar->hide ();
+ return false;
+ }
+ bar->setSearchInput (inputW);
+ return true;
+}
+
+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);
+}
+
+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);
+}
+
+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);
+ }
+}
+
+/******************************************************************************
+ * 兼容层胶水函数(通过 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) {
+ 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 {
+ qt_floating_search_bar_show (parent, false);
+ }
+}
+
+void
+qt_floating_search_init (string aux_url_str, string mode) {
+ QWidget* parent= get_provider_parent ();
+ if (!parent) return;
+ qt_floating_search_bar_init (parent, aux_url_str, mode);
+}
+
+void
+qt_floating_search_set_match_info (int current, int 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
new file mode 100644
index 0000000000..65fcf8af74
--- /dev/null
+++ b/src/Plugins/Qt/qt_floating_search_bar.hpp
@@ -0,0 +1,132 @@
+
+/******************************************************************************
+ * MODULE : qt_floating_search_bar.hpp
+ * DESCRIPTION: A VSCode-style floating search bar widget for TeXmacs
+ * COPYRIGHT : (C) 2026 Yuki Lu
+ ******************************************************************************
+ * 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"
+
+#include
+
+class QAbstractScrollArea;
+
+/**
+ * VSCode 风格的悬浮搜索栏组件。
+ *
+ * 布局:
+ * 左侧:嵌入的输入框(如 texmacs_input_widget)
+ * 右侧:[上一个] [下一个] [关闭] 按钮 + 匹配计数
+ */
+class QTMFloatingSearchBar : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit QTMFloatingSearchBar (QWidget* parent= nullptr);
+ ~QTMFloatingSearchBar () override;
+
+ /// 设置嵌入的搜索输入框。旧的输入框(如有)会被移除并 deleteLater。
+ void setSearchInput (QWidget* input);
+ /// 显示搜索栏并聚焦输入框。
+ void activate ();
+ /// 设置匹配信息(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);
+ void setModeIcon (bool mathMode);
+ void toggleMode ();
+
+signals:
+ void findNextRequested ();
+ void findPreviousRequested ();
+ void closeRequested ();
+ void modeToggled ();
+
+protected:
+ bool eventFilter (QObject* watched, QEvent* event) override;
+ void showEvent (QShowEvent* event) override;
+
+private:
+ void reposition ();
+ void connectSignals ();
+
+ QHBoxLayout* rowLayout_ = nullptr;
+ QWidget* inputQW_ = nullptr;
+ QAbstractScrollArea* inputScrollArea_= nullptr;
+ QLabel* infoLbl_ = nullptr;
+ QToolButton* modeBtn_ = nullptr;
+
+ string next_cmd_;
+ string prev_cmd_;
+ string close_cmd_;
+ bool mathMode_ = false;
+ 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 输入框初始化。\a mode 为 "text" 或 "math",决定输入框的数学环境。
+/// 失败时返回 false。
+bool qt_floating_search_bar_init (QWidget* parent, const string& aux_url_str,
+ const string& mode);
+
+/// 更新 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);
+
+/// Scheme 胶水函数:传入 search-buffer URL 和 mode ("text"/"math"),
+/// 创建 texmacs-input 并嵌入浮动搜索栏。
+void qt_floating_search_init (string aux_url_str, string mode);
+
+/// 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
diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua
index 30cc2501e3..5624668ee5 100644
--- a/src/Scheme/L5/glue_widget.lua
+++ b/src/Scheme/L5/glue_widget.lua
@@ -526,6 +526,49 @@ function main()
"int",
"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",
+ ret_type = "void",
+ arg_list = {
+ "string"
+ }
+ },
+ {
+ scm_name = "qt-floating-search-init",
+ cpp_name = "qt_floating_search_init",
+ ret_type = "void",
+ arg_list = {
+ "string",
+ "string"
+ }
+ },
+ {
+ scm_name = "qt-floating-search-set-match-info",
+ cpp_name = "qt_floating_search_set_match_info",
+ ret_type = "void",
+ arg_list = {
+ "int",
+ "int"
+ }
+ },
+ {
+ scm_name = "qt-floating-search-set-callbacks",
+ cpp_name = "qt_floating_search_set_callbacks",
+ ret_type = "void",
+ arg_list = {
+ "string",
+ "string",
+ "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"
diff --git a/tests/Plugins/Qt/qt_floating_search_bar_test.cpp b/tests/Plugins/Qt/qt_floating_search_bar_test.cpp
new file mode 100644
index 0000000000..e8604e36af
--- /dev/null
+++ b/tests/Plugins/Qt/qt_floating_search_bar_test.cpp
@@ -0,0 +1,122 @@
+
+/******************************************************************************
+ * MODULE : qt_floating_search_bar_test.cpp
+ * DESCRIPTION: Tests for QTMFloatingSearchBar widget
+ * COPYRIGHT : (C) 2026 Yuki Lu
+ *******************************************************************************
+ * 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/qt_floating_search_bar.hpp"
+#include "base.hpp"
+#include
+#include
+#include
+
+class TestFloatingSearchBar : public QObject {
+ Q_OBJECT
+
+private slots:
+ void init () { init_lolly (); }
+
+ // === 构造 ===
+ void test_constructor ();
+
+ // === setMatchInfo ===
+ void test_matchInfo_default_no_matches ();
+ void test_matchInfo_with_matches ();
+ void test_matchInfo_zero_matches ();
+
+ // === setModeIcon ===
+ void test_modeIcon_default_text_mode ();
+ void test_modeIcon_switch_to_math ();
+ void test_modeIcon_switch_back_to_text ();
+};
+
+/******************************************************************************
+ * 构造
+ ******************************************************************************/
+
+void
+TestFloatingSearchBar::test_constructor () {
+ QTMFloatingSearchBar bar;
+ QCOMPARE (bar.objectName (), QString ("floating_search_bar"));
+ QVERIFY (bar.findChild ("floating-search-info") != nullptr);
+ QVERIFY (bar.findChild ("floating-search-mode-text") !=
+ nullptr);
+ QVERIFY (bar.findChild ("floating-search-prev") != nullptr);
+ QVERIFY (bar.findChild ("floating-search-next") != nullptr);
+ QVERIFY (bar.findChild ("floating-search-close") != nullptr);
+}
+
+/******************************************************************************
+ * setMatchInfo
+ ******************************************************************************/
+
+void
+TestFloatingSearchBar::test_matchInfo_default_no_matches () {
+ QTMFloatingSearchBar bar;
+ auto* info= bar.findChild ("floating-search-info");
+ QVERIFY (info != nullptr);
+ // 构造后默认应显示 "No matches"(英文环境下)
+ QVERIFY (info->text ().contains ("No matches"));
+}
+
+void
+TestFloatingSearchBar::test_matchInfo_with_matches () {
+ QTMFloatingSearchBar bar;
+ bar.setMatchInfo (3, 10);
+ auto* info= bar.findChild ("floating-search-info");
+ QVERIFY (info != nullptr);
+ QVERIFY (info->text ().contains ("3"));
+ QVERIFY (info->text ().contains ("10"));
+}
+
+void
+TestFloatingSearchBar::test_matchInfo_zero_matches () {
+ QTMFloatingSearchBar bar;
+ // 先设置为有匹配
+ bar.setMatchInfo (1, 5);
+ // 再清零
+ bar.setMatchInfo (0, 0);
+ auto* info= bar.findChild ("floating-search-info");
+ QVERIFY (info != nullptr);
+ QVERIFY (info->text ().contains ("No matches"));
+}
+
+/******************************************************************************
+ * setModeIcon
+ ******************************************************************************/
+
+void
+TestFloatingSearchBar::test_modeIcon_default_text_mode () {
+ QTMFloatingSearchBar bar;
+ auto* modeBtn= bar.findChild ();
+ QVERIFY (modeBtn != nullptr);
+ // 默认是 text mode,objectName 应为 floating-search-mode-text
+ QCOMPARE (modeBtn->objectName (), QString ("floating-search-mode-text"));
+}
+
+void
+TestFloatingSearchBar::test_modeIcon_switch_to_math () {
+ QTMFloatingSearchBar bar;
+ bar.setModeIcon (true); // math mode
+ auto* modeBtn= bar.findChild ();
+ QVERIFY (modeBtn != nullptr);
+ QCOMPARE (modeBtn->objectName (), QString ("floating-search-mode-math"));
+}
+
+void
+TestFloatingSearchBar::test_modeIcon_switch_back_to_text () {
+ QTMFloatingSearchBar bar;
+ bar.setModeIcon (true); // math
+ bar.setModeIcon (false); // back to text
+ auto* modeBtn= bar.findChild ();
+ QVERIFY (modeBtn != nullptr);
+ QCOMPARE (modeBtn->objectName (), QString ("floating-search-mode-text"));
+}
+
+QTEST_MAIN (TestFloatingSearchBar)
+#include "qt_floating_search_bar_test.moc"