/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.framework.uitools; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import javax.accessibility.Accessible; import javax.accessibility.AccessibleContext; import javax.accessibility.AccessibleRole; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.ButtonModel; import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.ListModel; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.eclipse.persistence.tools.workbench.framework.resources.DefaultResourceRepository; import org.eclipse.persistence.tools.workbench.uitools.app.CollectionListValueModelAdapter; import org.eclipse.persistence.tools.workbench.uitools.app.CollectionValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.ListValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.swing.ListModelAdapter; import org.eclipse.persistence.tools.workbench.uitools.cell.AdaptableListCellRenderer; import org.eclipse.persistence.tools.workbench.uitools.cell.CellRendererAdapter; import org.eclipse.persistence.tools.workbench.uitools.swing.CompositeIcon; /** * A <code>CheckList</code> is a regular list that adds the capability to * select/deselect items in the list. When using a mouse, a click on the check * icon will toggle the selection. The selection can also be changed using the * space bar. * <p> * The selected items are stored into the <code>ListSelectionModel</code> passed * to this list. * <p> * Here the default layout: * <pre> * _______________________ * | |^| * | x Item1 | | * | o Item2 ||| * | x Item3 ||| * | ... | | * | |v| * -----------------------</pre> * * @version 10.1.3 * @author Pascal Filion */ public class CheckList extends AccessibleTitledPanel { /** * This label is used to tell JAWS the check selection has changed, this * seems to be the only way JAWS knows when the selection has changed. */ JLabel accessibleLabel; /** * Holds the list so we can give it the focus. */ EditableCheckList listBox; /** * The model used to store the selected items. */ ListSelectionModel selectionModel; /** * Creates a new <code>CheckList</code>. */ private CheckList() { super(new BorderLayout()); } /** * Creates a new <code>CheckList</code> and uses a default {@link LabelDecorator}. * * @param itemHolder The holder of the items to be shown in the list * @param selectionModel The model where the selected items are stored */ public CheckList(CollectionValueModel itemHolder, ListSelectionModel selectionModel) { this(itemHolder, selectionModel, CellRendererAdapter.DEFAULT_CELL_RENDERER_ADAPTER); } /** * Creates a new <code>CheckList</code>. * * @param itemHolder The holder of the items to be shown in the list * @param selectionModel The model where the selected items are stored * @param labelDecorator The {@link LabelDecorator} used to decorate the * items contained in the given collection holder */ public CheckList(CollectionValueModel itemHolder, ListSelectionModel selectionModel, CellRendererAdapter labelDecorator) { this(new CollectionListValueModelAdapter(itemHolder), selectionModel, labelDecorator); } /** * Creates a new <code>CheckList</code> and uses a default {@link LabelDecorator}. * * @param itemHolder The holder of the items to be shown in the list * @param selectionModel The model where the selected items are stored */ public CheckList(ListValueModel itemHolder, ListSelectionModel selectionModel) { this(itemHolder, selectionModel, CellRendererAdapter.DEFAULT_CELL_RENDERER_ADAPTER); } /** * Creates a new <code>CheckList</code>. * * @param itemHolder The holder of the items to be shown in the list * @param selectionModel The model where the selected items are stored * @param labelDecorator The {@link LabelDecorator} used to decorate the * items contained in the given collection holder */ public CheckList(ListValueModel itemHolder, ListSelectionModel selectionModel, CellRendererAdapter labelDecorator) { this(); initialize(itemHolder, selectionModel, labelDecorator); } /** * Creates a <code>FocusListener</code> that will ask the list to repaint * itself in order to update the renders. Unfortunately, this is not done * automatically by Swing. * * @return A new <code>FocusListener</code> */ private FocusListener buildFocusListener() { return new FocusListener() { public void focusGained(FocusEvent e) { CheckList.this.listBox.repaint(); } public void focusLost(FocusEvent e) { CheckList.this.listBox.repaint(); } }; } /** * Creates a new <code>ListModel</code> which will keep the listener up to * date with the changes made to the given <code>ListValueModel</code>. * * @param itemHolder The holder of the items to be shown in the list * @return A new <code>ListModel</code> */ private ListModel buildListModelAdapter(ListValueModel itemHolder) { return new ListModelAdapter(itemHolder); } /** * Creates the listener reponsible to keep the UI in sync with the selection * model. * * @return A new <code>ListSelectionListener</code> */ private ListSelectionListener buildSelectionModelListener() { return new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) return; for (int index = e.getFirstIndex(); index <= e.getLastIndex(); index++) { repaintCell(index); } } }; } /** * Initializes this <code>CheckList</code>. * * @param itemHolder The holder of the items to be shown in the list * @param selectionModel The model where the selected items are stored * @param cellRendererAdapter The {@link CellRendererAdapter} used to * decorate the items contained in the given collection holder * @exception NullPointerException The item holder, the selectionModel or the * <code>CellRendererAdapter</code> can't be <code>null</code> */ protected void initialize(ListValueModel itemHolder, ListSelectionModel listSelectionModel, CellRendererAdapter cellRendererAdapter) { if ((itemHolder == null) || (listSelectionModel == null) || (cellRendererAdapter == null)) throw new NullPointerException("ListValueModel, ListSelectionModel, CellRendererAdapter cannot be null"); this.selectionModel = listSelectionModel; listSelectionModel.addListSelectionListener(buildSelectionModelListener()); // Create the list this.listBox = new EditableCheckList(buildListModelAdapter(itemHolder)); this.listBox.addFocusListener(buildFocusListener()); this.listBox.setCellRenderer(new CheckBoxCellRenderer(cellRendererAdapter)); add(new JScrollPane(this.listBox), BorderLayout.CENTER); installEditing(); // This status bar pane is required to tell JAWS when the check selection // is changed, this seems to be the only way to make it work add(new StatusBarPane(), BorderLayout.PAGE_START); } /** * Installs the necessary handlers on the given list to mimic an real cell * editor. */ private void installEditing() { // Install a mouse listener which will select/unselect when clicking on // the check icon MouseHandler mouseHandler = new MouseHandler(); this.listBox.addMouseListener(mouseHandler); this.listBox.addMouseMotionListener(mouseHandler); // Map the space bar to the key in the ActionMap InputMap inputMap = (InputMap) UIManager.get("List.focusInputMap"); inputMap.put(KeyStroke.getKeyStroke("SPACE"), "pressed"); inputMap.put(KeyStroke.getKeyStroke("released SPACE"), "released"); // Map the actions ActionMap actionMap = this.listBox.getActionMap(); actionMap.put("pressed", new PressedAction()); actionMap.put("released", new ReleasedAction()); } /** * Determines whether the <code>AccessibleContext</code> of this * <code>CheckList</code> has been created or not. * * @return <code>true<code> if {@link accessibleContext} is not <code>null</code>, * otherwise <code>false<code> */ boolean isAccessibleContextInitialized() { return (this.accessibleContext != null); } /** * Repaints the list by optimizing its painting by only painting the cell * located at the given index. * * @param index The index of the cell to repaint */ void repaintCell(int index) { Rectangle bounds = this.listBox.getCellBounds(index, index); this.listBox.repaint(bounds); } /** * Redirects the focus selection to the actual component of this widget that * can really accept the focus, which is the list itself. */ public void requestFocus() { this.listBox.requestFocus(); } /** * Redirects the focus selection to the actual component of this widget that * can really accept the focus, which is the list itself. */ public boolean requestFocus(boolean temporary) { return this.listBox.requestFocus(temporary); } /** * Redirects the focus selection to the actual component of this widget that * can really accept the focus, which is the list itself. */ public boolean requestFocusInWindow() { return this.listBox.requestFocusInWindow(); } /** * Redirects the focus selection to the actual component of this widget that * can really accept the focus, which is the list itself. */ protected boolean requestFocusInWindow(boolean temporary) { return this.listBox.requestFocusInWindow(temporary); } /** * Updates the active descendant of the list in order for a screen reader to * be notified of the change. * * @param index The index of the item to be either selected or unselected, * the index has to be a valid index */ private void updateActiveDescendant(int index) { if (this.accessibleContext != null) { AccessibleContext accessible = this.listBox.getAccessibleContext(); Accessible accessibleChild = accessible.getAccessibleChild(index); this.accessibleLabel.setText(accessibleChild.getAccessibleContext().getAccessibleName()); } } /** * Updates the interval of selected items in the {@link #selectionModel} * based on the given index. If the item is selected, then it will become * unselected and vice versa. * * @param index The index of the item to be either selected or unselected, * the index has to be a valid index */ void updateSelection(int index) { updateSelection(new int[] { index }); } /** * Updates the selected items in the {@link #selectionModel} based on the * given list of indices. If the item is selected, then it will become * unselected and vice versa. * * @param indices The array of indices of new unselected/selected items, * the indices have to be valid */ void updateSelection(int[] indices) { // Retrieve the count of items that are already selected // in the given list of indices int selectedCount = 0; for (int index = indices.length; --index >= 0;) { if (this.selectionModel.isSelectedIndex(indices[index])) { selectedCount++; } } // Determine the action to perform: select all or unselect all boolean select = (indices.length != selectedCount); // Update the selection interval for (int index = 0; index < indices.length; index++) { int anIndex = indices[index]; if (select) this.selectionModel.addSelectionInterval(anIndex, anIndex); else this.selectionModel.removeSelectionInterval(anIndex, anIndex); } // Update the accessible list's children for (int index = 0; index < indices.length; index++) { updateActiveDescendant(indices[index]); } } /** * This check box is used by {@link CheckList.ListCellRenderer} to render * items as a checked/unchecked item. */ private class CheckBox extends JCheckBox { /** * Creates a new <code>CheckBox</code>. */ private CheckBox() { super(); initialize(); } /** * Initializes the UI of this check box to be properly rendered in a list. */ private void initialize() { setRolloverEnabled(true); setBorderPainted(true); } /** * Sets the default check box icon, which does not override the check icon. * * @param icon The new default icon or <code>null</code> to remove the old * icon */ public void setIcon(Icon icon) { if (UIManager.getLookAndFeel().getID().equals("GTK")) return; if (icon != null) { super.setIcon(new CompositeIcon(UIManager.getIcon("CheckBox.icon"), 3, icon)); } else { super.setIcon(UIManager.getIcon("CheckBox.icon")); } } } /** * This <code>ListCellRenderer</code> is responsible to decorate values from * the list model using a check box. */ private class CheckBoxCellRenderer extends AdaptableListCellRenderer { /** * Specified the index of the item that should be shown as armed, which * means the mouse has been pressed on the item but not released yet. */ int armedIndex; /** * Specified the index of the item that should be shown as pressed, which * means the mouse has been pressed on the item but not released yet. */ int pressedIndex; /** * Specified the index of the item that should be shown that the mouse is * over it. */ int rolloverIndex; /** * Creates a new <code>ListCellRenderer</code>. * * @param cellRenderer The {@link CellRendererAdapter} used to decorate * the items of a list */ CheckBoxCellRenderer(CellRendererAdapter adapter) { super(adapter); this.pressedIndex = -1; this.armedIndex = -1; this.rolloverIndex = -1; } /** * @see javax.swing.ListCellRenderer#getListCellRendererComponent(javax.swing.JList, Object, int, boolean, boolean) */ public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean cellHasFocus) { // Make sure the AccessibleContext is initialized so that the // accessible name can be updated if (CheckList.this.isAccessibleContextInitialized()) { this.getAccessibleContext(); } super.getListCellRendererComponent(list, value, index, selected, cellHasFocus); CheckBox checkBox = new CheckBox(); updateCheckBoxUI(checkBox, selected); updateCheckBoxVisual(checkBox); updateCheckBoxButtonModel(checkBox, index); return checkBox; } /** * Updates the states of the check label's button model based on the * selection state of the item rendered. * * @param checkBox The actual component returned by this * <code>ListCellRenderer</code>. * @param index The index of the item to be rendered */ private void updateCheckBoxButtonModel(CheckBox checkBox, int index) { ButtonModel model = checkBox.getModel(); model.setPressed(this.pressedIndex == index); model.setArmed(this.armedIndex == index); model.setRollover(this.rolloverIndex == index); checkBox.setSelected(CheckList.this.selectionModel.isSelectedIndex(index)); // This needs to be done after setPressed } /** * Updates the UI part of the check label. * * @param checkBox The actual component returned by this * <code>ListCellRenderer</code>. * @param selected Specifies whether the item rendered is selected or not */ private void updateCheckBoxUI(CheckBox checkBox, boolean selected) { checkBox.setHorizontalAlignment(getHorizontalAlignment()); checkBox.setHorizontalTextPosition(getHorizontalTextPosition()); checkBox.setVerticalAlignment(getVerticalAlignment()); checkBox.setVerticalTextPosition(getVerticalTextPosition()); checkBox.setComponentOrientation(getComponentOrientation()); checkBox.setFont(getFont()); checkBox.setEnabled(isEnabled()); checkBox.setOpaque(isOpaque()); checkBox.setBorder(getBorder()); if (!CheckList.this.listBox.hasFocus() && selected) { checkBox.setForeground(CheckList.this.listBox.getForeground()); checkBox.setBackground(UIManager.getColor("Panel.background")); } else { checkBox.setForeground(getForeground()); checkBox.setBackground(getBackground()); } } /** * Updates the icon and text of the check label. * * @param checkBox The actual component returned by this * <code>ListCellRenderer</code>. */ private void updateCheckBoxVisual(CheckBox checkBox) { checkBox.setIcon(getIcon()); checkBox.setText(getText()); // Only update the accessible name if a screen reader is running if (isAccessibleContextInitialized()) { checkBox.getAccessibleContext().setAccessibleName(this.accessibleContext.getAccessibleName()); } } } /** * This customized <code>JList</code> upgrade the accessiblity to support * checked items. */ private class EditableCheckList extends SwingComponentFactory.AccessibleList { /** * Creates a new <code>EditableCheckList</code>. * * @param model The <code>ListModel</code> containing the items of the list */ EditableCheckList(ListModel model) { super(model); } /** * Returns the <code>AccessibleContext</code> associated with this * <code>EditableCheckList</code>. * * @return An <code>AccessibleEditableCheckList</code> that serves as the * <code>AccessibleContext</code> of this <code>EditableCheckList</code> */ public AccessibleContext getAccessibleContext() { if (this.accessibleContext == null) { this.accessibleContext = new AccessibleEditableCheckList(); } return this.accessibleContext; } public boolean requestFocusInWindow(boolean temporary) { return super.requestFocusInWindow(temporary); } /** * The <code>AccessibleContext</code> for this <code>EditableCheckList</code>. */ protected class AccessibleEditableCheckList extends AccessibleAccessibleList implements ListSelectionListener { String ACCESSIBLE_CHECKBOX_CHECKED; String ACCESSIBLE_CHECKBOX_NOT_CHECKED; protected AccessibleEditableCheckList() { super(); initialize(); } public Accessible getAccessibleAt(Point location) { int index = locationToIndex(location); return getAccessibleChild(index); } public Accessible getAccessibleChild(int index) { if ((index < 0) || (index >= getModel().getSize())) return null; return new AccessibleEditableCheckListChild(EditableCheckList.this, index); } protected void initialize() { DefaultResourceRepository repository = new DefaultResourceRepository(UIToolsResourceBundle.class); this.ACCESSIBLE_CHECKBOX_CHECKED = " " + repository.getString("ACCESSIBLE_CHECKLIST_CHECKBOX_CHECKED"); this.ACCESSIBLE_CHECKBOX_NOT_CHECKED = " " + repository.getString("ACCESSIBLE_CHECKLIST_CHECKBOX_NOT_CHECKED"); } protected class AccessibleEditableCheckListChild extends AccessibleJListChild { private final int index; protected AccessibleEditableCheckListChild(JList list, int index) { super(list, index); this.index = index; } public String getAccessibleName() { String name = super.getAccessibleName(); if (CheckList.this.selectionModel.isSelectedIndex(this.index)) { name += AccessibleEditableCheckList.this.ACCESSIBLE_CHECKBOX_CHECKED; } else { name += AccessibleEditableCheckList.this.ACCESSIBLE_CHECKBOX_NOT_CHECKED; } return name; } } } } /** * This handler is responsible to update the check icon displayed on screen. * Certain look and feel show a different state of the check icon based on * rollover, pressed, etc. This class also update the selection on mouse * clicked. */ private class MouseHandler extends MouseAdapter implements MouseMotionListener { private int currentPressedIndex = -1; private int locationToIndex(Point location) { int iconWidth = SwingTools.checkBoxIconWidth(); int index = CheckList.this.listBox.locationToIndex(location); Rectangle bounds = CheckList.this.listBox.getCellBounds(index, index); if (!CheckList.this.listBox.getComponentOrientation().isLeftToRight()) { bounds.x += (bounds.width - iconWidth); } bounds.width = iconWidth; return bounds.contains(location.x, location.y) ? index : -1; } public void mouseDragged(MouseEvent e) { mouseMoved(e); } public void mouseExited(MouseEvent e) { CheckBoxCellRenderer renderer = (CheckBoxCellRenderer) CheckList.this.listBox.getCellRenderer(); int oldRolloverIndex = renderer.rolloverIndex; if (oldRolloverIndex != -1) { renderer.armedIndex = -1; renderer.rolloverIndex = -1; repaintCell(oldRolloverIndex); } } public void mouseMoved(MouseEvent e) { if (CheckList.this.listBox.getModel().getSize() == 0) { return; } Point location = e.getPoint(); // Get the index of the item where the mouse is int index = CheckList.this.listBox.locationToIndex(location); // Get the cell bounds of the cell over Rectangle bounds = CheckList.this.listBox.getCellBounds(index, index); // The mouse is not moved on an item if (!bounds.contains(location)) index = -1; CheckBoxCellRenderer renderer = (CheckBoxCellRenderer) CheckList.this.listBox.getCellRenderer(); int oldRolloverIndex = renderer.rolloverIndex; // Nothing to repaint if (oldRolloverIndex == index) return; // Update the properties renderer.rolloverIndex = index; renderer.armedIndex = (index == this.currentPressedIndex) ? index : -1; // Paint the new rollover cell if (index != -1) { repaintCell(index); } // Repaint the old rollover cell if (oldRolloverIndex != -1) { repaintCell(oldRolloverIndex); } } public void mousePressed(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) return; int index = locationToIndex(e.getPoint()); if (index != -1) { CheckBoxCellRenderer renderer = (CheckBoxCellRenderer) CheckList.this.listBox.getCellRenderer(); renderer.armedIndex = index; renderer.pressedIndex = index; this.currentPressedIndex = index; repaintCell(index); } } public void mouseReleased(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) return; CheckBoxCellRenderer renderer = (CheckBoxCellRenderer) CheckList.this.listBox.getCellRenderer(); // Get the index of the item where the mouse is Point location = e.getPoint(); int newIndex = CheckList.this.listBox.locationToIndex(location); Rectangle bounds = CheckList.this.listBox.getCellBounds(newIndex, newIndex); // The mouse was released outside of an item if (!bounds.contains(location)) { newIndex = -1; } // The mouse was released outside of the check icon or not on the item // where the mouse was pressed if ((this.currentPressedIndex != newIndex) || (locationToIndex(e.getPoint()) == -1)) { this.currentPressedIndex = -1; } renderer.armedIndex = this.currentPressedIndex; renderer.pressedIndex = -1; renderer.rolloverIndex = newIndex; // Only change the selection if the mouse was released on the check icon if (this.currentPressedIndex != -1) { updateSelection(this.currentPressedIndex); } this.currentPressedIndex = -1; // The mouse is released on another cell, show it with rollover if (newIndex != -1) { repaintCell(newIndex); } } } /** * This action is responsible to update the renderer by updating the pressed * and armed indices. */ private class PressedAction extends AbstractAction { public void actionPerformed(ActionEvent e) { int[] indices = CheckList.this.listBox.getSelectedIndices(); // Nothing to be updated if (indices.length != 1) return; // Update the renderer properties CheckBoxCellRenderer renderer = (CheckBoxCellRenderer) CheckList.this.listBox.getCellRenderer(); renderer.armedIndex = indices[0]; renderer.pressedIndex = indices[0]; // Update the UI repaintCell(indices[0]); } } /** * This action is responsible to update the renderer and the selection. */ private class ReleasedAction extends AbstractAction { public void actionPerformed(ActionEvent e) { int[] selectedIndices = CheckList.this.listBox.getSelectedIndices(); if (selectedIndices.length > 0) { CheckBoxCellRenderer renderer = (CheckBoxCellRenderer) CheckList.this.listBox.getCellRenderer(); renderer.armedIndex = -1; renderer.pressedIndex = -1; updateSelection(selectedIndices); } } } /** * This panel takes care to support accessibility regarding the check * selection change, which seems to not be supported through JList. */ private class StatusBarPane extends JPanel { public StatusBarPane() { super(new BorderLayout()); } /** * Creates the status bar that is required for JAWS to say the accessible * label's text. */ private void createStatusBar() { CheckList.this.accessibleLabel = new AccessibleLabel(); CheckList.this.accessibleLabel.setVisible(false); StatusBar statusBar = new StatusBar(); statusBar.setVisible(false); statusBar.add(CheckList.this.accessibleLabel); add(statusBar, BorderLayout.CENTER); validate(); } /** * Returns the <code>AccessibleContext</code> associated with this * <code>ContentPane</code>. * * @return An <code>AccessibleContentPane</code> that serves as the * <code>AccessibleContext</code> of this <code>ContentPane</code> */ public AccessibleContext getAccessibleContext() { if (this.accessibleContext == null) { this.accessibleContext = new AccessibleStatusBarPane(); createStatusBar(); } return this.accessibleContext; } /** * This <code>JLabel</code> is intended to fire an event that will ask * JAWS to read the description. */ private class AccessibleLabel extends JLabel { public void setText(String text) { String oldText = getText(); super.setText(text); if (this.accessibleContext != null) { this.accessibleContext.firePropertyChange(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, oldText, text); } } } /** * The <code>AccessibleContext</code> of this pane. */ protected class AccessibleStatusBarPane extends AccessibleJPanel { // nothing here... } /** * This <code>StatusBar</code> is required for JAWS to read the * accessible label when a new text has been set. */ private class StatusBar extends JPanel { public AccessibleContext getAccessibleContext() { if (this.accessibleContext == null) { this.accessibleContext = new AccessibleStatusBar(); } return this.accessibleContext; } protected class AccessibleStatusBar extends AccessibleJPanel { public AccessibleRole getAccessibleRole() { return AccessibleRole.STATUS_BAR; } } } } }