replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / ui / DialogWrapper.java
1 /*
2  * Copyright 2000-2017 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.openapi.ui;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.icons.AllIcons;
20 import com.intellij.ide.ui.UISettings;
21 import com.intellij.idea.ActionsBundle;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.MnemonicHelper;
24 import com.intellij.openapi.actionSystem.*;
25 import com.intellij.openapi.actionSystem.ex.ActionUtil;
26 import com.intellij.openapi.application.ApplicationInfo;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.application.ModalityState;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.help.HelpManager;
31 import com.intellij.openapi.keymap.KeymapUtil;
32 import com.intellij.openapi.project.DumbAwareAction;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.ui.popup.*;
35 import com.intellij.openapi.ui.popup.util.PopupUtil;
36 import com.intellij.openapi.util.*;
37 import com.intellij.openapi.util.registry.Registry;
38 import com.intellij.openapi.wm.IdeFocusManager;
39 import com.intellij.openapi.wm.IdeGlassPaneUtil;
40 import com.intellij.openapi.wm.WindowManager;
41 import com.intellij.ui.ColorUtil;
42 import com.intellij.ui.JBColor;
43 import com.intellij.ui.UIBundle;
44 import com.intellij.ui.awt.RelativePoint;
45 import com.intellij.ui.border.CustomLineBorder;
46 import com.intellij.ui.components.JBOptionButton;
47 import com.intellij.ui.components.JBScrollPane;
48 import com.intellij.ui.components.panels.NonOpaquePanel;
49 import com.intellij.util.Alarm;
50 import com.intellij.util.TimeoutUtil;
51 import com.intellij.util.containers.ContainerUtil;
52 import com.intellij.util.ui.*;
53 import org.intellij.lang.annotations.MagicConstant;
54 import org.jetbrains.annotations.Nls;
55 import org.jetbrains.annotations.NonNls;
56 import org.jetbrains.annotations.NotNull;
57 import org.jetbrains.annotations.Nullable;
58 import sun.swing.SwingUtilities2;
59
60 import javax.swing.*;
61 import javax.swing.border.Border;
62 import javax.swing.border.CompoundBorder;
63 import javax.swing.border.EmptyBorder;
64 import javax.swing.plaf.UIResource;
65 import java.awt.*;
66 import java.awt.event.*;
67 import java.util.*;
68 import java.util.List;
69 import java.util.stream.Collectors;
70
71 /**
72  * The standard base class for modal dialog boxes. The dialog wrapper could be used only on event dispatch thread.
73  * In case when the dialog must be created from other threads use
74  * {@link EventQueue#invokeLater(Runnable)} or {@link EventQueue#invokeAndWait(Runnable)}.
75  * <p/>
76  * See also http://www.jetbrains.org/intellij/sdk/docs/user_interface_components/dialog_wrapper.html.
77  */
78 @SuppressWarnings({"SSBasedInspection", "MethodMayBeStatic"})
79 public abstract class DialogWrapper {
80   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.ui.DialogWrapper");
81
82   public enum IdeModalityType {
83     IDE,
84     PROJECT,
85     MODELESS;
86
87     @NotNull
88     public Dialog.ModalityType toAwtModality() {
89       switch (this) {
90         case IDE:
91           return Dialog.ModalityType.APPLICATION_MODAL;
92         case PROJECT:
93           return Dialog.ModalityType.DOCUMENT_MODAL;
94         case MODELESS:
95           return Dialog.ModalityType.MODELESS;
96       }
97       throw new IllegalStateException(toString());
98     }
99   }
100
101   /**
102    * The default exit code for "OK" action.
103    */
104   public static final int OK_EXIT_CODE = 0;
105   /**
106    * The default exit code for "Cancel" action.
107    */
108   public static final int CANCEL_EXIT_CODE = 1;
109   /**
110    * The default exit code for "Close" action. Equal to cancel.
111    */
112   public static final int CLOSE_EXIT_CODE = CANCEL_EXIT_CODE;
113   /**
114    * If you use your own custom exit codes you have to start them with
115    * this constant.
116    */
117   public static final int NEXT_USER_EXIT_CODE = 2;
118
119   /**
120    * If your action returned by {@code createActions} method has non
121    * {@code null} value for this key, then the button that corresponds to the action will be the
122    * default button for the dialog. It's true if you don't change this behaviour
123    * of {@code createJButtonForAction(Action)} method.
124    */
125   @NonNls public static final String DEFAULT_ACTION = "DefaultAction";
126
127   @NonNls public static final String FOCUSED_ACTION = "FocusedAction";
128
129   @NonNls private static final String NO_AUTORESIZE = "NoAutoResizeAndFit";
130
131   private static final KeyStroke SHOW_OPTION_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,
132                                                                                 InputEvent.ALT_MASK | InputEvent.SHIFT_MASK);
133
134   @NotNull
135   private final DialogWrapperPeer myPeer;
136   private int myExitCode = CANCEL_EXIT_CODE;
137
138   /**
139    * The shared instance of default border for dialog's content pane.
140    */
141   public static final Border ourDefaultBorder = new EmptyBorder(UIUtil.PANEL_REGULAR_INSETS);
142
143   private float myHorizontalStretch = 1.0f;
144   private float myVerticalStretch = 1.0f;
145   /**
146    * Defines horizontal alignment of buttons.
147    */
148   private int myButtonAlignment = SwingConstants.RIGHT;
149   private boolean myCrossClosesWindow = true;
150   private Insets myButtonMargins = JBUI.insets(2, 16);
151
152   protected Action myOKAction;
153   protected Action myCancelAction;
154   protected Action myHelpAction;
155   private final Map<Action, JButton> myButtonMap = new LinkedHashMap<>();
156
157   private boolean myClosed = false;
158
159   protected boolean myPerformAction = false;
160
161   private Action myYesAction = null;
162   private Action myNoAction = null;
163
164   protected JCheckBox myCheckBoxDoNotShowDialog;
165   @Nullable
166   private DoNotAskOption myDoNotAsk;
167
168   protected JComponent myPreferredFocusedComponent;
169   private Computable<Point> myInitialLocationCallback;
170
171   private Dimension  myActualSize = null;
172
173   private List<ValidationInfo> myInfo = Collections.emptyList();
174
175   @NotNull
176   protected final Disposable myDisposable = new Disposable() {
177     @Override
178     public String toString() {
179       return DialogWrapper.this.toString();
180     }
181
182     @Override
183     public void dispose() {
184       DialogWrapper.this.dispose();
185     }
186   };
187   private final List<JBOptionButton> myOptionsButtons = new ArrayList<>();
188   private int myCurrentOptionsButtonIndex = -1;
189   private boolean myResizeInProgress = false;
190   private ComponentAdapter myResizeListener;
191
192   @NotNull
193   protected String getDoNotShowMessage() {
194     return CommonBundle.message("dialog.options.do.not.show");
195   }
196
197   public void setDoNotAskOption(@Nullable DoNotAskOption doNotAsk) {
198     myDoNotAsk = doNotAsk;
199   }
200
201   private ErrorText myErrorText;
202
203   private final Alarm myErrorTextAlarm = new Alarm();
204
205   private static final Color BALLOON_BORDER = new JBColor(new Color(0xe0a8a9), new Color(0x73454b));
206   private static final Color BALLOON_BACKGROUND = new JBColor(new Color(0xf5e6e7), new Color(0x593d41));
207
208   /**
209    * Creates modal {@code DialogWrapper}. The currently active window will be the dialog's parent.
210    *
211    * @param project     parent window for the dialog will be calculated based on focused window for the
212    *                    specified {@code project}. This parameter can be {@code null}. In this case parent window
213    *                    will be suggested based on current focused window.
214    * @param canBeParent specifies whether the dialog can be parent for other windows. This parameter is used
215    *                    by {@code WindowManager}.
216    * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread
217    */
218   protected DialogWrapper(@Nullable Project project, boolean canBeParent) {
219     this(project, canBeParent, IdeModalityType.IDE);
220   }
221
222   protected DialogWrapper(@Nullable Project project, boolean canBeParent, @NotNull IdeModalityType ideModalityType) {
223     this(project, null, canBeParent, ideModalityType);
224   }
225
226   protected DialogWrapper(@Nullable Project project, @Nullable Component parentComponent, boolean canBeParent, @NotNull IdeModalityType ideModalityType) {
227     myPeer = parentComponent == null ? createPeer(project, canBeParent, project == null ? IdeModalityType.IDE : ideModalityType) : createPeer(parentComponent, canBeParent);
228     final Window window = myPeer.getWindow();
229     if (window != null) {
230       myResizeListener = new ComponentAdapter() {
231         @Override
232         public void componentResized(ComponentEvent e) {
233           if (!myResizeInProgress) {
234             myActualSize = myPeer.getSize();
235             if (myErrorText != null && myErrorText.isVisible()) {
236               myActualSize.height -= myErrorText.myLabel.getHeight();
237             }
238           }
239         }
240       };
241       window.addComponentListener(myResizeListener);
242     }
243     createDefaultActions();
244   }
245
246   /**
247    * Creates modal {@code DialogWrapper} that can be parent for other windows.
248    * The currently active window will be the dialog's parent.
249    *
250    * @param project parent window for the dialog will be calculated based on focused window for the
251    *                specified {@code project}. This parameter can be {@code null}. In this case parent window
252    *                will be suggested based on current focused window.
253    * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread
254    * @see DialogWrapper#DialogWrapper(Project, boolean)
255    */
256   protected DialogWrapper(@Nullable Project project) {
257     this(project, true);
258   }
259
260   /**
261    * Creates modal {@code DialogWrapper}. The currently active window will be the dialog's parent.
262    *
263    * @param canBeParent specifies whether the dialog can be parent for other windows. This parameter is used
264    *                    by {@code WindowManager}.
265    * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread
266    */
267   protected DialogWrapper(boolean canBeParent) {
268     this((Project)null, canBeParent);
269   }
270
271   /**
272    * Typically, we should set a parent explicitly. Use WindowManager#suggestParentWindow
273    * method to find out the best parent for your dialog. Exceptions are cases
274    * when we do not have a project to figure out which window
275    * is more suitable as an owner for the dialog.
276    * <p/>
277    * Instead, use {@link DialogWrapper#DialogWrapper(Project, boolean, boolean)}
278    */
279   @Deprecated
280   protected DialogWrapper(boolean canBeParent, boolean applicationModalIfPossible) {
281     this(null, canBeParent, applicationModalIfPossible);
282   }
283
284   protected DialogWrapper(Project project, boolean canBeParent, boolean applicationModalIfPossible) {
285     ensureEventDispatchThread();
286     if (ApplicationManager.getApplication() != null) {
287       myPeer = createPeer(
288         project != null ? WindowManager.getInstance().suggestParentWindow(project) : WindowManager.getInstance().findVisibleFrame()
289         , canBeParent, applicationModalIfPossible);
290     }
291     else {
292       myPeer = createPeer(null, canBeParent, applicationModalIfPossible);
293     }
294     createDefaultActions();
295   }
296
297   /**
298    * @param parent      parent component which is used to calculate heavy weight window ancestor.
299    *                    {@code parent} cannot be {@code null} and must be showing.
300    * @param canBeParent can be parent
301    * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread
302    */
303   protected DialogWrapper(@NotNull Component parent, boolean canBeParent) {
304     ensureEventDispatchThread();
305     myPeer = createPeer(parent, canBeParent);
306     createDefaultActions();
307   }
308
309   //validation
310   private final Alarm myValidationAlarm = new Alarm(getValidationThreadToUse(), myDisposable);
311
312   @NotNull
313   protected Alarm.ThreadToUse getValidationThreadToUse() {
314     return Alarm.ThreadToUse.SWING_THREAD;
315   }
316
317   private int myValidationDelay = 300;
318   private boolean myDisposed = false;
319   private boolean myValidationStarted = false;
320   private final ErrorPainter myErrorPainter = new ErrorPainter();
321   private boolean myErrorPainterInstalled = false;
322
323   /**
324    * Allows to postpone first start of validation
325    *
326    * @return {@code false} if start validation in {@code init()} method
327    */
328   protected boolean postponeValidation() {
329     return true;
330   }
331
332   /**
333    * Validates user input and returns {@code null} if everything is fine
334    * or validation description with component where problem has been found.
335    *
336    * @return {@code null} if everything is OK or validation descriptor
337    */
338   @Nullable
339   protected ValidationInfo doValidate() {
340     return null;
341   }
342
343   /**
344    * Validates user input and returns {@code List<ValidationInfo>}.
345    * If everything is fine the returned list is empty otherwise
346    * the list contains all invalid fields with error messages.
347    * This method should preferably be used when validating forms with multiply
348    * fields that require validation.
349    *
350    * @return {@code List<ValidationInfo>} of invalid fields. List
351    * is empty if no errors found.
352    */
353   @NotNull
354   protected List<ValidationInfo> doValidateAll() {
355     ValidationInfo vi = doValidate();
356     return vi != null ? Collections.singletonList(vi) : Collections.EMPTY_LIST;
357   }
358
359   public void setValidationDelay(int delay) {
360     myValidationDelay = delay;
361   }
362
363   private void installErrorPainter() {
364     if (myErrorPainterInstalled) return;
365     myErrorPainterInstalled = true;
366     UIUtil.invokeLaterIfNeeded(() -> IdeGlassPaneUtil.installPainter(getContentPanel(), myErrorPainter, myDisposable));
367   }
368
369   protected void updateErrorInfo(@NotNull List<ValidationInfo> info) {
370     boolean updateNeeded = Registry.is("ide.inplace.errors.balloon") ?
371                            !myInfo.equals(info) : !myErrorText.isTextSet(info);
372
373     if (updateNeeded) {
374       SwingUtilities.invokeLater(() -> {
375         if (myDisposed) return;
376         setErrorInfoAll(info);
377         myPeer.getRootPane().getGlassPane().repaint();
378         getOKAction().setEnabled(info.isEmpty());
379       });
380     }
381   }
382
383   protected void createDefaultActions() {
384     myOKAction = new OkAction();
385     myCancelAction = new CancelAction();
386     myHelpAction = new HelpAction();
387   }
388
389   public void setUndecorated(boolean undecorated) {
390     myPeer.setUndecorated(undecorated);
391   }
392
393   public final void addMouseListener(@NotNull MouseListener listener) {
394     myPeer.addMouseListener(listener);
395   }
396
397   public final void addMouseListener(@NotNull MouseMotionListener listener) {
398     myPeer.addMouseListener(listener);
399   }
400
401   public final void addKeyListener(@NotNull KeyListener listener) {
402     myPeer.addKeyListener(listener);
403   }
404
405   /**
406    * Closes and disposes the dialog and sets the specified exit code.
407    *
408    * @param exitCode exit code
409    * @param isOk     is OK
410    * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread
411    */
412   public final void close(int exitCode, boolean isOk) {
413     ensureEventDispatchThread();
414     if (myClosed) return;
415     myClosed = true;
416     myExitCode = exitCode;
417     Window window = getWindow();
418     if (window != null && myResizeListener != null) {
419       window.removeComponentListener(myResizeListener);
420       myResizeListener = null;
421     }
422
423     if (isOk) {
424       processDoNotAskOnOk(exitCode);
425     }
426     else {
427       processDoNotAskOnCancel();
428     }
429
430     Disposer.dispose(myDisposable);
431   }
432
433   public final void close(int exitCode) {
434     close(exitCode, exitCode != CANCEL_EXIT_CODE);
435   }
436
437   /**
438    * Creates border for dialog's content pane. By default content
439    * pane has has empty border with {@code (8,12,8,12)} insets. Subclasses can
440    * return {@code null} for no border.
441    *
442    * @return content pane border
443    */
444   @Nullable
445   protected Border createContentPaneBorder() {
446     if (getStyle() == DialogStyle.COMPACT) {
447       return JBUI.Borders.empty();
448     }
449     return ourDefaultBorder;
450   }
451
452   protected static boolean isMoveHelpButtonLeft() {
453     return UIUtil.isUnderAquaBasedLookAndFeel() || UIUtil.isUnderDarcula() || UIUtil.isUnderWin10LookAndFeel();
454   }
455
456   private static boolean isRemoveHelpButton() {
457     return SystemInfo.isWindows && (UIUtil.isUnderDarcula() || UIUtil.isUnderIntelliJLaF()) && Registry.is("ide.win.frame.decoration") ||
458            Registry.is("ide.remove.help.button.from.dialogs");
459   }
460
461   /**
462    * Creates panel located at the south of the content pane. By default that
463    * panel contains dialog's buttons. This default implementation uses {@code createActions()}
464    * and {@code createJButtonForAction(Action)} methods to construct the panel.
465    *
466    * @return south panel
467    */
468   protected JComponent createSouthPanel() {
469     List<Action> actions = ContainerUtil.filter(createActions(), Condition.NOT_NULL);
470     List<Action> leftSideActions = ContainerUtil.newArrayList(createLeftSideActions());
471
472     if (!ApplicationInfo.contextHelpAvailable()) {
473       actions.remove(getHelpAction());
474     }
475
476     boolean hasHelpToMoveToLeftSide = false;
477     if (isRemoveHelpButton()) {
478       actions.remove(getHelpAction());
479     }
480     else if (isMoveHelpButtonLeft() && actions.contains(getHelpAction())) {
481       hasHelpToMoveToLeftSide = true;
482       actions.remove(getHelpAction());
483     }
484
485     if (SystemInfo.isMac) {
486       Action macOtherAction = ContainerUtil.find(actions, MacOtherAction.class::isInstance);
487       if (macOtherAction != null) {
488         leftSideActions.add(macOtherAction);
489         actions.remove(macOtherAction);
490       }
491
492       // move ok action to the right
493       int okNdx = actions.indexOf(getOKAction());
494       if (okNdx >= 0 && okNdx != actions.size() - 1) {
495         actions.remove(getOKAction());
496         actions.add(getOKAction());
497       }
498
499       // move cancel action to the left
500       int cancelNdx = actions.indexOf(getCancelAction());
501       if (cancelNdx > 0) {
502         actions.remove(getCancelAction());
503         actions.add(0, getCancelAction());
504       }
505     }
506     else if (UIUtil.isUnderGTKLookAndFeel() && actions.contains(getHelpAction())) {
507       leftSideActions.add(getHelpAction());
508       actions.remove(getHelpAction());
509     }
510
511     if (!UISettings.getShadowInstance().getAllowMergeButtons()) {
512       actions = flattenOptionsActions(actions);
513       leftSideActions = flattenOptionsActions(leftSideActions);
514     }
515
516
517     List<JButton> leftSideButtons = createButtons(leftSideActions);
518     List<JButton> rightSideButtons = createButtons(actions);
519
520     myButtonMap.clear();
521     for (JButton button : ContainerUtil.concat(leftSideButtons, rightSideButtons)) {
522       myButtonMap.put(button.getAction(), button);
523       if (button instanceof JBOptionButton) {
524         myOptionsButtons.add((JBOptionButton)button);
525       }
526     }
527
528
529     return createSouthPanel(leftSideButtons, rightSideButtons, hasHelpToMoveToLeftSide);
530   }
531
532   @NotNull
533   protected JButton createHelpButton(Insets insets) {
534     JButton helpButton = new JButton(getHelpAction());
535     helpButton.putClientProperty("JButton.buttonType", "help");
536     helpButton.setText("");
537     helpButton.setMargin(insets);
538     helpButton.setToolTipText(ActionsBundle.actionDescription("HelpTopics"));
539     return helpButton;
540   }
541
542   @NotNull
543   private static List<Action> flattenOptionsActions(@NotNull List<Action> actions) {
544     List<Action> newActions = new ArrayList<>();
545     for (Action action : actions) {
546       newActions.add(action);
547       if (action instanceof OptionAction) {
548         ContainerUtil.addAll(newActions, ((OptionAction)action).getOptions());
549       }
550     }
551     return newActions;
552   }
553
554   protected boolean shouldAddErrorNearButtons() {
555     return false;
556   }
557
558   @NotNull
559   protected DialogStyle getStyle() {
560     return DialogStyle.NO_STYLE;
561   }
562
563   protected boolean toBeShown() {
564     return !myCheckBoxDoNotShowDialog.isSelected();
565   }
566
567   public boolean isTypeAheadEnabled() {
568     return false;
569   }
570
571   @NotNull
572   private List<JButton> createButtons(@NotNull List<Action> actions) {
573     List<JButton> buttons = new ArrayList<>();
574     for (Action action : actions) {
575       buttons.add(createJButtonForAction(action));
576     }
577     return buttons;
578   }
579
580   @NotNull
581   private JPanel createSouthPanel(@NotNull List<JButton> leftSideButtons,
582                                   @NotNull List<JButton> rightSideButtons,
583                                   boolean hasHelpToMoveToLeftSide) {
584     JPanel panel = new JPanel(new BorderLayout()) {
585       @Override
586       public Color getBackground() {
587         final Color bg = UIManager.getColor("DialogWrapper.southPanelBackground");
588         if (getStyle() == DialogStyle.COMPACT && bg != null) {
589           return bg;
590         }
591         return super.getBackground();
592       }
593     };
594
595     if (myDoNotAsk != null) {
596       myCheckBoxDoNotShowDialog = new JCheckBox(myDoNotAsk.getDoNotShowMessage());
597       myCheckBoxDoNotShowDialog.setVisible(myDoNotAsk.canBeHidden());
598       myCheckBoxDoNotShowDialog.setSelected(!myDoNotAsk.isToBeShown());
599       DialogUtil.registerMnemonic(myCheckBoxDoNotShowDialog, '&');
600     }
601     JComponent doNotAskCheckbox = createDoNotAskCheckbox();
602
603     final JPanel lrButtonsPanel = new NonOpaquePanel(new GridBagLayout());
604     //noinspection UseDPIAwareInsets
605     final Insets insets = SystemInfo.isMacOSLeopard ?
606                           UIUtil.isUnderIntelliJLaF() ? JBUI.insets(0, 8) : JBUI.emptyInsets() :
607                           UIUtil.isUnderWin10LookAndFeel() ? JBUI.emptyInsets() : new Insets(8, 0, 0, 0); //don't wrap to JBInsets
608
609     if (rightSideButtons.size() > 0 || leftSideButtons.size() > 0) {
610       GridBag bag = new GridBag().setDefaultInsets(insets);
611
612       if (leftSideButtons.size() > 0) {
613         JPanel buttonsPanel = createButtonsPanel(leftSideButtons);
614         if (rightSideButtons.size() > 0) {
615           buttonsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20));  // leave some space between button groups
616         }
617         lrButtonsPanel.add(buttonsPanel, bag.next());
618       }
619       lrButtonsPanel.add(Box.createHorizontalGlue(), bag.next().weightx(1).fillCellHorizontally());   // left strut
620       if (rightSideButtons.size() > 0) {
621         JPanel buttonsPanel = createButtonsPanel(rightSideButtons);
622         if (shouldAddErrorNearButtons()) {
623           lrButtonsPanel.add(myErrorText, bag.next());
624           lrButtonsPanel.add(Box.createHorizontalStrut(10), bag.next());
625         }
626         lrButtonsPanel.add(buttonsPanel, bag.next());
627       }
628       if (SwingConstants.CENTER == myButtonAlignment && doNotAskCheckbox == null) {
629         lrButtonsPanel.add(Box.createHorizontalGlue(), bag.next().weightx(1).fillCellHorizontally());    // right strut
630       }
631     }
632
633     JComponent helpButton = null;
634     if (hasHelpToMoveToLeftSide) {
635       helpButton = createHelpButton(insets);
636     }
637
638     if (helpButton != null || doNotAskCheckbox != null) {
639       JPanel leftPanel = new JPanel(new BorderLayout());
640
641       if (helpButton != null) leftPanel.add(helpButton, BorderLayout.WEST);
642
643       if (doNotAskCheckbox != null) {
644         doNotAskCheckbox.setBorder(JBUI.Borders.emptyRight(20));
645         leftPanel.add(doNotAskCheckbox, BorderLayout.CENTER);
646       }
647
648       panel.add(leftPanel, BorderLayout.WEST);
649     }
650     panel.add(lrButtonsPanel, BorderLayout.CENTER);
651
652     if (getStyle() == DialogStyle.COMPACT) {
653       final Color color = UIManager.getColor("DialogWrapper.southPanelDivider");
654       Border line = new CustomLineBorder(color != null ? color : OnePixelDivider.BACKGROUND, 1, 0, 0, 0);
655       panel.setBorder(new CompoundBorder(line, JBUI.Borders.empty(8, 12)));
656     }
657     else {
658       panel.setBorder(JBUI.Borders.emptyTop(8));
659     }
660
661     return panel;
662   }
663
664   @Nullable
665   protected JComponent createDoNotAskCheckbox() {
666     return myCheckBoxDoNotShowDialog != null && myCheckBoxDoNotShowDialog.isVisible() ? myCheckBoxDoNotShowDialog : null;
667   }
668
669   @NotNull
670   private JPanel createButtonsPanel(@NotNull List<JButton> buttons) {
671     int hgap = SystemInfo.isMacOSLeopard ? UIUtil.isUnderIntelliJLaF() ? 8 : 0 : 5;
672     JPanel buttonsPanel = new NonOpaquePanel(new GridLayout(1, buttons.size(), hgap, 0));
673     for (final JButton button : buttons) {
674       buttonsPanel.add(button);
675     }
676     return buttonsPanel;
677   }
678
679   /**
680    *
681    * @param action should be registered to find corresponding JButton
682    * @return button for specified action or null if it's not found
683    */
684   @Nullable
685   protected JButton getButton(@NotNull Action action) {
686     return myButtonMap.get(action);
687   }
688
689   /**
690    * Creates {@code JButton} for the specified action. If the button has not {@code null}
691    * value for {@code DialogWrapper.DEFAULT_ACTION} key then the created button will be the
692    * default one for the dialog.
693    *
694    * @param action action for the button
695    * @return button with action specified
696    * @see DialogWrapper#DEFAULT_ACTION
697    */
698   protected JButton createJButtonForAction(Action action) {
699     JButton button;
700     if (action instanceof OptionAction && UISettings.getShadowInstance().getAllowMergeButtons()) {
701       button = createJOptionsButton((OptionAction)action);
702     }
703     else {
704       button = new JButton(action);
705     }
706
707     if (SystemInfo.isMac) {
708       button.putClientProperty("JButton.buttonType", "text");
709     }
710
711     Pair<Integer, String> pair = extractMnemonic(button.getText());
712     button.setText(pair.second);
713     int mnemonic = pair.first;
714
715     final Object value = action.getValue(Action.MNEMONIC_KEY);
716     if (value instanceof Integer) {
717       mnemonic = (Integer)value;
718     }
719     button.setMnemonic(mnemonic);
720
721     final Object name = action.getValue(Action.NAME);
722     if (mnemonic == KeyEvent.VK_Y && "Yes".equals(name)) {
723       myYesAction = action;
724     }
725     else if (mnemonic == KeyEvent.VK_N && "No".equals(name)) {
726       myNoAction = action;
727     }
728
729     setMargin(button);
730     if (action.getValue(DEFAULT_ACTION) != null) {
731       if (!myPeer.isHeadless()) {
732         getRootPane().setDefaultButton(button);
733       }
734     }
735
736     if (action.getValue(FOCUSED_ACTION) != null) {
737       myPreferredFocusedComponent = button;
738     }
739
740     return button;
741   }
742
743   @NotNull
744   private JButton createJOptionsButton(@NotNull OptionAction action) {
745     JBOptionButton optionButton = new JBOptionButton(action, action.getOptions());
746     optionButton.setOkToProcessDefaultMnemonics(false);
747     optionButton.setOptionTooltipText(
748       "Press " + KeymapUtil.getKeystrokeText(SHOW_OPTION_KEYSTROKE) + " to expand or use a mnemonic of a contained action");
749
750     final Set<JBOptionButton.OptionInfo> infos = optionButton.getOptionInfos();
751     for (final JBOptionButton.OptionInfo eachInfo : infos) {
752       if (eachInfo.getMnemonic() >= 0) {
753         final char mnemonic = (char)eachInfo.getMnemonic();
754         JRootPane rootPane = getPeer().getRootPane();
755         if (rootPane != null) {
756           new DumbAwareAction("Show JBOptionButton popup") {
757             @Override
758             public void actionPerformed(AnActionEvent e) {
759               final JBOptionButton buttonToActivate = eachInfo.getButton();
760               buttonToActivate.showPopup(eachInfo.getAction(), true);
761             }
762           }.registerCustomShortcutSet(MnemonicHelper.createShortcut(mnemonic), rootPane, myDisposable);
763         }
764       }
765     }
766
767     return optionButton;
768   }
769
770   @NotNull
771   private static Pair<Integer, String> extractMnemonic(@Nullable String text) {
772     if (text == null) return Pair.create(0, null);
773
774     int mnemonic = 0;
775     StringBuilder plainText = new StringBuilder();
776     for (int i = 0; i < text.length(); i++) {
777       char ch = text.charAt(i);
778       if (ch == '_' || ch == '&') {
779         i++;
780         if (i >= text.length()) {
781           break;
782         }
783         ch = text.charAt(i);
784         if (ch != '_' && ch != '&') {
785           // Mnemonic is case insensitive.
786           int vk = ch;
787           if (vk >= 'a' && vk <= 'z') {
788             vk -= 'a' - 'A';
789           }
790           mnemonic = vk;
791         }
792       }
793       plainText.append(ch);
794     }
795     return Pair.create(mnemonic, plainText.toString());
796   }
797
798   private void setMargin(@NotNull JButton button) {
799     // Aqua LnF does a good job of setting proper margin between buttons. Setting them specifically causes them be 'square' style instead of
800     // 'rounded', which is expected by apple users.
801     if (!SystemInfo.isMac) {
802       if (myButtonMargins == null) {
803         return;
804       }
805       button.setMargin(myButtonMargins);
806     }
807   }
808
809   @NotNull
810   protected DialogWrapperPeer createPeer(@NotNull Component parent, final boolean canBeParent) {
811     return DialogWrapperPeerFactory.getInstance().createPeer(this, parent, canBeParent);
812   }
813
814   /**
815    * Dialogs with no parents are discouraged.
816    * Instead, use e.g. {@link DialogWrapper#createPeer(Window, boolean, boolean)}
817    */
818   @Deprecated
819   @NotNull
820   protected DialogWrapperPeer createPeer(boolean canBeParent, boolean applicationModalIfPossible) {
821     return createPeer(null, canBeParent, applicationModalIfPossible);
822   }
823
824   @NotNull
825   protected DialogWrapperPeer createPeer(final Window owner, final boolean canBeParent, final IdeModalityType ideModalityType) {
826     return DialogWrapperPeerFactory.getInstance().createPeer(this, owner, canBeParent, ideModalityType);
827   }
828
829   @Deprecated
830   @NotNull
831   protected DialogWrapperPeer createPeer(final Window owner, final boolean canBeParent, final boolean applicationModalIfPossible) {
832     return DialogWrapperPeerFactory.getInstance()
833       .createPeer(this, owner, canBeParent, applicationModalIfPossible ? IdeModalityType.IDE : IdeModalityType.PROJECT);
834   }
835
836   @NotNull
837   protected DialogWrapperPeer createPeer(@Nullable final Project project,
838                                          final boolean canBeParent,
839                                          @NotNull IdeModalityType ideModalityType) {
840     return DialogWrapperPeerFactory.getInstance().createPeer(this, project, canBeParent, ideModalityType);
841   }
842
843   @NotNull
844   protected DialogWrapperPeer createPeer(@Nullable final Project project, final boolean canBeParent) {
845     return DialogWrapperPeerFactory.getInstance().createPeer(this, project, canBeParent);
846   }
847
848   @Nullable
849   protected JComponent createTitlePane() {
850     return null;
851   }
852
853   /**
854    * Factory method. It creates the panel located at the
855    * north of the dialog's content pane. The implementation can return {@code null}
856    * value. In this case there will be no input panel.
857    *
858    * @return north panel
859    */
860   @Nullable
861   protected JComponent createNorthPanel() {
862     return null;
863   }
864
865   /**
866    * Factory method. It creates panel with dialog options. Options panel is located at the
867    * center of the dialog's content pane. The implementation can return {@code null}
868    * value. In this case there will be no options panel.
869    *
870    * @return center panel
871    */
872   @Nullable
873   protected abstract JComponent createCenterPanel();
874
875   /**
876    * @see Window#toFront()
877    */
878   public void toFront() {
879     myPeer.toFront();
880   }
881
882   /**
883    * @see Window#toBack()
884    */
885   public void toBack() {
886     myPeer.toBack();
887   }
888
889   protected boolean setAutoAdjustable(boolean autoAdjustable) {
890     JRootPane rootPane = getRootPane();
891     if (rootPane == null) return false;
892     rootPane.putClientProperty(NO_AUTORESIZE, autoAdjustable ? null : Boolean.TRUE);
893     return true;
894   }
895
896   //true by default
897   public boolean isAutoAdjustable() {
898     JRootPane rootPane = getRootPane();
899     return rootPane == null || rootPane.getClientProperty(NO_AUTORESIZE) == null;
900   }
901
902   /**
903    * Dispose the wrapped and releases all resources allocated be the wrapper to help
904    * more efficient garbage collection. You should never invoke this method twice or
905    * invoke any method of the wrapper after invocation of {@code dispose}.
906    *
907    * @throws IllegalStateException if the dialog is disposed not on the event dispatch thread
908    */
909   protected void dispose() {
910     ensureEventDispatchThread();
911     myErrorTextAlarm.cancelAllRequests();
912     myValidationAlarm.cancelAllRequests();
913     myDisposed = true;
914
915     for (JButton button : myButtonMap.values()) {
916       button.setAction(null); // avoid memory leak via KeyboardManager
917     }
918     myButtonMap.clear();
919
920     final JRootPane rootPane = getRootPane();
921     // if rootPane = null, dialog has already been disposed
922     if (rootPane != null) {
923       unregisterKeyboardActions(rootPane);
924       if (myActualSize != null && isAutoAdjustable()) {
925         setSize(myActualSize.width, myActualSize.height);
926       }
927       myPeer.dispose();
928     }
929   }
930
931   public static void cleanupRootPane(@Nullable JRootPane rootPane) {
932     if (rootPane == null) return;
933     // Must be preserved:
934     //   Component#appContext, Component#appContext, Container#component
935     //   JRootPane#contentPane due to popup recycling & our border styling
936     // Must be cleared:
937     //   JComponent#clientProperties, contentPane children
938     RepaintManager.currentManager(rootPane).removeInvalidComponent(rootPane);
939     unregisterKeyboardActions(rootPane);
940     Container contentPane = rootPane.getContentPane();
941     if (contentPane != null) contentPane.removeAll();
942     Disposer.clearOwnFields(rootPane, field -> {
943       String clazz = field.getDeclaringClass().getName();
944       // keep AWT and Swing fields intact, except some
945       if (!clazz.startsWith("java.") && !clazz.startsWith("javax.")) return true;
946       String name = field.getName();
947       return "clientProperties".equals(name);
948     });
949   }
950
951   public static void unregisterKeyboardActions(@Nullable Component rootPane) {
952     int[] flags = {JComponent.WHEN_FOCUSED, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, JComponent.WHEN_IN_FOCUSED_WINDOW};
953     for (JComponent eachComp : UIUtil.uiTraverser(rootPane).traverse().filter(JComponent.class)) {
954       ActionMap actionMap = eachComp.getActionMap();
955       if (actionMap == null) continue;
956       for (KeyStroke eachStroke : eachComp.getRegisteredKeyStrokes()) {
957         boolean remove = true;
958         for (int i : flags) {
959           Object key = eachComp.getInputMap(i).get(eachStroke);
960           Action action = key == null ? null : actionMap.get(key);
961           if (action instanceof UIResource) remove = false;
962         }
963         if (remove) eachComp.unregisterKeyboardAction(eachStroke);
964       }
965     }
966   }
967
968   public static void cleanupWindowListeners(@Nullable Window window) {
969     if (window == null) return;
970     SwingUtilities.invokeLater(() -> {
971       for (WindowListener listener : window.getWindowListeners()) {
972         if (listener.getClass().getName().startsWith("com.intellij.")) {
973           LOG.warn("Stale listener: " + listener);
974           window.removeWindowListener(listener);
975         }
976       }
977     });
978   }
979
980
981   /**
982    * This method is invoked by default implementation of "Cancel" action. It just closes dialog
983    * with {@code CANCEL_EXIT_CODE}. This is convenient place to override functionality of "Cancel" action.
984    * Note that the method does nothing if "Cancel" action isn't enabled.
985    */
986   public void doCancelAction() {
987     if (getCancelAction().isEnabled()) {
988       close(CANCEL_EXIT_CODE);
989     }
990   }
991
992   private void processDoNotAskOnCancel() {
993     if (myDoNotAsk != null) {
994       if (myDoNotAsk.shouldSaveOptionsOnCancel() && myDoNotAsk.canBeHidden()) {
995         myDoNotAsk.setToBeShown(toBeShown(), CANCEL_EXIT_CODE);
996       }
997     }
998   }
999
1000   /**
1001    * You can use this method if you want to know by which event this actions got triggered. It is called only if
1002    * the cancel action was triggered by some input event, {@code doCancelAction} is called otherwise.
1003    *
1004    * @param source AWT event
1005    * @see #doCancelAction
1006    */
1007   public void doCancelAction(AWTEvent source) {
1008     doCancelAction();
1009   }
1010
1011   /**
1012    * Programmatically perform a "click" of default dialog's button. The method does
1013    * nothing if the dialog has no default button.
1014    */
1015   public void clickDefaultButton() {
1016     JButton button = getRootPane().getDefaultButton();
1017     if (button != null) {
1018       button.doClick();
1019     }
1020   }
1021
1022   /**
1023    * This method is invoked by default implementation of "OK" action. It just closes dialog
1024    * with {@code OK_EXIT_CODE}. This is convenient place to override functionality of "OK" action.
1025    * Note that the method does nothing if "OK" action isn't enabled.
1026    */
1027   protected void doOKAction() {
1028     if (getOKAction().isEnabled()) {
1029       close(OK_EXIT_CODE);
1030     }
1031   }
1032
1033   protected void processDoNotAskOnOk(int exitCode) {
1034     if (myDoNotAsk != null) {
1035       if (myDoNotAsk.canBeHidden()) {
1036         myDoNotAsk.setToBeShown(toBeShown(), exitCode);
1037       }
1038     }
1039   }
1040
1041   /**
1042    * @return whether the native window cross button closes the window or not.
1043    * {@code true} means that cross performs hide or dispose of the dialog.
1044    */
1045   public boolean shouldCloseOnCross() {
1046     return myCrossClosesWindow;
1047   }
1048
1049   /**
1050    * Creates actions for dialog.
1051    * <p/>
1052    * By default "OK" and "Cancel" actions are returned. The "Help" action is automatically added if
1053    * {@link #getHelpId()} returns non-null value.
1054    * <p/>
1055    * Each action is represented by {@code JButton} created by {@link #createJButtonForAction(Action)}.
1056    * These buttons are then placed into {@link #createSouthPanel() south panel} of dialog.
1057    *
1058    * @return dialog actions
1059    * @see #createSouthPanel
1060    * @see #createJButtonForAction
1061    */
1062   @NotNull
1063   protected Action[] createActions() {
1064     Action helpAction = getHelpAction();
1065
1066     return helpAction == myHelpAction && getHelpId() == null
1067            ? new Action[]{getOKAction(), getCancelAction()}
1068            : new Action[]{getOKAction(), getCancelAction(), helpAction};
1069   }
1070
1071   @NotNull
1072   protected Action[] createLeftSideActions() {
1073     return new Action[0];
1074   }
1075
1076   /**
1077    * @return default implementation of "OK" action. This action just invokes
1078    * {@code doOKAction()} method.
1079    * @see #doOKAction
1080    */
1081   @NotNull
1082   protected Action getOKAction() {
1083     return myOKAction;
1084   }
1085
1086   /**
1087    * @return default implementation of "Cancel" action. This action just invokes
1088    * {@code doCancelAction()} method.
1089    * @see #doCancelAction
1090    */
1091   @NotNull
1092   protected Action getCancelAction() {
1093     return myCancelAction;
1094   }
1095
1096   /**
1097    * @return default implementation of "Help" action. This action just invokes
1098    * {@code doHelpAction()} method.
1099    * @see #doHelpAction
1100    */
1101   @NotNull
1102   protected Action getHelpAction() {
1103     return myHelpAction;
1104   }
1105
1106   protected boolean isProgressDialog() {
1107     return false;
1108   }
1109
1110   public final boolean isModalProgress() {
1111     return isProgressDialog();
1112   }
1113
1114   /**
1115    * Returns content pane
1116    *
1117    * @return content pane
1118    * @see JDialog#getContentPane
1119    */
1120   public Container getContentPane() {
1121     return myPeer.getContentPane();
1122   }
1123
1124   /**
1125    * @see JDialog#validate
1126    */
1127   public void validate() {
1128     myPeer.validate();
1129   }
1130
1131   /**
1132    * @see JDialog#repaint
1133    */
1134   public void repaint() {
1135     myPeer.repaint();
1136   }
1137
1138   /**
1139    * Returns key for persisting dialog dimensions.
1140    * <p/>
1141    * Default implementation returns {@code null} (no persisting).
1142    *
1143    * @return dimension service key
1144    */
1145   @Nullable
1146   @NonNls
1147   protected String getDimensionServiceKey() {
1148     return null;
1149   }
1150
1151   @Nullable
1152   public final String getDimensionKey() {
1153     return getDimensionServiceKey();
1154   }
1155
1156   public int getExitCode() {
1157     return myExitCode;
1158   }
1159
1160   /**
1161    * @return component which should be focused when the dialog appears
1162    * on the screen.
1163    */
1164   @Nullable
1165   public JComponent getPreferredFocusedComponent() {
1166     return SystemInfo.isMac ? myPreferredFocusedComponent : null;
1167   }
1168
1169   /**
1170    * @return horizontal stretch of the dialog. It means that the dialog's horizontal size is
1171    * the product of horizontal stretch by horizontal size of packed dialog. The default value
1172    * is {@code 1.0f}
1173    */
1174   public final float getHorizontalStretch() {
1175     return myHorizontalStretch;
1176   }
1177
1178   /**
1179    * @return vertical stretch of the dialog. It means that the dialog's vertical size is
1180    * the product of vertical stretch by vertical size of packed dialog. The default value
1181    * is {@code 1.0f}
1182    */
1183   public final float getVerticalStretch() {
1184     return myVerticalStretch;
1185   }
1186
1187   protected final void setHorizontalStretch(float hStretch) {
1188     myHorizontalStretch = hStretch;
1189   }
1190
1191   protected final void setVerticalStretch(float vStretch) {
1192     myVerticalStretch = vStretch;
1193   }
1194
1195   /**
1196    * @return window owner
1197    * @see Window#getOwner
1198    */
1199   public Window getOwner() {
1200     return myPeer.getOwner();
1201   }
1202
1203   public Window getWindow() {
1204     return myPeer.getWindow();
1205   }
1206
1207   public JComponent getContentPanel() {
1208     return (JComponent)myPeer.getContentPane();
1209   }
1210
1211   /**
1212    * @return root pane
1213    * @see JDialog#getRootPane
1214    */
1215   public JRootPane getRootPane() {
1216     return myPeer.getRootPane();
1217   }
1218
1219   /**
1220    * @return dialog size
1221    * @see Window#getSize
1222    */
1223   public Dimension getSize() {
1224     return myPeer.getSize();
1225   }
1226
1227   /**
1228    * @return dialog title
1229    * @see Dialog#getTitle
1230    */
1231   public String getTitle() {
1232     return myPeer.getTitle();
1233   }
1234
1235   protected void init() {
1236     ensureEventDispatchThread();
1237     myErrorText = new ErrorText(getErrorTextAlignment());
1238     myErrorText.setVisible(false);
1239     final ComponentAdapter resizeListener = new ComponentAdapter() {
1240       private int myHeight;
1241
1242       @Override
1243       public void componentResized(ComponentEvent event) {
1244         int height = !myErrorText.isVisible() ? 0 : event.getComponent().getHeight();
1245         if (height != myHeight) {
1246           myHeight = height;
1247           myResizeInProgress = true;
1248           myErrorText.setMinimumSize(new Dimension(0, height));
1249           JRootPane root = myPeer.getRootPane();
1250           if (root != null) {
1251             root.validate();
1252           }
1253           if (myActualSize != null && !shouldAddErrorNearButtons()) {
1254             myPeer.setSize(myActualSize.width, myActualSize.height + height);
1255           }
1256           myErrorText.revalidate();
1257           myResizeInProgress = false;
1258         }
1259       }
1260     };
1261     myErrorText.myLabel.addComponentListener(resizeListener);
1262     Disposer.register(myDisposable, new Disposable() {
1263       @Override
1264       public void dispose() {
1265         myErrorText.myLabel.removeComponentListener(resizeListener);
1266       }
1267     });
1268
1269     final JPanel root = new JPanel(createRootLayout());
1270     //{
1271     //  @Override
1272     //  public void paint(Graphics g) {
1273     //    if (ApplicationManager.getApplication() != null) {
1274     //      UISettings.setupAntialiasing(g);
1275     //    }
1276     //    super.paint(g);
1277     //  }
1278     //};
1279     myPeer.setContentPane(root);
1280
1281     final CustomShortcutSet sc = new CustomShortcutSet(SHOW_OPTION_KEYSTROKE);
1282     final AnAction toggleShowOptions = new DumbAwareAction() {
1283       @Override
1284       public void actionPerformed(@NotNull AnActionEvent e) {
1285         expandNextOptionButton();
1286       }
1287     };
1288     toggleShowOptions.registerCustomShortcutSet(sc, root, myDisposable);
1289
1290     JComponent titlePane = createTitlePane();
1291     if (titlePane != null) {
1292       JPanel northSection = new JPanel(new BorderLayout());
1293       root.add(northSection, BorderLayout.NORTH);
1294
1295       northSection.add(titlePane, BorderLayout.CENTER);
1296     }
1297
1298     JComponent centerSection = new JPanel(new BorderLayout());
1299     root.add(centerSection, BorderLayout.CENTER);
1300
1301     root.setBorder(createContentPaneBorder());
1302
1303     final JComponent n = createNorthPanel();
1304     if (n != null) {
1305       centerSection.add(n, BorderLayout.NORTH);
1306     }
1307
1308     final JComponent c = createCenterPanel();
1309     if (c != null) {
1310       centerSection.add(c, BorderLayout.CENTER);
1311     }
1312
1313     final JPanel southSection = new JPanel(new BorderLayout());
1314     root.add(southSection, BorderLayout.SOUTH);
1315
1316     southSection.add(myErrorText, BorderLayout.CENTER);
1317     final JComponent south = createSouthPanel();
1318     if (south != null) {
1319       southSection.add(south, BorderLayout.SOUTH);
1320     }
1321
1322     MnemonicHelper.init(root);
1323     if (!postponeValidation()) {
1324       startTrackingValidation();
1325     }
1326     if (SystemInfo.isWindows) {
1327       installEnterHook(root, myDisposable);
1328     }
1329     myErrorTextAlarm.setActivationComponent(root);
1330   }
1331
1332   protected int getErrorTextAlignment() {
1333     return SwingConstants.LEADING;
1334   }
1335
1336   @NotNull
1337   LayoutManager createRootLayout() {
1338     return new BorderLayout();
1339   }
1340
1341   private static void installEnterHook(JComponent root, Disposable disposable) {
1342     new DumbAwareAction() {
1343       @Override
1344       public void actionPerformed(AnActionEvent e) {
1345         final Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
1346         if (owner instanceof JButton && owner.isEnabled()) {
1347           ((JButton)owner).doClick();
1348         }
1349       }
1350
1351       @Override
1352       public void update(AnActionEvent e) {
1353         final Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
1354         e.getPresentation().setEnabled(owner instanceof JButton && owner.isEnabled());
1355       }
1356     }.registerCustomShortcutSet(CustomShortcutSet.fromString("ENTER"), root, disposable);
1357   }
1358
1359   private void expandNextOptionButton() {
1360     if (myCurrentOptionsButtonIndex > 0) {
1361       myOptionsButtons.get(myCurrentOptionsButtonIndex).closePopup();
1362       myCurrentOptionsButtonIndex++;
1363     }
1364     else if (!myOptionsButtons.isEmpty()) {
1365       myCurrentOptionsButtonIndex = 0;
1366     }
1367
1368     if (myCurrentOptionsButtonIndex >= 0 && myCurrentOptionsButtonIndex < myOptionsButtons.size()) {
1369       myOptionsButtons.get(myCurrentOptionsButtonIndex).showPopup(null, true);
1370     }
1371   }
1372
1373   void startTrackingValidation() {
1374     SwingUtilities.invokeLater(() -> {
1375       if (!myValidationStarted && !myDisposed) {
1376         myValidationStarted = true;
1377         initValidation();
1378       }
1379     });
1380   }
1381
1382   protected final void initValidation() {
1383     myValidationAlarm.cancelAllRequests();
1384     final Runnable validateRequest = () -> {
1385       if (myDisposed) return;
1386       List<ValidationInfo> result = doValidateAll();
1387       if (!result.isEmpty()) {
1388         installErrorPainter();
1389       }
1390       myErrorPainter.setValidationInfo(result);
1391       updateErrorInfo(result);
1392
1393       if (!myDisposed) {
1394         initValidation();
1395       }
1396     };
1397
1398     if (getValidationThreadToUse() == Alarm.ThreadToUse.SWING_THREAD) {
1399       // null if headless
1400       JRootPane rootPane = getRootPane();
1401       myValidationAlarm.addRequest(validateRequest, myValidationDelay,
1402                                    ApplicationManager.getApplication() == null
1403                                    ? null
1404                                    : rootPane == null ? ModalityState.current() : ModalityState.stateForComponent(rootPane));
1405     }
1406     else {
1407       myValidationAlarm.addRequest(validateRequest, myValidationDelay);
1408     }
1409   }
1410
1411   protected boolean isNorthStrictedToPreferredSize() {
1412     return true;
1413   }
1414
1415   protected boolean isCenterStrictedToPreferredSize() {
1416     return false;
1417   }
1418
1419   protected boolean isSouthStrictedToPreferredSize() {
1420     return true;
1421   }
1422
1423   @NotNull
1424   protected JComponent createContentPane() {
1425     return new JPanel();
1426   }
1427
1428   /**
1429    * @see Window#pack
1430    */
1431   public void pack() {
1432     myPeer.pack();
1433   }
1434
1435   public Dimension getPreferredSize() {
1436     return myPeer.getPreferredSize();
1437   }
1438
1439   /**
1440    * Sets horizontal alignment of dialog's buttons.
1441    *
1442    * @param alignment alignment of the buttons. Acceptable values are
1443    *                  {@code SwingConstants.CENTER} and {@code SwingConstants.RIGHT}.
1444    *                  The {@code SwingConstants.RIGHT} is the default value.
1445    * @throws IllegalArgumentException if {@code alignment} isn't acceptable
1446    */
1447   protected final void setButtonsAlignment(@MagicConstant(intValues = {SwingConstants.CENTER, SwingConstants.RIGHT}) int alignment) {
1448     if (SwingConstants.CENTER != alignment && SwingConstants.RIGHT != alignment) {
1449       throw new IllegalArgumentException("unknown alignment: " + alignment);
1450     }
1451     myButtonAlignment = alignment;
1452   }
1453
1454   /**
1455    * Sets margin for command buttons ("OK", "Cancel", "Help").
1456    *
1457    * @param insets buttons margin
1458    */
1459   public final void setButtonsMargin(@Nullable Insets insets) {
1460     myButtonMargins = insets;
1461   }
1462
1463   public final void setCrossClosesWindow(boolean crossClosesWindow) {
1464     myCrossClosesWindow = crossClosesWindow;
1465   }
1466
1467   protected final void setCancelButtonIcon(Icon icon) {
1468     // Setting icons causes buttons be 'square' style instead of
1469     // 'rounded', which is expected by apple users.
1470     if (!SystemInfo.isMac) {
1471       myCancelAction.putValue(Action.SMALL_ICON, icon);
1472     }
1473   }
1474
1475   protected final void setCancelButtonText(String text) {
1476     myCancelAction.putValue(Action.NAME, text);
1477   }
1478
1479   public void setModal(boolean modal) {
1480     myPeer.setModal(modal);
1481   }
1482
1483   public boolean isModal() {
1484     return myPeer.isModal();
1485   }
1486
1487   public boolean isOKActionEnabled() {
1488     return myOKAction.isEnabled();
1489   }
1490
1491   public void setOKActionEnabled(boolean isEnabled) {
1492     myOKAction.setEnabled(isEnabled);
1493   }
1494
1495   protected final void setOKButtonIcon(Icon icon) {
1496     // Setting icons causes buttons be 'square' style instead of
1497     // 'rounded', which is expected by apple users.
1498     if (!SystemInfo.isMac) {
1499       myOKAction.putValue(Action.SMALL_ICON, icon);
1500     }
1501   }
1502
1503   /**
1504    * @param text action without mnemonic. If mnemonic is set, presentation would be shifted by one to the left
1505    *             {@link AbstractButton#setText(String)}
1506    *             {@link AbstractButton#updateDisplayedMnemonicIndex(String, int)}
1507    */
1508   protected final void setOKButtonText(String text) {
1509     myOKAction.putValue(Action.NAME, text);
1510   }
1511
1512   protected final void setOKButtonMnemonic(int c) {
1513     myOKAction.putValue(Action.MNEMONIC_KEY, c);
1514   }
1515
1516   /**
1517    * @return the help identifier or null if no help is available.
1518    */
1519   @Nullable @NonNls
1520   protected String getHelpId() {
1521     return null;
1522   }
1523
1524   /**
1525    * Invoked by default implementation of "Help" action.
1526    * Note that the method does nothing if "Help" action isn't enabled.
1527    * <p/>
1528    * The default implementation shows the help page with id returned
1529    * by {@link #getHelpId()}. If that method returns null,
1530    * a message box with message "no help available" is shown.
1531    */
1532   protected void doHelpAction() {
1533     if (myHelpAction.isEnabled()) {
1534       String helpId = getHelpId();
1535       if (helpId != null) {
1536         HelpManager.getInstance().invokeHelp(helpId);
1537       }
1538       else {
1539         Messages.showMessageDialog(getContentPane(), UIBundle.message("there.is.no.help.for.this.dialog.error.message"),
1540                                    UIBundle.message("no.help.available.dialog.title"), Messages.getInformationIcon());
1541       }
1542     }
1543   }
1544
1545   public boolean isOK() {
1546     return getExitCode() == OK_EXIT_CODE;
1547   }
1548
1549   /**
1550    * @return {@code true} if and only if visible
1551    * @see Component#isVisible
1552    */
1553   public boolean isVisible() {
1554     return myPeer.isVisible();
1555   }
1556
1557   /**
1558    * @return {@code true} if and only if showing
1559    * @see Window#isShowing
1560    */
1561   public boolean isShowing() {
1562     return myPeer.isShowing();
1563   }
1564
1565   /**
1566    * @param width  width
1567    * @param height height
1568    * @see JDialog#setSize
1569    */
1570   public void setSize(int width, int height) {
1571     myPeer.setSize(width, height);
1572   }
1573
1574   /**
1575    * @param title title
1576    * @see JDialog#setTitle
1577    */
1578   public void setTitle(@Nls(capitalization = Nls.Capitalization.Title) String title) {
1579     myPeer.setTitle(title);
1580   }
1581
1582   /**
1583    * @see JDialog#isResizable
1584    */
1585   public void isResizable() {
1586     myPeer.isResizable();
1587   }
1588
1589   /**
1590    * @param resizable is resizable
1591    * @see JDialog#setResizable
1592    */
1593   public void setResizable(boolean resizable) {
1594     myPeer.setResizable(resizable);
1595   }
1596
1597   /**
1598    * @return dialog location
1599    * @see JDialog#getLocation
1600    */
1601   @NotNull
1602   public Point getLocation() {
1603     return myPeer.getLocation();
1604   }
1605
1606   /**
1607    * @param p new dialog location
1608    * @see JDialog#setLocation(Point)
1609    */
1610   public void setLocation(@NotNull Point p) {
1611     myPeer.setLocation(p);
1612   }
1613
1614   /**
1615    * @param x x
1616    * @param y y
1617    * @see JDialog#setLocation(int, int)
1618    */
1619   public void setLocation(int x, int y) {
1620     myPeer.setLocation(x, y);
1621   }
1622
1623   public void centerRelativeToParent() {
1624     myPeer.centerInParent();
1625   }
1626
1627   /**
1628    * Show the dialog.
1629    *
1630    * @throws IllegalStateException if the method is invoked not on the event dispatch thread
1631    * @see #showAndGet()
1632    * @see #showAndGetOk()
1633    */
1634   public void show() {
1635     invokeShow();
1636   }
1637
1638   /**
1639    * Show the modal dialog and check if it was closed with OK.
1640    *
1641    * @return true if the {@link #getExitCode() exit code} is {@link #OK_EXIT_CODE}.
1642    * @throws IllegalStateException if the dialog is non-modal, or if the method is invoked not on the EDT.
1643    * @see #show()
1644    * @see #showAndGetOk()
1645    */
1646   public boolean showAndGet() {
1647     if (!isModal()) {
1648       throw new IllegalStateException("The showAndGet() method is for modal dialogs only");
1649     }
1650     show();
1651     return isOK();
1652   }
1653
1654   /**
1655    * You need this method ONLY for NON-MODAL dialogs. Otherwise, use {@link #show()} or {@link #showAndGet()}.
1656    *
1657    * @return result callback which set to "Done" on dialog close, and then its {@code getResult()} will contain {@code isOK()}
1658    */
1659   @NotNull
1660   public AsyncResult<Boolean> showAndGetOk() {
1661     if (isModal()) {
1662       throw new IllegalStateException("The showAndGetOk() method is for modeless dialogs only");
1663     }
1664     return invokeShow();
1665   }
1666
1667   @NotNull
1668   private AsyncResult<Boolean> invokeShow() {
1669     final AsyncResult<Boolean> result = new AsyncResult<>();
1670
1671     ensureEventDispatchThread();
1672     registerKeyboardShortcuts();
1673
1674     final Disposable uiParent = Disposer.get("ui");
1675     if (uiParent != null) { // may be null if no app yet (license agreement)
1676       Disposer.register(uiParent, myDisposable); // ensure everything is disposed on app quit
1677     }
1678
1679     Disposer.register(myDisposable, new Disposable() {
1680       @Override
1681       public void dispose() {
1682         result.setDone(isOK());
1683       }
1684     });
1685
1686     myPeer.show();
1687
1688     return result;
1689   }
1690
1691   /**
1692    * @return Location in absolute coordinates which is used when dialog has no dimension service key or no position was stored yet.
1693    * Can return null. In that case dialog will be centered relative to its owner.
1694    */
1695   @Nullable
1696   public Point getInitialLocation() {
1697     return myInitialLocationCallback == null ? null : myInitialLocationCallback.compute();
1698   }
1699
1700   public void setInitialLocationCallback(@NotNull Computable<Point> callback) {
1701     myInitialLocationCallback = callback;
1702   }
1703
1704   private void registerKeyboardShortcuts() {
1705     final JRootPane rootPane = getRootPane();
1706
1707     if (rootPane == null) return;
1708
1709     ActionListener cancelKeyboardAction = createCancelAction();
1710     if (cancelKeyboardAction != null) {
1711       rootPane
1712         .registerKeyboardAction(cancelKeyboardAction, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
1713       ActionUtil.registerForEveryKeyboardShortcut(getRootPane(), cancelKeyboardAction, CommonShortcuts.getCloseActiveWindow());
1714     }
1715
1716     if (ApplicationInfo.contextHelpAvailable() && !isProgressDialog()) {
1717       ActionListener helpAction = e -> doHelpAction();
1718       ActionUtil.registerForEveryKeyboardShortcut(getRootPane(), helpAction, CommonShortcuts.getContextHelp());
1719       rootPane.registerKeyboardAction(helpAction, KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
1720     }
1721
1722     rootPane.registerKeyboardAction(new AbstractAction() {
1723       @Override
1724       public void actionPerformed(ActionEvent e) {
1725         focusPreviousButton();
1726       }
1727     }, KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
1728     rootPane.registerKeyboardAction(new AbstractAction() {
1729       @Override
1730       public void actionPerformed(ActionEvent e) {
1731         focusNextButton();
1732       }
1733     }, KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
1734
1735     if (myYesAction != null) {
1736       rootPane.registerKeyboardAction(myYesAction, KeyStroke.getKeyStroke(KeyEvent.VK_Y, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
1737     }
1738
1739     if (myNoAction != null) {
1740       rootPane.registerKeyboardAction(myNoAction, KeyStroke.getKeyStroke(KeyEvent.VK_N, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
1741     }
1742   }
1743
1744   /**
1745    *
1746    * @return null if we should ignore <Esc> for window closing
1747    */
1748   @Nullable
1749   protected ActionListener createCancelAction() {
1750     return new ActionListener() {
1751         @Override
1752         public void actionPerformed(ActionEvent e) {
1753           if (!PopupUtil.handleEscKeyEvent()) {
1754             doCancelAction(e);
1755           }
1756         }
1757       };
1758   }
1759
1760   private void focusPreviousButton() {
1761     JButton[] myButtons = new ArrayList<>(myButtonMap.values()).toArray(new JButton[0]);
1762     for (int i = 0; i < myButtons.length; i++) {
1763       if (myButtons[i].hasFocus()) {
1764         if (i == 0) {
1765           myButtons[myButtons.length - 1].requestFocus();
1766           return;
1767         }
1768         myButtons[i - 1].requestFocus();
1769         return;
1770       }
1771     }
1772   }
1773
1774   private void focusNextButton() {
1775     JButton[] myButtons = new ArrayList<>(myButtonMap.values()).toArray(new JButton[0]);
1776     for (int i = 0; i < myButtons.length; i++) {
1777       if (myButtons[i].hasFocus()) {
1778         if (i == myButtons.length - 1) {
1779           myButtons[0].requestFocus();
1780           return;
1781         }
1782         myButtons[i + 1].requestFocus();
1783         return;
1784       }
1785     }
1786   }
1787
1788   public long getTypeAheadTimeoutMs() {
1789     return 0L;
1790   }
1791
1792   public boolean isToDispatchTypeAhead() {
1793     return isOK();
1794   }
1795
1796   public static boolean isMultipleModalDialogs() {
1797     final Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
1798     if (c != null) {
1799       final DialogWrapper wrapper = findInstance(c);
1800       return wrapper != null && wrapper.getPeer().getCurrentModalEntities().length > 1;
1801     }
1802     return false;
1803   }
1804
1805   /**
1806    * Base class for dialog wrapper actions that need to ensure that only
1807    * one action for the dialog is running.
1808    */
1809   protected abstract class DialogWrapperAction extends AbstractAction {
1810     /**
1811      * The constructor
1812      *
1813      * @param name the action name (see {@link Action#NAME})
1814      */
1815     protected DialogWrapperAction(@NotNull String name) {
1816       putValue(NAME, name);
1817     }
1818
1819     /**
1820      * {@inheritDoc}
1821      */
1822     @Override
1823     public void actionPerformed(ActionEvent e) {
1824       if (myClosed) return;
1825       if (myPerformAction) return;
1826       try {
1827         myPerformAction = true;
1828         doAction(e);
1829       }
1830       finally {
1831         myPerformAction = false;
1832       }
1833     }
1834
1835     /**
1836      * Do actual work for the action. This method is called only if no other action
1837      * is performed in parallel (checked using {@link DialogWrapper#myPerformAction}),
1838      * and dialog is active (checked using {@link DialogWrapper#myClosed})
1839      *
1840      * @param e action
1841      */
1842     protected abstract void doAction(ActionEvent e);
1843   }
1844
1845   protected class OkAction extends DialogWrapperAction {
1846     protected OkAction() {
1847       super(CommonBundle.getOkButtonText());
1848       putValue(DEFAULT_ACTION, Boolean.TRUE);
1849     }
1850
1851     @Override
1852     protected void doAction(ActionEvent e) {
1853       List<ValidationInfo> infoList = doValidateAll();
1854       if (!infoList.isEmpty()) {
1855         ValidationInfo info = infoList.get(0);
1856         if (info.component != null && info.component.isVisible()) {
1857           IdeFocusManager.getInstance(null).requestFocus(info.component, true);
1858         }
1859
1860         if (!Registry.is("ide.inplace.errors.balloon")) {
1861           DialogEarthquakeShaker.shake(getPeer().getWindow());
1862         }
1863
1864         startTrackingValidation();
1865         return;
1866       }
1867       doOKAction();
1868     }
1869   }
1870
1871   protected class CancelAction extends DialogWrapperAction {
1872     private CancelAction() {
1873       super(CommonBundle.getCancelButtonText());
1874     }
1875
1876     @Override
1877     protected void doAction(ActionEvent e) {
1878       doCancelAction();
1879     }
1880   }
1881
1882   /**
1883    * The action that just closes dialog with the specified exit code
1884    * (like the default behavior of the actions "Ok" and "Cancel").
1885    */
1886   protected class DialogWrapperExitAction extends DialogWrapperAction {
1887     /**
1888      * The exit code for the action
1889      */
1890     protected final int myExitCode;
1891
1892     /**
1893      * The constructor
1894      *
1895      * @param name     the action name
1896      * @param exitCode the exit code for dialog
1897      */
1898     public DialogWrapperExitAction(String name, int exitCode) {
1899       super(name);
1900       myExitCode = exitCode;
1901     }
1902
1903     @Override
1904     protected void doAction(ActionEvent e) {
1905       if (isEnabled()) {
1906         close(myExitCode);
1907       }
1908     }
1909   }
1910
1911   private class HelpAction extends AbstractAction {
1912     private HelpAction() {
1913       putValue(NAME, CommonBundle.getHelpButtonText());
1914     }
1915
1916     @Override
1917     public void actionPerformed(ActionEvent e) {
1918       doHelpAction();
1919     }
1920   }
1921
1922   /**
1923    * Don't override this method. It is not final for the API compatibility.
1924    * It will not be called by the DialogWrapper's validator.
1925    * Use this method only in circumstances when the exact invalid component is hard to
1926    * detect or the valid status is based on several fields. In other cases use
1927    * <code>{@link #setErrorText(String, JComponent)}</code> method.
1928    * @param text the error text to display
1929    */
1930   protected void setErrorText(@Nullable final String text) {
1931     setErrorText(text, null);
1932   }
1933
1934   protected void setErrorText(@Nullable final String text, @Nullable final JComponent component) {
1935     setErrorInfoAll((text == null) ?
1936                  Collections.EMPTY_LIST :
1937                  Collections.singletonList(new ValidationInfo(text, component)));
1938   }
1939
1940   protected void setErrorInfoAll(@NotNull List<ValidationInfo> info) {
1941     if (myInfo.equals(info)) return;
1942
1943     myErrorTextAlarm.cancelAllRequests();
1944     SwingUtilities.invokeLater(() -> myErrorText.clearError());
1945
1946     List<ValidationInfo> corrected = myInfo.stream().filter((vi) -> !info.contains(vi)).collect(Collectors.toList());
1947     if (Registry.is("ide.inplace.errors.outline")) {
1948       corrected.stream().filter(vi -> (vi.component != null && vi.component.getBorder() instanceof ErrorBorderCapable)).
1949             forEach(vi -> vi.component.putClientProperty("JComponent.error.outline", false));
1950     }
1951
1952     if (Registry.is("ide.inplace.errors.balloon")) {
1953       corrected.stream().filter(vi -> vi.component != null).forEach(vi -> {
1954         vi.component.putClientProperty("JComponent.error.balloon.builder", null);
1955
1956         Balloon balloon = (Balloon)vi.component.getClientProperty("JComponent.error.balloon");
1957         if (balloon != null && !balloon.isDisposed()) {
1958           balloon.hide();
1959         }
1960
1961         Component fc = getFocusable(vi.component);
1962         if (fc != null) {
1963           for (FocusListener fl : fc.getFocusListeners()) {
1964             if (fl instanceof ErrorFocusListener) {
1965               fc.removeFocusListener(fl);
1966             }
1967           }
1968         }
1969       });
1970     }
1971
1972     myInfo = info;
1973
1974     if (Registry.is("ide.inplace.errors.outline")) {
1975       myInfo.stream().filter(vi -> (vi.component != null && vi.component.getBorder() instanceof ErrorBorderCapable)).
1976         forEach(vi -> vi.component.putClientProperty("JComponent.error.outline", true));
1977     }
1978
1979     if (Registry.is("ide.inplace.errors.balloon") && !myInfo.isEmpty()) {
1980       for (ValidationInfo vi : myInfo) {
1981         Component fc = getFocusable(vi.component);
1982         if (fc != null && fc.isFocusable()) {
1983           if (vi.component.getClientProperty("JComponent.error.balloon.builder") == null) {
1984             JLabel label = new JLabel();
1985             label.setHorizontalAlignment(SwingConstants.LEADING);
1986             setErrorTipText(vi.component, label, vi.message);
1987
1988             BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createBalloonBuilder(label)
1989               .setDisposable(getDisposable())
1990               .setBorderInsets(UIManager.getInsets("Balloon.error.textInsets"))
1991               .setPointerSize(new JBDimension(17, 6))
1992               .setCornerToPointerDistance(JBUI.scale(30))
1993               .setHideOnKeyOutside(false)
1994               .setHideOnClickOutside(false)
1995               .setHideOnAction(false)
1996               .setBorderColor(BALLOON_BORDER)
1997               .setFillColor(BALLOON_BACKGROUND)
1998               .setHideOnFrameResize(false)
1999               .setRequestFocus(false)
2000               .setAnimationCycle(100)
2001               .setShadow(true);
2002
2003             vi.component.putClientProperty("JComponent.error.balloon.builder", balloonBuilder);
2004
2005             ErrorFocusListener fl = new ErrorFocusListener(label, vi.message, vi.component);
2006             if (fc.hasFocus()) {
2007               fl.showErrorTip();
2008             }
2009
2010             fc.addFocusListener(fl);
2011             Disposer.register(getDisposable(), () -> fc.removeFocusListener(fl));
2012           }
2013         } else {
2014           SwingUtilities.invokeLater(() -> myErrorText.appendError(vi.message));
2015         }
2016       }
2017     } else if (!myInfo.isEmpty()) {
2018       myErrorTextAlarm.addRequest(() -> {
2019         for (ValidationInfo vi : myInfo) {
2020           myErrorText.appendError(vi.message);
2021         }
2022       }, 300, null);
2023     }
2024   }
2025
2026   private void setErrorTipText(JComponent component, JLabel label, String text) {
2027     Insets insets = UIManager.getInsets("Balloon.error.textInsets");
2028     int oneLineWidth = SwingUtilities2.stringWidth(label, label.getFontMetrics(label.getFont()), text);
2029     int textWidth = getRootPane().getWidth() - component.getX() - insets.left - insets.right - JBUI.scale(30);
2030     if (textWidth < JBUI.scale(90)) textWidth = JBUI.scale(90);
2031     if (textWidth > oneLineWidth) textWidth = oneLineWidth;
2032
2033     String htmlText = String.format("<html><div width=%d>%s</div></html>", textWidth, text);
2034     label.setText(htmlText);
2035   }
2036
2037   private Component getFocusable(Component source) {
2038     if (source == null) {
2039       return null;
2040     } else if (source instanceof JScrollPane) {
2041       return ((JScrollPane)source).getViewport().getView();
2042     } else if (source instanceof JComboBox && ((JComboBox)source).isEditable()) {
2043       return ((JComboBox)source).getEditor().getEditorComponent();
2044     } else if (source instanceof JSpinner) {
2045       Container c = ((JSpinner)source).getEditor();
2046       synchronized (c.getTreeLock()) {
2047         return c.getComponent(0);
2048       }
2049     } else if (source instanceof Container) {
2050       Container container = (Container)source;
2051       List<Component> cl;
2052       synchronized (container.getTreeLock()) {
2053         cl = Arrays.asList(container.getComponents());
2054       }
2055       return cl.stream().filter(c -> c.isFocusable()).count() > 1 ? null : source;
2056     } else {
2057       return source;
2058     }
2059   }
2060
2061   private void updateSize() {
2062     if (myActualSize == null && !myErrorText.isVisible()) {
2063       myActualSize = getSize();
2064     }
2065   }
2066
2067   @Nullable
2068   public static DialogWrapper findInstance(Component c) {
2069     while (c != null) {
2070       if (c instanceof DialogWrapperDialog) {
2071         return ((DialogWrapperDialog)c).getDialogWrapper();
2072       }
2073       c = c.getParent();
2074     }
2075     return null;
2076   }
2077
2078   @Nullable
2079   public static DialogWrapper findInstanceFromFocus() {
2080     return findInstance(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner());
2081   }
2082
2083   private void resizeWithAnimation(@NotNull final Dimension size) {
2084     //todo[kb]: fix this PITA
2085     myResizeInProgress = true;
2086     if (!Registry.is("enable.animation.on.dialogs")) {
2087       setSize(size.width, size.height);
2088       myResizeInProgress = false;
2089       return;
2090     }
2091
2092     new Thread("DialogWrapper resizer") {
2093       int time = 200;
2094       int steps = 7;
2095
2096       @Override
2097       public void run() {
2098         int step = 0;
2099         final Dimension cur = getSize();
2100         int h = (size.height - cur.height) / steps;
2101         int w = (size.width - cur.width) / steps;
2102         while (step++ < steps) {
2103           setSize(cur.width + w * step, cur.height + h * step);
2104           TimeoutUtil.sleep(time / steps);
2105         }
2106         setSize(size.width, size.height);
2107         //repaint();
2108         if (myErrorText.shouldBeVisible()) {
2109           myErrorText.setVisible(true);
2110         }
2111         myResizeInProgress = false;
2112       }
2113     }.start();
2114   }
2115
2116   private class ErrorText extends JPanel {
2117     private final JLabel myLabel = new JLabel();
2118     private List<String> errors = new ArrayList<>();
2119
2120     private ErrorText(int horizontalAlignment) {
2121       setLayout(new BorderLayout());
2122       myLabel.setBorder(JBUI.Borders.empty(16, 13, 16, 13));
2123       myLabel.setHorizontalAlignment(horizontalAlignment);
2124       JBScrollPane pane =
2125         new JBScrollPane(myLabel, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
2126       pane.setBorder(JBUI.Borders.empty());
2127       pane.setBackground(null);
2128       pane.getViewport().setBackground(null);
2129       pane.setOpaque(false);
2130       add(pane, BorderLayout.CENTER);
2131     }
2132
2133     private void clearError() {
2134       errors.clear();
2135       myLabel.setBounds(0, 0, 0, 0);
2136       myLabel.setText("");
2137       setVisible(false);
2138       updateSize();
2139     }
2140
2141     private void appendError(String text) {
2142       errors.add(text);
2143       myLabel.setBounds(0, 0, 0, 0);
2144       StringBuilder sb = new StringBuilder("<html><font color='#" + ColorUtil.toHex(JBColor.RED) + "'>");
2145       errors.forEach(error -> sb.append("<left>").append(error).append("</left><br/>"));
2146       sb.append("</font></html>");
2147       myLabel.setText(sb.toString());
2148       setVisible(true);
2149       updateSize();
2150     }
2151
2152     private boolean shouldBeVisible() {
2153       return !errors.isEmpty();
2154     }
2155
2156     private boolean isTextSet(@NotNull List<ValidationInfo> info) {
2157       if (info.isEmpty()) {
2158         return errors.isEmpty();
2159       } else if (errors.size() == info.size()){
2160         return errors.equals(info.stream().map(i -> i.message).collect(Collectors.toList()));
2161       } else {
2162         return false;
2163       }
2164     }
2165   }
2166
2167   @NotNull
2168   public final DialogWrapperPeer getPeer() {
2169     return myPeer;
2170   }
2171
2172   /**
2173    * Ensure that dialog is used from even dispatch thread.
2174    *
2175    * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread
2176    */
2177   private static void ensureEventDispatchThread() {
2178     if (!EventQueue.isDispatchThread()) {
2179       throw new IllegalStateException("The DialogWrapper can only be used in event dispatch thread. Current thread: "+Thread.currentThread());
2180     }
2181   }
2182
2183   @NotNull
2184   public final Disposable getDisposable() {
2185     return myDisposable;
2186   }
2187
2188   /**
2189    * @see Adapter
2190    */
2191   public interface DoNotAskOption {
2192
2193     abstract class Adapter implements DoNotAskOption {
2194
2195       /**
2196        * Save the state of the checkbox in the settings, or perform some other related action.
2197        * This method is called right after the dialog is {@link #close(int) closed}.
2198        * <br/>
2199        * Note that this method won't be called in the case when the dialog is closed by {@link #CANCEL_EXIT_CODE Cancel}
2200        * if {@link #shouldSaveOptionsOnCancel() saving the choice on cancel is disabled} (which is by default).
2201        *
2202        * @param isSelected true if user selected "don't show again".
2203        * @param exitCode   the {@link #getExitCode() exit code} of the dialog.
2204        * @see #shouldSaveOptionsOnCancel()
2205        */
2206       public abstract void rememberChoice(boolean isSelected, int exitCode);
2207
2208       /**
2209        * Tells whether the checkbox should be selected by default or not.
2210        *
2211        * @return true if the checkbox should be selected by default.
2212        */
2213       public boolean isSelectedByDefault() {
2214         return false;
2215       }
2216
2217       @Override
2218       public boolean shouldSaveOptionsOnCancel() {
2219         return false;
2220       }
2221
2222       @NotNull
2223       @Override
2224       public String getDoNotShowMessage() {
2225         return CommonBundle.message("dialog.options.do.not.ask");
2226       }
2227
2228       @Override
2229       public final boolean isToBeShown() {
2230         return !isSelectedByDefault();
2231       }
2232
2233       @Override
2234       public final void setToBeShown(boolean toBeShown, int exitCode) {
2235         rememberChoice(!toBeShown, exitCode);
2236       }
2237
2238       @Override
2239       public final boolean canBeHidden() {
2240         return true;
2241       }
2242     }
2243
2244     /**
2245      * @return default selection state of checkbox (false -> checkbox selected)
2246      */
2247     boolean isToBeShown();
2248
2249     /**
2250      * @param toBeShown - if dialog should be shown next time (checkbox selected -> false)
2251      * @param exitCode of corresponding DialogWrapper
2252      */
2253     void setToBeShown(boolean toBeShown, int exitCode);
2254
2255     /**
2256      * @return true if checkbox should be shown
2257      */
2258     boolean canBeHidden();
2259
2260     boolean shouldSaveOptionsOnCancel();
2261
2262     @NotNull
2263     String getDoNotShowMessage();
2264   }
2265
2266   @NotNull
2267   private ErrorPaintingType getErrorPaintingType() {
2268     return ErrorPaintingType.SIGN;
2269   }
2270
2271   private class ErrorPainter extends AbstractPainter {
2272     private List<ValidationInfo> info;
2273
2274     @Override
2275     public void executePaint(Component component, Graphics2D g) {
2276       for (ValidationInfo i : info) {
2277         if (i.component != null && !(Registry.is("ide.inplace.errors.outline"))) {
2278           int w = i.component.getWidth();
2279           int h = i.component.getHeight();
2280           Point p;
2281           switch (getErrorPaintingType()) {
2282             case DOT:
2283               p = SwingUtilities.convertPoint(i.component, 2, h / 2, component);
2284               AllIcons.Ide.ErrorPoint.paintIcon(component, g, p.x, p.y);
2285               break;
2286             case SIGN:
2287               p = SwingUtilities.convertPoint(i.component, w, 0, component);
2288               AllIcons.General.Error.paintIcon(component, g, p.x - 8, p.y - 8);
2289               break;
2290             case LINE:
2291               p = SwingUtilities.convertPoint(i.component, 0, h, component);
2292               Graphics g2 = g.create();
2293               try {
2294                 //noinspection UseJBColor
2295                 g2.setColor(new Color(255, 0, 0, 100));
2296                 g2.fillRoundRect(p.x, p.y - 2, w, 4, 2, 2);
2297               } finally {
2298                 g2.dispose();
2299               }
2300               break;
2301           }
2302         }
2303       }
2304     }
2305
2306     @Override
2307     public boolean needsRepaint() {
2308       return true;
2309     }
2310
2311     private void setValidationInfo(@NotNull List<ValidationInfo> info) {
2312       this.info = info;
2313     }
2314   }
2315
2316   private static class ErrorTipTracker extends PositionTracker<Balloon> {
2317     private final int y;
2318
2319     private ErrorTipTracker(JComponent component, int y) {
2320       super(component);
2321       this.y = y;
2322     }
2323
2324     @Override public RelativePoint recalculateLocation(Balloon balloon) {
2325       int width = getComponent().getWidth();
2326       int delta = width < JBUI.scale(120) ? width / 2 : JBUI.scale(60);
2327       return new RelativePoint(getComponent(), new Point(delta, y));
2328     }
2329   }
2330
2331   private class ErrorFocusListener implements FocusListener {
2332     private final JLabel     label;
2333     private final String     text;
2334     private final JComponent component;
2335
2336     private ErrorFocusListener(JLabel label, String text, JComponent component) {
2337       this.label = label;
2338       this.text = text;
2339       this.component = component;
2340     }
2341
2342     @Override public void focusGained(FocusEvent e) {
2343       Balloon b = (Balloon)component.getClientProperty("JComponent.error.balloon");
2344       if (b == null || b.isDisposed()) {
2345         showErrorTip();
2346       }
2347     }
2348
2349     @Override public void focusLost(FocusEvent e) {
2350       Balloon b = (Balloon)component.getClientProperty("JComponent.error.balloon");
2351       if (b != null && !b.isDisposed()) {
2352         b.hide();
2353       }
2354     }
2355
2356     private void showErrorTip() {
2357       BalloonBuilder balloonBuilder = (BalloonBuilder)component.getClientProperty("JComponent.error.balloon.builder");
2358       if (balloonBuilder == null) return;
2359
2360       Balloon balloon = balloonBuilder.createBalloon();
2361
2362       ComponentListener rl = new ComponentAdapter() {
2363         @Override public void componentResized(ComponentEvent e) {
2364           if (!balloon.isDisposed()) {
2365             setErrorTipText(component, label, text);
2366             balloon.revalidate();
2367           }
2368         }
2369       };
2370
2371       balloon.addListener(new JBPopupListener.Adapter() {
2372         @Override public void onClosed(LightweightWindowEvent event) {
2373           JRootPane rootPane = getRootPane();
2374           if (rootPane != null) {
2375             rootPane.removeComponentListener(rl);
2376           }
2377
2378           if (component.getClientProperty("JComponent.error.balloon") == event.asBalloon()) {
2379             component.putClientProperty("JComponent.error.balloon", null);
2380           }
2381         }
2382       });
2383
2384       getRootPane().addComponentListener(rl);
2385
2386       Point componentPos = SwingUtilities.convertPoint(component, 0, 0, getRootPane().getLayeredPane());
2387       Dimension bSize = balloon.getPreferredSize();
2388
2389       Insets cInsets = component.getInsets();
2390       int top =  cInsets != null ? cInsets.top : 0;
2391       if (componentPos.y >= bSize.height + top) {
2392         balloon.show(new ErrorTipTracker(component, 0), Balloon.Position.above);
2393       } else {
2394         balloon.show(new ErrorTipTracker(component, component.getHeight()), Balloon.Position.below);
2395       }
2396       component.putClientProperty("JComponent.error.balloon", balloon);
2397     }
2398   }
2399
2400   private enum ErrorPaintingType {DOT, SIGN, LINE}
2401
2402   public enum DialogStyle {NO_STYLE, COMPACT}
2403
2404 }