All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openbp.swing.components.popupfield.JSelectionPopup Maven / Gradle / Ivy

/*
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package org.openbp.swing.components.popupfield;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ListDataListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;

/**
 * Popup for the {@link JSelectionField} class.
 *
 * @author Heiko Erhardt
 */
public class JSelectionPopup extends JPopupMenu
{
	/** Associated selection field */
	private JSelectionField selectionField;

	/** List component that holds the selectable items */
	private JList list;

	/** Scroll bar that scrolls the list */
	private JScrollPane scroller;

	/**
	 * Default constructor.
	 *
	 * @param selectionField Associated selection field
	 */
	public JSelectionPopup(JSelectionField selectionField)
	{
		setInvoker(selectionField);

		setLightWeightPopupEnabled(true);
		setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
		setBorderPainted(true);
		setBorder(BorderFactory.createLineBorder(Color.black));
		setOpaque(false);
		setDoubleBuffered(true);

		this.selectionField = selectionField;

		// Create and configure the components of the popup
		createComponent();

		add(scroller);
	}

	/**
	 * Creates the list component that is displayed in the popup and its enclosing scrollbar.
	 */
	protected void createComponent()
	{
		// Make a new list using a model that accesses the selection field items
		list = new JList(createSelectionFieldListModel());
		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

		// Set the ui properties of the list
		JTextField textField = selectionField.getTextField();
		list.setFont(textField.getFont());
		list.setForeground(UIManager.getColor("TextField.foreground"));
		list.setBackground(UIManager.getColor("TextField.background"));
		list.setSelectionForeground(UIManager.getColor("ComboBox.selectionForeground"));
		list.setSelectionBackground(UIManager.getColor("ComboBox.selectionBackground"));

		// Attach handlers
		MouseInputListener listMouseHandler = new ListMouseHandler();
		list.addMouseListener(listMouseHandler);
		list.addMouseMotionListener(listMouseHandler);
		list.addKeyListener(new ListKeyHandler());
		list.addFocusListener(new ListFocusHandler());

		// Create the scrollbar (vertical scroll only)
		scroller = new JScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		scroller.setRequestFocusEnabled(false);
		scroller.getVerticalScrollBar().setRequestFocusEnabled(false);
	}

	/**
	 * Creates a list model that maps to the items of the selection field.
	 * @return The new model
	 */
	protected ListModel createSelectionFieldListModel()
	{
		return new ListModel()
		{
			public int getSize()
			{
				return selectionField.getNumberOfItems();
			}

			public Object getElementAt(int index)
			{
				Object o = selectionField.getItemTextAt(index);
				if (o == null)
					o = " ";
				return o;
			}

			public void addListDataListener(ListDataListener l)
			{
			}

			public void removeListDataListener(ListDataListener l)
			{
			}
		};
	}

	/**
	 * Gets the list component that holds the selectable items.
	 */
	public JList getList()
	{
		return list;
	}

	//////////////////////////////////////////////////
	// @@ JComponent overrides
	//////////////////////////////////////////////////

	/**
	 * Requests the focus for this component.
	 */
	public void requestFocus()
	{
		// Delegate the focus to the text field
		list.requestFocus();
	}

	/**
	 * Overridden to unconditionally return false.
	 */
	public boolean isFocusTraversable()
	{
		return false;
	}

	//////////////////////////////////////////////////
	// @@ Show/hide methods
	//////////////////////////////////////////////////

	/**
	 * Shows the popup.
	 */
	public void showPopup()
	{
		selectionField.firePopup(PopupEvent.POPUP_OPENING);

		int x = 0;
		int y = selectionField.getHeight();
		int w = selectionField.getWidth() - 2;
		int h = computePopupHeightForRowCount(selectionField.getMaximumRowCount());
		if (selectionField.label != null)
		{
			int labelWidth = selectionField.label.getWidth();
			x += labelWidth;
			w -= labelWidth;
		}

		Rectangle popupBounds = computePopupBounds(x, y, w, h);
		Dimension popupSize = popupBounds.getSize();
		scroller.setMaximumSize(popupSize);
		scroller.setPreferredSize(popupSize);
		scroller.setMinimumSize(popupSize);

		list.invalidate();
		updateListIndex();

		show(selectionField, popupBounds.x, popupBounds.y);

		list.requestFocus();

		selectionField.firePopup(PopupEvent.POPUP_OPENED);
	}

	/**
	 * Cancels the popup menu without accepting the selection.
	 */
	public void cancelPopup()
	{
		list.setSelectedIndex(-1);
		if (isVisible())
		{
			hidePopup();
		}
	}

	/**
	 * Hides the popup.
	 */
	public void hidePopup()
	{
		selectionField.notifyPopupClosed();

		selectionField.firePopup(PopupEvent.POPUP_CLOSING);

		setVisible(false);

		SwingUtilities.invokeLater(new Runnable()
		{
			public void run()
			{
				selectionField.repaint();
				selectionField.requestFocus();
			}
		});

		selectionField.firePopup(PopupEvent.POPUP_CLOSED);
	}

	/**
	 * Transfers the list selection to the selection field and closes the popup.
	 */
	void acceptSelection()
	{
		updateSelectionFieldIndex();
		cancelPopup();
	}

	/**
	 * Updates the current selection of the list according to the current selection field index.
	 */
	void updateListIndex()
	{
		int selectedIndex = selectionField.getSelectedIndex();

		if (selectedIndex == -1)
		{
			list.clearSelection();
		}
		else
		{
			list.setSelectedIndex(selectedIndex);
		}
		list.ensureIndexIsVisible(selectedIndex);
	}

	/**
	 * Updates the selection field index according to the current selection of the list.
	 */
	void updateSelectionFieldIndex()
	{
		int index = list.getSelectedIndex();
		if (index >= 0)
		{
			selectionField.setSelectedIndex(index);
			selectionField.fireActionPerformed();
		}
	}

	//////////////////////////////////////////////////
	// @@ List event listeners
	//////////////////////////////////////////////////

	protected class ListMouseHandler extends MouseInputAdapter
	{
		/**
		 * Updates the selection field index and hides the popup when the mouse is released in the list.
		 */
		public void mouseReleased(MouseEvent e)
		{
			acceptSelection();
		}

		/**
		 * Changes the selected item as you move the mouse over the list.
		 * The selection change is not committed to the model, this is for user feedback only.
		 */
		public void mouseMoved(MouseEvent e)
		{
			Point location = e.getPoint();

			Rectangle r = new Rectangle();
			list.computeVisibleRect(r);
			if (r.contains(location))
			{
				updateListBoxSelectionForEvent(e);
			}
		}
	}

	public class ListKeyHandler extends KeyAdapter
	{
		/**
		 * This listener watches for the spacebar or enter being pressed and shows/hides the popup accordingly.
		 */
		public void keyPressed(KeyEvent e)
		{
			if (e.getKeyCode() == KeyEvent.VK_SPACE || e.getKeyCode() == KeyEvent.VK_ENTER)
			{
				if (isVisible())
				{
					acceptSelection();
				}
				else
				{
					showPopup();
				}
				e.consume();
				return;
			}

			if (e.getKeyCode() == KeyEvent.VK_ESCAPE)
			{
				cancelPopup();
				e.consume();
				return;
			}

			if (e.getKeyCode() == KeyEvent.VK_TAB)
			{
				acceptSelection();
				e.consume();
				return;
			}
		}
	}

	public class ListFocusHandler extends FocusAdapter
	{
		/**
		 * @see java.awt.event.FocusListener#focusLost(java.awt.event.FocusEvent)
		 */
		public void focusLost(FocusEvent e)
		{
			cancelPopup();
		}
	}

	//////////////////////////////////////////////////
	// @@ Helpers
	//////////////////////////////////////////////////

	/**
	 * Calculates the height of the popup given a maximum row count.
	 * If there are less items in the selection field, the popup will be smaller.
	 *
	 * @param maxRowCount Maximum row count
	 * @return The height (default of 100 if computation fails for any reason)
	 */
	protected int computePopupHeightForRowCount(int maxRowCount)
	{
		ListCellRenderer renderer = list.getCellRenderer();
		int n = selectionField.getNumberOfItems();
		int rowCount = Math.min(maxRowCount, n);
		int height = 0;

		for (int i = 0; i < rowCount; ++i)
		{
			Object value = list.getModel().getElementAt(i);
			if (value == null)
				value = "x";
			Component c = renderer.getListCellRendererComponent(list, value, i, false, false);
			height += c.getPreferredSize().height;
		}

		return height == 0 ? 100 : height;
	}

	/**
	 * Computes the bounds of the popup.
	 * Positions the popup beneath the selection field.
	 *
	 * @param px X coordinate (relative to the selection field)
	 * @param py Y coordinate (relative to the selection field)
	 * @param pw Width
	 * @param ph Height
	 * @return The bounds (relative to the selection field)
	 */
	protected Rectangle computePopupBounds(int px, int py, int pw, int ph)
	{
		// The popup must not exceed the container boundaries.
		Component container = SwingUtilities.getAncestorOfClass(JScrollPane.class, selectionField);
		if (container == null)
		{
			container = SwingUtilities.getWindowAncestor(selectionField);
		}
		Point containerLocation = container.getLocationOnScreen();
		int minY = containerLocation.y;
		int maxY = containerLocation.y + container.getHeight();

		Point selectionLocation = selectionField.getLocationOnScreen();

		// Check if the popup fits below the field
		int endy = selectionLocation.y + py + ph;
		if (endy < maxY)
		{
			return new Rectangle(px, py, pw, ph);
		}

		// Check if the popup fits above the field
		int starty = selectionLocation.y + py + ph - 1;
		if (starty > minY)
		{
			return new Rectangle(px, -ph - 1, pw, ph);
		}

		int diffBelow = maxY - py;
		int diffAbove = selectionLocation.y - minY - 1;
		if (diffBelow > diffAbove)
		{
			return new Rectangle(px, py, pw, diffBelow);
		}
		return new Rectangle(px, -diffAbove - 1, pw, diffAbove);
	}

	/**
	 * Given a mouse event, changes the list selection to the list item below the mouse.
	 *
	 * @param e Mouse event (refering to list coordinates)
	 */
	protected void updateListBoxSelectionForEvent(MouseEvent e)
	{
		Point location = e.getPoint();
		if (list == null)
			return;

		int index = list.locationToIndex(location);
		if (index == -1)
		{
			if (location.y < 0)
				index = 0;
			else
				index = selectionField.getNumberOfItems() - 1;
		}

		if (list.getSelectedIndex() != index)
		{
			list.setSelectedIndex(index);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy