Skip to content

Commit 9b8f817

Browse files
authored
Merge pull request #3500 from ControlSystemStudio/CS-3323
add extra paste as markdown textarea popup menu and shortcut key
2 parents 7908999 + 0b2c469 commit 9b8f817

File tree

3 files changed

+172
-23
lines changed

3 files changed

+172
-23
lines changed

app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ public class Messages
5959
SelectFolder,
6060
ShowHideDetails,
6161
SizeLimitsText,
62+
TextAreaContextMenuCopy,
63+
TextAreaContextMenuCut,
64+
TextAreaContextMenuDelete,
65+
TextAreaContextMenuPaste,
66+
TextAreaContextMenuPasteURLAsMarkdown,
67+
TextAreaContextMenuRedo,
68+
TextAreaContextMenuSelectAll,
69+
TextAreaContextMenuUndo,
6270
UnsupportedFileType,
6371
UpdateLogEntry;
6472

app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java

Lines changed: 156 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,21 @@
4949
import javafx.scene.control.TextArea;
5050
import javafx.scene.control.TextField;
5151
import javafx.scene.control.ToggleButton;
52+
import javafx.scene.control.SeparatorMenuItem;
5253
import javafx.scene.image.Image;
5354
import javafx.scene.image.ImageView;
55+
import javafx.scene.input.*;
5456
import javafx.scene.layout.HBox;
5557
import javafx.scene.layout.VBox;
5658
import javafx.scene.paint.Color;
59+
60+
61+
import java.net.MalformedURLException;
62+
import java.net.URL;
63+
import java.util.regex.Pattern;
64+
import java.util.regex.Matcher;
65+
import org.phoebus.logbook.olog.ui.LogbookUIPreferences;
66+
5767
import javafx.util.Callback;
5868
import javafx.util.StringConverter;
5969
import org.phoebus.framework.autocomplete.Proposal;
@@ -72,7 +82,6 @@
7282
import org.phoebus.logbook.LogbookPreferences;
7383
import org.phoebus.logbook.Tag;
7484
import org.phoebus.logbook.olog.ui.HelpViewer;
75-
import org.phoebus.logbook.olog.ui.LogbookUIPreferences;
7685
import org.phoebus.logbook.olog.ui.Messages;
7786
import org.phoebus.logbook.olog.ui.PreviewViewer;
7887
import org.phoebus.olog.es.api.model.OlogLog;
@@ -193,6 +202,8 @@ public class LogEntryEditorController {
193202
@SuppressWarnings("unused")
194203
private Node attachmentsPane;
195204

205+
206+
196207
private final ContextMenu logbookDropDown = new ContextMenu();
197208
private final ContextMenu tagDropDown = new ContextMenu();
198209

@@ -272,6 +283,7 @@ public LogEntryEditorController(LogEntry logEntry, LogEntry inReplyTo, EditMode
272283
@FXML
273284
public void initialize() {
274285

286+
275287
// Remote log service not reachable, so show error pane.
276288
if (!checkConnectivity()) {
277289
errorPane.visibleProperty().set(true);
@@ -394,16 +406,6 @@ public void initialize() {
394406
return text.substring(0, text.length() - 2);
395407
}, selectedLogbooks));
396408

397-
tagsSelection.textProperty().bind(Bindings.createStringBinding(() -> {
398-
if (selectedTags.isEmpty()) {
399-
return "";
400-
}
401-
StringBuilder stringBuilder = new StringBuilder();
402-
selectedTags.forEach(l -> stringBuilder.append(l).append(", "));
403-
String text = stringBuilder.toString();
404-
return text.substring(0, text.length() - 2);
405-
}, selectedTags));
406-
407409
logbooksDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) ->
408410
{
409411
if (!newVal && !tagDropDown.isShowing() && !logbookDropDown.isShowing())
@@ -451,16 +453,6 @@ public void initialize() {
451453
newSelection.forEach(t -> updateDropDown(tagDropDown, t, true));
452454
});
453455

454-
selectedLogbooks.addListener((ListChangeListener<String>) change -> {
455-
if (change.getList() == null) {
456-
return;
457-
}
458-
List<String> newSelection = new ArrayList<>(change.getList());
459-
logbooksPopOver.setAvailable(availableLogbooksAsStringList, newSelection);
460-
logbooksPopOver.setSelected(newSelection);
461-
newSelection.forEach(l -> updateDropDown(logbookDropDown, l, true));
462-
});
463-
464456
AutocompleteMenu autocompleteMenu = new AutocompleteMenu(new ProposalService(new ProposalProvider() {
465457
@Override
466458
public String getName() {
@@ -549,8 +541,149 @@ public LogTemplate fromString(String name) {
549541

550542
// Note: logbooks and tags are retrieved asynchronously from service
551543
getServerSideStaticData();
544+
545+
setupTextAreaContextMenu();
546+
552547
}
553548

549+
/**
550+
* Sets up the context menu for the {@link TextArea}. While a {@link TextArea} comes with a default
551+
* context menu containing the standard items (copy, paste...), the ability to access this context
552+
* menu is not possible since Java9, see <a href="https://stackoverflow.com/questions/71053358/javafx-17-custom-textarea-textfield-right-click-menu">this post</a>.
553+
* Any customization means the whole context menu must be built from scratch.
554+
*/
555+
private void setupTextAreaContextMenu() {
556+
// Create the context menu with default items
557+
ContextMenu contextMenu = new ContextMenu();
558+
559+
// Standard text editing items
560+
MenuItem undo = new MenuItem(Messages.TextAreaContextMenuUndo);
561+
undo.setOnAction(e -> textArea.undo());
562+
563+
MenuItem redo = new MenuItem(Messages.TextAreaContextMenuRedo);
564+
redo.setOnAction(e -> textArea.redo());
565+
566+
MenuItem cut = new MenuItem(Messages.TextAreaContextMenuCut);
567+
cut.setOnAction(e -> textArea.cut());
568+
569+
MenuItem copy = new MenuItem(Messages.TextAreaContextMenuCopy);
570+
copy.setOnAction(e -> textArea.copy());
571+
572+
MenuItem paste = new MenuItem(Messages.TextAreaContextMenuPaste);
573+
paste.setOnAction(e -> textArea.paste());
574+
575+
MenuItem delete = new MenuItem(Messages.TextAreaContextMenuDelete);
576+
delete.setOnAction(e -> textArea.replaceSelection(""));
577+
578+
MenuItem selectAll = new MenuItem(Messages.TextAreaContextMenuSelectAll);
579+
selectAll.setOnAction(e -> textArea.selectAll());
580+
581+
// Our custom menu item
582+
MenuItem pasteUrlItem = new MenuItem(Messages.TextAreaContextMenuPasteURLAsMarkdown);
583+
pasteUrlItem.setOnAction(event -> handleSmartPaste());
584+
pasteUrlItem.setAccelerator(new KeyCodeCombination(KeyCode.V,
585+
KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
586+
587+
// Add all items to the menu
588+
contextMenu.getItems().addAll(
589+
undo,
590+
redo,
591+
new SeparatorMenuItem(),
592+
cut,
593+
copy,
594+
paste,
595+
delete,
596+
new SeparatorMenuItem(),
597+
selectAll,
598+
new SeparatorMenuItem(),
599+
pasteUrlItem
600+
);
601+
602+
// Bind the menu items to the text area's state
603+
undo.disableProperty().bind(textArea.undoableProperty().not());
604+
redo.disableProperty().bind(textArea.redoableProperty().not());
605+
cut.disableProperty().bind(textArea.selectedTextProperty().isEmpty());
606+
copy.disableProperty().bind(textArea.selectedTextProperty().isEmpty());
607+
delete.disableProperty().bind(textArea.selectedTextProperty().isEmpty());
608+
contextMenu.setOnShowing(e -> {
609+
String clipboardContent = Clipboard.getSystemClipboard().getString();
610+
pasteUrlItem.setDisable(clipboardContent == null || !clipboardContent.toLowerCase().startsWith("http"));
611+
});
612+
// Set the context menu on the text area
613+
textArea.setContextMenu(contextMenu);
614+
}
615+
616+
private void handleSmartPaste() {
617+
final Clipboard clipboard = Clipboard.getSystemClipboard();
618+
if (!clipboard.hasString()) {
619+
return;
620+
}
621+
622+
String clipboardText = clipboard.getString();
623+
String ologUrl = extractOlogUrl(clipboardText);
624+
String selectedText = textArea.getSelectedText();
625+
626+
if (ologUrl != null) {
627+
// It's an Olog URL
628+
String logNumber = extractLogNumber(ologUrl);
629+
if (logNumber != null) {
630+
if (selectedText != null && !selectedText.isEmpty()) {
631+
// Use selected text as link text for the Olog reference
632+
String markdownLink = String.format("[%s](%s)", selectedText, ologUrl);
633+
textArea.replaceSelection(markdownLink);
634+
} else {
635+
// No selection - create a standard log entry reference
636+
String markdownLink = String.format("[%s](%s)", logNumber, ologUrl);
637+
textArea.replaceSelection(markdownLink);
638+
}
639+
}
640+
return;
641+
}
642+
643+
// Try to identify if clipboard content is a regular URL
644+
try {
645+
new URL(clipboardText);
646+
647+
if (selectedText != null && !selectedText.isEmpty()) {
648+
// Replace selection with markdown link using selected text
649+
String markdownLink = String.format("[%s](%s)", selectedText, clipboardText);
650+
textArea.replaceSelection(markdownLink);
651+
} else {
652+
// No selection - use URL as both link text and target
653+
String markdownLink = String.format("[%s](%s)", clipboardText, clipboardText);
654+
textArea.replaceSelection(markdownLink);
655+
}
656+
} catch (MalformedURLException e) {
657+
// Not a URL - do nothing
658+
}
659+
}
660+
661+
private String extractOlogUrl(String text) {
662+
String rootUrl = LogbookUIPreferences.web_client_root_URL;
663+
if (rootUrl == null || rootUrl.isEmpty()) {
664+
return null;
665+
}
666+
667+
if (text.toLowerCase().contains("olog") &&
668+
text.matches(".*?/logs/\\d+/?$")) {
669+
return text;
670+
}
671+
return null;
672+
}
673+
674+
private String extractLogNumber(String url) {
675+
if (url == null) {
676+
return null;
677+
}
678+
Pattern pattern = Pattern.compile("/logs/(\\d+)/?$");
679+
Matcher matcher = pattern.matcher(url);
680+
if (matcher.find()) {
681+
return matcher.group(1);
682+
}
683+
return null;
684+
}
685+
686+
554687
/**
555688
* Handler for Cancel button. Note that any selections in the {@link SelectionService} are
556689
* cleared to prevent next launch of {@link org.phoebus.logbook.olog.ui.menu.SendToLogBookApp}
@@ -760,7 +893,7 @@ private void getServerSideStaticData() {
760893
List<String> preSelectedLogbooks =
761894
logEntry.getLogbooks().stream().map(Logbook::getName).toList();
762895
List<String> defaultLogbooks = Arrays.asList(LogbookUIPreferences.default_logbooks);
763-
availableLogbooksAsStringList.forEach(logbook -> {
896+
for (String logbook : availableLogbooksAsStringList) {
764897
CheckBox checkBox = new CheckBox(logbook);
765898
CustomMenuItem newLogbook = new CustomMenuItem(checkBox);
766899
newLogbook.setHideOnClick(false);
@@ -781,7 +914,7 @@ private void getServerSideStaticData() {
781914
selectedLogbooks.add(logbook);
782915
}
783916
logbookDropDown.getItems().add(newLogbook);
784-
});
917+
}
785918

786919
availableTags = logClient.listTags();
787920
availableTagsAsStringList =

app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ TagsTitle=Select Tags
115115
TagsTooltip=Add tag to the log entry.
116116
Templates=Templates:
117117
Text=Text:
118+
TextAreaContextMenuCopy=Copy
119+
TextAreaContextMenuCut=Cut
120+
TextAreaContextMenuDelete=Delete
121+
TextAreaContextMenuPaste=Paste
122+
TextAreaContextMenuPasteURLAsMarkdown=Paste URL as Markdown
123+
TextAreaContextMenuRedo=Redo
124+
TextAreaContextMenuSelectAll=Select All
125+
TextAreaContextMenuUndo=Undo
118126
Time=Time:
119127
Title=Title:
120128
UnableToDownloadArchived=

0 commit comments

Comments
 (0)