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