diff --git a/core/src/main/java/info/openrocket/core/material/Material.java b/core/src/main/java/info/openrocket/core/material/Material.java index 2fee1bd49..c13214a55 100644 --- a/core/src/main/java/info/openrocket/core/material/Material.java +++ b/core/src/main/java/info/openrocket/core/material/Material.java @@ -4,6 +4,7 @@ import info.openrocket.core.l10n.Translator; import info.openrocket.core.startup.Application; import info.openrocket.core.unit.Unit; import info.openrocket.core.unit.UnitGroup; +import info.openrocket.core.util.Groupable; import info.openrocket.core.util.MathUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +19,7 @@ import org.slf4j.LoggerFactory; * @author Sampo Niskanen */ -public abstract class Material implements Comparable { +public abstract class Material implements Comparable, Groupable { private static final Translator trans = Application.getTranslator(); private static final Logger log = LoggerFactory.getLogger(Material.class); diff --git a/core/src/main/java/info/openrocket/core/material/MaterialGroup.java b/core/src/main/java/info/openrocket/core/material/MaterialGroup.java index 57bfdf426..9bbbc4784 100644 --- a/core/src/main/java/info/openrocket/core/material/MaterialGroup.java +++ b/core/src/main/java/info/openrocket/core/material/MaterialGroup.java @@ -2,11 +2,12 @@ package info.openrocket.core.material; import info.openrocket.core.l10n.Translator; import info.openrocket.core.startup.Application; +import info.openrocket.core.util.Group; /** * A class for categorizing materials. */ -public class MaterialGroup implements Comparable { +public class MaterialGroup implements Comparable, Group { private static final Translator trans = Application.getTranslator(); // When modifying this list, also update the MaterialGroupDTO class in the preset.xml package! (and the ALL_GROUPS array) diff --git a/core/src/main/java/info/openrocket/core/simulation/FlightDataType.java b/core/src/main/java/info/openrocket/core/simulation/FlightDataType.java index 15b4174bc..6366adedf 100644 --- a/core/src/main/java/info/openrocket/core/simulation/FlightDataType.java +++ b/core/src/main/java/info/openrocket/core/simulation/FlightDataType.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import info.openrocket.core.util.Groupable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,7 +33,7 @@ import info.openrocket.core.util.StringUtils; * * @author Sampo Niskanen */ -public class FlightDataType implements Comparable { +public class FlightDataType implements Comparable, Groupable { private static final Translator trans = Application.getTranslator(); private static final Logger log = LoggerFactory.getLogger(FlightDataType.class); diff --git a/core/src/main/java/info/openrocket/core/simulation/FlightDataTypeGroup.java b/core/src/main/java/info/openrocket/core/simulation/FlightDataTypeGroup.java index 2db1af4eb..3c26a008b 100644 --- a/core/src/main/java/info/openrocket/core/simulation/FlightDataTypeGroup.java +++ b/core/src/main/java/info/openrocket/core/simulation/FlightDataTypeGroup.java @@ -2,8 +2,9 @@ package info.openrocket.core.simulation; import info.openrocket.core.l10n.Translator; import info.openrocket.core.startup.Application; +import info.openrocket.core.util.Group; -public class FlightDataTypeGroup implements Comparable { +public class FlightDataTypeGroup implements Comparable, Group { private static final Translator trans = Application.getTranslator(); public static final FlightDataTypeGroup TIME = new FlightDataTypeGroup(trans.get("FlightDataTypeGroup.GROUP_TIME"), 0); diff --git a/core/src/main/java/info/openrocket/core/util/Group.java b/core/src/main/java/info/openrocket/core/util/Group.java new file mode 100644 index 000000000..f2da92c59 --- /dev/null +++ b/core/src/main/java/info/openrocket/core/util/Group.java @@ -0,0 +1,9 @@ +package info.openrocket.core.util; + +/** + * Interface for objects that a object can be grouped into. + */ +public interface Group { + String getName(); + int getPriority(); // Lower number = higher priority (can be used for sorting) +} diff --git a/core/src/main/java/info/openrocket/core/util/Groupable.java b/core/src/main/java/info/openrocket/core/util/Groupable.java new file mode 100644 index 000000000..6859fb70f --- /dev/null +++ b/core/src/main/java/info/openrocket/core/util/Groupable.java @@ -0,0 +1,9 @@ +package info.openrocket.core.util; + +/** + * Interface for objects that can be grouped into a object. + * @param the group type + */ +public interface Groupable { + G getGroup(); +} diff --git a/swing/src/main/java/info/openrocket/swing/gui/configdialog/MaterialPanel.java b/swing/src/main/java/info/openrocket/swing/gui/configdialog/MaterialPanel.java index de8776291..089e0fdba 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/configdialog/MaterialPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/configdialog/MaterialPanel.java @@ -89,13 +89,9 @@ public class MaterialPanel extends JPanel implements Invalidatable, Invalidating }); // Material selection combo box - this.materialCombo = MaterialComboBox.createComboBox(MaterialGroup.ALL_GROUPS, mm.getAllMaterials(), + this.materialCombo = MaterialComboBox.createComboBox(mm, MaterialGroup.ALL_GROUPS, mm.getAllMaterials(), customMaterialButton, editMaterialsButton); this.materialCombo.setSelectedItem(mm.getSelectedItem()); - this.materialCombo.addActionListener(e -> { - Material selectedMaterial = (Material) materialCombo.getSelectedItem(); - mm.setSelectedItem(selectedMaterial); - }); this.materialCombo.setToolTipText(trans.get("MaterialPanel.combo.ttip.ComponentMaterialAffects")); this.add(this.materialCombo, "spanx 4, growx, wrap paragraph"); order.add(this.materialCombo); @@ -175,9 +171,10 @@ public class MaterialPanel extends JPanel implements Invalidatable, Invalidating private static final Translator trans = Application.getTranslator(); public static SearchableAndCategorizableComboBox createComboBox( - MaterialGroup[] allGroups, Material[] materials, Component... extraCategoryWidgets) { - final Map materialGroupMap = createMaterialGroupMap(allGroups, materials); - return new SearchableAndCategorizableComboBox<>(materialGroupMap, + MaterialModel mm, MaterialGroup[] allGroups, Material[] materials, Component... extraCategoryWidgets) { + final Map> materialGroupMap = + createMaterialGroupMap(allGroups, materials); + return new SearchableAndCategorizableComboBox<>(mm, materialGroupMap, trans.get("MaterialPanel.MaterialComboBox.placeholder"), extraCategoryWidgets) { @Override public String getDisplayString(Material item) { @@ -192,7 +189,7 @@ public class MaterialPanel extends JPanel implements Invalidatable, Invalidating public static void updateComboBoxItems(SearchableAndCategorizableComboBox comboBox, MaterialGroup[] allGroups, Material[] materials) { - final Map materialGroupMap = createMaterialGroupMap(allGroups, materials); + final Map> materialGroupMap = createMaterialGroupMap(allGroups, materials); comboBox.updateItems(materialGroupMap); comboBox.invalidate(); comboBox.repaint(); @@ -204,13 +201,13 @@ public class MaterialPanel extends JPanel implements Invalidatable, Invalidating * @param materials the materials * @return the map linking the materials to their groups */ - private static Map createMaterialGroupMap( + private static Map> createMaterialGroupMap( MaterialGroup[] groups, Material[] materials) { // Sort the groups based on priority (lower number = higher priority) MaterialGroup[] sortedGroups = groups.clone(); Arrays.sort(sortedGroups, Comparator.comparingInt(MaterialGroup::getPriority)); - Map map = new LinkedHashMap<>(); + Map> map = new LinkedHashMap<>(); MaterialGroup materialGroup; for (MaterialGroup group : sortedGroups) { List itemsForGroup = new ArrayList<>(); @@ -223,11 +220,11 @@ public class MaterialPanel extends JPanel implements Invalidatable, Invalidating // Sort the types within each group based on priority itemsForGroup.sort(Comparator.comparingInt(Material::getGroupPriority)); - map.put(group, itemsForGroup.toArray(new Material[0])); + map.put(group, itemsForGroup); } // Remove empty groups - map.entrySet().removeIf(entry -> entry.getValue().length == 0); + map.entrySet().removeIf(entry -> entry.getValue().isEmpty()); return map; } diff --git a/swing/src/main/java/info/openrocket/swing/gui/simulation/FlightDataComboBox.java b/swing/src/main/java/info/openrocket/swing/gui/simulation/FlightDataComboBox.java index 6e1388144..cd74bb6f0 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/simulation/FlightDataComboBox.java +++ b/swing/src/main/java/info/openrocket/swing/gui/simulation/FlightDataComboBox.java @@ -18,7 +18,7 @@ public class FlightDataComboBox extends JComboBox { private static final Translator trans = Application.getTranslator(); public static SearchableAndCategorizableComboBox createComboBox(FlightDataTypeGroup[] allGroups, FlightDataType[] types) { - final Map typeGroupMap = createFlightDataGroupMap(allGroups, types); + final Map> typeGroupMap = createFlightDataGroupMap(allGroups, types); return new SearchableAndCategorizableComboBox<>(typeGroupMap, trans.get("FlightDataComboBox.placeholder")); } @@ -28,13 +28,13 @@ public class FlightDataComboBox extends JComboBox { * @param types the types * @return the map linking the types to their groups */ - private static Map createFlightDataGroupMap( + private static Map> createFlightDataGroupMap( FlightDataTypeGroup[] groups, FlightDataType[] types) { // Sort the groups based on priority (lower number = higher priority) FlightDataTypeGroup[] sortedGroups = groups.clone(); Arrays.sort(sortedGroups, Comparator.comparingInt(FlightDataTypeGroup::getPriority)); - Map map = new LinkedHashMap<>(); + Map> map = new LinkedHashMap<>(); for (FlightDataTypeGroup group : sortedGroups) { List itemsForGroup = new ArrayList<>(); for (FlightDataType type : types) { @@ -45,7 +45,7 @@ public class FlightDataComboBox extends JComboBox { // Sort the types within each group based on priority itemsForGroup.sort(Comparator.comparingInt(FlightDataType::getGroupPriority)); - map.put(group, itemsForGroup.toArray(new FlightDataType[0])); + map.put(group, itemsForGroup); } return map; diff --git a/swing/src/main/java/info/openrocket/swing/gui/widgets/SearchableAndCategorizableComboBox.java b/swing/src/main/java/info/openrocket/swing/gui/widgets/SearchableAndCategorizableComboBox.java index 287bd4c36..07984aec5 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/widgets/SearchableAndCategorizableComboBox.java +++ b/swing/src/main/java/info/openrocket/swing/gui/widgets/SearchableAndCategorizableComboBox.java @@ -1,9 +1,12 @@ package info.openrocket.swing.gui.widgets; +import info.openrocket.core.util.Group; +import info.openrocket.core.util.Groupable; import info.openrocket.swing.gui.util.GUIUtil; import info.openrocket.swing.gui.theme.UITheme; import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.JComboBox; @@ -13,7 +16,10 @@ import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; +import javax.swing.MutableComboBoxModel; import javax.swing.SwingUtilities; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.plaf.basic.BasicArrowButton; @@ -34,23 +40,26 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.Vector; /** * A combo box that has a search box for searching the items in the combobox. * If no text is entered, the combobox items are displayed in a categorized popup menu, grouped according to their groups. - * @param The type of the group - * @param The type of the items + * @param The type of the group + * @param The type of the groupable items * * @author Sibo Van Gool */ -public class SearchableAndCategorizableComboBox extends JComboBox { - +public class SearchableAndCategorizableComboBox> extends JComboBox { private final String placeHolderText; private JPopupMenu categoryPopup; private JPopupMenu searchPopup; @@ -59,13 +68,15 @@ public class SearchableAndCategorizableComboBox extends JComboBox { private final Component[] extraCategoryWidgets; private JList filteredList; - private T[] allItems; - private Map itemGroupMap; + private Map> itemGroupMap; + private List allItems; private int highlightedListIdx = -1; private static Color textSelectionBackground; + private static final String CHECKMARK = "\u2713"; + static { initColors(); } @@ -76,8 +87,9 @@ public class SearchableAndCategorizableComboBox extends JComboBox { * @param placeHolderText the placeholder text for the search field (when no text is entered) * @param extraCategoryWidgets extra widgets to add to the category popup. Each widget will be added as a separate menu item. */ - public SearchableAndCategorizableComboBox(Map itemGroupMap, String placeHolderText, Component... extraCategoryWidgets) { - super(); + public SearchableAndCategorizableComboBox(ComboBoxModel model, Map> itemGroupMap, String placeHolderText, + Component... extraCategoryWidgets) { + super(model != null ? model : new DefaultComboBoxModel<>()); setEditable(false); initColors(); @@ -87,6 +99,25 @@ public class SearchableAndCategorizableComboBox extends JComboBox { updateItems(itemGroupMap); setupMainRenderer(); + if (model != null) { + model.addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { + updateItemsFromModel(); + } + + @Override + public void intervalRemoved(ListDataEvent e) { + updateItemsFromModel(); + } + + @Override + public void contentsChanged(ListDataEvent e) { + updateItemsFromModel(); + } + }); + } + // Add key listener for the search fields searchFieldCategory.addKeyListener(new KeyAdapter() { @Override @@ -147,6 +178,10 @@ public class SearchableAndCategorizableComboBox extends JComboBox { addMouseListeners(); } + public SearchableAndCategorizableComboBox(Map> itemGroupMap, String placeHolderText, Component... extraCategoryWidgets) { + this(null, itemGroupMap, placeHolderText, extraCategoryWidgets); + } + private static void initColors() { updateColors(); UITheme.Theme.addUIThemeChangeListener(SearchableAndCategorizableComboBox::updateColors); @@ -169,32 +204,68 @@ public class SearchableAndCategorizableComboBox extends JComboBox { }); } - public void updateItems(Map itemGroupMap) { - this.itemGroupMap = itemGroupMap; + public void updateItems(Map> itemGroupMap) { + this.itemGroupMap = new LinkedHashMap<>(itemGroupMap); // Create a copy to avoid external modifications this.allItems = extractItemsFromMap(itemGroupMap); - setModel(new DefaultComboBoxModel<>(this.allItems)); - // Create the search field widget - this.searchFieldCategory = new PlaceholderTextField(); - this.searchFieldCategory.setPlaceholder(this.placeHolderText); - this.searchFieldSearch = new PlaceholderTextField(); + // Update the existing model instead of creating a new one + ComboBoxModel model = getModel(); + if (model instanceof MutableComboBoxModel mutableModel) { - // Create the filtered list + // Remove all existing elements + while (mutableModel.getSize() > 0) { + mutableModel.removeElementAt(0); + } + + // Add new elements + for (T item : allItems) { + mutableModel.addElement(item); + } + } else { + // If the model is not mutable, we need to set a new model + // This should be a rare case, as DefaultComboBoxModel is mutable + setModel(new DefaultComboBoxModel<>(new Vector<>(allItems))); + } + + // Recreate the search fields only if they don't exist + if (this.searchFieldCategory == null) { + this.searchFieldCategory = new PlaceholderTextField(); + this.searchFieldCategory.setPlaceholder(this.placeHolderText); + } + if (this.searchFieldSearch == null) { + this.searchFieldSearch = new PlaceholderTextField(); + } + + // Recreate the filtered list and popups this.filteredList = createFilteredList(); - - // Create the different popups this.categoryPopup = createCategoryPopup(); this.searchPopup = createSearchPopup(); this.searchPopup.setPreferredSize(this.categoryPopup.getPreferredSize()); + + revalidate(); + repaint(); } - private T[] extractItemsFromMap(Map itemGroupMap) { - Set uniqueItems = new HashSet<>(); // Use a Set to ensure uniqueness - for (E group : itemGroupMap.keySet()) { - uniqueItems.addAll(Arrays.asList(itemGroupMap.get(group))); + private void updateItemsFromModel() { + ComboBoxModel model = getModel(); + Map> newGroupMap = new HashMap<>(); + + for (int i = 0; i < model.getSize(); i++) { + T item = model.getElementAt(i); + G group = item.getGroup(); + newGroupMap.computeIfAbsent(group, k -> new ArrayList<>()).add(item); } - ArrayList items = new ArrayList<>(uniqueItems); - return items.toArray((T[]) new Object[0]); + + Map> newItemGroupMap = new HashMap<>(newGroupMap); + updateItems(newItemGroupMap); + } + + private List extractItemsFromMap(Map> itemGroupMap) { + Set uniqueItems = new HashSet<>(); // Use a Set to ensure uniqueness + for (G group : itemGroupMap.keySet()) { + uniqueItems.addAll(itemGroupMap.get(group)); + } + return new ArrayList<>(uniqueItems); } private JPopupMenu createCategoryPopup() { @@ -205,18 +276,18 @@ public class SearchableAndCategorizableComboBox extends JComboBox { menu.addSeparator(); // Separator between search field and menu items // Fill the menu with the groups - for (E group : itemGroupMap.keySet()) { + for (G group : itemGroupMap.keySet()) { JMenu groupMenu = new JMenu(group.toString()) { @Override public void paintComponent(Graphics g) { super.paintComponent(g); // If the group contains the selected item, draw a checkbox if (containsSelectedItem(group, (T) SearchableAndCategorizableComboBox.this.getSelectedItem())) { - g.drawString("\u2713", 5, getHeight() - 5); // Unicode for checked checkbox + g.drawString(CHECKMARK, 5, getHeight() - 5); // Unicode for checked checkbox } } }; - T[] itemsForGroup = itemGroupMap.get(group); + List itemsForGroup = itemGroupMap.get(group); if (itemsForGroup != null) { for (T item : itemsForGroup) { @@ -226,7 +297,7 @@ public class SearchableAndCategorizableComboBox extends JComboBox { super.paintComponent(g); // If the item is currently selected, draw a checkmark before it if (item == SearchableAndCategorizableComboBox.this.getSelectedItem()) { - g.drawString("\u2713 ", 5, getHeight() - 5); + g.drawString(CHECKMARK + " ", 5, getHeight() - 5); } } }; @@ -279,7 +350,7 @@ public class SearchableAndCategorizableComboBox extends JComboBox { // If the item is currently selected, draw a checkmark before it if (item == getSelectedItem()) { - itemName = "\u2713 " + itemName; + itemName = CHECKMARK + " " + itemName; } if (itemName.toLowerCase().contains(searchFieldSearch.getText().toLowerCase())) { @@ -358,16 +429,8 @@ public class SearchableAndCategorizableComboBox extends JComboBox { searchPopup.setVisible(false); } - private boolean containsSelectedItem(E group, T targetItem) { - T[] itemsInGroup = itemGroupMap.get(group); - if (itemsInGroup != null) { - for (T item : itemsInGroup) { - if (item == targetItem) { - return true; - } - } - } - return false; + private boolean containsSelectedItem(G group, T targetItem) { + return targetItem != null && targetItem.getGroup().equals(group); } private void filter(String text) {