package org.intellij.plugins.markdown.ui.split; import com.intellij.codeHighlighting.BackgroundEditorHighlighter; import com.intellij.ide.structureView.StructureViewBuilder; import com.intellij.openapi.editor.ex.EditorGutterComponentEx; import com.intellij.openapi.fileEditor.*; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.ui.JBSplitter; import org.intellij.plugins.markdown.MarkdownBundle; import org.intellij.plugins.markdown.settings.MarkdownApplicationSettings; 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.HashMap; import java.util.Map; public abstract class SplitFileEditor<E1 extends FileEditor, E2 extends FileEditor> extends UserDataHolderBase implements FileEditor { public static final Key<SplitFileEditor> PARENT_SPLIT_KEY = Key.create("parentSplit"); private static final String MY_PROPORTION_KEY = "SplitFileEditor.Proportion"; @NotNull protected final E1 myMainEditor; @NotNull protected final E2 mySecondEditor; @NotNull private final JComponent myComponent; @NotNull private SplitEditorLayout mySplitEditorLayout = MarkdownApplicationSettings.getInstance().getMarkdownPreviewSettings().getSplitEditorLayout(); @NotNull private final MyListenersMultimap myListenersGenerator = new MyListenersMultimap(); private SplitEditorToolbar myToolbarWrapper; public SplitFileEditor(@NotNull E1 mainEditor, @NotNull E2 secondEditor) { myMainEditor = mainEditor; mySecondEditor = secondEditor; myComponent = createComponent(); if (myMainEditor instanceof TextEditor) { myMainEditor.putUserData(PARENT_SPLIT_KEY, this); } if (mySecondEditor instanceof TextEditor) { mySecondEditor.putUserData(PARENT_SPLIT_KEY, this); } } @NotNull private JComponent createComponent() { final JBSplitter splitter = new JBSplitter(false, 0.5f, 0.15f, 0.85f); splitter.setSplitterProportionKey(MY_PROPORTION_KEY); splitter.setFirstComponent(myMainEditor.getComponent()); splitter.setSecondComponent(mySecondEditor.getComponent()); myToolbarWrapper = new SplitEditorToolbar(splitter); if (myMainEditor instanceof TextEditor) { myToolbarWrapper.addGutterToTrack(((EditorGutterComponentEx)((TextEditor)myMainEditor).getEditor().getGutter())); } if (mySecondEditor instanceof TextEditor) { myToolbarWrapper.addGutterToTrack(((EditorGutterComponentEx)((TextEditor)mySecondEditor).getEditor().getGutter())); } final JPanel result = new JPanel(new BorderLayout()); result.add(myToolbarWrapper, BorderLayout.NORTH); result.add(splitter, BorderLayout.CENTER); adjustEditorsVisibility(); return result; } public void triggerLayoutChange() { final int oldValue = mySplitEditorLayout.ordinal(); final int N = SplitEditorLayout.values().length; final int newValue = (oldValue + N - 1) % N; triggerLayoutChange(SplitEditorLayout.values()[newValue]); } public void triggerLayoutChange(@NotNull SplitFileEditor.SplitEditorLayout newLayout) { if (mySplitEditorLayout == newLayout) { return; } mySplitEditorLayout = newLayout; invalidateLayout(); } @NotNull public SplitEditorLayout getCurrentEditorLayout() { return mySplitEditorLayout; } private void invalidateLayout() { adjustEditorsVisibility(); myToolbarWrapper.refresh(); myComponent.repaint(); final JComponent focusComponent = getPreferredFocusedComponent(); if (focusComponent != null) { IdeFocusManager.findInstanceByComponent(focusComponent).requestFocus(focusComponent, true); } } private void adjustEditorsVisibility() { myMainEditor.getComponent().setVisible(mySplitEditorLayout.showFirst); mySecondEditor.getComponent().setVisible(mySplitEditorLayout.showSecond); } @NotNull public E1 getMainEditor() { return myMainEditor; } @NotNull public E2 getSecondEditor() { return mySecondEditor; } @NotNull @Override public JComponent getComponent() { return myComponent; } @Nullable @Override public JComponent getPreferredFocusedComponent() { return myMainEditor.getPreferredFocusedComponent(); } @NotNull @Override public FileEditorState getState(@NotNull FileEditorStateLevel level) { return new MyFileEditorState(mySplitEditorLayout.name(), myMainEditor.getState(level), mySecondEditor.getState(level)); } @Override public void setState(@NotNull FileEditorState state) { if (state instanceof MyFileEditorState) { final MyFileEditorState compositeState = (MyFileEditorState)state; if (compositeState.getFirstState() != null) { myMainEditor.setState(compositeState.getFirstState()); } if (compositeState.getSecondState() != null) { mySecondEditor.setState(compositeState.getSecondState()); } if (compositeState.getSplitLayout() != null) { mySplitEditorLayout = SplitEditorLayout.valueOf(compositeState.getSplitLayout()); invalidateLayout(); } } } @Override public boolean isModified() { return myMainEditor.isModified() || mySecondEditor.isModified(); } @Override public boolean isValid() { return myMainEditor.isValid() && mySecondEditor.isValid(); } @Override public void selectNotify() { myMainEditor.selectNotify(); mySecondEditor.selectNotify(); } @Override public void deselectNotify() { myMainEditor.deselectNotify(); mySecondEditor.deselectNotify(); } @Override public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { myMainEditor.addPropertyChangeListener(listener); mySecondEditor.addPropertyChangeListener(listener); final DoublingEventListenerDelegate delegate = myListenersGenerator.addListenerAndGetDelegate(listener); myMainEditor.addPropertyChangeListener(delegate); mySecondEditor.addPropertyChangeListener(delegate); } @Override public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { myMainEditor.removePropertyChangeListener(listener); mySecondEditor.removePropertyChangeListener(listener); final DoublingEventListenerDelegate delegate = myListenersGenerator.removeListenerAndGetDelegate(listener); if (delegate != null) { myMainEditor.removePropertyChangeListener(delegate); mySecondEditor.removePropertyChangeListener(delegate); } } @Nullable @Override public BackgroundEditorHighlighter getBackgroundHighlighter() { return myMainEditor.getBackgroundHighlighter(); } @Nullable @Override public FileEditorLocation getCurrentLocation() { return myMainEditor.getCurrentLocation(); } @Nullable @Override public StructureViewBuilder getStructureViewBuilder() { return myMainEditor.getStructureViewBuilder(); } @Override public void dispose() { Disposer.dispose(myMainEditor); Disposer.dispose(mySecondEditor); } static class MyFileEditorState implements FileEditorState { @Nullable private final String mySplitLayout; @Nullable private final FileEditorState myFirstState; @Nullable private final FileEditorState mySecondState; public MyFileEditorState(@Nullable String splitLayout, @Nullable FileEditorState firstState, @Nullable FileEditorState secondState) { mySplitLayout = splitLayout; myFirstState = firstState; mySecondState = secondState; } @Nullable public String getSplitLayout() { return mySplitLayout; } @Nullable public FileEditorState getFirstState() { return myFirstState; } @Nullable public FileEditorState getSecondState() { return mySecondState; } @Override public boolean canBeMergedWith(FileEditorState otherState, FileEditorStateLevel level) { return otherState instanceof MyFileEditorState && (myFirstState == null || myFirstState.canBeMergedWith(((MyFileEditorState)otherState).myFirstState, level)) && (mySecondState == null || mySecondState.canBeMergedWith(((MyFileEditorState)otherState).mySecondState, level)); } } private class DoublingEventListenerDelegate implements PropertyChangeListener { @NotNull private final PropertyChangeListener myDelegate; private DoublingEventListenerDelegate(@NotNull PropertyChangeListener delegate) { myDelegate = delegate; } @Override public void propertyChange(PropertyChangeEvent evt) { myDelegate.propertyChange(new PropertyChangeEvent(SplitFileEditor.this, evt.getPropertyName(), evt.getOldValue(), evt.getNewValue())); } } private class MyListenersMultimap { private final Map<PropertyChangeListener, Pair<Integer, DoublingEventListenerDelegate>> myMap = new HashMap<>(); @NotNull public DoublingEventListenerDelegate addListenerAndGetDelegate(@NotNull PropertyChangeListener listener) { if (!myMap.containsKey(listener)) { myMap.put(listener, Pair.create(1, new DoublingEventListenerDelegate(listener))); } else { final Pair<Integer, DoublingEventListenerDelegate> oldPair = myMap.get(listener); myMap.put(listener, Pair.create(oldPair.getFirst() + 1, oldPair.getSecond())); } return myMap.get(listener).getSecond(); } @Nullable public DoublingEventListenerDelegate removeListenerAndGetDelegate(@NotNull PropertyChangeListener listener) { final Pair<Integer, DoublingEventListenerDelegate> oldPair = myMap.get(listener); if (oldPair == null) { return null; } if (oldPair.getFirst() == 1) { myMap.remove(listener); } else { myMap.put(listener, Pair.create(oldPair.getFirst() - 1, oldPair.getSecond())); } return oldPair.getSecond(); } } public enum SplitEditorLayout { FIRST(true, false, MarkdownBundle.message("markdown.layout.editor.only")), SECOND(false, true, MarkdownBundle.message("markdown.layout.preview.only")), SPLIT(true, true, MarkdownBundle.message("markdown.layout.editor.and.preview")); public final boolean showFirst; public final boolean showSecond; public final String presentationName; SplitEditorLayout(boolean showFirst, boolean showSecond, String presentationName) { this.showFirst = showFirst; this.showSecond = showSecond; this.presentationName = presentationName; } @Override public String toString() { return presentationName; } } }