// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.widgets;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.accessibility.Accessible;
import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JTextField;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.text.JTextComponent;
import org.openstreetmap.josm.gui.util.GuiHelper;
/**
* Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br>
* This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
* @param <E> the type of the elements of this combo box
*
* @since 5429 (creation)
* @since 7015 (generics for Java 7)
*/
public class JosmComboBox<E> extends JComboBox<E> {
/**
* Creates a <code>JosmComboBox</code> with a default data model.
* The default data model is an empty list of objects.
* Use <code>addItem</code> to add items. By default the first item
* in the data model becomes selected.
*
* @see DefaultComboBoxModel
*/
public JosmComboBox() {
init(null);
}
/**
* Creates a <code>JosmComboBox</code> with a default data model and
* the specified prototype display value.
* The default data model is an empty list of objects.
* Use <code>addItem</code> to add items. By default the first item
* in the data model becomes selected.
*
* @param prototypeDisplayValue the <code>Object</code> used to compute
* the maximum number of elements to be displayed at once before
* displaying a scroll bar
*
* @see DefaultComboBoxModel
* @since 5450
*/
public JosmComboBox(E prototypeDisplayValue) {
init(prototypeDisplayValue);
}
/**
* Creates a <code>JosmComboBox</code> that takes its items from an
* existing <code>ComboBoxModel</code>. Since the
* <code>ComboBoxModel</code> is provided, a combo box created using
* this constructor does not create a default combo box model and
* may impact how the insert, remove and add methods behave.
*
* @param aModel the <code>ComboBoxModel</code> that provides the
* displayed list of items
* @see DefaultComboBoxModel
*/
public JosmComboBox(ComboBoxModel<E> aModel) {
super(aModel);
List<E> list = new ArrayList<>(aModel.getSize());
for (int i = 0; i < aModel.getSize(); i++) {
list.add(aModel.getElementAt(i));
}
init(findPrototypeDisplayValue(list));
}
/**
* Creates a <code>JosmComboBox</code> that contains the elements
* in the specified array. By default the first item in the array
* (and therefore the data model) becomes selected.
*
* @param items an array of objects to insert into the combo box
* @see DefaultComboBoxModel
*/
public JosmComboBox(E[] items) {
super(items);
init(findPrototypeDisplayValue(Arrays.asList(items)));
}
/**
* Returns the editor component
* @return the editor component
* @see ComboBoxEditor#getEditorComponent()
* @since 9484
*/
public JTextField getEditorComponent() {
return (JTextField) getEditor().getEditorComponent();
}
/**
* Finds the prototype display value to use among the given possible candidates.
* @param possibleValues The possible candidates that will be iterated.
* @return The value that needs the largest display height on screen.
* @since 5558
*/
protected final E findPrototypeDisplayValue(Collection<E> possibleValues) {
E result = null;
int maxHeight = -1;
if (possibleValues != null) {
// Remind old prototype to restore it later
E oldPrototype = getPrototypeDisplayValue();
// Get internal JList to directly call the renderer
@SuppressWarnings("rawtypes")
JList list = getList();
try {
// Index to give to renderer
int i = 0;
for (E value : possibleValues) {
if (value != null) {
// With a "classic" renderer, we could call setPrototypeDisplayValue(value) + getPreferredSize()
// but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1
// So we explicitely call the renderer by simulating a correct index for the current value
@SuppressWarnings("unchecked")
Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true);
if (c != null) {
// Get the real preferred size for the current value
Dimension dim = c.getPreferredSize();
if (dim.height > maxHeight) {
// Larger ? This is our new prototype
maxHeight = dim.height;
result = value;
}
}
}
i++;
}
} finally {
// Restore original prototype
setPrototypeDisplayValue(oldPrototype);
}
}
return result;
}
@SuppressWarnings("unchecked")
protected final JList<Object> getList() {
for (int i = 0; i < getUI().getAccessibleChildrenCount(this); i++) {
Accessible child = getUI().getAccessibleChild(this, i);
if (child instanceof ComboPopup) {
return ((ComboPopup) child).getList();
}
}
return null;
}
protected final void init(E prototype) {
if (prototype != null) {
setPrototypeDisplayValue(prototype);
int screenHeight = GuiHelper.getScreenSize().height;
// Compute maximum number of visible items based on the preferred size of the combo box.
// This assumes that items have the same height as the combo box, which is not granted by the look and feel
int maxsize = (screenHeight/getPreferredSize().height) / 2;
// If possible, adjust the maximum number of items with the real height of items
// It is not granted this works on every platform (tested OK on Windows)
JList<Object> list = getList();
if (list != null) {
if (!prototype.equals(list.getPrototypeCellValue())) {
list.setPrototypeCellValue(prototype);
}
int height = list.getFixedCellHeight();
if (height > 0) {
maxsize = (screenHeight/height) / 2;
}
}
setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
}
// Handle text contextual menus for editable comboboxes
ContextMenuHandler handler = new ContextMenuHandler();
addPropertyChangeListener("editable", handler);
addPropertyChangeListener("editor", handler);
}
protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener {
private JTextComponent component;
private PopupMenuLauncher launcher;
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("editable".equals(evt.getPropertyName())) {
if (evt.getNewValue().equals(Boolean.TRUE)) {
enableMenu();
} else {
disableMenu();
}
} else if ("editor".equals(evt.getPropertyName())) {
disableMenu();
if (isEditable()) {
enableMenu();
}
}
}
private void enableMenu() {
if (launcher == null && editor != null) {
Component editorComponent = editor.getEditorComponent();
if (editorComponent instanceof JTextComponent) {
component = (JTextComponent) editorComponent;
component.addMouseListener(this);
launcher = TextContextualPopupMenu.enableMenuFor(component, true);
}
}
}
private void disableMenu() {
if (launcher != null) {
TextContextualPopupMenu.disableMenuFor(component, launcher);
launcher = null;
component.removeMouseListener(this);
component = null;
}
}
@Override
public void mousePressed(MouseEvent e) {
processEvent(e);
}
@Override
public void mouseReleased(MouseEvent e) {
processEvent(e);
}
private void processEvent(MouseEvent e) {
if (launcher != null && !e.isPopupTrigger() && launcher.getMenu().isShowing()) {
launcher.getMenu().setVisible(false);
}
}
}
/**
* Reinitializes this {@link JosmComboBox} to the specified values. This may needed if a custom renderer is used.
* @param values The values displayed in the combo box.
* @since 5558
*/
public final void reinitialize(Collection<E> values) {
init(findPrototypeDisplayValue(values));
}
}