From 0d532ed64208134096a18e94acbd532b2e178d15 Mon Sep 17 00:00:00 2001 From: Cory Diers Date: Wed, 17 Aug 2016 10:22:28 -0700 Subject: [PATCH] Adding a custom gesture recognizer for workspace-level interaction. This should reduce overhead for gesture recognition and allow for better multi-touch interactions (like dragging children of currently-dragged blocks.) --- Blockly/Blockly.xcodeproj/project.pbxproj | 4 + .../BlocklyPanGestureRecognizer.swift | 235 +++++++++++++++++ .../WorkbenchViewController.swift | 242 ++++++++++-------- Blockly/Code/UI/Views/WorkspaceView.swift | 21 +- 4 files changed, 373 insertions(+), 129 deletions(-) create mode 100644 Blockly/Code/UI/View Controllers/BlocklyPanGestureRecognizer.swift diff --git a/Blockly/Blockly.xcodeproj/project.pbxproj b/Blockly/Blockly.xcodeproj/project.pbxproj index c03a65c3..4542a601 100644 --- a/Blockly/Blockly.xcodeproj/project.pbxproj +++ b/Blockly/Blockly.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 300CABD11D5CF816000E43B2 /* ConnectionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300CABD01D5CF816000E43B2 /* ConnectionValidator.swift */; }; 300CABE81D5E8606000E43B2 /* DefaultConnectionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300CABE71D5E8606000E43B2 /* DefaultConnectionValidator.swift */; }; + 30707E271D667B770000E418 /* BlocklyPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30707E261D667B770000E418 /* BlocklyPanGestureRecognizer.swift */; }; F98FF7E11BB2036A00A4F8E5 /* BlockFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98FF7E01BB2036A00A4F8E5 /* BlockFactory.swift */; }; F98FF7E51BB208EB00A4F8E5 /* BlockFactoryJSONTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98FF7E41BB208EB00A4F8E5 /* BlockFactoryJSONTest.swift */; }; F98FF7E71BB4911500A4F8E5 /* BlockBuilderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98FF7E61BB4911500A4F8E5 /* BlockBuilderTest.swift */; }; @@ -243,6 +244,7 @@ /* Begin PBXFileReference section */ 300CABD01D5CF816000E43B2 /* ConnectionValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionValidator.swift; sourceTree = ""; }; 300CABE71D5E8606000E43B2 /* DefaultConnectionValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultConnectionValidator.swift; sourceTree = ""; }; + 30707E261D667B770000E418 /* BlocklyPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlocklyPanGestureRecognizer.swift; sourceTree = ""; }; F98FF7E01BB2036A00A4F8E5 /* BlockFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockFactory.swift; sourceTree = ""; }; F98FF7E21BB2087700A4F8E5 /* block_factory_json_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = block_factory_json_test.json; sourceTree = ""; }; F98FF7E41BB208EB00A4F8E5 /* BlockFactoryJSONTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BlockFactoryJSONTest.swift; path = JSON/BlockFactoryJSONTest.swift; sourceTree = ""; }; @@ -780,6 +782,7 @@ FAA870161C642805000C7C61 /* TrashCanViewController.swift */, FAA870171C642805000C7C61 /* WorkbenchViewController.swift */, FA8425211D35D4140092CDDC /* WorkspaceViewController.swift */, + 30707E261D667B770000E418 /* BlocklyPanGestureRecognizer.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -1065,6 +1068,7 @@ FAD652741CB87B2200F73F11 /* DefaultLayoutEngine.swift in Sources */, FA42D7EB1C605A5F000C8EB4 /* FieldDateView.swift in Sources */, FA27267A1B83C54900777B49 /* BlockLayout.swift in Sources */, + 30707E271D667B770000E418 /* BlocklyPanGestureRecognizer.swift in Sources */, FAFAEE531CDABED400698179 /* FieldNumberView.swift in Sources */, FAA870091C64272C000C7C61 /* Logging.swift in Sources */, FA3FD1551CF7C886005B6D0F /* XMLConstants.swift in Sources */, diff --git a/Blockly/Code/UI/View Controllers/BlocklyPanGestureRecognizer.swift b/Blockly/Code/UI/View Controllers/BlocklyPanGestureRecognizer.swift new file mode 100644 index 00000000..7faa94fd --- /dev/null +++ b/Blockly/Code/UI/View Controllers/BlocklyPanGestureRecognizer.swift @@ -0,0 +1,235 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * 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. + */ + +import Foundation +import UIKit +import UIKit.UIGestureRecognizerSubclass + +/** + The delegate protocol for BlocklyPanGestureRecognizer. + */ +public protocol BlocklyPanGestureDelegate { + /** + The callback that's called when the BlocklyPanGestureRecognizer detects a valid block pan. Note: + This function returns a `BlockView`, in case this function changes the view that's passed in, + typically copying the view onto a new workspace. + + Parameter gesture: The gesture calling this function. + Parameter touchPosition: The UI space position of the touch. + Parameter block: The `BlockView` being touched. + Parameter state: The `UIGestureRecognizerState` for this individual touch. + Return: The `BlockView` currently being touched. Typically, this will be block, but blockTouched + might copy it into a new view. + */ + func blockTouched(gesture: BlocklyPanGestureRecognizer, touchPosition: CGPoint, block:BlockView, + state: UIGestureRecognizerState) -> BlockView; +} + +/** + The blockly gesture recognizer, which detects pan gestures on blocks in the workspace. + */ +public class BlocklyPanGestureRecognizer: UIGestureRecognizer { + // MARK: - Properties + + /// The delegate this gestureRecognizer operates on (`WorkbenchViewController` by default). + private var _target: BlocklyPanGestureDelegate + + /// The minimum distance for the gesture recognizer to count as a pan. + private let _minPanDistance: Float = 2.0 + + /// An ordered list of touches being handled by the recognizer. + private var _touches: [UITouch] + + /// An ordered list of blocks being dragged by the recognizer. + private var _blocks: [BlockView] + + /** + The container view blocks to be dragged are currently - either the "main" workspace, or a + toolbox/trash container. + */ + private let _originView: UIView + + /** + The container view blocks will end up in - either the same as the origin view, or the + workspace blocks are being copied into. + */ + private let _destinationView: UIView + + // MARK: - Initializer + + /** + Initializer for the BlocklyPanGestureRecognizer + + - Parameter target: The object that listens to the gesture recognizer callbacks + - Parameter action: The action to be performed on recognizer callbacks + - Parameter workbench: The workbench being operated on + */ + public init(target: BlocklyPanGestureDelegate, action: Selector, originView: UIView, + destView: UIView, workbench: WorkbenchViewController) + { + _target = workbench + _blocks = [BlockView]() + _touches = [UITouch]() + _originView = originView + _destinationView = destView + super.init(target: target as? AnyObject, action: action) + delaysTouchesBegan = false + } + + // MARK: - Super + + /** + Called when touches begin on the workspace. + */ + public override func touchesBegan(touches: Set, withEvent event: UIEvent) { + for touch in touches { + let location = touch.locationInView(_originView) + + // If the hit tested view is not an ancestor of a block, cancel the touch(es). + if let hitView = _originView.hitTest(location, withEvent: event), + var block = owningBlockView(hitView) + { + super.touchesBegan(touches, withEvent:event) + // Begin a new touch immediately if there is another touch being handled. Otherwise, the + // touch will begin once a touch has been moved enough to trigger a pan. + if state == .Began || state == .Changed { + // Check that we're not already dragging the block with a different touch + if !_blocks.contains(block) { + // Start the drag. + let touchPosition = touch.locationInView(_destinationView) + block = _target.blockTouched(self, + touchPosition: touchPosition, + block: block, + state: .Began) + } + } + + _touches.append(touch) + _blocks.append(block) + } + } + + // If none of the touches have hit a block, cancel the gesture. + if _touches.count == 0 { + state = .Cancelled + } + } + + /** + Called when touches are moved on the workspace. + */ + public override func touchesMoved(touches: Set, withEvent event: UIEvent) { + super.touchesMoved(touches, withEvent:event) + for touch in touches { + let touchPosition = touch.locationInView(_destinationView) + + if state == .Possible { + // Check the distance between the original touch and this one. + let previousPosition = touch.previousLocationInView(_destinationView) + let distance = hypotf(Float(previousPosition.x - touchPosition.x), + Float(previousPosition.y - touchPosition.y)) + // If the distance is sufficient, begin the touch. + if distance > _minPanDistance { + state = .Began + } else { + continue + } + } else if state == .Began || state == .Changed { + // Set the state to changed, so anything listening to the standard gesture recognizer can + // listen to standard gesture events. + state = .Changed + } + + if let index = _touches.indexOf(touch) { + let block = _blocks[index] + // Ignore any touch beyond the first, if multiple are touching the same block. + if _blocks.indexOf(block) < index { + continue + } + + if state == .Began { + // If the gesture just began, begin a touch on the delegate. + _blocks[index] = _target.blockTouched(self, + touchPosition: touchPosition, + block: block, + state: .Began) + continue + } + + _blocks[index] = _target.blockTouched(self, + touchPosition: touchPosition, + block: block, + state: .Changed) + } + } + } + + /** + Called when touches end on a workspace. + */ + public override func touchesEnded(touches: Set, withEvent event: UIEvent) { + super.touchesEnded(touches, withEvent:event) + for touch in touches { + if let index = _touches.indexOf(touch) { + let block = _blocks[index] + + _touches.removeAtIndex(index) + _blocks.removeAtIndex(index) + + // Only end the drag if no other touches are dragging the block. + if _blocks.contains(block) { + continue + } + + let touchPosition = touch.locationInView(_destinationView) + _target.blockTouched(self, touchPosition: touchPosition, block: block, state: .Ended) + } + } + + if _touches.count == 0 { + if state == .Changed { + // If the gesture succeeded, end the gesture. + state = .Ended + } else { + // If the gesture never began, cancel the gesture. + state = .Cancelled + } + } + } + + // MARK: - Private + + /** + Utility function for finding the first ancestor that is a `BlockView`. + + - Parameter view: The view to find an ancestor of + - Return: The first ancestor of the `UIView` that is a `BlockView` + */ + private func owningBlockView(view: UIView?) -> BlockView? { + var currentView = view + while !(currentView is BlockView) { + currentView = currentView?.superview + if currentView == nil { + return nil + } + + if currentView == _originView { + return nil + } + } + + return currentView as? BlockView + } +} diff --git a/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift b/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift index 7362c11c..d7f64f6b 100644 --- a/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift +++ b/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift @@ -32,7 +32,7 @@ public protocol WorkbenchViewControllerDelegate: class { TODO:(#61) Refactor parts of this code into `WorkspaceViewController`. */ @objc(BKYWorkbenchViewController) -public class WorkbenchViewController: UIViewController { +public class WorkbenchViewController: UIViewController, BlocklyPanGestureDelegate { // MARK: - Style Enum @@ -137,6 +137,9 @@ public class WorkbenchViewController: UIViewController { } } + /// Controller for managing the trash can workspace + public private(set) var trashCanViewController: TrashCanViewController! + /// The layout engine to use for all views public final let engine: LayoutEngine /// The layout builder to create layout hierarchies @@ -192,8 +195,6 @@ public class WorkbenchViewController: UIViewController { private let _dragger = Dragger() /// Controller for listing the toolbox categories private var _toolboxCategoryListViewController: ToolboxCategoryListViewController! - /// Controller for managing the trash can workspace - private var _trashCanViewController: TrashCanViewController! /// Flag indicating if the `self._trashCanViewController` is being shown private var _trashCanVisible: Bool = false /// Flag indicating if block highlighting is allowed @@ -233,11 +234,11 @@ public class WorkbenchViewController: UIViewController { private func commonInit() { // Set up trash can folder view controller - _trashCanViewController = TrashCanViewController( + trashCanViewController = TrashCanViewController( engine: engine, layoutBuilder: layoutBuilder, layoutDirection: style.trashLayoutDirection, viewFactory: viewFactory) - _trashCanViewController.delegate = self - addChildViewController(_trashCanViewController) + trashCanViewController.delegate = self + addChildViewController(trashCanViewController) // Set up toolbox category list view controller _toolboxCategoryListViewController = ToolboxCategoryListViewController( @@ -294,7 +295,7 @@ public class WorkbenchViewController: UIViewController { "toolboxCategoryView": toolboxCategoryViewController.view, "workspaceView": workspaceViewController.view, "trashCanView": trashCanView, - "trashCanFolderView": _trashCanViewController.view, + "trashCanFolderView": trashCanViewController.view, ] let metrics = [ "trashCanPadding": trashCanPadding, @@ -351,6 +352,24 @@ public class WorkbenchViewController: UIViewController { self.view.bky_addVisualFormatConstraints(constraints, metrics: metrics, views: views) self.view.sendSubviewToBack(workspaceViewController.view) + + let panGesture = BlocklyPanGestureRecognizer(target: self, action: nil, + originView: workspaceView.scrollView.containerView, + destView: workspaceView.scrollView.containerView, workbench: self) + panGesture.delegate = self + workspaceViewController.view.addGestureRecognizer(panGesture) + + let toolboxGesture = BlocklyPanGestureRecognizer(target: self, action: nil, + originView: toolboxCategoryViewController.workspaceView.scrollView.containerView, + destView: workspaceView.scrollView.containerView, workbench: self) + toolboxGesture.delegate = self + toolboxCategoryViewController.view.addGestureRecognizer(toolboxGesture) + + let trashGesture = BlocklyPanGestureRecognizer(target: self, action: nil, + originView: trashCanViewController.workspaceView.scrollView.containerView, + destView: workspaceView.scrollView.containerView, workbench: self) + trashGesture.delegate = self + trashCanViewController.view.addGestureRecognizer(trashGesture) } public override func viewDidLoad() { @@ -437,7 +456,7 @@ public class WorkbenchViewController: UIViewController { */ private func updateWorkspaceCapacity() { if let capacity = _workspaceLayout?.workspace.remainingCapacity { - _trashCanViewController.workspace?.deactivateBlockTrees(forGroupsGreaterThan: capacity) + trashCanViewController.workspace?.deactivateBlockTrees(forGroupsGreaterThan: capacity) _toolboxLayout?.toolbox.categories.forEach { $0.deactivateBlockTrees(forGroupsGreaterThan: capacity) } @@ -638,16 +657,17 @@ extension WorkbenchViewController { let size: CGFloat = visible ? 300 : 0 if style == .Default { - _trashCanViewController.setWorkspaceViewHeight(size, animated: animated) + trashCanViewController.setWorkspaceViewHeight(size, animated: animated) } else { - _trashCanViewController.setWorkspaceViewWidth(size, animated: animated) + trashCanViewController.setWorkspaceViewWidth(size, animated: animated) } _trashCanVisible = visible } - private func isGestureTouchingTrashCan(gesture: UIGestureRecognizer) -> Bool { + private func isTouchTouchingTrashCan(touchPosition: CGPoint, fromView: UIView?) -> Bool { if let trashCanView = self.trashCanView where !trashCanView.hidden { - return CGRectContainsPoint(trashCanView.bounds, gesture.locationInView(trashCanView)) + let trashSpacePosition = trashCanView.convertPoint(touchPosition, fromView: fromView) + return CGRectContainsPoint(trashCanView.bounds, trashSpacePosition) } return false @@ -660,11 +680,7 @@ extension WorkbenchViewController: WorkspaceViewControllerDelegate { public func workspaceViewController( workspaceViewController: WorkspaceViewController, didAddBlockView blockView: BlockView) { - if workspaceViewController == toolboxCategoryViewController || - workspaceViewController == _trashCanViewController - { - addGestureTrackingForWorkspaceFolderBlockView(blockView) - } else if workspaceViewController == self.workspaceViewController { + if workspaceViewController == self.workspaceViewController { addGestureTrackingForBlockView(blockView) } } @@ -672,11 +688,7 @@ extension WorkbenchViewController: WorkspaceViewControllerDelegate { public func workspaceViewController( workspaceViewController: WorkspaceViewController, didRemoveBlockView blockView: BlockView) { - if workspaceViewController == toolboxCategoryViewController || - workspaceViewController == _trashCanViewController - { - removeGestureTrackingForWorkspaceFolderBlockView(blockView) - } else if workspaceViewController == self.workspaceViewController { + if workspaceViewController == self.workspaceViewController { removeGestureTrackingForBlockView(blockView) } } @@ -698,89 +710,70 @@ extension WorkbenchViewController: WorkspaceViewControllerDelegate { extension WorkbenchViewController { /** - Adds a pan gesture recognizer to a block view that is part of a workspace "folder" (ie. trash + Removes all gesture recognizers from a block view that is part of a workspace flyout (ie. trash can or toolbox). - Parameter blockView: A given block view. */ - private func addGestureTrackingForWorkspaceFolderBlockView(blockView: BlockView) { + private func removeGestureTrackingForWorkspaceFolderBlockView(blockView: BlockView) { blockView.bky_removeAllGestureRecognizers() - - let panGesture = UIPanGestureRecognizer( - target: self, action: #selector(didRecognizeWorkspaceFolderPanGesture(_:))) - panGesture.maximumNumberOfTouches = 1 - panGesture.delegate = self - blockView.addGestureRecognizer(panGesture) } /** - Removes all gesture recognizers from a block view that is part of a workspace "folder" (ie. trash - can or toolbox). + Copies the specified block from a flyout (trash/toolbox) to the workspace. - - Parameter blockView: A given block view. + - Parameter blockView: The `BlockView` to copy + - Return: The new `BlockView` */ - private func removeGestureTrackingForWorkspaceFolderBlockView(blockView: BlockView) { - blockView.bky_removeAllGestureRecognizers() - } + public func copyBlockToWorkspace(blockView: BlockView) -> BlockView? { + // The block the user is dragging out of the toolbox/trash may be a child of a large nested + // block. We want to do a deep copy on the root block (not just the current block). + guard let rootBlockLayout = blockView.blockLayout?.rootBlockGroupLayout?.blockLayouts[0] + else + { + return nil + } - /** - Pan gesture event handler for a block view inside `self.toolboxView`. - */ - private dynamic func didRecognizeWorkspaceFolderPanGesture(gesture: UIPanGestureRecognizer) { - guard let aBlockView = gesture.view as? BlockView else { - return + // TODO:(#45) This should be copying the root block layout, not the root block view. + let rootBlockView: BlockView! = + ViewManager.sharedInstance.findBlockViewForLayout(rootBlockLayout) + + + // Copy the block view into the workspace view + let newBlockView: BlockView + do { + newBlockView = try copyBlockView(rootBlockView) + updateWorkspaceCapacity() + } catch let error as NSError { + bky_assertionFailure("Could not copy toolbox block view into workspace view: \(error)") + return nil } - if gesture.state == UIGestureRecognizerState.Began { - // The block the user is dragging out of the toolbox/trash may be a child of a large nested - // block. We want to do a deep copy on the root block (not just the current block). - guard let rootBlockLayout = aBlockView.blockLayout?.rootBlockGroupLayout?.blockLayouts[0] - else - { - return - } + return newBlockView + } - // TODO:(#45) This should be copying the root block layout, not the root block view. - let rootBlockView: BlockView! = - ViewManager.sharedInstance.findBlockViewForLayout(rootBlockLayout) + /** + Removes a `BlockView` from the trash, when moving it back to the workspace. + - Parameter blockView: The `BlockView` to remove. + */ + public func removeBlockFromTrash(blockView: BlockView) { + guard let rootBlockLayout = blockView.blockLayout?.rootBlockGroupLayout?.blockLayouts[0] + else + { + return + } - // Copy the block view into the workspace view - let newBlockView: BlockView + if let trashWorkspace = trashCanViewController.workspaceView.workspaceLayout?.workspace + where trashWorkspace.containsBlock(rootBlockLayout.block) + { do { - newBlockView = try copyBlockView(rootBlockView) - updateWorkspaceCapacity() + // Remove this block view from the trash can + try trashCanViewController.workspace?.removeBlockTree(rootBlockLayout.block) } catch let error as NSError { - bky_assertionFailure("Could not copy toolbox block view into workspace view: \(error)") + bky_assertionFailure("Could not remove block from trash can: \(error)") return } - - // Transfer this gesture recognizer from the original block view to the new block view - gesture.removeTarget(self, action: #selector(didRecognizeWorkspaceFolderPanGesture(_:))) - aBlockView.removeGestureRecognizer(gesture) - gesture.addTarget(self, action: #selector(didRecognizeWorkspacePanGesture(_:))) - newBlockView.addGestureRecognizer(gesture) - - // Start the first step of dragging the block layout - let touchPosition = workspaceView.workspacePositionFromGestureTouchLocation(gesture) - _dragger.startDraggingBlockLayout(newBlockView.blockLayout!, touchPosition: touchPosition) - - if let trashWorkspace = _trashCanViewController.workspaceView.workspaceLayout?.workspace - where trashWorkspace.containsBlock(rootBlockLayout.block) - { - do { - // Remove this block view from the trash can - try _trashCanViewController.workspace?.removeBlockTree(rootBlockLayout.block) - } catch let error as NSError { - bky_assertionFailure("Could not remove block from trash can: \(error)") - return - } - } else { - // Re-add gesture tracking to the original block view for future drags - addGestureTrackingForWorkspaceFolderBlockView(aBlockView) - } - - addUIStateValue(.DraggingBlock) } } } @@ -794,16 +787,8 @@ extension WorkbenchViewController { - Parameter blockView: A given block view. */ private func addGestureTrackingForBlockView(blockView: BlockView) { - // TODO:(#122) Gesture recognizing doesn't work simultaneously on a subview when a superview is - // already being dragged. - blockView.bky_removeAllGestureRecognizers() - let panGesture = - UIPanGestureRecognizer(target: self, action: #selector(didRecognizeWorkspacePanGesture(_:))) - panGesture.maximumNumberOfTouches = 1 - blockView.addGestureRecognizer(panGesture) - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didRecognizeWorkspaceTapGesture(_:))) blockView.addGestureRecognizer(tapGesture) @@ -825,32 +810,49 @@ extension WorkbenchViewController { /** Pan gesture event handler for a block view inside `self.workspaceView`. */ - private dynamic func didRecognizeWorkspacePanGesture(gesture: UIPanGestureRecognizer) { - guard let blockView = gesture.view as? BlockView, - blockLayout = blockView.blockLayout?.draggableBlockLayout else { - return + public dynamic func blockTouched(gesture: BlocklyPanGestureRecognizer, touchPosition: CGPoint, + block: BlockView, state: UIGestureRecognizerState) -> BlockView + { + guard let blockLayout = block.blockLayout?.draggableBlockLayout else { + return block } - let touchPosition = workspaceView.workspacePositionFromGestureTouchLocation(gesture) - let touchingTrashCan = isGestureTouchingTrashCan(gesture) + var blockView = block + let touchingTrashCan = isTouchTouchingTrashCan(touchPosition, + fromView: workspaceView.scrollView.containerView) + let workspacePosition = workspaceView.workspacePositionFromViewPoint(touchPosition) // TODO:(#44) Handle screen rotations (either lock the screen during drags or stop any // on-going drags when the screen is rotated). - if gesture.state == .Began { - // Temporarily remove gesture recognizer from the blockView - blockView.removeGestureRecognizer(gesture) + if state == .Began { + let inToolbox = gesture.view == toolboxCategoryViewController.view + let inTrash = gesture.view == trashCanViewController.view + // If the touch is in the toolbox, copy the block over to the workspace first. + if inToolbox { + guard let newBlock = copyBlockToWorkspace(blockView) else { + return blockView + } + blockView = newBlock + } else if inTrash { + let oldBlock = blockView - addUIStateValue(.DraggingBlock) - _dragger.startDraggingBlockLayout(blockLayout, touchPosition: touchPosition) + guard let newBlock = copyBlockToWorkspace(blockView) else { + return oldBlock + } + blockView = newBlock + removeBlockFromTrash(oldBlock) + } + + guard let blockLayout = blockView.blockLayout?.draggableBlockLayout else { + return blockView + } - // When the block layout is being dragged around, the corresponding view hierarchy may have - // been re-created (for example, if the block was disconnected from its parent). So we must - // find the view for the block layout and re-attach the gesture recognizer to it. - ViewManager.sharedInstance.findBlockViewForLayout(blockLayout)?.addGestureRecognizer(gesture) - } else if gesture.state == .Changed || gesture.state == .Cancelled || gesture.state == .Ended { addUIStateValue(.DraggingBlock) - _dragger.continueDraggingBlockLayout(blockLayout, touchPosition: touchPosition) + _dragger.startDraggingBlockLayout(blockLayout, touchPosition: workspacePosition) + } else if state == .Changed || state == .Cancelled || state == .Ended { + addUIStateValue(.DraggingBlock) + _dragger.continueDraggingBlockLayout(blockLayout, touchPosition: workspacePosition) if touchingTrashCan && blockLayout.block.deletable { addUIStateValue(.TrashCanHighlighted) @@ -859,13 +861,13 @@ extension WorkbenchViewController { } } - if gesture.state == .Cancelled || gesture.state == .Ended || gesture.state == .Failed { + if state == .Cancelled || state == .Ended || state == .Failed { if touchingTrashCan && blockLayout.block.deletable { // This block is being "deleted" -- cancel the drag and copy the block into the trash can _dragger.clearGestureDataForBlockLayout(blockLayout) do { - try _trashCanViewController.workspace?.copyBlockTree(blockLayout.block, editable: true) + try trashCanViewController.workspace?.copyBlockTree(blockLayout.block, editable: true) try _workspaceLayout?.workspace.removeBlockTree(blockLayout.block) updateWorkspaceCapacity() } catch let error as NSError { @@ -883,6 +885,8 @@ extension WorkbenchViewController { removeUIStateValue(.DraggingBlock) removeUIStateValue(.TrashCanHighlighted) } + + return blockView } /** @@ -1046,4 +1050,18 @@ extension WorkbenchViewController: UIGestureRecognizerDelegate { return true } + + public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, + shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool + { + let scrollView = workspaceViewController.workspaceView.scrollView + + // Force the scrollView pan and zoom gestures to fail unless this one fails + if otherGestureRecognizer == scrollView.panGestureRecognizer || + otherGestureRecognizer == scrollView.pinchGestureRecognizer { + return true + } + + return false + } } diff --git a/Blockly/Code/UI/Views/WorkspaceView.swift b/Blockly/Code/UI/Views/WorkspaceView.swift index 5b651970..933d00b0 100644 --- a/Blockly/Code/UI/Views/WorkspaceView.swift +++ b/Blockly/Code/UI/Views/WorkspaceView.swift @@ -155,19 +155,6 @@ public class WorkspaceView: LayoutView { blockGroupView.removeFromSuperview() } - /** - Maps a gesture's touch location relative to this view to a logical Workspace position. - - - Parameter gesture: The gesture - - Returns: The corresponding `WorkspacePoint` for the gesture - */ - public final func workspacePositionFromGestureTouchLocation(gesture: UIGestureRecognizer) - -> WorkspacePoint - { - let touchPosition = gesture.locationInView(scrollView.containerView) - return workspacePositionFromViewPoint(touchPosition) - } - /** Returns the logical Workspace position of a given `BlockView` based on its position relative to this `WorkspaceView`. @@ -236,8 +223,6 @@ public class WorkspaceView: LayoutView { } } - // MARK: - Private - /** Maps a `UIView` point relative to `self.scrollView.containerView` to a logical Workspace position. @@ -245,7 +230,7 @@ public class WorkspaceView: LayoutView { - Parameter point: The `UIView` point - Returns: The corresponding `WorkspacePoint` */ - private func workspacePositionFromViewPoint(point: CGPoint) -> WorkspacePoint { + public func workspacePositionFromViewPoint(point: CGPoint) -> WorkspacePoint { guard let workspaceLayout = self.workspaceLayout else { return WorkspacePointZero } @@ -268,6 +253,8 @@ public class WorkspaceView: LayoutView { return workspaceLayout.engine.scaledWorkspaceVectorFromViewVector(viewPoint) } + // MARK: - Private + private func canvasPadding() -> EdgeInsets { var scaled = EdgeInsets(0, 0, 0, 0) @@ -606,7 +593,7 @@ extension WorkspaceView { */ public class ScrollView: UIScrollView, UIGestureRecognizerDelegate { /// View which holds all content in the Workspace - private var containerView: ZIndexedGroupView = { + public var containerView: ZIndexedGroupView = { let view = ZIndexedGroupView(frame: CGRectZero) view.autoresizesSubviews = false return view