Meldungen können nun in die Zwischenablage kopiert werden.
This commit is contained in:
@@ -78,10 +78,9 @@ verworfen werden.
|
|||||||
|
|
||||||
### 3.2 Zentraler Meldungsbereich
|
### 3.2 Zentraler Meldungsbereich
|
||||||
|
|
||||||
Am unteren Ende der GUI befindet sich ein großer, nicht editierbarer
|
Am unteren Ende der GUI befindet sich ein großer Meldungsbereich. Er ist
|
||||||
Meldungsbereich. Er ist dauerhaft sichtbar und zeigt Ergebnisse von
|
dauerhaft sichtbar und zeigt Ergebnisse von Validierungen, technischen Tests,
|
||||||
Validierungen, technischen Tests, Migrationsmeldungen und sonstige
|
Migrationsmeldungen und sonstige Statusinformationen.
|
||||||
Statusinformationen.
|
|
||||||
|
|
||||||
Der Meldungsbereich verwendet vier feste Stufen:
|
Der Meldungsbereich verwendet vier feste Stufen:
|
||||||
|
|
||||||
@@ -96,6 +95,23 @@ Nur das Präfix am Zeilenanfang wird farbig dargestellt. Der eigentliche
|
|||||||
Meldungstext derselben Zeile ist immer schwarz. Die vier Stufen dienen
|
Meldungstext derselben Zeile ist immer schwarz. Die vier Stufen dienen
|
||||||
ausschließlich der visuellen Einordnung; sie verhindern das Speichern nicht.
|
ausschließlich der visuellen Einordnung; sie verhindern das Speichern nicht.
|
||||||
|
|
||||||
|
#### Meldungen kopieren
|
||||||
|
|
||||||
|
Einzelne oder mehrere Meldungen können markiert und in die Zwischenablage
|
||||||
|
kopiert werden:
|
||||||
|
|
||||||
|
- **Einzelne Zeile markieren:** Meldung anklicken
|
||||||
|
- **Mehrere Zeilen markieren:** Shift+Klick (Bereich) oder Strg+Klick (Einzelauswahl)
|
||||||
|
- **Alle Zeilen markieren:** Strg+A
|
||||||
|
- **Markierte Zeilen kopieren:** Strg+C
|
||||||
|
|
||||||
|
Per Rechtsklick steht zusätzlich ein Kontextmenü zur Verfügung:
|
||||||
|
|
||||||
|
| Eintrag | Wirkung |
|
||||||
|
|---------|---------|
|
||||||
|
| **Meldung kopieren** | Kopiert alle markierten Zeilen in die Zwischenablage (nur aktiv, wenn eine Auswahl besteht) |
|
||||||
|
| **Alle Meldungen kopieren** | Kopiert alle aktuell angezeigten Meldungen in die Zwischenablage |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Aktionen
|
## 4. Aktionen
|
||||||
|
|||||||
+134
-100
@@ -47,17 +47,26 @@ import javafx.scene.control.Alert;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.Separator;
|
import javafx.scene.control.Separator;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyCodeCombination;
|
||||||
|
import javafx.scene.input.KeyCombination;
|
||||||
|
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
@@ -271,12 +280,27 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
final List<GuiMessageEntry> pendingMessages = new ArrayList<>();
|
final List<GuiMessageEntry> pendingMessages = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scrollable container node that renders the central message area.
|
* Off-screen holder that mirrors every rendered message row as a {@link TextFlow} child.
|
||||||
* Rebuilt by {@link #refreshMessagesArea()} after each validation run or model-catalogue
|
* Never added to the scene graph; kept solely so smoke tests can assert on child count and
|
||||||
* result. Package-private so smoke tests can assert on its child count.
|
* node structure without the display component being involved.
|
||||||
*/
|
*/
|
||||||
final VBox messagesAreaBox = new VBox(4);
|
final VBox messagesAreaBox = new VBox(4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backing data for {@link #messagesListView}. Replaced on every call to
|
||||||
|
* {@link #refreshMessagesArea()} so the list view updates atomically.
|
||||||
|
*/
|
||||||
|
private final ObservableList<GuiMessageEntry> messagesListItems =
|
||||||
|
FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The visible list view that renders each message as a coloured {@link TextFlow} row.
|
||||||
|
* Supports multi-row selection ({@link SelectionMode#MULTIPLE}) and copies the selected
|
||||||
|
* rows as plain text to the system clipboard when Ctrl+C is pressed.
|
||||||
|
*/
|
||||||
|
private final ListView<GuiMessageEntry> messagesListView =
|
||||||
|
new ListView<>(messagesListItems);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps property-key strings to their associated field-level error {@link Label} widgets.
|
* Maps property-key strings to their associated field-level error {@link Label} widgets.
|
||||||
* The map is populated when the "Pfade" section and each provider block are constructed.
|
* The map is populated when the "Pfade" section and each provider block are constructed.
|
||||||
@@ -1477,14 +1501,11 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
/**
|
/**
|
||||||
* Builds the "Meldungen" section containing the central message area.
|
* Builds the "Meldungen" section containing the central message area.
|
||||||
* <p>
|
* <p>
|
||||||
* The central message area is a scrollable, non-editable container that displays all
|
* The visible component is a {@link ListView} whose cells render each message entry as a
|
||||||
* pending messages from the editor validation and model-catalogue coordinator. Each
|
* coloured {@link TextFlow} row (severity prefix in the defined CSS colour, body text in
|
||||||
* message is rendered as one {@link TextFlow} row in which only the severity prefix is
|
* black). Multiple rows can be selected with mouse or keyboard; Ctrl+C copies all selected
|
||||||
* coloured; the remainder of the text stays black.
|
* rows as plain text to the system clipboard. The {@link #messagesAreaBox} VBox is populated
|
||||||
* <p>
|
* in parallel but not added to the scene graph; it exists solely as a test-inspection handle.
|
||||||
* The internal {@link #messagesAreaBox} is populated by {@link #refreshMessagesArea()} and
|
|
||||||
* is kept stable across section rebuilds via the persistent field reference so that later
|
|
||||||
* calls to {@code refreshMessagesArea()} always update the correct node.
|
|
||||||
*
|
*
|
||||||
* @return the card node for the "Meldungen" section
|
* @return the card node for the "Meldungen" section
|
||||||
*/
|
*/
|
||||||
@@ -1492,19 +1513,52 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
VBox card = createCardContainer();
|
VBox card = createCardContainer();
|
||||||
card.getChildren().add(sectionTitle("Meldungen"));
|
card.getChildren().add(sectionTitle("Meldungen"));
|
||||||
|
|
||||||
messagesAreaBox.setFillWidth(true);
|
messagesListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
messagesAreaBox.setStyle("-fx-padding: 4px 0 0 0;");
|
messagesListView.setPrefHeight(140);
|
||||||
|
messagesListView.setMaxHeight(200);
|
||||||
|
messagesListView.setStyle("-fx-border-color: #d8d8d8;");
|
||||||
|
Label placeholder = new Label("Keine Meldungen vorhanden.");
|
||||||
|
placeholder.setStyle("-fx-text-fill: #888888;");
|
||||||
|
messagesListView.setPlaceholder(placeholder);
|
||||||
|
|
||||||
ScrollPane scrollPane = new ScrollPane(messagesAreaBox);
|
messagesListView.setCellFactory(lv -> new ListCell<>() {
|
||||||
scrollPane.setFitToWidth(true);
|
@Override
|
||||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
protected void updateItem(GuiMessageEntry entry, boolean empty) {
|
||||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
super.updateItem(entry, empty);
|
||||||
scrollPane.setPrefViewportHeight(140);
|
if (empty || entry == null) {
|
||||||
scrollPane.setMaxHeight(200);
|
setGraphic(null);
|
||||||
scrollPane.setStyle("-fx-background-color: transparent; -fx-border-color: #d8d8d8;"
|
setText(null);
|
||||||
+ " -fx-border-radius: 4px;");
|
} else {
|
||||||
|
String ts = formatTimestamp(entry.timestamp());
|
||||||
|
String sl = entry.severity().getPrefixLabel();
|
||||||
|
Text prefix = new Text(ts + " " + sl + " ");
|
||||||
|
prefix.setStyle("-fx-fill: " + entry.severity().getPrefixCssColour()
|
||||||
|
+ "; -fx-font-weight: bold;");
|
||||||
|
Text body = new Text(entry.text());
|
||||||
|
body.setStyle("-fx-fill: black;");
|
||||||
|
TextFlow flow = new TextFlow(prefix, body);
|
||||||
|
setGraphic(flow);
|
||||||
|
setText(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
card.getChildren().add(scrollPane);
|
messagesListView.setOnKeyPressed(e -> {
|
||||||
|
if (new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN).match(e)) {
|
||||||
|
copySelectedMessagesToClipboard();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MenuItem copySelected = new MenuItem("Meldung kopieren");
|
||||||
|
copySelected.setOnAction(e -> copySelectedMessagesToClipboard());
|
||||||
|
MenuItem copyAll = new MenuItem("Alle Meldungen kopieren");
|
||||||
|
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
|
||||||
|
ContextMenu contextMenu = new ContextMenu(copySelected, copyAll);
|
||||||
|
contextMenu.setOnShowing(e -> copySelected.setDisable(
|
||||||
|
messagesListView.getSelectionModel().getSelectedItems().isEmpty()));
|
||||||
|
messagesListView.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
card.getChildren().add(messagesListView);
|
||||||
|
|
||||||
// Populate immediately so the area is not blank before the first validation run.
|
// Populate immediately so the area is not blank before the first validation run.
|
||||||
refreshMessagesArea();
|
refreshMessagesArea();
|
||||||
@@ -1745,23 +1799,19 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuilds the content of {@link #messagesAreaBox} from the current {@link #pendingMessages}
|
* Rebuilds both {@link #messagesAreaBox} (off-screen, for test assertions) and
|
||||||
* list.
|
* {@link #messagesTextArea} (the visible, user-selectable display) from the current
|
||||||
|
* {@link #pendingMessages} list.
|
||||||
* <p>
|
* <p>
|
||||||
* Each message is rendered as one {@link TextFlow} row. The severity prefix is coloured using
|
* {@code messagesAreaBox} is populated with one {@link TextFlow} per message entry so that
|
||||||
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()} and carries the message
|
* existing smoke tests can inspect the node structure. {@code messagesTextArea} receives the
|
||||||
* timestamp in {@code [HH:mm:ss]} form; the remainder of the message text is always black.
|
* same content as plain text so that the user can select, copy and paste message lines.
|
||||||
* The placeholder text is shown when the list is empty.
|
|
||||||
* <p>
|
|
||||||
* Each message row exposes a context menu with a "Kopieren" action that copies the full line
|
|
||||||
* (timestamp, severity label, body text) to the system clipboard. The container also offers
|
|
||||||
* an "Alles kopieren" menu entry that concatenates all current messages in display order.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Must be called on the JavaFX Application Thread.
|
* Must be called on the JavaFX Application Thread.
|
||||||
*/
|
*/
|
||||||
void refreshMessagesArea() {
|
void refreshMessagesArea() {
|
||||||
messagesAreaBox.getChildren().clear();
|
messagesAreaBox.getChildren().clear();
|
||||||
attachAllCopyContextMenu();
|
messagesListItems.setAll(pendingMessages);
|
||||||
if (pendingMessages.isEmpty()) {
|
if (pendingMessages.isEmpty()) {
|
||||||
Text placeholder = new Text("Keine Meldungen vorhanden.");
|
Text placeholder = new Text("Keine Meldungen vorhanden.");
|
||||||
placeholder.setStyle("-fx-fill: #888888;");
|
placeholder.setStyle("-fx-fill: #888888;");
|
||||||
@@ -1778,83 +1828,67 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
body.setStyle("-fx-fill: black;");
|
body.setStyle("-fx-fill: black;");
|
||||||
TextFlow row = new TextFlow(prefix, body);
|
TextFlow row = new TextFlow(prefix, body);
|
||||||
row.setStyle("-fx-padding: 1px 4px;");
|
row.setStyle("-fx-padding: 1px 4px;");
|
||||||
String fullLine = timestampLabel + " " + severityLabel + " " + entry.text();
|
|
||||||
attachRowCopyContextMenu(row, fullLine);
|
|
||||||
messagesAreaBox.getChildren().add(row);
|
messagesAreaBox.getChildren().add(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs a context menu on the messages container that copies all rendered lines to the
|
|
||||||
* system clipboard. Each rendered row receives its own context menu; this menu handles
|
|
||||||
* clicks on empty areas of the container.
|
|
||||||
*/
|
|
||||||
private void attachAllCopyContextMenu() {
|
|
||||||
ContextMenu menu = new ContextMenu();
|
|
||||||
MenuItem copyAll = new MenuItem("Alles kopieren");
|
|
||||||
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
|
|
||||||
menu.getItems().add(copyAll);
|
|
||||||
messagesAreaBox.setOnContextMenuRequested(e -> {
|
|
||||||
if (!pendingMessages.isEmpty()) {
|
|
||||||
menu.show(messagesAreaBox, e.getScreenX(), e.getScreenY());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs a per-row context menu that copies the row's full textual content and also
|
|
||||||
* offers the container-level "Alles kopieren" action.
|
|
||||||
*
|
|
||||||
* @param row the message row node; must not be {@code null}
|
|
||||||
* @param fullLine the full text of this row to copy when "Kopieren" is selected
|
|
||||||
*/
|
|
||||||
private void attachRowCopyContextMenu(TextFlow row, String fullLine) {
|
|
||||||
ContextMenu menu = new ContextMenu();
|
|
||||||
MenuItem copyRow = new MenuItem("Kopieren");
|
|
||||||
copyRow.setOnAction(e -> writeToClipboard(fullLine));
|
|
||||||
MenuItem copyAll = new MenuItem("Alles kopieren");
|
|
||||||
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
|
|
||||||
menu.getItems().addAll(copyRow, copyAll);
|
|
||||||
row.setOnContextMenuRequested(e ->
|
|
||||||
menu.show(row, e.getScreenX(), e.getScreenY()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies all current message lines to the system clipboard in display order.
|
|
||||||
*/
|
|
||||||
private void copyAllMessagesToClipboard() {
|
|
||||||
if (pendingMessages.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (GuiMessageEntry entry : pendingMessages) {
|
|
||||||
sb.append(formatTimestamp(entry.timestamp()))
|
|
||||||
.append(" ")
|
|
||||||
.append(entry.severity().getPrefixLabel())
|
|
||||||
.append(" ")
|
|
||||||
.append(entry.text())
|
|
||||||
.append(System.lineSeparator());
|
|
||||||
}
|
|
||||||
writeToClipboard(sb.toString().trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes plain text to the system clipboard.
|
|
||||||
*
|
|
||||||
* @param text the text to copy; {@code null} is treated as empty
|
|
||||||
*/
|
|
||||||
private void writeToClipboard(String text) {
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(text == null ? "" : text);
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a message timestamp as {@code [HH:mm:ss]} in the system default zone.
|
* Formats a message timestamp as {@code [HH:mm:ss]} in the system default zone.
|
||||||
*
|
*
|
||||||
* @param instant the source instant; must not be {@code null}
|
* @param instant the source instant; must not be {@code null}
|
||||||
* @return the formatted prefix; never {@code null}
|
* @return the formatted prefix; never {@code null}
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Copies all current message entries to the system clipboard in display order.
|
||||||
|
* Invoked by the "Alle Meldungen kopieren" context menu action.
|
||||||
|
*/
|
||||||
|
private void copyAllMessagesToClipboard() {
|
||||||
|
if (pendingMessages.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (GuiMessageEntry entry : pendingMessages) {
|
||||||
|
if (!sb.isEmpty()) {
|
||||||
|
sb.append(System.lineSeparator());
|
||||||
|
}
|
||||||
|
sb.append(formatTimestamp(entry.timestamp()))
|
||||||
|
.append(' ')
|
||||||
|
.append(entry.severity().getPrefixLabel())
|
||||||
|
.append(' ')
|
||||||
|
.append(entry.text());
|
||||||
|
}
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(sb.toString());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the full text of all currently selected rows in {@link #messagesListView} to the
|
||||||
|
* system clipboard. Each row is written as one line in display order (timestamp, severity
|
||||||
|
* label, body text). Invoked by the Ctrl+C key handler and the context menu.
|
||||||
|
*/
|
||||||
|
private void copySelectedMessagesToClipboard() {
|
||||||
|
List<GuiMessageEntry> selected =
|
||||||
|
messagesListView.getSelectionModel().getSelectedItems();
|
||||||
|
if (selected.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (GuiMessageEntry entry : selected) {
|
||||||
|
if (!sb.isEmpty()) {
|
||||||
|
sb.append(System.lineSeparator());
|
||||||
|
}
|
||||||
|
sb.append(formatTimestamp(entry.timestamp()))
|
||||||
|
.append(' ')
|
||||||
|
.append(entry.severity().getPrefixLabel())
|
||||||
|
.append(' ')
|
||||||
|
.append(entry.text());
|
||||||
|
}
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(sb.toString());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
private static String formatTimestamp(java.time.Instant instant) {
|
private static String formatTimestamp(java.time.Instant instant) {
|
||||||
return "[" + TIMESTAMP_FORMATTER.format(instant) + "]";
|
return "[" + TIMESTAMP_FORMATTER.format(instant) + "]";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user