blob: 03a5a7fd806a10684dc93b5e9dd1f36d830a2484 [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.ide.commander;
import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.ide.CopyPasteDelegator;
import com.intellij.ide.DeleteProvider;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.IdeView;
import com.intellij.ide.projectView.ProjectViewNode;
import com.intellij.ide.projectView.impl.ModuleGroup;
import com.intellij.ide.projectView.impl.ProjectAbstractTreeStructureBase;
import com.intellij.ide.projectView.impl.nodes.LibraryGroupElement;
import com.intellij.ide.projectView.impl.nodes.NamedLibraryElement;
import com.intellij.ide.structureView.StructureViewTreeElement;
import com.intellij.ide.ui.customization.CustomActionsSchema;
import com.intellij.ide.util.DeleteHandler;
import com.intellij.ide.util.DirectoryChooserUtil;
import com.intellij.ide.util.EditSourceUtil;
import com.intellij.ide.util.EditorHelper;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.*;
import com.intellij.ui.components.JBList;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
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.BevelBorder;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Eugene Belyaev
*/
public class CommanderPanel extends JPanel {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.commander.CommanderPanel");
private static final Color DARK_BLUE = new Color(55, 85, 134);
private static final Color DARK_BLUE_BRIGHTER = new Color(58, 92, 149);
private static final Color DARK_BLUE_DARKER = new Color(38, 64, 106);
private Project myProject;
private AbstractListBuilder myBuilder;
private JPanel myTitlePanel;
private JLabel myParentTitle;
protected final JBList myList;
private final MyModel myModel;
private CopyPasteDelegator myCopyPasteDelegator;
protected final ListSpeedSearch myListSpeedSearch;
private final IdeView myIdeView = new MyIdeView();
private final MyDeleteElementProvider myDeleteElementProvider = new MyDeleteElementProvider();
@NonNls
private static final String ACTION_DRILL_DOWN = "DrillDown";
@NonNls
private static final String ACTION_GO_UP = "GoUp";
private ProjectAbstractTreeStructureBase myProjectTreeStructure;
private boolean myActive = true;
private final List<CommanderHistoryListener> myHistoryListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private boolean myMoveFocus = false;
private final boolean myEnableSearchHighlighting;
public CommanderPanel(final Project project, final boolean enablePopupMenu, final boolean enableSearchHighlighting) {
super(new BorderLayout());
myProject = project;
myEnableSearchHighlighting = enableSearchHighlighting;
myModel = new MyModel();
myList = new JBList(myModel);
myList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
if (enablePopupMenu) {
myCopyPasteDelegator = new CopyPasteDelegator(myProject, myList) {
@Override
@NotNull
protected PsiElement[] getSelectedElements() {
return CommanderPanel.this.getSelectedElements();
}
};
}
myListSpeedSearch = new ListSpeedSearch(myList);
myListSpeedSearch.setClearSearchOnNavigateNoMatch(true);
ListScrollingUtil.installActions(myList);
myList.registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (myBuilder == null) return;
myBuilder.buildRoot();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SLASH, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
JComponent.WHEN_FOCUSED);
myList.getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ACTION_DRILL_DOWN);
myList.getInputMap(WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK),
ACTION_DRILL_DOWN);
myList.getActionMap().put(ACTION_DRILL_DOWN, new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
drillDown();
}
});
new DoubleClickListener() {
@Override
protected boolean onDoubleClick(MouseEvent e) {
drillDown();
return true;
}
}.installOn(myList);
myList.getInputMap(WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK), ACTION_GO_UP);
myList.getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), ACTION_GO_UP);
myList.getActionMap().put(ACTION_GO_UP, new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
goUp();
}
});
myList.getActionMap().put("selectAll", new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
}
});
if (enablePopupMenu) {
myList.addMouseListener(new PopupHandler() {
@Override
public void invokePopup(final Component comp, final int x, final int y) {
CommanderPanel.this.invokePopup(comp, x, y);
}
});
}
myList.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(final FocusEvent e) {
setActive(true);
}
@Override
public void focusLost(final FocusEvent e) {
setActive(false);
}
});
}
public boolean isEnableSearchHighlighting() {
return myEnableSearchHighlighting;
}
public void addHistoryListener(@NotNull CommanderHistoryListener listener) {
myHistoryListeners.add(listener);
}
private void removeHistoryListener(CommanderHistoryListener listener) {
myHistoryListeners.remove(listener);
}
private void updateHistory(boolean elementExpanded) {
for (CommanderHistoryListener listener : myHistoryListeners) {
listener.historyChanged(getSelectedElement(), elementExpanded);
}
}
public final JList getList() {
return myList;
}
public final AbstractListBuilder.Model getModel() {
return myModel;
}
public void setMoveFocus(final boolean moveFocus) {
myMoveFocus = moveFocus;
}
public void goUp() {
if (myBuilder == null) {
return;
}
updateHistory(true);
myBuilder.goUp();
updateHistory(false);
}
public void drillDown() {
if (topElementIsSelected()) {
goUp();
return;
}
if (getSelectedValue() == null) {
return;
}
final AbstractTreeNode element = getSelectedNode();
if (element.getChildren().isEmpty()) {
if (!shouldDrillDownOnEmptyElement(element)) {
navigateSelectedElement();
return;
}
}
if (myBuilder == null) {
return;
}
updateHistory(false);
myBuilder.drillDown();
updateHistory(true);
}
public boolean navigateSelectedElement() {
final AbstractTreeNode selectedNode = getSelectedNode();
if (selectedNode != null) {
if (selectedNode.canNavigateToSource()) {
selectedNode.navigate(true);
return true;
}
}
return false;
}
protected boolean shouldDrillDownOnEmptyElement(final AbstractTreeNode node) {
return node instanceof ProjectViewNode && ((ProjectViewNode)node).shouldDrillDownOnEmptyElement();
}
private boolean topElementIsSelected() {
int[] selectedIndices = myList.getSelectedIndices();
return selectedIndices.length == 1 && selectedIndices[0] == 0 && myModel.getElementAt(selectedIndices[0]) instanceof TopLevelNode;
}
public final void setBuilder(final AbstractListBuilder builder) {
myBuilder = builder;
removeAll();
myTitlePanel = new JPanel(new BorderLayout());
myTitlePanel.setBackground(UIUtil.getControlColor());
myTitlePanel.setOpaque(true);
myParentTitle = new MyTitleLabel(myTitlePanel);
myParentTitle.setText(" ");
myParentTitle.setFont(UIUtil.getLabelFont().deriveFont(Font.BOLD));
myParentTitle.setForeground(JBColor.foreground());
myParentTitle.setUI(new RightAlignedLabelUI());
final JPanel panel1 = new JPanel(new BorderLayout());
panel1.setOpaque(false);
panel1.add(Box.createHorizontalStrut(10), BorderLayout.WEST);
panel1.add(myParentTitle, BorderLayout.CENTER);
myTitlePanel.add(panel1, BorderLayout.CENTER);
add(myTitlePanel, BorderLayout.NORTH);
final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myList);
scrollPane.setBorder(null);
scrollPane.getVerticalScrollBar().setFocusable(false); // otherwise the scrollbar steals focus and panel switching with tab is broken
scrollPane.getHorizontalScrollBar().setFocusable(false);
add(scrollPane, BorderLayout.CENTER);
myBuilder.setParentTitle(myParentTitle);
// TODO[vova,anton] it seems that the code below performs double focus request. Is it OK?
myTitlePanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(final MouseEvent e) {
myList.requestFocus();
}
@Override
public void mousePressed(final MouseEvent e) {
myList.requestFocus();
}
});
}
public final AbstractListBuilder getBuilder() {
return myBuilder;
}
public final PsiElement getSelectedElement() {
Object value = getValueAtIndex(getSelectedNode());
return (PsiElement)(value instanceof PsiElement ? value : null);
}
public final PsiElement getSelectedElement(int index) {
Object elementAtIndex = myModel.getElementAt(index);
Object value = getValueAtIndex(elementAtIndex instanceof AbstractTreeNode ? (AbstractTreeNode)elementAtIndex : null);
return (PsiElement)(value instanceof PsiElement ? value : null);
}
public AbstractTreeNode getSelectedNode() {
if (myBuilder == null) return null;
final int[] indices = myList.getSelectedIndices();
if (indices.length != 1) return null;
int index = indices[0];
if (index >= myModel.getSize()) return null;
Object elementAtIndex = myModel.getElementAt(index);
return elementAtIndex instanceof AbstractTreeNode ? (AbstractTreeNode)elementAtIndex : null;
}
@NotNull
private List<AbstractTreeNode> getSelectedNodes() {
if (myBuilder == null) return Collections.emptyList();
final int[] indices = myList.getSelectedIndices();
ArrayList<AbstractTreeNode> result = new ArrayList<AbstractTreeNode>();
for (int index : indices) {
if (index >= myModel.getSize()) continue;
Object elementAtIndex = myModel.getElementAt(index);
AbstractTreeNode node = elementAtIndex instanceof AbstractTreeNode ? (AbstractTreeNode)elementAtIndex : null;
if (node != null) {
result.add(node);
}
}
return result;
}
public Object getSelectedValue() {
return getValueAtIndex(getSelectedNode());
}
private PsiElement[] getSelectedElements() {
if (myBuilder == null) return PsiElement.EMPTY_ARRAY;
final int[] indices = myList.getSelectedIndices();
final ArrayList<PsiElement> elements = new ArrayList<PsiElement>();
for (int index : indices) {
final PsiElement element = getSelectedElement(index);
if (element != null) {
elements.add(element);
}
}
return PsiUtilCore.toPsiElementArray(elements);
}
private static Object getValueAtIndex(AbstractTreeNode node) {
if (node == null) return null;
Object value = node.getValue();
if (value instanceof StructureViewTreeElement) {
return ((StructureViewTreeElement)value).getValue();
}
return value;
}
public final void setActive(final boolean active) {
myActive = active;
if (active) {
myTitlePanel.setBackground(DARK_BLUE);
myTitlePanel.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED, DARK_BLUE_BRIGHTER, DARK_BLUE_DARKER));
myParentTitle.setForeground(Color.white);
}
else {
final Color color = UIUtil.getPanelBackground();
LOG.assertTrue(color != null);
myTitlePanel.setBackground(color);
myTitlePanel.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED, color.brighter(), color.darker()));
myParentTitle.setForeground(JBColor.foreground());
}
final int[] selectedIndices = myList.getSelectedIndices();
if (selectedIndices.length == 0 && myList.getModel().getSize() > 0) {
myList.setSelectedIndex(0);
if (!myList.hasFocus()) {
myList.requestFocus();
}
}
else if (myList.getModel().getSize() > 0) {
// need this to generate SelectionChanged events so that listeners, added by Commander, will be notified
myList.setSelectedIndices(selectedIndices);
}
}
public boolean isActive() {
return myActive;
}
private void invokePopup(final Component c, final int x, final int y) {
if (myBuilder == null) return;
if (myList.getSelectedIndices().length <= 1) {
final int popupIndex = myList.locationToIndex(new Point(x, y));
if (popupIndex >= 0) {
myList.setSelectedIndex(popupIndex);
myList.requestFocus();
}
}
final ActionGroup group = (ActionGroup)CustomActionsSchema.getInstance().getCorrectedAction(IdeActions.GROUP_COMMANDER_POPUP);
final ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.COMMANDER_POPUP, group);
popupMenu.getComponent().show(c, x, y);
}
public final void dispose() {
if (myBuilder != null) {
myBuilder.dispose();
myBuilder = null;
}
myProject = null;
}
public final void setTitlePanelVisible(final boolean flag) {
myTitlePanel.setVisible(flag);
}
public final Object getDataImpl(final String dataId) {
if (myBuilder == null) return null;
final Object selectedValue = getSelectedValue();
if (CommonDataKeys.PSI_ELEMENT.is(dataId)) {
final PsiElement selectedElement = getSelectedElement();
return selectedElement != null && selectedElement.isValid() ? selectedElement : null;
}
if (LangDataKeys.PSI_ELEMENT_ARRAY.is(dataId)) {
return filterInvalidElements(getSelectedElements());
}
if (LangDataKeys.PASTE_TARGET_PSI_ELEMENT.is(dataId)) {
final AbstractTreeNode parentNode = myBuilder.getParentNode();
final Object element = parentNode != null ? parentNode.getValue() : null;
return element instanceof PsiElement && ((PsiElement)element).isValid() ? element : null;
}
if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
return getNavigatables();
}
if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
return myCopyPasteDelegator != null ? myCopyPasteDelegator.getCopyProvider() : null;
}
if (PlatformDataKeys.CUT_PROVIDER.is(dataId)) {
return myCopyPasteDelegator != null ? myCopyPasteDelegator.getCutProvider() : null;
}
if (PlatformDataKeys.PASTE_PROVIDER.is(dataId)) {
return myCopyPasteDelegator != null ? myCopyPasteDelegator.getPasteProvider() : null;
}
if (LangDataKeys.IDE_VIEW.is(dataId)) {
return myIdeView;
}
if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
return myDeleteElementProvider;
}
if (LangDataKeys.MODULE.is(dataId)) {
return selectedValue instanceof Module ? selectedValue : null;
}
if (ModuleGroup.ARRAY_DATA_KEY.is(dataId)) {
return selectedValue instanceof ModuleGroup ? new ModuleGroup[]{(ModuleGroup)selectedValue} : null;
}
if (LibraryGroupElement.ARRAY_DATA_KEY.is(dataId)) {
return selectedValue instanceof LibraryGroupElement ? new LibraryGroupElement[]{(LibraryGroupElement)selectedValue} : null;
}
if (NamedLibraryElement.ARRAY_DATA_KEY.is(dataId)) {
return selectedValue instanceof NamedLibraryElement ? new NamedLibraryElement[]{(NamedLibraryElement)selectedValue} : null;
}
if (myProjectTreeStructure != null) {
return myProjectTreeStructure.getDataFromProviders(getSelectedNodes(), dataId);
}
return null;
}
private Navigatable[] getNavigatables() {
if (myBuilder == null) return null;
final int[] indices = myList.getSelectedIndices();
if (indices == null || indices.length == 0) return null;
final ArrayList<Navigatable> elements = new ArrayList<Navigatable>();
for (int index : indices) {
final Object element = myModel.getElementAt(index);
if (element instanceof AbstractTreeNode) {
elements.add((Navigatable)element);
}
}
return elements.toArray(new Navigatable[elements.size()]);
}
@Nullable
private static PsiElement[] filterInvalidElements(final PsiElement[] elements) {
if (elements == null || elements.length == 0) {
return null;
}
final List<PsiElement> validElements = new ArrayList<PsiElement>(elements.length);
for (final PsiElement element : elements) {
if (element.isValid()) {
validElements.add(element);
}
}
return validElements.size() == elements.length ? elements : PsiUtilCore.toPsiElementArray(validElements);
}
protected final Navigatable createEditSourceDescriptor() {
return EditSourceUtil.getDescriptor(getSelectedElement());
}
public void setProjectTreeStructure(final ProjectAbstractTreeStructureBase projectTreeStructure) {
myProjectTreeStructure = projectTreeStructure;
}
private static final class MyTitleLabel extends JLabel {
private final JPanel myPanel;
public MyTitleLabel(final JPanel panel) {
myPanel = panel;
}
@Override
public void setText(String text) {
if (text == null || text.isEmpty()) {
text = " ";
}
super.setText(text);
if (myPanel != null) {
myPanel.setToolTipText(text.trim().isEmpty() ? null : text);
}
}
}
private final class MyDeleteElementProvider implements DeleteProvider {
@Override
public void deleteElement(@NotNull final DataContext dataContext) {
LocalHistoryAction a = LocalHistory.getInstance().startAction(IdeBundle.message("progress.deleting"));
try {
final PsiElement[] elements = getSelectedElements();
DeleteHandler.deletePsiElement(elements, myProject);
}
finally {
a.finish();
}
}
@Override
public boolean canDeleteElement(@NotNull final DataContext dataContext) {
final PsiElement[] elements = getSelectedElements();
return DeleteHandler.shouldEnableDeleteAction(elements);
}
}
private final class MyIdeView implements IdeView {
@Override
public void selectElement(final PsiElement element) {
final boolean isDirectory = element instanceof PsiDirectory;
if (!isDirectory) {
EditorHelper.openInEditor(element);
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
myBuilder.selectElement(element, PsiUtilCore.getVirtualFile(element));
if (!isDirectory) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (myMoveFocus) {
ToolWindowManager.getInstance(myProject).activateEditorComponent();
}
}
});
}
}
}, ModalityState.NON_MODAL);
}
private PsiDirectory getDirectory() {
if (myBuilder == null) return null;
final Object parentElement = myBuilder.getParentNode();
if (parentElement instanceof AbstractTreeNode) {
final AbstractTreeNode parentNode = (AbstractTreeNode)parentElement;
if (!(parentNode.getValue() instanceof PsiDirectory)) return null;
return (PsiDirectory)parentNode.getValue();
}
else {
return null;
}
}
@NotNull
@Override
public PsiDirectory[] getDirectories() {
PsiDirectory directory = getDirectory();
return directory == null ? PsiDirectory.EMPTY_ARRAY : new PsiDirectory[]{directory};
}
@Override
public PsiDirectory getOrChooseDirectory() {
return DirectoryChooserUtil.getOrChooseDirectory(this);
}
}
public static final class MyModel extends AbstractListModel implements AbstractListBuilder.Model {
final List myElements = new ArrayList();
@Override
public void removeAllElements() {
int index1 = myElements.size() - 1;
myElements.clear();
if (index1 >= 0) {
fireIntervalRemoved(this, 0, index1);
}
}
@Override
public void addElement(final Object obj) {
int index = myElements.size();
myElements.add(obj);
fireIntervalAdded(this, index, index);
}
@Override
public void replaceElements(final List newElements) {
removeAllElements();
myElements.addAll(newElements);
fireIntervalAdded(this, 0, newElements.size());
}
@Override
public Object[] toArray() {
return ArrayUtil.toObjectArray(myElements);
}
@Override
public int indexOf(final Object o) {
return myElements.indexOf(o);
}
@Override
public int getSize() {
return myElements.size();
}
@Override
public Object getElementAt(final int index) {
return myElements.get(index);
}
}
}