| // Copyright 2016 The Tulsi Authors. 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 Cocoa |
| |
| |
| // Models a node in the source filter picker. |
| final class SourcePathNode: UISelectableOutlineViewNode { |
| |
| /// Whether or not this specific node is set as recursive-enabled (rather than some child marking |
| /// it as mixed state). |
| dynamic var explicitlyRecursive: Bool { |
| return recursive == NSOnState |
| } |
| |
| /// This node's recursive UI state (NSOnState/NSOffState/NSMixedState). |
| dynamic var recursive: Int { |
| get { |
| guard let entry = entry as? UISourcePath else { return NSOffState } |
| if entry.recursive { return NSOnState } |
| |
| for child in children as! [SourcePathNode] { |
| if child.recursive != NSOffState { |
| return NSMixedState |
| } |
| } |
| return NSOffState |
| } |
| |
| set { |
| // No work needs to be done for mixed state, as it does not affect the underlying values. |
| if newValue == NSMixedState { return } |
| |
| guard let entry = entry as? UISourcePath else { return } |
| let enabled = newValue == NSOnState |
| willChangeValue(forKey: "explicitlyRecursive") |
| entry.recursive = enabled |
| didChangeValue(forKey: "explicitlyRecursive") |
| |
| // If this node is newly recursive, force hasRecursiveEnabledParent, otherwise have children |
| // inherit this node's status. |
| setChildrenHaveRecursiveParent(enabled || hasRecursiveEnabledParent) |
| |
| // Notify KVO that this node's ancestors have also changed state. |
| var ancestor = parent |
| while ancestor != nil { |
| ancestor!.willChangeValue(forKey: "recursive") |
| ancestor!.didChangeValue(forKey: "recursive") |
| ancestor = ancestor!.parent |
| } |
| } |
| } |
| |
| dynamic var hasRecursiveEnabledParent: Bool = false { |
| willSet { |
| // If this node is recursive its children will still have a recursive parent and there's no |
| // need to update them. |
| if recursive == NSOnState || newValue == hasRecursiveEnabledParent { return } |
| setChildrenHaveRecursiveParent(newValue) |
| } |
| } |
| |
| // TODO(abaire): Use a custom control to override nextState: such that it's never set to mixed via user interaction. |
| func validateRecursive(_ ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws { |
| if let value = ioValue.pointee as? NSNumber { |
| if value.intValue == NSMixedState { |
| ioValue.pointee = NSNumber(value: NSOnState as Int) |
| } |
| } |
| } |
| |
| // MARK: - Private methods |
| |
| fileprivate func setChildrenHaveRecursiveParent(_ newValue: Bool) { |
| for child in children as! [SourcePathNode] { |
| child.hasRecursiveEnabledParent = newValue |
| // Children of a recursive-enabled node may not be recursive themselves (it's redundant and |
| // potentially confusing). |
| if newValue { |
| child.recursive = NSOffState |
| } |
| } |
| } |
| } |
| |
| |
| // Controller for the view allowing users to select a subset of the source files to include in the |
| // generated Xcode project. |
| final class ConfigEditorSourceFilterViewController: NSViewController, WizardSubviewProtocol { |
| dynamic var sourceFilterContentArray: [SourcePathNode] = [] |
| @IBOutlet weak var sourceFilterOutlineView: NSOutlineView! |
| |
| override func viewDidLoad() { |
| super.viewDidLoad() |
| let sourceTargetColumn = sourceFilterOutlineView.tableColumn(withIdentifier: "sourceTargets")! |
| sourceFilterOutlineView.sortDescriptors = [sourceTargetColumn.sortDescriptorPrototype!] |
| } |
| |
| // MARK: - WizardSubviewProtocol |
| |
| weak var presentingWizardViewController: ConfigEditorWizardViewController? = nil |
| |
| func wizardSubviewWillActivateMovingForward() { |
| let document = representedObject as! TulsiGeneratorConfigDocument |
| sourceFilterContentArray = [] |
| document.updateSourcePaths(populateOutlineView) |
| |
| // TODO(abaire): Set when toggling selection instead. |
| document.updateChangeCount(.changeDone) // TODO(abaire): Implement undo functionality. |
| } |
| |
| // MARK: - Private methods |
| |
| private func populateOutlineView(_ sourcePaths: [UISourcePath]) { |
| // Decompose each rule and merge into a tree of subelements. |
| let componentDelimiters = CharacterSet(charactersIn: "/:") |
| let splitSourcePaths = sourcePaths.map() { |
| $0.path.components(separatedBy: componentDelimiters) |
| } |
| |
| var recursiveNodes = [SourcePathNode]() |
| |
| let topNode = SourcePathNode(name: "") |
| for i in 0 ..< splitSourcePaths.count { |
| let label = splitSourcePaths[i] |
| var node = topNode |
| elementLoop: for element in label { |
| if element == "" { |
| continue |
| } |
| for child in node.children as! [SourcePathNode] { |
| if child.name == element { |
| node = child |
| continue elementLoop |
| } |
| } |
| let newNode = SourcePathNode(name: element) |
| node.addChild(newNode) |
| node = newNode |
| } |
| node.entry = sourcePaths[i] |
| if node.recursive == NSOnState { |
| recursiveNodes.append(node) |
| } |
| } |
| |
| // Patch up the recursive status now that the entire tree is constructed. |
| for node in recursiveNodes { |
| node.setChildrenHaveRecursiveParent(true) |
| } |
| |
| sourceFilterContentArray = topNode.children as! [SourcePathNode] |
| } |
| } |