support preserving `null` selected value when updating combobox drop-down list
[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 itemToSelect = comboBox.getSelectedItem();
274     boolean preserveSelection = true;
275     //noinspection SuspiciousMethodCalls
276     if (!newItems.contains(itemToSelect)) {
277       if (newItems.contains(newSelectedItemIfSelectionCannotBePreserved)) {
278         itemToSelect = newSelectedItemIfSelectionCannotBePreserved;
279       }
280       else {
281         itemToSelect = null;
282         preserveSelection = false;
283       }
284     }
285     comboBox.removeAllItems();
286     for (T newItem : newItems) {
287       comboBox.addItem(newItem);
288     }
289     if (preserveSelection) {
290       int count = comboBox.getItemCount();
291       for (int i = 0; i < count; i++) {
292         Object item = comboBox.getItemAt(i);
293         if (ComparatorUtil.equalsNullable(itemToSelect, item)) {
294           comboBox.setSelectedIndex(i);
295           break;
296         }
297       }
298     }
299   }
300
301   private static <T> boolean shouldUpdate(@NotNull JComboBox<T> comboBox, @NotNull List<T> newItems) {
302     int count = comboBox.getItemCount();
303     if (newItems.size() != count) {
304       return true;
305     }
306     for (int i = 0; i < count; i++) {
307       Object oldItem = comboBox.getItemAt(i);
308       T newItem = newItems.get(i);
309       if (!ComparatorUtil.equalsNullable(oldItem, newItem)) {
310         return true;
311       }
312     }
313     return false;
314   }
315
316   public static void setNoBorderCellRendererFor(@NotNull TableColumn column) {
317     final TableCellRenderer previous = column.getCellRenderer();
318     column.setCellRenderer(new DefaultTableCellRenderer() {
319       @Override
320       public Component getTableCellRendererComponent(JTable table,
321                                                      Object value,
322                                                      boolean isSelected,
323                                                      boolean hasFocus,
324                                                      int row,
325                                                      int column) {
326         Component component;
327         if (previous != null) {
328           component = previous.getTableCellRendererComponent(table, value, isSelected, false, row, column);
329         }
330         else {
331           component = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
332         }
333         if (component instanceof JComponent) {
334           ((JComponent)component).setBorder(null);
335         }
336         return component;
337       }
338     });
339   }
340
341   public static void addHistoryOnExpansion(@NotNull final TextFieldWithHistory textFieldWithHistory,
342                                            @NotNull final NotNullProducer<List<String>> historyProvider) {
343     textFieldWithHistory.addPopupMenuListener(new PopupMenuListener() {
344       @Override
345       public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
346         List<String> history = historyProvider.produce();
347         setHistory(textFieldWithHistory, ContainerUtil.notNullize(history), true);
348         // one-time initialization
349         textFieldWithHistory.removePopupMenuListener(this);
350       }
351
352       @Override
353       public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
354       }
355
356       @Override
357       public void popupMenuCanceled(PopupMenuEvent e) {
358       }
359     });
360   }
361
362   public static void setHistory(@NotNull TextFieldWithHistory textFieldWithHistory,
363                                 @NotNull List<String> history,
364                                 boolean mergeWithPrevHistory) {
365     Set<String> newHistorySet = ContainerUtil.newHashSet(history);
366     List<String> prevHistory = textFieldWithHistory.getHistory();
367     List<String> mergedHistory = ContainerUtil.newArrayListWithCapacity(history.size());
368     if (mergeWithPrevHistory) {
369       for (String item : prevHistory) {
370         if (!newHistorySet.contains(item)) {
371           mergedHistory.add(item);
372         }
373       }
374     }
375     mergedHistory.addAll(history);
376     String oldText = StringUtil.notNullize(textFieldWithHistory.getText());
377     String oldSelectedItem = ObjectUtils.tryCast(textFieldWithHistory.getSelectedItem(), String.class);
378     if (!mergedHistory.contains(oldSelectedItem)) {
379       oldSelectedItem = null;
380     }
381     textFieldWithHistory.setHistory(mergedHistory);
382     setLongestAsPrototype(textFieldWithHistory, mergedHistory);
383     if (oldSelectedItem != null) {
384       textFieldWithHistory.setSelectedItem(oldSelectedItem);
385     }
386     if (!oldText.equals(oldSelectedItem)) {
387       textFieldWithHistory.setText(oldText);
388     }
389   }
390
391   private static void setLongestAsPrototype(@NotNull JComboBox comboBox, @NotNull List<String> variants) {
392     Object prototypeDisplayValue = comboBox.getPrototypeDisplayValue();
393     String prototypeDisplayValueStr = null;
394     if (prototypeDisplayValue instanceof String) {
395       prototypeDisplayValueStr = (String)prototypeDisplayValue;
396     }
397     else if (prototypeDisplayValue != null) {
398       return;
399     }
400     String longest = StringUtil.notNullize(prototypeDisplayValueStr);
401     boolean updated = false;
402     for (String s : variants) {
403       if (longest.length() < s.length()) {
404         longest = s;
405         updated = true;
406       }
407     }
408     if (updated) {
409       comboBox.setPrototypeDisplayValue(longest);
410     }
411   }
412
413   public static void installFileCompletionAndBrowseDialog(@Nullable Project project,
414                                                           @NotNull TextFieldWithHistoryWithBrowseButton textFieldWithHistoryWithBrowseButton,
415                                                           @NotNull @Nls(capitalization = Nls.Capitalization.Title) String browseDialogTitle,
416                                                           @NotNull FileChooserDescriptor fileChooserDescriptor) {
417     doInstall(project,
418               textFieldWithHistoryWithBrowseButton,
419               textFieldWithHistoryWithBrowseButton.getChildComponent().getTextEditor(),
420               browseDialogTitle,
421               fileChooserDescriptor,
422               TextComponentAccessor.TEXT_FIELD_WITH_HISTORY_WHOLE_TEXT);
423   }
424
425   public static void installFileCompletionAndBrowseDialog(@Nullable Project project,
426                                                           @NotNull TextFieldWithBrowseButton textFieldWithBrowseButton,
427                                                           @NotNull @Nls(capitalization = Nls.Capitalization.Title) String browseDialogTitle,
428                                                           @NotNull FileChooserDescriptor fileChooserDescriptor) {
429     doInstall(project,
430               textFieldWithBrowseButton,
431               textFieldWithBrowseButton.getTextField(),
432               browseDialogTitle,
433               fileChooserDescriptor,
434               TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
435   }
436
437   private static <T extends JComponent> void doInstall(@Nullable Project project,
438                                                        @NotNull ComponentWithBrowseButton<T> componentWithBrowseButton,
439                                                        @NotNull JTextField textField,
440                                                        @NotNull @Nls(capitalization = Nls.Capitalization.Title) String browseDialogTitle,
441                                                        @NotNull FileChooserDescriptor fileChooserDescriptor,
442                                                        @NotNull TextComponentAccessor<T> textComponentAccessor) {
443     fileChooserDescriptor = fileChooserDescriptor.withShowHiddenFiles(SystemInfo.isUnix);
444     componentWithBrowseButton.addBrowseFolderListener(
445       project,
446       new ComponentWithBrowseButton.BrowseFolderActionListener<T>(
447         browseDialogTitle,
448         null,
449         componentWithBrowseButton,
450         project,
451         fileChooserDescriptor,
452         textComponentAccessor
453       ),
454       true
455     );
456     FileChooserFactory.getInstance().installFileCompletion(
457       textField,
458       fileChooserDescriptor,
459       true,
460       project
461     );
462   }
463
464   @NotNull
465   public static HyperlinkLabel createWebHyperlink(@NotNull String url) {
466     return createWebHyperlink(url, url);
467   }
468
469   @NotNull
470   public static HyperlinkLabel createWebHyperlink(@NotNull String text, @NotNull String url) {
471     HyperlinkLabel hyperlink = new HyperlinkLabel(text);
472     hyperlink.setHyperlinkTarget(url);
473
474     DefaultActionGroup actionGroup = new DefaultActionGroup();
475     actionGroup.add(new OpenLinkInBrowser(url));
476     actionGroup.add(new CopyLinkAction(url));
477
478     hyperlink.setComponentPopupMenu(ActionManager.getInstance().createActionPopupMenu("web hyperlink", actionGroup).getComponent());
479     return hyperlink;
480   }
481
482   public static void setPreferredWidth(@NotNull Component component, int width) {
483     Dimension preferredSize = component.getPreferredSize();
484     preferredSize.width = width;
485     component.setPreferredSize(preferredSize);
486   }
487
488   public static class HtmlViewerBuilder {
489     private boolean myCarryTextOver;
490     private String myDisabledHtml;
491     private Font myFont;
492     private Color myBackground;
493     private Color myForeground;
494
495     public JEditorPane create() {
496       final JEditorPane textPane = new JEditorPane() {
497         private boolean myEnabled = true;
498         private String myEnabledHtml;
499
500         @Override
501         public Dimension getPreferredSize() {
502           // This trick makes text component to carry text over to the next line
503           // if the text line width exceeds parent's width
504           Dimension dimension = super.getPreferredSize();
505           if (myCarryTextOver) {
506             dimension.width = 0;
507           }
508           return dimension;
509         }
510
511         @Override
512         public void setText(String t) {
513           if (myDisabledHtml != null) {
514             if (myEnabled) {
515               myEnabledHtml = t;
516             }
517           }
518           super.setText(t);
519         }
520
521         @Override
522         public void setEnabled(boolean enabled) {
523           if (myDisabledHtml != null) {
524             myEnabled = enabled;
525             if (myEnabled) {
526               setText(myEnabledHtml);
527             } else {
528               setText(myDisabledHtml);
529             }
530             super.setEnabled(true);
531           } else {
532             super.setEnabled(enabled);
533           }
534         }
535       };
536       textPane.setFont(myFont != null ? myFont : UIUtil.getLabelFont());
537       textPane.setContentType(UIUtil.HTML_MIME);
538       textPane.setEditable(false);
539       if (myBackground != null) {
540         textPane.setBackground(myBackground);
541       }
542       else {
543         textPane.setOpaque(false);
544       }
545       textPane.setForeground(myForeground != null ? myForeground : UIUtil.getLabelForeground());
546       textPane.setFocusable(false);
547       return textPane;
548     }
549
550     public HtmlViewerBuilder setCarryTextOver(boolean carryTextOver) {
551       myCarryTextOver = carryTextOver;
552       return this;
553     }
554
555     public HtmlViewerBuilder setDisabledHtml(String disabledHtml) {
556       myDisabledHtml = disabledHtml;
557       return this;
558     }
559
560     public HtmlViewerBuilder setFont(Font font) {
561       myFont = font;
562       return this;
563     }
564
565     public HtmlViewerBuilder setBackground(Color background) {
566       myBackground = background;
567       return this;
568     }
569
570     public HtmlViewerBuilder setForeground(Color foreground) {
571       myForeground = foreground;
572       return this;
573     }
574   }
575
576   @NotNull
577   public static JEditorPane createHtmlViewer(boolean carryTextOver,
578                                              @Nullable Font font,
579                                              @Nullable Color background,
580                                              @Nullable Color foreground) {
581     final JEditorPane textPane;
582     if (carryTextOver) {
583       textPane = new JEditorPane() {
584         @Override
585         public Dimension getPreferredSize() {
586           // This trick makes text component to carry text over to the next line
587           // if the text line width exceeds parent's width
588           Dimension dimension = super.getPreferredSize();
589           dimension.width = 0;
590           return dimension;
591         }
592       };
593     }
594     else {
595       textPane = new JEditorPane();
596     }
597     textPane.setFont(font != null ? font : UIUtil.getLabelFont());
598     textPane.setContentType(UIUtil.HTML_MIME);
599     textPane.setEditable(false);
600     if (background != null) {
601       textPane.setBackground(background);
602     }
603     else {
604       textPane.setOpaque(false);
605     }
606     textPane.setForeground(foreground != null ? foreground : UIUtil.getLabelForeground());
607     textPane.setFocusable(false);
608     return textPane;
609   }
610
611   public static void setHtml(@NotNull JEditorPane editorPane,
612                              @NotNull String bodyInnerHtml,
613                              @Nullable Color foregroundColor) {
614     String html = String.format(
615       "<html><head>%s</head><body>%s</body></html>",
616       UIUtil.getCssFontDeclaration(editorPane.getFont(), foregroundColor, null, null),
617       bodyInnerHtml
618     );
619     editorPane.setText(html);
620   }
621
622   @NotNull
623   public static TextFieldWithHistoryWithBrowseButton createTextFieldWithHistoryWithBrowseButton(@Nullable Project project,
624                                                                                                 @NotNull String browseDialogTitle,
625                                                                                                 @NotNull FileChooserDescriptor fileChooserDescriptor,
626                                                                                                 @Nullable NotNullProducer<List<String>> historyProvider) {
627     TextFieldWithHistoryWithBrowseButton textFieldWithHistoryWithBrowseButton = new TextFieldWithHistoryWithBrowseButton();
628     TextFieldWithHistory textFieldWithHistory = textFieldWithHistoryWithBrowseButton.getChildComponent();
629     textFieldWithHistory.setHistorySize(-1);
630     textFieldWithHistory.setMinimumAndPreferredWidth(0);
631     if (historyProvider != null) {
632       addHistoryOnExpansion(textFieldWithHistory, historyProvider);
633     }
634     installFileCompletionAndBrowseDialog(
635       project,
636       textFieldWithHistoryWithBrowseButton,
637       browseDialogTitle,
638       fileChooserDescriptor
639     );
640     return textFieldWithHistoryWithBrowseButton;
641   }
642
643   @NotNull
644   public static <C extends JComponent> ComponentWithBrowseButton<C> wrapWithInfoButton(@NotNull final C component,
645                                                                                        @NotNull String infoButtonTooltip,
646                                                                                        @NotNull ActionListener listener) {
647     ComponentWithBrowseButton<C> comp = new ComponentWithBrowseButton<C>(component, listener);
648     FixedSizeButton uiHelpButton = comp.getButton();
649     uiHelpButton.setToolTipText(infoButtonTooltip);
650     uiHelpButton.setIcon(UIUtil.getBalloonInformationIcon());
651     uiHelpButton.setHorizontalAlignment(SwingConstants.CENTER);
652     uiHelpButton.setVerticalAlignment(SwingConstants.CENTER);
653     return comp;
654   }
655
656   private static class CopyLinkAction extends AnAction {
657
658     private final String myUrl;
659
660     private CopyLinkAction(@NotNull String url) {
661       super("Copy Link Address", null, PlatformIcons.COPY_ICON);
662       myUrl = url;
663     }
664
665     @Override
666     public void update(AnActionEvent e) {
667       e.getPresentation().setEnabled(true);
668     }
669
670     @Override
671     public void actionPerformed(AnActionEvent e) {
672       Transferable content = new StringSelection(myUrl);
673       CopyPasteManager.getInstance().setContents(content);
674     }
675   }
676
677   private static class OpenLinkInBrowser extends AnAction {
678
679     private final String myUrl;
680
681     private OpenLinkInBrowser(@NotNull String url) {
682       super("Open Link in Browser", null, PlatformIcons.WEB_ICON);
683       myUrl = url;
684     }
685
686     @Override
687     public void update(AnActionEvent e) {
688       e.getPresentation().setEnabled(true);
689     }
690
691     @Override
692     public void actionPerformed(AnActionEvent e) {
693       BrowserUtil.browse(myUrl);
694     }
695   }
696
697   public final static String ELLIPSIS = "...";
698   public static final String ERROR_STR = "www";
699   public static String truncateStringWithEllipsis(final String text, final int maxWidth, final FontMetrics fm) {
700     return truncateStringWithEllipsis(text, maxWidth, new WidthCalculator() {
701       @Override
702       public int stringWidth(String s) {
703         return fm.stringWidth(s);
704       }
705
706       @Override
707       public int charWidth(char c) {
708         return fm.charWidth(c);
709       }
710     });
711   }
712
713   public interface WidthCalculator {
714     int stringWidth(final String s);
715     int charWidth(final char c);
716   }
717
718   public static String truncateStringWithEllipsis(@NotNull final String text, final int maxWidth, final WidthCalculator fm) {
719     final int error = fm.stringWidth(ERROR_STR);
720     final int wholeWidth = fm.stringWidth(text) + error;
721     if (wholeWidth <= maxWidth || text.isEmpty()) return text;
722     final int ellipsisWidth = fm.stringWidth(ELLIPSIS) + error; // plus some reserve
723     if (ellipsisWidth >= maxWidth) return ELLIPSIS;
724
725     final int availableWidth = maxWidth - ellipsisWidth;
726     int currentLen = (int)Math.floor(availableWidth / (((double) wholeWidth) / text.length()));
727
728     final String currentSubstring = text.substring(0, currentLen);
729     int realWidth = fm.stringWidth(currentSubstring);
730
731     if (realWidth >= availableWidth) {
732       int delta = 0;
733       for (int i = currentLen - 1; i >= 0; i--) {
734         if ((realWidth - delta) < availableWidth) return text.substring(0, i) + ELLIPSIS;
735         delta += fm.charWidth(currentSubstring.charAt(i));
736       }
737       return text.substring(0, 1) + ELLIPSIS;
738     } else {
739       int delta = 0;
740       for (int i = currentLen; i < text.length(); i++) {
741         if ((realWidth + delta) >= availableWidth) return text.substring(0, i) + ELLIPSIS;
742         delta += fm.charWidth(text.charAt(i));
743       }
744       return text.substring(0, currentLen) + ELLIPSIS;
745     }
746   }
747
748   public static JEditorPane createHtmlLabel(@NotNull final String innerHtml, @Nullable String disabledHtml,
749                                             @Nullable final Consumer<String> hyperlinkListener) {
750     disabledHtml = disabledHtml == null ? innerHtml : disabledHtml;
751     final Font font = UIUtil.getLabelFont();
752     String html = String.format(
753       "<html><head>%s</head><body>%s</body></html>",
754       UIUtil.getCssFontDeclaration(font, UIUtil.getInactiveTextColor(), null, null),
755       innerHtml
756     );
757     String disabled = String.format(
758       "<html><head>%s</head><body>%s</body></html>",
759       UIUtil.getCssFontDeclaration(font, UIUtil.getInactiveTextColor(), null, null),
760       disabledHtml
761     );
762
763     final JEditorPane pane = new SwingHelper.HtmlViewerBuilder()
764       .setCarryTextOver(false)
765       .setFont(UIUtil.getLabelFont())
766       .setDisabledHtml(disabled)
767       .create();
768     pane.setText(html);
769     pane.addHyperlinkListener(
770       new HyperlinkListener() {
771         public void hyperlinkUpdate(HyperlinkEvent e) {
772           if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
773             if (hyperlinkListener != null) hyperlinkListener.consume(e.getURL() == null ? "" : e.getURL().toString());
774             else BrowserUtil.browse(e.getURL());
775           }
776         }
777       }
778     );
779     return pane;
780   }
781 }