Find/Replace in editor: pixel hunting again
[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     int extraGap = Math.max(JBUI.isHiDPI() ? 0 : 1, (height + insets.top + insets.bottom - JBUI.scale(16)) / 2);
137     setBorder(new EmptyBorder(3 + Math.max(0, JBUI.scale(16) - height) / 2, 6, 3, 4));//In case of small fonts we shouldn't align to top
138     setLayout(new MigLayout("flowx, ins 0, gapx " + JBUI.scale(4)));
139     removeAll();
140     add(myHistoryPopupButton, "ay top, gaptop " + extraGap +", gapleft" + (JBUI.isHiDPI() ? 4 : 0));
141     add(myScrollPane, "ay top, growx, pushx");
142     //TODO combine icons/info modes
143     if (myInfoMode) {
144       add(myInfoLabel);
145     }
146     else {
147       add(myIconsPanel, "gaptop " + extraGap + ",ay top, gapright " + extraGap/2);
148       updateIconsLayout();
149     }
150   }
151
152   protected boolean isNewLineAvailable() {
153     return Registry.is("ide.find.show.add.newline.hint");
154   }
155
156   private void updateIconsLayout() {
157     if (myIconsPanel.getParent() == null) {
158       return;
159     }
160
161     boolean showClearIcon = !StringUtil.isEmpty(myTextArea.getText());
162     boolean showNewLine = isNewLineAvailable();
163     boolean wrongVisibility =
164       ((myClearButton.getParent() != null) != showClearIcon) || ((myNewLineButton.getParent() != null) != showNewLine);
165
166     LayoutManager layout = myIconsPanel.getLayout();
167     boolean wrongLayout = !(layout instanceof GridLayout);
168     boolean multiline = StringUtil.getLineBreakCount(myTextArea.getText()) > 0;
169     boolean wrongPositioning = !wrongLayout && (((GridLayout)layout).getRows() > 1) != multiline;
170     if (wrongLayout || wrongVisibility || wrongPositioning) {
171       myIconsPanel.removeAll();
172       int rows = multiline && showClearIcon && showNewLine ? 2 : 1;
173       int columns = !multiline && showClearIcon && showNewLine ? 2 : 1;
174       myIconsPanel.setLayout(new GridLayout(rows, columns, 8, 8));
175       if (!multiline && showNewLine) {
176         myIconsPanel.add(myNewLineButton);
177       }
178       if (showClearIcon) {
179         myIconsPanel.add(myClearButton);
180       }
181       if (multiline && showNewLine) {
182         myIconsPanel.add(myNewLineButton);
183       }
184       myIconsPanel.setBorder(JBUI.Borders.emptyBottom(rows == 2 ? 3 : 0));
185       myScrollPane.revalidate();
186       doLayout();
187     }
188   }
189
190
191   @NotNull
192   public JTextArea getTextArea() {
193     return myTextArea;
194   }
195
196   @Override
197   public Dimension getMinimumSize() {
198     return getPreferredSize();
199   }
200
201   @Override
202   public void propertyChange(PropertyChangeEvent evt) {
203     if ("background".equals(evt.getPropertyName())) {
204       repaint();
205     }
206     if ("font".equals(evt.getPropertyName())) {
207       updateLayout();
208     }
209   }
210
211   @Override
212   public void focusGained(FocusEvent e) {
213     repaint();
214   }
215
216   @Override
217   public void focusLost(FocusEvent e) {
218     repaint();
219   }
220
221   public void setInfoText(String info) {
222     myInfoLabel.setText(info);
223   }
224
225   private static Color enabledBorderColor = new JBColor(Gray._196, Gray._100);
226   private static Color disabledBorderColor = Gray._83;
227
228   @Override
229   public void paint(Graphics graphics) {
230     Graphics2D g = (Graphics2D)graphics.create();
231     boolean hasFocus = myTextArea.hasFocus();
232     try {
233       g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
234       g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
235       Rectangle r = new Rectangle(getSize());
236       r.height = Math.max(r.height, myScrollPane.getHeight() + getInsets().top + getInsets().bottom);
237       if (myIconsPanel.getParent() != null) {
238         r.height = Math.max(r.height, myIconsPanel.getHeight() + getInsets().top + getInsets().bottom);
239       }
240       if (r.height % 2 == 1) r.height--;
241       int arcSize = Math.min(Math.max(25, myTextArea.getFontMetrics(myTextArea.getFont()).getHeight() * 3 / 2), r.height - 1);
242       if (JBUI.isHiDPI()) arcSize = JBUI.scale(21);
243       Color borderColor = myTextArea.isEnabled() ? enabledBorderColor : disabledBorderColor;
244       if (SystemInfo.isMac && (UIUtil.isUnderIntelliJLaF() || UIUtil.isUnderAquaLookAndFeel())) {
245         g.setColor(borderColor);
246         MacIntelliJTextFieldUI.paintAquaSearchFocusRing(g, r, myTextArea);
247       }
248       else {
249         JBInsets.removeFrom(r, new JBInsets(3, 3, 3, 3));
250         if (hasFocus && (UIUtil.isUnderIntelliJLaF() || UIUtil.isUnderDarcula())) {
251           DarculaUIUtil.paintSearchFocusRing(g, r, myTextArea, arcSize);
252         }
253         else {
254           Shape shape = UIUtil.isUnderWindowsLookAndFeel()
255                         ? new Rectangle(r.x, r.y, r.width, r.height)
256                         : new RoundRectangle2D.Double(r.x, r.y, r.width, r.height, arcSize - JBUI.scale(5), arcSize - JBUI.scale(5));
257           g.setColor(myTextArea.getBackground());
258           g.fill(shape);
259           g.setColor(borderColor);
260           g.draw(shape);
261         }
262       }
263     }
264     finally {
265       g.dispose();
266     }
267     super.paint(graphics);
268
269     if (UIUtil.isUnderGTKLookAndFeel()) {
270       graphics.setColor(myTextArea.getBackground());
271       Rectangle bounds = myScrollPane.getViewport().getBounds();
272       if (myScrollPane.getVerticalScrollBar().isVisible()) {
273         bounds.width -= myScrollPane.getVerticalScrollBar().getWidth();
274       }
275       bounds = SwingUtilities.convertRectangle(myScrollPane.getViewport()/*myTextArea*/, bounds, this);
276       JBInsets.addTo(bounds, new JBInsets(2, 2, -1, -1));
277       ((Graphics2D)graphics).draw(bounds);
278     }
279   }
280
281   private class ShowHistoryAction extends DumbAwareAction {
282     private final boolean myShowSearchHistory;
283
284     public ShowHistoryAction(boolean search) {
285       super((search ? "Search" : "Replace") + " History",
286             (search ? "Search" : "Replace") + " history",
287             MacIntelliJIconCache.getIcon("searchFieldWithHistory"));
288
289       myShowSearchHistory = search;
290
291       KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK);
292       registerCustomShortcutSet(new CustomShortcutSet(new KeyboardShortcut(stroke, null)), myTextArea);
293     }
294
295     @Override
296     public void actionPerformed(AnActionEvent e) {
297       FeatureUsageTracker.getInstance().triggerFeatureUsed("find.recent.search");
298       FindInProjectSettings findInProjectSettings = FindInProjectSettings.getInstance(e.getProject());
299       String[] recent = myShowSearchHistory ? findInProjectSettings.getRecentFindStrings()
300                                             : findInProjectSettings.getRecentReplaceStrings();
301       String title = "Recent " + (myShowSearchHistory ? "Searches" : "Replaces");
302       JBList historyList = new JBList((Object[])ArrayUtil.reverseArray(recent));
303       Utils.showCompletionPopup(SearchTextArea.this, historyList, title, myTextArea, null);
304     }
305   }
306
307   private static ActionButton createButton(AnAction action) {
308     Presentation presentation = action.getTemplatePresentation();
309     Dimension d = new JBDimension(16, 16);
310     ActionButton button = new ActionButton(action, presentation, ActionPlaces.UNKNOWN, d) {
311       @Override
312       protected DataContext getDataContext() {
313         return DataManager.getInstance().getDataContext(this);
314       }
315     };
316     button.setLook(new InplaceActionButtonLook());
317     button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
318     button.updateIcon();
319     return button;
320   }
321
322   private class ClearAction extends DumbAwareAction {
323     public ClearAction() {
324       super(null, null, AllIcons.Actions.Clear);
325     }
326
327     @Override
328     public void actionPerformed(AnActionEvent e) {
329       myTextArea.setText("");
330     }
331   }
332
333   private class NewLineAction extends DumbAwareAction {
334     public NewLineAction() {
335       super(null, "New line (" + KeymapUtil.getKeystrokeText(SearchReplaceComponent.NEW_LINE_KEYSTROKE) + ")", AllIcons.Actions.SearchNewLine);
336     }
337
338     @Override
339     public void actionPerformed(AnActionEvent e) {
340       new DefaultEditorKit.InsertBreakAction().actionPerformed(new ActionEvent(myTextArea, 0, "action"));
341     }
342   }
343 }