fix checkbox togging with mouse (do not access `checkbox.getX()` as these components...
[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.containers.BidirectionalMap;
6 import com.intellij.util.ui.UIUtil;
7 import org.jetbrains.annotations.NotNull;
8 import org.jetbrains.annotations.Nullable;
9
10 import javax.swing.*;
11 import javax.swing.border.Border;
12 import javax.swing.border.EmptyBorder;
13 import javax.swing.plaf.basic.BasicRadioButtonUI;
14 import java.awt.*;
15 import java.awt.event.KeyAdapter;
16 import java.awt.event.KeyEvent;
17 import java.awt.event.MouseEvent;
18 import java.util.List;
19 import java.util.Map;
20
21 /**
22  * @author oleg
23  */
24 public class CheckBoxList<T> extends JBList {
25   private static final int DEFAULT_CHECK_BOX_WIDTH = 20;
26   private CheckBoxListListener checkBoxListListener;
27   private final BidirectionalMap<T, JCheckBox> myItemMap = new BidirectionalMap<T, JCheckBox>();
28
29   public CheckBoxList(final CheckBoxListListener checkBoxListListener) {
30     this(new DefaultListModel(), checkBoxListListener);
31   }
32   public CheckBoxList(final DefaultListModel dataModel, final CheckBoxListListener checkBoxListListener) {
33     this(dataModel);
34     setCheckBoxListListener(checkBoxListListener);
35   }
36
37   public CheckBoxList() {
38     this(new DefaultListModel());
39   }
40
41   public CheckBoxList(final DefaultListModel dataModel) {
42     super();
43     //noinspection unchecked
44     setModel(dataModel);
45     final CellRenderer cellRenderer = new CellRenderer();
46     setCellRenderer(cellRenderer);
47     setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
48     setBorder(BorderFactory.createEtchedBorder());
49     addKeyListener(new KeyAdapter() {
50       @Override
51       public void keyTyped(KeyEvent e) {
52         if (e.getKeyChar() == ' ') {
53           for (int index : getSelectedIndices()) {
54             if (index >= 0) {
55               JCheckBox checkbox = (JCheckBox)getModel().getElementAt(index);
56               setSelected(checkbox, index);
57             }
58           }
59         }
60       }
61     });
62     new ClickListener() {
63       @Override
64       public boolean onClick(@NotNull MouseEvent e, int clickCount) {
65         if (isEnabled()) {
66           int index = locationToIndex(e.getPoint());
67
68           if (index != -1) {
69             JCheckBox checkbox = (JCheckBox)getModel().getElementAt(index);
70             int iconArea;
71             try {
72               iconArea = checkbox.getMargin().left +
73                          ((BasicRadioButtonUI)checkbox.getUI()).getDefaultIcon().getIconWidth() +
74                          checkbox.getIconTextGap();
75             }
76             catch (ClassCastException c) {
77               iconArea = DEFAULT_CHECK_BOX_WIDTH;
78             }
79             int checkboxX = e.getX() - cellRenderer.getLeftBorderThickness();
80             if (checkboxX >= 0 && checkboxX < iconArea) {
81               setSelected(checkbox, index);
82               return true;
83             }
84           }
85         }
86         return false;
87       }
88     }.installOn(this);
89   }
90
91   public void setStringItems(final Map<String, Boolean> items) {
92     clear();
93     for (Map.Entry<String, Boolean> entry : items.entrySet()) {
94       //noinspection unchecked
95       addItem((T)entry.getKey(), entry.getKey(), entry.getValue());
96     }
97   }
98
99   public void setItems(final List<T> items, @Nullable Function<T, String> converter) {
100     clear();
101     for (T item : items) {
102       String text = converter != null ? converter.fun(item) : item.toString();
103       addItem(item, text, false);
104     }
105   }
106
107   public void addItem(T item, String text, boolean selected) {
108     JCheckBox checkBox = new JCheckBox(text, selected);
109     myItemMap.put(item, checkBox);
110     //noinspection unchecked
111     ((DefaultListModel) getModel()).addElement(checkBox);
112   }
113
114   public void updateItem(@NotNull T oldItem, @NotNull T newItem, @NotNull String newText) {
115     JCheckBox checkBox = myItemMap.remove(oldItem);
116     myItemMap.put(newItem, checkBox);
117     checkBox.setText(newText);
118     DefaultListModel model = (DefaultListModel)getModel();
119     int ind = model.indexOf(checkBox);
120     if (ind >= 0) {
121       model.set(ind, checkBox); // to fire contentsChanged event
122     }
123   }
124
125   @Nullable
126   public T getItemAt(int index) {
127     JCheckBox checkBox = (JCheckBox)getModel().getElementAt(index);
128     List<T> value = myItemMap.getKeysByValue(checkBox);
129     return value == null || value.isEmpty() ? null : value.get(0);
130   }
131
132   public void clear() {
133     ((DefaultListModel) getModel()).clear();
134     myItemMap.clear();
135   }
136
137   public boolean isItemSelected(int index) {
138     return ((JCheckBox)getModel().getElementAt(index)).isSelected();
139   }
140
141   public boolean isItemSelected(T item) {
142     JCheckBox checkBox = myItemMap.get(item);
143     return checkBox != null && checkBox.isSelected();
144   }
145
146   public void setItemSelected(T item, boolean selected) {
147     JCheckBox checkBox = myItemMap.get(item);
148     if (checkBox != null) {
149       checkBox.setSelected(selected);
150     }
151   }
152
153   private void setSelected(JCheckBox checkbox, int index) {
154     boolean value = !checkbox.isSelected();
155     checkbox.setSelected(value);
156     repaint();
157
158     // fire change notification in case if we've already initialized model
159     final ListModel model = getModel();
160     if (model instanceof DefaultListModel) {
161       //noinspection unchecked
162       ((DefaultListModel)model).setElementAt(getModel().getElementAt(index), index);
163     }
164
165     if (checkBoxListListener != null) {
166       checkBoxListListener.checkBoxSelectionChanged(index, value);
167     }
168   }
169
170   public void setCheckBoxListListener(CheckBoxListListener checkBoxListListener) {
171     this.checkBoxListListener = checkBoxListListener;
172   }
173
174   protected JComponent adjustRendering(JComponent rootComponent,
175                                        final JCheckBox checkBox,
176                                        int index,
177                                        final boolean selected,
178                                        final boolean hasFocus) {
179     return rootComponent;
180   }
181
182   private class CellRenderer implements ListCellRenderer {
183     private final Border mySelectedBorder;
184     private final Border myBorder;
185     private final int myLeftBorderThickness;
186
187     private CellRenderer() {
188       mySelectedBorder = UIManager.getBorder("List.focusCellHighlightBorder");
189       final Insets borderInsets = mySelectedBorder.getBorderInsets(new JCheckBox());
190       myBorder = new EmptyBorder(borderInsets);
191       myLeftBorderThickness = borderInsets.left;
192     }
193
194     @Override
195     public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
196       JCheckBox checkbox = (JCheckBox)value;
197
198       Color textColor = getForeground(isSelected);
199       Color backgroundColor = getBackground(isSelected);
200       Font font = getFont();
201
202       boolean shouldAdjustColors = !UIUtil.isUnderNimbusLookAndFeel();
203
204       if (shouldAdjustColors) {
205         checkbox.setBackground(backgroundColor);
206         checkbox.setForeground(textColor);
207       }
208
209       checkbox.setEnabled(isEnabled());
210       checkbox.setFont(font);
211       checkbox.setFocusPainted(false);
212
213       String auxText = getSecondaryText(index);
214
215       JComponent rootComponent;
216       if (auxText != null) {
217         JPanel panel = new JPanel(new BorderLayout());
218
219         checkbox.setBorderPainted(false);
220         panel.add(checkbox, BorderLayout.LINE_START);
221
222         JLabel infoLabel = new JLabel(auxText, SwingConstants.RIGHT);
223         infoLabel.setBorder(new EmptyBorder(0, 0, 0, checkbox.getInsets().left));
224         infoLabel.setFont(font);
225         panel.add(infoLabel, BorderLayout.CENTER);
226
227         if (shouldAdjustColors) {
228           panel.setBackground(backgroundColor);
229           infoLabel.setForeground(isSelected ? textColor : JBColor.GRAY);
230           infoLabel.setBackground(backgroundColor);
231         }
232
233         rootComponent = panel;
234       }
235       else {
236         checkbox.setBorderPainted(true);
237         rootComponent = checkbox;
238       }
239
240       rootComponent.setBorder(isSelected ? mySelectedBorder : myBorder);
241
242       rootComponent = adjustRendering(rootComponent, checkbox, index, isSelected, cellHasFocus);
243
244       return rootComponent;
245     }
246
247     private int getLeftBorderThickness() {
248       return myLeftBorderThickness;
249     }
250   }
251
252   @Nullable
253   protected String getSecondaryText(int index) {
254     return null;
255   }
256
257   protected Color getBackground(final boolean isSelected) {
258       return isSelected ? getSelectionBackground() : getBackground();
259     }
260
261   protected Color getForeground(final boolean isSelected) {
262     return isSelected ? getSelectionForeground() : getForeground();
263   }
264 }