Refactor SearchableAndCategorizableComboBox to use Group and Groupable interface

This commit is contained in:
SiboVG 2024-08-07 14:19:46 +02:00
parent 691f79fe3c
commit e3ce3ac7dd
9 changed files with 142 additions and 60 deletions

View File

@ -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 <sampo.niskanen@iki.fi>
*/
public abstract class Material implements Comparable<Material> {
public abstract class Material implements Comparable<Material>, Groupable<MaterialGroup> {
private static final Translator trans = Application.getTranslator();
private static final Logger log = LoggerFactory.getLogger(Material.class);

View File

@ -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<MaterialGroup> {
public class MaterialGroup implements Comparable<MaterialGroup>, 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)

View File

@ -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 <sampo.niskanen@iki.fi>
*/
public class FlightDataType implements Comparable<FlightDataType> {
public class FlightDataType implements Comparable<FlightDataType>, Groupable<FlightDataTypeGroup> {
private static final Translator trans = Application.getTranslator();
private static final Logger log = LoggerFactory.getLogger(FlightDataType.class);

View File

@ -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<FlightDataTypeGroup> {
public class FlightDataTypeGroup implements Comparable<FlightDataTypeGroup>, Group {
private static final Translator trans = Application.getTranslator();
public static final FlightDataTypeGroup TIME = new FlightDataTypeGroup(trans.get("FlightDataTypeGroup.GROUP_TIME"), 0);

View File

@ -0,0 +1,9 @@
package info.openrocket.core.util;
/**
* Interface for objects that a <Groupable> object can be grouped into.
*/
public interface Group {
String getName();
int getPriority(); // Lower number = higher priority (can be used for sorting)
}

View File

@ -0,0 +1,9 @@
package info.openrocket.core.util;
/**
* Interface for objects that can be grouped into a <Group> object.
* @param <G> the group type
*/
public interface Groupable<G extends Group> {
G getGroup();
}

View File

@ -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<MaterialGroup, Material> createComboBox(
MaterialGroup[] allGroups, Material[] materials, Component... extraCategoryWidgets) {
final Map<MaterialGroup, Material[]> materialGroupMap = createMaterialGroupMap(allGroups, materials);
return new SearchableAndCategorizableComboBox<>(materialGroupMap,
MaterialModel mm, MaterialGroup[] allGroups, Material[] materials, Component... extraCategoryWidgets) {
final Map<MaterialGroup, List<Material>> 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<MaterialGroup, Material> comboBox,
MaterialGroup[] allGroups, Material[] materials) {
final Map<MaterialGroup, Material[]> materialGroupMap = createMaterialGroupMap(allGroups, materials);
final Map<MaterialGroup, List<Material>> 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<MaterialGroup, Material[]> createMaterialGroupMap(
private static Map<MaterialGroup, List<Material>> 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<MaterialGroup, Material[]> map = new LinkedHashMap<>();
Map<MaterialGroup, List<Material>> map = new LinkedHashMap<>();
MaterialGroup materialGroup;
for (MaterialGroup group : sortedGroups) {
List<Material> 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;
}

View File

@ -18,7 +18,7 @@ public class FlightDataComboBox extends JComboBox<FlightDataType> {
private static final Translator trans = Application.getTranslator();
public static SearchableAndCategorizableComboBox<FlightDataTypeGroup, FlightDataType> createComboBox(FlightDataTypeGroup[] allGroups, FlightDataType[] types) {
final Map<FlightDataTypeGroup, FlightDataType[]> typeGroupMap = createFlightDataGroupMap(allGroups, types);
final Map<FlightDataTypeGroup, List<FlightDataType>> typeGroupMap = createFlightDataGroupMap(allGroups, types);
return new SearchableAndCategorizableComboBox<>(typeGroupMap, trans.get("FlightDataComboBox.placeholder"));
}
@ -28,13 +28,13 @@ public class FlightDataComboBox extends JComboBox<FlightDataType> {
* @param types the types
* @return the map linking the types to their groups
*/
private static Map<FlightDataTypeGroup, FlightDataType[]> createFlightDataGroupMap(
private static Map<FlightDataTypeGroup, List<FlightDataType>> 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<FlightDataTypeGroup, FlightDataType[]> map = new LinkedHashMap<>();
Map<FlightDataTypeGroup, List<FlightDataType>> map = new LinkedHashMap<>();
for (FlightDataTypeGroup group : sortedGroups) {
List<FlightDataType> itemsForGroup = new ArrayList<>();
for (FlightDataType type : types) {
@ -45,7 +45,7 @@ public class FlightDataComboBox extends JComboBox<FlightDataType> {
// 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;

View File

@ -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 <E> The type of the group
* @param <T> The type of the items
* @param <G> The type of the group
* @param <T> The type of the groupable items
*
* @author Sibo Van Gool <sibo.vangool@hotmail.com>
*/
public class SearchableAndCategorizableComboBox<E, T> extends JComboBox<T> {
public class SearchableAndCategorizableComboBox<G extends Group, T extends Groupable<G>> extends JComboBox<T> {
private final String placeHolderText;
private JPopupMenu categoryPopup;
private JPopupMenu searchPopup;
@ -59,13 +68,15 @@ public class SearchableAndCategorizableComboBox<E, T> extends JComboBox<T> {
private final Component[] extraCategoryWidgets;
private JList<T> filteredList;
private T[] allItems;
private Map<E, T[]> itemGroupMap;
private Map<G, List<T>> itemGroupMap;
private List<T> allItems;
private int highlightedListIdx = -1;
private static Color textSelectionBackground;
private static final String CHECKMARK = "\u2713";
static {
initColors();
}
@ -76,8 +87,9 @@ public class SearchableAndCategorizableComboBox<E, T> extends JComboBox<T> {
* @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<E, T[]> itemGroupMap, String placeHolderText, Component... extraCategoryWidgets) {
super();
public SearchableAndCategorizableComboBox(ComboBoxModel<T> model, Map<G, List<T>> itemGroupMap, String placeHolderText,
Component... extraCategoryWidgets) {
super(model != null ? model : new DefaultComboBoxModel<>());
setEditable(false);
initColors();
@ -87,6 +99,25 @@ public class SearchableAndCategorizableComboBox<E, T> extends JComboBox<T> {
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<E, T> extends JComboBox<T> {
addMouseListeners();
}
public SearchableAndCategorizableComboBox(Map<G, List<T>> 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<E, T> extends JComboBox<T> {
});
}
public void updateItems(Map<E, T[]> itemGroupMap) {
this.itemGroupMap = itemGroupMap;
public void updateItems(Map<G, List<T>> 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<T> model = getModel();
if (model instanceof MutableComboBoxModel<T> 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<E, T[]> itemGroupMap) {
Set<T> 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<T> model = getModel();
Map<G, List<T>> 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<T> items = new ArrayList<>(uniqueItems);
return items.toArray((T[]) new Object[0]);
Map<G, List<T>> newItemGroupMap = new HashMap<>(newGroupMap);
updateItems(newItemGroupMap);
}
private List<T> extractItemsFromMap(Map<G, List<T>> itemGroupMap) {
Set<T> 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<E, T> extends JComboBox<T> {
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<T> itemsForGroup = itemGroupMap.get(group);
if (itemsForGroup != null) {
for (T item : itemsForGroup) {
@ -226,7 +297,7 @@ public class SearchableAndCategorizableComboBox<E, T> extends JComboBox<T> {
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<E, T> extends JComboBox<T> {
// 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<E, T> extends JComboBox<T> {
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) {