From 2114d2d45311f91b3c52d6a16d31509ec2567708 Mon Sep 17 00:00:00 2001 From: rdavidek Date: Sun, 19 Apr 2026 14:51:00 +0200 Subject: [PATCH] fixed open with --- src/main/java/cz/kamma/kfmanager/MainApp.java | 2 +- .../cz/kamma/kfmanager/ui/FilePanelTab.java | 436 +++++++++++++++++- 2 files changed, 424 insertions(+), 14 deletions(-) diff --git a/src/main/java/cz/kamma/kfmanager/MainApp.java b/src/main/java/cz/kamma/kfmanager/MainApp.java index bc79a46..3dec484 100644 --- a/src/main/java/cz/kamma/kfmanager/MainApp.java +++ b/src/main/java/cz/kamma/kfmanager/MainApp.java @@ -15,7 +15,7 @@ import java.io.InputStreamReader; */ public class MainApp { - public static final String APP_VERSION = "1.3.1"; + public static final String APP_VERSION = "1.3.2"; public enum OS { WINDOWS, LINUX, MACOS, UNKNOWN diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index 8320aea..2771c06 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -21,6 +21,9 @@ import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.File; import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.FileReader; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -1932,19 +1935,50 @@ public class FilePanelTab extends JPanel { } } - // 2. If executable, start it directly - if (file.canExecute() && !file.isDirectory()) { - new ProcessBuilder(file.getAbsolutePath()) - .directory(file.getParentFile()) - .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(); - }); + // 2. If it's a directory, navigate to it + if (file.isDirectory()) { + loadDirectory(file); + return; } + + // 3. Try to find and use system file association + String associatedProgram = findAssociatedProgram(file); + if (associatedProgram != null && !associatedProgram.isEmpty()) { + try { + java.util.List 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) { try { 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 getSystemAssociations(String ext) { + java.util.Map 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 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 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 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 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 * mouse-driven selection is blocked). This mirrors the behavior of @@ -2119,12 +2499,14 @@ public class FilePanelTab extends JPanel { if (!isParentDir) { java.util.List owEntries = persistedConfig != null ? persistedConfig.getOpenWithEntries() : new java.util.ArrayList<>(); String itemType; + String ext = ""; if (isDir) { itemType = "directory"; } else { String name = item.getFile().getName().toLowerCase(); int dot = name.lastIndexOf('.'); itemType = (dot > 0 && dot < name.length() - 1) ? name.substring(dot + 1) : ""; + ext = "." + itemType; } java.util.List filtered = new java.util.ArrayList<>(); @@ -2140,13 +2522,41 @@ public class FilePanelTab extends JPanel { } } - if (!filtered.isEmpty()) { + // Get system file associations + java.util.Map systemApps = !isDir && !ext.isEmpty() ? getSystemAssociations(ext) : new java.util.HashMap<>(); + + if (!filtered.isEmpty() || !systemApps.isEmpty()) { JMenu openWithMenu = new JMenu("Open with"); + + // Add configured "Open with" entries for (AppConfig.OpenWithEntry e : filtered) { JMenuItem owItem = new JMenuItem(e.label); owItem.addActionListener(ae -> runExternalCommand(e.command, item.getFile())); 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 entry : systemApps.entrySet()) { + JMenuItem appItem = new JMenuItem(entry.getKey()); + String appPath = entry.getValue(); + appItem.addActionListener(ae -> { + try { + java.util.List 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); } else { JMenuItem openWithMenuItem = new JMenuItem("Open with");