blob: 15e804495c84b546bc673607a8d6cad7ca252f23 [file] [log] [blame]
/*
* 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.list;
import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeEventQueue;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.ui.popup.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.statistics.StatisticsInfo;
import com.intellij.psi.statistics.StatisticsManager;
import com.intellij.ui.JBListWithHintProvider;
import com.intellij.ui.ListScrollingUtil;
import com.intellij.ui.SeparatorWithText;
import com.intellij.ui.popup.ClosableByLeftArrow;
import com.intellij.ui.popup.WizardPopup;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
public class ListPopupImpl extends WizardPopup implements ListPopup {
private MyList myList;
private MyMouseMotionListener myMouseMotionListener;
private MyMouseListener myMouseListener;
private ListPopupModel myListModel;
private int myIndexForShowingChild = -1;
private int myMaxRowCount = 20;
private boolean myAutoHandleBeforeShow;
public ListPopupImpl(@NotNull ListPopupStep aStep, int maxRowCount) {
super(aStep);
if (maxRowCount != -1){
myMaxRowCount = maxRowCount;
}
}
public ListPopupImpl(@NotNull ListPopupStep aStep) {
this(aStep, -1);
}
public ListPopupImpl(WizardPopup aParent, @NotNull ListPopupStep aStep, Object parentValue) {
this(aParent, aStep, parentValue, -1);
}
public ListPopupImpl(WizardPopup aParent, @NotNull ListPopupStep aStep, Object parentValue, int maxRowCount) {
super(aParent, aStep);
setParentValue(parentValue);
if (maxRowCount != -1){
myMaxRowCount = maxRowCount;
}
}
protected ListPopupModel getListModel() {
return myListModel;
}
@Override
protected boolean beforeShow() {
myList.addMouseMotionListener(myMouseMotionListener);
myList.addMouseListener(myMouseListener);
myList.setVisibleRowCount(Math.min(myMaxRowCount, myListModel.getSize()));
boolean shouldShow = super.beforeShow();
if (myAutoHandleBeforeShow) {
final boolean toDispose = tryToAutoSelect(true);
shouldShow &= !toDispose;
}
return shouldShow;
}
@Override
protected void afterShow() {
tryToAutoSelect(false);
}
private boolean tryToAutoSelect(boolean handleFinalChoices) {
ListPopupStep<Object> listStep = getListStep();
boolean selected = false;
if (listStep instanceof MultiSelectionListPopupStep<?>) {
int[] indices = ((MultiSelectionListPopupStep)listStep).getDefaultOptionIndices();
if (indices.length > 0) {
ListScrollingUtil.ensureIndexIsVisible(myList, indices[0], 0);
myList.setSelectedIndices(indices);
selected = true;
}
}
else {
final int defaultIndex = listStep.getDefaultOptionIndex();
if (defaultIndex >= 0 && defaultIndex < myList.getModel().getSize()) {
ListScrollingUtil.selectItem(myList, defaultIndex);
selected = true;
}
}
if (!selected) {
selectFirstSelectableItem();
}
if (listStep.isAutoSelectionEnabled()) {
if (!isVisible() && getSelectableCount() == 1) {
return _handleSelect(handleFinalChoices, null);
} else if (isVisible() && hasSingleSelectableItemWithSubmenu()) {
return _handleSelect(handleFinalChoices, null);
}
}
return false;
}
private boolean autoSelectUsingStatistics() {
final String filter = getSpeedSearch().getFilter();
if (!StringUtil.isEmpty(filter)) {
int maxUseCount = -1;
int mostUsedValue = -1;
int elementsCount = myListModel.getSize();
for (int i = 0; i < elementsCount; i++) {
Object value = myListModel.getElementAt(i);
final String text = getListStep().getTextFor(value);
final int count =
StatisticsManager.getInstance().getUseCount(new StatisticsInfo("#list_popup:" + myStep.getTitle() + "#" + filter, text));
if (count > maxUseCount) {
maxUseCount = count;
mostUsedValue = i;
}
}
if (mostUsedValue > 0) {
ListScrollingUtil.selectItem(myList, mostUsedValue);
return true;
}
}
return false;
}
private void selectFirstSelectableItem() {
for (int i = 0; i < myListModel.getSize(); i++) {
if (getListStep().isSelectable(myListModel.getElementAt(i))) {
myList.setSelectedIndex(i);
break;
}
}
}
private boolean hasSingleSelectableItemWithSubmenu() {
boolean oneSubmenuFound = false;
int countSelectables = 0;
for (int i = 0; i < myListModel.getSize(); i++) {
Object elementAt = myListModel.getElementAt(i);
if (getListStep().isSelectable(elementAt) ) {
countSelectables ++;
if (getStep().hasSubstep(elementAt)) {
if (oneSubmenuFound) {
return false;
}
oneSubmenuFound = true;
}
}
}
return oneSubmenuFound && countSelectables == 1;
}
private int getSelectableCount() {
int count = 0;
for (int i = 0; i < myListModel.getSize(); i++) {
final Object each = myListModel.getElementAt(i);
if (getListStep().isSelectable(each)) {
count++;
}
}
return count;
}
public JList getList() {
return myList;
}
@Override
protected JComponent createContent() {
myMouseMotionListener = new MyMouseMotionListener();
myMouseListener = new MyMouseListener();
myListModel = new ListPopupModel(this, getSpeedSearch(), getListStep());
myList = new MyList();
if (myStep.getTitle() != null) {
myList.getAccessibleContext().setAccessibleName(myStep.getTitle());
}
myList.setSelectionMode(isMultiSelectionEnabled() ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : ListSelectionModel.SINGLE_SELECTION);
myList.setSelectedIndex(0);
Insets padding = UIUtil.getListViewportPadding();
myList.setBorder(new EmptyBorder(padding));
ListScrollingUtil.installActions(myList);
myList.setCellRenderer(getListElementRenderer());
myList.getActionMap().get("selectNextColumn").setEnabled(false);
myList.getActionMap().get("selectPreviousColumn").setEnabled(false);
registerAction("handleSelection1", KeyEvent.VK_ENTER, 0, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleSelect(true);
}
});
registerAction("handleSelection2", KeyEvent.VK_RIGHT, 0, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleSelect(false);
}
});
registerAction("goBack2", KeyEvent.VK_LEFT, 0, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (isClosableByLeftArrow()) {
goBack();
}
}
});
myList.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return myList;
}
private boolean isMultiSelectionEnabled() {
return getListStep() instanceof MultiSelectionListPopupStep<?>;
}
private boolean isClosableByLeftArrow() {
return getParent() != null || myStep instanceof ClosableByLeftArrow;
}
@Override
protected ActionMap getActionMap() {
return myList.getActionMap();
}
@Override
protected InputMap getInputMap() {
return myList.getInputMap();
}
protected ListCellRenderer getListElementRenderer() {
return new PopupListElementRenderer(this);
}
@Override
public ListPopupStep<Object> getListStep() {
return (ListPopupStep<Object>) myStep;
}
@Override
public void dispose() {
myList.removeMouseMotionListener(myMouseMotionListener);
myList.removeMouseListener(myMouseListener);
super.dispose();
}
protected int getSelectedIndex() {
return myList.getSelectedIndex();
}
protected Rectangle getCellBounds(int i) {
return myList.getCellBounds(i, i);
}
@Override
public void disposeChildren() {
setIndexForShowingChild(-1);
super.disposeChildren();
}
@Override
protected void onAutoSelectionTimer() {
if (myList.getModel().getSize() > 0 && !myList.isSelectionEmpty() ) {
handleSelect(false);
}
else {
disposeChildren();
setIndexForShowingChild(-1);
}
}
@Override
public void handleSelect(boolean handleFinalChoices) {
_handleSelect(handleFinalChoices, null);
}
@Override
public void handleSelect(boolean handleFinalChoices, InputEvent e) {
_handleSelect(handleFinalChoices, e);
}
private boolean _handleSelect(final boolean handleFinalChoices, @Nullable InputEvent e) {
if (myList.getSelectedIndex() == -1) return false;
if (getSpeedSearch().isHoldingFilter() && myList.getModel().getSize() == 0) return false;
if (myList.getSelectedIndex() == getIndexForShowingChild()) {
if (myChild != null && !myChild.isVisible()) setIndexForShowingChild(-1);
return false;
}
final Object[] selectedValues = myList.getSelectedValues();
final ListPopupStep<Object> listStep = getListStep();
if (!listStep.isSelectable(selectedValues[0])) return false;
if ((listStep instanceof MultiSelectionListPopupStep<?> && !((MultiSelectionListPopupStep<Object>)listStep).hasSubstep(Arrays.asList(selectedValues))
|| !listStep.hasSubstep(selectedValues[0])) && !handleFinalChoices) return false;
disposeChildren();
if (myListModel.getSize() == 0) {
setFinalRunnable(myStep.getFinalRunnable());
setOk(true);
disposeAllParents(e);
setIndexForShowingChild(-1);
return true;
}
valuesSelected(selectedValues);
final PopupStep nextStep;
if (listStep instanceof MultiSelectionListPopupStep<?>) {
nextStep = ((MultiSelectionListPopupStep<Object>)listStep).onChosen(Arrays.asList(selectedValues), handleFinalChoices);
}
else if (e != null && listStep instanceof ListPopupStepEx<?>) {
nextStep = ((ListPopupStepEx<Object>)listStep).onChosen(selectedValues[0], handleFinalChoices, e.getModifiers());
}
else {
nextStep = listStep.onChosen(selectedValues[0], handleFinalChoices);
}
return handleNextStep(nextStep, selectedValues.length == 1 ? selectedValues[0] : null, e);
}
private void valuesSelected(final Object[] values) {
final String filter = getSpeedSearch().getFilter();
if (!StringUtil.isEmpty(filter)) {
for (Object value : values) {
final String text = getListStep().getTextFor(value);
StatisticsManager.getInstance().incUseCount(new StatisticsInfo("#list_popup:" + getListStep().getTitle() + "#" + filter, text));
}
}
}
private boolean handleNextStep(final PopupStep nextStep, Object parentValue, InputEvent e) {
if (nextStep != PopupStep.FINAL_CHOICE) {
final Point point = myList.indexToLocation(myList.getSelectedIndex());
SwingUtilities.convertPointToScreen(point, myList);
myChild = createPopup(this, nextStep, parentValue);
if (myChild instanceof ListPopupImpl) {
for (ListSelectionListener listener : myList.getListSelectionListeners()) {
((ListPopupImpl)myChild).addListSelectionListener(listener);
}
}
final JComponent container = getContent();
assert container != null : "container == null";
int y = point.y;
if (parentValue != null && getListModel().isSeparatorAboveOf(parentValue)) {
SeparatorWithText swt = new SeparatorWithText();
swt.setCaption(getListModel().getCaptionAboveOf(parentValue));
y += swt.getPreferredSize().height - 1;
}
myChild.show(container, point.x + container.getWidth() - STEP_X_PADDING, y, true);
setIndexForShowingChild(myList.getSelectedIndex());
return false;
}
else {
setOk(true);
setFinalRunnable(myStep.getFinalRunnable());
disposeAllParents(e);
setIndexForShowingChild(-1);
return true;
}
}
@Override
public void addListSelectionListener(ListSelectionListener listSelectionListener) {
myList.addListSelectionListener(listSelectionListener);
}
private class MyMouseMotionListener extends MouseMotionAdapter {
private int myLastSelectedIndex = -2;
@Override
public void mouseMoved(MouseEvent e) {
Point point = e.getPoint();
int index = myList.locationToIndex(point);
if (index != myLastSelectedIndex) {
if (!isMultiSelectionEnabled() || !UIUtil.isSelectionButtonDown(e) && myList.getSelectedIndices().length <= 1) {
myList.setSelectedIndex(index);
}
restartTimer();
myLastSelectedIndex = index;
}
notifyParentOnChildSelection();
}
}
protected boolean isActionClick(MouseEvent e) {
return UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true);
}
public Object[] getSelectedValues() {
return myList.getSelectedValues();
}
private class MyMouseListener extends MouseAdapter {
@Override
public void mouseReleased(MouseEvent e) {
if (!isActionClick(e) || isMultiSelectionEnabled() && UIUtil.isSelectionButtonDown(e)) return;
IdeEventQueue.getInstance().blockNextEvents(e); // sometimes, after popup close, MOUSE_RELEASE event delivers to other components
final Object selectedValue = myList.getSelectedValue();
final ListPopupStep<Object> listStep = getListStep();
handleSelect(handleFinalChoices(e, selectedValue, listStep), e);
stopTimer();
}
}
protected boolean handleFinalChoices(MouseEvent e, Object selectedValue, ListPopupStep<Object> listStep) {
return selectedValue == null || !listStep.hasSubstep(selectedValue) || !listStep.isSelectable(selectedValue) || !isOnNextStepButton(e);
}
private boolean isOnNextStepButton(MouseEvent e) {
final int index = myList.getSelectedIndex();
final Rectangle bounds = myList.getCellBounds(index, index);
final Point point = e.getPoint();
return bounds != null && point.getX() > bounds.width + bounds.getX() - AllIcons.Icons.Ide.NextStep.getIconWidth();
}
@Override
protected void process(KeyEvent aEvent) {
myList.processKeyEvent(aEvent);
}
private int getIndexForShowingChild() {
return myIndexForShowingChild;
}
private void setIndexForShowingChild(int aIndexForShowingChild) {
myIndexForShowingChild = aIndexForShowingChild;
}
private class MyList extends JBListWithHintProvider implements DataProvider {
public MyList() {
super(myListModel);
}
@Override
protected PsiElement getPsiElementForHint(Object selectedValue) {
return selectedValue instanceof PsiElement ? (PsiElement)selectedValue : null;
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(super.getPreferredScrollableViewportSize().width, getPreferredSize().height);
}
@Override
public void processKeyEvent(KeyEvent e) {
e.setSource(this);
super.processKeyEvent(e);
}
@Override
protected void processMouseEvent(MouseEvent e) {
if (UIUtil.isActionClick(e, MouseEvent.MOUSE_PRESSED) && isOnNextStepButton(e)) {
e.consume();
}
super.processMouseEvent(e);
}
@Override
public Object getData(String dataId) {
if (PlatformDataKeys.SELECTED_ITEM.is(dataId)){
return myList.getSelectedValue();
}
if (PlatformDataKeys.SELECTED_ITEMS.is(dataId)){
return myList.getSelectedValues();
}
return null;
}
}
@Override
protected void onSpeedSearchPatternChanged() {
myListModel.refilter();
if (myListModel.getSize() > 0) {
if (!autoSelectUsingStatistics()) {
int fullMatchIndex = myListModel.getClosestMatchIndex();
if (fullMatchIndex != -1) {
myList.setSelectedIndex(fullMatchIndex);
}
if (myListModel.getSize() <= myList.getSelectedIndex() || !myListModel.isVisible(myList.getSelectedValue())) {
myList.setSelectedIndex(0);
}
}
}
}
@Override
protected void onSelectByMnemonic(Object value) {
if (myListModel.isVisible(value)) {
myList.setSelectedValue(value, true);
myList.repaint();
handleSelect(true);
}
}
@Override
protected JComponent getPreferredFocusableComponent() {
return myList;
}
@Override
protected void onChildSelectedFor(Object value) {
if (myList.getSelectedValue() != value) {
myList.setSelectedValue(value, false);
}
}
@Override
public void setHandleAutoSelectionBeforeShow(final boolean autoHandle) {
myAutoHandleBeforeShow = autoHandle;
}
@Override
public boolean isModalContext() {
return true;
}
}