fix checkbox toggling with mouse for custom CheckBoxList implementations (e.g. one...
[idea/community.git] / platform / platform-api / src / com / intellij / ui / CheckBoxList.java
1 package com.intellij.ui;
2
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;
11
12 import javax.swing.*;
13 import javax.swing.border.Border;
14 import javax.swing.border.EmptyBorder;
15 import javax.swing.plaf.basic.BasicRadioButtonUI;
16 import java.awt.*;
17 import java.awt.event.KeyAdapter;
18 import java.awt.event.KeyEvent;
19 import java.awt.event.MouseEvent;
20 import java.util.List;
21 import java.util.Map;
22
23 /**
24  * @author oleg
25  */
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>();
30
31   public CheckBoxList(final CheckBoxListListener checkBoxListListener) {
32     this(new DefaultListModel(), checkBoxListListener);
33   }
34
35   public CheckBoxList(final DefaultListModel dataModel, final CheckBoxListListener checkBoxListListener) {
36     this(dataModel);
37     setCheckBoxListListener(checkBoxListListener);
38   }
39
40   public CheckBoxList() {
41     this(new DefaultListModel());
42   }
43
44   public CheckBoxList(final DefaultListModel dataModel) {
45     super();
46     //noinspection unchecked
47     setModel(dataModel);
48     myCellRenderer = new CellRenderer();
49     setCellRenderer(myCellRenderer);
50     setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
51     setBorder(BorderFactory.createEtchedBorder());
52     addKeyListener(new KeyAdapter() {
53       @Override
54       public void keyTyped(KeyEvent e) {
55         if (e.getKeyChar() == ' ') {
56           for (int index : getSelectedIndices()) {
57             if (index >= 0) {
58               JCheckBox checkbox = (JCheckBox)getModel().getElementAt(index);
59               setSelected(checkbox, index);
60             }
61           }
62         }
63       }
64     });
65     new ClickListener() {
66       @Override
67       public boolean onClick(@NotNull MouseEvent e, int clickCount) {
68         if (isEnabled()) {
69           int index = locationToIndex(e.getPoint());
70           if (index != -1) {
71             JCheckBox checkBox = getCheckBoxAt(index);
72             Rectangle bounds = getCellBounds(index, index);
73             if (bounds == null) {
74               return false;
75             }
76             Point p = findPointRelativeToCheckBox(e.getX() - bounds.x, e.getY() - bounds.y, checkBox, index);
77             if (p != null) {
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);
81                 return true;
82               }
83             }
84           }
85         }
86         return false;
87       }
88     }.installOn(this);
89   }
90
91   @NotNull
92   private static Dimension getCheckBoxDimension(@NotNull JCheckBox checkBox) {
93     Icon icon = null;
94     BasicRadioButtonUI ui = ObjectUtils.tryCast(checkBox.getUI(), BasicRadioButtonUI.class);
95     if (ui != null) {
96       icon = ui.getDefaultIcon();
97     }
98     if (icon == null) {
99       // com.intellij.ide.ui.laf.darcula.ui.DarculaCheckBoxUI.getDefaultIcon()
100       icon = JBUI.emptyIcon(20);
101     }
102     Insets margin = checkBox.getMargin();
103     return new Dimension(margin.left + icon.getIconWidth(), margin.top + icon.getIconHeight());
104   }
105
106   /**
107    * Find point relative to the checkbox. Performs lightweight calculations suitable for default rendering.
108
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.
114    */
115   @Nullable
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;
120   }
121
122   /**
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.
125    *
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.
131    */
132   @Nullable
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);
138     bounds.x = 0;
139     bounds.y = 0;
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);
148         }
149       }
150     }
151     return null;
152   }
153
154   @Nullable
155   private static Point getChildLocationRelativeToAncestor(@NotNull Component ancestor, @NotNull Component child) {
156     int dx = 0, dy = 0;
157     Component c = child;
158     while (c != null && c != ancestor) {
159       Point p = c.getLocation();
160       dx += p.x;
161       dy += p.y;
162       c = child.getParent();
163     }
164     return c == ancestor ? new Point(dx, dy) : null;
165   }
166
167
168   @NotNull
169   private JCheckBox getCheckBoxAt(int index) {
170     return (JCheckBox)getModel().getElementAt(index);
171   }
172
173   public void setStringItems(final Map<String, Boolean> items) {
174     clear();
175     for (Map.Entry<String, Boolean> entry : items.entrySet()) {
176       //noinspection unchecked
177       addItem((T)entry.getKey(), entry.getKey(), entry.getValue());
178     }
179   }
180
181   public void setItems(final List<T> items, @Nullable Function<T, String> converter) {
182     clear();
183     for (T item : items) {
184       String text = converter != null ? converter.fun(item) : item.toString();
185       addItem(item, text, false);
186     }
187   }
188
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);
194   }
195
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);
202     if (ind >= 0) {
203       model.set(ind, checkBox); // to fire contentsChanged event
204     }
205   }
206
207   @Nullable
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);
212   }
213
214   public void clear() {
215     ((DefaultListModel)getModel()).clear();
216     myItemMap.clear();
217   }
218
219   public boolean isItemSelected(int index) {
220     return ((JCheckBox)getModel().getElementAt(index)).isSelected();
221   }
222
223   public boolean isItemSelected(T item) {
224     JCheckBox checkBox = myItemMap.get(item);
225     return checkBox != null && checkBox.isSelected();
226   }
227
228   public void setItemSelected(T item, boolean selected) {
229     JCheckBox checkBox = myItemMap.get(item);
230     if (checkBox != null) {
231       checkBox.setSelected(selected);
232     }
233   }
234
235   private void setSelected(JCheckBox checkbox, int index) {
236     boolean value = !checkbox.isSelected();
237     checkbox.setSelected(value);
238     repaint();
239
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);
245     }
246
247     if (checkBoxListListener != null) {
248       checkBoxListListener.checkBoxSelectionChanged(index, value);
249     }
250   }
251
252   public void setCheckBoxListListener(CheckBoxListListener checkBoxListListener) {
253     this.checkBoxListListener = checkBoxListListener;
254   }
255
256   protected JComponent adjustRendering(JComponent rootComponent,
257                                        final JCheckBox checkBox,
258                                        int index,
259                                        final boolean selected,
260                                        final boolean hasFocus) {
261     return rootComponent;
262   }
263
264   private class CellRenderer implements ListCellRenderer {
265     private final Border mySelectedBorder;
266     private final Border myBorder;
267     private final Insets myBorderInsets;
268
269     private CellRenderer() {
270       mySelectedBorder = UIManager.getBorder("List.focusCellHighlightBorder");
271       myBorderInsets = mySelectedBorder.getBorderInsets(new JCheckBox());
272       myBorder = new EmptyBorder(myBorderInsets);
273     }
274
275     @Override
276     public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
277       JCheckBox checkbox = (JCheckBox)value;
278
279       Color textColor = getForeground(isSelected);
280       Color backgroundColor = getBackground(isSelected);
281       Font font = getFont();
282
283       boolean shouldAdjustColors = !UIUtil.isUnderNimbusLookAndFeel();
284
285       if (shouldAdjustColors) {
286         checkbox.setBackground(backgroundColor);
287         checkbox.setForeground(textColor);
288       }
289
290       checkbox.setEnabled(isEnabled());
291       checkbox.setFont(font);
292       checkbox.setFocusPainted(false);
293
294       String auxText = getSecondaryText(index);
295
296       JComponent rootComponent;
297       if (auxText != null) {
298         JPanel panel = new JPanel(new BorderLayout());
299
300         checkbox.setBorderPainted(false);
301         panel.add(checkbox, BorderLayout.LINE_START);
302
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);
307
308         if (shouldAdjustColors) {
309           panel.setBackground(backgroundColor);
310           infoLabel.setForeground(isSelected ? textColor : JBColor.GRAY);
311           infoLabel.setBackground(backgroundColor);
312         }
313
314         rootComponent = panel;
315       }
316       else {
317         checkbox.setBorderPainted(true);
318         rootComponent = checkbox;
319       }
320
321       rootComponent.setBorder(isSelected ? mySelectedBorder : myBorder);
322
323       rootComponent = adjustRendering(rootComponent, checkbox, index, isSelected, cellHasFocus);
324
325       return rootComponent;
326     }
327
328     @NotNull
329     private Insets getBorderInsets() {
330       return myBorderInsets;
331     }
332   }
333
334   @Nullable
335   protected String getSecondaryText(int index) {
336     return null;
337   }
338
339   protected Color getBackground(final boolean isSelected) {
340     return isSelected ? getSelectionBackground() : getBackground();
341   }
342
343   protected Color getForeground(final boolean isSelected) {
344     return isSelected ? getSelectionForeground() : getForeground();
345   }
346 }