| // 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 | |
| import TulsiGenerator | |
| /// View controller for the editor that allows selection of BUILD files and high level options. | |
| final class ProjectEditorPackageManagerViewController: NSViewController, NewProjectViewControllerDelegate { | |
| /// Indices into the Add/Remove SegmentedControl (as built by Interface Builder). | |
| private enum SegmentedControlButtonIndex: Int { | |
| case add = 0 | |
| case remove = 1 | |
| } | |
| @IBOutlet var packageArrayController: NSArrayController! | |
| @IBOutlet weak var addRemoveSegmentedControl: NSSegmentedControl! | |
| var newProjectSheet: NewProjectViewController! = nil | |
| private var newProjectNeedsSaveAs = false | |
| @objc dynamic var numSelectedPackagePaths: Int = 0 { | |
| didSet { | |
| let enableRemoveButton = numSelectedPackagePaths > 0 | |
| addRemoveSegmentedControl.setEnabled(enableRemoveButton, | |
| forSegment: SegmentedControlButtonIndex.remove.rawValue) | |
| } | |
| } | |
| deinit { | |
| NSObject.unbind(NSBindingName(rawValue: "numSelectedPackagePaths")) | |
| } | |
| override func loadView() { | |
| ValueTransformer.setValueTransformer(PackagePathValueTransformer(), | |
| forName: NSValueTransformerName(rawValue: "PackagePathValueTransformer")) | |
| super.loadView() | |
| bind(NSBindingName(rawValue: "numSelectedPackagePaths"), | |
| to: packageArrayController, | |
| withKeyPath: "selectedObjects.@count", | |
| options: nil) | |
| } | |
| override func viewDidAppear() { | |
| super.viewDidAppear() | |
| let document = representedObject as! TulsiProjectDocument | |
| // Start the new project flow if the associated document is not linked to a project. | |
| if document.fileURL != nil || document.project != nil { return } | |
| newProjectSheet = NewProjectViewController() | |
| newProjectSheet.delegate = self | |
| // Present the NewProjectViewController as a sheet. | |
| // This is done via dispatch_async because we want it to happen after the window appearance | |
| // animation is complete. | |
| DispatchQueue.main.async(execute: { | |
| self.presentViewControllerAsSheet(self.newProjectSheet) | |
| }) | |
| } | |
| @IBAction func didClickAddRemoveSegmentedControl(_ sender: NSSegmentedCell) { | |
| // Ignore mouse up messages. | |
| if sender.selectedSegment < 0 { return } | |
| guard let button = SegmentedControlButtonIndex(rawValue: sender.selectedSegment) else { | |
| assertionFailure("Unexpected add/remove button index \(sender.selectedSegment)") | |
| return | |
| } | |
| switch button { | |
| case .add: | |
| didClickAddBUILDFile(sender) | |
| case .remove: | |
| didClickRemoveSelectedBUILDFiles(sender) | |
| } | |
| } | |
| func didClickAddBUILDFile(_ sender: AnyObject?) { | |
| guard let document = self.representedObject as? TulsiProjectDocument, | |
| let workspacePath = document.workspaceRootURL?.path else { | |
| return | |
| } | |
| let panel = FilteredOpenPanel.filteredOpenPanel() { | |
| (_: AnyObject, url: URL) -> Bool in | |
| var isDir: AnyObject? | |
| var isPackage: AnyObject? | |
| do { | |
| try (url as NSURL).getResourceValue(&isDir, forKey: URLResourceKey.isDirectoryKey) | |
| try (url as NSURL).getResourceValue(&isPackage, forKey: URLResourceKey.isPackageKey) | |
| if let isDir = isDir as? NSNumber, let isPackage = isPackage as? NSNumber, !isPackage.boolValue { | |
| if isDir.boolValue { return true } | |
| let filename = url.lastPathComponent | |
| if filename == "BUILD" || filename == "BUILD.bazel" { | |
| // Prevent anything outside of the selected workspace. | |
| return url.path.hasPrefix(workspacePath) && !document.containsBUILDFileURL(url) | |
| } | |
| } | |
| } catch _ { | |
| // Treat any exception as an invalid URL. | |
| } | |
| return false | |
| } | |
| panel.prompt = NSLocalizedString("ProjectEditor_AddBUILDFilePrompt", | |
| comment: "Label for the button used to confirm adding the selected BUILD file to the Tulsi project.") | |
| panel.canChooseDirectories = false | |
| panel.beginSheetModal(for: self.view.window!) { value in | |
| if value.rawValue == NSFileHandlingPanelOKButton { | |
| guard let URL = panel.url else { | |
| return | |
| } | |
| if !document.addBUILDFileURL(URL) { | |
| NSSound.beep() | |
| } | |
| } | |
| } | |
| } | |
| func didClickRemoveSelectedBUILDFiles(_ sender: AnyObject?) { | |
| let document = representedObject as! TulsiProjectDocument | |
| if document.hasChildConfigDocuments { | |
| let alert = NSAlert() | |
| alert.messageText = NSLocalizedString("ProjectEditor_CloseOpenedConfigDocumentsMessage", | |
| comment: "Message asking the user if they want to continue with an operation that requires that all opened TulsiGeneratorConfig documents be closed.") | |
| alert.addButton(withTitle: NSLocalizedString("ProjectEditor_CloseOpenedConfigDocumentsButtonOK", | |
| comment: "Title for a button that will proceed with an operation that requires that all opened TulsiGeneratorConfig documents be closed.")) | |
| alert.addButton(withTitle: NSLocalizedString("ProjectEditor_CloseOpenedConfigDocumentsButtonCancel", | |
| comment: "Title for a button that will cancel an operation that requires that all opened TulsiGeneratorConfig documents be closed.")) | |
| alert.beginSheetModal(for: self.view.window!) { value in | |
| if value == NSApplication.ModalResponse.alertFirstButtonReturn { | |
| document.closeChildConfigDocuments() | |
| self.didClickRemoveSelectedBUILDFiles(sender) | |
| } | |
| } | |
| return | |
| } | |
| packageArrayController.remove(atArrangedObjectIndexes: packageArrayController.selectionIndexes) | |
| let remainingObjects = packageArrayController.arrangedObjects as! [String] | |
| document.bazelPackages = remainingObjects | |
| } | |
| @IBAction func selectBazelPath(_ sender: AnyObject?) { | |
| let document = representedObject as! TulsiProjectDocument | |
| BazelSelectionPanel.beginSheetModalBazelSelectionPanelForWindow(self.view.window!, | |
| document: document) | |
| } | |
| @IBAction func didDoubleClickPackage(_ sender: NSTableView) { | |
| let clickedRow = sender.clickedRow | |
| guard clickedRow >= 0 else { return } | |
| let package = (packageArrayController.arrangedObjects as! [String])[clickedRow] | |
| let document = representedObject as! TulsiProjectDocument | |
| let buildFile = package + "/BUILD" | |
| if let url = document.workspaceRootURL?.appendingPathComponent(buildFile) { | |
| NSWorkspace.shared.open(url) | |
| } | |
| } | |
| func document(_ document: NSDocument, didSave: Bool, contextInfo: AnyObject) { | |
| if !didSave { | |
| // Nothing useful can be done if the initial save failed so close this window. | |
| self.view.window!.close() | |
| return | |
| } | |
| } | |
| // MARK: - NewProjectViewControllerDelegate | |
| func viewController(_ vc: NewProjectViewController, | |
| didCompleteWithReason reason: NewProjectViewController.CompletionReason) { | |
| defer {newProjectSheet = nil} | |
| dismissViewController(newProjectSheet) | |
| guard reason == .create else { | |
| // Nothing useful can be done if the user doesn't wish to create a new project, so close this | |
| // window. | |
| self.view.window!.close() | |
| return | |
| } | |
| let document = representedObject as! TulsiProjectDocument | |
| document.createNewProject(newProjectSheet.projectName!, | |
| workspaceFileURL: newProjectSheet.workspacePath!) | |
| newProjectNeedsSaveAs = true | |
| } | |
| } | |
| /// Transformer that converts a Bazel package path to an item displayable in the package table view | |
| /// This is primarily necessary to support BUILD files colocated with the workspace root. | |
| final class PackagePathValueTransformer : ValueTransformer { | |
| override class func transformedValueClass() -> AnyClass { | |
| return NSString.self | |
| } | |
| override class func allowsReverseTransformation() -> Bool { | |
| return false | |
| } | |
| override func transformedValue(_ value: Any?) -> Any? { | |
| guard let value = value as? String else { return nil } | |
| return "//\(value)" | |
| } | |
| } |