42612d4416ac1d450a8997679cda9b39b0911d23
[idea/community.git] / platform / lang-impl / src / com / intellij / find / SearchTextArea.java
1 /*
2  * Copyright 2000-2015 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.find;
17
18 import com.intellij.featureStatistics.FeatureUsageTracker;
19 import com.intellij.find.editorHeaderActions.Utils;
20 import com.intellij.icons.AllIcons;
21 import com.intellij.ide.DataManager;
22 import com.intellij.ide.ui.laf.darcula.DarculaUIUtil;
23 import com.intellij.ide.ui.laf.intellij.MacIntelliJIconCache;
24 import com.intellij.ide.ui.laf.intellij.MacIntelliJTextFieldUI;
25 import com.intellij.openapi.actionSystem.*;
26 import com.intellij.openapi.actionSystem.impl.ActionButton;
27 import com.intellij.openapi.actionSystem.impl.InplaceActionButtonLook;
28 import com.intellij.openapi.keymap.KeymapUtil;
29 import com.intellij.openapi.project.DumbAwareAction;
30 import com.intellij.openapi.util.SystemInfo;
31 import com.intellij.openapi.util.registry.Registry;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.ui.DocumentAdapter;
34 import com.intellij.ui.Gray;
35 import com.intellij.ui.JBColor;
36 import com.intellij.ui.components.JBLabel;
37 import com.intellij.ui.components.JBList;
38 import com.intellij.ui.components.JBScrollPane;
39 import com.intellij.ui.components.panels.NonOpaquePanel;
40 import com.intellij.util.ArrayUtil;
41 import com.intellij.util.ui.JBDimension;
42 import com.intellij.util.ui.JBInsets;
43 import com.intellij.util.ui.JBUI;
44 import com.intellij.util.ui.UIUtil;
45 import net.miginfocom.swing.MigLayout;
46 import org.jetbrains.annotations.NotNull;
47
48 import javax.swing.*;
49 import javax.swing.border.Border;
50 import javax.swing.border.EmptyBorder;
51 import javax.swing.event.DocumentEvent;
52 import javax.swing.text.DefaultEditorKit;
53 import java.awt.*;
54 import java.awt.event.*;
55 import java.awt.geom.RoundRectangle2D;
56 import java.beans.PropertyChangeEvent;
57 import java.beans.PropertyChangeListener;
58
59 public class SearchTextArea extends NonOpaquePanel implements PropertyChangeListener, FocusListener {
60   private final JTextArea myTextArea;
61   private final boolean myInfoMode;
62   private final JLabel myInfoLabel;
63   private JPanel myIconsPanel = null;
64   private ActionButton myNewLineButton;
65   private ActionButton myClearButton;
66   private JBScrollPane myScrollPane;
67   private ActionButton myHistoryPopupButton;
68
69   public SearchTextArea(boolean search) {
70     this(new JTextArea(), search, false);
71   }
72
73   public SearchTextArea(@NotNull JTextArea textArea, boolean search, boolean infoMode) {
74     myTextArea = textArea;
75     myInfoMode = infoMode;
76     myTextArea.addPropertyChangeListener("background", this);
77     myTextArea.addPropertyChangeListener("font", this);
78     myTextArea.addFocusListener(this);
79     myTextArea.getDocument().addDocumentListener(new DocumentAdapter() {
80       @Override
81       protected void textChanged(DocumentEvent e) {
82         updateIconsLayout();
83       }
84     });
85     myTextArea.setOpaque(false);
86     myScrollPane = new JBScrollPane(myTextArea,
87                                     ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
88                                     ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED) {
89       @Override
90       public Dimension getPreferredSize() {
91         Dimension d = super.getPreferredSize();
92         d.height = Math.min(d.height, myTextArea.getUI().getPreferredSize(myTextArea).height);
93         return d;
94       }
95     };
96     myTextArea.setBorder(new Border() {
97       @Override
98       public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
99
100       }
101
102       @Override
103       public Insets getBorderInsets(Component c) {
104         int bottom = (StringUtil.getLineBreakCount(myTextArea.getText()) > 0) ? 2 : UIUtil.isUnderDarcula() ? 1 : 0;
105         int top = myTextArea.getFontMetrics(myTextArea.getFont()).getHeight() <= 16 ? 2 : 1;
106         if (JBUI.isHiDPI()) bottom = 2;
107         if (JBUI.isHiDPI()) top = 2;
108         return new JBInsets(top, 0, bottom, 0);
109       }
110
111       @Override
112       public boolean isBorderOpaque() {
113         return false;
114       }
115     });
116     myScrollPane.getVerticalScrollBar().setBackground(UIUtil.TRANSPARENT_COLOR);
117     myScrollPane.getViewport().setBorder(null);
118     myScrollPane.getViewport().setOpaque(false);
119     myScrollPane.setBorder(JBUI.Borders.emptyRight(2));
120     myScrollPane.setOpaque(false);
121
122     myInfoLabel = new JBLabel(UIUtil.ComponentStyle.SMALL);
123     myInfoLabel.setForeground(JBColor.GRAY);
124
125     myHistoryPopupButton = createButton(new ShowHistoryAction(search));
126     myClearButton = createButton(new ClearAction());
127     myNewLineButton = createButton(new NewLineAction());
128     myIconsPanel = new NonOpaquePanel();
129
130     updateLayout();
131   }
132
133   protected void updateLayout() {
134     int height = UIUtil.getLineHeight(myTextArea);
135     Insets insets = myTextArea.getInsets();
136     height += insets.top + insets.bottom;
137     int extraGap = Math.max(0, (height - JBUI.scale(16)) / 2);
138     setBorder(new EmptyBorder(3, 6, 3, 4));
139     setLayout(new MigLayout("flowx, ins 0, gapx " + JBUI.scale(4)));
140     removeAll();
141     add(myHistoryPopupButton, "ay top, gaptop " + extraGap +", gapleft" + (JBUI.isHiDPI() ? 4 : 0));
142     add(myScrollPane, "ay top, growx, pushx");
143     //TODO combine icons/info modes
144     if (myInfoMode) {
145       add(myInfoLabel);
146     }
147     else {
148       add(myIconsPanel, "gaptop " + extraGap + ",ay top, gapright " + extraGap/2);
149       updateIconsLayout();
150     }
151   }
152
153   protected boolean isNewLineAvailable() {
154     return Registry.is("ide.find.show.add.newline.hint");
155   }
156
157   private void updateIconsLayout() {
158     if (myIconsPanel.getParent() == null) {
159       return;
160     }
161
162     boolean showClearIcon = !StringUtil.isEmpty(myTextArea.getText());
163     boolean showNewLine = isNewLineAvailable();
164     boolean wrongVisibility =
165       ((myClearButton.getParent() != null) != showClearIcon) || ((myNewLineButton.getParent() != null) != showNewLine);
166
167     LayoutManager layout = myIconsPanel.getLayout();
168     boolean wrongLayout = !(layout instanceof GridLayout);
169     boolean multiline = StringUtil.getLineBreakCount(myTextArea.getText()) > 0;
170     boolean wrongPositioning = !wrongLayout && (((GridLayout)layout).getRows() > 1) != multiline;
171     if (wrongLayout || wrongVisibility || wrongPositioning) {
172       myIconsPanel.removeAll();
173       int rows = multiline && showClearIcon && showNewLine ? 2 : 1;
174       int columns = !multiline && showClearIcon && showNewLine ? 2 : 1;
175       myIconsPanel.setLayout(new GridLayout(rows, columns, 8, 8));
176       if (!multiline && showNewLine) {
177         myIconsPanel.add(myNewLineButton);
178       }
179       if (showClearIcon) {
180         myIconsPanel.add(myClearButton);
181       }
182       if (multiline && showNewLine) {
183         myIconsPanel.add(myNewLineButton);
184       }
185       myIconsPanel.setBorder(JBUI.Borders.emptyBottom(rows == 2 ? 3 : 0));
186       myScrollPane.revalidate();
187       doLayout();
188     }
189   }
190
191
192   @NotNull
193   public JTextArea getTextArea() {
194     return myTextArea;
195   }
196
197   @Override
198   public Dimension getMinimumSize() {
199     return getPreferredSize();
200   }
201
202   @Override
203   public void propertyChange(PropertyChangeEvent evt) {
204     if ("background".equals(evt.getPropertyName())) {
205       repaint();
206     }
207     if ("font".equals(evt.getPropertyName())) {
208       updateLayout();
209     }
210   }
211
212   @Override
213   public void focusGained(FocusEvent e) {
214     repaint();
215   }
216
217   @Override
218   public void focusLost(FocusEvent e) {
219     repaint();
220   }
221
222   public void setInfoText(String info) {
223     myInfoLabel.setText(info);
224   }
225
226   private static Color enabledBorderColor = new JBColor(Gray._196, Gray._100);
227   private static Color disabledBorderColor = Gray._83;
228
229   @Override
230   public void paint(Graphics graphics) {
231     Graphics2D g = (Graphics2D)graphics.create();
232     boolean hasFocus = myTextArea.hasFocus();
233     try {
234       g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
235       g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
236       Rectangle r = new Rectangle(getSize());
237       r.height = Math.max(r.height, myScrollPane.getHeight() + getInsets().top + getInsets().bottom);
238       if (myIconsPanel.getParent() != null) {
239         r.height = Math.max(r.height, myIconsPanel.getHeight() + getInsets().top + getInsets().bottom);
240       }
241       if (r.height % 2 == 1) r.height--;
242       int arcSize = Math.min(Math.max(25, myTextArea.getFontMetrics(myTextArea.getFont()).getHeight() * 3 / 2), r.height - 1);
243       if (JBUI.isHiDPI()) arcSize = JBUI.scale(21);
244       Color borderColor = myTextArea.isEnabled() ? enabledBorderColor : disabledBorderColor;
245       if (SystemInfo.isMac && (UIUtil.isUnderIntelliJLaF() || UIUtil.isUnderAquaLookAndFeel())) {
246         g.setColor(borderColor);
247         MacIntelliJTextFieldUI.paintAquaSearchFocusRing(g, r, myTextArea);
248       }
249       else {
250         JBInsets.removeFrom(r, new JBInsets(3, 3, 3, 3));
251         if (hasFocus && (UIUtil.isUnderIntelliJLaF() || UIUtil.isUnderDarcula())) {
252           DarculaUIUtil.paintSearchFocusRing(g, r, myTextArea, arcSize);
253         }
254         else {
255           Shape shape = UIUtil.isUnderWindowsLookAndFeel()
256                         ? new Rectangle(r.x, r.y, r.width, r.height)
257                         : new RoundRectangle2D.Double(r.x, r.y, r.width, r.height, arcSize - JBUI.scale(5), arcSize - JBUI.scale(5));
258           g.setColor(myTextArea.getBackground());
259           g.fill(shape);
260           g.setColor(borderColor);
261           g.draw(shape);
262         }
263       }
264     }
265     finally {
266       g.dispose();
267     }
268     super.paint(graphics);
269
270     if (UIUtil.isUnderGTKLookAndFeel()) {
271       graphics.setColor(myTextArea.getBackground());
272       Rectangle bounds = myScrollPane.getViewport().getBounds();
273       if (myScrollPane.getVerticalScrollBar().isVisible()) {
274         bounds.width -= myScrollPane.getVerticalScrollBar().getWidth();
275       }
276       bounds = SwingUtilities.convertRectangle(myScrollPane.getViewport()/*myTextArea*/, bounds, this);
277       JBInsets.addTo(bounds, new JBInsets(2, 2, -1, -1));
278       ((Graphics2D)graphics).draw(bounds);
279     }
280   }
281
282   private class ShowHistoryAction extends DumbAwareAction {
283     private final boolean myShowSearchHistory;
284
285     public ShowHistoryAction(boolean search) {
286       super((search ? "Search" : "Replace") + " History",
287             (search ? "Search" : "Replace") + " history",
288             MacIntelliJIconCache.getIcon("searchFieldWithHistory"));
289
290       myShowSearchHistory = search;
291
292       KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK);
293       registerCustomShortcutSet(new CustomShortcutSet(new KeyboardShortcut(stroke, null)), myTextArea);
294     }
295
296     @Override
297     public void actionPerformed(AnActionEvent e) {
298       FeatureUsageTracker.getInstance().triggerFeatureUsed("find.recent.search");
299       FindInProjectSettings findInProjectSettings = FindInProjectSettings.getInstance(e.getProject());
300       String[] recent = myShowSearchHistory ? findInProjectSettings.getRecentFindStrings()
301                                             : findInProjectSettings.getRecentReplaceStrings();
302       String title = "Recent " + (myShowSearchHistory ? "Searches" : "Replaces");
303       JBList historyList = new JBList((Object[])ArrayUtil.reverseArray(recent));
304       Utils.showCompletionPopup(SearchTextArea.this, historyList, title, myTextArea, null);
305     }
306   }
307
308   private static ActionButton createButton(AnAction action) {
309     Presentation presentation = action.getTemplatePresentation();
310     Dimension d = new JBDimension(16, 16);
311     ActionButton button = new ActionButton(action, presentation, ActionPlaces.UNKNOWN, d) {
312       @Override
313       protected DataContext getDataContext() {
314         return DataManager.getInstance().getDataContext(this);
315       }
316     };
317     button.setLook(new InplaceActionButtonLook());
318     button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
319     button.updateIcon();
320     return button;
321   }
322
323   private class ClearAction extends DumbAwareAction {
324     public ClearAction() {
325       super(null, null, AllIcons.Actions.Clear);
326     }
327
328     @Override
329     public void actionPerformed(AnActionEvent e) {
330       myTextArea.setText("");
331     }
332   }
333
334   private class NewLineAction extends DumbAwareAction {
335     public NewLineAction() {
336       super(null, "New line (" + KeymapUtil.getKeystrokeText(SearchReplaceComponent.NEW_LINE_KEYSTROKE) + ")", AllIcons.Actions.SearchNewLine);
337     }
338
339     @Override
340     public void actionPerformed(AnActionEvent e) {
341       new DefaultEditorKit.InsertBreakAction().actionPerformed(new ActionEvent(myTextArea, 0, "action"));
342     }
343   }
344 }