Meldungen können nun in die Zwischenablage kopiert werden.
This commit is contained in:
+134
-100
@@ -47,17 +47,26 @@ import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.Clipboard;
|
||||
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.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -271,12 +280,27 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
final List<GuiMessageEntry> pendingMessages = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The scrollable container node that renders the central message area.
|
||||
* Rebuilt by {@link #refreshMessagesArea()} after each validation run or model-catalogue
|
||||
* result. Package-private so smoke tests can assert on its child count.
|
||||
* Off-screen holder that mirrors every rendered message row as a {@link TextFlow} child.
|
||||
* Never added to the scene graph; kept solely so smoke tests can assert on child count and
|
||||
* node structure without the display component being involved.
|
||||
*/
|
||||
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.
|
||||
* 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.
|
||||
* <p>
|
||||
* The central message area is a scrollable, non-editable container that displays all
|
||||
* pending messages from the editor validation and model-catalogue coordinator. Each
|
||||
* message is rendered as one {@link TextFlow} row in which only the severity prefix is
|
||||
* coloured; the remainder of the text stays black.
|
||||
* <p>
|
||||
* 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.
|
||||
* The visible component is a {@link ListView} whose cells render each message entry as a
|
||||
* coloured {@link TextFlow} row (severity prefix in the defined CSS colour, body text in
|
||||
* black). Multiple rows can be selected with mouse or keyboard; Ctrl+C copies all selected
|
||||
* rows as plain text to the system clipboard. The {@link #messagesAreaBox} VBox is populated
|
||||
* in parallel but not added to the scene graph; it exists solely as a test-inspection handle.
|
||||
*
|
||||
* @return the card node for the "Meldungen" section
|
||||
*/
|
||||
@@ -1492,19 +1513,52 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
VBox card = createCardContainer();
|
||||
card.getChildren().add(sectionTitle("Meldungen"));
|
||||
|
||||
messagesAreaBox.setFillWidth(true);
|
||||
messagesAreaBox.setStyle("-fx-padding: 4px 0 0 0;");
|
||||
messagesListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
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);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setPrefViewportHeight(140);
|
||||
scrollPane.setMaxHeight(200);
|
||||
scrollPane.setStyle("-fx-background-color: transparent; -fx-border-color: #d8d8d8;"
|
||||
+ " -fx-border-radius: 4px;");
|
||||
messagesListView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(GuiMessageEntry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
if (empty || entry == null) {
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
} 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.
|
||||
refreshMessagesArea();
|
||||
@@ -1745,23 +1799,19 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Rebuilds the content of {@link #messagesAreaBox} from the current {@link #pendingMessages}
|
||||
* list.
|
||||
* Rebuilds both {@link #messagesAreaBox} (off-screen, for test assertions) and
|
||||
* {@link #messagesTextArea} (the visible, user-selectable display) from the current
|
||||
* {@link #pendingMessages} list.
|
||||
* <p>
|
||||
* Each message is rendered as one {@link TextFlow} row. The severity prefix is coloured using
|
||||
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()} and carries the message
|
||||
* timestamp in {@code [HH:mm:ss]} form; the remainder of the message text is always black.
|
||||
* 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.
|
||||
* {@code messagesAreaBox} is populated with one {@link TextFlow} per message entry so that
|
||||
* existing smoke tests can inspect the node structure. {@code messagesTextArea} receives the
|
||||
* same content as plain text so that the user can select, copy and paste message lines.
|
||||
* <p>
|
||||
* Must be called on the JavaFX Application Thread.
|
||||
*/
|
||||
void refreshMessagesArea() {
|
||||
messagesAreaBox.getChildren().clear();
|
||||
attachAllCopyContextMenu();
|
||||
messagesListItems.setAll(pendingMessages);
|
||||
if (pendingMessages.isEmpty()) {
|
||||
Text placeholder = new Text("Keine Meldungen vorhanden.");
|
||||
placeholder.setStyle("-fx-fill: #888888;");
|
||||
@@ -1778,83 +1828,67 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
body.setStyle("-fx-fill: black;");
|
||||
TextFlow row = new TextFlow(prefix, body);
|
||||
row.setStyle("-fx-padding: 1px 4px;");
|
||||
String fullLine = timestampLabel + " " + severityLabel + " " + entry.text();
|
||||
attachRowCopyContextMenu(row, fullLine);
|
||||
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.
|
||||
*
|
||||
* @param instant the source instant; must not be {@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) {
|
||||
return "[" + TIMESTAMP_FORMATTER.format(instant) + "]";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user