mirror of
https://github.com/scratchfoundation/scratch-flash.git
synced 2024-12-14 01:51:09 -05:00
801 lines
25 KiB
ActionScript
801 lines
25 KiB
ActionScript
|
/*
|
||
|
* Scratch Project Editor and Player
|
||
|
* Copyright (C) 2014 Massachusetts Institute of Technology
|
||
|
*
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; either version 2
|
||
|
* of the License, or (at your option) any later version.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*/
|
||
|
|
||
|
// Block.as
|
||
|
// John Maloney, August 2009
|
||
|
//
|
||
|
// A Block is a graphical object representing a program statement (command)
|
||
|
// or function (reporter). A stack is a sequence of command blocks, where
|
||
|
// the following command and any nested commands (e.g. within a loop) are
|
||
|
// children. Blocks come in a variety of shapes and usually have some
|
||
|
// combination of label strings and arguments (also children).
|
||
|
//
|
||
|
// The Block class manages block shape, labels, arguments, layout, and
|
||
|
// block sequence. It also supports generation of the labels and argument
|
||
|
// sequence from a specification string (e.g. "%n + %n") and type (e.g. reporter).
|
||
|
|
||
|
package blocks {
|
||
|
import extensions.ExtensionManager;
|
||
|
|
||
|
import flash.display.*;
|
||
|
import flash.events.*;
|
||
|
import flash.filters.GlowFilter;
|
||
|
import flash.geom.*;
|
||
|
import flash.net.URLLoader;
|
||
|
import flash.text.*;
|
||
|
import assets.Resources;
|
||
|
import translation.Translator;
|
||
|
import util.*;
|
||
|
import uiwidgets.*;
|
||
|
|
||
|
public class Block extends Sprite {
|
||
|
|
||
|
private const minCommandWidth:int = 36;
|
||
|
private const minHatWidth:int = 80;
|
||
|
private const minLoopWidth:int = 80;
|
||
|
|
||
|
public static var argTextFormat:TextFormat;
|
||
|
public static var blockLabelFormat:TextFormat;
|
||
|
private static var vOffset:int;
|
||
|
|
||
|
// private static const blockLabelFormat:TextFormat = new TextFormat('LucidaBoldEmbedded', 10, 0xFFFFFF, true);
|
||
|
private static var useEmbeddedFont:Boolean = false;
|
||
|
|
||
|
public static var MenuHandlerFunction:Function; // optional function to handle block and blockArg menus
|
||
|
|
||
|
public var spec:String;
|
||
|
public var type:String;
|
||
|
public var op:String = "";
|
||
|
public var opFunction:Function;
|
||
|
public var args:Array = [];
|
||
|
public var defaultArgValues:Array = [];
|
||
|
public var parameterIndex:int = -1; // cache of parameter index, used by GET_PARAM block
|
||
|
public var parameterNames:Array; // used by procedure definition hats; null for other blocks
|
||
|
public var warpProcFlag:Boolean; // used by procedure definition hats to indicate warp speed
|
||
|
public var rightToLeft:Boolean;
|
||
|
|
||
|
public var isHat:Boolean = false;
|
||
|
public var isReporter:Boolean = false;
|
||
|
public var isTerminal:Boolean = false; // blocks that end a stack like "stop" or "forever"
|
||
|
|
||
|
// Blocking operations
|
||
|
public var isRequester:Boolean = false;
|
||
|
public var requestState:int = 0; // 0 - no request made, 1 - awaiting response, 2 - data ready
|
||
|
public var response:* = null;
|
||
|
public var requestLoader:URLLoader = null;
|
||
|
|
||
|
public var nextBlock:Block;
|
||
|
public var subStack1:Block;
|
||
|
public var subStack2:Block;
|
||
|
|
||
|
public var base:BlockShape;
|
||
|
|
||
|
private var suppressLayout:Boolean; // used to avoid extra layouts during block initialization
|
||
|
private var labelsAndArgs:Array = [];
|
||
|
private var argTypes:Array = [];
|
||
|
private var elseLabel:TextField;
|
||
|
|
||
|
private var indentTop:int = 2, indentBottom:int = 3;
|
||
|
private var indentLeft:int = 4, indentRight:int = 3;
|
||
|
|
||
|
public var wasInScriptsPane:Boolean;
|
||
|
private var originalX:int, originalY:int;
|
||
|
|
||
|
public function Block(spec:String, type:String = " ", color:int = 0xD00000, op:* = 0, defaultArgs:Array = null) {
|
||
|
this.spec = Translator.map(spec);
|
||
|
this.type = type;
|
||
|
this.op = op;
|
||
|
|
||
|
if ((Specs.CALL == op) ||
|
||
|
(Specs.GET_LIST == op) ||
|
||
|
(Specs.GET_PARAM == op) ||
|
||
|
(Specs.GET_VAR == op) ||
|
||
|
(Specs.PROCEDURE_DEF == op) ||
|
||
|
('proc_declaration' == op)) {
|
||
|
this.spec = spec; // don't translate var/list/param reporters
|
||
|
}
|
||
|
|
||
|
if (color == -1) return; // copy for clone; omit graphics
|
||
|
|
||
|
var shape:int;
|
||
|
if ((type == " ") || (type == "") || (type == "w")) {
|
||
|
base = new BlockShape(BlockShape.CmdShape, color);
|
||
|
indentTop = 3;
|
||
|
} else if (type == "b") {
|
||
|
base = new BlockShape(BlockShape.BooleanShape, color);
|
||
|
isReporter = true;
|
||
|
indentLeft = 9;
|
||
|
indentRight = 7;
|
||
|
} else if (type == "r" || type == "R") {
|
||
|
this.type = 'r';
|
||
|
base = new BlockShape(BlockShape.NumberShape, color);
|
||
|
isReporter = true;
|
||
|
isRequester = (type == 'R');
|
||
|
indentTop = 2;
|
||
|
indentBottom = 2;
|
||
|
indentLeft = 6;
|
||
|
indentRight = 4;
|
||
|
} else if (type == "h") {
|
||
|
base = new BlockShape(BlockShape.HatShape, color);
|
||
|
isHat = true;
|
||
|
indentTop = 12;
|
||
|
} else if (type == "c") {
|
||
|
base = new BlockShape(BlockShape.LoopShape, color);
|
||
|
} else if (type == "cf") {
|
||
|
base = new BlockShape(BlockShape.FinalLoopShape, color);
|
||
|
isTerminal = true;
|
||
|
} else if (type == "e") {
|
||
|
base = new BlockShape(BlockShape.IfElseShape, color);
|
||
|
addChild(elseLabel = makeLabel(Translator.map('else')));
|
||
|
} else if (type == "f") {
|
||
|
base = new BlockShape(BlockShape.FinalCmdShape, color);
|
||
|
isTerminal = true;
|
||
|
indentTop = 5;
|
||
|
} else if (type == "o") { // cmd outline for proc definition
|
||
|
base = new BlockShape(BlockShape.CmdOutlineShape, color);
|
||
|
base.filters = []; // no bezel
|
||
|
indentTop = 3;
|
||
|
} else if (type == "p") {
|
||
|
base = new BlockShape(BlockShape.ProcHatShape, color);
|
||
|
isHat = true;
|
||
|
} else {
|
||
|
base = new BlockShape(BlockShape.RectShape, color);
|
||
|
}
|
||
|
addChildAt(base, 0);
|
||
|
setSpec(this.spec, defaultArgs);
|
||
|
|
||
|
addEventListener(FocusEvent.KEY_FOCUS_CHANGE, focusChange);
|
||
|
}
|
||
|
|
||
|
public function setSpec(newSpec:String, defaultArgs:Array = null):void {
|
||
|
for each (var o:DisplayObject in labelsAndArgs) {
|
||
|
if (o.parent != null) o.parent.removeChild(o);
|
||
|
}
|
||
|
spec = newSpec;
|
||
|
if (op == Specs.PROCEDURE_DEF) {
|
||
|
// procedure hat: make an icon from my spec and use that as the label
|
||
|
indentTop = 20;
|
||
|
indentBottom = 5;
|
||
|
indentLeft = 5;
|
||
|
indentRight = 5;
|
||
|
|
||
|
labelsAndArgs = [];
|
||
|
argTypes = [];
|
||
|
var label:TextField = makeLabel(Translator.map('define'));
|
||
|
labelsAndArgs.push(label);
|
||
|
var b:Block;
|
||
|
labelsAndArgs.push(b = declarationBlock());
|
||
|
} else {
|
||
|
const loopBlocks:Array = ['doForever', 'doForeverIf', 'doRepeat', 'doUntil'];
|
||
|
base.hasLoopArrow = (loopBlocks.indexOf(op) >= 0);
|
||
|
addLabelsAndArgs(spec, base.color);
|
||
|
}
|
||
|
rightToLeft = Translator.rightToLeft;
|
||
|
if (rightToLeft) {
|
||
|
if (['+', '-', '*', '/', '%'].indexOf(op) > -1) rightToLeft = Translator.rightToLeftMath;
|
||
|
if (['>', '<'].indexOf(op) > -1) rightToLeft = false; // never change order of comparison ops
|
||
|
}
|
||
|
if (rightToLeft) {
|
||
|
// reverse specs that don't start with arg specifier or an ASCII character
|
||
|
labelsAndArgs.reverse();
|
||
|
argTypes.reverse();
|
||
|
if (defaultArgs) defaultArgs.reverse();
|
||
|
}
|
||
|
for each (var item:* in labelsAndArgs) addChild(item);
|
||
|
if (defaultArgs) setDefaultArgs(defaultArgs);
|
||
|
fixArgLayout();
|
||
|
}
|
||
|
|
||
|
public function normalizedArgs():Array {
|
||
|
return rightToLeft ? args.concat().reverse() : args;
|
||
|
}
|
||
|
|
||
|
public function changeOperator(newOp:String):void {
|
||
|
// Used to switch among a family of related operators (e.g. +, -, *, and /).
|
||
|
// Note: This does not deal with translation, so it only works for symbolic operators.
|
||
|
for each (var item:* in labelsAndArgs) {
|
||
|
if ((item is TextField) && (item.text == op)) item.text = newOp;
|
||
|
}
|
||
|
op = newOp;
|
||
|
opFunction = null;
|
||
|
fixArgLayout();
|
||
|
}
|
||
|
|
||
|
public static function setFonts(labelSize:int, argSize:int, boldFlag:Boolean, vOffset:int):void {
|
||
|
var font:String = Resources.chooseFont([
|
||
|
'Lucida Grande', 'Verdana', 'Arial', 'DejaVu Sans']);
|
||
|
blockLabelFormat = new TextFormat(font, labelSize, 0xFFFFFF, boldFlag);
|
||
|
argTextFormat = new TextFormat(font, argSize, 0x505050, boldFlag);
|
||
|
Block.vOffset = vOffset;
|
||
|
}
|
||
|
|
||
|
private function declarationBlock():Block {
|
||
|
// Create a block representing a procedure declaration to be embedded in a
|
||
|
// procedure definition header block. For each formal parameter, embed a
|
||
|
// reporter for that parameter.
|
||
|
var b:Block = new Block(spec, "o", Specs.procedureColor, 'proc_declaration');
|
||
|
if (!parameterNames) parameterNames = [];
|
||
|
for (var i:int = 0; i < parameterNames.length; i++) {
|
||
|
var argType:String = (typeof(defaultArgValues[i]) == 'boolean') ? 'b' : 'r';
|
||
|
var pBlock:Block = new Block(parameterNames[i], argType, Specs.parameterColor, Specs.GET_PARAM);
|
||
|
pBlock.parameterIndex = i;
|
||
|
b.setArg(i, pBlock);
|
||
|
}
|
||
|
b.fixArgLayout();
|
||
|
return b;
|
||
|
}
|
||
|
|
||
|
public function isProcDef():Boolean { return op == Specs.PROCEDURE_DEF }
|
||
|
|
||
|
public function isEmbeddedInProcHat():Boolean {
|
||
|
return (parent is Block) &&
|
||
|
(Block(parent).op == Specs.PROCEDURE_DEF) &&
|
||
|
(this != Block(parent).nextBlock);
|
||
|
}
|
||
|
|
||
|
public function isEmbeddedParameter():Boolean {
|
||
|
if ((op != Specs.GET_PARAM) || !(parent is Block)) return false;
|
||
|
return Block(parent).op == 'proc_declaration';
|
||
|
}
|
||
|
|
||
|
public function isInPalette():Boolean {
|
||
|
var o:DisplayObject = parent;
|
||
|
while (o) {
|
||
|
if ('isBlockPalette' in o) return true;
|
||
|
o = o.parent;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
public function setTerminal(flag:Boolean):void {
|
||
|
// Used to change the "stop" block shape.
|
||
|
removeChild(base);
|
||
|
isTerminal = flag;
|
||
|
var newShape:int = isTerminal ? BlockShape.FinalCmdShape : BlockShape.CmdShape;
|
||
|
base = new BlockShape(newShape, base.color);
|
||
|
addChildAt(base, 0);
|
||
|
fixArgLayout();
|
||
|
}
|
||
|
|
||
|
private function addLabelsAndArgs(spec:String, c:int):void {
|
||
|
var specParts:Array = ReadStream.tokenize(spec), i:int;
|
||
|
labelsAndArgs = [];
|
||
|
argTypes = [];
|
||
|
for (i = 0; i < specParts.length; i++) {
|
||
|
var o:DisplayObject = argOrLabelFor(specParts[i], c);
|
||
|
labelsAndArgs.push(o);
|
||
|
var argType:String = 'icon';
|
||
|
if (o is BlockArg) argType = specParts[i];
|
||
|
if (o is TextField) argType = 'label';
|
||
|
argTypes.push(argType);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function allBlocksDo(f:Function):void {
|
||
|
f(this);
|
||
|
for each (var arg:* in args) {
|
||
|
if (arg is Block) arg.allBlocksDo(f);
|
||
|
}
|
||
|
if (subStack1 != null) subStack1.allBlocksDo(f);
|
||
|
if (subStack2 != null) subStack2.allBlocksDo(f);
|
||
|
if (nextBlock != null) nextBlock.allBlocksDo(f);
|
||
|
}
|
||
|
|
||
|
public function showRunFeedback():void {
|
||
|
if (!filters || (filters.length == 0)) {
|
||
|
filters = runFeedbackFilters();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function hideRunFeedback():void {
|
||
|
if (filters && (filters.length > 0)) filters = [];
|
||
|
}
|
||
|
|
||
|
private function runFeedbackFilters():Array {
|
||
|
// filters for showing that a stack is running
|
||
|
var f:GlowFilter = new GlowFilter(0xfeffa0);
|
||
|
f.strength = 2;
|
||
|
f.blurX = f.blurY = 12;
|
||
|
f.quality = 3;
|
||
|
return [f];
|
||
|
}
|
||
|
|
||
|
public function saveOriginalPosition():void {
|
||
|
wasInScriptsPane = topBlock().parent is ScriptsPane;
|
||
|
originalX = x;
|
||
|
originalY = y;
|
||
|
}
|
||
|
|
||
|
public function restoreOriginalPosition():void {
|
||
|
x = originalX;
|
||
|
y = originalY;
|
||
|
}
|
||
|
|
||
|
private function setDefaultArgs(defaults:Array):void {
|
||
|
collectArgs();
|
||
|
for (var i:int = 0; i < Math.min(args.length, defaults.length); i++) {
|
||
|
var argLabel:String = null;
|
||
|
var v:* = defaults[i];
|
||
|
if (v is BlockArg) v = BlockArg(v).argValue;
|
||
|
if ('_edge_' == v) argLabel = Translator.map('edge');
|
||
|
if ('_mouse_' == v) argLabel = Translator.map('mouse-pointer');
|
||
|
if ('_myself_' == v) argLabel = Translator.map('myself');
|
||
|
if ('_stage_' == v) argLabel = Translator.map('Stage');
|
||
|
if (args[i] is BlockArg) args[i].setArgValue(v, argLabel);
|
||
|
}
|
||
|
defaultArgValues = defaults;
|
||
|
}
|
||
|
|
||
|
public function setArg(i:int, newArg:*):void {
|
||
|
// called on newly-created block (assumes argument being set is a BlockArg)
|
||
|
// newArg can be either a reporter block or a literal value (string, number, etc.)
|
||
|
collectArgs();
|
||
|
if (i >= args.length) return;
|
||
|
var oldArg:BlockArg = args[i];
|
||
|
if (newArg is Block) {
|
||
|
labelsAndArgs[labelsAndArgs.indexOf(oldArg)] = newArg;
|
||
|
args[i] = newArg;
|
||
|
removeChild(oldArg);
|
||
|
addChild(newArg);
|
||
|
} else {
|
||
|
oldArg.setArgValue(newArg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function fixExpressionLayout():void {
|
||
|
// fix expression layout up to the enclosing command block
|
||
|
var b:Block = this;
|
||
|
while (b.isReporter) {
|
||
|
b.fixArgLayout();
|
||
|
if (b.parent is Block) b = Block(b.parent)
|
||
|
else return;
|
||
|
}
|
||
|
if (b is Block) b.fixArgLayout();
|
||
|
}
|
||
|
|
||
|
public function fixArgLayout():void {
|
||
|
var item:DisplayObject, i:int;
|
||
|
if (suppressLayout) return;
|
||
|
var x:int = indentLeft - indentAjustmentFor(labelsAndArgs[0]);
|
||
|
var maxH:int = 0;
|
||
|
for (i = 0; i < labelsAndArgs.length; i++) {
|
||
|
item = labelsAndArgs[i];
|
||
|
// Next line moves the argument of if and if-else blocks right slightly:
|
||
|
if ((i == 1) && !(argTypes[i] == 'label')) x = Math.max(x, 30);
|
||
|
item.x = x;
|
||
|
maxH = Math.max(maxH, item.height);
|
||
|
x += item.width + 2;
|
||
|
if (argTypes[i] == 'icon') x += 3;
|
||
|
}
|
||
|
x -= indentAjustmentFor(labelsAndArgs[labelsAndArgs.length - 1]);
|
||
|
|
||
|
for (i = 0; i < labelsAndArgs.length; i++) {
|
||
|
item = labelsAndArgs[i];
|
||
|
item.y = indentTop + ((maxH - item.height) / 2) + vOffset;
|
||
|
if ((item is BlockArg) && (!BlockArg(item).isNumber)) item.y += 1;
|
||
|
}
|
||
|
|
||
|
if ([' ', '', 'o'].indexOf(type) >= 0) x = Math.max(x, minCommandWidth); // minimum width for command blocks
|
||
|
if (['c', 'cf', 'e'].indexOf(type) >= 0) x = Math.max(x, minLoopWidth); // minimum width for C and E blocks
|
||
|
if (['h'].indexOf(type) >= 0) x = Math.max(x, minHatWidth); // minimum width for hat blocks
|
||
|
|
||
|
base.setWidthAndTopHeight(x + indentRight, indentTop + maxH + indentBottom);
|
||
|
if ((type == "c") || (type == "e")) fixStackLayout();
|
||
|
base.redraw();
|
||
|
fixElseLabel();
|
||
|
collectArgs();
|
||
|
}
|
||
|
|
||
|
private function indentAjustmentFor(item:*):int {
|
||
|
var itemType:String = '';
|
||
|
if (item is Block) itemType = Block(item).type;
|
||
|
if (item is BlockArg) itemType = BlockArg(item).type;
|
||
|
if ((type == 'b') && (itemType == 'b')) return 4;
|
||
|
if ((type == 'r') && ((itemType == 'r') || (itemType == 'd') || (itemType == 'n'))) return 2;
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
public function fixStackLayout():void {
|
||
|
var b:Block = this;
|
||
|
while (b != null) {
|
||
|
if (b.base.canHaveSubstack1()) {
|
||
|
var substackH:int = BlockShape.EmptySubstackH;
|
||
|
if (b.subStack1) {
|
||
|
b.subStack1.fixStackLayout();
|
||
|
b.subStack1.x = BlockShape.SubstackInset;
|
||
|
b.subStack1.y = b.base.substack1y();
|
||
|
substackH = b.subStack1.getRect(b).height;
|
||
|
}
|
||
|
b.base.setSubstack1Height(substackH);
|
||
|
substackH = BlockShape.EmptySubstackH;
|
||
|
if (b.subStack2) {
|
||
|
b.subStack2.fixStackLayout();
|
||
|
b.subStack2.x = BlockShape.SubstackInset;
|
||
|
b.subStack2.y = b.base.substack2y();
|
||
|
substackH = b.subStack2.getRect(b).height;
|
||
|
}
|
||
|
b.base.setSubstack2Height(substackH);
|
||
|
b.base.redraw();
|
||
|
b.fixElseLabel();
|
||
|
}
|
||
|
if (b.nextBlock != null) {
|
||
|
b.nextBlock.x = 0;
|
||
|
b.nextBlock.y = b.base.nextBlockY();
|
||
|
}
|
||
|
b = b.nextBlock;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function fixElseLabel():void {
|
||
|
if (elseLabel) {
|
||
|
var metrics:TextLineMetrics = elseLabel.getLineMetrics(0);
|
||
|
var dy:int = (metrics.ascent + metrics.descent) / 2;
|
||
|
elseLabel.x = 4;
|
||
|
elseLabel.y = base.substack2y() - 11 - dy + vOffset;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function duplicate(forClone:Boolean, forStage:Boolean = false):Block {
|
||
|
var newSpec:String = spec;
|
||
|
if(forStage && op == 'whenClicked') newSpec = 'when Stage clicked';
|
||
|
var dup:Block = new Block(newSpec, type, (int)(forClone ? -1 : base.color), op);
|
||
|
dup.isRequester = isRequester;
|
||
|
dup.parameterNames = parameterNames;
|
||
|
dup.defaultArgValues = defaultArgValues;
|
||
|
dup.warpProcFlag = warpProcFlag;
|
||
|
if (forClone) dup.copyArgsForClone(args); else dup.copyArgs(args);
|
||
|
if (nextBlock != null) dup.addChild(dup.nextBlock = nextBlock.duplicate(forClone, forStage));
|
||
|
if (subStack1 != null) dup.addChild(dup.subStack1 = subStack1.duplicate(forClone, forStage));
|
||
|
if (subStack2 != null) dup.addChild(dup.subStack2 = subStack2.duplicate(forClone, forStage));
|
||
|
if (!forClone) {
|
||
|
dup.x = x;
|
||
|
dup.y = y;
|
||
|
dup.fixExpressionLayout();
|
||
|
dup.fixStackLayout();
|
||
|
}
|
||
|
return dup;
|
||
|
}
|
||
|
|
||
|
private function copyArgs(srcArgs:Array):void {
|
||
|
// called on a newly created block that is being duplicated to copy the
|
||
|
// argument values and/or expressions from the source block's arguments
|
||
|
var i:int;
|
||
|
collectArgs();
|
||
|
for (i = 0; i < srcArgs.length; i++) {
|
||
|
var argToCopy:* = srcArgs[i];
|
||
|
if (argToCopy is BlockArg) {
|
||
|
var arg:BlockArg = argToCopy;
|
||
|
BlockArg(args[i]).setArgValue(arg.argValue, arg.labelOrNull());
|
||
|
}
|
||
|
if (argToCopy is Block) {
|
||
|
var newArg:Block = Block(argToCopy).duplicate(false);
|
||
|
var oldArg:* = args[i];
|
||
|
labelsAndArgs[labelsAndArgs.indexOf(oldArg)] = newArg;
|
||
|
args[i] = newArg;
|
||
|
removeChild(oldArg);
|
||
|
addChild(newArg);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function copyArgsForClone(srcArgs:Array):void {
|
||
|
// called on a block that is being cloned.
|
||
|
args = [];
|
||
|
for (var i:int = 0; i < srcArgs.length; i++) {
|
||
|
var argToCopy:* = srcArgs[i];
|
||
|
if (argToCopy is BlockArg) {
|
||
|
var a:BlockArg = new BlockArg(argToCopy.type, -1);
|
||
|
a.argValue = argToCopy.argValue;
|
||
|
args.push(a);
|
||
|
}
|
||
|
if (argToCopy is Block) {
|
||
|
args.push(Block(argToCopy).duplicate(true));
|
||
|
}
|
||
|
}
|
||
|
for each (var arg:DisplayObject in args) addChild(arg); // fix for cloned proc bug xxx
|
||
|
}
|
||
|
|
||
|
private function collectArgs():void {
|
||
|
var i:int;
|
||
|
args = [];
|
||
|
for (i = 0; i < labelsAndArgs.length; i++) {
|
||
|
var a:* = labelsAndArgs[i];
|
||
|
if ((a is Block) || (a is BlockArg)) args.push(a);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function removeBlock(b:Block):void {
|
||
|
if (b.parent == this) removeChild(b);
|
||
|
if (b == nextBlock) {
|
||
|
nextBlock = null;
|
||
|
}
|
||
|
if (b == subStack1) subStack1 = null;
|
||
|
if (b == subStack2) subStack2 = null;
|
||
|
if (b.isReporter) {
|
||
|
var i:int = labelsAndArgs.indexOf(b);
|
||
|
if (i < 0) return;
|
||
|
var newArg:DisplayObject = argOrLabelFor(argTypes[i], base.color);
|
||
|
labelsAndArgs[i] = newArg;
|
||
|
addChild(newArg);
|
||
|
fixExpressionLayout();
|
||
|
|
||
|
// Cancel any outstanding requests (for blocking reporters, isRequester=true)
|
||
|
if(b.requestLoader)
|
||
|
b.requestLoader.close();
|
||
|
}
|
||
|
topBlock().fixStackLayout();
|
||
|
Scratch.app.runtime.checkForGraphicEffects();
|
||
|
}
|
||
|
|
||
|
public function insertBlock(b:Block):void {
|
||
|
var oldNext:Block = nextBlock;
|
||
|
|
||
|
if (oldNext != null) removeChild(oldNext);
|
||
|
|
||
|
addChild(b);
|
||
|
nextBlock = b;
|
||
|
if (oldNext != null) b.appendBlock(oldNext);
|
||
|
|
||
|
topBlock().fixStackLayout();
|
||
|
}
|
||
|
|
||
|
public function insertBlockAbove(b:Block):void {
|
||
|
b.x = this.x;
|
||
|
b.y = this.y - b.height + BlockShape.NotchDepth;
|
||
|
parent.addChild(b);
|
||
|
b.bottomBlock().insertBlock(this);
|
||
|
}
|
||
|
|
||
|
public function insertBlockAround(b:Block):void {
|
||
|
b.x = this.x - BlockShape.SubstackInset;
|
||
|
b.y = this.y - b.base.substack1y(); // + BlockShape.NotchDepth;
|
||
|
parent.addChild(b);
|
||
|
parent.removeChild(this);
|
||
|
b.addChild(this);
|
||
|
b.subStack1 = this;
|
||
|
b.fixStackLayout();
|
||
|
}
|
||
|
|
||
|
public function insertBlockSub1(b:Block):void {
|
||
|
var old:Block = subStack1;
|
||
|
if (old != null) old.parent.removeChild(old);
|
||
|
|
||
|
addChild(b);
|
||
|
subStack1 = b;
|
||
|
if (old != null) b.appendBlock(old);
|
||
|
topBlock().fixStackLayout();
|
||
|
}
|
||
|
|
||
|
public function insertBlockSub2(b:Block):void {
|
||
|
var old:Block = subStack2;
|
||
|
if (old != null) removeChild(old);
|
||
|
|
||
|
addChild(b);
|
||
|
subStack2 = b;
|
||
|
if (old != null) b.appendBlock(old);
|
||
|
topBlock().fixStackLayout();
|
||
|
}
|
||
|
|
||
|
public function replaceArgWithBlock(oldArg:DisplayObject, b:Block, pane:DisplayObjectContainer):void {
|
||
|
var i:int = labelsAndArgs.indexOf(oldArg);
|
||
|
if (i < 0) return;
|
||
|
|
||
|
// remove the old argument
|
||
|
removeChild(oldArg);
|
||
|
labelsAndArgs[i] = b;
|
||
|
addChild(b);
|
||
|
fixExpressionLayout();
|
||
|
|
||
|
if (oldArg is Block) {
|
||
|
// leave old block in pane
|
||
|
var o:Block = owningBlock();
|
||
|
var p:Point = pane.globalToLocal(o.localToGlobal(new Point(o.width + 5, (o.height - oldArg.height) / 2)));
|
||
|
oldArg.x = p.x;
|
||
|
oldArg.y = p.y;
|
||
|
pane.addChild(oldArg);
|
||
|
}
|
||
|
topBlock().fixStackLayout();
|
||
|
}
|
||
|
|
||
|
private function appendBlock(b:Block):void {
|
||
|
var bottom:Block = bottomBlock();
|
||
|
bottom.addChild(b);
|
||
|
bottom.nextBlock = b;
|
||
|
}
|
||
|
|
||
|
private function owningBlock():Block {
|
||
|
var b:Block = this;
|
||
|
while (true) {
|
||
|
if (b.parent is Block) {
|
||
|
b = Block(b.parent);
|
||
|
if (!b.isReporter) return b; // owning command block
|
||
|
} else {
|
||
|
return b; // top-level reporter block
|
||
|
}
|
||
|
}
|
||
|
return b; // never gets here
|
||
|
}
|
||
|
|
||
|
public function topBlock():Block {
|
||
|
var result:DisplayObject = this;
|
||
|
while (result.parent is Block) result = result.parent;
|
||
|
return Block(result);
|
||
|
}
|
||
|
|
||
|
public function bottomBlock():Block {
|
||
|
var result:Block = this;
|
||
|
while (result.nextBlock!= null) result = result.nextBlock;
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
private function argOrLabelFor(s:String, c:int):DisplayObject {
|
||
|
// Possible token formats:
|
||
|
// %<single letter>
|
||
|
// %m.<menuName>
|
||
|
// @<iconName>
|
||
|
// label (any string with no embedded white space that does not start with % or @)
|
||
|
// a token consisting of a single % or @ character is also a label
|
||
|
if ((s.length >= 2) && (s.charAt(0) == "%")) { // argument spec
|
||
|
var argSpec:String = s.charAt(1);
|
||
|
if (argSpec == "b") return new BlockArg("b", c);
|
||
|
if (argSpec == "c") return new BlockArg("c", c);
|
||
|
if (argSpec == "d") return new BlockArg("d", c, true, s.slice(3));
|
||
|
if (argSpec == "m") return new BlockArg("m", c, false, s.slice(3));
|
||
|
if (argSpec == "n") return new BlockArg("n", c, true);
|
||
|
if (argSpec == "s") return new BlockArg("s", c, true);
|
||
|
} else if ((s.length >= 2) && (s.charAt(0) == "@")) { // icon spec
|
||
|
var icon:* = Specs.IconNamed(s.slice(1));
|
||
|
return (icon) ? icon : makeLabel(s);
|
||
|
}
|
||
|
return makeLabel(s);
|
||
|
}
|
||
|
|
||
|
private function makeLabel(label:String):TextField {
|
||
|
var text:TextField = new TextField();
|
||
|
text.autoSize = TextFieldAutoSize.LEFT;
|
||
|
text.selectable = false;
|
||
|
text.background = false;
|
||
|
text.defaultTextFormat = blockLabelFormat;
|
||
|
text.text = label;
|
||
|
if (useEmbeddedFont) {
|
||
|
text.antiAliasType = AntiAliasType.ADVANCED;
|
||
|
text.embedFonts = true;
|
||
|
}
|
||
|
text.mouseEnabled = false;
|
||
|
return text;
|
||
|
}
|
||
|
|
||
|
/* Menu */
|
||
|
|
||
|
public function menu(evt:MouseEvent):void {
|
||
|
// Note: Unlike most menu() mehtods, this method invokes
|
||
|
// the menu itself rather than returning a menu to the caller.
|
||
|
if (MenuHandlerFunction == null) return;
|
||
|
if (isEmbeddedInProcHat()) MenuHandlerFunction(null, parent);
|
||
|
else MenuHandlerFunction(null, this);
|
||
|
}
|
||
|
|
||
|
public function handleTool(tool:String, evt:MouseEvent):void {
|
||
|
if (isEmbeddedParameter()) return;
|
||
|
if (!isInPalette()) {
|
||
|
if ('copy' == tool) duplicateStack(10, 5);
|
||
|
if ('cut' == tool) deleteStack();
|
||
|
}
|
||
|
if (tool == 'help') showHelp();
|
||
|
}
|
||
|
|
||
|
public function showHelp():void {
|
||
|
var i:int = -1;
|
||
|
if((i = op.indexOf('.')) > -1) {
|
||
|
var extName:String = op.substr(0, i);
|
||
|
if(Scratch.app.extensionManager.isInternal(extName))
|
||
|
Scratch.app.showTip('ext:'+extName);
|
||
|
else
|
||
|
DialogBox.notify('Help Missing', 'There is no documentation available for experimental extension "'+extName+'".', Scratch.app.stage);
|
||
|
}
|
||
|
else {
|
||
|
Scratch.app.showTip(op);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function duplicateStack(deltaX:Number, deltaY:Number):void {
|
||
|
if (isProcDef()) return; // don't duplicate procedure definition
|
||
|
var forStage:Boolean = Scratch.app.viewedObj() && Scratch.app.viewedObj().isStage;
|
||
|
var newStack:Block = BlockIO.stringToStack(BlockIO.stackToString(this), forStage);
|
||
|
newStack.x = x + deltaX;
|
||
|
newStack.y = y + deltaY;
|
||
|
parent.addChild(newStack);
|
||
|
Scratch.app.gh.grabOnMouseUp(newStack);
|
||
|
}
|
||
|
|
||
|
public function deleteStack():void {
|
||
|
if (isProcDef() || isEmbeddedInProcHat()) return; // don't delete procedure definition this way for now
|
||
|
if (parent == null) return;
|
||
|
var top:Block = topBlock();
|
||
|
// TODO: Remove any waiting reporter data in the Scratch.app.extensionManager
|
||
|
if (parent is Block) Block(parent).removeBlock(this);
|
||
|
else parent.removeChild(this);
|
||
|
this.cacheAsBitmap = false;
|
||
|
// set position for undelete
|
||
|
x = top.x;
|
||
|
y = top.y;
|
||
|
if (top != this) x += top.width + 5;
|
||
|
Scratch.app.runtime.recordForUndelete(this, x, y, 0, Scratch.app.viewedObj());
|
||
|
Scratch.app.scriptsPane.saveScripts();
|
||
|
Scratch.app.runtime.checkForGraphicEffects();
|
||
|
}
|
||
|
|
||
|
public function addComment():void {
|
||
|
var scriptsPane:ScriptsPane = topBlock().parent as ScriptsPane;
|
||
|
if (scriptsPane) scriptsPane.addComment(this);
|
||
|
}
|
||
|
|
||
|
/* Dragging */
|
||
|
|
||
|
public function objToGrab(evt:MouseEvent):Block {
|
||
|
if (isEmbeddedParameter() || isInPalette()) return duplicate(false);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/* Events */
|
||
|
|
||
|
public function click(evt:MouseEvent):void {
|
||
|
if (editArg(evt)) return;
|
||
|
Scratch.app.runtime.interp.toggleThread(topBlock(), Scratch.app.viewedObj(), 1);
|
||
|
}
|
||
|
|
||
|
public function doubleClick(evt:MouseEvent):void {
|
||
|
if (editArg(evt)) return;
|
||
|
Scratch.app.runtime.interp.toggleThread(topBlock(), Scratch.app.viewedObj(), 1);
|
||
|
}
|
||
|
|
||
|
private function editArg(evt:MouseEvent):Boolean {
|
||
|
var arg:BlockArg = evt.target as BlockArg;
|
||
|
if (!arg) arg = evt.target.parent as BlockArg;
|
||
|
if (arg && arg.isEditable && (arg.parent == this)) {
|
||
|
arg.startEditing();
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private function focusChange(evt:Event):void {
|
||
|
evt.preventDefault();
|
||
|
if (evt.target.parent.parent != this) return; // make sure the target TextField is in this block, not a child block
|
||
|
if (args.length == 0) return;
|
||
|
var i:int, focusIndex:int = -1;
|
||
|
for (i = 0; i < args.length; i++) {
|
||
|
if (stage.focus == args[i].field) focusIndex = i;
|
||
|
}
|
||
|
i = focusIndex + 1;
|
||
|
while (true) {
|
||
|
if (i >= args.length) i = 0;
|
||
|
var f:TextField = args[i].field;
|
||
|
if ((f != null) && f.selectable) {
|
||
|
stage.focus = args[i].field;
|
||
|
args[i].field.setSelection(0, 10000000);
|
||
|
return;
|
||
|
}
|
||
|
i++
|
||
|
if (i == (focusIndex + 1)) return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}}
|