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