a32625a0187871f9b0cbe22d27bb891e2f2307da
[idea/community.git] / platform / platform-impl / src / com / intellij / util / ui / SwingHelper.java
1 /*
2  * Copyright 2000-2014 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.util.ui;
17
18 import com.intellij.ide.BrowserUtil;
19 import com.intellij.openapi.actionSystem.ActionManager;
20 import com.intellij.openapi.actionSystem.AnAction;
21 import com.intellij.openapi.actionSystem.AnActionEvent;
22 import com.intellij.openapi.actionSystem.DefaultActionGroup;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
25 import com.intellij.openapi.fileChooser.FileChooserFactory;
26 import com.intellij.openapi.ide.CopyPasteManager;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.ui.ComponentWithBrowseButton;
29 import com.intellij.openapi.ui.DialogWrapper;
30 import com.intellij.openapi.ui.TextComponentAccessor;
31 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
32 import com.intellij.openapi.util.SystemInfo;
33 import com.intellij.openapi.util.text.StringUtil;
34 import com.intellij.ui.HyperlinkLabel;
35 import com.intellij.ui.TextFieldWithHistory;
36 import com.intellij.ui.TextFieldWithHistoryWithBrowseButton;
37 import com.intellij.util.NotNullProducer;
38 import com.intellij.util.PlatformIcons;
39 import com.intellij.util.containers.ComparatorUtil;
40 import com.intellij.util.containers.ContainerUtil;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import javax.swing.*;
45 import javax.swing.event.PopupMenuEvent;
46 import javax.swing.event.PopupMenuListener;
47 import javax.swing.table.DefaultTableCellRenderer;
48 import javax.swing.table.TableCellRenderer;
49 import javax.swing.table.TableColumn;
50 import java.awt.*;
51 import java.awt.datatransfer.StringSelection;
52 import java.awt.datatransfer.Transferable;
53 import java.util.Collection;
54 import java.util.List;
55 import java.util.Set;
56
57 public class SwingHelper {
58
59   private static final Logger LOG = Logger.getInstance(SwingHelper.class);
60
61   /**
62    * Creates panel whose content consists of given {@code children} components
63    * stacked vertically each on another in a given order.
64    *
65    * @param childAlignmentX Component.LEFT_ALIGNMENT, Component.CENTER_ALIGNMENT or Component.RIGHT_ALIGNMENT
66    * @param children        children components
67    * @return created panel
68    */
69   @NotNull
70   public static JPanel newVerticalPanel(float childAlignmentX, Component... children) {
71     return newGenericBoxPanel(true, childAlignmentX, children);
72   }
73
74   @NotNull
75   public static JPanel newLeftAlignedVerticalPanel(Component... children) {
76     return newVerticalPanel(Component.LEFT_ALIGNMENT, children);
77   }
78
79   @NotNull
80   public static JPanel newLeftAlignedVerticalPanel(@NotNull Collection<Component> children) {
81     return newVerticalPanel(Component.LEFT_ALIGNMENT, children);
82   }
83
84   @NotNull
85   public static JPanel newVerticalPanel(float childAlignmentX, @NotNull Collection<Component> children) {
86     return newVerticalPanel(childAlignmentX, children.toArray(new Component[children.size()]));
87   }
88
89   /**
90    * Creates panel whose content consists of given {@code children} components horizontally
91    * stacked each on another in a given order.
92    *
93    * @param childAlignmentY Component.TOP_ALIGNMENT, Component.CENTER_ALIGNMENT or Component.BOTTOM_ALIGNMENT
94    * @param children        children components
95    * @return created panel
96    */
97   @NotNull
98   public static JPanel newHorizontalPanel(float childAlignmentY, Component... children) {
99     return newGenericBoxPanel(false, childAlignmentY, children);
100   }
101
102   @NotNull
103   public static JPanel newHorizontalPanel(float childAlignmentY, @NotNull Collection<Component> children) {
104     return newHorizontalPanel(childAlignmentY, children.toArray(new Component[children.size()]));
105   }
106
107   private static JPanel newGenericBoxPanel(boolean verticalOrientation,
108                                            float childAlignment,
109                                            Component... children) {
110     JPanel panel = new JPanel();
111     int axis = verticalOrientation ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS;
112     panel.setLayout(new BoxLayout(panel, axis));
113     for (Component child : children) {
114       panel.add(child, childAlignment);
115       if (child instanceof JComponent) {
116         JComponent jChild = (JComponent)child;
117         if (verticalOrientation) {
118           jChild.setAlignmentX(childAlignment);
119         }
120         else {
121           jChild.setAlignmentY(childAlignment);
122         }
123       }
124     }
125     return panel;
126   }
127
128   @NotNull
129   public static JPanel wrapWithoutStretch(@NotNull JComponent component) {
130     JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
131     panel.add(component);
132     return panel;
133   }
134
135   @NotNull
136   public static JPanel wrapWithHorizontalStretch(@NotNull JComponent component) {
137     JPanel panel = new JPanel(new BorderLayout(0, 0));
138     panel.add(component, BorderLayout.NORTH);
139     return panel;
140   }
141
142   public static void setPreferredWidthToFitText(@NotNull TextFieldWithHistoryWithBrowseButton component) {
143     int childWidth = calcWidthToFitText(component.getChildComponent().getTextEditor(), 35);
144     setPreferredWidthForComponentWithBrowseButton(component, childWidth);
145   }
146
147   public static void setPreferredWidthToFitText(@NotNull TextFieldWithBrowseButton component) {
148     int childWidth = calcWidthToFitText(component.getChildComponent(), 20);
149     setPreferredWidthForComponentWithBrowseButton(component, childWidth);
150   }
151
152   private static <T extends JComponent> void setPreferredWidthForComponentWithBrowseButton(@NotNull ComponentWithBrowseButton<T> component,
153                                                                                            int childPrefWidth) {
154     Dimension buttonPrefSize = component.getButton().getPreferredSize();
155     setPreferredWidth(component, childPrefWidth + buttonPrefSize.width);
156   }
157
158   public static void setPreferredWidthToFitText(@NotNull JTextField textField) {
159     setPreferredWidthToFitText(textField, 15);
160   }
161
162   public static void setPreferredWidthToFitText(@NotNull JTextField textField, int additionalWidth) {
163     setPreferredSizeToFitText(textField, StringUtil.notNullize(textField.getText()), additionalWidth);
164   }
165
166   public static void setPreferredWidthToFitText(@NotNull JTextField textField, @NotNull String text) {
167     setPreferredSizeToFitText(textField, text, 15);
168   }
169
170   private static void setPreferredSizeToFitText(@NotNull JTextField textField, @NotNull String text, int additionalWidth) {
171     int width = calcWidthToFitText(textField, text, additionalWidth);
172     setPreferredWidth(textField, width);
173   }
174
175   private static int calcWidthToFitText(@NotNull JTextField textField, int additionalWidth) {
176     return calcWidthToFitText(textField, textField.getText(), additionalWidth);
177   }
178
179   private static int calcWidthToFitText(@NotNull JTextField textField, @NotNull String text, int additionalWidth) {
180     return textField.getFontMetrics(textField.getFont()).stringWidth(text) + additionalWidth;
181   }
182
183   public static void adjustDialogSizeToFitPreferredSize(@NotNull DialogWrapper dialogWrapper) {
184     JRootPane rootPane = dialogWrapper.getRootPane();
185     Dimension componentSize = rootPane.getSize();
186     Dimension componentPreferredSize = rootPane.getPreferredSize();
187     if (componentPreferredSize.width <= componentSize.width && componentPreferredSize.height <= componentSize.height) {
188       return;
189     }
190     int dw = Math.max(0, componentPreferredSize.width - componentSize.width);
191     int dh = Math.max(0, componentPreferredSize.height - componentSize.height);
192
193     Dimension oldDialogSize = dialogWrapper.getSize();
194     Dimension newDialogSize = new Dimension(oldDialogSize.width + dw, oldDialogSize.height + dh);
195
196     dialogWrapper.setSize(newDialogSize.width, newDialogSize.height);
197     rootPane.revalidate();
198     rootPane.repaint();
199
200     LOG.info("DialogWrapper '" + dialogWrapper.getTitle() + "' has been resized (added width: " + dw + ", added height: " + dh + ")");
201   }
202
203   public static <T> void updateItems(@NotNull JComboBox comboBox,
204                                      @NotNull List<T> newItems,
205                                      @Nullable T newSelectedItemIfSelectionCannotBePreserved) {
206     if (!shouldUpdate(comboBox, newItems)) {
207       return;
208     }
209     Object selectedItem = comboBox.getSelectedItem();
210     //noinspection SuspiciousMethodCalls
211     if (selectedItem != null && !newItems.contains(selectedItem)) {
212       selectedItem = null;
213     }
214     if (selectedItem == null && newItems.contains(newSelectedItemIfSelectionCannotBePreserved)) {
215       selectedItem = newSelectedItemIfSelectionCannotBePreserved;
216     }
217     comboBox.removeAllItems();
218     for (T newItem : newItems) {
219       comboBox.addItem(newItem);
220     }
221     if (selectedItem != null) {
222       int count = comboBox.getItemCount();
223       for (int i = 0; i < count; i++) {
224         Object item = comboBox.getItemAt(i);
225         if (selectedItem.equals(item)) {
226           comboBox.setSelectedIndex(i);
227           break;
228         }
229       }
230     }
231   }
232
233   private static <T> boolean shouldUpdate(@NotNull JComboBox comboBox, @NotNull List<T> newItems) {
234     int count = comboBox.getItemCount();
235     if (newItems.size() != count) {
236       return true;
237     }
238     for (int i = 0; i < count; i++) {
239       Object oldItem = comboBox.getItemAt(i);
240       T newItem = newItems.get(i);
241       if (!ComparatorUtil.equalsNullable(oldItem, newItem)) {
242         return true;
243       }
244     }
245     return false;
246   }
247
248   public static void setNoBorderCellRendererFor(@NotNull TableColumn column) {
249     final TableCellRenderer previous = column.getCellRenderer();
250     column.setCellRenderer(new DefaultTableCellRenderer() {
251       @Override
252       public Component getTableCellRendererComponent(JTable table,
253                                                      Object value,
254                                                      boolean isSelected,
255                                                      boolean hasFocus,
256                                                      int row,
257                                                      int column) {
258         Component component;
259         if (previous != null) {
260           component = previous.getTableCellRendererComponent(table, value, isSelected, false, row, column);
261         }
262         else {
263           component = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
264         }
265         if (component instanceof JComponent) {
266           ((JComponent)component).setBorder(null);
267         }
268         return component;
269       }
270     });
271   }
272
273   public static void addHistoryOnExpansion(@NotNull final TextFieldWithHistory textFieldWithHistory,
274                                            @NotNull final NotNullProducer<List<String>> historyProvider) {
275     textFieldWithHistory.addPopupMenuListener(new PopupMenuListener() {
276       @Override
277       public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
278         setHistory(textFieldWithHistory, historyProvider.produce(), true);
279         // one-time initialization
280         textFieldWithHistory.removePopupMenuListener(this);
281       }
282
283       @Override
284       public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
285       }
286
287       @Override
288       public void popupMenuCanceled(PopupMenuEvent e) {
289       }
290     });
291   }
292
293   public static void setHistory(@NotNull TextFieldWithHistory textFieldWithHistory,
294                                 @NotNull List<String> history,
295                                 boolean mergeWithPrevHistory) {
296     Set<String> newHistorySet = ContainerUtil.newHashSet(history);
297     List<String> prevHistory = textFieldWithHistory.getHistory();
298     List<String> mergedHistory = ContainerUtil.newArrayList();
299     if (mergeWithPrevHistory) {
300       for (String item : prevHistory) {
301         if (!newHistorySet.contains(item)) {
302           mergedHistory.add(item);
303         }
304       }
305     }
306     else {
307       String currentText = textFieldWithHistory.getText();
308       if (StringUtil.isNotEmpty(currentText) && !newHistorySet.contains(currentText)) {
309         mergedHistory.add(currentText);
310       }
311     }
312     mergedHistory.addAll(history);
313     textFieldWithHistory.setHistory(mergedHistory);
314     setLongestAsPrototype(textFieldWithHistory, mergedHistory);
315   }
316
317   private static void setLongestAsPrototype(@NotNull JComboBox comboBox, @NotNull List<String> variants) {
318     Object prototypeDisplayValue = comboBox.getPrototypeDisplayValue();
319     String prototypeDisplayValueStr = null;
320     if (prototypeDisplayValue instanceof String) {
321       prototypeDisplayValueStr = (String)prototypeDisplayValue;
322     }
323     else if (prototypeDisplayValue != null) {
324       return;
325     }
326     String longest = StringUtil.notNullize(prototypeDisplayValueStr);
327     boolean updated = false;
328     for (String s : variants) {
329       if (longest.length() < s.length()) {
330         longest = s;
331         updated = true;
332       }
333     }
334     if (updated) {
335       comboBox.setPrototypeDisplayValue(longest);
336     }
337   }
338
339   public static void installFileCompletionAndBrowseDialog(@Nullable Project project,
340                                                           @NotNull TextFieldWithHistoryWithBrowseButton textFieldWithHistoryWithBrowseButton,
341                                                           @NotNull String browseDialogTitle,
342                                                           @NotNull FileChooserDescriptor fileChooserDescriptor) {
343     doInstall(project,
344               textFieldWithHistoryWithBrowseButton,
345               textFieldWithHistoryWithBrowseButton.getChildComponent().getTextEditor(),
346               browseDialogTitle,
347               fileChooserDescriptor,
348               TextComponentAccessor.TEXT_FIELD_WITH_HISTORY_WHOLE_TEXT);
349   }
350
351   public static void installFileCompletionAndBrowseDialog(@Nullable Project project,
352                                                           @NotNull TextFieldWithBrowseButton textFieldWithBrowseButton,
353                                                           @NotNull String browseDialogTitle,
354                                                           @NotNull FileChooserDescriptor fileChooserDescriptor) {
355     doInstall(project,
356               textFieldWithBrowseButton,
357               textFieldWithBrowseButton.getTextField(),
358               browseDialogTitle,
359               fileChooserDescriptor,
360               TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
361   }
362
363   private static <T extends JComponent> void doInstall(@Nullable Project project,
364                                                        @NotNull ComponentWithBrowseButton<T> componentWithBrowseButton,
365                                                        @NotNull JTextField textField,
366                                                        @NotNull String browseDialogTitle,
367                                                        @NotNull FileChooserDescriptor fileChooserDescriptor,
368                                                        @NotNull TextComponentAccessor<T> textComponentAccessor) {
369     fileChooserDescriptor = fileChooserDescriptor.withShowHiddenFiles(SystemInfo.isUnix);
370     componentWithBrowseButton.addBrowseFolderListener(
371       project,
372       new ComponentWithBrowseButton.BrowseFolderActionListener<T>(
373         browseDialogTitle,
374         null,
375         componentWithBrowseButton,
376         project,
377         fileChooserDescriptor,
378         textComponentAccessor
379       ),
380       true
381     );
382     FileChooserFactory.getInstance().installFileCompletion(
383       textField,
384       fileChooserDescriptor,
385       true,
386       project
387     );
388   }
389
390   @NotNull
391   public static HyperlinkLabel createWebHyperlink(@NotNull String url) {
392     return createWebHyperlink(url, url);
393   }
394
395   @NotNull
396   public static HyperlinkLabel createWebHyperlink(@NotNull String text, @NotNull String url) {
397     HyperlinkLabel hyperlink = new HyperlinkLabel(text);
398     hyperlink.setHyperlinkTarget(url);
399
400     DefaultActionGroup actionGroup = new DefaultActionGroup();
401     actionGroup.add(new OpenLinkInBrowser(url));
402     actionGroup.add(new CopyLinkAction(url));
403
404     hyperlink.setComponentPopupMenu(ActionManager.getInstance().createActionPopupMenu("web hyperlink", actionGroup).getComponent());
405     return hyperlink;
406   }
407
408   public static void setPreferredWidth(@NotNull JComponent component, int width) {
409     Dimension preferredSize = component.getPreferredSize();
410     preferredSize.width = width;
411     component.setPreferredSize(preferredSize);
412   }
413
414   public static class HtmlViewerBuilder {
415     private boolean myCarryTextOver;
416     private String myDisabledHtml;
417     private Font myFont;
418     private Color myBackground;
419     private Color myForeground;
420
421     public JEditorPane create() {
422       final JEditorPane textPane = new JEditorPane() {
423         private boolean myEnabled = true;
424         private String myEnabledHtml;
425
426         @Override
427         public Dimension getPreferredSize() {
428           // This trick makes text component to carry text over to the next line
429           // if the text line width exceeds parent's width
430           Dimension dimension = super.getPreferredSize();
431           if (myCarryTextOver) {
432             dimension.width = 0;
433           }
434           return dimension;
435         }
436
437         @Override
438         public void setText(String t) {
439           if (myDisabledHtml != null) {
440             if (myEnabled) {
441               myEnabledHtml = t;
442             }
443           }
444           super.setText(t);
445         }
446
447         @Override
448         public void setEnabled(boolean enabled) {
449           if (myDisabledHtml != null) {
450             myEnabled = enabled;
451             if (myEnabled) {
452               setText(myEnabledHtml);
453             } else {
454               setText(myDisabledHtml);
455             }
456             super.setEnabled(true);
457           } else {
458             super.setEnabled(enabled);
459           }
460         }
461       };
462       textPane.setFont(myFont != null ? myFont : UIUtil.getLabelFont());
463       textPane.setContentType(UIUtil.HTML_MIME);
464       textPane.setEditable(false);
465       if (myBackground != null) {
466         textPane.setBackground(myBackground);
467       }
468       else {
469         textPane.setOpaque(false);
470       }
471       textPane.setForeground(myForeground != null ? myForeground : UIUtil.getLabelForeground());
472       textPane.setFocusable(false);
473       return textPane;
474     }
475
476     public HtmlViewerBuilder setCarryTextOver(boolean carryTextOver) {
477       myCarryTextOver = carryTextOver;
478       return this;
479     }
480
481     public HtmlViewerBuilder setDisabledHtml(String disabledHtml) {
482       myDisabledHtml = disabledHtml;
483       return this;
484     }
485
486     public HtmlViewerBuilder setFont(Font font) {
487       myFont = font;
488       return this;
489     }
490
491     public HtmlViewerBuilder setBackground(Color background) {
492       myBackground = background;
493       return this;
494     }
495
496     public HtmlViewerBuilder setForeground(Color foreground) {
497       myForeground = foreground;
498       return this;
499     }
500   }
501
502   @NotNull
503   public static JEditorPane createHtmlViewer(boolean carryTextOver,
504                                              @Nullable Font font,
505                                              @Nullable Color background,
506                                              @Nullable Color foreground) {
507     final JEditorPane textPane;
508     if (carryTextOver) {
509       textPane = new JEditorPane() {
510         @Override
511         public Dimension getPreferredSize() {
512           // This trick makes text component to carry text over to the next line
513           // if the text line width exceeds parent's width
514           Dimension dimension = super.getPreferredSize();
515           dimension.width = 0;
516           return dimension;
517         }
518       };
519     }
520     else {
521       textPane = new JEditorPane();
522     }
523     textPane.setFont(font != null ? font : UIUtil.getLabelFont());
524     textPane.setContentType(UIUtil.HTML_MIME);
525     textPane.setEditable(false);
526     if (background != null) {
527       textPane.setBackground(background);
528     }
529     else {
530       textPane.setOpaque(false);
531     }
532     textPane.setForeground(foreground != null ? foreground : UIUtil.getLabelForeground());
533     textPane.setFocusable(false);
534     return textPane;
535   }
536
537   public static void setHtml(@NotNull JEditorPane editorPane,
538                              @NotNull String bodyInnerHtml,
539                              @Nullable Color foregroundColor) {
540     String html = String.format(
541       "<html><head>%s</head><body>%s</body></html>",
542       UIUtil.getCssFontDeclaration(editorPane.getFont(), foregroundColor, null, null),
543       bodyInnerHtml
544     );
545     editorPane.setText(html);
546   }
547
548   @NotNull
549   public static TextFieldWithHistoryWithBrowseButton createTextFieldWithHistoryWithBrowseButton(@Nullable Project project,
550                                                                                                 @NotNull String browseDialogTitle,
551                                                                                                 @NotNull FileChooserDescriptor fileChooserDescriptor,
552                                                                                                 @Nullable NotNullProducer<List<String>> historyProvider) {
553     TextFieldWithHistoryWithBrowseButton textFieldWithHistoryWithBrowseButton = new TextFieldWithHistoryWithBrowseButton();
554     TextFieldWithHistory textFieldWithHistory = textFieldWithHistoryWithBrowseButton.getChildComponent();
555     textFieldWithHistory.setHistorySize(-1);
556     textFieldWithHistory.setMinimumAndPreferredWidth(0);
557     if (historyProvider != null) {
558       addHistoryOnExpansion(textFieldWithHistory, historyProvider);
559     }
560     installFileCompletionAndBrowseDialog(
561       project,
562       textFieldWithHistoryWithBrowseButton,
563       browseDialogTitle,
564       fileChooserDescriptor
565     );
566     return textFieldWithHistoryWithBrowseButton;
567   }
568
569   private static class CopyLinkAction extends AnAction {
570
571     private final String myUrl;
572
573     private CopyLinkAction(@NotNull String url) {
574       super("Copy Link Address", null, PlatformIcons.COPY_ICON);
575       myUrl = url;
576     }
577
578     @Override
579     public void update(AnActionEvent e) {
580       e.getPresentation().setEnabled(true);
581     }
582
583     @Override
584     public void actionPerformed(AnActionEvent e) {
585       Transferable content = new StringSelection(myUrl);
586       CopyPasteManager.getInstance().setContents(content);
587     }
588   }
589
590   private static class OpenLinkInBrowser extends AnAction {
591
592     private final String myUrl;
593
594     private OpenLinkInBrowser(@NotNull String url) {
595       super("Open Link in Browser", null, PlatformIcons.WEB_ICON);
596       myUrl = url;
597     }
598
599     @Override
600     public void update(AnActionEvent e) {
601       e.getPresentation().setEnabled(true);
602     }
603
604     @Override
605     public void actionPerformed(AnActionEvent e) {
606       BrowserUtil.browse(myUrl);
607     }
608   }
609 }