diff --git a/src/main/java/cz/kamma/kfmanager/model/FileItem.java b/src/main/java/cz/kamma/kfmanager/model/FileItem.java
index 8bab761..a67376a 100644
--- a/src/main/java/cz/kamma/kfmanager/model/FileItem.java
+++ b/src/main/java/cz/kamma/kfmanager/model/FileItem.java
@@ -18,57 +18,63 @@ public class FileItem {
private final boolean isDirectory;
private final Icon icon;
private boolean marked;
-
+ private String displayPath;
+
public FileItem(File file) {
+ this(file, null);
+ }
+
+ public FileItem(File file, String displayPath) {
this.file = file;
this.name = file.getName();
this.size = file.length();
this.modified = new Date(file.lastModified());
this.isDirectory = file.isDirectory();
this.marked = false;
-
+ this.displayPath = displayPath;
+
// Load icon from system
this.icon = FileSystemView.getFileSystemView().getSystemIcon(file);
}
-
+
public File getFile() {
return file;
}
-
+
public String getName() {
return name;
}
-
+
public String getFormattedSize() {
if (isDirectory) {
return "
";
}
return formatSize(size);
}
-
+
public long getSize() {
return size;
}
-
+
public String getFormattedDate() {
SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm");
return sdf.format(modified);
}
-
+
public Date getModified() {
return modified;
}
-
+
public boolean isDirectory() {
return isDirectory;
}
-
+
public Icon getIcon() {
return icon;
}
-
+
public String getPath() {
- return file.getAbsolutePath();
+ return displayPath != null ? displayPath : file.getAbsolutePath();
}
public boolean isMarked() {
diff --git a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java
index fa35ca9..3cd29cd 100644
--- a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java
+++ b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java
@@ -238,7 +238,7 @@ public class FileOperations {
/**
* Search files by pattern
*/
- public static void search(File directory, String pattern, boolean recursive, SearchCallback callback) throws IOException {
+ public static void search(File directory, String pattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
if (pattern == null) return;
// Prepare a compiled regex if the pattern contains wildcards to avoid recompiling per-file
Pattern filenameRegex = null;
@@ -249,29 +249,30 @@ public class FileOperations {
.replace("?", ".");
filenameRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
}
- searchRecursive(directory.toPath(), pattern.toLowerCase(), filenameRegex, recursive, callback);
+ searchRecursive(directory.toPath(), pattern.toLowerCase(), filenameRegex, recursive, searchArchives, callback);
}
/**
* Search file contents for a text fragment (case-insensitive).
* Calls callback.onFileFound(file) when a file contains the text.
*/
- public static void searchContents(File directory, String text, boolean recursive, SearchCallback callback) throws IOException {
+ public static void searchContents(File directory, String text, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
if (text == null) return;
// Precompile a case-insensitive pattern for content search to avoid per-line lowercasing
Pattern contentPattern = Pattern.compile(Pattern.quote(text), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
- searchContentsRecursive(directory.toPath(), contentPattern, recursive, callback);
+ searchContentsRecursive(directory.toPath(), contentPattern, recursive, searchArchives, callback);
}
- private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, boolean recursive, SearchCallback callback) throws IOException {
+ private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
try (DirectoryStream stream = Files.newDirectoryStream(directory)) {
for (Path entry : stream) {
if (Files.isDirectory(entry)) {
if (recursive) {
- searchRecursive(entry, patternLower, filenameRegex, recursive, callback);
+ searchRecursive(entry, patternLower, filenameRegex, recursive, searchArchives, callback);
}
} else {
- String fileName = entry.getFileName().toString();
+ File file = entry.toFile();
+ String fileName = file.getName();
String fileNameLower = fileName.toLowerCase();
boolean matched = false;
if (fileNameLower.contains(patternLower)) matched = true;
@@ -280,7 +281,12 @@ public class FileOperations {
if (m.matches()) matched = true;
}
if (matched) {
- callback.onFileFound(entry.toFile());
+ callback.onFileFound(file, null);
+ }
+
+ // SEARCH IN ARCHIVES
+ if (searchArchives && isArchiveFile(file)) {
+ searchInArchive(file, fileNameLower, filenameRegex, callback);
}
}
}
@@ -289,14 +295,15 @@ public class FileOperations {
}
}
- private static void searchContentsRecursive(Path directory, Pattern contentPattern, boolean recursive, SearchCallback callback) throws IOException {
+ private static void searchContentsRecursive(Path directory, Pattern contentPattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
try (DirectoryStream stream = Files.newDirectoryStream(directory)) {
for (Path entry : stream) {
if (Files.isDirectory(entry)) {
if (recursive) {
- searchContentsRecursive(entry, contentPattern, recursive, callback);
+ searchContentsRecursive(entry, contentPattern, recursive, searchArchives, callback);
}
} else {
+ File file = entry.toFile();
// Try reading file as text line-by-line and search for pattern (case-insensitive via compiled Pattern)
try (BufferedReader br = Files.newBufferedReader(entry)) {
String line;
@@ -307,16 +314,160 @@ public class FileOperations {
break;
}
}
- if (found) callback.onFileFound(entry.toFile());
+ if (found) callback.onFileFound(file, null);
} catch (IOException ex) {
// Skip files that cannot be read as text
}
+
+ // SEARCH IN ARCHIVES CONTENTS
+ if (searchArchives && isArchiveFile(file)) {
+ searchContentsInArchive(file, contentPattern, callback);
+ }
}
}
} catch (AccessDeniedException e) {
// Ignore directories without access
}
}
+
+ private static boolean isArchiveFile(File f) {
+ if (f == null) return false;
+ String n = f.getName().toLowerCase();
+ return n.endsWith(".zip") || n.endsWith(".jar") || n.endsWith(".tar") || n.endsWith(".tar.gz") || n.endsWith(".tgz") || n.endsWith(".7z") || n.endsWith(".rar");
+ }
+
+ private static void searchInArchive(File archive, String patternLower, Pattern filenameRegex, SearchCallback callback) {
+ String name = archive.getName().toLowerCase();
+ try {
+ if (name.endsWith(".zip") || name.endsWith(".jar")) {
+ try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
+ ZipEntry entry;
+ while ((entry = zis.getNextEntry()) != null) {
+ if (matchEntry(entry.getName(), patternLower, filenameRegex)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
+ }
+ }
+ }
+ } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
+ InputStream is = new FileInputStream(archive);
+ if (name.endsWith(".gz") || name.endsWith(".tgz")) {
+ is = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(is);
+ }
+ try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) {
+ org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
+ while ((entry = tais.getNextTarEntry()) != null) {
+ if (matchEntry(entry.getName(), patternLower, filenameRegex)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
+ }
+ }
+ }
+ } else if (name.endsWith(".7z")) {
+ try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
+ org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
+ while ((entry = sevenZFile.getNextEntry()) != null) {
+ if (matchEntry(entry.getName(), patternLower, filenameRegex)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
+ }
+ }
+ }
+ } else if (name.endsWith(".rar")) {
+ try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
+ for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
+ if (matchEntry(fh.getFileName(), patternLower, filenameRegex)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + fh.getFileName());
+ }
+ }
+ }
+ }
+ } catch (Exception ignore) {}
+ }
+
+ private static void searchContentsInArchive(File archive, Pattern contentPattern, SearchCallback callback) {
+ String name = archive.getName().toLowerCase();
+ try {
+ if (name.endsWith(".zip") || name.endsWith(".jar")) {
+ try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
+ ZipEntry entry;
+ while ((entry = zis.getNextEntry()) != null) {
+ if (!entry.isDirectory() && searchInStream(zis, contentPattern)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
+ }
+ }
+ }
+ } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
+ InputStream is = new FileInputStream(archive);
+ if (name.endsWith(".gz") || name.endsWith(".tgz")) {
+ is = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(is);
+ }
+ try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) {
+ org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
+ while ((entry = tais.getNextTarEntry()) != null) {
+ if (!entry.isDirectory() && searchInStream(tais, contentPattern)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
+ }
+ }
+ }
+ } else if (name.endsWith(".7z")) {
+ try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
+ org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
+ while ((entry = sevenZFile.getNextEntry()) != null) {
+ if (!entry.isDirectory()) {
+ // SevenZFile.read(buffer) reads from current entry
+ if (searchInStream(new InputStream() {
+ @Override
+ public int read() throws IOException {
+ return sevenZFile.read();
+ }
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return sevenZFile.read(b, off, len);
+ }
+ }, contentPattern)) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
+ }
+ }
+ }
+ }
+ } else if (name.endsWith(".rar")) {
+ try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
+ for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
+ if (!fh.isDirectory()) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ rar.extractFile(fh, baos);
+ if (contentPattern.matcher(new String(baos.toByteArray())).find()) {
+ callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + fh.getFileName());
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception ignore) {}
+ }
+
+ private static boolean matchEntry(String entryName, String patternLower, Pattern filenameRegex) {
+ if (entryName == null) return false;
+ String nameShort = entryName;
+ int lastSlash = entryName.lastIndexOf('/');
+ if (lastSlash != -1) nameShort = entryName.substring(lastSlash + 1);
+
+ String nameLower = nameShort.toLowerCase();
+ if (nameLower.contains(patternLower)) return true;
+ if (filenameRegex != null && filenameRegex.matcher(nameShort).matches()) return true;
+ return false;
+ }
+
+ private static boolean searchInStream(InputStream is, Pattern contentPattern) {
+ try {
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (contentPattern.matcher(line).find()) {
+ return true;
+ }
+ }
+ } catch (Exception ignore) {}
+ return false;
+ }
/**
* Zip files/directories into a target zip file
@@ -431,6 +582,6 @@ public class FileOperations {
* Callback for search
*/
public interface SearchCallback {
- void onFileFound(File file);
+ void onFileFound(File file, String virtualPath);
}
}
diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java
index d820857..f470684 100644
--- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java
+++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java
@@ -283,17 +283,44 @@ public class MainWindow extends JFrame {
JTextField tf = (JTextField) editorComp;
tf.setFocusTraversalKeysEnabled(false);
tf.addActionListener(e -> executeCommand(tf.getText()));
+
+ // Enable standard clipboard operations (Cut, Copy, Paste) even if not focused initially
+ tf.getComponentPopupMenu(); // Ensure it has a menu or at least default actions works
+
+ // Context menu with Clipboard operations
+ JPopupMenu clipboardMenu = new JPopupMenu();
+ Action cutAction = new javax.swing.text.DefaultEditorKit.CutAction();
+ cutAction.putValue(Action.NAME, "Cut");
+ clipboardMenu.add(cutAction);
+
+ Action copyAction = new javax.swing.text.DefaultEditorKit.CopyAction();
+ copyAction.putValue(Action.NAME, "Copy");
+ clipboardMenu.add(copyAction);
+
+ Action pasteAction = new javax.swing.text.DefaultEditorKit.PasteAction();
+ pasteAction.putValue(Action.NAME, "Paste");
+ clipboardMenu.add(pasteAction);
+
+ clipboardMenu.addSeparator();
+
+ Action selectAllAction = new AbstractAction("Select All") {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ tf.selectAll();
+ }
+ };
+ clipboardMenu.add(selectAllAction);
+
+ tf.setComponentPopupMenu(clipboardMenu);
// Let the panels catch focus back if user presses ESC or TAB in command line
tf.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
- if (!tf.getText().isEmpty()) {
- tf.setText("");
- if (activePanel != null && activePanel.getFileTable() != null) {
- activePanel.getFileTable().requestFocusInWindow();
- }
+ tf.setText("");
+ if (activePanel != null && activePanel.getFileTable() != null) {
+ activePanel.getFileTable().requestFocusInWindow();
}
e.consume();
} else if (e.getKeyCode() == KeyEvent.VK_TAB) {
@@ -715,6 +742,24 @@ public class MainWindow extends JFrame {
updateComponentBackground(getContentPane(), bg);
if (leftPanel != null) leftPanel.applyBackgroundColor(bg);
if (rightPanel != null) rightPanel.applyBackgroundColor(bg);
+
+ // Update command line colors
+ if (commandLine != null) {
+ Component ed = commandLine.getEditor().getEditorComponent();
+ if (ed instanceof JTextField) {
+ JTextField tf = (JTextField) ed;
+ tf.setBackground(bg);
+ boolean dark = isDark(bg);
+ tf.setForeground(dark ? Color.WHITE : Color.BLACK);
+ Color selColor = config.getSelectionColor();
+ if (selColor != null) {
+ tf.setSelectionColor(selColor);
+ tf.setCaretColor(selColor);
+ } else {
+ tf.setCaretColor(dark ? Color.WHITE : Color.BLACK);
+ }
+ }
+ }
});
}
@@ -722,6 +767,18 @@ public class MainWindow extends JFrame {
if (sel != null) {
if (leftPanel != null) leftPanel.applySelectionColor(sel);
if (rightPanel != null) rightPanel.applySelectionColor(sel);
+
+ // Apply selection color to command line editor for cursor and selection
+ if (commandLine != null) {
+ Component ed = commandLine.getEditor().getEditorComponent();
+ if (ed instanceof JTextField) {
+ JTextField tf = (JTextField) ed;
+ tf.setSelectionColor(sel);
+ tf.setCaretColor(sel);
+ tf.setSelectedTextColor(Color.WHITE); // Ensure selected text is readable on selection bg
+ }
+ }
+
// Ensure the active panel border uses the updated configuration color immediately
SwingUtilities.invokeLater(() -> updateActivePanelBorder());
}
@@ -750,6 +807,8 @@ public class MainWindow extends JFrame {
if (container == null) return;
container.setBackground(bg);
boolean dark = isDark(bg);
+ Color selColor = config != null ? config.getSelectionColor() : null;
+
for (Component c : container.getComponents()) {
if (c instanceof JPanel || c instanceof JToolBar || c instanceof JScrollPane || c instanceof JViewport || c instanceof JTabbedPane || c instanceof JButton) {
c.setBackground(bg);
@@ -757,6 +816,17 @@ public class MainWindow extends JFrame {
if (c instanceof JLabel || c instanceof JCheckBox || c instanceof JRadioButton || c instanceof JButton) {
c.setForeground(dark ? Color.WHITE : Color.BLACK);
}
+ if (c instanceof javax.swing.text.JTextComponent) {
+ javax.swing.text.JTextComponent tc = (javax.swing.text.JTextComponent) c;
+ tc.setBackground(bg);
+ tc.setForeground(dark ? Color.WHITE : Color.BLACK);
+ if (selColor != null) {
+ tc.setSelectionColor(selColor);
+ tc.setCaretColor(selColor);
+ } else {
+ tc.setCaretColor(dark ? Color.WHITE : Color.BLACK);
+ }
+ }
if (c instanceof Container) {
updateComponentBackground((Container) c, bg);
}
diff --git a/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java b/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java
index a0dcd38..df4f920 100644
--- a/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java
+++ b/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java
@@ -22,6 +22,7 @@ public class SearchDialog extends JDialog {
private JComboBox contentPatternCombo;
private JCheckBox recursiveCheckBox;
private JCheckBox contentSearchCheckBox;
+ private JCheckBox archiveSearchCheckBox;
private JTable resultsTable;
private ResultsTableModel tableModel;
private JButton searchButton;
@@ -112,13 +113,17 @@ public class SearchDialog extends JDialog {
contentPatternCombo.setToolTipText("Text to search inside files");
searchPanel.add(contentPatternCombo, gbc);
- gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 2;
contentSearchCheckBox = new JCheckBox("Search inside file contents", false);
searchPanel.add(contentSearchCheckBox, gbc);
gbc.gridy = 4;
+ archiveSearchCheckBox = new JCheckBox("Search inside archives", false);
+ archiveSearchCheckBox.setMnemonic(KeyEvent.VK_R);
+ searchPanel.add(archiveSearchCheckBox, gbc);
+
+ gbc.gridy = 5;
JLabel pathLabel = new JLabel("Directory: " + searchDirectory.getAbsolutePath());
pathLabel.setFont(pathLabel.getFont().deriveFont(Font.ITALIC));
searchPanel.add(pathLabel, gbc);
@@ -296,6 +301,18 @@ public class SearchDialog extends JDialog {
}
});
+ // Alt+R toggles archive search
+ getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
+ KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.ALT_DOWN_MASK), "toggleArchiveSearch");
+ getRootPane().getActionMap().put("toggleArchiveSearch", new AbstractAction() {
+ @Override
+ public void actionPerformed(java.awt.event.ActionEvent e) {
+ if (archiveSearchCheckBox != null) {
+ archiveSearchCheckBox.setSelected(!archiveSearchCheckBox.isSelected());
+ }
+ }
+ });
+
// Escape closes the dialog (and cancels ongoing search)
getRootPane().registerKeyboardAction(e -> {
searching = false;
@@ -433,6 +450,7 @@ public class SearchDialog extends JDialog {
}
final String pattern = isContentSearch ? contentPat : namePat;
+ final boolean searchArchives = archiveSearchCheckBox != null && archiveSearchCheckBox.isSelected();
// Reset and show status
foundCount = 0;
@@ -441,27 +459,29 @@ public class SearchDialog extends JDialog {
statusProgressBar.setIndeterminate(true);
// Spustit vyhledávání v samostatném vlákně
- SwingWorker worker = new SwingWorker() {
+ SwingWorker worker = new SwingWorker() {
@Override
protected Void doInBackground() throws Exception {
if (isContentSearch) {
- FileOperations.searchContents(searchDirectory, pattern, recursiveCheckBox.isSelected(), file -> {
+ FileOperations.searchContents(searchDirectory, pattern, recursiveCheckBox.isSelected(), searchArchives, (file, virtualPath) -> {
if (!searching) return;
- publish(file);
+ publish(new Object[]{file, virtualPath});
});
} else {
- FileOperations.search(searchDirectory, pattern, recursiveCheckBox.isSelected(), file -> {
+ FileOperations.search(searchDirectory, pattern, recursiveCheckBox.isSelected(), searchArchives, (file, virtualPath) -> {
if (!searching) return;
- publish(file);
+ publish(new Object[]{file, virtualPath});
});
}
return null;
}
@Override
- protected void process(List chunks) {
- for (File file : chunks) {
- tableModel.addResult(new FileItem(file));
+ protected void process(List