/* * 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.ide.ui.tree; import com.intellij.openapi.editor.colors.ColorKey; import com.intellij.openapi.editor.colors.EditorColorsManager; import jetbrains.mps.ide.icons.IdeIcons; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.Icon; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeNode; import java.awt.Color; import java.awt.Font; import java.awt.font.TextAttribute; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * @author Kostik */ public class MPSTreeNode extends DefaultMutableTreeNode implements Iterable<MPSTreeNode> { private static final Logger LOG = LogManager.getLogger(MPSTreeNode.class); private MPSTree myTree; private boolean myAdded; private Icon myCollapsedIcon = IdeIcons.CLOSED_FOLDER; private Icon myExpandedIcon = IdeIcons.OPENED_FOLDER; private String myNodeIdentifier; private String myText; private String myAdditionalText = null; private String myTooltipText; private Color myColor = EditorColorsManager.getInstance().getGlobalScheme().getColor(ColorKey.createColorKey("FILESTATUS_NOT_CHANGED")); private int myFontStyle = Font.PLAIN; private boolean myAutoExpandable = true; private ErrorState myErrorState = ErrorState.NONE; private ErrorState myCombinedErrorState = ErrorState.NONE; private final Object myTreeMessagesLock = new Object(); private List<TreeMessage> myTreeMessages = null; private Map<TextAttribute, Object> myFontAttributes = new HashMap<TextAttribute, Object>(); private int myToggleClickCount = 2; public MPSTreeNode() { super(null); } public MPSTreeNode(Object userObject) { super(userObject); } @Override @SuppressWarnings("unchecked") public Iterator<MPSTreeNode> iterator() { if (children == null) { return Collections.<MPSTreeNode>emptySet().iterator(); } return children.iterator(); } public MPSTree getTree() { if (myTree == null && getParent() instanceof MPSTreeNode) { return ((MPSTreeNode) getParent()).getTree(); } return myTree; } /** * returns the closest ancestor (nodes or the containing tree) which implements the given class */ public <T> T getAncestor(@NotNull Class<T> cls) { TreeNode parent = getParent(); while (parent != null) { if (cls.isInstance(parent)) { return cls.cast(parent); } parent = parent.getParent(); } if (myTree != null && cls.isInstance(myTree)) { return cls.cast(myTree); } return null; } public void setTree(MPSTree tree) { myTree = tree; } public boolean isInitialized() { return true; } public boolean hasInfiniteSubtree() { return false; } public void doubleClick() { } protected void onRemove() { getTree().fireTreeNodeRemoved(this); } protected void onAdd() { updatePresentation(); getTree().fireTreeNodeAdded(this); } /** * Deemed for tree clients to ensure node is initialized (i.e. has its children). * If the node is already {@link #isInitialized() initialized}, does nothing. * Otherwise, {@link jetbrains.mps.ide.ui.tree.MPSTree#performInit(MPSTreeNode)} delegates} to owning tree, if any, * to perform actual initialization, with respect to tree's considerations (e.g. might wrap with model read action or * "Loading..." notification nodes). * Tree shall call {@link #doInit()} at some point where actual node initialization takes place. * Although not final, extra care should be taken if overriding (FIXME perhaps, shall make final, and move isInitialized field here as well). */ public void init() { if (isInitialized()) { return; } MPSTree tree = getTree(); if (tree != null) { tree.performInit(this); } else { doInit(); } } /** * This method shall not be invoked by code outside of MPSTree framework. * Subclasses shall override and perform actual node initialization here. * Default implementation does nothing, subclasses don't need to invoke <code>super.doInit()</code> */ protected void doInit() { } public void updateSubTree() { getTree().runRebuildAction(new Runnable() { @Override public void run() { update(); } }, true); } public void update() { doUpdate(); ((DefaultTreeModel) getTree().getModel()).nodeStructureChanged(this); } protected void doUpdate() { } @Override public void remove(int childIndex) { if (myAdded && getTree() != null && !getTree().isDisposed()) { ((MPSTreeNode) getChildAt(childIndex)).removeThisAndChildren(); } super.remove(childIndex); updateErrorState(); } @Override public void insert(MutableTreeNode newChild, int childIndex) { super.insert(newChild, childIndex); if (myAdded && getTree() != null && !getTree().isDisposed()) { ((MPSTreeNode) getChildAt(childIndex)).addThisAndChildren(); } updateErrorState(); } public boolean hasChild(MutableTreeNode node) { for (int i = 0; i < getChildCount(); i++) { if (getChildAt(i) == node) return true; } return false; } final void removeThisAndChildren() { if (!myAdded) { throw new IllegalStateException( String.format("Trying to remove tree node which have not been added, tree=%s, node=%s", myTree != null ? myTree.getClass().getName() : "null", getClass().getName())); } try { onRemove(); } catch (Throwable t) { LOG.error(null, t); } myAdded = false; if (!isInitialized()) { return; } for (MPSTreeNode node : this) { node.removeThisAndChildren(); } setParent(null); children = null; } final void addThisAndChildren() { if (myAdded) { throw new IllegalStateException( String.format("Trying to add tree node which have already been added, tree=%s, node=%s", myTree != null ? myTree.getClass().getName() : "null", getClass().getName())); } try { onAdd(); } catch (Throwable t) { LOG.error(null, t); } myAdded = true; if (!isInitialized()) { return; } for (int i = 0; i < getChildCount(); i++) { MPSTreeNode node = (MPSTreeNode) getChildAt(i); node.addThisAndChildren(); } } public MPSTreeNode findExactChildWith(Object userObject) { for (MPSTreeNode child : this) { if (child.getUserObject() == userObject) return child; } return null; } /** * Ignores subtree of nodes that have not been initialized yet. */ @Nullable public final MPSTreeNode findDescendantWith(Object userObject) { if (getUserObject() == null ? userObject == null : getUserObject().equals(userObject)) return this; if (isInitialized()) { for (int i = 0; i < getChildCount(); i++) { MPSTreeNode result = ((MPSTreeNode) getChildAt(i)).findDescendantWith(userObject); if (result != null) return result; } } return null; } /** * Default value is: 2 */ public int getToggleClickCount() { return myToggleClickCount; } public void setToggleClickCount(int clickCount) { myToggleClickCount = clickCount; } //updates and refreshes tree public void renewPresentation() { final MPSTree tree = getTree(); if (tree == null || tree.isDisposed()) { return; } updatePresentation(); updateNodePresentationInTree(); } //todo make final protected void updatePresentation() { setColor(EditorColorsManager.getInstance().getGlobalScheme().getColor(ColorKey.createColorKey("FILESTATUS_NOT_CHANGED"))); doUpdatePresentation(); if (myTree == null) { myTree = getTree(); } if (myTree != null) { myTree.fireTreeNodeUpdated(this); } Color c = null; String additionalText = null; synchronized (myTreeMessagesLock) { if (myTreeMessages != null) { int maxColorPriority = Integer.MIN_VALUE; int maxAdditionalTextPriority = Integer.MIN_VALUE; for (TreeMessage message : myTreeMessages) { if (maxColorPriority < message.getPriority() && message.alternatesColor()) { c = message.getColor(); } if (maxAdditionalTextPriority < message.getPriority() && message.hasAdditionalText()) { additionalText = message.getAdditionalText(); } } } } if (c != null) { setColor(c); } if (additionalText != null) { setAdditionalText(additionalText); } } public void updatePresentation(final boolean reloadSubTree, final boolean updateAncestors) { renewPresentation(); if (reloadSubTree) { updateSubTree(); } if (updateAncestors) { updateAncestorsPresentationInTree(); } } /** * Attach an extra message to a node. Messages are identified by their {@link jetbrains.mps.ide.ui.tree.TreeMessageOwner owner}. * This method may be invoked from any thread, and doesn't trigger UI update, use {@link #renewPresentation()} from correct (EDT/UI) thread * if needed (e.g. if messages are attached the moment tree is being constructed, there's no reason to renew each node individually, * they get a chance to update them once tree becomes visible) * @param message message to attach */ public void addTreeMessage(@NotNull TreeMessage message) { synchronized (myTreeMessagesLock) { if (myTreeMessages == null) { myTreeMessages = new ArrayList<TreeMessage>(1); } myTreeMessages.add(message); } } /** * Detach all messages of the specified owner. * This method can be invoked from any thread. * To trigger UI update, use {@link #renewPresentation()} from correct (EDT/UI) thread. * @param owner identifies messages to remove * @return set of detached messages, or empty collection if none found */ @NotNull public Set<TreeMessage> removeTreeMessages(TreeMessageOwner owner) { Set<TreeMessage> result = new HashSet<TreeMessage>(1); if (owner == null) { return result; } final ArrayList<TreeMessage> copy; synchronized (myTreeMessagesLock) { if (myTreeMessages == null) { return result; } copy = new ArrayList<TreeMessage>(myTreeMessages); } for (TreeMessage message : copy) { if (owner.equals(message.getOwner())) { result.add(message); } } synchronized (myTreeMessagesLock) { myTreeMessages.removeAll(result); } return result; } protected void doUpdatePresentation() { } public final Icon getIcon(boolean expanded) { if (expanded) { return myExpandedIcon; } else { return myCollapsedIcon; } } public final void setIcon(Icon newIcon, boolean expanded) { if (expanded) { myExpandedIcon = newIcon; } else { myCollapsedIcon = newIcon; } } public final void setIcon(Icon newIcon) { setIcon(newIcon, true); setIcon(newIcon, false); } public final Color getColor() { return myColor; } public final void setColor(Color color) { myColor = color; } public final int getFontStyle() { return myFontStyle; } public final void setFontStyle(int fontStyle) { myFontStyle = fontStyle; } public final void addFontAttribute(TextAttribute key, Object value) { myFontAttributes.put(key, value); } public final Map getFontAttributes() { return myFontAttributes; } @NotNull public final String getNodeIdentifier() { if (myNodeIdentifier == null) { // extra info for assertion failed due to MPS-12305 String parentId = null; if (getParent() instanceof MPSTreeNode) { parentId = ((MPSTreeNode) getParent()).getNodeIdentifier(); } throw new IllegalStateException("MPSTreeNode identifier cannot be null, class=" + getClass().getName() + ", parent id=" + parentId); } else { return myNodeIdentifier; } } public final void setNodeIdentifier(@NotNull String newNodeIdentifier) { myNodeIdentifier = newNodeIdentifier; } public final String getAdditionalText() { return myAdditionalText; } public final void setAdditionalText(String newAdditionalText) { myAdditionalText = newAdditionalText; } public final String getText() { if (myText == null) { return getNodeIdentifier(); } else { return myText; } } public final void setText(String text) { myText = text; } public final String getTooltipText() { return myTooltipText; } public final void setTooltipText(String tooltipText) { myTooltipText = tooltipText; } public final boolean isErrorState() { return myErrorState == ErrorState.ERROR; } public final void setErrorState(ErrorState state) { myErrorState = state; updateErrorState(); } public final ErrorState getErrorState() { return myErrorState; } public final ErrorState getAggregatedErrorState() { return myCombinedErrorState; } protected void updateErrorState() { ErrorState state = ErrorState.NONE; if (propogateErrorUpwards()) { for (MPSTreeNode node : this) { state = state.combine(node.getAggregatedErrorState()); } } myCombinedErrorState = state.combine(myErrorState); if (getParent() != null) { ((MPSTreeNode) getParent()).updateErrorState(); } } protected boolean propogateErrorUpwards() { return true; } public String toString() { return getText(); } public final boolean isAutoExpandable() { return myAutoExpandable; } public final void setAutoExpandable(boolean autoExpandable) { myAutoExpandable = autoExpandable; } public final void updateNodePresentationInTree() { if (getTree() == null) return; ((DefaultTreeModel) getTree().getModel()).nodeChanged(this); } public void updateAncestorsPresentationInTree() { updateNodePresentationInTree(); if (getParent() == null) return; ((MPSTreeNode) getParent()).updateAncestorsPresentationInTree(); } protected boolean canBeOpened() { return true; } public void autoscroll() { } public boolean isLoadingEnabled() { return false; } }