keyboard editing support

activate:
      - shift + click on a scripting pane's background
      - shift + click on any block
      - shift + enter in the IDE's edit mode

    stop editing:
      - left-click on scripting pane's background
      - esc

    navigate among scripts:
      - tab: next script
      - backtab (shift + tab): last script

    start editing a new script:
      - shift + enter

    navigate among commands within a script:
      - down arrow: next command
      - up arrow: last command

    navigate among all elements within a script:
      - right arrow: next element (block or input)
      - left arrow: last element

    move the currently edited script (stack of blocks):
      - shift + arrow keys (left, right, up, down)

    editing scripts:

      - backspace:
        * delete currently focused reporter
        * delete command above current insertion mark (blinking)
        * collapse currently focused variadic input by one element

      - enter:
        * edit currently focused input slot
        * expand currently focused variadic input by one element

      - space:
        * activate currently focused input slot's pull-down menu, if any
        * show a menu of reachable variables for the focused input or
reporter

      - any other key:
        start searching for insertable matching blocks

      - in menus triggered by this feature:
        * navigate with up / down arrow keys
        * trigger selection with enter
        * cancel menu with esc

      - in the search bar triggered b this feature:
        * keep typing / deleting to narrow and update matches
        * navigate among shown matches with up / down arrow keys
        * insert selected match at the focus' position with enter
        * cancel searching and inserting with esc

    running the currently edited script:
        * shift+ctrl+enter simulates clicking the edited script with
the mouse
pull/3/merge
Jens Mönig 2015-07-26 23:37:10 +02:00
rodzic 60554d0059
commit 76d9d6bd49
4 zmienionych plików z 1040 dodań i 75 usunięć

894
blocks.js
Wyświetl plik

@ -65,6 +65,7 @@
RingMorph
BoxMorph*
CommentMorph
ScriptFocusMorph
* from morphic.js
@ -155,7 +156,7 @@ DialogBoxMorph, BlockInputFragmentMorph, PrototypeHatBlockMorph, Costume*/
// Global stuff ////////////////////////////////////////////////////////
modules.blocks = '2015-June-25';
modules.blocks = '2015-July-26';
var SyntaxElementMorph;
var BlockMorph;
@ -182,6 +183,7 @@ var SymbolMorph;
var CommentMorph;
var ArgLabelMorph;
var TextSlotMorph;
var ScriptFocusMorph;
WorldMorph.prototype.customMorphs = function () {
// add examples to the world's demo menu
@ -624,6 +626,48 @@ SyntaxElementMorph.prototype.topBlock = function () {
return this;
};
// SyntaxElementMorph reachable variables
SyntaxElementMorph.prototype.getVarNamesDict = function () {
var block = this.parentThatIsA(BlockMorph),
rcvr,
tempVars = [],
dict;
if (!block) {
return {};
}
rcvr = block.receiver();
block.allParents().forEach(function (morph) {
if (morph instanceof PrototypeHatBlockMorph) {
tempVars.push.apply(
tempVars,
morph.inputs()[0].inputFragmentNames()
);
} else if (morph instanceof BlockMorph) {
morph.inputs().forEach(function (inp) {
if (inp instanceof TemplateSlotMorph) {
tempVars.push(inp.contents());
} else if (inp instanceof MultiArgMorph) {
inp.children.forEach(function (m) {
if (m instanceof TemplateSlotMorph) {
tempVars.push(m.contents());
}
});
}
});
}
});
if (rcvr) {
dict = rcvr.variables.allNamesDict();
tempVars.forEach(function (name) {
dict[name] = name;
});
return dict;
}
return {};
};
// SyntaxElementMorph drag & drop:
SyntaxElementMorph.prototype.reactToGrabOf = function (grabbedMorph) {
@ -2950,6 +2994,17 @@ BlockMorph.prototype.getHighlight = function () {
return null;
};
BlockMorph.prototype.outline = function (color, border) {
var highlight = new BlockHighlightMorph(),
fb = this.fullBounds(),
edge = border;
highlight.setExtent(fb.extent().add(edge * 2));
highlight.color = color;
highlight.image = this.highlightImage(color, border);
highlight.setPosition(fb.origin.subtract(new Point(edge, edge)));
return highlight;
};
// BlockMorph zebra coloring
BlockMorph.prototype.fixBlockColor = function (nearestBlock, isForced) {
@ -3096,7 +3151,11 @@ BlockMorph.prototype.fullCopy = function () {
BlockMorph.prototype.mouseClickLeft = function () {
var top = this.topBlock(),
receiver = top.receiver(),
shiftClicked = this.world().currentKey === 16,
stage;
if (shiftClicked && !this.isTemplate) {
return this.focus();
}
if (top instanceof PrototypeHatBlockMorph) {
return top.mouseClickLeft();
}
@ -3108,6 +3167,21 @@ BlockMorph.prototype.mouseClickLeft = function () {
}
};
BlockMorph.prototype.focus = function () {
var scripts = this.parentThatIsA(ScriptsMorph),
world = this.world(),
focus;
if (!scripts || !ScriptsMorph.prototype.enableKeyboard) {return; }
if (scripts.focus) {scripts.focus.stopEditing(); }
world.stopEditing();
focus = new ScriptFocusMorph(scripts, this);
scripts.focus = focus;
focus.getFocus(world);
if (this instanceof HatBlockMorph) {
focus.nextCommand();
}
};
// BlockMorph thumbnail
BlockMorph.prototype.thumbnail = function (scale, clipWidth) {
@ -4794,6 +4868,7 @@ ScriptsMorph.uber = FrameMorph.prototype;
ScriptsMorph.prototype.cleanUpMargin = 20;
ScriptsMorph.prototype.cleanUpSpacing = 15;
ScriptsMorph.prototype.isPreferringEmptySlots = true;
ScriptsMorph.prototype.enableKeyboard = true;
// ScriptsMorph instance creation:
@ -4813,6 +4888,9 @@ ScriptsMorph.prototype.init = function (owner) {
this.lastPreservedBlocks = null;
this.lastNextBlock = null;
// keyboard editing support:
this.focus = null;
ScriptsMorph.uber.init.call(this);
this.setColor(new Color(70, 70, 70));
};
@ -4823,6 +4901,9 @@ ScriptsMorph.prototype.fullCopy = function () {
var cpy = new ScriptsMorph(),
pos = this.position(),
child;
if (this.focus) {
this.focus.stopEditing();
}
this.children.forEach(function (morph) {
if (!morph.block) { // omit anchored comments
child = morph.fullCopy();
@ -4842,13 +4923,18 @@ ScriptsMorph.prototype.fullCopy = function () {
// ScriptsMorph stepping:
ScriptsMorph.prototype.step = function () {
var hand = this.world().hand,
var world = this.world(),
hand = world.hand,
block;
if (this.feedbackMorph.parent) {
this.feedbackMorph.destroy();
this.feedbackMorph.parent = null;
}
if (this.focus && (!world.keyboardReceiver ||
world.keyboardReceiver instanceof StageMorph)) {
this.focus.getFocus(world);
}
if (hand.children.length === 0) {
return null;
}
@ -5311,6 +5397,27 @@ ScriptsMorph.prototype.reactToDropOf = function (droppedMorph, hand) {
this.adjustBounds();
};
// ScriptsMorph events
ScriptsMorph.prototype.mouseClickLeft = function (pos) {
var shiftClicked = this.world().currentKey === 16;
if (shiftClicked) {
return this.edit(pos);
}
if (this.focus) {this.focus.stopEditing(); }
};
// ScriptsMorph keyboard support
ScriptsMorph.prototype.edit = function (pos) {
var world = this.world();
if (this.focus) {this.focus.stopEditing(); }
world.stopEditing();
if (!ScriptsMorph.prototype.enableKeyboard) {return; }
this.focus = new ScriptFocusMorph(this, this, pos);
this.focus.getFocus(world);
};
// ArgMorph //////////////////////////////////////////////////////////
/*
@ -6575,7 +6682,7 @@ InputSlotMorph.prototype.setContents = function (aStringOrFloat) {
// InputSlotMorph drop-down menu:
InputSlotMorph.prototype.dropDownMenu = function () {
InputSlotMorph.prototype.dropDownMenu = function (enableKeyboard) {
var choices = this.choices,
key,
menu = new MenuMorph(
@ -6606,7 +6713,12 @@ InputSlotMorph.prototype.dropDownMenu = function () {
}
}
if (menu.items.length > 0) {
menu.popUpAtHand(this.world());
if (enableKeyboard) {
menu.popup(this.world(), this.bottomLeft());
menu.getFocus();
} else {
menu.popUpAtHand(this.world());
}
} else {
return null;
}
@ -6861,46 +6973,6 @@ InputSlotMorph.prototype.soundsMenu = function () {
return dict;
};
InputSlotMorph.prototype.getVarNamesDict = function () {
var block = this.parentThatIsA(BlockMorph),
rcvr,
tempVars = [],
dict;
if (!block) {
return {};
}
rcvr = block.receiver();
block.allParents().forEach(function (morph) {
if (morph instanceof PrototypeHatBlockMorph) {
tempVars.push.apply(
tempVars,
morph.inputs()[0].inputFragmentNames()
);
} else if (morph instanceof BlockMorph) {
morph.inputs().forEach(function (inp) {
if (inp instanceof TemplateSlotMorph) {
tempVars.push(inp.contents());
} else if (inp instanceof MultiArgMorph) {
inp.children.forEach(function (m) {
if (m instanceof TemplateSlotMorph) {
tempVars.push(m.contents());
}
});
}
});
}
});
if (rcvr) {
dict = rcvr.variables.allNamesDict();
tempVars.forEach(function (name) {
dict[name] = name;
});
return dict;
}
return {};
};
InputSlotMorph.prototype.setChoices = function (dict, readonly) {
// externally specify choices and read-only status,
// used for custom blocks
@ -10981,3 +11053,737 @@ CommentMorph.prototype.destroy = function () {
CommentMorph.prototype.stackHeight = function () {
return this.height();
};
// ScriptFocusMorph //////////////////////////////////////////////////////////
/*
I offer keyboard navigation for syntax elements, blocks and scripts:
activate:
- shift + click on a scripting pane's background
- shift + click on any block
- shift + enter in the IDE's edit mode
stop editing:
- left-click on scripting pane's background
- esc
navigate among scripts:
- tab: next script
- backtab (shift + tab): last script
start editing a new script:
- shift + enter
navigate among commands within a script:
- down arrow: next command
- up arrow: last command
navigate among all elements within a script:
- right arrow: next element (block or input)
- left arrow: last element
move the currently edited script (stack of blocks):
- shift + arrow keys (left, right, up, down)
editing scripts:
- backspace:
* delete currently focused reporter
* delete command above current insertion mark (blinking)
* collapse currently focused variadic input by one element
- enter:
* edit currently focused input slot
* expand currently focused variadic input by one element
- space:
* activate currently focused input slot's pull-down menu, if any
* show a menu of reachable variables for the focused input or reporter
- any other key:
start searching for insertable matching blocks
- in menus triggered by this feature:
* navigate with up / down arrow keys
* trigger selection with enter
* cancel menu with esc
- in the search bar triggered b this feature:
* keep typing / deleting to narrow and update matches
* navigate among shown matches with up / down arrow keys
* insert selected match at the focus' position with enter
* cancel searching and inserting with esc
running the currently edited script:
* shift+ctrl+enter simulates clicking the edited script with the mouse
*/
// ScriptFocusMorph inherits from BoxMorph:
ScriptFocusMorph.prototype = new BoxMorph();
ScriptFocusMorph.prototype.constructor = ScriptFocusMorph;
ScriptFocusMorph.uber = BoxMorph.prototype;
// ScriptFocusMorph instance creation:
function ScriptFocusMorph(editor, initialElement, position) {
this.init(editor, initialElement, position);
}
ScriptFocusMorph.prototype.init = function (
editor,
initialElement,
position
) {
this.editor = editor; // a ScriptsMorph
this.element = initialElement;
this.atEnd = false;
ScriptFocusMorph.uber.init.call(this);
if (this.element instanceof ScriptsMorph) {
this.setPosition(position);
}
};
// ScriptFocusMorph keyboard focus:
ScriptFocusMorph.prototype.getFocus = function (world) {
if (!world) {world = this.world(); }
if (world && world.keyboardReceiver !== this) {
world.stopEditing();
}
world.keyboardReceiver = this;
this.fixLayout();
};
// ScriptFocusMorph layout:
ScriptFocusMorph.prototype.fixLayout = function () {
this.changed();
if (this.element instanceof CommandBlockMorph ||
this.element instanceof CommandSlotMorph ||
this.element instanceof ScriptsMorph) {
this.manifestStatement();
} else {
this.manifestExpression();
}
this.editor.add(this); // come to front
this.scrollIntoView();
this.changed();
};
ScriptFocusMorph.prototype.manifestStatement = function () {
var newScript = this.element instanceof ScriptsMorph,
y = this.element.top();
this.border = 0;
this.edge = 0;
this.alpha = 1;
this.color = this.editor.feedbackColor;
this.setExtent(new Point(
newScript ?
SyntaxElementMorph.prototype.hatWidth : this.element.width(),
Math.max(
SyntaxElementMorph.prototype.corner,
SyntaxElementMorph.prototype.feedbackMinHeight
)
));
if (this.element instanceof CommandSlotMorph) {
y += SyntaxElementMorph.prototype.corner;
} else if (this.atEnd) {
y = this.element.bottom();
}
if (!newScript) {
this.setPosition(new Point(
this.element.left(),
y
));
}
this.fps = 2;
this.show();
this.step = function () {
this.toggleVisibility();
};
};
ScriptFocusMorph.prototype.manifestExpression = function () {
this.edge = SyntaxElementMorph.prototype.rounding;
this.border = Math.max(
SyntaxElementMorph.prototype.edge,
3
);
this.color = this.editor.feedbackColor.copy();
this.color.a = 0.5;
this.borderColor = this.editor.feedbackColor;
this.bounds = this.element.fullBounds()
.expandBy(Math.max(
SyntaxElementMorph.prototype.edge * 2,
SyntaxElementMorph.prototype.reporterDropFeedbackPadding
));
this.drawNew();
delete this.fps;
delete this.step;
this.show();
};
// ScriptFocusMorph editing
ScriptFocusMorph.prototype.trigger = function () {
var current = this.element;
if (current instanceof MultiArgMorph) {
if (current.arrows().children[1].isVisible) {
current.addInput();
this.fixLayout();
}
return;
}
if (current.parent instanceof TemplateSlotMorph) {
current.mouseClickLeft();
return;
}
if (current instanceof InputSlotMorph) {
if (!current.isReadOnly) {
delete this.fps;
delete this.step;
this.hide();
current.contents().edit();
this.world().onNextStep = function () {
current.contents().selectAll();
};
} else if (current.choices) {
current.dropDownMenu(true);
delete this.fps;
delete this.step;
this.hide();
}
}
};
ScriptFocusMorph.prototype.menu = function () {
var current = this.element;
if (current instanceof InputSlotMorph && current.choices) {
current.dropDownMenu(true);
delete this.fps;
delete this.step;
this.hide();
} else {
this.insertVariableGetter();
}
};
ScriptFocusMorph.prototype.deleteLastElement = function () {
var current = this.element;
if (current.parent instanceof ScriptsMorph) {
if (this.atEnd || current instanceof ReporterBlockMorph) {
current.destroy();
this.element = this.editor;
this.atEnd = false;
}
} else if (current instanceof MultiArgMorph) {
if (current.arrows().children[0].isVisible) {
current.removeInput();
}
} else if (current instanceof ReporterBlockMorph) {
if (!current.isTemplate) {
this.lastElement();
current.prepareToBeGrabbed();
current.destroy();
}
} else if (current instanceof CommandBlockMorph) {
if (this.atEnd) {
this.element = current.parent;
current.userDestroy();
} else {
if (current.parent instanceof CommandBlockMorph) {
current.parent.userDestroy();
}
}
}
this.editor.adjustBounds();
this.fixLayout();
};
ScriptFocusMorph.prototype.insertBlock = function (block) {
var pb;
block.isTemplate = false;
block.isDraggable = true;
if (block.snapSound) {
block.snapSound.play();
}
if (this.element instanceof ScriptsMorph) {
this.editor.add(block);
this.element = block;
if (block instanceof CommandBlockMorph) {
block.setLeft(this.left());
if (block.isStop()) {
block.setTop(this.top());
} else {
block.setBottom(this.top());
this.atEnd = true;
}
} else {
block.setCenter(this.center());
block.setLeft(this.left());
}
} else if (this.element instanceof CommandBlockMorph) {
if (this.atEnd) {
this.element.nextBlock(block);
this.element = block;
this.fixLayout();
} else {
// to be done: special case if block.isStop()
pb = this.element.parent;
if (pb instanceof ScriptsMorph) { // top block
block.setLeft(this.element.left());
block.setBottom(this.element.top() + this.element.corner);
this.editor.add(block);
block.nextBlock(this.element);
this.fixLayout();
} else if (pb instanceof CommandSlotMorph) {
pb.nestedBlock(block);
} else if (pb instanceof CommandBlockMorph) {
pb.nextBlock(block);
}
}
} else if (this.element instanceof CommandSlotMorph) {
// to be done: special case if block.isStop()
this.element.nestedBlock(block);
this.element = block;
this.atEnd = true;
} else {
pb = this.element.parent;
if (pb instanceof ScriptsMorph) {
this.editor.add(block);
block.setPosition(this.element.position());
this.element.destroy();
} else {
pb.replaceInput(this.element, block);
}
this.element = block;
}
block.fixBlockColor();
this.editor.adjustBounds();
// block.scrollIntoView();
this.fixLayout();
};
ScriptFocusMorph.prototype.insertVariableGetter = function () {
var types = this.blockTypes(),
vars,
myself = this,
menu = new MenuMorph();
if (!types || !contains(types, 'reporter')) {
return;
}
vars = InputSlotMorph.prototype.getVarNamesDict.call(this.element);
Object.keys(vars).forEach(function (vName) {
var block = SpriteMorph.prototype.variableBlock(vName);
block.addShadow(new Point(3, 3));
menu.addItem(
block,
function () {
block.removeShadow();
myself.insertBlock(block);
}
);
});
if (menu.items.length > 0) {
menu.popup(this.world(), this.element.bottomLeft());
menu.getFocus();
}
};
ScriptFocusMorph.prototype.stopEditing = function () {
this.editor.focus = null;
this.world().keyboardReceiver = null;
this.destroy();
};
// ScriptFocusMorph navigation
ScriptFocusMorph.prototype.lastElement = function () {
var items = this.items(),
idx;
if (!items.length) {
this.shiftScript(new Point(-50, 0));
return;
}
if (this.atEnd) {
this.element = items[items.length - 1];
this.atEnd = false;
} else {
idx = items.indexOf(this.element) - 1;
if (idx < 0) {idx = items.length - 1; }
this.element = items[idx];
}
if (this.element instanceof CommandSlotMorph &&
this.element.nestedBlock()) {
this.lastElement();
} else if (this.element instanceof HatBlockMorph) {
if (items.length > 1) {
this.lastElement();
} else {
this.atEnd = true;
}
}
this.fixLayout();
};
ScriptFocusMorph.prototype.nextElement = function () {
var items = this.items(), idx, nb;
if (!items.length) {
this.shiftScript(new Point(50, 0));
return;
}
idx = items.indexOf(this.element) + 1;
if (idx >= items.length) {
idx = 0;
}
this.atEnd = false;
this.element = items[idx];
if (this.element instanceof CommandSlotMorph) {
nb = this.element.nestedBlock();
if (nb) {this.element = nb; }
} else if (this.element instanceof HatBlockMorph) {
if (items.length === 1) {
this.atEnd = true;
} else {
this.nextElement();
}
}
this.fixLayout();
};
ScriptFocusMorph.prototype.lastCommand = function () {
var cm = this.element.parentThatIsA(CommandBlockMorph),
pb;
if (!cm) {
if (this.element instanceof ScriptsMorph) {
this.shiftScript(new Point(0, -50));
}
return;
}
if (this.element instanceof CommandBlockMorph) {
if (this.atEnd) {
this.atEnd = false;
} else {
pb = cm.parent.parentThatIsA(CommandBlockMorph);
if (pb) {
this.element = pb;
} else {
pb = cm.topBlock().bottomBlock();
if (pb) {
this.element = pb;
this.atEnd = true;
}
}
}
} else {
this.element = cm;
this.atEnd = false;
}
if (this.element instanceof HatBlockMorph && !this.atEnd) {
this.lastCommand();
}
this.fixLayout();
};
ScriptFocusMorph.prototype.nextCommand = function () {
var cm = this.element,
tb,
nb,
cs;
if (cm instanceof ScriptsMorph) {
this.shiftScript(new Point(0, 50));
return;
}
while (!(cm instanceof CommandBlockMorph)) {
cm = cm.parent;
if (cm instanceof ScriptsMorph) {
return;
}
}
if (this.atEnd) {
cs = cm.parentThatIsA(CommandSlotMorph);
if (cs) {
this.element = cs.parentThatIsA(CommandBlockMorph);
this.atEnd = false;
this.nextCommand();
} else {
tb = cm.topBlock().parentThatIsA(CommandBlockMorph);
if (tb) {
this.element = tb;
this.atEnd = false;
if (this.element instanceof HatBlockMorph) {
this.nextCommand();
}
}
}
} else {
nb = cm.nextBlock();
if (nb) {
this.element = nb;
} else {
this.element = cm;
this.atEnd = true;
}
}
this.fixLayout();
};
ScriptFocusMorph.prototype.nextScript = function () {
var scripts = this.sortedScripts(),
idx;
if (scripts.length < 1) {return; }
if (this.element instanceof ScriptsMorph) {
this.element = scripts[0];
}
idx = scripts.indexOf(this.element.topBlock()) + 1;
if (idx >= scripts.length) {idx = 0; }
this.element = scripts[idx];
this.element.scrollIntoView();
this.atEnd = false;
if (this.element instanceof HatBlockMorph) {
return this.nextElement();
}
this.fixLayout();
};
ScriptFocusMorph.prototype.lastScript = function () {
var scripts = this.sortedScripts(),
idx;
if (scripts.length < 1) {return; }
if (this.element instanceof ScriptsMorph) {
this.element = scripts[0];
}
idx = scripts.indexOf(this.element.topBlock()) - 1;
if (idx < 0) {idx = scripts.length - 1; }
this.element = scripts[idx];
this.element.scrollIntoView();
this.atEnd = false;
if (this.element instanceof HatBlockMorph) {
return this.nextElement();
}
this.fixLayout();
};
ScriptFocusMorph.prototype.shiftScript = function (deltaPoint) {
var tb;
if (this.element instanceof ScriptsMorph) {
this.moveBy(deltaPoint);
} else {
tb = this.element.topBlock();
if (tb && !(tb instanceof PrototypeHatBlockMorph)) {
tb.moveBy(deltaPoint);
}
}
this.editor.adjustBounds();
this.fixLayout();
};
ScriptFocusMorph.prototype.newScript = function () {
var pos = this.position();
if (!(this.element instanceof ScriptsMorph)) {
pos = this.element.topBlock().fullBounds().bottomLeft().add(
new Point(0, 50)
);
}
this.setPosition(pos);
this.element = this.editor;
this.editor.adjustBounds();
this.fixLayout();
};
ScriptFocusMorph.prototype.runScript = function () {
if (this.element instanceof ScriptsMorph) {return; }
this.element.topBlock().mouseClickLeft();
};
ScriptFocusMorph.prototype.items = function () {
if (this.element instanceof ScriptsMorph) {return []; }
var script = this.element.topBlock();
return script.allChildren().filter(function (each) {
return each instanceof SyntaxElementMorph &&
!(each instanceof TemplateSlotMorph) &&
(!each.isStatic ||
each.choices ||
each instanceof MultiArgMorph ||
each instanceof CommandSlotMorph);
});
};
ScriptFocusMorph.prototype.sortedScripts = function () {
var scripts = this.editor.children.filter(function (each) {
return each instanceof BlockMorph;
});
scripts.sort(function (a, b) {
// make sure the prototype hat block always stays on top
return a instanceof PrototypeHatBlockMorph ? 0 : a.top() - b.top();
});
return scripts;
};
// ScriptFocusMorph block types
ScriptFocusMorph.prototype.blockTypes = function () {
// answer an array of possible block types that fit into
// the current situation, NULL if no block can be inserted
if (this.element.isTemplate) {return null; }
if (this.element instanceof ScriptsMorph) {
return ['hat', 'command', 'reporter', 'predicate', 'ring'];
}
if (this.element instanceof HatBlockMorph ||
this.element instanceof CommandSlotMorph) {
return ['command'];
}
if (this.element instanceof CommandBlockMorph) {
if (this.atEnd && this.element.isStop()) {
return null;
}
if (this.element.parent instanceof ScriptsMorph) {
return ['hat', 'command'];
}
return ['command'];
}
if (this.element instanceof ReporterBlockMorph) {
if (this.element.getSlotSpec() === '%n') {
return ['reporter'];
}
return ['reporter', 'predicate', 'ring'];
}
if (this.element.getSpec() === '%n') {
return ['reporter'];
}
if (this.element.isStatic) {
return null;
}
return ['reporter', 'predicate', 'ring'];
};
// ScriptFocusMorph keyboard events
ScriptFocusMorph.prototype.processKeyDown = function (event) {
this.processKeyEvent(
event,
this.reactToKeyEvent
);
};
ScriptFocusMorph.prototype.processKeyUp = function (event) {
nop(event);
};
ScriptFocusMorph.prototype.processKeyPress = function (event) {
nop(event);
};
ScriptFocusMorph.prototype.processKeyEvent = function (event, action) {
var keyName, ctrl, shift;
//console.log(event.keyCode);
this.world().hand.destroyTemporaries(); // remove result bubbles, if any
switch (event.keyCode) {
case 8:
keyName = 'backspace';
break;
case 9:
keyName = 'tab';
break;
case 13:
keyName = 'enter';
break;
case 16:
case 17:
case 18:
return;
case 27:
keyName = 'esc';
break;
case 32:
keyName = 'space';
break;
case 37:
keyName = 'left arrow';
break;
case 39:
keyName = 'right arrow';
break;
case 38:
keyName = 'up arrow';
break;
case 40:
keyName = 'down arrow';
break;
default:
keyName = String.fromCharCode(event.keyCode || event.charCode);
}
ctrl = (event.ctrlKey || event.metaKey) ? 'ctrl ' : '';
shift = event.shiftKey ? 'shift ' : '';
keyName = ctrl + shift + keyName;
action.call(this, keyName);
};
ScriptFocusMorph.prototype.reactToKeyEvent = function (key) {
var evt = key.toLowerCase(),
shift = 50,
types,
vNames;
// console.log(evt);
switch (evt) {
case 'esc':
return this.stopEditing();
case 'enter':
return this.trigger();
case 'shift enter':
return this.newScript();
case 'ctrl shift enter':
return this.runScript();
case 'space':
return this.menu();
case 'left arrow':
return this.lastElement();
case 'shift left arrow':
return this.shiftScript(new Point(-shift, 0));
case 'right arrow':
return this.nextElement();
case 'shift right arrow':
return this.shiftScript(new Point(shift, 0));
case 'up arrow':
return this.lastCommand();
case 'shift up arrow':
return this.shiftScript(new Point(0, -shift));
case 'down arrow':
return this.nextCommand();
case 'shift down arrow':
return this.shiftScript(new Point(0, shift));
case 'tab':
return this.nextScript();
case 'shift tab':
return this.lastScript();
case 'backspace':
return this.deleteLastElement();
default:
types = this.blockTypes();
if (!(this.element instanceof ScriptsMorph) &&
contains(types, 'reporter')) {
vNames = Object.keys(this.element.getVarNamesDict());
}
if (types) {
delete this.fps;
delete this.step;
this.show();
this.editor.owner.searchBlocks(
key,
types,
vNames,
this
);
}
}
};

Wyświetl plik

@ -106,7 +106,7 @@ SymbolMorph, isNil*/
// Global stuff ////////////////////////////////////////////////////////
modules.byob = '2015-June-25';
modules.byob = '2015-July-26';
// Declarations
@ -1966,8 +1966,13 @@ PrototypeHatBlockMorph.prototype.init = function (definition) {
PrototypeHatBlockMorph.prototype.mouseClickLeft = function () {
// relay the mouse click to my prototype block to
// pop-up a Block Dialog
// pop-up a Block Dialog, unless the shift key
// is pressed, in which case initiate keyboard
// editing support
if (this.world().currentKey === 16) { // shift-clicked
return this.focus();
}
this.children[0].mouseClickLeft();
};

48
gui.js
Wyświetl plik

@ -65,11 +65,11 @@ ScriptsMorph, isNil, SymbolMorph, BlockExportDialogMorph,
BlockImportDialogMorph, SnapTranslator, localize, List, InputSlotMorph,
SnapCloud, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512,
sb, CommentMorph, CommandBlockMorph, BlockLabelPlaceHolderMorph, Audio,
SpeechBubbleMorph*/
SpeechBubbleMorph, ScriptFocusMorph*/
// Global stuff ////////////////////////////////////////////////////////
modules.gui = '2015-June-25';
modules.gui = '2015-July-26';
// Declarations
@ -1806,7 +1806,8 @@ IDE_Morph.prototype.applySavedSettings = function () {
click = this.getSetting('click'),
longform = this.getSetting('longform'),
longurls = this.getSetting('longurls'),
plainprototype = this.getSetting('plainprototype');
plainprototype = this.getSetting('plainprototype'),
keyboard = this.getSetting('keyboard');
// design
if (design === 'flat') {
@ -1846,6 +1847,13 @@ IDE_Morph.prototype.applySavedSettings = function () {
this.projectsInURLs = false;
}
// keyboard editing
if (keyboard) {
ScriptsMorph.prototype.enableKeyboard = true;
} else {
ScriptsMorph.prototype.enableKeyboard = false;
}
// plain prototype labels
if (plainprototype) {
BlockLabelPlaceHolderMorph.prototype.plainLabel = true;
@ -2352,6 +2360,22 @@ IDE_Morph.prototype.settingsMenu = function () {
'check to enable\nsprite composition',
true
);
addPreference(
'Keyboard Editing',
function () {
ScriptsMorph.prototype.enableKeyboard =
!ScriptsMorph.prototype.enableKeyboard;
if (ScriptsMorph.prototype.enableKeyboard) {
myself.saveSetting('keyboard', true);
} else {
myself.removeSetting('keyboard');
}
},
ScriptsMorph.prototype.enableKeyboard,
'uncheck to disable\nkeyboard editing support',
'check to enable\nkeyboard editing support',
false
);
menu.addLine(); // everything below this line is stored in the project
addPreference(
'Thread safe scripts',
@ -2840,6 +2864,7 @@ IDE_Morph.prototype.newProject = function () {
StageMorph.prototype.codeMappings = {};
StageMorph.prototype.codeHeaders = {};
StageMorph.prototype.enableCodeMapping = false;
StageMorph.prototype.enableInheritance = false;
SpriteMorph.prototype.useFlatLineEnds = false;
this.setProjectName('');
this.projectNotes = '';
@ -3059,6 +3084,7 @@ IDE_Morph.prototype.rawOpenProjectString = function (str) {
StageMorph.prototype.codeMappings = {};
StageMorph.prototype.codeHeaders = {};
StageMorph.prototype.enableCodeMapping = false;
StageMorph.prototype.enableInheritance = false;
if (Process.prototype.isCatchingErrors) {
try {
this.serializer.openProject(
@ -3100,6 +3126,7 @@ IDE_Morph.prototype.rawOpenCloudDataString = function (str) {
StageMorph.prototype.codeMappings = {};
StageMorph.prototype.codeHeaders = {};
StageMorph.prototype.enableCodeMapping = false;
StageMorph.prototype.enableInheritance = false;
if (Process.prototype.isCatchingErrors) {
try {
model = this.serializer.parse(str);
@ -3446,6 +3473,9 @@ IDE_Morph.prototype.toggleAppMode = function (appMode) {
morph.hide();
}
});
if (world.keyboardReceiver instanceof ScriptFocusMorph) {
world.keyboardReceiver.stopEditing();
}
} else {
this.setColor(this.backgroundColor);
this.controlBar.setColor(this.frameColor);
@ -5220,7 +5250,7 @@ function SpriteIconMorph(aSprite, aTemplate) {
}
SpriteIconMorph.prototype.init = function (aSprite, aTemplate) {
var colors, action, query, myself = this;
var colors, action, query, hover, myself = this;
if (!aTemplate) {
colors = [
@ -5250,6 +5280,11 @@ SpriteIconMorph.prototype.init = function (aSprite, aTemplate) {
return false;
};
hover = function () {
if (!aSprite.exemplar) {return null; }
return (localize('parent' + ':\n' + aSprite.exemplar.name));
};
// additional properties:
this.object = aSprite || new SpriteMorph(); // mandatory, actually
this.version = this.object.version;
@ -5265,7 +5300,7 @@ SpriteIconMorph.prototype.init = function (aSprite, aTemplate) {
this.object.name, // label string
query, // predicate/selector
null, // environment
null, // hint
hover, // hint
aTemplate // optional, for cached background images
);
@ -5436,6 +5471,9 @@ SpriteIconMorph.prototype.userMenu = function () {
menu.addItem("duplicate", 'duplicateSprite');
menu.addItem("delete", 'removeSprite');
menu.addLine();
if (StageMorph.prototype.enableInheritance) {
menu.addItem("parent...", 'chooseExemplar');
}
if (this.object.anchor) {
menu.addItem(
localize('detach from') + ' ' + this.object.anchor.name,

Wyświetl plik

@ -900,17 +900,20 @@ SpriteMorph.prototype.initBlocks = function () {
reifyScript: {
type: 'ring',
category: 'other',
spec: '%rc %ringparms'
spec: '%rc %ringparms',
alias: 'command ring lambda'
},
reifyReporter: {
type: 'ring',
category: 'other',
spec: '%rr %ringparms'
spec: '%rr %ringparms',
alias: 'reporter ring lambda'
},
reifyPredicate: {
type: 'ring',
category: 'other',
spec: '%rp %ringparms'
spec: '%rp %ringparms',
alias: 'predicate ring lambda'
},
reportSum: {
type: 'reporter',
@ -920,12 +923,14 @@ SpriteMorph.prototype.initBlocks = function () {
reportDifference: {
type: 'reporter',
category: 'operators',
spec: '%n \u2212 %n'
spec: '%n \u2212 %n',
alias: '-'
},
reportProduct: {
type: 'reporter',
category: 'operators',
spec: '%n \u00D7 %n'
spec: '%n \u00D7 %n',
alias: '*'
},
reportQuotient: {
type: 'reporter',
@ -2357,15 +2362,30 @@ SpriteMorph.prototype.freshPalette = function (category) {
// SpriteMorph blocks searching
SpriteMorph.prototype.blocksMatching = function (searchString, strictly) {
SpriteMorph.prototype.blocksMatching = function (
searchString,
strictly,
types, // optional, ['hat', 'command', 'reporter', 'predicate']
varNames // optional, list of reachable unique variable names
) {
// answer an array of block templates whose spec contains
// the given search string, ordered by descending relevance
// types is an optional array containing block types the search
// is limited to, e.g. "command", "hat", "reporter", "predicate".
// Note that "predicate" is not subsumed by "reporter" and has
// to be specified explicitly.
// if no types are specified all blocks are searched
var blocks = [],
blocksDict,
myself = this,
search = searchString.toLowerCase(),
stage = this.parentThatIsA(StageMorph);
if (!types || !types.length) {
types = ['hat', 'command', 'reporter', 'predicate', 'ring'];
}
if (!varNames) {varNames = []; }
function labelOf(aBlockSpec) {
var words = (BlockMorph.prototype.parseSpec(aBlockSpec)),
filtered = words.filter(
@ -2396,29 +2416,39 @@ SpriteMorph.prototype.blocksMatching = function (searchString, strictly) {
return newBlock;
}
// variable getters
varNames.forEach(function (vName) {
var rel = relevance(labelOf(vName), search);
if (rel !== -1) {
blocks.push([myself.variableBlock(vName), rel + '1']);
}
});
// custom blocks
[this.customBlocks, stage.globalBlocks].forEach(function (blocksList) {
blocksList.forEach(function (definition) {
var spec = localize(definition.blockSpec()).toLowerCase(),
rel = relevance(labelOf(spec), search);
if (rel !== -1) {
blocks.push([definition.templateInstance(), rel + '1']);
if (contains(types, definition.type)) {
var spec = localize(definition.blockSpec()).toLowerCase(),
rel = relevance(labelOf(spec), search);
if (rel !== -1) {
blocks.push([definition.templateInstance(), rel + '2']);
}
}
});
});
// primitives
blocksDict = SpriteMorph.prototype.blocks;
Object.keys(blocksDict).forEach(function (selector) {
if (!StageMorph.prototype.hiddenPrimitives[selector]) {
if (!StageMorph.prototype.hiddenPrimitives[selector] &&
contains(types, blocksDict[selector].type)) {
var block = blocksDict[selector],
spec = localize(block.spec).toLowerCase(),
spec = localize(block.alias || block.spec).toLowerCase(),
rel = relevance(labelOf(spec), search);
if (
(rel !== -1) &&
(!block.dev) &&
(!block.only || (block.only === myself.constructor))
) {
blocks.push([primitive(selector), rel + '2']);
blocks.push([primitive(selector), rel + '3']);
}
}
});
@ -2426,18 +2456,43 @@ SpriteMorph.prototype.blocksMatching = function (searchString, strictly) {
return blocks.map(function (each) {return each[0]; });
};
SpriteMorph.prototype.searchBlocks = function () {
SpriteMorph.prototype.searchBlocks = function (
searchString,
types,
varNames,
scriptFocus
) {
var myself = this,
unit = SyntaxElementMorph.prototype.fontSize,
ide = this.parentThatIsA(IDE_Morph),
oldSearch = '',
searchBar = new InputFieldMorph(''),
searchPane = ide.createPalette('forSearch');
searchBar = new InputFieldMorph(searchString || ''),
searchPane = ide.createPalette('forSearch'),
blocksList = [],
selection,
focus;
function showSelection() {
if (focus) {focus.destroy(); }
if (!selection || !scriptFocus) {return; }
focus = selection.outline(
MorphicPreferences.isFlat ? new Color(150, 200, 255)
: new Color(255, 255, 255),
2
);
searchPane.contents.add(focus);
focus.scrollIntoView();
}
function show(blocks) {
var oldFlag = Morph.prototype.trackChanges,
x = searchPane.contents.left() + 5,
y = (searchBar.bottom() + unit);
blocksList = blocks;
selection = null;
if (blocks.length && scriptFocus) {
selection = blocks[0];
}
Morph.prototype.trackChanges = false;
searchPane.contents.children = [searchPane.contents.children[0]];
blocks.forEach(function (block) {
@ -2447,6 +2502,7 @@ SpriteMorph.prototype.searchBlocks = function () {
y += unit * 0.3;
});
Morph.prototype.trackChanges = oldFlag;
showSelection();
searchPane.changed();
}
@ -2463,17 +2519,52 @@ SpriteMorph.prototype.searchBlocks = function () {
searchBar.drawNew();
searchPane.accept = function () {
var search = searchBar.getValue();
if (search.length > 0) {
show(myself.blocksMatching(search));
var search;
if (scriptFocus) {
searchBar.cancel();
if (selection) {
scriptFocus.insertBlock(selection);
}
} else {
search = searchBar.getValue();
if (search.length > 0) {
show(myself.blocksMatching(search));
}
}
};
searchPane.reactToKeystroke = function () {
var search = searchBar.getValue();
if (search !== oldSearch) {
oldSearch = search;
show(myself.blocksMatching(search, search.length < 2));
searchPane.reactToKeystroke = function (evt) {
var search, idx, code = evt ? evt.keyCode : 0;
switch (code) {
case 38: // up arrow
if (!scriptFocus || !selection) {return; }
idx = blocksList.indexOf(selection) - 1;
if (idx < 0) {
idx = blocksList.length - 1;
}
selection = blocksList[idx];
showSelection();
return;
case 40: // down arrow
if (!scriptFocus || !selection) {return; }
idx = blocksList.indexOf(selection) + 1;
if (idx >= blocksList.length) {
idx = 0;
}
selection = blocksList[idx];
showSelection();
return;
default:
search = searchBar.getValue();
if (search !== oldSearch) {
oldSearch = search;
show(myself.blocksMatching(
search,
search.length < 2,
types,
varNames
));
}
}
};
@ -2484,6 +2575,7 @@ SpriteMorph.prototype.searchBlocks = function () {
ide.fixLayout('refreshPalette');
searchBar.edit();
if (searchString) {searchPane.reactToKeystroke(); }
};
// SpriteMorph variable management
@ -4791,6 +4883,8 @@ StageMorph.prototype.processKeyEvent = function (event, action) {
keyName = 'enter';
if (event.ctrlKey || event.metaKey) {
keyName = 'ctrl enter';
} else if (event.shiftKey) {
keyName = 'shift enter';
}
break;
case 27:
@ -4831,6 +4925,9 @@ StageMorph.prototype.fireKeyEvent = function (key) {
if (evt === 'ctrl enter') {
return this.fireGreenFlagEvent();
}
if (evt === 'shift enter') {
return this.editScripts();
}
if (evt === 'ctrl f') {
if (!ide.isAppMode) {ide.currentSprite.searchBlocks(); }
return;
@ -4931,6 +5028,25 @@ StageMorph.prototype.removeAllClones = function () {
this.cloneCount = 0;
};
StageMorph.prototype.editScripts = function () {
var ide = this.parentThatIsA(IDE_Morph),
scripts,
sorted;
if (ide.isAppMode || !ScriptsMorph.prototype.enableKeyboard) {return; }
scripts = this.parentThatIsA(IDE_Morph).currentSprite.scripts;
scripts.edit(scripts.position());
sorted = scripts.focus.sortedScripts();
if (sorted.length) {
scripts.focus.element = sorted[0];
if (scripts.focus.element instanceof HatBlockMorph) {
scripts.focus.nextCommand();
}
} else {
scripts.focus.moveBy(new Point(50, 50));
}
scripts.focus.fixLayout();
};
// StageMorph block templates
StageMorph.prototype.blockTemplates = function (category) {