first commit
This commit is contained in:
commit
7b07d98dab
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal 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
47
README.md
Normal 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
47
pom.xml
Normal 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>
|
||||||
26
src/main/java/com/kfmanager/MainApp.java
Normal file
26
src/main/java/com/kfmanager/MainApp.java
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
337
src/main/java/com/kfmanager/config/AppConfig.java
Normal file
337
src/main/java/com/kfmanager/config/AppConfig.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/main/java/com/kfmanager/model/FileItem.java
Normal file
100
src/main/java/com/kfmanager/model/FileItem.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
src/main/java/com/kfmanager/service/FileOperations.java
Normal file
205
src/main/java/com/kfmanager/service/FileOperations.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/main/java/com/kfmanager/ui/DriveSelector.java
Normal file
168
src/main/java/com/kfmanager/ui/DriveSelector.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
700
src/main/java/com/kfmanager/ui/FileEditor.java
Normal file
700
src/main/java/com/kfmanager/ui/FileEditor.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
528
src/main/java/com/kfmanager/ui/FilePanel.java
Normal file
528
src/main/java/com/kfmanager/ui/FilePanel.java
Normal 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 má aktuální tab
|
||||||
|
if (currentMode != null) {
|
||||||
|
tab.setViewMode(currentMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
String tabTitle = getTabTitle(path);
|
||||||
|
|
||||||
|
tabbedPane.addTab(tabTitle, tab);
|
||||||
|
tabbedPane.setSelectedComponent(tab);
|
||||||
|
|
||||||
|
// Aktualizovat path field
|
||||||
|
updatePathField();
|
||||||
|
|
||||||
|
// Nastavit focus na tabulku v novém tabu
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
tab.getFileTable().requestFocusInWindow();
|
||||||
|
// Ensure renderers are attached now that the tab is added to the UI
|
||||||
|
tab.ensureRenderers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Přidá nový tab a explicitně nastaví ViewMode pro tento tab.
|
||||||
|
*/
|
||||||
|
public void addNewTabWithMode(String path, ViewMode mode) {
|
||||||
|
FilePanelTab tab = new FilePanelTab(path);
|
||||||
|
if (appConfig != null) tab.setAppConfig(appConfig);
|
||||||
|
tab.setOnDirectoryChanged(() -> updateTabTitle(tab));
|
||||||
|
tab.setOnSwitchPanelRequested(switchPanelCallback);
|
||||||
|
|
||||||
|
if (mode != null) {
|
||||||
|
tab.setViewMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
String tabTitle = getTabTitle(path);
|
||||||
|
tabbedPane.addTab(tabTitle, tab);
|
||||||
|
tabbedPane.setSelectedComponent(tab);
|
||||||
|
|
||||||
|
updatePathField();
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
tab.getFileTable().requestFocusInWindow();
|
||||||
|
tab.ensureRenderers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide AppConfig so tabs can persist/retrieve sort settings
|
||||||
|
*/
|
||||||
|
public void setAppConfig(com.kfmanager.config.AppConfig cfg) {
|
||||||
|
this.appConfig = cfg;
|
||||||
|
// propagate to existing tabs
|
||||||
|
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
|
||||||
|
Component c = tabbedPane.getComponentAt(i);
|
||||||
|
if (c instanceof FilePanelTab) {
|
||||||
|
((FilePanelTab) c).setAppConfig(cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1043
src/main/java/com/kfmanager/ui/FilePanel.java.bak
Normal file
1043
src/main/java/com/kfmanager/ui/FilePanel.java.bak
Normal file
File diff suppressed because it is too large
Load Diff
1441
src/main/java/com/kfmanager/ui/FilePanelTab.java
Normal file
1441
src/main/java/com/kfmanager/ui/FilePanelTab.java
Normal file
File diff suppressed because it is too large
Load Diff
192
src/main/java/com/kfmanager/ui/FontChooserDialog.java
Normal file
192
src/main/java/com/kfmanager/ui/FontChooserDialog.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
901
src/main/java/com/kfmanager/ui/MainWindow.java
Normal file
901
src/main/java/com/kfmanager/ui/MainWindow.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/main/java/com/kfmanager/ui/SearchDialog.java
Normal file
246
src/main/java/com/kfmanager/ui/SearchDialog.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/main/java/com/kfmanager/ui/SettingsDialog.java
Normal file
173
src/main/java/com/kfmanager/ui/SettingsDialog.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/com/kfmanager/ui/ViewMode.java
Normal file
9
src/main/java/com/kfmanager/ui/ViewMode.java
Normal 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user