/* * 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())); } } }