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 c18a0c313c..b8691fcec1 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" "") @@ -1608,6 +1610,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 +1874,8 @@ ("previous screen" "后一屏") ("previous similar" "相似的(前一个)") ("previous" "前一个") +("Previous (Ctrl+Enter)" "上一个 (Ctrl+Enter)") +("Previous (Cmd+Enter)" "上一个 (Cmd+Enter)") ("primary" "") ("prime" "") ("print all to file" "全部打印为文件") diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index 0c281c7a28..2a4047aadf 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,26 @@ ;; ---- ;; 此函数用于管理搜索辅助缓冲区的生命周期,确保每个主文档视图有唯一的搜索缓冲区。 (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 搜索激活且当前在 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) + (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 +344,40 @@ ) ;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 搜索激活且当前在 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 + (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 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)))) (tm-define (inside-replace-buffer?) (== (current-buffer) (replace-buffer))) @@ -417,16 +443,23 @@ ) ;define (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* + (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)) + (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) + (display* "Warning: accept-search-result? error: " msg "\n") + (if chat-tab-search-active? #t #f))) ) ;or ) ;define @@ -451,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))) @@ -637,7 +673,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1112,6 +1148,80 @@ ) ;when ) ;tm-define +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Chat tab search (floating search bar) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(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) + (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 + (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)) + ;; 同步暗色样式到搜索缓冲区 + (when (== (get-preference "gui theme") "liii-night") + (with-buffer aux + (when (not (has-style-package? "dark")) + (add-style-package "dark")))) + (qt-floating-search "true"))) + +(tm-define (chat-tab-search-next forward?) + (when (and chat-tab-search-target chat-tab-search-aux) + (with-buffer chat-tab-search-target + (search-rotate-match forward?)))) + +(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) + (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))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Search and replace widget ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1393,6 +1503,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) @@ -1626,19 +1737,57 @@ (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 + ((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:// 缓冲区保持禁用搜索 + (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)) ) ;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..9fa87c1dd1 --- /dev/null +++ b/devel/1042.md @@ -0,0 +1,167 @@ +# [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 确定性测试(单元测试) +无单元测试,需手动验证。 + +### 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 缩放、系统字体、暗色主题跟随 + +## 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 端新增 `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"` 样式包 + +--- + +## 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..9652d2ab34 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_; } @@ -705,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_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..df281c077e --- /dev/null +++ b/src/Plugins/Qt/qt_floating_search_bar.cpp @@ -0,0 +1,427 @@ + +/****************************************************************************** + * 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_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)); + 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), + 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, [this] () { + bool isMath= (modeBtn_->objectName () == + QStringLiteral ("floating-search-mode-text")); + setModeIcon (isMath); + eval_scheme ("(chat-tab-search-toggle-mode)"); + }); + + if (parent) parent->installEventFilter (this); + + hide (); +} + +QTMFloatingSearchBar::~QTMFloatingSearchBar () { + if (parent ()) parent ()->removeEventFilter (this); +} + +void +QTMFloatingSearchBar::setSearchInput (QWidget* input) { + if (inputQW_) { + QAbstractScrollArea* oldArea= inputQW_->findChild (); + if (oldArea) oldArea->removeEventFilter (this); + rowLayout_->removeWidget (inputQW_); + inputQW_->deleteLater (); + } + inputQW_= input; + if (input) { + input->setObjectName ("floating-search-input"); + QAbstractScrollArea* scrollArea= input->findChild (); + if (scrollArea) { + scrollArea->viewport ()->setBackgroundRole (QPalette::Base); + scrollArea->installEventFilter (this); + } + rowLayout_->insertWidget (0, input, 1); + } +} + +void +QTMFloatingSearchBar::activate () { + show (); + raise (); + if (inputQW_) { + QAbstractScrollArea* 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_); }); + } + if (!is_empty (close_cmd_)) { + connect (this, &QTMFloatingSearchBar::closeRequested, this, [this] () { + eval_scheme (close_cmd_); + hide (); + }); + } +} + +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 () && + 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); +} + +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) { + 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); + 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) { + QWidget* parent= get_provider_parent (); + if (!parent) return; + qt_floating_search_bar_init (parent, aux_url_str); +} + +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..024865274d --- /dev/null +++ b/src/Plugins/Qt/qt_floating_search_bar.hpp @@ -0,0 +1,125 @@ + +/****************************************************************************** + * 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 "string.hpp" + +#include + +/** + * 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); + +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; + QLabel* infoLbl_ = nullptr; + QToolButton* modeBtn_ = nullptr; + + string next_cmd_; + string prev_cmd_; + string close_cmd_; + string mode_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); + +/// Scheme 胶水函数:传入 search-buffer URL,创建 texmacs-input +/// 并嵌入浮动搜索栏。 +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 diff --git a/src/Scheme/L5/glue_widget.lua b/src/Scheme/L5/glue_widget.lua index 30cc2501e3..ba9da6918b 100644 --- a/src/Scheme/L5/glue_widget.lua +++ b/src/Scheme/L5/glue_widget.lua @@ -526,6 +526,48 @@ 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" + } + }, + { + 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"