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