commit abb138b6c1004db8c91560c75e9ffb92aa3a9f3a
Author: Koji Yokota <[email protected]>
Date:   Fri Oct 23 17:34:58 2026 +0900

    Fix #13284 (Emacs-like two-stroke shortcuts doesn't work)
    
    macOS automatically registers the text after '\t' in a menu item
    as a shortcut and it overrides the shortcut set in QAction.
    
    The patch basically diables menu actions in the prefix-key stage.
    Also, appearance of two-stroke key shortcut in menu is adjusted to
    match with that of one-stroke key shortcut.
    
    Assisted-by: Claude
---
 configure.ac                    |  4 ++++
 src/frontends/qt/CMakeLists.txt | 12 ++++++++--
 src/frontends/qt/GuiView.cpp    | 36 ++++++++++++++++++++++++++++++
 src/frontends/qt/Makefile.am    |  5 +++++
 src/frontends/qt/Menus.cpp      | 49 ++++++++++++++++++++++++++++++-----------
 5 files changed, 91 insertions(+), 15 deletions(-)

diff --git a/configure.ac b/configure.ac
index 1dae44109a..4e49e9c8fd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -67,6 +67,10 @@ AC_LANG(C++)
 AC_PROG_OBJC
 _AM_DEPENDENCIES([OBJC])
 
+### Objective-C++ compiler
+AC_PROG_OBJCXX
+_AM_DEPENDENCIES([OBJCXX])
+
 ### Add extra directories to check for libraries.
 LYX_WITH_DIR([extra-lib],[extra library directory],extra_lib, NONE)
 LYX_LOOP_DIR($lyx_cv_extra_lib,LYX_ADD_LIB_DIR(lyx_ldflags,$dir))
diff --git a/src/frontends/qt/CMakeLists.txt b/src/frontends/qt/CMakeLists.txt
index 05d4c4abf4..ab2ecfb8c2 100644
--- a/src/frontends/qt/CMakeLists.txt
+++ b/src/frontends/qt/CMakeLists.txt
@@ -27,6 +27,12 @@ file(GLOB moc_files 
${TOP_SRC_DIR}/src/frontends/qt/${LYX_MOC_FILES})
 
 list(REMOVE_ITEM frontends_qt_sources ${moc_files} .)
 
+if(APPLE)
+       set(frontends_qt_objcxx_sources 
${TOP_SRC_DIR}/src/frontends/qt/MenusMacOS.mm)
+else()
+       set(frontends_qt_objcxx_sources "")
+endif()
+
 file(GLOB frontends_qt_headers 
${TOP_SRC_DIR}/src/frontends/qt/${LYX_HPP_FILES})
 file(GLOB frontend_qt_UI ${TOP_SRC_DIR}/src/frontends/qt/ui/*.ui)
 
@@ -57,9 +63,9 @@ if(LYX_MERGE_FILES)
        set_source_files_properties(_allinone_const.C PROPERTIES OBJECT_DEPENDS 
"${depends_moc_uic}")
        set_source_files_properties(_allinone_touched.C PROPERTIES 
OBJECT_DEPENDS "${depends_moc_uic}")
        add_library(frontend_qt ${library_type}
-               ${_allinone_files} ${frontends_qt_headers} ${ui_files} 
${resource_files})
+               ${_allinone_files} ${frontends_qt_objcxx_sources} 
${frontends_qt_headers} ${ui_files} ${resource_files})
 else()
-       add_library(frontend_qt ${library_type} ${frontends_qt_sources} 
${frontends_qt_headers} ${ui_files} ${resource_files})
+       add_library(frontend_qt ${library_type} ${frontends_qt_sources} 
${frontends_qt_objcxx_sources} ${frontends_qt_headers} ${ui_files} 
${resource_files})
 endif()
 set_target_properties(frontend_qt PROPERTIES FOLDER "applications/LyX" 
QT_NO_UNICODE_DEFINES TRUE)
 
@@ -83,6 +89,8 @@ lyx_add_gcc_pch(frontend_qt)
 
 if(WIN32)
        target_link_libraries(frontend_qt gdi32)
+elseif(APPLE)
+       target_link_libraries(frontend_qt "-framework AppKit")
 endif()
 
 project_source_group("${GROUP_CODE}" frontends_qt_sources frontends_qt_headers)
diff --git a/src/frontends/qt/GuiView.cpp b/src/frontends/qt/GuiView.cpp
index b81fa54728..36df8c7b16 100644
--- a/src/frontends/qt/GuiView.cpp
+++ b/src/frontends/qt/GuiView.cpp
@@ -109,6 +109,7 @@
 #include <QPainter>
 #include <QPixmap>
 #include <QPoint>
+#include <QPointer>
 #include <QSettings>
 #include <QShowEvent>
 #include <QSlider>
@@ -568,6 +569,35 @@ public:
        /// equivalent time_to_update = 0; but better to see it the code
        bool stats_update_trigger_ = false;
 
+       /// Menu actions disabled while a multi-key binding is pending
+       QList<QPointer<QAction>> key_equiv_blocked_;
+
+       /// On macOS, Qt turns the binding shown after '\t' in a menu label into
+       /// a native key equivalent, which then steals the second stroke of a
+       /// multi-key binding (e.g. C-s in C-x C-s). Disable those menu items
+       /// while a prefix is pending (bug #13284).
+       void blockMenuKeyEquivalents(bool block)
+       {
+               if (!block) {
+                       for (QPointer<QAction> const & a : key_equiv_blocked_)
+                               if (a)
+                                       a->setEnabled(true);
+                       key_equiv_blocked_.clear();
+                       return;
+               }
+               if (!key_equiv_blocked_.isEmpty())
+                       return;
+               QList<QAction *> todo = gv_->menuBar()->actions();
+               while (!todo.isEmpty()) {
+                       QAction * a = todo.takeLast();
+                       if (QMenu * m = a->menu())
+                               todo += m->actions();
+                       else if (a->isEnabled() && a->text().contains('\t')) {
+                               a->setEnabled(false);
+                               key_equiv_blocked_.append(a);
+                       }
+               }
+       }
 };
 
 QSet<Buffer const *> GuiView::GuiViewPrivate::busyBuffers;
@@ -595,6 +625,12 @@ GuiView::GuiView(int id)
        // Fill up the menu bar.
        guiApp->menus().fillMenuBar(menuBar(), this, true);
 
+#if defined(Q_OS_MAC)
+       connect(guiApp, &GuiApplication::keyCommandStateSet, this, [this]() {
+               d.blockMenuKeyEquivalents(guiApp->isInCommandMode());
+       });
+#endif
+
        setCentralWidget(d.stack_widget_);
 
        // Start autosave timer
diff --git a/src/frontends/qt/Makefile.am b/src/frontends/qt/Makefile.am
index e87478abe8..4705295799 100644
--- a/src/frontends/qt/Makefile.am
+++ b/src/frontends/qt/Makefile.am
@@ -365,4 +365,9 @@ liblyxqt_a_SOURCES = \
        $(MOCHEADER) \
        $(NOMOCHEADER)
 
+if INSTALL_MACOSX
+liblyxqt_a_SOURCES += MenusMacOS.mm
+AM_OBJCXXFLAGS = $(AM_CXXFLAGS)
+endif
+
 EXTRA_DIST = CMakeLists.txt $(UIFILES:%=ui/%)
diff --git a/src/frontends/qt/Menus.cpp b/src/frontends/qt/Menus.cpp
index e8f4cc0902..f676d8e2ce 100644
--- a/src/frontends/qt/Menus.cpp
+++ b/src/frontends/qt/Menus.cpp
@@ -89,6 +89,12 @@ using namespace lyx::support;
 namespace lyx {
 namespace frontend {
 
+#if defined(Q_OS_MAC)
+// Defined in MenusMacOS.mm. Shows the key bindings appended to the labels of
+// the native menu items of \p qmenu (and its submenus) in a dimmed color.
+void greyOutBindings(QMenu * qmenu);
+#endif
+
 namespace {
 
 // macOS specific stuff is at the end.
@@ -2332,7 +2338,8 @@ struct Menu::Impl
 
 
 /// Get a MenuDefinition item label from the menu backend
-static QString label(MenuItem const & mi, MenuDefinition const & menu)
+static QString label(MenuItem const & mi, MenuDefinition const & menu,
+                     QString const & binding)
 {
        QString label = mi.label();
        label.replace("&", "&&");
@@ -2345,17 +2352,22 @@ static QString label(MenuItem const & mi, 
MenuDefinition const & menu)
                        label.replace(pos, 0, "&");
        }
 
-       QString const binding = mi.binding();
+       if (!binding.isEmpty()) {
 #if defined(Q_OS_MAC)
-       // MacOS cannot display complex bindings, so hide those
-       // https://doc.qt.io/qt-6/macos-issues.html#menu-actions
-       // #12693
-       if (!binding.isEmpty() && !binding.contains(" "))
-               label += '\t' + binding;
+               // Strings after '\t' are automatically registered as a shortcut
+               // in macOS which causes a problem in two-stroke shortcuts
+               // https://www.lyx.org/trac/ticket/13284
+               //
+               // Also note:
+               // MacOS cannot display complex bindings, so hide those
+               // https://doc.qt.io/qt-6/macos-issues.html#menu-actions
+               // #12693
+               QString mod = binding;
+               label += '\t' + mod.replace(" ", ",");
 #else
-       if (!binding.isEmpty())
                label += '\t' + binding;
 #endif
+       }
 
        return label;
 }
@@ -2374,20 +2386,28 @@ void Menu::Impl::populate(QMenu * qMenu, MenuDefinition 
const & menu)
                        qMenu->addSeparator();
                        break;
                case MenuItem::Submenu: {
-                       QMenu * subMenu = qMenu->addMenu(label(m, menu));
+                       QMenu * subMenu = qMenu->addMenu(label(m, menu, 
QString()));
                        populate(subMenu, m.submenu());
                        subMenu->setEnabled(!subMenu->isEmpty());
                        break;
                }
                case MenuItem::Command:
-               default:
+               default: {
                        // FIXME: A previous comment assured that 
MenuItem::Command was the
                        // only possible case in practice, but this is wrong.  
It would be
                        // good to document which cases are actually treated 
here.
-                       qMenu->addAction(new Action(m.func(), QIcon(), label(m, 
menu),
-                                                   m.tooltip(), qMenu));
+                       QString const binding = m.binding();
+                       QString const lbl = label(m, menu, binding);
+                       Action * act = new Action(m.func(), QIcon(), lbl, 
m.tooltip(), qMenu);
+#if defined(Q_OS_MAC)
+                       // used by greyOutBindings() to locate the binding in 
the title
+                       if (!binding.isEmpty())
+                               act->setProperty("lyx_binding", 
lbl.section('\t', 1));
+#endif
+                       qMenu->addAction(act);
                        break;
                }
+               }
 }
 
 #if (defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN))
@@ -2435,6 +2455,9 @@ Menu::~Menu()
 void Menu::updateView()
 {
        guiApp->menus().updateMenu(this);
+#if defined(Q_OS_MAC)
+       greyOutBindings(this);
+#endif
 }
 
 
@@ -2934,7 +2957,7 @@ void Menus::fillMenuBar(QMenuBar * qmb, GuiView * view, 
bool initial)
                }
 
                Menu * menuptr = new Menu(view, m.submenuname(), true);
-               menuptr->setTitle(label(m, menu));
+               menuptr->setTitle(label(m, menu, QString()));
 
 #if defined(Q_OS_MAC)
                // On Mac OS with Qt/Cocoa, the menu is not displayed if there 
is no action
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs

Reply via email to