blob: 6014cb829993c7bfe802d2a6a38925be863000b0 [file] [log] [blame]
/*
* Copyright 2000-2014 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;
import com.intellij.icons.AllIcons;
import com.intellij.ide.CopyProvider;
import com.intellij.ide.DataManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiElement;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBList;
import com.intellij.ui.speedSearch.ListWithFilter;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @param <T> List item type. Must implement {@code equals()/hashCode()} correctly.
* @since 13.0
*/
public abstract class FinderRecursivePanel<T> extends JBSplitter implements DataProvider, Disposable {
@NotNull
private final Project myProject;
@Nullable
private final String myGroupId;
@Nullable
private final FinderRecursivePanel myParent;
@Nullable
private JComponent myChild = null;
private JBList myList;
private final CollectionListModel<T> myListModel = new CollectionListModel<T>();
private final MergingUpdateQueue myMergingUpdateQueue = new MergingUpdateQueue("FinderRecursivePanel", 100, true, this, this);
private final AtomicBoolean myUpdateSelectedPathModeActive = new AtomicBoolean();
private final CopyProvider myCopyProvider = new CopyProvider() {
@Override
public void performCopy(@NotNull DataContext dataContext) {
final T value = getSelectedValue();
CopyPasteManager.getInstance().setContents(new StringSelection(getItemText(value)));
}
@Override
public boolean isCopyEnabled(@NotNull DataContext dataContext) {
return getSelectedValue() != null;
}
@Override
public boolean isCopyVisible(@NotNull DataContext dataContext) {
return false;
}
};
protected FinderRecursivePanel(@NotNull FinderRecursivePanel parent) {
this(parent.getProject(), parent, parent.getGroupId());
}
protected FinderRecursivePanel(@NotNull Project project, @Nullable String groupId) {
this(project, null, groupId);
}
protected FinderRecursivePanel(@NotNull Project project,
@Nullable FinderRecursivePanel parent,
@Nullable String groupId) {
super(false, 0f);
myProject = project;
myParent = parent;
myGroupId = groupId;
if (myParent != null) {
Disposer.register(myParent, this);
}
}
public void init() {
initWithoutUpdatePanel();
updatePanel();
}
private void initWithoutUpdatePanel() {
setFirstComponent(createLeftComponent());
setSecondComponent(createDefaultRightComponent());
if (getGroupId() != null) {
setAndLoadSplitterProportionKey(getGroupId() + "[" + getIndex() + "]");
}
setDividerWidth(3);
setShowDividerIcon(false);
setShowDividerControls(true);
}
/**
* Called in read action.
*
* @return Items for list.
*/
@NotNull
protected abstract List<T> getListItems();
protected String getListEmptyText() {
return "No entries";
}
@NotNull
protected abstract String getItemText(T t);
@Nullable
protected Icon getItemIcon(T t) {
return null;
}
protected abstract boolean hasChildren(T t);
/**
* To determine item list background color (if enabled).
*
* @param t Current item.
* @return Containing file.
*/
@Nullable
protected VirtualFile getContainingFile(T t) {
return null;
}
protected boolean isEditable() {
return getSelectedValue() != null;
}
@Nullable
protected JComponent createRightComponent(T t) {
return new JPanel();
}
@Nullable
protected JComponent createDefaultRightComponent() {
final JBLabel label = new JBLabel("Nothing selected", SwingConstants.CENTER);
label.setFontColor(UIUtil.FontColor.BRIGHTER);
return label;
}
protected JComponent createLeftComponent() {
myList = createList();
final JScrollPane pane =
ScrollPaneFactory.createScrollPane(myList,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
return ListWithFilter.wrap(myList, pane, new Function<T, String>() {
@Override
public String fun(T o) {
return getItemText(o);
}
});
}
protected JBList createList() {
final JBList list = new JBList(myListModel);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setEmptyText(getListEmptyText());
list.setCellRenderer(createListCellRenderer());
installListActions(list);
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent event) {
if (event.getValueIsAdjusting()) return;
if (myUpdateSelectedPathModeActive.get()) return;
updateRightComponent(true);
}
});
ListScrollingUtil.installActions(list);
// installSpeedSearch(list); // TODO
installEditOnDoubleClick(list);
return list;
}
private void handleGotoPrevious() {
IdeFocusManager.getInstance(myProject).requestFocus(myList, true);
}
private void handleGotoNext() {
if (!myList.isEmpty()) {
if (myList.getSelectedValue() == null) {
myList.setSelectedIndex(0);
updateRightComponent(true);
}
}
IdeFocusManager.getInstance(myProject).requestFocus(myList, true);
}
private void installListActions(JBList list) {
AnAction previousPanelAction = new AnAction("Previous", null, AllIcons.Actions.Back) {
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(!isRootPanel());
}
@Override
public void actionPerformed(AnActionEvent e) {
assert myParent != null;
myParent.handleGotoPrevious();
}
};
previousPanelAction.registerCustomShortcutSet(KeyEvent.VK_LEFT, 0, list);
AnAction nextPanelAction = new AnAction("Next", null, AllIcons.Actions.Forward) {
@Override
public void update(AnActionEvent e) {
final T value = getSelectedValue();
e.getPresentation().setEnabled(value != null &&
hasChildren(value) &&
getSecondComponent() instanceof FinderRecursivePanel);
}
@Override
public void actionPerformed(AnActionEvent e) {
FinderRecursivePanel finderRecursivePanel = (FinderRecursivePanel)getSecondComponent();
finderRecursivePanel.handleGotoNext();
}
};
nextPanelAction.registerCustomShortcutSet(KeyEvent.VK_RIGHT, 0, list);
AnAction editAction = new AnAction("Edit", null, AllIcons.Actions.Edit) {
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(isEditable());
}
@Override
public void actionPerformed(AnActionEvent e) {
performEditAction();
}
};
editAction.registerCustomShortcutSet(CommonShortcuts.ENTER, list);
AnAction[] actions = new AnAction[]{
previousPanelAction,
nextPanelAction,
Separator.getInstance(),
editAction};
final AnAction[] customActions = getCustomListActions();
if (customActions.length > 0) {
actions = ArrayUtil.append(actions, Separator.getInstance());
actions = ArrayUtil.mergeArrays(actions, customActions);
}
ActionGroup contextActionGroup = new DefaultActionGroup(actions);
PopupHandler.installUnknownPopupHandler(list, contextActionGroup, ActionManager.getInstance());
}
protected AnAction[] getCustomListActions() {
return AnAction.EMPTY_ARRAY;
}
private void installSpeedSearch(JBList list) {
final ListSpeedSearch search = new ListSpeedSearch(list, new Function<Object, String>() {
@Override
public String fun(Object o) {
//noinspection unchecked
return getItemText((T)o);
}
});
search.setComparator(new SpeedSearchComparator(false));
}
private void installEditOnDoubleClick(JBList list) {
new DoubleClickListener() {
@Override
protected boolean onDoubleClick(MouseEvent event) {
performEditAction();
return true;
}
}.installOn(list);
}
protected boolean performEditAction() {
Navigatable data = CommonDataKeys.NAVIGATABLE.getData(DataManager.getInstance().getDataContext(myList));
if (data != null && data.canNavigate()) {
data.navigate(true);
}
return false;
}
protected ListCellRenderer createListCellRenderer() {
return new ColoredListCellRenderer() {
private final FileColorManager myFileColorManager = FileColorManager.getInstance(getProject());
public Component getListCellRendererComponent(JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
mySelected = isSelected;
myForeground = UIUtil.getTreeTextForeground();
mySelectionForeground = cellHasFocus ? list.getSelectionForeground() : UIUtil.getTreeTextForeground();
clear();
setFont(UIUtil.getListFont());
//noinspection unchecked
final T t = (T)value;
setIcon(getItemIcon(t));
append(getItemText(t));
doCustomizeCellRenderer(this, list, t, index, isSelected, cellHasFocus);
Color bg = isSelected ? UIUtil.getTreeSelectionBackground(cellHasFocus) : UIUtil.getTreeTextBackground();
if (!isSelected && myFileColorManager.isEnabled()) {
final Color fileBgColor = myFileColorManager.getRendererBackground(getContainingFile(t));
bg = fileBgColor == null ? bg : fileBgColor;
}
setBackground(bg);
if (hasChildren(t)) {
JPanel result = new JPanel(new BorderLayout(0, 0));
JLabel childrenLabel = new JLabel();
childrenLabel.setOpaque(true);
childrenLabel.setVisible(true);
childrenLabel.setBackground(bg);
final boolean isDark = ColorUtil.isDark(UIUtil.getListSelectionBackground());
childrenLabel.setIcon(isSelected ? isDark ? AllIcons.Icons.Ide.NextStepInverted
: AllIcons.Icons.Ide.NextStep
: AllIcons.Icons.Ide.NextStepGrayed);
result.add(this, BorderLayout.CENTER);
result.add(childrenLabel, BorderLayout.EAST);
return result;
}
return this;
}
@Override
protected final void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) {
}
};
}
protected void doCustomizeCellRenderer(SimpleColoredComponent comp, JList list, T value, int index, boolean selected, boolean hasFocus) {
}
@Nullable
@Override
public Object getData(@NonNls String dataId) {
Object selectedValue = getSelectedValue();
if (selectedValue == null) return null;
if (CommonDataKeys.PSI_ELEMENT.is(dataId) && selectedValue instanceof PsiElement) {
return selectedValue;
}
if (CommonDataKeys.NAVIGATABLE.is(dataId) && selectedValue instanceof Navigatable) {
return selectedValue;
}
if (selectedValue instanceof DataProvider) {
return ((DataProvider)selectedValue).getData(dataId);
}
if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
return myCopyProvider;
}
return null;
}
@Override
public void dispose() {
super.dispose();
myMergingUpdateQueue.cancelAllUpdates();
}
@SuppressWarnings("unchecked")
@Nullable
public T getSelectedValue() {
return (T)myList.getSelectedValue();
}
/**
* Performs recursive update selecting given values.
*
* @param pathToSelect Values to select.
* @since 14
*/
public void updateSelectedPath(Object... pathToSelect) {
if (!myUpdateSelectedPathModeActive.compareAndSet(false, true)) return;
FinderRecursivePanel panel = this;
for (int i = 0; i < pathToSelect.length; i++) {
Object selectedValue = pathToSelect[i];
panel.setSelectedValue(selectedValue);
if (i < pathToSelect.length - 1) {
panel = (FinderRecursivePanel)panel.getSecondComponent();
assert panel != null : Arrays.toString(pathToSelect);
}
}
IdeFocusManager.getInstance(myProject).requestFocus(panel.myList, true);
myUpdateSelectedPathModeActive.set(false);
}
private void setSelectedValue(final Object value) {
if (value.equals(myList.getSelectedValue())) {
return;
}
// load list items synchronously
myList.setPaintBusy(true);
try {
final List<T> listItems = ApplicationManager.getApplication().runReadAction(new Computable<List<T>>() {
@Override
public List<T> compute() {
return getListItems();
}
});
mergeListItems(myListModel, listItems);
}
finally {
myList.setPaintBusy(false);
}
myList.setSelectedValue(value, true);
// always recreate since instance might depend on this one's selected value
createRightComponent(false);
}
@NotNull
public Project getProject() {
return myProject;
}
@Nullable
public FinderRecursivePanel getParentPanel() {
return myParent;
}
@Nullable
protected String getGroupId() {
return myGroupId;
}
public void updatePanel() {
if (myUpdateSelectedPathModeActive.get()) {
return;
}
myList.setPaintBusy(true);
myMergingUpdateQueue.queue(new Update("update") {
@Override
public void run() {
final T oldValue = getSelectedValue();
final int oldIndex = myList.getSelectedIndex();
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
DumbService.getInstance(getProject()).runReadActionInSmartMode(new Runnable() {
@Override
public void run() {
try {
final List<T> listItems = getListItems();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
mergeListItems(myListModel, listItems);
if (myList.getSelectedIndex() < 0) {
myList.setSelectedIndex(myListModel.getSize() > oldIndex ? oldIndex : 0);
}
else {
Object newValue = myList.getSelectedValue();
updateRightComponent(oldValue == null || oldValue != newValue);
}
}
});
}
finally {
myList.setPaintBusy(false);
}
}
});
}
});
}
});
}
public static <T> void mergeListItems(@NotNull CollectionListModel<T> listModel, @NotNull List<T> newItems) {
// remove items
for (int i = listModel.getSize() - 1; i >= 0; i--) {
if (!newItems.contains(listModel.getElementAt(i))) {
listModel.remove(i);
}
}
// add items
for (int i = 0; i < newItems.size(); i++) {
T newItem = newItems.get(i);
if (i < listModel.getSize()) {
if (!listModel.getElementAt(i).equals(newItem)) {
listModel.add(i, newItem);
}
}
else {
listModel.add(newItem);
}
}
}
public void updateRightComponent(boolean force) {
if (force) {
createRightComponent(true);
}
else if (myChild instanceof FinderRecursivePanel) {
((FinderRecursivePanel)myChild).updatePanel();
}
}
private void createRightComponent(boolean withUpdatePanel) {
T value = getSelectedValue();
if (value != null) {
if (myChild instanceof Disposable) {
Disposer.dispose((Disposable)myChild);
}
myChild = createRightComponent(value);
if (myChild instanceof FinderRecursivePanel) {
final FinderRecursivePanel childPanel = (FinderRecursivePanel)myChild;
if (withUpdatePanel) {
childPanel.init();
} else {
childPanel.initWithoutUpdatePanel();
}
}
setSecondComponent(myChild);
}
}
private int getIndex() {
int index = 0;
FinderRecursivePanel parent = myParent;
while (parent != null) {
index++;
parent = parent.getParentPanel();
}
return index;
}
protected boolean isRootPanel() {
return getParentPanel() == null;
}
@Override
public void doLayout() {
if (myProportion == 0) {
int total = getOrientation() ? getHeight() : getWidth();
float proportion = (float)getFirstComponentPreferredSize() / (total - getDividerWidth());
if (proportion > .0f && proportion < 1.0f) {
setProportion(proportion);
}
}
super.doLayout();
}
protected int getFirstComponentPreferredSize() {
return 200;
}
}