From fc28914455dc3ca69513177e7d3429051e0de4be Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:18:11 +0000 Subject: [PATCH] desktop: fix saving files with '[', '*', or '?' in name on Linux (#7290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * desktop: fix saving files with '[', '*', or '?' in name on Linux JFileChooser's BasicFileChooserUI treats filenames containing '[', '*', or '?' as glob patterns on Unix when the file does not exist on disk. The Save button's ApproveSelectionAction then applies a GlobFilter and returns without saving — the dialog stays open and nothing is written. Regression introduced in #2789 (commit 02d00944f, 2023-07-28), which started pre-populating the filename via selectedFile = File(filename). Before that the save dialog ran in DIRECTORIES_ONLY mode and the JDK glob check never saw the filename. Fix: on Linux save dialogs, find the Save button by identity (BasicFileChooserUI.getApproveSelectionAction() is public and returns the exact ActionListener wired on the button) and replace it with a wrapper that delegates to the original action for non-glob names and calls approveSelection() directly for glob names. Windows (per the JDK only '*' and '?' are glob chars there) and macOS (uses AWT FileDialog) are unaffected. Open dialogs keep glob filtering for power-user search. * desktop: always bypass JFileChooser glob-on-save on Linux Earlier in this branch we added a heuristic to delegate to the original ApproveSelectionAction for non-'[' names so that typing '*.pdf' in the save dialog would still glob-filter the listing. That preserved a Swing-only quirk that no native OS save dialog implements: - macOS uses NSSavePanel (already used in this app via AWT FileDialog) — no glob-on-save. - Native Windows / Linux GTK / KDE save dialogs — no glob-on-save. - JFileChooser on Windows — has it, but '*' and '?' aren't even legal NTFS filename chars, so users won't be typing them. It also blocks legitimate filenames containing '*' or '?' on Linux (legal on POSIX filesystems) from being saved, since they too get intercepted as glob. Replace the Save button's action with a handler that always treats the typed text as a literal filename. The result: any filename can be saved on Linux, behaviour aligns with macOS and with every native OS save dialog. The open dialog is untouched, so '*.pdf' glob-filtering there still works. * desktop: restore directory-only save dialog on Linux The previous commit installed the literal-filename glob bypass for every Linux save dialog (!isLoad && isLinux), including the DIRECTORIES_ONLY mode used when filename == null — e.g. the "Save QR code as image" flow (saveTempImageUncompressed → saveDialog.awaitResult() with no params). In that mode the bypass broke directory selection entirely: selecting a subfolder + Save traversed into it instead of approving it, and Save with an empty filename field no-oped. Net result: QR-code image saving was unusable on Linux. The glob-on-save bug only arises because a filename is pre-populated (selectedFile = File(filename)), which never happens in DIRECTORIES_ONLY mode. Gate the bypass on filename != null so the directory-save path keeps JFileChooser's original approve action. * desktop: simplify Linux save glob bypass via getDefaultButton --------- Co-authored-by: shum --- .../views/helpers/DefaultDialog.desktop.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt index 7341c6af23..79aaf5c727 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt @@ -14,10 +14,12 @@ import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.awt.FileDialog +import java.awt.event.ActionListener import java.io.File import javax.swing.JFileChooser import javax.swing.filechooser.FileFilter import javax.swing.filechooser.FileNameExtensionFilter +import javax.swing.plaf.basic.BasicFileChooserUI @Composable actual fun DefaultDialog( @@ -77,6 +79,13 @@ fun FrameWindowScope.FileDialogChooserMultiple( fileChooser.dialogTitle = title fileChooser.isMultiSelectionEnabled = allowMultiple && isLoad fileChooser.isAcceptAllFileFilterUsed = fileFilter == null + // Only install the glob bypass for the real file-save case (filename != null). When filename + // is null the dialog runs in DIRECTORIES_ONLY mode (e.g. "Save QR code as image"), where the + // Save button must approve the selected directory — the literal-filename handler would instead + // traverse into it (or no-op on an empty field), making directory selection impossible. + if (!isLoad && filename != null && desktopPlatform.isLinux()) { + installUnixSaveGlobBypass(fileChooser) + } if (fileFilter != null && fileFilterDescription != null) { fileChooser.addChoosableFileFilter(object: FileFilter() { override fun accept(file: File?): Boolean = fileFilter(file) @@ -120,6 +129,28 @@ fun FrameWindowScope.FileDialogChooserMultiple( } } +// Replace the Save button's action with a literal-filename handler. This bypasses JFileChooser's +// glob-on-save behaviour, which mis-handles '[' as a glob char on Unix (breaking filenames like +// '[1].pdf') and is not a feature of any native OS save dialog — macOS NSSavePanel and native +// Windows / Linux GTK / KDE save dialogs all treat the typed filename as a literal name. +private fun installUnixSaveGlobBypass(fc: JFileChooser) { + val ui = fc.ui as? BasicFileChooserUI ?: return + val original: ActionListener = ui.approveSelectionAction + val btn = ui.getDefaultButton(fc) ?: return + btn.removeActionListener(original) + btn.addActionListener { + val name = ui.fileName?.takeIf { it.isNotEmpty() } ?: return@addActionListener + val typed = File(name) + val target = if (typed.isAbsolute) typed else File(fc.currentDirectory, name) + if (target.isDirectory && fc.isTraversable(target)) { + fc.currentDirectory = target + } else { + fc.selectedFile = target + fc.approveSelection() + } + } +} + /* * Has graphic glitches on many Linux distributions, so use only on non-Linux systems. Also file filter doesn't work on Windows * */