diff --git a/Blockly/Blockly.xcodeproj/project.pbxproj b/Blockly/Blockly.xcodeproj/project.pbxproj index 60b6cf0f..0e07edd8 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 */; }; + 30DC64DB1D875C88002D2186 /* BlocklyPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC64DA1D875C88002D2186 /* 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 */; }; @@ -244,6 +245,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 = ""; }; + 30DC64DA1D875C88002D2186 /* 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 = ""; }; @@ -574,6 +576,7 @@ children = ( FAA870131C642805000C7C61 /* View Controllers */, FA4BB5681B7C03EA000980E9 /* Views */, + 30DC64DA1D875C88002D2186 /* BlocklyPanGestureRecognizer.swift */, FAA870111C64276A000C7C61 /* WorkspaceBezierPath.swift */, ); path = UI; @@ -1069,6 +1072,7 @@ FAD652741CB87B2200F73F11 /* DefaultLayoutEngine.swift in Sources */, FA42D7EB1C605A5F000C8EB4 /* FieldDateView.swift in Sources */, FA27267A1B83C54900777B49 /* BlockLayout.swift in Sources */, + 30DC64DB1D875C88002D2186 /* BlocklyPanGestureRecognizer.swift in Sources */, FAFAEE531CDABED400698179 /* FieldNumberView.swift in Sources */, FAA870091C64272C000C7C61 /* Logging.swift in Sources */, FA3FD1551CF7C886005B6D0F /* XMLConstants.swift in Sources */, diff --git a/Blockly/Code/Control/Dragger.swift b/Blockly/Code/Control/Dragger.swift index e21d9638..d1f1409f 100644 --- a/Blockly/Code/Control/Dragger.swift +++ b/Blockly/Code/Control/Dragger.swift @@ -105,6 +105,10 @@ public class Dragger: NSObject { return } + // Set dragging to true, so the block groups displays with correct alpha through changes to the + // group mid-drag + layout.rootBlockGroupLayout?.dragging = true + // Set the connection manager group to "drag mode" to avoid wasting compute cycles during the // drag gestureData.connectionGroup.dragMode = true diff --git a/Blockly/Code/UI/BlocklyPanGestureRecognizer.swift b/Blockly/Code/UI/BlocklyPanGestureRecognizer.swift new file mode 100644 index 00000000..303e73ef --- /dev/null +++ b/Blockly/Code/UI/BlocklyPanGestureRecognizer.swift @@ -0,0 +1,336 @@ +/* + * 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: class { + /** + 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 block: The `BlockView` being touched. + Parameter touch: The `UITouch` hitting the block. + Parameter touchState: The `TouchState` for this individual touch. + */ + func blocklyPanGestureRecognizer(gesture: BlocklyPanGestureRecognizer, + didTouchBlock block: BlockView, + touch: UITouch, + touchState: BlocklyPanGestureRecognizer.TouchState) +} + +/** + The blockly gesture recognizer, which detects pan gestures on blocks in the workspace. + */ +public class BlocklyPanGestureRecognizer: UIGestureRecognizer { + // MARK: - Properties + + /// 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 states of the individual touches in the `BlocklyPanGestureRecognizer` + */ + @objc + public enum BKYBlocklyPanGestureRecognizerTouchState: Int { + /// Specifies an individual touch has just begun on a `BlockView` + case Began = 0, + /// Specifies an individual touch has just changed on a `BlockView` + Changed, + /// Specifies an individual touch has just ended on a `BlockView` + Ended + } + public typealias TouchState = BKYBlocklyPanGestureRecognizerTouchState + + // TODO:(#176) - Replace maximumTouches + + /// Maximum number of touches handled by the recognizer + public var maximumTouches = Int.max + + /// The minimum distance for the gesture recognizer to count as a pan, in the UIView coordinate + /// system. + public var minimumPanDistance: Float = 2.0 + + /// The delegate this gestureRecognizer operates on (`WorkbenchViewController` by default). + public weak var targetDelegate: BlocklyPanGestureDelegate? + + // MARK: - Initializer + + /** + Initializer for the BlocklyPanGestureRecognizer + + - Parameter targetDelegate: The object that listens to the gesture recognizer callbacks + */ + public init(targetDelegate: BlocklyPanGestureDelegate) + { + self.targetDelegate = targetDelegate + super.init(target: nil, action: nil) + delaysTouchesBegan = false + } + + // MARK: - Super + + /** + Called when touches begin on the workspace. + */ + public override func touchesBegan(touches: Set, withEvent event: UIEvent) { + super.touchesBegan(touches, withEvent:event) + for touch in touches { + let location = touch.locationInView(view) + + // If the hit tested view is not an ancestor of a block, cancel the touch(es). + if let attachedView = view, + let hitView = attachedView.hitTest(location, withEvent: event), + let block = owningBlockView(hitView) + where _touches.count < maximumTouches + { + let blockAlreadyTouched = _blocks.contains(block) + _touches.append(touch) + _blocks.append(block) + + // 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) && !blockAlreadyTouched { + // Start the drag. + targetDelegate?.blocklyPanGestureRecognizer(self, + didTouchBlock: block, + touch: touch, + touchState: .Began) + } + } + } + + // 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) + // If the gesture has yet to start, check if it should start. + if state == .Possible { + for touch in touches { + let touchPosition = touch.locationInView(view) + + // Check the distance between the original touch and this one. + let previousPosition = touch.previousLocationInView(view) + let distance = hypotf(Float(previousPosition.x - touchPosition.x), + Float(previousPosition.y - touchPosition.y)) + // If the distance is sufficient, begin the gesture. + if distance > minimumPanDistance { + state = .Began + break + // If not, check the next touch to see if it should begin the gesture. + } else { + continue + } + } + + // If the gesture still hasn't started, end here. + if state == .Possible { + return + } + // Set the state to changed, so anything listening to the standard gesture recognizer can + // listen to standard gesture events. Note UIGestureRecognizer requires setting the state to + // changed even if it's already there, to fire the correct delegates. + } else if state == .Began || state == .Changed { + state = .Changed + } + + // When we begin the gesture, start a touch on every currently-touched block. + if state == .Began { + for touch in _touches { + 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 + } + + targetDelegate?.blocklyPanGestureRecognizer(self, + didTouchBlock: block, + touch: touch, + touchState: .Began) + } + } + } else { + for touch in touches { + 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 + } + + targetDelegate?.blocklyPanGestureRecognizer(self, + didTouchBlock: block, + touch: touch, + touchState: .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 + } + + // TODO:(#175) Fix blocks jumping from touch to touch when one block is hit by two touches. + + targetDelegate?.blocklyPanGestureRecognizer(self, + didTouchBlock: block, + touch: touch, + touchState: .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 + } + } + } + + /** + Called when touches are cancelled on a workspace. + */ + public override func touchesCancelled(touches: Set, withEvent event: UIEvent) { + super.touchesCancelled(touches, withEvent: event) + for touch in touches { + if let index = _touches.indexOf(touch) { + _touches.removeAtIndex(index) + _blocks.removeAtIndex(index) + } + } + + if _touches.count == 0 { + state = .Cancelled + } + } + + /** + Manually cancels the touches of the gesture recognizer. + */ + public func cancelAllTouches() { + _touches.removeAll() + _blocks.removeAll() + + state = .Cancelled + } + + /** + Calculates the delta of the first touch in a given view. + + - Parameter view: The view to calculate the location of the touch position. + - Return: The difference between the current position and the previous position. + */ + public func firstTouchDeltaInView(view: UIView?) -> CGPoint { + if _touches.count > 0 { + let currentPosition = _touches[0].locationInView(view) + let previousPosition = _touches[0].previousLocationInView(view) + + return currentPosition - previousPosition + } + + return CGPointZero + } + + /** + Updates the block at the given index, when the `BlockView` has changed (typically when it is + copied to a new workspace.) + + - Parameter block: The old `BlockView` to be tracked. + - Parameter newBlock: The new `BlockView` to be tracked. + */ + public func replaceBlock(block: BlockView, withNewBlock newBlock: BlockView) { + guard let touchIndex = _blocks.indexOf(block) else { + return + } + + _blocks[touchIndex] = newBlock + } + + /** + Checks if any touch handled by the gesture recognizer is inside a given view. + + - Parameter view: The `UIView` to be checked against. + */ + public func isTouchingView(otherView: UIView) -> Bool { + for touch in _touches { + let touchPosition = touch.locationInView(otherView) + if CGRectContainsPoint(otherView.bounds, touchPosition) { + return true + } + } + + return false + } + + // 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 == self.view { + 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 0c14c11d..0df3bbe8 100644 --- a/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift +++ b/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift @@ -278,6 +278,7 @@ public class WorkbenchViewController: UIViewController { // Create main workspace view workspaceViewController = WorkspaceViewController(viewFactory: viewFactory) + workspaceViewController.workspaceView.allowZoom = true workspaceViewController.workspaceView.scrollView.panGestureRecognizer .addTarget(self, action: #selector(didPanWorkspaceView(_:))) let tapGesture = @@ -355,6 +356,18 @@ public class WorkbenchViewController: UIViewController { self.view.bky_addVisualFormatConstraints(constraints, metrics: metrics, views: views) self.view.sendSubviewToBack(workspaceViewController.view) + + let panGesture = BlocklyPanGestureRecognizer(targetDelegate: self) + panGesture.delegate = self + workspaceViewController.view.addGestureRecognizer(panGesture) + + let toolboxGesture = BlocklyPanGestureRecognizer(targetDelegate: self) + toolboxGesture.delegate = self + toolboxCategoryViewController.view.addGestureRecognizer(toolboxGesture) + + let trashGesture = BlocklyPanGestureRecognizer(targetDelegate: self) + trashGesture.delegate = self + _trashCanViewController.view.addGestureRecognizer(trashGesture) } public override func viewDidLoad() { @@ -650,9 +663,18 @@ extension WorkbenchViewController { _trashCanVisible = visible } - private func isGestureTouchingTrashCan(gesture: UIGestureRecognizer) -> Bool { + private func isGestureTouchingTrashCan(gesture: BlocklyPanGestureRecognizer) -> Bool { + if let trashCanView = self.trashCanView where !trashCanView.hidden { + return gesture.isTouchingView(trashCanView) + } + + return false + } + + 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 @@ -665,11 +687,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) } } @@ -677,11 +695,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) } } @@ -703,90 +717,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.workspace - where trashWorkspace.containsBlock(rootBlockLayout.block) - { - do { - // Remove this block view from the trash can - try _trashCanViewController.workspaceLayoutCoordinator? - .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) } } } @@ -800,16 +794,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) @@ -828,70 +814,6 @@ 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 - } - - let touchPosition = workspaceView.workspacePositionFromGestureTouchLocation(gesture) - let touchingTrashCan = isGestureTouchingTrashCan(gesture) - - // 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) - - addUIStateValue(.DraggingBlock) - _dragger.startDraggingBlockLayout(blockLayout, touchPosition: touchPosition) - - // 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) - - if touchingTrashCan && blockLayout.block.deletable { - addUIStateValue(.TrashCanHighlighted) - } else { - removeUIStateValue(.TrashCanHighlighted) - } - } - - if gesture.state == .Cancelled || gesture.state == .Ended || gesture.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.workspaceLayoutCoordinator? - .copyBlockTree(blockLayout.block, editable: true) - try _workspaceLayoutCoordinator?.removeBlockTree(blockLayout.block) - updateWorkspaceCapacity() - } catch let error as NSError { - bky_assertionFailure("Could not copy block to trash can: \(error)") - } - } else { - _dragger.finishDraggingBlockLayout(blockLayout) - } - - // HACK: Re-add gesture tracking for the block view, as there is a problem re-recognizing - // them when dragging multiple blocks simultaneously - addGestureTrackingForBlockView(blockView) - - // Update the UI state - removeUIStateValue(.DraggingBlock) - removeUIStateValue(.TrashCanHighlighted) - } - } - /** Tap gesture event handler for a block view inside `self.workspaceView`. */ @@ -1023,34 +945,144 @@ extension WorkbenchViewController { } } +// MARK: - BlocklyPanGestureDelegate + +extension WorkbenchViewController: BlocklyPanGestureDelegate { + /** + Pan gesture event handler for a block view inside `self.workspaceView`. + */ + public func blocklyPanGestureRecognizer(gesture: BlocklyPanGestureRecognizer, + didTouchBlock block: BlockView, touch: UITouch, + touchState: BlocklyPanGestureRecognizer.TouchState) + { + guard let blockLayout = block.blockLayout?.draggableBlockLayout else { + return + } + + var blockView = block + let touchPosition = touch.locationInView(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 touchState == .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 + } + gesture.replaceBlock(block, withNewBlock: newBlock) + blockView = newBlock + } else if inTrash { + let oldBlock = blockView + + guard let newBlock = copyBlockToWorkspace(blockView) else { + return + } + gesture.replaceBlock(block, withNewBlock: newBlock) + blockView = newBlock + removeBlockFromTrash(oldBlock) + } + + guard let blockLayout = blockView.blockLayout?.draggableBlockLayout else { + return + } + + addUIStateValue(.DraggingBlock) + _dragger.startDraggingBlockLayout(blockLayout, touchPosition: workspacePosition) + } else if touchState == .Changed || touchState == .Ended { + addUIStateValue(.DraggingBlock) + _dragger.continueDraggingBlockLayout(blockLayout, touchPosition: workspacePosition) + + if isGestureTouchingTrashCan(gesture) && blockLayout.block.deletable { + addUIStateValue(.TrashCanHighlighted) + } else { + removeUIStateValue(.TrashCanHighlighted) + } + } + + if touchState == .Ended { + let touchTouchingTrashCan = isTouchTouchingTrashCan(touchPosition, + fromView: workspaceView.scrollView.containerView) + if touchTouchingTrashCan && 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 _workspaceLayout?.workspace.removeBlockTree(blockLayout.block) + updateWorkspaceCapacity() + } catch let error as NSError { + bky_assertionFailure("Could not copy block to trash can: \(error)") + } + } else { + _dragger.finishDraggingBlockLayout(blockLayout) + } + + // HACK: Re-add gesture tracking for the block view, as there is a problem re-recognizing + // them when dragging multiple blocks simultaneously + addGestureTrackingForBlockView(blockView) + + // Update the UI state + removeUIStateValue(.DraggingBlock) + if !isGestureTouchingTrashCan(gesture) { + removeUIStateValue(.TrashCanHighlighted) + } + } + + return + } +} + // MARK: - UIGestureRecognizerDelegate extension WorkbenchViewController: UIGestureRecognizerDelegate { public func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { - if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, - let blockView = gestureRecognizer.view as? BlockView, - let block = blockView.blockLayout?.block, - let toolboxCategory = toolboxCategoryViewController.category - where toolboxCategory.containsBlock(block) + if let panGestureRecognizer = gestureRecognizer as? BlocklyPanGestureRecognizer + where gestureRecognizer.view == toolboxCategoryViewController.view { // For toolbox blocks, only fire the pan gesture if the user is panning in the direction // perpendicular to the toolbox scrolling. Otherwise, don't let it fire, so the user can // simply continue scrolling the toolbox. - let velocity = panGestureRecognizer.velocityInView(panGestureRecognizer.view) + let delta = panGestureRecognizer.firstTouchDeltaInView(panGestureRecognizer.view) - // Figure out angle of velocity vector, relative to the scroll direction + // Figure out angle of delta vector, relative to the scroll direction let radians: CGFloat if style.toolboxOrientation == .Vertical { - radians = atan(abs(velocity.x) / abs(velocity.y)) + radians = atan(abs(delta.x) / abs(delta.y)) } else { - radians = atan(abs(velocity.y) / abs(velocity.x)) + radians = atan(abs(delta.y) / abs(delta.x)) } // Fire the gesture if it started more than 20 degrees in the perpendicular direction let angle = (radians / CGFloat(M_PI)) * 180 - return angle > 20 + if angle > 20 { + return true + } else { + panGestureRecognizer.cancelAllTouches() + return false + } } return true } + + public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, + shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool + { + let scrollView = workspaceViewController.workspaceView.scrollView + let toolboxScrollView = toolboxCategoryViewController.workspaceView.scrollView + + // Force the scrollView pan and zoom gestures to fail unless this one fails + if otherGestureRecognizer == scrollView.panGestureRecognizer || + otherGestureRecognizer == toolboxScrollView.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..267d03f8 100644 --- a/Blockly/Code/UI/Views/WorkspaceView.swift +++ b/Blockly/Code/UI/Views/WorkspaceView.swift @@ -61,6 +61,9 @@ public class WorkspaceView: LayoutView { */ public var scrollIntoViewEdgeInsets = EdgeInsets(20, 20, 100, 20) + /// Enables/disables the zooming of a workspace. Defaults to false. + public var allowZoom = false + /// The last known value for `workspaceLayout.contentOrigin` private var _lastKnownContentOrigin: CGPoint = CGPointZero @@ -103,8 +106,10 @@ public class WorkspaceView: LayoutView { } runAnimatableCode(animated) { - self.scrollView.minimumZoomScale = layout.engine.minimumScale / layout.engine.scale - self.scrollView.maximumZoomScale = layout.engine.maximumScale / layout.engine.scale + if self.allowZoom { + self.scrollView.minimumZoomScale = layout.engine.minimumScale / layout.engine.scale + self.scrollView.maximumZoomScale = layout.engine.maximumScale / layout.engine.scale + } if flags.intersectsWith([Layout.Flag_NeedsDisplay, WorkspaceLayout.Flag_UpdateCanvasSize]) { self.updateCanvasSizeFromLayout() @@ -155,19 +160,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 +228,6 @@ public class WorkspaceView: LayoutView { } } - // MARK: - Private - /** Maps a `UIView` point relative to `self.scrollView.containerView` to a logical Workspace position. @@ -245,7 +235,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 +258,8 @@ public class WorkspaceView: LayoutView { return workspaceLayout.engine.scaledWorkspaceVectorFromViewVector(viewPoint) } + // MARK: - Private + private func canvasPadding() -> EdgeInsets { var scaled = EdgeInsets(0, 0, 0, 0) @@ -338,7 +330,7 @@ public class WorkspaceView: LayoutView { // Position the contentView relative to the top-right corner let containerOrigin = CGPointMake( newContentSize.width - containerViewSize.width - - contentPadding.leading, contentPadding.top) + - contentPadding.leading, contentPadding.top) scrollView.containerView.frame = CGRectMake( containerOrigin.x, containerOrigin.y, containerViewSize.width, containerViewSize.height) @@ -392,7 +384,7 @@ public class WorkspaceView: LayoutView { return } if !ignoreRestrictions && - (scrollView.tracking || scrollView.dragging || scrollView.decelerating) + (scrollView.dragging || scrollView.decelerating) { return } @@ -606,7 +598,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 diff --git a/Blockly/Code/UI/Views/ZIndexedGroupView.swift b/Blockly/Code/UI/Views/ZIndexedGroupView.swift index 35ba9547..720f06db 100644 --- a/Blockly/Code/UI/Views/ZIndexedGroupView.swift +++ b/Blockly/Code/UI/Views/ZIndexedGroupView.swift @@ -34,9 +34,35 @@ public protocol ZIndexedView { `ZIndexedView` will result in an app crash. */ public final class ZIndexedGroupView: UIView { + // MARK: - Properties + /// The highest z-index `UIView` that has been added to this group private var highestInsertedZIndex: UInt = 0 + // MARK: - Super + + /** + Allows for hit testing while sub views are outside the bounds of a groupView. + + - Parameter point: The location to be tested, in local space. + - Parameter event: The event requesting the hit test. + */ + public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { + for target in subviews.lazy.reverse() { + let pointForTargetView = target.convertPoint(point, fromView: self) + + // If the touch is inside any child of this view, return the hit test for it. + if (CGRectContainsPoint(target.bounds, pointForTargetView)) { + return target.hitTest(pointForTargetView, withEvent: event) + } + } + + // If none of the children of this view have been hit, continue hit testing as usual. + return super.hitTest(point, withEvent: event) + } + + // MARK: - Public + /** Inserts or updates a `UIView` in this group, where it is sorted amongst other subviews based on its `zIndex`. @@ -94,6 +120,8 @@ public final class ZIndexedGroupView: UIView { upsertView(view, atIndex: min) } + // MARK: - Private + private func upsertViewAtEnd(view: T) { upsertView(view, atIndex: -1) }