/*
* 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.smodel;
import jetbrains.mps.ide.icons.IconManager;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNodeEx;
import jetbrains.mps.ide.ui.tree.TreeElement;
import jetbrains.mps.ide.ui.tree.TreeNodeTextSource;
import jetbrains.mps.ide.ui.tree.TreeNodeVisitor;
import jetbrains.mps.smodel.DependencyRecorder;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.util.ConditionalIterable;
import jetbrains.mps.util.InternUtil;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.util.SNodePresentationComparator;
import jetbrains.mps.util.ToStringComparator;
import jetbrains.mps.util.annotation.ToRemove;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeAccessUtil;
import org.jetbrains.mps.util.Condition;
import javax.swing.Icon;
import javax.swing.tree.DefaultTreeModel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class SModelTreeNode extends MPSTreeNode implements TreeElement {
private final SModel myModelDescriptor;
private final TreeNodeTextSource<SModelTreeNode> myTextSource;
private List<SModelTreeNode> myChildModelTreeNodes = new ArrayList<SModelTreeNode>();
private boolean myInitialized = false;
private boolean myInitializing = false;
private List<SNodeGroupTreeNode> myRootGroups = new ArrayList<SNodeGroupTreeNode>();
private final Condition<SNode> myNodesCondition;
private final DependencyRecorder<SNodeTreeNode> myDependencyRecorder = new DependencyRecorder<SNodeTreeNode>();
private Map<String, PackageNode> myPackageNodes = new HashMap<String, PackageNode>();
private Icon myBaseIcon;
@Deprecated
@ToRemove(version = 3.3)
public SModelTreeNode(SModel model,
String label,
boolean showLongName,
Condition<SNode> condition,
int countNamePart) {
myTextSource = showLongName ? new LongModelNameText() : new ShortModelNameText();
myModelDescriptor = model;
myNodesCondition = condition;
setUserObject(NameUtil.getModelLongName(model));
if (myModelDescriptor != null) {
setNodeIdentifier(myModelDescriptor.toString());
} else {
setNodeIdentifier("");
}
setText(myTextSource.calculateText(this));
setBaseIcon(IconManager.getIconFor(model));
setIcon(IconManager.getIconFor(model));
}
public SModelTreeNode(@NotNull SModel model) {
this(model, new LongModelNameText());
}
public SModelTreeNode(@NotNull SModel model, @NotNull TreeNodeTextSource<SModelTreeNode> textSource) {
myModelDescriptor = model;
myTextSource = textSource;
setUserObject(model.getName().getLongName());
setNodeIdentifier(model.toString());
Icon icon = IconManager.getIconFor(model);
setIcon(icon);
setBaseIcon(icon);
myNodesCondition = Condition.TRUE_CONDITION;
// invocation of external code with not completely initialized this is bad. Perhaps, shall rely on doUpdatePresentation invoked from onAdd()?
setText(myTextSource.calculateText(this));
}
public void setBaseIcon(@Nullable Icon baseIcon) {
myBaseIcon = baseIcon;
}
/**
* Base/default icon is not necessarily the one actually displayed, which may include different overlays,
* like 'modified' indicator.
*
* @return base icon, if any
*/
@Nullable
public Icon getBaseIcon() {
// XXX How come base icon is only for models, not for SNodeTreeNode as well?
return myBaseIcon;
}
/**
* @deprecated renamed to {@link #getBaseIcon()}
*/
@Deprecated
@ToRemove(version = 3.3)
public Icon getDefaultIcon() {
return getBaseIcon();
}
@Override
public boolean isLeaf() {
return false;
}
public boolean hasModelsUnder() {
return !getSubfolderSModelTreeNodes().isEmpty();
}
//do not use!
public DependencyRecorder<SNodeTreeNode> getDependencyRecorder() {
return myDependencyRecorder;
}
protected SNodeGroupTreeNode getNodeGroupFor(SNode node) {
boolean packagesEnabled = true;
if (!packagesEnabled) {
return null;
}
String nodePackage = SNodeAccessUtil.getProperty(node, SNodeUtil.property_BaseConcept_virtualPackage);
if (nodePackage != null && !"".equals(nodePackage)) {
String[] packages = nodePackage.split("\\.");
String pack = "";
PackageNode current = null;
for (String aPackage : packages) {
if (pack.length() > 0) {
pack += ".";
}
pack += aPackage;
if (!myPackageNodes.containsKey(pack)) {
PackageNode parent = current;
current = new PackageNode(this, aPackage, parent);
myPackageNodes.put(pack, current);
current.registerInModelNode(this, parent);
}
current = myPackageNodes.get(pack);
}
return current;
}
return null;
}
void register(SNodeGroupTreeNode parent, SNodeGroupTreeNode groupTreeNode) {
if (parent == null) {
int index = -1;
for (int i = 0; i < myRootGroups.size(); i++) {
SNodeGroupTreeNode group = myRootGroups.get(i);
String rp = groupTreeNode.toString();
String cp = group.toString();
if (rp.compareTo(cp) < 0) {
index = i;
break;
}
}
if (index == -1) {
index = myRootGroups.size();
}
myRootGroups.add(index, groupTreeNode);
if (myInitialized || myInitializing) {
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
treeModel.insertNodeInto(groupTreeNode, this, index + myChildModelTreeNodes.size());
}
} else {
int index = -1;
int groupCount = 0;
for (int i = 0; i < parent.getChildCount(); i++) {
if (!(parent.getChildAt(i) instanceof SNodeGroupTreeNode)) {
break;
}
groupCount++;
SNodeGroupTreeNode group = (SNodeGroupTreeNode) parent.getChildAt(i);
String rp = groupTreeNode.toString();
String cp = group.toString();
if (rp.compareTo(cp) < 0) {
index = i;
break;
}
}
if (index == -1) {
index = groupCount;
}
if (myInitialized || myInitializing) {
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
treeModel.insertNodeInto(groupTreeNode, parent, index);
} else {
parent.insert(groupTreeNode, index);
}
}
}
public void groupBecameEmpty(SNodeGroupTreeNode node) {
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
myRootGroups.remove(node);
MPSTreeNode parent = (MPSTreeNode) node.getParent();
if (node.isAutoDelete()) {
treeModel.removeNodeFromParent(node);
}
if (parent instanceof SNodeGroupTreeNode && parent.getChildCount() == 0) {
groupBecameEmpty((SNodeGroupTreeNode) parent);
}
if (node instanceof PackageNode) {
myPackageNodes.remove(((PackageNode) node).getPackage());
}
}
public SModel getModel() {
return myModelDescriptor;
}
@NotNull
public final SNodeTreeNode createSNodeTreeNode(SNode node) {
return createSNodeTreeNode(node, (String) null);
}
@NotNull
public final SNodeTreeNode createSNodeTreeNode(SNode node, Condition<SNode> condition) {
return createSNodeTreeNode(node, null, condition);
}
@NotNull
public final SNodeTreeNode createSNodeTreeNode(SNode node, String role) {
return createSNodeTreeNode(node, role, Condition.TRUE_CONDITION);
}
@NotNull
public SNodeTreeNode createSNodeTreeNode(SNode node, String role, Condition<SNode> condition) {
return new SNodeTreeNode(node, role, condition);
}
@Override
public boolean isInitialized() {
return myInitialized;
}
public boolean isSubfolderModel(@NotNull SModel candidate) {
final String modelName = myModelDescriptor.getName().getLongName();
String candidateName = candidate.getName().getLongName();
if (!candidateName.startsWith(modelName) || modelName.equals(candidateName)) {
return false;
}
if (candidateName.charAt(modelName.length()) == '.') {
String modelStereotype = myModelDescriptor.getName().getStereotype();
String candidateStereotype = candidate.getName().getStereotype();
if (!modelStereotype.equals(candidateStereotype)) {
return false;
}
String shortName = candidateName.substring(modelName.length() + 1);
if (shortName.indexOf('.') > 0) {
String maxPackage = candidateName.substring(0, candidateName.lastIndexOf('.'));
// Imagine, we need to figure out whether a.b.c.d is subfolder model of a.b (iow, 'a.b'.isSubfolderModel('a.b.c.d'))
// Guess, 'subfolder' means 'shall be displayed as my immediate child' here.
// As I understood, the idea is to check whether there's model a.b.c (sic!) inside same module and thus candidate model
// shall get reported as its subfolder rather than that of myModelDescriptor.
// XXX there's a defect that two models like a.b.x.y1 and a.b.x.y2 (namely, jetbrains.mps.ide solution,
// findusages.findalgorithm.finders.specific and findusages.view.optionseditor)
// are visualized as distinct x.y1 and x.y2 when there's no a.b.x model (i.e. they are not grouped under
// same 'x' node unless there are nodes in 'x' model).
// Another defect in present implementation is that it doesn't take into account actual set of visualized models
// and assumes all models of a module are visible, but this can't be fixed unless the whole approach (see below) is fixed.
// FIXME This whole code with implicit assumption of iterating over models from the same module, and recursive processing of
// sorted(!) collection of models with int index (i.e. SModelsSubtree.buildChildModels()) needs refactoring.
// Sorting ensures we didn't create SModelTreeNode for a child before the one for the parent.
// Can't refactor right away as mbeddr subclasses our tree nodes and heavily relies on implementation.
final Stream<SModel> modelsInMyModule = StreamSupport.stream(myModelDescriptor.getModule().getModels().spliterator(), false);
if (modelsInMyModule.anyMatch(m -> maxPackage.equals(m.getName().getLongName()))) {
return false;
}
}
return true;
}
return false;
}
public void addChildModel(SModelTreeNode model) {
myChildModelTreeNodes.add(model);
}
public List<SModelTreeNode> getSubfolderSModelTreeNodes() {
return Collections.unmodifiableList(myChildModelTreeNodes);
}
public List<SModelTreeNode> getAllSubfolderSModelTreeNodes() {
List<SModelTreeNode> result = new ArrayList<SModelTreeNode>();
if (myChildModelTreeNodes.isEmpty()) {
result.add(this);
} else {
for (SModelTreeNode treeNode : myChildModelTreeNodes) {
result.addAll(treeNode.getAllSubfolderSModelTreeNodes());
}
}
return result;
}
@Override
protected void doUpdate() {
myInitialized = false;
this.removeAllChildren();
}
@Override
protected void doUpdatePresentation() {
setText(myTextSource.calculateText(this));
}
@Override
protected void doInit() {
try {
myInitializing = true;
removeAllChildren();
myPackageNodes.clear();
myRootGroups.clear();
for (SModelTreeNode newChildModel : myChildModelTreeNodes) {
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
int index = myChildModelTreeNodes.indexOf(newChildModel);
treeModel.insertNodeInto(newChildModel, this, index);
}
org.jetbrains.mps.openapi.model.SModel model = getModel();
List<SNode> filteredRoots = new ArrayList<SNode>();
for (SNode node : new ConditionalIterable<SNode>(model.getRootNodes(), myNodesCondition)) {
filteredRoots.add(node);
}
Comparator<Object> childrenComparator = getTree().getChildrenComparator();
if (childrenComparator != null) {
Collections.sort(filteredRoots, childrenComparator);
} else {
Collections.sort(filteredRoots, new SNodePresentationComparator());
}
for (SNode sortedRoot : filteredRoots) {
MPSTreeNodeEx treeNode = createSNodeTreeNode(sortedRoot, myNodesCondition);
MPSTreeNode group = getNodeGroupFor(sortedRoot);
if (group != null) {
group.add(treeNode);
} else {
add(treeNode);
}
}
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
treeModel.nodeStructureChanged(this);
} finally {
myInitialized = true;
myInitializing = false;
}
}
@Override
protected final boolean canBeOpened() {
return false;
}
public void insertRoots(Set<SNode> addedRoots) {
if (addedRoots.isEmpty()) return;
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
final ArrayList<SNode> allRoots = new ArrayList<SNode>();
for (SNode root1 : getModel().getRootNodes()) {
allRoots.add(root1);
}
Collections.sort(allRoots, new ToStringComparator(true));
List<SNode> added = new ArrayList<SNode>(addedRoots);
Collections.sort(added, new Comparator<SNode>() {
@Override
public int compare(SNode o1, SNode o2) {
return new Integer(allRoots.indexOf(o1)).compareTo(allRoots.indexOf(o2));
}
});
//Assuming that "added" as well as targetNode.children for all targetNodes are sorted already,
//so we merge the two by always remembering the last insertion point
final HashMap<MPSTreeNode, Integer> lastPositions = new HashMap<MPSTreeNode, Integer>();
for (SNode root : added) {
SNodeTreeNode nodeToInsert = new SNodeTreeNode(root);
MPSTreeNode targetNode = getNodeGroupFor(root);
if (targetNode == null) {
targetNode = SModelTreeNode.this;
}
int index = -1;
Integer lastPosition = lastPositions.get(targetNode);
if (lastPosition == null) lastPosition = 0;
for (int i = lastPosition; i < targetNode.getChildCount(); i++) {
if (!(targetNode.getChildAt(i) instanceof SNodeTreeNode)) {
continue;
}
SNodeTreeNode child = (SNodeTreeNode) targetNode.getChildAt(i);
String rp = root.toString();
String cp = child.getSNode().toString();
if (rp.compareTo(cp) < 0) {
index = i;
break;
}
}
if (index == -1) {
index = targetNode.getChildCount();
}
lastPositions.put(targetNode, index + 1);
treeModel.insertNodeInto(nodeToInsert, targetNode, index);
}
}
@Override
public void accept(@NotNull TreeNodeVisitor visitor) {
visitor.visitModelNode(this);
}
public static class LongModelNameText implements TreeNodeTextSource<SModelTreeNode> {
@Override
public String calculateText(SModelTreeNode treeNode) {
SModel model = treeNode.getModel();
return model == null ? "<null>" : InternUtil.intern(model.getModelName());
}
}
public static class ShortModelNameText implements TreeNodeTextSource<SModelTreeNode> {
@Override
public String calculateText(SModelTreeNode treeNode) {
SModel model = treeNode.getModel();
// model long name is likely to be encountered more than once. Does it make sense to intern short name?
// It's indeed saves some space for aspect models (all are named the same, but are short) at expense of occupied slot in InternUtil.
return model == null ? "<null>" : NameUtil.shortNameFromLongName(InternUtil.intern(model.getModelName()));
}
}
}