| /* |
| * 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.openapi.fileEditor.impl; |
| |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.CommonDataKeys; |
| import com.intellij.openapi.actionSystem.DataProvider; |
| import com.intellij.openapi.actionSystem.IdeActions; |
| import com.intellij.openapi.actionSystem.PlatformDataKeys; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.colors.EditorColors; |
| import com.intellij.openapi.editor.colors.EditorColorsManager; |
| import com.intellij.openapi.fileEditor.*; |
| import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; |
| import com.intellij.openapi.fileEditor.ex.FileEditorProviderManager; |
| import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory; |
| import com.intellij.openapi.project.DumbService; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.wm.FocusWatcher; |
| import com.intellij.ui.PrevNextActionsDescriptor; |
| import com.intellij.ui.SideBorder; |
| import com.intellij.ui.TabbedPaneWrapper; |
| import com.intellij.ui.tabs.UiDecorator; |
| import com.intellij.util.SmartList; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NotNull; |
| |
| import javax.swing.*; |
| import javax.swing.border.EmptyBorder; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import java.awt.*; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * This class hides internal structure of UI component which represent |
| * set of opened editors. For example, one myEditor is represented by its |
| * component, more then one myEditor is wrapped into tabbed pane. |
| * |
| * @author Vladimir Kondratyev |
| */ |
| public abstract class EditorComposite implements Disposable { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.EditorComposite"); |
| |
| /** |
| * File for which composite is created |
| */ |
| @NotNull private final VirtualFile myFile; |
| /** |
| * Whether the composite is pinned or not |
| */ |
| private boolean myPinned; |
| /** |
| * Editors which are opened in the composite |
| */ |
| protected final FileEditor[] myEditors; |
| /** |
| * This is initial timestamp of the file. It uses to implement |
| * "close non modified editors first" feature. |
| */ |
| private final long myInitialFileTimeStamp; |
| protected final TabbedPaneWrapper myTabbedPaneWrapper; |
| private final MyComponent myComponent; |
| private final FocusWatcher myFocusWatcher; |
| /** |
| * Currently selected myEditor |
| */ |
| private FileEditor mySelectedEditor; |
| private final FileEditorManagerEx myFileEditorManager; |
| private final Map<FileEditor, JComponent> myTopComponents = new HashMap<FileEditor, JComponent>(); |
| private final Map<FileEditor, JComponent> myBottomComponents = new HashMap<FileEditor, JComponent>(); |
| |
| /** |
| * @param file <code>file</code> for which composite is being constructed |
| * |
| * @param editors <code>edittors</code> that should be placed into the composite |
| * |
| * @exception java.lang.IllegalArgumentException if <code>editors</code> |
| * is <code>null</code> or <code>providers</code> is <code>null</code> or <code>myEditor</code> arrays is empty |
| */ |
| EditorComposite(@NotNull final VirtualFile file, |
| @NotNull final FileEditor[] editors, |
| @NotNull final FileEditorManagerEx fileEditorManager) { |
| myFile = file; |
| myEditors = editors; |
| myFileEditorManager = fileEditorManager; |
| myInitialFileTimeStamp = myFile.getTimeStamp(); |
| |
| Disposer.register(fileEditorManager.getProject(), this); |
| |
| if(editors.length > 1){ |
| PrevNextActionsDescriptor descriptor = new PrevNextActionsDescriptor(IdeActions.ACTION_NEXT_EDITOR_TAB, IdeActions.ACTION_PREVIOUS_EDITOR_TAB); |
| final TabbedPaneWrapper.AsJBTabs wrapper = new TabbedPaneWrapper.AsJBTabs(fileEditorManager.getProject(), SwingConstants.BOTTOM, descriptor, this); |
| wrapper.getTabs().getPresentation().setPaintBorder(0, 0, 0, 0).setTabSidePaintBorder(1).setGhostsAlwaysVisible(true).setUiDecorator(new UiDecorator() { |
| @Override |
| @NotNull |
| public UiDecoration getDecoration() { |
| return new UiDecoration(null, new Insets(0, 8, 0, 8)); |
| } |
| }); |
| wrapper.getTabs().getComponent().setBorder(new EmptyBorder(0, 0, 1, 0)); |
| |
| myTabbedPaneWrapper=wrapper; |
| myComponent=new MyComponent(wrapper.getComponent()){ |
| @Override |
| public boolean requestFocusInWindow() { |
| return wrapper.getComponent().requestFocusInWindow(); |
| } |
| |
| @Override |
| public void requestFocus() { |
| wrapper.getComponent().requestFocus(); |
| } |
| |
| @Override |
| public boolean requestDefaultFocus() { |
| return wrapper.getComponent().requestDefaultFocus(); |
| } |
| }; |
| for (FileEditor editor : editors) { |
| wrapper.addTab(editor.getName(), createEditorComponent(editor)); |
| } |
| myTabbedPaneWrapper.addChangeListener(new MyChangeListener()); |
| } |
| else if(editors.length==1){ |
| myTabbedPaneWrapper=null; |
| myComponent = new MyComponent(createEditorComponent(editors[0])){ |
| @Override |
| public void requestFocus() { |
| JComponent component = editors[0].getPreferredFocusedComponent(); |
| if (component != null) { |
| component.requestFocus(); |
| } |
| } |
| |
| @Override |
| public boolean requestFocusInWindow() { |
| JComponent component = editors[0].getPreferredFocusedComponent(); |
| if (component != null) { |
| return component.requestFocusInWindow(); |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public boolean requestDefaultFocus() { |
| JComponent component = editors[0].getPreferredFocusedComponent(); |
| if (component != null) { |
| return component.requestDefaultFocus(); |
| } |
| return false; |
| } |
| }; |
| } |
| else{ |
| throw new IllegalArgumentException("editors array cannot be empty"); |
| } |
| |
| mySelectedEditor = editors[0]; |
| myFocusWatcher = new FocusWatcher(); |
| myFocusWatcher.install(myComponent); |
| |
| myFileEditorManager.addFileEditorManagerListener(new FileEditorManagerAdapter() { |
| @Override |
| public void selectionChanged(@NotNull final FileEditorManagerEvent event) { |
| final VirtualFile oldFile = event.getOldFile(); |
| final VirtualFile newFile = event.getNewFile(); |
| if (Comparing.equal(oldFile, newFile) && Comparing.equal(getFile(), newFile)) { |
| Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| final FileEditor oldEditor = event.getOldEditor(); |
| if (oldEditor != null) oldEditor.deselectNotify(); |
| final FileEditor newEditor = event.getNewEditor(); |
| if (newEditor != null) newEditor.selectNotify(); |
| ((FileEditorProviderManagerImpl)FileEditorProviderManager.getInstance()).providerSelected(EditorComposite.this); |
| ((IdeDocumentHistoryImpl)IdeDocumentHistory.getInstance(myFileEditorManager.getProject())).onSelectionChanged(); |
| } |
| }; |
| if (ApplicationManager.getApplication().isDispatchThread()) { |
| CommandProcessor.getInstance().executeCommand(myFileEditorManager.getProject(), runnable, "Switch Active Editor", null); |
| } |
| else { |
| runnable.run(); // not invoked by user |
| } |
| } |
| } |
| }, this); |
| } |
| |
| private JComponent createEditorComponent(final FileEditor editor) { |
| JPanel component = new JPanel(new BorderLayout()); |
| JComponent comp = editor.getComponent(); |
| if (!FileEditorManagerImpl.isDumbAware(editor)) { |
| comp = DumbService.getInstance(myFileEditorManager.getProject()).wrapGently(comp, editor); |
| } |
| |
| component.add(comp, BorderLayout.CENTER); |
| |
| JPanel topPanel = new TopBottomPanel(); |
| myTopComponents.put(editor, topPanel); |
| component.add(topPanel, BorderLayout.NORTH); |
| |
| final JPanel bottomPanel = new TopBottomPanel(); |
| myBottomComponents.put(editor, bottomPanel); |
| component.add(bottomPanel, BorderLayout.SOUTH); |
| |
| return component; |
| } |
| |
| /** |
| * @return whether myEditor composite is pinned |
| */ |
| public boolean isPinned(){ |
| return myPinned; |
| } |
| |
| /** |
| * Sets new "pinned" state |
| */ |
| void setPinned(final boolean pinned){ |
| myPinned = pinned; |
| } |
| |
| private void fireSelectedEditorChanged(final FileEditor oldSelectedEditor, final FileEditor newSelectedEditor){ |
| if ((!EventQueue.isDispatchThread() || !myFileEditorManager.isInsideChange()) && !Comparing.equal(oldSelectedEditor, newSelectedEditor)) { |
| myFileEditorManager.notifyPublisher(new Runnable() { |
| @Override |
| public void run() { |
| final FileEditorManagerEvent event = new FileEditorManagerEvent(myFileEditorManager, myFile, oldSelectedEditor, myFile, newSelectedEditor); |
| final FileEditorManagerListener publisher = myFileEditorManager.getProject().getMessageBus().syncPublisher(FileEditorManagerListener.FILE_EDITOR_MANAGER); |
| publisher.selectionChanged(event); |
| } |
| }); |
| final JComponent component = newSelectedEditor.getComponent(); |
| final EditorWindowHolder holder = UIUtil.getParentOfType(EditorWindowHolder.class, component); |
| if (holder != null) { |
| ((FileEditorManagerImpl)myFileEditorManager).addSelectionRecord(myFile, holder.getEditorWindow()); |
| } |
| } |
| } |
| |
| |
| /** |
| * @return preferred focused component inside myEditor composite. Composite uses FocusWatcher to |
| * track focus movement inside the myEditor. |
| */ |
| public JComponent getPreferredFocusedComponent(){ |
| if (mySelectedEditor == null) return null; |
| |
| final Component component = myFocusWatcher.getFocusedComponent(); |
| if(!(component instanceof JComponent) || !component.isShowing() || !component.isEnabled() || !component.isFocusable()){ |
| return getSelectedEditor().getPreferredFocusedComponent(); |
| } |
| return (JComponent)component; |
| } |
| |
| /** |
| * @return file for which composite was created. |
| */ |
| @NotNull |
| public VirtualFile getFile() { |
| return myFile; |
| } |
| |
| public FileEditorManager getFileEditorManager() { |
| return myFileEditorManager; |
| } |
| |
| /** |
| * @return initial time stamp of the file (on moment of creation of |
| * the composite) |
| */ |
| public long getInitialFileTimeStamp() { |
| return myInitialFileTimeStamp; |
| } |
| |
| /** |
| * @return editors which are opened in the composite. <b>Do not modify |
| * this array</b>. |
| */ |
| @NotNull |
| public FileEditor[] getEditors() { |
| return myEditors; |
| } |
| |
| @NotNull |
| public List<JComponent> getTopComponents(@NotNull FileEditor editor) { |
| return getTopBottomComponents(editor, true); |
| } |
| |
| @NotNull |
| public List<JComponent> getBottomComponents(@NotNull FileEditor editor) { |
| return getTopBottomComponents(editor, false); |
| } |
| |
| @NotNull |
| private List<JComponent> getTopBottomComponents(@NotNull FileEditor editor, boolean top) { |
| SmartList<JComponent> result = new SmartList<JComponent>(); |
| JComponent container = top ? myTopComponents.get(editor) : myBottomComponents.get(editor); |
| for (Component each : container.getComponents()) { |
| if (each instanceof TopBottomComponentWrapper) { |
| result.add(((TopBottomComponentWrapper)each).getWrappee()); |
| } |
| } |
| return Collections.unmodifiableList(result); |
| } |
| |
| public void addTopComponent(FileEditor editor, JComponent component) { |
| manageTopOrBottomComponent(editor, component, true, false); |
| } |
| |
| public void removeTopComponent(FileEditor editor, JComponent component) { |
| manageTopOrBottomComponent(editor, component, true, true); |
| } |
| |
| public void addBottomComponent(FileEditor editor, JComponent component) { |
| manageTopOrBottomComponent(editor, component, false, false); |
| } |
| |
| public void removeBottomComponent(FileEditor editor, JComponent component) { |
| manageTopOrBottomComponent(editor, component, false, true); |
| } |
| |
| private void manageTopOrBottomComponent(FileEditor editor, JComponent component, boolean top, boolean remove) { |
| final JComponent container = top ? myTopComponents.get(editor) : myBottomComponents.get(editor); |
| assert container != null; |
| |
| if (remove) { |
| container.remove(component.getParent()); |
| } else { |
| container.add(new TopBottomComponentWrapper(component, top)); |
| } |
| container.revalidate(); |
| } |
| |
| /** |
| * @return currently selected myEditor. |
| */ |
| @NotNull |
| FileEditor getSelectedEditor() { |
| return getSelectedEditorWithProvider().getFirst (); |
| } |
| |
| public boolean isDisposed() { |
| return myTabbedPaneWrapper != null && myTabbedPaneWrapper.isDisposed(); |
| } |
| |
| /** |
| * @return currently selected myEditor with its provider. |
| */ |
| @NotNull |
| public abstract Pair<FileEditor, FileEditorProvider> getSelectedEditorWithProvider(); |
| |
| void setSelectedEditor(final int index){ |
| if(myEditors.length == 1){ |
| // nothing to do |
| LOG.assertTrue(myTabbedPaneWrapper == null); |
| } |
| else{ |
| LOG.assertTrue(myTabbedPaneWrapper != null); |
| myTabbedPaneWrapper.setSelectedIndex(index); |
| } |
| } |
| |
| /** |
| * @return component which represents set of file editors in the UI |
| */ |
| public JComponent getComponent() { |
| return myComponent; |
| } |
| |
| /** |
| * @return <code>true</code> if the composite contains at least one |
| * modified myEditor |
| */ |
| public boolean isModified(){ |
| for(int i=myEditors.length-1;i>=0;i--){ |
| if(myEditors[i].isModified()){ |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Handles changes of selected myEditor |
| */ |
| private final class MyChangeListener implements ChangeListener{ |
| @Override |
| public void stateChanged(ChangeEvent e) { |
| FileEditor oldSelectedEditor = mySelectedEditor; |
| LOG.assertTrue(oldSelectedEditor != null); |
| int selectedIndex = myTabbedPaneWrapper.getSelectedIndex(); |
| LOG.assertTrue(selectedIndex != -1); |
| mySelectedEditor = myEditors[selectedIndex]; |
| fireSelectedEditorChanged(oldSelectedEditor, mySelectedEditor); |
| } |
| } |
| |
| private abstract class MyComponent extends JPanel implements DataProvider{ |
| public MyComponent(JComponent realComponent){ |
| super(new BorderLayout()); |
| add(realComponent, BorderLayout.CENTER); |
| } |
| |
| @Override |
| public final Object getData(String dataId){ |
| if (PlatformDataKeys.FILE_EDITOR.is(dataId)) { |
| return getSelectedEditor(); |
| } |
| else if(CommonDataKeys.VIRTUAL_FILE.is(dataId)){ |
| return myFile.isValid() ? myFile : null; |
| } |
| else if(CommonDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)){ |
| return myFile.isValid() ? new VirtualFile[] {myFile} : null; |
| } |
| else{ |
| JComponent component = getPreferredFocusedComponent(); |
| if(component instanceof DataProvider && component != this){ |
| return ((DataProvider)component).getData(dataId); |
| } |
| else{ |
| return null; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| for (FileEditor editor : myEditors) { |
| if (!Disposer.isDisposed(editor)) { |
| Disposer.dispose(editor); |
| } |
| } |
| myFocusWatcher.deinstall(myFocusWatcher.getTopComponent()); |
| } |
| |
| private static class TopBottomPanel extends JPanel { |
| private TopBottomPanel() { |
| setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); |
| } |
| |
| @Override |
| public Color getBackground() { |
| Color color = EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.GUTTER_BACKGROUND); |
| return color == null ? Color.gray : color; |
| } |
| } |
| |
| private static class TopBottomComponentWrapper extends JPanel { |
| private final JComponent myWrappee; |
| |
| public TopBottomComponentWrapper(JComponent component, boolean top) { |
| super(new BorderLayout()); |
| myWrappee = component; |
| setOpaque(false); |
| |
| setBorder(new SideBorder(null, top ? SideBorder.BOTTOM : SideBorder.TOP, true) { |
| @Override |
| public Color getLineColor() { |
| Color result = EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.TEARLINE_COLOR); |
| return result == null ? Color.black : result; |
| } |
| }); |
| |
| add(component); |
| } |
| |
| @NotNull |
| public JComponent getWrappee() { |
| return myWrappee; |
| } |
| } |
| } |