diff --git a/TeXmacs/progs/generic/text-toolbar.scm b/TeXmacs/progs/generic/text-toolbar.scm new file mode 100644 index 0000000000..ba5a370b85 --- /dev/null +++ b/TeXmacs/progs/generic/text-toolbar.scm @@ -0,0 +1,40 @@ + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; MODULE : text-toolbar.scm +;; DESCRIPTION : text selection toolbar icons +;; COPYRIGHT : (C) 2026 Jie Chen +;; Yifan 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 . +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(texmacs-module (generic text-toolbar) + (:use (generic format-edit) + (generic format-menu) + (generic generic-edit))) + +(menu-bind text-toolbar-icons + (=> (balloon (icon "tm_section.xpm") "chapter::menu") + (link chapter-menu)) + (=> (balloon (icon "tm_theorem.xpm") "enunciation") + (link enunciation-menu)) + ((balloon (icon "tm_bold.xpm") "Write bold text") + (toggle-bold)) + ((balloon (icon "tm_italic.xpm") "Write italic text") + (toggle-italic)) + ((balloon (icon "tm_underline.xpm") "Write underline") + (toggle-underlined)) + ((balloon (icon "tm_marked.xpm") "Marked text") + (mark-text)) + (=> (balloon (icon "tm_color.xpm") "Select a foreground color") + (link color-menu)) + ((balloon (icon "tm_cell_left.xpm") "left aligned") + (make 'padded-left-aligned)) + ((balloon (icon "tm_cell_center.xpm") "center") + (make 'padded-center)) + ((balloon (icon "tm_cell_right.xpm") "right aligned") + (make 'padded-right-aligned))) \ No newline at end of file diff --git a/TeXmacs/tests/201_63.scm b/TeXmacs/tests/201_63.scm new file mode 100644 index 0000000000..18c9ecb983 --- /dev/null +++ b/TeXmacs/tests/201_63.scm @@ -0,0 +1,180 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; MODULE : 201_63.scm +;; DESCRIPTION : Unit tests for text toolbar functionality +;; 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)) + +(check-set-mode! 'report-failed) + +(tm-define (test_201_63) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helper functions for testing +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; 模拟缓存时间检查 +(define (cache-still-valid? last-check current-time) + (< (- current-time last-check) 100)) + +;; 模拟缓存失效判断 +(define (should-invalidate-cache? last-check current-time) + (>= (- current-time last-check) 100)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Cache mechanism tests +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; 缓存有效期内应该返回true +(check (cache-still-valid? 1000 1099) => #t) + +;; 缓存过期边界(正好100ms)应该返回false +(check (cache-still-valid? 1000 1100) => #f) + +;; 缓存过期后应该返回false +(check (cache-still-valid? 1000 1101) => #f) + +;; 缓存失效判断:有效期内 +(check (should-invalidate-cache? 1000 1099) => #f) + +;; 缓存失效判断:正好过期 +(check (should-invalidate-cache? 1000 1100) => #t) + +;; 缓存失效判断:已过期 +(check (should-invalidate-cache? 1000 1200) => #t) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Rectangle validity tests +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (rectangle-valid? x1 y1 x2 y2) + (and (< x1 x2) (< y1 y2))) + +;; 有效矩形检测 +(check (rectangle-valid? 100 200 300 400) => #t) + +;; 零宽度矩形应该无效 +(check (rectangle-valid? 100 200 100 400) => #f) + +;; 零高度矩形应该无效 +(check (rectangle-valid? 100 200 300 200) => #f) + +;; 负宽度矩形应该无效 +(check (rectangle-valid? 300 200 100 400) => #f) + +;; 负高度矩形应该无效 +(check (rectangle-valid? 100 400 300 200) => #f) + +;; 最小有效矩形(1x1) +(check (rectangle-valid? 0 0 1 1) => #t) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Coordinate conversion tests (simulating SI to pixel conversion) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define INV_UNIT (/ 1.0 256)) + +(define (si->pixel si-coord) + (inexact->exact (round (* si-coord INV_UNIT)))) + +;; 坐标转换:2560 -> 10 +(check (si->pixel 2560) => 10) + +;; 坐标转换:5120 -> 20 +(check (si->pixel 5120) => 20) + +;; 坐标转换:0 -> 0 +(check (si->pixel 0) => 0) + +;; 坐标转换:255 -> 1(四舍五入) +(check (si->pixel 255) => 1) + +;; 坐标转换:256 -> 1(正好1单位) +(check (si->pixel 256) => 1) + +;; 坐标转换:257 -> 1(略大于1) +(check (si->pixel 257) => 1) + +;; 坐标转换:大数值1000000 +(check (si->pixel 1000000) => 3906) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Mode checking simulation tests +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (should-show-toolbar? in-math? in-prog? in-code? in-verbatim? + has-selection? selection-empty?) + (and (not in-math?) + (not in-prog?) + (not in-code?) + (not in-verbatim?) + has-selection? + (not selection-empty?))) + +;; 普通文本选区:应该显示 +(check (should-show-toolbar? #f #f #f #f #t #f) => #t) + +;; 数学模式中:不应该显示 +(check (should-show-toolbar? #t #f #f #f #t #f) => #f) + +;; 编程模式中:不应该显示 +(check (should-show-toolbar? #f #t #f #f #t #f) => #f) + +;; 代码模式中:不应该显示 +(check (should-show-toolbar? #f #f #t #f #t #f) => #f) + +;; 原文模式中:不应该显示 +(check (should-show-toolbar? #f #f #f #t #t #f) => #f) + +;; 无选区时:不应该显示 +(check (should-show-toolbar? #f #f #f #f #f #f) => #f) + +;; 空选区时:不应该显示 +(check (should-show-toolbar? #f #f #f #f #t #t) => #f) + +;; 数学模式 + 无选区:不应该显示 +(check (should-show-toolbar? #t #f #f #f #f #f) => #f) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Viewport intersection tests +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (selection-in-view? sel-x1 sel-y1 sel-x2 sel-y2 + view-x1 view-y1 view-x2 view-y2) + (not (or (< sel-x2 view-x1) + (> sel-x1 view-x2) + (< sel-y2 view-y1) + (> sel-y1 view-y2)))) + +;; 选区完全在视图内 +(check (selection-in-view? 100 100 200 200 0 0 500 500) => #t) + +;; 选区部分在视图内(左侧) +(check (selection-in-view? -50 100 50 200 0 0 500 500) => #t) + +;; 选区部分在视图内(右侧) +(check (selection-in-view? 450 100 550 200 0 0 500 500) => #t) + +;; 选区完全在视图左侧 +(check (selection-in-view? -100 100 -50 200 0 0 500 500) => #f) + +;; 选区完全在视图右侧 +(check (selection-in-view? 550 100 600 200 0 0 500 500) => #f) + +;; 选区完全在视图上方 +(check (selection-in-view? 100 -100 200 -50 0 0 500 500) => #f) + +;; 选区完全在视图下方 +(check (selection-in-view? 100 550 200 600 0 0 500 500) => #f) + +;; 选区正好接触视图边界(应该算在视图内) +(check (selection-in-view? 0 0 100 100 0 0 500 500) => #t) + + (check-report)) diff --git a/devel/201_63.md b/devel/201_63.md index 4750b731d5..7723e3aa7f 100644 --- a/devel/201_63.md +++ b/devel/201_63.md @@ -33,13 +33,20 @@ - 快速移动鼠标,观察工具栏是否稳定(100ms缓存) - 选区变化后,观察是否及时更新 +4. **按键功能测试** + - 测试按键行为是否和图标一致 + ### 自动测试 -```bash -# 编译测试 -xmake b text_toolbar_test -# 运行测试 -xmake r text_toolbar_test +#### cpp +```bash +# 运行脚本 +./bin/test_only text_toolbar_test +``` +#### scheme +```bash +# 运行脚本 +./bin/test_only 201_63 ``` **测试覆盖**: @@ -49,6 +56,11 @@ xmake r text_toolbar_test - 有效选区矩形计算 - 坐标转换一致性 +## 2026/3/13 通过 scheme 加入具体的按钮 +### What +- 添加功能按键 +- 优化部分代码 +- 补充scheme测试,优化cpp测试 ## 2026/3/6 性能优化与代码清理 ### What diff --git a/src/Edit/Interface/edit_interface.cpp b/src/Edit/Interface/edit_interface.cpp index 9c35f599e4..c2786bef09 100644 --- a/src/Edit/Interface/edit_interface.cpp +++ b/src/Edit/Interface/edit_interface.cpp @@ -650,6 +650,9 @@ edit_interface_rep::notify_change (int change) { needs_update (); if ((change & (THE_TREE | THE_SELECTION | THE_CURSOR)) != 0) manual_focus_set (path (), (change & THE_TREE) != 0); + // 选区变化时,使文本工具栏缓存失效 + // 输入字符时选区变化会触发 THE_SELECTION,进而隐藏工具栏 + if ((change & THE_SELECTION) != 0) invalidate_text_toolbar_cache (); } bool diff --git a/src/Plugins/Qt/QTMTextToolbar.cpp b/src/Plugins/Qt/QTMTextToolbar.cpp index 4e22f52cc0..51d045cae2 100644 --- a/src/Plugins/Qt/QTMTextToolbar.cpp +++ b/src/Plugins/Qt/QTMTextToolbar.cpp @@ -85,10 +85,51 @@ QTMTextToolbar::clearButtons () { void QTMTextToolbar::rebuildButtonsFromScheme () { + eval ("(use-modules (generic text-toolbar))"); + object menu= eval ("'(horizontal (link text-toolbar-icons))"); + object obj = call ("make-menu-widget", menu, 0); + if (!is_widget (obj)) return; + + text_toolbar_widget = concrete (as_widget (obj)); + QList* list= text_toolbar_widget->get_qactionlist (); + if (!list) return; + clearButtons (); - QLabel* placeholder= new QLabel ("文本工具栏", this); - placeholder->setAlignment (Qt::AlignCenter); - layout->addWidget (placeholder); + + for (int i= 0; i < list->count (); ++i) { + QAction* action= list->at (i); + if (!action) continue; + + if (action->isSeparator ()) { + QFrame* sep= new QFrame (this); + sep->setFrameShape (QFrame::VLine); + sep->setFrameShadow (QFrame::Plain); + sep->setFixedWidth (1); + sep->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Expanding); + layout->addWidget (sep); + continue; + } + + if (action->text ().isNull () && action->icon ().isNull ()) { + layout->addSpacing (8); + continue; + } + + if (QWidgetAction* wa= qobject_cast (action)) { + QWidget* w= wa->requestWidget (this); + if (w) layout->addWidget (w); + continue; + } + + QToolButton* button= new QToolButton (this); + button->setObjectName ("text-toolbar-button"); + button->setAutoRaise (true); + button->setDefaultAction (action); + button->setPopupMode (QToolButton::InstantPopup); + if (tm_style_sheet == "") button->setStyle (qtmstyle ()); + layout->addWidget (button); + } + autoSize (); } diff --git a/tests/Edit/Interface/text_toolbar_test.cpp b/tests/Edit/Interface/text_toolbar_test.cpp index 163e796226..64dec4ba10 100644 --- a/tests/Edit/Interface/text_toolbar_test.cpp +++ b/tests/Edit/Interface/text_toolbar_test.cpp @@ -13,98 +13,121 @@ #include "edit_interface.hpp" #include -// 模拟 edit_interface_rep 的部分功能用于测试 +// 测试文本工具栏缓存机制 class TestTextToolbar : public QObject { Q_OBJECT private slots: - void test_should_show_text_toolbar_cache (); - void test_invalidate_text_toolbar_cache (); - void test_get_text_selection_rect_empty (); - void test_get_text_selection_rect_valid (); - void test_is_point_in_text_toolbar_conversion (); + void test_cache_timeout_boundary (); + void test_cache_invalidation (); + void test_rectangle_validity (); + void test_coordinate_conversion (); + void test_empty_selection_handling (); }; +// 测试缓存超时边界(100ms) void -TestTextToolbar::test_should_show_text_toolbar_cache () { - // 测试缓存机制:连续调用应该返回相同结果 - // 注意:这里使用模拟数据,实际测试需要完整的 edit_interface_rep 实例 - - // 验证缓存初始状态 - time_t initial_check= 0; - QVERIFY (initial_check == 0); - - // 模拟缓存更新 - time_t now = texmacs_time (); - initial_check= now; - QVERIFY (initial_check > 0); - - // 模拟100ms内的重复调用应该使用缓存 - // 实际测试中应该验证 should_show_text_toolbar() 的行为 +TestTextToolbar::test_cache_timeout_boundary () { + // 验证时间差计算 + time_t t1= 1000; + time_t t2= 1099; // 差99ms,应该使用缓存 + time_t t3= 1100; // 差100ms,应该重新检查 + + QVERIFY ((t2 - t1) < 100); // 99 < 100,缓存有效 + QVERIFY ((t3 - t1) >= 100); // 100 >= 100,缓存过期 } +// 测试缓存失效机制 void -TestTextToolbar::test_invalidate_text_toolbar_cache () { - // 测试缓存失效机制 - time_t cache_time= texmacs_time (); - QVERIFY (cache_time > 0); +TestTextToolbar::test_cache_invalidation () { + // 测试缓存失效的核心逻辑:重置时间戳会使缓存失效 + time_t last_check= 1000; // 模拟一个过去的时间戳 + + // 验证初始状态 + QVERIFY (last_check == 1000); + + // 模拟 invalidate_text_toolbar_cache():重置为0 + last_check= 0; - // 模拟 invalidate_text_toolbar_cache() 的行为 - cache_time= 0; - QVERIFY (cache_time == 0); + // 验证缓存已失效(时间戳被重置) + QVERIFY (last_check == 0); - // 验证下次调用会重新计算 + // 验证逻辑:任何正数时间戳与0的差都 >= 100(假设当前时间 >= 100) + // 这个测试不依赖 texmacs_time() 的具体值,只测试逻辑 + time_t simulated_now= 200; // 模拟当前时间 + QVERIFY ((simulated_now - last_check) >= 100); // 200 - 0 >= 100 } +// 测试矩形有效性检查 void -TestTextToolbar::test_get_text_selection_rect_empty () { - // 测试空选区时的矩形计算 - // 当没有选区时,应该返回空矩形 - - rectangle empty_rect; - // 验证空矩形的默认值 - QVERIFY (empty_rect->x1 == 0); - QVERIFY (empty_rect->y1 == 0); - QVERIFY (empty_rect->x2 == 0); - QVERIFY (empty_rect->y2 == 0); +TestTextToolbar::test_rectangle_validity () { + // 有效矩形(非零面积) + rectangle valid (100, 200, 300, 400); + QVERIFY (valid->x1 < valid->x2); + QVERIFY (valid->y1 < valid->y2); + + // 无效矩形:零宽度 + rectangle zero_width (100, 200, 100, 400); + QVERIFY (zero_width->x1 >= zero_width->x2); // 应该被检测为无效 + + // 无效矩形:零高度 + rectangle zero_height (100, 200, 300, 200); + QVERIFY (zero_height->y1 >= zero_height->y2); // 应该被检测为无效 + + // 无效矩形:负面积(x1 > x2) + rectangle negative_x (300, 200, 100, 400); + QVERIFY (negative_x->x1 > negative_x->x2); + + // 无效矩形:负面积(y1 > y2) + rectangle negative_y (100, 400, 300, 200); + QVERIFY (negative_y->y1 > negative_y->y2); } +// 测试坐标转换精度 void -TestTextToolbar::test_get_text_selection_rect_valid () { - // 测试有效选区的矩形计算 - rectangle valid_rect (100, 200, 300, 400); - - // 验证矩形坐标 - QVERIFY (valid_rect->x1 == 100); - QVERIFY (valid_rect->y1 == 200); - QVERIFY (valid_rect->x2 == 300); - QVERIFY (valid_rect->y2 == 400); - - // 验证非零面积检查 - QVERIFY (valid_rect->x1 < valid_rect->x2); - QVERIFY (valid_rect->y1 < valid_rect->y2); +TestTextToolbar::test_coordinate_conversion () { + constexpr double INV_UNIT= 1.0 / 256.0; + + // 基础转换测试 + QCOMPARE (int (std::round (2560 * INV_UNIT)), 10); + QCOMPARE (int (std::round (5120 * INV_UNIT)), 20); + + // 边界值测试 + QCOMPARE (int (std::round (0 * INV_UNIT)), 0); + QCOMPARE (int (std::round (255 * INV_UNIT)), 1); // 接近1的值 + QCOMPARE (int (std::round (256 * INV_UNIT)), 1); // 正好1个单位 + QCOMPARE (int (std::round (257 * INV_UNIT)), 1); // 略大于1 + + // 大数值精度测试 + SI large = 1000000; + int large_pixel= int (std::round (large * INV_UNIT)); + QCOMPARE (large_pixel, 3906); + + // 验证反向计算误差在可接受范围 + double back_calc= large_pixel / INV_UNIT; + double error = std::abs (back_calc - large); + QVERIFY (error < 256); // 误差小于1个像素单位 } +// 测试空选区处理 void -TestTextToolbar::test_is_point_in_text_toolbar_conversion () { - // 测试坐标转换的一致性 - // 验证逻辑坐标到像素坐标的转换 - - SI logical_x= 2560; // 10 * 256 (一个常见的坐标值) - SI logical_y= 5120; // 20 * 256 - - double inv_unit= 1.0 / 256.0; - int pixel_x = int (std::round (logical_x * inv_unit)); - int pixel_y = int (std::round (logical_y * inv_unit)); - - // 验证转换结果 - QVERIFY (pixel_x == 10); - QVERIFY (pixel_y == 20); - - // 验证大数值的转换精度 - SI large_x = 1000000; - int large_pixel= int (std::round (large_x * inv_unit)); - QVERIFY (large_pixel == 3906); // 1000000 / 256 ≈ 3906 +TestTextToolbar::test_empty_selection_handling () { + // 默认构造的空矩形 + rectangle empty; + QCOMPARE (empty->x1, 0); + QCOMPARE (empty->y1, 0); + QCOMPARE (empty->x2, 0); + QCOMPARE (empty->y2, 0); + + // 空矩形应该被检测为无效(零面积) + bool is_empty_invalid= (empty->x1 >= empty->x2) || (empty->y1 >= empty->y2); + QVERIFY (is_empty_invalid); + + // 最小有效矩形(1x1像素) + rectangle minimal (0, 0, 1, 1); + bool is_minimal_valid= + (minimal->x1 < minimal->x2) && (minimal->y1 < minimal->y2); + QVERIFY (is_minimal_valid); } QTEST_MAIN (TestTextToolbar)