4949import javafx .scene .control .TextArea ;
5050import javafx .scene .control .TextField ;
5151import javafx .scene .control .ToggleButton ;
52+ import javafx .scene .control .SeparatorMenuItem ;
5253import javafx .scene .image .Image ;
5354import javafx .scene .image .ImageView ;
55+ import javafx .scene .input .*;
5456import javafx .scene .layout .HBox ;
5557import javafx .scene .layout .VBox ;
5658import 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+
5767import javafx .util .Callback ;
5868import javafx .util .StringConverter ;
5969import org .phoebus .framework .autocomplete .Proposal ;
7282import org .phoebus .logbook .LogbookPreferences ;
7383import org .phoebus .logbook .Tag ;
7484import org .phoebus .logbook .olog .ui .HelpViewer ;
75- import org .phoebus .logbook .olog .ui .LogbookUIPreferences ;
7685import org .phoebus .logbook .olog .ui .Messages ;
7786import org .phoebus .logbook .olog .ui .PreviewViewer ;
7887import 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 =
0 commit comments