package bluej.editor.stride;
import bluej.stride.generic.ExtensionDescription;
import bluej.stride.generic.ExtensionDescription.ExtensionSource;
import bluej.stride.generic.Frame;
import bluej.stride.generic.FrameCanvas;
import bluej.stride.generic.FrameCursor;
import bluej.stride.generic.InteractionManager;
import bluej.stride.operations.AbstractOperation;
import bluej.stride.operations.AbstractOperation.ItemLabel;
import bluej.stride.operations.FrameOperation;
import bluej.stride.slots.EditableSlot.MenuItems;
import bluej.stride.slots.EditableSlot.SortedMenuItem;
import bluej.stride.slots.EditableSlot.TopLevelMenu;
import bluej.utility.javafx.FXPlatformRunnable;
import bluej.utility.javafx.FXRunnable;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.MultiListener;
import bluej.utility.Utility;
import threadchecker.OnThread;
import threadchecker.Tag;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.paint.Color;
import javafx.scene.shape.FillRule;
| A class for keeping track of the current frame selection. A frame selection is
| a contiguous group of frames in a single canvas, and is represented graphically
| by a rectangle around the frames.
|
@OnThread(Tag.FXPlatform)
public class FrameSelection
{
@OnThread(Tag.FXPlatform)
private final ObservableList<Frame> selection = FXCollections.observableList(new ArrayList<>());
private final Canvas selectionHighlight = new Canvas();
private final InteractionManager editor;
private boolean deletePreview;
private boolean pullUpPreview;
@OnThread(Tag.FX)
public FrameSelection(InteractionManager editor)
{
this.editor = editor;
selectionHighlight.setMouseTransparent(true);
deletePreview = false;
pullUpPreview = false;
Function<Frame, MultiListener.RemoveAndUpdate> removeAndUpdate = f -> {
FXRunnable removeA = JavaFXUtil.addChangeListener(f.getNode().localToSceneTransformProperty(), x -> JavaFXUtil.runNowOrLater(() -> redraw()));
FXRunnable removeB = JavaFXUtil.addChangeListener(f.getNode().boundsInLocalProperty(), x -> JavaFXUtil.runNowOrLater(() -> redraw()));
return JavaFXUtil.sequence(removeA, removeB)::run;
};
MultiListener<Frame> positionListener = new MultiListener<Frame>(removeAndUpdate);
addChangeListener(() -> {
redraw();
positionListener.listenOnlyTo(selection.stream());
});
}
| Recalculates position of selection rectangle (or removes it if selection has become empty)
|
@OnThread(Tag.FXPlatform)
private void redraw()
{
editor.getCodeOverlayPane().removeOverlay(selectionHighlight);
if (!selection.isEmpty())
{
editor.getCodeOverlayPane().addOverlay(selectionHighlight, selection.get(0).getNode(), null, null);
Node topNode = selection.get(0).getNode();
Node bottomNode = selection.get(selection.size() - 1).getNode();
selectionHighlight.setWidth(topNode.getBoundsInParent().getWidth());
selectionHighlight.setHeight(bottomNode.getBoundsInParent().getMinY() + bottomNode.getLayoutBounds().getHeight() - topNode.getBoundsInParent().getMinY());
GraphicsContext gc = selectionHighlight.getGraphicsContext2D();
gc.clearRect(0, 0, selectionHighlight.getWidth(), selectionHighlight.getHeight());
if (deletePreview || pullUpPreview)
{
gc.setFill(new Color(1, 0.4, 0.4, 0.7));
if (pullUpPreview)
{
gc.setFillRule(FillRule.EVEN_ODD);
for (Frame f : selection)
{
gc.beginPath();
roundedRectPath(gc, 0.5, 0.5, selectionHighlight.getWidth() - 1, selectionHighlight.getHeight() - 1, 7);
for (FrameCanvas c : Utility.iterableStream(f.getCanvases()))
{
Bounds sceneBounds = c.getContentSceneBounds();
Bounds b = selectionHighlight.sceneToLocal(sceneBounds);
roundedRectPath(gc, b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight(), 5);
}
gc.fill();
gc.closePath();
}
}
else
{
gc.fillRoundRect(0.5, 0.5, selectionHighlight.getWidth() - 1, selectionHighlight.getHeight() - 1, 7, 7);
}
}
gc.setStroke(Color.BLACK);
gc.setLineWidth(2);
gc.strokeRoundRect(0.5, 0.5, selectionHighlight.getWidth() - 1, selectionHighlight.getHeight() - 1, 7, 7);
}
}
| Draws a rounded rectangle path with the given extents and corner radius:
|
private void roundedRectPath(GraphicsContext gc, double minX, double minY, double width, double height, double arc)
{
gc.moveTo(minX + arc, minY);
gc.lineTo(minX + width - arc, minY);
gc.arcTo(minX + width, minY, minX + width, minY + arc, arc);
gc.lineTo(minX + width, minY + height - arc);
gc.arcTo(minX + width, minY + height, minX + width - arc, minY + height, arc);
gc.lineTo(minX + arc, minY + height);
gc.arcTo(minX, minY + height, minX, minY + height - arc, arc);
gc.lineTo(minX, minY + arc);
gc.arcTo(minX, minY, minX + arc, minY, arc);
}
@OnThread(Tag.FXPlatform)
public void clear()
{
selection.clear();
}
@OnThread(Tag.FXPlatform)
public boolean contains(Frame f)
{
return selection.contains(f);
}
@OnThread(Tag.FXPlatform)
public List getSelected()
{
return Collections.unmodifiableList(selection);
}
| The user has moved the frame cursor down while holding shift
| Either add or remove from selection
|
@OnThread(Tag.FXPlatform)
public void toggleSelectDown(Frame f)
{
if (f == null)
{
return;
}
if (selection.size() > 0 && selection.get(0) == f)
{
selection.remove(0);
}
else
{
selection.add(f);
}
}
| The user has moved the frame cursor up while holding shift
| Either add or remove from selection
|
@OnThread(Tag.FXPlatform)
public void toggleSelectUp(Frame f)
{
if (f == null)
{
return;
}
if (selection.size() > 0 && selection.get(selection.size() - 1) == f)
{
selection.remove(selection.size() - 1);
}
else {
selection.add(0, f);
}
}
| Gets the context menu items which are valid across the whole selection
|
@OnThread(Tag.FXPlatform)
public MenuItems getMenuItems(boolean contextMenu)
{
if (selection.size() == 0) {
return new MenuItems(FXCollections.observableArrayList());
}
else if (selection.size() == 1) {
return asMenuItems(selection.get(0).getContextOperations(), 0, contextMenu);
}
HashMap<String, List<FrameOperation>> ops = new HashMap<>();
for (Frame f : selection) {
for (FrameOperation op : f.getContextOperations()) {
if (!ops.containsKey(op.getIdentifier())) {
ops.put(op.getIdentifier(), new ArrayList<>());
}
ops.get(op.getIdentifier()).add(op);
}
}
List<FrameOperation> r = new ArrayList<>();
for (final List<FrameOperation> opEntry : ops.values()) {
FrameOperation frameOperation = opEntry.get(0);
if ((frameOperation.combine() == AbstractOperation.Combine.ALL && opEntry.size() == selection.size())
|| frameOperation.combine() == AbstractOperation.Combine.ANY
|| (frameOperation.combine() == AbstractOperation.Combine.ONE && selection.size() == 1)) {
r.add(frameOperation);
}
}
return asMenuItems(r, 0, contextMenu);
}
| Gets the edit menu items for selected frames
|
public Map getEditMenuItems(boolean contextMenu)
{
return Collections.singletonMap(TopLevelMenu.EDIT, getMenuItems(contextMenu));
}
private static MenuItems asMenuItems(List<FrameOperation> originalOps, int depth, boolean contextMenu)
{
List<FrameOperation> ops = originalOps.stream().filter(op -> contextMenu || !op.onlyOnContextMenu()).collect(Collectors.toList());
List<SortedMenuItem> r = new ArrayList<>();
Set<ItemLabel> subMenuNames = ops.stream().filter(op -> op.getLabels().size() > depth + 1).map(op -> op.getLabels().get(depth)).collect(Collectors.toSet());
subMenuNames.forEach(subMenuName -> {
final MenuItems menuItems = asMenuItems(ops.stream().filter(op -> op.getLabels().get(depth).equals(subMenuName)).collect(Collectors.toList()), depth + 1, contextMenu);
Menu subMenu = menuItems.makeSubMenu();
subMenu.textProperty().bind(subMenuName.getLabel());
r.add(subMenuName.getOrder().item(subMenu));
});
List<FrameOperation> opsAtRightLevel = ops.stream().filter(op -> op.getLabels().size() == depth + 1).collect(Collectors.toList());
Map<FrameOperation, SortedMenuItem> opsAtRightLevelItems = new IdentityHashMap<>();
for (FrameOperation op : opsAtRightLevel)
{
SortedMenuItem item = op.getMenuItem(contextMenu);
r.add(item);
opsAtRightLevelItems.put(op, item);
}
return new MenuItems(FXCollections.observableArrayList(r)) {
@Override
@OnThread(Tag.FXPlatform)
public void onShowing()
{
opsAtRightLevel.forEach(op -> {
final SortedMenuItem sortedMenuItem = opsAtRightLevelItems.get(op);
final MenuItem item = sortedMenuItem.getItem();
if (item instanceof CustomMenuItem)
op.onMenuShowing((CustomMenuItem) item);
});
}
@Override
@OnThread(Tag.FXPlatform)
public void onHidden()
{
opsAtRightLevel.forEach(op -> {
final SortedMenuItem sortedMenuItem = opsAtRightLevelItems.get(op);
final MenuItem item = sortedMenuItem.getItem();
if (item instanceof CustomMenuItem)
op.onMenuHidden((CustomMenuItem) item);
});
}
};
}
public ContextMenu getContextMenu()
{
MenuItems ops = getMenuItems(true);
if (ops.isEmpty()) {
return null;
}
return MenuItems.makeContextMenu(Collections.singletonMap(TopLevelMenu.EDIT, ops));
}
@OnThread(Tag.FXPlatform)
public void setDeletePreview(boolean deletePreview)
{
this.deletePreview = deletePreview;
this.pullUpPreview = false;
redraw();
}
@OnThread(Tag.FXPlatform)
public void setPullUpPreview(boolean pullUpPreview)
{
this.deletePreview = false;
this.pullUpPreview = pullUpPreview;
redraw();
}
@OnThread(Tag.FXPlatform)
public void set(List<Frame> frames)
{
selection.clear();
selection.setAll(frames);
}
@OnThread(Tag.FXPlatform)
public boolean isEmpty()
{
return selection.isEmpty();
}
@OnThread(Tag.FX)
public void addChangeListener(FXPlatformRunnable listener)
{
JavaFXUtil.runNowOrLater(() -> selection.addListener((ListChangeListener<Frame>)c -> listener.run()));
}
@OnThread(Tag.FXPlatform)
public FrameCursor getCursorAfter()
{
if (selection.size() == 0)
return null;
else{ return (selection.get(selection.size() - 1).getCursorAfter());
}
}
@OnThread(Tag.FXPlatform)
public FrameCursor getCursorBefore()
{
if (selection.size() == 0)
return null;
else{ return (selection.get(0).getCursorBefore());
}
}
@OnThread(Tag.FXPlatform)
public boolean executeKey(FrameCursor cursor, final char key)
{
if (selection.size() == 1) {
for (ExtensionDescription extension : selection.get(0).getAvailableExtensions(null, null)) {
if (extension.getShortcutKey() == key &&
(extension.validFor(ExtensionSource.AFTER)
|| extension.validFor(ExtensionSource.BEFORE)
|| extension.validFor(ExtensionSource.SELECTION)))
{
extension.activate();
return true;
}
}
}
if (key == '\\') {
boolean allDisabled = getCanHaveEnabledState(false).allMatch(f -> !f.isFrameEnabled());
editor.beginRecordingState(cursor);
getCanHaveEnabledState(allDisabled ? true : false).forEach(t -> t.setFrameEnabled(allDisabled ? true : false));
editor.endRecordingState(cursor);
return true;
}
if (getNonIgnored().allMatch(f -> f.getAvailableExtensions(null, null).stream()
.filter(m -> m.validFor(ExtensionSource.SELECTION) && m.getShortcutKey() == key).count() == 1)) {
getNonIgnored().flatMap(f -> f.getAvailableExtensions(null, null).stream())
.filter(m -> m.validFor(ExtensionSource.SELECTION) && m.getShortcutKey() == key)
.findAny().ifPresent(e -> e.activate(getNonIgnored().collect(Collectors.toList())));
return true;
}
return false;
}
private Stream getCanHaveEnabledState(boolean state)
{
return selection.stream().filter(f -> f.canHaveEnabledState(state));
}
private Stream getNonIgnored()
{
return selection.stream().filter(f -> f.isEffectiveFrame());
}
}
top,
use,
map,
class FrameSelection
. FrameSelection
. redraw
. roundedRectPath
. clear
. contains
. getSelected
. toggleSelectDown
. toggleSelectUp
. getMenuItems
. getEditMenuItems
. asMenuItems
. onShowing
. onHidden
. getContextMenu
. setDeletePreview
. setPullUpPreview
. set
. isEmpty
. addChangeListener
. getCursorAfter
. getCursorBefore
. executeKey
. getCanHaveEnabledState
. getNonIgnored
504 neLoCode
+ 11 LoComm