first commit

This commit is contained in:
Radek Davidek 2025-11-16 20:08:45 +01:00
commit 7b07d98dab
17 changed files with 6188 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -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

47
README.md Normal file
View File

@ -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+

47
pom.xml Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kfmanager</groupId>
<artifactId>kf-manager</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>KF File Manager</name>
<description>Dual-panel file manager similar to Total Commander</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.kfmanager.MainApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
});
}
}

View File

@ -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<String> paths, java.util.List<String> 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<String> paths, java.util.List<String> 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);
}
}
}

View File

@ -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 "<DIR>";
}
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));
}
}
}

View File

@ -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<FileItem> 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<FileItem> 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<FileItem> 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<Path>() {
@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<Path>() {
@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<Path> 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);
}
}

View File

@ -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<DriveInfo> drives = new ArrayList<>();
for (File root : roots) {
drives.add(new DriveInfo(root));
}
// Seznam disků
JList<DriveInfo> 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));
}
}
}
}

View File

@ -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<Integer> 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();
}
}

View File

@ -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<File> 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 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<String> getTabPaths() {
java.util.List<String> 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<String> getTabViewModes() {
java.util.List<String> 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<String> paths, java.util.List<String> 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<FileItem> 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);
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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<String> fontList;
private JList<Integer> sizeList;
private JList<String> 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;
}
}

View File

@ -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<String> paths = new java.util.ArrayList<>();
java.util.List<String> 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<String> paths = new java.util.ArrayList<>();
java.util.List<String> 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<FileItem> 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<FileItem> 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<FileItem> 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<FileItem> 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<FileItem> 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<FileItem> 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<String> leftPaths = leftPanel.getTabPaths();
java.util.List<String> leftModes = leftPanel.getTabViewModes();
int leftSelected = leftPanel.getSelectedTabIndex();
config.saveLeftPanelTabs(leftPaths, leftModes, leftSelected);
} catch (Exception ex) {
// ignore
}
try {
java.util.List<String> rightPaths = rightPanel.getTabPaths();
java.util.List<String> 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;
}
}

View File

@ -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<Void, File> worker = new SwingWorker<Void, File>() {
@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<File> 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<FileItem> 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;
}
}
}
}

View File

@ -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<String> 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<String, JPanel> 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<String> 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());
}
}

View File

@ -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
}