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