c4a5551ea219ff8ca451cc46b5801b7c7910e267
[idea/community.git] / platform / platform-api / src / com / intellij / ui / SearchTextField.java
1 /*
2  * Copyright 2000-2013 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ui;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.openapi.actionSystem.ActionManager;
20 import com.intellij.openapi.actionSystem.AnAction;
21 import com.intellij.openapi.actionSystem.CommonShortcuts;
22 import com.intellij.openapi.actionSystem.IdeActions;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.ui.JBMenuItem;
25 import com.intellij.openapi.ui.JBPopupMenu;
26 import com.intellij.openapi.ui.popup.JBPopup;
27 import com.intellij.openapi.ui.popup.JBPopupFactory;
28 import com.intellij.openapi.util.SystemInfo;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.ui.components.JBList;
31 import com.intellij.util.ui.UIUtil;
32
33 import javax.swing.*;
34 import javax.swing.border.Border;
35 import javax.swing.border.CompoundBorder;
36 import javax.swing.event.DocumentListener;
37 import java.awt.*;
38 import java.awt.event.*;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 /**
43  * @author max
44  */
45 public class SearchTextField extends JPanel {
46
47   private int myHistorySize = 5;
48   private final MyModel myModel;
49   private final TextFieldWithProcessing myTextField;
50
51   private JBPopup myPopup;
52   private JLabel myClearFieldLabel;
53   private JLabel myToggleHistoryLabel;
54   private JPopupMenu myNativeSearchPopup;
55   private JMenuItem myNoItems;
56
57   public SearchTextField() {
58     this(true);
59   }
60
61   public SearchTextField(boolean historyEnabled) {
62     super(new BorderLayout());
63
64     myModel = new MyModel();
65
66     myTextField = new TextFieldWithProcessing() {
67       @Override
68       public void processKeyEvent(final KeyEvent e) {
69         if (preprocessEventForTextField(e)) return;
70         super.processKeyEvent(e);
71       }
72
73       @Override
74       public void setBackground(final Color bg) {
75         super.setBackground(bg);
76         if (!hasIconsOutsideOfTextField()) {
77           if (myClearFieldLabel != null) {
78             myClearFieldLabel.setBackground(bg);
79           }
80         }
81         if (myToggleHistoryLabel != null) {
82           myToggleHistoryLabel.setBackground(bg);
83         }
84       }
85     };
86     myTextField.setColumns(15);
87     myTextField.addFocusListener(new FocusAdapter() {
88       @Override
89       public void focusLost(FocusEvent e) {
90         onFocusLost();
91         super.focusLost(e);
92       }
93
94       @Override
95       public void focusGained(FocusEvent e) {
96         onFocusGained();
97         super.focusGained(e);
98       }
99     });
100     add(myTextField, BorderLayout.CENTER);
101     myTextField.addKeyListener(new KeyAdapter() {
102       @Override
103       public void keyPressed(KeyEvent e) {
104         if (e.getKeyCode() == KeyEvent.VK_DOWN) {
105           if (isSearchControlUISupported() && myNativeSearchPopup != null) {
106             myNativeSearchPopup.show(myTextField, 5, myTextField.getHeight());
107           } else if (myPopup == null || !myPopup.isVisible()) {
108             showPopup();
109           }
110         }
111       }
112     });
113
114     if (isSearchControlUISupported() || UIUtil.isUnderDarcula() || UIUtil.isUnderIntelliJLaF()) {
115       myTextField.putClientProperty("JTextField.variant", "search");
116     }
117     if (isSearchControlUISupported()) {
118       if (historyEnabled) {
119         myNativeSearchPopup = new JBPopupMenu();
120         myNoItems = new JBMenuItem("No recent searches");
121         myNoItems.setEnabled(false);
122
123         updateMenu();
124         myTextField.putClientProperty("JTextField.Search.FindPopup", myNativeSearchPopup);
125       }
126     }
127     else {
128       myToggleHistoryLabel = new JLabel(AllIcons.Actions.Search);
129       myToggleHistoryLabel.setOpaque(true);
130       myToggleHistoryLabel.addMouseListener(new MouseAdapter() {
131         public void mousePressed(MouseEvent e) {
132           togglePopup();
133         }
134       });
135       if (historyEnabled) {
136         add(myToggleHistoryLabel, BorderLayout.WEST);
137       }
138
139       myClearFieldLabel = new JLabel(UIUtil.isUnderDarcula() ? AllIcons.Actions.Clean : AllIcons.Actions.CleanLight);
140       myClearFieldLabel.setOpaque(true);
141       add(myClearFieldLabel, BorderLayout.EAST);
142       myClearFieldLabel.addMouseListener(new MouseAdapter() {
143         public void mousePressed(MouseEvent e) {
144           myTextField.setText("");
145           onFieldCleared();
146         }
147       });
148
149       if (!hasIconsOutsideOfTextField()) {
150         final Border originalBorder;
151         if (SystemInfo.isMac) {
152           originalBorder = BorderFactory.createLoweredBevelBorder();
153         }
154         else {
155           originalBorder = myTextField.getBorder();
156         }
157
158         myToggleHistoryLabel.setBackground(myTextField.getBackground());
159         myClearFieldLabel.setBackground(myTextField.getBackground());
160
161         setBorder(new CompoundBorder(IdeBorderFactory.createEmptyBorder(2, 0, 2, 0), originalBorder));
162
163         myTextField.setOpaque(true);
164         myTextField.setBorder(IdeBorderFactory.createEmptyBorder(0, 5, 0, 5));
165       }
166       else {
167         setBorder(IdeBorderFactory.createEmptyBorder(2, 0, 2, 0));
168       }
169     }
170
171     if (ApplicationManager.getApplication() != null) { //tests
172       final ActionManager actionManager = ActionManager.getInstance();
173       if (actionManager != null) {
174         final AnAction clearTextAction = actionManager.getAction(IdeActions.ACTION_CLEAR_TEXT);
175         if (clearTextAction.getShortcutSet().getShortcuts().length == 0) {
176           clearTextAction.registerCustomShortcutSet(CommonShortcuts.ESCAPE, this);
177         }
178       }
179     }
180   }
181
182   protected void onFieldCleared() {
183   }
184
185   protected void onFocusLost() {
186   }
187
188   protected void onFocusGained() {
189   }
190
191   private void updateMenu() {
192     if (myNativeSearchPopup != null) {
193       myNativeSearchPopup.removeAll();
194       final int itemsCount = myModel.getSize();
195       if (itemsCount == 0) {
196         myNativeSearchPopup.add(myNoItems);
197       }
198       else {
199         for (int i = 0; i < itemsCount; i++) {
200           final String item = myModel.getElementAt(i);
201           addMenuItem(item);
202         }
203       }
204     }
205   }
206
207   protected boolean isSearchControlUISupported() {
208     return (SystemInfo.isMacOSLeopard && UIUtil.isUnderAquaLookAndFeel()) || UIUtil.isUnderDarcula() || UIUtil.isUnderIntelliJLaF();
209   }
210
211   protected boolean hasIconsOutsideOfTextField() {
212     return UIUtil.isUnderGTKLookAndFeel() || UIUtil.isUnderNimbusLookAndFeel();
213   }
214
215   public void addDocumentListener(DocumentListener listener) {
216     getTextEditor().getDocument().addDocumentListener(listener);
217   }
218
219   public void removeDocumentListener(DocumentListener listener) {
220     getTextEditor().getDocument().removeDocumentListener(listener);
221   }
222
223   public void addKeyboardListener(final KeyListener listener) {
224     getTextEditor().addKeyListener(listener);
225   }
226
227   public void setEnabled(boolean enabled) {
228     super.setEnabled(enabled);
229     if (myToggleHistoryLabel != null) {
230       final Color bg = enabled ? UIUtil.getTextFieldBackground() : UIUtil.getPanelBackground();
231       myToggleHistoryLabel.setBackground(bg);
232       myClearFieldLabel.setBackground(bg);
233     }
234   }
235
236   public void setHistorySize(int historySize) {
237     if (historySize <= 0) throw new IllegalArgumentException("history size must be a positive number");
238     myHistorySize = historySize;
239   }
240
241   public void setHistory(List<String> aHistory) {
242     myModel.setItems(aHistory);
243   }
244
245   public List<String> getHistory() {
246     final int itemsCount = myModel.getSize();
247     final List<String> history = new ArrayList<String>(itemsCount);
248     for (int i = 0; i < itemsCount; i++) {
249       history.add(myModel.getElementAt(i));
250     }
251     return history;
252   }
253
254   public void setText(String aText) {
255     getTextEditor().setText(aText);
256   }
257
258   public String getText() {
259     return getTextEditor().getText();
260   }
261
262   public void removeNotify() {
263     super.removeNotify();
264     hidePopup();
265   }
266
267   public void addCurrentTextToHistory() {
268     if ((myNativeSearchPopup != null && myNativeSearchPopup.isVisible()) || (myPopup != null && myPopup.isVisible())) {
269       return;
270     }
271     final String item = getText();
272     myModel.addElement(item);
273   }
274
275   private void addMenuItem(final String item) {
276     if (myNativeSearchPopup != null) {
277       myNativeSearchPopup.remove(myNoItems);
278       final JMenuItem menuItem = new JBMenuItem(item);
279       myNativeSearchPopup.add(menuItem);
280       menuItem.addActionListener(new ActionListener() {
281         public void actionPerformed(final ActionEvent e) {
282           myTextField.setText(item);
283           addCurrentTextToHistory();
284         }
285       });
286     }
287   }
288
289   public void selectText() {
290     getTextEditor().selectAll();
291   }
292
293   public JTextField getTextEditor() {
294     return myTextField;
295   }
296
297   public boolean requestFocusInWindow() {
298     return myTextField.requestFocusInWindow();
299   }
300
301   public void requestFocus() {
302     getTextEditor().requestFocus();
303   }
304
305   public class MyModel extends AbstractListModel {
306     private List<String> myFullList = new ArrayList<String>();
307
308     private String mySelectedItem;
309
310     public String getElementAt(int index) {
311       return myFullList.get(index);
312     }
313
314     public int getSize() {
315       return Math.min(myHistorySize, myFullList.size());
316     }
317
318     public void addElement(String item) {
319       final String newItem = item.trim();
320       if (newItem.isEmpty()) {
321         return;
322       }
323
324       final int length = myFullList.size();
325       int index = -1;
326       for (int i = 0; i < length; i++) {
327         if (StringUtil.equalsIgnoreCase(myFullList.get(i), newItem)) {
328           index = i;
329           break;
330         }
331       }
332       if (index == 0) {
333         // item is already at the top of the list
334         return;
335       }
336       else if (index > 0) {
337         // move item to top of the list
338         myFullList.remove(index);
339       }
340       else if (myFullList.size() >= myHistorySize && myFullList.size() > 0) {
341         // trim list
342         myFullList.remove(myFullList.size() - 1);
343       }
344       insertElementAt(newItem, 0);
345     }
346
347     public void insertElementAt(String item, int index) {
348       myFullList.add(index, item);
349       fireContentsChanged();
350     }
351
352     public String getSelectedItem() {
353       return mySelectedItem;
354     }
355
356     public void setSelectedItem(String anItem) {
357       mySelectedItem = anItem;
358     }
359
360     public void fireContentsChanged() {
361       fireContentsChanged(this, -1, -1);
362       updateMenu();
363     }
364
365     public void setItems(List<String> aList) {
366       myFullList = new ArrayList<String>(aList);
367       fireContentsChanged();
368     }
369   }
370
371   private void hidePopup() {
372     if (myPopup != null) {
373       myPopup.cancel();
374       myPopup = null;
375     }
376   }
377
378   @Override
379   public Dimension getPreferredSize() {
380     Dimension size = super.getPreferredSize();
381     Border border = super.getBorder();
382     if (border != null && UIUtil.isUnderAquaLookAndFeel()) {
383       Insets insets = border.getBorderInsets(this);
384       size.height += insets.top + insets.bottom;
385       size.width += insets.left + insets.right;
386     }
387     return size;
388   }
389
390   protected Runnable createItemChosenCallback(final JList list) {
391     return new Runnable() {
392       public void run() {
393         final String value = (String)list.getSelectedValue();
394         getTextEditor().setText(value != null ? value : "");
395         addCurrentTextToHistory();
396         if (myPopup != null) {
397           myPopup.cancel();
398           myPopup = null;
399         }
400       }
401     };
402   }
403
404   protected void showPopup() {
405     if (myPopup == null || !myPopup.isVisible()) {
406       final JList list = new JBList(myModel);
407       final Runnable chooseRunnable = createItemChosenCallback(list);
408       myPopup = JBPopupFactory.getInstance().createListPopupBuilder(list)
409         .setMovable(false)
410         .setRequestFocus(true)
411         .setItemChoosenCallback(chooseRunnable).createPopup();
412       if (isShowing()) {
413         myPopup.showUnderneathOf(getPopupLocationComponent());
414       }
415     }
416   }
417
418   protected Component getPopupLocationComponent() {
419     return hasIconsOutsideOfTextField() ? myToggleHistoryLabel : this;
420   }
421
422   private void togglePopup() {
423     if (myPopup == null) {
424       showPopup();
425     }
426     else {
427       hidePopup();
428     }
429   }
430
431   public void setSelectedItem(final String s) {
432     getTextEditor().setText(s);
433   }
434
435   public int getSelectedIndex() {
436     return myModel.myFullList.indexOf(getText());
437   }
438
439   protected static class TextFieldWithProcessing extends JTextField {
440     public void processKeyEvent(KeyEvent e) {
441       super.processKeyEvent(e);
442     }
443   }
444
445   public final void keyEventToTextField(KeyEvent e) {
446     myTextField.processKeyEvent(e);
447   }
448
449   protected boolean preprocessEventForTextField(KeyEvent e) {
450     return false;
451   }
452   
453   public void setSearchIcon(final Icon icon) {
454     if (! isSearchControlUISupported()) {
455       myToggleHistoryLabel.setIcon(icon);
456     }
457   }
458 }