Prevent a force unwrap nil crash by disabling the 'Generate' and other config buttons until Tulsi has finished initializing required components. PiperOrigin-RevId: 232519606
diff --git a/src/Tulsi/Base.lproj/Main.storyboard b/src/Tulsi/Base.lproj/Main.storyboard index 2539937..18230e8 100644 --- a/src/Tulsi/Base.lproj/Main.storyboard +++ b/src/Tulsi/Base.lproj/Main.storyboard
@@ -434,17 +434,12 @@ </segmentedControl> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TIj-CZ-t8e"> <rect key="frame" x="640" y="13" width="96" height="32"/> - <buttonCell key="cell" type="push" title="Generate" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Bpc-vd-bqs"> + <buttonCell key="cell" type="push" title="Generate" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Bpc-vd-bqs"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> </buttonCell> <connections> <action selector="doGenerate:" target="Pgt-4s-lhO" id="F2L-0G-7Eb"/> - <binding destination="EPJ-qo-bmN" name="enabled" keyPath="selectionIndexes.count" id="3si-Fr-iw5"> - <dictionary key="options"> - <string key="NSValueTransformerName">IsOneValueTransformer</string> - </dictionary> - </binding> </connections> </button> </subviews> @@ -462,6 +457,7 @@ <connections> <outlet property="addRemoveSegmentedControl" destination="Igh-yT-sbk" id="TWE-xZ-kHk"/> <outlet property="configArrayController" destination="EPJ-qo-bmN" id="mIf-bC-JqH"/> + <outlet property="generateButton" destination="TIj-CZ-t8e" id="r34-ll-ZLf"/> </connections> </viewController> <customObject id="Yqh-DQ-phg" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
diff --git a/src/Tulsi/ProjectEditorConfigManagerViewController.swift b/src/Tulsi/ProjectEditorConfigManagerViewController.swift index fbafa10..45059ec 100644 --- a/src/Tulsi/ProjectEditorConfigManagerViewController.swift +++ b/src/Tulsi/ProjectEditorConfigManagerViewController.swift
@@ -32,21 +32,30 @@ @IBOutlet var configArrayController: NSArrayController! @IBOutlet weak var addRemoveSegmentedControl: NSSegmentedControl! + @IBOutlet var generateButton: NSButton! @objc dynamic var numBazelPackages: Int = 0 { didSet { - let enableAddButton = numBazelPackages > 0 + let enableAddButton = numBazelPackages > 0 && infoExtractorInitialized addRemoveSegmentedControl.setEnabled(enableAddButton, forSegment: SegmentedControlButtonIndex.add.rawValue) } } + // Whether or not the Tulsi project document is still initializing components required for + // generation. + @objc dynamic var infoExtractorInitialized: Bool = false { + didSet { + updateButtonsState() + Thread.doOnMainQueue() { + self.generateButton.title = self.infoExtractorInitialized ? "Generate" : "Initializing..." + } + } + } + @objc dynamic var numSelectedConfigs: Int = 0 { didSet { - addRemoveSegmentedControl.setEnabled(numSelectedConfigs > 0, - forSegment: SegmentedControlButtonIndex.remove.rawValue) - addRemoveSegmentedControl.setEnabled(numSelectedConfigs == 1, - forSegment: SegmentedControlButtonIndex.action.rawValue) + updateButtonsState() } } @@ -57,11 +66,16 @@ to: concreteRepresentedObject, withKeyPath: "bazelPackages.@count", options: nil) + bind(NSBindingName(rawValue: "infoExtractorInitialized"), + to: concreteRepresentedObject, + withKeyPath: "infoExtractorInitialized", + options: nil) } } } deinit { + NSObject.unbind(NSBindingName(rawValue: "infoExtractorInitialized")) NSObject.unbind(NSBindingName(rawValue: "numBazelPackages")) NSObject.unbind(NSBindingName(rawValue: "numSelectedConfigs")) } @@ -76,6 +90,22 @@ options: nil) } + // Toggle the state of the buttons depending on the current selection as well as if any required + // components are still being initialized. + func updateButtonsState() { + Thread.doOnMainQueue() { + let numSelectedConfigs = self.numSelectedConfigs + let infoExtractorInitialized = self.infoExtractorInitialized + self.addRemoveSegmentedControl.setEnabled(self.numBazelPackages > 0 && infoExtractorInitialized, + forSegment: SegmentedControlButtonIndex.add.rawValue) + self.addRemoveSegmentedControl.setEnabled(numSelectedConfigs > 0 && infoExtractorInitialized, + forSegment: SegmentedControlButtonIndex.remove.rawValue) + self.addRemoveSegmentedControl.setEnabled(numSelectedConfigs == 1 && infoExtractorInitialized, + forSegment: SegmentedControlButtonIndex.action.rawValue) + self.generateButton.isEnabled = (numSelectedConfigs == 1 && infoExtractorInitialized) + } + } + @IBAction func didClickAddRemoveSegmentedControl(_ sender: NSSegmentedCell) { // Ignore mouse up messages. if sender.selectedSegment < 0 { return }
diff --git a/src/Tulsi/TulsiProjectDocument.swift b/src/Tulsi/TulsiProjectDocument.swift index 94f6774..b13f75d 100644 --- a/src/Tulsi/TulsiProjectDocument.swift +++ b/src/Tulsi/TulsiProjectDocument.swift
@@ -124,7 +124,14 @@ return fileURL?.appendingPathComponent(TulsiProjectDocument.ProjectConfigsSubpath) } - var infoExtractor: TulsiProjectInfoExtractor! = nil + /// Whether or not the document has finished initializing the info extractor. + @objc dynamic var infoExtractorInitialized: Bool = false + + var infoExtractor: TulsiProjectInfoExtractor! = nil { + didSet { + infoExtractorInitialized = (infoExtractor != nil) + } + } private var logEventObserver: NSObjectProtocol! = nil /// Array of user-facing messages, generally output by the Tulsi generator.