package greenfoot.guifx.classes;
import bluej.Config;
import bluej.debugger.gentype.Reflective;
import bluej.extensions.ClassNotFoundException;
import bluej.extensions.PackageNotFoundException;
import bluej.extensions.ProjectNotOpenException;
import bluej.pkgmgr.Project;
import bluej.pkgmgr.target.ClassTarget;
import bluej.pkgmgr.target.Target;
import bluej.utility.Utility;
import bluej.utility.javafx.FXPlatformRunnable;
import bluej.utility.javafx.FXRunnable;
import bluej.utility.javafx.JavaFXUtil;
import bluej.views.View;
import bluej.views.ViewFilter;
import bluej.views.ViewFilter.StaticOrInstance;
import greenfoot.guifx.GreenfootStage;
import javafx.beans.binding.ObjectExpression;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import threadchecker.OnThread;
import threadchecker.Tag;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
| The class diagram on the right-hand side of the Greenfoot window.
|
| For now, this is very primitive, but is useful for implementing other Greenfoot functionality.
|
@OnThread(Tag.FXPlatform)
public class GClassDiagram
extends BorderPane{
private final ContextMenu contextMenu;
| Is there a user-made World subclass (i.e. excluding World itself)?
|
public boolean hasUserWorld()
{
return worldClasses.streamAllClasses()
.filter(c -> !c.getQualifiedName().equals("greenfoot.World"))
.findFirst().isPresent();
}
| Is there a World subclass that we could instantiate using a package-visible
| no-args constructor?
|
public boolean hasInstantiatableWorld()
{
return worldClasses.streamAllClasses().anyMatch(c -> {
Target t = project.getTarget(c.getQualifiedName());
if (t instanceof ClassTarget)
{
Class<?> cl = project.loadClass(((ClassTarget)t).getQualifiedName());
if (cl == null)
{
return false;
}
View view = View.getView(cl);
if (!java.lang.reflect.Modifier.isAbstract(cl.getModifiers()))
{
ViewFilter filter = new ViewFilter(StaticOrInstance.INSTANCE, "");
return Arrays.stream(view.getConstructors())
.filter(filter)
.anyMatch(cv -> !cv.hasParameters());
}
}
return false;
});
}
public static enum GClassType { ACTOR, WORLD, OTHER
}
private final ClassDisplaySelectionManager selectionManager = new ClassDisplaySelectionManager();
private final ClassGroup worldClasses;
private final ClassGroup actorClasses;
private final ClassGroup otherClasses;
private final GreenfootStage greenfootStage;
private Project project;
| Construct a GClassDiagram for the given stage.
|
public GClassDiagram(GreenfootStage greenfootStage)
{
this.greenfootStage = greenfootStage;
getStyleClass().add("gclass-diagram");
this.worldClasses = new ClassGroup(greenfootStage);
this.actorClasses = new ClassGroup(greenfootStage);
this.otherClasses = new ClassGroup(greenfootStage);
setTop(worldClasses);
setCenter(actorClasses);
setBottom(otherClasses);
BorderPane.setAlignment(actorClasses, Pos.TOP_LEFT);
BorderPane.setAlignment(otherClasses, Pos.BOTTOM_LEFT);
BorderPane.setMargin(actorClasses, new Insets(20, 0, 20, 0));
BorderPane.setMargin(otherClasses, new Insets(0, 0, ClassGroup.VERTICAL_SPACING, 0));
setMaxWidth(USE_PREF_SIZE);
setMaxHeight(Double.MAX_VALUE);
contextMenu = new ContextMenu();
contextMenu.getItems().add(JavaFXUtil.makeMenuItem(
Config.getString("new.other.class"),
() -> greenfootStage.newNonImageClass(project.getUnnamedPackage(), null), null));
contextMenu.getItems().add(JavaFXUtil.makeMenuItem(
Config.getString("import.action"),
() -> greenfootStage.doImportClass(), null));
setOnContextMenuRequested(e -> {
hideContextMenu();
contextMenu.show(this, e.getScreenX(), e.getScreenY());
e.consume();
});
}
| Hide the context menu if it is showing.
|
protected void hideContextMenu()
{
if (contextMenu.isShowing())
{
contextMenu.hide();
}
}
| Set the project for this class diagram.
|
| @param project the project whose classes to display (may be null)
|
public void setProject(Project project)
{
this.project = project;
if (project != null)
{
recalculateGroups();
setDisable(false);
}
else
{
worldClasses.setClasses(Collections.emptyList());
actorClasses.setClasses(Collections.emptyList());
otherClasses.setClasses(Collections.emptyList());
setDisable(true);
}
}
| Looks up list of ClassTargets in the project, and puts them into a tree structure
| according to their superclass relations, with Actor and World subclasses
| going into their own group. It needs to be called whenever the class inheritance hierarchy changes.
|
public void recalculateGroups()
{
ArrayList<ClassTarget> originalClassTargets = project.getUnnamedPackage().getClassTargets();
HashMap<ClassTarget, Boolean> classTargets = new HashMap<>();
for (ClassTarget originalClassTarget : originalClassTargets)
{
classTargets.put(originalClassTarget, false);
}
List<GClassNode> worldSubclasses = findAllSubclasses("greenfoot.World", classTargets, GClassType.WORLD);
GClassNode worldClassesInfo = new BuiltInGClassNode(GClassType.WORLD, worldSubclasses, this);
worldClasses.setClasses(Collections.singletonList(worldClassesInfo));
List<GClassNode> actorSubclasses = findAllSubclasses("greenfoot.Actor", classTargets, GClassType.ACTOR);
GClassNode actorClassesInfo = new BuiltInGClassNode(GClassType.ACTOR, actorSubclasses, this);
actorClasses.setClasses(Collections.singletonList(actorClassesInfo));
otherClasses.setClasses(findAllSubclasses(null, classTargets, GClassType.OTHER));
}
| Finds all subclasses of the given fully-qualified parent class name. The subclass search
| is recursive, so if you pass "Grandparent", then both "Parent" and "Child" will be found
|* and removed. Any found subclasses will have their boolean changed to true in the given map,
* and only those that currently map to false will be searched.
*
* @param parentClassName The fully-qualified parent class name to search. If null, then all classes
| in the classTargets list will be processed and returned.
| @param classTargets Class targets to search -- only those mapped to false will be searched. If
| they are processed into a GClassNode, their value will be flipped to true.
| @return The list of GClassNode at the requested level (there may be a deeper tree inside).
|
private List findAllSubclasses(String parentClassName, Map<ClassTarget, Boolean> classTargets, GClassType type)
{
List<GClassNode> curLevel = new ArrayList<>();
for (Entry<ClassTarget, Boolean> classTargetAndVal : classTargets.entrySet())
{
if (classTargetAndVal.getValue() == true)
continue;
ClassTarget classTarget = classTargetAndVal.getKey();
bluej.parser.symtab.ClassInfo classInfo = classTarget.analyseSource();
String superClassName = null;
if (classInfo != null)
{
superClassName = classInfo.getSuperclass();
}
else
{
try {
Class<?> javaClass = classTarget.getBClassTarget().getBClass().getJavaClass();
if (javaClass != null)
{
Class<?> superClass = javaClass.getSuperclass();
if (superClass != null)
{
superClassName = superClass.getName();
}
}
}
catch (ProjectNotOpenException | ClassNotFoundException | PackageNotFoundException e) {
e.printStackTrace();
}
}
boolean includeAtThisLevel;
if (parentClassName == null)
{
String finalSuperClassName = superClassName;
includeAtThisLevel = finalSuperClassName == null || !classTargets.keySet().stream().anyMatch(ct -> Objects.equals(ct.getQualifiedName(), finalSuperClassName));
}
else
{
includeAtThisLevel = Objects.equals(superClassName, parentClassName);
}
if (includeAtThisLevel)
{
classTargetAndVal.setValue(true);
List<GClassNode> subClasses = findAllSubclasses(classTarget.getQualifiedName(), classTargets, type);
curLevel.add(makeClassInfo(classTarget, subClasses, type));
}
}
return curLevel;
}
| Adds a new class to the diagram at the appropriate place, based on its superclass,
| and returns the constructed class info.
|
| @return A class info reference for the class added.
|
public LocalGClassNode addClass(ClassTarget classTarget)
{
String superClass = null;
bluej.parser.symtab.ClassInfo info = classTarget.analyseSource();
if (info != null)
{
superClass = info.getSuperclass();
}
if (superClass != null)
{
for (GClassType type : GClassType.values())
{
ClassGroup classGroup;
switch (type)
{
case ACTOR:
classGroup = actorClasses;
break;
case WORLD:
classGroup = worldClasses;
break;
case OTHER:
classGroup = otherClasses;
break;
default:
continue;
}
LocalGClassNode classInfo = findAndAdd(classGroup.getLiveTopLevelClasses(), classTarget, superClass, type);
if (classInfo != null)
{
classGroup.updateAfterAdd();
return classInfo;
}
}
}
LocalGClassNode classInfo = makeClassInfo(classTarget, Collections.emptyList(), GClassType.OTHER);
otherClasses.getLiveTopLevelClasses().add(classInfo);
otherClasses.updateAfterAdd();
return classInfo;
}
| Looks within the whole tree formed by the list of class info for the right place for classTarget.
| If found, create a class info for the class target passed, adds it and return it.
|
| @param classInfos The tree to search. The list itself will not be modified.
| @param classTarget The class to add to the tree.
| @param classTargetSuperClass The super-class of classTarget
| @param type The source type of the class added.
| @return The class info created if right place found and added, null if not.
|
private LocalGClassNode findAndAdd(List<GClassNode> classInfos, ClassTarget classTarget,
String classTargetSuperClass, GClassType type)
{
for (GClassNode classInfo : classInfos)
{
if (classInfo.getQualifiedName().equals(classTargetSuperClass))
{
LocalGClassNode newClassInfo = makeClassInfo(classTarget, Collections.emptyList(), type);
classInfo.add(newClassInfo);
return newClassInfo;
}
else
{
LocalGClassNode newClassInfo = findAndAdd(classInfo.getSubClasses(), classTarget,
classTargetSuperClass, type);
if (newClassInfo != null)
{
return newClassInfo;
}
}
}
return null;
}
| Make the LocalGClassNode for a ClassTarget
|
protected LocalGClassNode makeClassInfo(ClassTarget classTarget, List<GClassNode> subClasses, GClassType type)
{
return new LocalGClassNode(this, classTarget, subClasses, type);
}
| Make a context menu item with the given text and action, and the inbuilt-menu-item
| style (which shows up as dark-red, and italic on non-Mac)
|
public static MenuItem contextInbuilt(String text, FXPlatformRunnable action)
{
MenuItem menuItem = JavaFXUtil.makeMenuItem(text, action, null);
JavaFXUtil.addStyleClass(menuItem, ClassTarget.MENU_STYLE_INBUILT);
return menuItem;
}
| Gets the currently selected class target in the diagram. May be null if no selection,
| or if the selection is a class outside the default package (e.g. greenfoot.World)
|
public ClassTarget getSelectedClassTarget()
{
ClassDisplay selected = selectionManager.getSelected();
if (selected != null)
{
Target target = project.getUnnamedPackage().getTarget(selected.getQualifiedName());
if (target instanceof ClassTarget)
{
return (ClassTarget) target;
}
}
return null;
}
| Gets the selection manager for this class diagram
|
public ClassDisplaySelectionManager getSelectionManager()
{
return selectionManager;
}
| Gets the GreenfootStage which contains this class diagram
|
public GreenfootStage getGreenfootStage()
{
return greenfootStage;
}
| Save class-related properties to the given property map.
|
public void save(Properties p)
{
worldClasses.saveImageSelections(p);
actorClasses.saveImageSelections(p);
otherClasses.saveImageSelections(p);
}
| Get the name of the image file for an actor class (searching up the class hierarchy
| if there is no specific image set for the specified class). May return null.
|
public String getImageForActorClass(Reflective r)
{
List<Reflective> inheritanceChain = new ArrayList<Reflective>();
inheritanceChain.add(r);
Reflective[] superClass = new Reflective[1];
while (r != null && ! r.getName().equals("greenfoot.Actor"))
{
superClass[0] = null;
r.getSuperTypesR().stream()
.filter(s -> ! s.isInterface())
.findFirst()
.ifPresent(e -> { superClass[0] = e; inheritanceChain.add(e);
});
r = superClass[0];
}
if (r == null)
{
return null;
}
int i = inheritanceChain.size() - 1;
ClassGroup group = actorClasses;
GClassNode classNode = group.getLiveTopLevelClasses().get(0);
i--;
String fileName = null;
outer_loop:
while (i >= 0)
{
List<GClassNode> subs = classNode.getSubClasses();
for (GClassNode candidate : subs)
{
if (candidate.getQualifiedName().equals(inheritanceChain.get(i).getName()))
{
i--;
classNode = candidate;
String candidateImage = candidate.getImageFilename();
if (candidateImage != null)
{
fileName = candidateImage;
}
continue outer_loop;
}
}
break;
}
return fileName;
}
}
top,
use,
map,
class GClassDiagram
. hasUserWorld
. hasInstantiatableWorld
. GClassDiagram
. hideContextMenu
. setProject
. recalculateGroups
. findAllSubclasses
. addClass
. findAndAdd
. makeClassInfo
. contextInbuilt
. getSelectedClassTarget
. getSelectionManager
. getGreenfootStage
. save
. getImageForActorClass
564 neLoCode
+ 38 LoComm