Repo // + UserDefRenderableCocoa & extending AppKit.
This commit is contained in:
parent
368f9bb653
commit
b8c915dca0
|
@ -0,0 +1,396 @@
|
|||
// (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.
|
||||
|
||||
import AppKit
|
||||
import SwiftExtension
|
||||
|
||||
// MARK: - NSAlert
|
||||
|
||||
public extension NSAlert {
|
||||
func beginSheetModal(at window: NSWindow?, completionHandler handler: @escaping (NSApplication.ModalResponse) -> Void) {
|
||||
if let window = window ?? NSApp.keyWindow {
|
||||
beginSheetModal(for: window, completionHandler: handler)
|
||||
} else {
|
||||
handler(runModal())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSOpenPanel
|
||||
|
||||
public extension NSOpenPanel {
|
||||
func beginSheetModal(at window: NSWindow?, completionHandler handler: @escaping (NSApplication.ModalResponse) -> Void) {
|
||||
if let window = window ?? NSApp.keyWindow {
|
||||
beginSheetModal(for: window, completionHandler: handler)
|
||||
} else {
|
||||
handler(runModal())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSButton
|
||||
|
||||
public extension NSButton {
|
||||
convenience init(verbatim title: String, target: AnyObject?, action: Selector?) {
|
||||
self.init()
|
||||
self.title = title
|
||||
self.target = target
|
||||
self.action = action
|
||||
bezelStyle = .rounded
|
||||
}
|
||||
|
||||
convenience init(_ title: String, target: AnyObject?, action: Selector?) {
|
||||
self.init(verbatim: title.localized, target: target, action: action)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenient Constructor for NSEdgeInsets.
|
||||
|
||||
public extension NSEdgeInsets {
|
||||
static func new(all: CGFloat? = nil, top: CGFloat? = nil, bottom: CGFloat? = nil, left: CGFloat? = nil, right: CGFloat? = nil) -> NSEdgeInsets {
|
||||
NSEdgeInsets(top: top ?? all ?? 0, left: left ?? all ?? 0, bottom: bottom ?? all ?? 0, right: right ?? all ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Constrains and Box Container Modifier.
|
||||
|
||||
public extension NSView {
|
||||
@discardableResult func makeSimpleConstraint(
|
||||
_ attribute: NSLayoutConstraint.Attribute,
|
||||
relation: NSLayoutConstraint.Relation,
|
||||
value: CGFloat?
|
||||
) -> NSView {
|
||||
guard let value = value else { return self }
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
let widthConstraint = NSLayoutConstraint(
|
||||
item: self, attribute: attribute, relatedBy: relation, toItem: nil,
|
||||
attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: value
|
||||
)
|
||||
addConstraint(widthConstraint)
|
||||
return self
|
||||
}
|
||||
|
||||
func boxed(title: String = "") -> NSBox {
|
||||
let maxDimension = fittingSize
|
||||
let result = NSBox()
|
||||
result.title = title.localized
|
||||
if result.title.isEmpty {
|
||||
result.titlePosition = .noTitle
|
||||
}
|
||||
let minWidth = Swift.max(maxDimension.width + 12, result.intrinsicContentSize.width)
|
||||
let minHeight = Swift.max(maxDimension.height + result.titleRect.height + 14, result.intrinsicContentSize.height)
|
||||
result.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: minWidth)
|
||||
result.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: minHeight)
|
||||
result.contentView = self
|
||||
if let self = self as? NSStackView, self.orientation == .horizontal {
|
||||
self.spacing = 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stacks
|
||||
|
||||
public extension NSStackView {
|
||||
var requiresConstraintBasedLayout: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
static func buildSection(
|
||||
_ orientation: NSUserInterfaceLayoutOrientation = .vertical,
|
||||
width: CGFloat? = nil,
|
||||
withDividers: Bool = true,
|
||||
@ArrayBuilder<NSView?> views: () -> [NSView?]
|
||||
) -> NSStackView? {
|
||||
let viewsRendered = views().compactMap {
|
||||
// 下述註解是用來協助偵錯的。
|
||||
// $0?.wantsLayer = true
|
||||
// $0?.layer?.backgroundColor = NSColor.red.cgColor
|
||||
$0
|
||||
}
|
||||
guard !viewsRendered.isEmpty else { return nil }
|
||||
var itemWidth = width
|
||||
var splitterDelta: CGFloat = 4
|
||||
splitterDelta = withDividers ? splitterDelta : 0
|
||||
if let width = width, orientation == .horizontal, viewsRendered.count > 0 {
|
||||
itemWidth = (width - splitterDelta) / CGFloat(viewsRendered.count) - 6
|
||||
}
|
||||
func giveViews() -> [NSView?] { viewsRendered }
|
||||
let result = build(orientation, divider: withDividers, width: itemWidth, views: giveViews)?
|
||||
.withInsets(.new(all: 4))
|
||||
return result
|
||||
}
|
||||
|
||||
static func build(
|
||||
_ orientation: NSUserInterfaceLayoutOrientation,
|
||||
divider: Bool = false,
|
||||
width: CGFloat? = nil,
|
||||
height: CGFloat? = nil,
|
||||
insets: NSEdgeInsets? = nil,
|
||||
@ArrayBuilder<NSView?> views: () -> [NSView?]
|
||||
) -> NSStackView? {
|
||||
let result = views().compactMap {
|
||||
$0?
|
||||
.makeSimpleConstraint(.width, relation: .equal, value: width)
|
||||
.makeSimpleConstraint(.height, relation: .equal, value: height)
|
||||
}
|
||||
guard !result.isEmpty else { return nil }
|
||||
return result.stack(orientation, divider: divider)?.withInsets(insets)
|
||||
}
|
||||
|
||||
func withInsets(_ newValue: NSEdgeInsets?) -> NSStackView {
|
||||
edgeInsets = newValue ?? edgeInsets
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element == NSView {
|
||||
func stack(
|
||||
_ orientation: NSUserInterfaceLayoutOrientation,
|
||||
divider: Bool = false,
|
||||
insets: NSEdgeInsets? = nil
|
||||
) -> NSStackView? {
|
||||
guard !isEmpty else { return nil }
|
||||
let outerStack = NSStackView()
|
||||
if #unavailable(macOS 10.11) {
|
||||
outerStack.hasEqualSpacing = true
|
||||
} else {
|
||||
outerStack.distribution = .equalSpacing
|
||||
}
|
||||
outerStack.orientation = orientation
|
||||
|
||||
if #unavailable(macOS 10.10) {
|
||||
outerStack.spacing = Swift.max(1, outerStack.spacing) - 1
|
||||
}
|
||||
|
||||
outerStack.setHuggingPriority(.fittingSizeCompression, for: .horizontal)
|
||||
outerStack.setHuggingPriority(.fittingSizeCompression, for: .vertical)
|
||||
|
||||
forEach { subView in
|
||||
if divider, !outerStack.views.isEmpty {
|
||||
let divider = NSView()
|
||||
divider.wantsLayer = true
|
||||
divider.layer?.backgroundColor = NSColor.gray.withAlphaComponent(0.2).cgColor
|
||||
switch orientation {
|
||||
case .horizontal:
|
||||
divider.makeSimpleConstraint(.width, relation: .equal, value: 1)
|
||||
case .vertical:
|
||||
divider.makeSimpleConstraint(.height, relation: .equal, value: 1)
|
||||
@unknown default: break
|
||||
}
|
||||
divider.translatesAutoresizingMaskIntoConstraints = false
|
||||
outerStack.addView(divider, in: orientation == .horizontal ? .leading : .top)
|
||||
}
|
||||
subView.layoutSubtreeIfNeeded()
|
||||
switch orientation {
|
||||
case .horizontal:
|
||||
subView.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.height)
|
||||
subView.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.width)
|
||||
case .vertical:
|
||||
subView.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.width)
|
||||
subView.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.height)
|
||||
@unknown default: break
|
||||
}
|
||||
subView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
subView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
||||
subView.translatesAutoresizingMaskIntoConstraints = false
|
||||
outerStack.addView(subView, in: orientation == .horizontal ? .leading : .top)
|
||||
}
|
||||
|
||||
switch orientation {
|
||||
case .horizontal:
|
||||
outerStack.alignment = .centerY
|
||||
case .vertical:
|
||||
outerStack.alignment = .leading
|
||||
@unknown default: break
|
||||
}
|
||||
return outerStack.withInsets(insets)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Make NSAttributedString into Label
|
||||
|
||||
public extension NSAttributedString {
|
||||
func makeNSLabel(fixWidth: CGFloat? = nil) -> NSTextField {
|
||||
let textField = NSTextField()
|
||||
textField.attributedStringValue = self
|
||||
textField.isEditable = false
|
||||
textField.isBordered = false
|
||||
textField.backgroundColor = .clear
|
||||
if let fixWidth = fixWidth {
|
||||
textField.preferredMaxLayoutWidth = fixWidth
|
||||
}
|
||||
return textField
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Make String into Label
|
||||
|
||||
public extension String {
|
||||
func makeNSLabel(descriptive: Bool = false, localized: Bool = true, fixWidth: CGFloat? = nil) -> NSTextField {
|
||||
let rawAttributedString = NSMutableAttributedString(string: localized ? self.localized : self)
|
||||
rawAttributedString.addAttributes([.kern: 0], range: .init(location: 0, length: rawAttributedString.length))
|
||||
let textField = rawAttributedString.makeNSLabel(fixWidth: fixWidth)
|
||||
if descriptive {
|
||||
if #available(macOS 10.10, *) {
|
||||
textField.textColor = .secondaryLabelColor
|
||||
} else {
|
||||
textField.textColor = .textColor.withAlphaComponent(0.55)
|
||||
}
|
||||
textField.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
|
||||
}
|
||||
return textField
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSTabView
|
||||
|
||||
public extension NSTabView {
|
||||
struct TabPage {
|
||||
public let title: String
|
||||
public let view: NSView
|
||||
|
||||
public init?(title: String, view: NSView?) {
|
||||
self.title = title
|
||||
guard let view = view else { return nil }
|
||||
self.view = view
|
||||
}
|
||||
|
||||
public init(title: String, view: NSView) {
|
||||
self.title = title
|
||||
self.view = view
|
||||
}
|
||||
|
||||
public init?(title: String, @ArrayBuilder<NSView?> views: () -> [NSView?]) {
|
||||
self.title = title
|
||||
let viewsRendered = views()
|
||||
guard !viewsRendered.isEmpty else { return nil }
|
||||
func giveViews() -> [NSView?] { viewsRendered }
|
||||
let result = NSStackView.build(.vertical, insets: .new(all: 14, top: 0), views: giveViews)
|
||||
guard let result = result else { return nil }
|
||||
view = result
|
||||
}
|
||||
}
|
||||
|
||||
static func build(
|
||||
@ArrayBuilder<TabPage?> pages: () -> [TabPage?]
|
||||
) -> NSTabView? {
|
||||
let tabPages = pages().compactMap { $0 }
|
||||
guard !tabPages.isEmpty else { return nil }
|
||||
let finalTabView = NSTabView()
|
||||
tabPages.forEach { currentPage in
|
||||
finalTabView.addTabViewItem({
|
||||
let currentItem = NSTabViewItem(identifier: UUID())
|
||||
currentItem.label = currentPage.title.localized
|
||||
let stacked = NSStackView.build(.vertical) {
|
||||
currentPage.view
|
||||
}
|
||||
stacked?.alignment = .centerX
|
||||
currentItem.view = stacked
|
||||
return currentItem
|
||||
}())
|
||||
}
|
||||
return finalTabView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSMenu
|
||||
|
||||
public extension NSMenu {
|
||||
@discardableResult func appendItems(_ target: AnyObject? = nil, @ArrayBuilder<NSMenuItem?> items: () -> [NSMenuItem?]) -> NSMenu {
|
||||
let theItems = items()
|
||||
for currentItem in theItems {
|
||||
guard let currentItem = currentItem else { continue }
|
||||
addItem(currentItem)
|
||||
guard let target = target else { continue }
|
||||
currentItem.target = target
|
||||
currentItem.submenu?.propagateTarget(target)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func propagateTarget(_ obj: AnyObject?) -> NSMenu {
|
||||
for currentItem in items {
|
||||
currentItem.target = obj
|
||||
currentItem.submenu?.propagateTarget(obj)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
static func buildSubMenu(verbatim: String?, @ArrayBuilder<NSMenuItem?> items: () -> [NSMenuItem?]) -> NSMenuItem? {
|
||||
guard let verbatim = verbatim, !verbatim.isEmpty else { return nil }
|
||||
let newItem = NSMenu.Item(verbatim: verbatim)
|
||||
newItem?.submenu = .init(title: verbatim).appendItems(items: items)
|
||||
return newItem
|
||||
}
|
||||
|
||||
static func buildSubMenu(_ title: String?, @ArrayBuilder<NSMenuItem?> items: () -> [NSMenuItem?]) -> NSMenuItem? {
|
||||
guard let title = title?.localized, !title.isEmpty else { return nil }
|
||||
return buildSubMenu(verbatim: title, items: items)
|
||||
}
|
||||
|
||||
typealias Item = NSMenuItem
|
||||
}
|
||||
|
||||
public extension Array where Element == NSMenuItem? {
|
||||
func propagateTarget(_ obj: AnyObject?) {
|
||||
forEach { currentItem in
|
||||
guard let currentItem = currentItem else { return }
|
||||
currentItem.target = obj
|
||||
currentItem.submenu?.propagateTarget(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSMenuItem {
|
||||
convenience init?(verbatim: String?) {
|
||||
guard let verbatim = verbatim, !verbatim.isEmpty else { return nil }
|
||||
self.init(title: verbatim, action: nil, keyEquivalent: "")
|
||||
}
|
||||
|
||||
convenience init?(_ title: String?) {
|
||||
guard let title = title?.localized, !title.isEmpty else { return nil }
|
||||
self.init(verbatim: title)
|
||||
}
|
||||
|
||||
@discardableResult func hotkey(_ keyEquivalent: String, mask: NSEvent.ModifierFlags? = nil) -> NSMenuItem {
|
||||
keyEquivalentModifierMask = mask ?? keyEquivalentModifierMask
|
||||
self.keyEquivalent = keyEquivalent
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func state(_ givenState: Bool) -> NSMenuItem {
|
||||
state = givenState ? .on : .off
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func act(_ action: Selector) -> NSMenuItem {
|
||||
self.action = action
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func nulled(_ condition: Bool) -> NSMenuItem? {
|
||||
condition ? nil : self
|
||||
}
|
||||
|
||||
@discardableResult func mask(_ flags: NSEvent.ModifierFlags) -> NSMenuItem {
|
||||
keyEquivalentModifierMask = flags
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func represent(_ object: Any?) -> NSMenuItem {
|
||||
representedObject = object
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func tag(_ givenTag: Int?) -> NSMenuItem {
|
||||
guard let givenTag = givenTag else { return self }
|
||||
tag = givenTag
|
||||
return self
|
||||
}
|
||||
}
|
|
@ -48,12 +48,19 @@ public extension NSWindowController {
|
|||
}
|
||||
|
||||
public extension NSWindow {
|
||||
@discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse {
|
||||
(self as NSWindow?).callAlert(title: title, text: text)
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSWindow? {
|
||||
@discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = title
|
||||
if let text = text { alert.informativeText = text }
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||
var result: NSApplication.ModalResponse = .alertFirstButtonReturn
|
||||
guard let self = self else { return alert.runModal() }
|
||||
alert.beginSheetModal(for: self) { theResponce in
|
||||
result = theResponce
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ public enum UserDef: String, CaseIterable, Identifiable {
|
|||
public struct MetaData {
|
||||
public var userDef: UserDef
|
||||
public var shortTitle: String?
|
||||
public var control: AnyObject?
|
||||
public var prompt: String?
|
||||
public var inlinePrompt: String?
|
||||
public var popupPrompt: String?
|
|
@ -0,0 +1,275 @@
|
|||
// (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.identifier, currentTIS.vChewingLocalizedName))
|
||||
}
|
||||
optionsLocalizedAsIdentifiables = objOptions
|
||||
case .kBasicKeyboardLayout:
|
||||
IMKHelper.allowedBasicLayoutsAsTISInputSources.forEach { currentTIS in
|
||||
guard let currentTIS = currentTIS else {
|
||||
objOptions.append(nil)
|
||||
return
|
||||
}
|
||||
objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName))
|
||||
}
|
||||
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
|
||||
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 {
|
||||
textLabel?.preferredMaxLayoutWidth = fixedWidth - controlWidth
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue