diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TextEntryWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TextEntryWidget.java index 405cee85db..72692861ca 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TextEntryWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TextEntryWidget.java @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright (c) 2015-2020 Oak Ridge National Laboratory. + * Copyright (c) 2025 Thales. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -9,22 +10,32 @@ import static org.csstudio.display.builder.model.ModelPlugin.logger; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newBooleanPropertyDescriptor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newIntegerPropertyDescriptor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newPVNamePropertyDescriptor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newStringPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propConfirmDialog; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propConfirmMessage; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propEnabled; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propForegroundColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFormat; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontalAlignment; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propItemsFromPV; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPrecision; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propShowUnits; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propVerticalAlignment; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propWrapWords; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; +import java.util.stream.Collectors; import org.csstudio.display.builder.model.MacroizedWidgetProperty; import org.csstudio.display.builder.model.Messages; import org.csstudio.display.builder.model.Version; @@ -77,6 +88,46 @@ public Widget createWidget() public static final WidgetPropertyDescriptor propMultiLine = newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "multi_line", Messages.WidgetProperties_MultiLine); + + + /** + * 'items' property: list of items (string properties) for auto-completion + */ + public static final WidgetPropertyDescriptor propItems = + newPVNamePropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "autocompleteitems", "Suggestions"); + + /** + * 'min_chars' property: minimum characters to trigger autocomplete + */ + private static final WidgetPropertyDescriptor propMinCharacters = + newIntegerPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "min_characters", "Min Characters"); + + /** + * 'case_sensitive' property: whether matching is case sensitive + */ + private static final WidgetPropertyDescriptor propCaseSensitive = + newBooleanPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "case_sensitive", "Case Sensitive"); + + /** + * 'placeholder' property: placeholder text when empty + */ + private static final WidgetPropertyDescriptor propPlaceholder = + newStringPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "placeholder", "Placeholder Text"); + + /** + * 'allowcustom' property: allow custom values + */ + private static final WidgetPropertyDescriptor propCustom = + newBooleanPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "allow_custom", + "Allow custom values"); + + /** + * 'filter_mode' property: how to filter suggestions (starts_with, contains, fuzzy) + */ + private static final WidgetPropertyDescriptor propFilterMode = + newStringPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "filter_mode", "Filter Mode"); + + private static class CustomWidgetConfigurator extends WidgetConfigurator { public CustomWidgetConfigurator(final Version xml_version) @@ -282,6 +333,17 @@ static void readLegacyFormat(final Element xml, final WidgetProperty multi_line; private volatile WidgetProperty horizontal_alignment; private volatile WidgetProperty vertical_alignment; + private volatile WidgetProperty items; + private volatile WidgetProperty items_from_pv; + private volatile WidgetProperty confirm_dialog; + private volatile WidgetProperty confirm_message; + private volatile WidgetProperty password; + private volatile WidgetProperty min_characters; + private volatile WidgetProperty case_sensitive; + private volatile WidgetProperty placeholder; + private volatile WidgetProperty filter_mode; + private volatile WidgetProperty allow_custom; + private volatile List itemsList = List.of(); /** Constructor */ public TextEntryWidget() @@ -317,6 +379,18 @@ protected void defineProperties(final List> properties) properties.add(wrap_words = propWrapWords.createProperty(this, false)); properties.add(multi_line = propMultiLine.createProperty(this, false)); BorderSupport.addBorderProperties(this, properties); + + properties.add(items = propItems.createProperty(this, "")); + properties.add(items_from_pv = propItemsFromPV.createProperty(this, false)); + properties.add(min_characters = propMinCharacters.createProperty(this, 1)); + properties.add(case_sensitive = propCaseSensitive.createProperty(this, false)); + properties.add(placeholder = propPlaceholder.createProperty(this, "Type to search...")); + properties.add(filter_mode = propFilterMode.createProperty(this, "fuzzy")); + properties.add(allow_custom = propCustom.createProperty(this, false)); + + properties.add(confirm_dialog = propConfirmDialog.createProperty(this, false)); + properties.add(confirm_message = propConfirmMessage.createProperty(this, "Are you sure you want to do this?")); + properties.add(password = propPassword.createProperty(this, "")); } /** @return 'foreground_color' property */ @@ -384,4 +458,139 @@ public WidgetProperty propVerticalAlignment() { return vertical_alignment; } + + /** + * @return 'items' property + */ + public WidgetProperty propItems() { + return items; + } + + /** + * Convenience routine for script to fetch items + * + * @return Items currently available for auto-completion + */ + public Collection getItems() { + return itemsList; + } + + /** + * Set the items list for auto-completion + * + * @param items List of items to set + */ + public void setItems(List items) { + this.itemsList = Objects.requireNonNullElseGet(items, List::of); + } + + /** + * @return 'items_from_PV' property + */ + public WidgetProperty propItemsFromPV() { + return items_from_pv; + } + + /** + * @return 'confirm_dialog' property + */ + public WidgetProperty propConfirmDialog() { + return confirm_dialog; + } + + /** + * @return 'confirm_message' property + */ + public WidgetProperty propConfirmMessage() { + return confirm_message; + } + + /** + * @return 'password' property + */ + public WidgetProperty propPassword() { + return password; + } + + /** + * @return 'min_characters' property + */ + public WidgetProperty propMinCharacters() { + return min_characters; + } + + /** + * @return 'case_sensitive' property + */ + public WidgetProperty propCaseSensitive() { + return case_sensitive; + } + + /** + * @return 'placeholder' property + */ + public WidgetProperty propPlaceholder() { + return placeholder; + } + + /** + * @return 'filter_mode' property + */ + public WidgetProperty propFilterMode() { + return filter_mode; + } + + /** + * @return 'allow_custom' property + */ + public WidgetProperty propCustom() { + return allow_custom; + } + + /** + * Filter items based on input text and return top N matches + * + * @param inputText Text to filter by + * @return List of filtered suggestions (max N items) + */ + public List getFilteredSuggestions(final String inputText) { + if (inputText == null || inputText.length() < min_characters.getValue()) { + return List.of(); + } + + final String searchText = case_sensitive.getValue() ? inputText : inputText.toLowerCase(); + final String mode = filter_mode.getValue(); + + return getItems().stream() + .filter(item -> { + final String itemText = case_sensitive.getValue() ? item : item.toLowerCase(); + return switch (mode) { + case "starts_with" -> itemText.startsWith(searchText); + case "fuzzy" -> fuzzyMatch(itemText, searchText); + default -> itemText.contains(searchText); + }; + }) + .collect(Collectors.toList()); + } + + /** + * Simple fuzzy matching algorithm for filtering option. + * + * @param text Text to search in + * @param pattern Pattern to search for + * @return true if pattern fuzzy matches text + */ + private boolean fuzzyMatch(final String text, final String pattern) { + int textIndex = 0; + int patternIndex = 0; + + while (textIndex < text.length() && patternIndex < pattern.length()) { + if (text.charAt(textIndex) == pattern.charAt(patternIndex)) { + patternIndex++; + } + textIndex++; + } + + return patternIndex == pattern.length(); + } } diff --git a/app/display/model/src/main/resources/examples/01_main.bob b/app/display/model/src/main/resources/examples/01_main.bob index 7bba02d356..add3939bcd 100644 --- a/app/display/model/src/main/resources/examples/01_main.bob +++ b/app/display/model/src/main/resources/examples/01_main.bob @@ -1,830 +1,850 @@ - - - Main - 1010 - 570 - - - - - - Label_9 - COMMENT - Press buttons to see sub-displays. - -Note you can also navigate between buttons via <Tab> key or <Shift> and cursor keys, -then press <SPACE> to activate button. - 10 - 490 - 640 - 80 - - - - - - - - - true - true - - - Label - TITLE - Display Builder Examples - 0 - 0 - 280 - 30 - - - - - - - - - true - - - Information - Information - 50 - 120 - 30 - - - - - - - Action Button - - - 02_intro.bob - replace - Introduction - - - 80 - 120 - 26 - $(actions) - - - Action Button_7 - - - 03_properties.bob - replace - Properties - - - 120 - 120 - 26 - $(actions) - - - Action Button_25 - - - use_classes.bob - replace - Classes - - - 160 - 120 - 26 - $(actions) - - - Action Button_9 - - - 04_macros.bob - replace - Macros - - - 200 - 120 - 26 - $(actions) - - - Action Button_10 - - - 05_actions.bob - replace - Actions - - - 240 - 120 - 26 - $(actions) - - - Action Button_37 - - - 09_rules.bob - replace - Rules - - - 280 - 120 - 26 - $(actions) - - - Action Button_6 - - - 10_scripts.bob - replace - Scripts - - - 320 - 120 - 26 - $(actions) - - - Action Button_40 - - - 11_datasources.bob - replace - Datasources - - - 360 - 120 - 26 - $(actions) - - - Action Button_41 - - - 12_formulas.bob - replace - Formulas - - - 400 - 120 - 26 - $(actions) - - - Action Button_26 - - - enablement.bob - replace - Enablement - - - 440 - 120 - 26 - $(actions) - - - Widgets - Widgets - 190 - 50 - 120 - 30 - - - - - - - Graphics - Graphics - 190 - 90 - 120 - 30 - - - - - - - Action Button_1 - - - graphics_label.bob - replace - Label - - - 190 - 120 - 120 - 26 - $(actions) - - - Action Button_8 - - - graphics_picture.bob - replace - Picture - - - 190 - 160 - 120 - 26 - $(actions) - - - Action Button_17 - - - graphics_rect.bob - replace - Rectangle - - - 190 - 200 - 120 - 26 - $(actions) - - - Action Button_38 - - - graphics_arc.bob - replace - Arc - - - 190 - 240 - 120 - 26 - $(actions) - - - Action Button_39 - - - graphics_poly.bob - replace - Polygon/line - - - 190 - 280 - 120 - 26 - $(actions) - - - Monitors - Monitors - 330 - 90 - 120 - 30 - - - - - - - Action Button_2 - - - monitors_textupdate.bob - replace - Text Update - - - 330 - 120 - 120 - 26 - $(actions) - - - Action Button_3 - - - monitors_led.bob - replace - LED - - - 330 - 160 - 120 - 26 - $(actions) - - - Action Button_20 - - - monitors_bytemonitor.bob - replace - Byte Monitor - - - 330 - 200 - 120 - 26 - $(actions) - - - Action Button_34 - - - monitors_tank.bob - replace - Tank - - - 330 - 240 - 120 - 26 - $(actions) - - - Action Button_35 - - - monitors_table.bob - replace - Table - - - 330 - 280 - 120 - 26 - $(actions) - - - Action Button_27 - - - monitors_gauge.bob - replace - Gauges - - - 330 - 360 - 120 - 26 - - - - $(actions) - - - Action Button_28 - - - monitors_meter.bob - replace - Meters - - - 330 - 400 - 120 - 26 - - - - $(actions) - - - Action Button_32 - - - monitors_symbols.bob - replace - Symbols - - - 330 - 320 - 120 - 26 - $(actions) - - - Controls - Controls - 470 - 90 - 120 - 30 - - - - - - - Action Button_14 - - - controls_textentry.bob - replace - Text Entry - - - 470 - 120 - 120 - 26 - $(actions) - - - Action Button_15 - - - controls_toggle_buttons.bob - replace - Toggle Buttons - - - 470 - 160 - 120 - 26 - $(actions) - - - Action Button_16 - - - controls_action_buttons.bob - replace - Action Buttons - - - 470 - 200 - 120 - 26 - $(actions) - - - Action Button_18 - - - controls_spinner_slider_scrollbar.bob - replace - Incr. Controls - - - 470 - 240 - 120 - 26 - $(actions) - - - Action Button_19 - - - controls_combo.bob - replace - Combo Box - - - 470 - 280 - 120 - 26 - $(actions) - - - Action Button_36 - - - controls_checkboxes_slidebuttons.bob - replace - Check Boxes - - - 470 - 320 - 120 - 26 - $(actions) - - - Action Button_23 - - - controls_radio.bob - replace - Radio Button - - - 470 - 360 - 120 - 26 - $(actions) - - - Action Button_22 - - - controls_fileselector.bob - replace - File Selector - - - 470 - 400 - 120 - 26 - $(actions) - - - Action Button_34 - - - controls_thumbwheel.bob - replace - Thumb Wheel - - - 470 - 480 - 120 - 26 - - - - $(actions) - - - Plots - Plots - 610 - 90 - 120 - 30 - - - - - - - XY - - - plots_xy.bob - replace - X/Y Plot - - - 610 - 120 - 120 - 26 - $(actions) - - - Strip - - - plots_strip.bob - replace - Strip Chart - - - 610 - 160 - 120 - 26 - $(actions) - - - DB - - - plots_databrowser.bob - replace - Data Browser - - - 610 - 200 - 120 - 26 - $(actions) - - - Image - - - plots_image.bob - replace - Image - - - 610 - 240 - 120 - 26 - $(actions) - - - Structure - Structure - 750 - 90 - 120 - 30 - - - - - - - Action Button_13 - - - structure_group.bob - replace - Group - - - 750 - 120 - 120 - 26 - $(actions) - - - Action Button_11 - - - embedded/structure_embedded.bob - replace - Embedded - - - 750 - 160 - 120 - 26 - $(actions) - - - Action Button_12 - - - structure_tabs.bob - replace - Tabs - - - 750 - 200 - 120 - 26 - $(actions) - - - Action Button_31 - - - structure_navtabs.bob - replace - Navigation Tabs - - - 750 - 240 - 120 - 26 - $(actions) - - - Action Button_30 - - - structure_array.bob - replace - Array - - - 750 - 280 - 120 - 26 - $(actions) - - - Action Button_21 - - - template_and_instance/example.bob - replace - Template & Inst. - - - 750 - 320 - 120 - 26 - $(actions) - - - Misc - Miscellaneous - 890 - 90 - 120 - 30 - - - - - - - Action Button_20 - - - misc_webbrowser.bob - replace - Web Browser - - - 890 - 120 - 120 - 26 - $(actions) - - - Action Button_24 - - - misc_clock.bob - replace - Clocks - - - 890 - 200 - 120 - 26 - - - - $(actions) - - - Action Button_29 - - - fishtank/heater.bob - replace - Fishtank - - - 890 - 160 - 120 - 26 - $(actions) - - \ No newline at end of file + + + Main + 1010 + 570 + + + + + + Label_9 + COMMENT + Press buttons to see sub-displays. + +Note you can also navigate between buttons via <Tab> key or <Shift> and cursor keys, +then press <SPACE> to activate button. + 10 + 520 + 640 + 80 + + + + + + + + + true + true + + + Label + TITLE + Display Builder Examples + 0 + 0 + 280 + 30 + + + + + + + + + true + + + Information + Information + 50 + 120 + 30 + + + + + + + Action Button + + + Introduction + 02_intro.bob + replace + + + 80 + 120 + 26 + $(actions) + + + Action Button_7 + + + Properties + 03_properties.bob + replace + + + 120 + 120 + 26 + $(actions) + + + Action Button_25 + + + Classes + use_classes.bob + replace + + + 160 + 120 + 26 + $(actions) + + + Action Button_9 + + + Macros + 04_macros.bob + replace + + + 200 + 120 + 26 + $(actions) + + + Action Button_10 + + + Actions + 05_actions.bob + replace + + + 240 + 120 + 26 + $(actions) + + + Action Button_37 + + + Rules + 09_rules.bob + replace + + + 280 + 120 + 26 + $(actions) + + + Action Button_6 + + + Scripts + 10_scripts.bob + replace + + + 320 + 120 + 26 + $(actions) + + + Action Button_40 + + + Datasources + 11_datasources.bob + replace + + + 360 + 120 + 26 + $(actions) + + + Action Button_41 + + + Formulas + 12_formulas.bob + replace + + + 400 + 120 + 26 + $(actions) + + + Action Button_26 + + + Enablement + enablement.bob + replace + + + 440 + 120 + 26 + $(actions) + + + Widgets + Widgets + 190 + 50 + 120 + 30 + + + + + + + Graphics + Graphics + 190 + 90 + 120 + 30 + + + + + + + Action Button_1 + + + Label + graphics_label.bob + replace + + + 190 + 120 + 120 + 26 + $(actions) + + + Action Button_8 + + + Picture + graphics_picture.bob + replace + + + 190 + 160 + 120 + 26 + $(actions) + + + Action Button_17 + + + Rectangle + graphics_rect.bob + replace + + + 190 + 200 + 120 + 26 + $(actions) + + + Action Button_38 + + + Arc + graphics_arc.bob + replace + + + 190 + 240 + 120 + 26 + $(actions) + + + Action Button_39 + + + Polygon/line + graphics_poly.bob + replace + + + 190 + 280 + 120 + 26 + $(actions) + + + Monitors + Monitors + 330 + 90 + 120 + 30 + + + + + + + Action Button_2 + + + Text Update + monitors_textupdate.bob + replace + + + 330 + 120 + 120 + 26 + $(actions) + + + Action Button_3 + + + LED + monitors_led.bob + replace + + + 330 + 160 + 120 + 26 + $(actions) + + + Action Button_20 + + + Byte Monitor + monitors_bytemonitor.bob + replace + + + 330 + 200 + 120 + 26 + $(actions) + + + Action Button_34 + + + Tank + monitors_tank.bob + replace + + + 330 + 240 + 120 + 26 + $(actions) + + + Action Button_35 + + + Table + monitors_table.bob + replace + + + 330 + 280 + 120 + 26 + $(actions) + + + Action Button_27 + + + Gauges + monitors_gauge.bob + replace + + + 330 + 360 + 120 + 26 + + + + $(actions) + + + Action Button_28 + + + Meters + monitors_meter.bob + replace + + + 330 + 400 + 120 + 26 + + + + $(actions) + + + Action Button_32 + + + Symbols + monitors_symbols.bob + replace + + + 330 + 320 + 120 + 26 + $(actions) + + + Controls + Controls + 470 + 90 + 120 + 30 + + + + + + + Action Button_14 + + + Text Entry + controls_textentry.bob + replace + + + 470 + 120 + 120 + 26 + $(actions) + + + Action Button_15 + + + Toggle Buttons + controls_toggle_buttons.bob + replace + + + 470 + 160 + 120 + 26 + $(actions) + + + Action Button_16 + + + Action Buttons + controls_action_buttons.bob + replace + + + 470 + 200 + 120 + 26 + $(actions) + + + Action Button_18 + + + Incr. Controls + controls_spinner_slider_scrollbar.bob + replace + + + 470 + 240 + 120 + 26 + $(actions) + + + Action Button_19 + + + Combo Box + controls_combo.bob + replace + + + 470 + 280 + 120 + 26 + $(actions) + + + Action Button_36 + + + Check Boxes + controls_checkboxes_slidebuttons.bob + replace + + + 470 + 320 + 120 + 26 + $(actions) + + + Action Button_23 + + + Radio Button + controls_radio.bob + replace + + + 470 + 360 + 120 + 26 + $(actions) + + + Action Button_22 + + + File Selector + controls_fileselector.bob + replace + + + 470 + 400 + 120 + 26 + $(actions) + + + Action Button_33 + + + Knob + controls_knob.bob + replace + + + 470 + 440 + 120 + 26 + + + + $(actions) + + + Action Button_34 + + + Thumb Wheel + controls_thumbwheel.bob + replace + + + 470 + 480 + 120 + 26 + + + + $(actions) + + + Plots + Plots + 610 + 90 + 120 + 30 + + + + + + + XY + + + X/Y Plot + plots_xy.bob + replace + + + 610 + 120 + 120 + 26 + $(actions) + + + Strip + + + Strip Chart + plots_strip.bob + replace + + + 610 + 160 + 120 + 26 + $(actions) + + + DB + + + Data Browser + plots_databrowser.bob + replace + + + 610 + 200 + 120 + 26 + $(actions) + + + Image + + + Image + plots_image.bob + replace + + + 610 + 240 + 120 + 26 + $(actions) + + + Structure + Structure + 750 + 90 + 120 + 30 + + + + + + + Action Button_13 + + + Group + structure_group.bob + replace + + + 750 + 120 + 120 + 26 + $(actions) + + + Action Button_11 + + + Embedded + embedded/structure_embedded.bob + replace + + + 750 + 160 + 120 + 26 + $(actions) + + + Action Button_12 + + + Tabs + structure_tabs.bob + replace + + + 750 + 200 + 120 + 26 + $(actions) + + + Action Button_31 + + + Navigation Tabs + structure_navtabs.bob + replace + + + 750 + 240 + 120 + 26 + $(actions) + + + Action Button_30 + + + Array + structure_array.bob + replace + + + 750 + 280 + 120 + 26 + $(actions) + + + Action Button_21 + + + Template & Inst. + template_and_instance/example.bob + replace + + + 750 + 320 + 120 + 26 + $(actions) + + + Misc + Miscellaneous + 890 + 90 + 120 + 30 + + + + + + + Action Button_20 + + + Web Browser + misc_webbrowser.bob + replace + + + 890 + 120 + 120 + 26 + $(actions) + + + Action Button_24 + + + Clocks + misc_clock.bob + replace + + + 890 + 200 + 120 + 26 + + + + $(actions) + + + Action Button_29 + + + Fishtank + fishtank/heater.bob + replace + + + 890 + 160 + 120 + 26 + $(actions) + + diff --git a/app/display/model/src/main/resources/examples/controls_textentry.bob b/app/display/model/src/main/resources/examples/controls_textentry.bob index e47ed04a6e..be25cf280c 100644 --- a/app/display/model/src/main/resources/examples/controls_textentry.bob +++ b/app/display/model/src/main/resources/examples/controls_textentry.bob @@ -1,237 +1,516 @@ - - - Text Entry - 850 - - Label - TITLE - Text Entry Widget - 0 - 0 - 241 - 31 - - - - - - - - - true - - - Label_1 - Displays the value of a PV as text and allows entering a new value. - 41 - 481 - 30 - - - Label_2 - Example: - 91 - 181 - - - Label_4 - Can be configured to be ugly: - 131 - 201 - - - Label_6 - Default: - 261 - 111 - - - Label_7 - Formatting options: - 231 - 201 - - - - - - - Label_8 - Don't show units: - 291 - 161 - - - Label_9 - Decimal w/ 2 digits: - 321 - 191 - - - Label_10 - Exponential Notation: - 351 - 191 - - - Text Entry - sim://sine - 201 - 261 - 210 - - - Text Entry_1 - sim://sine - 201 - 291 - 210 - false - - - Text Entry_2 - sim://sine - 201 - 321 - 210 - 1 - - - Text Entry_3 - sim://sine - 201 - 351 - 210 - 2 - - - Text Entry_4 - loc://number(3.14) - 201 - 91 - - - Text Entry_5 - loc://number(3.14) - 201 - 131 - 250 - 70 - - - - - - - - - - - Label_11 - COMMENT - The text entry widget updates whenever the value of the -associated PV changes. - -To permit editing, updates are suppressed as soon as one presses -any key, including for example 'Alt', 'Control' or cursor keys, within -the widget. - -Pressing 'Escape' or selecting a different widget aborts editing -and restores the original value of the PV. - -Pressing 'Enter' confirms editing and writes the entered value to the PV. - 391 - 451 - 210 - - - - - - - - - true - true - - - Label_12 - Multiple Lines: - 470 - 231 - 201 - - - - - - - Text Entry_6 - loc://long_text("Some long text -with multiple -lines...") - 470 - 261 - 289 - 110 - true - true - - - Label_13 - COMMENT - The text entry widget can be "multi-lined", -starting a new line on each newline character in the text. -Optionally, it can also wrap words to a new line when -reaching the end of a line. - -When activated, the multi-line variant will not select the complete text because users are more likely to edit just a subsection of the longer text. - -With 'Enter' now simply starting a new line, 'Control-Enter' confirms editing and writes the entered value to the PV. -Since pressing 'Control-Enter' seems too hard for some users, the multi-lined text field will also submit the entered value to the PV when simply leaving the widget by clicking elsewhere. -To cancel editing and revert to the original value, press 'Escape'. - - 470 - 391 - 370 - 290 - - - - - - - - - true - true - - - Label_3 - COMMENT - Text Entry widgets display the current value -of a PV. -When you click the widget or type anything -into the widget, it enters an 'active' state, -indicated by a different background color. - -It will now stop displaying received PV -updates so you can edit without interruptions. -Press 'Enter' to confirm. -Press 'Escape', 'Tab', or click elsewhere to abort. - 470 - 31 - 310 - 189 - - - - - - - - - true - true - - + + + Text Entry + 850 + + Label + TITLE + Text Entry Widget + 0 + 0 + 241 + 31 + + + + + + + + + true + + + Label_1 + Displays the value of a PV as text and allows entering a new value. + 41 + 481 + 30 + + + Label_2 + Example: + 91 + 181 + + + Label_4 + Can be configured to be ugly: + 131 + 201 + + + Label_6 + Default: + 261 + 111 + + + Label_7 + Formatting options: + 231 + 201 + + + + + + + Label_8 + Don't show units: + 291 + 161 + + + Label_9 + Decimal w/ 2 digits: + 321 + 191 + + + Label_10 + Exponential Notation: + 351 + 191 + + + Text Entry + sim://sine + 201 + 261 + 210 + + + Text Entry_1 + sim://sine + 201 + 291 + 210 + false + + + Text Entry_2 + sim://sine + 201 + 321 + 210 + 1 + + + Text Entry_3 + sim://sine + 201 + 351 + 210 + 2 + + + Text Entry_4 + loc://number(3.14) + 201 + 91 + + + Text Entry_5 + loc://number(3.14) + 201 + 131 + 250 + 70 + + + + + + + + + + + Label_11 + COMMENT + The text entry widget updates whenever the value of the +associated PV changes. + +To permit editing, updates are suppressed as soon as one presses +any key, including for example 'Alt', 'Control' or cursor keys, within +the widget. + +Pressing 'Escape' or selecting a different widget aborts editing +and restores the original value of the PV. + +Pressing 'Enter' confirms editing and writes the entered value to the PV. + 391 + 451 + 210 + + + + + + + + + true + true + + + Label_12 + Multiple Lines: + 470 + 231 + 201 + + + + + + + Text Entry_6 + loc://long_text("Some long text +with multiple +lines...") + 470 + 261 + 289 + 110 + true + true + + + Label_13 + COMMENT + The text entry widget can be "multi-lined", +starting a new line on each newline character in the text. +Optionally, it can also wrap words to a new line when +reaching the end of a line. + +When activated, the multi-line variant will not select the complete text because users are more likely to edit just a subsection of the longer text. + +With 'Enter' now simply starting a new line, 'Control-Enter' confirms editing and writes the entered value to the PV. +Since pressing 'Control-Enter' seems too hard for some users, the multi-lined text field will also submit the entered value to the PV when simply leaving the widget by clicking elsewhere. +To cancel editing and revert to the original value, press 'Escape'. + + 470 + 391 + 370 + 290 + + + + + + + + + true + true + + + Label_3 + COMMENT + Text Entry widgets display the current value +of a PV. +When you click the widget or type anything +into the widget, it enters an 'active' state, +indicated by a different background color. + +It will now stop displaying received PV +updates so you can edit without interruptions. +Press 'Enter' to confirm. +Press 'Escape', 'Tab', or click elsewhere to abort. + 470 + 31 + 310 + 189 + + + + + + + + + true + true + + + Label_5 + A Text Entry can have suggestions. + 850 + 71 + 481 + + + Text Entry_7 + loc://suggestions_test<VString> + 850 + 101 + 220 + a suggestion +something +something else +possibly +nothing + + + Label_14 + Like a Combo Box, suggestions can be based on PV value. + 850 + 131 + 481 + + + Text Entry_8 + loc://suggestions_test1<VEnum>(0, "NORTH", "SOUTH", "EAST", "WEST") + 850 + 161 + 220 + a suggestion +something +something else +possibly +nothing + true + + + Label_16 + Suggestions + 850 + 31 + 201 + + + + + + + Label_17 + Miscellaneous options: + 850 + 201 + 481 + 30 + + + Text Entry_10 + loc://suggestions_test2<VEnum>(0, "NORTH", "SOUTH", "EAST", "WEST") + 1000 + 230 + 220 + a suggestion +something +something else +possibly +nothing + true + true + test + + + Label_18 + Password: + 850 + 231 + 80 + 30 + + + Text Entry_11 + loc://suggestions_test3<VEnum>(0, "NORTH", "SOUTH", "EAST", "WEST") + 1000 + 261 + 220 + a suggestion +something +something else +possibly +nothing + true + true + + + Label_19 + Case sensitive: + 850 + 262 + 150 + 30 + + + Text Entry_12 + loc://suggestions_test4<VEnum>(0, "NORTH", "SOUTH", "EAST", "WEST") + 1000 + 292 + 220 + a suggestion +something +something else +possibly +nothing + true + 3 + + + Label_20 + Minimum characters: + 850 + 293 + 150 + 30 + + + Label_22 + Pattern matching: + 850 + 323 + 481 + 30 + + + Label_23 + fuzzy*: + 850 + 353 + 201 + 18 + + + Label_24 + COMMENT + *Fuzzy string searching (or approximate string matching): +It is the technique of finding strings that match a pattern approximately + 850 + 410 + 370 + 80 + + + + + + + + + true + true + + + Text Entry_14 + loc://suggestions_test5<VString> + 850 + 371 + 220 + banana +strawberry +raspberry +blueberry +apple +pineapple +pear +kiwi +blackberry +lemon +orange +apricot +mango +avocado +cherry +bilberry +peach +satsuma +persimmon + true + + + Text Entry_15 + loc://suggestions_test5<VString> + 1080 + 371 + 220 + banana +strawberry +raspberry +blueberry +apple +pineapple +pear +kiwi +blackberry +lemon +orange +apricot +mango +avocado +cherry +bilberry +peach +satsuma +persimmon + true + starts_with + + + Text Entry_16 + loc://suggestions_test5<VString> + 1310 + 371 + 220 + banana +strawberry +raspberry +blueberry +apple +pineapple +pear +kiwi +blackberry +lemon +orange +apricot +mango +avocado +cherry +bilberry +peach +satsuma +persimmon + true + contains + + + Label_25 + starts_with*: + 1080 + 353 + 201 + 18 + + + Label_26 + contains*: + 1310 + 353 + 201 + 18 + + + Label_27 + COMMENT + Unlike a Combobox, the suggestion system has no limits on the number of suggestions it can make. + 850 + 500 + 370 + 80 + + + + + + + + + true + true + + diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TextEntryWidgetTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TextEntryWidgetTest.java new file mode 100644 index 0000000000..fb86fb1c7b --- /dev/null +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TextEntryWidgetTest.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright (c) 2025 Thales. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.csstudio.display.builder.model.widgets; + +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; + +/** + * JUnit test suite for TextEntryWidget functionality. + * This test class validates the TextEntryWidget's filtering and suggestion + * capabilities, including various search modes, case sensitivity options, + * minimum character requirements, and XML serialization/deserialization. + */ +@SuppressWarnings("nls") +public class TextEntryWidgetTest +{ + /** + * Creates a new TextEntryWidget instance with the specified suggestion items. + * + * @param items the list of suggestion items to set on the widget + * @return a new TextEntryWidget configured with the provided items + */ + private TextEntryWidget newWidgetWithItems(List items) + { + final TextEntryWidget w = new TextEntryWidget(); + w.setItems(items); + return w; + } + + /** + * Tests case-insensitive substring matching functionality. + * Verifies that the widget correctly filters suggestions using case-insensitive + * "contains" mode. The test ensures that partial matches are found regardless + * of character case in both the input and the suggestion items. + */ + @Test + public void testContainsCaseInsensitive() + { + final TextEntryWidget w = newWidgetWithItems(List.of("Alpha", "beta", "ALPHABET", "gamma")); + + w.propMinCharacters().setValue(2); + w.propCaseSensitive().setValue(false); + w.propFilterMode().setValue("contains"); + + final List result = w.getFilteredSuggestions("ha"); + assertThat(result, equalTo(List.of("Alpha", "ALPHABET"))); + } + + /** + * Tests case-sensitive prefix matching functionality. + * Verifies that the widget correctly filters suggestions using case-sensitive + * "starts with" mode. Only items that begin with the exact case-matched + * input string should be included in the results. + */ + @Test + public void testStartsWithCaseSensitive() + { + final TextEntryWidget w = newWidgetWithItems(List.of("Foo", "Foobar", "foo", "FOO", "bar")); + + w.propMinCharacters().setValue(1); + w.propCaseSensitive().setValue(true); + w.propFilterMode().setValue("starts_with"); + + final List result = w.getFilteredSuggestions("Fo"); + assertThat(result, equalTo(List.of("Foo", "Foobar"))); + } + + /** + * Tests fuzzy matching functionality. + * Verifies that the widget correctly performs fuzzy matching where input + * characters can match non-consecutive characters in suggestion items. + * The fuzzy algorithm should find items containing all input characters + * in the correct order, but not necessarily consecutively. + */ + @Test + public void testFuzzyMatching() + { + final TextEntryWidget w = newWidgetWithItems(List.of("cartwheel", "carthorse", "cow", "chart")); + + w.propMinCharacters().setValue(3); + w.propCaseSensitive().setValue(false); + w.propFilterMode().setValue("fuzzy"); + + final List result = w.getFilteredSuggestions("crt"); + assertThat(result, equalTo(List.of("cartwheel", "carthorse", "chart"))); + } + + /** + * Tests minimum character requirement enforcement. + * Verifies that the widget respects the minimum character setting and + * returns no suggestions when the input length is below the configured + * minimum threshold. + */ + @Test + public void testMinCharactersBlocksShortInput() + { + final TextEntryWidget w = newWidgetWithItems(List.of("one", "two", "three")); + + w.propMinCharacters().setValue(3); + w.propCaseSensitive().setValue(false); + w.propFilterMode().setValue("contains"); + + final List result = w.getFilteredSuggestions("ab"); + assertThat(result, equalTo(List.of())); + } + + /** + * Tests XML serialization and deserialization round-trip to ensure all + * key TextEntryWidget properties are correctly preserved. + * This test: + * - Creates a TextEntryWidget with custom property values including + * suggestion items, filtering options, and UI settings + * - Serializes it to XML using ModelWriter + * - Deserializes it back using ModelReader + * - Verifies that all critical properties retain their values after + * the round-trip operation + * The test covers persistence of suggestion items, minimum character + * requirements, case sensitivity, filter mode, placeholder text, + * and custom input settings. + */ + @Test + public void testXMLRoundtripPreservesKeyProperties() throws Exception + { + TextEntryWidget w = new TextEntryWidget(); + w.setItems(List.of("one", "Two", "three")); + w.propItems().setValue("one\nTwo\nthree"); + w.propMinCharacters().setValue(2); + w.propCaseSensitive().setValue(true); + w.propFilterMode().setValue("starts_with"); + w.propPlaceholder().setValue("Enter value…"); + w.propCustom().setValue(true); + + final String xml = ModelWriter.getXML(List.of(w)); + + final DisplayModel model = ModelReader.parseXML(xml); + final List widgets = model.getChildren(); + assertThat(widgets.size(), equalTo(1)); + assertThat(widgets.get(0), instanceOf(TextEntryWidget.class)); + + w = (TextEntryWidget) widgets.get(0); + + assertThat(w.propItems().getValue(), equalTo("one\nTwo\nthree")); + assertThat(w.propMinCharacters().getValue(), equalTo(2)); + assertThat(w.propCaseSensitive().getValue(), equalTo(true)); + assertThat(w.propFilterMode().getValue(), equalTo("starts_with")); + assertThat(w.propPlaceholder().getValue(), equalTo("Enter value…")); + assertThat(w.propCustom().getValue(), equalTo(true)); + } +} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java index b6db502bd7..caaa512ff9 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java @@ -1,130 +1,131 @@ -/******************************************************************************* - * Copyright (c) 2017-2021 Oak Ridge National Laboratory. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - *******************************************************************************/ -package org.csstudio.display.builder.representation.javafx.widgets; - -import static java.util.Map.entry; - -import java.util.Map; - -import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.WidgetDescriptor; -import org.csstudio.display.builder.model.widgets.ActionButtonWidget; -import org.csstudio.display.builder.model.widgets.ArcWidget; -import org.csstudio.display.builder.model.widgets.ArrayWidget; -import org.csstudio.display.builder.model.widgets.BoolButtonWidget; -import org.csstudio.display.builder.model.widgets.ByteMonitorWidget; -import org.csstudio.display.builder.model.widgets.CheckBoxWidget; -import org.csstudio.display.builder.model.widgets.ChoiceButtonWidget; -import org.csstudio.display.builder.model.widgets.ComboWidget; -import org.csstudio.display.builder.model.widgets.EllipseWidget; -import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; -import org.csstudio.display.builder.model.widgets.FileSelectorWidget; -import org.csstudio.display.builder.model.widgets.GroupWidget; -import org.csstudio.display.builder.model.widgets.LEDWidget; -import org.csstudio.display.builder.model.widgets.LabelWidget; -import org.csstudio.display.builder.model.widgets.MeterWidget; -import org.csstudio.display.builder.model.widgets.MultiStateLEDWidget; -import org.csstudio.display.builder.model.widgets.NavigationTabsWidget; -import org.csstudio.display.builder.model.widgets.PictureWidget; -import org.csstudio.display.builder.model.widgets.PolygonWidget; -import org.csstudio.display.builder.model.widgets.PolylineWidget; -import org.csstudio.display.builder.model.widgets.ProgressBarWidget; -import org.csstudio.display.builder.model.widgets.RadioWidget; -import org.csstudio.display.builder.model.widgets.RectangleWidget; -import org.csstudio.display.builder.model.widgets.ScaledSliderWidget; -import org.csstudio.display.builder.model.widgets.ScrollBarWidget; -import org.csstudio.display.builder.model.widgets.SlideButtonWidget; -import org.csstudio.display.builder.model.widgets.SpinnerWidget; -import org.csstudio.display.builder.model.widgets.SymbolWidget; -import org.csstudio.display.builder.model.widgets.TableWidget; -import org.csstudio.display.builder.model.widgets.TabsWidget; -import org.csstudio.display.builder.model.widgets.TankWidget; -import org.csstudio.display.builder.model.widgets.TemplateInstanceWidget; -import org.csstudio.display.builder.model.widgets.TextEntryWidget; -import org.csstudio.display.builder.model.widgets.TextSymbolWidget; -import org.csstudio.display.builder.model.widgets.TextUpdateWidget; -import org.csstudio.display.builder.model.widgets.ThermometerWidget; -import org.csstudio.display.builder.model.widgets.Viewer3dWidget; -import org.csstudio.display.builder.model.widgets.WebBrowserWidget; -import org.csstudio.display.builder.model.widgets.plots.DataBrowserWidget; -import org.csstudio.display.builder.model.widgets.plots.ImageWidget; -import org.csstudio.display.builder.model.widgets.plots.StripchartWidget; -import org.csstudio.display.builder.model.widgets.plots.XYPlotWidget; -import org.csstudio.display.builder.representation.WidgetRepresentation; -import org.csstudio.display.builder.representation.WidgetRepresentationFactory; -import org.csstudio.display.builder.representation.javafx.widgets.plots.DataBrowserRepresentation; -import org.csstudio.display.builder.representation.javafx.widgets.plots.ImageRepresentation; -import org.csstudio.display.builder.representation.javafx.widgets.plots.StripchartRepresentation; -import org.csstudio.display.builder.representation.javafx.widgets.plots.XYPlotRepresentation; -import org.csstudio.display.builder.representation.spi.WidgetRepresentationsService; - -/** SPI for representations of base widgets - * @author Kay Kasemir - */ -public class BaseWidgetRepresentations implements WidgetRepresentationsService -{ - @SuppressWarnings({ "unchecked", "rawtypes", "nls" }) - @Override - public Map> getWidgetRepresentationFactories() - { - final WidgetDescriptor unknown_widget = new WidgetDescriptor(WidgetRepresentationFactory.UNKNOWN, null, WidgetRepresentationFactory.UNKNOWN, null, "Unknown Widget") - { - @Override - public Widget createWidget() - { - // Cannot create instance - return null; - } - }; - - return Map.ofEntries( - entry(ActionButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ActionButtonRepresentation()), - entry(ArcWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ArcRepresentation()), - entry(ArrayWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ArrayRepresentation()), - entry(BoolButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new BoolButtonRepresentation()), - entry(ByteMonitorWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ByteMonitorRepresentation()), - entry(CheckBoxWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new CheckBoxRepresentation()), - entry(ChoiceButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ChoiceButtonRepresentation()), - entry(ComboWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ComboRepresentation()), - entry(DataBrowserWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new DataBrowserRepresentation()), - entry(EmbeddedDisplayWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new EmbeddedDisplayRepresentation()), - entry(EllipseWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new EllipseRepresentation()), - entry(FileSelectorWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new FileSelectorRepresentation()), - entry(GroupWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new GroupRepresentation()), - entry(ImageWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ImageRepresentation()), - entry(LabelWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new LabelRepresentation()), - entry(LEDWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new LEDRepresentation()), - entry(MeterWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new MeterRepresentation()), - entry(MultiStateLEDWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new MultiStateLEDRepresentation()), - entry(NavigationTabsWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new NavigationTabsRepresentation()), - entry(PictureWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PictureRepresentation()), - entry(PolygonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PolygonRepresentation()), - entry(PolylineWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PolylineRepresentation()), - entry(ProgressBarWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ProgressBarRepresentation()), - entry(RadioWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new RadioRepresentation()), - entry(RectangleWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new RectangleRepresentation()), - entry(ScaledSliderWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ScaledSliderRepresentation()), - entry(ScrollBarWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ScrollBarRepresentation()), - entry(SlideButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new SlideButtonRepresentation()), - entry(SpinnerWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new SpinnerRepresentation()), - entry(StripchartWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new StripchartRepresentation()), - entry(SymbolWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new SymbolRepresentation()), - entry(TableWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TableRepresentation()), - entry(TabsWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TabsRepresentation()), - entry(TankWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TankRepresentation()), - entry(TemplateInstanceWidget.WIDGET_DESCRIPTOR,() -> (WidgetRepresentation) new TemplateInstanceRepresentation()), - entry(TextEntryWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TextEntryRepresentation()), - entry(TextSymbolWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TextSymbolRepresentation()), - entry(TextUpdateWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TextUpdateRepresentation()), - entry(ThermometerWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ThermometerRepresentation()), - entry(Viewer3dWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new Viewer3dRepresentation()), - entry(WebBrowserWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new WebBrowserRepresentation()), - entry(XYPlotWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new XYPlotRepresentation()), - entry(unknown_widget, () -> (WidgetRepresentation) new UnknownRepresentation())); - } -} +/******************************************************************************* + * Copyright (c) 2017-2021 Oak Ridge National Laboratory. + * Copyright (c) 2025 Thales. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.csstudio.display.builder.representation.javafx.widgets; + +import static java.util.Map.entry; + +import java.util.Map; + +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.WidgetDescriptor; +import org.csstudio.display.builder.model.widgets.ActionButtonWidget; +import org.csstudio.display.builder.model.widgets.ArcWidget; +import org.csstudio.display.builder.model.widgets.ArrayWidget; +import org.csstudio.display.builder.model.widgets.BoolButtonWidget; +import org.csstudio.display.builder.model.widgets.ByteMonitorWidget; +import org.csstudio.display.builder.model.widgets.CheckBoxWidget; +import org.csstudio.display.builder.model.widgets.ChoiceButtonWidget; +import org.csstudio.display.builder.model.widgets.ComboWidget; +import org.csstudio.display.builder.model.widgets.EllipseWidget; +import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; +import org.csstudio.display.builder.model.widgets.FileSelectorWidget; +import org.csstudio.display.builder.model.widgets.GroupWidget; +import org.csstudio.display.builder.model.widgets.LEDWidget; +import org.csstudio.display.builder.model.widgets.LabelWidget; +import org.csstudio.display.builder.model.widgets.MeterWidget; +import org.csstudio.display.builder.model.widgets.MultiStateLEDWidget; +import org.csstudio.display.builder.model.widgets.NavigationTabsWidget; +import org.csstudio.display.builder.model.widgets.PictureWidget; +import org.csstudio.display.builder.model.widgets.PolygonWidget; +import org.csstudio.display.builder.model.widgets.PolylineWidget; +import org.csstudio.display.builder.model.widgets.ProgressBarWidget; +import org.csstudio.display.builder.model.widgets.RadioWidget; +import org.csstudio.display.builder.model.widgets.RectangleWidget; +import org.csstudio.display.builder.model.widgets.ScaledSliderWidget; +import org.csstudio.display.builder.model.widgets.ScrollBarWidget; +import org.csstudio.display.builder.model.widgets.SlideButtonWidget; +import org.csstudio.display.builder.model.widgets.SpinnerWidget; +import org.csstudio.display.builder.model.widgets.SymbolWidget; +import org.csstudio.display.builder.model.widgets.TableWidget; +import org.csstudio.display.builder.model.widgets.TabsWidget; +import org.csstudio.display.builder.model.widgets.TankWidget; +import org.csstudio.display.builder.model.widgets.TemplateInstanceWidget; +import org.csstudio.display.builder.model.widgets.TextEntryWidget; +import org.csstudio.display.builder.model.widgets.TextSymbolWidget; +import org.csstudio.display.builder.model.widgets.TextUpdateWidget; +import org.csstudio.display.builder.model.widgets.ThermometerWidget; +import org.csstudio.display.builder.model.widgets.Viewer3dWidget; +import org.csstudio.display.builder.model.widgets.WebBrowserWidget; +import org.csstudio.display.builder.model.widgets.plots.DataBrowserWidget; +import org.csstudio.display.builder.model.widgets.plots.ImageWidget; +import org.csstudio.display.builder.model.widgets.plots.StripchartWidget; +import org.csstudio.display.builder.model.widgets.plots.XYPlotWidget; +import org.csstudio.display.builder.representation.WidgetRepresentation; +import org.csstudio.display.builder.representation.WidgetRepresentationFactory; +import org.csstudio.display.builder.representation.javafx.widgets.plots.DataBrowserRepresentation; +import org.csstudio.display.builder.representation.javafx.widgets.plots.ImageRepresentation; +import org.csstudio.display.builder.representation.javafx.widgets.plots.StripchartRepresentation; +import org.csstudio.display.builder.representation.javafx.widgets.plots.XYPlotRepresentation; +import org.csstudio.display.builder.representation.spi.WidgetRepresentationsService; + +/** SPI for representations of base widgets + * @author Kay Kasemir + */ +public class BaseWidgetRepresentations implements WidgetRepresentationsService +{ + @SuppressWarnings({ "unchecked", "rawtypes", "nls" }) + @Override + public Map> getWidgetRepresentationFactories() + { + final WidgetDescriptor unknown_widget = new WidgetDescriptor(WidgetRepresentationFactory.UNKNOWN, null, WidgetRepresentationFactory.UNKNOWN, null, "Unknown Widget") + { + @Override + public Widget createWidget() + { + // Cannot create instance + return null; + } + }; + + return Map.ofEntries( + entry(ActionButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ActionButtonRepresentation()), + entry(ArcWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ArcRepresentation()), + entry(ArrayWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ArrayRepresentation()), + entry(BoolButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new BoolButtonRepresentation()), + entry(ByteMonitorWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ByteMonitorRepresentation()), + entry(CheckBoxWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new CheckBoxRepresentation()), + entry(ChoiceButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ChoiceButtonRepresentation()), + entry(ComboWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ComboRepresentation()), + entry(DataBrowserWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new DataBrowserRepresentation()), + entry(EmbeddedDisplayWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new EmbeddedDisplayRepresentation()), + entry(EllipseWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new EllipseRepresentation()), + entry(FileSelectorWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new FileSelectorRepresentation()), + entry(GroupWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new GroupRepresentation()), + entry(ImageWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ImageRepresentation()), + entry(LabelWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new LabelRepresentation()), + entry(LEDWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new LEDRepresentation()), + entry(MeterWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new MeterRepresentation()), + entry(MultiStateLEDWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new MultiStateLEDRepresentation()), + entry(NavigationTabsWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new NavigationTabsRepresentation()), + entry(PictureWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PictureRepresentation()), + entry(PolygonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PolygonRepresentation()), + entry(PolylineWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PolylineRepresentation()), + entry(ProgressBarWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ProgressBarRepresentation()), + entry(RadioWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new RadioRepresentation()), + entry(RectangleWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new RectangleRepresentation()), + entry(ScaledSliderWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ScaledSliderRepresentation()), + entry(ScrollBarWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ScrollBarRepresentation()), + entry(SlideButtonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new SlideButtonRepresentation()), + entry(SpinnerWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new SpinnerRepresentation()), + entry(StripchartWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new StripchartRepresentation()), + entry(SymbolWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new SymbolRepresentation()), + entry(TableWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TableRepresentation()), + entry(TabsWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TabsRepresentation()), + entry(TankWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TankRepresentation()), + entry(TemplateInstanceWidget.WIDGET_DESCRIPTOR,() -> (WidgetRepresentation) new TemplateInstanceRepresentation()), + entry(TextEntryWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TextEntryRepresentation()), + entry(TextSymbolWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TextSymbolRepresentation()), + entry(TextUpdateWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new TextUpdateRepresentation()), + entry(ThermometerWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ThermometerRepresentation()), + entry(Viewer3dWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new Viewer3dRepresentation()), + entry(WebBrowserWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new WebBrowserRepresentation()), + entry(XYPlotWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new XYPlotRepresentation()), + entry(unknown_widget, () -> (WidgetRepresentation) new UnknownRepresentation())); + } +} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TextEntryRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TextEntryRepresentation.java index 6ae8f2c1a9..843376fe43 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TextEntryRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TextEntryRepresentation.java @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright (c) 2015-2024 Oak Ridge National Laboratory. + * Copyright (c) 2025 Thales. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -9,12 +10,23 @@ import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; +import java.util.stream.Stream; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.css.PseudoClass; +import javafx.geometry.Bounds; import javafx.geometry.Pos; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.MultipleSelectionModel; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; +import javafx.stage.Popup; import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; import org.csstudio.display.builder.model.WidgetProperty; @@ -25,14 +37,13 @@ import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.model.widgets.PVWidget; import org.csstudio.display.builder.model.widgets.TextEntryWidget; -import org.csstudio.display.builder.representation.javafx.Cursors; import org.csstudio.display.builder.representation.javafx.JFXUtil; import org.epics.vtype.Alarm; +import org.epics.vtype.VEnum; import org.epics.vtype.VType; -import org.phoebus.ui.javafx.Styles; +import org.phoebus.ui.vtype.FormatOption; import org.phoebus.ui.vtype.FormatOptionHandler; -import javafx.scene.Cursor; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.TextInputControl; @@ -61,7 +72,14 @@ public class TextEntryRepresentation extends RegionBaseRepresentation pvNameListener = this::pvnameChanged; private volatile String value_text = ""; - private static WidgetColor active_color = WidgetColorService.getColor(NamedWidgetColors.ACTIVE_TEXT); + // autocomplete + private List items = List.of(); + private Popup suggestionsPopup; + private ListView suggestionsListView; + private ObservableList suggestions; + private javafx.beans.value.ChangeListener textChangeListener; + + private static final WidgetColor active_color = WidgetColorService.getColor(NamedWidgetColors.ACTIVE_TEXT); private volatile Pos pos; @@ -69,17 +87,17 @@ public class TextEntryRepresentation extends RegionBaseRepresentation - { - switch (event.getCode()) - { - case TAB: - // For multiline, it's like any other entered key. - if (model_widget.propMultiLine().getValue() && ! event.isShiftDown()) - setActive(true); - // Otherwise results in lost focus and is handled as thus - break; - case ESCAPE: - if (active) - { // Revert original value, leave active state - restore(); - setActive(false); - } - break; - case ENTER: - // With Java 8, the main keyboard sent 'ENTER', - // but the numeric keypad's enter key sent UNDEFINED?! - // -> Was handled by checking for char 13 in onKeyTyped handler. - // With Java 9, always get ENTER in here, - // and _not_ receiving char 13 in onKeyTyped any more, - // so all enter keys handled in here. - - // Single line mode uses plain ENTER. - // Multi line mode requires Control or Command-ENTER. - if (!isMultiLine() || event.isShortcutDown()) - { - // Submit value, leave active state - submit(); - setActive(false); - } - break; - default: - // Any other key results in active state - setActive(true); - } - }); - // Clicking into widget also activates - text.setOnMouseClicked(event -> { - // Secondary mouse button should bring up context menu - // but not enable editing. - if(event.getButton().equals(MouseButton.PRIMARY)){ - setActive(true); - } - else{ - text.setEditable(false); - } - }); - // While getting the focus does not activate the widget - // (first need to type something or click), - // _loosing_ focus de-activates the widget. - // Otherwise widget where one moves the cursor, then clicks - // someplace else would remain active and not show any updates - text.focusedProperty().addListener((prop, old, focused) -> - { - if (active && !focused) - { - // For multi-line, submit on exit because users - // cannot remember Ctrl-Enter. - // For plain text field, require Enter to submit - // and cancel editing when focus is lost. - if (isMultiLine()) - submit(); - else - restore(); - setActive(false); - } - }); + initPopup(); + setupEventHandlers(text); } // Non-managed widget reduces expensive Node.notifyParentOfBoundsChange() calls. @@ -201,6 +146,368 @@ protected boolean isFilteringEditModeClicks() return true; } + + /** + * Initializes the suggestions popup and its ListView. + * The popup will display a list of suggestions based on user input. + */ + private void initPopup() + { + suggestionsPopup = new Popup(); + suggestionsPopup.setAutoHide(false); + suggestionsPopup.setHideOnEscape(false); + suggestionsPopup.setConsumeAutoHidingEvents(false); + + suggestions = FXCollections.observableArrayList(); + suggestionsListView = new ListView<>(suggestions); + suggestionsListView.setFocusTraversable(false); + + suggestionsListView.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item); + setStyle(isSelected() ? "-fx-text-fill: white;" : "-fx-background-color: transparent;"); + } + }); + + suggestionsListView.setStyle( + "-fx-background-color: white; -fx-border-color: #cccccc; -fx-border-width: 1px; " + + "-fx-border-radius: 3px; -fx-background-radius: 3px; -fx-focus-color: transparent; " + + "-fx-faint-focus-color: transparent; -fx-selection-bar-non-focused: #3498db;" + ); + + suggestionsListView.setOnMouseClicked(e -> { + String selected = suggestionsListView.getSelectionModel().getSelectedItem(); + if (selected != null) { + selectSuggestion(selected); + } + }); + + suggestionsPopup.getContent().add(suggestionsListView); + } + + /** + * Method to set up once all listeners and actions on the TextField. + * + * @param textField AutoComplete JavaFX TextField. + */ + private void setupEventHandlers(TextInputControl textField) + { + textChangeListener = (obs, old, text) -> updateSuggestions(text); + textField.textProperty().addListener(textChangeListener); + + textField.setOnMouseClicked(e -> { + if (!textField.isDisabled() && textField.isEditable() && e.getButton() == MouseButton.PRIMARY) { + showSuggestions(); + } + }); + + textField.setOnKeyPressed(this::handleKeyPressed); + + textField.focusedProperty().addListener((obs, old, focused) -> { + if (focused) { + showSuggestions(); + } else { + // For multi-line, submit on exit because users + // cannot remember Ctrl-Enter. + // For plain text field, require Enter to submit + // and cancel editing when focus is lost. + if (isMultiLine()) + submit(); + setActive(false); + + suggestionsPopup.hide(); + textField.textProperty().removeListener(textChangeListener); + restore(); + textField.textProperty().addListener(textChangeListener); + } + }); + } + + /** + * Handles key presses in the TextField to navigate through suggestions. + * - DOWN: Selects the next suggestion. + * - UP: Selects the previous suggestion. + * - ENTER: Selects the currently highlighted suggestion. + * - ESCAPE: Hides the suggestions popup and resets the text field. + * + * @param event KeyEvent triggered by user input. + */ + private void handleKeyPressed(KeyEvent event) + { + switch (event.getCode()) { + case TAB -> { + // For multiline, it's like any other entered key. + if (model_widget.propMultiLine().getValue() && !event.isShiftDown()) + setActive(true); + // Otherwise results in lost focus and is handled as thus + event.consume(); + } + case DOWN -> { + if (isMultiLine()) + return; + + navigateSuggestions(1); + event.consume(); + } + case UP -> { + if (isMultiLine()) + return; + + navigateSuggestions(-1); + event.consume(); + } + case ENTER -> { + if (suggestionsPopup.isShowing()) { + String selected = suggestionsListView.getSelectionModel().getSelectedItem(); + + if (selected == null && !suggestionsListView.getItems().isEmpty()) { + int idx = suggestionsListView.getSelectionModel().getSelectedIndex(); + if (idx < 0) idx = 0; + suggestionsListView.getSelectionModel().select(idx); + selected = suggestionsListView.getSelectionModel().getSelectedItem(); + } + + if (selected != null) { + selectSuggestion(selected); + event.consume(); + return; + } + } + + if (isMultiLine() && !event.isShortcutDown()) { + suggestionsPopup.hide(); + setActive(true); + event.consume(); + return; + } + + if (!model_widget.propCustom().getValue() && !suggestions.isEmpty()) + restore(); + submit(); + setActive(false); + event.consume(); + } + case ESCAPE -> { + jfx_node.getParent().requestFocus(); + suggestionsPopup.hide(); + restore(); + setActive(false); + event.consume(); + } + } + } + + /** + * Navigate through the suggestions list based on the direction. + * If direction is positive, it moves down; if negative, it moves up. + * + * @param direction 1 for down, -1 for up. + */ + private void navigateSuggestions(int direction) + { + if (suggestions == null || suggestions.isEmpty()) + return; + + final int size = suggestions.size(); + final MultipleSelectionModel sm = suggestionsListView.getSelectionModel(); + int currentIndex = sm.getSelectedIndex(); + + if (currentIndex < 0) { + currentIndex = (direction > 0) ? 0 : size - 1; + } else { + if (direction > 0) { + currentIndex = (currentIndex + 1) % size; + } else { + currentIndex = (currentIndex - 1 + size) % size; + } + } + + sm.select(currentIndex); + if (currentIndex >= 0 && currentIndex < size) { + suggestionsListView.scrollTo(currentIndex); + } + } + + /** + * Method called when user is typing in the text field in order to update suggestions based + * on filtering option. + * + * @param inputText new text input string. + */ + private void updateSuggestions(String inputText) + { + if (inputText == null) { + inputText = ""; + } + + int min = model_widget.propMinCharacters().getValue(); + + if (inputText.length() < min) { + showSuggestions(); + return; + } + + showFilteredSuggestions(model_widget.getFilteredSuggestions(inputText)); + } + + /** + * Show suggestions based on the current text in the TextField. + * If the TextField is empty, show all items. + * If there are no items, hide the suggestions popup. + */ + private void showSuggestions() + { + if (!jfx_node.isFocused()) { + suggestionsPopup.hide(); + return; + } + if (jfx_node.isDisabled() || !jfx_node.isEditable() || items.isEmpty()) return; + + String text = jfx_node.getText(); + List toShow = (text == null || text.isEmpty()) + ? items + : model_widget.getFilteredSuggestions(text); + + showFilteredSuggestions(toShow); + } + + /** + * Show the filtered suggestions in the popup. + * + * @param filtered List of filtered suggestions to display. + */ + private void showFilteredSuggestions(List filtered) + { + if (!jfx_node.isFocused()) { + suggestionsPopup.hide(); + return; + } + + if (filtered.isEmpty()) { + suggestionsPopup.hide(); + return; + } + + suggestions.setAll(filtered); + + if (!suggestionsPopup.isShowing()) { + if (!isMultiLine()) { + showPopup(filtered.size()); + } + } else { + updatePopupSize(filtered.size()); + } + } + + /** + * Show the suggestions popup below the text field. + * + * @param itemCount number of items to show in the popup. + */ + private void showPopup(int itemCount) { + Bounds bounds = jfx_node.localToScreen(jfx_node.getBoundsInLocal()); + + updatePopupSize(itemCount); + + suggestionsPopup.show(jfx_node, bounds.getMinX(), bounds.getMaxY()); + } + + /** + * Update the size of the suggestions popup based on the number of items. + * + * @param itemCount number of items in the suggestions list. + */ + private void updatePopupSize(int itemCount) + { + double width = jfx_node.getWidth(); + suggestionsListView.setPrefWidth(width); + suggestionsListView.setMaxWidth(width); + suggestionsListView.setMinWidth(width); + + double maxHeight = Math.min(itemCount * 23.5, 300); + suggestionsListView.setPrefHeight(maxHeight); + suggestionsListView.setMaxHeight(maxHeight); + } + + /** + * Selection of a suggestion in the dropdown. + * + * @param suggestion selected suggestion. + */ + private void selectSuggestion(String suggestion) + { + jfx_node.textProperty().removeListener(textChangeListener); + jfx_node.setText(suggestion); + jfx_node.textProperty().addListener(textChangeListener); + + suggestionsPopup.hide(); + + if (model_widget.runtimePropPVWritable().getValue()) { + confirmValue(suggestion); + } + } + + /** + * Confirm selected value and write it to PV or local variable. + * + * @param value Selected value by user. + */ + private void confirmValue(String value) + { + if (!model_widget.runtimePropPVWritable().getValue() || value == null) return; + + if (!model_widget.propCustom().getValue()) { + boolean valid = items.stream().anyMatch(item -> + model_widget.propCaseSensitive().getValue() ? + item.equals(value) : item.equalsIgnoreCase(value)); + if (!valid) { + restore(); + return; + } + } + + if (model_widget.propConfirmDialog().getValue()) { + String password = model_widget.propPassword().getValue(); + String message = model_widget.propConfirmMessage().getValue(); + + if (!password.isEmpty()) { + if (toolkit.showPasswordDialog(model_widget, message, password) == null) return; + } else if (!toolkit.showConfirmationDialog(model_widget, message)) { + return; + } + } + + try { + VType currentPvValue = model_widget.runtimePropValue().getValue(); + Object mappedValue = (currentPvValue instanceof VEnum) ? + FormatOptionHandler.parse(currentPvValue, value, FormatOption.DEFAULT) : + parseValue(value); + + toolkit.fireWrite(model_widget, mappedValue); + } catch (Exception e) { + logger.log(Level.WARNING, "Error writing to PV", e); + } + } + + /** + * Parses a string value into an appropriate type (Integer or Double). + * If the value cannot be parsed, it returns the original string. + * + * @param value String value to parse. + * @return Parsed value as Integer, Double, or original String if parsing fails. + */ + private Object parseValue(String value) + { + try { + return value.contains(".") ? Double.parseDouble(value) : Integer.parseInt(value); + } catch (NumberFormatException e) { + VType converted = VType.toVType(value); + return converted != null ? converted : value; + } + } + private void setActive(final boolean active) { if (this.active == active) @@ -296,10 +603,38 @@ protected void registerListeners() model_widget.propHorizontalAlignment().addUntypedPropertyListener(styleListener); model_widget.propVerticalAlignment().addUntypedPropertyListener(styleListener); + Stream.of( + model_widget.propPlaceholder(), model_widget.propItemsFromPV(), + model_widget.propItems(), model_widget.propCustom() + ).forEach(prop -> prop.addUntypedPropertyListener(contentListener)); contentChanged(null, null, null); } + /** + * Add a value to the list of suggestion items. + * + * @return List of computed items. + */ + private List computeItems() + { + VType value = model_widget.runtimePropValue().getValue(); + + if (model_widget.propItemsFromPV().getValue() && value instanceof VEnum) { + return ((VEnum) value).getDisplay().getChoices(); + } + + String itemsString = model_widget.propItems().getValue(); + if (itemsString == null || itemsString.trim().isEmpty()) { + return List.of(); + } + + return Arrays.stream(itemsString.split("\n")) + .map(String::trim) + .filter(item -> !item.isEmpty()) + .toList(); + } + @Override protected void unregisterListeners() { @@ -317,6 +652,12 @@ protected void unregisterListeners() model_widget.propPVName().removePropertyListener(pvNameListener); model_widget.propHorizontalAlignment().removePropertyListener(styleListener); model_widget.propVerticalAlignment().removePropertyListener(styleListener); + + Stream.of( + model_widget.propPlaceholder(), model_widget.propItemsFromPV(), + model_widget.propItems(), model_widget.propCustom() + ).forEach(prop -> prop.removePropertyListener(contentListener)); + super.unregisterListeners(); } @@ -371,6 +712,12 @@ private void pvnameChanged(final WidgetProperty property, final String o private void contentChanged(final WidgetProperty property, final Object old_value, final Object new_value) { value_text = computeText(model_widget.runtimePropValue().getValue()); + items = computeItems(); + + if (!toolkit.isEditMode()) { + model_widget.setItems(items); + } + dirty_content.mark(); if (! active) toolkit.scheduleUpdate(this);