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