diff --git a/platform/lang-impl/src/com/intellij/refactoring/rename/inplace/InplaceRefactoring.java b/platform/lang-impl/src/com/intellij/refactoring/rename/inplace/InplaceRefactoring.java index 131dc41254db..22c9b6ae3e83 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/rename/inplace/InplaceRefactoring.java +++ b/platform/lang-impl/src/com/intellij/refactoring/rename/inplace/InplaceRefactoring.java @@ -36,10 +36,7 @@ import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.command.impl.FinishMarkAction; import com.intellij.openapi.command.impl.StartMarkAction; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.RangeMarker; -import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.impl.EditorImpl; @@ -699,10 +696,15 @@ public abstract class InplaceRefactoring { } }); myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + final JBPopupFactory popupFactory = JBPopupFactory.getInstance(); myBalloon.show(new PositionTracker(myEditor.getContentComponent()) { @Override public RelativePoint recalculateLocation(Balloon object) { - final RelativePoint target = JBPopupFactory.getInstance().guessBestPopupLocation(myEditor); + if (myTarget != null && !popupFactory.isBestPopupLocationVisible(myEditor)) { + return myTarget; + } + final RelativePoint target = popupFactory.guessBestPopupLocation(myEditor); + if (target == null) return myTarget; final Point screenPoint = target.getScreenPoint(); int y = screenPoint.y; if (target.getPoint().getY() > myEditor.getLineHeight() + myBalloon.getPreferredSize().getHeight()) { diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java index 85321761dcb7..9da62fb6f49a 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java @@ -247,6 +247,13 @@ public abstract class JBPopupFactory { */ public abstract RelativePoint guessBestPopupLocation(Editor editor); + /** + * @param editor the editor over which the popup is shown. + * @return true if popup location is located in visible area + * false if center would be suggested instead + */ + public abstract boolean isBestPopupLocationVisible(Editor editor); + public abstract Point getCenterOf(JComponent container, JComponent content); @Nullable diff --git a/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java b/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java index 5a26e30ccbeb..f6f0a34e7435 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java @@ -1,949 +1,960 @@ -/* - * Copyright 2000-2012 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.ui.popup; - -import com.intellij.CommonBundle; -import com.intellij.icons.AllIcons; -import com.intellij.ide.DataManager; -import com.intellij.ide.IdeEventQueue; -import com.intellij.ide.IdeTooltipManager; -import com.intellij.openapi.actionSystem.*; -import com.intellij.openapi.actionSystem.ex.ActionUtil; -import com.intellij.openapi.actionSystem.impl.ActionMenu; -import com.intellij.openapi.actionSystem.impl.Utils; -import com.intellij.openapi.application.ex.ApplicationEx; -import com.intellij.openapi.application.ex.ApplicationManagerEx; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.CaretModel; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.VisualPosition; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.MessageType; -import com.intellij.openapi.ui.popup.*; -import com.intellij.openapi.ui.popup.util.BaseListPopupStep; -import com.intellij.openapi.util.Condition; -import com.intellij.openapi.util.EmptyRunnable; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.wm.WindowManager; -import com.intellij.openapi.wm.ex.WindowManagerEx; -import com.intellij.openapi.wm.impl.IdeFrameImpl; -import com.intellij.ui.ColorUtil; -import com.intellij.ui.FocusTrackback; -import com.intellij.ui.HintHint; -import com.intellij.ui.awt.RelativePoint; -import com.intellij.ui.components.panels.NonOpaquePanel; -import com.intellij.ui.popup.list.ListPopupImpl; -import com.intellij.ui.popup.mock.MockConfirmation; -import com.intellij.ui.popup.tree.TreePopupImpl; -import com.intellij.util.ui.EmptyIcon; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.event.HyperlinkListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import java.awt.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -public class PopupFactoryImpl extends JBPopupFactory { - - /** - * Allows to get an editor position for which a popup with auxiliary information might be shown. - *

- * Primary intention for this key is to hint popup position for the non-caret location. - */ - public static final Key ANCHOR_POPUP_POSITION = Key.create("popup.anchor.position"); - - private static final Logger LOG = Logger.getInstance("#com.intellij.ui.popup.PopupFactoryImpl"); - - private static final Icon QUICK_LIST_ICON = AllIcons.Actions.QuickList; - - public ListPopup createConfirmation(String title, final Runnable onYes, int defaultOptionIndex) { - return createConfirmation(title, CommonBundle.getYesButtonText(), CommonBundle.getNoButtonText(), onYes, defaultOptionIndex); - } - - public ListPopup createConfirmation(String title, final String yesText, String noText, final Runnable onYes, int defaultOptionIndex) { - return createConfirmation(title, yesText, noText, onYes, EmptyRunnable.getInstance(), defaultOptionIndex); - } - - public JBPopup createMessage(String text) { - return createListPopup(new BaseListPopupStep(null, new String[]{text})); - } - - @Override - public Balloon getParentBalloonFor(@Nullable Component c) { - if (c == null) return null; - Component eachParent = c; - while (eachParent != null) { - if (eachParent instanceof JComponent) { - Object balloon = ((JComponent)eachParent).getClientProperty(Balloon.KEY); - if (balloon instanceof Balloon) { - return (Balloon)balloon; - } - } - eachParent = eachParent.getParent(); - } - - return null; - } - - public ListPopup createConfirmation(String title, - final String yesText, - String noText, - final Runnable onYes, - final Runnable onNo, - int defaultOptionIndex) - { - - final BaseListPopupStep step = new BaseListPopupStep(title, new String[]{yesText, noText}) { - public PopupStep onChosen(String selectedValue, final boolean finalChoice) { - if (selectedValue.equals(yesText)) { - onYes.run(); - } - else { - onNo.run(); - } - return FINAL_CHOICE; - } - - public void canceled() { - onNo.run(); - } - - public boolean isMnemonicsNavigationEnabled() { - return true; - } - }; - step.setDefaultOptionIndex(defaultOptionIndex); - - final ApplicationEx app = ApplicationManagerEx.getApplicationEx(); - return app == null || !app.isUnitTestMode() ? new ListPopupImpl(step) : new MockConfirmation(step, yesText); - } - - - private static ListPopup createActionGroupPopup(final String title, - final ActionGroup actionGroup, - @NotNull DataContext dataContext, - boolean showNumbers, - boolean useAlphaAsNumbers, - boolean showDisabledActions, - boolean honorActionMnemonics, - final Runnable disposeCallback, - final int maxRowCount) { - return createActionGroupPopup(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, disposeCallback, - maxRowCount, null, null); - } - - public ListPopup createActionGroupPopup(final String title, - final ActionGroup actionGroup, - @NotNull DataContext dataContext, - boolean showNumbers, - boolean showDisabledActions, - boolean honorActionMnemonics, - final Runnable disposeCallback, - final int maxRowCount) { - return createActionGroupPopup(title, actionGroup, dataContext, showNumbers, showDisabledActions, honorActionMnemonics, disposeCallback, - maxRowCount, null); - } - - private static ListPopup createActionGroupPopup(final String title, - final ActionGroup actionGroup, - @NotNull DataContext dataContext, - boolean showNumbers, - boolean useAlphaAsNumbers, - boolean showDisabledActions, - boolean honorActionMnemonics, - final Runnable disposeCallback, - final int maxRowCount, - final Condition preselectActionCondition, @Nullable final String actionPlace) { - return new ActionGroupPopup(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, - disposeCallback, maxRowCount, preselectActionCondition, actionPlace); - } - - public static class ActionGroupPopup extends ListPopupImpl { - - private final Runnable myDisposeCallback; - private final Component myComponent; - - public ActionGroupPopup(final String title, final ActionGroup actionGroup, @NotNull DataContext dataContext, - boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, boolean honorActionMnemonics, - final Runnable disposeCallback, final int maxRowCount, final Condition preselectActionCondition, - @Nullable final String actionPlace) { - super(createStep(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, - preselectActionCondition, actionPlace), - maxRowCount); - myDisposeCallback = disposeCallback; - myComponent = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext); - - addListSelectionListener(new ListSelectionListener() { - public void valueChanged(ListSelectionEvent e) { - final JList list = (JList)e.getSource(); - final ActionItem actionItem = (ActionItem)list.getSelectedValue(); - if (actionItem == null) return; - AnAction action = actionItem.getAction(); - Presentation presentation = new Presentation(); - presentation.setDescription(action.getTemplatePresentation().getDescription()); - final String actualActionPlace = actionPlace == null ? ActionPlaces.UNKNOWN : actionPlace; - action.update(new AnActionEvent(null, DataManager.getInstance().getDataContext(myComponent), actualActionPlace, presentation, - ActionManager.getInstance(), 0)); - ActionMenu.showDescriptionInStatusBar(true, myComponent, presentation.getDescription()); - } - }); - } - - private static ListPopupStep createStep(String title, ActionGroup actionGroup, @NotNull DataContext dataContext, - boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, - boolean honorActionMnemonics, - Condition preselectActionCondition, @Nullable String actionPlace) { - final Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext); - - final ActionStepBuilder builder = - new ActionStepBuilder(dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics); - if (actionPlace != null) { - builder.setActionPlace(actionPlace); - } - builder.buildGroup(actionGroup); - final List items = builder.getItems(); - - return new ActionPopupStep(items, title, component, showNumbers || honorActionMnemonics && itemsHaveMnemonics(items), - preselectActionCondition, false, showDisabledActions); - } - - @Override - public void dispose() { - if (myDisposeCallback != null) { - myDisposeCallback.run(); - } - ActionMenu.showDescriptionInStatusBar(true, myComponent, null); - super.dispose(); - } - } - - public ListPopup createActionGroupPopup(final String title, - final ActionGroup actionGroup, - @NotNull DataContext dataContext, - boolean showNumbers, - boolean showDisabledActions, - boolean honorActionMnemonics, - final Runnable disposeCallback, - final int maxRowCount, - final Condition preselectActionCondition) { - return createActionGroupPopup(title, actionGroup, dataContext, showNumbers, true, showDisabledActions, honorActionMnemonics, - disposeCallback, maxRowCount, preselectActionCondition, null); - } - - public ListPopup createActionGroupPopup(String title, - ActionGroup actionGroup, - @NotNull DataContext dataContext, - ActionSelectionAid selectionAidMethod, - boolean showDisabledActions) { - return createActionGroupPopup(title, actionGroup, dataContext, - selectionAidMethod == ActionSelectionAid.NUMBERING || selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, - selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, - showDisabledActions, - selectionAidMethod == ActionSelectionAid.MNEMONICS, - null, -1); - } - - public ListPopup createActionGroupPopup(String title, - ActionGroup actionGroup, - @NotNull DataContext dataContext, - ActionSelectionAid selectionAidMethod, - boolean showDisabledActions, - @Nullable String actionPlace) { - return createActionGroupPopup(title, actionGroup, dataContext, - selectionAidMethod == ActionSelectionAid.NUMBERING || selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, - selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, - showDisabledActions, - selectionAidMethod == ActionSelectionAid.MNEMONICS, - null, -1, null, actionPlace); - } - - public ListPopup createActionGroupPopup(String title, - ActionGroup actionGroup, - @NotNull DataContext dataContext, - ActionSelectionAid selectionAidMethod, - boolean showDisabledActions, - Runnable disposeCallback, - int maxRowCount) { - return createActionGroupPopup(title, actionGroup, dataContext, - selectionAidMethod == ActionSelectionAid.NUMBERING || selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, - selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, - showDisabledActions, - selectionAidMethod == ActionSelectionAid.MNEMONICS, - disposeCallback, - maxRowCount); - } - - public ListPopupStep createActionsStep(final ActionGroup actionGroup, - @NotNull DataContext dataContext, - final boolean showNumbers, - final boolean showDisabledActions, - final String title, - final Component component, - final boolean honorActionMnemonics) { - return createActionsStep(actionGroup, dataContext, showNumbers, showDisabledActions, title, component, honorActionMnemonics, 0, false); - } - - private static ListPopupStep createActionsStep(ActionGroup actionGroup, @NotNull DataContext dataContext, - boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, - String title, Component component, boolean honorActionMnemonics, - final int defaultOptionIndex, final boolean autoSelectionEnabled) { - final List items = makeActionItemsFromActionGroup(actionGroup, dataContext, showNumbers, useAlphaAsNumbers, - showDisabledActions, honorActionMnemonics); - return new ActionPopupStep(items, title, component, showNumbers || honorActionMnemonics && itemsHaveMnemonics(items), - new Condition() { - @Override - public boolean value(AnAction action) { - return defaultOptionIndex >= 0 && - defaultOptionIndex < items.size() && - items.get(defaultOptionIndex).getAction().equals(action); - } - }, autoSelectionEnabled, showDisabledActions); - } - - private static List makeActionItemsFromActionGroup(ActionGroup actionGroup, - DataContext dataContext, - boolean showNumbers, - boolean useAlphaAsNumbers, - boolean showDisabledActions, - boolean honorActionMnemonics) { - final ActionStepBuilder builder = new ActionStepBuilder(dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, - honorActionMnemonics); - builder.buildGroup(actionGroup); - return builder.getItems(); - } - - private static ListPopupStep createActionsStep(ActionGroup actionGroup, @NotNull DataContext dataContext, - boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, - String title, Component component, boolean honorActionMnemonics, - Condition preselectActionCondition, boolean autoSelectionEnabled) { - final List items = makeActionItemsFromActionGroup(actionGroup, dataContext, showNumbers, useAlphaAsNumbers, - showDisabledActions, honorActionMnemonics); - return new ActionPopupStep(items, title, component, showNumbers || honorActionMnemonics && itemsHaveMnemonics(items), preselectActionCondition, - autoSelectionEnabled, showDisabledActions); - } - - public ListPopupStep createActionsStep(ActionGroup actionGroup, @NotNull DataContext dataContext, boolean showNumbers, boolean showDisabledActions, - String title, Component component, boolean honorActionMnemonics, int defaultOptionIndex, - final boolean autoSelectionEnabled) { - return createActionsStep(actionGroup, dataContext, showNumbers, true, showDisabledActions, title, component, honorActionMnemonics, - defaultOptionIndex, autoSelectionEnabled); - } - - private static boolean itemsHaveMnemonics(final List items) { - for (ActionItem item : items) { - if (item.getAction().getTemplatePresentation().getMnemonic() != 0) return true; - } - - return false; - } - - public ListPopup createWizardStep(PopupStep step) { - return new ListPopupImpl((ListPopupStep) step); - } - - public ListPopup createListPopup(ListPopupStep step) { - return new ListPopupImpl(step); - } - - public TreePopup createTree(JBPopup parent, TreePopupStep aStep, Object parentValue) { - return new TreePopupImpl(parent, aStep, parentValue); - } - - public TreePopup createTree(TreePopupStep aStep) { - return new TreePopupImpl(aStep); - } - - public ComponentPopupBuilder createComponentPopupBuilder(JComponent content, JComponent prefferableFocusComponent) { - return new ComponentPopupBuilderImpl(content, prefferableFocusComponent); - } - - - public RelativePoint guessBestPopupLocation(@NotNull DataContext dataContext) { - KeyboardFocusManager focusManager=KeyboardFocusManager.getCurrentKeyboardFocusManager(); - Component component = focusManager.getFocusOwner(); - JComponent focusOwner=component instanceof JComponent ? (JComponent)component : null; - - if (focusOwner == null) { - Project project = PlatformDataKeys.PROJECT.getData(dataContext); - IdeFrameImpl frame = project == null ? null : ((WindowManagerEx)WindowManager.getInstance()).getFrame(project); - focusOwner = frame == null ? null : frame.getRootPane(); - if (focusOwner == null) { - throw new IllegalArgumentException("focusOwner cannot be null"); - } - } - - final Point point = PlatformDataKeys.CONTEXT_MENU_POINT.getData(dataContext); - if (point != null) { - return new RelativePoint(focusOwner, point); - } - - Editor editor = PlatformDataKeys.EDITOR.getData(dataContext); - if (editor != null && focusOwner == editor.getContentComponent()) { - return guessBestPopupLocation(editor); - } - else { - return guessBestPopupLocation(focusOwner); - } - } - - public RelativePoint guessBestPopupLocation(final JComponent component) { - Point popupMenuPoint = null; - final Rectangle visibleRect = component.getVisibleRect(); - if (component instanceof JList) { // JList - JList list = (JList)component; - int firstVisibleIndex = list.getFirstVisibleIndex(); - int lastVisibleIndex = list.getLastVisibleIndex(); - int[] selectedIndices = list.getSelectedIndices(); - for (int index : selectedIndices) { - if (firstVisibleIndex <= index && index <= lastVisibleIndex) { - Rectangle cellBounds = list.getCellBounds(index, index); - popupMenuPoint = new Point(visibleRect.x + visibleRect.width / 4, cellBounds.y + cellBounds.height); - break; - } - } - } - else if (component instanceof JTree) { // JTree - JTree tree = (JTree)component; - int[] selectionRows = tree.getSelectionRows(); - if (selectionRows != null) { - Arrays.sort(selectionRows); - for (int i = 0; i < selectionRows.length; i++) { - int row = selectionRows[i]; - Rectangle rowBounds = tree.getRowBounds(row); - if (visibleRect.contains(rowBounds)) { - popupMenuPoint = new Point(rowBounds.x + 2, rowBounds.y + rowBounds.height - 1); - break; - } - } - if (popupMenuPoint == null) {//All selected rows are out of visible rect - Point visibleCenter = new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2); - double minDistance = Double.POSITIVE_INFINITY; - int bestRow = -1; - Point rowCenter; - double distance; - for (int i = 0; i < selectionRows.length; i++) { - int row = selectionRows[i]; - Rectangle rowBounds = tree.getRowBounds(row); - rowCenter = new Point(rowBounds.x + rowBounds.width / 2, rowBounds.y + rowBounds.height / 2); - distance = visibleCenter.distance(rowCenter); - if (minDistance > distance) { - minDistance = distance; - bestRow = row; - } - } - - if (bestRow != -1) { - Rectangle rowBounds = tree.getRowBounds(bestRow); - tree.scrollRectToVisible(new Rectangle(rowBounds.x, rowBounds.y, Math.min(visibleRect.width, rowBounds.width), rowBounds.height)); - popupMenuPoint = new Point(rowBounds.x + 2, rowBounds.y + rowBounds.height - 1); - } - } - } - } else if (component instanceof PopupOwner){ - popupMenuPoint = ((PopupOwner)component).getBestPopupPosition(); - } - // TODO[vova] add usability for JTable - if (popupMenuPoint == null) { - popupMenuPoint = new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2); - } - - return new RelativePoint(component, popupMenuPoint); - } - - public RelativePoint guessBestPopupLocation(Editor editor) { - VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION); - - if (visualPosition == null) { - CaretModel caretModel = editor.getCaretModel(); - if (caretModel.isUpToDate()) { - visualPosition = caretModel.getVisualPosition(); - } - else { - visualPosition = editor.offsetToVisualPosition(caretModel.getOffset()); - } - } - - Point p = editor.visualPositionToXY(new VisualPosition(visualPosition.line + 1, visualPosition.column)); - - final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea(); - if (!visibleArea.contains(p)) { - p = new Point((visibleArea.x + visibleArea.width) / 2, (visibleArea.y + visibleArea.height) / 2); - } - - return new RelativePoint(editor.getContentComponent(), p); - } - - public Point getCenterOf(JComponent container, JComponent content) { - return AbstractPopup.getCenterOf(container, content); - } - - public static class ActionItem { - private final AnAction myAction; - private final String myText; - private final boolean myIsEnabled; - private final Icon myIcon; - private final boolean myPrependWithSeparator; - private final String mySeparatorText; - - private ActionItem(@NotNull AnAction action, @NotNull String text, boolean enabled, Icon icon, final boolean prependWithSeparator, String separatorText) { - myAction = action; - myText = text; - myIsEnabled = enabled; - myIcon = icon; - myPrependWithSeparator = prependWithSeparator; - mySeparatorText = separatorText; - } - - @NotNull - public AnAction getAction() { - return myAction; - } - - @NotNull - public String getText() { - return myText; - } - - public Icon getIcon() { - return myIcon; - } - - public boolean isPrependWithSeparator() { - return myPrependWithSeparator; - } - - public String getSeparatorText() { - return mySeparatorText; - } - - public boolean isEnabled() { return myIsEnabled; } - } - - private static class ActionPopupStep implements ListPopupStep, MnemonicNavigationFilter, SpeedSearchFilter { - private final List myItems; - private final String myTitle; - private final Component myContext; - private final boolean myEnableMnemonics; - private final int myDefaultOptionIndex; - private final boolean myAutoSelectionEnabled; - private final boolean myShowDisabledActions; - private Runnable myFinalRunnable; - @Nullable private final Condition myPreselectActionCondition; - - private ActionPopupStep(@NotNull final List items, final String title, Component context, boolean enableMnemonics, - @Nullable Condition preselectActionCondition, final boolean autoSelection, boolean showDisabledActions) { - myItems = items; - myTitle = title; - myContext = context; - myEnableMnemonics = enableMnemonics; - myDefaultOptionIndex = getDefaultOptionIndexFromSelectCondition(preselectActionCondition, items); - myPreselectActionCondition = preselectActionCondition; - myAutoSelectionEnabled = autoSelection; - myShowDisabledActions = showDisabledActions; - } - - private static int getDefaultOptionIndexFromSelectCondition(@Nullable Condition preselectActionCondition, - @NotNull List items) { - int defaultOptionIndex = 0; - if (preselectActionCondition != null) { - for (int i = 0; i < items.size(); i++) { - final AnAction action = items.get(i).getAction(); - if (preselectActionCondition.value(action)) { - defaultOptionIndex = i; - break; - } - } - } - return defaultOptionIndex; - } - - @NotNull - public List getValues() { - return myItems; - } - - public boolean isSelectable(final ActionItem value) { - return value.isEnabled(); - } - - public int getMnemonicPos(final ActionItem value) { - final String text = getTextFor(value); - int i = text.indexOf(UIUtil.MNEMONIC); - if (i < 0) { - i = text.indexOf('&'); - } - if (i < 0) { - i = text.indexOf('_'); - } - return i; - } - - public Icon getIconFor(final ActionItem aValue) { - return aValue.getIcon(); - } - - @NotNull - public String getTextFor(final ActionItem value) { - return value.getText(); - } - - public ListSeparator getSeparatorAbove(final ActionItem value) { - return value.isPrependWithSeparator() ? new ListSeparator(value.getSeparatorText()) : null; - } - - public int getDefaultOptionIndex() { - return myDefaultOptionIndex; - } - - public String getTitle() { - return myTitle; - } - - public PopupStep onChosen(final ActionItem actionChoice, final boolean finalChoice) { - if (!actionChoice.isEnabled()) return FINAL_CHOICE; - final AnAction action = actionChoice.getAction(); - DataManager mgr = DataManager.getInstance(); - - final DataContext dataContext = myContext != null ? mgr.getDataContext(myContext) : mgr.getDataContext(); - - if (action instanceof ActionGroup && (!finalChoice || !((ActionGroup)action).canBePerformed(dataContext))) { - return createActionsStep((ActionGroup)action, dataContext, myEnableMnemonics, true, myShowDisabledActions, null, myContext, false, - myPreselectActionCondition, false); - } - else { - myFinalRunnable = new Runnable() { - public void run() { - action.actionPerformed( - new AnActionEvent(null, dataContext, ActionPlaces.UNKNOWN, action.getTemplatePresentation().clone(), - ActionManager.getInstance(), 0)); - } - }; - return FINAL_CHOICE; - } - } - - public Runnable getFinalRunnable() { - return myFinalRunnable; - } - - public boolean hasSubstep(final ActionItem selectedValue) { - return selectedValue != null && selectedValue.isEnabled() && selectedValue.getAction() instanceof ActionGroup; - } - - public void canceled() { - } - - public boolean isMnemonicsNavigationEnabled() { - return myEnableMnemonics; - } - - public MnemonicNavigationFilter getMnemonicNavigationFilter() { - return this; - } - - public boolean canBeHidden(final ActionItem value) { - return true; - } - - public String getIndexedString(final ActionItem value) { - return getTextFor(value); - } - - public boolean isSpeedSearchEnabled() { - return true; - } - - public boolean isAutoSelectionEnabled() { - return myAutoSelectionEnabled; - } - - public SpeedSearchFilter getSpeedSearchFilter() { - return this; - } - } - - @Nullable - public List getChildPopups(@NotNull final Component component) { - return FocusTrackback.getChildPopups(component); - } - - @Override - public boolean isPopupActive() { - return IdeEventQueue.getInstance().isPopupActive(); - } - - private static class ActionStepBuilder { - private final List myListModel; - private final DataContext myDataContext; - private final boolean myShowNumbers; - private final boolean myUseAlphaAsNumbers; - private final boolean myShowDisabled; - private final HashMap myAction2presentation; - private int myCurrentNumber; - private boolean myPrependWithSeparator; - private String mySeparatorText; - private final boolean myHonorActionMnemonics; - private Icon myEmptyIcon; - private int myMaxIconWidth = -1; - private int myMaxIconHeight = -1; - @NotNull private String myActionPlace; - - private ActionStepBuilder(@NotNull DataContext dataContext, final boolean showNumbers, final boolean useAlphaAsNumbers, - final boolean showDisabled, final boolean honorActionMnemonics) { - myUseAlphaAsNumbers = useAlphaAsNumbers; - myListModel = new ArrayList(); - myDataContext = dataContext; - myShowNumbers = showNumbers; - myShowDisabled = showDisabled; - myAction2presentation = new HashMap(); - myCurrentNumber = 0; - myPrependWithSeparator = false; - mySeparatorText = null; - myHonorActionMnemonics = honorActionMnemonics; - myActionPlace = ActionPlaces.UNKNOWN; - } - - public void setActionPlace(@NotNull String actionPlace) { - myActionPlace = actionPlace; - } - - public List getItems() { - return myListModel; - } - - public void buildGroup(ActionGroup actionGroup) { - calcMaxIconSize(actionGroup); - myEmptyIcon = myMaxIconHeight != -1 && myMaxIconWidth != -1 ? new EmptyIcon(myMaxIconWidth, myMaxIconHeight) : null; - - appendActionsFromGroup(actionGroup); - - if (myListModel.isEmpty()) { - myListModel.add(new ActionItem(Utils.EMPTY_MENU_FILLER, Utils.NOTHING_HERE, false, null, false, null)); - } - } - - private void calcMaxIconSize(final ActionGroup actionGroup) { - AnAction[] actions = actionGroup.getChildren(new AnActionEvent(null, myDataContext, myActionPlace, - getPresentation(actionGroup), ActionManager.getInstance(), 0)); - for (AnAction action : actions) { - if (action == null) continue; - if (action instanceof ActionGroup) { - final ActionGroup group = (ActionGroup)action; - if (!group.isPopup()) { - calcMaxIconSize(group); - continue; - } - } - - Icon icon = action.getTemplatePresentation().getIcon(); - if (icon != null) { - final int width = icon.getIconWidth(); - final int height = icon.getIconHeight(); - if (myMaxIconWidth < width) { - myMaxIconWidth = width; - } - if (myMaxIconHeight < height) { - myMaxIconHeight = height; - } - } - } - } - - private void appendActionsFromGroup(final ActionGroup actionGroup) { - AnAction[] actions = actionGroup.getChildren(new AnActionEvent(null, myDataContext, myActionPlace, - getPresentation(actionGroup), ActionManager.getInstance(), 0)); - for (AnAction action : actions) { - if (action == null) { - LOG.error("null action in group " + actionGroup); - continue; - } - if (action instanceof Separator) { - myPrependWithSeparator = true; - mySeparatorText = ((Separator)action).getText(); - } - else { - if (action instanceof ActionGroup) { - ActionGroup group = (ActionGroup)action; - if (group.isPopup()) { - appendAction(group); - } - else { - appendActionsFromGroup(group); - } - } - else { - appendAction(action); - } - } - } - } - - private void appendAction(@NotNull AnAction action) { - Presentation presentation = getPresentation(action); - AnActionEvent event = new AnActionEvent(null, myDataContext, myActionPlace, presentation, ActionManager.getInstance(), 0); - - ActionUtil.performDumbAwareUpdate(action, event, true); - if ((myShowDisabled || presentation.isEnabled()) && presentation.isVisible()) { - String text = presentation.getText(); - if (myShowNumbers) { - if (myCurrentNumber < 9) { - text = "&" + (myCurrentNumber + 1) + ". " + text; - } - else if (myCurrentNumber == 9) { - text = "&" + 0 + ". " + text; - } - else if (myUseAlphaAsNumbers) { - text = "&" + (char)('A' + myCurrentNumber - 10) + ". " + text; - } - myCurrentNumber++; - } - else if (myHonorActionMnemonics) { - text = Presentation.restoreTextWithMnemonic(text, action.getTemplatePresentation().getMnemonic()); - } - - Icon icon = presentation.getIcon(); - if (icon == null) { - @NonNls final String actionId = ActionManager.getInstance().getId(action); - icon = actionId != null && actionId.startsWith("QuickList.") ? QUICK_LIST_ICON : myEmptyIcon; - } - else { - icon = new IconWrapper(icon); - } - boolean prependSeparator = (!myListModel.isEmpty() || mySeparatorText != null) && myPrependWithSeparator; - assert text != null : action + " has no presentation"; - myListModel.add(new ActionItem(action, text, presentation.isEnabled(), icon, prependSeparator, mySeparatorText)); - myPrependWithSeparator = false; - mySeparatorText = null; - } - } - - /** - * Adjusts icon size to maximum, so that icons with different sizes were aligned correctly. - */ - private class IconWrapper implements Icon { - - private Icon myIcon; - - IconWrapper(Icon icon) { - myIcon = icon; - } - - @Override - public void paintIcon(Component c, Graphics g, int x, int y) { - myIcon.paintIcon(c, g, x, y); - } - - @Override - public int getIconWidth() { - return myMaxIconWidth; - } - - @Override - public int getIconHeight() { - return myMaxIconHeight; - } - } - - private Presentation getPresentation(@NotNull AnAction action) { - Presentation presentation = myAction2presentation.get(action); - if (presentation == null) { - presentation = action.getTemplatePresentation().clone(); - myAction2presentation.put(action, presentation); - } - return presentation; - } - } - - public BalloonBuilder createBalloonBuilder(@NotNull final JComponent content) { - return new BalloonPopupBuilderImpl(content); - } - - @Override - public BalloonBuilder createDialogBalloonBuilder(@NotNull JComponent content, String title) { - final BalloonPopupBuilderImpl builder = new BalloonPopupBuilderImpl(content); - final Color bg = UIManager.getColor("Panel.background"); - final Color borderOriginal = Color.darkGray; - final Color border = ColorUtil.toAlpha(borderOriginal, 75); - builder - .setDialogMode(true) - .setTitle(title) - .setAnimationCycle(200) - .setFillColor(bg) - .setBorderColor(border) - .setHideOnClickOutside(false) - .setHideOnKeyOutside(false) - .setHideOnAction(false) - .setCloseButtonEnabled(true) - .setShadow(true); - - return builder; - } - - public BalloonBuilder createHtmlTextBalloonBuilder(@NotNull final String htmlContent, @Nullable final Icon icon, final Color fillColor, - @Nullable final HyperlinkListener listener) { - - - JEditorPane text = IdeTooltipManager.initPane(htmlContent, new HintHint().setAwtTooltip(true), null); - - if (listener != null) { - text.addHyperlinkListener(listener); - } - text.setEditable(false); - NonOpaquePanel.setTransparent(text); - text.setBorder(null); - - - JLabel label = new JLabel(); - final JPanel content = new NonOpaquePanel(new BorderLayout((int)(label.getIconTextGap() * 1.5), (int)(label.getIconTextGap() * 1.5))); - - final NonOpaquePanel textWrapper = new NonOpaquePanel(new GridBagLayout()); - JScrollPane scrolledText = new JScrollPane(text); - scrolledText.setBackground(fillColor); - scrolledText.getViewport().setBackground(fillColor); - scrolledText.getViewport().setBorder(null); - scrolledText.setBorder(null); - textWrapper.add(scrolledText); - content.add(textWrapper, BorderLayout.CENTER); - - final NonOpaquePanel north = new NonOpaquePanel(new BorderLayout()); - north.add(new JLabel(icon), BorderLayout.NORTH); - content.add(north, BorderLayout.WEST); - - content.setBorder(new EmptyBorder(2, 4, 2, 4)); - - final BalloonBuilder builder = createBalloonBuilder(content); - - builder.setFillColor(fillColor); - - return builder; - } - - @Override - public BalloonBuilder createHtmlTextBalloonBuilder(@NotNull String htmlContent, - MessageType messageType, - @Nullable HyperlinkListener listener) { - return createHtmlTextBalloonBuilder(htmlContent, messageType.getDefaultIcon(), messageType.getPopupBackground(), listener); - } -} +/* + * Copyright 2000-2012 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.ui.popup; + +import com.intellij.CommonBundle; +import com.intellij.icons.AllIcons; +import com.intellij.ide.DataManager; +import com.intellij.ide.IdeEventQueue; +import com.intellij.ide.IdeTooltipManager; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ex.ActionUtil; +import com.intellij.openapi.actionSystem.impl.ActionMenu; +import com.intellij.openapi.actionSystem.impl.Utils; +import com.intellij.openapi.application.ex.ApplicationEx; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.VisualPosition; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.popup.*; +import com.intellij.openapi.ui.popup.util.BaseListPopupStep; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.EmptyRunnable; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.openapi.wm.ex.WindowManagerEx; +import com.intellij.openapi.wm.impl.IdeFrameImpl; +import com.intellij.ui.ColorUtil; +import com.intellij.ui.FocusTrackback; +import com.intellij.ui.HintHint; +import com.intellij.ui.awt.RelativePoint; +import com.intellij.ui.components.panels.NonOpaquePanel; +import com.intellij.ui.popup.list.ListPopupImpl; +import com.intellij.ui.popup.mock.MockConfirmation; +import com.intellij.ui.popup.tree.TreePopupImpl; +import com.intellij.util.ui.EmptyIcon; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +public class PopupFactoryImpl extends JBPopupFactory { + + /** + * Allows to get an editor position for which a popup with auxiliary information might be shown. + *

+ * Primary intention for this key is to hint popup position for the non-caret location. + */ + public static final Key ANCHOR_POPUP_POSITION = Key.create("popup.anchor.position"); + + private static final Logger LOG = Logger.getInstance("#com.intellij.ui.popup.PopupFactoryImpl"); + + private static final Icon QUICK_LIST_ICON = AllIcons.Actions.QuickList; + + public ListPopup createConfirmation(String title, final Runnable onYes, int defaultOptionIndex) { + return createConfirmation(title, CommonBundle.getYesButtonText(), CommonBundle.getNoButtonText(), onYes, defaultOptionIndex); + } + + public ListPopup createConfirmation(String title, final String yesText, String noText, final Runnable onYes, int defaultOptionIndex) { + return createConfirmation(title, yesText, noText, onYes, EmptyRunnable.getInstance(), defaultOptionIndex); + } + + public JBPopup createMessage(String text) { + return createListPopup(new BaseListPopupStep(null, new String[]{text})); + } + + @Override + public Balloon getParentBalloonFor(@Nullable Component c) { + if (c == null) return null; + Component eachParent = c; + while (eachParent != null) { + if (eachParent instanceof JComponent) { + Object balloon = ((JComponent)eachParent).getClientProperty(Balloon.KEY); + if (balloon instanceof Balloon) { + return (Balloon)balloon; + } + } + eachParent = eachParent.getParent(); + } + + return null; + } + + public ListPopup createConfirmation(String title, + final String yesText, + String noText, + final Runnable onYes, + final Runnable onNo, + int defaultOptionIndex) + { + + final BaseListPopupStep step = new BaseListPopupStep(title, new String[]{yesText, noText}) { + public PopupStep onChosen(String selectedValue, final boolean finalChoice) { + if (selectedValue.equals(yesText)) { + onYes.run(); + } + else { + onNo.run(); + } + return FINAL_CHOICE; + } + + public void canceled() { + onNo.run(); + } + + public boolean isMnemonicsNavigationEnabled() { + return true; + } + }; + step.setDefaultOptionIndex(defaultOptionIndex); + + final ApplicationEx app = ApplicationManagerEx.getApplicationEx(); + return app == null || !app.isUnitTestMode() ? new ListPopupImpl(step) : new MockConfirmation(step, yesText); + } + + + private static ListPopup createActionGroupPopup(final String title, + final ActionGroup actionGroup, + @NotNull DataContext dataContext, + boolean showNumbers, + boolean useAlphaAsNumbers, + boolean showDisabledActions, + boolean honorActionMnemonics, + final Runnable disposeCallback, + final int maxRowCount) { + return createActionGroupPopup(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, disposeCallback, + maxRowCount, null, null); + } + + public ListPopup createActionGroupPopup(final String title, + final ActionGroup actionGroup, + @NotNull DataContext dataContext, + boolean showNumbers, + boolean showDisabledActions, + boolean honorActionMnemonics, + final Runnable disposeCallback, + final int maxRowCount) { + return createActionGroupPopup(title, actionGroup, dataContext, showNumbers, showDisabledActions, honorActionMnemonics, disposeCallback, + maxRowCount, null); + } + + private static ListPopup createActionGroupPopup(final String title, + final ActionGroup actionGroup, + @NotNull DataContext dataContext, + boolean showNumbers, + boolean useAlphaAsNumbers, + boolean showDisabledActions, + boolean honorActionMnemonics, + final Runnable disposeCallback, + final int maxRowCount, + final Condition preselectActionCondition, @Nullable final String actionPlace) { + return new ActionGroupPopup(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, + disposeCallback, maxRowCount, preselectActionCondition, actionPlace); + } + + public static class ActionGroupPopup extends ListPopupImpl { + + private final Runnable myDisposeCallback; + private final Component myComponent; + + public ActionGroupPopup(final String title, final ActionGroup actionGroup, @NotNull DataContext dataContext, + boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, boolean honorActionMnemonics, + final Runnable disposeCallback, final int maxRowCount, final Condition preselectActionCondition, + @Nullable final String actionPlace) { + super(createStep(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, + preselectActionCondition, actionPlace), + maxRowCount); + myDisposeCallback = disposeCallback; + myComponent = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext); + + addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + final JList list = (JList)e.getSource(); + final ActionItem actionItem = (ActionItem)list.getSelectedValue(); + if (actionItem == null) return; + AnAction action = actionItem.getAction(); + Presentation presentation = new Presentation(); + presentation.setDescription(action.getTemplatePresentation().getDescription()); + final String actualActionPlace = actionPlace == null ? ActionPlaces.UNKNOWN : actionPlace; + action.update(new AnActionEvent(null, DataManager.getInstance().getDataContext(myComponent), actualActionPlace, presentation, + ActionManager.getInstance(), 0)); + ActionMenu.showDescriptionInStatusBar(true, myComponent, presentation.getDescription()); + } + }); + } + + private static ListPopupStep createStep(String title, ActionGroup actionGroup, @NotNull DataContext dataContext, + boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, + boolean honorActionMnemonics, + Condition preselectActionCondition, @Nullable String actionPlace) { + final Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext); + + final ActionStepBuilder builder = + new ActionStepBuilder(dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics); + if (actionPlace != null) { + builder.setActionPlace(actionPlace); + } + builder.buildGroup(actionGroup); + final List items = builder.getItems(); + + return new ActionPopupStep(items, title, component, showNumbers || honorActionMnemonics && itemsHaveMnemonics(items), + preselectActionCondition, false, showDisabledActions); + } + + @Override + public void dispose() { + if (myDisposeCallback != null) { + myDisposeCallback.run(); + } + ActionMenu.showDescriptionInStatusBar(true, myComponent, null); + super.dispose(); + } + } + + public ListPopup createActionGroupPopup(final String title, + final ActionGroup actionGroup, + @NotNull DataContext dataContext, + boolean showNumbers, + boolean showDisabledActions, + boolean honorActionMnemonics, + final Runnable disposeCallback, + final int maxRowCount, + final Condition preselectActionCondition) { + return createActionGroupPopup(title, actionGroup, dataContext, showNumbers, true, showDisabledActions, honorActionMnemonics, + disposeCallback, maxRowCount, preselectActionCondition, null); + } + + public ListPopup createActionGroupPopup(String title, + ActionGroup actionGroup, + @NotNull DataContext dataContext, + ActionSelectionAid selectionAidMethod, + boolean showDisabledActions) { + return createActionGroupPopup(title, actionGroup, dataContext, + selectionAidMethod == ActionSelectionAid.NUMBERING || selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, + selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, + showDisabledActions, + selectionAidMethod == ActionSelectionAid.MNEMONICS, + null, -1); + } + + public ListPopup createActionGroupPopup(String title, + ActionGroup actionGroup, + @NotNull DataContext dataContext, + ActionSelectionAid selectionAidMethod, + boolean showDisabledActions, + @Nullable String actionPlace) { + return createActionGroupPopup(title, actionGroup, dataContext, + selectionAidMethod == ActionSelectionAid.NUMBERING || selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, + selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, + showDisabledActions, + selectionAidMethod == ActionSelectionAid.MNEMONICS, + null, -1, null, actionPlace); + } + + public ListPopup createActionGroupPopup(String title, + ActionGroup actionGroup, + @NotNull DataContext dataContext, + ActionSelectionAid selectionAidMethod, + boolean showDisabledActions, + Runnable disposeCallback, + int maxRowCount) { + return createActionGroupPopup(title, actionGroup, dataContext, + selectionAidMethod == ActionSelectionAid.NUMBERING || selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, + selectionAidMethod == ActionSelectionAid.ALPHA_NUMBERING, + showDisabledActions, + selectionAidMethod == ActionSelectionAid.MNEMONICS, + disposeCallback, + maxRowCount); + } + + public ListPopupStep createActionsStep(final ActionGroup actionGroup, + @NotNull DataContext dataContext, + final boolean showNumbers, + final boolean showDisabledActions, + final String title, + final Component component, + final boolean honorActionMnemonics) { + return createActionsStep(actionGroup, dataContext, showNumbers, showDisabledActions, title, component, honorActionMnemonics, 0, false); + } + + private static ListPopupStep createActionsStep(ActionGroup actionGroup, @NotNull DataContext dataContext, + boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, + String title, Component component, boolean honorActionMnemonics, + final int defaultOptionIndex, final boolean autoSelectionEnabled) { + final List items = makeActionItemsFromActionGroup(actionGroup, dataContext, showNumbers, useAlphaAsNumbers, + showDisabledActions, honorActionMnemonics); + return new ActionPopupStep(items, title, component, showNumbers || honorActionMnemonics && itemsHaveMnemonics(items), + new Condition() { + @Override + public boolean value(AnAction action) { + return defaultOptionIndex >= 0 && + defaultOptionIndex < items.size() && + items.get(defaultOptionIndex).getAction().equals(action); + } + }, autoSelectionEnabled, showDisabledActions); + } + + private static List makeActionItemsFromActionGroup(ActionGroup actionGroup, + DataContext dataContext, + boolean showNumbers, + boolean useAlphaAsNumbers, + boolean showDisabledActions, + boolean honorActionMnemonics) { + final ActionStepBuilder builder = new ActionStepBuilder(dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, + honorActionMnemonics); + builder.buildGroup(actionGroup); + return builder.getItems(); + } + + private static ListPopupStep createActionsStep(ActionGroup actionGroup, @NotNull DataContext dataContext, + boolean showNumbers, boolean useAlphaAsNumbers, boolean showDisabledActions, + String title, Component component, boolean honorActionMnemonics, + Condition preselectActionCondition, boolean autoSelectionEnabled) { + final List items = makeActionItemsFromActionGroup(actionGroup, dataContext, showNumbers, useAlphaAsNumbers, + showDisabledActions, honorActionMnemonics); + return new ActionPopupStep(items, title, component, showNumbers || honorActionMnemonics && itemsHaveMnemonics(items), preselectActionCondition, + autoSelectionEnabled, showDisabledActions); + } + + public ListPopupStep createActionsStep(ActionGroup actionGroup, @NotNull DataContext dataContext, boolean showNumbers, boolean showDisabledActions, + String title, Component component, boolean honorActionMnemonics, int defaultOptionIndex, + final boolean autoSelectionEnabled) { + return createActionsStep(actionGroup, dataContext, showNumbers, true, showDisabledActions, title, component, honorActionMnemonics, + defaultOptionIndex, autoSelectionEnabled); + } + + private static boolean itemsHaveMnemonics(final List items) { + for (ActionItem item : items) { + if (item.getAction().getTemplatePresentation().getMnemonic() != 0) return true; + } + + return false; + } + + public ListPopup createWizardStep(PopupStep step) { + return new ListPopupImpl((ListPopupStep) step); + } + + public ListPopup createListPopup(ListPopupStep step) { + return new ListPopupImpl(step); + } + + public TreePopup createTree(JBPopup parent, TreePopupStep aStep, Object parentValue) { + return new TreePopupImpl(parent, aStep, parentValue); + } + + public TreePopup createTree(TreePopupStep aStep) { + return new TreePopupImpl(aStep); + } + + public ComponentPopupBuilder createComponentPopupBuilder(JComponent content, JComponent prefferableFocusComponent) { + return new ComponentPopupBuilderImpl(content, prefferableFocusComponent); + } + + + public RelativePoint guessBestPopupLocation(@NotNull DataContext dataContext) { + KeyboardFocusManager focusManager=KeyboardFocusManager.getCurrentKeyboardFocusManager(); + Component component = focusManager.getFocusOwner(); + JComponent focusOwner=component instanceof JComponent ? (JComponent)component : null; + + if (focusOwner == null) { + Project project = PlatformDataKeys.PROJECT.getData(dataContext); + IdeFrameImpl frame = project == null ? null : ((WindowManagerEx)WindowManager.getInstance()).getFrame(project); + focusOwner = frame == null ? null : frame.getRootPane(); + if (focusOwner == null) { + throw new IllegalArgumentException("focusOwner cannot be null"); + } + } + + final Point point = PlatformDataKeys.CONTEXT_MENU_POINT.getData(dataContext); + if (point != null) { + return new RelativePoint(focusOwner, point); + } + + Editor editor = PlatformDataKeys.EDITOR.getData(dataContext); + if (editor != null && focusOwner == editor.getContentComponent()) { + return guessBestPopupLocation(editor); + } + else { + return guessBestPopupLocation(focusOwner); + } + } + + public RelativePoint guessBestPopupLocation(final JComponent component) { + Point popupMenuPoint = null; + final Rectangle visibleRect = component.getVisibleRect(); + if (component instanceof JList) { // JList + JList list = (JList)component; + int firstVisibleIndex = list.getFirstVisibleIndex(); + int lastVisibleIndex = list.getLastVisibleIndex(); + int[] selectedIndices = list.getSelectedIndices(); + for (int index : selectedIndices) { + if (firstVisibleIndex <= index && index <= lastVisibleIndex) { + Rectangle cellBounds = list.getCellBounds(index, index); + popupMenuPoint = new Point(visibleRect.x + visibleRect.width / 4, cellBounds.y + cellBounds.height); + break; + } + } + } + else if (component instanceof JTree) { // JTree + JTree tree = (JTree)component; + int[] selectionRows = tree.getSelectionRows(); + if (selectionRows != null) { + Arrays.sort(selectionRows); + for (int i = 0; i < selectionRows.length; i++) { + int row = selectionRows[i]; + Rectangle rowBounds = tree.getRowBounds(row); + if (visibleRect.contains(rowBounds)) { + popupMenuPoint = new Point(rowBounds.x + 2, rowBounds.y + rowBounds.height - 1); + break; + } + } + if (popupMenuPoint == null) {//All selected rows are out of visible rect + Point visibleCenter = new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2); + double minDistance = Double.POSITIVE_INFINITY; + int bestRow = -1; + Point rowCenter; + double distance; + for (int i = 0; i < selectionRows.length; i++) { + int row = selectionRows[i]; + Rectangle rowBounds = tree.getRowBounds(row); + rowCenter = new Point(rowBounds.x + rowBounds.width / 2, rowBounds.y + rowBounds.height / 2); + distance = visibleCenter.distance(rowCenter); + if (minDistance > distance) { + minDistance = distance; + bestRow = row; + } + } + + if (bestRow != -1) { + Rectangle rowBounds = tree.getRowBounds(bestRow); + tree.scrollRectToVisible(new Rectangle(rowBounds.x, rowBounds.y, Math.min(visibleRect.width, rowBounds.width), rowBounds.height)); + popupMenuPoint = new Point(rowBounds.x + 2, rowBounds.y + rowBounds.height - 1); + } + } + } + } else if (component instanceof PopupOwner){ + popupMenuPoint = ((PopupOwner)component).getBestPopupPosition(); + } + // TODO[vova] add usability for JTable + if (popupMenuPoint == null) { + popupMenuPoint = new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2); + } + + return new RelativePoint(component, popupMenuPoint); + } + + @Override + public boolean isBestPopupLocationVisible(Editor editor) { + return getVisibleBestPopupLocation(editor) != null; + } + + public RelativePoint guessBestPopupLocation(Editor editor) { + Point p = getVisibleBestPopupLocation(editor); + if (p == null) { + final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea(); + p = new Point((visibleArea.x + visibleArea.width) / 2, (visibleArea.y + visibleArea.height) / 2); + } + return new RelativePoint(editor.getContentComponent(), p); + } + + @Nullable + private static Point getVisibleBestPopupLocation(Editor editor) { + VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION); + + if (visualPosition == null) { + CaretModel caretModel = editor.getCaretModel(); + if (caretModel.isUpToDate()) { + visualPosition = caretModel.getVisualPosition(); + } + else { + visualPosition = editor.offsetToVisualPosition(caretModel.getOffset()); + } + } + + Point p = editor.visualPositionToXY(new VisualPosition(visualPosition.line + 1, visualPosition.column)); + + final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea(); + return visibleArea.contains(p) ? p : null; + } + + public Point getCenterOf(JComponent container, JComponent content) { + return AbstractPopup.getCenterOf(container, content); + } + + public static class ActionItem { + private final AnAction myAction; + private final String myText; + private final boolean myIsEnabled; + private final Icon myIcon; + private final boolean myPrependWithSeparator; + private final String mySeparatorText; + + private ActionItem(@NotNull AnAction action, @NotNull String text, boolean enabled, Icon icon, final boolean prependWithSeparator, String separatorText) { + myAction = action; + myText = text; + myIsEnabled = enabled; + myIcon = icon; + myPrependWithSeparator = prependWithSeparator; + mySeparatorText = separatorText; + } + + @NotNull + public AnAction getAction() { + return myAction; + } + + @NotNull + public String getText() { + return myText; + } + + public Icon getIcon() { + return myIcon; + } + + public boolean isPrependWithSeparator() { + return myPrependWithSeparator; + } + + public String getSeparatorText() { + return mySeparatorText; + } + + public boolean isEnabled() { return myIsEnabled; } + } + + private static class ActionPopupStep implements ListPopupStep, MnemonicNavigationFilter, SpeedSearchFilter { + private final List myItems; + private final String myTitle; + private final Component myContext; + private final boolean myEnableMnemonics; + private final int myDefaultOptionIndex; + private final boolean myAutoSelectionEnabled; + private final boolean myShowDisabledActions; + private Runnable myFinalRunnable; + @Nullable private final Condition myPreselectActionCondition; + + private ActionPopupStep(@NotNull final List items, final String title, Component context, boolean enableMnemonics, + @Nullable Condition preselectActionCondition, final boolean autoSelection, boolean showDisabledActions) { + myItems = items; + myTitle = title; + myContext = context; + myEnableMnemonics = enableMnemonics; + myDefaultOptionIndex = getDefaultOptionIndexFromSelectCondition(preselectActionCondition, items); + myPreselectActionCondition = preselectActionCondition; + myAutoSelectionEnabled = autoSelection; + myShowDisabledActions = showDisabledActions; + } + + private static int getDefaultOptionIndexFromSelectCondition(@Nullable Condition preselectActionCondition, + @NotNull List items) { + int defaultOptionIndex = 0; + if (preselectActionCondition != null) { + for (int i = 0; i < items.size(); i++) { + final AnAction action = items.get(i).getAction(); + if (preselectActionCondition.value(action)) { + defaultOptionIndex = i; + break; + } + } + } + return defaultOptionIndex; + } + + @NotNull + public List getValues() { + return myItems; + } + + public boolean isSelectable(final ActionItem value) { + return value.isEnabled(); + } + + public int getMnemonicPos(final ActionItem value) { + final String text = getTextFor(value); + int i = text.indexOf(UIUtil.MNEMONIC); + if (i < 0) { + i = text.indexOf('&'); + } + if (i < 0) { + i = text.indexOf('_'); + } + return i; + } + + public Icon getIconFor(final ActionItem aValue) { + return aValue.getIcon(); + } + + @NotNull + public String getTextFor(final ActionItem value) { + return value.getText(); + } + + public ListSeparator getSeparatorAbove(final ActionItem value) { + return value.isPrependWithSeparator() ? new ListSeparator(value.getSeparatorText()) : null; + } + + public int getDefaultOptionIndex() { + return myDefaultOptionIndex; + } + + public String getTitle() { + return myTitle; + } + + public PopupStep onChosen(final ActionItem actionChoice, final boolean finalChoice) { + if (!actionChoice.isEnabled()) return FINAL_CHOICE; + final AnAction action = actionChoice.getAction(); + DataManager mgr = DataManager.getInstance(); + + final DataContext dataContext = myContext != null ? mgr.getDataContext(myContext) : mgr.getDataContext(); + + if (action instanceof ActionGroup && (!finalChoice || !((ActionGroup)action).canBePerformed(dataContext))) { + return createActionsStep((ActionGroup)action, dataContext, myEnableMnemonics, true, myShowDisabledActions, null, myContext, false, + myPreselectActionCondition, false); + } + else { + myFinalRunnable = new Runnable() { + public void run() { + action.actionPerformed( + new AnActionEvent(null, dataContext, ActionPlaces.UNKNOWN, action.getTemplatePresentation().clone(), + ActionManager.getInstance(), 0)); + } + }; + return FINAL_CHOICE; + } + } + + public Runnable getFinalRunnable() { + return myFinalRunnable; + } + + public boolean hasSubstep(final ActionItem selectedValue) { + return selectedValue != null && selectedValue.isEnabled() && selectedValue.getAction() instanceof ActionGroup; + } + + public void canceled() { + } + + public boolean isMnemonicsNavigationEnabled() { + return myEnableMnemonics; + } + + public MnemonicNavigationFilter getMnemonicNavigationFilter() { + return this; + } + + public boolean canBeHidden(final ActionItem value) { + return true; + } + + public String getIndexedString(final ActionItem value) { + return getTextFor(value); + } + + public boolean isSpeedSearchEnabled() { + return true; + } + + public boolean isAutoSelectionEnabled() { + return myAutoSelectionEnabled; + } + + public SpeedSearchFilter getSpeedSearchFilter() { + return this; + } + } + + @Nullable + public List getChildPopups(@NotNull final Component component) { + return FocusTrackback.getChildPopups(component); + } + + @Override + public boolean isPopupActive() { + return IdeEventQueue.getInstance().isPopupActive(); + } + + private static class ActionStepBuilder { + private final List myListModel; + private final DataContext myDataContext; + private final boolean myShowNumbers; + private final boolean myUseAlphaAsNumbers; + private final boolean myShowDisabled; + private final HashMap myAction2presentation; + private int myCurrentNumber; + private boolean myPrependWithSeparator; + private String mySeparatorText; + private final boolean myHonorActionMnemonics; + private Icon myEmptyIcon; + private int myMaxIconWidth = -1; + private int myMaxIconHeight = -1; + @NotNull private String myActionPlace; + + private ActionStepBuilder(@NotNull DataContext dataContext, final boolean showNumbers, final boolean useAlphaAsNumbers, + final boolean showDisabled, final boolean honorActionMnemonics) { + myUseAlphaAsNumbers = useAlphaAsNumbers; + myListModel = new ArrayList(); + myDataContext = dataContext; + myShowNumbers = showNumbers; + myShowDisabled = showDisabled; + myAction2presentation = new HashMap(); + myCurrentNumber = 0; + myPrependWithSeparator = false; + mySeparatorText = null; + myHonorActionMnemonics = honorActionMnemonics; + myActionPlace = ActionPlaces.UNKNOWN; + } + + public void setActionPlace(@NotNull String actionPlace) { + myActionPlace = actionPlace; + } + + public List getItems() { + return myListModel; + } + + public void buildGroup(ActionGroup actionGroup) { + calcMaxIconSize(actionGroup); + myEmptyIcon = myMaxIconHeight != -1 && myMaxIconWidth != -1 ? new EmptyIcon(myMaxIconWidth, myMaxIconHeight) : null; + + appendActionsFromGroup(actionGroup); + + if (myListModel.isEmpty()) { + myListModel.add(new ActionItem(Utils.EMPTY_MENU_FILLER, Utils.NOTHING_HERE, false, null, false, null)); + } + } + + private void calcMaxIconSize(final ActionGroup actionGroup) { + AnAction[] actions = actionGroup.getChildren(new AnActionEvent(null, myDataContext, myActionPlace, + getPresentation(actionGroup), ActionManager.getInstance(), 0)); + for (AnAction action : actions) { + if (action == null) continue; + if (action instanceof ActionGroup) { + final ActionGroup group = (ActionGroup)action; + if (!group.isPopup()) { + calcMaxIconSize(group); + continue; + } + } + + Icon icon = action.getTemplatePresentation().getIcon(); + if (icon != null) { + final int width = icon.getIconWidth(); + final int height = icon.getIconHeight(); + if (myMaxIconWidth < width) { + myMaxIconWidth = width; + } + if (myMaxIconHeight < height) { + myMaxIconHeight = height; + } + } + } + } + + private void appendActionsFromGroup(final ActionGroup actionGroup) { + AnAction[] actions = actionGroup.getChildren(new AnActionEvent(null, myDataContext, myActionPlace, + getPresentation(actionGroup), ActionManager.getInstance(), 0)); + for (AnAction action : actions) { + if (action == null) { + LOG.error("null action in group " + actionGroup); + continue; + } + if (action instanceof Separator) { + myPrependWithSeparator = true; + mySeparatorText = ((Separator)action).getText(); + } + else { + if (action instanceof ActionGroup) { + ActionGroup group = (ActionGroup)action; + if (group.isPopup()) { + appendAction(group); + } + else { + appendActionsFromGroup(group); + } + } + else { + appendAction(action); + } + } + } + } + + private void appendAction(@NotNull AnAction action) { + Presentation presentation = getPresentation(action); + AnActionEvent event = new AnActionEvent(null, myDataContext, myActionPlace, presentation, ActionManager.getInstance(), 0); + + ActionUtil.performDumbAwareUpdate(action, event, true); + if ((myShowDisabled || presentation.isEnabled()) && presentation.isVisible()) { + String text = presentation.getText(); + if (myShowNumbers) { + if (myCurrentNumber < 9) { + text = "&" + (myCurrentNumber + 1) + ". " + text; + } + else if (myCurrentNumber == 9) { + text = "&" + 0 + ". " + text; + } + else if (myUseAlphaAsNumbers) { + text = "&" + (char)('A' + myCurrentNumber - 10) + ". " + text; + } + myCurrentNumber++; + } + else if (myHonorActionMnemonics) { + text = Presentation.restoreTextWithMnemonic(text, action.getTemplatePresentation().getMnemonic()); + } + + Icon icon = presentation.getIcon(); + if (icon == null) { + @NonNls final String actionId = ActionManager.getInstance().getId(action); + icon = actionId != null && actionId.startsWith("QuickList.") ? QUICK_LIST_ICON : myEmptyIcon; + } + else { + icon = new IconWrapper(icon); + } + boolean prependSeparator = (!myListModel.isEmpty() || mySeparatorText != null) && myPrependWithSeparator; + assert text != null : action + " has no presentation"; + myListModel.add(new ActionItem(action, text, presentation.isEnabled(), icon, prependSeparator, mySeparatorText)); + myPrependWithSeparator = false; + mySeparatorText = null; + } + } + + /** + * Adjusts icon size to maximum, so that icons with different sizes were aligned correctly. + */ + private class IconWrapper implements Icon { + + private Icon myIcon; + + IconWrapper(Icon icon) { + myIcon = icon; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + myIcon.paintIcon(c, g, x, y); + } + + @Override + public int getIconWidth() { + return myMaxIconWidth; + } + + @Override + public int getIconHeight() { + return myMaxIconHeight; + } + } + + private Presentation getPresentation(@NotNull AnAction action) { + Presentation presentation = myAction2presentation.get(action); + if (presentation == null) { + presentation = action.getTemplatePresentation().clone(); + myAction2presentation.put(action, presentation); + } + return presentation; + } + } + + public BalloonBuilder createBalloonBuilder(@NotNull final JComponent content) { + return new BalloonPopupBuilderImpl(content); + } + + @Override + public BalloonBuilder createDialogBalloonBuilder(@NotNull JComponent content, String title) { + final BalloonPopupBuilderImpl builder = new BalloonPopupBuilderImpl(content); + final Color bg = UIManager.getColor("Panel.background"); + final Color borderOriginal = Color.darkGray; + final Color border = ColorUtil.toAlpha(borderOriginal, 75); + builder + .setDialogMode(true) + .setTitle(title) + .setAnimationCycle(200) + .setFillColor(bg) + .setBorderColor(border) + .setHideOnClickOutside(false) + .setHideOnKeyOutside(false) + .setHideOnAction(false) + .setCloseButtonEnabled(true) + .setShadow(true); + + return builder; + } + + public BalloonBuilder createHtmlTextBalloonBuilder(@NotNull final String htmlContent, @Nullable final Icon icon, final Color fillColor, + @Nullable final HyperlinkListener listener) { + + + JEditorPane text = IdeTooltipManager.initPane(htmlContent, new HintHint().setAwtTooltip(true), null); + + if (listener != null) { + text.addHyperlinkListener(listener); + } + text.setEditable(false); + NonOpaquePanel.setTransparent(text); + text.setBorder(null); + + + JLabel label = new JLabel(); + final JPanel content = new NonOpaquePanel(new BorderLayout((int)(label.getIconTextGap() * 1.5), (int)(label.getIconTextGap() * 1.5))); + + final NonOpaquePanel textWrapper = new NonOpaquePanel(new GridBagLayout()); + JScrollPane scrolledText = new JScrollPane(text); + scrolledText.setBackground(fillColor); + scrolledText.getViewport().setBackground(fillColor); + scrolledText.getViewport().setBorder(null); + scrolledText.setBorder(null); + textWrapper.add(scrolledText); + content.add(textWrapper, BorderLayout.CENTER); + + final NonOpaquePanel north = new NonOpaquePanel(new BorderLayout()); + north.add(new JLabel(icon), BorderLayout.NORTH); + content.add(north, BorderLayout.WEST); + + content.setBorder(new EmptyBorder(2, 4, 2, 4)); + + final BalloonBuilder builder = createBalloonBuilder(content); + + builder.setFillColor(fillColor); + + return builder; + } + + @Override + public BalloonBuilder createHtmlTextBalloonBuilder(@NotNull String htmlContent, + MessageType messageType, + @Nullable HyperlinkListener listener) { + return createHtmlTextBalloonBuilder(htmlContent, messageType.getDefaultIcon(), messageType.getPopupBackground(), listener); + } +}