blob: d8a704d891bfff7b98f393bd11c8db8fe5fee077 [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.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;
}
}
}