vChewing-macOS/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift

285 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
// UserDefRenderable AppKit SwiftUI
import AppKit
import CocoaExtension
import Foundation
import IMKUtils
public class UserDefRenderableCocoa: NSObject, Identifiable {
public let def: UserDef
public var id: String { def.rawValue }
public var optionsLocalized: [(Int, String)?]
private var optionsLocalizedAsIdentifiables: [(String, String)?] = [] // Int
public var inlineDescriptionLocalized: String?
public var hideTitle: Bool = false
public var mainViewOverride: (() -> NSView?)?
public var currentControl: NSControl?
public var tinySize: Bool = false
public init(def: UserDef) {
self.def = def
if let rawOptions = def.metaData?.options, !rawOptions.isEmpty {
var newOptions: [Int: String] = [:]
rawOptions.forEach { key, value in
newOptions[key] = value.localized
}
optionsLocalized = rawOptions.sorted(by: { $0.key < $1.key })
} else {
optionsLocalized = []
}
var objOptions = [(String, String)?]()
var intOptions = [(Int, String)?]()
checkDef: switch def {
case .kAlphanumericalKeyboardLayout:
IMKHelper.allowedAlphanumericalTISInputSources.forEach { currentTIS in
objOptions.append((currentTIS.id, currentTIS.titleLocalized))
}
optionsLocalizedAsIdentifiables = objOptions
case .kBasicKeyboardLayout:
IMKHelper.allowedBasicLayoutsAsTISInputSources.forEach { currentTIS in
guard let currentTIS = currentTIS else {
objOptions.append(nil)
return
}
objOptions.append((currentTIS.id, currentTIS.titleLocalized))
}
optionsLocalizedAsIdentifiables = objOptions
case .kKeyboardParser:
KeyboardParser.allCases.forEach { currentParser in
if [7, 100].contains(currentParser.rawValue) { intOptions.append(nil) }
intOptions.append((currentParser.rawValue, currentParser.localizedMenuName))
}
optionsLocalized = intOptions
default: break checkDef
}
super.init()
guard let metaData = def.metaData else {
inlineDescriptionLocalized = nil
return
}
var stringStack = [String]()
if let promptText = metaData.inlinePrompt?.localized, !promptText.isEmpty {
stringStack.append(promptText)
}
if let descText = metaData.description?.localized, !descText.isEmpty {
stringStack.append(descText)
}
if metaData.minimumOS > 10.9 {
var strOSReq = ""
strOSReq += String(
format: "This feature requires macOS %@ and above.".localized, arguments: ["12.0"]
)
stringStack.append(strOSReq)
}
currentControl = renderFunctionControl()
guard !stringStack.isEmpty else {
inlineDescriptionLocalized = nil
return
}
inlineDescriptionLocalized = stringStack.joined(separator: "\n")
}
}
public extension UserDefRenderableCocoa {
func render(fixWidth fixedWith: CGFloat? = nil) -> NSView? {
let result: NSStackView? = NSStackView.build(.vertical) {
renderMainLine(fixedWidth: fixedWith)
renderDescription(fixedWidth: fixedWith)
}
result?.makeSimpleConstraint(.width, relation: .equal, value: fixedWith)
return result
}
func renderDescription(fixedWidth: CGFloat? = nil) -> NSTextField? {
guard let text = inlineDescriptionLocalized else { return nil }
let textField = text.makeNSLabel(descriptive: true)
if #available(macOS 10.10, *), tinySize {
textField.controlSize = .small
textField.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
}
textField.preferredMaxLayoutWidth = fixedWidth ?? 0
if let fixedWidth = fixedWidth {
textField.makeSimpleConstraint(.width, relation: .lessThanOrEqual, value: fixedWidth)
textField.sizeToFit()
textField.makeSimpleConstraint(.height, relation: .lessThanOrEqual, value: textField.fittingSize.height)
}
return textField
}
func renderMainLine(fixedWidth: CGFloat? = nil) -> NSView? {
if let mainViewOverride = mainViewOverride {
return mainViewOverride()
}
guard let control: NSView = currentControl ?? renderFunctionControl() else { return nil }
let controlWidth = control.fittingSize.width
let textLabel: NSTextField? = {
if !hideTitle, let strTitle = def.metaData?.shortTitle {
return strTitle.makeNSLabel()
}
return nil
}()
let result = NSStackView.build(.horizontal) {
if !hideTitle, let textlabel = textLabel {
textlabel
NSView()
}
control
}
if let fixedWidth = fixedWidth, let textLabel = textLabel {
let specifiedWidth = fixedWidth - controlWidth - NSFont.systemFontSize
textLabel.preferredMaxLayoutWidth = specifiedWidth
textLabel.makeSimpleConstraint(.width, relation: .lessThanOrEqual, value: specifiedWidth)
textLabel.sizeToFit()
textLabel.makeSimpleConstraint(.height, relation: .lessThanOrEqual, value: textLabel.fittingSize.height)
}
textLabel?.sizeToFit()
return result
}
private func renderFunctionControl() -> NSControl? {
var result: NSControl? {
switch def.dataType {
case .string where def == .kCandidateKeys:
let comboBox = NSComboBox()
comboBox.makeSimpleConstraint(.width, relation: .equal, value: 128)
comboBox.font = NSFont.systemFont(ofSize: 12)
comboBox.intercellSpacing = NSSize(width: 0.0, height: 10.0)
comboBox.addItems(withObjectValues: CandidateKey.suggestions)
comboBox.bind(
.value,
to: NSUserDefaultsController.shared,
withKeyPath: "values.\(def.rawValue)"
)
return comboBox
case .bool where optionsLocalized.isEmpty:
let checkBox: NSControl
if #unavailable(macOS 10.15) {
checkBox = NSButton()
(checkBox as? NSButton)?.setButtonType(.switch)
(checkBox as? NSButton)?.title = ""
} else {
checkBox = NSSwitch()
checkBox.controlSize = .mini
}
checkBox.bind(
.value,
to: NSUserDefaultsController.shared,
withKeyPath: "values.\(def.rawValue)",
options: [.continuouslyUpdatesValue: true]
)
// 滿
checkDef: switch def {
case .kAlwaysExpandCandidateWindow:
checkBox.bind(
.enabled,
to: NSUserDefaultsController.shared,
withKeyPath: "values.\(UserDef.kCandidateWindowShowOnlyOneLine.rawValue)",
options: [
.valueTransformerName: NSValueTransformerName.negateBooleanTransformerName,
]
)
case .kUseDynamicCandidateWindowOrigin:
checkBox.bind(
.enabled,
to: NSUserDefaultsController.shared,
withKeyPath: "values.\(UserDef.kUseRearCursorMode.rawValue)",
options: [
.valueTransformerName: NSValueTransformerName.negateBooleanTransformerName,
]
)
default: break checkDef
}
//
return checkBox
case .integer, .double,
.bool where !optionsLocalized.isEmpty,
.string where !optionsLocalized.isEmpty,
.string where !optionsLocalizedAsIdentifiables.isEmpty:
let dropMenu: NSMenu = .init()
let btnPopup = NSPopUpButton()
var itemShouldBeChosen: NSMenuItem?
if !optionsLocalizedAsIdentifiables.isEmpty {
btnPopup.bind(
.selectedObject,
to: NSUserDefaultsController.shared,
withKeyPath: "values.\(def.rawValue)",
options: [.continuouslyUpdatesValue: true]
)
optionsLocalizedAsIdentifiables.forEach { entity in
guard let obj = entity?.0, let title = entity?.1.localized else {
dropMenu.addItem(.separator())
return
}
let newItem = NSMenuItem(title: title, action: nil, keyEquivalent: "")
newItem.representedObject = .init(obj)
if obj == UserDefaults.current.object(forKey: def.rawValue) as? String {
itemShouldBeChosen = newItem
}
dropMenu.addItem(newItem)
}
} else {
btnPopup.bind(
.selectedTag,
to: NSUserDefaultsController.shared,
withKeyPath: "values.\(def.rawValue)",
options: [.continuouslyUpdatesValue: true]
)
optionsLocalized.forEach { entity in
guard let tag = entity?.0, let title = entity?.1.localized else {
dropMenu.addItem(.separator())
return
}
let newItem = NSMenuItem(title: title, action: nil, keyEquivalent: "")
newItem.tag = tag
if tag == UserDefaults.current.integer(forKey: def.rawValue) {
itemShouldBeChosen = newItem
}
if Double(tag) == UserDefaults.current.double(forKey: def.rawValue) {
itemShouldBeChosen = newItem
}
dropMenu.addItem(newItem)
}
}
btnPopup.menu = dropMenu
btnPopup.font = NSFont.systemFont(ofSize: 12)
btnPopup.setFrameSize(btnPopup.fittingSize)
btnPopup.select(itemShouldBeChosen)
return btnPopup
case .array, .dictionary, .other: return nil
default: return nil
}
}
if #available(macOS 10.10, *), tinySize {
result?.controlSize = .small
return result?.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: Swift.max(14, result?.fittingSize.height ?? 14)) as? NSControl
}
return result?.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: Swift.max(16, result?.fittingSize.height ?? 16)) as? NSControl
}
}
// MARK: - External Extensions.
public extension UserDef {
func render(fixWidth: CGFloat? = nil, extraOps: ((inout UserDefRenderableCocoa) -> Void)? = nil) -> NSView? {
var renderable = toCocoaRenderable()
extraOps?(&renderable)
return renderable.render(fixWidth: fixWidth)
}
func toCocoaRenderable() -> UserDefRenderableCocoa {
.init(def: self)
}
}