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