diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/TextEditorWithPreview.java b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/TextEditorWithPreview.java new file mode 100644 index 00000000000..9f54555cf36 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/TextEditorWithPreview.java @@ -0,0 +1,482 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.scratch.ui; + + +import com.intellij.codeHighlighting.BackgroundEditorHighlighter; +import com.intellij.icons.AllIcons; +import com.intellij.ide.structureView.StructureViewBuilder; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.editor.ex.EditorGutterComponentEx; +import com.intellij.openapi.fileEditor.*; +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 com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.ApiStatus; +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; + +/** + * Two panel editor with three states: Editor, Preview and Editor with Preview. + * Based on SplitFileEditor by Valentin Fondaratov + *
+ * NOTE: This class is a copy of {@link com.intellij.openapi.fileEditor.TextEditorWithPreview} from the most recent intellij-community + * repository. We cannot use bundled version of this class because it doesn't yet have customization methods + * (namely {@link TextEditorWithPreview#createLeftToolbarActionGroup()}). + * + * {@link SplitEditorToolbar} is also copied from the platform. + * + * This class also may have some minimal customizations to allow tracking when its layout have been changed. In the future we hope to + * remove this copied class entirely and to use the bundled version. + */ +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 final 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()); + splitter.setDividerWidth(3); + + myToolbarWrapper = createMarkdownToolbarWrapper(splitter); + Disposer.register(this, myToolbarWrapper); + + if (myLayout == null) { + String lastUsed = PropertiesComponent.getInstance().getValue(getLayoutPropertyName()); + myLayout = Layout.fromName(lastUsed, Layout.SHOW_EDITOR_AND_PREVIEW); + } + adjustEditorsVisibility(); + + myComponent = JBUI.Panels.simplePanel(splitter).addToTop(myToolbarWrapper); + } + return myComponent; + } + + @NotNull + private SplitEditorToolbar createMarkdownToolbarWrapper (@NotNull JComponent targetComponentForActions) { + final ActionToolbar leftToolbar = createToolbar(); + if (leftToolbar != null) { + leftToolbar.setTargetComponent(targetComponentForActions); + leftToolbar.setReservePlaceAutoPopupIcon(false); + } + + final ActionToolbar rightToolbar = createRightToolbar(); + rightToolbar.setTargetComponent(targetComponentForActions); + rightToolbar.setReservePlaceAutoPopupIcon(false); + + return new SplitEditorToolbar(leftToolbar, rightToolbar); + } + + @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() { + switch (myLayout) { + case SHOW_EDITOR_AND_PREVIEW: + case SHOW_EDITOR: + return myEditor.getPreferredFocusedComponent(); + case SHOW_PREVIEW: + return myPreview.getPreferredFocusedComponent(); + default: + throw new IllegalStateException(myLayout.myName); + } + } + + @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); + } + } + + @NotNull + public TextEditor getTextEditor() { + return myEditor; + } + + public Layout getLayout() { + return myLayout; + } + + static class MyFileEditorState implements FileEditorState { + private final Layout mySplitLayout; + private final FileEditorState myFirstState; + private final FileEditorState mySecondState; + + 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