/*
* Copyright 2003-2015 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 jetbrains.mps.ide.editor;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ui.JBUI;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorPanelManagerImpl;
import jetbrains.mps.nodeEditor.InspectorTool;
import jetbrains.mps.nodeEditor.MementoPersistence;
import jetbrains.mps.nodeEditor.NodeEditorComponent;
import jetbrains.mps.nodeEditor.configuration.EditorConfigurationBuilder;
import jetbrains.mps.openapi.editor.Editor;
import jetbrains.mps.openapi.editor.EditorComponentState;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.EditorState;
import jetbrains.mps.openapi.editor.extensions.EditorExtensionUtil;
import jetbrains.mps.project.Project;
import jetbrains.mps.util.EqualUtil;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import java.awt.BorderLayout;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class BaseNodeEditor implements Editor {
private static final Logger LOG = LogManager.getLogger(BaseNodeEditor.class);
private NodeEditorComponent myEditorComponent;
private final JComponent myComponent = new EditorPanel();
private final JComponent myEditorPanel = new JPanel();
protected final Project myProject;
private JComponent myReplace = null;
private SNodeReference myCurrentlyEditedNode = null;
protected final Map<TaskType, PrioritizedTask> myType2TaskMap = new HashMap<>();
private boolean mySelected;
public BaseNodeEditor(@NotNull Project mpsProject) {
myProject = mpsProject;
myEditorPanel.setLayout(new BorderLayout());
myEditorPanel.setBorder(new EmptyBorder(JBUI.emptyInsets()));
myComponent.add(myEditorPanel, BorderLayout.CENTER);
showEditor();
}
public abstract List<Document> getAllEditedDocuments();
public JComponent getComponent() {
return myComponent;
}
protected JComponent getEditorPanel() {
return myEditorPanel;
}
@Override
public NodeEditorComponent getCurrentEditorComponent() {
return myEditorComponent;
}
@Override
public EditorContext getEditorContext() {
return myEditorComponent == null ? null : myEditorComponent.getEditorContext();
}
@Override
public SNodeReference getCurrentlyEditedNode() {
return myCurrentlyEditedNode;
}
protected void editNode(@NotNull final SNodeReference nodeToEdit, @Nullable final SNodeReference nodeToSelect) {
assert myEditorComponent != null;
executeInEDT(new PrioritizedTask(TaskType.EDIT_NODE, myType2TaskMap) {
@Override
public void performTask() {
SNode node = nodeToEdit.resolve(myProject.getRepository());
if (node == null) {
return;
}
myEditorComponent.editNode(node);
SNode toSelect = nodeToSelect == null ? null : nodeToSelect.resolve(myProject.getRepository());
if (toSelect != null) {
myEditorComponent.getEditorContext().selectWRTFocusPolicy(toSelect, false); // XXX findNodeCell(, true)? to reveal even folded?
}
}
});
myCurrentlyEditedNode = nodeToEdit;
}
protected void executeInEDT(PrioritizedTask task) {
// XXX I'm not sure this is the right approach (used to be ModelAccess.isInEDT()) -
// callers expect model read, and check for EDT only is sort of an implicit knowledge about reads/writes.
// Why not runReadInEDT always?
if (myProject.getModelAccess().canRead() && ThreadUtils.isInEDT()) {
task.run();
} else {
myProject.getModelAccess().runReadInEDT(task);
}
}
@Override
public void dispose() {
setEditorComponent(null);
}
@Override
public boolean isTabbed() {
return false;
}
protected abstract Object getData(@NonNls String dataId);
private class EditorPanel extends JPanel implements DataProvider {
private EditorPanel() {
setLayout(new BorderLayout());
setBorder(new EmptyBorder(JBUI.emptyInsets()));
}
@Override
@Nullable
public Object getData(@NonNls String dataId) {
if (dataId.equals(MPSEditorDataKeys.MPS_EDITOR.getName())) {
return BaseNodeEditor.this;
}
Object data = BaseNodeEditor.this.getData(dataId);
if (data != null) {
return data;
}
NodeEditorComponent editorComponent = getCurrentEditorComponent();
return editorComponent == null ? null : editorComponent.getData(dataId);
}
}
protected void showEditor() {
if (myReplace != null) {
myEditorPanel.remove(myReplace);
myReplace = null;
}
NodeEditorComponent editorComponent =
new NodeEditorComponent(myProject.getRepository(), new EditorConfigurationBuilder().editorPanelManager(new EditorPanelManagerImpl(myProject)));
EditorExtensionUtil.extendUsingProject(editorComponent, myProject);
setEditorComponent(editorComponent);
}
protected void showComponent(JComponent replace) {
setEditorComponent(null);
if (myReplace != null) {
myEditorPanel.remove(myReplace);
myReplace = null;
}
myReplace = replace;
myEditorPanel.add(myReplace, BorderLayout.CENTER);
myComponent.validate();
}
private void pauseOrResumeHighlighter() {
if (myEditorComponent == null) {
return;
}
myEditorComponent.getHighlighter().setPaused(!mySelected);
}
private void setEditorComponent(NodeEditorComponent editorComponent) {
if (myEditorComponent == editorComponent) {
return;
}
if (myEditorComponent != null) {
// Remove old editor component
myEditorPanel.remove(myEditorComponent.getExternalComponent());
ThreadUtils.runInUIThreadNoWait(myEditorComponent::dispose);
myEditorComponent = null;
myCurrentlyEditedNode = null;
}
myEditorComponent = editorComponent;
if (myEditorComponent != null) {
// Add new editor component
myEditorComponent = editorComponent;
JComponent externalComponent = myEditorComponent.getExternalComponent();
//HACK to avoid strange gray border in ScrollPane after empty aspect tab
if (externalComponent.getComponent(0) instanceof JBScrollPane) {
((JBScrollPane) externalComponent.getComponent(0)).setBorder(new EmptyBorder(JBUI.emptyInsets()));
((JBScrollPane) externalComponent.getComponent(0)).getInsets().set(0, 0, 0, 0);
}
myEditorPanel.add(externalComponent, BorderLayout.CENTER);
myComponent.validate();
pauseOrResumeHighlighter();
}
}
//---state---
@Override
public EditorState saveState() {
BaseEditorState state = new BaseEditorState();
saveState(state);
return state;
}
protected void saveState(BaseEditorState state) {
EditorContext editorContext = getEditorContext();
if (editorContext != null) {
state.memento = editorContext.getEditorComponentState();
NodeEditorComponent editorComponent = getCurrentEditorComponent();
if (editorComponent != null) {
state.isEditorFocused = editorComponent.getFocusTracker().getEffectiveFocusState();
EditorComponent inspector = editorComponent.getInspector();
if (inspector != null) {
state.inspectorMemento = inspector.getEditorContext().getEditorComponentState();
state.isInspectorFocused = !state.isEditorFocused && inspector.getFocusTracker().getEffectiveFocusState();
}
}
}
}
@Override
public void loadState(@NotNull EditorState state) {
if (!(state instanceof BaseEditorState)) {
return;
}
final BaseEditorState s = (BaseEditorState) state;
final EditorContext editorContext = getEditorContext();
final NodeEditorComponent editorComponent = getCurrentEditorComponent();
if (s.memento == null || editorContext == null || editorComponent == null) {
return;
}
final IdeFocusManager focusManager = myProject.getComponent(IdeFocusManager.class);
executeInEDT(new PrioritizedTask(TaskType.EDITOR_MEMENTO, myType2TaskMap) {
@Override
public void performTask() {
if (editorComponent.isDisposed()) {
return;
}
editorContext.restoreEditorComponentState(s.memento);
editorComponent.getFocusTracker().setEffectiveFocusState(s.isEditorFocused);
if (s.isEditorFocused && focusManager != null) {
focusManager.requestFocus(editorComponent, true);
}
}
});
if (s.inspectorMemento == null) {
return;
}
final EditorComponent inspectorEditorComponent = editorComponent.getInspector();
if (inspectorEditorComponent == null) {
LOG.error("No inspector - memento will not be restored");
return;
}
final EditorContext inspectorEditorContext = inspectorEditorComponent.getEditorContext();
executeInEDT(new PrioritizedTask(TaskType.INSPECTOR_MEMENTO, myType2TaskMap) {
@Override
public void performTask() {
inspectorEditorContext.restoreEditorComponentState(s.inspectorMemento);
inspectorEditorComponent.getFocusTracker().setEffectiveFocusState(s.isInspectorFocused);
if (s.isInspectorFocused && focusManager != null) {
InspectorTool inspectorTool = myProject.getComponent(InspectorTool.class);
if (inspectorTool != null && inspectorTool.isAvailable()) {
inspectorTool.activate();
}
focusManager.requestFocus(inspectorEditorComponent, true);
}
}
});
}
@Override
public void selectNotify() {
setSelected(true);
}
@Override
public void deselectNotify() {
setSelected(false);
}
private void setSelected(boolean selected) {
mySelected = selected;
pauseOrResumeHighlighter();
}
public abstract static class PrioritizedTask implements Runnable {
private final TaskType myType;
private final Map<TaskType, PrioritizedTask> myType2TaskMap;
public PrioritizedTask(TaskType type, Map<TaskType, PrioritizedTask> type2TaskMap) {
synchronized (type2TaskMap) {
myType = type;
myType2TaskMap = type2TaskMap;
myType2TaskMap.put(myType, this);
}
}
@Override
public void run() {
synchronized (myType2TaskMap) {
if (myType2TaskMap.get(myType) != this) {
return;
}
myType2TaskMap.remove(myType);
}
performTask();
}
protected abstract void performTask();
}
public enum TaskType {
EDIT_NODE,
EDITOR_MEMENTO,
INSPECTOR_MEMENTO,
UPDATE_PROPERTIES
}
public static class BaseEditorState implements EditorState {
private static final String MEMENTO = "memento";
private static final String INSPECTOR_MEMENTO = "inspectorMemento";
private static final String FOCUSED_COMPONENT_EDITOR = "editorFocused";
private static final String FOCUSED_COMPONENT_INSPECTOR = "inspectorFocused";
private EditorComponentState memento;
private boolean isEditorFocused;
private EditorComponentState inspectorMemento;
private boolean isInspectorFocused;
@Override
public void save(Element e) {
if (memento != null) {
Element mementoElem = new Element(MEMENTO);
MementoPersistence.saveMemento(memento, mementoElem);
e.addContent(mementoElem);
}
if (inspectorMemento != null) {
Element mementoElem = new Element(INSPECTOR_MEMENTO);
MementoPersistence.saveMemento(inspectorMemento, mementoElem);
e.addContent(mementoElem);
}
if (isEditorFocused) {
e.setAttribute(FOCUSED_COMPONENT_EDITOR, Boolean.TRUE.toString());
}
if (isInspectorFocused) {
e.setAttribute(FOCUSED_COMPONENT_EDITOR, Boolean.TRUE.toString());
}
}
@Override
public void load(Element e) {
Element mementoElem = e.getChild(MEMENTO);
if (mementoElem != null) {
memento = MementoPersistence.loadMemento(mementoElem);
}
Element inspectorMementoElem = e.getChild(INSPECTOR_MEMENTO);
if (inspectorMementoElem != null) {
inspectorMemento = MementoPersistence.loadMemento(inspectorMementoElem);
}
isEditorFocused = Boolean.parseBoolean(e.getAttributeValue(FOCUSED_COMPONENT_EDITOR));
isInspectorFocused = Boolean.parseBoolean(e.getAttributeValue(FOCUSED_COMPONENT_INSPECTOR));
}
@Override
public void clearSessionState() {
if (memento != null) {
memento.clearSessionState();
}
if (inspectorMemento != null) {
inspectorMemento.clearSessionState();
}
}
@Override
public int hashCode() {
int result = memento != null ? memento.hashCode() : 0;
result = 31 * result + (inspectorMemento != null ? inspectorMemento.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BaseEditorState that = (BaseEditorState) o;
return EqualUtil.equals(that.memento, memento) && EqualUtil.equals(that.inspectorMemento, inspectorMemento) &&
that.isEditorFocused == isEditorFocused && that.isInspectorFocused == isInspectorFocused;
}
}
}