fixed open with
This commit is contained in:
parent
3b85234e55
commit
2114d2d453
@ -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
|
||||
|
||||
@ -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()) {
|
||||
// 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<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();
|
||||
} 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();
|
||||
});
|
||||
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<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
|
||||
* mouse-driven selection is blocked). This mirrors the behavior of
|
||||
@ -2119,12 +2499,14 @@ public class FilePanelTab extends JPanel {
|
||||
if (!isParentDir) {
|
||||
java.util.List<AppConfig.OpenWithEntry> 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<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");
|
||||
|
||||
// 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<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);
|
||||
} else {
|
||||
JMenuItem openWithMenuItem = new JMenuItem("Open with");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user