diff --git a/blight-assets/src/main/resources/Textures/internal/foliage/weeping_willow.png b/blight-assets/src/main/resources/Textures/internal/foliage/weeping_willow.png new file mode 100644 index 0000000..38c32ea Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/foliage/weeping_willow.png differ diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 0c122b2..50b7c67 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -96,7 +96,7 @@ public class EditorApp extends Application { private String contentLastSection = "character"; private ToggleButton contentCharBtn, contentQuestBtn, contentFraktionBtn, contentItemBtn, contentRezeptBtn, contentCraftingBtn, - contentAreaBtn, contentLocationBtn; + contentAreaBtn, contentLocationBtn, contentLangBtn; private CheckMenuItem dualWindowMenuItem; private CheckMenuItem showGridItem; private Button contentSwitchBtn; // "> Content" in Welt-Toolbar (nur 1-Fenster) @@ -9642,7 +9642,13 @@ public class EditorApp extends Application { else if (input.ctrlHeld) input.debugNoLightToggle = true; } else if (input.ctrlHeld) input.debugNoLightToggle = true; } - case F9 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wasserMergedBtn != null) wasserMergedBtn.fire(); }); } + case F9 -> { + if (pressed && input.ctrlHeld) Platform.runLater(() -> { + if (!dualWindowMode && !"content".equals(currentTool)) switchToContentEditor(); + if (contentLangBtn != null) contentLangBtn.fire(); + }); + else if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wasserMergedBtn != null) wasserMergedBtn.fire(); }); + } case F10 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wegeBtn != null) wegeBtn.fire(); }); } case F11 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (zonenBtn != null) zonenBtn.fire(); }); } case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) Platform.runLater(() -> { if (playToolBtn != null) playToolBtn.fire(); }); } @@ -12959,10 +12965,12 @@ public class EditorApp extends Application { contentCraftingBtn = new ToggleButton("# Crafting"); contentAreaBtn = new ToggleButton("□ Area"); contentLocationBtn = new ToggleButton("^ Location"); + contentLangBtn = new ToggleButton("≈ Lang"); String bold = "-fx-font-weight:bold;"; for (ToggleButton b : new ToggleButton[]{contentCharBtn, contentQuestBtn, contentFraktionBtn, - contentItemBtn, contentRezeptBtn, contentCraftingBtn, contentAreaBtn, contentLocationBtn}) { + contentItemBtn, contentRezeptBtn, contentCraftingBtn, contentAreaBtn, contentLocationBtn, + contentLangBtn}) { b.setStyle(bold); } @@ -12975,6 +12983,7 @@ public class EditorApp extends Application { contentCraftingBtn.setToggleGroup(cg); contentAreaBtn.setToggleGroup(cg); contentLocationBtn.setToggleGroup(cg); + contentLangBtn.setToggleGroup(cg); contentCharBtn.setOnAction(e -> { contentLastSection = "character"; buildCharacterViewInContent(); @@ -13000,6 +13009,9 @@ public class EditorApp extends Application { contentLocationBtn.setOnAction(e -> { contentLastSection = "location"; buildLocationViewInContent(); }); + contentLangBtn.setOnAction(e -> { + contentLastSection = "lang"; buildLocalizationViewInContent(); + }); ToolBar tb = new ToolBar(); tb.getItems().addAll( @@ -13008,7 +13020,8 @@ public class EditorApp extends Application { new Separator(Orientation.VERTICAL), contentFraktionBtn, new Separator(Orientation.VERTICAL), contentItemBtn, new Separator(Orientation.VERTICAL), contentRezeptBtn, contentCraftingBtn, - new Separator(Orientation.VERTICAL), contentAreaBtn, contentLocationBtn); + new Separator(Orientation.VERTICAL), contentAreaBtn, contentLocationBtn, + new Separator(Orientation.VERTICAL), contentLangBtn); if (includeWeltBtn) { Button weltBtn = new Button("< Welt"); weltBtn.setStyle("-fx-font-weight:bold;"); @@ -13443,6 +13456,7 @@ public class EditorApp extends Application { case "crafting" -> contentCraftingBtn.fire(); case "area" -> contentAreaBtn.fire(); case "location" -> contentLocationBtn.fire(); + case "lang" -> contentLangBtn.fire(); } } @@ -13574,6 +13588,15 @@ public class EditorApp extends Application { if (contentLocationNode != null) contentLocationNode.setExpanded(true); } + private void buildLocalizationViewInContent() { + collapseAllContentSections(); + onF5 = null; + contentTarget().setCenter(new de.blight.editor.ui.LocalizationEditorView( + de.blight.editor.ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"))); + contentTarget().setRight(null); + contentTarget().setBottom(null); + } + // ── Baum-Refresh-Methoden ───────────────────────────────────────────────── private void refreshContentQuestTree() { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java index bc51be8..cadfe1b 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java @@ -8,7 +8,6 @@ import de.blight.common.model.TextKeyStore; import de.blight.common.model.TextRegistry; import de.blight.editor.ProjectRoot; import javafx.animation.PauseTransition; -import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -24,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; public class LocalizationEditorView extends BorderPane { @@ -33,10 +33,14 @@ public class LocalizationEditorView extends BorderPane { private final ComboBox langCombo = new ComboBox<>(); // Text-Tab - private final ObservableList tableData = FXCollections.observableArrayList(); - private final TableView table = new TableView<>(tableData); + private final ObservableList tableData = FXCollections.observableArrayList(); + private final FilteredList filteredData = new FilteredList<>(tableData, s -> true); + private final TableView table = new TableView<>(filteredData); private TextBundle currentBundle = null; + // Cache bekannter Keys für Row-Highlighting (aktualisiert in loadLanguage) + private Set knownKeys = new HashSet<>(); + // Audio-Tab private final ObservableList audioData = FXCollections.observableArrayList(); private final TableView audioTable = new TableView<>(audioData); @@ -59,7 +63,7 @@ public class LocalizationEditorView extends BorderPane { Label langLbl = new Label("Sprache:"); langLbl.setStyle("-fx-text-fill: #aaa;"); langCombo.setPrefWidth(120); - langCombo.setOnAction(e -> loadLanguage(langCombo.getValue())); + langCombo.valueProperty().addListener((obs, o, n) -> loadLanguage(n)); Button addLangBtn = new Button("+ Sprache"); addLangBtn.setOnAction(e -> addLanguage()); @@ -87,43 +91,87 @@ public class LocalizationEditorView extends BorderPane { // Text-Tab private VBox buildTextTabContent() { - Button addKeyBtn = new Button("+ Schluessel"); + // ── Filter ──────────────────────────────────────────────────────────── + TextField filterField = new TextField(); + filterField.setPromptText("Filtern (Schlüssel oder Text)…"); + filterField.setStyle("-fx-background-color: #252535; -fx-text-fill: #ddd; -fx-prompt-text-fill: #666;" + + "-fx-border-color: #444; -fx-border-width: 1; -fx-background-radius: 3; -fx-border-radius: 3;"); + filterField.textProperty().addListener((obs, o, n) -> { + String q = n == null ? "" : n.toLowerCase(); + filteredData.setPredicate(row -> + q.isBlank() + || row[0].toLowerCase().contains(q) + || row[1].toLowerCase().contains(q)); + }); + + HBox filterBar = new HBox(6, new Label(" "), filterField); + HBox.setHgrow(filterField, Priority.ALWAYS); + filterBar.setPadding(new Insets(5, 12, 5, 12)); + filterBar.setAlignment(Pos.CENTER_LEFT); + filterBar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;"); + Label filterLbl = new Label("Filter:"); + filterLbl.setStyle("-fx-text-fill: #aaa;"); + filterBar.getChildren().set(0, filterLbl); + + // ── Button-Leiste ───────────────────────────────────────────────────── + Button addKeyBtn = new Button("+ Schlüssel"); addKeyBtn.setOnAction(e -> addKey()); Button delKeyBtn = new Button("- Entfernen"); delKeyBtn.setOnAction(e -> deleteKey()); - Button sortBtn = new Button("A->Z"); + Button sortBtn = new Button("A→Z"); sortBtn.setOnAction(e -> tableData.sort(Comparator.comparing(r -> r[0]))); - HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn); + Button fillBtn = new Button("Fehlende ergänzen"); + fillBtn.setStyle("-fx-background-color: #2a4a2a; -fx-text-fill: #ccffcc;"); + fillBtn.setOnAction(e -> fillMissingKeys()); + + HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn, new Separator(), fillBtn); subBar.setPadding(new Insets(6, 12, 6, 12)); + subBar.setAlignment(Pos.CENTER_LEFT); subBar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;"); + // ── Tabelle ─────────────────────────────────────────────────────────── table.setEditable(true); table.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"); @SuppressWarnings("deprecation") var policy = TableView.CONSTRAINED_RESIZE_POLICY; table.setColumnResizePolicy(policy); - TableColumn keyCol = new TableColumn<>("Schluessel"); - keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0])); - keyCol.setCellFactory(TextFieldTableCell.forTableColumn()); - keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); table.refresh(); scheduleSave(); }); + // Schlüssel: read-only + TableColumn keyCol = new TableColumn<>("Schlüssel"); + keyCol.setCellValueFactory(cd -> new javafx.beans.property.SimpleStringProperty(cd.getValue()[0])); + keyCol.setEditable(false); keyCol.setPrefWidth(280); + // Text: editierbar TableColumn valCol = new TableColumn<>("Text"); - valCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1])); + valCol.setCellValueFactory(cd -> new javafx.beans.property.SimpleStringProperty(cd.getValue()[1])); valCol.setCellFactory(TextFieldTableCell.forTableColumn()); valCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); table.refresh(); scheduleSave(); }); table.getColumns().addAll(List.of(keyCol, valCol)); + + // Row-Factory: unbekannte Schlüssel orange hervorheben + table.setRowFactory(tv -> new TableRow<>() { + @Override + protected void updateItem(String[] item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null || item[0].isBlank()) { + setStyle(""); + } else { + setStyle(knownKeys.contains(item[0]) ? "" : "-fx-background-color: #3a2510;"); + } + } + }); + VBox.setVgrow(table, Priority.ALWAYS); - Label hint = new Label("Doppelklick zum Bearbeiten eines Eintrags."); + Label hint = new Label("Schlüssel (orange = nicht in TextKeyStore registriert). Doppelklick auf Text zum Bearbeiten."); hint.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-padding: 2 12 4 12;"); - VBox box = new VBox(subBar, table, hint); + VBox box = new VBox(filterBar, subBar, table, hint); VBox.setVgrow(table, Priority.ALWAYS); return box; } @@ -131,16 +179,16 @@ public class LocalizationEditorView extends BorderPane { // Audio-Tab private VBox buildAudioTabContent() { - Button addKeyBtn = new Button("+ Schluessel"); + Button addKeyBtn = new Button("+ Schlüssel"); addKeyBtn.setOnAction(e -> addAudioKey()); Button delKeyBtn = new Button("- Entfernen"); delKeyBtn.setOnAction(e -> deleteAudioKey()); - Button sortBtn = new Button("A->Z"); + Button sortBtn = new Button("A→Z"); sortBtn.setOnAction(e -> audioData.sort(Comparator.comparing(r -> r[0]))); - Button fillDialogBtn = new Button("Dialog-Schluessel befuellen"); + Button fillDialogBtn = new Button("Dialog-Schlüssel befüllen"); fillDialogBtn.setStyle("-fx-background-color: #3a4a6a; -fx-text-fill: white;"); fillDialogBtn.setOnAction(e -> autoFillDialogKeys()); @@ -154,14 +202,14 @@ public class LocalizationEditorView extends BorderPane { var policy2 = TableView.CONSTRAINED_RESIZE_POLICY; audioTable.setColumnResizePolicy(policy2); - TableColumn keyCol = new TableColumn<>("Schluessel"); - keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0])); + TableColumn keyCol = new TableColumn<>("Schlüssel"); + keyCol.setCellValueFactory(cd -> new javafx.beans.property.SimpleStringProperty(cd.getValue()[0])); keyCol.setCellFactory(TextFieldTableCell.forTableColumn()); keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); audioTable.refresh(); scheduleSave(); }); keyCol.setPrefWidth(220); TableColumn pathCol = new TableColumn<>("Datei"); - pathCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1])); + pathCol.setCellValueFactory(cd -> new javafx.beans.property.SimpleStringProperty(cd.getValue()[1])); pathCol.setCellFactory(TextFieldTableCell.forTableColumn()); pathCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); audioTable.refresh(); scheduleSave(); }); @@ -177,7 +225,7 @@ public class LocalizationEditorView extends BorderPane { int idx = getIndex(); if (idx < 0 || idx >= audioData.size()) return; FileChooser fc = new FileChooser(); - fc.setTitle("Audio-Datei waehlen"); + fc.setTitle("Audio-Datei wählen"); fc.getExtensionFilters().addAll( new FileChooser.ExtensionFilter("Audio", "*.ogg", "*.wav", "*.mp3"), new FileChooser.ExtensionFilter("Alle Dateien", "*.*") @@ -211,7 +259,7 @@ public class LocalizationEditorView extends BorderPane { audioTable.getColumns().addAll(List.of(keyCol, pathCol, browseCol)); VBox.setVgrow(audioTable, Priority.ALWAYS); - Label hint = new Label("Schluessel muss dem AudioReference-Key im Dialog entsprechen."); + Label hint = new Label("Schlüssel muss dem AudioReference-Key im Dialog entsprechen."); hint.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-padding: 2 12 4 12;"); VBox box = new VBox(subBar, audioTable, hint); @@ -243,6 +291,9 @@ public class LocalizationEditorView extends BorderPane { } tableData.clear(); currentBundle.getEntries().forEach((k, v) -> tableData.add(new String[]{k, v})); + + knownKeys = new HashSet<>(TextKeyStore.getKeys()); + TextRegistry.clear(); TextRegistry.registerAll(currentBundle.getEntries()); @@ -254,7 +305,7 @@ public class LocalizationEditorView extends BorderPane { private void addLanguage() { TextInputDialog dlg = new TextInputDialog(); dlg.initOwner(Dialogs.primaryWindow()); - dlg.setTitle("Sprache hinzufuegen"); + dlg.setTitle("Sprache hinzufügen"); dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):"); dlg.showAndWait().ifPresent(lang -> { lang = lang.trim().toLowerCase(); @@ -274,7 +325,7 @@ public class LocalizationEditorView extends BorderPane { String lang = langCombo.getValue(); if (lang == null) return; Alert confirm = Dialogs.alert(Alert.AlertType.CONFIRMATION, - "Sprache '" + lang + "' wirklich loeschen?", ButtonType.YES, ButtonType.NO); + "Sprache '" + lang + "' wirklich löschen?", ButtonType.YES, ButtonType.NO); confirm.showAndWait().ifPresent(bt -> { if (bt == ButtonType.YES) { try { @@ -296,23 +347,25 @@ public class LocalizationEditorView extends BorderPane { private void addKey() { if (currentBundle == null) return; - Set existing = currentBundle.getEntries().keySet(); - List available = new ArrayList<>(TextKeyStore.getKeys()); - available.removeAll(existing); + Set existing = new HashSet<>(); + for (String[] row : tableData) existing.add(row[0]); + List available = TextKeyStore.getKeys().stream() + .filter(k -> !existing.contains(k)) + .collect(Collectors.toList()); if (available.isEmpty()) { TextInputDialog dlg = new TextInputDialog(); dlg.initOwner(Dialogs.primaryWindow()); - dlg.setTitle("Schluessel hinzufuegen"); - dlg.setHeaderText("Alle bekannten Schluessel bereits vorhanden.\nNeuen Schluessel eingeben:"); + dlg.setTitle("Schlüssel hinzufügen"); + dlg.setHeaderText("Alle bekannten Schlüssel bereits vorhanden.\nNeuen Schlüssel eingeben:"); dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(this::insertKey); return; } Dialog dlg = new Dialog<>(); dlg.initOwner(Dialogs.primaryWindow()); - dlg.setTitle("Schluessel waehlen"); - dlg.setHeaderText("Noch nicht uebersetzte Schluessel:"); + dlg.setTitle("Schlüssel wählen"); + dlg.setHeaderText("Noch nicht übersetzte Schlüssel:"); dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); ObservableList items = FXCollections.observableArrayList(available); @@ -335,8 +388,14 @@ public class LocalizationEditorView extends BorderPane { private void insertKey(String key) { tableData.add(new String[]{key, ""}); - table.scrollTo(tableData.size() - 1); - table.getSelectionModel().select(tableData.size() - 1); + // Scroll zum neuen Eintrag — in filteredData nach Index suchen + for (int i = 0; i < filteredData.size(); i++) { + if (filteredData.get(i)[0].equals(key)) { + table.scrollTo(i); + table.getSelectionModel().select(i); + break; + } + } scheduleSave(); } @@ -345,13 +404,36 @@ public class LocalizationEditorView extends BorderPane { if (sel != null) { tableData.remove(sel); scheduleSave(); } } + private void fillMissingKeys() { + Set existing = new HashSet<>(); + for (String[] row : tableData) existing.add(row[0]); + + List toAdd = TextKeyStore.getKeys().stream() + .filter(k -> !existing.contains(k)) + .sorted() + .collect(Collectors.toList()); + + for (String k : toAdd) tableData.add(new String[]{k, ""}); + if (!toAdd.isEmpty()) { + tableData.sort(Comparator.comparing(r -> r[0])); + knownKeys = new HashSet<>(TextKeyStore.getKeys()); + scheduleSave(); + } + + Dialogs.alert(Alert.AlertType.INFORMATION, + toAdd.isEmpty() + ? "Alle bekannten Schlüssel sind bereits vorhanden." + : toAdd.size() + " Schlüssel ergänzt.", + ButtonType.OK).showAndWait(); + } + // Audio-Eintrags-Verwaltung private void addAudioKey() { TextInputDialog dlg = new TextInputDialog(); dlg.initOwner(Dialogs.primaryWindow()); - dlg.setTitle("Audio-Schluessel hinzufuegen"); - dlg.setHeaderText("Schluessel eingeben (muss dem AudioReference-Key im Dialog entsprechen):"); + dlg.setTitle("Audio-Schlüssel hinzufügen"); + dlg.setHeaderText("Schlüssel eingeben (muss dem AudioReference-Key im Dialog entsprechen):"); dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(key -> { audioData.add(new String[]{key, ""}); audioTable.scrollTo(audioData.size() - 1); @@ -382,8 +464,8 @@ public class LocalizationEditorView extends BorderPane { scheduleSave(); } Dialogs.alert(Alert.AlertType.INFORMATION, - added.isEmpty() ? "Alle Dialog-Schluessel bereits vorhanden." - : added.size() + " Schluessel hinzugefuegt.", + added.isEmpty() ? "Alle Dialog-Schlüssel bereits vorhanden." + : added.size() + " Schlüssel hinzugefügt.", ButtonType.OK).showAndWait(); } @@ -407,7 +489,7 @@ public class LocalizationEditorView extends BorderPane { private void saveCurrent() { if (currentBundle == null) { - Dialogs.alert(Alert.AlertType.WARNING, "Keine Sprache ausgewaehlt.", ButtonType.OK).showAndWait(); + Dialogs.alert(Alert.AlertType.WARNING, "Keine Sprache ausgewählt.", ButtonType.OK).showAndWait(); return; } Map textEntries = new LinkedHashMap<>();