/* * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.ui; import com.intellij.CommonBundle; import com.intellij.icons.AllIcons; import com.intellij.ide.ui.UISettings; import com.intellij.idea.ActionsBundle; import com.intellij.openapi.Disposable; import com.intellij.openapi.MnemonicHelper; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.help.HelpManager; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.*; import com.intellij.openapi.ui.popup.util.PopupUtil; import com.intellij.openapi.util.*; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeGlassPane; import com.intellij.openapi.wm.IdeGlassPaneUtil; import com.intellij.openapi.wm.WindowManager; import com.intellij.ui.ColorUtil; import com.intellij.ui.JBColor; import com.intellij.ui.UIBundle; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.border.CustomLineBorder; import com.intellij.ui.components.JBOptionButton; import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.panels.NonOpaquePanel; import com.intellij.util.Alarm; import com.intellij.util.IconUtil; import com.intellij.util.TimeoutUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.*; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import sun.swing.SwingUtilities2; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.plaf.UIResource; import java.awt.*; import java.awt.event.*; import java.util.*; import java.util.List; import java.util.stream.Collectors; /** * The standard base class for modal dialog boxes. The dialog wrapper could be used only on event dispatch thread. * In case when the dialog must be created from other threads use * {@link EventQueue#invokeLater(Runnable)} or {@link EventQueue#invokeAndWait(Runnable)}. * <p/> * See also http://www.jetbrains.org/intellij/sdk/docs/user_interface_components/dialog_wrapper.html. */ @SuppressWarnings({"SSBasedInspection", "MethodMayBeStatic"}) public abstract class DialogWrapper { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.ui.DialogWrapper"); public enum IdeModalityType { IDE, PROJECT, MODELESS; @NotNull public Dialog.ModalityType toAwtModality() { switch (this) { case IDE: return Dialog.ModalityType.APPLICATION_MODAL; case PROJECT: return Dialog.ModalityType.DOCUMENT_MODAL; case MODELESS: return Dialog.ModalityType.MODELESS; } throw new IllegalStateException(toString()); } } /** * The default exit code for "OK" action. */ public static final int OK_EXIT_CODE = 0; /** * The default exit code for "Cancel" action. */ public static final int CANCEL_EXIT_CODE = 1; /** * The default exit code for "Close" action. Equal to cancel. */ public static final int CLOSE_EXIT_CODE = CANCEL_EXIT_CODE; /** * If you use your own custom exit codes you have to start them with * this constant. */ public static final int NEXT_USER_EXIT_CODE = 2; /** * If your action returned by <code>createActions</code> method has non * <code>null</code> value for this key, then the button that corresponds to the action will be the * default button for the dialog. It's true if you don't change this behaviour * of <code>createJButtonForAction(Action)</code> method. */ @NonNls public static final String DEFAULT_ACTION = "DefaultAction"; @NonNls public static final String FOCUSED_ACTION = "FocusedAction"; @NonNls private static final String NO_AUTORESIZE = "NoAutoResizeAndFit"; private static final KeyStroke SHOW_OPTION_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.ALT_MASK | InputEvent.SHIFT_MASK); @NotNull private final DialogWrapperPeer myPeer; private int myExitCode = CANCEL_EXIT_CODE; /** * The shared instance of default border for dialog's content pane. */ public static final Border ourDefaultBorder = new EmptyBorder(UIUtil.PANEL_REGULAR_INSETS); private float myHorizontalStretch = 1.0f; private float myVerticalStretch = 1.0f; /** * Defines horizontal alignment of buttons. */ private int myButtonAlignment = SwingConstants.RIGHT; private boolean myCrossClosesWindow = true; private Insets myButtonMargins = JBUI.insets(2, 16); protected Action myOKAction; protected Action myCancelAction; protected Action myHelpAction; private final Map<Action, JButton> myButtonMap = new LinkedHashMap<>(); private boolean myClosed = false; protected boolean myPerformAction = false; private Action myYesAction = null; private Action myNoAction = null; protected JCheckBox myCheckBoxDoNotShowDialog; @Nullable private DoNotAskOption myDoNotAsk; protected JComponent myPreferredFocusedComponent; private Computable<Point> myInitialLocationCallback; private Dimension myActualSize = null; private List<ValidationInfo> myInfo = Collections.emptyList(); @NotNull protected final Disposable myDisposable = new Disposable() { @Override public String toString() { return DialogWrapper.this.toString(); } @Override public void dispose() { DialogWrapper.this.dispose(); } }; private final List<JBOptionButton> myOptionsButtons = new ArrayList<>(); private int myCurrentOptionsButtonIndex = -1; private boolean myResizeInProgress = false; private ComponentAdapter myResizeListener; @NotNull protected String getDoNotShowMessage() { return CommonBundle.message("dialog.options.do.not.show"); } public void setDoNotAskOption(@Nullable DoNotAskOption doNotAsk) { myDoNotAsk = doNotAsk; } private ErrorText myErrorText; private final Alarm myErrorTextAlarm = new Alarm(); private static final Color BALLOON_BORDER = new JBColor(new Color(0xe0a8a9), new Color(0x73454b)); private static final Color BALLOON_BACKGROUND = new JBColor(new Color(0xf5e6e7), new Color(0x593d41)); /** * Creates modal <code>DialogWrapper</code>. The currently active window will be the dialog's parent. * * @param project parent window for the dialog will be calculated based on focused window for the * specified <code>project</code>. This parameter can be <code>null</code>. In this case parent window * will be suggested based on current focused window. * @param canBeParent specifies whether the dialog can be parent for other windows. This parameter is used * by <code>WindowManager</code>. * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread */ protected DialogWrapper(@Nullable Project project, boolean canBeParent) { this(project, canBeParent, IdeModalityType.IDE); } protected DialogWrapper(@Nullable Project project, boolean canBeParent, @NotNull IdeModalityType ideModalityType) { this(project, null, canBeParent, ideModalityType); } protected DialogWrapper(@Nullable Project project, @Nullable Component parentComponent, boolean canBeParent, @NotNull IdeModalityType ideModalityType) { myPeer = parentComponent == null ? createPeer(project, canBeParent, project == null ? IdeModalityType.IDE : ideModalityType) : createPeer(parentComponent, canBeParent); final Window window = myPeer.getWindow(); if (window != null) { myResizeListener = new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (!myResizeInProgress) { myActualSize = myPeer.getSize(); if (myErrorText != null && myErrorText.isVisible()) { myActualSize.height -= myErrorText.myLabel.getHeight(); } } } }; window.addComponentListener(myResizeListener); } createDefaultActions(); } /** * Creates modal <code>DialogWrapper</code> that can be parent for other windows. * The currently active window will be the dialog's parent. * * @param project parent window for the dialog will be calculated based on focused window for the * specified <code>project</code>. This parameter can be <code>null</code>. In this case parent window * will be suggested based on current focused window. * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread * @see DialogWrapper#DialogWrapper(Project, boolean) */ protected DialogWrapper(@Nullable Project project) { this(project, true); } /** * Creates modal <code>DialogWrapper</code>. The currently active window will be the dialog's parent. * * @param canBeParent specifies whether the dialog can be parent for other windows. This parameter is used * by <code>WindowManager</code>. * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread */ protected DialogWrapper(boolean canBeParent) { this((Project)null, canBeParent); } /** * Typically, we should set a parent explicitly. Use WindowManager#suggestParentWindow * method to find out the best parent for your dialog. Exceptions are cases * when we do not have a project to figure out which window * is more suitable as an owner for the dialog. * <p/> * Instead, use {@link DialogWrapper#DialogWrapper(Project, boolean, boolean)} */ @Deprecated protected DialogWrapper(boolean canBeParent, boolean applicationModalIfPossible) { this(null, canBeParent, applicationModalIfPossible); } protected DialogWrapper(Project project, boolean canBeParent, boolean applicationModalIfPossible) { ensureEventDispatchThread(); if (ApplicationManager.getApplication() != null) { myPeer = createPeer( project != null ? WindowManager.getInstance().suggestParentWindow(project) : WindowManager.getInstance().findVisibleFrame() , canBeParent, applicationModalIfPossible); } else { myPeer = createPeer(null, canBeParent, applicationModalIfPossible); } createDefaultActions(); } /** * @param parent parent component which is used to calculate heavy weight window ancestor. * <code>parent</code> cannot be <code>null</code> and must be showing. * @param canBeParent can be parent * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread */ protected DialogWrapper(@NotNull Component parent, boolean canBeParent) { ensureEventDispatchThread(); myPeer = createPeer(parent, canBeParent); createDefaultActions(); } //validation private final Alarm myValidationAlarm = new Alarm(getValidationThreadToUse(), myDisposable); @NotNull protected Alarm.ThreadToUse getValidationThreadToUse() { return Alarm.ThreadToUse.SWING_THREAD; } private int myValidationDelay = 300; private boolean myDisposed = false; private boolean myValidationStarted = false; private final ErrorPainter myErrorPainter = new ErrorPainter(); private boolean myErrorPainterInstalled = false; /** * Allows to postpone first start of validation * * @return <code>false</code> if start validation in <code>init()</code> method */ protected boolean postponeValidation() { return true; } /** * Validates user input and returns <code>null</code> if everything is fine * or validation description with component where problem has been found. * * @return <code>null</code> if everything is OK or validation descriptor */ @Nullable protected ValidationInfo doValidate() { return null; } /** * Validates user input and returns <code>List<ValidationInfo></code>. * If everything is fine the returned list is empty otherwise * the list contains all invalid fields with error messages. * This method should preferably be used when validating forms with multiply * fields that require validation. * * @return <code>List<ValidationInfo></code> of invalid fields. List * is empty if no errors found. */ @NotNull protected List<ValidationInfo> doValidateAll() { ValidationInfo vi = doValidate(); return vi != null ? Collections.singletonList(vi) : Collections.EMPTY_LIST; } public void setValidationDelay(int delay) { myValidationDelay = delay; } private void installErrorPainter() { if (myErrorPainterInstalled) return; myErrorPainterInstalled = true; UIUtil.invokeLaterIfNeeded(() -> IdeGlassPaneUtil.installPainter(getContentPanel(), myErrorPainter, myDisposable)); } protected void updateErrorInfo(@NotNull List<ValidationInfo> info) { boolean updateNeeded = Registry.is("ide.inplace.errors.balloon") ? !myInfo.equals(info) : !myErrorText.isTextSet(info); if (updateNeeded) { SwingUtilities.invokeLater(() -> { if (myDisposed) return; setErrorInfoAll(info); myPeer.getRootPane().getGlassPane().repaint(); getOKAction().setEnabled(info.isEmpty()); }); } } protected void createDefaultActions() { myOKAction = new OkAction(); myCancelAction = new CancelAction(); myHelpAction = new HelpAction(); } public void setUndecorated(boolean undecorated) { myPeer.setUndecorated(undecorated); } public final void addMouseListener(@NotNull MouseListener listener) { myPeer.addMouseListener(listener); } public final void addMouseListener(@NotNull MouseMotionListener listener) { myPeer.addMouseListener(listener); } public final void addKeyListener(@NotNull KeyListener listener) { myPeer.addKeyListener(listener); } /** * Closes and disposes the dialog and sets the specified exit code. * * @param exitCode exit code * @param isOk is OK * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread */ public final void close(int exitCode, boolean isOk) { ensureEventDispatchThread(); if (myClosed) return; myClosed = true; myExitCode = exitCode; Window window = getWindow(); if (window != null && myResizeListener != null) { window.removeComponentListener(myResizeListener); myResizeListener = null; } if (isOk) { processDoNotAskOnOk(exitCode); } else { processDoNotAskOnCancel(); } Disposer.dispose(myDisposable); } public final void close(int exitCode) { close(exitCode, exitCode != CANCEL_EXIT_CODE); } /** * Creates border for dialog's content pane. By default content * pane has has empty border with <code>(8,12,8,12)</code> insets. Subclasses can * return <code>null</code> for no border. * * @return content pane border */ @Nullable protected Border createContentPaneBorder() { if (getStyle() == DialogStyle.COMPACT) { return JBUI.Borders.empty(); } return ourDefaultBorder; } protected static boolean isMoveHelpButtonLeft() { return UIUtil.isUnderAquaBasedLookAndFeel() || UIUtil.isUnderDarcula() || UIUtil.isUnderWin10LookAndFeel(); } private static boolean isRemoveHelpButton() { return SystemInfo.isWindows && (UIUtil.isUnderDarcula() || UIUtil.isUnderIntelliJLaF()) && Registry.is("ide.win.frame.decoration") || Registry.is("ide.remove.help.button.from.dialogs"); } /** * Creates panel located at the south of the content pane. By default that * panel contains dialog's buttons. This default implementation uses <code>createActions()</code> * and <code>createJButtonForAction(Action)</code> methods to construct the panel. * * @return south panel */ protected JComponent createSouthPanel() { List<Action> actions = ContainerUtil.filter(createActions(), Condition.NOT_NULL); List<Action> leftSideActions = ContainerUtil.newArrayList(createLeftSideActions()); if (!ApplicationInfo.contextHelpAvailable()) { actions.remove(getHelpAction()); } boolean hasHelpToMoveToLeftSide = false; if (isRemoveHelpButton()) { actions.remove(getHelpAction()); } else if (isMoveHelpButtonLeft() && actions.contains(getHelpAction())) { hasHelpToMoveToLeftSide = true; actions.remove(getHelpAction()); } if (SystemInfo.isMac) { Action macOtherAction = ContainerUtil.find(actions, MacOtherAction.class::isInstance); if (macOtherAction != null) { leftSideActions.add(macOtherAction); actions.remove(macOtherAction); } // move ok action to the right int okNdx = actions.indexOf(getOKAction()); if (okNdx >= 0 && okNdx != actions.size() - 1) { actions.remove(getOKAction()); actions.add(getOKAction()); } // move cancel action to the left int cancelNdx = actions.indexOf(getCancelAction()); if (cancelNdx > 0) { actions.remove(getCancelAction()); actions.add(0, getCancelAction()); } } else if (UIUtil.isUnderGTKLookAndFeel() && actions.contains(getHelpAction())) { leftSideActions.add(getHelpAction()); actions.remove(getHelpAction()); } if (!UISettings.getShadowInstance().getAllowMergeButtons()) { actions = flattenOptionsActions(actions); leftSideActions = flattenOptionsActions(leftSideActions); } List<JButton> leftSideButtons = createButtons(leftSideActions); List<JButton> rightSideButtons = createButtons(actions); myButtonMap.clear(); for (JButton button : ContainerUtil.concat(leftSideButtons, rightSideButtons)) { myButtonMap.put(button.getAction(), button); if (button instanceof JBOptionButton) { myOptionsButtons.add((JBOptionButton)button); } } return createSouthPanel(leftSideButtons, rightSideButtons, hasHelpToMoveToLeftSide); } @NotNull protected JButton createHelpButton(Insets insets) { final JButton helpButton; if ((UIUtil.isUnderWin10LookAndFeel())) { helpButton = new JButton(getHelpAction()) { @Override public void paint(Graphics g) { IconUtil.paintInCenterOf(this, g, AllIcons.Windows.WinHelp); } @Override public Dimension getPreferredSize() { return new Dimension(AllIcons.Windows.WinHelp.getIconWidth(), AllIcons.Windows.WinHelp.getIconHeight()); } }; helpButton.setOpaque(false); } else { helpButton = new JButton(getHelpAction()); } helpButton.putClientProperty("JButton.buttonType", "help"); helpButton.setText(""); helpButton.setMargin(insets); helpButton.setToolTipText(ActionsBundle.actionDescription("HelpTopics")); return helpButton; } @NotNull private static List<Action> flattenOptionsActions(@NotNull List<Action> actions) { List<Action> newActions = new ArrayList<>(); for (Action action : actions) { newActions.add(action); if (action instanceof OptionAction) { ContainerUtil.addAll(newActions, ((OptionAction)action).getOptions()); } } return newActions; } protected boolean shouldAddErrorNearButtons() { return false; } @NotNull protected DialogStyle getStyle() { return DialogStyle.NO_STYLE; } protected boolean toBeShown() { return !myCheckBoxDoNotShowDialog.isSelected(); } public boolean isTypeAheadEnabled() { return false; } @NotNull private List<JButton> createButtons(@NotNull List<Action> actions) { List<JButton> buttons = new ArrayList<>(); for (Action action : actions) { buttons.add(createJButtonForAction(action)); } return buttons; } @NotNull private JPanel createSouthPanel(@NotNull List<JButton> leftSideButtons, @NotNull List<JButton> rightSideButtons, boolean hasHelpToMoveToLeftSide) { JPanel panel = new JPanel(new BorderLayout()) { @Override public Color getBackground() { final Color bg = UIManager.getColor("DialogWrapper.southPanelBackground"); if (getStyle() == DialogStyle.COMPACT && bg != null) { return bg; } return super.getBackground(); } }; if (myDoNotAsk != null) { myCheckBoxDoNotShowDialog = new JCheckBox(myDoNotAsk.getDoNotShowMessage()); myCheckBoxDoNotShowDialog.setVisible(myDoNotAsk.canBeHidden()); myCheckBoxDoNotShowDialog.setSelected(!myDoNotAsk.isToBeShown()); DialogUtil.registerMnemonic(myCheckBoxDoNotShowDialog, '&'); } JComponent doNotAskCheckbox = createDoNotAskCheckbox(); final JPanel lrButtonsPanel = new NonOpaquePanel(new GridBagLayout()); //noinspection UseDPIAwareInsets final Insets insets = SystemInfo.isMacOSLeopard ? UIUtil.isUnderIntelliJLaF() ? JBUI.insets(0, 8) : JBUI.emptyInsets() : new Insets(8, 0, 0, 0); //don't wrap to JBInsets if (rightSideButtons.size() > 0 || leftSideButtons.size() > 0) { GridBag bag = new GridBag().setDefaultInsets(insets); if (leftSideButtons.size() > 0) { JPanel buttonsPanel = createButtonsPanel(leftSideButtons); if (rightSideButtons.size() > 0) { buttonsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20)); // leave some space between button groups } lrButtonsPanel.add(buttonsPanel, bag.next()); } lrButtonsPanel.add(Box.createHorizontalGlue(), bag.next().weightx(1).fillCellHorizontally()); // left strut if (rightSideButtons.size() > 0) { JPanel buttonsPanel = createButtonsPanel(rightSideButtons); if (shouldAddErrorNearButtons()) { lrButtonsPanel.add(myErrorText, bag.next()); lrButtonsPanel.add(Box.createHorizontalStrut(10), bag.next()); } lrButtonsPanel.add(buttonsPanel, bag.next()); } if (SwingConstants.CENTER == myButtonAlignment && doNotAskCheckbox == null) { lrButtonsPanel.add(Box.createHorizontalGlue(), bag.next().weightx(1).fillCellHorizontally()); // right strut } } JComponent helpButton = null; if (hasHelpToMoveToLeftSide) { helpButton = createHelpButton(insets); } if (helpButton != null || doNotAskCheckbox != null) { JPanel leftPanel = new JPanel(new BorderLayout()); if (helpButton != null) leftPanel.add(helpButton, BorderLayout.WEST); if (doNotAskCheckbox != null) { doNotAskCheckbox.setBorder(JBUI.Borders.emptyRight(20)); leftPanel.add(doNotAskCheckbox, BorderLayout.CENTER); } panel.add(leftPanel, BorderLayout.WEST); } panel.add(lrButtonsPanel, BorderLayout.CENTER); if (getStyle() == DialogStyle.COMPACT) { final Color color = UIManager.getColor("DialogWrapper.southPanelDivider"); Border line = new CustomLineBorder(color != null ? color : OnePixelDivider.BACKGROUND, 1, 0, 0, 0); panel.setBorder(new CompoundBorder(line, JBUI.Borders.empty(8, 12))); } else { panel.setBorder(JBUI.Borders.emptyTop(8)); } return panel; } @Nullable protected JComponent createDoNotAskCheckbox() { return myCheckBoxDoNotShowDialog != null && myCheckBoxDoNotShowDialog.isVisible() ? myCheckBoxDoNotShowDialog : null; } @NotNull private JPanel createButtonsPanel(@NotNull List<JButton> buttons) { int hgap = SystemInfo.isMacOSLeopard ? UIUtil.isUnderIntelliJLaF() ? 8 : 0 : 5; JPanel buttonsPanel = new NonOpaquePanel(new GridLayout(1, buttons.size(), hgap, 0)); for (final JButton button : buttons) { buttonsPanel.add(button); } return buttonsPanel; } /** * * @param action should be registered to find corresponding JButton * @return button for specified action or null if it's not found */ @Nullable protected JButton getButton(@NotNull Action action) { return myButtonMap.get(action); } /** * Creates <code>JButton</code> for the specified action. If the button has not <code>null</code> * value for <code>DialogWrapper.DEFAULT_ACTION</code> key then the created button will be the * default one for the dialog. * * @param action action for the button * @return button with action specified * @see DialogWrapper#DEFAULT_ACTION */ protected JButton createJButtonForAction(Action action) { JButton button; if (action instanceof OptionAction && UISettings.getShadowInstance().getAllowMergeButtons()) { button = createJOptionsButton((OptionAction)action); } else { button = new JButton(action); } if (SystemInfo.isMac) { button.putClientProperty("JButton.buttonType", "text"); } Pair<Integer, String> pair = extractMnemonic(button.getText()); button.setText(pair.second); int mnemonic = pair.first; final Object value = action.getValue(Action.MNEMONIC_KEY); if (value instanceof Integer) { mnemonic = (Integer)value; } button.setMnemonic(mnemonic); final Object name = action.getValue(Action.NAME); if (mnemonic == KeyEvent.VK_Y && "Yes".equals(name)) { myYesAction = action; } else if (mnemonic == KeyEvent.VK_N && "No".equals(name)) { myNoAction = action; } setMargin(button); if (action.getValue(DEFAULT_ACTION) != null) { if (!myPeer.isHeadless()) { getRootPane().setDefaultButton(button); } } if (action.getValue(FOCUSED_ACTION) != null) { myPreferredFocusedComponent = button; } return button; } @NotNull private JButton createJOptionsButton(@NotNull OptionAction action) { JBOptionButton optionButton = new JBOptionButton(action, action.getOptions()); optionButton.setOkToProcessDefaultMnemonics(false); optionButton.setOptionTooltipText( "Press " + KeymapUtil.getKeystrokeText(SHOW_OPTION_KEYSTROKE) + " to expand or use a mnemonic of a contained action"); final Set<JBOptionButton.OptionInfo> infos = optionButton.getOptionInfos(); for (final JBOptionButton.OptionInfo eachInfo : infos) { if (eachInfo.getMnemonic() >= 0) { final char mnemonic = (char)eachInfo.getMnemonic(); JRootPane rootPane = getPeer().getRootPane(); if (rootPane != null) { new DumbAwareAction("Show JBOptionButton popup") { @Override public void actionPerformed(AnActionEvent e) { final JBOptionButton buttonToActivate = eachInfo.getButton(); buttonToActivate.showPopup(eachInfo.getAction(), true); } }.registerCustomShortcutSet(MnemonicHelper.createShortcut(mnemonic), rootPane, myDisposable); } } } return optionButton; } @NotNull private static Pair<Integer, String> extractMnemonic(@Nullable String text) { if (text == null) return Pair.create(0, null); int mnemonic = 0; StringBuilder plainText = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (ch == '_' || ch == '&') { i++; if (i >= text.length()) { break; } ch = text.charAt(i); if (ch != '_' && ch != '&') { // Mnemonic is case insensitive. int vk = ch; if (vk >= 'a' && vk <= 'z') { vk -= 'a' - 'A'; } mnemonic = vk; } } plainText.append(ch); } return Pair.create(mnemonic, plainText.toString()); } private void setMargin(@NotNull JButton button) { // Aqua LnF does a good job of setting proper margin between buttons. Setting them specifically causes them be 'square' style instead of // 'rounded', which is expected by apple users. if (!SystemInfo.isMac) { if (myButtonMargins == null) { return; } button.setMargin(myButtonMargins); } } @NotNull protected DialogWrapperPeer createPeer(@NotNull Component parent, final boolean canBeParent) { return DialogWrapperPeerFactory.getInstance().createPeer(this, parent, canBeParent); } /** * Dialogs with no parents are discouraged. * Instead, use e.g. {@link DialogWrapper#createPeer(Window, boolean, boolean)} */ @Deprecated @NotNull protected DialogWrapperPeer createPeer(boolean canBeParent, boolean applicationModalIfPossible) { return createPeer(null, canBeParent, applicationModalIfPossible); } @NotNull protected DialogWrapperPeer createPeer(final Window owner, final boolean canBeParent, final IdeModalityType ideModalityType) { return DialogWrapperPeerFactory.getInstance().createPeer(this, owner, canBeParent, ideModalityType); } @Deprecated @NotNull protected DialogWrapperPeer createPeer(final Window owner, final boolean canBeParent, final boolean applicationModalIfPossible) { return DialogWrapperPeerFactory.getInstance() .createPeer(this, owner, canBeParent, applicationModalIfPossible ? IdeModalityType.IDE : IdeModalityType.PROJECT); } @NotNull protected DialogWrapperPeer createPeer(@Nullable final Project project, final boolean canBeParent, @NotNull IdeModalityType ideModalityType) { return DialogWrapperPeerFactory.getInstance().createPeer(this, project, canBeParent, ideModalityType); } @NotNull protected DialogWrapperPeer createPeer(@Nullable final Project project, final boolean canBeParent) { return DialogWrapperPeerFactory.getInstance().createPeer(this, project, canBeParent); } @Nullable protected JComponent createTitlePane() { return null; } /** * Factory method. It creates the panel located at the * north of the dialog's content pane. The implementation can return <code>null</code> * value. In this case there will be no input panel. * * @return north panel */ @Nullable protected JComponent createNorthPanel() { return null; } /** * Factory method. It creates panel with dialog options. Options panel is located at the * center of the dialog's content pane. The implementation can return <code>null</code> * value. In this case there will be no options panel. * * @return center panel */ @Nullable protected abstract JComponent createCenterPanel(); /** * @see Window#toFront() */ public void toFront() { myPeer.toFront(); } /** * @see Window#toBack() */ public void toBack() { myPeer.toBack(); } protected boolean setAutoAdjustable(boolean autoAdjustable) { JRootPane rootPane = getRootPane(); if (rootPane == null) return false; rootPane.putClientProperty(NO_AUTORESIZE, autoAdjustable ? null : Boolean.TRUE); return true; } //true by default public boolean isAutoAdjustable() { JRootPane rootPane = getRootPane(); return rootPane == null || rootPane.getClientProperty(NO_AUTORESIZE) == null; } /** * Dispose the wrapped and releases all resources allocated be the wrapper to help * more efficient garbage collection. You should never invoke this method twice or * invoke any method of the wrapper after invocation of <code>dispose</code>. * * @throws IllegalStateException if the dialog is disposed not on the event dispatch thread */ protected void dispose() { ensureEventDispatchThread(); myErrorTextAlarm.cancelAllRequests(); myValidationAlarm.cancelAllRequests(); myDisposed = true; for (JButton button : myButtonMap.values()) { button.setAction(null); // avoid memory leak via KeyboardManager } myButtonMap.clear(); final JRootPane rootPane = getRootPane(); // if rootPane = null, dialog has already been disposed if (rootPane != null) { unregisterKeyboardActions(rootPane); if (myActualSize != null && isAutoAdjustable()) { setSize(myActualSize.width, myActualSize.height); } myPeer.dispose(); } } public static void cleanupRootPane(@Nullable JRootPane rootPane) { if (rootPane == null) return; // Must be preserved: // Component#appContext, Component#appContext, Container#component // JRootPane#contentPane due to popup recycling & our border styling // Must be cleared: // JComponent#clientProperties, contentPane children RepaintManager.currentManager(rootPane).removeInvalidComponent(rootPane); unregisterKeyboardActions(rootPane); Container contentPane = rootPane.getContentPane(); if (contentPane != null) contentPane.removeAll(); Disposer.clearOwnFields(rootPane, field -> { String clazz = field.getDeclaringClass().getName(); // keep AWT and Swing fields intact, except some if (!clazz.startsWith("java.") && !clazz.startsWith("javax.")) return true; String name = field.getName(); return "clientProperties".equals(name); }); } public static void unregisterKeyboardActions(@Nullable Component rootPane) { int[] flags = {JComponent.WHEN_FOCUSED, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, JComponent.WHEN_IN_FOCUSED_WINDOW}; for (JComponent eachComp : UIUtil.uiTraverser(rootPane).traverse().filter(JComponent.class)) { ActionMap actionMap = eachComp.getActionMap(); if (actionMap == null) continue; for (KeyStroke eachStroke : eachComp.getRegisteredKeyStrokes()) { boolean remove = true; for (int i : flags) { Object key = eachComp.getInputMap(i).get(eachStroke); Action action = key == null ? null : actionMap.get(key); if (action instanceof UIResource) remove = false; } if (remove) eachComp.unregisterKeyboardAction(eachStroke); } } } public static void cleanupWindowListeners(@Nullable Window window) { if (window == null) return; SwingUtilities.invokeLater(() -> { for (WindowListener listener : window.getWindowListeners()) { if (listener.getClass().getName().startsWith("com.intellij.")) { LOG.warn("Stale listener: " + listener); window.removeWindowListener(listener); } } }); } /** * This method is invoked by default implementation of "Cancel" action. It just closes dialog * with <code>CANCEL_EXIT_CODE</code>. This is convenient place to override functionality of "Cancel" action. * Note that the method does nothing if "Cancel" action isn't enabled. */ public void doCancelAction() { if (getCancelAction().isEnabled()) { close(CANCEL_EXIT_CODE); } } private void processDoNotAskOnCancel() { if (myDoNotAsk != null) { if (myDoNotAsk.shouldSaveOptionsOnCancel() && myDoNotAsk.canBeHidden()) { myDoNotAsk.setToBeShown(toBeShown(), CANCEL_EXIT_CODE); } } } /** * You can use this method if you want to know by which event this actions got triggered. It is called only if * the cancel action was triggered by some input event, <code>doCancelAction</code> is called otherwise. * * @param source AWT event * @see #doCancelAction */ public void doCancelAction(AWTEvent source) { doCancelAction(); } /** * Programmatically perform a "click" of default dialog's button. The method does * nothing if the dialog has no default button. */ public void clickDefaultButton() { JButton button = getRootPane().getDefaultButton(); if (button != null) { button.doClick(); } } /** * This method is invoked by default implementation of "OK" action. It just closes dialog * with <code>OK_EXIT_CODE</code>. This is convenient place to override functionality of "OK" action. * Note that the method does nothing if "OK" action isn't enabled. */ protected void doOKAction() { if (getOKAction().isEnabled()) { close(OK_EXIT_CODE); } } protected void processDoNotAskOnOk(int exitCode) { if (myDoNotAsk != null) { if (myDoNotAsk.canBeHidden()) { myDoNotAsk.setToBeShown(toBeShown(), exitCode); } } } /** * @return whether the native window cross button closes the window or not. * <code>true</code> means that cross performs hide or dispose of the dialog. */ public boolean shouldCloseOnCross() { return myCrossClosesWindow; } /** * Creates actions for dialog. * <p/> * By default "OK" and "Cancel" actions are returned. The "Help" action is automatically added if * {@link #getHelpId()} returns non-null value. * <p/> * Each action is represented by <code>JButton</code> created by {@link #createJButtonForAction(Action)}. * These buttons are then placed into {@link #createSouthPanel() south panel} of dialog. * * @return dialog actions * @see #createSouthPanel * @see #createJButtonForAction */ @NotNull protected Action[] createActions() { Action helpAction = getHelpAction(); return helpAction == myHelpAction && getHelpId() == null ? new Action[]{getOKAction(), getCancelAction()} : new Action[]{getOKAction(), getCancelAction(), helpAction}; } @NotNull protected Action[] createLeftSideActions() { return new Action[0]; } /** * @return default implementation of "OK" action. This action just invokes * <code>doOKAction()</code> method. * @see #doOKAction */ @NotNull protected Action getOKAction() { return myOKAction; } /** * @return default implementation of "Cancel" action. This action just invokes * <code>doCancelAction()</code> method. * @see #doCancelAction */ @NotNull protected Action getCancelAction() { return myCancelAction; } /** * @return default implementation of "Help" action. This action just invokes * <code>doHelpAction()</code> method. * @see #doHelpAction */ @NotNull protected Action getHelpAction() { return myHelpAction; } protected boolean isProgressDialog() { return false; } public final boolean isModalProgress() { return isProgressDialog(); } /** * Returns content pane * * @return content pane * @see JDialog#getContentPane */ public Container getContentPane() { return myPeer.getContentPane(); } /** * @see JDialog#validate */ public void validate() { myPeer.validate(); } /** * @see JDialog#repaint */ public void repaint() { myPeer.repaint(); } /** * Returns key for persisting dialog dimensions. * <p/> * Default implementation returns <code>null</code> (no persisting). * * @return dimension service key */ @Nullable @NonNls protected String getDimensionServiceKey() { return null; } @Nullable public final String getDimensionKey() { return getDimensionServiceKey(); } public int getExitCode() { return myExitCode; } /** * @return component which should be focused when the dialog appears * on the screen. */ @Nullable public JComponent getPreferredFocusedComponent() { return SystemInfo.isMac ? myPreferredFocusedComponent : null; } /** * @return horizontal stretch of the dialog. It means that the dialog's horizontal size is * the product of horizontal stretch by horizontal size of packed dialog. The default value * is <code>1.0f</code> */ public final float getHorizontalStretch() { return myHorizontalStretch; } /** * @return vertical stretch of the dialog. It means that the dialog's vertical size is * the product of vertical stretch by vertical size of packed dialog. The default value * is <code>1.0f</code> */ public final float getVerticalStretch() { return myVerticalStretch; } protected final void setHorizontalStretch(float hStretch) { myHorizontalStretch = hStretch; } protected final void setVerticalStretch(float vStretch) { myVerticalStretch = vStretch; } /** * @return window owner * @see Window#getOwner */ public Window getOwner() { return myPeer.getOwner(); } public Window getWindow() { return myPeer.getWindow(); } public JComponent getContentPanel() { return (JComponent)myPeer.getContentPane(); } /** * @return root pane * @see JDialog#getRootPane */ public JRootPane getRootPane() { return myPeer.getRootPane(); } /** * @return dialog size * @see Window#getSize */ public Dimension getSize() { return myPeer.getSize(); } /** * @return dialog title * @see Dialog#getTitle */ public String getTitle() { return myPeer.getTitle(); } protected void init() { ensureEventDispatchThread(); myErrorText = new ErrorText(getErrorTextAlignment()); myErrorText.setVisible(false); final ComponentAdapter resizeListener = new ComponentAdapter() { private int myHeight; @Override public void componentResized(ComponentEvent event) { int height = !myErrorText.isVisible() ? 0 : event.getComponent().getHeight(); if (height != myHeight) { myHeight = height; myResizeInProgress = true; myErrorText.setMinimumSize(new Dimension(0, height)); JRootPane root = myPeer.getRootPane(); if (root != null) { root.validate(); } if (myActualSize != null && !shouldAddErrorNearButtons()) { myPeer.setSize(myActualSize.width, myActualSize.height + height); } myErrorText.revalidate(); myResizeInProgress = false; } } }; myErrorText.myLabel.addComponentListener(resizeListener); Disposer.register(myDisposable, new Disposable() { @Override public void dispose() { myErrorText.myLabel.removeComponentListener(resizeListener); } }); final JPanel root = new JPanel(createRootLayout()); //{ // @Override // public void paint(Graphics g) { // if (ApplicationManager.getApplication() != null) { // UISettings.setupAntialiasing(g); // } // super.paint(g); // } //}; myPeer.setContentPane(root); final CustomShortcutSet sc = new CustomShortcutSet(SHOW_OPTION_KEYSTROKE); final AnAction toggleShowOptions = new DumbAwareAction() { @Override public void actionPerformed(@NotNull AnActionEvent e) { expandNextOptionButton(); } }; toggleShowOptions.registerCustomShortcutSet(sc, root, myDisposable); JComponent titlePane = createTitlePane(); if (titlePane != null) { JPanel northSection = new JPanel(new BorderLayout()); root.add(northSection, BorderLayout.NORTH); northSection.add(titlePane, BorderLayout.CENTER); } JComponent centerSection = new JPanel(new BorderLayout()); root.add(centerSection, BorderLayout.CENTER); root.setBorder(createContentPaneBorder()); final JComponent n = createNorthPanel(); if (n != null) { centerSection.add(n, BorderLayout.NORTH); } final JComponent c = createCenterPanel(); if (c != null) { centerSection.add(c, BorderLayout.CENTER); } final JPanel southSection = new JPanel(new BorderLayout()); root.add(southSection, BorderLayout.SOUTH); southSection.add(myErrorText, BorderLayout.CENTER); final JComponent south = createSouthPanel(); if (south != null) { southSection.add(south, BorderLayout.SOUTH); } MnemonicHelper.init(root); if (!postponeValidation()) { startTrackingValidation(); } if (SystemInfo.isWindows) { installEnterHook(root, myDisposable); } myErrorTextAlarm.setActivationComponent(root); } protected int getErrorTextAlignment() { return SwingConstants.LEADING; } @NotNull LayoutManager createRootLayout() { return new BorderLayout(); } private static void installEnterHook(JComponent root, Disposable disposable) { new DumbAwareAction() { @Override public void actionPerformed(AnActionEvent e) { final Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); if (owner instanceof JButton && owner.isEnabled()) { ((JButton)owner).doClick(); } } @Override public void update(AnActionEvent e) { final Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); e.getPresentation().setEnabled(owner instanceof JButton && owner.isEnabled()); } }.registerCustomShortcutSet(CustomShortcutSet.fromString("ENTER"), root, disposable); } private void expandNextOptionButton() { if (myCurrentOptionsButtonIndex > 0) { myOptionsButtons.get(myCurrentOptionsButtonIndex).closePopup(); myCurrentOptionsButtonIndex++; } else if (!myOptionsButtons.isEmpty()) { myCurrentOptionsButtonIndex = 0; } if (myCurrentOptionsButtonIndex >= 0 && myCurrentOptionsButtonIndex < myOptionsButtons.size()) { myOptionsButtons.get(myCurrentOptionsButtonIndex).showPopup(null, true); } } void startTrackingValidation() { SwingUtilities.invokeLater(() -> { if (!myValidationStarted && !myDisposed) { myValidationStarted = true; initValidation(); } }); } protected final void initValidation() { myValidationAlarm.cancelAllRequests(); final Runnable validateRequest = () -> { if (myDisposed) return; List<ValidationInfo> result = doValidateAll(); if (!result.isEmpty()) { installErrorPainter(); } myErrorPainter.setValidationInfo(result); updateErrorInfo(result); if (!myDisposed) { initValidation(); } }; if (getValidationThreadToUse() == Alarm.ThreadToUse.SWING_THREAD) { // null if headless JRootPane rootPane = getRootPane(); myValidationAlarm.addRequest(validateRequest, myValidationDelay, ApplicationManager.getApplication() == null ? null : rootPane == null ? ModalityState.current() : ModalityState.stateForComponent(rootPane)); } else { myValidationAlarm.addRequest(validateRequest, myValidationDelay); } } protected boolean isNorthStrictedToPreferredSize() { return true; } protected boolean isCenterStrictedToPreferredSize() { return false; } protected boolean isSouthStrictedToPreferredSize() { return true; } @NotNull protected JComponent createContentPane() { return new JPanel(); } /** * @see Window#pack */ public void pack() { myPeer.pack(); } public Dimension getPreferredSize() { return myPeer.getPreferredSize(); } /** * Sets horizontal alignment of dialog's buttons. * * @param alignment alignment of the buttons. Acceptable values are * <code>SwingConstants.CENTER</code> and <code>SwingConstants.RIGHT</code>. * The <code>SwingConstants.RIGHT</code> is the default value. * @throws IllegalArgumentException if <code>alignment</code> isn't acceptable */ protected final void setButtonsAlignment(@MagicConstant(intValues = {SwingConstants.CENTER, SwingConstants.RIGHT}) int alignment) { if (SwingConstants.CENTER != alignment && SwingConstants.RIGHT != alignment) { throw new IllegalArgumentException("unknown alignment: " + alignment); } myButtonAlignment = alignment; } /** * Sets margin for command buttons ("OK", "Cancel", "Help"). * * @param insets buttons margin */ public final void setButtonsMargin(@Nullable Insets insets) { myButtonMargins = insets; } public final void setCrossClosesWindow(boolean crossClosesWindow) { myCrossClosesWindow = crossClosesWindow; } protected final void setCancelButtonIcon(Icon icon) { // Setting icons causes buttons be 'square' style instead of // 'rounded', which is expected by apple users. if (!SystemInfo.isMac) { myCancelAction.putValue(Action.SMALL_ICON, icon); } } protected final void setCancelButtonText(String text) { myCancelAction.putValue(Action.NAME, text); } public void setModal(boolean modal) { myPeer.setModal(modal); } public boolean isModal() { return myPeer.isModal(); } public boolean isOKActionEnabled() { return myOKAction.isEnabled(); } public void setOKActionEnabled(boolean isEnabled) { myOKAction.setEnabled(isEnabled); } protected final void setOKButtonIcon(Icon icon) { // Setting icons causes buttons be 'square' style instead of // 'rounded', which is expected by apple users. if (!SystemInfo.isMac) { myOKAction.putValue(Action.SMALL_ICON, icon); } } /** * @param text action without mnemonic. If mnemonic is set, presentation would be shifted by one to the left * {@link AbstractButton#setText(String)} * {@link AbstractButton#updateDisplayedMnemonicIndex(String, int)} */ protected final void setOKButtonText(String text) { myOKAction.putValue(Action.NAME, text); } protected final void setOKButtonMnemonic(int c) { myOKAction.putValue(Action.MNEMONIC_KEY, c); } /** * @return the help identifier or null if no help is available. */ @Nullable @NonNls protected String getHelpId() { return null; } /** * Invoked by default implementation of "Help" action. * Note that the method does nothing if "Help" action isn't enabled. * <p/> * The default implementation shows the help page with id returned * by {@link #getHelpId()}. If that method returns null, * a message box with message "no help available" is shown. */ protected void doHelpAction() { if (myHelpAction.isEnabled()) { String helpId = getHelpId(); if (helpId != null) { HelpManager.getInstance().invokeHelp(helpId); } else { Messages.showMessageDialog(getContentPane(), UIBundle.message("there.is.no.help.for.this.dialog.error.message"), UIBundle.message("no.help.available.dialog.title"), Messages.getInformationIcon()); } } } public boolean isOK() { return getExitCode() == OK_EXIT_CODE; } /** * @return <code>true</code> if and only if visible * @see Component#isVisible */ public boolean isVisible() { return myPeer.isVisible(); } /** * @return <code>true</code> if and only if showing * @see Window#isShowing */ public boolean isShowing() { return myPeer.isShowing(); } /** * @param width width * @param height height * @see JDialog#setSize */ public void setSize(int width, int height) { myPeer.setSize(width, height); } /** * @param title title * @see JDialog#setTitle */ public void setTitle(@Nls(capitalization = Nls.Capitalization.Title) String title) { myPeer.setTitle(title); } /** * @see JDialog#isResizable */ public void isResizable() { myPeer.isResizable(); } /** * @param resizable is resizable * @see JDialog#setResizable */ public void setResizable(boolean resizable) { myPeer.setResizable(resizable); } /** * @return dialog location * @see JDialog#getLocation */ @NotNull public Point getLocation() { return myPeer.getLocation(); } /** * @param p new dialog location * @see JDialog#setLocation(Point) */ public void setLocation(@NotNull Point p) { myPeer.setLocation(p); } /** * @param x x * @param y y * @see JDialog#setLocation(int, int) */ public void setLocation(int x, int y) { myPeer.setLocation(x, y); } public void centerRelativeToParent() { myPeer.centerInParent(); } /** * Show the dialog. * * @throws IllegalStateException if the method is invoked not on the event dispatch thread * @see #showAndGet() * @see #showAndGetOk() */ public void show() { invokeShow(); } /** * Show the modal dialog and check if it was closed with OK. * * @return true if the {@link #getExitCode() exit code} is {@link #OK_EXIT_CODE}. * @throws IllegalStateException if the dialog is non-modal, or if the method is invoked not on the EDT. * @see #show() * @see #showAndGetOk() */ public boolean showAndGet() { if (!isModal()) { throw new IllegalStateException("The showAndGet() method is for modal dialogs only"); } show(); return isOK(); } /** * You need this method ONLY for NON-MODAL dialogs. Otherwise, use {@link #show()} or {@link #showAndGet()}. * * @return result callback which set to "Done" on dialog close, and then its {@code getResult()} will contain {@code isOK()} */ @NotNull public AsyncResult<Boolean> showAndGetOk() { if (isModal()) { throw new IllegalStateException("The showAndGetOk() method is for modeless dialogs only"); } return invokeShow(); } @NotNull private AsyncResult<Boolean> invokeShow() { final AsyncResult<Boolean> result = new AsyncResult<>(); ensureEventDispatchThread(); registerKeyboardShortcuts(); final Disposable uiParent = Disposer.get("ui"); if (uiParent != null) { // may be null if no app yet (license agreement) Disposer.register(uiParent, myDisposable); // ensure everything is disposed on app quit } Disposer.register(myDisposable, new Disposable() { @Override public void dispose() { result.setDone(isOK()); } }); myPeer.show(); return result; } /** * @return Location in absolute coordinates which is used when dialog has no dimension service key or no position was stored yet. * Can return null. In that case dialog will be centered relative to its owner. */ @Nullable public Point getInitialLocation() { return myInitialLocationCallback == null ? null : myInitialLocationCallback.compute(); } public void setInitialLocationCallback(@NotNull Computable<Point> callback) { myInitialLocationCallback = callback; } private void registerKeyboardShortcuts() { final JRootPane rootPane = getRootPane(); if (rootPane == null) return; ActionListener cancelKeyboardAction = createCancelAction(); if (cancelKeyboardAction != null) { rootPane .registerKeyboardAction(cancelKeyboardAction, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); ActionUtil.registerForEveryKeyboardShortcut(getRootPane(), cancelKeyboardAction, CommonShortcuts.getCloseActiveWindow()); } if (ApplicationInfo.contextHelpAvailable() && !isProgressDialog()) { ActionListener helpAction = e -> doHelpAction(); ActionUtil.registerForEveryKeyboardShortcut(getRootPane(), helpAction, CommonShortcuts.getContextHelp()); rootPane.registerKeyboardAction(helpAction, KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); } rootPane.registerKeyboardAction(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { focusPreviousButton(); } }, KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); rootPane.registerKeyboardAction(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { focusNextButton(); } }, KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); if (myYesAction != null) { rootPane.registerKeyboardAction(myYesAction, KeyStroke.getKeyStroke(KeyEvent.VK_Y, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); } if (myNoAction != null) { rootPane.registerKeyboardAction(myNoAction, KeyStroke.getKeyStroke(KeyEvent.VK_N, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); } } /** * * @return null if we should ignore <Esc> for window closing */ @Nullable protected ActionListener createCancelAction() { return new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (!PopupUtil.handleEscKeyEvent()) { doCancelAction(e); } } }; } private void focusPreviousButton() { JButton[] myButtons = new ArrayList<>(myButtonMap.values()).toArray(new JButton[0]); for (int i = 0; i < myButtons.length; i++) { if (myButtons[i].hasFocus()) { if (i == 0) { myButtons[myButtons.length - 1].requestFocus(); return; } myButtons[i - 1].requestFocus(); return; } } } private void focusNextButton() { JButton[] myButtons = new ArrayList<>(myButtonMap.values()).toArray(new JButton[0]); for (int i = 0; i < myButtons.length; i++) { if (myButtons[i].hasFocus()) { if (i == myButtons.length - 1) { myButtons[0].requestFocus(); return; } myButtons[i + 1].requestFocus(); return; } } } public long getTypeAheadTimeoutMs() { return 0L; } public boolean isToDispatchTypeAhead() { return isOK(); } public static boolean isMultipleModalDialogs() { final Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); if (c != null) { final DialogWrapper wrapper = findInstance(c); return wrapper != null && wrapper.getPeer().getCurrentModalEntities().length > 1; } return false; } /** * Base class for dialog wrapper actions that need to ensure that only * one action for the dialog is running. */ protected abstract class DialogWrapperAction extends AbstractAction { /** * The constructor * * @param name the action name (see {@link Action#NAME}) */ protected DialogWrapperAction(@NotNull String name) { putValue(NAME, name); } /** * {@inheritDoc} */ @Override public void actionPerformed(ActionEvent e) { if (myClosed) return; if (myPerformAction) return; try { myPerformAction = true; doAction(e); } finally { myPerformAction = false; } } /** * Do actual work for the action. This method is called only if no other action * is performed in parallel (checked using {@link DialogWrapper#myPerformAction}), * and dialog is active (checked using {@link DialogWrapper#myClosed}) * * @param e action */ protected abstract void doAction(ActionEvent e); } protected class OkAction extends DialogWrapperAction { protected OkAction() { super(CommonBundle.getOkButtonText()); putValue(DEFAULT_ACTION, Boolean.TRUE); } @Override protected void doAction(ActionEvent e) { List<ValidationInfo> infoList = doValidateAll(); if (!infoList.isEmpty()) { ValidationInfo info = infoList.get(0); if (info.component != null && info.component.isVisible()) { IdeFocusManager.getInstance(null).requestFocus(info.component, true); } if (!Registry.is("ide.inplace.errors.balloon")) { DialogEarthquakeShaker.shake(getPeer().getWindow()); } startTrackingValidation(); return; } doOKAction(); } } protected class CancelAction extends DialogWrapperAction { private CancelAction() { super(CommonBundle.getCancelButtonText()); } @Override protected void doAction(ActionEvent e) { doCancelAction(); } } /** * The action that just closes dialog with the specified exit code * (like the default behavior of the actions "Ok" and "Cancel"). */ protected class DialogWrapperExitAction extends DialogWrapperAction { /** * The exit code for the action */ protected final int myExitCode; /** * The constructor * * @param name the action name * @param exitCode the exit code for dialog */ public DialogWrapperExitAction(String name, int exitCode) { super(name); myExitCode = exitCode; } @Override protected void doAction(ActionEvent e) { if (isEnabled()) { close(myExitCode); } } } private class HelpAction extends AbstractAction { private HelpAction() { putValue(NAME, CommonBundle.getHelpButtonText()); } @Override public void actionPerformed(ActionEvent e) { doHelpAction(); } } /** * Don't override this method. It is not final for the API compatibility. * It will not be called by the DialogWrapper's validator. * Use this method only in circumstances when the exact invalid component is hard to * detect or the valid status is based on several fields. In other cases use * <code>{@link #setErrorText(String, JComponent)}</code> method. * @param text the error text to display */ protected void setErrorText(@Nullable final String text) { setErrorText(text, null); } protected void setErrorText(@Nullable final String text, @Nullable final JComponent component) { setErrorInfoAll((text == null) ? Collections.EMPTY_LIST : Collections.singletonList(new ValidationInfo(text, component))); } protected void setErrorInfoAll(@NotNull List<ValidationInfo> info) { if (myInfo.equals(info)) return; myErrorTextAlarm.cancelAllRequests(); SwingUtilities.invokeLater(() -> myErrorText.clearError()); List<ValidationInfo> corrected = myInfo.stream().filter((vi) -> !info.contains(vi)).collect(Collectors.toList()); if (Registry.is("ide.inplace.errors.outline")) { corrected.stream().filter(vi -> (vi.component != null && vi.component.getBorder() instanceof ErrorBorderCapable)). forEach(vi -> vi.component.putClientProperty("JComponent.error.outline", false)); } if (Registry.is("ide.inplace.errors.balloon")) { corrected.stream().filter(vi -> vi.component != null).forEach(vi -> { vi.component.putClientProperty("JComponent.error.balloon.builder", null); Balloon balloon = (Balloon)vi.component.getClientProperty("JComponent.error.balloon"); if (balloon != null && !balloon.isDisposed()) { balloon.hide(); } Component fc = getFocusable(vi.component); if (fc != null) { for (FocusListener fl : fc.getFocusListeners()) { if (fl instanceof ErrorFocusListener) { fc.removeFocusListener(fl); } } } }); } myInfo = info; if (Registry.is("ide.inplace.errors.outline")) { myInfo.stream().filter(vi -> (vi.component != null && vi.component.getBorder() instanceof ErrorBorderCapable)). forEach(vi -> vi.component.putClientProperty("JComponent.error.outline", true)); } if (Registry.is("ide.inplace.errors.balloon") && !myInfo.isEmpty()) { for (ValidationInfo vi : myInfo) { Component fc = getFocusable(vi.component); if (fc != null && fc.isFocusable()) { if (vi.component.getClientProperty("JComponent.error.balloon.builder") == null) { JLabel label = new JLabel(); label.setHorizontalAlignment(SwingConstants.LEADING); setErrorTipText(vi.component, label, vi.message); BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createBalloonBuilder(label) .setDisposable(getDisposable()) .setBorderInsets(UIManager.getInsets("Balloon.error.textInsets")) .setPointerSize(new JBDimension(17, 6)) .setCornerToPointerDistance(JBUI.scale(30)) .setHideOnKeyOutside(false) .setHideOnClickOutside(false) .setHideOnAction(false) .setBorderColor(BALLOON_BORDER) .setFillColor(BALLOON_BACKGROUND) .setHideOnFrameResize(false) .setRequestFocus(false) .setAnimationCycle(100) .setShadow(true); vi.component.putClientProperty("JComponent.error.balloon.builder", balloonBuilder); ErrorFocusListener fl = new ErrorFocusListener(label, vi.message, vi.component); if (fc.hasFocus()) { fl.showErrorTip(); } fc.addFocusListener(fl); Disposer.register(getDisposable(), () -> fc.removeFocusListener(fl)); } } else { SwingUtilities.invokeLater(() -> myErrorText.appendError(vi.message)); } } } else if (!myInfo.isEmpty()) { myErrorTextAlarm.addRequest(() -> { for (ValidationInfo vi : myInfo) { myErrorText.appendError(vi.message); } }, 300, null); } } private void setErrorTipText(JComponent component, JLabel label, String text) { Insets insets = UIManager.getInsets("Balloon.error.textInsets"); int oneLineWidth = SwingUtilities2.stringWidth(label, label.getFontMetrics(label.getFont()), text); int textWidth = getRootPane().getWidth() - component.getX() - insets.left - insets.right - JBUI.scale(30); if (textWidth < JBUI.scale(90)) textWidth = JBUI.scale(90); if (textWidth > oneLineWidth) textWidth = oneLineWidth; String htmlText = String.format("<html><div width=%d>%s</div></html>", textWidth, text); label.setText(htmlText); } private Component getFocusable(Component source) { if (source == null) { return null; } else if (source instanceof JScrollPane) { return ((JScrollPane)source).getViewport().getView(); } else if (source instanceof JComboBox && ((JComboBox)source).isEditable()) { return ((JComboBox)source).getEditor().getEditorComponent(); } else if (source instanceof JSpinner) { Container c = ((JSpinner)source).getEditor(); synchronized (c.getTreeLock()) { return c.getComponent(0); } } else if (source instanceof Container) { Container container = (Container)source; List<Component> cl; synchronized (container.getTreeLock()) { cl = Arrays.asList(container.getComponents()); } return cl.stream().filter(c -> c.isFocusable()).count() > 1 ? null : source; } else { return source; } } private void updateSize() { if (myActualSize == null && !myErrorText.isVisible()) { myActualSize = getSize(); } } @Nullable public static DialogWrapper findInstance(Component c) { while (c != null) { if (c instanceof DialogWrapperDialog) { return ((DialogWrapperDialog)c).getDialogWrapper(); } c = c.getParent(); } return null; } @Nullable public static DialogWrapper findInstanceFromFocus() { return findInstance(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); } private void resizeWithAnimation(@NotNull final Dimension size) { //todo[kb]: fix this PITA myResizeInProgress = true; if (!Registry.is("enable.animation.on.dialogs")) { setSize(size.width, size.height); myResizeInProgress = false; return; } new Thread("DialogWrapper resizer") { int time = 200; int steps = 7; @Override public void run() { int step = 0; final Dimension cur = getSize(); int h = (size.height - cur.height) / steps; int w = (size.width - cur.width) / steps; while (step++ < steps) { setSize(cur.width + w * step, cur.height + h * step); TimeoutUtil.sleep(time / steps); } setSize(size.width, size.height); //repaint(); if (myErrorText.shouldBeVisible()) { myErrorText.setVisible(true); } myResizeInProgress = false; } }.start(); } private class ErrorText extends JPanel { private final JLabel myLabel = new JLabel(); private List<String> errors = new ArrayList<>(); private ErrorText(int horizontalAlignment) { setLayout(new BorderLayout()); myLabel.setBorder(JBUI.Borders.empty(16, 13, 16, 13)); myLabel.setHorizontalAlignment(horizontalAlignment); JBScrollPane pane = new JBScrollPane(myLabel, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); pane.setBorder(JBUI.Borders.empty()); pane.setBackground(null); pane.getViewport().setBackground(null); pane.setOpaque(false); add(pane, BorderLayout.CENTER); } private void clearError() { errors.clear(); myLabel.setBounds(0, 0, 0, 0); myLabel.setText(""); setVisible(false); updateSize(); } private void appendError(String text) { errors.add(text); myLabel.setBounds(0, 0, 0, 0); StringBuilder sb = new StringBuilder("<html><font color='#" + ColorUtil.toHex(JBColor.RED) + "'>"); errors.forEach(error -> sb.append("<left>").append(error).append("</left><br/>")); sb.append("</font></html>"); myLabel.setText(sb.toString()); setVisible(true); updateSize(); } private boolean shouldBeVisible() { return !errors.isEmpty(); } private boolean isTextSet(@NotNull List<ValidationInfo> info) { if (info.isEmpty()) { return errors.isEmpty(); } else if (errors.size() == info.size()){ return errors.equals(info.stream().map(i -> i.message).collect(Collectors.toList())); } else { return false; } } } @NotNull public final DialogWrapperPeer getPeer() { return myPeer; } /** * Ensure that dialog is used from even dispatch thread. * * @throws IllegalStateException if the dialog is invoked not on the event dispatch thread */ private static void ensureEventDispatchThread() { if (!EventQueue.isDispatchThread()) { throw new IllegalStateException("The DialogWrapper can only be used in event dispatch thread. Current thread: "+Thread.currentThread()); } } @NotNull public final Disposable getDisposable() { return myDisposable; } /** * @see Adapter */ public interface DoNotAskOption { abstract class Adapter implements DoNotAskOption { /** * Save the state of the checkbox in the settings, or perform some other related action. * This method is called right after the dialog is {@link #close(int) closed}. * <br/> * Note that this method won't be called in the case when the dialog is closed by {@link #CANCEL_EXIT_CODE Cancel} * if {@link #shouldSaveOptionsOnCancel() saving the choice on cancel is disabled} (which is by default). * * @param isSelected true if user selected "don't show again". * @param exitCode the {@link #getExitCode() exit code} of the dialog. * @see #shouldSaveOptionsOnCancel() */ public abstract void rememberChoice(boolean isSelected, int exitCode); /** * Tells whether the checkbox should be selected by default or not. * * @return true if the checkbox should be selected by default. */ public boolean isSelectedByDefault() { return false; } @Override public boolean shouldSaveOptionsOnCancel() { return false; } @NotNull @Override public String getDoNotShowMessage() { return CommonBundle.message("dialog.options.do.not.ask"); } @Override public final boolean isToBeShown() { return !isSelectedByDefault(); } @Override public final void setToBeShown(boolean toBeShown, int exitCode) { rememberChoice(!toBeShown, exitCode); } @Override public final boolean canBeHidden() { return true; } } /** * @return default selection state of checkbox (false -> checkbox selected) */ boolean isToBeShown(); /** * @param toBeShown - if dialog should be shown next time (checkbox selected -> false) * @param exitCode of corresponding DialogWrapper */ void setToBeShown(boolean toBeShown, int exitCode); /** * @return true if checkbox should be shown */ boolean canBeHidden(); boolean shouldSaveOptionsOnCancel(); @NotNull String getDoNotShowMessage(); } @NotNull private ErrorPaintingType getErrorPaintingType() { return ErrorPaintingType.SIGN; } private class ErrorPainter extends AbstractPainter { private List<ValidationInfo> info; @Override public void executePaint(Component component, Graphics2D g) { for (ValidationInfo i : info) { if (i.component != null && !(Registry.is("ide.inplace.errors.outline"))) { int w = i.component.getWidth(); int h = i.component.getHeight(); Point p; switch (getErrorPaintingType()) { case DOT: p = SwingUtilities.convertPoint(i.component, 2, h / 2, component); AllIcons.Ide.ErrorPoint.paintIcon(component, g, p.x, p.y); break; case SIGN: p = SwingUtilities.convertPoint(i.component, w, 0, component); AllIcons.General.Error.paintIcon(component, g, p.x - 8, p.y - 8); break; case LINE: p = SwingUtilities.convertPoint(i.component, 0, h, component); Graphics g2 = g.create(); try { //noinspection UseJBColor g2.setColor(new Color(255, 0, 0, 100)); g2.fillRoundRect(p.x, p.y - 2, w, 4, 2, 2); } finally { g2.dispose(); } break; } } } } @Override public boolean needsRepaint() { return true; } private void setValidationInfo(@NotNull List<ValidationInfo> info) { this.info = info; } } private static class ErrorTipTracker extends PositionTracker<Balloon> { private final int y; private ErrorTipTracker(JComponent component, int y) { super(component); this.y = y; } @Override public RelativePoint recalculateLocation(Balloon balloon) { int width = getComponent().getWidth(); int delta = width < JBUI.scale(120) ? width / 2 : JBUI.scale(60); return new RelativePoint(getComponent(), new Point(delta, y)); } } private class ErrorFocusListener implements FocusListener { private final JLabel label; private final String text; private final JComponent component; private ErrorFocusListener(JLabel label, String text, JComponent component) { this.label = label; this.text = text; this.component = component; } @Override public void focusGained(FocusEvent e) { Balloon b = (Balloon)component.getClientProperty("JComponent.error.balloon"); if (b == null || b.isDisposed()) { showErrorTip(); } } @Override public void focusLost(FocusEvent e) { Balloon b = (Balloon)component.getClientProperty("JComponent.error.balloon"); if (b != null && !b.isDisposed()) { b.hide(); } } private void showErrorTip() { BalloonBuilder balloonBuilder = (BalloonBuilder)component.getClientProperty("JComponent.error.balloon.builder"); if (balloonBuilder == null) return; Balloon balloon = balloonBuilder.createBalloon(); ComponentListener rl = new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (!balloon.isDisposed()) { setErrorTipText(component, label, text); balloon.revalidate(); } } }; balloon.addListener(new JBPopupListener.Adapter() { @Override public void onClosed(LightweightWindowEvent event) { JRootPane rootPane = getRootPane(); if (rootPane != null) { rootPane.removeComponentListener(rl); } if (component.getClientProperty("JComponent.error.balloon") == event.asBalloon()) { component.putClientProperty("JComponent.error.balloon", null); } } }); getRootPane().addComponentListener(rl); Point componentPos = SwingUtilities.convertPoint(component, 0, 0, getRootPane().getLayeredPane()); Dimension bSize = balloon.getPreferredSize(); Insets cInsets = component.getInsets(); int top = cInsets != null ? cInsets.top : 0; if (componentPos.y >= bSize.height + top) { balloon.show(new ErrorTipTracker(component, 0), Balloon.Position.above); } else { balloon.show(new ErrorTipTracker(component, component.getHeight()), Balloon.Position.below); } component.putClientProperty("JComponent.error.balloon", balloon); } } private enum ErrorPaintingType {DOT, SIGN, LINE} public enum DialogStyle {NO_STYLE, COMPACT} }