/*
* Copyright 2003-2016 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.nodeEditor;
import com.intellij.util.containers.SortedList;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.nodeEditor.inspector.InspectorEditorComponent;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.openapi.editor.update.UpdaterListener;
import jetbrains.mps.openapi.editor.update.UpdaterListenerAdapter;
import jetbrains.mps.util.containers.ManyToManyMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.ModelAccess;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class NodeHighlightManager implements EditorMessageOwner {
private static final Comparator<SimpleEditorMessage> EDITOR_MESSAGES_COMPARATOR = new Comparator<SimpleEditorMessage>() {
@Override
public int compare(SimpleEditorMessage m1, SimpleEditorMessage m2) {
if (m1.getPriority() != m2.getPriority()) {
return m1.getPriority() - m2.getPriority();
}
return m1.getStatus().ordinal() - m2.getStatus().ordinal();
}
};
// TODO: replace myMessagesLock usages with this ?
private final Object myMessagesLock = new Object();
@NotNull
private final EditorComponent myEditor;
private final Set<SimpleEditorMessage> myMessages = new HashSet<SimpleEditorMessage>();
private final Map<EditorMessageOwner, Set<SimpleEditorMessage>> myOwnerToMessages = new HashMap<EditorMessageOwner, Set<SimpleEditorMessage>>();
private final ManyToManyMap<SimpleEditorMessage, SNode> myMessagesToNodes = new ManyToManyMap<SimpleEditorMessage, SNode>();
/**
* All caches are synchronized using myMessagesLock
*/
private Map<EditorCell, List<SimpleEditorMessage>> myMessagesCache = Collections.emptyMap();
private volatile boolean myRebuildMessagesCache = false;
private final UpdaterListener myRebuildListener = new RebuildMessagesOnEditorUpdate();
private Set<EditorMessageIconRenderer> myIconRenderersCache = new HashSet<EditorMessageIconRenderer>();
private volatile boolean myRebuildIconRenderersCacheFlag = false;
private boolean myDisposed = false;
public NodeHighlightManager(@NotNull EditorComponent editor) {
myEditor = editor;
myEditor.getUpdater().addListener(myRebuildListener);
}
/**
* scheduling lazy rebuild of myMessagesCache and myIconRenderersCache
* this method can be called from any thread
* this method should be called inside synchronize(myMessagesLock) block only
*/
private void invalidateMessagesCaches() {
myRebuildMessagesCache = true;
myRebuildIconRenderersCacheFlag = true;
}
private Map<EditorCell, List<SimpleEditorMessage>> getMessagesCache() {
synchronized (myMessagesLock) {
return myMessagesCache;
}
}
private void refreshMessagesCache() {
assert ThreadUtils.isInEDT() : "refreshMessagesCache() should be called from EDT only";
assert getModelAccess().canRead() : "refreshMessagesCache() should be called inside model read action only";
synchronized (myMessagesLock) {
if (!myRebuildMessagesCache) {
return;
}
myRebuildMessagesCache = false;
if (myMessages.isEmpty() || myEditor.getRootCell() == null) {
myMessagesCache = Collections.emptyMap();
} else {
myMessagesCache = new HashMap<EditorCell, List<SimpleEditorMessage>>();
rebuildMessages(myEditor.getRootCell());
}
}
}
private ModelAccess getModelAccess() {
return myEditor.getRepository().getModelAccess();
}
/**
* part of myMessagesCache rebuild process
* this method should be called inside synchronize(myMessagesLock) block only
*/
private void rebuildMessages(EditorCell root) {
List<SimpleEditorMessage> messages = calculateMessages(root);
if (!messages.isEmpty()) {
myMessagesCache.put(root, messages);
}
if (root instanceof EditorCell_Collection) {
for (EditorCell cell : (EditorCell_Collection) root) {
rebuildMessages(cell);
}
}
}
public List<SimpleEditorMessage> getMessages(EditorCell cell) {
assert !myDisposed;
List<SimpleEditorMessage> result = getMessagesCache().get(cell);
if (result != null) {
return new ArrayList<SimpleEditorMessage>(result);
}
return Collections.emptyList();
}
/**
* part of myMessagesCache rebuild process
* this method should be called inside synchronize(myMessagesLock) block only
*/
private List<SimpleEditorMessage> calculateMessages(EditorCell cell) {
final SNode node = cell.getSNode();
if (node == null) {
return Collections.emptyList();
}
final List<SimpleEditorMessage> result = new SortedList<SimpleEditorMessage>(EDITOR_MESSAGES_COMPARATOR);
Set<SimpleEditorMessage> messageSet = myMessagesToNodes.getBySecond(node);
for (SimpleEditorMessage message : messageSet) {
if (!(message instanceof EditorMessage) || ((EditorMessage) message).acceptCell(cell, myEditor)) {
result.add(message);
}
}
if (myEditor.getRootCell() != cell || !(myEditor instanceof InspectorEditorComponent)) {
// the condition above is because an inspector for the node
// does not have cells for some node's children (they are edited in main editor)
// but the cell should not be highlighted only because of this
if (cell.isBig()) {
for (SNode child : node.getChildren()) {
EditorCell cellForChild = myEditor.findNodeCell(child);
if (cellForChild == null) {
getMessagesFromDescendants(child, result);
}
}
}
}
return result;
}
private void getMessagesFromDescendants(SNode nodeWithoutCell, List<SimpleEditorMessage> messages) {
messages.addAll(myMessagesToNodes.getBySecond(nodeWithoutCell));
for (SNode child : nodeWithoutCell.getChildren()) {
EditorCell cellForChild = myEditor.findNodeCell(child);
if (cellForChild == null) {
getMessagesFromDescendants(child, messages);
}
}
}
private void addMessage(SimpleEditorMessage m) {
if (m.getNode() == null) {
return;
}
EditorMessageOwner owner = m.getOwner();
if (!myOwnerToMessages.containsKey(owner)) {
myOwnerToMessages.put(owner, new HashSet<SimpleEditorMessage>());
}
myOwnerToMessages.get(owner).add(m);
myMessages.add(m);
myMessagesToNodes.addLink(m, m.getNode());
}
private boolean removeMessage(SimpleEditorMessage m) {
if (m == null) {
return false;
}
EditorMessageOwner owner = m.getOwner();
Set<SimpleEditorMessage> messages = myOwnerToMessages.get(owner);
if (messages != null) {
messages.remove(m);
if (messages.isEmpty()) {
myOwnerToMessages.remove(owner);
}
}
myMessages.remove(m);
if (myEditor.hasUI()) {
myEditor.getMessagesGutter().remove(m);
}
myMessagesToNodes.clearFirst(m);
return true;
}
public void mark(SimpleEditorMessage message) {
for (SimpleEditorMessage msg : getMessages()) {
if (msg.sameAs(message)) return;
}
synchronized (myMessagesLock) {
addMessage(message);
invalidateMessagesCaches();
}
if (message.showInGutter() && myEditor.hasUI()) {
myEditor.getMessagesGutter().add(message);
}
}
public void unmark(SimpleEditorMessage message) {
synchronized (myMessagesLock) {
if (removeMessage(message)) {
invalidateMessagesCaches();
}
}
}
public boolean clearForOwner(EditorMessageOwner owner) {
return clearForOwner(owner, true);
}
public boolean hasMessages(EditorMessageOwner owner) {
synchronized (myMessagesLock) {
return myOwnerToMessages.containsKey(owner);
}
}
public boolean clearForOwner(EditorMessageOwner owner, boolean repaintAndRebuild) {
boolean result = myEditor.getMessagesGutter().removeMessages(owner);
synchronized (myMessagesLock) {
if (myOwnerToMessages.containsKey(owner)) {
ArrayList<SimpleEditorMessage> messages = new ArrayList<SimpleEditorMessage>(myOwnerToMessages.get(owner));
for (SimpleEditorMessage m : messages) {
removeMessage(m);
}
invalidateMessagesCaches();
}
}
if (repaintAndRebuild) {
repaintAndRebuildEditorMessages();
}
return result;
}
/**
* perform refresh of messages visible in LeftEditorHighlighter
* and repaint associated EditorComponent
*/
public void repaintAndRebuildEditorMessages() {
getModelAccess().runReadInEDT(new Runnable() {
@Override
public void run() {
if (myDisposed) {
return;
}
refreshMessagesCache();
if (myEditor.hasUI()) {
refreshLeftHighlighterMessages();
myEditor.repaintExternalComponent();
}
}
});
}
private void refreshLeftHighlighterMessages() {
assert ThreadUtils.isInEDT() : "refreshLeftHighlighterMessages() should be called from EDT only";
Set<EditorMessageIconRenderer> oldIconRenderers;
Set<EditorMessageIconRenderer> newIconRenderers;
synchronized (myMessagesLock) {
if (!myRebuildIconRenderersCacheFlag) {
return;
}
myRebuildIconRenderersCacheFlag = false;
oldIconRenderers = myIconRenderersCache;
newIconRenderers = myIconRenderersCache = new HashSet<EditorMessageIconRenderer>();
for (SimpleEditorMessage message : myMessages) {
if (message instanceof EditorMessageIconRenderer) {
myIconRenderersCache.add((EditorMessageIconRenderer) message);
}
}
}
myEditor.getLeftEditorHighlighter().removeAllIconRenderers(oldIconRenderers);
myEditor.getLeftEditorHighlighter().addAllIconRenderers(newIconRenderers);
}
public void mark(SNode node, Color color, String messageText, EditorMessageOwner owner) {
if (node == null) return;
mark(new DefaultEditorMessage(node, color, messageText, owner));
}
public void mark(List<? extends SimpleEditorMessage> messages) {
for (SimpleEditorMessage message : messages) {
mark(message);
}
repaintAndRebuildEditorMessages();
}
/**
* Should work even if NodeHighlightManager is disposed because it can be called by the Highlighter thread
*/
public Set<SimpleEditorMessage> getMessages() {
Set<SimpleEditorMessage> result = new HashSet<SimpleEditorMessage>();
synchronized (myMessagesLock) {
result.addAll(myMessages);
}
return result;
}
public List<SimpleEditorMessage> getMessagesFor(SNode node) {
List<SimpleEditorMessage> result = new ArrayList<SimpleEditorMessage>();
synchronized (myMessagesLock) {
result.addAll(myMessagesToNodes.getBySecond(node));
}
return result;
}
public List<SimpleEditorMessage> getMessagesFor(SNode node, EditorMessageOwner owner) {
List<SimpleEditorMessage> result = new ArrayList<SimpleEditorMessage>();
synchronized (myMessagesLock) {
for (SimpleEditorMessage message : myMessagesToNodes.getBySecond(node)) {
if (message.getOwner() == owner) {
result.add(message);
}
}
}
return result;
}
public void dispose() {
assert ThreadUtils.isInEDT() : "dispose() should be called from EDT only";
myDisposed = true;
myEditor.getUpdater().removeListener(myRebuildListener);
}
private class RebuildMessagesOnEditorUpdate extends UpdaterListenerAdapter {
@Override
public void editorUpdated(jetbrains.mps.openapi.editor.EditorComponent editorComponent) {
assert !myDisposed;
if (needRebuild()) {
invalidateMessagesCaches();
repaintAndRebuildEditorMessages();
}
}
private boolean needRebuild() {
if (getMessagesCache().isEmpty()) {
return true;
}
for (EditorCell cell : getMessagesCache().keySet()) {
if (!myEditor.isValid(cell)) {
return true;
}
}
return false;
}
}
}