| /* |
| * 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.content.impl; |
| |
| import com.intellij.ide.DataManager; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.actionSystem.DataProvider; |
| import com.intellij.openapi.actionSystem.PlatformDataKeys; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.components.ServiceManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.wm.FocusCommand; |
| import com.intellij.openapi.wm.IdeFocusManager; |
| import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy; |
| import com.intellij.ui.components.panels.NonOpaquePanel; |
| import com.intellij.ui.components.panels.Wrapper; |
| import com.intellij.ui.content.*; |
| import com.intellij.ui.switcher.SwitchProvider; |
| import com.intellij.ui.switcher.SwitchTarget; |
| import com.intellij.util.SmartList; |
| 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 java.awt.*; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * @author Anton Katilin |
| * @author Vladimir Kondratyev |
| */ |
| public class ContentManagerImpl implements ContentManager, PropertyChangeListener, Disposable.Parent { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.ui.content.impl.ContentManagerImpl"); |
| |
| private ContentUI myUI; |
| private final List<Content> myContents = new ArrayList<Content>(); |
| private final List<ContentManagerListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final List<Content> mySelection = new ArrayList<Content>(); |
| private final boolean myCanCloseContents; |
| |
| private Wrapper.FocusHolder myFocusProxy; |
| private MyNonOpaquePanel myComponent; |
| |
| private final Set<Content> myContentWithChangedComponent = new HashSet<Content>(); |
| |
| private boolean myDisposed; |
| private final Project myProject; |
| |
| private final List<DataProvider> dataProviders = new SmartList<DataProvider>(); |
| |
| /** |
| * WARNING: as this class adds listener to the ProjectManager which is removed on projectClosed event, all instances of this class |
| * must be created on already OPENED projects, otherwise there will be memory leak! |
| */ |
| public ContentManagerImpl(@NotNull ContentUI contentUI, boolean canCloseContents, @NotNull Project project) { |
| myProject = project; |
| myCanCloseContents = canCloseContents; |
| myUI = contentUI; |
| myUI.setManager(this); |
| |
| Disposer.register(project, this); |
| Disposer.register(this, contentUI); |
| } |
| |
| @Override |
| public boolean canCloseContents() { |
| return myCanCloseContents; |
| } |
| |
| @NotNull |
| @Override |
| public JComponent getComponent() { |
| if (myComponent == null) { |
| myComponent = new MyNonOpaquePanel(); |
| |
| myFocusProxy = new Wrapper.FocusHolder(); |
| myFocusProxy.setOpaque(false); |
| myFocusProxy.setPreferredSize(new Dimension(0, 0)); |
| |
| MyContentComponent contentComponent = new MyContentComponent(); |
| contentComponent.setContent(myUI.getComponent()); |
| contentComponent.setFocusCycleRoot(true); |
| |
| myComponent.add(myFocusProxy, BorderLayout.NORTH); |
| myComponent.add(contentComponent, BorderLayout.CENTER); |
| } |
| return myComponent; |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback getReady(@NotNull Object requestor) { |
| Content selected = getSelectedContent(); |
| if (selected == null) return new ActionCallback.Done(); |
| BusyObject busyObject = selected.getBusyObject(); |
| return busyObject != null ? busyObject.getReady(requestor) : new ActionCallback.Done(); |
| } |
| |
| private class MyNonOpaquePanel extends NonOpaquePanel implements DataProvider { |
| public MyNonOpaquePanel() { |
| super(new BorderLayout()); |
| } |
| |
| @Override |
| @Nullable |
| public Object getData(@NonNls String dataId) { |
| if (PlatformDataKeys.CONTENT_MANAGER.is(dataId) || PlatformDataKeys.NONEMPTY_CONTENT_MANAGER.is(dataId) && getContentCount() > 1) { |
| return ContentManagerImpl.this; |
| } |
| |
| for (DataProvider dataProvider : dataProviders) { |
| Object data = dataProvider.getData(dataId); |
| if (data != null) { |
| return data; |
| } |
| } |
| |
| if (myUI instanceof DataProvider) { |
| return ((DataProvider)myUI).getData(dataId); |
| } |
| |
| DataProvider provider = DataManager.getDataProvider(this); |
| return provider == null ? null : provider.getData(dataId); |
| } |
| } |
| |
| private class MyContentComponent extends NonOpaquePanel implements SwitchProvider { |
| @Override |
| public List<SwitchTarget> getTargets(boolean onlyVisible, boolean originalProvider) { |
| if (myUI instanceof SwitchProvider) { |
| return ((SwitchProvider)myUI).getTargets(onlyVisible, false); |
| } |
| return new SmartList<SwitchTarget>(); |
| } |
| |
| @Override |
| public SwitchTarget getCurrentTarget() { |
| return myUI instanceof SwitchProvider ? ((SwitchProvider)myUI).getCurrentTarget() : null; |
| } |
| |
| @Override |
| public JComponent getComponent() { |
| return myUI instanceof SwitchProvider ? myUI.getComponent() : this; |
| } |
| |
| @Override |
| public boolean isCycleRoot() { |
| return myUI instanceof SwitchProvider && ((SwitchProvider)myUI).isCycleRoot(); |
| } |
| } |
| |
| @Override |
| public void addContent(@NotNull Content content, final int order) { |
| doAddContent(content, order); |
| } |
| |
| @Override |
| public void addContent(@NotNull Content content) { |
| doAddContent(content, -1); |
| } |
| |
| @Override |
| public void addContent(@NotNull final Content content, final Object constraints) { |
| doAddContent(content, -1); |
| } |
| |
| private void doAddContent(@NotNull final Content content, final int index) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| if (myContents.contains(content)) { |
| myContents.remove(content); |
| myContents.add(index == -1 ? myContents.size() : index, content); |
| return; |
| } |
| |
| ((ContentImpl)content).setManager(this); |
| final int insertIndex = index == -1 ? myContents.size() : index; |
| myContents.add(insertIndex, content); |
| content.addPropertyChangeListener(this); |
| fireContentAdded(content, insertIndex, ContentManagerEvent.ContentOperation.add); |
| if (myUI.isToSelectAddedContent() || mySelection.isEmpty() && !myUI.canBeEmptySelection()) { |
| if (myUI.isSingleSelection()) { |
| setSelectedContent(content); |
| } |
| else { |
| addSelectedContent(content); |
| } |
| } |
| |
| Disposer.register(this, content); |
| } |
| |
| @Override |
| public boolean removeContent(@NotNull Content content, final boolean dispose) { |
| return removeContent(content, true, dispose).isDone(); |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback removeContent(@NotNull Content content, boolean dispose, final boolean trackFocus, final boolean forcedFocus) { |
| final ActionCallback result = new ActionCallback(); |
| removeContent(content, true, dispose).doWhenDone(new Runnable() { |
| @Override |
| public void run() { |
| if (trackFocus) { |
| Content current = getSelectedContent(); |
| if (current != null) { |
| setSelectedContent(current, true, true, !forcedFocus); |
| } |
| else { |
| result.setDone(); |
| } |
| } |
| else { |
| result.setDone(); |
| } |
| } |
| }); |
| |
| return result; |
| } |
| |
| @NotNull |
| private ActionCallback removeContent(@NotNull Content content, boolean trackSelection, boolean dispose) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| int indexToBeRemoved = getIndexOfContent(content); |
| if (indexToBeRemoved == -1) return new ActionCallback.Rejected(); |
| |
| try { |
| Content selection = mySelection.isEmpty() ? null : mySelection.get(mySelection.size() - 1); |
| int selectedIndex = selection != null ? myContents.indexOf(selection) : -1; |
| |
| if (!fireContentRemoveQuery(content, indexToBeRemoved, ContentManagerEvent.ContentOperation.undefined)) { |
| return new ActionCallback.Rejected(); |
| } |
| if (!content.isValid()) { |
| return new ActionCallback.Rejected(); |
| } |
| |
| boolean wasSelected = isSelected(content); |
| if (wasSelected) { |
| removeFromSelection(content); |
| } |
| |
| int indexToSelect = -1; |
| if (wasSelected) { |
| int i = indexToBeRemoved - 1; |
| if (i >= 0) { |
| indexToSelect = i; |
| } |
| else if (getContentCount() > 1) { |
| indexToSelect = 0; |
| } |
| } |
| else if (selectedIndex > indexToBeRemoved) { |
| indexToSelect = selectedIndex - 1; |
| } |
| |
| myContents.remove(content); |
| content.removePropertyChangeListener(this); |
| |
| fireContentRemoved(content, indexToBeRemoved, ContentManagerEvent.ContentOperation.remove); |
| ((ContentImpl)content).setManager(null); |
| |
| |
| if (dispose) { |
| Disposer.dispose(content); |
| } |
| |
| int newSize = myContents.size(); |
| |
| ActionCallback result = new ActionCallback(); |
| |
| if (newSize > 0 && trackSelection) { |
| if (indexToSelect > -1) { |
| final Content toSelect = myContents.get(indexToSelect); |
| if (!isSelected(toSelect)) { |
| if (myUI.isSingleSelection()) { |
| setSelectedContentCB(toSelect).notify(result); |
| } |
| else { |
| addSelectedContent(toSelect); |
| result.setDone(); |
| } |
| } |
| } |
| } |
| else { |
| mySelection.clear(); |
| } |
| |
| return result; |
| } |
| finally { |
| if (ApplicationManager.getApplication().isDispatchThread()) { |
| myUI.getComponent().updateUI(); //cleanup visibleComponent from Alloy...TabbedPaneUI |
| } |
| } |
| } |
| |
| @Override |
| public void removeAllContents(final boolean dispose) { |
| Content[] contents = getContents(); |
| for (Content content : contents) { |
| removeContent(content, dispose); |
| } |
| } |
| |
| @Override |
| public int getContentCount() { |
| return myContents.size(); |
| } |
| |
| @Override |
| @NotNull |
| public Content[] getContents() { |
| return myContents.toArray(new Content[myContents.size()]); |
| } |
| |
| //TODO[anton,vova] is this method needed? |
| @Override |
| public Content findContent(String displayName) { |
| for (Content content : myContents) { |
| if (content.getDisplayName().equals(displayName)) { |
| return content; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public Content getContent(int index) { |
| return index >= 0 && index < myContents.size() ? myContents.get(index) : null; |
| } |
| |
| @Override |
| public Content getContent(JComponent component) { |
| Content[] contents = getContents(); |
| for (Content content : contents) { |
| if (Comparing.equal(component, content.getComponent())) { |
| return content; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public int getIndexOfContent(Content content) { |
| return myContents.indexOf(content); |
| } |
| |
| @NotNull |
| @Override |
| public String getCloseActionName() { |
| return myUI.getCloseActionName(); |
| } |
| |
| @NotNull |
| @Override |
| public String getCloseAllButThisActionName() { |
| return myUI.getCloseAllButThisActionName(); |
| } |
| |
| @NotNull |
| @Override |
| public String getPreviousContentActionName() { |
| return myUI.getPreviousContentActionName(); |
| } |
| |
| @NotNull |
| @Override |
| public String getNextContentActionName() { |
| return myUI.getNextContentActionName(); |
| } |
| |
| @Override |
| public List<AnAction> getAdditionalPopupActions(@NotNull final Content content) { |
| return null; |
| } |
| |
| @Override |
| public boolean canCloseAllContents() { |
| if (!canCloseContents()) { |
| return false; |
| } |
| for (Content content : myContents) { |
| if (content.isCloseable()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void addSelectedContent(@NotNull final Content content) { |
| if (!checkSelectionChangeShouldBeProcessed(content, false)) return; |
| |
| if (getIndexOfContent(content) == -1) { |
| throw new IllegalArgumentException("content not found: " + content); |
| } |
| if (!isSelected(content)) { |
| mySelection.add(content); |
| fireSelectionChanged(content, ContentManagerEvent.ContentOperation.add); |
| } |
| } |
| |
| private boolean checkSelectionChangeShouldBeProcessed(Content content, boolean implicit) { |
| if (!myUI.canChangeSelectionTo(content, implicit)) { |
| return false; |
| } |
| |
| final boolean result = !isSelected(content) || myContentWithChangedComponent.contains(content); |
| myContentWithChangedComponent.remove(content); |
| |
| return result; |
| } |
| |
| @Override |
| public void removeFromSelection(@NotNull Content content) { |
| if (!isSelected(content)) return; |
| mySelection.remove(content); |
| fireSelectionChanged(content, ContentManagerEvent.ContentOperation.remove); |
| } |
| |
| @Override |
| public boolean isSelected(@NotNull Content content) { |
| return mySelection.contains(content); |
| } |
| |
| @Override |
| @NotNull |
| public Content[] getSelectedContents() { |
| return mySelection.toArray(new Content[mySelection.size()]); |
| } |
| |
| @Override |
| @Nullable |
| public Content getSelectedContent() { |
| return mySelection.isEmpty() ? null : mySelection.get(0); |
| } |
| |
| @Override |
| public void setSelectedContent(@NotNull Content content, boolean requestFocus) { |
| setSelectedContentCB(content, requestFocus); |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback setSelectedContentCB(@NotNull final Content content, final boolean requestFocus) { |
| return setSelectedContentCB(content, requestFocus, true); |
| } |
| |
| @Override |
| public void setSelectedContent(@NotNull Content content, boolean requestFocus, boolean forcedFocus) { |
| setSelectedContentCB(content, requestFocus, forcedFocus); |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback setSelectedContentCB(@NotNull final Content content, final boolean requestFocus, final boolean forcedFocus) { |
| return setSelectedContent(content, requestFocus, forcedFocus, false); |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback setSelectedContent(@NotNull final Content content, final boolean requestFocus, final boolean forcedFocus, boolean implicit) { |
| if (isSelected(content) && requestFocus) { |
| return requestFocus(content, forcedFocus); |
| } |
| |
| if (!checkSelectionChangeShouldBeProcessed(content, implicit)) { |
| return new ActionCallback.Rejected(); |
| } |
| if (!myContents.contains(content)) { |
| throw new IllegalArgumentException("Cannot find content:" + content.getDisplayName()); |
| } |
| |
| final boolean focused = isSelectionHoldsFocus(); |
| |
| final Content[] old = getSelectedContents(); |
| |
| final ActiveRunnable selection = new ActiveRunnable() { |
| @NotNull |
| @Override |
| public ActionCallback run() { |
| if (myDisposed || getIndexOfContent(content) == -1) return new ActionCallback.Rejected(); |
| |
| for (Content each : old) { |
| removeFromSelection(each); |
| } |
| |
| addSelectedContent(content); |
| |
| if (requestFocus) { |
| return requestFocus(content, forcedFocus); |
| } |
| return new ActionCallback.Done(); |
| } |
| }; |
| |
| final ActionCallback result = new ActionCallback(); |
| boolean enabledFocus = getFocusManager().isFocusTransferEnabled(); |
| if (focused || requestFocus) { |
| if (enabledFocus) { |
| return getFocusManager().requestFocus(myFocusProxy, true).doWhenProcessed(new Runnable() { |
| @Override |
| public void run() { |
| selection.run().notify(result); |
| } |
| }); |
| } |
| return selection.run().notify(result); |
| } |
| else { |
| return selection.run().notify(result); |
| } |
| } |
| |
| private boolean isSelectionHoldsFocus() { |
| boolean focused = false; |
| final Content[] selection = getSelectedContents(); |
| for (Content each : selection) { |
| if (UIUtil.isFocusAncestor(each.getComponent())) { |
| focused = true; |
| break; |
| } |
| } |
| return focused; |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback setSelectedContentCB(@NotNull Content content) { |
| return setSelectedContentCB(content, false); |
| } |
| |
| @Override |
| public void setSelectedContent(@NotNull final Content content) { |
| setSelectedContentCB(content); |
| } |
| |
| @Override |
| public ActionCallback selectPreviousContent() { |
| int contentCount = getContentCount(); |
| LOG.assertTrue(contentCount > 1); |
| Content selectedContent = getSelectedContent(); |
| int index = getIndexOfContent(selectedContent); |
| index = (index - 1 + contentCount) % contentCount; |
| final Content content = getContent(index); |
| if (content == null) { |
| return null; |
| } |
| return setSelectedContentCB(content, true); |
| } |
| |
| @Override |
| public ActionCallback selectNextContent() { |
| int contentCount = getContentCount(); |
| LOG.assertTrue(contentCount > 1); |
| Content selectedContent = getSelectedContent(); |
| int index = getIndexOfContent(selectedContent); |
| index = (index + 1) % contentCount; |
| final Content content = getContent(index); |
| if (content == null) { |
| return null; |
| } |
| return setSelectedContentCB(content, true); |
| } |
| |
| @Override |
| public void addContentManagerListener(@NotNull ContentManagerListener l) { |
| myListeners.add(0,l); |
| } |
| |
| @Override |
| public void removeContentManagerListener(@NotNull ContentManagerListener l) { |
| myListeners.remove(l); |
| } |
| |
| |
| private void fireContentAdded(Content content, int newIndex, ContentManagerEvent.ContentOperation operation) { |
| ContentManagerEvent event = new ContentManagerEvent(this, content, newIndex, operation); |
| for (ContentManagerListener listener : myListeners) { |
| listener.contentAdded(event); |
| } |
| } |
| |
| private void fireContentRemoved(Content content, int oldIndex, ContentManagerEvent.ContentOperation operation) { |
| ContentManagerEvent event = new ContentManagerEvent(this, content, oldIndex, operation); |
| for (ContentManagerListener listener : myListeners) { |
| listener.contentRemoved(event); |
| } |
| } |
| |
| private void fireSelectionChanged(Content content, ContentManagerEvent.ContentOperation operation) { |
| ContentManagerEvent event = new ContentManagerEvent(this, content, myContents.indexOf(content), operation); |
| for (ContentManagerListener listener : myListeners) { |
| listener.selectionChanged(event); |
| } |
| } |
| |
| private boolean fireContentRemoveQuery(Content content, int oldIndex, ContentManagerEvent.ContentOperation operation) { |
| ContentManagerEvent event = new ContentManagerEvent(this, content, oldIndex, operation); |
| for (ContentManagerListener listener : myListeners) { |
| listener.contentRemoveQuery(event); |
| if (event.isConsumed()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @NotNull |
| @Override |
| public ActionCallback requestFocus(final Content content, final boolean forced) { |
| final Content toSelect = content == null ? getSelectedContent() : content; |
| if (toSelect == null) return new ActionCallback.Rejected(); |
| assert myContents.contains(toSelect); |
| |
| |
| return getFocusManager().requestFocus(new FocusCommand(content, toSelect.getPreferredFocusableComponent()) { |
| @NotNull |
| @Override |
| public ActionCallback run() { |
| return doRequestFocus(toSelect); |
| } |
| }, forced); |
| } |
| |
| private IdeFocusManager getFocusManager() { |
| return IdeFocusManager.getInstance(myProject); |
| } |
| |
| private static ActionCallback doRequestFocus(final Content toSelect) { |
| JComponent toFocus = computeWillFocusComponent(toSelect); |
| |
| if (toFocus != null) { |
| toFocus.requestFocus(); |
| } |
| |
| return new ActionCallback.Done(); |
| } |
| |
| private static JComponent computeWillFocusComponent(Content toSelect) { |
| JComponent toFocus = toSelect.getPreferredFocusableComponent(); |
| if (toFocus != null) { |
| toFocus = IdeFocusTraversalPolicy.getPreferredFocusedComponent(toFocus); |
| } |
| |
| if (toFocus == null) toFocus = toSelect.getPreferredFocusableComponent(); |
| return toFocus; |
| } |
| |
| @Override |
| public void addDataProvider(@NotNull final DataProvider provider) { |
| dataProviders.add(provider); |
| } |
| |
| @Override |
| public void propertyChange(@NotNull PropertyChangeEvent event) { |
| if (Content.PROP_COMPONENT.equals(event.getPropertyName())) { |
| myContentWithChangedComponent.add((Content)event.getSource()); |
| } |
| } |
| |
| @Override |
| @NotNull |
| public ContentFactory getFactory() { |
| return ServiceManager.getService(ContentFactory.class); |
| } |
| |
| @Override |
| public void beforeTreeDispose() { |
| myUI.beforeDispose(); |
| } |
| |
| @Override |
| public void dispose() { |
| myDisposed = true; |
| |
| myContents.clear(); |
| mySelection.clear(); |
| myContentWithChangedComponent.clear(); |
| myUI = null; |
| myListeners.clear(); |
| dataProviders.clear(); |
| } |
| |
| @Override |
| public boolean isDisposed() { |
| return myDisposed; |
| } |
| |
| @Override |
| public boolean isSingleSelection() { |
| return myUI.isSingleSelection(); |
| } |
| } |