diff --git a/core/bubble.js b/core/bubble.js index df3b69e3f..e6a8ada2a 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -279,28 +279,33 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { * @private */ Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) { - this.promote_(); - Blockly.Bubble.unbindDragEvents_(); - if (Blockly.utils.isRightButton(e)) { - // No right-click. - e.stopPropagation(); - return; - } else if (Blockly.utils.isTargetInput(e)) { - // When focused on an HTML text input widget, don't trap any events. - return; + var gesture = this.workspace_.getGesture(e); + if (gesture) { + gesture.handleBubbleStart(e, this); } - // Left-click (or middle click) - this.workspace_.startDrag(e, new goog.math.Coordinate( - this.workspace_.RTL ? -this.relativeLeft_ : this.relativeLeft_, - this.relativeTop_)); - Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document, - 'mouseup', this, Blockly.Bubble.bubbleMouseUp_); - Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(document, - 'mousemove', this, this.bubbleMouseMove_); - Blockly.hideChaff(); - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); + // this.promote_(); + // Blockly.Bubble.unbindDragEvents_(); + // if (Blockly.utils.isRightButton(e)) { + // // No right-click. + // e.stopPropagation(); + // return; + // } else if (Blockly.utils.isTargetInput(e)) { + // // When focused on an HTML text input widget, don't trap any events. + // return; + // } + // // Left-click (or middle click) + // this.workspace_.startDrag(e, new goog.math.Coordinate( + // this.workspace_.RTL ? -this.relativeLeft_ : this.relativeLeft_, + // this.relativeTop_)); + + // Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document, + // 'mouseup', this, Blockly.Bubble.bubbleMouseUp_); + // Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(document, + // 'mousemove', this, this.bubbleMouseMove_); + // Blockly.hideChaff(); + // // This event has been handled. No need to bubble up to the document. + // e.stopPropagation(); }; /** @@ -445,8 +450,11 @@ Blockly.Bubble.prototype.positionBubble_ = function() { left += this.relativeLeft_; } var top = this.relativeTop_ + this.anchorXY_.y; - this.bubbleGroup_.setAttribute('transform', - 'translate(' + left + ',' + top + ')'); + this.moveTo_(left, top); +}; + +Blockly.Bubble.prototype.moveTo_ = function(x, y) { + this.bubbleGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')'); }; /** @@ -595,3 +603,43 @@ Blockly.Bubble.prototype.dispose = function() { this.content_ = null; this.shape_ = null; }; + +Blockly.Bubble.prototype.moveToDragSurface = function(dragSurface) { + //var bubbleXY = this.getRelativeToSurfaceXY(); + //var anchorXY = this.getAnchorRelativeToSurfaceXY(); + + // TODO: check RTL. + var x = this.anchorXY_.x + this.relativeLeft_; + var y = this.anchorXY_.y + this.relativeTop_; + this.savedRelativeXY_ = + new goog.math.Coordinate(this.relativeLeft_, this.relativeTop_); + this.savedAnchorXY_ = this.anchorXY_; + this.moveTo_(0, 0); + dragSurface.translateSurface(x, y); + // Execute the move on the top-level SVG component. + dragSurface.setBlocksAndShow(this.bubbleGroup_); +}; + +Blockly.Bubble.prototype.moveDuringDrag = function(dragSurface, newLoc) { + console.log(newLoc); + dragSurface.translateSurface(newLoc.x, newLoc.y); + this.relativeLeft_ = this.savedRelativeXY_.x + newLoc.x; + this.relativeTop_ = this.savedRelativeXY_.y + newLoc.y; + this.renderArrow_(); +}; + +Blockly.Bubble.prototype.moveOffDragSurface = function(dragSurface, newXY) { + + //this.savedAnchorXY_ = this.anchorXY_; + this.anchorXY_ = this.savedAnchorXY_; + this.moveTo_(newXY.x, newXY.y); + dragSurface.clearAndHide(this.workspace_.getBubbleCanvas()); +}; + + +Blockly.Bubble.prototype.getRelativeToSurfaceXY = function() { + // This may not be quite right. It's probably the top-left of the bubble + // group. + return new goog.math.Coordinate(this.anchorXY_.x + this.relativeLeft_, + this.anchorXY_.y + this.relativeTop_); +}; diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js new file mode 100644 index 000000000..e66b500bf --- /dev/null +++ b/core/bubble_dragger.js @@ -0,0 +1,282 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2018 Google Inc. + * https://developers.google.com/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 Methods for dragging a bubble visually. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.BubbleDragger'); + +goog.require('Blockly.DraggedConnectionManager'); + +goog.require('goog.math.Coordinate'); +goog.require('goog.asserts'); + + +/** + * Class for a block dragger. It moves blocks around the workspace when they + * are being dragged by a mouse or touch. + * @param {!Blockly.Bubble} bubble The bubble to drag. + * @param {!Blockly.WorkspaceSvg} workspace The workspace to drag on. + * @constructor + */ +Blockly.BubbleDragger = function(bubble, workspace) { + /** + * The top block in the stack that is being dragged. + * @type {!Blockly.BlockSvg} + * @private + */ + this.draggingBubble_ = bubble; + + /** + * The workspace on which the bubble is being dragged. + * @type {!Blockly.WorkspaceSvg} + * @private + */ + this.workspace_ = workspace; + + /** + * Object that keeps track of connections on dragged blocks. + * @type {!Blockly.DraggedConnectionManager} + * @private + */ + this.draggedConnectionManager_ = null; + // new Blockly.DraggedConnectionManager( + // this.draggingBubble_); + + /** + * Which delete area the mouse pointer is over, if any. + * One of {@link Blockly.DELETE_AREA_TRASH}, + * {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}. + * @type {?number} + * @private + */ + this.deleteArea_ = null; + + /** + * Whether the block would be deleted if dropped immediately. + * @type {boolean} + * @private + */ + this.wouldDeleteBlock_ = false; + + /** + * The location of the top left corner of the dragging block at the beginning + * of the drag in workspace coordinates. + * @type {!goog.math.Coordinate} + * @private + */ + this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY(); + + // TODO: validate, getters, etc. + this.dragSurface_ = workspace.blockDragSurface_; +}; + +/** + * Sever all links from this object. + * @package + */ +Blockly.BubbleDragger.prototype.dispose = function() { + this.draggingBubble_ = null; + this.workspace_ = null; + this.startWorkspace_ = null; + this.dragIconData_.length = 0; + + if (this.draggedConnectionManager_) { + this.draggedConnectionManager_.dispose(); + this.draggedConnectionManager_ = null; + } +}; + +/** + * Start dragging a block. This includes moving it to the drag surface. + * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has + * moved from the position at mouse down, in pixel units. + * @package + */ +Blockly.BubbleDragger.prototype.startBubbleDrag = function(currentDragDeltaXY) { + console.log('starting bubble drag'); + if (!Blockly.Events.getGroup()) { + Blockly.Events.setGroup(true); + } + + this.workspace_.setResizesEnabled(false); + + // if (this.draggingBubble_.getParent()) { + // this.draggingBubble_.unplug(); + // var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); + // var newLoc = goog.math.Coordinate.sum(this.startXY_, delta); + + // this.draggingBubble_.translate(newLoc.x, newLoc.y); + // this.draggingBubble_.disconnectUiEffect(); + // } + //this.draggingBubble_.setDragging(true); + // For future consideration: we may be able to put moveToDragSurface inside + // the block dragger, which would also let the block not track the block drag + // surface. + this.draggingBubble_.moveToDragSurface(this.dragSurface_); + + if (this.workspace_.toolbox_) { + var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' : + 'blocklyToolboxGrab'; + this.workspace_.toolbox_.addStyle(style); + } +}; + +/** + * Execute a step of block dragging, based on the given event. Update the + * display accordingly. + * @param {!Event} e The most recent move event. + * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has + * moved from the position at the start of the drag, in pixel units. + * @package + */ +Blockly.BubbleDragger.prototype.dragBubble = function(e, currentDragDeltaXY) { + var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); + var newLoc = goog.math.Coordinate.sum(this.startXY_, delta); + + this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc); + //this.dragIcons_(delta); + + this.deleteArea_ = this.workspace_.isDeleteArea(e); + //this.draggedConnectionManager_.update(delta, this.deleteArea_); + + //this.updateCursorDuringBubbleDrag_(); +}; + +/** + * Finish a block drag and put the block back on the workspace. + * @param {!Event} e The mouseup/touchend event. + * @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has + * moved from the position at the start of the drag, in pixel units. + * @package + */ +Blockly.BubbleDragger.prototype.endBubbleDrag = function(e, currentDragDeltaXY) { + // Make sure internal state is fresh. + this.dragBubble(e, currentDragDeltaXY); + this.dragIconData_ = []; + + Blockly.BlockSvg.disconnectUiStop_(); + + var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); + var newLoc = goog.math.Coordinate.sum(this.startXY_, delta); + this.draggingBubble_.moveOffDragSurface(this.dragSurface_, newLoc); + + var deleted = true;//this.maybeDeleteBlock_(); + if (!deleted) { + // These are expensive and don't need to be done if we're deleting. + this.draggingBubble_.moveConnections_(delta.x, delta.y); + this.draggingBubble_.setDragging(false); + //this.draggedConnectionManager_.applyConnections(); + this.draggingBubble_.render(); + this.fireMoveEvent_(); + this.draggingBubble_.scheduleSnapAndBump(); + } + this.workspace_.setResizesEnabled(true); + + if (this.workspace_.toolbox_) { + var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' : + 'blocklyToolboxGrab'; + this.workspace_.toolbox_.removeStyle(style); + } + Blockly.Events.setGroup(false); +}; + +/** + * Fire a move event at the end of a block drag. + * @private + */ +Blockly.BubbleDragger.prototype.fireMoveEvent_ = function() { + var event = new Blockly.Events.BlockMove(this.draggingBubble_); + event.oldCoordinate = this.startXY_; + event.recordNew(); + Blockly.Events.fire(event); +}; + +/** + * Shut the trash can and, if necessary, delete the dragging block. + * Should be called at the end of a block drag. + * @return {boolean} whether the block was deleted. + * @private + */ +Blockly.BubbleDragger.prototype.maybeDeleteBubble_ = function() { + var trashcan = this.workspace_.trashcan; + + if (this.wouldDeleteBlock_) { + if (trashcan) { + goog.Timer.callOnce(trashcan.close, 100, trashcan); + } + // Fire a move event, so we know where to go back to for an undo. + this.fireMoveEvent_(); + this.draggingBubble_.dispose(false, true); + } else if (trashcan) { + // Make sure the trash can is closed. + trashcan.close(); + } + return this.wouldDeleteBlock_; +}; + +/** + * Update the cursor (and possibly the trash can lid) to reflect whether the + * dragging block would be deleted if released immediately. + * @private + */ +Blockly.BubbleDragger.prototype.updateCursorDuringBubbleDrag_ = function() { + this.wouldDeleteBlock_ = false; //this.draggedConnectionManager_.wouldDeleteBlock(); + var trashcan = this.workspace_.trashcan; + if (this.wouldDeleteBlock_) { + this.draggingBubble_.setDeleteStyle(true); + if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) { + trashcan.setOpen_(true); + } + } else { + this.draggingBubble_.setDeleteStyle(false); + if (trashcan) { + trashcan.setOpen_(false); + } + } +}; + +/** + * Convert a coordinate object from pixels to workspace units, including a + * correction for mutator workspaces. + * This function does not consider differing origins. It simply scales the + * input's x and y values. + * @param {!goog.math.Coordinate} pixelCoord A coordinate with x and y values + * in css pixel units. + * @return {!goog.math.Coordinate} The input coordinate divided by the workspace + * scale. + * @private + */ +Blockly.BubbleDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) { + var result = new goog.math.Coordinate(pixelCoord.x / this.workspace_.scale, + pixelCoord.y / this.workspace_.scale); + if (this.workspace_.isMutator) { + // If we're in a mutator, its scale is always 1, purely because of some + // oddities in our rendering optimizations. The actual scale is the same as + // the scale on the parent workspace. + // Fix that for dragging. + var mainScale = this.workspace_.options.parentWorkspace.scale; + result = result.scale(1 / mainScale); + } + return result; +}; diff --git a/core/gesture.js b/core/gesture.js index 8fb8938c0..f7b9262ce 100644 --- a/core/gesture.js +++ b/core/gesture.js @@ -28,6 +28,7 @@ goog.provide('Blockly.Gesture'); goog.require('Blockly.BlockDragger'); +goog.require('Blockly.BubbleDragger'); goog.require('Blockly.constants'); goog.require('Blockly.FlyoutDragger'); goog.require('Blockly.Tooltip'); @@ -68,6 +69,14 @@ Blockly.Gesture = function(e, creatorWorkspace) { */ this.currentDragDeltaXY_ = 0; + /** + * The bubble that the gesture started on, or null if it did not start on a + * bubble. + * @type {Blockly.Bubble} + * @private + */ + this.startBubble_ = null; + /** * The field that the gesture started on, or null if it did not start on a * field. @@ -136,6 +145,13 @@ Blockly.Gesture = function(e, creatorWorkspace) { */ this.isDraggingBlock_ = false; + /** + * Whether the bubble is currently being dragged. + * @type {boolean} + * @private + */ + this.isDraggingBubble_ = false; + /** * The event that most recently updated this gesture. * @type {!Event} @@ -159,6 +175,13 @@ Blockly.Gesture = function(e, creatorWorkspace) { */ this.onUpWrapper_ = null; + /** + * The object tracking a bubble drag, or null if none is in progress. + * @type {Blockly.BubbleDragger} + * @private + */ + this.bubbleDragger_ = null; + /** * The object tracking a block drag, or null if none is in progress. * @type {Blockly.BlockDragger} @@ -235,6 +258,10 @@ Blockly.Gesture.prototype.dispose = function() { this.workspaceDragger_.dispose(); this.workspaceDragger_ = null; } + if (this.bubbleDragger_) { + this.bubbleDragger_.dispose(); + this.bubbleDragger_ = null; + } }; /** @@ -312,6 +339,25 @@ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { return false; }; +/** + * Update this gesture to record whether a bubble is being dragged. + * This function should be called on a mouse/touch move event the first time the + * drag radius is exceeded. It should be called no more than once per gesture. + * If a bubble should be dragged this function creates the necessary + * BubbleDragger and starts the drag. + * @return {boolean} true if a bubble is being dragged. + * @private + */ +Blockly.Gesture.prototype.updateIsDraggingBubble_ = function() { + if (!this.startBubble_) { + return false; + } + + this.isDraggingBubble_ = true; + this.startDraggingBubble_(); + return true; +}; + /** * Update this gesture to record whether a block is being dragged. * This function should be called on a mouse/touch move event the first time the @@ -377,7 +423,11 @@ Blockly.Gesture.prototype.updateIsDragging_ = function() { 'updateIsDragging_ should only be called once per gesture.'); this.calledUpdateIsDragging_ = true; - // First check if it was a block drag. + // First check if it was a bubble drag. Bubbles always sit on top of blocks. + if (this.updateIsDraggingBubble_()) { + return; + } + // Then check if it was a block drag. if (this.updateIsDraggingBlock_()) { return; } @@ -397,6 +447,19 @@ Blockly.Gesture.prototype.startDraggingBlock_ = function() { this.currentDragDeltaXY_); }; +/** + * Create a bubble dragger and start dragging the selected bubble. + * TODO (fenichel): Possibly combine this and startDraggingBlock_. + * @private + */ +Blockly.Gesture.prototype.startDraggingBubble_ = function() { + this.bubbleDragger_ = new Blockly.BubbleDragger(this.startBubble_, + this.startWorkspace_); + this.bubbleDragger_.startBubbleDrag(this.currentDragDeltaXY_); + this.bubbleDragger_.dragBubble(this.mostRecentEvent_, + this.currentDragDeltaXY_); +}; + /** * Start a gesture: update the workspace to indicate that a gesture is in * progress and bind mousemove and mouseup handlers. @@ -470,6 +533,9 @@ Blockly.Gesture.prototype.handleMove = function(e) { } else if (this.isDraggingBlock_) { this.blockDragger_.dragBlock(this.mostRecentEvent_, this.currentDragDeltaXY_); + } else if (this.isDraggingBubble_) { + this.bubbleDragger_.dragBubble(this.mostRecentEvent_, + this.currentDragDeltaXY_); } e.preventDefault(); e.stopPropagation(); @@ -492,7 +558,11 @@ Blockly.Gesture.prototype.handleUp = function(e) { // The ordering of these checks is important: drags have higher priority than // clicks. Fields have higher priority than blocks; blocks have higher // priority than workspaces. - if (this.isDraggingBlock_) { + // The ordering within drags does not matter, because the three types of + // dragging are exclusive. + if (this.isDraggingBubble_) { + this.bubbleDragger_.endBubbleDrag(e, this.currentDragDeltaXY_); + } else if (this.isDraggingBlock_) { this.blockDragger_.endBlockDrag(e, this.currentDragDeltaXY_); } else if (this.isDraggingWorkspace_) { this.workspaceDragger_.endDrag(this.currentDragDeltaXY_); @@ -522,7 +592,10 @@ Blockly.Gesture.prototype.cancel = function() { return; } Blockly.longStop_(); - if (this.isDraggingBlock_) { + if (this.isDraggingBubble_) { + this.bubbleDragger_.endBubbleDrag(this.mostRecentEvent_, + this.currentDragDeltaXY_); + } else if (this.isDraggingBlock_) { this.blockDragger_.endBlockDrag(this.mostRecentEvent_, this.currentDragDeltaXY_); } else if (this.isDraggingWorkspace_) { @@ -546,6 +619,7 @@ Blockly.Gesture.prototype.handleRightClick = function(e) { this.startWorkspace_.showContextMenu_(e); } + // TODO: Handle right-click on a bubble. e.preventDefault(); e.stopPropagation(); @@ -595,6 +669,20 @@ Blockly.Gesture.prototype.handleBlockStart = function(e, block) { this.mostRecentEvent_ = e; }; +/** + * Handle a mousedown/touchstart event on a bubble. + * @param {!Event} e A mouse down or touch start event. + * @param {!Blockly.Bubble} bubble The bubble the event hit. + * @package + */ +Blockly.Gesture.prototype.handleBubbleStart = function(e, bubble) { + goog.asserts.assert(!this.hasStarted_, + 'Tried to call gesture.handleBubbleStart, but the gesture had already ' + + 'been started.'); + this.setStartBubble(bubble); + this.mostRecentEvent_ = e; +}; + /* Begin functions defining what actions to take to execute clicks on each type * of target. Any developer wanting to add behaviour on clicks should modify * only this code. */ @@ -644,6 +732,7 @@ Blockly.Gesture.prototype.doWorkspaceClick_ = function() { /* End functions defining what actions to take to execute clicks on each type * of target. */ +// TODO (fenichel): Move bubbles to the front. /** * Move the dragged/clicked block to the front of the workspace so that it is * not occluded by other blocks. @@ -672,6 +761,17 @@ Blockly.Gesture.prototype.setStartField = function(field) { } }; +/** + * Record the bubble that a gesture started on + * @param {Blockly.Bubble} bubble The bubble the gesture started on. + * @package + */ +Blockly.Gesture.prototype.setStartBubble = function(bubble) { + if (!this.startBubble_) { + this.startBubble_ = bubble; + } +}; + /** * Record the block that a gesture started on, and set the target block * appropriately. @@ -778,7 +878,8 @@ Blockly.Gesture.prototype.isWorkspaceClick_ = function() { * @package */ Blockly.Gesture.prototype.isDragging = function() { - return this.isDraggingWorkspace_ || this.isDraggingBlock_; + return this.isDraggingWorkspace_ || this.isDraggingBlock_ || + this.isDraggingBubble_; }; /**