fixed open with

This commit is contained in:
rdavidek 2026-04-19 14:51:00 +02:00
parent 3b85234e55
commit 2114d2d453
2 changed files with 424 additions and 14 deletions

View File

@ -15,7 +15,7 @@ import java.io.InputStreamReader;
*/ */
public class MainApp { public class MainApp {
public static final String APP_VERSION = "1.3.1"; public static final String APP_VERSION = "1.3.2";
public enum OS { public enum OS {
WINDOWS, LINUX, MACOS, UNKNOWN WINDOWS, LINUX, MACOS, UNKNOWN

View File

@ -21,6 +21,9 @@ import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileReader;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -1932,19 +1935,50 @@ public class FilePanelTab extends JPanel {
} }
} }
// 2. If executable, start it directly // 2. If it's a directory, navigate to it
if (file.canExecute() && !file.isDirectory()) { if (file.isDirectory()) {
new ProcessBuilder(file.getAbsolutePath()) loadDirectory(file);
.directory(file.getParentFile()) return;
.start();
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
// 3. Fallback to system default
Desktop.getDesktop().open(file);
// Try to keep focus or at least set it back for when user returns
SwingUtilities.invokeLater(() -> {
fileTable.requestFocusInWindow();
});
} }
// 3. Try to find and use system file association
String associatedProgram = findAssociatedProgram(file);
if (associatedProgram != null && !associatedProgram.isEmpty()) {
try {
java.util.List<String> cmdList = new java.util.ArrayList<>();
cmdList.add(associatedProgram);
cmdList.add(file.getAbsolutePath());
new ProcessBuilder(cmdList).directory(file.getParentFile()).start();
return;
} catch (Exception ex) {
// If association fails, continue to Desktop.open() below
}
}
// 4. Try Desktop.open() - most reliable for non-executable files
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
try {
Desktop.getDesktop().open(file);
return;
} catch (Exception desktopEx) {
// Continue to next fallback
}
}
// 5. If executable and other methods failed, try to run directly
if (file.canExecute()) {
try {
new ProcessBuilder(file.getAbsolutePath())
.directory(file.getParentFile())
.start();
return;
} catch (Exception ex) {
// Continue to error
}
}
// 6. If nothing worked, show error
throw new IOException("Cannot open file: " + file.getName() + " (no associated application found)");
} catch (Exception ex) { } catch (Exception ex) {
try { try {
JOptionPane.showMessageDialog(this, "Error opening file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(this, "Error opening file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
@ -1996,6 +2030,352 @@ public class FilePanelTab extends JPanel {
} }
} }
/**
* Find the system file association for a given file.
* Returns the path to the associated program, or null if not found.
*/
private String findAssociatedProgram(File file) {
if (file == null || !file.isFile()) return null;
String fileName = file.getName().toLowerCase();
String ext = "";
int dot = fileName.lastIndexOf('.');
if (dot > 0 && dot < fileName.length() - 1) {
ext = fileName.substring(dot).toLowerCase();
}
if (ext.isEmpty()) return null;
try {
if (MainApp.CURRENT_OS == MainApp.OS.WINDOWS) {
return findWindowsAssociation(ext);
} else if (MainApp.CURRENT_OS == MainApp.OS.LINUX) {
return findLinuxAssociation(ext);
} else if (MainApp.CURRENT_OS == MainApp.OS.MACOS) {
return "open"; // macOS uses 'open' command with -a flag for associations
}
} catch (Exception ignored) {
}
return null;
}
/**
* Get system file associations for a given extension.
* Returns a map of display names to application paths.
*/
private java.util.Map<String, String> getSystemAssociations(String ext) {
java.util.Map<String, String> result = new java.util.LinkedHashMap<>();
if (ext == null || ext.isEmpty()) return result;
try {
if (MainApp.CURRENT_OS == MainApp.OS.WINDOWS) {
getWindowsSystemAssociations(ext, result);
} else if (MainApp.CURRENT_OS == MainApp.OS.LINUX) {
getLinuxSystemAssociations(ext, result);
} else if (MainApp.CURRENT_OS == MainApp.OS.MACOS) {
getMacSystemAssociations(ext, result);
}
} catch (Exception ignored) {
}
return result;
}
/**
* Get Windows system associations by querying registry.
*/
private void getWindowsSystemAssociations(String ext, java.util.Map<String, String> result) {
try {
// Step 1: Get the file type
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "assoc " + ext);
Process p = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = reader.readLine();
p.waitFor();
String fileType = null;
if (line != null && line.contains("=")) {
fileType = line.substring(line.indexOf("=") + 1).trim();
}
if (fileType == null || fileType.isEmpty()) return;
// Step 2: Query registry for shell commands (open, openas, etc.)
pb = new ProcessBuilder("reg", "query", "HKEY_CLASSES_ROOT\\" + fileType + "\\shell");
p = pb.start();
reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
java.util.Set<String> verbs = new java.util.HashSet<>();
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.startsWith("HKEY_CLASSES_ROOT")) {
String verb = line.substring(line.lastIndexOf("\\") + 1);
if (!verb.isEmpty()) {
verbs.add(verb);
}
}
}
p.waitFor();
// Step 3: For each verb, get the command
for (String verb : verbs) {
try {
pb = new ProcessBuilder("reg", "query", "HKEY_CLASSES_ROOT\\" + fileType + "\\shell\\" + verb + "\\command");
p = pb.start();
reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
while ((line = reader.readLine()) != null) {
if (line.contains("REG_SZ")) {
String command = line.replaceAll(".*REG_SZ", "").trim();
// Extract executable path
if (command.startsWith("\"")) {
command = command.substring(1);
int endIdx = command.indexOf("\"");
if (endIdx > 0) {
command = command.substring(0, endIdx);
}
} else {
// Handle case without quotes: path\to\app.exe "%1"
int spaceIdx = command.indexOf(" ");
if (spaceIdx > 0) {
command = command.substring(0, spaceIdx);
}
}
File execFile = new File(command);
if (execFile.exists() && command.toLowerCase().endsWith(".exe")) {
// Get the executable name for display
String displayName = execFile.getName();
if (displayName.toLowerCase().endsWith(".exe")) {
displayName = displayName.substring(0, displayName.length() - 4);
}
// Capitalize first letter
if (!displayName.isEmpty()) {
displayName = displayName.substring(0, 1).toUpperCase() + displayName.substring(1);
}
result.put(displayName + " (" + verb + ")", command);
break; // Got command for this verb
}
}
}
p.waitFor();
} catch (Exception ex) {
// Continue with next verb
}
}
} catch (Exception ignored) {
}
}
/**
* Get Linux system associations using xdg-mime.
*/
private void getLinuxSystemAssociations(String ext, java.util.Map<String, String> result) {
try {
// Get mime type
ProcessBuilder pb = new ProcessBuilder("xdg-mime", "query", "filetype", "dummy" + ext);
Process p = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String mimeType = reader.readLine();
p.waitFor();
if (mimeType == null || mimeType.trim().isEmpty()) {
result.put("Default application", "xdg-open");
return;
}
// Get list of possible applications for this mime type
pb = new ProcessBuilder("xdg-mime", "query", "default", mimeType.trim());
p = pb.start();
reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String defaultApp = reader.readLine();
p.waitFor();
if (defaultApp != null && !defaultApp.trim().isEmpty()) {
String[] searchPaths = {
System.getProperty("user.home") + "/.local/share/applications/" + defaultApp.trim(),
"/usr/share/applications/" + defaultApp.trim(),
"/usr/local/share/applications/" + defaultApp.trim()
};
for (String path : searchPaths) {
File f = new File(path);
if (f.exists()) {
try (BufferedReader fr = new BufferedReader(new FileReader(f))) {
String line;
while ((line = fr.readLine()) != null) {
if (line.startsWith("Name=")) {
String appName = line.substring(5).trim();
String exec = parseDesktopFile(f);
if (exec != null && !exec.isEmpty()) {
result.put(appName, exec);
}
break;
}
}
}
break;
}
}
}
// Fallback
if (result.isEmpty()) {
result.put("Default application", "xdg-open");
}
} catch (Exception ignored) {
}
}
/**
* Get macOS system associations using open command.
*/
private void getMacSystemAssociations(String ext, java.util.Map<String, String> result) {
// On macOS, we can use the 'open' command with -a flag to choose an application
// But getting all available apps is complex, so we'll just add the default
result.put("Default application", "open");
}
/**
* Find Windows file association using assoc and ftype commands.
* More reliable than registry queries.
*/
private String findWindowsAssociation(String ext) {
try {
// Step 1: Use 'assoc .html' to get the file type (e.g., 'htmlfile')
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "assoc " + ext);
Process p = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = reader.readLine();
p.waitFor();
String fileType = null;
if (line != null && line.contains("=")) {
// Format: '.html=htmlfile'
fileType = line.substring(line.indexOf("=") + 1).trim();
}
if (fileType == null || fileType.isEmpty()) return null;
// Step 2: Use 'ftype htmlfile' to get the command (e.g., '"C:\Program Files\...\iexplore.exe" "%1"')
pb = new ProcessBuilder("cmd", "/c", "ftype " + fileType);
p = pb.start();
reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
line = reader.readLine();
p.waitFor();
if (line != null && line.contains("=")) {
// Format: 'htmlfile="C:\Program Files\Internet Explorer\iexplore.exe" "%1"'
String command = line.substring(line.indexOf("=") + 1).trim();
// Extract the executable path from the command
// Format: "C:\Program Files\...\app.exe" "%1" or similar
if (command.startsWith("\"")) {
command = command.substring(1); // Remove leading quote
int endIdx = command.indexOf("\"");
if (endIdx > 0) {
command = command.substring(0, endIdx);
}
}
// Verify the executable exists
File execFile = new File(command);
if (execFile.exists() && command.toLowerCase().endsWith(".exe")) {
return command;
}
}
} catch (Exception ignored) {
}
return null;
}
/**
* Find Linux file association using xdg-mime and xdg-open.
*/
private String findLinuxAssociation(String ext) {
try {
// Use xdg-mime to find the mime type, then find the associated desktop application
String mimeType = null;
// Query mime type for extension
ProcessBuilder pb = new ProcessBuilder("xdg-mime", "query", "filetype", "dummy" + ext);
Process p = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = reader.readLine();
if (line != null) {
mimeType = line.trim();
}
p.waitFor();
if (mimeType == null || mimeType.isEmpty() || mimeType.equals("application/octet-stream")) {
// Try generic approach with xdg-open (it handles associations internally)
return "xdg-open";
}
// Find default application for mime type
pb = new ProcessBuilder("xdg-mime", "query", "default", mimeType);
p = pb.start();
reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String desktopFile = reader.readLine();
p.waitFor();
if (desktopFile != null && !desktopFile.trim().isEmpty()) {
desktopFile = desktopFile.trim();
// Search for the .desktop file in standard locations
String[] searchPaths = {
System.getProperty("user.home") + "/.local/share/applications/" + desktopFile,
"/usr/share/applications/" + desktopFile,
"/usr/local/share/applications/" + desktopFile
};
for (String path : searchPaths) {
File f = new File(path);
if (f.exists()) {
// Parse .desktop file to find the Exec line
String exec = parseDesktopFile(f);
if (exec != null && !exec.isEmpty()) {
return exec;
}
}
}
}
// Fallback to xdg-open
return "xdg-open";
} catch (Exception ignored) {
}
return "xdg-open"; // Fallback
}
/**
* Parse a .desktop file to extract the Exec command.
*/
private String parseDesktopFile(File desktopFile) {
try (BufferedReader reader = new BufferedReader(new FileReader(desktopFile))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("Exec=")) {
String exec = line.substring(5).trim();
// Remove any %f, %u, %F, %U placeholders
exec = exec.replaceAll("%[fuFU]", "").trim();
// Keep only the first word (command)
String[] parts = exec.split("\\s+");
if (parts.length > 0 && !parts[0].isEmpty()) {
return parts[0];
}
}
}
} catch (Exception ignored) {
}
return null;
}
/** /**
* Open the item located at the given point (used for double-clicks while * Open the item located at the given point (used for double-clicks while
* mouse-driven selection is blocked). This mirrors the behavior of * mouse-driven selection is blocked). This mirrors the behavior of
@ -2119,12 +2499,14 @@ public class FilePanelTab extends JPanel {
if (!isParentDir) { if (!isParentDir) {
java.util.List<AppConfig.OpenWithEntry> owEntries = persistedConfig != null ? persistedConfig.getOpenWithEntries() : new java.util.ArrayList<>(); java.util.List<AppConfig.OpenWithEntry> owEntries = persistedConfig != null ? persistedConfig.getOpenWithEntries() : new java.util.ArrayList<>();
String itemType; String itemType;
String ext = "";
if (isDir) { if (isDir) {
itemType = "directory"; itemType = "directory";
} else { } else {
String name = item.getFile().getName().toLowerCase(); String name = item.getFile().getName().toLowerCase();
int dot = name.lastIndexOf('.'); int dot = name.lastIndexOf('.');
itemType = (dot > 0 && dot < name.length() - 1) ? name.substring(dot + 1) : ""; itemType = (dot > 0 && dot < name.length() - 1) ? name.substring(dot + 1) : "";
ext = "." + itemType;
} }
java.util.List<AppConfig.OpenWithEntry> filtered = new java.util.ArrayList<>(); java.util.List<AppConfig.OpenWithEntry> filtered = new java.util.ArrayList<>();
@ -2140,13 +2522,41 @@ public class FilePanelTab extends JPanel {
} }
} }
if (!filtered.isEmpty()) { // Get system file associations
java.util.Map<String, String> systemApps = !isDir && !ext.isEmpty() ? getSystemAssociations(ext) : new java.util.HashMap<>();
if (!filtered.isEmpty() || !systemApps.isEmpty()) {
JMenu openWithMenu = new JMenu("Open with"); JMenu openWithMenu = new JMenu("Open with");
// Add configured "Open with" entries
for (AppConfig.OpenWithEntry e : filtered) { for (AppConfig.OpenWithEntry e : filtered) {
JMenuItem owItem = new JMenuItem(e.label); JMenuItem owItem = new JMenuItem(e.label);
owItem.addActionListener(ae -> runExternalCommand(e.command, item.getFile())); owItem.addActionListener(ae -> runExternalCommand(e.command, item.getFile()));
openWithMenu.add(owItem); openWithMenu.add(owItem);
} }
// Add separator if we have both custom and system entries
if (!filtered.isEmpty() && !systemApps.isEmpty()) {
openWithMenu.addSeparator();
}
// Add system associations
for (java.util.Map.Entry<String, String> entry : systemApps.entrySet()) {
JMenuItem appItem = new JMenuItem(entry.getKey());
String appPath = entry.getValue();
appItem.addActionListener(ae -> {
try {
java.util.List<String> cmd = new java.util.ArrayList<>();
cmd.add(appPath);
cmd.add(item.getFile().getAbsolutePath());
new ProcessBuilder(cmd).directory(item.getFile().getParentFile()).start();
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot open file: " + ex.getMessage()); } catch (Exception ignore) {}
}
});
openWithMenu.add(appItem);
}
menu.add(openWithMenu); menu.add(openWithMenu);
} else { } else {
JMenuItem openWithMenuItem = new JMenuItem("Open with"); JMenuItem openWithMenuItem = new JMenuItem("Open with");