1 package com.intellij.ui;
3 import com.intellij.ui.components.JBList;
4 import com.intellij.util.Function;
5 import com.intellij.util.ObjectUtils;
6 import com.intellij.util.containers.BidirectionalMap;
7 import com.intellij.util.ui.JBUI;
8 import com.intellij.util.ui.UIUtil;
9 import org.jetbrains.annotations.NotNull;
10 import org.jetbrains.annotations.Nullable;
13 import javax.swing.border.Border;
14 import javax.swing.border.EmptyBorder;
15 import javax.swing.plaf.basic.BasicRadioButtonUI;
17 import java.awt.event.KeyAdapter;
18 import java.awt.event.KeyEvent;
19 import java.awt.event.MouseEvent;
20 import java.util.List;
26 public class CheckBoxList<T> extends JBList {
27 private final CellRenderer myCellRenderer;
28 private CheckBoxListListener checkBoxListListener;
29 private final BidirectionalMap<T, JCheckBox> myItemMap = new BidirectionalMap<T, JCheckBox>();
31 public CheckBoxList(final CheckBoxListListener checkBoxListListener) {
32 this(new DefaultListModel(), checkBoxListListener);
35 public CheckBoxList(final DefaultListModel dataModel, final CheckBoxListListener checkBoxListListener) {
37 setCheckBoxListListener(checkBoxListListener);
40 public CheckBoxList() {
41 this(new DefaultListModel());
44 public CheckBoxList(final DefaultListModel dataModel) {
46 //noinspection unchecked
48 myCellRenderer = new CellRenderer();
49 setCellRenderer(myCellRenderer);
50 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
51 setBorder(BorderFactory.createEtchedBorder());
52 addKeyListener(new KeyAdapter() {
54 public void keyTyped(KeyEvent e) {
55 if (e.getKeyChar() == ' ') {
56 for (int index : getSelectedIndices()) {
58 JCheckBox checkbox = (JCheckBox)getModel().getElementAt(index);
59 setSelected(checkbox, index);
67 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
69 int index = locationToIndex(e.getPoint());
71 JCheckBox checkBox = getCheckBoxAt(index);
72 Rectangle bounds = getCellBounds(index, index);
76 Point p = findPointRelativeToCheckBox(e.getX() - bounds.x, e.getY() - bounds.y, checkBox, index);
78 Dimension dim = getCheckBoxDimension(checkBox);
79 if (p.x >= 0 && p.x < dim.width && p.y >= 0 && p.y < dim.height) {
80 setSelected(checkBox, index);
92 private static Dimension getCheckBoxDimension(@NotNull JCheckBox checkBox) {
94 BasicRadioButtonUI ui = ObjectUtils.tryCast(checkBox.getUI(), BasicRadioButtonUI.class);
96 icon = ui.getDefaultIcon();
99 // com.intellij.ide.ui.laf.darcula.ui.DarculaCheckBoxUI.getDefaultIcon()
100 icon = JBUI.emptyIcon(20);
102 Insets margin = checkBox.getMargin();
103 return new Dimension(margin.left + icon.getIconWidth(), margin.top + icon.getIconHeight());
107 * Find point relative to the checkbox. Performs lightweight calculations suitable for default rendering.
109 * @param x x-coordinate relative to the rendered component
110 * @param y y-coordinate relative to the rendered component
111 * @param checkBox JCheckBox instance
112 * @param index The list cell index
113 * @return A point relative to the checkbox or null, if it's outside of the checkbox.
116 protected Point findPointRelativeToCheckBox(int x, int y, @NotNull JCheckBox checkBox, int index) {
117 int cx = x - myCellRenderer.getBorderInsets().left;
118 int cy = y - myCellRenderer.getBorderInsets().top;
119 return cx >= 0 && cy >= 0 ? new Point(cx, cy) : null;
123 * Find point relative to the checkbox. Performs heavy calculations suitable for adjusted rendering
124 * where the checkbox location can be arbitrary inside the rendered component.
126 * @param x x-coordinate relative to the rendered component
127 * @param y y-coordinate relative to the rendered component
128 * @param checkBox JCheckBox instance
129 * @param index The list cell index
130 * @return A point relative to the checkbox or null, if it's outside of the checkbox.
133 protected Point findPointRelativeToCheckBoxWithAdjustedRendering(int x, int y, @NotNull JCheckBox checkBox, int index) {
134 boolean selected = isSelectedIndex(index);
135 boolean hasFocus = hasFocus();
136 Component component = myCellRenderer.getListCellRendererComponent(this, checkBox, index, selected, hasFocus);
137 Rectangle bounds = getCellBounds(index, index);
140 component.setBounds(bounds);
141 if (component instanceof Container) {
142 Container c = (Container)component;
143 Component found = c.findComponentAt(x, y);
144 if (found == checkBox) {
145 Point checkBoxLocation = getChildLocationRelativeToAncestor(component, checkBox);
146 if (checkBoxLocation != null) {
147 return new Point(x - checkBoxLocation.x, y - checkBoxLocation.y);
155 private static Point getChildLocationRelativeToAncestor(@NotNull Component ancestor, @NotNull Component child) {
158 while (c != null && c != ancestor) {
159 Point p = c.getLocation();
162 c = child.getParent();
164 return c == ancestor ? new Point(dx, dy) : null;
169 private JCheckBox getCheckBoxAt(int index) {
170 return (JCheckBox)getModel().getElementAt(index);
173 public void setStringItems(final Map<String, Boolean> items) {
175 for (Map.Entry<String, Boolean> entry : items.entrySet()) {
176 //noinspection unchecked
177 addItem((T)entry.getKey(), entry.getKey(), entry.getValue());
181 public void setItems(final List<T> items, @Nullable Function<T, String> converter) {
183 for (T item : items) {
184 String text = converter != null ? converter.fun(item) : item.toString();
185 addItem(item, text, false);
189 public void addItem(T item, String text, boolean selected) {
190 JCheckBox checkBox = new JCheckBox(text, selected);
191 myItemMap.put(item, checkBox);
192 //noinspection unchecked
193 ((DefaultListModel)getModel()).addElement(checkBox);
196 public void updateItem(@NotNull T oldItem, @NotNull T newItem, @NotNull String newText) {
197 JCheckBox checkBox = myItemMap.remove(oldItem);
198 myItemMap.put(newItem, checkBox);
199 checkBox.setText(newText);
200 DefaultListModel model = (DefaultListModel)getModel();
201 int ind = model.indexOf(checkBox);
203 model.set(ind, checkBox); // to fire contentsChanged event
208 public T getItemAt(int index) {
209 JCheckBox checkBox = (JCheckBox)getModel().getElementAt(index);
210 List<T> value = myItemMap.getKeysByValue(checkBox);
211 return value == null || value.isEmpty() ? null : value.get(0);
214 public void clear() {
215 ((DefaultListModel)getModel()).clear();
219 public boolean isItemSelected(int index) {
220 return ((JCheckBox)getModel().getElementAt(index)).isSelected();
223 public boolean isItemSelected(T item) {
224 JCheckBox checkBox = myItemMap.get(item);
225 return checkBox != null && checkBox.isSelected();
228 public void setItemSelected(T item, boolean selected) {
229 JCheckBox checkBox = myItemMap.get(item);
230 if (checkBox != null) {
231 checkBox.setSelected(selected);
235 private void setSelected(JCheckBox checkbox, int index) {
236 boolean value = !checkbox.isSelected();
237 checkbox.setSelected(value);
240 // fire change notification in case if we've already initialized model
241 final ListModel model = getModel();
242 if (model instanceof DefaultListModel) {
243 //noinspection unchecked
244 ((DefaultListModel)model).setElementAt(getModel().getElementAt(index), index);
247 if (checkBoxListListener != null) {
248 checkBoxListListener.checkBoxSelectionChanged(index, value);
252 public void setCheckBoxListListener(CheckBoxListListener checkBoxListListener) {
253 this.checkBoxListListener = checkBoxListListener;
256 protected JComponent adjustRendering(JComponent rootComponent,
257 final JCheckBox checkBox,
259 final boolean selected,
260 final boolean hasFocus) {
261 return rootComponent;
264 private class CellRenderer implements ListCellRenderer {
265 private final Border mySelectedBorder;
266 private final Border myBorder;
267 private final Insets myBorderInsets;
269 private CellRenderer() {
270 mySelectedBorder = UIManager.getBorder("List.focusCellHighlightBorder");
271 myBorderInsets = mySelectedBorder.getBorderInsets(new JCheckBox());
272 myBorder = new EmptyBorder(myBorderInsets);
276 public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
277 JCheckBox checkbox = (JCheckBox)value;
279 Color textColor = getForeground(isSelected);
280 Color backgroundColor = getBackground(isSelected);
281 Font font = getFont();
283 boolean shouldAdjustColors = !UIUtil.isUnderNimbusLookAndFeel();
285 if (shouldAdjustColors) {
286 checkbox.setBackground(backgroundColor);
287 checkbox.setForeground(textColor);
290 checkbox.setEnabled(isEnabled());
291 checkbox.setFont(font);
292 checkbox.setFocusPainted(false);
294 String auxText = getSecondaryText(index);
296 JComponent rootComponent;
297 if (auxText != null) {
298 JPanel panel = new JPanel(new BorderLayout());
300 checkbox.setBorderPainted(false);
301 panel.add(checkbox, BorderLayout.LINE_START);
303 JLabel infoLabel = new JLabel(auxText, SwingConstants.RIGHT);
304 infoLabel.setBorder(new EmptyBorder(0, 0, 0, checkbox.getInsets().left));
305 infoLabel.setFont(font);
306 panel.add(infoLabel, BorderLayout.CENTER);
308 if (shouldAdjustColors) {
309 panel.setBackground(backgroundColor);
310 infoLabel.setForeground(isSelected ? textColor : JBColor.GRAY);
311 infoLabel.setBackground(backgroundColor);
314 rootComponent = panel;
317 checkbox.setBorderPainted(true);
318 rootComponent = checkbox;
321 rootComponent.setBorder(isSelected ? mySelectedBorder : myBorder);
323 rootComponent = adjustRendering(rootComponent, checkbox, index, isSelected, cellHasFocus);
325 return rootComponent;
329 private Insets getBorderInsets() {
330 return myBorderInsets;
335 protected String getSecondaryText(int index) {
339 protected Color getBackground(final boolean isSelected) {
340 return isSelected ? getSelectionBackground() : getBackground();
343 protected Color getForeground(final boolean isSelected) {
344 return isSelected ? getSelectionForeground() : getForeground();