From 7b07d98dabcbca0b9ec0fcbb1ea0f9130c7e6131 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Sun, 16 Nov 2025 20:08:45 +0100 Subject: [PATCH] first commit --- .gitignore | 25 + README.md | 47 + pom.xml | 47 + src/main/java/com/kfmanager/MainApp.java | 26 + .../java/com/kfmanager/config/AppConfig.java | 337 ++++ .../java/com/kfmanager/model/FileItem.java | 100 ++ .../com/kfmanager/service/FileOperations.java | 205 +++ .../java/com/kfmanager/ui/DriveSelector.java | 168 ++ .../java/com/kfmanager/ui/FileEditor.java | 700 ++++++++ src/main/java/com/kfmanager/ui/FilePanel.java | 528 ++++++ .../java/com/kfmanager/ui/FilePanel.java.bak | 1043 ++++++++++++ .../java/com/kfmanager/ui/FilePanelTab.java | 1441 +++++++++++++++++ .../com/kfmanager/ui/FontChooserDialog.java | 192 +++ .../java/com/kfmanager/ui/MainWindow.java | 901 +++++++++++ .../java/com/kfmanager/ui/SearchDialog.java | 246 +++ .../java/com/kfmanager/ui/SettingsDialog.java | 173 ++ src/main/java/com/kfmanager/ui/ViewMode.java | 9 + 17 files changed, 6188 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/kfmanager/MainApp.java create mode 100644 src/main/java/com/kfmanager/config/AppConfig.java create mode 100644 src/main/java/com/kfmanager/model/FileItem.java create mode 100644 src/main/java/com/kfmanager/service/FileOperations.java create mode 100644 src/main/java/com/kfmanager/ui/DriveSelector.java create mode 100644 src/main/java/com/kfmanager/ui/FileEditor.java create mode 100644 src/main/java/com/kfmanager/ui/FilePanel.java create mode 100644 src/main/java/com/kfmanager/ui/FilePanel.java.bak create mode 100644 src/main/java/com/kfmanager/ui/FilePanelTab.java create mode 100644 src/main/java/com/kfmanager/ui/FontChooserDialog.java create mode 100644 src/main/java/com/kfmanager/ui/MainWindow.java create mode 100644 src/main/java/com/kfmanager/ui/SearchDialog.java create mode 100644 src/main/java/com/kfmanager/ui/SettingsDialog.java create mode 100644 src/main/java/com/kfmanager/ui/ViewMode.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb54cae --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +.classpath +.project +.settings/ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8de1ac --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# KF File Manager + +Dvoupanelový souborový manažer podobný Total Commander, vytvořený v Java 11. + +## Funkce + +- **Two panels** for browsing files and directories +- **Copying** files and directories (F5) +- **Moving** files and directories (F6) +- **Create directory** (F7) +- **Delete** files and directories (F8) +- **Přejmenování** (Shift+F6) +- **Vyhledávání** souborů (Ctrl+F) +- **Přepínání** mezi panely (TAB) +- **Navigation** - double-click or Enter to open a directory +- **Zobrazení** velikosti souborů, data modifikace + +## Spuštění + +```bash +mvn clean compile +mvn exec:java -Dexec.mainClass="com.kfmanager.MainApp" +``` + +Nebo vytvoření JAR souboru: + +```bash +mvn clean package +java -jar target/kf-manager-1.0-SNAPSHOT.jar +``` + +## Klávesové zkratky + +- **F5** - Kopírovat +- **F6** - Přesunout +- **Shift+F6** - Přejmenovat +- **F7** - Nový adresář +- **F8** - Smazat +- **TAB** - Přepnout mezi panely +- **Ctrl+F** - Vyhledat soubory +- **Enter** - Otevřít adresář +- **Backspace** - Nadřazený adresář + +## Požadavky + +- Java 11 nebo vyšší +- Maven 3.6+ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8c3b2ae --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.kfmanager + kf-manager + 1.0-SNAPSHOT + jar + + KF File Manager + Dual-panel file manager similar to Total Commander + + + UTF-8 + 11 + 11 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + com.kfmanager.MainApp + + + + + + + diff --git a/src/main/java/com/kfmanager/MainApp.java b/src/main/java/com/kfmanager/MainApp.java new file mode 100644 index 0000000..1cdd1c6 --- /dev/null +++ b/src/main/java/com/kfmanager/MainApp.java @@ -0,0 +1,26 @@ +package com.kfmanager; + +import com.kfmanager.ui.MainWindow; + +import javax.swing.*; + +/** + * Hlavní třída aplikace KF File Manager + */ +public class MainApp { + + public static void main(String[] args) { + // Nastavení look and feel podle systému + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + + // Spuštění GUI v Event Dispatch Thread + SwingUtilities.invokeLater(() -> { + MainWindow mainWindow = new MainWindow(); + mainWindow.setVisible(true); + }); + } +} diff --git a/src/main/java/com/kfmanager/config/AppConfig.java b/src/main/java/com/kfmanager/config/AppConfig.java new file mode 100644 index 0000000..10fcac8 --- /dev/null +++ b/src/main/java/com/kfmanager/config/AppConfig.java @@ -0,0 +1,337 @@ +package com.kfmanager.config; + +import java.awt.*; +import java.io.*; +import java.util.Properties; + +/** + * Class for saving and loading the application configuration + */ +public class AppConfig { + + private static final String CONFIG_FILE = System.getProperty("user.home") + + File.separator + ".kfmanager" + File.separator + "config.properties"; + + private Properties properties; + + public AppConfig() { + properties = new Properties(); + loadConfig(); + } + + /** + * Load configuration from file + */ + private void loadConfig() { + File configFile = new File(CONFIG_FILE); + if (configFile.exists()) { + try (FileInputStream fis = new FileInputStream(configFile)) { + properties.load(fis); + } catch (IOException e) { + System.err.println("Nepodařilo se načíst konfiguraci: " + e.getMessage()); + } + } + } + + /** + * Save configuration to file + */ + public void saveConfig() { + File configFile = new File(CONFIG_FILE); + File configDir = configFile.getParentFile(); + + // Create configuration directory if it does not exist + if (!configDir.exists()) { + configDir.mkdirs(); + } + + try (FileOutputStream fos = new FileOutputStream(configFile)) { + properties.store(fos, "KF File Manager Configuration"); + } catch (IOException e) { + System.err.println("Nepodařilo se uložit konfiguraci: " + e.getMessage()); + } + } + + // Getters and setters for individual configuration values + + public int getWindowX() { + return Integer.parseInt(properties.getProperty("window.x", "100")); + } + + public void setWindowX(int x) { + properties.setProperty("window.x", String.valueOf(x)); + } + + public int getWindowY() { + return Integer.parseInt(properties.getProperty("window.y", "100")); + } + + public void setWindowY(int y) { + properties.setProperty("window.y", String.valueOf(y)); + } + + public int getWindowWidth() { + return Integer.parseInt(properties.getProperty("window.width", "1200")); + } + + public void setWindowWidth(int width) { + properties.setProperty("window.width", String.valueOf(width)); + } + + public int getWindowHeight() { + return Integer.parseInt(properties.getProperty("window.height", "700")); + } + + public void setWindowHeight(int height) { + properties.setProperty("window.height", String.valueOf(height)); + } + + public boolean isWindowMaximized() { + return Boolean.parseBoolean(properties.getProperty("window.maximized", "false")); + } + + public void setWindowMaximized(boolean maximized) { + properties.setProperty("window.maximized", String.valueOf(maximized)); + } + + public String getLeftPanelPath() { + return properties.getProperty("leftPanel.path", System.getProperty("user.home")); + } + + public void setLeftPanelPath(String path) { + properties.setProperty("leftPanel.path", path); + } + + public String getRightPanelPath() { + return properties.getProperty("rightPanel.path", System.getProperty("user.home")); + } + + public void setRightPanelPath(String path) { + properties.setProperty("rightPanel.path", path); + } + + // ViewMode konfigurace + public String getLeftPanelViewMode() { + return properties.getProperty("leftPanel.viewMode", "FULL"); + } + + public void setLeftPanelViewMode(String viewMode) { + properties.setProperty("leftPanel.viewMode", viewMode); + } + + public String getRightPanelViewMode() { + return properties.getProperty("rightPanel.viewMode", "FULL"); + } + + public void setRightPanelViewMode(String viewMode) { + properties.setProperty("rightPanel.viewMode", viewMode); + } + + // --- Tab/session persistence --- + public int getLeftPanelTabCount() { + return Integer.parseInt(properties.getProperty("leftPanel.tabs.count", "0")); + } + + public String getLeftPanelTabPath(int index) { + return properties.getProperty("leftPanel.tab." + index + ".path", null); + } + + public String getLeftPanelTabViewMode(int index) { + return properties.getProperty("leftPanel.tab." + index + ".viewMode", "FULL"); + } + + public int getLeftPanelSelectedIndex() { + return Integer.parseInt(properties.getProperty("leftPanel.selectedIndex", "0")); + } + + public void saveLeftPanelTabs(java.util.List paths, java.util.List viewModes, int selectedIndex) { + properties.setProperty("leftPanel.tabs.count", String.valueOf(paths.size())); + for (int i = 0; i < paths.size(); i++) { + properties.setProperty("leftPanel.tab." + i + ".path", paths.get(i)); + properties.setProperty("leftPanel.tab." + i + ".viewMode", viewModes.get(i)); + } + properties.setProperty("leftPanel.selectedIndex", String.valueOf(selectedIndex)); + } + + public int getRightPanelTabCount() { + return Integer.parseInt(properties.getProperty("rightPanel.tabs.count", "0")); + } + + public String getRightPanelTabPath(int index) { + return properties.getProperty("rightPanel.tab." + index + ".path", null); + } + + public String getRightPanelTabViewMode(int index) { + return properties.getProperty("rightPanel.tab." + index + ".viewMode", "FULL"); + } + + public int getRightPanelSelectedIndex() { + return Integer.parseInt(properties.getProperty("rightPanel.selectedIndex", "0")); + } + + public void saveRightPanelTabs(java.util.List paths, java.util.List viewModes, int selectedIndex) { + properties.setProperty("rightPanel.tabs.count", String.valueOf(paths.size())); + for (int i = 0; i < paths.size(); i++) { + properties.setProperty("rightPanel.tab." + i + ".path", paths.get(i)); + properties.setProperty("rightPanel.tab." + i + ".viewMode", viewModes.get(i)); + } + properties.setProperty("rightPanel.selectedIndex", String.valueOf(selectedIndex)); + } + + // Font konfigurace + public String getEditorFontName() { + return properties.getProperty("editor.font.name", "Monospaced"); + } + + public void setEditorFontName(String fontName) { + properties.setProperty("editor.font.name", fontName); + } + + public int getEditorFontSize() { + return Integer.parseInt(properties.getProperty("editor.font.size", "12")); + } + + public void setEditorFontSize(int fontSize) { + properties.setProperty("editor.font.size", String.valueOf(fontSize)); + } + + public int getEditorFontStyle() { + return Integer.parseInt(properties.getProperty("editor.font.style", String.valueOf(Font.PLAIN))); + } + + public void setEditorFontStyle(int style) { + properties.setProperty("editor.font.style", String.valueOf(style)); + } + + public Font getEditorFont() { + return new Font(getEditorFontName(), getEditorFontStyle(), getEditorFontSize()); + } + + public void setEditorFont(Font font) { + setEditorFontName(font.getName()); + setEditorFontSize(font.getSize()); + setEditorFontStyle(font.getStyle()); + } + + // --- Appearance (global) settings --- + public String getGlobalFontName() { + return properties.getProperty("global.font.name", "Monospaced"); + } + + public void setGlobalFontName(String name) { + properties.setProperty("global.font.name", name); + } + + public int getGlobalFontSize() { + return Integer.parseInt(properties.getProperty("global.font.size", "12")); + } + + public void setGlobalFontSize(int size) { + properties.setProperty("global.font.size", String.valueOf(size)); + } + + public Font getGlobalFont() { + return new Font(getGlobalFontName(), getGlobalFontStyle(), getGlobalFontSize()); + } + + public void setGlobalFont(Font font) { + setGlobalFontName(font.getName()); + setGlobalFontSize(font.getSize()); + setGlobalFontStyle(font.getStyle()); + } + + public int getGlobalFontStyle() { + return Integer.parseInt(properties.getProperty("global.font.style", String.valueOf(Font.PLAIN))); + } + + public void setGlobalFontStyle(int style) { + properties.setProperty("global.font.style", String.valueOf(style)); + } + + // Colors stored as hex strings (e.g. #RRGGBB) + public Color getBackgroundColor() { + String v = properties.getProperty("appearance.bg", null); + if (v == null) return null; + try { return Color.decode(v); } catch (Exception ex) { return null; } + } + + public void setBackgroundColor(Color c) { + if (c == null) { + properties.remove("appearance.bg"); + } else { + properties.setProperty("appearance.bg", String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue())); + } + } + + public Color getSelectionColor() { + String v = properties.getProperty("appearance.selection", null); + if (v == null) return null; + try { return Color.decode(v); } catch (Exception ex) { return null; } + } + + public void setSelectionColor(Color c) { + if (c == null) { + properties.remove("appearance.selection"); + } else { + properties.setProperty("appearance.selection", String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue())); + } + } + + public Color getMarkedColor() { + String v = properties.getProperty("appearance.marked", null); + if (v == null) return null; + try { return Color.decode(v); } catch (Exception ex) { return null; } + } + + public void setMarkedColor(Color c) { + if (c == null) { + properties.remove("appearance.marked"); + } else { + properties.setProperty("appearance.marked", String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue())); + } + } + + // -- Sorting persistence (global default) + public int getDefaultSortColumn() { + return Integer.parseInt(properties.getProperty("global.sort.column", "-1")); + } + + public void setDefaultSortColumn(int col) { + properties.setProperty("global.sort.column", String.valueOf(col)); + } + + public boolean getDefaultSortAscending() { + return Boolean.parseBoolean(properties.getProperty("global.sort.ascending", "true")); + } + + public void setDefaultSortAscending(boolean asc) { + properties.setProperty("global.sort.ascending", String.valueOf(asc)); + } + + /** + * Save window state + */ + public void saveWindowState(Frame frame) { + if (frame.getExtendedState() == Frame.MAXIMIZED_BOTH) { + setWindowMaximized(true); + } else { + setWindowMaximized(false); + setWindowX(frame.getX()); + setWindowY(frame.getY()); + setWindowWidth(frame.getWidth()); + setWindowHeight(frame.getHeight()); + } + } + + /** + * Restore window state + */ + public void restoreWindowState(Frame frame) { + frame.setLocation(getWindowX(), getWindowY()); + frame.setSize(getWindowWidth(), getWindowHeight()); + + if (isWindowMaximized()) { + frame.setExtendedState(Frame.MAXIMIZED_BOTH); + } + } +} diff --git a/src/main/java/com/kfmanager/model/FileItem.java b/src/main/java/com/kfmanager/model/FileItem.java new file mode 100644 index 0000000..545aeb2 --- /dev/null +++ b/src/main/java/com/kfmanager/model/FileItem.java @@ -0,0 +1,100 @@ +package com.kfmanager.model; + +import javax.swing.Icon; +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Model representing a file or directory for display in the table + */ +public class FileItem { + + private final File file; + private final String name; + private final long size; + private final Date modified; + private final boolean isDirectory; + private final Icon icon; + private boolean marked; + + public FileItem(File file) { + this.file = file; + this.name = file.getName(); + this.size = file.length(); + this.modified = new Date(file.lastModified()); + this.isDirectory = file.isDirectory(); + this.marked = false; + + // Načíst ikonu ze systému + 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(); + } + + public boolean isMarked() { + return marked; + } + + public void setMarked(boolean marked) { + this.marked = marked; + } + + public void toggleMarked() { + this.marked = !this.marked; + } + + /** + * Format file size into a human-readable string + */ + private String formatSize(long size) { + if (size < 1024) { + return size + " B"; + } else if (size < 1024 * 1024) { + return String.format("%.1f KB", size / 1024.0); + } else if (size < 1024 * 1024 * 1024) { + return String.format("%.1f MB", size / (1024.0 * 1024.0)); + } else { + return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); + } + } +} diff --git a/src/main/java/com/kfmanager/service/FileOperations.java b/src/main/java/com/kfmanager/service/FileOperations.java new file mode 100644 index 0000000..f62cd53 --- /dev/null +++ b/src/main/java/com/kfmanager/service/FileOperations.java @@ -0,0 +1,205 @@ +package com.kfmanager.service; + +import com.kfmanager.model.FileItem; + +import java.io.*; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; + +/** + * Service for file operations - copy, move, delete, etc. + */ +public class FileOperations { + + /** + * Copy files/directories to target directory + */ + public static void copy(List items, File targetDirectory, ProgressCallback callback) throws IOException { + if (targetDirectory == null || !targetDirectory.isDirectory()) { + throw new IOException("Target directory does not exist"); + } + + int current = 0; + int total = items.size(); + + for (FileItem item : items) { + current++; + File source = item.getFile(); + File target = new File(targetDirectory, source.getName()); + + if (callback != null) { + callback.onProgress(current, total, source.getName()); + } + + if (source.isDirectory()) { + copyDirectory(source.toPath(), target.toPath()); + } else { + copyFile(source.toPath(), target.toPath()); + } + } + } + + /** + * Move files/directories to target directory + */ + public static void move(List items, File targetDirectory, ProgressCallback callback) throws IOException { + if (targetDirectory == null || !targetDirectory.isDirectory()) { + throw new IOException("Target directory does not exist"); + } + + int current = 0; + int total = items.size(); + + for (FileItem item : items) { + current++; + File source = item.getFile(); + File target = new File(targetDirectory, source.getName()); + + if (callback != null) { + callback.onProgress(current, total, source.getName()); + } + + Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + + /** + * Delete files/directories + */ + public static void delete(List items, ProgressCallback callback) throws IOException { + int current = 0; + int total = items.size(); + + for (FileItem item : items) { + current++; + File file = item.getFile(); + + if (callback != null) { + callback.onProgress(current, total, file.getName()); + } + + if (file.isDirectory()) { + deleteDirectory(file.toPath()); + } else { + Files.delete(file.toPath()); + } + } + } + + /** + * Rename a file or directory + */ + public static void rename(File file, String newName) throws IOException { + File target = new File(file.getParentFile(), newName); + Files.move(file.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + /** + * Create a new directory + */ + public static void createDirectory(File parentDirectory, String name) throws IOException { + File newDir = new File(parentDirectory, name); + if (!newDir.mkdir()) { + throw new IOException("Failed to create directory"); + } + } + + /** + * Copy a file + */ + private static void copyFile(Path source, Path target) throws IOException { + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + } + + /** + * Copy directory recursively + */ + private static void copyDirectory(Path source, Path target) throws IOException { + Files.walkFileTree(source, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Path targetDir = target.resolve(source.relativize(dir)); + Files.createDirectories(targetDir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path targetFile = target.resolve(source.relativize(file)); + copyFile(file, targetFile); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * Delete directory recursively + */ + private static void deleteDirectory(Path directory) throws IOException { + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * Search files by pattern + */ + public static void search(File directory, String pattern, boolean recursive, SearchCallback callback) throws IOException { + searchRecursive(directory.toPath(), pattern.toLowerCase(), recursive, callback); + } + + private static void searchRecursive(Path directory, String pattern, boolean recursive, SearchCallback callback) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { + for (Path entry : stream) { + if (Files.isDirectory(entry)) { + if (recursive) { + searchRecursive(entry, pattern, recursive, callback); + } + } else { + String fileName = entry.getFileName().toString().toLowerCase(); + if (fileName.contains(pattern) || matchesPattern(fileName, pattern)) { + callback.onFileFound(entry.toFile()); + } + } + } + } catch (AccessDeniedException e) { + // Ignore directories without access + } + } + + /** + * Check whether filename matches the pattern (supports * and ?) + */ + private static boolean matchesPattern(String fileName, String pattern) { + String regex = pattern + .replace(".", "\\.") + .replace("*", ".*") + .replace("?", "."); + return fileName.matches(regex); + } + + /** + * Callback pro progress operací + */ + public interface ProgressCallback { + void onProgress(int current, int total, String currentFile); + } + + /** + * Callback pro vyhledávání + */ + public interface SearchCallback { + void onFileFound(File file); + } +} diff --git a/src/main/java/com/kfmanager/ui/DriveSelector.java b/src/main/java/com/kfmanager/ui/DriveSelector.java new file mode 100644 index 0000000..c588d61 --- /dev/null +++ b/src/main/java/com/kfmanager/ui/DriveSelector.java @@ -0,0 +1,168 @@ +package com.kfmanager.ui; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Dialog for selecting a drive + */ +public class DriveSelector extends JDialog { + + private File selectedDrive = null; + + public DriveSelector(Frame parent) { + super(parent, "Select drive", true); + initComponents(); + setSize(400, 300); + setLocationRelativeTo(parent); + } + + private void initComponents() { + setLayout(new BorderLayout(10, 10)); + ((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Získat seznam dostupných disků + File[] roots = File.listRoots(); + List drives = new ArrayList<>(); + + for (File root : roots) { + drives.add(new DriveInfo(root)); + } + + // Seznam disků + JList driveList = new JList<>(drives.toArray(new DriveInfo[0])); + driveList.setFont(new Font("Monospaced", Font.PLAIN, 14)); + driveList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + // Custom renderer to display drive information + driveList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof DriveInfo) { + DriveInfo info = (DriveInfo) value; + setText(info.getDisplayText()); + } + + return this; + } + }); + + // Double-click pro výběr + driveList.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + if (e.getClickCount() == 2) { + int index = driveList.locationToIndex(e.getPoint()); + if (index >= 0) { + selectedDrive = drives.get(index).getRoot(); + dispose(); + } + } + } + }); + + // Enter pro výběr + driveList.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) { + int index = driveList.getSelectedIndex(); + if (index >= 0) { + selectedDrive = drives.get(index).getRoot(); + dispose(); + } + } + } + }); + + JScrollPane scrollPane = new JScrollPane(driveList); + add(scrollPane, BorderLayout.CENTER); + + // Panel s tlačítky + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + JButton okButton = new JButton("OK"); + okButton.addActionListener(e -> { + int index = driveList.getSelectedIndex(); + if (index >= 0) { + selectedDrive = drives.get(index).getRoot(); + dispose(); + } else { + JOptionPane.showMessageDialog(this, + "Select a drive", + "Drive selection", + JOptionPane.INFORMATION_MESSAGE); + } + }); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> dispose()); + + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + + add(buttonPanel, BorderLayout.SOUTH); + + // Automaticky vybrat první disk + if (drives.size() > 0) { + driveList.setSelectedIndex(0); + } + + driveList.requestFocus(); + } + + public File getSelectedDrive() { + return selectedDrive; + } + + /** + * Pomocná třída pro informace o disku + */ + private static class DriveInfo { + private final File root; + + public DriveInfo(File root) { + this.root = root; + } + + public File getRoot() { + return root; + } + + public String getDisplayText() { + String path = root.getAbsolutePath(); + long totalSpace = root.getTotalSpace(); + long freeSpace = root.getFreeSpace(); + long usedSpace = totalSpace - freeSpace; + + if (totalSpace > 0) { + return String.format("%s %s / %s free", + path, + formatSize(usedSpace), + formatSize(freeSpace)); + } else { + return path; + } + } + + private String formatSize(long size) { + if (size < 1024) { + return size + " B"; + } else if (size < 1024L * 1024) { + return String.format("%.1f KB", size / 1024.0); + } else if (size < 1024L * 1024 * 1024) { + return String.format("%.1f MB", size / (1024.0 * 1024.0)); + } else if (size < 1024L * 1024 * 1024 * 1024) { + return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); + } else { + return String.format("%.1f TB", size / (1024.0 * 1024.0 * 1024.0 * 1024.0)); + } + } + } +} diff --git a/src/main/java/com/kfmanager/ui/FileEditor.java b/src/main/java/com/kfmanager/ui/FileEditor.java new file mode 100644 index 0000000..c0cd74a --- /dev/null +++ b/src/main/java/com/kfmanager/ui/FileEditor.java @@ -0,0 +1,700 @@ +package com.kfmanager.ui; + +import com.kfmanager.config.AppConfig; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.InputEvent; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Files; + +/** + * Internal file editor/viewer + */ +public class FileEditor extends JDialog { + private JTextArea textArea; + private File file; + private AppConfig config; + private boolean modified = false; + private boolean readOnly; + private JLabel statusPosLabel; + private JLabel statusSelLabel; + // Hex view support + private boolean hexMode = false; + private byte[] fileBytes = null; + // For mapping byte index -> text offset in hex dump (for current page) + private java.util.List byteTextOffsets = new java.util.ArrayList<>(); + // Paged streaming fields + private RandomAccessFile raf = null; + private long fileLength = 0L; + private long pageOffsetBytes = 0L; + private final int pageSizeBytes = 64 * 1024; // 64 KB page size + // Allow loading entire file into memory up to this limit (100 MB) + private final long maxFullLoadBytes = 100L * 1024L * 1024L; // 100 MB + private JPanel hexControlPanel = null; + private JButton prevPageBtn = null; + private JButton nextPageBtn = null; + private JLabel pageOffsetLabel = null; + + public FileEditor(Window parent, File file, AppConfig config, boolean readOnly) { + super(parent, (readOnly ? "Prohlížeč - " : "Editor - ") + file.getName(), ModalityType.MODELESS); + this.file = file; + this.config = config; + this.readOnly = readOnly; + + initComponents(); + loadFile(); + + setSize(800, 600); + setLocationRelativeTo(parent); + // Intercept window close (X) so we run the same save-confirm flow as other close actions + setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(java.awt.event.WindowEvent e) { + closeEditor(); + } + }); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + // Menu bar + createMenuBar(); + + // Textová oblast (editable nebo read-only) + textArea = new JTextArea(); + textArea.setFont(config.getEditorFont()); + textArea.setTabSize(4); + textArea.setEditable(!readOnly); + + // V read-only režimu zajistit viditelnost kurzoru a možnost označování + if (readOnly) { + textArea.getCaret().setVisible(true); + textArea.setCaretColor(Color.BLACK); + } + + // Sledování změn (pouze pro editovatelný režim) + if (!readOnly) { + textArea.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { + public void insertUpdate(javax.swing.event.DocumentEvent e) { setModified(true); } + public void removeUpdate(javax.swing.event.DocumentEvent e) { setModified(true); } + public void changedUpdate(javax.swing.event.DocumentEvent e) { setModified(true); } + }); + } + + JScrollPane scrollPane = new JScrollPane(textArea); + add(scrollPane, BorderLayout.CENTER); + // Status bar (position, selection) + JPanel statusPanel = new JPanel(new BorderLayout()); + statusPosLabel = new JLabel(" "); + statusSelLabel = new JLabel(" "); + statusPosLabel.setBorder(BorderFactory.createEmptyBorder(2,6,2,6)); + statusSelLabel.setBorder(BorderFactory.createEmptyBorder(2,6,2,6)); + statusPanel.add(statusPosLabel, BorderLayout.WEST); + statusPanel.add(statusSelLabel, BorderLayout.EAST); + add(statusPanel, BorderLayout.SOUTH); + + // Klávesové zkratky + setupKeyBindings(); + + // Caret listener to update position and selection info + textArea.addCaretListener(e -> updateStatus()); + // Also update status when document changes (to refresh byte counts) + textArea.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { + public void insertUpdate(javax.swing.event.DocumentEvent e) { updateStatus(); } + public void removeUpdate(javax.swing.event.DocumentEvent e) { updateStatus(); } + public void changedUpdate(javax.swing.event.DocumentEvent e) { updateStatus(); } + }); + } + + private void createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + + // File menu + JMenu fileMenu = new JMenu("File"); + fileMenu.setMnemonic(java.awt.event.KeyEvent.VK_S); + + // Uložit - pouze v editovacím režimu + if (!readOnly) { + JMenuItem saveItem = new JMenuItem("Uložit"); + saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)); + saveItem.addActionListener(e -> saveFile()); + fileMenu.add(saveItem); + + fileMenu.addSeparator(); + } + + JMenuItem closeItem = new JMenuItem("Zavřít"); + closeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0)); + closeItem.addActionListener(e -> closeEditor()); + fileMenu.add(closeItem); + + menuBar.add(fileMenu); + + // Menu Nastavení + JMenu settingsMenu = new JMenu("Nastavení"); + settingsMenu.setMnemonic(java.awt.event.KeyEvent.VK_N); + + JMenuItem fontItem = new JMenuItem("Font..."); + fontItem.addActionListener(e -> changeFont()); + settingsMenu.add(fontItem); + + menuBar.add(settingsMenu); + + // Add View menu (hex toggle) + createViewMenu(menuBar); + setJMenuBar(menuBar); + } + + private void createViewMenu(JMenuBar menuBar) { + JMenu viewMenu = new JMenu("View"); + JCheckBoxMenuItem hexItem = new JCheckBoxMenuItem("Hex view"); + hexItem.setState(hexMode); + hexItem.addActionListener(e -> { + boolean newState = hexItem.getState(); + setHexMode(newState); + }); + viewMenu.add(hexItem); + menuBar.add(viewMenu); + } + + private void ensureHexControls() { + if (hexControlPanel != null) return; + hexControlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + prevPageBtn = new JButton("◀ Prev"); + nextPageBtn = new JButton("Next ▶"); + pageOffsetLabel = new JLabel("Offset: 0x0"); + + prevPageBtn.addActionListener(e -> prevHexPage()); + nextPageBtn.addActionListener(e -> nextHexPage()); + + hexControlPanel.add(prevPageBtn); + hexControlPanel.add(pageOffsetLabel); + hexControlPanel.add(nextPageBtn); + } + + private void setupKeyBindings() { + JRootPane rootPane = getRootPane(); + + // F2 - Uložit (pouze v editovacím režimu) + if (!readOnly) { + rootPane.registerKeyboardAction(e -> saveFile(), + KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + } + + // ESC - Zavřít + rootPane.registerKeyboardAction(e -> closeEditor(), + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F3/F4 - Zavřít + rootPane.registerKeyboardAction(e -> closeEditor(), + KeyStroke.getKeyStroke(readOnly ? KeyEvent.VK_F3 : KeyEvent.VK_F4, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+S - Save (common shortcut) but show confirmation dialog when there are unsaved changes + if (!readOnly) { + rootPane.registerKeyboardAction(e -> handleCtrlS(), + KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + } + // PageUp/PageDown when in hex mode -> move pages + rootPane.registerKeyboardAction(e -> { + if (hexMode) prevHexPage(); + }, + KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + rootPane.registerKeyboardAction(e -> { + if (hexMode) nextHexPage(); + }, + KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + } + + private void setHexMode(boolean on) { + this.hexMode = on; + if (on) { + // ensure bytes loaded + try { + // For large files, prefer streaming only when file exceeds maxFullLoadBytes + long size = Files.size(file.toPath()); + if (size > maxFullLoadBytes) { + // Open RAF for streaming + if (raf == null) raf = new RandomAccessFile(file, "r"); + fileLength = raf.length(); + pageOffsetBytes = 0L; + loadHexPage(); + } else { + if (fileBytes == null) fileBytes = java.nio.file.Files.readAllBytes(file.toPath()); + buildHexViewText(0L); + } + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, "Cannot load file for hex view: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + return; + } + textArea.setEditable(false); + // ensure monospaced font for alignment + textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize())); + ensureHexControls(); + if (hexControlPanel.getParent() == null) { + add(hexControlPanel, BorderLayout.NORTH); + } + hexControlPanel.setVisible(true); + } else { + // switch back to text view if possible + // close RA if open + try { if (raf != null) { raf.close(); raf = null; } } catch (Exception ignore) {} + pageOffsetBytes = 0L; + fileLength = 0L; + loadFile(); + textArea.setEditable(!readOnly); + textArea.setFont(config.getEditorFont()); + } + updateStatus(); + } + + private void buildHexViewText() { + buildHexViewText(0L); + } + + private void buildHexViewText(long baseOffset) { + byteTextOffsets.clear(); + if (fileBytes == null) fileBytes = new byte[0]; + StringBuilder sb = new StringBuilder(); + int bytesPerLine = 16; + for (int i = 0; i < fileBytes.length; i += bytesPerLine) { + long displayOffset = baseOffset + i; + sb.append(String.format("%08X ", displayOffset)); + // hex bytes + for (int j = 0; j < bytesPerLine; j++) { + int idx = i + j; + if (idx < fileBytes.length) { + int b = fileBytes[idx] & 0xFF; + int pos = sb.length(); + byteTextOffsets.add(pos); + sb.append(String.format("%02X", b)); + } else { + // placeholder for missing byte + sb.append(" "); + } + if (j != bytesPerLine - 1) sb.append(' '); + if (j == 7) sb.append(' '); // extra gap after 8 bytes + } + sb.append(" "); + // ASCII representation + for (int j = 0; j < bytesPerLine; j++) { + int idx = i + j; + if (idx < fileBytes.length) { + int b = fileBytes[idx] & 0xFF; + char c = (b >= 0x20 && b <= 0x7E) ? (char) b : '.'; + sb.append(c); + } else { + sb.append(' '); + } + } + if (i + bytesPerLine < fileBytes.length) sb.append('\n'); + } + textArea.setText(sb.toString()); + // Ensure caret stays within the rendered region; caller may set caret to preferred pos + if (textArea.getCaretPosition() > textArea.getText().length()) { + textArea.setCaretPosition(0); + } + // Ensure byteTextOffsets has one entry per byte (if some bytes were skipped, fill with -1) + while (byteTextOffsets.size() < fileBytes.length) { + byteTextOffsets.add(textArea.getText().length()); + } + // Ensure byteTextOffsets length equals fileBytes length + // If some bytes were missing due to empty file, leave as is + } + + private void loadHexPage() { + if (raf == null) return; + try { + raf.seek(pageOffsetBytes); + int toRead = (int)Math.min(pageSizeBytes, fileLength - pageOffsetBytes); + byte[] page = new byte[toRead]; + raf.readFully(page); + this.fileBytes = page; + buildHexViewText(pageOffsetBytes); + if (pageOffsetLabel != null) { + long start = pageOffsetBytes; + long end = pageOffsetBytes + page.length - 1; + pageOffsetLabel.setText(String.format("Offset: 0x%08X - 0x%08X", start, end)); + } + // Position caret at first byte hex position for consistent mapping + if (byteTextOffsets != null && !byteTextOffsets.isEmpty()) { + int pos = byteTextOffsets.get(0); + textArea.setCaretPosition(Math.max(0, pos)); + } else { + textArea.setCaretPosition(0); + } + } catch (IOException e) { + JOptionPane.showMessageDialog(this, "Error reading file page: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } + } + + private void prevHexPage() { + if (pageOffsetBytes <= 0) return; + pageOffsetBytes = Math.max(0, pageOffsetBytes - pageSizeBytes); + pageOffsetBytes = (pageOffsetBytes / 16) * 16; + loadHexPage(); + } + + private void nextHexPage() { + if (pageOffsetBytes + pageSizeBytes >= fileLength) return; + pageOffsetBytes = pageOffsetBytes + pageSizeBytes; + pageOffsetBytes = (pageOffsetBytes / 16) * 16; + loadHexPage(); + } + + /** + * Handle Ctrl+S: show the save/confirm dialog if document was modified, otherwise perform a save. + */ + private void handleCtrlS() { + if (readOnly) return; + if (!modified) { + // No changes - inform the user briefly + JOptionPane.showMessageDialog(this, "Žádné změny k uložení.", "Info", JOptionPane.INFORMATION_MESSAGE); + return; + } + + int result = showSaveConfirmDialog("Soubor byl změněn. Uložit změny?", "Uložit změny"); + + if (result == JOptionPane.YES_OPTION) { + saveFile(); + } else if (result == JOptionPane.NO_OPTION) { + // Do nothing (user decided not to save) + } else { + // CANCEL - do nothing + } + } + + private void loadFile() { + try { + // Determine file size first to decide streaming vs full load + long size = Files.size(file.toPath()); + // Read a small probe to detect binary nature without loading whole file + int probeLen = (int)Math.min(512, size); + byte[] probe = new byte[probeLen]; + try (java.io.InputStream is = Files.newInputStream(file.toPath())) { + int read = is.read(probe); + if (read < probeLen) { + byte[] tmp = new byte[read]; + System.arraycopy(probe, 0, tmp, 0, read); + probe = tmp; + } + } + + boolean binaryProbe = isBinary(probe); + + if (binaryProbe && readOnly && size > maxFullLoadBytes) { + // Open RAF and stream pages + if (raf == null) raf = new RandomAccessFile(file, "r"); + fileLength = raf.length(); + pageOffsetBytes = 0L; + loadHexPage(); + ensureHexControls(); + if (hexControlPanel.getParent() == null) add(hexControlPanel, BorderLayout.NORTH); + hexControlPanel.setVisible(true); + textArea.setEditable(false); + textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize())); + hexMode = true; + } else { + // Small or text file: load fully + fileBytes = Files.readAllBytes(file.toPath()); + boolean binary = isBinary(fileBytes); + if (binary && readOnly) { + hexMode = true; + buildHexViewText(0L); + textArea.setEditable(false); + textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize())); + ensureHexControls(); + if (hexControlPanel.getParent() == null) add(hexControlPanel, BorderLayout.NORTH); + hexControlPanel.setVisible(true); + } else if (hexMode) { + buildHexViewText(0L); + } else { + String content = new String(fileBytes, "UTF-8"); + textArea.setText(content); + textArea.setCaretPosition(0); + } + } + modified = false; + updateTitle(); + updateStatus(); + } catch (IOException e) { + textArea.setText("Error loading file:\n" + e.getMessage()); + } + } + + private boolean isBinary(byte[] bytes) { + if (bytes == null || bytes.length == 0) return false; + int nonPrintable = 0; + int len = Math.min(bytes.length, 512); + for (int i = 0; i < len; i++) { + int b = bytes[i] & 0xFF; + if (b == 0) return true; // NUL -> binary + if (b < 0x09) return true; + if (b > 0x7E) nonPrintable++; + } + return nonPrintable > (len / 4); + } + + private void saveFile() { + try { + String content = textArea.getText(); + Files.write(file.toPath(), content.getBytes("UTF-8")); + modified = false; + updateTitle(); + JOptionPane.showMessageDialog(this, + "Soubor uložen", + "Úspěch", + JOptionPane.INFORMATION_MESSAGE); + updateStatus(); + } catch (IOException e) { + JOptionPane.showMessageDialog(this, + "Chyba při ukládání:\n" + e.getMessage(), + "Chyba", + JOptionPane.ERROR_MESSAGE); + } + } + + private void closeEditor() { + if (!readOnly && modified) { + int result = showSaveConfirmDialog("Soubor byl změněn. Uložit změny?", "Neuložené změny"); + + if (result == JOptionPane.YES_OPTION) { + saveFile(); + dispose(); + } else if (result == JOptionPane.NO_OPTION) { + dispose(); + } + // CANCEL_OPTION - nedělat nic + } else { + dispose(); + } + } + + private void setModified(boolean modified) { + this.modified = modified; + updateTitle(); + } + + private void updateTitle() { + String title = (readOnly ? "Prohlížeč - " : "Editor - ") + file.getName(); + if (!readOnly && modified) { + title += " *"; + } + setTitle(title); + } + + private void changeFont() { + Font newFont = FontChooserDialog.showDialog(this, textArea.getFont()); + if (newFont != null) { + textArea.setFont(newFont); + config.setEditorFont(newFont); + config.saveConfig(); + updateStatus(); + } + } + + /** + * Show a modal save-confirm dialog with Yes/No/Cancel options. + * Buttons can be switched using left/right arrow keys and activated with Enter. + * Returns JOptionPane.YES_OPTION / NO_OPTION / CANCEL_OPTION + */ + private int showSaveConfirmDialog(String message, String title) { + final JDialog dlg = new JDialog(this, title, Dialog.ModalityType.APPLICATION_MODAL); + dlg.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dlg.setLayout(new BorderLayout(8, 8)); + dlg.add(new JLabel(message), BorderLayout.CENTER); + + JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton yesBtn = new JButton("Ano"); + JButton noBtn = new JButton("Ne"); + JButton cancelBtn = new JButton("Zrušit"); + + final int[] result = {JOptionPane.CANCEL_OPTION}; + + yesBtn.addActionListener(e -> { + result[0] = JOptionPane.YES_OPTION; + dlg.dispose(); + }); + noBtn.addActionListener(e -> { + result[0] = JOptionPane.NO_OPTION; + dlg.dispose(); + }); + cancelBtn.addActionListener(e -> { + result[0] = JOptionPane.CANCEL_OPTION; + dlg.dispose(); + }); + + btnPanel.add(yesBtn); + btnPanel.add(noBtn); + btnPanel.add(cancelBtn); + dlg.add(btnPanel, BorderLayout.SOUTH); + + // Key bindings: left/right to move focus between buttons, Enter to press, ESC to cancel + JRootPane root = dlg.getRootPane(); + + Action focusLeft = new AbstractAction() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { + java.awt.Component c = dlg.getFocusOwner(); + java.awt.Component[] comps = {yesBtn, noBtn, cancelBtn}; + int idx = -1; + for (int i = 0; i < comps.length; i++) if (comps[i] == c) { idx = i; break; } + if (idx == -1) { yesBtn.requestFocusInWindow(); return; } + int prev = (idx - 1 + comps.length) % comps.length; + comps[prev].requestFocusInWindow(); + } + }; + Action focusRight = new AbstractAction() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { + java.awt.Component c = dlg.getFocusOwner(); + java.awt.Component[] comps = {yesBtn, noBtn, cancelBtn}; + int idx = -1; + for (int i = 0; i < comps.length; i++) if (comps[i] == c) { idx = i; break; } + if (idx == -1) { yesBtn.requestFocusInWindow(); return; } + int next = (idx + 1) % comps.length; + comps[next].requestFocusInWindow(); + } + }; + + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "focusLeft"); + root.getActionMap().put("focusLeft", focusLeft); + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "focusRight"); + root.getActionMap().put("focusRight", focusRight); + + // Enter -> click focused button + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "press"); + root.getActionMap().put("press", new AbstractAction() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { + java.awt.Component c = dlg.getFocusOwner(); + if (c instanceof JButton) ((JButton)c).doClick(); + } + }); + + // ESC -> cancel + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "esc"); + root.getActionMap().put("esc", new AbstractAction() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { + result[0] = JOptionPane.CANCEL_OPTION; + dlg.dispose(); + } + }); + + dlg.pack(); + dlg.setLocationRelativeTo(this); + // Focus yes button initially + SwingUtilities.invokeLater(() -> yesBtn.requestFocusInWindow()); + dlg.setVisible(true); + return result[0]; + } + + /** Update status bar: caret position (line:col, offset, percent) and selection count (chars/bytes) */ + private void updateStatus() { + SwingUtilities.invokeLater(() -> { + try { + if (hexMode && fileBytes != null) { + // When streaming (paged) we prefer to show the offset corresponding + // to the real visible position in the file (top of viewport). This + // makes the displayed offset follow scrolling (PgUp/PgDn or scrollbar) + // rather than only caret movement. + int refPos; + if (raf != null) { + try { + Rectangle vis = textArea.getVisibleRect(); + Point topLeft = new Point(vis.x, vis.y); + refPos = textArea.viewToModel2D(topLeft); + } catch (Exception ex) { + refPos = textArea.getCaretPosition(); + } + } else { + refPos = textArea.getCaretPosition(); + } + int caret = refPos; + long byteIndex = mapCaretToByteIndex(caret); + long totalBytes = (raf != null && fileLength > 0) ? fileLength : (fileBytes != null ? fileBytes.length : 0); + int percent = totalBytes > 0 ? (int)((byteIndex * 100L) / totalBytes) : 0; + statusPosLabel.setText(String.format("Offset %d/%d (%d%%)", byteIndex, totalBytes, percent)); + + int selStart = textArea.getSelectionStart(); + int selEnd = textArea.getSelectionEnd(); + long selBytes = 0; + if (selEnd > selStart) { + long b1 = mapCaretToByteIndex(selStart); + long b2 = mapCaretToByteIndex(Math.max(selStart, selEnd-1)); + selBytes = Math.max(0L, b2 - b1 + 1L); + } + if (selBytes > 0) { + statusSelLabel.setText(String.format("Selected: %d bytes", selBytes)); + } else { + statusSelLabel.setText(" "); + } + + } else { + int caret = textArea.getCaretPosition(); + String text = textArea.getText(); + int total = text != null ? text.length() : 0; + int line = 0, col = 0; + if (total == 0) { + line = 0; col = 0; + } else { + line = textArea.getLineOfOffset(Math.max(0, caret)) + 1; + int lineStart = textArea.getLineStartOffset(Math.max(0, line-1)); + col = caret - lineStart + 1; + } + int percent = total > 0 ? (int) ((caret * 100L) / total) : 0; + statusPosLabel.setText(String.format("Ln %d, Col %d | Offset %d/%d (%d%%)", line, col, caret, total, percent)); + + int selStart = textArea.getSelectionStart(); + int selEnd = textArea.getSelectionEnd(); + int selChars = Math.max(0, selEnd - selStart); + int selBytes = 0; + if (selChars > 0) { + try { + String selText = textArea.getDocument().getText(selStart, selChars); + selBytes = selText.getBytes("UTF-8").length; + } catch (Exception ex) { + selBytes = 0; + } + } + if (selChars > 0) { + statusSelLabel.setText(String.format("Selected: %d chars / %d bytes", selChars, selBytes)); + } else { + statusSelLabel.setText(" "); + } + } + } catch (Exception ex) { + // ignore status update errors + } + }); + } + + private long mapCaretToByteIndex(int caretPos) { + if (byteTextOffsets == null || byteTextOffsets.isEmpty()) return 0L; + // find greatest index i such that byteTextOffsets.get(i) <= caretPos + int idx = java.util.Collections.binarySearch(byteTextOffsets, caretPos); + if (idx >= 0) return (long) idx + pageOffsetBytes; + int ins = -idx - 1; + int candidate = Math.max(0, ins - 1); + int local = Math.min(candidate, Math.max(0, (fileBytes != null ? fileBytes.length : 0) - 1)); + long global = (long)local + pageOffsetBytes; + if (fileLength > 0) global = Math.min(global, fileLength - 1); + return global; + } + + @Override + public void dispose() { + try { + if (raf != null) raf.close(); + } catch (Exception ignore) {} + super.dispose(); + } +} diff --git a/src/main/java/com/kfmanager/ui/FilePanel.java b/src/main/java/com/kfmanager/ui/FilePanel.java new file mode 100644 index 0000000..8e2e792 --- /dev/null +++ b/src/main/java/com/kfmanager/ui/FilePanel.java @@ -0,0 +1,528 @@ +package com.kfmanager.ui; + +import com.kfmanager.model.FileItem; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.File; +import java.util.List; + +/** + * File panel with tab support + */ +public class FilePanel extends JPanel { + + private JTabbedPane tabbedPane; + private JComboBox driveCombo; + private JLabel driveInfoLabel; + private com.kfmanager.config.AppConfig appConfig; + + public FilePanel(String initialPath) { + initComponents(); + addNewTab(initialPath); + } + + private Runnable switchPanelCallback; + + public void setSwitchPanelCallback(Runnable cb) { + this.switchPanelCallback = cb; + } + + private void initComponents() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // Top panel with path field + JPanel topPanel = new JPanel(new BorderLayout()); + + // Drive selection dropdown placed before the path field + driveCombo = new JComboBox<>(); + // Allow the combo to receive focus so keyboard navigation and popup interaction work + driveCombo.setFocusable(true); + driveCombo.setToolTipText("Select drive"); + // renderer to show friendly name and path + driveCombo.setRenderer(new DefaultListCellRenderer() { + private final javax.swing.filechooser.FileSystemView fsv = javax.swing.filechooser.FileSystemView.getFileSystemView(); + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof File) { + File f = (File) value; + String name = fsv.getSystemDisplayName(f); + if (name == null) name = ""; + name = name.trim(); + // Strip surrounding parentheses if present (e.g. "(C)") + if (name.startsWith("(") && name.endsWith(")") && name.length() > 1) { + name = name.substring(1, name.length() - 1).trim(); + } + String path = f.getAbsolutePath(); + String driveLabel; + // Prefer Windows-style drive letter like "C:" when available + if (path != null && path.length() >= 2 && path.charAt(1) == ':') { + driveLabel = path.substring(0, 2); + } else { + // Fall back to path or empty + driveLabel = path != null ? path : ""; + } + // Remove redundant drive-letter fragments from name (e.g. "C", "C:", "(C)") + if (!driveLabel.isEmpty() && !name.isEmpty()) { + // remove occurrences of driveLabel or driveLabel + ':' and stray parentheses + name = name.replace(driveLabel, "").replace(driveLabel + ":", "").replace("(", "").replace(")", "").trim(); + } + String text = driveLabel; + if (!name.isEmpty()) text = text + " " + name; + setText(text); + } + return this; + } + }); + + // Small label next to the combo to show drive info (label, capacity, free space) + driveInfoLabel = new JLabel(); + driveInfoLabel.setFont(new Font("SansSerif", Font.PLAIN, 12)); + driveInfoLabel.setBorder(BorderFactory.createEmptyBorder(0,6,0,6)); + + // Require explicit confirmation (Enter) to load selected drive. + // Selection changes (mouse/typing) only update the selected item; loading occurs on Enter. + driveCombo.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ENTER, 0), "confirmDrive"); + driveCombo.getActionMap().put("confirmDrive", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Object selObj = driveCombo.getSelectedItem(); + if (selObj instanceof File) { + File sel = (File) selObj; + FilePanelTab currentTab = getCurrentTab(); + if (currentTab != null) { + currentTab.loadDirectory(sel); + SwingUtilities.invokeLater(() -> { + try { currentTab.getFileTable().requestFocusInWindow(); } catch (Exception ignore) {} + }); + } + } + } + }); + // Compose a small left panel with the combo and info label + JPanel leftPanel = new JPanel(); + leftPanel.setLayout(new BoxLayout(leftPanel, BoxLayout.X_AXIS)); + leftPanel.add(driveCombo); + leftPanel.add(driveInfoLabel); + topPanel.add(leftPanel, BorderLayout.WEST); + // Populate drives after the info label is created so updateDriveInfo() can safely access it + populateDrives(); + + // Path field removed; path is shown in tab titles instead + + // Button for parent directory + JButton upButton = new JButton("↑"); + upButton.setToolTipText("Parent directory (Backspace)"); + upButton.addActionListener(e -> { + FilePanelTab currentTab = getCurrentTab(); + if (currentTab != null) { + currentTab.navigateUp(); + } + }); + topPanel.add(upButton, BorderLayout.EAST); + + add(topPanel, BorderLayout.NORTH); + + // JTabbedPane pro taby + tabbedPane = new JTabbedPane(); + tabbedPane.setTabPlacement(JTabbedPane.TOP); + + // Listener pro aktualizaci cesty při změně tabu + tabbedPane.addChangeListener(e -> updatePathField()); + + add(tabbedPane, BorderLayout.CENTER); + } + + /** + * Show the drive dropdown popup and focus it. + */ + public void showDrivePopup() { + if (driveCombo == null) return; + SwingUtilities.invokeLater(() -> { + try { + driveCombo.requestFocusInWindow(); + driveCombo.showPopup(); + } catch (Exception ignore) {} + }); + } + + /** + * Add a new tab with a directory + */ + public void addNewTab(String path) { + // Získat view mode z aktuálního tabu + ViewMode currentMode = getViewMode(); + + FilePanelTab tab = new FilePanelTab(path); + if (appConfig != null) tab.setAppConfig(appConfig); + + // Nastavit callback pro aktualizaci názvu tabu při změně adresáře + tab.setOnDirectoryChanged(() -> updateTabTitle(tab)); + + // Forward switchPanel callback to the tab so TAB works from any tab + tab.setOnSwitchPanelRequested(switchPanelCallback); + + // Nastavit stejný view mode jako má aktuální tab + if (currentMode != null) { + tab.setViewMode(currentMode); + } + + String tabTitle = getTabTitle(path); + + tabbedPane.addTab(tabTitle, tab); + tabbedPane.setSelectedComponent(tab); + + // Aktualizovat path field + updatePathField(); + + // Nastavit focus na tabulku v novém tabu + SwingUtilities.invokeLater(() -> { + tab.getFileTable().requestFocusInWindow(); + // Ensure renderers are attached now that the tab is added to the UI + tab.ensureRenderers(); + }); + } + + /** + * Přidá nový tab a explicitně nastaví ViewMode pro tento tab. + */ + public void addNewTabWithMode(String path, ViewMode mode) { + FilePanelTab tab = new FilePanelTab(path); + if (appConfig != null) tab.setAppConfig(appConfig); + tab.setOnDirectoryChanged(() -> updateTabTitle(tab)); + tab.setOnSwitchPanelRequested(switchPanelCallback); + + if (mode != null) { + tab.setViewMode(mode); + } + + String tabTitle = getTabTitle(path); + tabbedPane.addTab(tabTitle, tab); + tabbedPane.setSelectedComponent(tab); + + updatePathField(); + + SwingUtilities.invokeLater(() -> { + tab.getFileTable().requestFocusInWindow(); + tab.ensureRenderers(); + }); + } + + /** + * Provide AppConfig so tabs can persist/retrieve sort settings + */ + public void setAppConfig(com.kfmanager.config.AppConfig cfg) { + this.appConfig = cfg; + // propagate to existing tabs + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + ((FilePanelTab) c).setAppConfig(cfg); + } + } + } + + public java.util.List getTabPaths() { + java.util.List paths = new java.util.ArrayList<>(); + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + FilePanelTab t = (FilePanelTab) c; + File dir = t.getCurrentDirectory(); + paths.add(dir != null ? dir.getAbsolutePath() : System.getProperty("user.home")); + } + } + return paths; + } + + public java.util.List getTabViewModes() { + java.util.List modes = new java.util.ArrayList<>(); + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + FilePanelTab t = (FilePanelTab) c; + modes.add(t.getViewMode() != null ? t.getViewMode().name() : ViewMode.FULL.name()); + } + } + return modes; + } + + public int getSelectedTabIndex() { + return tabbedPane.getSelectedIndex(); + } + + /** + * Obnoví sadu tabů podle zadaných cest a view módů. Pokud je seznam prázdný, nic se nestane. + */ + public void restoreTabs(java.util.List paths, java.util.List viewModes, int selectedIndex) { + if (paths == null || paths.isEmpty()) return; + + tabbedPane.removeAll(); + + for (int i = 0; i < paths.size(); i++) { + String p = paths.get(i); + ViewMode mode = ViewMode.FULL; + if (viewModes != null && i < viewModes.size()) { + try { + mode = ViewMode.valueOf(viewModes.get(i)); + } catch (IllegalArgumentException ex) { + mode = ViewMode.FULL; + } + } + addNewTabWithMode(p, mode); + } + + if (selectedIndex >= 0 && selectedIndex < tabbedPane.getTabCount()) { + tabbedPane.setSelectedIndex(selectedIndex); + } else if (tabbedPane.getTabCount() > 0) { + tabbedPane.setSelectedIndex(0); + } + + updatePathField(); + } + + private void populateDrives() { + driveCombo.removeAllItems(); + File[] roots = File.listRoots(); + if (roots != null) { + for (File r : roots) { + try { + driveCombo.addItem(r); + } catch (Exception ignore) {} + } + // select first drive by default + if (roots.length > 0) driveCombo.setSelectedItem(roots[0]); + } + + // Update info for currently selected drive + updateDriveInfoFromSelection(); + + // Update info label on selection changes (visual selection only) + driveCombo.addItemListener(ev -> { + if (ev.getStateChange() == java.awt.event.ItemEvent.SELECTED) { + updateDriveInfoFromSelection(); + } + }); + } + + /** + * Get tab title from path + */ + private String getTabTitle(String path) { + String tabTitle = new File(path).getName(); + if (tabTitle.isEmpty()) { + tabTitle = path; // Pro root cesty jako "C:\" + } + return tabTitle; + } + + /** + * Update the title of a tab according to its current directory + */ + private void updateTabTitle(FilePanelTab tab) { + int index = tabbedPane.indexOfComponent(tab); + if (index >= 0) { + File currentDir = tab.getCurrentDirectory(); + if (currentDir != null) { + String title = getTabTitle(currentDir.getAbsolutePath()); + tabbedPane.setTitleAt(index, title); + } + } + } + + /** + * Remove the current tab + */ + public void closeCurrentTab() { + int index = tabbedPane.getSelectedIndex(); + if (index >= 0 && tabbedPane.getTabCount() > 1) { + tabbedPane.removeTabAt(index); + updatePathField(); + + // Set focus to the table in the newly active tab + FilePanelTab currentTab = getCurrentTab(); + if (currentTab != null) { + SwingUtilities.invokeLater(() -> currentTab.getFileTable().requestFocusInWindow()); + } + } + } + + /** + * Return the current active tab + */ + public FilePanelTab getCurrentTab() { + int index = tabbedPane.getSelectedIndex(); + if (index >= 0) { + return (FilePanelTab) tabbedPane.getComponentAt(index); + } + return null; + } + + /** + * Update path field based on current tab + */ + private void updatePathField() { + FilePanelTab currentTab = getCurrentTab(); + if (currentTab != null) { + // Update drive combo selection to match current directory + try { + File cur = currentTab.getCurrentDirectory(); + if (cur != null) { + File root = cur.toPath().getRoot().toFile(); + driveCombo.setSelectedItem(root); + // also update info label to reflect current tab's root + updateDriveInfo(root); + } + } catch (Exception ignore) {} + } + } + + private void updateDriveInfoFromSelection() { + Object sel = driveCombo.getSelectedItem(); + if (sel instanceof File) { + updateDriveInfo((File) sel); + } else { + driveInfoLabel.setText(""); + } + } + + private void updateDriveInfo(File drive) { + try { + javax.swing.filechooser.FileSystemView fsv = javax.swing.filechooser.FileSystemView.getFileSystemView(); + String name = fsv.getSystemDisplayName(drive); + if (name == null) name = ""; + name = name.trim(); + // strip surrounding parentheses left by some platform names + if (name.startsWith("(") && name.endsWith(")") && name.length() > 1) { + name = name.substring(1, name.length() - 1).trim(); + } + if (name.isEmpty()) { + // fallback to drive letter like "C:" + String path = drive != null ? drive.getAbsolutePath() : ""; + if (path != null && path.length() >= 2 && path.charAt(1) == ':') { + name = path.substring(0, 2); + } else { + name = path != null ? path : ""; + } + } + long total = drive.getTotalSpace(); + long free = drive.getUsableSpace(); + String freeGb = formatGbShort(free); + String totalGb = formatGbShort(total); + String info = String.format("%s %s free of %s GB", name, freeGb, totalGb); + driveInfoLabel.setText(info); + } catch (Exception ex) { + driveInfoLabel.setText(""); + } + } + + private static String formatGbShort(long bytes) { + if (bytes <= 0) return "0"; // fallback + double gb = bytes / 1024.0 / 1024.0 / 1024.0; + return String.format("%.1f", gb); + } + + private static String humanReadableByteCount(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + String pre = "KMGTPE".charAt(exp-1) + ""; + return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre); + } + + /** + * Request focus on the table in the current tab + */ + public void requestFocusOnCurrentTab() { + FilePanelTab currentTab = getCurrentTab(); + if (currentTab != null) { + currentTab.getFileTable().requestFocusInWindow(); + } + } + + // Delegování metod na aktuální tab + + public JTable getFileTable() { + FilePanelTab tab = getCurrentTab(); + return tab != null ? tab.getFileTable() : null; + } + + public File getCurrentDirectory() { + FilePanelTab tab = getCurrentTab(); + return tab != null ? tab.getCurrentDirectory() : null; + } + + public List getSelectedItems() { + FilePanelTab tab = getCurrentTab(); + return tab != null ? tab.getSelectedItems() : java.util.Collections.emptyList(); + } + + public void setViewMode(ViewMode mode) { + FilePanelTab tab = getCurrentTab(); + if (tab != null) { + tab.setViewMode(mode); + } + } + + public ViewMode getViewMode() { + FilePanelTab tab = getCurrentTab(); + return tab != null ? tab.getViewMode() : ViewMode.FULL; + } + + public void loadDirectory(File directory) { + FilePanelTab tab = getCurrentTab(); + if (tab != null) { + tab.loadDirectory(directory); + updatePathField(); + } + } + + public void toggleSelectionAndMoveDown() { + FilePanelTab tab = getCurrentTab(); + if (tab != null) { + tab.toggleSelectionAndMoveDown(); + } + } + + // --- Appearance application helpers --- + public void applyGlobalFont(Font font) { + // Apply to all existing tabs + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + ((FilePanelTab) c).applyGlobalFont(font); + } + } + } + + public void applyBackgroundColor(Color bg) { + setBackground(bg); + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + ((FilePanelTab) c).applyBackgroundColor(bg); + } + } + } + + public void applySelectionColor(Color sel) { + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + ((FilePanelTab) c).applySelectionColor(sel); + } + } + } + + public void applyMarkedColor(Color mark) { + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component c = tabbedPane.getComponentAt(i); + if (c instanceof FilePanelTab) { + ((FilePanelTab) c).applyMarkedColor(mark); + } + } + } +} diff --git a/src/main/java/com/kfmanager/ui/FilePanel.java.bak b/src/main/java/com/kfmanager/ui/FilePanel.java.bak new file mode 100644 index 0000000..1b722a5 --- /dev/null +++ b/src/main/java/com/kfmanager/ui/FilePanel.java.bak @@ -0,0 +1,1043 @@ +package com.kfmanager.ui; + +import com.kfmanager.model.FileItem; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * Panel zobrazující seznam souborů a adresářů + */ +public class FilePanel extends JPanel { + + public enum ViewMode { + FULL, // Plné informace (název, velikost, datum) + BRIEF // Pouze názvy + } + + private File currentDirectory; + private JTable fileTable; + private FileTableModel tableModel; + private JTextField pathField; + private JLabel statusLabel; + private ViewMode viewMode = ViewMode.FULL; + private int briefCurrentColumn = 0; // Aktuální sloupec v BRIEF módu + private int briefColumnBeforeEnter = 0; // Sloupec před vstupem do adresáře (pro návrat) + + public FilePanel(String initialPath) { + this.currentDirectory = new File(initialPath); + initComponents(); + loadDirectory(currentDirectory); + } + + private void initComponents() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // Panel s cestou + JPanel topPanel = new JPanel(new BorderLayout()); + pathField = new JTextField(); + pathField.setEditable(false); + pathField.setFont(new Font("Monospaced", Font.PLAIN, 12)); + topPanel.add(pathField, BorderLayout.CENTER); + + // Tlačítko pro nadřazený adresář + JButton upButton = new JButton("↑"); + upButton.setToolTipText("Nadřazený adresář (Backspace)"); + upButton.addActionListener(e -> navigateUp()); + topPanel.add(upButton, BorderLayout.EAST); + + add(topPanel, BorderLayout.NORTH); + + // Tabulka se soubory + tableModel = new FileTableModel(); + fileTable = new JTable(tableModel); + fileTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + fileTable.setFont(new Font("Monospaced", Font.PLAIN, 12)); + fileTable.setRowHeight(20); + fileTable.setShowGrid(false); + fileTable.setIntercellSpacing(new Dimension(0, 0)); + + // Nastavit renderery podle výchozího režimu + updateColumnRenderers(); + updateColumnWidths(); + + // Double-click pro otevření adresáře + fileTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + // V BRIEF módu zaznamenat aktuální sloupec + if (viewMode == ViewMode.BRIEF) { + int col = fileTable.columnAtPoint(e.getPoint()); + if (col >= 0) { + briefCurrentColumn = col; + fileTable.repaint(); + } + } + + // Aktualizovat status bar po kliknutí + updateStatus(); + + if (e.getClickCount() == 2) { + openSelectedItem(); + } + } + }); + + // Enter pro otevření + fileTable.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) { + openSelectedItem(); + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_BACK_SPACE) { + navigateUp(); + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_HOME) { + // Skok na první položku + if (viewMode == ViewMode.BRIEF) { + // V BRIEF módu skočit na první položku (index 0) + if (tableModel.items.size() > 0) { + briefCurrentColumn = 0; + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + fileTable.repaint(); + } + } else { + // Ve FULL módu první řádek + if (fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + } + } + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_END) { + // Skok na poslední položku + if (viewMode == ViewMode.BRIEF) { + // V BRIEF módu skočit na poslední položku + int totalItems = tableModel.items.size(); + if (totalItems > 0) { + int lastIndex = totalItems - 1; + int lastCol = lastIndex / tableModel.briefRowsPerColumn; + int lastRow = lastIndex % tableModel.briefRowsPerColumn; + briefCurrentColumn = lastCol; + fileTable.setRowSelectionInterval(lastRow, lastRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, lastCol, true)); + fileTable.repaint(); + } + } else { + // Ve FULL módu poslední řádek + int lastRow = fileTable.getRowCount() - 1; + if (lastRow >= 0) { + fileTable.setRowSelectionInterval(lastRow, lastRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, 0, true)); + } + } + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_PAGE_UP) { + // Page Up - posun o počet viditelných řádků nahoru + if (viewMode == ViewMode.BRIEF) { + handleBriefPageNavigation(true); + } else { + // Ve FULL módu posun o počet viditelných řádků + Rectangle visible = fileTable.getVisibleRect(); + int rowHeight = fileTable.getRowHeight(); + int visibleRows = visible.height / rowHeight; + + int currentRow = fileTable.getSelectedRow(); + int newRow = Math.max(0, currentRow - visibleRows); + + fileTable.setRowSelectionInterval(newRow, newRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, 0, true)); + } + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_PAGE_DOWN) { + // Page Down - posun o počet viditelných řádků dolů + if (viewMode == ViewMode.BRIEF) { + handleBriefPageNavigation(false); + } else { + // Ve FULL módu posun o počet viditelných řádků + Rectangle visible = fileTable.getVisibleRect(); + int rowHeight = fileTable.getRowHeight(); + int visibleRows = visible.height / rowHeight; + + int currentRow = fileTable.getSelectedRow(); + int newRow = Math.min(fileTable.getRowCount() - 1, currentRow + visibleRows); + + fileTable.setRowSelectionInterval(newRow, newRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, 0, true)); + } + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_INSERT) { + // Insert - přepnout označení aktuálního řádku a posunout se dolů + toggleSelectionAndMoveDown(); + e.consume(); + } else if (viewMode == ViewMode.BRIEF && + (e.getKeyCode() == java.awt.event.KeyEvent.VK_DOWN || + e.getKeyCode() == java.awt.event.KeyEvent.VK_UP)) { + // V BRIEF módu vlastní navigace - pohyb mezi položkami, ne řádky + handleBriefNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_DOWN); + e.consume(); + } else if (viewMode == ViewMode.BRIEF && + (e.getKeyCode() == java.awt.event.KeyEvent.VK_LEFT || + e.getKeyCode() == java.awt.event.KeyEvent.VK_RIGHT)) { + // V BRIEF módu šipky vlevo/vpravo - pohyb mezi sloupci + handleBriefHorizontalNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_RIGHT); + e.consume(); + } + } + }); + + JScrollPane scrollPane = new JScrollPane(fileTable); + add(scrollPane, BorderLayout.CENTER); + + // Přidat listener pro změnu velikosti - potřebné pro přepočet BRIEF layoutu + scrollPane.addComponentListener(new java.awt.event.ComponentAdapter() { + @Override + public void componentResized(java.awt.event.ComponentEvent e) { + if (viewMode == ViewMode.BRIEF) { + // Zapamatovat si aktuálně vybranou položku před přepočtem + int selectedRow = fileTable.getSelectedRow(); + FileItem selectedItem = null; + if (selectedRow >= 0) { + selectedItem = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } + + final FileItem itemToReselect = selectedItem; + + SwingUtilities.invokeLater(() -> { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + + // Znovu najít a vybrat položku po přepočtu layoutu + if (itemToReselect != null) { + selectItemByName(itemToReselect.getName()); + } + }); + } + } + }); + + // Stavový řádek + statusLabel = new JLabel(" "); + statusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + add(statusLabel, BorderLayout.SOUTH); + } + + /** + * Načte obsah adresáře + */ + public void loadDirectory(File directory) { + loadDirectory(directory, true); + } + + /** + * Načte obsah adresáře + * @param directory adresář k načtení + * @param autoSelectFirst true pokud má být automaticky vybrán první řádek + */ + private void loadDirectory(File directory, boolean autoSelectFirst) { + if (directory == null || !directory.isDirectory()) { + return; + } + + this.currentDirectory = directory; + pathField.setText(directory.getAbsolutePath()); + + // Resetovat aktuální sloupec v BRIEF módu na první sloupec + briefCurrentColumn = 0; + + File[] files = directory.listFiles(); + List items = new ArrayList<>(); + + // Vždy přidat položku pro nadřazený adresář (pokud existuje) + File parent = directory.getParentFile(); + if (parent != null) { + items.add(new FileItem(parent) { + @Override + public String getName() { + return ".."; + } + }); + } + + if (files != null && files.length > 0) { + // Seřadit: nejdříve adresáře, pak soubory, abecedně + Arrays.sort(files, Comparator + .comparing((File f) -> !f.isDirectory()) + .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + + for (File file : files) { + items.add(new FileItem(file)); + } + } + + tableModel.setItems(items); + + // V BRIEF módu zajistit správné překreslení po načtení + if (viewMode == ViewMode.BRIEF) { + boolean selectFirst = autoSelectFirst; + SwingUtilities.invokeLater(() -> { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + fileTable.revalidate(); + fileTable.repaint(); + + // Automaticky vybrat první řádek po přepočtu layoutu + if (selectFirst && fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + } + }); + } else { + // Automaticky vybrat první řádek (pokud je to požadováno) + if (autoSelectFirst && fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + } + } + + updateStatus(); + } + + /** + * Otevře vybranou položku (adresář) + */ + private void openSelectedItem() { + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + + if (item == null) { + return; + } + + if (item.getName().equals("..")) { + // Pro nadřazený adresář použít navigateUp() pro správné chování + navigateUp(); + } else if (item.isDirectory()) { + // Před vstupem do adresáře si zapamatovat aktuální sloupec (pro návrat zpět) + briefColumnBeforeEnter = briefCurrentColumn; + loadDirectory(item.getFile()); + } + } + } + + /** + * Přejde do nadřazeného adresáře + */ + public void navigateUp() { + File parent = currentDirectory.getParentFile(); + if (parent != null) { + // Zapamatovat si aktuální adresář + String previousDirName = currentDirectory.getName(); + + // Načíst nadřazený adresář bez auto-výběru prvního řádku + loadDirectory(parent, false); + + // Stejný mechanismus jako při resize - přepočítat layout a znovu vybrat položku + if (viewMode == ViewMode.BRIEF) { + SwingUtilities.invokeLater(() -> { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + + // Znovu najít a vybrat položku po přepočtu layoutu + selectItemByName(previousDirName); + }); + } else { + SwingUtilities.invokeLater(() -> selectItemByName(previousDirName)); + } + } + } + + /** + * Vybere položku v tabulce podle názvu + */ + private void selectItemByName(String name) { + // V BRIEF režimu musíme hledat přes všechny položky + if (viewMode == ViewMode.BRIEF) { + for (int row = 0; row < tableModel.getRowCount(); row++) { + for (int col = 0; col < tableModel.getColumnCount(); col++) { + FileItem item = tableModel.getItemFromBriefLayout(row, col); + if (item != null && item.getName().equals(name)) { + // Obnovit uložený sloupec + briefCurrentColumn = col; + fileTable.setRowSelectionInterval(row, row); + fileTable.scrollRectToVisible(fileTable.getCellRect(row, col, true)); + fileTable.repaint(); + return; + } + } + } + } else { + for (int i = 0; i < tableModel.getRowCount(); i++) { + FileItem item = tableModel.getItem(i); + if (item != null && item.getName().equals(name)) { + fileTable.setRowSelectionInterval(i, i); + fileTable.scrollRectToVisible(fileTable.getCellRect(i, 0, true)); + break; + } + } + } + } + + /** + * Přepne označení aktuálního řádku a posune se o řádek dolů + */ + private void toggleSelectionAndMoveDown() { + int currentRow = fileTable.getSelectedRow(); + if (currentRow < 0) { + return; + } + + // Přepnout označení v FileItem + FileItem item; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(currentRow, briefCurrentColumn); + } else { + item = tableModel.getItem(currentRow); + } + + if (item != null) { + item.toggleMarked(); + + // Překreslit tabulku + fileTable.repaint(); + } + + // Posunout se na další položku + if (viewMode == ViewMode.BRIEF) { + handleBriefNavigation(true); + } else { + // Posunout se na další řádek ve FULL módu + int nextRow = currentRow + 1; + if (nextRow < fileTable.getRowCount()) { + fileTable.setRowSelectionInterval(nextRow, nextRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(nextRow, 0, true)); + } + } + + updateStatus(); + } + + /** + * Obsluha navigace v BRIEF módu - pohybuje se mezi položkami správným směrem + * @param down true pro pohyb dolů, false pro pohyb nahoru + */ + private void handleBriefNavigation(boolean down) { + int currentRow = fileTable.getSelectedRow(); + + if (currentRow < 0) { + return; + } + + // Vypočítat aktuální index položky - použít uložený sloupec + int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow; + int totalItems = tableModel.items.size(); + + if (currentIndex < 0 || currentIndex >= totalItems) { + return; + } + + // Vypočítat nový index + int newIndex = down ? currentIndex + 1 : currentIndex - 1; + + // Kontrola hranic + if (newIndex < 0 || newIndex >= totalItems) { + return; + } + + // Převést nový index na řádek a sloupec + int newCol = newIndex / tableModel.briefRowsPerColumn; + int newRow = newIndex % tableModel.briefRowsPerColumn; + + // Uložit aktuální sloupec + briefCurrentColumn = newCol; + + // Přesunout výběr na nový řádek + fileTable.setRowSelectionInterval(newRow, newRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true)); + + // Překreslit pro aktualizaci zvýraznění + fileTable.repaint(); + + updateStatus(); + } + + /** + * Obsluha horizontální navigace v BRIEF módu - pohyb mezi sloupci + * @param right true pro pohyb doprava, false pro pohyb doleva + */ + private void handleBriefHorizontalNavigation(boolean right) { + int currentRow = fileTable.getSelectedRow(); + + if (currentRow < 0) { + return; + } + + // Spočítat nový sloupec + int newColumn = right ? briefCurrentColumn + 1 : briefCurrentColumn - 1; + + // Zkontrolovat, zda nový sloupec existuje + if (newColumn < 0 || newColumn >= tableModel.getColumnCount()) { + return; + } + + // Zkontrolovat, zda existuje položka na této pozici + FileItem item = tableModel.getItemFromBriefLayout(currentRow, newColumn); + int targetRow = currentRow; + + // Pokud na aktuálním řádku není položka, najít nejbližší položku směrem nahoru + if (item == null) { + // Hledat směrem nahoru (nižší řádky) + for (int row = currentRow - 1; row >= 0; row--) { + item = tableModel.getItemFromBriefLayout(row, newColumn); + if (item != null) { + targetRow = row; + break; + } + } + + // Pokud jsme nenašli položku ani směrem nahoru, nepřesouvat se + if (item == null) { + return; + } + } + + // Přesunout se na nový sloupec a řádek + briefCurrentColumn = newColumn; + fileTable.setRowSelectionInterval(targetRow, targetRow); + + // Zajistit viditelnost + fileTable.scrollRectToVisible(fileTable.getCellRect(targetRow, briefCurrentColumn, true)); + fileTable.repaint(); + + updateStatus(); + } + + /** + * Obsluha Page Up/Down v BRIEF módu - posun o počet viditelných položek + * @param pageUp true pro Page Up, false pro Page Down + */ + private void handleBriefPageNavigation(boolean pageUp) { + int currentRow = fileTable.getSelectedRow(); + if (currentRow < 0) { + return; + } + + // Vypočítat aktuální index položky + int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow; + + // Vypočítat počet viditelných položek + Rectangle visible = fileTable.getVisibleRect(); + int rowHeight = fileTable.getRowHeight(); + int visibleRows = visible.height / rowHeight; + + // Počet viditelných sloupců + int visibleColumns = tableModel.getColumnCount(); + + // Celkový počet viditelných položek + int visibleItems = visibleRows * visibleColumns; + + // Vypočítat nový index + int newIndex; + if (pageUp) { + newIndex = Math.max(0, currentIndex - visibleItems); + } else { + newIndex = Math.min(tableModel.items.size() - 1, currentIndex + visibleItems); + } + + // Převést nový index na řádek a sloupec + int newCol = newIndex / tableModel.briefRowsPerColumn; + int newRow = newIndex % tableModel.briefRowsPerColumn; + + // Uložit aktuální sloupec + briefCurrentColumn = newCol; + + // Přesunout výběr a zajistit viditelnost + fileTable.setRowSelectionInterval(newRow, newRow); + Rectangle cellRect = fileTable.getCellRect(newRow, newCol, true); + fileTable.scrollRectToVisible(cellRect); + fileTable.repaint(); + + updateStatus(); + } + + /** + * Obnoví zobrazení aktuálního adresáře + */ + public void refresh() { + loadDirectory(currentDirectory); + } + + /** + * Nastaví režim zobrazení + */ + public void setViewMode(ViewMode mode) { + if (this.viewMode != mode) { + // Zapamatovat si aktuální vybranou položku + String selectedItemName = null; + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = null; + if (this.viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + if (item != null) { + selectedItemName = item.getName(); + } + } + + this.viewMode = mode; + + final String itemToSelect = selectedItemName; + + // V BRIEF režimu musíme počkat, až bude panel zobrazen + SwingUtilities.invokeLater(() -> { + tableModel.updateViewMode(mode); + + // Resetovat aktuální sloupec při přepnutí režimu + briefCurrentColumn = 0; + + // Aktualizovat renderery podle režimu + updateColumnRenderers(); + + // Aktualizovat šířky sloupců + updateColumnWidths(); + + fileTable.revalidate(); + fileTable.repaint(); + + // Znovu vybrat původní položku + if (itemToSelect != null) { + selectItemByName(itemToSelect); + } else if (fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + } + + // Vrátit focus na tabulku + fileTable.requestFocusInWindow(); + }); + } + } + + public ViewMode getViewMode() { + return viewMode; + } + + /** + * Aktualizuje renderery sloupců podle režimu zobrazení + */ + private void updateColumnRenderers() { + int columnCount = viewMode == ViewMode.FULL ? 3 : tableModel.getColumnCount(); + + // Zkontrolovat, že máme správný počet sloupců v modelu + int actualColumnCount = fileTable.getColumnModel().getColumnCount(); + if (actualColumnCount < columnCount) { + // Model ještě nemá správný počet sloupců, zkusíme později + return; + } + + for (int i = 0; i < columnCount; i++) { + final int colIndex = i; + DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + // Získat položku podle aktuálního režimu + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(row, column); + } else { + item = tableModel.getItem(row); + } + + if (item == null) { + // Prázdná buňka v BRIEF režimu + setBackground(FilePanel.this.getBackground()); + setText(""); + setIcon(null); + return this; + } + + boolean isMarked = item.isMarked(); + + // V BRIEF módu kontrolovat konkrétní buňku, ne celý řádek + boolean isCurrentCell = false; + if (viewMode == ViewMode.BRIEF) { + isCurrentCell = isSelected && column == briefCurrentColumn; + } else { + isCurrentCell = isSelected; + } + + // Nejdřív nastavit pozadí podle aktuálního stavu + if (isCurrentCell && table.hasFocus()) { + setBackground(new Color(184, 207, 229)); + } else if (isCurrentCell) { + setBackground(new Color(220, 220, 220)); + } else { + // Použít background panelu + setBackground(FilePanel.this.getBackground()); + } + + // Pak nastavit barvu textu a font podle označení + if (isMarked) { + setForeground(new Color(204, 153, 0)); + setFont(new Font("Monospaced", Font.BOLD, 12)); + } else { + setForeground(Color.BLACK); + setFont(new Font("Monospaced", Font.PLAIN, 12)); + } + + // Nastavit ikonu pro první sloupec (název) + if ((viewMode == ViewMode.BRIEF) || (viewMode == ViewMode.FULL && colIndex == 0)) { + Icon icon = FileSystemView.getFileSystemView().getSystemIcon(item.getFile()); + setIcon(icon); + } else { + setIcon(null); + } + + // Zbavit se borderu + setBorder(null); + + return this; + } + }; + + if (viewMode == ViewMode.FULL && colIndex == 1) { + renderer.setHorizontalAlignment(SwingConstants.RIGHT); + } + + fileTable.getColumnModel().getColumn(i).setCellRenderer(renderer); + } + } + + /** + * Aktualizuje šířky sloupců podle režimu zobrazení + */ + private void updateColumnWidths() { + // Zkontrolovat, že máme sloupce v modelu + if (fileTable.getColumnModel().getColumnCount() == 0) { + return; + } + + if (viewMode == ViewMode.FULL) { + if (fileTable.getColumnModel().getColumnCount() >= 3) { + fileTable.getColumnModel().getColumn(0).setPreferredWidth(300); + fileTable.getColumnModel().getColumn(1).setPreferredWidth(100); + fileTable.getColumnModel().getColumn(2).setPreferredWidth(150); + } + } else { + // V BRIEF režimu vypočítat šířku podle nejdelšího názvu + int columnCount = fileTable.getColumnCount(); + if (columnCount > 0 && fileTable.getColumnModel().getColumnCount() == columnCount) { + // Najít nejdelší název + String longestName = ""; + for (FileItem item : tableModel.items) { + String name = item.getName(); + if (name.length() > longestName.length()) { + longestName = name; + } + } + + // Vypočítat šířku sloupce podle nejdelšího názvu + // Použít FontMetrics pro přesnější výpočet + java.awt.FontMetrics fm = fileTable.getFontMetrics(fileTable.getFont()); + int columnWidth = fm.stringWidth(longestName.isEmpty() ? "WWWWWWWWWW" : longestName) + 30; // +30 pro padding + + // Nastavit všem sloupcům stejnou preferovanou šířku + for (int i = 0; i < columnCount; i++) { + fileTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidth); + } + } + } + } + + /** + * Aktualizuje stavový řádek + */ + private void updateStatus() { + // Spočítat označené položky + long totalSize = 0; + int fileCount = 0; + int dirCount = 0; + int markedCount = 0; + + // V BRIEF režimu musíme projít všechny items, ne podle řádků tabulky + if (viewMode == ViewMode.BRIEF) { + for (int i = 0; i < tableModel.items.size(); i++) { + FileItem item = tableModel.items.get(i); + if (item.isMarked() && !item.getName().equals("..")) { + markedCount++; + if (item.isDirectory()) { + dirCount++; + } else { + fileCount++; + totalSize += item.getSize(); + } + } + } + } else { + // V FULL režimu procházíme podle řádků + for (int i = 0; i < tableModel.getRowCount(); i++) { + FileItem item = tableModel.getItem(i); + if (item != null && item.isMarked() && !item.getName().equals("..")) { + markedCount++; + if (item.isDirectory()) { + dirCount++; + } else { + fileCount++; + totalSize += item.getSize(); + } + } + } + } + + if (markedCount > 0) { + // Zobrazit souhrn označených položek + statusLabel.setText(String.format(" Označeno: %d souborů, %d adresářů (%s)", + fileCount, dirCount, formatSize(totalSize))); + } else { + // Zobrazit informace o aktuálně vybrané položce + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + + if (item != null && !item.getName().equals("..")) { + if (item.isDirectory()) { + // Pro adresář zobrazit název a datum + statusLabel.setText(String.format(" [ADRESÁŘ] %s | %s", + item.getName(), + item.getFormattedDate())); + } else { + // Pro soubor zobrazit název, velikost a datum + statusLabel.setText(String.format(" %s | %s | %s", + item.getName(), + formatSize(item.getSize()), + item.getFormattedDate())); + } + } else { + // Zobrazit celkový počet položek + statusLabel.setText(String.format(" Položek: %d", tableModel.items.size())); + } + } else { + // Zobrazit celkový počet položek + statusLabel.setText(String.format(" Položek: %d", tableModel.items.size())); + } + } + } + + private String formatSize(long size) { + if (size < 1024) { + return size + " B"; + } else if (size < 1024 * 1024) { + return String.format("%.1f KB", size / 1024.0); + } else if (size < 1024 * 1024 * 1024) { + return String.format("%.1f MB", size / (1024.0 * 1024.0)); + } else { + return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); + } + } + + // Gettery + + public File getCurrentDirectory() { + return currentDirectory; + } + + public List getSelectedItems() { + List selected = new ArrayList<>(); + boolean hasMarkedItems = false; + + // Projít všechny položky a najít označené - musíme procházet přes items, ne přes řádky + for (FileItem item : tableModel.items) { + if (item.isMarked() && !item.getName().equals("..")) { + selected.add(item); + hasMarkedItems = true; + } + } + + // Pokud není nic označeno, vrátit aktuální vybraný řádek + if (!hasMarkedItems) { + int currentRow = fileTable.getSelectedRow(); + if (currentRow >= 0) { + FileItem item; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(currentRow, briefCurrentColumn); + } else { + item = tableModel.getItem(currentRow); + } + + if (item != null && !item.getName().equals("..")) { + selected.add(item); + } + } + } + + return selected; + } + + public JTable getFileTable() { + return fileTable; + } + + /** + * Model tabulky pro soubory + */ + private class FileTableModel extends AbstractTableModel { + + private final String[] fullColumnNames = {"Název", "Velikost", "Datum změny"}; + private List items = new ArrayList<>(); + private ViewMode currentViewMode = ViewMode.FULL; + private int briefColumns = 1; // Počet sloupců v BRIEF režimu + private int briefRowsPerColumn = 20; // Počet řádků na sloupec v BRIEF režimu + + public void setItems(List items) { + this.items = items; + if (currentViewMode == ViewMode.BRIEF) { + calculateBriefLayout(); + } + fireTableDataChanged(); + } + + public void updateViewMode(ViewMode mode) { + this.currentViewMode = mode; + if (mode == ViewMode.BRIEF) { + calculateBriefLayout(); + } + fireTableStructureChanged(); + } + + /** + * Vypočítá layout pro BRIEF režim + */ + public void calculateBriefLayout() { + if (items.size() == 0) { + briefColumns = 1; + briefRowsPerColumn = 1; + return; + } + + // Zjistit dostupnou výšku pro tabulku + int availableHeight = 0; + if (fileTable.getParent() != null) { + availableHeight = fileTable.getParent().getHeight(); + } + + if (availableHeight <= 0) { + availableHeight = 400; // Výchozí hodnota + } + + int rowHeight = fileTable.getRowHeight(); + if (rowHeight <= 0) { + rowHeight = 20; + } + + // Vypočítat maximální počet řádků na sloupec + briefRowsPerColumn = Math.max(1, availableHeight / rowHeight); + + // Vypočítat potřebný počet sloupců + briefColumns = (int) Math.ceil((double) items.size() / briefRowsPerColumn); + briefColumns = Math.max(1, briefColumns); + } + + public FileItem getItem(int row) { + if (currentViewMode == ViewMode.BRIEF) { + return getItemFromBriefLayout(row, 0); + } + if (row >= 0 && row < items.size()) { + return items.get(row); + } + return null; + } + + /** + * Získá položku z BRIEF layoutu podle řádku a sloupce + */ + public FileItem getItemFromBriefLayout(int row, int column) { + int index = column * briefRowsPerColumn + row; + if (index >= 0 && index < items.size()) { + return items.get(index); + } + return null; + } + + @Override + public int getRowCount() { + if (currentViewMode == ViewMode.BRIEF) { + return briefRowsPerColumn; + } + return items.size(); + } + + @Override + public int getColumnCount() { + if (currentViewMode == ViewMode.FULL) { + return fullColumnNames.length; + } else { + return briefColumns; + } + } + + @Override + public String getColumnName(int column) { + if (currentViewMode == ViewMode.FULL) { + return fullColumnNames[column]; + } else { + return ""; // V BRIEF režimu žádné názvy sloupců + } + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (currentViewMode == ViewMode.BRIEF) { + FileItem item = getItemFromBriefLayout(rowIndex, columnIndex); + return item != null ? item.getName() : ""; + } + + if (rowIndex >= items.size()) { + return null; + } + + FileItem item = items.get(rowIndex); + switch (columnIndex) { + case 0: + return item.getName(); + case 1: + return item.getFormattedSize(); + case 2: + return item.getFormattedDate(); + default: + return null; + } + } + } +} diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java new file mode 100644 index 0000000..990d7fb --- /dev/null +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -0,0 +1,1441 @@ +package com.kfmanager.ui; + +import com.kfmanager.model.FileItem; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * Single tab in a panel - displays the contents of one directory + */ +public class FilePanelTab extends JPanel { + + private File currentDirectory; + private JTable fileTable; + private FileTableModel tableModel; + private JLabel statusLabel; + private ViewMode viewMode = ViewMode.FULL; + private int briefCurrentColumn = 0; + private int briefColumnBeforeEnter = 0; + private Runnable onDirectoryChanged; + private Runnable onSwitchPanelRequested; + // Appearance customization + private Color selectionColor = new Color(184, 207, 229); + private Color selectionInactiveColor = new Color(220, 220, 220); + private Color markedColor = new Color(204, 153, 0); + // Sorting state for FULL mode header clicks + private int sortColumn = -1; // 0=name,1=size,2=date + private boolean sortAscending = true; + private com.kfmanager.config.AppConfig persistedConfig; + // If an archive was opened, we may extract it to a temp directory; track it so + // we can cleanup older temp directories when navigation changes. + private Path currentArchiveTempDir = null; + private File currentArchiveSourceFile = null; + + public FilePanelTab(String initialPath) { + this.currentDirectory = new File(initialPath); + initComponents(); + loadDirectory(currentDirectory); + } + + /** + * Ensure that column renderers are attached. Useful when the tab was just added to a container + * and the ColumnModel may not yet be in its final state. + */ + public void ensureRenderers() { + SwingUtilities.invokeLater(() -> { + updateColumnRenderers(); + updateColumnWidths(); + fileTable.revalidate(); + fileTable.repaint(); + }); + } + + // --- Appearance application methods --- + public void applyGlobalFont(Font font) { + if (font == null) return; + fileTable.setFont(font); + statusLabel.setFont(font); + // Update row height based on font metrics + FontMetrics fm = fileTable.getFontMetrics(font); + fileTable.setRowHeight(Math.max(18, fm.getHeight() + 4)); + // If in BRIEF mode, recalculate brief layout (like on resize) so columns/rows fit + SwingUtilities.invokeLater(() -> { + if (viewMode == ViewMode.BRIEF) { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + } else { + updateColumnWidths(); + } + ensureRenderers(); + fileTable.revalidate(); + fileTable.repaint(); + statusLabel.revalidate(); + statusLabel.repaint(); + }); + } + + public void applyBackgroundColor(Color bg) { + if (bg == null) return; + setBackground(bg); + fileTable.setBackground(bg); + statusLabel.setBackground(bg); + repaint(); + } + + public void applySelectionColor(Color sel) { + if (sel == null) return; + this.selectionColor = sel; + // Derive an inactive variant + this.selectionInactiveColor = sel.brighter(); + fileTable.repaint(); + } + + public void applyMarkedColor(Color mark) { + if (mark == null) return; + this.markedColor = mark; + fileTable.repaint(); + } + + /** + * Set callback to notify when directory changes + */ + public void setOnDirectoryChanged(Runnable callback) { + this.onDirectoryChanged = callback; + } + + public void setOnSwitchPanelRequested(Runnable callback) { + this.onSwitchPanelRequested = callback; + } + + private void initComponents() { + setLayout(new BorderLayout()); + + // Table showing files + tableModel = new FileTableModel(); + // Use a custom JTable subclass to intercept mouse events so that mouse-driven + // selection is disabled while still allowing double-click to open items. + // Also override mouse-motion processing to suppress drag events and + // prevent any drag-and-drop transfer handling. + fileTable = new JTable(tableModel) { + @Override + protected void processMouseEvent(java.awt.event.MouseEvent e) { + // Intercept mouse events to prevent changing selection via mouse. + // Allow single-click to toggle marking of an item, allow BRIEF column + // tracking, and preserve double-click to open items. Dragging/presses + // that would normally change selection are consumed. + if (e.getID() == java.awt.event.MouseEvent.MOUSE_CLICKED) { + int col = columnAtPoint(e.getPoint()); + int row = rowAtPoint(e.getPoint()); + + if (viewMode == ViewMode.BRIEF && col >= 0) { + briefCurrentColumn = col; + repaint(); + } + + // Single left-click should focus/select the item under cursor but + // should NOT toggle its marked state. This preserves keyboard + // marking (Insert) while making mouse clicks act as simple focus. + if (e.getClickCount() == 1 && javax.swing.SwingUtilities.isLeftMouseButton(e)) { + if (row >= 0) { + // Convert brief layout coordinates to absolute index where needed + if (viewMode == ViewMode.BRIEF) { + FileItem item = tableModel.getItemFromBriefLayout(row, col); + if (item != null) { + int index = tableModel.items.indexOf(item); + if (index >= 0) { + int selRow = index % tableModel.briefRowsPerColumn; + int selCol = index / tableModel.briefRowsPerColumn; + briefCurrentColumn = selCol; + fileTable.setRowSelectionInterval(selRow, selRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true)); + } + } + } else { + // FULL mode: rows map directly + fileTable.setRowSelectionInterval(row, row); + fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true)); + } + fileTable.requestFocusInWindow(); + repaint(); + updateStatus(); + } + } + + // Double-click opens the item under cursor (directories) + if (e.getClickCount() == 2) { + openItemAtPoint(e.getPoint()); + } + + // Consume to avoid default selection behavior (no selection change by mouse) + e.consume(); + return; + } + + // Ignore mouse pressed/released/dragged events to prevent selection changes + if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED || + e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED || + e.getID() == java.awt.event.MouseEvent.MOUSE_DRAGGED) { + e.consume(); + return; + } + + super.processMouseEvent(e); + } + @Override + protected void processMouseMotionEvent(java.awt.event.MouseEvent e) { + // Block mouse-dragged events so dragging cannot change selection + // or initiate any drag behavior. Consume the event and do nothing. + if (e.getID() == java.awt.event.MouseEvent.MOUSE_DRAGGED) { + e.consume(); + return; + } + super.processMouseMotionEvent(e); + } + }; + + // Disable Swing drag-and-drop support and transfer handling on the table + // to ensure no drag gestures or DnD occur. + fileTable.setDragEnabled(false); + fileTable.setTransferHandler(null); + try { + fileTable.setDropMode(null); + } catch (Exception ignore) { + // Some JVMs may not support null DropMode; ignore if unsupported. + } + // Prevent column reordering via header drag + if (fileTable.getTableHeader() != null) { + fileTable.getTableHeader().setReorderingAllowed(false); + // Set header renderer to show sort arrow when applicable + javax.swing.table.TableCellRenderer defaultHeaderRenderer = fileTable.getTableHeader().getDefaultRenderer(); + fileTable.getTableHeader().setDefaultRenderer(new SortHeaderRenderer(defaultHeaderRenderer)); + // Add header click listener to change sorting in FULL mode + fileTable.getTableHeader().addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + if (viewMode != ViewMode.FULL) return; + int col = fileTable.columnAtPoint(e.getPoint()); + if (col < 0) return; + // Toggle sort order if same column, otherwise set ascending + if (sortColumn == col) { + sortAscending = !sortAscending; + } else { + sortColumn = col; + // Default sort direction: for names ascending, for size/date show newest/largest first + if (col == 0) { + sortAscending = true; + } else { + sortAscending = false; + } + } + sortItemsByColumn(sortColumn, sortAscending); + tableModel.fireTableDataChanged(); + updateStatus(); + if (persistedConfig != null) { + persistedConfig.setDefaultSortColumn(sortColumn); + persistedConfig.setDefaultSortAscending(sortAscending); + persistedConfig.saveConfig(); + } + if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint(); + } + }); + } + fileTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + Font initialFont = new Font("Monospaced", Font.PLAIN, 12); + fileTable.setFont(initialFont); + // Set row height according to font metrics so rows scale with font size + FontMetrics fmInit = fileTable.getFontMetrics(initialFont); + fileTable.setRowHeight(Math.max(16, fmInit.getHeight() + 4)); + // Default auto-resize: in FULL mode we allow automatic column resizing, in BRIEF + // mode we will disable auto-resize so horizontal scrolling works and full names + // remain visible. + fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + + // Nastavit Cell Selection mode pouze v BRIEF režimu + fileTable.setCellSelectionEnabled(false); + fileTable.setRowSelectionAllowed(true); + fileTable.setColumnSelectionAllowed(false); + + // Odstranit bordery z tabulky + fileTable.setShowGrid(false); + fileTable.setIntercellSpacing(new java.awt.Dimension(0, 0)); + + // Nastavit pozadí tabulky stejné jako pozadí panelu + fileTable.setBackground(this.getBackground()); + fileTable.setOpaque(true); + + JScrollPane scrollPane = new JScrollPane(fileTable); + // Enable horizontal scrollbar when needed so BRIEF mode can scroll left-right + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + add(scrollPane, BorderLayout.CENTER); + + // Status bar + statusLabel = new JLabel(" "); + statusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + add(statusLabel, BorderLayout.SOUTH); + + // Add listener to handle panel resize + scrollPane.addComponentListener(new java.awt.event.ComponentAdapter() { + @Override + public void componentResized(java.awt.event.ComponentEvent e) { + if (viewMode == ViewMode.BRIEF) { + // Při změně velikosti v BRIEF módu přepočítat layout + SwingUtilities.invokeLater(() -> { + // Zapamatovat si vybranou položku + String selectedItemName = null; + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + if (item != null) { + selectedItemName = item.getName(); + } + } + + // Přepočítat layout + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + + // Znovu vybrat položku + if (selectedItemName != null) { + selectItemByName(selectedItemName); + } + }); + } + } + }); + + // Note: mouse handling is implemented inside the custom JTable above so + // that mouse-driven selection is disabled. + + // Setup custom key handling (Home/End etc.) + setupKeyListeners(); + + // Ensure TAB key switches panels even when multiple tabs are open. + // Map TAB to an action that asks the top-level window to switch panels. + fileTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_TAB, 0), "switchPanelFromTab"); + fileTable.getActionMap().put("switchPanelFromTab", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + if (onSwitchPanelRequested != null) { + onSwitchPanelRequested.run(); + } + } + }); + } + + private void setupKeyListeners() { + fileTable.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) { + openSelectedItem(); + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_BACK_SPACE) { + navigateUp(); + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_INSERT) { + toggleSelectionAndMoveDown(); + e.consume(); + } else if (viewMode == ViewMode.BRIEF) { + handleBriefKeyNavigation(e); + } else if (viewMode == ViewMode.FULL) { + // Support Home/End in FULL mode to jump to first/last row + if (e.getKeyCode() == java.awt.event.KeyEvent.VK_HOME) { + if (tableModel.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + updateStatus(); + } + e.consume(); + } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_END) { + int last = tableModel.getRowCount() - 1; + if (last >= 0) { + fileTable.setRowSelectionInterval(last, last); + fileTable.scrollRectToVisible(fileTable.getCellRect(last, 0, true)); + updateStatus(); + } + e.consume(); + } + } + } + }); + } + + private void handleBriefKeyNavigation(java.awt.event.KeyEvent e) { + switch (e.getKeyCode()) { + case java.awt.event.KeyEvent.VK_UP: + case java.awt.event.KeyEvent.VK_DOWN: + handleBriefNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_DOWN); + e.consume(); + break; + case java.awt.event.KeyEvent.VK_LEFT: + case java.awt.event.KeyEvent.VK_RIGHT: + handleBriefHorizontalNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_RIGHT); + e.consume(); + break; + case java.awt.event.KeyEvent.VK_HOME: + if (tableModel.items.size() > 0) { + briefCurrentColumn = 0; + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + fileTable.repaint(); + updateStatus(); + } + e.consume(); + break; + case java.awt.event.KeyEvent.VK_END: + if (tableModel.items.size() > 0) { + int lastIndex = tableModel.items.size() - 1; + int lastColumn = lastIndex / tableModel.briefRowsPerColumn; + int lastRow = lastIndex % tableModel.briefRowsPerColumn; + if (lastColumn < tableModel.getColumnCount()) { + briefCurrentColumn = lastColumn; + fileTable.setRowSelectionInterval(lastRow, lastRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, lastColumn, true)); + fileTable.repaint(); + updateStatus(); + } + } + e.consume(); + break; + case java.awt.event.KeyEvent.VK_PAGE_UP: + case java.awt.event.KeyEvent.VK_PAGE_DOWN: + handleBriefPageNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_PAGE_UP); + e.consume(); + break; + } + } + + private void handleBriefNavigation(boolean down) { + int currentRow = fileTable.getSelectedRow(); + if (currentRow < 0) return; + + int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow; + int newIndex = down ? currentIndex + 1 : currentIndex - 1; + + if (newIndex < 0 || newIndex >= tableModel.items.size()) { + return; + } + + int newColumn = newIndex / tableModel.briefRowsPerColumn; + int newRow = newIndex % tableModel.briefRowsPerColumn; + + if (newColumn < tableModel.getColumnCount()) { + briefCurrentColumn = newColumn; + fileTable.setRowSelectionInterval(newRow, newRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true)); + fileTable.repaint(); + updateStatus(); + } + } + + private void handleBriefHorizontalNavigation(boolean right) { + int currentRow = fileTable.getSelectedRow(); + if (currentRow < 0) return; + + int newColumn = right ? briefCurrentColumn + 1 : briefCurrentColumn - 1; + int colCount = tableModel.getColumnCount(); + + // If moving beyond edges, jump to first/last item instead of doing nothing + if (newColumn < 0 || newColumn >= colCount) { + if (right && briefCurrentColumn >= colCount - 1) { + // Jump to last item + int lastIndex = Math.max(0, tableModel.items.size() - 1); + int lastCol = lastIndex / tableModel.briefRowsPerColumn; + int lastRow = lastIndex % tableModel.briefRowsPerColumn; + briefCurrentColumn = Math.min(lastCol, Math.max(0, colCount - 1)); + fileTable.setRowSelectionInterval(lastRow, lastRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, briefCurrentColumn, true)); + fileTable.repaint(); + updateStatus(); + } else if (!right && briefCurrentColumn <= 0) { + // Jump to first item + briefCurrentColumn = 0; + if (tableModel.items.size() > 0) { + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + } + fileTable.repaint(); + updateStatus(); + } + return; + } + + FileItem item = tableModel.getItemFromBriefLayout(currentRow, newColumn); + int targetRow = currentRow; + + if (item == null) { + for (int row = currentRow - 1; row >= 0; row--) { + item = tableModel.getItemFromBriefLayout(row, newColumn); + if (item != null) { + targetRow = row; + break; + } + } + if (item == null) return; + } + + briefCurrentColumn = newColumn; + fileTable.setRowSelectionInterval(targetRow, targetRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(targetRow, briefCurrentColumn, true)); + fileTable.repaint(); + updateStatus(); + } + + private void handleBriefPageNavigation(boolean pageUp) { + int currentRow = fileTable.getSelectedRow(); + if (currentRow < 0) return; + + int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow; + + Rectangle visible = fileTable.getVisibleRect(); + int rowHeight = fileTable.getRowHeight(); + int visibleRows = visible.height / rowHeight; + if (visibleRows <= 0) visibleRows = 1; + + // Compute how many columns are visible in the current viewport (from left-most visible column) + int visibleColumns = 1; + try { + int colCount = fileTable.getColumnModel().getColumnCount(); + // Find first visible column by using columnAtPoint on the left edge of the visible rect + int firstVisibleCol = fileTable.columnAtPoint(new Point(visible.x, visible.y)); + if (firstVisibleCol < 0) firstVisibleCol = 0; + + int acc = 0; + for (int c = firstVisibleCol; c < colCount; c++) { + int w = fileTable.getColumnModel().getColumn(c).getWidth(); + // Determine the column's starting X by summing widths of prior columns + acc += w; + // If accumulated width (relative to firstVisibleCol) exceeds viewport width, stop + if (acc > visible.width) { + break; + } + visibleColumns = c - firstVisibleCol + 1; + } + } catch (Exception ex) { + visibleColumns = Math.max(1, fileTable.getColumnModel().getColumnCount()); + } + + if (visibleColumns <= 0) visibleColumns = 1; + + int visibleItems = visibleRows * visibleColumns; + if (visibleItems <= 0) visibleItems = visibleRows; + + int newIndex = pageUp ? currentIndex - visibleItems : currentIndex + visibleItems; + newIndex = Math.max(0, Math.min(newIndex, tableModel.items.size() - 1)); + + int newColumn = newIndex / tableModel.briefRowsPerColumn; + int newRow = newIndex % tableModel.briefRowsPerColumn; + + // Clamp newColumn to available columns + int maxCols = tableModel.getColumnCount(); + if (newColumn >= maxCols) { + newColumn = Math.max(0, maxCols - 1); + // Recompute newRow to fit within that column + int baseIndex = newColumn * tableModel.briefRowsPerColumn; + newRow = Math.min(tableModel.briefRowsPerColumn - 1, newIndex - baseIndex); + } + + briefCurrentColumn = newColumn; + fileTable.setRowSelectionInterval(newRow, newRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true)); + fileTable.repaint(); + updateStatus(); + } + + public void loadDirectory(File directory) { + loadDirectory(directory, true); + } + + private void loadDirectory(File directory, boolean autoSelectFirst) { + // If we are switching directories, cleanup any previously extracted archive temp dirs + cleanupArchiveTempDirIfNeeded(directory); + if (directory == null || !directory.isDirectory()) { + return; + } + + this.currentDirectory = directory; + briefCurrentColumn = 0; + + File[] files = directory.listFiles(); + List items = new ArrayList<>(); + + File parent = directory.getParentFile(); + if (parent != null) { + items.add(new FileItem(parent) { + @Override + public String getName() { + return ".."; + } + }); + } + + if (files != null && files.length > 0) { + Arrays.sort(files, Comparator + .comparing((File f) -> !f.isDirectory()) + .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + + for (File file : files) { + items.add(new FileItem(file)); + } + } + + tableModel.setItems(items); + + if (viewMode == ViewMode.BRIEF) { + boolean selectFirst = autoSelectFirst; + SwingUtilities.invokeLater(() -> { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + fileTable.revalidate(); + fileTable.repaint(); + + if (selectFirst && fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true)); + } + }); + } else { + if (autoSelectFirst && fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + } + } + + updateStatus(); + + // Oznámit změnu adresáře + if (onDirectoryChanged != null) { + onDirectoryChanged.run(); + } + } + + /** + * Cleanup previous archive temp dir when navigating away from it. + */ + private void cleanupArchiveTempDirIfNeeded(File newDirectory) { + try { + if (currentArchiveTempDir != null) { + Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null; + if (newPath == null || !newPath.startsWith(currentArchiveTempDir)) { + deleteTempDirRecursively(currentArchiveTempDir); + currentArchiveTempDir = null; + } + } + } catch (Exception ignore) { + // best-effort cleanup + } + } + + private boolean isArchiveFile(File f) { + if (f == null) return false; + String n = f.getName().toLowerCase(); + return n.endsWith(".zip") || n.endsWith(".jar"); + } + + private Path extractArchiveToTemp(File archive) { + if (archive == null || !archive.isFile()) return null; + try { + Path tempDir = Files.createTempDirectory("kfmanager-archive-"); + + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(archive.toPath()))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String entryName = entry.getName(); + // Normalize entry name and prevent zip-slip + Path resolved = tempDir.resolve(entryName).normalize(); + if (!resolved.startsWith(tempDir)) { + // suspicious entry, skip + zis.closeEntry(); + continue; + } + + if (entry.isDirectory() || entryName.endsWith("/")) { + Files.createDirectories(resolved); + } else { + Path parent = resolved.getParent(); + if (parent != null) Files.createDirectories(parent); + // Copy entry contents + Files.copy(zis, resolved, StandardCopyOption.REPLACE_EXISTING); + } + zis.closeEntry(); + } + } + + return tempDir; + } catch (IOException ex) { + // extraction failed; attempt best-effort cleanup + try { + if (currentArchiveTempDir != null) deleteTempDirRecursively(currentArchiveTempDir); + } catch (Exception ignore) {} + return null; + } + } + + private void deleteTempDirRecursively(Path dir) { + if (dir == null) return; + try { + if (!Files.exists(dir)) return; + Files.walk(dir) + .sorted(java.util.Comparator.reverseOrder()) + .forEach(p -> { + try { Files.deleteIfExists(p); } catch (Exception ignore) {} + }); + } catch (IOException ignore) { + } + } + + private void openSelectedItem() { + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + + if (item == null) return; + + if (item.getName().equals("..")) { + navigateUp(); + } else if (isArchiveFile(item.getFile())) { + Path temp = extractArchiveToTemp(item.getFile()); + if (temp != null) { + // Delete any previous temp dir if different + try { + if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) { + deleteTempDirRecursively(currentArchiveTempDir); + } + } catch (Exception ignore) {} + currentArchiveTempDir = temp; + currentArchiveSourceFile = item.getFile(); + loadDirectory(temp.toFile()); + } + } else if (item.isDirectory()) { + briefColumnBeforeEnter = briefCurrentColumn; + loadDirectory(item.getFile()); + } + } + } + + /** + * Open the item located at the given point (used for double-clicks while + * mouse-driven selection is blocked). This mirrors the behavior of + * openSelectedItem but works from a mouse location instead of the table + * selection. + */ + private void openItemAtPoint(Point p) { + int row = fileTable.rowAtPoint(p); + int col = fileTable.columnAtPoint(p); + if (row < 0) return; + + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(row, col); + } else { + item = tableModel.getItem(row); + } + + if (item == null) return; + + if (item.getName().equals("..")) { + navigateUp(); + } else if (isArchiveFile(item.getFile())) { + Path temp = extractArchiveToTemp(item.getFile()); + if (temp != null) { + try { + if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) { + deleteTempDirRecursively(currentArchiveTempDir); + } + } catch (Exception ignore) {} + currentArchiveTempDir = temp; + currentArchiveSourceFile = item.getFile(); + loadDirectory(temp.toFile()); + } + } else if (item.isDirectory()) { + briefColumnBeforeEnter = briefCurrentColumn; + loadDirectory(item.getFile()); + } + } + + public void navigateUp() { + // If we're currently browsing an extracted archive root, navigate back to the + // original archive's parent and select the archive file. + try { + if (currentArchiveTempDir != null && currentArchiveSourceFile != null) { + Path cur = currentDirectory.toPath().toAbsolutePath().normalize(); + if (cur.equals(currentArchiveTempDir)) { + File parent = currentArchiveSourceFile.getParentFile(); + if (parent != null) { + String archiveName = currentArchiveSourceFile.getName(); + // cleanup temp dir before switching back + deleteTempDirRecursively(currentArchiveTempDir); + currentArchiveTempDir = null; + currentArchiveSourceFile = null; + loadDirectory(parent, false); + + if (viewMode == ViewMode.BRIEF) { + SwingUtilities.invokeLater(() -> { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + selectItemByName(archiveName); + }); + } else { + selectItemByName(archiveName); + } + } + return; + } + } + } catch (Exception ignore) {} + + File parent = currentDirectory.getParentFile(); + if (parent != null) { + String previousDirName = currentDirectory.getName(); + loadDirectory(parent, false); + + if (viewMode == ViewMode.BRIEF) { + SwingUtilities.invokeLater(() -> { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + updateColumnRenderers(); + updateColumnWidths(); + selectItemByName(previousDirName); + }); + } else { + selectItemByName(previousDirName); + } + } + } + + private void selectItemByName(String name) { + if (viewMode == ViewMode.BRIEF) { + for (int i = 0; i < tableModel.items.size(); i++) { + FileItem item = tableModel.items.get(i); + if (item.getName().equals(name)) { + int column = i / tableModel.briefRowsPerColumn; + int row = i % tableModel.briefRowsPerColumn; + + if (column < tableModel.getColumnCount()) { + briefCurrentColumn = column; + fileTable.setRowSelectionInterval(row, row); + fileTable.scrollRectToVisible(fileTable.getCellRect(row, column, true)); + fileTable.repaint(); + updateStatus(); + } + return; + } + } + } else { + for (int i = 0; i < tableModel.getRowCount(); i++) { + FileItem item = tableModel.getItem(i); + if (item != null && item.getName().equals(name)) { + fileTable.setRowSelectionInterval(i, i); + fileTable.scrollRectToVisible(fileTable.getCellRect(i, 0, true)); + updateStatus(); + return; + } + } + } + } + + public void toggleSelectionAndMoveDown() { + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + + if (item != null && !item.getName().equals("..")) { + item.setMarked(!item.isMarked()); + fileTable.repaint(); + + if (viewMode == ViewMode.BRIEF) { + handleBriefNavigation(true); + } else { + int nextRow = selectedRow + 1; + if (nextRow < fileTable.getRowCount()) { + fileTable.setRowSelectionInterval(nextRow, nextRow); + fileTable.scrollRectToVisible(fileTable.getCellRect(nextRow, 0, true)); + } + } + updateStatus(); + } + } + } + + public List getSelectedItems() { + List selected = new ArrayList<>(); + for (FileItem item : tableModel.items) { + if (item.isMarked() && !item.getName().equals("..")) { + selected.add(item); + } + } + + if (selected.isEmpty()) { + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + if (item != null && !item.getName().equals("..")) { + selected.add(item); + } + } + } + + return selected; + } + + public void setViewMode(ViewMode mode) { + if (this.viewMode != mode) { + String selectedItemName = null; + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = null; + if (this.viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + if (item != null) { + selectedItemName = item.getName(); + } + } + + this.viewMode = mode; + final String itemToSelect = selectedItemName; + + SwingUtilities.invokeLater(() -> { + tableModel.updateViewMode(mode); + // Switch auto-resize behavior depending on mode so BRIEF can scroll horizontally + if (mode == ViewMode.BRIEF) { + fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + } else { + fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + } + // Hide table header in BRIEF mode to save vertical space and match requirements + if (fileTable.getTableHeader() != null) { + fileTable.getTableHeader().setVisible(mode != ViewMode.BRIEF); + } + briefCurrentColumn = 0; + updateColumnRenderers(); + updateColumnWidths(); + fileTable.revalidate(); + fileTable.repaint(); + + if (itemToSelect != null) { + selectItemByName(itemToSelect); + } else if (fileTable.getRowCount() > 0) { + fileTable.setRowSelectionInterval(0, 0); + } + + fileTable.requestFocusInWindow(); + }); + } + } + + private void updateColumnRenderers() { + int columnCount = tableModel.getColumnCount(); + if (columnCount == 0 || fileTable.getColumnModel().getColumnCount() != columnCount) { + // Sloupce tabulky se možná ještě nepřevedly po změně struktury. + // Zkusíme to znovu asynchronně po krátkém odložení. + SwingUtilities.invokeLater(() -> updateColumnRenderers()); + return; + } + + for (int i = 0; i < columnCount; i++) { + final int colIndex = i; + DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(row, column); + } else { + item = tableModel.getItem(row); + } + + if (item == null) { + setBackground(FilePanelTab.this.getBackground()); + setText(""); + setIcon(null); + return this; + } + + // Prepare displayed text: wrap directory names in square brackets + String displayText = ""; + if (viewMode == ViewMode.BRIEF) { + displayText = item.getName(); + if (item.isDirectory()) { + displayText = "[" + displayText + "]"; + } + } else { + // FULL mode: only the first column shows the name + if (column == 0) { + displayText = item.getName(); + if (item.isDirectory()) { + displayText = "[" + displayText + "]"; + } + } else if (column == 1) { + displayText = item.getFormattedSize(); + } else if (column == 2) { + displayText = item.getFormattedDate(); + } else { + displayText = item.getName(); + } + } + setText(displayText); + + boolean isMarked = item.isMarked(); + boolean isCurrentCell = false; + if (viewMode == ViewMode.BRIEF) { + isCurrentCell = isSelected && column == briefCurrentColumn; + } else { + isCurrentCell = isSelected; + } + + if (isCurrentCell && table.hasFocus()) { + setBackground(selectionColor); + } else if (isCurrentCell) { + setBackground(selectionInactiveColor); + } else { + setBackground(FilePanelTab.this.getBackground()); + } + + // Preserve the table's base font/style and only add the marked-bold + Font baseFont = table.getFont(); + if (baseFont == null) baseFont = getFont(); + int baseStyle = baseFont != null ? baseFont.getStyle() : Font.PLAIN; + if (isMarked) { + // Ensure marked items are at least bold, preserve italic if present + int newStyle = baseStyle | Font.BOLD; + setFont(baseFont.deriveFont(newStyle)); + setForeground(markedColor); + } else { + // Preserve whatever style the base font has (do not force plain) + setFont(baseFont.deriveFont(baseStyle)); + setForeground(Color.BLACK); + } + + // Zobrazit ikonu pro názvy souborů + Icon icon = item.getIcon(); + if (viewMode == ViewMode.BRIEF) { + // V BRIEF módu jsou všechny sloupce názvy + setIcon(icon); + } else if (viewMode == ViewMode.FULL && column == 0) { + // V FULL módu je ikona pouze v prvním sloupci (názvy) + setIcon(icon); + } else { + setIcon(null); + } + + setBorder(null); + return this; + } + }; + + if (viewMode == ViewMode.FULL && colIndex == 1) { + renderer.setHorizontalAlignment(SwingConstants.RIGHT); + } + + fileTable.getColumnModel().getColumn(i).setCellRenderer(renderer); + } + } + + private void updateColumnWidths() { + if (viewMode == ViewMode.FULL) { + if (fileTable.getColumnModel().getColumnCount() == 3) { + fileTable.getColumnModel().getColumn(0).setPreferredWidth(300); + fileTable.getColumnModel().getColumn(1).setPreferredWidth(100); + fileTable.getColumnModel().getColumn(2).setPreferredWidth(150); + } + } else if (viewMode == ViewMode.BRIEF) { + int columnCount = tableModel.getColumnCount(); + if (columnCount == 0) { + return; + } + + // Pokud se ColumnModel ještě nepřizpůsobil nové struktuře (např. po fireTableStructureChanged()), + // počkej a zkus nastavit renderery později - jinak nové sloupce nedostanou renderer a ikony se + // neprojeví v FULL módu. + if (fileTable.getColumnModel().getColumnCount() != columnCount) { + SwingUtilities.invokeLater(() -> updateColumnRenderers()); + return; + } + + // Turn off auto-resize so preferred widths are honored and horizontal scrolling appears + fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + + // Compute the preferred column width based on the longest displayed + // name in the current directory (include brackets for directories) + // plus the widest icon width and some padding. + java.awt.FontMetrics fm = fileTable.getFontMetrics(fileTable.getFont()); + int maxTextWidth = 0; + int maxIconWidth = 0; + + for (FileItem item : tableModel.items) { + if (item == null) continue; + String display = item.getName(); + if (item.isDirectory()) { + display = "[" + display + "]"; + } + int w = fm.stringWidth(display); + if (w > maxTextWidth) maxTextWidth = w; + + Icon icon = item.getIcon(); + if (icon != null) { + int iw = icon.getIconWidth(); + if (iw > maxIconWidth) maxIconWidth = iw; + } + } + + if (maxTextWidth == 0) { + maxTextWidth = fm.stringWidth("WWWWWWWWWW"); + } + + // Add icon width and padding (left/right + gap between icon and text) + int padding = 36; // reasonable extra space + int columnWidth = maxTextWidth + maxIconWidth + padding; + + for (int i = 0; i < columnCount; i++) { + fileTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidth); + } + } + } + + private void updateStatus() { + long totalSize = 0; + int fileCount = 0; + int dirCount = 0; + int markedCount = 0; + + if (viewMode == ViewMode.BRIEF) { + for (int i = 0; i < tableModel.items.size(); i++) { + FileItem item = tableModel.items.get(i); + if (item.isMarked() && !item.getName().equals("..")) { + markedCount++; + if (item.isDirectory()) { + dirCount++; + } else { + fileCount++; + totalSize += item.getSize(); + } + } + } + } else { + for (int i = 0; i < tableModel.getRowCount(); i++) { + FileItem item = tableModel.getItem(i); + if (item != null && item.isMarked() && !item.getName().equals("..")) { + markedCount++; + if (item.isDirectory()) { + dirCount++; + } else { + fileCount++; + totalSize += item.getSize(); + } + } + } + } + + if (markedCount > 0) { + statusLabel.setText(String.format(" Označeno: %d souborů, %d adresářů (%s)", + fileCount, dirCount, formatSize(totalSize))); + } else { + int selectedRow = fileTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = null; + if (viewMode == ViewMode.BRIEF) { + item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn); + } else { + item = tableModel.getItem(selectedRow); + } + + if (item != null && !item.getName().equals("..")) { + if (item.isDirectory()) { + // Always display directory names in square brackets + statusLabel.setText(String.format(" [%s] | %s", + item.getName(), + item.getFormattedDate())); + } else { + statusLabel.setText(String.format(" %s | %s | %s", + item.getName(), + formatSize(item.getSize()), + item.getFormattedDate())); + } + } else { + statusLabel.setText(String.format(" Položek: %d", tableModel.items.size())); + } + } else { + statusLabel.setText(String.format(" Položek: %d", tableModel.items.size())); + } + } + } + + /** + * Sort items in the current table model according to column (FULL mode). + * column: 0=name, 1=size, 2=date + */ + private void sortItemsByColumn(int column, boolean asc) { + if (tableModel == null || tableModel.items == null) return; + java.util.List items = tableModel.items; + if (items.isEmpty()) return; + + java.util.Comparator comp; + switch (column) { + case 1: // size + comp = (a, b) -> { + boolean da = a.isDirectory(), db = b.isDirectory(); + if (da != db) return da ? -1 : 1; + if (da && db) return a.getName().compareToIgnoreCase(b.getName()); + int r = Long.compare(a.getSize(), b.getSize()); + if (r == 0) r = a.getName().compareToIgnoreCase(b.getName()); + return r; + }; + break; + case 2: // date + // Sort by modification time regardless of directory flag so "newest first" places the newest item + comp = (a, b) -> { + int r = Long.compare(a.getModified().getTime(), b.getModified().getTime()); + if (r == 0) r = a.getName().compareToIgnoreCase(b.getName()); + return r; + }; + break; + default: // name + comp = (a, b) -> { + boolean da = a.isDirectory(), db = b.isDirectory(); + if (da != db) return da ? -1 : 1; + return a.getName().compareToIgnoreCase(b.getName()); + }; + break; + } + + if (!asc) comp = comp.reversed(); + + items.sort(comp); + + // Refresh table on EDT + SwingUtilities.invokeLater(() -> { + if (viewMode == ViewMode.BRIEF) { + tableModel.calculateBriefLayout(); + tableModel.fireTableStructureChanged(); + } else { + tableModel.fireTableDataChanged(); + } + updateColumnRenderers(); + updateColumnWidths(); + if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint(); + }); + } + + /** Header renderer that decorates the column title with an arrow for the sort column/direction */ + private class SortHeaderRenderer implements javax.swing.table.TableCellRenderer { + private final javax.swing.table.TableCellRenderer delegate; + + public SortHeaderRenderer(javax.swing.table.TableCellRenderer delegate) { + this.delegate = delegate; + } + + @Override + public java.awt.Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) { + java.awt.Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (c instanceof javax.swing.JLabel) { + javax.swing.JLabel lbl = (javax.swing.JLabel) c; + String txt = value != null ? value.toString() : ""; + if (sortColumn == column && fileTable.getTableHeader().isVisible()) { + String arrow = sortAscending ? " ▲" : " ▼"; + lbl.setText(txt + arrow); + } else { + lbl.setText(txt); + } + } + return c; + } + } + + /** + * Provide AppConfig so this tab can persist and restore sort settings. + */ + public void setAppConfig(com.kfmanager.config.AppConfig cfg) { + this.persistedConfig = cfg; + // Apply persisted sort if present + if (cfg != null) { + int col = cfg.getDefaultSortColumn(); + boolean asc = cfg.getDefaultSortAscending(); + if (col >= 0) { + this.sortColumn = col; + this.sortAscending = asc; + sortItemsByColumn(sortColumn, sortAscending); + SwingUtilities.invokeLater(() -> { + tableModel.fireTableDataChanged(); + if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint(); + }); + } + } + } + + private String formatSize(long size) { + if (size < 1024) { + return size + " B"; + } else if (size < 1024 * 1024) { + return String.format("%.1f KB", size / 1024.0); + } else if (size < 1024 * 1024 * 1024) { + return String.format("%.1f MB", size / (1024.0 * 1024.0)); + } else { + return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); + } + } + + // Gettery + public JTable getFileTable() { + return fileTable; + } + + public File getCurrentDirectory() { + return currentDirectory; + } + + public ViewMode getViewMode() { + return viewMode; + } + + // FileTableModel - stejný jako v původním FilePanel + private class FileTableModel extends AbstractTableModel { + private List items = new ArrayList<>(); + private String[] columnNames = {"Název", "Velikost", "Datum"}; + public int briefColumns = 1; + public int briefRowsPerColumn = 10; + + public void setItems(List items) { + this.items = items; + if (viewMode == ViewMode.BRIEF) { + calculateBriefLayout(); + } + fireTableDataChanged(); + } + + public void updateViewMode(ViewMode mode) { + if (mode == ViewMode.BRIEF) { + calculateBriefLayout(); + } + fireTableStructureChanged(); + } + + public void calculateBriefLayout() { + if (items.isEmpty()) { + briefColumns = 1; + briefRowsPerColumn = 1; + return; + } + + int availableHeight = fileTable.getParent().getHeight(); + int rowHeight = fileTable.getRowHeight(); + + if (availableHeight <= 0 || rowHeight <= 0) { + briefRowsPerColumn = Math.max(1, items.size()); + briefColumns = 1; + return; + } + + briefRowsPerColumn = Math.max(1, availableHeight / rowHeight); + briefColumns = (int) Math.ceil((double) items.size() / briefRowsPerColumn); + } + + public FileItem getItemFromBriefLayout(int row, int column) { + if (viewMode != ViewMode.BRIEF) { + return getItem(row); + } + + int index = column * briefRowsPerColumn + row; + if (index >= 0 && index < items.size()) { + return items.get(index); + } + return null; + } + + @Override + public int getRowCount() { + if (viewMode == ViewMode.BRIEF) { + return briefRowsPerColumn; + } + return items.size(); + } + + @Override + public int getColumnCount() { + if (viewMode == ViewMode.BRIEF) { + return briefColumns; + } + return 3; + } + + @Override + public String getColumnName(int column) { + if (viewMode == ViewMode.BRIEF) { + return ""; + } + return columnNames[column]; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (viewMode == ViewMode.BRIEF) { + FileItem item = getItemFromBriefLayout(rowIndex, columnIndex); + return item != null ? item.getName() : ""; + } + + FileItem item = getItem(rowIndex); + if (item == null) { + return ""; + } + + switch (columnIndex) { + case 0: return item.getName(); + case 1: return item.getFormattedSize(); + case 2: return item.getFormattedDate(); + default: return ""; + } + } + + public FileItem getItem(int index) { + if (index >= 0 && index < items.size()) { + return items.get(index); + } + return null; + } + } +} diff --git a/src/main/java/com/kfmanager/ui/FontChooserDialog.java b/src/main/java/com/kfmanager/ui/FontChooserDialog.java new file mode 100644 index 0000000..be998d9 --- /dev/null +++ b/src/main/java/com/kfmanager/ui/FontChooserDialog.java @@ -0,0 +1,192 @@ +package com.kfmanager.ui; + +import javax.swing.*; +import java.awt.*; + +/** + * Dialog pro výběr fontu + */ +public class FontChooserDialog extends JDialog { + private Font selectedFont; + private boolean approved = false; + + private JList fontList; + private JList sizeList; + private JList styleList; + private JTextArea previewArea; + + public FontChooserDialog(Window parent, Font initialFont) { + super(parent, "Výběr fontu", ModalityType.APPLICATION_MODAL); + this.selectedFont = initialFont; + + initComponents(); + selectFont(initialFont); + + setSize(600, 400); + setLocationRelativeTo(parent); + } + + private void initComponents() { + setLayout(new BorderLayout(10, 10)); + + // Panel pro výběr (font, velikost, styl) + JPanel selectionPanel = new JPanel(new GridLayout(1, 3, 10, 0)); + + // Seznam fontů + JPanel fontPanel = new JPanel(new BorderLayout()); + fontPanel.add(new JLabel("Font:"), BorderLayout.NORTH); + + String[] availableFonts = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getAvailableFontFamilyNames(); + fontList = new JList<>(availableFonts); + fontList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + fontList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + updatePreview(); + } + }); + + JScrollPane fontScroll = new JScrollPane(fontList); + fontPanel.add(fontScroll, BorderLayout.CENTER); + + // Seznam velikostí + JPanel sizePanel = new JPanel(new BorderLayout()); + sizePanel.add(new JLabel("Velikost:"), BorderLayout.NORTH); + + Integer[] sizes = {8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 26, 28, 32, 36, 40, 48, 56, 64, 72}; + sizeList = new JList<>(sizes); + sizeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + sizeList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + updatePreview(); + } + }); + + JScrollPane sizeScroll = new JScrollPane(sizeList); + sizePanel.add(sizeScroll, BorderLayout.CENTER); + + // Seznam stylů (Plain, Bold, Italic, Bold Italic) + JPanel stylePanel = new JPanel(new BorderLayout()); + stylePanel.add(new JLabel("Styl:"), BorderLayout.NORTH); + + String[] styles = {"Plain", "Bold", "Italic", "Bold Italic"}; + styleList = new JList<>(styles); + styleList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + styleList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + updatePreview(); + } + }); + JScrollPane styleScroll = new JScrollPane(styleList); + stylePanel.add(styleScroll, BorderLayout.CENTER); + + selectionPanel.add(fontPanel); + selectionPanel.add(sizePanel); + selectionPanel.add(stylePanel); + + // Preview oblast + JPanel previewPanel = new JPanel(new BorderLayout()); + previewPanel.setBorder(BorderFactory.createTitledBorder("Náhled")); + + previewArea = new JTextArea(); + previewArea.setText("AaBbCcDdEeFfGgHhIiJjKkLl\n0123456789\nThe quick brown fox jumps over the lazy dog"); + previewArea.setEditable(false); + previewArea.setLineWrap(true); + previewArea.setWrapStyleWord(true); + + JScrollPane previewScroll = new JScrollPane(previewArea); + previewScroll.setPreferredSize(new Dimension(0, 120)); + previewPanel.add(previewScroll, BorderLayout.CENTER); + + // Tlačítka + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + JButton okButton = new JButton("OK"); + okButton.addActionListener(e -> { + approved = true; + dispose(); + }); + + JButton cancelButton = new JButton("Zrušit"); + cancelButton.addActionListener(e -> { + approved = false; + dispose(); + }); + + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + + // Layout + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + mainPanel.add(selectionPanel, BorderLayout.CENTER); + mainPanel.add(previewPanel, BorderLayout.SOUTH); + + add(mainPanel, BorderLayout.CENTER); + add(buttonPanel, BorderLayout.SOUTH); + } + + private void selectFont(Font font) { + // Najít a vybrat font + fontList.setSelectedValue(font.getName(), true); + + // Najít a vybrat velikost + sizeList.setSelectedValue(font.getSize(), true); + + // Najít a vybrat styl + int style = font.getStyle(); + switch (style) { + case Font.BOLD: + styleList.setSelectedValue("Bold", true); + break; + case Font.ITALIC: + styleList.setSelectedValue("Italic", true); + break; + case Font.BOLD | Font.ITALIC: + styleList.setSelectedValue("Bold Italic", true); + break; + default: + styleList.setSelectedValue("Plain", true); + break; + } + + updatePreview(); + } + + private void updatePreview() { + String fontName = fontList.getSelectedValue(); + Integer fontSize = sizeList.getSelectedValue(); + String styleName = styleList.getSelectedValue(); + + if (fontName != null && fontSize != null && styleName != null) { + int style = Font.PLAIN; + if ("Bold".equals(styleName)) style = Font.BOLD; + else if ("Italic".equals(styleName)) style = Font.ITALIC; + else if ("Bold Italic".equals(styleName)) style = Font.BOLD | Font.ITALIC; + + selectedFont = new Font(fontName, style, fontSize); + previewArea.setFont(selectedFont); + } + } + + public Font getSelectedFont() { + return selectedFont; + } + + public boolean isApproved() { + return approved; + } + + /** + * Zobrazí dialog a vrátí vybraný font, nebo null pokud byl zrušen + */ + public static Font showDialog(Window parent, Font initialFont) { + FontChooserDialog dialog = new FontChooserDialog(parent, initialFont); + dialog.setVisible(true); + + if (dialog.isApproved()) { + return dialog.getSelectedFont(); + } + return null; + } +} diff --git a/src/main/java/com/kfmanager/ui/MainWindow.java b/src/main/java/com/kfmanager/ui/MainWindow.java new file mode 100644 index 0000000..6f06623 --- /dev/null +++ b/src/main/java/com/kfmanager/ui/MainWindow.java @@ -0,0 +1,901 @@ +package com.kfmanager.ui; + +import com.kfmanager.config.AppConfig; +import com.kfmanager.model.FileItem; +import com.kfmanager.service.FileOperations; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.io.File; +import java.util.List; + + /** + * Main application window with two panels + */ +public class MainWindow extends JFrame { + + private FilePanel leftPanel; + private FilePanel rightPanel; + private FilePanel activePanel; + private JPanel buttonPanel; + private AppConfig config; + + public MainWindow() { + super("KF File Manager"); + + // Load configuration + config = new AppConfig(); + + initComponents(); + setupKeyBindings(); + // Apply appearance from saved configuration at startup + applyAppearanceSettings(); + + // Restore window size and position from configuration + config.restoreWindowState(this); + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + // Add listener to save configuration on exit + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + MainWindow.this.saveConfigAndExit(); + } + }); + + // After start, set focus and selection to the left panel + SwingUtilities.invokeLater(() -> { + leftPanel.getFileTable().requestFocus(); + }); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + // Toolbar + createToolBar(); + + // Panel containing the two file panels + JPanel mainPanel = new JPanel(new GridLayout(1, 2, 5, 0)); + + // Left panel - load path and ViewMode from configuration + String leftPath = config.getLeftPanelPath(); + leftPanel = new FilePanel(leftPath); + leftPanel.setAppConfig(config); + leftPanel.setBorder(BorderFactory.createTitledBorder("Left panel")); + // Provide a callback so tabs inside the panel can request switching panels with TAB + leftPanel.setSwitchPanelCallback(() -> switchPanelsFromChild()); + + // Load and set ViewMode for left panel + try { + ViewMode leftViewMode = ViewMode.valueOf(config.getLeftPanelViewMode()); + leftPanel.setViewMode(leftViewMode); + } catch (IllegalArgumentException e) { + // Výchozí hodnota FULL je již nastavena + } + + // Right panel - load path and ViewMode from configuration + String rightPath = config.getRightPanelPath(); + rightPanel = new FilePanel(rightPath); + rightPanel.setAppConfig(config); + rightPanel.setBorder(BorderFactory.createTitledBorder("Right panel")); + // Provide a callback so tabs inside the panel can request switching panels with TAB + rightPanel.setSwitchPanelCallback(() -> switchPanelsFromChild()); + + // Load and set ViewMode for right panel + try { + ViewMode rightViewMode = ViewMode.valueOf(config.getRightPanelViewMode()); + rightPanel.setViewMode(rightViewMode); + } catch (IllegalArgumentException e) { + // Výchozí hodnota FULL je již nastavena + } + + mainPanel.add(leftPanel); + mainPanel.add(rightPanel); + + add(mainPanel, BorderLayout.CENTER); + + // Set left panel as active by default + activePanel = leftPanel; + updateActivePanelBorder(); + + // Restore saved tabs for both panels if present in configuration + try { + int leftCount = config.getLeftPanelTabCount(); + if (leftCount > 0) { + java.util.List paths = new java.util.ArrayList<>(); + java.util.List modes = new java.util.ArrayList<>(); + for (int i = 0; i < leftCount; i++) { + String p = config.getLeftPanelTabPath(i); + if (p == null) p = System.getProperty("user.home"); + paths.add(p); + modes.add(config.getLeftPanelTabViewMode(i)); + } + int sel = config.getLeftPanelSelectedIndex(); + leftPanel.restoreTabs(paths, modes, sel); + } + } catch (Exception ex) { + // ignore and keep default + } + + try { + int rightCount = config.getRightPanelTabCount(); + if (rightCount > 0) { + java.util.List paths = new java.util.ArrayList<>(); + java.util.List modes = new java.util.ArrayList<>(); + for (int i = 0; i < rightCount; i++) { + String p = config.getRightPanelTabPath(i); + if (p == null) p = System.getProperty("user.home"); + paths.add(p); + modes.add(config.getRightPanelTabViewMode(i)); + } + int sel = config.getRightPanelSelectedIndex(); + rightPanel.restoreTabs(paths, modes, sel); + } + } catch (Exception ex) { + // ignore and keep default + } + + // Focus listeners to track active panel + leftPanel.getFileTable().addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + activePanel = leftPanel; + updateActivePanelBorder(); + // Zajistit, že je vybrán nějaký řádek + JTable leftTable = leftPanel.getFileTable(); + if (leftTable.getSelectedRow() == -1 && leftTable.getRowCount() > 0) { + leftTable.setRowSelectionInterval(0, 0); + } + // Překreslit oba panely + leftPanel.getFileTable().repaint(); + rightPanel.getFileTable().repaint(); + } + + @Override + public void focusLost(FocusEvent e) { + // Překreslit při ztrátě focusu + leftPanel.getFileTable().repaint(); + } + }); + + rightPanel.getFileTable().addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + activePanel = rightPanel; + updateActivePanelBorder(); + // Zajistit, že je vybrán nějaký řádek + JTable rightTable = rightPanel.getFileTable(); + if (rightTable.getSelectedRow() == -1 && rightTable.getRowCount() > 0) { + rightTable.setRowSelectionInterval(0, 0); + } + // Překreslit oba panely + leftPanel.getFileTable().repaint(); + rightPanel.getFileTable().repaint(); + } + + @Override + public void focusLost(FocusEvent e) { + // Překreslit při ztrátě focusu + rightPanel.getFileTable().repaint(); + } + }); + + // Add TAB handler to switch between panels + addTabKeyHandler(leftPanel.getFileTable()); + addTabKeyHandler(rightPanel.getFileTable()); + + // Bottom panel with buttons + createButtonPanel(); + add(buttonPanel, BorderLayout.SOUTH); + + // Menu + createMenuBar(); + } + + /** + * Create toolbar with buttons for changing view mode + */ + private void createToolBar() { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + toolBar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // Button for BRIEF mode + JButton btnBrief = new JButton("☰ Brief"); + btnBrief.setToolTipText("Brief mode - multiple columns (Ctrl+F1)"); + btnBrief.setFocusable(false); + btnBrief.addActionListener(e -> { + if (activePanel != null) { + activePanel.setViewMode(ViewMode.BRIEF); + } + }); + + // Button for FULL mode + JButton btnFull = new JButton("▤ Full"); + btnFull.setToolTipText("Full mode - full information (Ctrl+F2)"); + btnFull.setFocusable(false); + btnFull.addActionListener(e -> { + if (activePanel != null) { + activePanel.setViewMode(ViewMode.FULL); + } + }); + + toolBar.add(btnBrief); + toolBar.add(btnFull); + + toolBar.addSeparator(); + + // Informační label + JLabel infoLabel = new JLabel(" View Mode "); + infoLabel.setFont(infoLabel.getFont().deriveFont(Font.PLAIN, 10f)); + toolBar.add(infoLabel); + + add(toolBar, BorderLayout.NORTH); + } + + /** + * Create button panel (like Total Commander) + */ + private void createButtonPanel() { + buttonPanel = new JPanel(new GridLayout(1, 8, 5, 5)); + buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + JButton btnView = new JButton("F3 View"); + btnView.addActionListener(e -> viewFile()); + + JButton btnEdit = new JButton("F4 Edit"); + btnEdit.addActionListener(e -> editFile()); + + JButton btnCopy = new JButton("F5 Copy"); + btnCopy.addActionListener(e -> copyFiles()); + + JButton btnMove = new JButton("F6 Move"); + btnMove.addActionListener(e -> moveFiles()); + + JButton btnNewDir = new JButton("F7 New Dir"); + btnNewDir.addActionListener(e -> createNewDirectory()); + + JButton btnDelete = new JButton("F8 Delete"); + btnDelete.addActionListener(e -> deleteFiles()); + + JButton btnRename = new JButton("F9 Rename"); + btnRename.addActionListener(e -> renameFile()); + + JButton btnExit = new JButton("F10 Exit"); + btnExit.addActionListener(e -> saveConfigAndExit()); + + buttonPanel.add(btnView); + buttonPanel.add(btnEdit); + buttonPanel.add(btnCopy); + buttonPanel.add(btnMove); + buttonPanel.add(btnNewDir); + buttonPanel.add(btnDelete); + buttonPanel.add(btnRename); + buttonPanel.add(btnExit); + } + + /** + * Create menu bar + */ + private void createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + + // File menu + JMenu fileMenu = new JMenu("File"); + + JMenuItem searchItem = new JMenuItem("Search..."); + searchItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK)); + searchItem.addActionListener(e -> showSearchDialog()); + + JMenuItem refreshItem = new JMenuItem("Refresh"); + refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK)); + refreshItem.addActionListener(e -> refreshPanels()); + + JMenuItem exitItem = new JMenuItem("Exit"); + exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0)); + exitItem.addActionListener(e -> saveConfigAndExit()); + + fileMenu.add(searchItem); + fileMenu.add(refreshItem); + fileMenu.addSeparator(); + fileMenu.add(exitItem); + + // View menu + JMenu viewMenu = new JMenu("View"); + + JMenuItem fullViewItem = new JMenuItem("Full details"); + fullViewItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.CTRL_DOWN_MASK)); + fullViewItem.addActionListener(e -> setActiveViewMode(ViewMode.FULL)); + + JMenuItem briefViewItem = new JMenuItem("Names only"); + briefViewItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.CTRL_DOWN_MASK)); + briefViewItem.addActionListener(e -> setActiveViewMode(ViewMode.BRIEF)); + + viewMenu.add(fullViewItem); + viewMenu.add(briefViewItem); + + // Settings menu + JMenu settingsMenu = new JMenu("Settings"); + JMenuItem appearanceItem = new JMenuItem("Appearance..."); + appearanceItem.addActionListener(e -> showSettingsDialog()); + settingsMenu.add(appearanceItem); + + // Help menu + JMenu helpMenu = new JMenu("Help"); + + JMenuItem aboutItem = new JMenuItem("About"); + aboutItem.addActionListener(e -> showAboutDialog()); + + helpMenu.add(aboutItem); + + menuBar.add(fileMenu); + menuBar.add(viewMenu); + menuBar.add(settingsMenu); + menuBar.add(helpMenu); + + setJMenuBar(menuBar); + } + + /** + * Show settings dialog and apply appearance changes + */ + private void showSettingsDialog() { + SettingsDialog dlg = new SettingsDialog(this, config, () -> applyAppearanceSettings()); + dlg.setVisible(true); + // After dialog closed, ensure appearance applied + applyAppearanceSettings(); + } + + /** + * Apply appearance settings (font/colors) from config to UI components. + */ + private void applyAppearanceSettings() { + Font gfont = config.getGlobalFont(); + if (gfont != null) { + // Apply to toolbars, buttons and tables + SwingUtilities.invokeLater(() -> { + for (Component c : getContentPane().getComponents()) { + c.setFont(gfont); + } + // Apply to panels' tables + if (leftPanel != null && leftPanel.getFileTable() != null) { + leftPanel.applyGlobalFont(gfont); + } + if (rightPanel != null && rightPanel.getFileTable() != null) { + rightPanel.applyGlobalFont(gfont); + } + }); + } + + Color bg = config.getBackgroundColor(); + if (bg != null) { + SwingUtilities.invokeLater(() -> { + getContentPane().setBackground(bg); + if (leftPanel != null) leftPanel.applyBackgroundColor(bg); + if (rightPanel != null) rightPanel.applyBackgroundColor(bg); + }); + } + + Color sel = config.getSelectionColor(); + if (sel != null) { + if (leftPanel != null) leftPanel.applySelectionColor(sel); + if (rightPanel != null) rightPanel.applySelectionColor(sel); + } + + Color mark = config.getMarkedColor(); + if (mark != null) { + if (leftPanel != null) leftPanel.applyMarkedColor(mark); + if (rightPanel != null) rightPanel.applyMarkedColor(mark); + } + } + + /** + * Setup keyboard shortcuts + */ + private void setupKeyBindings() { + JRootPane rootPane = getRootPane(); + + // F3 - Prohlížeč + rootPane.registerKeyboardAction(e -> viewFile(), + KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F4 - Editor + rootPane.registerKeyboardAction(e -> editFile(), + KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F5 - Copy + rootPane.registerKeyboardAction(e -> copyFiles(), + KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F6 - Move + rootPane.registerKeyboardAction(e -> moveFiles(), + KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Shift+F6 - Rename (alternative to F9) + rootPane.registerKeyboardAction(e -> renameFile(), + KeyStroke.getKeyStroke(KeyEvent.VK_F6, InputEvent.SHIFT_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F7 - New directory + rootPane.registerKeyboardAction(e -> createNewDirectory(), + KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F8 - Delete + rootPane.registerKeyboardAction(e -> deleteFiles(), + KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F9 - Rename + rootPane.registerKeyboardAction(e -> renameFile(), + KeyStroke.getKeyStroke(KeyEvent.VK_F9, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // TAB - switch panel (global) - works even when additional tabs are opened + rootPane.registerKeyboardAction(e -> switchPanels(), + KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Alt+F1 - Select drive for left panel + rootPane.registerKeyboardAction(e -> selectDriveForLeftPanel(), + KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.ALT_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Alt+F2 - Select drive for right panel + rootPane.registerKeyboardAction(e -> selectDriveForRightPanel(), + KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.ALT_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+F - Search + rootPane.registerKeyboardAction(e -> showSearchDialog(), + KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+F1 - Full details + rootPane.registerKeyboardAction(e -> setActiveViewMode(ViewMode.FULL), + KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+F2 - Names only + rootPane.registerKeyboardAction(e -> setActiveViewMode(ViewMode.BRIEF), + KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+T - New tab + rootPane.registerKeyboardAction(e -> addNewTabToActivePanel(), + KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Ctrl+W - Close current tab + rootPane.registerKeyboardAction(e -> closeCurrentTabInActivePanel(), + KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + } + + /** + * Update panel borders according to active panel + */ + private void updateActivePanelBorder() { + leftPanel.setBorder(BorderFactory.createTitledBorder( + activePanel == leftPanel ? "Left panel [ACTIVE]" : "Left panel")); + rightPanel.setBorder(BorderFactory.createTitledBorder( + activePanel == rightPanel ? "Right panel [ACTIVE]" : "Right panel")); + } + + /** + * Switch between panels + */ + private void switchPanels() { + // Determine which panel currently (or recently) has focus by inspecting the focus owner. + java.awt.Component owner = java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); + boolean ownerInLeft = false; + boolean ownerInRight = false; + if (owner != null) { + Component p = owner; + while (p != null) { + if (p == leftPanel) { + ownerInLeft = true; + break; + } + if (p == rightPanel) { + ownerInRight = true; + break; + } + p = p.getParent(); + } + } + + if (ownerInLeft) { + rightPanel.requestFocusOnCurrentTab(); + activePanel = rightPanel; + } else if (ownerInRight) { + leftPanel.requestFocusOnCurrentTab(); + activePanel = leftPanel; + } else { + // Fallback to previous behavior based on activePanel + if (activePanel == leftPanel) { + rightPanel.requestFocusOnCurrentTab(); + activePanel = rightPanel; + } else { + leftPanel.requestFocusOnCurrentTab(); + activePanel = leftPanel; + } + } + + // Update panel borders and force repaint so renderer can update focus colors + updateActivePanelBorder(); + JTable lt = leftPanel.getFileTable(); + JTable rt = rightPanel.getFileTable(); + if (lt != null) lt.repaint(); + if (rt != null) rt.repaint(); + } + + /** + * Public wrapper so child components (tabs) can request a panel switch. + */ + public void switchPanelsFromChild() { + switchPanels(); + } + + /** + * Attach TAB handling to switch panels + */ + private void addTabKeyHandler(JTable table) { + // Odstraníme standardní chování TAB ve Swing + table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "switchPanel"); + table.getActionMap().put("switchPanel", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + switchPanels(); + } + }); + + // Přidáme F8 pro mazání s vyšší prioritou než defaultní Swing akce + table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), "deleteFiles"); + table.getActionMap().put("deleteFiles", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + deleteFiles(); + } + }); + } + + /** + * Copy selected files to the opposite panel + */ + private void copyFiles() { + List selectedItems = activePanel.getSelectedItems(); + if (selectedItems.isEmpty()) { + JOptionPane.showMessageDialog(this, + "No files selected", + "Copy", + JOptionPane.INFORMATION_MESSAGE); + return; + } + + FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel; + File targetDir = targetPanel.getCurrentDirectory(); + + int result = JOptionPane.showConfirmDialog(this, + String.format("Copy %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()), + "Copy", + JOptionPane.OK_CANCEL_OPTION); + + if (result == JOptionPane.OK_OPTION) { + performFileOperation(() -> { + FileOperations.copy(selectedItems, targetDir, null); + }, "Kopírování dokončeno", targetPanel); + } + } + + /** + * Move selected files to the opposite panel + */ + private void moveFiles() { + List selectedItems = activePanel.getSelectedItems(); + if (selectedItems.isEmpty()) { + JOptionPane.showMessageDialog(this, + "No files selected", + "Move", + JOptionPane.INFORMATION_MESSAGE); + return; + } + + FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel; + File targetDir = targetPanel.getCurrentDirectory(); + + int result = JOptionPane.showConfirmDialog(this, + String.format("Move %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()), + "Move", + JOptionPane.OK_CANCEL_OPTION); + + if (result == JOptionPane.OK_OPTION) { + performFileOperation(() -> { + FileOperations.move(selectedItems, targetDir, null); + }, "Přesouvání dokončeno", activePanel, targetPanel); + } + } + + /** + * Delete selected files + */ + private void deleteFiles() { + List selectedItems = activePanel.getSelectedItems(); + if (selectedItems.isEmpty()) { + JOptionPane.showMessageDialog(this, + "No files selected", + "Delete", + JOptionPane.INFORMATION_MESSAGE); + return; + } + + StringBuilder message = new StringBuilder("Really delete the following items?\n\n"); + for (FileItem item : selectedItems) { + message.append(item.getName()).append("\n"); + if (message.length() > 500) { + message.append("..."); + break; + } + } + + int result = JOptionPane.showConfirmDialog(this, + message.toString(), + "Mazání", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + + if (result == JOptionPane.YES_OPTION) { + performFileOperation(() -> { + FileOperations.delete(selectedItems, null); + }, "Mazání dokončeno", activePanel); + } + } + + /** + * Rename selected file + */ + private void renameFile() { + List selectedItems = activePanel.getSelectedItems(); + if (selectedItems.size() != 1) { + JOptionPane.showMessageDialog(this, + "Select one file to rename", + "Rename", + JOptionPane.INFORMATION_MESSAGE); + return; + } + + FileItem item = selectedItems.get(0); + String newName = JOptionPane.showInputDialog(this, + "New name:", + item.getName()); + + if (newName != null && !newName.trim().isEmpty() && !newName.equals(item.getName())) { + performFileOperation(() -> { + FileOperations.rename(item.getFile(), newName.trim()); + }, "Přejmenování dokončeno", activePanel); + } + } + + /** + * Create a new directory + */ + private void createNewDirectory() { + String dirName = JOptionPane.showInputDialog(this, + "New directory name:", + "New directory"); + + if (dirName != null && !dirName.trim().isEmpty()) { + performFileOperation(() -> { + FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName.trim()); + }, "Directory created", activePanel); + } + } + + /** + * Show search dialog + */ + private void showSearchDialog() { + SearchDialog dialog = new SearchDialog(this, activePanel.getCurrentDirectory()); + dialog.setVisible(true); + } + + /** + * Show file in internal viewer + */ + private void viewFile() { + List selectedItems = activePanel.getSelectedItems(); + if (selectedItems.isEmpty()) { + return; + } + + FileItem item = selectedItems.get(0); + if (item.isDirectory() || item.getName().equals("..")) { + return; + } + + File file = item.getFile(); + + // Removed previous 10 MB limit: allow opening large files in the viewer (paged hex will stream large binaries). + + FileEditor viewer = new FileEditor(this, file, config, true); + viewer.setVisible(true); + } + + /** + * Open file in internal editor + */ + private void editFile() { + List selectedItems = activePanel.getSelectedItems(); + if (selectedItems.isEmpty()) { + return; + } + + FileItem item = selectedItems.get(0); + if (item.isDirectory() || item.getName().equals("..")) { + return; + } + + File file = item.getFile(); + + // Removed previous 10 MB limit: allow opening large files in the editor. The editor may still choose hex/paged mode for large binaries. + + FileEditor editor = new FileEditor(this, file, config, false); + editor.setVisible(true); + } + + /** + * Refresh both panels + */ + private void refreshPanels() { + // Refresh je nyní automatický při změnách + // Pokud je potřeba manuální refresh, můžeme zavolat loadDirectory + if (leftPanel.getCurrentDirectory() != null) { + leftPanel.loadDirectory(leftPanel.getCurrentDirectory()); + } + if (rightPanel.getCurrentDirectory() != null) { + rightPanel.loadDirectory(rightPanel.getCurrentDirectory()); + } + } + + /** + * Set the view mode for the active panel + */ + private void setActiveViewMode(ViewMode mode) { + if (activePanel != null) { + activePanel.setViewMode(mode); + } + } + + /** + * Add a new tab to the active panel + */ + private void addNewTabToActivePanel() { + if (activePanel != null) { + File currentDir = activePanel.getCurrentDirectory(); + String path = currentDir != null ? currentDir.getAbsolutePath() : System.getProperty("user.home"); + activePanel.addNewTab(path); + } + } + + /** + * Close the current tab in the active panel + */ + private void closeCurrentTabInActivePanel() { + if (activePanel != null) { + activePanel.closeCurrentTab(); + } + } + + /** + * Show About dialog + */ + private void showAboutDialog() { + JOptionPane.showMessageDialog(this, + "KF File Manager 1.0\n\n" + + "Two-panel file manager\n" + + "Java 11\n\n" + + "Keyboard shortcuts:\n" + + "F5 - Copy\n" + + "F6 - Move\n" + + "F7 - New directory\n" + + "F8 - Delete\n" + + "F9 / Shift+F6 - Rename\n" + + "TAB - Switch panel\n" + + "Ctrl+F - Search\n" + + "Enter - Open directory\n" + + "Backspace - Parent directory", + "About", + JOptionPane.INFORMATION_MESSAGE); + } + + /** + * Execute file operation with error handling + */ + private void performFileOperation(FileOperation operation, String successMessage, FilePanel... panelsToRefresh) { + try { + operation.execute(); + for (FilePanel panel : panelsToRefresh) { + if (panel.getCurrentDirectory() != null) { + panel.loadDirectory(panel.getCurrentDirectory()); + } + } + // Info okna o úspěchu zrušena - operace proběhne tiše + } catch (Exception e) { + JOptionPane.showMessageDialog(this, + "Chyba: " + e.getMessage(), + "Chyba", + JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Show drive selector for left panel + */ + private void selectDriveForLeftPanel() { + // Open the drive dropdown in the left panel + if (leftPanel != null) leftPanel.showDrivePopup(); + } + + /** + * Show drive selector for right panel + */ + private void selectDriveForRightPanel() { + // Open the drive dropdown in the right panel + if (rightPanel != null) rightPanel.showDrivePopup(); + } + + /** + * Save configuration and exit application + */ + private void saveConfigAndExit() { + // Save window state + config.saveWindowState(this); + + // Save current panel paths (for backward compatibility) + if (leftPanel.getCurrentDirectory() != null) { + config.setLeftPanelPath(leftPanel.getCurrentDirectory().getAbsolutePath()); + } + if (rightPanel.getCurrentDirectory() != null) { + config.setRightPanelPath(rightPanel.getCurrentDirectory().getAbsolutePath()); + } + + // Save open tabs + view modes and selected index for both panels + try { + java.util.List leftPaths = leftPanel.getTabPaths(); + java.util.List leftModes = leftPanel.getTabViewModes(); + int leftSelected = leftPanel.getSelectedTabIndex(); + config.saveLeftPanelTabs(leftPaths, leftModes, leftSelected); + } catch (Exception ex) { + // ignore + } + + try { + java.util.List rightPaths = rightPanel.getTabPaths(); + java.util.List rightModes = rightPanel.getTabViewModes(); + int rightSelected = rightPanel.getSelectedTabIndex(); + config.saveRightPanelTabs(rightPaths, rightModes, rightSelected); + } catch (Exception ex) { + // ignore + } + + // Uložit konfiguraci do souboru + config.saveConfig(); + + // Exit application + System.exit(0); + } + + @FunctionalInterface + private interface FileOperation { + void execute() throws Exception; + } +} diff --git a/src/main/java/com/kfmanager/ui/SearchDialog.java b/src/main/java/com/kfmanager/ui/SearchDialog.java new file mode 100644 index 0000000..0fe949a --- /dev/null +++ b/src/main/java/com/kfmanager/ui/SearchDialog.java @@ -0,0 +1,246 @@ +package com.kfmanager.ui; + +import com.kfmanager.model.FileItem; +import com.kfmanager.service.FileOperations; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.awt.*; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Dialog for searching files + */ +public class SearchDialog extends JDialog { + + private JTextField patternField; + private JCheckBox recursiveCheckBox; + private JTable resultsTable; + private ResultsTableModel tableModel; + private JButton searchButton; + private JButton cancelButton; + private File searchDirectory; + private volatile boolean searching = false; + + public SearchDialog(Frame parent, File searchDirectory) { + super(parent, "Search files", true); + this.searchDirectory = searchDirectory; + initComponents(); + setSize(700, 500); + setLocationRelativeTo(parent); + } + + private void initComponents() { + setLayout(new BorderLayout(10, 10)); + ((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Panel pro zadání kritérií + JPanel searchPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + + gbc.gridx = 0; + gbc.gridy = 0; + searchPanel.add(new JLabel("Hledat:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + patternField = new JTextField(); + patternField.setToolTipText("Enter filename or pattern (* for any chars, ? for single char)"); + searchPanel.add(patternField, gbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.gridwidth = 2; + recursiveCheckBox = new JCheckBox("Include subdirectories", true); + searchPanel.add(recursiveCheckBox, gbc); + + gbc.gridy = 2; + JLabel pathLabel = new JLabel("Directory: " + searchDirectory.getAbsolutePath()); + pathLabel.setFont(pathLabel.getFont().deriveFont(Font.ITALIC)); + searchPanel.add(pathLabel, gbc); + + add(searchPanel, BorderLayout.NORTH); + + // Tabulka s výsledky + tableModel = new ResultsTableModel(); + resultsTable = new JTable(tableModel); + resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + resultsTable.setFont(new Font("Monospaced", Font.PLAIN, 12)); + + resultsTable.getColumnModel().getColumn(0).setPreferredWidth(400); + resultsTable.getColumnModel().getColumn(1).setPreferredWidth(100); + resultsTable.getColumnModel().getColumn(2).setPreferredWidth(150); + + // Double-click pro otevření umístění + resultsTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + if (e.getClickCount() == 2) { + openSelectedFile(); + } + } + }); + + JScrollPane scrollPane = new JScrollPane(resultsTable); + scrollPane.setBorder(BorderFactory.createTitledBorder("Výsledky")); + add(scrollPane, BorderLayout.CENTER); + + // Panel s tlačítky + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + searchButton = new JButton("Hledat"); + searchButton.addActionListener(e -> performSearch()); + + cancelButton = new JButton("Zavřít"); + cancelButton.addActionListener(e -> dispose()); + + JButton openButton = new JButton("Otevřít umístění"); + openButton.addActionListener(e -> openSelectedFile()); + + buttonPanel.add(searchButton); + buttonPanel.add(openButton); + buttonPanel.add(cancelButton); + + add(buttonPanel, BorderLayout.SOUTH); + + // Enter pro spuštění hledání + patternField.addActionListener(e -> performSearch()); + } + + /** + * Provede vyhledávání + */ + private void performSearch() { + String pattern = patternField.getText().trim(); + if (pattern.isEmpty()) { + JOptionPane.showMessageDialog(this, + "Zadejte hledaný vzor", + "Chyba", + JOptionPane.WARNING_MESSAGE); + return; + } + + tableModel.clear(); + searchButton.setEnabled(false); + searching = true; + + // Spustit vyhledávání v samostatném vlákně + SwingWorker worker = new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + FileOperations.search(searchDirectory, pattern, recursiveCheckBox.isSelected(), + file -> { + if (!searching) { + return; + } + publish(file); + }); + return null; + } + + @Override + protected void process(List chunks) { + for (File file : chunks) { + tableModel.addResult(new FileItem(file)); + } + } + + @Override + protected void done() { + searchButton.setEnabled(true); + searching = false; + try { + get(); + if (tableModel.getRowCount() == 0) { + JOptionPane.showMessageDialog(SearchDialog.this, + "Nebyly nalezeny žádné soubory", + "Výsledek", + JOptionPane.INFORMATION_MESSAGE); + } + } catch (Exception e) { + JOptionPane.showMessageDialog(SearchDialog.this, + "Chyba při hledání: " + e.getMessage(), + "Chyba", + JOptionPane.ERROR_MESSAGE); + } + } + }; + + worker.execute(); + } + + /** + * Otevře umístění vybraného souboru v exploreru + */ + private void openSelectedFile() { + int selectedRow = resultsTable.getSelectedRow(); + if (selectedRow >= 0) { + FileItem item = tableModel.getResult(selectedRow); + try { + Desktop.getDesktop().open(item.getFile().getParentFile()); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, + "Nepodařilo se otevřít umístění: " + e.getMessage(), + "Chyba", + JOptionPane.ERROR_MESSAGE); + } + } + } + + /** + * Model tabulky pro výsledky hledání + */ + private class ResultsTableModel extends AbstractTableModel { + + private final String[] columnNames = {"Cesta", "Velikost", "Datum změny"}; + private List results = new ArrayList<>(); + + public void addResult(FileItem item) { + results.add(item); + fireTableRowsInserted(results.size() - 1, results.size() - 1); + } + + public void clear() { + results.clear(); + fireTableDataChanged(); + } + + public FileItem getResult(int row) { + return results.get(row); + } + + @Override + public int getRowCount() { + return results.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public String getColumnName(int column) { + return columnNames[column]; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + FileItem item = results.get(rowIndex); + switch (columnIndex) { + case 0: + return item.getPath(); + case 1: + return item.getFormattedSize(); + case 2: + return item.getFormattedDate(); + default: + return null; + } + } + } +} diff --git a/src/main/java/com/kfmanager/ui/SettingsDialog.java b/src/main/java/com/kfmanager/ui/SettingsDialog.java new file mode 100644 index 0000000..b0a1add --- /dev/null +++ b/src/main/java/com/kfmanager/ui/SettingsDialog.java @@ -0,0 +1,173 @@ +package com.kfmanager.ui; + +import com.kfmanager.config.AppConfig; + +import javax.swing.*; +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +/** + * Settings dialog with categories on the left and parameters on the right. + */ +public class SettingsDialog extends JDialog { + private final AppConfig config; + private final Runnable onChange; + private final JList categoryList; + private final JPanel cards; + private final CardLayout cardLayout; + + // Appearance controls + private JButton appearanceFontBtn; + private JButton appearanceBgBtn; + private JButton appearanceSelBtn; + private JButton appearanceMarkBtn; + + // Editor controls + private JButton editorFontBtn; + + private final Map panels = new HashMap<>(); + + public SettingsDialog(Window parent, AppConfig config, Runnable onChange) { + super(parent, "Settings", ModalityType.APPLICATION_MODAL); + this.config = config; + this.onChange = onChange; + + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setSize(700, 420); + setLocationRelativeTo(parent); + + // Left: categories + DefaultListModel model = new DefaultListModel<>(); + model.addElement("Appearance"); + model.addElement("Editor"); + categoryList = new JList<>(model); + categoryList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + categoryList.setSelectedIndex(0); + + cardLayout = new CardLayout(); + cards = new JPanel(cardLayout); + + // Build category panels + cards.add(buildAppearancePanel(), "Appearance"); + cards.add(buildEditorPanel(), "Editor"); + + categoryList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + String sel = categoryList.getSelectedValue(); + if (sel != null) cardLayout.show(cards, sel); + } + }); + + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, + new JScrollPane(categoryList), cards); + split.setResizeWeight(0); + split.setDividerLocation(160); + + add(split, BorderLayout.CENTER); + + JPanel btns = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton ok = new JButton("OK"); + ok.addActionListener(e -> { + // Persist is handled by individual actions; ensure saved and close + config.saveConfig(); + dispose(); + }); + JButton cancel = new JButton("Cancel"); + cancel.addActionListener(e -> dispose()); + btns.add(ok); + btns.add(cancel); + add(btns, BorderLayout.SOUTH); + } + + private JPanel buildAppearancePanel() { + JPanel p = new JPanel(new BorderLayout(8, 8)); + JPanel grid = new JPanel(new GridLayout(4, 2, 8, 8)); + grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + + grid.add(new JLabel("Application font:")); + appearanceFontBtn = new JButton(getFontDescription(config.getGlobalFont())); + appearanceFontBtn.addActionListener(e -> { + Font nf = FontChooserDialog.showDialog(this, config.getGlobalFont()); + if (nf != null) { + config.setGlobalFont(nf); + appearanceFontBtn.setText(getFontDescription(nf)); + if (onChange != null) onChange.run(); + } + }); + grid.add(appearanceFontBtn); + + grid.add(new JLabel("Background color:")); + appearanceBgBtn = new JButton(); + Color bg = config.getBackgroundColor(); + appearanceBgBtn.setBackground(bg != null ? bg : UIManager.getColor("Panel.background")); + appearanceBgBtn.addActionListener(e -> { + Color chosen = JColorChooser.showDialog(this, "Choose background color", appearanceBgBtn.getBackground()); + if (chosen != null) { + appearanceBgBtn.setBackground(chosen); + config.setBackgroundColor(chosen); + if (onChange != null) onChange.run(); + } + }); + grid.add(appearanceBgBtn); + + grid.add(new JLabel("Selection color:")); + appearanceSelBtn = new JButton(); + Color sel = config.getSelectionColor(); + appearanceSelBtn.setBackground(sel != null ? sel : new Color(184, 207, 229)); + appearanceSelBtn.addActionListener(e -> { + Color chosen = JColorChooser.showDialog(this, "Choose selection color", appearanceSelBtn.getBackground()); + if (chosen != null) { + appearanceSelBtn.setBackground(chosen); + config.setSelectionColor(chosen); + if (onChange != null) onChange.run(); + } + }); + grid.add(appearanceSelBtn); + + grid.add(new JLabel("Marked item color:")); + appearanceMarkBtn = new JButton(); + Color mark = config.getMarkedColor(); + appearanceMarkBtn.setBackground(mark != null ? mark : new Color(204, 153, 0)); + appearanceMarkBtn.addActionListener(e -> { + Color chosen = JColorChooser.showDialog(this, "Choose marked item color", appearanceMarkBtn.getBackground()); + if (chosen != null) { + appearanceMarkBtn.setBackground(chosen); + config.setMarkedColor(chosen); + if (onChange != null) onChange.run(); + } + }); + grid.add(appearanceMarkBtn); + + p.add(grid, BorderLayout.NORTH); + panels.put("Appearance", p); + return p; + } + + private JPanel buildEditorPanel() { + JPanel p = new JPanel(new BorderLayout(8, 8)); + JPanel grid = new JPanel(new GridLayout(1, 2, 8, 8)); + grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + + grid.add(new JLabel("Editor font:")); + editorFontBtn = new JButton(getFontDescription(config.getEditorFont())); + editorFontBtn.addActionListener(e -> { + Font nf = FontChooserDialog.showDialog(this, config.getEditorFont()); + if (nf != null) { + config.setEditorFont(nf); + editorFontBtn.setText(getFontDescription(nf)); + if (onChange != null) onChange.run(); + } + }); + grid.add(editorFontBtn); + + p.add(grid, BorderLayout.NORTH); + panels.put("Editor", p); + return p; + } + + private String getFontDescription(Font f) { + if (f == null) return "(default)"; + return String.format("%s %dpt", f.getName(), f.getSize()); + } +} diff --git a/src/main/java/com/kfmanager/ui/ViewMode.java b/src/main/java/com/kfmanager/ui/ViewMode.java new file mode 100644 index 0000000..1e16d1a --- /dev/null +++ b/src/main/java/com/kfmanager/ui/ViewMode.java @@ -0,0 +1,9 @@ +package com.kfmanager.ui; + +/** + * Display mode for the panel + */ +public enum ViewMode { + FULL, // Full details (name, size, date) + BRIEF // Names only in multiple columns +}