diff --git a/CoreAnalyticView/src/au/gov/asd/tac/constellation/views/analyticview/AnalyticConfigurationPane.java b/CoreAnalyticView/src/au/gov/asd/tac/constellation/views/analyticview/AnalyticConfigurationPane.java index a22f471327..5a7bc9c2a6 100644 --- a/CoreAnalyticView/src/au/gov/asd/tac/constellation/views/analyticview/AnalyticConfigurationPane.java +++ b/CoreAnalyticView/src/au/gov/asd/tac/constellation/views/analyticview/AnalyticConfigurationPane.java @@ -481,8 +481,10 @@ private void updateGlobalParameters() { } else if (questionListPane.isExpanded() && currentQuestion != null) { final Class> questionAggregatorType = currentQuestion.getAggregatorType(); aggregators.add(new AnalyticAggregatorParameterValue(AnalyticUtilities.lookupAnalyticAggregator(questionAggregatorType))); - SingleChoiceParameterType.setOptionsData(aggregatorParameter, aggregators); - SingleChoiceParameterType.setChoiceData(aggregatorParameter, aggregators.get(0)); + if (aggregators != null) { + SingleChoiceParameterType.setOptionsData(aggregatorParameter, aggregators); + SingleChoiceParameterType.setChoiceData(aggregatorParameter, aggregators.get(0)); + } } pluginList.getItems().forEach(selectablePlugin -> selectablePlugin.setUpdatedParameter(aggregatorParameter.getId(), aggregatorParameter.getStringValue())); diff --git a/CoreDataAccessView/src/au/gov/asd/tac/constellation/views/dataaccess/plugins/experimental/TestParametersPlugin.java b/CoreDataAccessView/src/au/gov/asd/tac/constellation/views/dataaccess/plugins/experimental/TestParametersPlugin.java index a1888a4b5d..c075443a1d 100644 --- a/CoreDataAccessView/src/au/gov/asd/tac/constellation/views/dataaccess/plugins/experimental/TestParametersPlugin.java +++ b/CoreDataAccessView/src/au/gov/asd/tac/constellation/views/dataaccess/plugins/experimental/TestParametersPlugin.java @@ -225,28 +225,13 @@ public PluginParameters createParameters() { // A single choice list with a subtype of String. final SingleChoiceParameterValue robotpv = new SingleChoiceParameterValue(StringParameterValue.class); - robotpv.setGuiInit(control -> { - @SuppressWarnings("unchecked") //control will be of type ComboBox which extends from Region - final ComboBox field = (ComboBox) control; - final Image img = new Image(ALIEN_ICON); - field.setCellFactory((ListView param) -> new ListCell() { - @Override - protected void updateItem(final ParameterValue item, final boolean empty) { - super.updateItem(item, empty); - this.setText(empty ? "" : item.toString()); - final float f = empty ? 0 : item.toString().length() / 11F; - final Color c = Color.color(1 - f / 2F, 0, 0); - setTextFill(c); - setGraphic(empty ? null : new ImageView(img)); - } - }); - }); final PluginParameter robotOptions = SingleChoiceParameterType.build(ROBOT_PARAMETER_ID, robotpv); robotOptions.setName("Robot options"); robotOptions.setDescription("A list of robots to choose from"); // Use the helper method to add string options. SingleChoiceParameterType.setOptions(robotOptions, Arrays.asList("Bender", "Gort", "Maximillian", "Robbie", "Tom Servo")); + SingleChoiceParameterType.setIcons(robotOptions, Arrays.asList(new Image(ALIEN_ICON), new Image(ALIEN_ICON), new Image(ALIEN_ICON), new Image(ALIEN_ICON), new Image(ALIEN_ICON))); // Create a ParameterValue of the underlying type (in this case, String) to set the default choice. final StringParameterValue robotChoice = new StringParameterValue("Gort"); diff --git a/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/gui/SingleChoiceInputPane.java b/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/gui/SingleChoiceInputPane.java index 3a375b6a7b..cda6834a23 100644 --- a/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/gui/SingleChoiceInputPane.java +++ b/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/gui/SingleChoiceInputPane.java @@ -15,126 +15,97 @@ */ package au.gov.asd.tac.constellation.plugins.gui; +import au.gov.asd.tac.constellation.plugins.parameters.ParameterChange; +import static au.gov.asd.tac.constellation.plugins.parameters.ParameterChange.ENABLED; +import static au.gov.asd.tac.constellation.plugins.parameters.ParameterChange.PROPERTY; +import static au.gov.asd.tac.constellation.plugins.parameters.ParameterChange.VALUE; +import static au.gov.asd.tac.constellation.plugins.parameters.ParameterChange.VISIBLE; import au.gov.asd.tac.constellation.plugins.parameters.PluginParameter; +import au.gov.asd.tac.constellation.plugins.parameters.PluginParameterListener; import au.gov.asd.tac.constellation.plugins.parameters.types.ParameterValue; import au.gov.asd.tac.constellation.plugins.parameters.types.SingleChoiceParameterType; import au.gov.asd.tac.constellation.plugins.parameters.types.SingleChoiceParameterType.SingleChoiceParameterValue; +import au.gov.asd.tac.constellation.utilities.gui.field.SingleChoiceInput; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants.ChoiceType; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputListener; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.scene.layout.HBox; -import org.controlsfx.control.SearchableComboBox; /** - * A drop-down combo box which is the GUI element corresponding to a - * {@link PluginParameter} of + * A drop-down combo box which is the GUI element corresponding to a {@link PluginParameter} of * {@link au.gov.asd.tac.constellation.plugins.parameters.types.SingleChoiceParameterType}. *

- * Selecting an item from the drop-down will set the choice data for the - * underlying {@link PluginParameter}. + * Selecting an item from the drop-down will set the choice data for the underlying {@link PluginParameter}. * - * @see - * au.gov.asd.tac.constellation.plugins.parameters.types.SingleChoiceParameterType + * @see au.gov.asd.tac.constellation.plugins.parameters.types.SingleChoiceParameterType * * @author ruby_crucis */ -public class SingleChoiceInputPane extends HBox { +public class SingleChoiceInputPane extends ParameterInputPane { private static final Logger LOGGER = Logger.getLogger(SingleChoiceInputPane.class.getName()); - - public static final int DEFAULT_WIDTH = 300; - - private final SearchableComboBox field; - private boolean initialRun = true; public SingleChoiceInputPane(final PluginParameter parameter) { - field = new SearchableComboBox<>(); - field.setPromptText(parameter.getDescription()); - - final ObservableList optionsList = FXCollections.observableArrayList(); - optionsList.setAll(SingleChoiceParameterType.getOptionsData(parameter)); - field.setItems(optionsList); - - final ParameterValue initialValue = parameter.getParameterValue(); - if (initialValue.getObjectValue() != null) { - field.getSelectionModel().select(initialValue); - } - - field.setPrefWidth(DEFAULT_WIDTH); - field.setDisable(!parameter.isEnabled()); - field.setManaged(parameter.isVisible()); - field.setVisible(parameter.isVisible()); - this.setManaged(parameter.isVisible()); - this.setVisible(parameter.isVisible()); - - if (parameter.getParameterValue().getGuiInit() != null) { - parameter.getParameterValue().getGuiInit().init(field); - } - - field.setOnAction(event -> SingleChoiceParameterType.setChoiceData(parameter, field.getSelectionModel().getSelectedItem())); - - parameter.addListener((scParameter, change) -> Platform.runLater(() -> { - if (scParameter.getParameterValue() instanceof SingleChoiceParameterValue scParameterValue){ - switch (change) { - case VALUE -> { - // Don't change the value if it isn't necessary. - final List param = scParameterValue.getOptionsData(); - final ParameterValue value = field.getSelectionModel().getSelectedItem(); - - //Checks that the currently selected value is in the new parameters list - if (!param.contains(value)) { - field.getSelectionModel().select(scParameterValue.getChoiceData()); - } - - // give a visual indicator if a required parameter is empty - field.setId(scParameter.isRequired() && field.getSelectionModel().isEmpty() ? "invalid selection" : ""); - field.setStyle("invalid selection".equals(field.getId()) ? "-fx-color: #8A1D1D" : ""); - } - case PROPERTY -> { - final ObservableList options = FXCollections.observableArrayList(); - final EventHandler handler = field.getOnAction(); - field.setOnAction(null); - - options.setAll(scParameterValue.getOptionsData()); - field.setItems(options); - field.setOnAction(handler); + super(new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN), parameter); + final SingleChoiceParameterType.SingleChoiceParameterValue pv = parameter.getParameterValue(); + ((SingleChoiceInput) input).setOptions(pv.getOptionsData()); + ((SingleChoiceInput) input).setIcons(pv.getIcons()); + setFieldValue(pv.getChoiceData()); + } - if (initialRun) { - // This is a workaround to fix dynamically changing drop downs. - // Otherwise when the Constellation is loaded for the first time, - // such lists wouldn't populate until clicked twice on the arrow. - // E.g. `Type Category` drop down in `Select Top N` plugin - field.show(); - field.hide(); - field.requestFocus(); - initialRun = false; - } + @Override + public ConstellationInputListener getFieldChangeListener(final PluginParameter parameter) { + return (ConstellationInputListener) (final ParameterValue newValue) -> { + if (newValue != null) { + SingleChoiceParameterType.setChoiceData(parameter, newValue); + } + }; + } - // Only keep the value if it's in the new choices. - if (options.contains(scParameterValue.getChoiceData())) { - field.getSelectionModel().select(scParameter.getSingleChoice()); - } else { - field.getSelectionModel().clearSelection(); - } - } + @Override + public PluginParameterListener getPluginParameterListener() { + // The listener needs to be assigned and then returned otherwise it doesn't update as intended + final PluginParameterListener listener = (final PluginParameter parameter, final ParameterChange change) -> { + final PluginParameter scParameterValue = (PluginParameter) parameter; + switch (change) { + case VALUE -> { + final List paramOptions = SingleChoiceParameterType.getOptionsData(scParameterValue); + ((SingleChoiceInput) input).setOptions(paramOptions); + + // Only keep the value if its in the new choices + if (paramOptions.stream().anyMatch(paramOptions::contains)) { + setFieldValue(SingleChoiceParameterType.getChoiceData(scParameterValue)); + } else { + setFieldValue(null); + } + // Don't change the value if it isn't necessary + final ParameterValue selection = getFieldValue(); + if (selection != null && !selection.equals(SingleChoiceParameterType.getChoiceData(scParameterValue))) { + setFieldValue(selection); + } + } - case ENABLED -> field.setDisable(!scParameter.isEnabled()); - case VISIBLE -> { - field.setManaged(scParameter.isVisible()); - field.setVisible(scParameter.isVisible()); - this.setVisible(scParameter.isVisible()); - this.setManaged(scParameter.isVisible()); + case PROPERTY -> { + // Update the pane if the options have changed + final List paramOptions = (List) SingleChoiceParameterType.getChoiceData(scParameterValue); + if (paramOptions != null) { + ((SingleChoiceInput) input).setOptions(paramOptions); + if (paramOptions.contains(SingleChoiceParameterType.getChoiceData(scParameterValue))) { + setFieldValue(SingleChoiceParameterType.getChoiceData(scParameterValue)); + } else { + setFieldValue(null); } - default -> LOGGER.log(Level.FINE, "ignoring parameter change type {0}.", change); } } - })); - - getChildren().add(field); + case ENABLED -> + updateFieldEnablement(); + case VISIBLE -> + updateFieldVisibility(); + default -> + LOGGER.log(Level.FINE, "ignoring parameter change type {0}.", change); + } + }; + return listener; } } diff --git a/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/parameters/types/SingleChoiceParameterType.java b/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/parameters/types/SingleChoiceParameterType.java index 457d436dde..b7325cf52b 100644 --- a/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/parameters/types/SingleChoiceParameterType.java +++ b/CorePluginFramework/src/au/gov/asd/tac/constellation/plugins/parameters/types/SingleChoiceParameterType.java @@ -26,7 +26,8 @@ import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import org.openide.util.lookup.ServiceProvider; /** @@ -246,6 +247,14 @@ public static boolean isEditable(final PluginParameter parameter) { public static void setEditable(final PluginParameter parameter, boolean editable) { parameter.setProperty(EDITABLE, editable); } + + public static void setIcons(final PluginParameter parameter, final List icons) { + parameter.getParameterValue().setIcons(icons.stream().map(icon -> new ImageView(icon)).toList()); + } + + public static List getIcons(final PluginParameter parameter) { + return parameter.getParameterValue().getIcons(); + } /** * Constructs a new instance of this type. @@ -272,6 +281,7 @@ public static class SingleChoiceParameterValue extends ParameterValue { // If it's a nested class, make sure it's a static nested class rather than an inner class, // to avoid possible NoSuchMethodExceptions private final List options; + private final List icons; private ParameterValue choice; private final Class innerClass; @@ -281,6 +291,7 @@ public static class SingleChoiceParameterValue extends ParameterValue { */ public SingleChoiceParameterValue() { options = new ArrayList<>(); + icons = new ArrayList<>(); choice = null; innerClass = StringParameterValue.class; } @@ -294,6 +305,7 @@ public SingleChoiceParameterValue() { */ public SingleChoiceParameterValue(final Class innerClass) { options = new ArrayList<>(); + icons = new ArrayList<>(); choice = null; this.innerClass = innerClass; } @@ -310,6 +322,8 @@ public SingleChoiceParameterValue(final Class innerCla public SingleChoiceParameterValue(final SingleChoiceParameterValue sc) { options = new ArrayList<>(); options.addAll(sc.options); + icons = new ArrayList<>(); + icons.addAll(sc.icons); choice = sc.choice != null ? sc.choice.copy() : null; innerClass = sc.innerClass; } @@ -349,6 +363,26 @@ public void setOptions(final Iterable options) { } choice = null; } + + /** + * Set the collection of icons from a list of ImageIcons. + * + * @param icons A list of ImageIcons to set the collection of icons + * from. + */ + public void setIcons(final List icons) { + this.icons.clear(); + this.icons.addAll(icons); + } + + /** + * Get the collection of options from a list of Strings. + * + * @return A list of ImageIcons representing the icons. + */ + public List getIcons() { + return Collections.unmodifiableList(this.icons); + } /** * Get the collection of options as a list of {@link ParameterValue}. diff --git a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/MultiChoiceInput.java b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/MultiChoiceInput.java index 6b83466026..1da0037309 100644 --- a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/MultiChoiceInput.java +++ b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/MultiChoiceInput.java @@ -33,7 +33,9 @@ import au.gov.asd.tac.constellation.utilities.gui.field.framework.RightButtonSupport; import java.util.Map; import javafx.collections.ObservableList; +import javafx.event.EventHandler; import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.input.MouseEvent; /** * A {@link ChoiceInput} for managing multiple choice selection. This input @@ -59,12 +61,12 @@ public final class MultiChoiceInput extends ChoiceInputField options) { super(options); - initialiseDepedantComponents(); + initialiseDependantComponents(); } // @@ -231,20 +233,20 @@ public List getLocalMenuItems() { @Override public RightButton getRightButton() { return new RightButton( - new Label(""), ButtonType.DROPDOWN) { - + new Label("Select"), ButtonType.DROPDOWN) { + @Override public void show() { // show our menu instead - executeRightButtonAction(); + executeRightButtonAction(); } - + @Override public void hide() { // this is triggered when clicking away from button setMenuShown(false); } - }; + }; } @Override diff --git a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/SingleChoiceInput.java b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/SingleChoiceInput.java new file mode 100644 index 0000000000..f35d11db2e --- /dev/null +++ b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/SingleChoiceInput.java @@ -0,0 +1,351 @@ +/* + * Copyright 2010-2026 Australian Signals Directorate + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package au.gov.asd.tac.constellation.utilities.gui.field; + +import au.gov.asd.tac.constellation.utilities.gui.field.framework.AutoCompleteSupport; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ChoiceInputField; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInput; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputButton.ButtonType; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants.ChoiceType; +import static au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants.ChoiceType.SINGLE_DROPDOWN; +import static au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants.ChoiceType.SINGLE_SPINNER; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputDropDown; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputListener; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.LeftButtonSupport; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.RightButtonSupport; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ShortcutSupport; +import java.util.ArrayList; +import java.util.List; +import javafx.event.EventHandler; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.Labeled; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; + +/** + * A {@link ChoiceInput} for managing single choice selection. This input provides the following + * {@link ConstellationInput} support features + *

    + *
  • {@link RightButtonSupport} - Increments the choice in spinners and triggers a drop down menu to select a choice + * from the list of options.
  • + *
  • {@link LeftButtonSupport} - Only used in Spinner inputs to decrement the choice.
  • + *
  • {@link ShortcutSupport} - Increments and decrements the data chronologically with up and down arrow.
  • + *
  • {@link AutoCompleteSupport} - Provides a list of colors with a name that matches the text in the input + * field.
  • + *
+ * See referenced classes and interfaces for further details on inherited and implemented features. + * + * @param The type of object represented by this input. + * + * @author capricornunicorn123 + */ +public class SingleChoiceInput extends ChoiceInputField implements RightButtonSupport, LeftButtonSupport, AutoCompleteSupport, ShortcutSupport { + + private final ChoiceType type; + private boolean showDropDown = true; + + public SingleChoiceInput(final ChoiceType type) { + this.type = type; + initialiseDependantComponents(); + + addListener(newValue -> { + if (!getTextArea().isInFocus() && !isValid()) { + setText(""); + } + }); + } + + @Override + public EventHandler getShortcuts() { + //Add shortcuts where users can increment and decrement the date using up and down arrows + return event -> { + if (event.getCode() == KeyCode.UP) { + this.decrementChoice(); + event.consume(); + } else if (event.getCode() == KeyCode.DOWN) { + this.incrementChoice(); + event.consume(); + } + }; + } + + public C getChoice() { + final List matches = getOptions().stream().filter(choice -> choice.toString().equals(getText())).toList(); + return matches.isEmpty() ? null : matches.getFirst(); + } + + /** + * Changes the List of selected Choices to ensure the provided choice is included. if single choice selection mode + * then the old choice is removed if multi choice the old choice is retained + * + * @param choice + */ + public void setChoice(final C choice) { + if (choice != null && this.getOptions().contains(choice)) { + this.setText(choice.toString()); + } else { + clearChoices(); + } + } + + /** + * Removes the provided choice from the currently selected choices. + * + * @param choice + */ + public void removeChoice(final C choice) { + if (getChoice() == choice) { + clearChoices(); + } + } + + /** + * Used in single choice Options to increment a selected choice. If the choice is the last choice in the list of + * options the next choice is the first option. + */ + private void incrementChoice() { + final C selection = this.getChoice(); + if (selection != null) { + final int nextSelectionIndex = this.getOptions().indexOf(selection) + 1; + if (nextSelectionIndex < this.getOptions().size()) { + this.setChoice(this.getOptions().get(nextSelectionIndex)); + } else { + this.setChoice(this.getOptions().getFirst()); + } + } else { + this.setChoice(this.getOptions().getLast()); + } + } + + /** + * Used in single choice Options to decrement a selected choice. If the choice is the first choice in the list of + * options the previous choice is the last option. + */ + private void decrementChoice() { + final C selection = this.getChoice(); + if (selection != null) { + final int prevSelectionIndex = this.getOptions().indexOf(selection) - 1; + if (prevSelectionIndex < this.getOptions().size()) { + this.setChoice(this.getOptions().get(prevSelectionIndex)); + } else { + this.setChoice(this.getOptions().getLast()); + } + } else { + this.setChoice(this.getOptions().getFirst()); + } + } + + @Override + public C getValue() { + return getChoice(); + } + + @Override + public void setValue(final C value) { + this.setChoice(value); + } + + @Override + public boolean isValidContent() { + return getText().isBlank() || getChoice() != null; + } + + @Override + public List getLocalMenuItems() { + final List items = new ArrayList<>(); + if (type != null) { + if (type == SINGLE_SPINNER) { + final MenuItem next = new MenuItem("Increment"); + next.setOnAction(value -> executeRightButtonAction()); + items.add(next); + + final MenuItem prev = new MenuItem("Decrement"); + prev.setOnAction(value -> executeLeftButtonAction()); + items.add(prev); + } + final MenuItem choose = new MenuItem("Select Choice"); + choose.setOnAction(value -> executeRightButtonAction()); + items.add(choose); + } + return items; + } + + @Override + public LeftButton getLeftButton() { + if (type == SINGLE_SPINNER) { + return new LeftButton(new Label(ConstellationInputConstants.PREVIOUS_BUTTON_LABEL), ButtonType.CHANGER) { + public EventHandler action() { + return event -> executeLeftButtonAction(); + } + }; + } else { + return null; + } + } + + @Override + public RightButton getRightButton() { + final Label label; + final ButtonType buttonType; + + switch (type) { + case SINGLE_SPINNER -> { + label = new Label(ConstellationInputConstants.NEXT_BUTTON_LABEL); + buttonType = ButtonType.CHANGER; + } + case SINGLE_DROPDOWN -> { + label = new Label(ConstellationInputConstants.SELECT_BUTTON_LABEL); + buttonType = ButtonType.DROPDOWN; + } + default -> { + return null; + } + } + return new RightButton(label, buttonType) { + @Override + public void show() { + // show our menu instead + executeRightButtonAction(); + } + + @Override + public void hide() { + // this is triggered when clicking away from button + setMenuShown(false); + } + }; + } + + @Override + public void executeLeftButtonAction() { + if (type == SINGLE_SPINNER) { + decrementChoice(); + } + } + + @Override + public void executeRightButtonAction() { + if (type == SINGLE_SPINNER) { + this.incrementChoice(); + } else if (type == SINGLE_DROPDOWN) { + this.showDropDown(new ChoiceInputDropDown(this)); + } + } + + @Override + public List getAutoCompleteSuggestions() { + if (!showDropDown) { + showDropDown = true; + return null; + } + + final List suggestions = new ArrayList<>(); + // Get suggestions based on the text showing + this.getOptions().stream().map(value -> value) + .filter(value -> value.toString().toUpperCase().contains(getText().toUpperCase())) + .forEach(value -> { + final int index = this.getOptions().indexOf(value); + final MenuItem item = new MenuItem(value.toString()); + if (!this.icons.isEmpty()) { + item.setGraphic(this.icons.get(index)); + } + item.setOnAction(event -> { + showDropDown = false; + this.setChoice(value); + this.setMenuShown(false); + }); + suggestions.add(item); + }); + + // If the text matches a suggestion, show all suggestions + // The currently selected option will show at the top of the suggestions list + if (!suggestions.isEmpty()) { + final String match = suggestions.get(0).getText(); + if (suggestions.size() == 1 && match.toUpperCase().equals(getText().toUpperCase())) { + this.getOptions().stream().map(value -> value).forEach(value -> { + if (!match.equals(value.toString())) { + final int index = this.getOptions().indexOf(value); + final MenuItem item = new MenuItem(value.toString()); + if (!this.icons.isEmpty()) { + item.setGraphic(this.icons.get(index)); + } + item.setOnAction(event -> { + showDropDown = false; + this.setChoice(value); + this.setMenuShown(false); + }); + suggestions.add(item); + } + }); + } + } + return suggestions; + } + + /** + * A context menu to be used as a drop down for {@link ChoiceInputFields} + */ + private class ChoiceInputDropDown extends ConstellationInputDropDown { + + final List boxes = new ArrayList<>(); + + public ChoiceInputDropDown(final SingleChoiceInput field) { + super(field); + + if (getOptions() != null) { + final Object[] optionsList = getOptions().toArray(); + for (int i = 0; i < optionsList.length; i++) { + final C choice = (C) optionsList[i]; + + final Labeled item = switch (field.type) { + case SINGLE_DROPDOWN -> { + final Label label = new Label(choice.toString()); + label.setOnMouseClicked(event -> field.setChoice(choice)); + yield label; + } + case SINGLE_SPINNER -> new Label(); + }; + + if (!icons.isEmpty()) { + item.setGraphic(icons.get(i)); + } + final CustomMenuItem menuItem = registerCustomMenuItem(item); + menuItem.setHideOnClick(true); + } + } + + final ConstellationInputListener> cl = (final List newValue) -> { + if (newValue != null) { + final List stringrep = newValue.stream().map(Object::toString).toList(); + for (final CheckBox box : boxes) { + box.setSelected(stringrep.contains(box.getText())); + } + } + }; + + // Register the Context Menu as a listener whilst it is open incase choices are modified externaly. + this.setOnShowing(value -> field.addListener(cl)); + + // This context menu may be superseeded by a new context menu so deregister it when hidden. + this.setOnHiding(value -> field.removeListener(cl)); + } + } +} diff --git a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ChoiceInputField.java b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ChoiceInputField.java index f040a401e7..4fa3cd8561 100644 --- a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ChoiceInputField.java +++ b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ChoiceInputField.java @@ -46,8 +46,7 @@ protected ChoiceInputField() { protected ChoiceInputField(final ObservableList options) { if (options == null) { - throw new InvalidOperationException( - "Attempting to Set Options with null options"); + throw new InvalidOperationException("Attempting to Set Options with null options"); } this.options.addAll(options); } @@ -64,8 +63,7 @@ public final void setOptions(final List options) { if (options != null) { this.options.addAll(options); } else { - throw new InvalidOperationException( - "Attempting to Set Options with null options"); + throw new InvalidOperationException("Attempting to Set Options with null options"); } } diff --git a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInput.java b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInput.java index 97325502fa..cf141a6664 100644 --- a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInput.java +++ b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInput.java @@ -190,7 +190,7 @@ protected ConstellationInput(final TextType type) { }); textArea.setContextMenu(contextMenu); textArea.setMinHeight(DEFAULT_CELL_HEIGHT); - textArea.setPadding(new Insets(0, 5, 0, 0)); + textArea.setPadding(new Insets(0, 0, 0, 0)); } // @@ -238,7 +238,7 @@ private void buildInputFieldLayers(final ConstellationTextArea textArea) { * initializing. If not done this way, we get some ugly errors where methods * and objects don't exist... */ - protected void initialiseDepedantComponents() { + protected void initialiseDependantComponents() { final HBox interactableContent = getInteractableContent(); // Build out the visual Input components on the FX thread. @@ -288,7 +288,7 @@ protected void initialiseDepedantComponents() { menu.setAutoFix(true); menu.setWidth(textArea.getWidth()); //Listen for key events for when arrows are pressed or when to hide the menu - addEventFilter(KeyEvent.KEY_PRESSED, (KeyEvent event) -> { + addEventFilter(KeyEvent.KEY_PRESSED, (final KeyEvent event) -> { menu.hide(); setMenuShown(false); }); @@ -478,6 +478,10 @@ public final void setTooltip(final Tooltip tooltip) { public final String getText() { return textArea.getText(); } + + public final ConstellationTextArea getTextArea() { + return textArea; + } /** * Sets the current string value of the {@link ConstellationTextArea}. @@ -640,7 +644,7 @@ public final List getAllMenuItems() { * * @param menu */ - protected final void showDropDown(final ConstellationInputDropDown menu) { + public final void showDropDown(final ConstellationInputDropDown menu) { menu.show(this, Side.TOP, USE_PREF_SIZE, USE_PREF_SIZE); setMenuShown(true); } diff --git a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInputButton.java b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInputButton.java index 1e286c33bc..86c4193b52 100644 --- a/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInputButton.java +++ b/CoreUtilities/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/ConstellationInputButton.java @@ -15,10 +15,14 @@ */ package au.gov.asd.tac.constellation.utilities.gui.field.framework; +import javafx.beans.property.DoubleProperty; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; /** * This button will be used within the ConstellationInput framework. @@ -33,13 +37,35 @@ public abstract class ConstellationInputButton extends ComboBox { private final ButtonType btnType; - private Region arrowBtn = null ; + private Region arrowBtn = null; + private static final int END_CELL_PREF_WIDTH = 50; + private static final int DEFAULT_CELL_HEIGHT = 22; + private final Color buttonColor = Color.color(25/255D, 84/255D, 154/255D); + private final Color optionColor = Color.color(97/255D, 99/255D, 102/255D); + private final Rectangle background = new Rectangle(END_CELL_PREF_WIDTH, DEFAULT_CELL_HEIGHT); protected ConstellationInputButton(final Label label, final ButtonType type) { btnType = type; if (!label.getText().isEmpty()) { this.setValue(label.getText()); } + + final Color color = switch (type){ + case POPUP -> buttonColor; + default -> optionColor; + }; + background.setFill(color); + + background.setOnMouseEntered(event -> background.setFill(color.brighter())); + background.setOnMouseExited(event -> background.setFill(color)); + label.setMouseTransparent(true); + label.setPrefWidth(END_CELL_PREF_WIDTH); + label.setAlignment(Pos.CENTER); + this.getChildren().addAll(background, label); + } + + public DoubleProperty getHeightProperty(){ + return background.heightProperty(); } @Override @@ -62,7 +88,6 @@ protected void layoutChildren() { } } - public enum ButtonType{ POPUP, DROPDOWN, diff --git a/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/SingleChoiceInputNGTest.java b/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/SingleChoiceInputNGTest.java new file mode 100644 index 0000000000..7e00d51c52 --- /dev/null +++ b/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/SingleChoiceInputNGTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2010-2026 Australian Signals Directorate + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package au.gov.asd.tac.constellation.utilities.gui.field; + +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.ConstellationInputConstants.ChoiceType; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.LeftButtonSupport; +import au.gov.asd.tac.constellation.utilities.gui.field.framework.RightButtonSupport; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javafx.scene.control.MenuItem; +import static org.assertj.core.api.Assertions.assertThatCode; +import org.mockito.Mockito; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import org.testfx.api.FxToolkit; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Test class for SingleChoiceInput class + * + * @author Delphinus8821 + */ +public class SingleChoiceInputNGTest { + + private static final Logger LOGGER = Logger.getLogger(SingleChoiceInputNGTest.class.getName()); + + @BeforeClass + public static void setUpClass() throws Exception { + if (!FxToolkit.isFXApplicationThreadRunning()) { + FxToolkit.registerPrimaryStage(); + } + } + + @AfterClass + public static void tearDownClass() throws Exception { + try { + FxToolkit.cleanupStages(); + } catch (final TimeoutException ex) { + LOGGER.log(Level.WARNING, "FxToolkit timedout trying to cleanup stages", ex); + } + } + + /** + * Test of getChoice method, of class SingleChoiceInput. + */ + @Test + public void testGetSetAndRemoveChoice() { + System.out.println("getChoice"); + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + + // Get null choice + final Object nullResult = instance.getChoice(); + assertNull(nullResult); + + // Set an option + final Object choice = "choice"; + final List options = new ArrayList<>(); + options.add(choice); + instance.setOptions(options); + instance.setChoice(choice); + final Object result = instance.getChoice(); + assertEquals(result, choice); + + // Remove choice + instance.removeChoice(choice); + final Object emptyResult = instance.getChoice(); + assertNull(emptyResult); + } + + /** + * Test of getValue method, of class SingleChoiceInput. + */ + @Test + public void testGetAndSetValue() { + System.out.println("getValue"); + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + final Object choice = "choice"; + final List options = new ArrayList<>(); + options.add(choice); + instance.setOptions(options); + instance.setValue(choice); + final Object result = instance.getValue(); + assertEquals(result, choice); + } + + /** + * Test of isValidContent method, of class SingleChoiceInput. + */ + @Test + public void testIsValidContent() { + System.out.println("isValidContent"); + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + + // Test empty value + boolean result = instance.isValidContent(); + assertTrue(result); + + // Test with a value + final Object choice = "choice"; + final List options = new ArrayList<>(); + options.add(choice); + instance.setOptions(options); + instance.setChoice(choice); + result = instance.isValidContent(); + assertTrue(result); + } + + /** + * Test of getLocalMenuItems method, of class SingleChoiceInput. + */ + @Test + public void testGetLocalMenuItems() { + System.out.println("getLocalMenuItems"); + + // Test single dropdown + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + final List expResult = new ArrayList<>(); + final MenuItem choose = new MenuItem("Select Choice"); + expResult.add(choose); + + List result = instance.getLocalMenuItems(); + assertEquals(result.size(), expResult.size()); + + // Test single spinner + final SingleChoiceInput spinnerInstance = new SingleChoiceInput(ChoiceType.SINGLE_SPINNER); + final List biggerResult = new ArrayList<>(); + final MenuItem next = new MenuItem("Increment"); + final MenuItem prev = new MenuItem("Decrement"); + biggerResult.add(next); + biggerResult.add(prev); + biggerResult.add(choose); + + result = spinnerInstance.getLocalMenuItems(); + assertEquals(result.size(), biggerResult.size()); + } + + /** + * Test of getLeftButton method, of class SingleChoiceInput. + */ + @Test + public void testGetLeftButton() { + System.out.println("getLeftButton"); + + // Test single dropdown + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + LeftButtonSupport.LeftButton expResult = null; + LeftButtonSupport.LeftButton result = instance.getLeftButton(); + assertEquals(result, expResult); + + // Test single spinner + final SingleChoiceInput spinnerInstance = new SingleChoiceInput(ChoiceType.SINGLE_SPINNER); + final String spinnerName = ConstellationInputConstants.PREVIOUS_BUTTON_LABEL; + LeftButtonSupport.LeftButton spinnerResult = spinnerInstance.getLeftButton(); + assertEquals(spinnerName, spinnerResult.getValue()); + } + + /** + * Test of getRightButton method, of class SingleChoiceInput. + */ + @Test + public void testGetRightButton() { + System.out.println("getRightButton"); + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + final String dropDownResult = ConstellationInputConstants.SELECT_BUTTON_LABEL; + final RightButtonSupport.RightButton result = instance.getRightButton(); + assertEquals(result.getValue(), dropDownResult); + + final SingleChoiceInput spinnerInstance = new SingleChoiceInput(ChoiceType.SINGLE_SPINNER); + final String spinnerResult = ConstellationInputConstants.NEXT_BUTTON_LABEL; + final RightButtonSupport.RightButton newResult = spinnerInstance.getRightButton(); + assertEquals(newResult.getValue(), spinnerResult); + } + + /** + * Test of executeRightButtonAction method, of class SingleChoiceInput. + */ + @Test + public void testExecuteRightButtonAction() { + System.out.println("executeRightButtonAction"); + final SingleChoiceInput singleChoiceInput = spy(new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN)); + doNothing().when(singleChoiceInput).showDropDown(Mockito.any()); + assertThatCode(() -> singleChoiceInput.executeRightButtonAction()).doesNotThrowAnyException(); + + doReturn(false).when(singleChoiceInput).isMenuShown(); + doNothing().when(singleChoiceInput).showDropDown(Mockito.any()); + + final RightButtonSupport.RightButton rightButton = singleChoiceInput.getRightButton(); + rightButton.show(); + verify(singleChoiceInput, times(2)).executeRightButtonAction(); + + doReturn(true).when(singleChoiceInput).isMenuShown(); + // second consecutive time it is called, setMenuShown(false) is called + rightButton.show(); + verify(singleChoiceInput, times(3)).executeRightButtonAction(); + } + + /** + * Test of getAutoCompleteSuggestions method, of class SingleChoiceInput. + */ + @Test + public void testGetAutoCompleteSuggestions() { + System.out.println("getAutoCompleteSuggestions"); + final SingleChoiceInput instance = new SingleChoiceInput(ChoiceType.SINGLE_DROPDOWN); + final Object choice = "choice"; + final List options = new ArrayList<>(); + options.add(choice); + instance.setOptions(options); + instance.setChoice(choice); + instance.setText("choice"); + final List result = instance.getAutoCompleteSuggestions(); + assertEquals(result.size(), options.size()); + } + +} diff --git a/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/MultiChoiceInputNGTest.java b/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/MultiChoiceInputNGTest.java index 29ed89313c..704b24409a 100644 --- a/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/MultiChoiceInputNGTest.java +++ b/CoreUtilities/test/unit/src/au/gov/asd/tac/constellation/utilities/gui/field/framework/MultiChoiceInputNGTest.java @@ -178,7 +178,7 @@ public void testMultiChoiceInput_rightButtonSupport() { doNothing().when(multiChoiceInput).executeRightButtonAction(); final RightButtonSupport.RightButton rightButton = multiChoiceInput.getRightButton(); - Assert.assertNull(rightButton.getValue()); //label default + Assert.assertEquals(rightButton.getValue(), "Select"); //label default rightButton.show(); verify(multiChoiceInput, times(1)).executeRightButtonAction(); verify(multiChoiceInput, times(0)).setMenuShown(false);