diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/TextEditorWithPreview.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/TextEditorWithPreview.java new file mode 100644 index 000000000000..3b47012a999d --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/TextEditorWithPreview.java @@ -0,0 +1,442 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.codeHighlighting.BackgroundEditorHighlighter; +import com.intellij.icons.AllIcons; +import com.intellij.ide.structureView.StructureViewBuilder; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.editor.ex.EditorGutterComponentEx; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.util.Disposer; +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.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Konstantin Bulenkov + */ +public class TextEditorWithPreview extends UserDataHolderBase implements FileEditor { + protected final TextEditor myEditor; + protected final FileEditor myPreview; + @NotNull + private final MyListenersMultimap myListenersGenerator = new MyListenersMultimap(); + private Layout myLayout; + private JComponent myComponent; + private SplitEditorToolbar myToolbarWrapper; + private String myName; + + public TextEditorWithPreview(@NotNull TextEditor editor, @NotNull FileEditor preview, @NotNull String editorName) { + myEditor = editor; + myPreview = preview; + myName = editorName; + } + + public TextEditorWithPreview(@NotNull TextEditor editor, @NotNull FileEditor preview) { + this(editor, preview, "TextEditorWithPreview"); + } + + @Nullable + @Override + public BackgroundEditorHighlighter getBackgroundHighlighter() { + return myEditor.getBackgroundHighlighter(); + } + + @Nullable + @Override + public FileEditorLocation getCurrentLocation() { + return myEditor.getCurrentLocation(); + } + + @Nullable + @Override + public StructureViewBuilder getStructureViewBuilder() { + return myEditor.getStructureViewBuilder(); + } + + @Override + public void dispose() { + Disposer.dispose(myEditor); + Disposer.dispose(myPreview); + } + + @Override + public void selectNotify() { + myEditor.selectNotify(); + myPreview.selectNotify(); + } + + @Override + public void deselectNotify() { + myEditor.deselectNotify(); + myPreview.deselectNotify(); + } + + @NotNull + @Override + public JComponent getComponent() { + if (myComponent == null) { + final JBSplitter splitter = new JBSplitter(false, 0.5f, 0.15f, 0.85f); + splitter.setSplitterProportionKey(getSplitterProportionKey()); + splitter.setFirstComponent(myEditor.getComponent()); + splitter.setSecondComponent(myPreview.getComponent()); + + + myToolbarWrapper = new SplitEditorToolbar(splitter); + myToolbarWrapper.addGutterToTrack(((EditorGutterComponentEx)(myEditor).getEditor().getGutter())); + + if (myPreview instanceof TextEditor) { + myToolbarWrapper.addGutterToTrack(((EditorGutterComponentEx)((TextEditor)myPreview).getEditor().getGutter())); + } + + final JPanel result = new JPanel(new BorderLayout()); + result.add(myToolbarWrapper, BorderLayout.NORTH); + result.add(splitter, BorderLayout.CENTER); + if (myLayout == null) myLayout = Layout.SHOW_EDITOR_AND_PREVIEW; + adjustEditorsVisibility(); + + myComponent = result; + + } + return myComponent; + } + + @Override + public void setState(@NotNull FileEditorState state) { + if (state instanceof MyFileEditorState) { + final MyFileEditorState compositeState = (MyFileEditorState)state; + if (compositeState.getFirstState() != null) { + myEditor.setState(compositeState.getFirstState()); + } + if (compositeState.getSecondState() != null) { + myPreview.setState(compositeState.getSecondState()); + } + if (compositeState.getSplitLayout() != null) { + myLayout = compositeState.getSplitLayout(); + invalidateLayout(); + } + } + } + + private void adjustEditorsVisibility() { + myEditor.getComponent().setVisible(myLayout == Layout.SHOW_EDITOR || myLayout == Layout.SHOW_EDITOR_AND_PREVIEW); + myPreview.getComponent().setVisible(myLayout == Layout.SHOW_PREVIEW || myLayout == Layout.SHOW_EDITOR_AND_PREVIEW); + } + + private void invalidateLayout() { + adjustEditorsVisibility(); + myToolbarWrapper.refresh(); + myComponent.repaint(); + + final JComponent focusComponent = getPreferredFocusedComponent(); + if (focusComponent != null) { + IdeFocusManager.findInstanceByComponent(focusComponent).requestFocus(focusComponent, true); + } + } + + @NotNull + protected String getSplitterProportionKey() { + return "TextEditorWithPreview.SplitterProportionKey"; + } + + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + return myEditor.getPreferredFocusedComponent(); + } + + @NotNull + @Override + public String getName() { + return myName; + } + + @NotNull + @Override + public FileEditorState getState(@NotNull FileEditorStateLevel level) { + return new MyFileEditorState(myLayout, myEditor.getState(level), myPreview.getState(level)); + } + + + @Override + public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { + myEditor.addPropertyChangeListener(listener); + myPreview.addPropertyChangeListener(listener); + + final DoublingEventListenerDelegate delegate = myListenersGenerator.addListenerAndGetDelegate(listener); + myEditor.addPropertyChangeListener(delegate); + myPreview.addPropertyChangeListener(delegate); + } + + @Override + public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { + myEditor.removePropertyChangeListener(listener); + myPreview.removePropertyChangeListener(listener); + + final DoublingEventListenerDelegate delegate = myListenersGenerator.removeListenerAndGetDelegate(listener); + if (delegate != null) { + myEditor.removePropertyChangeListener(delegate); + myPreview.removePropertyChangeListener(delegate); + } + } + + static class MyFileEditorState implements FileEditorState { + private final Layout mySplitLayout; + private final FileEditorState myFirstState; + private final FileEditorState mySecondState; + + public MyFileEditorState(Layout layout, FileEditorState firstState, FileEditorState secondState) { + mySplitLayout = layout; + myFirstState = firstState; + mySecondState = secondState; + } + + @Nullable + public Layout 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)); + } + } + + @Override + public boolean isModified() { + return myEditor.isModified() || myPreview.isModified(); + } + + @Override + public boolean isValid() { + return myEditor.isValid() && myPreview.isValid(); + } + + 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(TextEditorWithPreview.this, evt.getPropertyName(), evt.getOldValue(), evt.getNewValue())); + } + } + + private class MyListenersMultimap { + private final Map> 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 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 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 class SplitEditorToolbar extends JPanel implements Disposable { + private static final String LEFT_TOOLBAR_GROUP_ID = "Markdown.Toolbar.Left"; + private static final String RIGHT_TOOLBAR_GROUP_ID = "Markdown.Toolbar.Right"; + + //private final MySpacingPanel mySpacingPanel; + + private final ActionToolbar myRightToolbar; + + private final List myGutters = new ArrayList<>(); + + private final ComponentAdapter myAdjustToGutterListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + adjustSpacing(); + } + + @Override + public void componentShown(ComponentEvent e) { + adjustSpacing(); + } + + @Override + public void componentHidden(ComponentEvent e) { + adjustSpacing(); + } + }; + + public SplitEditorToolbar(@NotNull final JComponent targetComponentForActions) { + super(new BorderLayout()); + + final ActionToolbar leftToolbar = createToolbar(); + if (leftToolbar != null) { + leftToolbar.setTargetComponent(targetComponentForActions); + add(leftToolbar.getComponent(), BorderLayout.WEST); + } + + ActionGroup group = new DefaultActionGroup( + new ChangeViewModeAction(Layout.SHOW_EDITOR), + new ChangeViewModeAction(Layout.SHOW_EDITOR_AND_PREVIEW), + new ChangeViewModeAction(Layout.SHOW_PREVIEW) + ); + myRightToolbar = ActionManager.getInstance().createActionToolbar("TextEditorWithPreview", group, true); + myRightToolbar.setTargetComponent(targetComponentForActions); + add(myRightToolbar.getComponent(), BorderLayout.EAST); + + addComponentListener(myAdjustToGutterListener); + } + + public void addGutterToTrack(@NotNull EditorGutterComponentEx gutterComponentEx) { + myGutters.add(gutterComponentEx); + + gutterComponentEx.addComponentListener(myAdjustToGutterListener); + } + + public void refresh() { + adjustSpacing(); + myRightToolbar.updateActionsImmediately(); + } + + private void adjustSpacing() { + EditorGutterComponentEx leftMostGutter = null; + for (EditorGutterComponentEx gutter : myGutters) { + if (!gutter.isShowing()) { + continue; + } + if (leftMostGutter == null || leftMostGutter.getX() > gutter.getX()) { + leftMostGutter = gutter; + } + } + + final int spacing; + if (leftMostGutter == null) { + spacing = 0; + } + else { + spacing = leftMostGutter.getWhitespaceSeparatorOffset(); + } + //mySpacingPanel.setSpacing(spacing); + + revalidate(); + repaint(); + } + + @Override + public void dispose() { + removeComponentListener(myAdjustToGutterListener); + for (EditorGutterComponentEx gutter : myGutters) { + gutter.removeComponentListener(myAdjustToGutterListener); + } + } + } + + @Nullable + protected ActionToolbar createToolbar() { + return null; + } + + public enum Layout { + SHOW_EDITOR("Editor only", AllIcons.General.LayoutEditorOnly), + SHOW_PREVIEW("Preview only", AllIcons.General.LayoutPreviewOnly), + SHOW_EDITOR_AND_PREVIEW("Editor and Preview", AllIcons.General.LayoutEditorPreview); + + private final String myName; + private final Icon myIcon; + + Layout(String name, Icon icon) { + myName = name; + myIcon = icon; + } + + public String getName() { + return myName; + } + + public Icon getIcon() { + return myIcon; + } + } + + private class ChangeViewModeAction extends ToggleAction implements DumbAware { + private Layout myActionLayout; + + public ChangeViewModeAction(Layout layout) { + super(layout.getName(), layout.getName(), layout.getIcon()); + myActionLayout = layout; + } + + @Override + public boolean isSelected(AnActionEvent e) { + return myLayout == myActionLayout; + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + if (state) { + myLayout = myActionLayout; + adjustEditorsVisibility(); + } + } + } +}