| // 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 | |
| /// Models a Tulsi generator action whose progress should be monitored. | |
| class ProgressItem: NSObject { | |
| @objc dynamic let label: String | |
| @objc dynamic let maxValue: Int | |
| @objc dynamic var value: Int | |
| @objc dynamic var indeterminate: Bool | |
| init(notifier: AnyObject?, values: [AnyHashable: Any]) { | |
| let taskName = values[ProgressUpdatingTaskName] as! String | |
| label = NSLocalizedString(taskName, | |
| value: taskName, | |
| comment: "User friendly version of \(taskName)") | |
| maxValue = values[ProgressUpdatingTaskMaxValue] as! Int | |
| value = 0 | |
| indeterminate = values[ProgressUpdatingTaskStartIndeterminate] as? Bool ?? false | |
| super.init() | |
| let notificationCenter = NotificationCenter.default | |
| notificationCenter.addObserver(self, | |
| selector: #selector(ProgressItem.progressUpdate(_:)), | |
| name: NSNotification.Name(rawValue: ProgressUpdatingTaskProgress), | |
| object: notifier) | |
| } | |
| deinit { | |
| NotificationCenter.default.removeObserver(self) | |
| } | |
| @objc func progressUpdate(_ notification: Notification) { | |
| indeterminate = false | |
| if let newValue = notification.userInfo?[ProgressUpdatingTaskProgressValue] as? Int { | |
| value = newValue | |
| } | |
| } | |
| } | |
| /// Handles generation of an Xcode project and displaying progress. | |
| class XcodeProjectGenerationProgressViewController: NSViewController { | |
| @objc dynamic var progressItems = [ProgressItem]() | |
| weak var outputFolderOpenPanel: NSOpenPanel? = nil | |
| var outputFolderURL: URL? = nil | |
| deinit { | |
| NotificationCenter.default.removeObserver(self) | |
| } | |
| override func viewDidLoad() { | |
| super.viewDidLoad() | |
| let notificationCenter = NotificationCenter.default | |
| notificationCenter.addObserver(self, | |
| selector: #selector(XcodeProjectGenerationProgressViewController.progressUpdatingTaskDidStart(_:)), | |
| name: NSNotification.Name(rawValue: ProgressUpdatingTaskDidStart), | |
| object: nil) | |
| } | |
| // Extracts source paths for selected source rules, generates a PBXProject and opens it. | |
| func generateProjectForConfigName(_ name: String, completionHandler: @escaping (URL?) -> Void) { | |
| assert(view.window != nil, "Must not be called until after the view controller is presented.") | |
| if outputFolderURL == nil { | |
| showOutputFolderPicker() { (url: URL?) in | |
| guard let url = url else { | |
| completionHandler(nil) | |
| return | |
| } | |
| self.outputFolderURL = url | |
| self.generateProjectForConfigName(name, completionHandler: completionHandler) | |
| } | |
| return | |
| } | |
| generateXcodeProjectForConfigName(name) { (projectURL: URL?) in | |
| completionHandler(projectURL) | |
| } | |
| } | |
| @objc func progressUpdatingTaskDidStart(_ notification: Notification) { | |
| guard let values = notification.userInfo else { | |
| assertionFailure("Progress task notification received without parameters.") | |
| return | |
| } | |
| progressItems.append(ProgressItem(notifier: notification.object as AnyObject?, values: values)) | |
| } | |
| // MARK: - Private methods | |
| private func showOutputFolderPicker(_ completionHandler: @escaping (URL?) -> Void) { | |
| let panel = NSOpenPanel() | |
| panel.title = NSLocalizedString("ProjectGeneration_SelectProjectOutputFolderTitle", | |
| comment: "Title for open panel through which the user should select where to generate the Xcode project.") | |
| panel.message = NSLocalizedString("ProjectGeneration_SelectProjectOutputFolderMessage", | |
| comment: "Message to show at the top of the Xcode output folder sheet, explaining what to do.") | |
| panel.prompt = NSLocalizedString("ProjectGeneration_SelectProjectOutputFolderAndGeneratePrompt", | |
| comment: "Label for the button used to confirm the selected output folder for the generated Xcode project which will also start generating immediately.") | |
| panel.canChooseDirectories = true | |
| panel.canCreateDirectories = true | |
| panel.canChooseFiles = false | |
| panel.beginSheetModal(for: view.window!) { | |
| let url: URL? | |
| if $0 == NSApplication.ModalResponse.OK { | |
| url = panel.url | |
| } else { | |
| url = nil | |
| } | |
| completionHandler(url) | |
| } | |
| } | |
| /// Asynchronously generates an Xcode project, invoking the given completionHandler on the main | |
| /// thread with an URL to the generated project (or nil to indicate failure). | |
| private func generateXcodeProjectForConfigName(_ name: String, completionHandler: @escaping ((URL?) -> Void)) { | |
| let document = self.representedObject as! TulsiProjectDocument | |
| guard let workspaceRootURL = document.workspaceRootURL else { | |
| LogMessage.postError(NSLocalizedString("Error_BadWorkspace", | |
| comment: "General error when project does not have a valid Bazel workspace.")) | |
| Thread.doOnMainQueue() { | |
| completionHandler(nil) | |
| } | |
| return | |
| } | |
| guard let concreteOutputFolderURL = outputFolderURL else { | |
| // This should never actually happen and indicates an unexpected path through the UI. | |
| LogMessage.postError(NSLocalizedString("Error_NoOutputFolder", | |
| comment: "Error for a generation attempt without a valid target output folder")) | |
| LogMessage.displayPendingErrors() | |
| Thread.doOnMainQueue() { | |
| completionHandler(nil) | |
| } | |
| return | |
| } | |
| guard let configDocument = getConfigDocumentNamed(name) else { | |
| // Error messages have already been displayed. | |
| completionHandler(nil) | |
| return | |
| } | |
| Thread.doOnQOSUserInitiatedThread() { | |
| let url = configDocument.generateXcodeProjectInFolder(concreteOutputFolderURL, | |
| withWorkspaceRootURL: workspaceRootURL) | |
| Thread.doOnMainQueue() { | |
| completionHandler(url) | |
| } | |
| } | |
| } | |
| /// Loads a previously created config with the given name as a sparse document. | |
| private func getConfigDocumentNamed(_ name: String) -> TulsiGeneratorConfigDocument? { | |
| let document = self.representedObject as! TulsiProjectDocument | |
| let errorInfo: String | |
| do { | |
| return try document.loadSparseConfigDocumentNamed(name) | |
| } catch TulsiProjectDocument.DocumentError.noSuchConfig { | |
| errorInfo = "No URL for config named '\(name)'" | |
| } catch TulsiProjectDocument.DocumentError.configLoadFailed(let info) { | |
| errorInfo = info | |
| } catch TulsiProjectDocument.DocumentError.invalidWorkspace(let info) { | |
| errorInfo = "Invalid workspace: \(info)" | |
| } catch { | |
| errorInfo = "An unexpected exception occurred while loading config named '\(name)'" | |
| } | |
| let msg = NSLocalizedString("Error_GeneralProjectGenerationFailure", | |
| comment: "A general, critical failure during project generation.") | |
| LogMessage.postError(msg, details: errorInfo) | |
| LogMessage.displayPendingErrors() | |
| return nil | |
| } | |
| } |