diff --git a/TeXmacs/misc/pixmaps/liii-night/20x20/mode/left-align.svg b/TeXmacs/misc/pixmaps/liii-night/20x20/mode/left-align.svg
new file mode 100644
index 0000000000..002d21b022
--- /dev/null
+++ b/TeXmacs/misc/pixmaps/liii-night/20x20/mode/left-align.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/TeXmacs/misc/pixmaps/liii-night/20x20/mode/middle-align.svg b/TeXmacs/misc/pixmaps/liii-night/20x20/mode/middle-align.svg
new file mode 100644
index 0000000000..7c55d0603b
--- /dev/null
+++ b/TeXmacs/misc/pixmaps/liii-night/20x20/mode/middle-align.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/TeXmacs/misc/pixmaps/liii-night/20x20/mode/right-align.svg b/TeXmacs/misc/pixmaps/liii-night/20x20/mode/right-align.svg
new file mode 100644
index 0000000000..a06c661af7
--- /dev/null
+++ b/TeXmacs/misc/pixmaps/liii-night/20x20/mode/right-align.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/TeXmacs/misc/pixmaps/liii/20x20/mode/left-align.svg b/TeXmacs/misc/pixmaps/liii/20x20/mode/left-align.svg
new file mode 100644
index 0000000000..e63297a38a
--- /dev/null
+++ b/TeXmacs/misc/pixmaps/liii/20x20/mode/left-align.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/TeXmacs/misc/pixmaps/liii/20x20/mode/middle-align.svg b/TeXmacs/misc/pixmaps/liii/20x20/mode/middle-align.svg
new file mode 100644
index 0000000000..1ffc2c05b6
--- /dev/null
+++ b/TeXmacs/misc/pixmaps/liii/20x20/mode/middle-align.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/TeXmacs/misc/pixmaps/liii/20x20/mode/right-align.svg b/TeXmacs/misc/pixmaps/liii/20x20/mode/right-align.svg
new file mode 100644
index 0000000000..4cf62a85d8
--- /dev/null
+++ b/TeXmacs/misc/pixmaps/liii/20x20/mode/right-align.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/TeXmacs/misc/themes/liii-night.css b/TeXmacs/misc/themes/liii-night.css
index 5f41f33eec..abb2220ff7 100644
--- a/TeXmacs/misc/themes/liii-night.css
+++ b/TeXmacs/misc/themes/liii-night.css
@@ -873,3 +873,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;
+}
diff --git a/TeXmacs/misc/themes/liii.css b/TeXmacs/misc/themes/liii.css
index 823770835e..109400fb2d 100644
--- a/TeXmacs/misc/themes/liii.css
+++ b/TeXmacs/misc/themes/liii.css
@@ -850,3 +850,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;
+}
diff --git a/TeXmacs/progs/generic/text-toolbar.scm b/TeXmacs/progs/generic/text-toolbar.scm
new file mode 100644
index 0000000000..38bc329bb0
--- /dev/null
+++ b/TeXmacs/progs/generic/text-toolbar.scm
@@ -0,0 +1,38 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; MODULE : text-toolbar.scm
+;; DESCRIPTION : text selection toolbar icons
+;; COPYRIGHT : (C) 2026
+;;
+;; 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_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.svg") "Marked text")
+ (mark-text))
+ (=> (balloon (icon "tm_color.xpm") "Select a foreground color")
+ (link color-menu))
+ (=> (balloon (icon "tm_section.xpm") "chapter::menu")
+ (link chapter-menu))
+ (=> (balloon (icon "tm_theorem.xpm") "enunciation")
+ (link enunciation-menu))
+ ((balloon (icon "left-align.xpm") "left aligned")
+ (make-line-with "par-mode" "left"))
+ ((balloon (icon "middle-align.xpm") "center")
+ (make-line-with "par-mode" "center"))
+ ((balloon (icon "right-align.xpm") "right aligned")
+ (make-line-with "par-mode" "right")))
diff --git a/devel/201_63.md b/devel/201_63.md
new file mode 100644
index 0000000000..f4147d353a
--- /dev/null
+++ b/devel/201_63.md
@@ -0,0 +1,8 @@
+# [201_63] 文字选中悬浮窗口初步实现
+
+## 如何测试
+1. 输入一段文字通过键盘或鼠标选中,会出现悬浮窗口
+2. 依次点击悬浮窗口按钮,实现的功能和模式工具栏一致
+3. 窗口位置的优先级应该是选区顶部>选区底部>窗口中央
+4. 切换标签页不会导致原标签页的文本选中窗口失效
+5. 单独选中任意数学公式环境该悬浮窗口都不出现,选中公式文本混合内容悬浮窗口出现
diff --git a/src/Edit/Interface/edit_interface.cpp b/src/Edit/Interface/edit_interface.cpp
index 019c84c6e2..afb6eb3d45 100644
--- a/src/Edit/Interface/edit_interface.cpp
+++ b/src/Edit/Interface/edit_interface.cpp
@@ -990,6 +990,8 @@ edit_interface_rep::apply_changes () {
selection_rects= rs;
invalidate (selection_rects);
}
+ // 选区改变后更新文本工具栏
+ update_text_toolbar ();
}
// cout << "Handling alternative selection\n";
diff --git a/src/Edit/Interface/edit_interface.hpp b/src/Edit/Interface/edit_interface.hpp
index 2f99a1ee0d..f9e6524f19 100644
--- a/src/Edit/Interface/edit_interface.hpp
+++ b/src/Edit/Interface/edit_interface.hpp
@@ -237,6 +237,13 @@ 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 ();
/* the footer */
tree compute_text_footer (tree st);
diff --git a/src/Edit/Interface/edit_keyboard.cpp b/src/Edit/Interface/edit_keyboard.cpp
index 7997903d43..7bad5e2b22 100644
--- a/src/Edit/Interface/edit_keyboard.cpp
+++ b/src/Edit/Interface/edit_keyboard.cpp
@@ -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";
diff --git a/src/Edit/Interface/edit_mouse.cpp b/src/Edit/Interface/edit_mouse.cpp
index 7f9b9feefa..25008be80e 100644
--- a/src/Edit/Interface/edit_mouse.cpp
+++ b/src/Edit/Interface/edit_mouse.cpp
@@ -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"
@@ -780,10 +781,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);
@@ -879,10 +884,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]) *
@@ -1014,3 +1023,113 @@ 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 () {
+ if (as_bool (call ("in-math?")) || as_bool (call ("in-prog?")) ||
+ as_bool (call ("in-code?")) || as_bool (call ("in-verbatim?")))
+ return false;
+ // 检查是否有活动的文本选区
+ if (!selection_active_any ()) return false;
+
+ // 检查选区是否非空
+ tree sel_tree= selection_get ();
+ if (is_atomic (sel_tree) && as_string (sel_tree) == "") return false;
+
+ return true;
+}
+
+rectangle
+edit_interface_rep::get_text_selection_rect () {
+ rectangle sel_rect;
+
+ if (selection_active_any () && !is_nil (selection_rects)) {
+ // 使用现有的选区矩形
+ sel_rect= least_upper_bound (selection_rects);
+ }
+ else if (selection_active_any ()) {
+ // 如果没有选区矩形,但选区存在,计算一个默认矩形
+ 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显示文本工具栏
+ // this指针实际上是edit_interface_rep,它继承自editor_rep,而editor_rep继承自simple_widget_rep
+ // 在Qt环境下,simple_widget_rep实际上是qt_simple_widget_rep
+ qt_simple_widget_rep* qsw= static_cast (this);
+ qsw->show_text_toolbar (selr, magf, scroll_x, scroll_y, canvas_x, canvas_y);
+}
+
+void
+edit_interface_rep::hide_text_toolbar () {
+ // 通过qt_simple_widget隐藏文本工具栏
+ qt_simple_widget_rep* qsw= static_cast (this);
+ qsw->hide_text_toolbar ();
+}
+
+bool
+edit_interface_rep::is_point_in_text_toolbar (SI x, SI y) {
+ // 通过qt_simple_widget检查点是否在文本工具栏内
+ qt_simple_widget_rep* qsw= static_cast (this);
+ return qsw->is_point_in_text_toolbar (x, y);
+}
+
+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 ();
+ // 检查矩形是否有效(非零面积)
+ // 注意:rectangle 不是 list 类型,不能使用 is_nil
+ // 我们检查矩形坐标是否有效
+ if (text_selr->x1 < text_selr->x2 && text_selr->y1 < text_selr->y2) {
+ update_visible ();
+ SI sel_x1= min (text_selr->x1, text_selr->x2);
+ SI sel_x2= max (text_selr->x1, text_selr->x2);
+ SI sel_y1= min (text_selr->y1, text_selr->y2);
+ SI sel_y2= max (text_selr->y1, text_selr->y2);
+ bool sel_in_view=
+ !(sel_x2 < vx1 || sel_x1 > vx2 || sel_y2 < vy1 || sel_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 ();
+ }
+ }
+ else {
+ hide_text_toolbar ();
+ }
+}
diff --git a/src/Plugins/Qt/QTMTextToolbar.cpp b/src/Plugins/Qt/QTMTextToolbar.cpp
new file mode 100644
index 0000000000..18f2242c4b
--- /dev/null
+++ b/src/Plugins/Qt/QTMTextToolbar.cpp
@@ -0,0 +1,321 @@
+/******************************************************************************
+ * MODULE : QTMTextToolbar.cpp
+ * DESCRIPTION: Text selection toolbar popup widget implementation
+ * COPYRIGHT : (C) 2025
+ *******************************************************************************
+ * 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 "QTMTextToolbar.hpp"
+#include "QTMStyle.hpp"
+#include "bitmap_font.hpp"
+#include "moebius/data/scheme.hpp"
+#include "object_l5.hpp"
+#include "qt_renderer.hpp"
+#include "qt_utilities.hpp"
+#include "scheme.hpp"
+#include "server.hpp"
+#include "tm_ostream.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// 悬浮工具栏创建函数
+QTMTextToolbar::QTMTextToolbar (QWidget* parent, qt_simple_widget_rep* owner)
+ : QWidget (parent), owner (owner), layout (nullptr),
+ cached_selection_mid_x (0), cached_selection_mid_y (0),
+ cached_scroll_x (0), cached_scroll_y (0), cached_canvas_x (0),
+ cached_canvas_y (0), cached_magf (0.0), painted (false),
+ painted_count (0) {
+ Q_INIT_RESOURCE (images);
+ setObjectName ("text_toolbar");
+ setWindowFlags (Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
+ setAttribute (Qt::WA_ShowWithoutActivating);
+ setMouseTracking (true);
+ setFocusPolicy (Qt::NoFocus);
+ layout= new QHBoxLayout (this);
+ layout->setContentsMargins (0, 0, 0, 0);
+ layout->setSizeConstraint (QLayout::SetMinimumSize);
+ layout->setSpacing (1);
+ setLayout (layout);
+
+ // 添加阴影效果
+ QGraphicsDropShadowEffect* effect= new QGraphicsDropShadowEffect (this);
+ effect->setBlurRadius (40);
+ effect->setOffset (0, 4);
+ effect->setColor (QColor (0, 0, 0, 120));
+ this->setGraphicsEffect (effect);
+
+ rebuildButtonsFromScheme ();
+}
+
+QTMTextToolbar::~QTMTextToolbar () {}
+
+void
+QTMTextToolbar::clearButtons () {
+ if (!layout) return;
+ QLayoutItem* item= nullptr;
+ while ((item= layout->takeAt (0)) != nullptr) {
+ if (QWidget* w= item->widget ()) {
+ w->setParent (nullptr);
+ delete w;
+ }
+ else if (QLayout* l= item->layout ()) {
+ delete l;
+ }
+ delete item;
+ }
+}
+
+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 ();
+
+ 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 ();
+}
+
+void
+QTMTextToolbar::showTextToolbar (qt_renderer_rep* ren, rectangle selr,
+ double magf, int scroll_x, int scroll_y,
+ int canvas_x, int canvas_y) {
+ cachePosition (selr, magf, scroll_x, scroll_y, canvas_x, canvas_y);
+ if (!selectionInView ()) {
+ hide ();
+ return;
+ }
+ updatePosition (ren);
+ show ();
+ raise ();
+}
+
+void
+QTMTextToolbar::updatePosition (qt_renderer_rep* ren) {
+ if (!selectionInView ()) {
+ hide ();
+ return;
+ }
+ int x, y;
+ getCachedPosition (ren, x, y);
+ move (x, y);
+}
+
+void
+QTMTextToolbar::scrollBy (int x, int y) {
+ cached_scroll_x-= (int) (x / cached_magf);
+ cached_scroll_y-= (int) (y / cached_magf);
+}
+
+void
+QTMTextToolbar::cachePosition (rectangle selr, double magf, int scroll_x,
+ int scroll_y, int canvas_x, int canvas_y) {
+ cached_rect = selr;
+ cached_magf = magf;
+ cached_scroll_x= scroll_x;
+ cached_scroll_y= scroll_y;
+ cached_canvas_x= canvas_x;
+ cached_canvas_y= canvas_y;
+
+ // 计算选区中心位置
+ cached_selection_mid_x= (selr->x1 + selr->x2) * 0.5;
+ cached_selection_mid_y=
+ selr->y2; // 使用选区底部位置,使工具栏显示在选中文字正上方
+}
+
+void
+QTMTextToolbar::getCachedPosition (qt_renderer_rep* ren, int& x, int& y) {
+ rectangle selr = cached_rect;
+ double inv_unit = 1.0 / 256.0;
+ double cx_logic = (selr->x1 + selr->x2) * 0.5;
+ double sel_top_logic = (selr->y1 > selr->y2) ? selr->y1 : selr->y2;
+ double sel_bottom_logic= (selr->y1 > selr->y2) ? selr->y2 : selr->y1;
+
+ // 使用公式计算QT坐标
+ double cx_px=
+ ((cx_logic - cached_scroll_x) * cached_magf + cached_canvas_x) * inv_unit;
+ double top_px= -(sel_top_logic - cached_scroll_y) * cached_magf * inv_unit;
+ double bottom_px=
+ -(sel_bottom_logic - cached_scroll_y) * cached_magf * inv_unit;
+
+ // 修正:视口 > 表面:存在空白顶部
+ double blank_top= 0.0;
+ if (owner && owner->scrollarea () && owner->scrollarea ()->viewport () &&
+ owner->scrollarea ()->surface ()) {
+ int vp_h = owner->scrollarea ()->viewport ()->height ();
+ int surf_h= owner->scrollarea ()->surface ()->height ();
+ if (vp_h > surf_h) blank_top= (vp_h - surf_h) * 0.5;
+ }
+ top_px+= blank_top;
+ bottom_px+= blank_top;
+
+ const int above_y=
+ int (std::round (top_px - cached_height - 10)); // 在选区顶部上方显示
+ const int below_y=
+ int (std::round (bottom_px + 10)); // 如果上面空间不够,显示在选区下方
+
+ x= int (std::round (cx_px - cached_width * 0.5));
+ y= above_y;
+
+ // 确保工具栏在视口内
+ if (owner && owner->scrollarea () && owner->scrollarea ()->viewport ()) {
+ int vp_w= owner->scrollarea ()->viewport ()->width ();
+ int vp_h= owner->scrollarea ()->viewport ()->height ();
+
+ const bool above_fits= (above_y >= 0) && (above_y + cached_height <= vp_h);
+ const bool below_fits= (below_y >= 0) && (below_y + cached_height <= vp_h);
+
+ if (above_fits) y= above_y;
+ else if (below_fits) y= below_y;
+ else {
+ x= std::max (0, (vp_w - cached_width) / 2);
+ y= std::max (0, (vp_h - cached_height) / 2);
+ }
+
+ if (x < 0) x= 0;
+ if (x + cached_width > vp_w) x= vp_w - cached_width;
+ if (y < 0) y= 0;
+ if (y + cached_height > vp_h) y= vp_h - cached_height;
+ }
+ else {
+ if (y < 0) y= below_y;
+ }
+}
+
+bool
+QTMTextToolbar::selectionInView () const {
+ if (!owner || !owner->scrollarea () || !owner->scrollarea ()->viewport ())
+ return true;
+
+ rectangle selr = cached_rect;
+ double inv_unit= 1.0 / 256.0;
+
+ double x1_px=
+ ((selr->x1 - cached_scroll_x) * cached_magf + cached_canvas_x) * inv_unit;
+ double x2_px=
+ ((selr->x2 - cached_scroll_x) * cached_magf + cached_canvas_x) * inv_unit;
+ double y1_px= -(selr->y1 - cached_scroll_y) * cached_magf * inv_unit;
+ double y2_px= -(selr->y2 - cached_scroll_y) * cached_magf * inv_unit;
+
+ double blank_top= 0.0;
+ if (owner->scrollarea ()->surface ()) {
+ int vp_h = owner->scrollarea ()->viewport ()->height ();
+ int surf_h= owner->scrollarea ()->surface ()->height ();
+ if (vp_h > surf_h) blank_top= (vp_h - surf_h) * 0.5;
+ }
+ y1_px+= blank_top;
+ y2_px+= blank_top;
+
+ double left = std::min (x1_px, x2_px);
+ double right = std::max (x1_px, x2_px);
+ double top = std::min (y1_px, y2_px);
+ double bottom= std::max (y1_px, y2_px);
+
+ int vp_w= owner->scrollarea ()->viewport ()->width ();
+ int vp_h= owner->scrollarea ()->viewport ()->height ();
+
+ if (right < 0.0 || left > vp_w) return false;
+ if (bottom < 0.0 || top > vp_h) return false;
+ return true;
+}
+
+void
+QTMTextToolbar::autoSize () {
+ // 根据DPI和缩放因子自动调整大小
+ QScreen* Screen= QGuiApplication::primaryScreen ();
+ const double Dpi = Screen ? Screen->logicalDotsPerInch () : 96.0;
+ const double Scale = Dpi / 96.0;
+ const double totalScale=
+ Scale * cached_magf * 12.0; // 原始3.0倍,扩大4倍后为12.0倍
+ int btn_size;
+
+#if defined(Q_OS_MAC)
+ btn_size= int (50 * totalScale);
+#else
+ btn_size= int (40 * totalScale);
+#endif
+
+ if (cached_magf <= 0.16) {
+ btn_size= 25;
+ }
+
+ // 设置按钮大小
+ QSize icon_size (btn_size, btn_size);
+ QSize fixed_size (btn_size + 32,
+ btn_size + 32); // 内边距也扩大4倍 (8 * 4.0 = 32)
+ const QList buttons=
+ findChildren (QString (), Qt::FindChildrenRecursively);
+ for (QToolButton* button : buttons) {
+ if (!button) continue;
+ if (button->objectName ().isEmpty ())
+ button->setObjectName ("text-toolbar-button");
+ button->setIconSize (icon_size);
+ button->setFixedSize (fixed_size);
+ }
+
+ // 调整窗口大小
+ adjustSize ();
+ cached_width = width ();
+ cached_height= height ();
+}
+
+bool
+QTMTextToolbar::eventFilter (QObject* obj, QEvent* event) {
+ // 处理事件过滤
+ return QWidget::eventFilter (obj, event);
+}
diff --git a/src/Plugins/Qt/QTMTextToolbar.hpp b/src/Plugins/Qt/QTMTextToolbar.hpp
new file mode 100644
index 0000000000..d60e76c1ba
--- /dev/null
+++ b/src/Plugins/Qt/QTMTextToolbar.hpp
@@ -0,0 +1,62 @@
+/******************************************************************************
+ * MODULE : QTMTextToolbar.hpp
+ * DESCRIPTION: Text selection toolbar popup widget
+ * COPYRIGHT : (C) 2025
+ *******************************************************************************
+ * 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_TEXT_TOOLBAR_HPP
+#define QT_TEXT_TOOLBAR_HPP
+
+#include "qt_simple_widget.hpp"
+#include "rectangles.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+class QTMTextToolbar : public QWidget {
+protected:
+ qt_simple_widget_rep* owner;
+ QHBoxLayout* layout;
+ QGraphicsDropShadowEffect* effect;
+ int cached_selection_mid_x;
+ int cached_selection_mid_y;
+ rectangle cached_rect;
+ int cached_scroll_x; // 页面滚动位置x
+ int cached_scroll_y; // 页面滚动位置y
+ int cached_canvas_x;
+ int cached_canvas_y;
+ int cached_width;
+ int cached_height;
+ double cached_magf; // 缩放因子
+ bool painted;
+ int painted_count;
+ qt_widget text_toolbar_widget;
+
+public:
+ QTMTextToolbar (QWidget* parent, qt_simple_widget_rep* owner);
+ ~QTMTextToolbar ();
+
+ void showTextToolbar (qt_renderer_rep* ren, rectangle selr, double magf,
+ int scroll_x, int scroll_y, int canvas_x, int canvas_y);
+ void updatePosition (qt_renderer_rep* ren);
+ void scrollBy (int x, int y);
+ void autoSize ();
+
+protected:
+ void cachePosition (rectangle selr, double magf, int scroll_x, int scroll_y,
+ int canvas_x, int canvas_y);
+ void getCachedPosition (qt_renderer_rep* ren, int& x, int& y);
+ bool selectionInView () const;
+ void rebuildButtonsFromScheme ();
+ void clearButtons ();
+ bool eventFilter (QObject* obj, QEvent* event) override;
+};
+
+#endif // QT_TEXT_TOOLBAR_HPP
diff --git a/src/Plugins/Qt/QTMWidget.cpp b/src/Plugins/Qt/QTMWidget.cpp
index 78580a9568..65bf212611 100644
--- a/src/Plugins/Qt/QTMWidget.cpp
+++ b/src/Plugins/Qt/QTMWidget.cpp
@@ -14,6 +14,7 @@
#include "array.hpp"
#include "boot.hpp"
#include "converter.hpp"
+#include "edit_interface.hpp"
#include "object_l5.hpp"
#include "preferences.hpp"
#include "qt_gui.hpp"
@@ -156,6 +157,11 @@ QTMWidget::scrollContentsBy (int dx, int dy) {
tm_widget ()->scroll_completion_popup_by (dx, dy);
tm_widget ()->scroll_math_completion_popup_by (dx, dy);
tm_widget ()->scroll_image_popup_by (dx, dy);
+ tm_widget ()->scroll_text_toolbar_by (dx, dy);
+ if (edit_interface_rep* ed=
+ dynamic_cast (tm_widget ())) {
+ ed->update_text_toolbar ();
+ }
}
void
diff --git a/src/Plugins/Qt/qt_simple_widget.cpp b/src/Plugins/Qt/qt_simple_widget.cpp
index 3774278a9e..1c90e6cb48 100644
--- a/src/Plugins/Qt/qt_simple_widget.cpp
+++ b/src/Plugins/Qt/qt_simple_widget.cpp
@@ -21,6 +21,7 @@
#include "QTMMathCompletionPopup.hpp"
#include "QTMMenuHelper.hpp"
#include "QTMStyle.hpp"
+#include "QTMTextToolbar.hpp"
#include "QTMWidget.hpp"
#include
#include
@@ -46,6 +47,7 @@ qt_simple_widget_rep::~qt_simple_widget_rep () {
if (backingPixmap != NULL) delete backingPixmap;
#endif
if (completionPopUp != nullptr) delete completionPopUp;
+ if (textToolbar != nullptr) delete textToolbar;
}
QWidget*
@@ -789,4 +791,67 @@ qt_simple_widget_rep::scroll_image_popup_by (SI x, SI y) {
qt_renderer_rep* ren= the_qt_renderer ();
imagePopUp->updatePosition (ren);
}
-}
\ No newline at end of file
+}
+
+/******************************************************************************
+ * Text toolbar support
+ ******************************************************************************/
+
+void
+qt_simple_widget_rep::ensure_text_toolbar () {
+ if (!canvas ()) return;
+ if (textToolbar) {
+ if (textToolbar->parent () != canvas ()) {
+ textToolbar->setParent (canvas ());
+ }
+ return;
+ }
+ textToolbar= new QTMTextToolbar (canvas (), this);
+ if (is_empty (tm_style_sheet)) {
+ textToolbar->setStyle (qtmstyle ());
+ }
+}
+
+void
+qt_simple_widget_rep::show_text_toolbar (rectangle selr, double magf,
+ int scroll_x, int scroll_y,
+ int canvas_x, int canvas_y) {
+ ensure_text_toolbar ();
+ qt_renderer_rep* ren= the_qt_renderer ();
+ textToolbar->showTextToolbar (ren, selr, magf, scroll_x, scroll_y, canvas_x,
+ canvas_y);
+}
+
+void
+qt_simple_widget_rep::hide_text_toolbar () {
+ if (textToolbar) {
+ textToolbar->hide ();
+ }
+}
+
+void
+qt_simple_widget_rep::scroll_text_toolbar_by (SI x, SI y) {
+ if (textToolbar) {
+ QPoint qp (x, y);
+ coord2 p= from_qpoint (qp);
+ textToolbar->scrollBy (p.x1, p.x2);
+ qt_renderer_rep* ren= the_qt_renderer ();
+ textToolbar->updatePosition (ren);
+ }
+}
+
+bool
+qt_simple_widget_rep::is_point_in_text_toolbar (SI x, SI y) {
+ if (!textToolbar) return false;
+
+ // 将逻辑坐标转换为像素坐标
+ double inv_unit= 1.0 / 256.0;
+ int px = int (std::round (x * inv_unit));
+ int py = int (std::round (y * inv_unit));
+
+ // 获取工具栏的几何位置
+ QRect toolbarRect= textToolbar->geometry ();
+
+ // 检查点是否在工具栏内
+ return toolbarRect.contains (px, py);
+}
diff --git a/src/Plugins/Qt/qt_simple_widget.hpp b/src/Plugins/Qt/qt_simple_widget.hpp
index 99fb02d199..55c217229f 100644
--- a/src/Plugins/Qt/qt_simple_widget.hpp
+++ b/src/Plugins/Qt/qt_simple_widget.hpp
@@ -24,6 +24,7 @@
class QTMCompletionPopup;
class QTMMathCompletionPopup;
class QTMImagePopup;
+class QTMTextToolbar;
/*! A widget containing a TeXmacs canvas.
@@ -121,6 +122,14 @@ class qt_simple_widget_rep : public qt_widget_rep {
void hide_image_popup ();
void scroll_image_popup_by (SI x, SI y);
+ ////////////////////// Text toolbar support
+ void ensure_text_toolbar ();
+ void show_text_toolbar (rectangle selr, double magf, int scroll_x,
+ int scroll_y, int canvas_x, int canvas_y);
+ void hide_text_toolbar ();
+ void scroll_text_toolbar_by (SI x, SI y);
+ bool is_point_in_text_toolbar (SI x, SI y);
+
////////////////////// backing store management
static void repaint_all (); // called by qt_gui_rep::update()
@@ -131,6 +140,7 @@ class qt_simple_widget_rep : public qt_widget_rep {
QPointer completionPopUp;
QPointer mathCompletionPopUp;
QPointer imagePopUp;
+ QPointer textToolbar;
#ifdef USE_MUPDF_RENDERER
double bs_zoomf;
picture backing_store;