Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions devel/202_106.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# [202_106] Cmd+V paste not working in macOS save file dialog

### How to test
1. Open Mogan on macOS
2. Copy some text to the clipboard
3. Open **Save As** dialog (File → Save as)
4. In the filename text field, press **Cmd+V** to paste
5. Verify the clipboard text is pasted into the filename field
6. Also verify **Cmd+C**, **Cmd+X**, **Cmd+A** work in the dialog
7. After closing the dialog, verify editor shortcuts still work normally

## 2026/03/01
### What
Temporarily disable QAction keyboard shortcuts while a native file dialog is open on macOS, then restore them after the dialog closes.

### Why
On macOS, QAction shortcuts registered in the menu bar are converted to NSMenuItem key equivalents. When a native file dialog (NSSavePanel/NSOpenPanel) is shown via `QFileDialog::exec()`, pressing Cmd+V triggers the QAction for "Paste" in Mogan's menu bar instead of the standard Cocoa `paste:` action in the dialog's text field. This prevents users from pasting filenames in the Save As dialog.

The `ShortcutOverride` mechanism in `QTMWidget::event()` does not help here because the QTMWidget does not have focus while a modal dialog is open.

Fixes: https://github.com/XmacsLabs/mogan/issues/2894

### How
In `qt_chooser_widget_rep::perform_dialog()` in `src/Plugins/Qt/qt_chooser_widget.cpp`:

- Before `dialog->exec()`: on macOS (`Q_OS_MACOS`), iterate all `QAction`s in the active window, save their shortcuts, then clear them via `action->setShortcut(QKeySequence())`
- After `dialog->exec()` returns: restore all saved shortcuts

This removes the NSMenuItem key equivalents while the dialog is open, allowing the native dialog to process standard editing shortcuts (Cmd+V/C/X/A) through the Cocoa responder chain.
27 changes: 27 additions & 0 deletions src/Plugins/Qt/qt_chooser_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
#include "mupdf_picture.hpp"
#endif

#include <QApplication>
#include <QByteArray>
#include <QDebug>
#include <QFileDialog>
#include <QKeySequence>
#include <QString>
#include <QStringList>

Expand Down Expand Up @@ -344,6 +346,24 @@ qt_chooser_widget_rep::perform_dialog () {
r.moveCenter (pos);
dialog->setGeometry (r);

#ifdef Q_OS_MACOS
// On macOS, QAction shortcuts registered in the menu bar become NSMenuItem
// key equivalents. When a native file dialog is open, these key equivalents
Comment on lines +349 to +351
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This function already uses OS_MACOS earlier, but the new shortcut-disabling logic is guarded with Q_OS_MACOS. Mixing platform macros in the same file makes it harder to reason about which builds include the code and can lead to accidental omissions in some configurations. Consider using the same macOS guard consistently (whichever is standard for this module).

Copilot uses AI. Check for mistakes.
// intercept standard editing shortcuts (Cmd+V, Cmd+C, Cmd+X, Cmd+A) before
// the dialog's text field can handle them. Temporarily clearing all QAction
// shortcuts allows the native dialog to process these keys normally.
QList<QPair<QAction*, QKeySequence>> savedShortcuts;
QWidget* mainWin= QApplication::activeWindow ();
if (mainWin) {
for (QAction* action : mainWin->findChildren<QAction*> ()) {
if (!action->shortcut ().isEmpty ()) {
savedShortcuts.append (qMakePair (action, action->shortcut ()));
Comment on lines +352 to +360
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This loop clears shortcuts for all actions under the active window, not just the standard editing shortcuts called out in the PR description. That can temporarily disable other important app/window shortcuts while the dialog is open. Consider limiting the clearing/restoring to only the conflicting sequences (Copy/Paste/Cut/SelectAll, and optionally Undo/Redo) to reduce user-visible side effects.

Suggested change
// intercept standard editing shortcuts (Cmd+V, Cmd+C, Cmd+X, Cmd+A) before
// the dialog's text field can handle them. Temporarily clearing all QAction
// shortcuts allows the native dialog to process these keys normally.
QList<QPair<QAction*, QKeySequence>> savedShortcuts;
QWidget* mainWin= QApplication::activeWindow ();
if (mainWin) {
for (QAction* action : mainWin->findChildren<QAction*> ()) {
if (!action->shortcut ().isEmpty ()) {
savedShortcuts.append (qMakePair (action, action->shortcut ()));
// intercept standard editing shortcuts (Cmd+V, Cmd+C, Cmd+X, Cmd+A, etc.)
// before the dialog's text field can handle them. Temporarily clearing
// shortcuts only for these standard editing sequences allows the native
// dialog to process them normally while preserving other application
// shortcuts.
QList<QPair<QAction*, QKeySequence>> savedShortcuts;
QWidget* mainWin= QApplication::activeWindow ();
if (mainWin) {
const QList<QKeySequence> conflictingSequences = {
QKeySequence::Copy,
QKeySequence::Paste,
QKeySequence::Cut,
QKeySequence::SelectAll,
QKeySequence::Undo,
QKeySequence::Redo
};
for (QAction* action : mainWin->findChildren<QAction*> ()) {
const QKeySequence shortcut = action->shortcut ();
if (!shortcut.isEmpty () && conflictingSequences.contains (shortcut)) {
savedShortcuts.append (qMakePair (action, shortcut));

Copilot uses AI. Check for mistakes.
action->setShortcut (QKeySequence ());
}
}
}
#endif

QStringList fileNames;
file= "#f";
if (dialog->exec ()) {
Expand Down Expand Up @@ -429,6 +449,13 @@ qt_chooser_widget_rep::perform_dialog () {

delete dialog;

#ifdef Q_OS_MACOS
// Restore menu shortcuts after the native dialog is closed
for (const auto& pair : savedShortcuts) {
pair.first->setShortcut (pair.second);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The restore loop assumes every QAction* in savedShortcuts is still alive. While a native modal dialog is open on macOS, it’s possible for actions/windows to be destroyed (e.g., via menu interactions), which would leave dangling pointers here and can crash. Consider storing QPointer<QAction> (or tracking destroyed) and skipping entries whose action has been deleted before restoring.

Suggested change
pair.first->setShortcut (pair.second);
QAction* action = pair.first;
if (!action)
continue;
action->setShortcut (pair.second);

Copilot uses AI. Check for mistakes.
}
#endif

cmd ();
if (!is_nil (quit)) quit ();
}
Loading