/* * Copyright 2000-2016 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.dvcs.ui; import com.intellij.ide.DataManager; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.impl.SimpleDataContext; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.ListPopupStep; import com.intellij.openapi.ui.popup.PopupStep; import com.intellij.openapi.util.Condition; import com.intellij.openapi.vcs.ui.FlatSpeedSearchPopup; import com.intellij.ui.ErrorLabel; import com.intellij.ui.JBColor; import com.intellij.ui.ScrollingUtil; import com.intellij.ui.SeparatorWithText; import com.intellij.ui.components.panels.OpaquePanel; import com.intellij.ui.popup.KeepingPopupOpenAction; import com.intellij.ui.popup.PopupFactoryImpl; import com.intellij.ui.popup.WizardPopup; import com.intellij.ui.popup.list.IconListPopupRenderer; import com.intellij.ui.popup.list.ListPopupImpl; import com.intellij.ui.popup.list.ListPopupModel; import com.intellij.ui.popup.list.PopupListElementRenderer; import com.intellij.util.ObjectUtils; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.util.List; import static com.intellij.util.ui.UIUtil.DEFAULT_HGAP; import static com.intellij.util.ui.UIUtil.DEFAULT_VGAP; public class BranchActionGroupPopup extends FlatSpeedSearchPopup { private static final DataKey<ListPopupModel> POPUP_MODEL = DataKey.create("VcsPopupModel"); private MyPopupListElementRenderer myListElementRenderer; public BranchActionGroupPopup(@NotNull String title, @NotNull Project project, @NotNull Condition<AnAction> preselectActionCondition, @NotNull ActionGroup actions) { super(title, new DefaultActionGroup(actions, createBranchSpeedSearchActionGroup(actions)), SimpleDataContext.getProjectContext(project), preselectActionCondition, true); DataManager.registerDataProvider(getList(), dataId -> POPUP_MODEL.is(dataId) ? getListModel() : null); installOnHoverIconsSupport(getListElementRenderer()); } //for child popups only private BranchActionGroupPopup(@Nullable WizardPopup aParent, @NotNull ListPopupStep aStep, @Nullable Object parentValue) { super(aParent, aStep, DataContext.EMPTY_CONTEXT, parentValue); DataManager.registerDataProvider(getList(), dataId -> POPUP_MODEL.is(dataId) ? getListModel() : null); installOnHoverIconsSupport(getListElementRenderer()); } @NotNull public static ActionGroup createBranchSpeedSearchActionGroup(@NotNull ActionGroup actionGroup) { DefaultActionGroup speedSearchActions = new DefaultActionGroup(); createSpeedSearchActions(actionGroup, speedSearchActions, true); return speedSearchActions; } @Override protected boolean isResizable() { return true; } private static void createSpeedSearchActions(@NotNull ActionGroup actionGroup, @NotNull DefaultActionGroup speedSearchActions, boolean isFirstLevel) { // add per repository branches into the model as Speed Search elements and show them only if regular items were not found by mask; if (!isFirstLevel) speedSearchActions.addSeparator(actionGroup.getTemplatePresentation().getText()); for (AnAction child : actionGroup.getChildren(null)) { if (child instanceof ActionGroup) { ActionGroup childGroup = (ActionGroup)child; if (isFirstLevel) { createSpeedSearchActions(childGroup, speedSearchActions, false); } else if (childGroup instanceof BranchActionGroup) { speedSearchActions.add(createSpeedSearchActionGroupWrapper(childGroup)); } else if (childGroup instanceof HideableActionGroup) { speedSearchActions.add(createSpeedSearchActionGroupWrapper(((HideableActionGroup)childGroup).getDelegate())); } } } } @Override public void handleSelect(boolean handleFinalChoices) { super.handleSelect(handleFinalChoices, null); if (getSpecificAction(getList().getSelectedValue(), MoreAction.class) != null) { getListModel().refilter(); } } @Override public void handleSelect(boolean handleFinalChoices, InputEvent e) { BranchActionGroup branchActionGroup = getSelectedBranchGroup(); if (branchActionGroup != null && e instanceof MouseEvent && myListElementRenderer.isIconAt(((MouseEvent)e).getPoint())) { branchActionGroup.toggle(); getList().repaint(); } else { super.handleSelect(handleFinalChoices, e); } } @Override protected void handleToggleAction() { BranchActionGroup branchActionGroup = getSelectedBranchGroup(); if (branchActionGroup != null) { branchActionGroup.toggle(); getList().repaint(); } else { super.handleToggleAction(); } } @Nullable private BranchActionGroup getSelectedBranchGroup() { return getSpecificAction(getList().getSelectedValue(), BranchActionGroup.class); } @Override protected void onSpeedSearchPatternChanged() { super.onSpeedSearchPatternChanged(); ScrollingUtil.ensureSelectionExists(getList()); } protected boolean shouldBeShowing(@NotNull AnAction action) { if (!super.shouldBeShowing(action)) return false; if (getSpeedSearch().isHoldingFilter()) return !(action instanceof MoreAction); if (action instanceof MoreAction) return !((MoreAction)action).myIsExpanded; if (action instanceof MoreHideableActionGroup) return ((MoreHideableActionGroup)action).shouldBeShown(); return true; } @Override protected WizardPopup createPopup(WizardPopup parent, PopupStep step, Object parentValue) { WizardPopup popup = createListPopupStep(parent, step, parentValue); RootAction rootAction = getRootAction(parentValue); if (rootAction != null) { popup.setAdText((rootAction).getCaption()); } return popup; } private WizardPopup createListPopupStep(WizardPopup parent, PopupStep step, Object parentValue) { if (step instanceof ListPopupStep) { return new BranchActionGroupPopup(parent, (ListPopupStep)step, parentValue); } return super.createPopup(parent, step, parentValue); } @Nullable private static RootAction getRootAction(Object value) { return getSpecificAction(value, RootAction.class); } private static <T> T getSpecificAction(Object value, @NotNull Class<T> clazz) { if (value instanceof PopupFactoryImpl.ActionItem) { AnAction action = ((PopupFactoryImpl.ActionItem)value).getAction(); if (clazz.isInstance(action)) { return clazz.cast(action); } else if (action instanceof EmptyAction.MyDelegatingActionGroup) { ActionGroup group = ((EmptyAction.MyDelegatingActionGroup)action).getDelegate(); return clazz.isInstance(group) ? clazz.cast(group) : null; } } return null; } @Override protected MyPopupListElementRenderer getListElementRenderer() { if (myListElementRenderer == null) { myListElementRenderer = new MyPopupListElementRenderer(this); } return myListElementRenderer; } private class MyPopupListElementRenderer extends PopupListElementRenderer<Object> implements IconListPopupRenderer { private ErrorLabel myInfoLabel; private IconComponent myIconLabel; public MyPopupListElementRenderer(ListPopupImpl aPopup) { super(aPopup); } @Override protected SeparatorWithText createSeparator() { return new MyTextSeparator(); } @Override public boolean isIconAt(@NotNull Point point) { JList list = getList(); int index = getList().locationToIndex(point); Rectangle bounds = getList().getCellBounds(index, index); Component renderer = getListCellRendererComponent(list, list.getSelectedValue(), index, true, true); renderer.setBounds(bounds); renderer.doLayout(); point.translate(-bounds.x, -bounds.y); return SwingUtilities.getDeepestComponentAt(renderer, point.x, point.y) instanceof IconComponent; } @Override protected void customizeComponent(JList list, Object value, boolean isSelected) { MoreAction more = getSpecificAction(value, MoreAction.class); if (more != null) { myTextLabel.setForeground(JBColor.gray); } super.customizeComponent(list, value, isSelected); myTextLabel.setIcon(null); myTextLabel.setDisabledIcon(null); myIconLabel.setIcon(myDescriptor.getIconFor(value)); PopupElementWithAdditionalInfo additionalInfoAction = getSpecificAction(value, PopupElementWithAdditionalInfo.class); String infoText = additionalInfoAction != null ? additionalInfoAction.getInfoText() : null; if (infoText != null) { myInfoLabel.setVisible(true); myInfoLabel.setText(infoText); if (isSelected) { setSelected(myInfoLabel); } else { myInfoLabel.setBackground(getBackground()); myInfoLabel.setForeground(JBColor.GRAY); // different foreground than for other elements } } else { myInfoLabel.setVisible(false); } } @Override protected JComponent createItemComponent() { myTextLabel = new ErrorLabel(); myTextLabel.setOpaque(true); myTextLabel.setBorder(JBUI.Borders.empty(1)); myInfoLabel = new ErrorLabel(); myInfoLabel.setOpaque(true); myInfoLabel.setBorder(JBUI.Borders.empty(1, DEFAULT_HGAP, 1, 1)); JPanel compoundPanel = new OpaquePanel(new BorderLayout(), JBColor.WHITE); myIconLabel = new IconComponent(); myInfoLabel.setHorizontalAlignment(SwingConstants.RIGHT); JPanel textPanel = new OpaquePanel(new BorderLayout(), compoundPanel.getBackground()); compoundPanel.add(myIconLabel, BorderLayout.WEST); textPanel.add(myTextLabel, BorderLayout.WEST); textPanel.add(myInfoLabel, BorderLayout.CENTER); compoundPanel.add(textPanel, BorderLayout.CENTER); return layoutComponent(compoundPanel); } private class IconComponent extends JLabel { } } private static class MyTextSeparator extends SeparatorWithText { public MyTextSeparator() { super(); setTextForeground(JBColor.BLACK); setCaptionCentered(false); UIUtil.addInsets(this, DEFAULT_VGAP, UIUtil.getListCellHPadding(), 0, 0); } @Override protected void paintLine(Graphics g, int x, int y, int width) { } } private static class MoreAction extends DumbAwareAction implements KeepingPopupOpenAction { private boolean myIsExpanded = false; public MoreAction(int numberOfHiddenNodes) { super(); assert numberOfHiddenNodes > 0; getTemplatePresentation().setText(numberOfHiddenNodes + " more..."); } @Override public void actionPerformed(AnActionEvent e) { myIsExpanded = true; InputEvent event = e.getInputEvent(); if (event != null && event.getSource() instanceof JComponent) { DataProvider dataProvider = DataManager.getDataProvider((JComponent)event.getSource()); if (dataProvider != null) { ObjectUtils.assertNotNull(POPUP_MODEL.getData(dataProvider)).refilter(); } } } } interface MoreHideableActionGroup { boolean shouldBeShown(); } private static class HideableActionGroup extends EmptyAction.MyDelegatingActionGroup implements MoreHideableActionGroup { @NotNull private final MoreAction myMoreAction; private HideableActionGroup(@NotNull ActionGroup actionGroup, @NotNull MoreAction moreAction) { super(actionGroup); myMoreAction = moreAction; } @Override public boolean shouldBeShown() { return myMoreAction.myIsExpanded; } } public static void wrapWithMoreActionIfNeeded(@NotNull DefaultActionGroup parentGroup, @NotNull List<? extends ActionGroup> actionList, int maxIndex) { if (actionList.size() > maxIndex) { MoreAction moreAction = new MoreAction(actionList.size() - maxIndex); for (int i = 0; i < actionList.size(); i++) { parentGroup.add(i < maxIndex ? actionList.get(i) : new HideableActionGroup(actionList.get(i), moreAction)); } parentGroup.add(moreAction); } else { parentGroup.addAll(actionList); } } }