diff --git a/commander/pom.xml b/commander/pom.xml index e3291643..503a41e8 100644 --- a/commander/pom.xml +++ b/commander/pom.xml @@ -12,22 +12,22 @@ UTF-8 5.12.1 + 21.0.6 org.openjfx - javafx - 21.0.6 - pom - compile + javafx-controls + ${javafx.version} org.openjfx javafx-fxml - 21.0.6 + ${javafx.version} + org.junit.jupiter junit-jupiter-api @@ -53,13 +53,13 @@ 25 + org.openjfx javafx-maven-plugin 0.0.8 - default-cli hse.java.commander/hse.java.commander.CommanderApplication diff --git a/commander/src/main/java/hse/java/commander/CommanderApplication.java b/commander/src/main/java/hse/java/commander/CommanderApplication.java index 5b1ecff3..2c33f7df 100644 --- a/commander/src/main/java/hse/java/commander/CommanderApplication.java +++ b/commander/src/main/java/hse/java/commander/CommanderApplication.java @@ -4,16 +4,21 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; - import java.io.IOException; public class CommanderApplication extends Application { @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(CommanderApplication.class.getResource("commander-ui.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 400, 400); + FXMLLoader fxml_loader = new FXMLLoader(CommanderApplication.class.getResource("commander-ui.fxml")); + Scene scene = new Scene(fxml_loader.load()); + var css = CommanderApplication.class.getResource("commander.css"); + + if (css != null) scene.getStylesheets().add(css.toExternalForm()); + stage.setTitle("Commander"); stage.setScene(scene); + stage.sizeToScene(); + stage.setResizable(false); stage.show(); } -} +} \ No newline at end of file diff --git a/commander/src/main/java/hse/java/commander/FileOperations.java b/commander/src/main/java/hse/java/commander/FileOperations.java new file mode 100644 index 00000000..2789d720 --- /dev/null +++ b/commander/src/main/java/hse/java/commander/FileOperations.java @@ -0,0 +1,76 @@ +package hse.java.commander; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +public final class FileOperations { + + private FileOperations() { + } + + public static Path detectProjectRoot() { + Path dir = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize(); + if (dir.getFileName() != null && "commander".equals(dir.getFileName().toString()) && dir.getParent() != null) + return dir.getParent().toAbsolutePath().normalize(); + return dir; + } + + public static List list(Path dir, Path root_dir) throws IOException { + List entries = new ArrayList<>(); + if (dir == null || !Files.isDirectory(dir)) return entries; + + Path parent = dir.getParent(); + if (parent != null && root_dir != null && parent.startsWith(root_dir) && !dir.equals(root_dir)) entries.add(PathEntry.parent(parent)); + + try (var ds = Files.newDirectoryStream(dir)) { + for (Path p : ds) entries.add(PathEntry.of(p)); + } + + sort(entries); + return entries; + } + + public static void move(Path source, Path destination) throws IOException { + Files.move(source, destination, REPLACE_EXISTING); + } + + public static void rename(Path source, String new_name) throws IOException { + Files.move(source, source.getParent().resolve(new_name).normalize(), REPLACE_EXISTING); + } + + public static void deleteRecursively(Path path) throws IOException { + if (Files.notExists(path)) return; + + if (Files.isDirectory(path)) { + try (var ds = Files.newDirectoryStream(path)) { + for (Path child : ds) deleteRecursively(child); + } + } + + Files.deleteIfExists(path); + } + + private static void sort(List entries) { + for (int i = 1; i < entries.size(); i++) { + PathEntry key = entries.get(i); + int j = i - 1; + + while (j >= 0 && compare(entries.get(j), key) > 0) { + entries.set(j + 1, entries.get(j)); + j -= 1; + } + + entries.set(j + 1, key); + } + } + + private static int compare(PathEntry a, PathEntry b) { + if (a.isDirectory() != b.isDirectory()) return a.isDirectory() ? -1 : 1; + return String.CASE_INSENSITIVE_ORDER.compare(a.toString(), b.toString()); + } +} \ No newline at end of file diff --git a/commander/src/main/java/hse/java/commander/MainController.java b/commander/src/main/java/hse/java/commander/MainController.java index 19c700fb..2c9d661d 100644 --- a/commander/src/main/java/hse/java/commander/MainController.java +++ b/commander/src/main/java/hse/java/commander/MainController.java @@ -1,36 +1,263 @@ package hse.java.commander; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.ListView; +import javafx.scene.control.*; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.input.MouseEvent; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; public class MainController { @FXML - public Button move; + private ListView left; - public void initialize() { - move.setOnMouseClicked(event -> { + @FXML + private ListView right; - }); - System.out.println(System.getProperty("user.home")); - left.getItems().add("Kek"); - - left.setOnMouseClicked(event -> { - if (event.getClickCount() == 2) { - int index = left.getSelectionModel().getSelectedIndex(); - if (index >= 0) { - left.getItems().set(index, "clicked"); - } - } - }); - } + @FXML + private Button left_to_right; + + @FXML + private Button right_to_left; @FXML - public ListView left; + private Button rename; @FXML - public ListView right; + private Button remove; + + @FXML + private Button refresh; + + private Path root_dir; + private Path left_dir; + private Path right_dir; + + private boolean left_active = true; + private String css_url; + + public void initialize() { + root_dir = FileOperations.detectProjectRoot(); + left_dir = root_dir; + right_dir = root_dir; + + var css = MainController.class.getResource("commander.css"); + css_url = css == null ? null : css.toExternalForm(); + + left.setOnMouseClicked(new EventHandler<>() { + @Override + public void handle(MouseEvent event) { + left_active = true; + if (event.getClickCount() != 2) return; + openSelected(true); + } + }); + + right.setOnMouseClicked(new EventHandler<>() { + @Override + public void handle(MouseEvent event) { + left_active = false; + if (event.getClickCount() != 2) return; + openSelected(false); + } + }); + + left_to_right.setOnAction(new EventHandler<>() { + @Override + public void handle(ActionEvent event) { + move(true); + } + }); + + right_to_left.setOnAction(new EventHandler<>() { + @Override + public void handle(ActionEvent event) { + move(false); + } + }); + + rename.setOnAction(new EventHandler<>() { + @Override + public void handle(ActionEvent event) { + renameSelected(); + } + }); + + remove.setOnAction(new EventHandler<>() { + @Override + public void handle(ActionEvent event) { + removeSelected(); + } + }); + + refresh.setOnAction(new EventHandler<>() { + @Override + public void handle(ActionEvent event) { + refreshBoth(); + } + }); + + refreshBoth(); + } + + private void refreshBoth() { + refreshPanel(true); + refreshPanel(false); + } + + private void refreshPanel(boolean is_left) { + Path dir = is_left ? left_dir : right_dir; + ListView view = is_left ? left : right; + + try { + List entries = FileOperations.list(dir, root_dir); + view.getItems().setAll(entries); + } catch (IOException e) { + view.getItems().clear(); + } + } + + private void openSelected(boolean is_left) { + ListView view = is_left ? left : right; + PathEntry selected = view.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + if (selected.isParent() || selected.isDirectory()) { + if (is_left) left_dir = selected.path(); + else right_dir = selected.path(); + refreshPanel(is_left); + return; + } + + previewFile(selected.path()); + } + + private void move(boolean left_to_right_move) { + ListView from_view = left_to_right_move ? left : right; + Path from_dir = left_to_right_move ? left_dir : right_dir; + Path to_dir = left_to_right_move ? right_dir : left_dir; + + PathEntry selected = from_view.getSelectionModel().getSelectedItem(); + if (selected == null || selected.isParent()) return; + + Path source = selected.path(); + Path destination = to_dir.resolve(source.getFileName()).normalize(); + + if (Files.exists(destination) && isCancelled("Заменить существующий файл?", destination.toString())) return; + + try { + FileOperations.move(source, destination); + refreshBoth(); + } catch (IOException e) { + error("Не удалось перенести", e.getMessage()); + } + } + + private void renameSelected() { + ListView view = left_active ? left : right; + PathEntry selected = view.getSelectionModel().getSelectedItem(); + if (selected == null || selected.isParent()) return; + + Path path = selected.path(); + String current_name = path.getFileName().toString(); + + TextInputDialog dialog = new TextInputDialog(current_name); + dialog.setTitle("Переименовать"); + dialog.setHeaderText("Переименовать"); + dialog.setContentText("Новое имя:"); + applyDialogTheme(dialog.getDialogPane()); + + String new_name = dialog.showAndWait().orElse("").trim(); + if (new_name.isEmpty() || new_name.equals(current_name)) return; + + if (new_name.indexOf('/') >= 0 || new_name.indexOf('\\') >= 0) { + error("Некорректное имя", "Имя не должно содержать разделители пути"); + return; + } + + Path destination = path.getParent().resolve(new_name).normalize(); + if (Files.exists(destination) && isCancelled("Заменить существующий файл?", destination.toString())) return; + + try { + FileOperations.rename(path, new_name); + refreshBoth(); + } catch (IOException e) { + error("Не удалось переименовать", e.getMessage()); + } + } + + private void removeSelected() { + ListView view = left_active ? left : right; + PathEntry selected = view.getSelectionModel().getSelectedItem(); + if (selected == null || selected.isParent()) return; + + Path path = selected.path(); + if (isCancelled("Удалить выбранный объект?", path.toString())) return; + + try { + FileOperations.deleteRecursively(path); + refreshBoth(); + } catch (IOException e) { + error("Не удалось удалить", e.getMessage()); + } + } + + private void previewFile(Path path) { + String content; + try { + content = Files.readString(path); + } catch (IOException e) { + error("Не удалось прочитать файл", e.getMessage()); + return; + } + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Просмотр"); + alert.setHeaderText(path.getFileName() == null ? path.toString() : path.getFileName().toString()); + + TextArea area = new TextArea(content); + area.setEditable(false); + area.setWrapText(false); + area.setPrefColumnCount(80); + area.setPrefRowCount(25); + + alert.getDialogPane().setContent(area); + applyDialogTheme(alert.getDialogPane()); + alert.getButtonTypes().setAll(new ButtonType("Закрыть", ButtonData.CANCEL_CLOSE)); + alert.showAndWait(); + } + + private boolean isCancelled(String header, String content) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Подтверждение"); + alert.setHeaderText(header); + alert.setContentText(content); + applyDialogTheme(alert.getDialogPane()); + + ButtonType ok = new ButtonType("ОК", ButtonData.OK_DONE); + ButtonType cancel = new ButtonType("Отмена", ButtonData.CANCEL_CLOSE); + alert.getButtonTypes().setAll(ok, cancel); + + return alert.showAndWait().orElse(cancel) == cancel; + } + private void error(String title, String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(title); + alert.setContentText(message == null ? "" : message); + applyDialogTheme(alert.getDialogPane()); + alert.showAndWait(); + } -} + private void applyDialogTheme(DialogPane pane) { + if (css_url == null || pane.getStylesheets().contains(css_url)) return; + pane.getStylesheets().add(css_url); + } +} \ No newline at end of file diff --git a/commander/src/main/java/hse/java/commander/PathEntry.java b/commander/src/main/java/hse/java/commander/PathEntry.java new file mode 100644 index 00000000..0f8f5b61 --- /dev/null +++ b/commander/src/main/java/hse/java/commander/PathEntry.java @@ -0,0 +1,36 @@ +package hse.java.commander; + +import java.nio.file.Files; +import java.nio.file.Path; + +public final class PathEntry { + + private final Path path; + private final String display_name; + private final boolean parent; + + private PathEntry(Path path, String display_name, boolean parent) { + this.path = path; + this.display_name = display_name; + this.parent = parent; + } + + public static PathEntry parent(Path parent_dir) { + return new PathEntry(parent_dir, "..", true); + } + + public static PathEntry of(Path path) { + String name = path.getFileName() == null ? path.toString() : path.getFileName().toString(); + if (Files.isDirectory(path)) name += "/"; + return new PathEntry(path, name, false); + } + + public Path path() {return path;} + + public boolean isParent() {return parent;} + + public boolean isDirectory() {return Files.isDirectory(path);} + + @Override + public String toString() {return display_name;} +} \ No newline at end of file diff --git a/commander/src/main/resources/hse/java/commander/commander-ui.fxml b/commander/src/main/resources/hse/java/commander/commander-ui.fxml index 23d8db23..9e616389 100644 --- a/commander/src/main/resources/hse/java/commander/commander-ui.fxml +++ b/commander/src/main/resources/hse/java/commander/commander-ui.fxml @@ -3,12 +3,21 @@ - + - + + + +