diff --git a/core/src/net/sf/openrocket/simulation/FlightDataTypeGroup.java b/core/src/net/sf/openrocket/simulation/FlightDataTypeGroup.java index 0b4b837cb..e7ef5c916 100644 --- a/core/src/net/sf/openrocket/simulation/FlightDataTypeGroup.java +++ b/core/src/net/sf/openrocket/simulation/FlightDataTypeGroup.java @@ -54,6 +54,6 @@ public class FlightDataTypeGroup { @Override public String toString() { - return name; + return getName(); } } diff --git a/swing/src/net/sf/openrocket/gui/simulation/FlightDataComboBox.java b/swing/src/net/sf/openrocket/gui/simulation/FlightDataComboBox.java index 118c13d8d..d61ce038b 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/FlightDataComboBox.java +++ b/swing/src/net/sf/openrocket/gui/simulation/FlightDataComboBox.java @@ -1,139 +1,26 @@ package net.sf.openrocket.gui.simulation; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.UITheme; -import net.sf.openrocket.gui.widgets.PlaceholderTextField; +import net.sf.openrocket.gui.widgets.SearchableAndCategorizableComboBox; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightDataTypeGroup; import net.sf.openrocket.startup.Application; -import javax.swing.DefaultListCellRenderer; -import javax.swing.DefaultListModel; import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JMenu; -import javax.swing.JMenuItem; -import javax.swing.JPopupMenu; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.plaf.basic.BasicArrowButton; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.EventQueue; -import java.awt.Point; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; public class FlightDataComboBox extends JComboBox { private static final Translator trans = Application.getTranslator(); - private final JPopupMenu categoryPopup; - private final JPopupMenu searchPopup; - private final PlaceholderTextField searchFieldCategory; - private final PlaceholderTextField searchFieldSearch; - private final JList filteredList; - - private final FlightDataType[] allTypes; - private final Hashtable typeGroupMap; - - private int highlightedListIdx = -1; - - private static Color textSelectionBackground; - - static { - initColors(); - } - - public FlightDataComboBox(FlightDataTypeGroup[] allGroups, FlightDataType[] types) { - super(types); - setEditable(false); - - this.allTypes = types; - - initColors(); - - // Create the map of flight data group and corresponding flight data types - typeGroupMap = createFlightDataGroupMap(allGroups, types); - - // Create the search field widget - searchFieldCategory = new PlaceholderTextField(); - searchFieldCategory.setPlaceholder(trans.get("FlightDataComboBox.placeholder")); - searchFieldSearch = new PlaceholderTextField(); - - // Create the filtered list - filteredList = createFilteredList(); - - // Create the different popups - categoryPopup = createCategoryPopup(); - searchPopup = createSearchPopup(); - searchPopup.setPreferredSize(categoryPopup.getPreferredSize()); - - // Add key listener for the search fields - searchFieldCategory.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - overrideActionKeys(e); - } - - public void keyTyped(KeyEvent e) { - EventQueue.invokeLater(() -> { - String text = searchFieldCategory.getText(); - highlightedListIdx = 0; // Start with the first item selected - searchFieldSearch.setText(text); - if (!text.isEmpty() && !searchPopup.isVisible()) { - hideCategoryPopup(); - showSearchPopup(); - filter(text); - } - }); - } - }); - searchFieldSearch.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - overrideActionKeys(e); - } - - @Override - public void keyTyped(KeyEvent e) { - EventQueue.invokeLater(() -> { - String text = searchFieldSearch.getText(); - highlightedListIdx = 0; // Start with the first item selected - searchFieldCategory.setText(text); - if (text.isEmpty() && !categoryPopup.isVisible()) { - hideSearchPopup(); - showCategoryPopup(); - } - filter(text); - }); - } - }); - - // Override the mouse listeners to use our custom popup - for (MouseListener mouseListener : getMouseListeners()) { - removeMouseListener(mouseListener); - } - - addMouseListeners(); - } - - private static void initColors() { - updateColors(); - UITheme.Theme.addUIThemeChangeListener(FlightDataComboBox::updateColors); - } - - private static void updateColors() { - textSelectionBackground = GUIUtil.getUITheme().getTextSelectionBackgroundColor(); + public static SearchableAndCategorizableComboBox createComboBox(FlightDataTypeGroup[] allGroups, FlightDataType[] types) { + final Map typeGroupMap = createFlightDataGroupMap(allGroups, types); + return new SearchableAndCategorizableComboBox<>(typeGroupMap, trans.get("FlightDataComboBox.placeholder")); } /** @@ -142,273 +29,26 @@ public class FlightDataComboBox extends JComboBox { * @param types the types * @return the map linking the types to their groups */ - private Hashtable createFlightDataGroupMap(FlightDataTypeGroup[] groups, FlightDataType[] types) { - Hashtable map = new Hashtable<>(); - for (FlightDataTypeGroup group : groups) { - ArrayList listForGroup = new ArrayList<>(); + 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<>(); + for (FlightDataTypeGroup group : sortedGroups) { + List itemsForGroup = new ArrayList<>(); for (FlightDataType type : types) { if (type.getGroup().equals(group)) { - listForGroup.add(type); + itemsForGroup.add(type); } } - map.put(group, listForGroup.toArray(new FlightDataType[0])); + // Sort the types within each group based on priority + itemsForGroup.sort(Comparator.comparingInt(FlightDataType::getGroupPriority)); + + map.put(group, itemsForGroup.toArray(new FlightDataType[0])); } return map; } - - private JPopupMenu createCategoryPopup() { - final JPopupMenu menu = new JPopupMenu(); - - // Add the search field at the top - menu.add(searchFieldCategory); - menu.addSeparator(); // Separator between search field and menu items - - // Fill the menu with the groups - for (FlightDataTypeGroup group : typeGroupMap.keySet()) { - JMenu groupList = new JMenu(group.getName()); - FlightDataType[] typesForGroup = typeGroupMap.get(group); - - if (typesForGroup != null) { - for (FlightDataType type : typesForGroup) { - JMenuItem typeItem = new JMenuItem(type.getName()); - typeItem.addActionListener(e -> { - setSelectedItem(type); - }); - groupList.add(typeItem); - } - } - - menu.add(groupList); - } - - return menu; - } - - private JPopupMenu createSearchPopup() { - final JPopupMenu menu = new JPopupMenu(); - menu.setLayout(new BorderLayout()); - - // Add the search field at the top - menu.add(searchFieldSearch, BorderLayout.NORTH); - menu.addSeparator(); - - menu.add(new JScrollPane(filteredList)); - - return menu; - } - - private JList createFilteredList() { - JList list = new JList<>(); - list.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - FlightDataType type = (FlightDataType) value; - String typeName = type.toString(); - - if (typeName.toLowerCase().contains(searchFieldSearch.getText().toLowerCase())) { - // Use HTML to underline matching text - typeName = typeName.replaceAll("(?i)(" + searchFieldSearch.getText() + ")", "$1"); - label.setText("" + typeName + ""); - } - - // Set the hover color - if (highlightedListIdx == index || isSelected) { - label.setBackground(textSelectionBackground); - label.setOpaque(true); - } else { - label.setOpaque(false); - } - - return label; - } - }); - - list.addMouseMotionListener(new MouseAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - Point p = new Point(e.getX(),e.getY()); - int index = list.locationToIndex(p); - if (index != highlightedListIdx) { - highlightedListIdx = index; - list.repaint(); - } - } - }); - - list.addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - // Check if the event is in the final phase of change - if (!e.getValueIsAdjusting()) { - selectComboBoxItemFromFilteredList(); - } - } - }); - - return list; - } - - private void selectComboBoxItemFromFilteredList() { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - FlightDataType selectedType = filteredList.getSelectedValue(); - if (selectedType != null) { - FlightDataComboBox.this.setSelectedItem(selectedType); - // Hide the popups after selection - hideCategoryPopup(); - hideSearchPopup(); - } - } - }); - } - - private void showCategoryPopup() { - categoryPopup.show(this, 0, getHeight()); - searchFieldSearch.setText(""); - searchFieldCategory.setText(""); - } - - private void hideCategoryPopup() { - categoryPopup.setVisible(false); - } - - private void showSearchPopup() { - searchPopup.show(this, 0, getHeight()); - } - - private void hideSearchPopup() { - searchPopup.setVisible(false); - } - - private void filter(String text) { - filteredList.removeAll(); - String searchText = text.toLowerCase(); - DefaultListModel filteredModel = new DefaultListModel<>(); - - for (FlightDataType item : this.allTypes) { - if (item.toString().toLowerCase().contains(searchText)) { - filteredModel.addElement(item); - } - } - - filteredList.setModel(filteredModel); - filteredList.revalidate(); - filteredList.repaint(); - } - - private Component getArrowButton() { - for (Component child : getComponents()) { - if (child instanceof BasicArrowButton) { - return child; - } - } - return null; - } - - @Override - public void showPopup() { - // Override the default JComboBox showPopup() to do nothing - // Our custom popup will be shown by the MouseListener - } - - @Override - public boolean isPopupVisible() { - return categoryPopup.isVisible() || searchPopup.isVisible(); - } - - /** - * Override the default action keys (escape, enter, arrow keys) to do our own actions. - * @param e the key event - */ - private void overrideActionKeys(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { - hideCategoryPopup(); - hideSearchPopup(); - } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { - selectHighlightedItemInFilteredList(); - } else if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_RIGHT) { - highlightNextItemInFilteredList(); - } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_LEFT) { - highlightPreviousItemInFilteredList(); - } - } - - /** - * Select the highlighted item in the filtered list and hide the popups. - */ - private void selectHighlightedItemInFilteredList() { - if (highlightedListIdx >= filteredList.getModel().getSize() || highlightedListIdx < 0 || !searchPopup.isVisible()) { - return; - } - filteredList.setSelectedIndex(highlightedListIdx); - selectComboBoxItemFromFilteredList(); - } - - /** - * Highlight the next item in the filtered list. - */ - private void highlightNextItemInFilteredList() { - if (highlightedListIdx + 1 >= filteredList.getModel().getSize() || !searchPopup.isVisible()) { - return; - } - highlightedListIdx++; - filteredList.ensureIndexIsVisible(highlightedListIdx); - filteredList.repaint(); - } - - /** - * Highlight the previous item in the filtered list. - */ - private void highlightPreviousItemInFilteredList() { - if (highlightedListIdx <= 0 || !searchPopup.isVisible()) { - return; - } - highlightedListIdx--; - filteredList.ensureIndexIsVisible(highlightedListIdx); - filteredList.repaint(); - } - - - - /** - * Add mouse listener to widgets of the combobox to open our custom popup menu. - */ - private void addMouseListeners() { - addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - if (!isPopupVisible()) { - showCategoryPopup(); - } - } - }); - } - }); - - Component arrowButton = getArrowButton(); - if (arrowButton != null) { - for (MouseListener mouseListener : arrowButton.getMouseListeners()) { - arrowButton.removeMouseListener(mouseListener); - } - arrowButton.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - if (!isPopupVisible()) { - showCategoryPopup(); - } - } - }); - } - }); - } - } } diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java index 99148415b..d881025a5 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java @@ -488,7 +488,7 @@ public class SimulationPlotPanel extends JPanel { private final String[] POSITIONS = { AUTO_NAME, LEFT_NAME, RIGHT_NAME }; private final int index; - private JComboBox typeSelector; + private final JComboBox typeSelector; private UnitSelector unitSelector; private JComboBox axisSelector; @@ -498,7 +498,7 @@ public class SimulationPlotPanel extends JPanel { this.index = plotIndex; - typeSelector = new FlightDataComboBox(FlightDataTypeGroup.ALL_GROUPS, types); + typeSelector = FlightDataComboBox.createComboBox(FlightDataTypeGroup.ALL_GROUPS, types); typeSelector.setSelectedItem(type); typeSelector.addItemListener(new ItemListener() { @Override diff --git a/swing/src/net/sf/openrocket/gui/widgets/SearchableAndCategorizableComboBox.java b/swing/src/net/sf/openrocket/gui/widgets/SearchableAndCategorizableComboBox.java new file mode 100644 index 000000000..ac9415a2f --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/widgets/SearchableAndCategorizableComboBox.java @@ -0,0 +1,481 @@ +package net.sf.openrocket.gui.widgets; + +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.UITheme; + +import javax.swing.AbstractListModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.plaf.basic.BasicArrowButton; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.EventQueue; +import java.awt.Point; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * 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 + */ +public class SearchableAndCategorizableComboBox extends JComboBox { + + private final JPopupMenu categoryPopup; + private final JPopupMenu searchPopup; + private final PlaceholderTextField searchFieldCategory; + private final PlaceholderTextField searchFieldSearch; + private final JList filteredList; + + private final T[] allItems; + private final Map itemGroupMap; + + private int highlightedListIdx = -1; + + private static Color textSelectionBackground; + + static { + initColors(); + } + + /** + * Create a searchable and categorizable combo box. + * @param itemGroupMap the map of items and their corresponding groups + * @param placeHolderText the placeholder text for the search field (when no text is entered) + */ + public SearchableAndCategorizableComboBox(Map itemGroupMap, String placeHolderText) { + super(); + setEditable(false); + + this.itemGroupMap = itemGroupMap; + this.allItems = extractItemsFromMap(itemGroupMap); + setModel(new DefaultComboBoxModel<>(allItems)); + + initColors(); + + // Create the search field widget + searchFieldCategory = new PlaceholderTextField(); + searchFieldCategory.setPlaceholder(placeHolderText); + searchFieldSearch = new PlaceholderTextField(); + + // Create the filtered list + filteredList = createFilteredList(); + + // Create the different popups + categoryPopup = createCategoryPopup(); + searchPopup = createSearchPopup(); + searchPopup.setPreferredSize(categoryPopup.getPreferredSize()); + + // Add key listener for the search fields + searchFieldCategory.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + overrideActionKeys(e); + } + + public void keyTyped(KeyEvent e) { + EventQueue.invokeLater(() -> { + String text = searchFieldCategory.getText(); + highlightedListIdx = 0; // Start with the first item selected + searchFieldSearch.setText(text); + if (!text.isEmpty() && !searchPopup.isVisible()) { + hideCategoryPopup(); + showSearchPopup(); + filter(text); + } + }); + } + }); + searchFieldSearch.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + overrideActionKeys(e); + } + + @Override + public void keyTyped(KeyEvent e) { + EventQueue.invokeLater(() -> { + String text = searchFieldSearch.getText(); + highlightedListIdx = 0; // Start with the first item selected + searchFieldCategory.setText(text); + if (text.isEmpty() && !categoryPopup.isVisible()) { + hideSearchPopup(); + showCategoryPopup(); + } + filter(text); + }); + } + }); + + // Override the mouse listeners to use our custom popup + for (MouseListener mouseListener : getMouseListeners()) { + removeMouseListener(mouseListener); + } + + addMouseListeners(); + } + + private static void initColors() { + updateColors(); + UITheme.Theme.addUIThemeChangeListener(SearchableAndCategorizableComboBox::updateColors); + } + + private static void updateColors() { + textSelectionBackground = GUIUtil.getUITheme().getTextSelectionBackgroundColor(); + } + + 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))); + } + ArrayList items = new ArrayList<>(uniqueItems); + return items.toArray((T[]) new Object[0]); + } + + private JPopupMenu createCategoryPopup() { + final JPopupMenu menu = new JPopupMenu(); + + // Add the search field at the top + menu.add(searchFieldCategory); + menu.addSeparator(); // Separator between search field and menu items + + // Fill the menu with the groups + for (E group : itemGroupMap.keySet()) { + JMenu groupList = new JMenu(group.toString()); + T[] itemsForGroup = itemGroupMap.get(group); + + if (itemsForGroup != null) { + for (T item : itemsForGroup) { + JMenuItem itemMenu = new JMenuItem(item.toString()); + itemMenu.addActionListener(e -> { + setSelectedItem(item); + }); + groupList.add(itemMenu); + } + } + + menu.add(groupList); + } + + return menu; + } + + private JPopupMenu createSearchPopup() { + final JPopupMenu menu = new JPopupMenu(); + menu.setLayout(new BorderLayout()); + + // Add the search field at the top + menu.add(searchFieldSearch, BorderLayout.NORTH); + menu.addSeparator(); + + menu.add(new JScrollPane(filteredList)); + + return menu; + } + + private JList createFilteredList() { + JList list = new JList<>(); // Don't fill the list with the items yet, this will be done during filtering + + list.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + T item = (T) value; + String itemName = item.toString(); + + if (itemName.toLowerCase().contains(searchFieldSearch.getText().toLowerCase())) { + // Use HTML to underline matching text + itemName = itemName.replaceAll("(?i)(" + searchFieldSearch.getText() + ")", "$1"); + label.setText("" + itemName + ""); + } + + // Set the hover color + if (highlightedListIdx == index || isSelected) { + label.setBackground(textSelectionBackground); + label.setOpaque(true); + } else { + label.setOpaque(false); + } + + return label; + } + }); + + list.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + Point p = new Point(e.getX(),e.getY()); + int index = list.locationToIndex(p); + if (index != highlightedListIdx) { + highlightedListIdx = index; + list.repaint(); + } + } + }); + + list.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + // Check if the event is in the final phase of change + if (!e.getValueIsAdjusting()) { + selectComboBoxItemFromFilteredList(); + } + } + }); + + return list; + } + + private void selectComboBoxItemFromFilteredList() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + T selectedItem = filteredList.getSelectedValue(); + if (selectedItem != null) { + SearchableAndCategorizableComboBox.this.setSelectedItem(selectedItem); + // Hide the popups after selection + hideCategoryPopup(); + hideSearchPopup(); + } + } + }); + } + + private void showCategoryPopup() { + categoryPopup.show(this, 0, getHeight()); + searchFieldSearch.setText(""); + searchFieldCategory.setText(""); + } + + private void hideCategoryPopup() { + categoryPopup.setVisible(false); + } + + private void showSearchPopup() { + searchPopup.show(this, 0, getHeight()); + } + + private void hideSearchPopup() { + searchPopup.setVisible(false); + } + + private void filter(String text) { + filteredList.removeAll(); + String searchText = text.toLowerCase(); + SortedListModel filteredModel = new SortedListModel<>(); + + for (T item : this.allItems) { + if (item.toString().toLowerCase().contains(searchText)) { + filteredModel.add(item); + } + } + + filteredList.setModel(filteredModel); + filteredList.revalidate(); + filteredList.repaint(); + } + + private Component getArrowButton() { + for (Component child : getComponents()) { + if (child instanceof BasicArrowButton) { + return child; + } + } + return null; + } + + @Override + public void showPopup() { + // Override the default JComboBox showPopup() to do nothing + // Our custom popup will be shown by the MouseListener + } + + @Override + public boolean isPopupVisible() { + return categoryPopup.isVisible() || searchPopup.isVisible(); + } + + /** + * Override the default action keys (escape, enter, arrow keys) to do our own actions. + * @param e the key event + */ + private void overrideActionKeys(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + hideCategoryPopup(); + hideSearchPopup(); + } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { + selectHighlightedItemInFilteredList(); + } else if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_RIGHT) { + highlightNextItemInFilteredList(); + } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_LEFT) { + highlightPreviousItemInFilteredList(); + } + } + + /** + * Select the highlighted item in the filtered list and hide the popups. + */ + private void selectHighlightedItemInFilteredList() { + if (highlightedListIdx >= filteredList.getModel().getSize() || highlightedListIdx < 0 || !searchPopup.isVisible()) { + return; + } + filteredList.setSelectedIndex(highlightedListIdx); + selectComboBoxItemFromFilteredList(); + } + + /** + * Highlight the next item in the filtered list. + */ + private void highlightNextItemInFilteredList() { + if (highlightedListIdx + 1 >= filteredList.getModel().getSize() || !searchPopup.isVisible()) { + return; + } + highlightedListIdx++; + filteredList.ensureIndexIsVisible(highlightedListIdx); + filteredList.repaint(); + } + + /** + * Highlight the previous item in the filtered list. + */ + private void highlightPreviousItemInFilteredList() { + if (highlightedListIdx <= 0 || !searchPopup.isVisible()) { + return; + } + highlightedListIdx--; + filteredList.ensureIndexIsVisible(highlightedListIdx); + filteredList.repaint(); + } + + + + /** + * Add mouse listener to widgets of the combobox to open our custom popup menu. + */ + private void addMouseListeners() { + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (!isPopupVisible()) { + showCategoryPopup(); + } + } + }); + } + }); + + Component arrowButton = getArrowButton(); + if (arrowButton != null) { + for (MouseListener mouseListener : arrowButton.getMouseListeners()) { + arrowButton.removeMouseListener(mouseListener); + } + arrowButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (!isPopupVisible()) { + showCategoryPopup(); + } + } + }); + } + }); + } + } + + private static class SortedListModel extends AbstractListModel { + private final SortedSet model; + + public SortedListModel() { + Comparator alphabeticalComparator = new Comparator() { + @Override + public int compare(T o1, T o2) { + return o1.toString().compareToIgnoreCase(o2.toString()); + } + }; + + model = new TreeSet<>(alphabeticalComparator); + } + + public int getSize() { + return model.size(); + } + + public T getElementAt(int index) { + return (T) model.toArray()[index]; + } + + public void add(T element) { + if (model.add(element)) { + fireContentsChanged(this, 0, getSize()); + } + } + public void addAll(T[] elements) { + Collection c = Arrays.asList(elements); + model.addAll(c); + fireContentsChanged(this, 0, getSize()); + } + + public void clear() { + model.clear(); + fireContentsChanged(this, 0, getSize()); + } + + public boolean contains(T element) { + return model.contains(element); + } + + public T firstElement() { + return model.first(); + } + + public Iterator iterator() { + return model.iterator(); + } + + public T lastElement() { + return model.last(); + } + + public boolean removeElement(T element) { + boolean removed = model.remove(element); + if (removed) { + fireContentsChanged(this, 0, getSize()); + } + return removed; + } + } + +} +