/**
* Visual Blocks Editor
*
* Copyright 2011 Google Inc.
* http://code.google.com/p/google-blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Object representing a code comment.
* @author fraser@google.com (Neil Fraser)
*/
/**
* Class for a comment.
* @param {!Blockly.Block} block The block associated with this comment.
* @param {!Element} commentGroup The SVG group to append the comment bubble.
* @constructor
*/
Blockly.Comment = function(block, commentGroup) {
this.block_ = block;
this.createIcon_(block);
this.createBubble_(commentGroup);
this.setPinned(false);
this.updateColour();
};
/**
* Radius of the comment icon.
*/
Blockly.Comment.ICON_RADIUS = 8;
/**
* Width of the border around the comment bubble.
*/
Blockly.Comment.BORDER_WIDTH = 6;
/**
* Determines the thickness of the base of the arrow in relation to the size
* of the comment bubble. Higher numbers result in thinner arrows.
*/
Blockly.Comment.ARROW_THICKNESS = 10;
/**
* The number of degrees that the arrow bends counter-clockwise.
*/
Blockly.Comment.ARROW_ANGLE = 20;
/**
* The sharpness of the arrow's bend. Higher numbers result in smoother arrows.
*/
Blockly.Comment.ARROW_BEND = 4;
/**
* Wrapper function called when a mouseUp occurs during a drag operation.
* @type {Function}
*/
Blockly.Comment.onMouseUpWrapper_ = null;
/**
* Wrapper function called when a mouseMove occurs during a drag operation.
* @type {Function}
*/
Blockly.Comment.onMouseMoveWrapper_ = null;
/**
* Stop binding to the global mouseup and mousemove events.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.Comment.unbindDragEvents_ = function(e) {
if (Blockly.Comment.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.svgDoc, 'mouseup',
Blockly.Comment.onMouseUpWrapper_);
Blockly.Comment.onMouseUpWrapper_ = null;
}
if (Blockly.Comment.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.svgDoc, 'mousemove',
Blockly.Comment.onMouseMoveWrapper_);
Blockly.Comment.onMouseMoveWrapper_ = null;
}
};
/**
* X coordinate of icon's center.
* @private
*/
Blockly.Comment.prototype.iconX_ = 0;
/**
* Y coordinate of icon's centre.
* @private
*/
Blockly.Comment.prototype.iconY_ = 0;
/**
* Absolute X coordinate of bubble.
* The initial value controls the default horizontal offset from the block.
* @private
*/
Blockly.Comment.prototype.offsetLeft_ = -100;
/**
* Absolute Y coordinate of bubble.
* The initial value controls the default vertical offset from the block.
* @private
*/
Blockly.Comment.prototype.offsetTop_ = -120;
/**
* Width of bubble.
* @private
*/
Blockly.Comment.prototype.width_ = 160;
/**
* Height of bubble.
* @private
*/
Blockly.Comment.prototype.height_ = 80;
/**
* Is the comment always visible?
* @private
*/
Blockly.Comment.prototype.isPinned_ = false;
/**
* Create the icon on the block.
* @param {!Blockly.Block} block The block associated with this comment.
* @private
*/
Blockly.Comment.prototype.createIcon_ = function(block) {
/* Here's the markup that will be generated:
*/
this.iconGroup_ = Blockly.svgDoc.createElementNS(SVG_NS, 'g');
this.iconGroup_.setAttribute('class', 'commentGroup');
var iconShield = Blockly.svgDoc.createElementNS(SVG_NS, 'circle');
iconShield.setAttribute('class', 'commentShield');
iconShield.setAttribute('r', Blockly.Comment.ICON_RADIUS);
iconShield.setAttribute('cx', Blockly.Comment.ICON_RADIUS);
iconShield.setAttribute('cy', Blockly.Comment.ICON_RADIUS);
this.iconMark_ = Blockly.svgDoc.createElementNS(SVG_NS, 'text');
this.iconMark_.setAttribute('class', 'commentMark');
this.iconMark_.appendChild(Blockly.svgDoc.createTextNode('?'));
this.iconMark_.setAttribute('x', Blockly.Comment.ICON_RADIUS / 2);
this.iconMark_.setAttribute('y', 2 * Blockly.Comment.ICON_RADIUS - 3);
this.iconGroup_.appendChild(iconShield);
this.iconGroup_.appendChild(this.iconMark_);
block.svgGroup.appendChild(this.iconGroup_);
Blockly.bindEvent_(this.iconGroup_, 'click', this, this.iconClick_);
Blockly.bindEvent_(this.iconGroup_, 'mouseover', this, this.iconMouseOver_);
Blockly.bindEvent_(this.iconGroup_, 'mouseout', this, this.iconMouseOut_);
};
/**
* Create the icon on the block.
* @param {!Element} commentGroup The SVG group to append the comment bubble.
* @private
*/
Blockly.Comment.prototype.createBubble_ = function(commentGroup) {
/* Create the editor. Here's the markup that will be generated:
*/
this.bubbleGroup_ = Blockly.svgDoc.createElementNS(SVG_NS, 'g');
var bubbleEmboss = Blockly.svgDoc.createElementNS(SVG_NS, 'g');
bubbleEmboss.setAttribute('filter', 'url(#emboss)');
this.bubbleArrow_ = Blockly.svgDoc.createElementNS(SVG_NS, 'path');
this.bubbleBack_ = Blockly.svgDoc.createElementNS(SVG_NS, 'rect');
this.bubbleBack_.setAttribute('class', 'dragable');
this.bubbleBack_.setAttribute('x', 0);
this.bubbleBack_.setAttribute('y', 0);
this.bubbleBack_.setAttribute('rx', Blockly.Comment.BORDER_WIDTH);
this.bubbleBack_.setAttribute('ry', Blockly.Comment.BORDER_WIDTH);
this.resizeGroup_ = Blockly.svgDoc.createElementNS(SVG_NS, 'g');
this.resizeGroup_.setAttribute('class', 'resizeSE');
var resizeSize = 2 * Blockly.Comment.BORDER_WIDTH;
var resizeBack = Blockly.svgDoc.createElementNS(SVG_NS, 'polygon');
resizeBack.setAttribute('points', '0,x x,x x,0'.replace(/x/g, resizeSize));
var resizeLine1 = Blockly.svgDoc.createElementNS(SVG_NS, 'line');
resizeLine1.setAttribute('class', 'resizeLine');
resizeLine1.setAttribute('x1', resizeSize / 3);
resizeLine1.setAttribute('y1', resizeSize - 1);
resizeLine1.setAttribute('x2', resizeSize - 1);
resizeLine1.setAttribute('y2', resizeSize / 3);
var resizeLine2 = Blockly.svgDoc.createElementNS(SVG_NS, 'line');
resizeLine2.setAttribute('class', 'resizeLine');
resizeLine2.setAttribute('x1', resizeSize * 2 / 3);
resizeLine2.setAttribute('y1', resizeSize - 1);
resizeLine2.setAttribute('x2', resizeSize - 1);
resizeLine2.setAttribute('y2', resizeSize * 2 / 3);
this.foreignObject_ = Blockly.svgDoc.createElementNS(SVG_NS, 'foreignObject');
this.foreignObject_.setAttribute('x', Blockly.Comment.BORDER_WIDTH);
this.foreignObject_.setAttribute('y', Blockly.Comment.BORDER_WIDTH);
var body = Blockly.svgDoc.createElementNS(HTML_NS, 'body');
body.setAttribute('xmlns', HTML_NS);
body.setAttribute('class', 'minimalBody');
this.textarea_ = Blockly.svgDoc.createElementNS(HTML_NS, 'textarea');
this.textarea_.setAttribute('class', 'commentTextarea');
body.appendChild(this.textarea_);
this.foreignObject_.appendChild(body);
this.bubbleGroup_.appendChild(bubbleEmboss);
bubbleEmboss.appendChild(this.bubbleArrow_);
bubbleEmboss.appendChild(this.bubbleBack_);
this.bubbleGroup_.appendChild(this.resizeGroup_);
this.resizeGroup_.appendChild(resizeBack);
this.resizeGroup_.appendChild(resizeLine1);
this.resizeGroup_.appendChild(resizeLine2);
this.bubbleGroup_.appendChild(this.foreignObject_);
// Fetch the absolute coordinates of the block.
var absoluteXY = this.block_.getAbsoluteXY();
this.setBubbleLocation(absoluteXY.x + this.offsetLeft_,
absoluteXY.y + this.offsetTop_, true);
this.setBubbleSize(this.width_, this.height_);
commentGroup.appendChild(this.bubbleGroup_);
Blockly.bindEvent_(this.bubbleBack_, 'mousedown', this,
this.bubbleMouseDown_);
Blockly.bindEvent_(this.resizeGroup_, 'mousedown', this,
this.resizeMouseDown_);
Blockly.bindEvent_(this.textarea_, 'mouseup', this,
this.textareaFocus_);
};
/**
* Set whether the comment bubble is always visible or not.
* @param {boolean} pinned True if the bubble should be always visible.
*/
Blockly.Comment.prototype.setPinned = function(pinned) {
this.isPinned_ = pinned;
this.iconMark_.style.fill = pinned ? '#fff' : '';
this.setVisible_(pinned);
};
/**
* Show or hide the comment bubble.
* @param {boolean} visible True if the bubble should be visible.
* @private
*/
Blockly.Comment.prototype.setVisible_ = function(visible) {
this.bubbleGroup_.style.display = visible ? '' : 'none';
};
/**
* Clicking on the icon toggles if the bubble is pinned.
* @param {!Event} e Mouse click event.
* @private
*/
Blockly.Comment.prototype.iconClick_ = function(e) {
this.setPinned(!this.isPinned_);
};
/**
* Mousing over the icon makes the bubble visible.
* @param {!Event} e Mouse over event.
* @private
*/
Blockly.Comment.prototype.iconMouseOver_ = function(e) {
if (!this.isPinned_ && Blockly.Block.dragMode_ == 0) {
this.setVisible_(true);
}
};
/**
* Mousing off of the icon hides the bubble (unless it is pinned).
* @param {!Event} e Mouse out event.
* @private
*/
Blockly.Comment.prototype.iconMouseOut_ = function(e) {
if (!this.isPinned_ && Blockly.Block.dragMode_ == 0) {
this.setVisible_(false);
}
};
/**
* Handle a mouse-down on comment bubble.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Comment.prototype.bubbleMouseDown_ = function(e) {
this.promote_();
Blockly.Comment.unbindDragEvents_();
if (e.button == 2) {
// Right-click.
return;
} else if (Blockly.isTargetInput_(e)) {
// When focused on an HTML text input widget, don't trap any events.
return;
}
// Left-click (or middle click)
Blockly.setCursorHand_(true);
// Record the starting offset between the current location and the mouse.
this.dragDeltaX = this.offsetLeft_ - e.clientX;
this.dragDeltaY = this.offsetTop_ - e.clientY;
Blockly.Comment.onMouseUpWrapper_ = Blockly.bindEvent_(Blockly.svgDoc,
'mouseup', this, Blockly.Comment.unbindDragEvents_);
Blockly.Comment.onMouseMoveWrapper_ = Blockly.bindEvent_(Blockly.svgDoc,
'mousemove', this, this.bubbleMouseMove_);
// If a tooltip is visible, hide it.
Blockly.Tooltip.hide();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Drag this comment to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.Comment.prototype.bubbleMouseMove_ = function(e) {
var x = this.dragDeltaX + e.clientX;
var y = this.dragDeltaY + e.clientY;
this.setBubbleLocation(x, y, true);
};
/**
* Handle a mouse-down on comment bubble's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Comment.prototype.resizeMouseDown_ = function(e) {
this.promote_();
Blockly.Comment.unbindDragEvents_();
if (e.button == 2) {
// Right-click.
return;
}
// Left-click (or middle click)
Blockly.setCursorHand_(true);
// Record the starting offset between the current location and the mouse.
this.resizeDeltaX = this.width_ - e.clientX;
this.resizeDeltaY = this.height_ - e.clientY;
Blockly.Comment.onMouseUpWrapper_ = Blockly.bindEvent_(Blockly.svgDoc,
'mouseup', this, Blockly.Comment.unbindDragEvents_);
Blockly.Comment.onMouseMoveWrapper_ = Blockly.bindEvent_(Blockly.svgDoc,
'mousemove', this, this.resizeMouseMove_);
// If a tooltip is visible, hide it.
Blockly.Tooltip.hide();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Resize this comment to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.Comment.prototype.resizeMouseMove_ = function(e) {
var x = this.resizeDeltaX + e.clientX;
var y = this.resizeDeltaY + e.clientY;
this.setBubbleSize(x, y);
};
/**
* Bring the comment to the top of the stack when clicked on.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.Comment.prototype.textareaFocus_ = function(e) {
// Ideally this would be hooked to the focus event for the comment.
// However doing so in Firefox swallows the cursor for unkown reasons.
// So this is hooked to mouseup instead. No big deal.
this.promote_();
// Since the act of moving this node within the DOM causes a loss of focus,
// we need to reapply the focus.
console.log('focus');
this.textarea_.focus();
};
/**
* Move this comment to the top of the stack.
* @private
*/
Blockly.Comment.prototype.promote_ = function() {
var commentGroup = this.bubbleGroup_.parentNode;
commentGroup.appendChild(this.bubbleGroup_);
};
/**
* Get the location of this comment's bubble.
* @return {!Object} Object with x and y properties.
*/
Blockly.Comment.prototype.getBubbleLocation = function() {
return {x: this.offsetLeft_, y: this.offsetTop_};
};
/**
* Move the comment bubble to an absolute location.
* @param {number} x X coordinate of the bubble.
* @param {number} y Y coordinate of the bubble.
* @param {boolean} redrawArrow Redraw the arrow to the code block.
*/
Blockly.Comment.prototype.setBubbleLocation = function(x, y, redrawArrow) {
this.offsetLeft_ = x;
this.offsetTop_ = y;
this.bubbleGroup_.setAttribute('transform',
'translate(' + x + ', ' + y + ')');
if (redrawArrow) {
this.renderArrow_();
}
};
/**
* Get the dimensions of this comment's bubble.
* @return {!Object} Object with width and height properties.
*/
Blockly.Comment.prototype.getBubbleSize = function() {
return {width: this.width_, height: this.height_};
};
/**
* Size this comment's bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
*/
Blockly.Comment.prototype.setBubbleSize = function(width, height) {
var doubleBorderWidth = 2 * Blockly.Comment.BORDER_WIDTH;
width = Math.max(width, doubleBorderWidth + 45);
height = Math.max(height, doubleBorderWidth + 18);
this.width_ = width;
this.height_ = height;
this.bubbleBack_.setAttribute('width', width);
this.bubbleBack_.setAttribute('height', height);
this.resizeGroup_.setAttribute('transform', 'translate(' +
(width - doubleBorderWidth) + ', ' +
(height - doubleBorderWidth) + ')');
this.foreignObject_.setAttribute('width', width - doubleBorderWidth);
this.foreignObject_.setAttribute('height', height - doubleBorderWidth);
this.textarea_.style.width = (width - doubleBorderWidth - 4) + 'px';
this.textarea_.style.height = (height - doubleBorderWidth - 4) + 'px';
this.renderArrow_();
};
/**
* Draw the arrow between the comment bubble and the block.
* @private
*/
Blockly.Comment.prototype.renderArrow_ = function() {
steps = [];
// Find the relative coordinates of the center of the bubble.
var relBubbleX = this.width_ / 2;
var relBubbleY = this.height_ / 2;
// Find the absolute coordinates of the center of the bubble.
var absBubbleX = this.offsetLeft_ + relBubbleX;
var absBubbleY = this.offsetTop_ + relBubbleY;
// Find the relative coordinates of the center of the icon.
var relIconX = relBubbleX + this.iconX_ - absBubbleX;
var relIconY = relBubbleY + this.iconY_ - absBubbleY;
if (relBubbleX == relIconX && relBubbleY == relIconY) {
// Null case. Bubble is directly on top of the icon.
// Short circuit this rather than wade through divide by zeros.
steps.push('M ' + relBubbleX + ',' + relBubbleY);
} else {
// Compute the angle of the arrow's line.
var run = relIconX - relBubbleX;
var rise = relIconY - relBubbleY;
var hypotenuse = Math.sqrt(rise * rise + run * run);
var angle = Math.acos(run / hypotenuse);
if (rise < 0) {
angle = 2 * Math.PI - angle;
}
// Compute a line perpendicular to the arrow.
var rightAngle = angle + Math.PI / 2;
if (rightAngle > Math.PI * 2) {
rightAngle -= Math.PI * 2;
}
var rightRise = Math.sin(rightAngle);
var rightRun = Math.cos(rightAngle);
// Calculate the thickness of the base of the arrow.
var bubbleSize = this.getBubbleSize();
var thickness = (bubbleSize.width + bubbleSize.height) /
Blockly.Comment.ARROW_THICKNESS;
thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 2;
// Back the tip of the arrow off of the icon.
var backoffRatio = 1 - Blockly.Comment.ICON_RADIUS / hypotenuse;
relIconX = relBubbleX + backoffRatio * run;
relIconY = relBubbleY + backoffRatio * rise;
// Coordinates for the base of the arrow.
var baseX1 = relBubbleX + thickness * rightRun;
var baseY1 = relBubbleY + thickness * rightRise;
var baseX2 = relBubbleX - thickness * rightRun;
var baseY2 = relBubbleY - thickness * rightRise;
// Distortion to curve the arrow.
var swirlAngle = angle + (Blockly.Comment.ARROW_ANGLE / 360 * Math.PI * 2);
if (swirlAngle > Math.PI * 2) {
swirlAngle -= Math.PI * 2;
}
var swirlRise = Math.sin(swirlAngle) *
hypotenuse / Blockly.Comment.ARROW_BEND;
var swirlRun = Math.cos(swirlAngle) *
hypotenuse / Blockly.Comment.ARROW_BEND;
steps.push('M' + baseX1 + ',' + baseY1);
steps.push('C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) +
' ' + relIconX + ',' + relIconY +
' ' + relIconX + ',' + relIconY);
steps.push('C' + relIconX + ',' + relIconY +
' ' + (baseX2 + swirlRun) + ',' + (baseY2 + swirlRise) +
' ' + baseX2 + ',' + baseY2);
}
steps.push('z');
this.bubbleArrow_.setAttribute('d', steps.join(' '));
};
/**
* Returns this comment's text.
* @return {string} Comment text.
*/
Blockly.Comment.prototype.getText = function() {
return this.textarea_.value;
};
/**
* Set this comment's text.
* @param {string} text Comment text.
*/
Blockly.Comment.prototype.setText = function(text) {
this.textarea_.value = text;
};
/**
* Change the colour of a comment to match its block.
*/
Blockly.Comment.prototype.updateColour = function() {
this.bubbleBack_.setAttribute('fill', this.block_.getColour());
this.bubbleArrow_.setAttribute('fill', this.block_.getColour());
};
/**
* Destroy this comment.
*/
Blockly.Comment.prototype.destroy = function() {
Blockly.Comment.unbindDragEvents_();
// Destroy and unlink the icon.
this.iconGroup_.parentNode.removeChild(this.iconGroup_);
this.iconGroup_ = null;
// Destroy and unlink the bubble.
this.bubbleGroup_.parentNode.removeChild(this.bubbleGroup_);
this.textarea_ = null;
this.bubbleGroup_ = null;
// Disconnect links between the block and the comment.
this.block_.comment = null;
this.block_ = null;
};
/**
* Render the icon for this comment.
* @param {number} titleX Horizontal offset at which to position the icon.
* @return {number} Width of icon.
*/
Blockly.Comment.prototype.renderIcon = function(titleX) {
var topMargin = 4;
this.iconGroup_.setAttribute('transform',
'translate(' + titleX + ', ' + topMargin + ')');
// Find absolute coordinates for the centre of the icon and update the arrow.
var blockXY = this.block_.getAbsoluteXY();
this.iconX_ = blockXY.x + Blockly.Comment.ICON_RADIUS + titleX;
this.iconY_ = blockXY.y + Blockly.Comment.ICON_RADIUS + topMargin;
this.renderArrow_();
return 2 * Blockly.Comment.ICON_RADIUS;
};
/**
* Notification that the icon has moved. Update the arrow accordingly.
* @param {number} dx Horizontal offset from current location.
* @param {number} dy Vertical offset from current location.
*/
Blockly.Comment.prototype.moveIcon = function(dx, dy) {
this.iconX_ += dx;
this.iconY_ += dy;
// No need to rerender the arrow. It was translated during the move.
};