diff --git a/Blockly/Blockly.xcodeproj/project.pbxproj b/Blockly/Blockly.xcodeproj/project.pbxproj index c03a65c3..60b6cf0f 100644 --- a/Blockly/Blockly.xcodeproj/project.pbxproj +++ b/Blockly/Blockly.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ FAC035071D516EE10017C1C8 /* FieldDropdownLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC035061D516EE10017C1C8 /* FieldDropdownLayout.swift */; }; FAC035111D5170B20017C1C8 /* FieldVariableLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC035101D5170B20017C1C8 /* FieldVariableLayout.swift */; }; FAC1D2971BFA7E2E0019CE45 /* Input+Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC1D2961BFA7E2E0019CE45 /* Input+Builder.swift */; }; + FACBAB591D8371220063A975 /* WorkspaceLayoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACBAB581D8371220063A975 /* WorkspaceLayoutCoordinator.swift */; }; FAD3ABF71BCDD7CD00C0B254 /* LayoutFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD3ABF61BCDD7CD00C0B254 /* LayoutFactory.swift */; }; FAD6522E1CB5DDFB00F73F11 /* ViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD6522D1CB5DDFB00F73F11 /* ViewFactory.swift */; }; FAD6523F1CB7210C00F73F11 /* DefaultBlockGroupLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD6523B1CB7210C00F73F11 /* DefaultBlockGroupLayout.swift */; }; @@ -375,6 +376,7 @@ FAC035061D516EE10017C1C8 /* FieldDropdownLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldDropdownLayout.swift; sourceTree = ""; }; FAC035101D5170B20017C1C8 /* FieldVariableLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldVariableLayout.swift; sourceTree = ""; }; FAC1D2961BFA7E2E0019CE45 /* Input+Builder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Input+Builder.swift"; sourceTree = ""; }; + FACBAB581D8371220063A975 /* WorkspaceLayoutCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkspaceLayoutCoordinator.swift; sourceTree = ""; }; FAD3ABF61BCDD7CD00C0B254 /* LayoutFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutFactory.swift; sourceTree = ""; }; FAD6522D1CB5DDFB00F73F11 /* ViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewFactory.swift; sourceTree = ""; }; FAD6523B1CB7210C00F73F11 /* DefaultBlockGroupLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultBlockGroupLayout.swift; sourceTree = ""; }; @@ -458,6 +460,7 @@ FAA8700F1C64273E000C7C61 /* LayoutHelper.swift */, FA4E840A1CAE4AAE009FB0CD /* ToolboxLayout.swift */, FA2726791B83C54900777B49 /* WorkspaceLayout.swift */, + FACBAB581D8371220063A975 /* WorkspaceLayoutCoordinator.swift */, FA9D2FBC1C111A1300D0E528 /* WorkspaceFlowLayout.swift */, ); path = Layout; @@ -820,10 +823,10 @@ children = ( FAABDD621CA20BA400F9E7D4 /* BlockBumper.swift */, FAE5576D1BE0219C0019D0D4 /* ConnectionManager.swift */, - FAE557781BE02B270019D0D4 /* Dragger.swift */, - FAFAEE701CDC0D2F00698179 /* NameManager.swift */, 300CABD01D5CF816000E43B2 /* ConnectionValidator.swift */, 300CABE71D5E8606000E43B2 /* DefaultConnectionValidator.swift */, + FAE557781BE02B270019D0D4 /* Dragger.swift */, + FAFAEE701CDC0D2F00698179 /* NameManager.swift */, ); path = Control; sourceTree = ""; @@ -1025,6 +1028,7 @@ FAC034F61D5159FE0017C1C8 /* FieldDateLayout.swift in Sources */, FA41C41B1C582AB800D46967 /* FieldDropdownView.swift in Sources */, FAC035071D516EE10017C1C8 /* FieldDropdownLayout.swift in Sources */, + FACBAB591D8371220063A975 /* WorkspaceLayoutCoordinator.swift in Sources */, FAF5005D1D50669E009E4B24 /* FieldCheckboxLayout.swift in Sources */, FA6232521B6B12CF00F1EF42 /* Field.swift in Sources */, FAFAEE6D1CDBD5AB00698179 /* InsetTextField.swift in Sources */, diff --git a/Blockly/Code/Common/BlocklyError.swift b/Blockly/Code/Common/BlocklyError.swift index 13d1e6b3..60886cee 100644 --- a/Blockly/Code/Common/BlocklyError.swift +++ b/Blockly/Code/Common/BlocklyError.swift @@ -44,7 +44,8 @@ public class BlocklyError: NSError { FileNotFound = 600, FileNotReadable = 601, IllegalState = 700, - IllegalArgument = 701 + IllegalArgument = 701, + IllegalOperation = 702 } public typealias Code = BKYBlocklyErrorCode diff --git a/Blockly/Code/Control/BlockBumper.swift b/Blockly/Code/Control/BlockBumper.swift index 621a7dea..38758d19 100644 --- a/Blockly/Code/Control/BlockBumper.swift +++ b/Blockly/Code/Control/BlockBumper.swift @@ -22,17 +22,19 @@ import Foundation public class BlockBumper: NSObject { // MARK: - Properties - /// The X and Y amount to bump blocks away from each other, specified as a Workspace coordinate - /// system unit - public var bumpDistance: CGFloat - - /// The workspace layout where blocks are being bumped - public var workspaceLayout: WorkspaceLayout? + /// The workspace layout coordinator where blocks are being bumped + public weak var workspaceLayoutCoordinator: WorkspaceLayoutCoordinator? - // MARK: - Initializers + /// Convenience property for `self.workspaceLayoutCoordinator?.workspaceLayout` + private var workspaceLayout: WorkspaceLayout? { + return workspaceLayoutCoordinator?.workspaceLayout + } - public init(bumpDistance: CGFloat) { - self.bumpDistance = bumpDistance + /// The X and Y amount to bump blocks away from each other, specified as a Workspace coordinate + /// system unit. This value is read from `self.workspaceLayout.config` using the key + /// `LayoutConfig.BlockBumpDistance`. If no value exists for that key, this defaults to `0`. + private var bumpDistance: CGFloat { + return workspaceLayout?.config.unitFor(LayoutConfig.BlockBumpDistance).workspaceUnit ?? 0 } // MARK: - Public @@ -52,8 +54,8 @@ public class BlockBumper: NSObject { return } - let dx = stationaryConnection.position.x + self.bumpDistance - impingingConnection.position.x - let dy = stationaryConnection.position.y + self.bumpDistance - impingingConnection.position.y + let dx = stationaryConnection.position.x + bumpDistance - impingingConnection.position.x + let dy = stationaryConnection.position.y + bumpDistance - impingingConnection.position.y let newPosition = WorkspacePointMake( blockGroupLayout.absolutePosition.x + dx, blockGroupLayout.absolutePosition.y + dy) @@ -100,14 +102,14 @@ public class BlockBumper: NSObject { */ private func bumpBlockLayoutOfConnectionAwayFromNeighbours(connection: Connection) { guard - let connectionManager = workspaceLayout?.connectionManager, + let connectionManager = workspaceLayoutCoordinator?.connectionManager, let rootBlockGroupLayout = connection.sourceBlock.layout?.rootBlockGroupLayout else { return } let neighbours = - connectionManager.stationaryNeighboursForConnection(connection, maxRadius: self.bumpDistance) + connectionManager.stationaryNeighboursForConnection(connection, maxRadius: bumpDistance) for neighbour in neighbours { // Bump away from the first neighbour that isn't in the same block group as the target @@ -126,14 +128,14 @@ public class BlockBumper: NSObject { */ private func bumpAllBlocksNearConnection(connection: Connection) { guard - let connectionManager = workspaceLayout?.connectionManager, + let connectionManager = workspaceLayoutCoordinator?.connectionManager, let rootBlockGroupLayout = connection.sourceBlock.layout?.rootBlockGroupLayout else { return } let neighbours = - connectionManager.stationaryNeighboursForConnection(connection, maxRadius: self.bumpDistance) + connectionManager.stationaryNeighboursForConnection(connection, maxRadius: bumpDistance) for neighbour in neighbours { // Only bump blocks that aren't in the same block group as the target connection's block group diff --git a/Blockly/Code/Control/Dragger.swift b/Blockly/Code/Control/Dragger.swift index b5951967..e21d9638 100644 --- a/Blockly/Code/Control/Dragger.swift +++ b/Blockly/Code/Control/Dragger.swift @@ -20,34 +20,22 @@ Controller for dragging blocks around in the workspace. */ @objc(BKYDragger) public class Dragger: NSObject { - // MARK: - Static Properties - - /** - Blocks "snap" toward each other at the end of drags if they have compatible connections - near each other. This is the farthest they can snap. This value is in the UIView coordinate - system. - */ - private static let MAX_SNAP_DISTANCE: CGFloat = 25 - // MARK: - Properties /// The workspace layout where blocks are being dragged - public var workspaceLayout: WorkspaceLayout? { + public var workspaceLayoutCoordinator: WorkspaceLayoutCoordinator? { didSet { - if workspaceLayout == oldValue { + if workspaceLayoutCoordinator == oldValue { return } - // Reset all gesture data and update the block bumper with the new workspace layout + // Reset all gesture data _dragGestureData.keys.forEach { clearGestureData(forUUID: $0) } - _blockBumper.workspaceLayout = workspaceLayout } } /// Stores the data for each active drag gesture, keyed by the corresponding block view's layout /// uuid private var _dragGestureData = [String: DragGestureData]() - /// Object responsible for bumping blocks out of the way - private let _blockBumper = BlockBumper(bumpDistance: Dragger.MAX_SNAP_DISTANCE) // MARK: - Public @@ -60,7 +48,12 @@ public class Dragger: NSObject { system */ public func startDraggingBlockLayout(layout: BlockLayout, touchPosition: WorkspacePoint) { - if !layout.block.draggable { + guard let workspaceLayoutCoordinator = self.workspaceLayoutCoordinator, + let connectionManager = workspaceLayoutCoordinator.connectionManager + where layout.block.draggable && + workspaceLayoutCoordinator.workspaceLayout + .allVisibleBlockLayoutsInWorkspace().contains(layout) else + { return } @@ -70,30 +63,33 @@ public class Dragger: NSObject { // Disconnect this block from its previous or output connections prior to moving it let block = layout.block - block.previousConnection?.disconnect() - block.outputConnection?.disconnect() + if let previousConnection = block.previousConnection { + workspaceLayoutCoordinator.disconnect(previousConnection) + } + if let outputConnection = block.outputConnection { + workspaceLayoutCoordinator.disconnect(outputConnection) + } // Highlight this block layout.highlighted = true layout.rootBlockGroupLayout?.dragging = true // Bring its block group layout to the front - self.workspaceLayout?.bringBlockGroupLayoutToFront(layout.rootBlockGroupLayout) + self.workspaceLayoutCoordinator?.workspaceLayout.bringBlockGroupLayoutToFront( + layout.rootBlockGroupLayout) // Start a new connection group for this block group layout - if let newConnectionGroup = - self.workspaceLayout?.connectionManager.startGroupForBlock(block) - { - // Keep track of the gesture data for this drag - let dragGestureData = DragGestureData( - blockLayout: layout, - blockLayoutStartPosition: layout.absolutePosition, - touchStartPosition: touchPosition, - connectionGroup: newConnectionGroup - ) - - self._dragGestureData[layout.uuid] = dragGestureData - } + let newConnectionGroup = connectionManager.startGroupForBlock(block) + + // Keep track of the gesture data for this drag + let dragGestureData = DragGestureData( + blockLayout: layout, + blockLayoutStartPosition: layout.absolutePosition, + touchStartPosition: touchPosition, + connectionGroup: newConnectionGroup + ) + + self._dragGestureData[layout.uuid] = dragGestureData } } @@ -133,6 +129,10 @@ public class Dragger: NSObject { - Parameter layout: The given block layout */ public func finishDraggingBlockLayout(layout: BlockLayout) { + guard let workspaceLayoutCoordinator = self.workspaceLayoutCoordinator else { + return + } + Layout.animate { // Remove the highlight for this block layout.highlighted = false @@ -142,7 +142,7 @@ public class Dragger: NSObject { if let drag = self._dragGestureData[layout.uuid], let connectionPair = self.findBestConnectionForDrag(drag) { - self.connectPair(connectionPair) + workspaceLayoutCoordinator.connectPair(connectionPair) self.clearGestureDataForBlockLayout(layout, moveConnectionsToGroup: connectionPair.fromConnectionManagerGroup) @@ -153,11 +153,11 @@ public class Dragger: NSObject { // during the drag for performance reasons, so we have to update it now). Also, there is // no need to call this method in the `if` part of this `if/else` block, since // `self.connectPair(:)` implicitly calls it already. - self.workspaceLayout?.updateCanvasSize() + workspaceLayoutCoordinator.workspaceLayout.updateCanvasSize() } // Bump any neighbours of the block layout - self._blockBumper.bumpNeighboursOfBlockLayout(layout) + workspaceLayoutCoordinator.blockBumper.bumpNeighboursOfBlockLayout(layout) // Update the highlighted connections for all other drags (due to potential changes in block // sizes) @@ -199,114 +199,13 @@ public class Dragger: NSObject { } // Move connections to a different group in the connection manager - workspaceLayout?.connectionManager + workspaceLayoutCoordinator?.connectionManager? .mergeGroup(gestureData.connectionGroup, intoGroup: group) removeHighlightedConnectionForDrag(gestureData) _dragGestureData[uuid] = nil } - /** - Connects a pair of connections, disconnecting and possibly reattaching any existing connections, - depending on the operation. - - - Parameter connectionPair: The pair to connect - */ - private func connectPair(connectionPair: ConnectionManager.ConnectionPair) { - let moving = connectionPair.moving - let target = connectionPair.target - - do { - switch (moving.type) { - case .InputValue: - try connectValueConnections(superior: moving, inferior: target) - case .OutputValue: - try connectValueConnections(superior: target, inferior: moving) - case .NextStatement: - try connectStatementConnections(superior: moving, inferior: target) - case .PreviousStatement: - try connectStatementConnections(superior: target, inferior: moving) - } - } catch let error as NSError { - bky_assertionFailure("Could not connect pair together: \(error)") - } - } - - /** - Connects two value connections. If a block was previously connected to the superior connection, - this method attempts to reattach it to the end of the inferior connection's block input value - chain. If unsuccessful, the disconnected block is bumped away. - */ - private func connectValueConnections(superior superior: Connection, inferior: Connection) throws { - let previouslyConnectedBlock = superior.targetBlock - - // NOTE: Layouts are automatically re-computed after disconnecting/reconnecting - superior.disconnect() - inferior.disconnect() - try superior.connectTo(inferior) - - // Bring the entire block group layout to the front - if let workspaceLayout = self.workspaceLayout, - let rootBlockGroupLayout = superior.sourceBlock?.layout?.rootBlockGroupLayout - { - workspaceLayout.bringBlockGroupLayoutToFront(rootBlockGroupLayout) - } - - if let previousOutputConnection = previouslyConnectedBlock?.outputConnection { - if let lastInputConnection = inferior.sourceBlock?.lastInputValueConnectionInChain() - where lastInputConnection.canConnectTo(previousOutputConnection) - { - // Try to reconnect previously connected block to the end of the input value chain - try lastInputConnection.connectTo(previousOutputConnection) - } else { - // Bump previously connected block away from the superior connection - _blockBumper.bumpBlockLayoutOfConnection(previousOutputConnection, - awayFromConnection: superior) - } - } - } - - /** - Connects two statement connections. If a block was previously connected to the superior - connection, this method attempts to reattach it to the end of the inferior connection's block - chain. If unsuccessful, the disconnected block is bumped away. - - - Parameter superior: A connection of type `.NextStatement` - - Parameter inferior: A connection of type `.PreviousStatement` - - Throws: - `BlocklyError`: Thrown if the previous/next statements could not be connected together or if - the previously disconnected block could not be re-connected to the end of the block chain. - */ - private func connectStatementConnections(superior superior: Connection, inferior: Connection) - throws - { - let previouslyConnectedBlock = superior.targetBlock - - // NOTE: Layouts are automatically re-computed after disconnecting/reconnecting - superior.disconnect() - inferior.disconnect() - try superior.connectTo(inferior) - - // Bring the entire block group layout to the front - if let workspaceLayout = self.workspaceLayout, - let rootBlockGroupLayout = superior.sourceBlock?.layout?.rootBlockGroupLayout - { - workspaceLayout.bringBlockGroupLayoutToFront(rootBlockGroupLayout) - } - - if let previousConnection = previouslyConnectedBlock?.previousConnection { - if let lastConnection = inferior.sourceBlock?.lastBlockInChain().nextConnection - where lastConnection.canConnectTo(previousConnection) - { - // Reconnect previously connected block to the end of the block chain - try lastConnection.connectTo(previousConnection) - } else { - // Bump previously connected block away from the superior connection - _blockBumper.bumpBlockLayoutOfConnection(previousConnection, awayFromConnection: superior) - } - } - } - /** Updates the highlighted connection for a dragged block. @@ -346,11 +245,14 @@ public class Dragger: NSObject { private func findBestConnectionForDrag(drag: DragGestureData) -> ConnectionManager.ConnectionPair? { - if let workspaceLayout = self.workspaceLayout { - let maxRadius = workspaceLayout.engine.workspaceUnitFromViewUnit(Dragger.MAX_SNAP_DISTANCE) + if let workspaceLayout = workspaceLayoutCoordinator?.workspaceLayout, + let connectionManager = workspaceLayoutCoordinator?.connectionManager + { + let maxRadius = workspaceLayout.config.unitFor( + LayoutConfig.BlockSnapDistance, defaultValue: LayoutConfig.Unit(0)).workspaceUnit - return workspaceLayout.connectionManager.findBestConnectionForGroup(drag.connectionGroup, - maxRadius: maxRadius) + return + connectionManager.findBestConnectionForGroup(drag.connectionGroup, maxRadius: maxRadius) } return nil } diff --git a/Blockly/Code/Layout/LayoutConfig.swift b/Blockly/Code/Layout/LayoutConfig.swift index d3ed21e6..9577a4a8 100644 --- a/Blockly/Code/Layout/LayoutConfig.swift +++ b/Blockly/Code/Layout/LayoutConfig.swift @@ -66,6 +66,13 @@ public class LayoutConfig: NSObject { /// Total number of `PropertyKey` values that have been created via `newPropertyKey()`. private static var NUMBER_OF_PROPERTY_KEYS = 0 + /// [`Unit`] The distance to bump blocks away from each other + public static let BlockBumpDistance = LayoutConfig.newPropertyKey() + + /// [`Unit`] The maximum distance allowed for blocks to "snap" toward each other at the end of + /// drags, if they have compatible connections near each other. + public static let BlockSnapDistance = LayoutConfig.newPropertyKey() + /// [`Unit`] Horizontal padding around inline elements (such as fields or inputs) public static let InlineXPadding = LayoutConfig.newPropertyKey() @@ -141,6 +148,8 @@ public class LayoutConfig: NSObject { super.init() // Set default values for base config keys + setUnit(Unit(25), forKey: LayoutConfig.BlockBumpDistance) + setUnit(Unit(25), forKey: LayoutConfig.BlockSnapDistance) setUnit(Unit(10), forKey: LayoutConfig.InlineXPadding) setUnit(Unit(5), forKey: LayoutConfig.InlineYPadding) setUnit(Unit(10), forKey: LayoutConfig.WorkspaceFlowXSeparatorSpace) diff --git a/Blockly/Code/Layout/ToolboxLayout.swift b/Blockly/Code/Layout/ToolboxLayout.swift index 41517fd5..2f6418e1 100644 --- a/Blockly/Code/Layout/ToolboxLayout.swift +++ b/Blockly/Code/Layout/ToolboxLayout.swift @@ -37,8 +37,8 @@ public class ToolboxLayout: NSObject { /// The layout builder to use when creating new `WorkspaceFlowLayout` instances for each /// category in `toolbox` public let layoutBuilder: LayoutBuilder - /// The associated list of `WorkspaceFlowLayout` instances for `toolbox.categories` - public var categoryLayouts = [WorkspaceFlowLayout]() + /// The associated list of `WorkspaceLayoutCoordinator` instances for `toolbox.categories` + public var categoryLayoutCoordinators = [WorkspaceLayoutCoordinator]() // MARK: - Initializers @@ -46,15 +46,15 @@ public class ToolboxLayout: NSObject { Creates a new `ToolboxLayout`. - Parameter toolbox: The `Toolbox` to associate with this object. - - Parameter layoutDirection: The layout direction to use when creating new - `WorkspaceFlowLayout` instances for each category in `toolbox` - Parameter engine: The layout engine to use when creating new `WorkspaceFlowLayout` instances for each category in `toolbox` + - Parameter layoutDirection: The layout direction to use when creating new + `WorkspaceFlowLayout` instances for each category in `toolbox` - Parameter layoutBuilder: The layout builder to use when creating new `WorkspaceFlowLayout` instances for each category in `toolbox` */ - public init(toolbox: Toolbox, layoutDirection: WorkspaceFlowLayout.LayoutDirection, - engine: LayoutEngine, layoutBuilder: LayoutBuilder) + public init(toolbox: Toolbox, engine: LayoutEngine, + layoutDirection: WorkspaceFlowLayout.LayoutDirection, layoutBuilder: LayoutBuilder) { self.toolbox = toolbox self.engine = engine @@ -64,19 +64,21 @@ public class ToolboxLayout: NSObject { super.init() for category in self.toolbox.categories { - addLayoutForToolboxCategory(category) + addLayoutCoordinatorForToolboxCategory(category) } } // MARK: - Private - private func addLayoutForToolboxCategory(category: Toolbox.Category) { + private func addLayoutCoordinatorForToolboxCategory(category: Toolbox.Category) { do { - let layout = try WorkspaceFlowLayout(workspace: category, - layoutDirection: self.layoutDirection, engine: self.engine, layoutBuilder: layoutBuilder) - categoryLayouts.append(layout) + let layout = + WorkspaceFlowLayout(workspace: category, engine: engine, layoutDirection: layoutDirection) + let coordinator = try WorkspaceLayoutCoordinator( + workspaceLayout: layout, layoutBuilder: layoutBuilder, connectionManager: nil) + categoryLayoutCoordinators.append(coordinator) } catch let error as NSError { - bky_assertionFailure("Could not create WorkspaceListLayout: \(error)") + bky_assertionFailure("Could not create WorkspaceFlowLayout: \(error)") } } } diff --git a/Blockly/Code/Layout/WorkspaceFlowLayout.swift b/Blockly/Code/Layout/WorkspaceFlowLayout.swift index 6657d200..fc42ee4b 100644 --- a/Blockly/Code/Layout/WorkspaceFlowLayout.swift +++ b/Blockly/Code/Layout/WorkspaceFlowLayout.swift @@ -51,12 +51,11 @@ public class WorkspaceFlowLayout: WorkspaceLayout { // MARK: - Initializers - public init(workspace: WorkspaceFlow, layoutDirection: LayoutDirection, engine: LayoutEngine, - layoutBuilder: LayoutBuilder) throws + public init(workspace: WorkspaceFlow, engine: LayoutEngine, layoutDirection: LayoutDirection) { self.workspaceFlow = workspace self.layoutDirection = layoutDirection - try super.init(workspace: workspace, engine: engine, layoutBuilder: layoutBuilder) + super.init(workspace: workspace, engine: engine) } public override func performLayout(includeChildren includeChildren: Bool) { diff --git a/Blockly/Code/Layout/WorkspaceLayout.swift b/Blockly/Code/Layout/WorkspaceLayout.swift index 7c60088f..0d9fc321 100644 --- a/Blockly/Code/Layout/WorkspaceLayout.swift +++ b/Blockly/Code/Layout/WorkspaceLayout.swift @@ -30,12 +30,6 @@ public class WorkspaceLayout: Layout { /// The `Workspace` to layout public final let workspace: Workspace - /// The locations of all connections in this workspace - public final let connectionManager: ConnectionManager - - /// Builder for constructing layouts under this workspace - public final let layoutBuilder: LayoutBuilder - /// All child `BlockGroupLayout` objects that have been appended to this layout public final var blockGroupLayouts = [BlockGroupLayout]() @@ -50,28 +44,12 @@ public class WorkspaceLayout: Layout { // MARK: - Initializers - public init(workspace: Workspace, engine: LayoutEngine, layoutBuilder: LayoutBuilder, - connectionManager: ConnectionManager? = nil) throws { + public init(workspace: Workspace, engine: LayoutEngine) { self.workspace = workspace - self.layoutBuilder = layoutBuilder - self.connectionManager = connectionManager ?? ConnectionManager() super.init(engine: engine) - // Assign the layout as the workspace's delegate so it can listen for new events that - // occur on the workspace - workspace.delegate = self - - // Build the layout tree, based on the existing state of the workspace. This creates a set of - // layout objects for all of its blocks/inputs/fields - try self.layoutBuilder.buildLayoutTree(self) - - // Perform a layout update for the entire tree - updateLayoutDownTree() - - // Immediately start tracking connections of all visible blocks in the workspace - for blockLayout in allVisibleBlockLayoutsInWorkspace() { - trackConnections(forBlockLayout: blockLayout) - } + // Set the workspace's layout property + workspace.layout = self } // MARK: - Super @@ -232,297 +210,4 @@ public class WorkspaceLayout: Layout { // positions of block groups also change. refreshViewPositionsForTree() } - - // MARK: - Private - - private func trackConnections(forBlockLayout blockLayout: BlockLayout) { - guard blockLayout.visible else { - // Only track connections for visible block layouts - return - } - - // Automatically track changes to the connection so we can update the layout hierarchy - // accordingly - for connection in blockLayout.block.directConnections { - connection.targetDelegate = self - connectionManager.trackConnection(connection) - } - } - - private func untrackConnections(forBlockLayout blockLayout: BlockLayout) { - // Detach connection tracking for the block - for connection in blockLayout.block.directConnections { - connection.targetDelegate = nil - connectionManager.untrackConnection(connection) - } - } -} - -// MARK: - WorkspaceDelegate implementation - -extension WorkspaceLayout: WorkspaceDelegate { - public func workspace(workspace: Workspace, didAddBlock block: Block) { - if !block.topLevel { - // We only need to create layout trees for top level blocks - return - } - - do { - // Create the layout tree for this newly added block - if let blockGroupLayout = - try self.layoutBuilder.buildLayoutTreeForTopLevelBlock(block, workspaceLayout: self) - { - // Perform a layout for the tree - blockGroupLayout.updateLayoutDownTree() - - // Track connections of all new block layouts that were added - for blockLayout in blockGroupLayout.flattenedLayoutTree(ofType: BlockLayout.self) { - trackConnections(forBlockLayout: blockLayout) - } - - // Update the content size - updateCanvasSize() - - // Schedule change event for an added block layout - sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) - } - } catch let error as NSError { - bky_assertionFailure("Could not create the layout tree for block: \(error)") - } - } - - public func workspace(workspace: Workspace, willRemoveBlock block: Block) { - if !block.topLevel { - // We only need to remove layout trees for top-level blocks - return - } - - if let blockGroupLayout = block.layout?.parentBlockGroupLayout { - // Untrack connections for all block layouts that will be removed - for blockLayout in blockGroupLayout.flattenedLayoutTree(ofType: BlockLayout.self) { - untrackConnections(forBlockLayout: blockLayout) - } - - removeBlockGroupLayout(blockGroupLayout) - - sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) - } - } -} - -// MARK: - ConnectionTargetDelegate - -extension WorkspaceLayout: ConnectionTargetDelegate { - public func didChangeTarget(forConnection connection: Connection, oldTarget: Connection?) - { - do { - try updateLayoutTree(forConnection: connection, oldTarget: oldTarget) - } catch let error as NSError { - bky_assertionFailure("Could not update layout tree for connection: \(error)") - } - } - - public func didChangeShadow(forConnection connection: Connection, oldShadow: Connection?) - { - do { - if connection.shadowConnected && !connection.connected { - // There's a new shadow block for the connection and it is not connected to anything. - // Add the shadow block layout tree. - try addShadowBlockLayoutTree(forConnection: connection) - } else if !connection.shadowConnected { - // There's no shadow block for the connection. Remove the shadow block layout tree. - try removeShadowBlockLayoutTree(forShadowBlock: oldShadow?.sourceBlock) - } - } catch let error as NSError { - bky_assertionFailure("Could not update shadow block layout tree for connection: \(error)") - } - } - - /** - Adds shadow blocks for a given connection to the layout tree. If the shadow blocks already exist - or if no shadow blocks are connected to the given connection, nothing happens. - - - Note: This method only updates the layout tree if the given connection is of type - `.NextStatement` or `.InputValue`. Otherwise, this method does nothing. - - - Parameter connection: The connection that should have its shadow blocks added to the layout - tree - */ - private func addShadowBlockLayoutTree(forConnection connection: Connection?) throws { - guard let aConnection = connection, - shadowBlock = aConnection.shadowBlock - where (aConnection.type == .NextStatement || aConnection.type == .InputValue) && - shadowBlock.layout == nil && !aConnection.connected else - { - // Only next/input connectors are responsible for updating the shadow block group - // layout hierarchy, not previous/output connectors. - return - } - - // Nothing is connected to aConnection. Re-create the shadow block hierarchy since it doesn't - // exist. - let shadowBlockGroupLayout = - try layoutBuilder.layoutFactory.layoutForBlockGroupLayout(engine: engine) - try layoutBuilder.buildLayoutTreeForBlockGroupLayout(shadowBlockGroupLayout, block: shadowBlock) - let shadowBlockLayouts = shadowBlockGroupLayout.blockLayouts - - // Add shadow block layouts to proper block group - if let blockGroupLayout = - (aConnection.sourceInput?.layout?.blockGroupLayout ?? // For input values or statements - aConnection.sourceBlock.layout?.parentBlockGroupLayout) // For a block's next statement - { - Layout.doNotAnimate { - blockGroupLayout.appendBlockLayouts(shadowBlockLayouts, updateLayout: true) - blockGroupLayout.performLayout(includeChildren: true) - blockGroupLayout.refreshViewPositionsForTree() - } - - blockGroupLayout.updateLayoutUpTree() - } - - // Update connection tracking - let allBlockLayouts = shadowBlockLayouts.flatMap { - $0.flattenedLayoutTree(ofType: BlockLayout.self) - } - for blockLayout in allBlockLayouts { - trackConnections(forBlockLayout: blockLayout) - } - - if allBlockLayouts.count > 0 { - sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) - } - } - - /** - Removes a shadow block layout tree from its parent layout, starting from a given shadow block. - - - Parameter shadowBlock: The shadow block - */ - private func removeShadowBlockLayoutTree(forShadowBlock shadowBlock: Block?) throws { - guard let shadowBlockLayout = shadowBlock?.layout, - shadowBlockLayoutParent = shadowBlockLayout.parentBlockGroupLayout - where (shadowBlock?.shadow ?? false) else - { - // There is no shadow block layout for this block. - return - } - - // Remove all layouts connected to this shadow block layout - let removedLayouts = shadowBlockLayoutParent - .removeAllStartingFromBlockLayout(shadowBlockLayout, updateLayout: false) - .flatMap { $0.flattenedLayoutTree(ofType: BlockLayout.self) } - - for removedLayout in removedLayouts { - // Set the delegate of the block to nil (effectively removing its BlockLayout) - removedLayout.block.delegate = nil - // Untrack connections for the layout - untrackConnections(forBlockLayout: removedLayout) - } - - if removedLayouts.count > 0 { - sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) - } - } - - /** - Whenever a connection has been changed for a block in the workspace, this method is called to - ensure that the layout tree is properly kept in sync to reflect this change. - - - Note: This method only updates the layout tree if the given connection is of type - `.PreviousStatement` or `.OutputValue`. Otherwise, this method does nothing. - - - Parameter connection: The connection that changed - - Parameter oldTarget: The previous value of `connection.targetConnection` - */ - private func updateLayoutTree(forConnection connection: Connection, oldTarget: Connection?) throws - { - // TODO:(#29) Optimize re-rendering all layouts affected by this method - - guard connection.type == .PreviousStatement || connection.type == .OutputValue else { - // Only previous/output connectors are responsible for updating the block group - // layout hierarchy, not next/input connectors. - return - } - - // Check that there are layouts for both the source and target blocks of this connection - guard let sourceBlock = connection.sourceBlock, - let sourceBlockLayout = sourceBlock.layout - where connection.targetConnection?.sourceInput == nil || - connection.targetConnection?.sourceInput?.layout != nil || - connection.targetConnection?.sourceBlock == nil || - connection.targetConnection?.sourceBlock?.layout != nil - else - { - throw BlocklyError(.IllegalState, "Can't connect a block without a layout. ") - } - - // Check that this layout is connected to a block group layout - guard sourceBlock.layout?.parentBlockGroupLayout != nil else { - throw BlocklyError(.IllegalState, - "Block layout is not connected to a parent block group layout. ") - } - - guard (connection.targetBlock == nil || workspace.containsBlock(connection.targetBlock!)) && - workspace.containsBlock(sourceBlock) else - { - throw BlocklyError(.IllegalState, "Can't connect blocks from different workspaces") - } - - // Keep a reference to the old parent block group layout, in case we need to clean it up later - // (if it becomes empty). - let oldParentLayout = sourceBlockLayout.parentBlockGroupLayout - - if let targetConnection = connection.targetConnection { - // `targetConnection` is connected to something now. - // Remove its shadow block layout tree (if it exists). - try removeShadowBlockLayoutTree(forShadowBlock: targetConnection.shadowBlock) - - // Move `sourceBlockLayout` and its followers to a new block group layout - if let targetInputLayout = targetConnection.sourceInput?.layout { - // Move them to target input's block group layout - targetInputLayout.blockGroupLayout - .claimBlockLayoutAndFollowers(sourceBlockLayout, updateLayouts: true) - } else if let targetBlockLayout = targetConnection.sourceBlock.layout { - // Move them to the target block's group layout - targetBlockLayout.parentBlockGroupLayout? - .claimBlockLayoutAndFollowers(sourceBlockLayout, updateLayouts: true) - } - } else { - // The connection is no longer connected to anything. - - // Block was disconnected and added to the workspace level. - // Create a new block group layout and set its `relativePosition` to the current absolute - // position of the block that was disconnected - let layoutFactory = self.layoutBuilder.layoutFactory - let blockGroupLayout = try layoutFactory.layoutForBlockGroupLayout(engine: self.engine) - blockGroupLayout.relativePosition = sourceBlockLayout.absolutePosition - - Layout.doNotAnimate { - // Add this new block group layout to the workspace level - self.appendBlockGroupLayout(blockGroupLayout, updateLayout: true) - self.bringBlockGroupLayoutToFront(blockGroupLayout) - } - - blockGroupLayout.claimBlockLayoutAndFollowers(sourceBlockLayout, updateLayouts: true) - } - - // Re-create the shadow block layout tree for the previous connection target (if it has one). - try addShadowBlockLayoutTree(forConnection: oldTarget) - - // If the previous block group layout parent of `sourceBlockLayout` is now empty and is at the - // the top-level of the workspace, remove it - if let emptyBlockGroupLayout = oldParentLayout - where emptyBlockGroupLayout.blockLayouts.count == 0 && - emptyBlockGroupLayout.parentLayout == workspace.layout - { - Layout.doNotAnimate { - // Remove this block's old parent group layout from the workspace level - self.removeBlockGroupLayout(emptyBlockGroupLayout, updateLayout: true) - } - } - - Layout.doNotAnimate { - self.updateCanvasSize() - } - } } diff --git a/Blockly/Code/Layout/WorkspaceLayoutCoordinator.swift b/Blockly/Code/Layout/WorkspaceLayoutCoordinator.swift new file mode 100644 index 00000000..bb588f0b --- /dev/null +++ b/Blockly/Code/Layout/WorkspaceLayoutCoordinator.swift @@ -0,0 +1,542 @@ +/* + * 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 + +/** + Object that is responsible for managing a worksapce layout. This includes maintaining the layout + hierarchy of the workspace and ensuring that model and layout objects under this workspace layout + remains in-sync. + */ +@objc(BKYWorkspaceLayoutCoordinator) +public class WorkspaceLayoutCoordinator: NSObject { + /// The workspace layout whose layout hierarchy is being managed by this object + public let workspaceLayout: WorkspaceLayout + + /// Builder for constructing layouts under `self.workspaceLayout` + public final let layoutBuilder: LayoutBuilder + + /// Manager for tracking all connection positions under `self.workspaceLayout`. If this value + /// is `nil`, connection positions aren't being tracked. + public private(set) final var connectionManager: ConnectionManager? + + /// Object responsible for bumping blocks away from each other + public let blockBumper = BlockBumper() + + // MARK: - Initializers / De-initializers + + /** + Initializes the workspace layout coordinator. + + - Parameter workspaceLayout: The `WorkspaceLayout` that should be managed by this coordinator + - Parameter layoutBuilder: Builder for constructing layouts under `workspaceLayout` + - Parameter connectionManager: Manager for tracking all connection positions under + `workspaceLayout`. If this value is `nil`, connection positions will not be tracked. + */ + public init( + workspaceLayout: WorkspaceLayout, layoutBuilder: LayoutBuilder, + connectionManager: ConnectionManager?) throws + { + self.workspaceLayout = workspaceLayout + self.layoutBuilder = layoutBuilder + self.connectionManager = connectionManager + + super.init() + + blockBumper.workspaceLayoutCoordinator = self + + // Listen for changes in the workspace, so this object can update the layout hierarchy + // appropriately + workspaceLayout.workspace.listeners.add(self) + + // Build the layout tree, based on the existing state of the workspace. This creates a set of + // layout objects for all of its blocks/inputs/fields + try layoutBuilder.buildLayoutTree(workspaceLayout) + + // Perform a layout update for the entire tree + workspaceLayout.updateLayoutDownTree() + + // Immediately start tracking connections of all visible blocks in the workspace + for blockLayout in workspaceLayout.allVisibleBlockLayoutsInWorkspace() { + trackConnections(forBlockLayout: blockLayout) + } + } + + deinit { + workspaceLayout.workspace.listeners.remove(self) + } + + // MARK: - Public + + public func addBlockTree(rootBlock: Block) throws { + return try workspaceLayout.workspace.addBlockTree(rootBlock) + } + + /** + Disconnects a given block from its previous/output connections, and removes it and all of its + connected blocks from the workspace. + + - Parameter rootBlock: The root block to remove. + - Throws: + `BlocklyError`: Thrown if the tree of blocks could not be removed from the workspace. + */ + public func removeBlockTree(rootBlock: Block) throws { + // Disconnect this block from anything + if let previousConnection = rootBlock.previousConnection { + disconnect(previousConnection) + } + if let outputConnection = rootBlock.outputConnection { + disconnect(outputConnection) + } + + try workspaceLayout.workspace.removeBlockTree(rootBlock) + } + + /** + Deep copies a block and adds all of the copied blocks into the workspace. + + - Parameter rootBlock: The root block to copy + - Parameter editable: Sets whether each block is `editable` or not + - Returns: The root block that was copied + - Throws: + `BlocklyError`: Thrown if the block could not be copied + */ + public func copyBlockTree(rootBlock: Block, editable: Bool) throws -> Block { + return try workspaceLayout.workspace.copyBlockTree(rootBlock, editable: editable) + } + + /** + Connects a pair of connections, disconnecting and possibly reattaching any existing connections, + depending on the operation. + + - Parameter connectionPair: The pair to connect + */ + public func connectPair(connectionPair: ConnectionManager.ConnectionPair) { + let moving = connectionPair.moving + let target = connectionPair.target + + do { + switch (moving.type) { + case .InputValue: + try connectValueConnections(superior: moving, inferior: target) + case .OutputValue: + try connectValueConnections(superior: target, inferior: moving) + case .NextStatement: + try connectStatementConnections(superior: moving, inferior: target) + case .PreviousStatement: + try connectStatementConnections(superior: target, inferior: moving) + } + } catch let error as NSError { + bky_assertionFailure("Could not connect pair together: \(error)") + } + } + + public func disconnect(connection: Connection) { + let oldTarget = connection.targetConnection + connection.disconnect() + + didChangeTarget(forConnection: connection, oldTarget: oldTarget) + if let oldTarget = oldTarget { + didChangeTarget(forConnection: oldTarget, oldTarget: connection) + } + } + + public func connect(connection1: Connection, _ connection2: Connection) throws { + let oldTarget1 = connection1.targetConnection + let oldTarget2 = connection2.targetConnection + try connection1.connectTo(connection2) + + didChangeTarget(forConnection: connection1, oldTarget: oldTarget1) + didChangeTarget(forConnection: connection2, oldTarget: oldTarget2) + } + + // MARK: - Private + + /** + Connects two value connections. If a block was previously connected to the superior connection, + this method attempts to reattach it to the end of the inferior connection's block input value + chain. If unsuccessful, the disconnected block is bumped away. + */ + private func connectValueConnections(superior superior: Connection, inferior: Connection) throws { + let previouslyConnectedBlock = superior.targetBlock + + // NOTE: Layouts are automatically re-computed after disconnecting/reconnecting + disconnect(superior) + disconnect(inferior) + try connect(superior, inferior) + + // Bring the entire block group layout to the front + if let rootBlockGroupLayout = superior.sourceBlock?.layout?.rootBlockGroupLayout { + workspaceLayout.bringBlockGroupLayoutToFront(rootBlockGroupLayout) + } + + if let previousOutputConnection = previouslyConnectedBlock?.outputConnection { + if let lastInputConnection = inferior.sourceBlock?.lastInputValueConnectionInChain() + where lastInputConnection.canConnectTo(previousOutputConnection) + { + // Try to reconnect previously connected block to the end of the input value chain + try connect(lastInputConnection, previousOutputConnection) + } else { + // Bump previously connected block away from the superior connection + blockBumper.bumpBlockLayoutOfConnection(previousOutputConnection, + awayFromConnection: superior) + } + } + } + + /** + Connects two statement connections. If a block was previously connected to the superior + connection, this method attempts to reattach it to the end of the inferior connection's block + chain. If unsuccessful, the disconnected block is bumped away. + + - Parameter superior: A connection of type `.NextStatement` + - Parameter inferior: A connection of type `.PreviousStatement` + - Throws: + `BlocklyError`: Thrown if the previous/next statements could not be connected together or if + the previously disconnected block could not be re-connected to the end of the block chain. + */ + private func connectStatementConnections(superior superior: Connection, inferior: Connection) + throws + { + let previouslyConnectedBlock = superior.targetBlock + + // NOTE: Layouts are automatically re-computed after disconnecting/reconnecting + disconnect(superior) + disconnect(inferior) + try connect(superior, inferior) + + // Bring the entire block group layout to the front + if let rootBlockGroupLayout = superior.sourceBlock?.layout?.rootBlockGroupLayout { + workspaceLayout.bringBlockGroupLayoutToFront(rootBlockGroupLayout) + } + + if let previousConnection = previouslyConnectedBlock?.previousConnection { + if let lastConnection = inferior.sourceBlock?.lastBlockInChain().nextConnection + where lastConnection.canConnectTo(previousConnection) + { + // Reconnect previously connected block to the end of the block chain + try connect(lastConnection, previousConnection) + } else { + // Bump previously connected block away from the superior connection + blockBumper.bumpBlockLayoutOfConnection(previousConnection, awayFromConnection: superior) + } + } + } + + private func didChangeTarget(forConnection connection: Connection, oldTarget: Connection?) + { + do { + try updateLayoutTree(forConnection: connection, oldTarget: oldTarget) + } catch let error as NSError { + bky_assertionFailure("Could not update layout tree for connection: \(error)") + } + } + + private func didChangeShadow(forConnection connection: Connection, oldShadow: Connection?) + { + do { + if connection.shadowConnected && !connection.connected { + // There's a new shadow block for the connection and it is not connected to anything. + // Add the shadow block layout tree. + try addShadowBlockLayoutTree(forConnection: connection) + } else if !connection.shadowConnected { + // There's no shadow block for the connection. Remove the shadow block layout tree. + try removeShadowBlockLayoutTree(forShadowBlock: oldShadow?.sourceBlock) + } + } catch let error as NSError { + bky_assertionFailure("Could not update shadow block layout tree for connection: \(error)") + } + } + + /** + Adds shadow blocks for a given connection to the layout tree. If the shadow blocks already exist + or if no shadow blocks are connected to the given connection, nothing happens. + + - Note: This method only updates the layout tree if the given connection is of type + `.NextStatement` or `.InputValue`. Otherwise, this method does nothing. + + - Parameter connection: The connection that should have its shadow blocks added to the layout + tree + */ + private func addShadowBlockLayoutTree(forConnection connection: Connection?) throws { + guard let aConnection = connection, + shadowBlock = aConnection.shadowBlock + where (aConnection.type == .NextStatement || aConnection.type == .InputValue) && + shadowBlock.layout == nil && !aConnection.connected else + { + // Only next/input connectors are responsible for updating the shadow block group + // layout hierarchy, not previous/output connectors. + return + } + + // Nothing is connected to aConnection. Re-create the shadow block hierarchy since it doesn't + // exist. + let shadowBlockGroupLayout = + try layoutBuilder.layoutFactory.layoutForBlockGroupLayout(engine: workspaceLayout.engine) + try layoutBuilder.buildLayoutTreeForBlockGroupLayout(shadowBlockGroupLayout, block: shadowBlock) + let shadowBlockLayouts = shadowBlockGroupLayout.blockLayouts + + // Add shadow block layouts to proper block group + if let blockGroupLayout = + (aConnection.sourceInput?.layout?.blockGroupLayout ?? // For input values or statements + aConnection.sourceBlock.layout?.parentBlockGroupLayout) // For a block's next statement + { + Layout.doNotAnimate { + blockGroupLayout.appendBlockLayouts(shadowBlockLayouts, updateLayout: true) + blockGroupLayout.performLayout(includeChildren: true) + blockGroupLayout.refreshViewPositionsForTree() + } + + blockGroupLayout.updateLayoutUpTree() + } + + // Update connection tracking + let allBlockLayouts = shadowBlockLayouts.flatMap { + $0.flattenedLayoutTree(ofType: BlockLayout.self) + } + for blockLayout in allBlockLayouts { + trackConnections(forBlockLayout: blockLayout) + } + + if allBlockLayouts.count > 0 { + workspaceLayout.sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) + } + } + + /** + Removes a shadow block layout tree from its parent layout, starting from a given shadow block. + + - Parameter shadowBlock: The shadow block + */ + private func removeShadowBlockLayoutTree(forShadowBlock shadowBlock: Block?) throws { + guard let shadowBlockLayout = shadowBlock?.layout, + shadowBlockLayoutParent = shadowBlockLayout.parentBlockGroupLayout + where (shadowBlock?.shadow ?? false) else + { + // There is no shadow block layout for this block. + return + } + + // Remove all layouts connected to this shadow block layout + let removedLayouts = shadowBlockLayoutParent + .removeAllStartingFromBlockLayout(shadowBlockLayout, updateLayout: false) + .flatMap { $0.flattenedLayoutTree(ofType: BlockLayout.self) } + + for removedLayout in removedLayouts { + // Set the delegate of the block to nil (effectively removing its BlockLayout) + removedLayout.block.delegate = nil + // Untrack connections for the layout + untrackConnections(forBlockLayout: removedLayout) + } + + if removedLayouts.count > 0 { + workspaceLayout.sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) + } + } + + /** + Whenever a connection has been changed for a block in the workspace, this method is called to + ensure that the layout tree is properly kept in sync to reflect this change. + + - Note: This method only updates the layout tree if the given connection is of type + `.PreviousStatement` or `.OutputValue`. Otherwise, this method does nothing. + + - Parameter connection: The connection that changed + - Parameter oldTarget: The previous value of `connection.targetConnection` + */ + private func updateLayoutTree(forConnection connection: Connection, oldTarget: Connection?) throws + { + // TODO:(#29) Optimize re-rendering all layouts affected by this method + + guard connection.type == .PreviousStatement || connection.type == .OutputValue else { + // Only previous/output connectors are responsible for updating the block group + // layout hierarchy, not next/input connectors. + return + } + + // Check that there are layouts for both the source and target blocks of this connection + guard let sourceBlock = connection.sourceBlock, + let sourceBlockLayout = sourceBlock.layout + where connection.targetConnection?.sourceInput == nil || + connection.targetConnection?.sourceInput?.layout != nil || + connection.targetConnection?.sourceBlock == nil || + connection.targetConnection?.sourceBlock?.layout != nil + else + { + throw BlocklyError(.IllegalState, "Can't connect a block without a layout. ") + } + + // Check that this layout is connected to a block group layout + guard sourceBlock.layout?.parentBlockGroupLayout != nil else { + throw BlocklyError(.IllegalState, + "Block layout is not connected to a parent block group layout. ") + } + + let workspace = workspaceLayout.workspace + + guard (connection.targetBlock == nil || workspace.containsBlock(connection.targetBlock!)) && + workspace.containsBlock(sourceBlock) else + { + throw BlocklyError(.IllegalState, "Can't connect blocks from different workspaces") + } + + // Keep a reference to the old parent block group layout, in case we need to clean it up later + // (if it becomes empty). + let oldParentLayout = sourceBlockLayout.parentBlockGroupLayout + + if let targetConnection = connection.targetConnection { + // `targetConnection` is connected to something now. + // Remove its shadow block layout tree (if it exists). + try removeShadowBlockLayoutTree(forShadowBlock: targetConnection.shadowBlock) + + // Move `sourceBlockLayout` and its followers to a new block group layout + if let targetInputLayout = targetConnection.sourceInput?.layout { + // Move them to target input's block group layout + targetInputLayout.blockGroupLayout + .claimBlockLayoutAndFollowers(sourceBlockLayout, updateLayouts: true) + } else if let targetBlockLayout = targetConnection.sourceBlock.layout { + // Move them to the target block's group layout + targetBlockLayout.parentBlockGroupLayout? + .claimBlockLayoutAndFollowers(sourceBlockLayout, updateLayouts: true) + } + } else { + // The connection is no longer connected to anything. + + // Block was disconnected and added to the workspace level. + // Create a new block group layout and set its `relativePosition` to the current absolute + // position of the block that was disconnected + let layoutFactory = layoutBuilder.layoutFactory + let blockGroupLayout = + try layoutFactory.layoutForBlockGroupLayout(engine: workspaceLayout.engine) + blockGroupLayout.relativePosition = sourceBlockLayout.absolutePosition + + Layout.doNotAnimate { + // Add this new block group layout to the workspace level + self.workspaceLayout.appendBlockGroupLayout(blockGroupLayout, updateLayout: true) + self.workspaceLayout.bringBlockGroupLayoutToFront(blockGroupLayout) + } + + blockGroupLayout.claimBlockLayoutAndFollowers(sourceBlockLayout, updateLayouts: true) + } + + // Re-create the shadow block layout tree for the previous connection target (if it has one). + try addShadowBlockLayoutTree(forConnection: oldTarget) + + // If the previous block group layout parent of `sourceBlockLayout` is now empty and is at the + // the top-level of the workspace, remove it + if let emptyBlockGroupLayout = oldParentLayout + where emptyBlockGroupLayout.blockLayouts.count == 0 && + emptyBlockGroupLayout.parentLayout == workspaceLayout + { + Layout.doNotAnimate { + // Remove this block's old parent group layout from the workspace level + self.workspaceLayout.removeBlockGroupLayout(emptyBlockGroupLayout, updateLayout: true) + } + } + + Layout.doNotAnimate { + self.workspaceLayout.updateCanvasSize() + } + } + + /** + Tracks connections for a given block layout under `self.connectionManager`. + If `self.connectionManager is nil, nothing happens. + + - Parameter blockLayout: The `BlockLayout` whose connections should be tracked. + */ + private func trackConnections(forBlockLayout blockLayout: BlockLayout) { + guard let connectionManager = self.connectionManager + where blockLayout.visible else + { + // Only track connections for visible block layouts + return + } + + // Track positional changes for each connection in the connection manager + for connection in blockLayout.block.directConnections { + connectionManager.trackConnection(connection) + } + } + + /** + Untracks connections for a given block layout under `self.connectionManager`. + If `self.connectionManager is nil, nothing happens. + + - Parameter blockLayout: The `BlockLayout` whose connections should be untracked. + */ + private func untrackConnections(forBlockLayout blockLayout: BlockLayout) { + guard let connectionManager = self.connectionManager else { + return + } + + // Untrack positional changes for each connection in the connection manager + for connection in blockLayout.block.directConnections { + connectionManager.untrackConnection(connection) + } + } +} + +// MARK: - WorkspaceListener implementation + +extension WorkspaceLayoutCoordinator: WorkspaceListener { + public func workspace(workspace: Workspace, didAddBlock block: Block) { + if !block.topLevel { + // We only need to create layout trees for top level blocks + return + } + + do { + // Create the layout tree for this newly added block + if let blockGroupLayout = + try layoutBuilder.buildLayoutTreeForTopLevelBlock(block, workspaceLayout: workspaceLayout) + { + // Perform a layout for the tree + blockGroupLayout.updateLayoutDownTree() + + // Track connections of all new block layouts that were added + for blockLayout in blockGroupLayout.flattenedLayoutTree(ofType: BlockLayout.self) { + trackConnections(forBlockLayout: blockLayout) + } + + // Update the content size + workspaceLayout.updateCanvasSize() + + // Schedule change event for an added block layout + workspaceLayout.sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) + } + } catch let error as NSError { + bky_assertionFailure("Could not create the layout tree for block: \(error)") + } + } + + public func workspace(workspace: Workspace, willRemoveBlock block: Block) { + if !block.topLevel { + // We only need to remove layout trees for top-level blocks + return + } + + if let blockGroupLayout = block.layout?.parentBlockGroupLayout { + // Untrack connections for all block layouts that will be removed + for blockLayout in blockGroupLayout.flattenedLayoutTree(ofType: BlockLayout.self) { + untrackConnections(forBlockLayout: blockLayout) + } + + workspaceLayout.removeBlockGroupLayout(blockGroupLayout) + + workspaceLayout.sendChangeEventWithFlags(WorkspaceLayout.Flag_NeedsDisplay) + } + } +} diff --git a/Blockly/Code/Model/Connection.swift b/Blockly/Code/Model/Connection.swift index 55f172df..c0e83541 100644 --- a/Blockly/Code/Model/Connection.swift +++ b/Blockly/Code/Model/Connection.swift @@ -28,28 +28,6 @@ public protocol ConnectionHighlightDelegate { func didChangeHighlightForConnection(connection: Connection) } -/** - Delegate for events that modify the `targetConnection` or `shadowConnection` of a `Connection`. - */ -@objc(BKYConnectionTargetDelegate) -public protocol ConnectionTargetDelegate { - /** - Event that is called when the target connection has changed for a given connection. - - - Parameter connection: The connection whose `targetConnection` value has changed. - - Parameter oldTarget: The previous value of `targetConnection`. - */ - func didChangeTarget(forConnection connection: Connection, oldTarget: Connection?) - - /** - Event that is called when the shadow connection has changed for a given connection. - - - Parameter connection: The connection whose `shadowConnection` value has changed. - - Parameter oldShadow: The previous value of `shadowConnection`. - */ - func didChangeShadow(forConnection connection: Connection, oldShadow: Connection?) -} - /** Delegate for position events that occur on a `Connection`. */ @@ -235,9 +213,6 @@ public final class Connection : NSObject { /// Connection position delegate public final weak var positionDelegate: ConnectionPositionDelegate? - /// Connection target delegate - public final weak var targetDelegate: ConnectionTargetDelegate? - /// Keeps track of all block uuid's that are telling this connection to be /// highlighted private var _highlights = Set() @@ -275,16 +250,9 @@ public final class Connection : NSObject { } if let newConnection = connection { - // Set targetConnection for both sides before sending out delegate events - let oldTarget1 = targetConnection - let oldTarget2 = newConnection.targetConnection + // Set targetConnection for both sides targetConnection = newConnection newConnection.targetConnection = self - - // Send delegate events - targetDelegate?.didChangeTarget(forConnection: self, oldTarget: oldTarget1) - newConnection.targetDelegate? - .didChangeTarget(forConnection: newConnection, oldTarget: oldTarget2) } } @@ -308,16 +276,9 @@ public final class Connection : NSObject { } if let newConnection = connection { - // Set shadowConnection for both sides before sending out delegate events - let oldShadow1 = shadowConnection - let oldShadow2 = newConnection.shadowConnection + // Set shadowConnection for both sides shadowConnection = newConnection newConnection.shadowConnection = self - - // Send delegate events - targetDelegate?.didChangeShadow(forConnection: self, oldShadow: oldShadow1) - newConnection.targetDelegate? - .didChangeShadow(forConnection: newConnection, oldShadow: oldShadow2) } } @@ -330,14 +291,9 @@ public final class Connection : NSObject { return } - // Set targetConnection for both sides before sending out delegate events + // Remove targetConnection for both sides targetConnection = nil oldTargetConnection.targetConnection = nil - - // Send delegate events - targetDelegate?.didChangeTarget(forConnection: self, oldTarget: oldTargetConnection) - oldTargetConnection.targetDelegate? - .didChangeTarget(forConnection: oldTargetConnection, oldTarget: self) } /** @@ -349,14 +305,9 @@ public final class Connection : NSObject { return } - // Set shadowConnection for both sides before sending out delegate events + // Remove shadowConnection for both sides shadowConnection = nil oldShadowConnection.shadowConnection = nil - - // Send delegate events - targetDelegate?.didChangeShadow(forConnection: self, oldShadow: oldShadowConnection) - oldShadowConnection.targetDelegate? - .didChangeShadow(forConnection: oldShadowConnection, oldShadow: self) } /** diff --git a/Blockly/Code/Model/Workspace.swift b/Blockly/Code/Model/Workspace.swift index 4018ae73..b85c2071 100644 --- a/Blockly/Code/Model/Workspace.swift +++ b/Blockly/Code/Model/Workspace.swift @@ -45,10 +45,10 @@ public func WorkspaceEdgeInsetsMake( } /** - Protocol for events that occur on a `Workspace` instance. + Listener protocol for events that occur on a `Workspace` instance. */ -@objc(BKYWorkspaceDelegate) -public protocol WorkspaceDelegate: class { +@objc(BKYWorkspaceListener) +public protocol WorkspaceListener: class { /** Event that is called when a block has been added to a workspace. @@ -103,13 +103,11 @@ public class Workspace : NSObject { } } - /// The delegate for events that occur in this workspace - public weak var delegate: WorkspaceDelegate? + /// The listener for events that occur in this workspace + public var listeners = WeakSet() - /// Convenience property for accessing `self.delegate` as a `WorkspaceLayout` - public var layout: WorkspaceLayout? { - return self.delegate as? WorkspaceLayout - } + /// The layout associated with this workspace + public weak var layout: WorkspaceLayout? /// Manager responsible for keeping track of all variable names under this workspace public var variableNameManager: NameManager? = NameManager() { @@ -196,22 +194,25 @@ public class Workspace : NSObject { // Notify delegate for each block addition, now that all of them have been added to the // workspace for block in newBlocks { - delegate?.workspace(self, didAddBlock: block) + listeners.forEach { $0.workspace(self, didAddBlock: block) } } } /** - Disconnects a given block from its previous/output connections, and removes it and all of its - connected blocks from the workspace. + Removes a given block and all of its connected child blocks from the workspace. - Parameter rootBlock: The root block to remove. - Throws: `BlocklyError`: Thrown if the tree of blocks could not be removed from the workspace. */ public func removeBlockTree(rootBlock: Block) throws { - // Disconnect this block from anything - rootBlock.previousConnection?.disconnect() - rootBlock.outputConnection?.disconnect() + if (rootBlock.previousConnection?.connected ?? false) || + (rootBlock.outputConnection?.connected ?? false) + { + throw BlocklyError(.IllegalOperation, + "The root block must be disconnected from its previous and/or output connections prior " + + "to being removed from the workspace") + } var blocksToRemove = [Block]() @@ -219,7 +220,7 @@ public class Workspace : NSObject { for block in rootBlock.allBlocksForTree() { if containsBlock(block) { blocksToRemove.append(block) - delegate?.workspace(self, willRemoveBlock: block) + listeners.forEach { $0.workspace(self, willRemoveBlock: block) } } } diff --git a/Blockly/Code/UI/View Controllers/ToolboxCategoryListViewController.swift b/Blockly/Code/UI/View Controllers/ToolboxCategoryListViewController.swift index cb479732..b468b0c6 100644 --- a/Blockly/Code/UI/View Controllers/ToolboxCategoryListViewController.swift +++ b/Blockly/Code/UI/View Controllers/ToolboxCategoryListViewController.swift @@ -145,7 +145,7 @@ public class ToolboxCategoryListViewController: UICollectionViewController { public override func collectionView( collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return toolboxLayout?.categoryLayouts.count ?? 0 + return toolboxLayout?.categoryLayoutCoordinators.count ?? 0 } public override func collectionView(collectionView: UICollectionView, @@ -186,8 +186,8 @@ public class ToolboxCategoryListViewController: UICollectionViewController { return nil } - for i in 0 ..< toolboxLayout!.categoryLayouts.count { - if toolboxLayout!.categoryLayouts[i].workspace == category { + for i in 0 ..< toolboxLayout!.categoryLayoutCoordinators.count { + if toolboxLayout!.categoryLayoutCoordinators[i].workspaceLayout.workspace == category { return NSIndexPath(forRow: i, inSection: 0) } } @@ -195,7 +195,8 @@ public class ToolboxCategoryListViewController: UICollectionViewController { } private func categoryForIndexPath(indexPath: NSIndexPath) -> Toolbox.Category { - return toolboxLayout!.categoryLayouts[indexPath.row].workspace as! Toolbox.Category + return toolboxLayout!.categoryLayoutCoordinators[indexPath.row].workspaceLayout.workspace + as! Toolbox.Category } } diff --git a/Blockly/Code/UI/View Controllers/ToolboxCategoryViewController.swift b/Blockly/Code/UI/View Controllers/ToolboxCategoryViewController.swift index d7122fbe..92bfda1f 100644 --- a/Blockly/Code/UI/View Controllers/ToolboxCategoryViewController.swift +++ b/Blockly/Code/UI/View Controllers/ToolboxCategoryViewController.swift @@ -29,6 +29,8 @@ public class ToolboxCategoryViewController: WorkspaceViewController { // MARK: - Properties + /// The toolbox layout to display + public var toolboxLayout: ToolboxLayout? /// The current category being displayed public private(set) var category: Toolbox.Category? /// Width constraint for this view @@ -86,10 +88,15 @@ public class ToolboxCategoryViewController: WorkspaceViewController { do { // Clear the layout so all current blocks are removed - try loadWorkspaceLayout(nil) + try loadWorkspaceLayoutCoordinator(nil) // Set the new layout - try loadWorkspaceLayout(category?.layout) + if let layoutCoordinator = + toolboxLayout?.categoryLayoutCoordinators + .filter({ $0.workspaceLayout.workspace == category }).first + { + try loadWorkspaceLayoutCoordinator(layoutCoordinator) + } } catch let error as NSError { bky_assertionFailure("Could not load category: \(error)") return diff --git a/Blockly/Code/UI/View Controllers/TrashCanViewController.swift b/Blockly/Code/UI/View Controllers/TrashCanViewController.swift index 1cc0cd6f..e586789b 100644 --- a/Blockly/Code/UI/View Controllers/TrashCanViewController.swift +++ b/Blockly/Code/UI/View Controllers/TrashCanViewController.swift @@ -29,8 +29,6 @@ public class TrashCanViewController: WorkspaceViewController { /// The layout engine to use for displaying the trash can public let engine: LayoutEngine - /// The layout builder to create layout hierarchies inside the trash can - public let layoutBuilder: LayoutBuilder /// The layout direction to use for `self.workspaceLayout` public let layoutDirection: WorkspaceFlowLayout.LayoutDirection @@ -48,7 +46,6 @@ public class TrashCanViewController: WorkspaceViewController { layoutDirection: WorkspaceFlowLayout.LayoutDirection, viewFactory: ViewFactory) { self.engine = engine - self.layoutBuilder = layoutBuilder self.layoutDirection = layoutDirection super.init(viewFactory: viewFactory) @@ -57,10 +54,11 @@ public class TrashCanViewController: WorkspaceViewController { workspace.readOnly = true do { - let workspaceLayout = try WorkspaceFlowLayout( - workspace: workspace, layoutDirection: layoutDirection, engine: engine, - layoutBuilder: layoutBuilder) - try loadWorkspaceLayout(workspaceLayout) + let workspaceLayout = + WorkspaceFlowLayout(workspace: workspace, engine: engine, layoutDirection: layoutDirection) + let workspaceLayoutCoordinator = try WorkspaceLayoutCoordinator( + workspaceLayout: workspaceLayout, layoutBuilder: layoutBuilder, connectionManager: nil) + try loadWorkspaceLayoutCoordinator(workspaceLayoutCoordinator) } catch let error as NSError { bky_assertionFailure("Could not create WorkspaceFlowLayout: \(error)") } diff --git a/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift b/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift index 7362c11c..0c14c11d 100644 --- a/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift +++ b/Blockly/Code/UI/View Controllers/WorkbenchViewController.swift @@ -154,12 +154,16 @@ public class WorkbenchViewController: UIViewController { public var toolbox: Toolbox? { return _toolboxLayout?.toolbox } - /// The underlying workspace layout - private var _workspaceLayout: WorkspaceLayout? { + /// The main workspace layout coordinator + private var _workspaceLayoutCoordinator: WorkspaceLayoutCoordinator? { didSet { - _dragger.workspaceLayout = _workspaceLayout + _dragger.workspaceLayoutCoordinator = _workspaceLayoutCoordinator } } + /// The underlying workspace layout + private var _workspaceLayout: WorkspaceLayout? { + return _workspaceLayoutCoordinator?.workspaceLayout + } /// The underlying toolbox layout private var _toolboxLayout: ToolboxLayout? @@ -365,22 +369,25 @@ public class WorkbenchViewController: UIViewController { // MARK: - Public /** - Automatically creates a `WorkspaceLayout` for a given `Workspace` (using both the `self.engine` - and `self.layoutBuilder` instances) and loads it into the view controller. Also passes a - connection manager to allow for custom block validation. + Automatically creates a `WorkspaceLayout` and `WorkspaceLayoutCoordinator` for a given workspace + (using both the `self.engine` and `self.layoutBuilder` instances). The workspace is then + rendered into the view controller. - Parameter workspace: The `Workspace` to load - - Parameter withConnectionManager: The (custom) ConnectionManager to set on the WorkspaceLayout + - Parameter connectionManager: A `ConnectionManager` to track connections in the workspace. + If none is specified, a default one is automatically created. - Throws: `BlocklyError`: Thrown if an associated `WorkspaceLayout` could not be created for the workspace. */ - public func loadWorkspace(workspace: Workspace, withConnectionManager: ConnectionManager? = nil) + public func loadWorkspace(workspace: Workspace, connectionManager: ConnectionManager? = nil) throws { // Create a layout for the workspace, which is required for viewing the workspace - let workspaceLayout = - try WorkspaceLayout(workspace: workspace, engine: engine, layoutBuilder: layoutBuilder, - connectionManager: withConnectionManager) - _workspaceLayout = workspaceLayout + let workspaceLayout = WorkspaceLayout(workspace: workspace, engine: engine) + let aConnectionManager = connectionManager ?? ConnectionManager() + _workspaceLayoutCoordinator = + try WorkspaceLayoutCoordinator(workspaceLayout: workspaceLayout, + layoutBuilder: layoutBuilder, + connectionManager: aConnectionManager) refreshView() } @@ -395,8 +402,8 @@ public class WorkbenchViewController: UIViewController { */ public func loadToolbox(toolbox: Toolbox) throws { let toolboxLayout = ToolboxLayout( - toolbox: toolbox, layoutDirection: style.toolboxCategoryLayoutDirection, - engine: engine, layoutBuilder: layoutBuilder) + toolbox: toolbox, engine: engine, layoutDirection: style.toolboxCategoryLayoutDirection, + layoutBuilder: layoutBuilder) _toolboxLayout = toolboxLayout refreshView() @@ -407,9 +414,7 @@ public class WorkbenchViewController: UIViewController { */ public func refreshView() { do { - if let workspaceLayout = _workspaceLayout { - try workspaceViewController?.loadWorkspaceLayout(workspaceLayout) - } + try workspaceViewController?.loadWorkspaceLayoutCoordinator(_workspaceLayoutCoordinator) } catch let error as NSError { bky_assertionFailure("Could not load workspace layout: \(error)") } @@ -417,6 +422,8 @@ public class WorkbenchViewController: UIViewController { _toolboxCategoryListViewController?.toolboxLayout = _toolboxLayout _toolboxCategoryListViewController?.refreshView() + toolboxCategoryViewController?.toolboxLayout = _toolboxLayout + resetUIState() updateWorkspaceCapacity() } @@ -464,13 +471,11 @@ public class WorkbenchViewController: UIViewController { guard let blockLayout = blockView.blockLayout else { throw BlocklyError(.LayoutNotFound, "No layout was set for the `blockView` parameter") } - guard let workspaceLayout = _workspaceLayout else { - throw BlocklyError( - .LayoutNotFound, "No workspace layout has been set for `self._workspaceLayout`") + guard let workspaceLayoutCoordinator = _workspaceLayoutCoordinator else { + throw BlocklyError(.LayoutNotFound, + "No workspace layout coordinator has been set for `self._workspaceLayoutCoordinator`") } - let workspace = workspaceLayout.workspace - // Get the position of the block view relative to this view, and use that as // the position for the newly created block. // Note: This is done before creating a new block since adding a new block might change the @@ -479,7 +484,7 @@ public class WorkbenchViewController: UIViewController { // Create a deep copy of this block in this workspace (which will automatically create a layout // tree for the block) - let newBlock = try workspace.copyBlockTree(blockLayout.block, editable: true) + let newBlock = try workspaceLayoutCoordinator.copyBlockTree(blockLayout.block, editable: true) // Set its new workspace position newBlock.layout?.parentBlockGroupLayout?.moveToWorkspacePosition(newWorkspacePosition) @@ -765,12 +770,13 @@ extension WorkbenchViewController { let touchPosition = workspaceView.workspacePositionFromGestureTouchLocation(gesture) _dragger.startDraggingBlockLayout(newBlockView.blockLayout!, touchPosition: touchPosition) - if let trashWorkspace = _trashCanViewController.workspaceView.workspaceLayout?.workspace + if let trashWorkspace = _trashCanViewController.workspace where trashWorkspace.containsBlock(rootBlockLayout.block) { do { // Remove this block view from the trash can - try _trashCanViewController.workspace?.removeBlockTree(rootBlockLayout.block) + try _trashCanViewController.workspaceLayoutCoordinator? + .removeBlockTree(rootBlockLayout.block) } catch let error as NSError { bky_assertionFailure("Could not remove block from trash can: \(error)") return @@ -865,8 +871,9 @@ extension WorkbenchViewController { _dragger.clearGestureDataForBlockLayout(blockLayout) do { - try _trashCanViewController.workspace?.copyBlockTree(blockLayout.block, editable: true) - try _workspaceLayout?.workspace.removeBlockTree(blockLayout.block) + 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)") diff --git a/Blockly/Code/UI/View Controllers/WorkspaceViewController.swift b/Blockly/Code/UI/View Controllers/WorkspaceViewController.swift index e7143a5b..33523aad 100644 --- a/Blockly/Code/UI/View Controllers/WorkspaceViewController.swift +++ b/Blockly/Code/UI/View Controllers/WorkspaceViewController.swift @@ -44,8 +44,13 @@ public protocol WorkspaceViewControllerDelegate { @objc(BKYWorkspaceViewController) public class WorkspaceViewController: UIViewController { - /// The workspace layout this view controller operates on - public private(set) var workspaceLayout: WorkspaceLayout? + /// The workspace layout coordinator this view controller operates on + public private(set) var workspaceLayoutCoordinator: WorkspaceLayoutCoordinator? + + /// A convenience property for accessing `self.workspaceLayoutCoordinator?.workspaceLayout` + public var workspaceLayout: WorkspaceLayout? { + return workspaceLayoutCoordinator?.workspaceLayout + } /// A convenience property for `self.workspaceLayout.workspace` public var workspace: Workspace? { @@ -93,14 +98,16 @@ public class WorkspaceViewController: UIViewController { // MARK: - Public /** - Loads a `WorkspaceLayout` into `self.workspaceView`. This method automatically creates and - manages all the views required to render the workspace. + Loads the workspace associated with a workspace layout coordinator, automatically creating all views required to render the workspace. - - Parameter workspaceLayout: A `WorkspaceLayout`. + - Parameter workspaceLayoutCoordinator: A `WorkspaceLayoutCoordinator`. */ - public func loadWorkspaceLayout(workspaceLayout: WorkspaceLayout?) throws { - self.workspaceLayout = workspaceLayout + public func loadWorkspaceLayoutCoordinator( + workspaceLayoutCoordinator: WorkspaceLayoutCoordinator?) throws + { + self.workspaceLayoutCoordinator = workspaceLayoutCoordinator + let workspaceLayout = workspaceLayoutCoordinator?.workspaceLayout workspaceView.layout = workspaceLayout workspaceLayout?.updateLayoutDownTree() try _viewBuilder.buildViewTree(forWorkspaceView: workspaceView)