d79e2d659918a5a822b5afedb0276152c6fa085b
[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.application.ApplicationManager;
24 import com.intellij.openapi.application.ModalityState;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
27 import com.intellij.openapi.fileChooser.FileChooserFactory;
28 import com.intellij.openapi.ide.CopyPasteManager;
29 import com.intellij.openapi.options.ex.SingleConfigurableEditor;
30 import com.intellij.openapi.options.newEditor.SettingsDialog;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.ui.*;
33 import com.intellij.openapi.util.SystemInfo;
34 import com.intellij.openapi.util.text.StringUtil;
35 import com.intellij.ui.HyperlinkLabel;
36 import com.intellij.ui.TextFieldWithHistory;
37 import com.intellij.ui.TextFieldWithHistoryWithBrowseButton;
38 import com.intellij.util.Consumer;
39 import com.intellij.util.NotNullProducer;
40 import com.intellij.util.ObjectUtils;
41 import com.intellij.util.PlatformIcons;
42 import com.intellij.util.containers.ComparatorUtil;
43 import com.intellij.util.containers.ContainerUtil;
44 import com.intellij.util.ui.update.UiNotifyConnector;
45 import org.jetbrains.annotations.Nls;
46 import org.jetbrains.annotations.NotNull;
47 import org.jetbrains.annotations.Nullable;
48
49 import javax.swing.*;
50 import javax.swing.event.HyperlinkEvent;
51 import javax.swing.event.HyperlinkListener;
52 import javax.swing.event.PopupMenuEvent;
53 import javax.swing.event.PopupMenuListener;
54 import javax.swing.table.DefaultTableCellRenderer;
55 import javax.swing.table.TableCellRenderer;
56 import javax.swing.table.TableColumn;
57 import java.awt.*;
58 import java.awt.datatransfer.StringSelection;
59 import java.awt.datatransfer.Transferable;
60 import java.awt.event.ActionListener;
61 import java.util.Collection;
62 import java.util.List;
63 import java.util.Set;
64
65 public class SwingHelper {
66
67   private static final Logger LOG = Logger.getInstance(SwingHelper.class);
68   private static final String DIALOG_RESIZED_TO_FIT_TEXT = "INTELLIJ_DIALOG_RESIZED_TO_FIT_TEXT";
69
70   /**
71    * Creates panel whose content consists of given {@code children} components
72    * stacked vertically each on another in a given order.
73    *
74    * @param childAlignmentX Component.LEFT_ALIGNMENT, Component.CENTER_ALIGNMENT or Component.RIGHT_ALIGNMENT
75    * @param children        children components
76    * @return created panel
77    */
78   @NotNull
79   public static JPanel newVerticalPanel(float childAlignmentX, Component... children) {
80     return newGenericBoxPanel(true, childAlignmentX, children);
81   }
82
83   @NotNull
84   public static JPanel newLeftAlignedVerticalPanel(Component... children) {
85     return newVerticalPanel(Component.LEFT_ALIGNMENT, children);
86   }
87
88   @NotNull
89   public static JPanel newLeftAlignedVerticalPanel(@NotNull Collection<Component> children) {
90     return newVerticalPanel(Component.LEFT_ALIGNMENT, children);
91   }
92
93   @NotNull
94   public static JPanel newVerticalPanel(float childAlignmentX, @NotNull Collection<Component> children) {
95     return newVerticalPanel(childAlignmentX, children.toArray(new Component[children.size()]));
96   }
97
98   /**
99    * Creates panel whose content consists of given {@code children} components horizontally
100    * stacked each on another in a given order.
101    *
102    * @param childAlignmentY Component.TOP_ALIGNMENT, Component.CENTER_ALIGNMENT or Component.BOTTOM_ALIGNMENT
103    * @param children        children components
104    * @return created panel
105    */
106   @NotNull
107   public static JPanel newHorizontalPanel(float childAlignmentY, Component... children) {
108     return newGenericBoxPanel(false, childAlignmentY, children);
109   }
110
111   @NotNull
112   public static JPanel newHorizontalPanel(float childAlignmentY, @NotNull Collection<Component> children) {
113     return newHorizontalPanel(childAlignmentY, children.toArray(new Component[children.size()]));
114   }
115
116   private static JPanel newGenericBoxPanel(boolean verticalOrientation,
117                                            float childAlignment,
118                                            Component... children) {
119     JPanel panel = new JPanel();
120     int axis = verticalOrientation ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS;
121     panel.setLayout(new BoxLayout(panel, axis));
122     for (Component child : children) {
123       panel.add(child, childAlignment);
124       if (child instanceof JComponent) {
125         JComponent jChild = (JComponent)child;
126         if (verticalOrientation) {
127           jChild.setAlignmentX(childAlignment);
128         }
129         else {
130           jChild.setAlignmentY(childAlignment);
131         }
132       }
133     }
134     return panel;
135   }
136
137   @NotNull
138   public static JPanel wrapWithoutStretch(@NotNull JComponent component) {
139     JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
140     panel.add(component);
141     return panel;
142   }
143
144   @NotNull
145   public static JPanel wrapWithHorizontalStretch(@NotNull JComponent component) {
146     JPanel panel = new JPanel(new BorderLayout(0, 0));
147     panel.add(component, BorderLayout.NORTH);
148     return panel;
149   }
150
151   public static void setPreferredWidthToFitText(@NotNull TextFieldWithHistoryWithBrowseButton component) {
152     int childWidth = calcWidthToFitText(component.getChildComponent().getTextEditor(), JBUI.scale(32));
153     setPreferredWidthForComponentWithBrowseButton(component, childWidth);
154   }
155
156   public static void setPreferredWidthToFitText(@NotNull TextFieldWithBrowseButton component) {
157     int childWidth = calcWidthToFitText(component.getChildComponent(), JBUI.scale(20));
158     setPreferredWidthForComponentWithBrowseButton(component, childWidth);
159   }
160
161   private static <T extends JComponent> void setPreferredWidthForComponentWithBrowseButton(@NotNull ComponentWithBrowseButton<T> component,
162                                                                                            int childPrefWidth) {
163     Dimension buttonPrefSize = component.getButton().getPreferredSize();
164     setPreferredWidth(component, childPrefWidth + buttonPrefSize.width);
165   }
166
167   public static void setPreferredWidthToFitText(@NotNull JTextField textField) {
168     setPreferredWidthToFitText(textField, JBUI.scale(15));
169   }
170
171   public static void setPreferredWidthToFitText(@NotNull JTextField textField, int additionalWidth) {
172     setPreferredSizeToFitText(textField, StringUtil.notNullize(textField.getText()), additionalWidth);
173   }
174
175   public static void setPreferredWidthToFitText(@NotNull JTextField textField, @NotNull String text) {
176     setPreferredSizeToFitText(textField, text, JBUI.scale(15));
177   }
178
179   private static void setPreferredSizeToFitText(@NotNull JTextField textField, @NotNull String text, int additionalWidth) {
180     int width = calcWidthToFitText(textField, text, additionalWidth);
181     setPreferredWidth(textField, width);
182   }
183
184   private static int calcWidthToFitText(@NotNull JTextField textField, int additionalWidth) {
185     return calcWidthToFitText(textField, textField.getText(), additionalWidth);
186   }
187
188   private static int calcWidthToFitText(@NotNull JTextField textField, @NotNull String text, int additionalWidth) {
189     return textField.getFontMetrics(textField.getFont()).stringWidth(text) + additionalWidth;
190   }
191
192   public static void adjustDialogSizeToFitPreferredSize(@NotNull DialogWrapper dialogWrapper) {
193     JRootPane rootPane = dialogWrapper.getRootPane();
194     Dimension componentSize = rootPane.getSize();
195     Dimension componentPreferredSize = rootPane.getPreferredSize();
196     if (componentPreferredSize.width <= componentSize.width && componentPreferredSize.height <= componentSize.height) {
197       return;
198     }
199     int dw = Math.max(0, componentPreferredSize.width - componentSize.width);
200     int dh = Math.max(0, componentPreferredSize.height - componentSize.height);
201
202     Dimension oldDialogSize = dialogWrapper.getSize();
203     Dimension newDialogSize = new Dimension(oldDialogSize.width + dw, oldDialogSize.height + dh);
204
205     dialogWrapper.setSize(newDialogSize.width, newDialogSize.height);
206     rootPane.revalidate();
207     rootPane.repaint();
208
209     LOG.info("DialogWrapper '" + dialogWrapper.getTitle() + "' has been re-sized (added width: " + dw + ", added height: " + dh + ")");
210   }
211
212   public static void resizeDialogToFitTextFor(@NotNull final JComponent... components) {
213     if (components.length == 0) return;
214     doWithDialogWrapper(components[0], new Consumer<DialogWrapper>() {
215       @Override
216       public void consume(final DialogWrapper dialogWrapper) {
217         if (dialogWrapper instanceof SettingsDialog || dialogWrapper instanceof SingleConfigurableEditor) {
218           for (Component component : components) {
219             if (component instanceof TextFieldWithHistoryWithBrowseButton) {
220               setPreferredWidthToFitText((TextFieldWithHistoryWithBrowseButton)component);
221             }
222             else if (component instanceof TextFieldWithBrowseButton) {
223               setPreferredWidthToFitText((TextFieldWithBrowseButton)component);
224             }
225             else if (component instanceof JTextField) {
226               setPreferredWidthToFitText((JTextField)component);
227             }
228           }
229           ApplicationManager.getApplication().invokeLater(new Runnable() {
230             @Override
231             public void run() {
232               adjustDialogSizeToFitPreferredSize(dialogWrapper);
233             }
234           }, ModalityState.any());
235         }
236       }
237     });
238   }
239
240   private static void doWithDialogWrapper(@NotNull final JComponent component, @NotNull final Consumer<DialogWrapper> consumer) {
241     UIUtil.invokeLaterIfNeeded(new Runnable() {
242       @Override
243       public void run() {
244         if (component.getClientProperty(DIALOG_RESIZED_TO_FIT_TEXT) != null) {
245           return;
246         }
247         component.putClientProperty(DIALOG_RESIZED_TO_FIT_TEXT, true);
248         DialogWrapper dialogWrapper = DialogWrapper.findInstance(component);
249         if (dialogWrapper != null) {
250           consumer.consume(dialogWrapper);
251         }
252         else {
253           UiNotifyConnector.doWhenFirstShown(component, new Runnable() {
254             @Override
255             public void run() {
256               DialogWrapper dialogWrapper = DialogWrapper.findInstance(component);
257               if (dialogWrapper != null) {
258                 consumer.consume(dialogWrapper);
259               }
260             }
261           });
262         }
263       }
264     });
265   }
266
267   public static <T> void updateItems(@NotNull JComboBox<T> comboBox,
268                                      @NotNull List<T> newItems,
269                                      @Nullable T newSelectedItemIfSelectionCannotBePreserved) {
270     if (!shouldUpdate(comboBox, newItems)) {
271       return;
272     }
273     Object selectedItem = comboBox.getSelectedItem();
274     //noinspection SuspiciousMethodCalls
275     if (selectedItem != null && !newItems.contains(selectedItem)) {
276       selectedItem = null;
277     }
278     if (selectedItem == null && newItems.contains(newSelectedItemIfSelectionCannotBePreserved)) {
279       selectedItem = newSelectedItemIfSelectionCannotBePreserved;
280     }
281     comboBox.removeAllItems();
282     for (T newItem : newItems) {
283       comboBox.addItem(newItem);
284     }
285     if (selectedItem != null) {
286       int count = comboBox.getItemCount();
287       for (int i = 0; i < count; i++) {
288         Object item = comboBox.getItemAt(i);
289         if (selectedItem.equals(item)) {
290           comboBox.setSelectedIndex(i);
291           break;
292         }
293       }
294     }
295   }
296
297   private static <T> boolean shouldUpdate(@NotNull JComboBox<T> comboBox, @NotNull List<T> newItems) {
298     int count = comboBox.getItemCount();
299     if (newItems.size() != count) {
300       return true;
301     }
302     for (int i = 0; i < count; i++) {
303       Object oldItem = comboBox.getItemAt(i);
304       T newItem = newItems.get(i);
305       if (!ComparatorUtil.equalsNullable(oldItem, newItem)) {
306         return true;
307       }
308     }
309     return false;
310   }
311
312   public static void setNoBorderCellRendererFor(@NotNull TableColumn column) {
313     final TableCellRenderer previous = column.getCellRenderer();
314     column.setCellRenderer(new DefaultTableCellRenderer() {
315       @Override
316       public Component getTableCellRendererComponent(JTable table,
317                                                      Object value,
318                                                      boolean isSelected,
319                                                      boolean hasFocus,
320                                                      int row,
321                                                      int column) {
322         Component component;
323         if (previous != null) {
324           component = previous.getTableCellRendererComponent(table, value, isSelected, false, row, column);
325         }
326         else {
327           component = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
328         }
329         if (component instanceof JComponent) {
330           ((JComponent)component).setBorder(null);
331         }
332         return component;
333       }
334     });
335   }
336
337   public static void addHistoryOnExpansion(@NotNull final TextFieldWithHistory textFieldWithHistory,
338                                            @NotNull final NotNullProducer<List<String>> historyProvider) {
339     textFieldWithHistory.addPopupMenuListener(new PopupMenuListener() {
340       @Override
341       public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
342         List<String> history = historyProvider.produce();
343         setHistory(textFieldWithHistory, ContainerUtil.notNullize(history), true);
344         // one-time initialization
345         textFieldWithHistory.removePopupMenuListener(this);
346       }
347
348       @Override
349       public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
350       }
351
352       @Override
353       public void popupMenuCanceled(PopupMenuEvent e) {
354       }
355     });
356   }
357
358   public static void setHistory(@NotNull TextFieldWithHistory textFieldWithHistory,
359                                 @NotNull List<String> history,
360                                 boolean mergeWithPrevHistory) {
361     Set<String> newHistorySet = ContainerUtil.newHashSet(history);
362     List<String> prevHistory = textFieldWithHistory.getHistory();
363     List<String> mergedHistory = ContainerUtil.newArrayListWithCapacity(history.size());
364     if (mergeWithPrevHistory) {
365       for (String item : prevHistory) {
366         if (!newHistorySet.contains(item)) {
367           mergedHistory.add(item);
368         }
369       }
370     }
371     mergedHistory.addAll(history);
372     String oldText = StringUtil.notNullize(textFieldWithHistory.getText());
373     String oldSelectedItem = ObjectUtils.tryCast(textFieldWithHistory.getSelectedItem(), String.class);
374     if (!mergedHistory.contains(oldSelectedItem)) {
375       oldSelectedItem = null;
376     }
377     textFieldWithHistory.setHistory(mergedHistory);
378     setLongestAsPrototype(textFieldWithHistory, mergedHistory);
379     if (oldSelectedItem != null) {
380       textFieldWithHistory.setSelectedItem(oldSelectedItem);
381     }
382     if (!oldText.equals(oldSelectedItem)) {
383       textFieldWithHistory.setText(oldText);
384     }
385   }
386
387   private static void setLongestAsPrototype(@NotNull JComboBox comboBox, @NotNull List<String> variants) {
388     Object prototypeDisplayValue = comboBox.getPrototypeDisplayValue();
389     String prototypeDisplayValueStr = null;
390     if (prototypeDisplayValue instanceof String) {
391       prototypeDisplayValueStr = (String)prototypeDisplayValue;
392     }
393     else if (prototypeDisplayValue != null) {
394       return;
395     }
396     String longest = StringUtil.notNullize(prototypeDisplayValueStr);
397     boolean updated = false;
398     for (String s : variants) {
399       if (longest.length() < s.length()) {
400         longest = s;
401         updated = true;
402       }
403     }
404     if (updated) {
405       comboBox.setPrototypeDisplayValue(longest);
406     }
407   }
408
409   public static void installFileCompletionAndBrowseDialog(@Nullable Project project,
410                                                           @NotNull TextFieldWithHistoryWithBrowseButton textFieldWithHistoryWithBrowseButton,
411                                                           @NotNull @Nls(capitalization = Nls.Capitalization.Title) String browseDialogTitle,
412                                                           @NotNull FileChooserDescriptor fileChooserDescriptor) {
413     doInstall(project,
414               textFieldWithHistoryWithBrowseButton,
415               textFieldWithHistoryWithBrowseButton.getChildComponent().getTextEditor(),
416               browseDialogTitle,
417               fileChooserDescriptor,
418               TextComponentAccessor.TEXT_FIELD_WITH_HISTORY_WHOLE_TEXT);
419   }
420
421   public static void installFileCompletionAndBrowseDialog(@Nullable Project project,
422                                                           @NotNull TextFieldWithBrowseButton textFieldWithBrowseButton,
423                                                           @NotNull @Nls(capitalization = Nls.Capitalization.Title) String browseDialogTitle,
424                                                           @NotNull FileChooserDescriptor fileChooserDescriptor) {
425     doInstall(project,
426               textFieldWithBrowseButton,
427               textFieldWithBrowseButton.getTextField(),
428               browseDialogTitle,
429               fileChooserDescriptor,
430               TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
431   }
432
433   private static <T extends JComponent> void doInstall(@Nullable Project project,
434                                                        @NotNull ComponentWithBrowseButton<T> componentWithBrowseButton,
435                                                        @NotNull JTextField textField,
436                                                        @NotNull @Nls(capitalization = Nls.Capitalization.Title) String browseDialogTitle,
437                                                        @NotNull FileChooserDescriptor fileChooserDescriptor,
438                                                        @NotNull TextComponentAccessor<T> textComponentAccessor) {
439     fileChooserDescriptor = fileChooserDescriptor.withShowHiddenFiles(SystemInfo.isUnix);
440     componentWithBrowseButton.addBrowseFolderListener(
441       project,
442       new ComponentWithBrowseButton.BrowseFolderActionListener<T>(
443         browseDialogTitle,
444         null,
445         componentWithBrowseButton,
446         project,
447         fileChooserDescriptor,
448         textComponentAccessor
449       ),
450       true
451     );
452     FileChooserFactory.getInstance().installFileCompletion(
453       textField,
454       fileChooserDescriptor,
455       true,
456       project
457     );
458   }
459
460   @NotNull
461   public static HyperlinkLabel createWebHyperlink(@NotNull String url) {
462     return createWebHyperlink(url, url);
463   }
464
465   @NotNull
466   public static HyperlinkLabel createWebHyperlink(@NotNull String text, @NotNull String url) {
467     HyperlinkLabel hyperlink = new HyperlinkLabel(text);
468     hyperlink.setHyperlinkTarget(url);
469
470     DefaultActionGroup actionGroup = new DefaultActionGroup();
471     actionGroup.add(new OpenLinkInBrowser(url));
472     actionGroup.add(new CopyLinkAction(url));
473
474     hyperlink.setComponentPopupMenu(ActionManager.getInstance().createActionPopupMenu("web hyperlink", actionGroup).getComponent());
475     return hyperlink;
476   }
477
478   public static void setPreferredWidth(@NotNull Component component, int width) {
479     Dimension preferredSize = component.getPreferredSize();
480     preferredSize.width = width;
481     component.setPreferredSize(preferredSize);
482   }
483
484   public static class HtmlViewerBuilder {
485     private boolean myCarryTextOver;
486     private String myDisabledHtml;
487     private Font myFont;
488     private Color myBackground;
489     private Color myForeground;
490
491     public JEditorPane create() {
492       final JEditorPane textPane = new JEditorPane() {
493         private boolean myEnabled = true;
494         private String myEnabledHtml;
495
496         @Override
497         public Dimension getPreferredSize() {
498           // This trick makes text component to carry text over to the next line
499           // if the text line width exceeds parent's width
500           Dimension dimension = super.getPreferredSize();
501           if (myCarryTextOver) {
502             dimension.width = 0;
503           }
504           return dimension;
505         }
506
507         @Override
508         public void setText(String t) {
509           if (myDisabledHtml != null) {
510             if (myEnabled) {
511               myEnabledHtml = t;
512             }
513           }
514           super.setText(t);
515         }
516
517         @Override
518         public void setEnabled(boolean enabled) {
519           if (myDisabledHtml != null) {
520             myEnabled = enabled;
521             if (myEnabled) {
522               setText(myEnabledHtml);
523             } else {
524               setText(myDisabledHtml);
525             }
526             super.setEnabled(true);
527           } else {
528             super.setEnabled(enabled);
529           }
530         }
531       };
532       textPane.setFont(myFont != null ? myFont : UIUtil.getLabelFont());
533       textPane.setContentType(UIUtil.HTML_MIME);
534       textPane.setEditable(false);
535       if (myBackground != null) {
536         textPane.setBackground(myBackground);
537       }
538       else {
539         textPane.setOpaque(false);
540       }
541       textPane.setForeground(myForeground != null ? myForeground : UIUtil.getLabelForeground());
542       textPane.setFocusable(false);
543       return textPane;
544     }
545
546     public HtmlViewerBuilder setCarryTextOver(boolean carryTextOver) {
547       myCarryTextOver = carryTextOver;
548       return this;
549     }
550
551     public HtmlViewerBuilder setDisabledHtml(String disabledHtml) {
552       myDisabledHtml = disabledHtml;
553       return this;
554     }
555
556     public HtmlViewerBuilder setFont(Font font) {
557       myFont = font;
558       return this;
559     }
560
561     public HtmlViewerBuilder setBackground(Color background) {
562       myBackground = background;
563       return this;
564     }
565
566     public HtmlViewerBuilder setForeground(Color foreground) {
567       myForeground = foreground;
568       return this;
569     }
570   }
571
572   @NotNull
573   public static JEditorPane createHtmlViewer(boolean carryTextOver,
574                                              @Nullable Font font,
575                                              @Nullable Color background,
576                                              @Nullable Color foreground) {
577     final JEditorPane textPane;
578     if (carryTextOver) {
579       textPane = new JEditorPane() {
580         @Override
581         public Dimension getPreferredSize() {
582           // This trick makes text component to carry text over to the next line
583           // if the text line width exceeds parent's width
584           Dimension dimension = super.getPreferredSize();
585           dimension.width = 0;
586           return dimension;
587         }
588       };
589     }
590     else {
591       textPane = new JEditorPane();
592     }
593     textPane.setFont(font != null ? font : UIUtil.getLabelFont());
594     textPane.setContentType(UIUtil.HTML_MIME);
595     textPane.setEditable(false);
596     if (background != null) {
597       textPane.setBackground(background);
598     }
599     else {
600       textPane.setOpaque(false);
601     }
602     textPane.setForeground(foreground != null ? foreground : UIUtil.getLabelForeground());
603     textPane.setFocusable(false);
604     return textPane;
605   }
606
607   public static void setHtml(@NotNull JEditorPane editorPane,
608                              @NotNull String bodyInnerHtml,
609                              @Nullable Color foregroundColor) {
610     String html = String.format(
611       "<html><head>%s</head><body>%s</body></html>",
612       UIUtil.getCssFontDeclaration(editorPane.getFont(), foregroundColor, null, null),
613       bodyInnerHtml
614     );
615     editorPane.setText(html);
616   }
617
618   @NotNull
619   public static TextFieldWithHistoryWithBrowseButton createTextFieldWithHistoryWithBrowseButton(@Nullable Project project,
620                                                                                                 @NotNull String browseDialogTitle,
621                                                                                                 @NotNull FileChooserDescriptor fileChooserDescriptor,
622                                                                                                 @Nullable NotNullProducer<List<String>> historyProvider) {
623     TextFieldWithHistoryWithBrowseButton textFieldWithHistoryWithBrowseButton = new TextFieldWithHistoryWithBrowseButton();
624     TextFieldWithHistory textFieldWithHistory = textFieldWithHistoryWithBrowseButton.getChildComponent();
625     textFieldWithHistory.setHistorySize(-1);
626     textFieldWithHistory.setMinimumAndPreferredWidth(0);
627     if (historyProvider != null) {
628       addHistoryOnExpansion(textFieldWithHistory, historyProvider);
629     }
630     installFileCompletionAndBrowseDialog(
631       project,
632       textFieldWithHistoryWithBrowseButton,
633       browseDialogTitle,
634       fileChooserDescriptor
635     );
636     return textFieldWithHistoryWithBrowseButton;
637   }
638
639   @NotNull
640   public static <C extends JComponent> ComponentWithBrowseButton<C> wrapWithInfoButton(@NotNull final C component,
641                                                                                        @NotNull String infoButtonTooltip,
642                                                                                        @NotNull ActionListener listener) {
643     ComponentWithBrowseButton<C> comp = new ComponentWithBrowseButton<C>(component, listener);
644     FixedSizeButton uiHelpButton = comp.getButton();
645     uiHelpButton.setToolTipText(infoButtonTooltip);
646     uiHelpButton.setIcon(UIUtil.getBalloonInformationIcon());
647     uiHelpButton.setHorizontalAlignment(SwingConstants.CENTER);
648     uiHelpButton.setVerticalAlignment(SwingConstants.CENTER);
649     return comp;
650   }
651
652   private static class CopyLinkAction extends AnAction {
653
654     private final String myUrl;
655
656     private CopyLinkAction(@NotNull String url) {
657       super("Copy Link Address", null, PlatformIcons.COPY_ICON);
658       myUrl = url;
659     }
660
661     @Override
662     public void update(AnActionEvent e) {
663       e.getPresentation().setEnabled(true);
664     }
665
666     @Override
667     public void actionPerformed(AnActionEvent e) {
668       Transferable content = new StringSelection(myUrl);
669       CopyPasteManager.getInstance().setContents(content);
670     }
671   }
672
673   private static class OpenLinkInBrowser extends AnAction {
674
675     private final String myUrl;
676
677     private OpenLinkInBrowser(@NotNull String url) {
678       super("Open Link in Browser", null, PlatformIcons.WEB_ICON);
679       myUrl = url;
680     }
681
682     @Override
683     public void update(AnActionEvent e) {
684       e.getPresentation().setEnabled(true);
685     }
686
687     @Override
688     public void actionPerformed(AnActionEvent e) {
689       BrowserUtil.browse(myUrl);
690     }
691   }
692
693   public final static String ELLIPSIS = "...";
694   public static final String ERROR_STR = "www";
695   public static String truncateStringWithEllipsis(final String text, final int maxWidth, final FontMetrics fm) {
696     return truncateStringWithEllipsis(text, maxWidth, new WidthCalculator() {
697       @Override
698       public int stringWidth(String s) {
699         return fm.stringWidth(s);
700       }
701
702       @Override
703       public int charWidth(char c) {
704         return fm.charWidth(c);
705       }
706     });
707   }
708
709   public interface WidthCalculator {
710     int stringWidth(final String s);
711     int charWidth(final char c);
712   }
713
714   public static String truncateStringWithEllipsis(@NotNull final String text, final int maxWidth, final WidthCalculator fm) {
715     final int error = fm.stringWidth(ERROR_STR);
716     final int wholeWidth = fm.stringWidth(text) + error;
717     if (wholeWidth <= maxWidth || text.isEmpty()) return text;
718     final int ellipsisWidth = fm.stringWidth(ELLIPSIS) + error; // plus some reserve
719     if (ellipsisWidth >= maxWidth) return ELLIPSIS;
720
721     final int availableWidth = maxWidth - ellipsisWidth;
722     int currentLen = (int)Math.floor(availableWidth / (((double) wholeWidth) / text.length()));
723
724     final String currentSubstring = text.substring(0, currentLen);
725     int realWidth = fm.stringWidth(currentSubstring);
726
727     if (realWidth >= availableWidth) {
728       int delta = 0;
729       for (int i = currentLen - 1; i >= 0; i--) {
730         if ((realWidth - delta) < availableWidth) return text.substring(0, i) + ELLIPSIS;
731         delta += fm.charWidth(currentSubstring.charAt(i));
732       }
733       return text.substring(0, 1) + ELLIPSIS;
734     } else {
735       int delta = 0;
736       for (int i = currentLen; i < text.length(); i++) {
737         if ((realWidth + delta) >= availableWidth) return text.substring(0, i) + ELLIPSIS;
738         delta += fm.charWidth(text.charAt(i));
739       }
740       return text.substring(0, currentLen) + ELLIPSIS;
741     }
742   }
743
744   public static JEditorPane createHtmlLabel(@NotNull final String innerHtml, @Nullable String disabledHtml,
745                                             @Nullable final Consumer<String> hyperlinkListener) {
746     disabledHtml = disabledHtml == null ? innerHtml : disabledHtml;
747     final Font font = UIUtil.getLabelFont();
748     String html = String.format(
749       "<html><head>%s</head><body>%s</body></html>",
750       UIUtil.getCssFontDeclaration(font, UIUtil.getInactiveTextColor(), null, null),
751       innerHtml
752     );
753     String disabled = String.format(
754       "<html><head>%s</head><body>%s</body></html>",
755       UIUtil.getCssFontDeclaration(font, UIUtil.getInactiveTextColor(), null, null),
756       disabledHtml
757     );
758
759     final JEditorPane pane = new SwingHelper.HtmlViewerBuilder()
760       .setCarryTextOver(false)
761       .setFont(UIUtil.getLabelFont())
762       .setDisabledHtml(disabled)
763       .create();
764     pane.setText(html);
765     pane.addHyperlinkListener(
766       new HyperlinkListener() {
767         public void hyperlinkUpdate(HyperlinkEvent e) {
768           if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
769             if (hyperlinkListener != null) hyperlinkListener.consume(e.getURL() == null ? "" : e.getURL().toString());
770             else BrowserUtil.browse(e.getURL());
771           }
772         }
773       }
774     );
775     return pane;
776   }
777 }