diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java index 2219281d76..60d2bd92cc 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java @@ -59,6 +59,14 @@ public class Messages SelectFolder, ShowHideDetails, SizeLimitsText, + TextAreaContextMenuCopy, + TextAreaContextMenuCut, + TextAreaContextMenuDelete, + TextAreaContextMenuPaste, + TextAreaContextMenuPasteURLAsMarkdown, + TextAreaContextMenuRedo, + TextAreaContextMenuSelectAll, + TextAreaContextMenuUndo, UnsupportedFileType, UpdateLogEntry; diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java index 2768de3958..a4ae21527c 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java @@ -49,11 +49,21 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.input.*; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; + + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import org.phoebus.logbook.olog.ui.LogbookUIPreferences; + import javafx.util.Callback; import javafx.util.StringConverter; import org.phoebus.framework.autocomplete.Proposal; @@ -72,7 +82,6 @@ import org.phoebus.logbook.LogbookPreferences; import org.phoebus.logbook.Tag; import org.phoebus.logbook.olog.ui.HelpViewer; -import org.phoebus.logbook.olog.ui.LogbookUIPreferences; import org.phoebus.logbook.olog.ui.Messages; import org.phoebus.logbook.olog.ui.PreviewViewer; import org.phoebus.olog.es.api.model.OlogLog; @@ -193,6 +202,8 @@ public class LogEntryEditorController { @SuppressWarnings("unused") private Node attachmentsPane; + + private final ContextMenu logbookDropDown = new ContextMenu(); private final ContextMenu tagDropDown = new ContextMenu(); @@ -272,6 +283,7 @@ public LogEntryEditorController(LogEntry logEntry, LogEntry inReplyTo, EditMode @FXML public void initialize() { + // Remote log service not reachable, so show error pane. if (!checkConnectivity()) { errorPane.visibleProperty().set(true); @@ -394,16 +406,6 @@ public void initialize() { return text.substring(0, text.length() - 2); }, selectedLogbooks)); - tagsSelection.textProperty().bind(Bindings.createStringBinding(() -> { - if (selectedTags.isEmpty()) { - return ""; - } - StringBuilder stringBuilder = new StringBuilder(); - selectedTags.forEach(l -> stringBuilder.append(l).append(", ")); - String text = stringBuilder.toString(); - return text.substring(0, text.length() - 2); - }, selectedTags)); - logbooksDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) -> { if (!newVal && !tagDropDown.isShowing() && !logbookDropDown.isShowing()) @@ -451,16 +453,6 @@ public void initialize() { newSelection.forEach(t -> updateDropDown(tagDropDown, t, true)); }); - selectedLogbooks.addListener((ListChangeListener) change -> { - if (change.getList() == null) { - return; - } - List newSelection = new ArrayList<>(change.getList()); - logbooksPopOver.setAvailable(availableLogbooksAsStringList, newSelection); - logbooksPopOver.setSelected(newSelection); - newSelection.forEach(l -> updateDropDown(logbookDropDown, l, true)); - }); - AutocompleteMenu autocompleteMenu = new AutocompleteMenu(new ProposalService(new ProposalProvider() { @Override public String getName() { @@ -549,8 +541,149 @@ public LogTemplate fromString(String name) { // Note: logbooks and tags are retrieved asynchronously from service getServerSideStaticData(); + + setupTextAreaContextMenu(); + } + /** + * Sets up the context menu for the {@link TextArea}. While a {@link TextArea} comes with a default + * context menu containing the standard items (copy, paste...), the ability to access this context + * menu is not possible since Java9, see this post. + * Any customization means the whole context menu must be built from scratch. + */ + private void setupTextAreaContextMenu() { + // Create the context menu with default items + ContextMenu contextMenu = new ContextMenu(); + + // Standard text editing items + MenuItem undo = new MenuItem(Messages.TextAreaContextMenuUndo); + undo.setOnAction(e -> textArea.undo()); + + MenuItem redo = new MenuItem(Messages.TextAreaContextMenuRedo); + redo.setOnAction(e -> textArea.redo()); + + MenuItem cut = new MenuItem(Messages.TextAreaContextMenuCut); + cut.setOnAction(e -> textArea.cut()); + + MenuItem copy = new MenuItem(Messages.TextAreaContextMenuCopy); + copy.setOnAction(e -> textArea.copy()); + + MenuItem paste = new MenuItem(Messages.TextAreaContextMenuPaste); + paste.setOnAction(e -> textArea.paste()); + + MenuItem delete = new MenuItem(Messages.TextAreaContextMenuDelete); + delete.setOnAction(e -> textArea.replaceSelection("")); + + MenuItem selectAll = new MenuItem(Messages.TextAreaContextMenuSelectAll); + selectAll.setOnAction(e -> textArea.selectAll()); + + // Our custom menu item + MenuItem pasteUrlItem = new MenuItem(Messages.TextAreaContextMenuPasteURLAsMarkdown); + pasteUrlItem.setOnAction(event -> handleSmartPaste()); + pasteUrlItem.setAccelerator(new KeyCodeCombination(KeyCode.V, + KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); + + // Add all items to the menu + contextMenu.getItems().addAll( + undo, + redo, + new SeparatorMenuItem(), + cut, + copy, + paste, + delete, + new SeparatorMenuItem(), + selectAll, + new SeparatorMenuItem(), + pasteUrlItem + ); + + // Bind the menu items to the text area's state + undo.disableProperty().bind(textArea.undoableProperty().not()); + redo.disableProperty().bind(textArea.redoableProperty().not()); + cut.disableProperty().bind(textArea.selectedTextProperty().isEmpty()); + copy.disableProperty().bind(textArea.selectedTextProperty().isEmpty()); + delete.disableProperty().bind(textArea.selectedTextProperty().isEmpty()); + contextMenu.setOnShowing(e -> { + String clipboardContent = Clipboard.getSystemClipboard().getString(); + pasteUrlItem.setDisable(clipboardContent == null || !clipboardContent.toLowerCase().startsWith("http")); + }); + // Set the context menu on the text area + textArea.setContextMenu(contextMenu); + } + + private void handleSmartPaste() { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + if (!clipboard.hasString()) { + return; + } + + String clipboardText = clipboard.getString(); + String ologUrl = extractOlogUrl(clipboardText); + String selectedText = textArea.getSelectedText(); + + if (ologUrl != null) { + // It's an Olog URL + String logNumber = extractLogNumber(ologUrl); + if (logNumber != null) { + if (selectedText != null && !selectedText.isEmpty()) { + // Use selected text as link text for the Olog reference + String markdownLink = String.format("[%s](%s)", selectedText, ologUrl); + textArea.replaceSelection(markdownLink); + } else { + // No selection - create a standard log entry reference + String markdownLink = String.format("[%s](%s)", logNumber, ologUrl); + textArea.replaceSelection(markdownLink); + } + } + return; + } + + // Try to identify if clipboard content is a regular URL + try { + new URL(clipboardText); + + if (selectedText != null && !selectedText.isEmpty()) { + // Replace selection with markdown link using selected text + String markdownLink = String.format("[%s](%s)", selectedText, clipboardText); + textArea.replaceSelection(markdownLink); + } else { + // No selection - use URL as both link text and target + String markdownLink = String.format("[%s](%s)", clipboardText, clipboardText); + textArea.replaceSelection(markdownLink); + } + } catch (MalformedURLException e) { + // Not a URL - do nothing + } + } + + private String extractOlogUrl(String text) { + String rootUrl = LogbookUIPreferences.web_client_root_URL; + if (rootUrl == null || rootUrl.isEmpty()) { + return null; + } + + if (text.toLowerCase().contains("olog") && + text.matches(".*?/logs/\\d+/?$")) { + return text; + } + return null; + } + + private String extractLogNumber(String url) { + if (url == null) { + return null; + } + Pattern pattern = Pattern.compile("/logs/(\\d+)/?$"); + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + /** * Handler for Cancel button. Note that any selections in the {@link SelectionService} are * cleared to prevent next launch of {@link org.phoebus.logbook.olog.ui.menu.SendToLogBookApp} @@ -760,7 +893,7 @@ private void getServerSideStaticData() { List preSelectedLogbooks = logEntry.getLogbooks().stream().map(Logbook::getName).toList(); List defaultLogbooks = Arrays.asList(LogbookUIPreferences.default_logbooks); - availableLogbooksAsStringList.forEach(logbook -> { + for (String logbook : availableLogbooksAsStringList) { CheckBox checkBox = new CheckBox(logbook); CustomMenuItem newLogbook = new CustomMenuItem(checkBox); newLogbook.setHideOnClick(false); @@ -781,7 +914,7 @@ private void getServerSideStaticData() { selectedLogbooks.add(logbook); } logbookDropDown.getItems().add(newLogbook); - }); + } availableTags = logClient.listTags(); availableTagsAsStringList = diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties index ffa16f2efd..d07f1c680a 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties @@ -115,6 +115,14 @@ TagsTitle=Select Tags TagsTooltip=Add tag to the log entry. Templates=Templates: Text=Text: +TextAreaContextMenuCopy=Copy +TextAreaContextMenuCut=Cut +TextAreaContextMenuDelete=Delete +TextAreaContextMenuPaste=Paste +TextAreaContextMenuPasteURLAsMarkdown=Paste URL as Markdown +TextAreaContextMenuRedo=Redo +TextAreaContextMenuSelectAll=Select All +TextAreaContextMenuUndo=Undo Time=Time: Title=Title: UnableToDownloadArchived=