desktop: fix saving files with '[', '*', or '?' in name on Linux (#7290)

* 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 <github.shum@liber.li>
This commit is contained in:
Narasimha-sc
2026-07-23 10:18:11 +01:00
committed by GitHub
co-authored by shum
parent 177a591466
commit fc28914455
@@ -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
* */