Refactor SearchableAndCategorizableComboBox to use Group and Groupable interface
This commit is contained in:
parent
691f79fe3c
commit
e3ce3ac7dd
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
9
core/src/main/java/info/openrocket/core/util/Group.java
Normal file
9
core/src/main/java/info/openrocket/core/util/Group.java
Normal 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)
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user