ddad7ecc36f6fc4f01f90f88ce7d91717abe67b8
[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.*;
29 import com.intellij.openapi.util.SystemInfo;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.ui.HyperlinkLabel;
32 import com.intellij.ui.TextFieldWithHistory;
33 import com.intellij.ui.TextFieldWithHistoryWithBrowseButton;
34 import com.intellij.util.NotNullProducer;
35 import com.intellij.util.PlatformIcons;
36 import com.intellij.util.containers.ComparatorUtil;
37 import com.intellij.util.containers.ContainerUtil;
38 import org.jetbrains.annotations.Nls;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import javax.swing.*;
43 import javax.swing.event.PopupMenuEvent;
44 import javax.swing.event.PopupMenuListener;
45 import javax.swing.table.DefaultTableCellRenderer;
46 import javax.swing.table.TableCellRenderer;
47 import javax.swing.table.TableColumn;
48 import java.awt.*;
49 import java.awt.datatransfer.StringSelection;
50 import java.awt.datatransfer.Transferable;
51 import java.awt.event.ActionListener;
52 import java.util.Collection;
53 import java.util.List;
54 import java.util.Set;
55
56 public class SwingHelper {
57
58   private static final Logger LOG = Logger.getInstance(SwingHelper.class);
59
60   /**
61    * Creates panel whose content consists of given {@code children} components
62    * stacked vertically each on another in a given order.
63    *
64    * @param childAlignmentX Component.LEFT_ALIGNMENT, Component.CENTER_ALIGNMENT or Component.RIGHT_ALIGNMENT
65    * @param children        children components
66    * @return created panel
67    */
68   @NotNull
69   public static JPanel newVerticalPanel(float childAlignmentX, Component... children) {
70     return newGenericBoxPanel(true, childAlignmentX, children);
71   }
72
73   @NotNull
74   public static JPanel newLeftAlignedVerticalPanel(Component... children) {
75     return newVerticalPanel(Component.LEFT_ALIGNMENT, children);
76   }
77
78   @NotNull
79   public static JPanel newLeftAlignedVerticalPanel(@NotNull Collection<Component> children) {
80     return newVerticalPanel(Component.LEFT_ALIGNMENT, children);
81   }
82
83   @NotNull
84   public static JPanel newVerticalPanel(float childAlignmentX, @NotNull Collection<Component> children) {
85     return newVerticalPanel(childAlignmentX, children.toArray(new Component[children.size()]));
86   }
87
88   /**
89    * Creates panel whose content consists of given {@code children} components horizontally
90    * stacked each on another in a given order.
91    *
92    * @param childAlignmentY Component.TOP_ALIGNMENT, Component.CENTER_ALIGNMENT or Component.BOTTOM_ALIGNMENT
93    * @param children        children components
94    * @return created panel
95    */
96   @NotNull
97   public static JPanel newHorizontalPanel(float childAlignmentY, Component... children) {
98     return newGenericBoxPanel(false, childAlignmentY, children);
99   }
100
101   @NotNull
102   public static JPanel newHorizontalPanel(float childAlignmentY, @NotNull Collection<Component> children) {
103     return newHorizontalPanel(childAlignmentY, children.toArray(new Component[children.size()]));
104   }
105
106   private static JPanel newGenericBoxPanel(boolean verticalOrientation,
107                                            float childAlignment,
108                                            Component... children) {
109     JPanel panel = new JPanel();
110     int axis = verticalOrientation ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS;
111     panel.setLayout(new BoxLayout(panel, axis));
112     for (Component child : children) {
113       panel.add(child, childAlignment);
114       if (child instanceof JComponent) {
115         JComponent jChild = (JComponent)child;
116         if (verticalOrientation) {
117           jChild.setAlignmentX(childAlignment);
118         }
119         else {
120           jChild.setAlignmentY(childAlignment);
121         }
122       }
123     }
124     return panel;
125   }
126
127   @NotNull
128   public static JPanel wrapWithoutStretch(@NotNull JComponent component) {
129     JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
130     panel.add(component);
131     return panel;
132   }
133
134   @NotNull
135   public static JPanel wrapWithHorizontalStretch(@NotNull JComponent component) {
136     JPanel panel = new JPanel(new BorderLayout(0, 0));
137     panel.add(component, BorderLayout.NORTH);
138     return panel;
139   }
140
141   public static void setPreferredWidthToFitText(@NotNull TextFieldWithHistoryWithBrowseButton component) {
142     int childWidth = calcWidthToFitText(component.getChildComponent().getTextEditor(), JBUI.scale(32));
143     setPreferredWidthForComponentWithBrowseButton(component, childWidth);
144   }
145
146   public static void setPreferredWidthToFitText(@NotNull TextFieldWithBrowseButton component) {
147     int childWidth = calcWidthToFitText(component.getChildComponent(), JBUI.scale(20));
148     setPreferredWidthForComponentWithBrowseButton(component, childWidth);
149   }
150
151   private static <T extends JComponent> void setPreferredWidthForComponentWithBrowseButton(@NotNull ComponentWithBrowseButton<T> component,
152                                                                                            int childPrefWidth) {
153     Dimension buttonPrefSize = component.getButton().getPreferredSize();
154     setPreferredWidth(component, childPrefWidth + buttonPrefSize.width);
155   }
156
157   public static void setPreferredWidthToFitText(@NotNull JTextField textField) {
158     setPreferredWidthToFitText(textField, JBUI.scale(15));
159   }
160
161   public static void setPreferredWidthToFitText(@NotNull JTextField textField, int additionalWidth) {
162     setPreferredSizeToFitText(textField, StringUtil.notNullize(textField.getText()), additionalWidth);
163   }
164
165   public static void setPreferredWidthToFitText(@NotNull JTextField textField, @NotNull String text) {
166     setPreferredSizeToFitText(textField, text, JBUI.scale(15));
167   }
168
169   private static void setPreferredSizeToFitText(@NotNull JTextField textField, @NotNull String text, int additionalWidth) {
170     int width = calcWidthToFitText(textField, text, additionalWidth);
171     setPreferredWidth(textField, width);
172   }
173
174   private static int calcWidthToFitText(@NotNull JTextField textField, int additionalWidth) {
175     return calcWidthToFitText(textField, textField.getText(), additionalWidth);
176   }
177
178   private static int calcWidthToFitText(@NotNull JTextField textField, @NotNull String text, int additionalWidth) {
179     return textField.getFontMetrics(textField.getFont()).stringWidth(text) + additionalWidth;
180   }
181
182   public static void adjustDialogSizeToFitPreferredSize(@NotNull DialogWrapper dialogWrapper) {
183     JRootPane rootPane = dialogWrapper.getRootPane();
184     Dimension componentSize = rootPane.getSize();
185     Dimension componentPreferredSize = rootPane.getPreferredSize();
186     if (componentPreferredSize.width <= componentSize.width && componentPreferredSize.height <= componentSize.height) {
187       return;
188     }
189     int dw = Math.max(0, componentPreferredSize.width - componentSize.width);
190     int dh = Math.max(0, componentPreferredSize.height - componentSize.height);
191
192     Dimension oldDialogSize = dialogWrapper.getSize();
193     Dimension newDialogSize = new Dimension(oldDialogSize.width + dw, oldDialogSize.height + dh);
194
195     dialogWrapper.setSize(newDialogSize.width, newDialogSize.height);
196     rootPane.revalidate();
197     rootPane.repaint();
198
199     LOG.info("DialogWrapper '" + dialogWrapper.getTitle() + "' has been re-sized (added width: " + dw + ", added height: " + dh + ")");
200   }
201
202   public static <T> void updateItems(@NotNull JComboBox comboBox,
203                                      @NotNull List<T> newItems,
204                                      @Nullable T newSelectedItemIfSelectionCannotBePreserved) {
205     if (!shouldUpdate(comboBox, newItems)) {
206       return;
207     }
208     Object selectedItem = comboBox.getSelectedItem();
209     //noinspection SuspiciousMethodCalls
210     if (selectedItem != null && !newItems.contains(selectedItem)) {
211       selectedItem = null;
212     }
213     if (selectedItem == null && newItems.contains(newSelectedItemIfSelectionCannotBePreserved)) {
214       selectedItem = newSelectedItemIfSelectionCannotBePreserved;
215     }
216     comboBox.removeAllItems();
217     for (T newItem : newItems) {
218       comboBox.addItem(newItem);
219     }
220     if (selectedItem != null) {
221       int count = comboBox.getItemCount();
222       for (int i = 0; i < count; i++) {
223         Object item = comboBox.getItemAt(i);
224         if (selectedItem.equals(item)) {
225           comboBox.setSelectedIndex(i);
226           break;
227         }
228       }
229     }
230   }
231
232   private static <T> boolean shouldUpdate(@NotNull JComboBox comboBox, @NotNull List<T> newItems) {
233     int count = comboBox.getItemCount();
234     if (newItems.size() != count) {
235       return true;
236     }
237     for (int i = 0; i < count; i++) {
238       Object oldItem = comboBox.getItemAt(i);
239       T newItem = newItems.get(i);
240       if (!ComparatorUtil.equalsNullable(oldItem, newItem)) {
241         return true;
242       }
243     }
244     return false;
245   }
246
247   public static void setNoBorderCellRendererFor(@NotNull TableColumn column) {
248     final TableCellRenderer previous = column.getCellRenderer();
249     column.setCellRenderer(new DefaultTableCellRenderer() {
250       @Override
251       public Component getTableCellRendererComponent(JTable table,
252                                                      Object value,
253                                                      boolean isSelected,
254                                                      boolean hasFocus,
255                                                      int row,
256                                                      int column) {
257         Component component;
258         if (previous != null) {
259           component = previous.getTableCellRendererComponent(table, value, isSelected, false, row, column);
260         }
261         else {
262           component = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
263         }
264         if (component instanceof JComponent) {
265           ((JComponent)component).setBorder(null);
266         }
267         return component;
268       }
269     });
270   }
271
272   public static void addHistoryOnExpansion(@NotNull final TextFieldWithHistory textFieldWithHistory,
273                                            @NotNull final NotNullProducer<List<String>> historyProvider) {
274     textFieldWithHistory.addPopupMenuListener(new PopupMenuListener() {
275       @Override
276       public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
277         List<String> history = historyProvider.produce();
278         setHistory(textFieldWithHistory, ContainerUtil.notNullize(history), 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 @Nls(capitalization = Nls.Capitalization.Title) 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 @Nls(capitalization = Nls.Capitalization.Title) 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   public static <T extends JComponent> void doInstall(@Nullable Project project,
364                                                        @NotNull ComponentWithBrowseButton<T> componentWithBrowseButton,
365                                                        @NotNull JTextField textField,
366                                                        @NotNull @Nls(capitalization = Nls.Capitalization.Title) 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 Component 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   @NotNull
570   public static <C extends JComponent> ComponentWithBrowseButton<C> wrapWithInfoButton(@NotNull final C component,
571                                                                                        @NotNull String infoButtonTooltip,
572                                                                                        @NotNull ActionListener listener) {
573     ComponentWithBrowseButton<C> comp = new ComponentWithBrowseButton<C>(component, listener);
574     FixedSizeButton uiHelpButton = comp.getButton();
575     uiHelpButton.setToolTipText(infoButtonTooltip);
576     uiHelpButton.setIcon(UIUtil.getBalloonInformationIcon());
577     uiHelpButton.setHorizontalAlignment(SwingConstants.CENTER);
578     uiHelpButton.setVerticalAlignment(SwingConstants.CENTER);
579     return comp;
580   }
581
582   private static class CopyLinkAction extends AnAction {
583
584     private final String myUrl;
585
586     private CopyLinkAction(@NotNull String url) {
587       super("Copy Link Address", null, PlatformIcons.COPY_ICON);
588       myUrl = url;
589     }
590
591     @Override
592     public void update(AnActionEvent e) {
593       e.getPresentation().setEnabled(true);
594     }
595
596     @Override
597     public void actionPerformed(AnActionEvent e) {
598       Transferable content = new StringSelection(myUrl);
599       CopyPasteManager.getInstance().setContents(content);
600     }
601   }
602
603   private static class OpenLinkInBrowser extends AnAction {
604
605     private final String myUrl;
606
607     private OpenLinkInBrowser(@NotNull String url) {
608       super("Open Link in Browser", null, PlatformIcons.WEB_ICON);
609       myUrl = url;
610     }
611
612     @Override
613     public void update(AnActionEvent e) {
614       e.getPresentation().setEnabled(true);
615     }
616
617     @Override
618     public void actionPerformed(AnActionEvent e) {
619       BrowserUtil.browse(myUrl);
620     }
621   }
622 }