diff --git a/TeXmacs/progs/source/shortcut-widgets.scm b/TeXmacs/progs/source/shortcut-widgets.scm index ce95209b7e..8e38b1e75e 100644 --- a/TeXmacs/progs/source/shortcut-widgets.scm +++ b/TeXmacs/progs/source/shortcut-widgets.scm @@ -75,6 +75,8 @@ (set-user-shortcut sh (global-ref u :cmd)) (refresh-now "shortcuts-list"))) // // + ("open" (open-shortcuts-widget)) + // // ("Ok" (begin (and-with sh (get-shortcut u) (set-user-shortcut sh (global-ref u :cmd))) @@ -139,3 +141,8 @@ (dialogue-window (shortcuts-editor u) (lambda x (noop)) "Shortcuts editor" u)))) + +(tm-define (open-shortcuts-widget) + (:interactive #t) + (set-shortcut-widget)) + diff --git a/devel/201_50.md b/devel/201_50.md index 7a09bd6dac..02e5b36754 100644 --- a/devel/201_50.md +++ b/devel/201_50.md @@ -49,4 +49,8 @@ get-bindings-by-command函数返回一个 Scheme 列表,返回值的整体结构 ) ``` +4. 点击`工具->键盘->编辑键盘快捷键->打开`,观察快捷键编辑器界面是否打开(注:目前里面的数据仅用于示例,还无法实现编辑快捷键的功能)。 + +## 2026/1/9 初步创建快捷键编辑器界面。 + ## 2026/1/8 新增函数,能够获取指定快捷键的绑定详情,指定命令所绑定的快捷键,指定条件列表下所绑定的快捷键 \ No newline at end of file diff --git a/src/Graphics/Gui/message.hpp b/src/Graphics/Gui/message.hpp index a5f8db05bd..6d51baffdc 100644 --- a/src/Graphics/Gui/message.hpp +++ b/src/Graphics/Gui/message.hpp @@ -91,6 +91,7 @@ enum slot_id { SLOT_INPUT_PROPOSAL, SLOT_FILE, SLOT_DIRECTORY, + SLOT_SHORTCUT_WIDGET, slot_id__LAST // Please leave last and don't assign integer values to members }; @@ -601,6 +602,11 @@ set_auxiliary_widget_headline (widget w, string title) { send (w, SLOT_AUXILIARY_WIDGET_TITLE, title); } +inline void +set_shortcut_widgets (widget w) { + send (w, SLOT_SHORTCUT_WIDGET); +} + inline bool get_auxiliary_widget_visibility (widget w) { return query (w, SLOT_AUXILIARY_WIDGET_VISIBILITY); diff --git a/src/Graphics/Gui/widget.cpp b/src/Graphics/Gui/widget.cpp index f6a1571659..829ceab20a 100644 --- a/src/Graphics/Gui/widget.cpp +++ b/src/Graphics/Gui/widget.cpp @@ -88,6 +88,7 @@ slot_name (const slot s) { "SLOT_INPUT_PROPOSAL", "SLOT_FILE", "SLOT_DIRECTORY", + "SLOT_SHORTCUT_WIDGET", "slot_id__LAST"}; diff --git a/src/Plugins/Qt/qt_shortcut_widget.cpp b/src/Plugins/Qt/qt_shortcut_widget.cpp new file mode 100644 index 0000000000..b81f7b916b --- /dev/null +++ b/src/Plugins/Qt/qt_shortcut_widget.cpp @@ -0,0 +1,261 @@ +#include "qt_shortcut_widget.hpp" +#include "qcontainerfwd.h" +#include +#include +#include +#include +#include +#include +#include + +// ========================================== +// KeySequenceDelegate 实现 +// ========================================== + +KeySequenceDelegate::KeySequenceDelegate (QObject* parent) + : QStyledItemDelegate (parent) {} + +QWidget* +KeySequenceDelegate::createEditor (QWidget* parent, const QStyleOptionViewItem&, + const QModelIndex&) const { + QKeySequenceEdit* editor= new QKeySequenceEdit (parent); + // 保持背景透明,融入单元格 + editor->setStyleSheet ( + "QKeySequenceEdit { border: none; background: transparent; }"); + return editor; +} + +void +KeySequenceDelegate::setEditorData (QWidget* editor, + const QModelIndex& index) const { + QString value= index.model ()->data (index, Qt::EditRole).toString (); + QKeySequenceEdit* keyEditor= qobject_cast (editor); + if (keyEditor) { + keyEditor->setKeySequence (QKeySequence (value)); + } +} + +void +KeySequenceDelegate::setModelData (QWidget* editor, QAbstractItemModel* model, + const QModelIndex& index) const { + QKeySequenceEdit* keyEditor= qobject_cast (editor); + if (keyEditor) { + QKeySequence seq= keyEditor->keySequence (); + model->setData (index, seq.toString (QKeySequence::NativeText), + Qt::EditRole); + } +} + +void +KeySequenceDelegate::updateEditorGeometry (QWidget* editor, + const QStyleOptionViewItem& option, + const QModelIndex&) const { + editor->setGeometry (option.rect); +} + +// ========================================== +// Widget 主窗口实现 +// ========================================== + +Qshortcut_widget::Qshortcut_widget (QString title, QWidget* parent) + : QWidget (parent) { + this->setWindowTitle (title); + setupUi (); + setupStyle (); + initData (); +} + +Qshortcut_widget::~Qshortcut_widget () {} + +void +Qshortcut_widget::setupUi () { + resize (800, 600); + + QVBoxLayout* mainLayout= new QVBoxLayout (this); + mainLayout->setSpacing (10); + mainLayout->setContentsMargins (15, 15, 15, 15); + + // --- 1. 顶部搜索栏 --- + searchEdit= new QLineEdit (this); + searchEdit->setPlaceholderText ("搜索..."); + searchEdit->setClearButtonEnabled (true); + connect (searchEdit, &QLineEdit::textChanged, this, + &Qshortcut_widget::onSearchTextChanged); + mainLayout->addWidget (searchEdit); + + // --- 2. 中间树形列表 --- + treeWidget= new QTreeWidget (this); + treeWidget->setColumnCount (2); + treeWidget->setHeaderLabels (QStringList () << "操作" << "快捷键"); + + // 设置列宽 + treeWidget->header ()->setSectionResizeMode (0, QHeaderView::Stretch); + treeWidget->header ()->setSectionResizeMode (1, QHeaderView::Fixed); + treeWidget->header ()->resizeSection (1, 200); + treeWidget->header ()->setStretchLastSection (false); + + treeWidget->setSelectionBehavior (QAbstractItemView::SelectItems); + + // 只能单选 + treeWidget->setSelectionMode (QAbstractItemView::SingleSelection); + + treeWidget->setAlternatingRowColors (true); + treeWidget->setAnimated (true); + treeWidget->setFocusPolicy (Qt::NoFocus); + + treeWidget->setItemDelegateForColumn (0, new ReadOnlyDelegate (this)); + treeWidget->setItemDelegateForColumn (1, new KeySequenceDelegate (this)); + + treeWidget->setEditTriggers (QAbstractItemView::DoubleClicked | + QAbstractItemView::EditKeyPressed | + QAbstractItemView::AnyKeyPressed); + + mainLayout->addWidget (treeWidget); + + // --- 3. 底部按钮栏 --- + QHBoxLayout* bottomLayout= new QHBoxLayout (); + QPushButton* btnDefault = new QPushButton ("默认", this); + QPushButton* btnCustomize= new QPushButton ("自定义快捷键", this); + QPushButton* btnOk = new QPushButton ("确定", this); + QPushButton* btnCancel = new QPushButton ("取消", this); + + btnOk->setDefault (true); + + bottomLayout->addWidget (btnDefault); + bottomLayout->addStretch (); + bottomLayout->addWidget (btnCustomize); + bottomLayout->addWidget (btnOk); + bottomLayout->addWidget (btnCancel); + + mainLayout->addLayout (bottomLayout); +} + +void +Qshortcut_widget::setupStyle () { + //this->setStyleSheet (qss); +} + +void +Qshortcut_widget::addShortcutItem (QTreeWidgetItem* parent, const QString& name, + const QString& key) { + QTreeWidgetItem* item= new QTreeWidgetItem (parent); + item->setText (0, name); + item->setText (1, key); + + item->setFlags (Qt::ItemIsSelectable | Qt::ItemIsEditable | + Qt::ItemIsEnabled); +} + +void +Qshortcut_widget::initData () { + + QTreeWidgetItem* globalGroup= new QTreeWidgetItem (treeWidget); + globalGroup->setText (0, "全局"); + globalGroup->setFont (0, QFont ("Microsoft YaHei", 10, QFont::Bold)); + + globalGroup->setFlags (Qt::ItemIsEnabled | Qt::ItemIsSelectable); + globalGroup->setExpanded (true); + + int globalIndex= treeWidget->indexOfTopLevelItem (globalGroup); + + treeWidget->setFirstColumnSpanned (globalIndex, QModelIndex (), true); + addShortcutItem (globalGroup, "保存文件 (Save)", "Ctrl+S"); + addShortcutItem (globalGroup, "打开文件 (Open)", "Ctrl+O"); + addShortcutItem (globalGroup, "新建窗口 (New Window)", "Ctrl+Shift+N"); + addShortcutItem (globalGroup, "关闭标签页 (Close Tab)", "Ctrl+W"); + + QTreeWidgetItem* editGroup= new QTreeWidgetItem (treeWidget); + editGroup->setText (0, "文本模式"); + editGroup->setFont (0, QFont ("Microsoft YaHei", 10, QFont::Bold)); + editGroup->setFlags (Qt::ItemIsEnabled | Qt::ItemIsSelectable); + editGroup->setExpanded (true); + int editIndex= treeWidget->indexOfTopLevelItem (editGroup); + treeWidget->setFirstColumnSpanned (editIndex, QModelIndex (), true); + + addShortcutItem (editGroup, "复制当前行", "Ctrl+D"); + addShortcutItem (editGroup, "删除当前行", "Ctrl+Shift+K"); + addShortcutItem (editGroup, "向上移动行", "Alt+Up"); + addShortcutItem (editGroup, "向下移动行", "Alt+Down"); + + QTreeWidgetItem* viewGroup= new QTreeWidgetItem (treeWidget); + viewGroup->setText (0, "数学模式"); + viewGroup->setFont (0, QFont ("Microsoft YaHei", 10, QFont::Bold)); + viewGroup->setFlags (Qt::ItemIsEnabled | Qt::ItemIsSelectable); + int viewIndex= treeWidget->indexOfTopLevelItem (viewGroup); + treeWidget->setFirstColumnSpanned (viewIndex, QModelIndex (), true); + + addShortcutItem (viewGroup, "放大字体", "Ctrl++"); + addShortcutItem (viewGroup, "缩小字体", "Ctrl+-"); + addShortcutItem (viewGroup, "重置缩放", "Ctrl+0"); + QTreeWidgetItem* tabGrop = new QTreeWidgetItem (viewGroup); + tabGrop->setText(0, "tab循环"); + // 3. 设置样式(粗体,类似顶级分组) + tabGrop->setFont(0, QFont("Microsoft YaHei", 10, QFont::Bold)); + + // 4. 设置Flag:允许选中,但禁止编辑 + tabGrop->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + + // 5. 默认展开 + tabGrop->setExpanded(true); + addShortcutItem (tabGrop, "复制当前行", "Ctrl+D"); + // 1. 获取父节点 (viewGroup) 的 ModelIndex + QModelIndex parentIndex = treeWidget->indexFromItem(viewGroup); + + // 2. 获取 tabGrop 在父节点中的行号 + int childRow = viewGroup->indexOfChild(tabGrop); + + // 3. 设置跨列 (注意第二个参数传入了 parentIndex) + treeWidget->setFirstColumnSpanned(childRow, parentIndex, true); + // 6. 让这个分组标题跨越两列(这样更好看,防止分割线穿过文字) + // 注意:需要通过 treeWidget 去设置跨列 + //treeWidget->setFirstColumnSpanned(tabGrop, true); + + connect (treeWidget, &QTreeWidget::itemChanged, this, + &Qshortcut_widget::onShortcutChanged); +} + +void +Qshortcut_widget::onSearchTextChanged (const QString& text) { + if (text.isEmpty ()) { + for (int i= 0; i < treeWidget->topLevelItemCount (); ++i) { + QTreeWidgetItem* topItem= treeWidget->topLevelItem (i); + topItem->setHidden (false); + for (int j= 0; j < topItem->childCount (); ++j) { + topItem->child (j)->setHidden (false); + } + } + return; + } + + for (int i= 0; i < treeWidget->topLevelItemCount (); ++i) { + filterTreeItems (treeWidget->topLevelItem (i), text); + } +} + +void +Qshortcut_widget::onShortcutChanged (QTreeWidgetItem* item, int column) { + if (column == 1) { + QString actionName= item->text (0); // 操作名 + QString newKey = item->text (1); // 新快捷键 + + // qDebug () << "快捷键已更新 -> 操作:" << actionName << "新键位:" << + // newKey; + } +} + +bool +Qshortcut_widget::filterTreeItems (QTreeWidgetItem* item, const QString& text) { + bool childVisible= false; + for (int i= 0; i < item->childCount (); ++i) { + bool v = filterTreeItems (item->child (i), text); + childVisible= childVisible || v; + } + bool selfMatch= item->text (0).contains (text, Qt::CaseInsensitive) || + item->text (1).contains (text, Qt::CaseInsensitive); + bool shouldShow= selfMatch || childVisible; + item->setHidden (!shouldShow); + if (childVisible) { + item->setExpanded (true); + } + return shouldShow; +} diff --git a/src/Plugins/Qt/qt_shortcut_widget.hpp b/src/Plugins/Qt/qt_shortcut_widget.hpp new file mode 100644 index 0000000000..152362a271 --- /dev/null +++ b/src/Plugins/Qt/qt_shortcut_widget.hpp @@ -0,0 +1,75 @@ +#ifndef QT_SHORTCUT_WIDGET_HPP +#define QT_SHORTCUT_WIDGET_HPP + +#include "qcontainerfwd.h" +#include +#include +#include +#include +#include + +// ========================================== +// 类 1: 只读代理 +// ========================================== +class ReadOnlyDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit ReadOnlyDelegate (QObject* parent= nullptr) + : QStyledItemDelegate (parent) {} + QWidget* createEditor (QWidget*, const QStyleOptionViewItem&, + const QModelIndex&) const override { + return nullptr; + } +}; + +// ========================================== +// 类 2: 快捷键录入代理 +// ========================================== +class KeySequenceDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit KeySequenceDelegate (QObject* parent= nullptr); + QWidget* createEditor (QWidget* parent, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + void setEditorData (QWidget* editor, const QModelIndex& index) const override; + void setModelData (QWidget* editor, QAbstractItemModel* model, + const QModelIndex& index) const override; + void updateEditorGeometry (QWidget* editor, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override; +}; + +// ========================================== +// 类 3: 主窗口 +// ========================================== +class Qshortcut_widget : public QWidget { + Q_OBJECT + +public: + explicit Qshortcut_widget (QString tittle, QWidget* parent= nullptr); + ~Qshortcut_widget (); + +private slots: + // 搜索过滤槽函数 + void onSearchTextChanged (const QString& text); + // 快捷键变化时的槽函数 + void onShortcutChanged (QTreeWidgetItem* item, int column); + +private: + QLineEdit* searchEdit; // 搜索框 + QTreeWidget* treeWidget; + + // 初始化界面布局 + void setupUi (); + // 初始化样式表 + void setupStyle (); + // 初始化数据 + void initData (); + // 辅助函数:添加快捷键项 + void addShortcutItem (QTreeWidgetItem* parent, const QString& name, + const QString& key); + // 辅助函数:递归过滤树节点 + bool filterTreeItems (QTreeWidgetItem* item, const QString& text); +}; + +#endif diff --git a/src/Plugins/Qt/qt_tm_widget.cpp b/src/Plugins/Qt/qt_tm_widget.cpp index 2e835cc828..df12f70a2b 100644 --- a/src/Plugins/Qt/qt_tm_widget.cpp +++ b/src/Plugins/Qt/qt_tm_widget.cpp @@ -455,6 +455,7 @@ qt_tm_widget_rep::qt_tm_widget_rep (int mask, command _quit) sideTools = new QDockWidget ("side tools", 0); leftTools = new QDockWidget ("left tools", 0); auxiliaryWidget= new QTMAuxiliaryWidget ("auxiliary widget", 0); + shortcut_widget= new Qshortcut_widget ("shortcut"); // HACK: Wrap the dock in a "fake" window widget (last parameter = true) to // have clicks report the right position. static int cnt = 0; @@ -1036,6 +1037,9 @@ qt_tm_widget_rep::send (slot s, blackbox val) { string title= open_box (val); auxiliaryWidget->setWindowTitle (to_qstring (title)); } break; + case SLOT_SHORTCUT_WIDGET: { + shortcut_widget->show (); + } break; case SLOT_LEFT_FOOTER: { check_type (val, s); string msg= open_box (val); diff --git a/src/Plugins/Qt/qt_tm_widget.hpp b/src/Plugins/Qt/qt_tm_widget.hpp index 3dc2e854d0..93e3d386b3 100644 --- a/src/Plugins/Qt/qt_tm_widget.hpp +++ b/src/Plugins/Qt/qt_tm_widget.hpp @@ -14,6 +14,7 @@ #include "list.hpp" +#include "qt_shortcut_widget.hpp" #include "qt_simple_widget.hpp" #include "qt_widget.hpp" #include "qt_window_widget.hpp" @@ -80,6 +81,7 @@ class qt_tm_widget_rep : public qt_window_widget_rep { QTMTabPageContainer* tabPageContainer; QTMAuxiliaryWidget* auxiliaryWidget; QWK::WidgetWindowAgent* windowAgent; + Qshortcut_widget* shortcut_widget; QWK::GuestNotificationBar* guestNotificationBar; // 新增:访客提示条 QWK::LoginButton* loginButton; QWK::LoginDialog* m_loginDialog; diff --git a/src/Scheme/Glue/glue_server.lua b/src/Scheme/Glue/glue_server.lua index 35f71edae6..6c4a2fab57 100644 --- a/src/Scheme/Glue/glue_server.lua +++ b/src/Scheme/Glue/glue_server.lua @@ -166,6 +166,13 @@ function main() "string", -- 标题 } }, + { + --快捷键编辑窗口 + scm_name = "set-shortcut-widget", + cpp_name = "shortcut_widget", + ret_type = "void", + arg_list = {} + }, { scm_name = "show-bottom-tools", cpp_name = "show_bottom_tools", diff --git a/src/Texmacs/Window/tm_frame.cpp b/src/Texmacs/Window/tm_frame.cpp index 767baf0870..c455e2c682 100644 --- a/src/Texmacs/Window/tm_frame.cpp +++ b/src/Texmacs/Window/tm_frame.cpp @@ -214,6 +214,18 @@ tm_frame_rep::set_auxiliary_widget_title (string title) { concrete_window ()->set_auxiliary_widget_new_title (title); } +void +tm_frame_rep::shortcut_widget () { + if (!has_current_view ()) return; + url current_view= get_current_view (); + if (is_tmfs_view_type (as_string (current_view), "aux")) { + url vw= get_most_recent_view (); + concrete_view (vw)->win->set_shortcut_widget (); + return; + } + concrete_window ()->set_shortcut_widget (); +} + bool tm_frame_rep::auxiliary_widget_visible () { if (!has_current_view ()) return false; diff --git a/src/Texmacs/Window/tm_window.cpp b/src/Texmacs/Window/tm_window.cpp index 712ce44b41..9ba340a574 100644 --- a/src/Texmacs/Window/tm_window.cpp +++ b/src/Texmacs/Window/tm_window.cpp @@ -507,6 +507,11 @@ tm_window_rep::set_auxiliary_widget_new_title (string title) { set_auxiliary_widget_headline (wid, title); } +void +tm_window_rep::set_shortcut_widget () { + set_shortcut_widgets (wid); +} + void tm_window_rep::set_bottom_tools_flag (int which, bool flag) { if (which == 0) set_bottom_tools_visibility (wid, flag); diff --git a/src/Texmacs/server.hpp b/src/Texmacs/server.hpp index 2fd0243d5a..5d82909264 100644 --- a/src/Texmacs/server.hpp +++ b/src/Texmacs/server.hpp @@ -84,6 +84,7 @@ class server_rep : public abstract_struct { virtual void full_screen_mode (bool on, bool edit) = 0; virtual bool in_full_screen_mode () = 0; virtual bool in_full_screen_edit_mode () = 0; + virtual void shortcut_widget () = 0; virtual void show_footer (bool flag) = 0; virtual bool visible_footer () = 0; diff --git a/src/Texmacs/tm_frame.hpp b/src/Texmacs/tm_frame.hpp index 8244bc8751..09d3bbee18 100644 --- a/src/Texmacs/tm_frame.hpp +++ b/src/Texmacs/tm_frame.hpp @@ -12,6 +12,7 @@ #ifndef TM_FRAME_H #define TM_FRAME_H #include "server.hpp" +#include "widget.hpp" class tm_frame_rep : virtual public server_rep { protected: @@ -55,6 +56,7 @@ class tm_frame_rep : virtual public server_rep { void set_auxiliary_widget_title (string title); void auxiliary_widget (widget w, string name); void bottom_tools (int which, string menu); + void shortcut_widget (); /* canvas */ void set_window_zoom_factor (double zoom); diff --git a/src/Texmacs/tm_window.hpp b/src/Texmacs/tm_window.hpp index 5ca7b970b9..36326f166f 100644 --- a/src/Texmacs/tm_window.hpp +++ b/src/Texmacs/tm_window.hpp @@ -60,6 +60,7 @@ class tm_window_rep { void set_side_tools_flag (int which, bool flag); void set_auxiliary_widget_flag (bool flag); void set_auxiliary_widget_new_title (string title); + void set_shortcut_widget (); void set_bottom_tools_flag (int which, bool flag); bool get_header_flag (); bool get_auxiliary_widget_flag ();