Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
91b164d
feat: Add Widget, dropdown menu with autocomplete from predefined list
flef Jun 23, 2025
c3afdb3
fix: allow to clean variable (write "" into PV)
flef Jun 24, 2025
963abb4
fix: write PV only on Enter Key-Pressed or suggestion selection.
flef Jul 28, 2025
bd1842f
fix: merge issue
flef Jul 28, 2025
e5976cc
Merge branch 'master' of https://github.com/ThalesGroup/phoebus into …
flef Jul 28, 2025
5e5dbb1
fix: OnEscape : Hide suggestion if text is equals to pv, else restore…
flef Jul 28, 2025
d7f7a12
fix: focus complexity
Jul 29, 2025
dd6c0cf
Merge branch 'feature/widget-autocomplete' of ssh://bitbuckettsabdx.p…
Jul 29, 2025
e37c2d9
fix: cleanup listeners, focus rules
Jul 29, 2025
f4050ae
fix: case when you press ENTER and there is no suggestion = reset
Jul 29, 2025
68d305b
fix: ESCAPE case simplification
Jul 29, 2025
a64e715
fix: remove unused import
Jul 29, 2025
0f12619
feat: add autocomplete widget
flef Jul 29, 2025
cc9db89
clean: add comment on String PV handling
flef Jul 29, 2025
623a0a1
Merge branch 'master' of https://github.com/ThalesGroup/phoebus into …
flef Aug 1, 2025
6dc9ba9
fix: show suggestions even before typing (combobox use case).
flef Aug 1, 2025
e773012
feat: merged autocomplete functionnalities into TextEntry widget
Aug 19, 2025
7f6742b
Merge branch 'feature/widget-autocomplete' of ssh://bitbuckettsabdx.p…
Aug 19, 2025
dbcede6
fix: removed autocomplete widget
Aug 19, 2025
9e9483f
feat: added suggestions explanation in textentry
Aug 19, 2025
51fd853
docs: removed useless author
Aug 19, 2025
631430c
test: added TextEntry JUnit tests
Aug 20, 2025
fdcf73e
fix: remove useless icons
Sep 9, 2025
d836168
chore(legal): added thales copyright
Sep 9, 2025
6d5a86f
Merge remote-tracking branch 'origin/master' into feature/widget-auto…
Sep 9, 2025
b8b89f1
doc: added javadoc to tests
Sep 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -77,6 +88,46 @@ public Widget createWidget()
public static final WidgetPropertyDescriptor<Boolean> propMultiLine =
newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "multi_line", Messages.WidgetProperties_MultiLine);



/**
* 'items' property: list of items (string properties) for auto-completion
*/
public static final WidgetPropertyDescriptor<String> propItems =
newPVNamePropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "autocompleteitems", "Suggestions");

/**
* 'min_chars' property: minimum characters to trigger autocomplete
*/
private static final WidgetPropertyDescriptor<Integer> propMinCharacters =
newIntegerPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "min_characters", "Min Characters");

/**
* 'case_sensitive' property: whether matching is case sensitive
*/
private static final WidgetPropertyDescriptor<Boolean> propCaseSensitive =
newBooleanPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "case_sensitive", "Case Sensitive");

/**
* 'placeholder' property: placeholder text when empty
*/
private static final WidgetPropertyDescriptor<String> propPlaceholder =
newStringPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "placeholder", "Placeholder Text");

/**
* 'allowcustom' property: allow custom values
*/
private static final WidgetPropertyDescriptor<Boolean> propCustom =
newBooleanPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "allow_custom",
"Allow custom values");

/**
* 'filter_mode' property: how to filter suggestions (starts_with, contains, fuzzy)
*/
private static final WidgetPropertyDescriptor<String> propFilterMode =
newStringPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "filter_mode", "Filter Mode");


private static class CustomWidgetConfigurator extends WidgetConfigurator
{
public CustomWidgetConfigurator(final Version xml_version)
Expand Down Expand Up @@ -282,6 +333,17 @@ static void readLegacyFormat(final Element xml, final WidgetProperty<FormatOptio
private volatile WidgetProperty<Boolean> multi_line;
private volatile WidgetProperty<HorizontalAlignment> horizontal_alignment;
private volatile WidgetProperty<VerticalAlignment> vertical_alignment;
private volatile WidgetProperty<String> items;
private volatile WidgetProperty<Boolean> items_from_pv;
private volatile WidgetProperty<Boolean> confirm_dialog;
private volatile WidgetProperty<String> confirm_message;
private volatile WidgetProperty<String> password;
private volatile WidgetProperty<Integer> min_characters;
private volatile WidgetProperty<Boolean> case_sensitive;
private volatile WidgetProperty<String> placeholder;
private volatile WidgetProperty<String> filter_mode;
private volatile WidgetProperty<Boolean> allow_custom;
private volatile List<String> itemsList = List.of();

/** Constructor */
public TextEntryWidget()
Expand Down Expand Up @@ -317,6 +379,18 @@ protected void defineProperties(final List<WidgetProperty<?>> 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 */
Expand Down Expand Up @@ -384,4 +458,139 @@ public WidgetProperty<VerticalAlignment> propVerticalAlignment()
{
return vertical_alignment;
}

/**
* @return 'items' property
*/
public WidgetProperty<String> propItems() {
return items;
}

/**
* Convenience routine for script to fetch items
*
* @return Items currently available for auto-completion
*/
public Collection<String> getItems() {
return itemsList;
}

/**
* Set the items list for auto-completion
*
* @param items List of items to set
*/
public void setItems(List<String> items) {
this.itemsList = Objects.requireNonNullElseGet(items, List::of);
}

/**
* @return 'items_from_PV' property
*/
public WidgetProperty<Boolean> propItemsFromPV() {
return items_from_pv;
}

/**
* @return 'confirm_dialog' property
*/
public WidgetProperty<Boolean> propConfirmDialog() {
return confirm_dialog;
}

/**
* @return 'confirm_message' property
*/
public WidgetProperty<String> propConfirmMessage() {
return confirm_message;
}

/**
* @return 'password' property
*/
public WidgetProperty<String> propPassword() {
return password;
}

/**
* @return 'min_characters' property
*/
public WidgetProperty<Integer> propMinCharacters() {
return min_characters;
}

/**
* @return 'case_sensitive' property
*/
public WidgetProperty<Boolean> propCaseSensitive() {
return case_sensitive;
}

/**
* @return 'placeholder' property
*/
public WidgetProperty<String> propPlaceholder() {
return placeholder;
}

/**
* @return 'filter_mode' property
*/
public WidgetProperty<String> propFilterMode() {
return filter_mode;
}

/**
* @return 'allow_custom' property
*/
public WidgetProperty<Boolean> 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<String> 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();
}
}
Loading