Skip to content
Merged
25 changes: 25 additions & 0 deletions TeXmacs/misc/themes/liii-night.css
Original file line number Diff line number Diff line change
Expand Up @@ -916,3 +916,28 @@ QWidget#centralWidget QWidget {
background: #202020;
color: #ffffff;
}

/*文本工具栏窗口样式*/
QWidget#text_toolbar {
background: #333333;
border: none;
border-radius: 8px;
}

/*文本工具栏按钮样式*/
QToolButton#text-toolbar-button {
background-color: transparent;
border: none;
}

/*文本工具栏按钮悬停样式*/
QToolButton#text-toolbar-button:hover {
background-color: rgba(255, 255, 255, 0.1);
border: none;
}

/*文本工具栏按钮按下样式*/
QToolButton#text-toolbar-button:pressed {
background-color: rgba(255, 255, 255, 0.2);
border: none;
}
25 changes: 25 additions & 0 deletions TeXmacs/misc/themes/liii.css
Original file line number Diff line number Diff line change
Expand Up @@ -892,3 +892,28 @@ QWidget#auxiliary_container {
#guestNotificationCloseButton:pressed {
background-color: rgba(0, 0, 0, 0.2);
}

/*文本工具栏窗口样式*/
QWidget#text_toolbar {
background: #ffffff;
border: none;
border-radius: 8px;
}

/*文本工具栏按钮样式*/
QToolButton#text-toolbar-button {
background-color: transparent;
border: none;
}

/*文本工具栏按钮悬停样式*/
QToolButton#text-toolbar-button:hover {
background-color: rgba(128, 128, 128, 0.3);
border: none;
}

/*文本工具栏按钮按下样式*/
QToolButton#text-toolbar-button:pressed {
background-color: rgba(128, 128, 128, 0.5);
border: none;
}
107 changes: 107 additions & 0 deletions devel/201_63.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@

# 201_63 文本选中悬浮框

## 如何测试

### 手动测试
1. **基本功能测试**
- 选中一段文本,测试是否会弹出文本工具栏
- 点击工具栏外部,测试是否会隐藏
- 滚动页面,测试工具栏是否跟随移动

2. **显示条件测试**

**会显示的情况**(需同时满足):
- ✅ 不在数学模式(`in-math?`)
- ✅ 不在编程模式(`in-prog?`)
- ✅ 不在代码模式(`in-code?`)
- ✅ 不在原文模式(`in-verbatim?`)
- ✅ 有活动的文本选区
- ✅ 选区内容非空

**不会显示的情况**(满足任一):
- ❌ 在数学公式中选中
- ❌ 在代码块中选中
- ❌ 在程序块中选中
- ❌ 在原文环境中选中
- ❌ 没有选区(仅光标)
- ❌ 选区为空字符串
- ❌ 正在拖拽鼠标(`left_dragging`)
- ❌ 选区不在视图范围内

3. **缓存机制测试**
- 快速移动鼠标,观察工具栏是否稳定(100ms缓存)
- 选区变化后,观察是否及时更新

### 自动测试
```bash
# 编译测试
xmake b text_toolbar_test

# 运行测试
xmake r text_toolbar_test
```

**测试覆盖**:
- 缓存机制验证
- 缓存失效功能
- 空选区处理
- 有效选区矩形计算
- 坐标转换一致性


## 2026/3/6 性能优化与代码清理
### What
- 基于 Chen Jie 已经实现的代码,进行拆分优化
- 添加 should_show_text_toolbar() 缓存机制,减少 Scheme 调用
- 消除 selection_active_any() 重复检查
- 简化 update_text_toolbar() 中的冗余计算

### Why
- 每次鼠标移动调用 4 次 Scheme call(),性能开销大
- selection_active_any() 被重复调用
- min/max 计算冗余
- 存在未使用的代码残留

### How
1. **添加缓存字段**(edit_interface.hpp):
```cpp
time_t text_toolbar_last_check = 0;
bool text_toolbar_last_result = false;
```

2. **优化 should_show_text_toolbar()**:
- 使用 100ms 缓存,避免频繁 Scheme 调用
- 缓存上次检查结果和时间戳

3. **简化 get_text_selection_rect()**:
- 单点检查 selection_active_any()
- 移除重复逻辑分支

4. **重构 update_text_toolbar()**:
- 早期返回无效矩形
- 移除冗余 min/max 计算
- 简化控制流程

5. **修复类型转换安全性**(edit_mouse.cpp):
- static_cast 改为 dynamic_cast
- 添加空指针检查
```cpp
// 修改前
qt_simple_widget_rep* qsw = static_cast<qt_simple_widget_rep*>(this);

// 修改后
if (qt_simple_widget_rep* qsw = dynamic_cast<qt_simple_widget_rep*>(this)) {
qsw->show_text_toolbar(...);
}
```

6. **添加缓存失效方法**(edit_interface.hpp/cpp):
```cpp
void invalidate_text_toolbar_cache();
// 实现:text_toolbar_last_check = 0;
```

7. **添加单元测试**(tests/Edit/Interface/text_toolbar_test.cpp):
- 测试缓存机制和失效
- 测试矩形有效性验证
2 changes: 2 additions & 0 deletions src/Edit/Interface/edit_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,8 @@ edit_interface_rep::apply_changes () {
selection_rects= rs;
invalidate (selection_rects);
}
// 选区改变后更新文本工具栏
update_text_toolbar ();
}
Comment on lines 994 to 997
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_changes() calls update_text_toolbar() on THE_SELECTION, but should_show_text_toolbar() caches its result for 100ms. Without invalidating the cache here, a selection change can be ignored until the cache expires, causing the toolbar to show/hide late. Call invalidate_text_toolbar_cache() (or otherwise force a recheck) before update_text_toolbar() when the selection changes.

Copilot uses AI. Check for mistakes.

// cout << "Handling alternative selection\n";
Expand Down
12 changes: 12 additions & 0 deletions src/Edit/Interface/edit_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ class edit_interface_rep : virtual public editor_rep {
SI table_line_second_size= 0;
bool table_line_wide_flag = false; // 用于格线移动的标记,只在点击时更新
double table_line_mark = 0.0;

// 文本工具栏缓存,用于性能优化
time_t text_toolbar_last_check = 0;
bool text_toolbar_last_result= false;
bool table_line_hit (SI x, SI y, table_hit& hit);
void table_line_start (const table_hit& hit, SI x, SI y);
void table_line_apply (SI x, SI y);
Expand Down Expand Up @@ -257,6 +261,14 @@ class edit_interface_rep : virtual public editor_rep {
void update_mouse_loci ();
void update_focus_loci ();
bool should_show_image_popup (tree t);
bool should_show_text_toolbar ();
rectangle get_text_selection_rect ();
void show_text_toolbar (rectangle selr, double magf, int scroll_x,
int scroll_y, int canvas_x, int canvas_y);
void hide_text_toolbar ();
bool is_point_in_text_toolbar (SI x, SI y);
void update_text_toolbar ();
void invalidate_text_toolbar_cache (); // 重置工具栏缓存

/* the footer */
tree get_shortcut_suffix (string cmd_s);
Expand Down
2 changes: 2 additions & 0 deletions src/Edit/Interface/edit_keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ edit_interface_rep::handle_keypress (string key_u8, time_t t) {
if (!is_nil (focus_ids) && got_focus)
call ("link-follow-ids", object (focus_ids), object ("focus"));
notify_change (THE_DECORATIONS);
// 键盘事件后更新文本工具栏显示状态
update_text_toolbar ();
end_editing ();
// time_t t2= texmacs_time ();
// if (t2 - t1 >= 10) cout << "handle_keypress took " << t2-t1 << "ms\n";
Expand Down
149 changes: 146 additions & 3 deletions src/Edit/Interface/edit_mouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "path.hpp"
#include "qapplication.h"
#include "qnamespace.h"
#include "qt_simple_widget.hpp"
#include "scheme.hpp"
#include "sys_utils.hpp"
#include "tm_buffer.hpp"
Expand Down Expand Up @@ -917,10 +918,14 @@ edit_interface_rep::mouse_any (string type, SI x, SI y, int mods, time_t t,
show_image_popup (tree_of_image_parent, selr, magf, get_scroll_x (),
get_scroll_y (), get_canvas_x (), get_canvas_y ());
}
hide_text_toolbar ();
}
else {
set_cursor_style ("normal");
hide_image_popup ();

// 检查是否应该显示文本工具栏
update_text_toolbar ();
}

if (type == "move") mouse_message ("move", x, y);
Expand Down Expand Up @@ -1030,10 +1035,14 @@ edit_interface_rep::mouse_any (string type, SI x, SI y, int mods, time_t t,
if (type == "press-up") mouse_scroll (x, y, true);
if (type == "press-down") mouse_scroll (x, y, false);

if ((type == "press-left") || (type == "release-left") ||
(type == "end-drag-left") || (type == "press-middle") ||
(type == "press-right"))
if ((type == "press-left") || (type == "press-middle") ||
(type == "press-right")) {
// 当用户点击其他地方(不在文本工具栏内)时,隐藏文本工具栏
if (!is_point_in_text_toolbar (x, y)) {
hide_text_toolbar ();
}
notify_change (THE_DECORATIONS);
}

if (type == "wheel" && N (data) == 2)
eval ("(wheel-event " * as_string (data[0]) * " " * as_string (data[1]) *
Expand Down Expand Up @@ -1165,3 +1174,137 @@ edit_interface_rep::handle_mouse (string kind, SI x, SI y, int m, time_t t,
}
handle_exceptions ();
}

/******************************************************************************
* Text toolbar support
******************************************************************************/

bool
edit_interface_rep::should_show_text_toolbar () {
// 缓存结果100ms,避免过多的Scheme调用
time_t now= texmacs_time ();
if (now - text_toolbar_last_check < 100) {
return text_toolbar_last_result;
}
text_toolbar_last_check= now;

if (as_bool (call ("in-math?")) || as_bool (call ("in-prog?")) ||
as_bool (call ("in-code?")) || as_bool (call ("in-verbatim?"))) {
text_toolbar_last_result= false;
return false;
}
// 检查是否有活动的文本选区
if (!selection_active_any ()) {
text_toolbar_last_result= false;
return false;
}

// 检查选区是否非空
tree sel_tree= selection_get ();
if (is_atomic (sel_tree) && as_string (sel_tree) == "") {
text_toolbar_last_result= false;
return false;
}

text_toolbar_last_result= true;
return true;
}

rectangle
edit_interface_rep::get_text_selection_rect () {
rectangle sel_rect;

// 单次检查selection_active_any,避免重复调用
if (!selection_active_any ()) return sel_rect;

if (!is_nil (selection_rects)) {
// 使用现有的选区矩形
sel_rect= least_upper_bound (selection_rects);
}
else {
// 如果没有选区矩形,但选区存在,计算一个默认矩形
path p1, p2;
selection_get (p1, p2);
if (p1 != p2) {
selection sel= search_selection (p1, p2);
if (!is_nil (sel->rs)) {
sel_rect= least_upper_bound (sel->rs);
}
else {
// 如果选区矩形为空,使用光标位置创建一个最小矩形
cursor cu= get_cursor ();
sel_rect = rectangle (cu->ox - 10 * pixel, cu->oy - 5 * pixel,
cu->ox + 10 * pixel, cu->oy + 5 * pixel);
}
}
}

return sel_rect;
}

void
edit_interface_rep::show_text_toolbar (rectangle selr, double magf,
int scroll_x, int scroll_y, int canvas_x,
int canvas_y) {
// 通过qt_simple_widget显示文本工具栏
// 使用dynamic_cast进行安全的类型转换
if (qt_simple_widget_rep* qsw= dynamic_cast<qt_simple_widget_rep*> (this)) {
qsw->show_text_toolbar (selr, magf, scroll_x, scroll_y, canvas_x, canvas_y);
}
// 如果转换失败,静默返回(非Qt环境)
}

void
edit_interface_rep::hide_text_toolbar () {
// 通过qt_simple_widget隐藏文本工具栏
if (qt_simple_widget_rep* qsw= dynamic_cast<qt_simple_widget_rep*> (this)) {
qsw->hide_text_toolbar ();
}
}

bool
edit_interface_rep::is_point_in_text_toolbar (SI x, SI y) {
// 通过qt_simple_widget检查点是否在文本工具栏内
if (qt_simple_widget_rep* qsw= dynamic_cast<qt_simple_widget_rep*> (this)) {
return qsw->is_point_in_text_toolbar (x, y);
}
return false;
}

void
edit_interface_rep::invalidate_text_toolbar_cache () {
// 重置工具栏缓存,强制下次重新检查
text_toolbar_last_check= 0;
}

void
edit_interface_rep::update_text_toolbar () {
if (left_dragging) {
hide_text_toolbar ();
return;
}
// 检查是否应该显示文本工具栏
if (should_show_text_toolbar ()) {
rectangle text_selr= get_text_selection_rect ();
// 检查矩形是否有效(非零面积)
if (text_selr->x1 >= text_selr->x2 || text_selr->y1 >= text_selr->y2) {
hide_text_toolbar ();
return;
}

update_visible ();
// 使用原始坐标检查选区是否在视图内
// (无需min/max,因为已经验证过矩形有效性)
bool sel_in_view= !(text_selr->x2 < vx1 || text_selr->x1 > vx2 ||
text_selr->y2 < vy1 || text_selr->y1 > vy2);
if (!sel_in_view) {
hide_text_toolbar ();
return;
}
show_text_toolbar (text_selr, magf, get_scroll_x (), get_scroll_y (),
get_canvas_x (), get_canvas_y ());
}
else {
hide_text_toolbar ();
}
}
Loading