blob: de6234b1d263330fce1c195ed878c94e5045a3da [file] [log] [blame]
// 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 option as a node suitable for display in the options editor.
class OptionsEditorNode: NSObject {
// Note: The values here are also used as storyboard identifiers for table columns.
enum OptionLevel: String {
case Target = "TargetValue"
case Project = "ProjectValue"
case Default = "DefaultValue"
}
@objc var name: String {
assertionFailure("Must be overridden by subclasses")
return "<ERROR>"
}
@objc var toolTip: String {
assertionFailure("Must be overridden by subclasses")
return ""
}
/// The display strings for this option node (assuming it's a multi-select option type).
var multiSelectItems: [String] {
assertionFailure("multiSelectItems accessed for options node that is not a multi-select type")
return []
}
/// Whether or not double clicking on this node's view should pop a multiline text editor.
var supportsMultilineEditor: Bool {
return true
}
/// Returns the most specialized option level containing a value that has been set by the user.
var mostSpecializedOptionLevel: OptionLevel {
return .Default
}
var valueType: TulsiOption.ValueType {
assertionFailure("Must be overridden by subclasses")
return .string
}
/// The value to display in the "default" column.
@objc var defaultValueDisplayItem: String {
return ""
}
/// This node's children.
@objc var children = [OptionsEditorNode]()
func editableForOptionLevel(_ level: OptionLevel) -> Bool {
assertionFailure("Must be overridden by subclasses")
return false
}
/// Returns the display item for this option at the given option level and whether or not it was
/// explicitly set or inherited from a lower option level.
func displayItemForOptionLevel(_ level: OptionLevel) -> (displayItem: String, inherited: Bool) {
assertionFailure("Must be overridden by subclasses")
return ("", false)
}
func setDisplayItem(_ displayItem: String?, forOptionLevel level: OptionLevel) {
assertionFailure("Must be overridden by subclasses")
}
/// Removes the most specialized value for this option that has been set by the user. Returns true
/// if a value was removed.
func deleteMostSpecializedValue() -> Bool {
if mostSpecializedOptionLevel == .Default { return false }
removeValueForOptionLevel(mostSpecializedOptionLevel)
return true
}
func removeValueForOptionLevel(_ level: OptionLevel) {
assertionFailure("Must be overridden by subclasses")
}
// MARK: - CustomDebugStringConvertible
override var debugDescription: String {
return "\(super.debugDescription) - \(name)"
}
}
/// A logical grouping of editor nodes.
class OptionsEditorGroupNode: OptionsEditorNode {
let key: TulsiOptionKeyGroup
@objc override var name: String {
return displayName
}
let displayName: String
@objc override var toolTip: String {
return toolTipValue
}
let toolTipValue: String
override var mostSpecializedOptionLevel: OptionLevel {
var mostSpecializedOptions = Set<OptionLevel>()
for child in children {
mostSpecializedOptions.insert(child.mostSpecializedOptionLevel)
}
let orderedOptionLevels: [OptionLevel] = [.Target, .Project]
for level in orderedOptionLevels {
if mostSpecializedOptions.contains(level) {
return level
}
}
return .Default
}
override var valueType: TulsiOption.ValueType {
return children.first!.valueType
}
override var defaultValueDisplayItem: String {
return mergedDefaultValueDisplayItem
}
var mergedDefaultValueDisplayItem: String = ""
init(key: TulsiOptionKeyGroup, displayName: String, description: String) {
self.key = key
self.displayName = displayName
self.toolTipValue = description
super.init()
}
func addChildNode(_ node: OptionsEditorNode) {
if children.isEmpty {
mergedDefaultValueDisplayItem = node.defaultValueDisplayItem
}
children.append(node)
children.sort() { $0.name < $1.name }
if node.defaultValueDisplayItem != mergedDefaultValueDisplayItem {
mergedDefaultValueDisplayItem = NSLocalizedString("OptionsEditor_MultipleValues",
comment: "String to show in the option editor for group nodes when there are multiple per-config values.")
}
}
override func editableForOptionLevel(_ level: OptionLevel) -> Bool {
// Children are expected to be symmetric with respect to what is editable.
return children[0].editableForOptionLevel(level)
}
override func displayItemForOptionLevel(_ level: OptionLevel) -> (displayItem: String, inherited: Bool) {
var valueMap = [String: Bool]()
for child in children {
let (displayItem, inherited) = child.displayItemForOptionLevel(level)
valueMap[displayItem] = inherited
}
if valueMap.count == 1 {
let pair = valueMap.first!
return (displayItem: pair.key, inherited: pair.value)
}
let displayItem = NSLocalizedString("OptionsEditor_MultipleValues",
comment: "String to show in the option editor for group nodes when there are multiple per-config values.")
// The pseudo "multiple values" item is never explicitly set by the user so it should be
// considered inherited by the system.
return (displayItem, true)
}
override func setDisplayItem(_ displayItem: String?, forOptionLevel level: OptionLevel) {
for child in children {
child.setDisplayItem(displayItem, forOptionLevel: level)
}
}
override func removeValueForOptionLevel(_ level: OptionLevel) {
for child in children {
child.removeValueForOptionLevel(level)
}
}
}
/// Models a Tulsi option with a String-based value.
class OptionsEditorStringNode: OptionsEditorNode {
let key: TulsiOptionKey
let option: TulsiOption
let model: OptionsEditorModelProtocol?
// The UIRuleEntry selected in the target picker or nil if the BUILD file is selected.
let target: UIRuleInfo?
@objc override var name: String {
return option.displayName
}
@objc override var toolTip: String {
return option.userDescription
}
override var valueType: TulsiOption.ValueType {
return option.valueType
}
override var defaultValueDisplayItem: String {
if let parentOption = model?.parentOptionForOptionKey(key) {
if let value = parentOption.commonValue ?? parentOption.defaultValue {
return displayItemForValue(value)
}
} else if let value = option.defaultValue {
return displayItemForValue(value)
}
return NSLocalizedString("OptionsEditor_NoDefault",
comment: "String to show in the options editor's 'default' column when there is no default value.")
}
override var mostSpecializedOptionLevel: OptionLevel {
if let targetLabel = target?.fullLabel, option.targetValues?[targetLabel] != nil {
return .Target
}
if option.projectValue != nil {
return .Project
}
return .Default
}
init(key: TulsiOptionKey, option: TulsiOption, model: OptionsEditorModelProtocol?, target: UIRuleInfo?) {
self.key = key
self.option = option
self.model = model
self.target = target
super.init()
}
func displayItemForValue(_ value: String?) -> String {
if value == nil { return "" }
return value!
}
func valueForDisplayItem(_ item: String?) -> String {
if item == nil { return "" }
return item!
}
override func setDisplayItem(_ displayItem: String?, forOptionLevel level: OptionLevel) {
let sanitizedValue = option.sanitizeValue(valueForDisplayItem(displayItem))
let value: String?
// If the value is the same as the currently inherited value, clear out this option level.
if sanitizedValue == (mostSpecializedValueBeneathLevel(level) ?? "") {
value = nil
} else {
value = sanitizedValue
}
switch(level) {
case .Target:
guard let targetLabel = target?.fullLabel else {
assertionFailure("Attempt to edit target option value but no target is set")
return
}
if option.targetValues == nil {
assertionFailure("Attempt to edit target option value but option does not support target specialization")
return
}
option.targetValues![targetLabel] = value
model?.updateChangeCount(.changeDone)
case .Project:
option.projectValue = value
model?.updateChangeCount(.changeDone)
default:
assertionFailure("Editor node accessed via unknown subscript \(level)")
return
}
}
override func displayItemForOptionLevel(_ level: OptionLevel) -> (displayItem: String, inherited: Bool) {
let (value, inherited) = valueForOptionLevel(level)
return (displayItemForValue(value), inherited)
}
override func removeValueForOptionLevel(_ level: OptionLevel) {
switch level {
case .Default:
return
case .Target:
_ = option.targetValues?.removeValue(forKey: target!.fullLabel)
model?.updateChangeCount(.changeDone)
case .Project:
option.projectValue = nil
model?.updateChangeCount(.changeDone)
}
}
override func editableForOptionLevel(_ level: OptionLevel) -> Bool {
if level == .Target {
return option.targetValues != nil
}
return true
}
// MARK: - Private methods
private func valueForOptionLevel(_ level: OptionLevel) -> (value: String?, inherited: Bool) {
if level == .Target,
let targetLabel = target?.fullLabel,
let value = option.valueForTarget(targetLabel, inherit: false) {
return (value, false)
}
if level == .Target || level == .Project,
let value = option.projectValue {
return (value, level != .Project)
}
return (option.defaultValue, true)
}
private func mostSpecializedValueBeneathLevel(_ level: OptionLevel) -> String? {
if level == .Target, let value = option.projectValue {
return value
}
return option.defaultValue
}
}
/// An editor node that provides multiple string options.
class OptionsEditorConstrainedStringNode: OptionsEditorStringNode {
override var supportsMultilineEditor: Bool {
return false
}
override var multiSelectItems: [String] {
if case .stringEnum(let values) = valueType {
return Array(values)
}
return []
}
}
/// An editor node that provides multiple boolean options and maps between display strings and
/// serialization strings.
class OptionsEditorBooleanNode: OptionsEditorStringNode {
static let trueDisplayString =
NSLocalizedString("OptionsEditor_TrueValue",
comment: "Value to show when a boolean option is 'true'. This should match Xcode's localization.")
static let falseDisplayString =
NSLocalizedString("OptionsEditor_FalseValue",
comment: "Value to show when a boolean option is 'false'. This should match Xcode's localization.")
// The string display values used by pop up menus for boolean options.
static let booleanOptionValues = [
OptionsEditorBooleanNode.trueDisplayString,
OptionsEditorBooleanNode.falseDisplayString
]
override var supportsMultilineEditor: Bool {
return false
}
override var multiSelectItems: [String] {
return OptionsEditorBooleanNode.booleanOptionValues
}
override func displayItemForValue(_ value: String?) -> String {
if value == TulsiOption.BooleanTrueValue {
return OptionsEditorBooleanNode.booleanOptionValues[0]
}
return OptionsEditorBooleanNode.booleanOptionValues[1]
}
override func valueForDisplayItem(_ item: String?) -> String {
assert(item != nil, "Display item for boolean option node unexpectedly nil")
if item == nil { return TulsiOption.BooleanFalseValue }
switch item! {
case OptionsEditorBooleanNode.trueDisplayString:
return TulsiOption.BooleanTrueValue
case OptionsEditorBooleanNode.falseDisplayString:
return TulsiOption.BooleanFalseValue
default:
assertionFailure("Display item for boolean option set to unexpected value \(item!)")
return TulsiOption.BooleanFalseValue
}
}
}