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 {
|
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 {
|
@discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = title
|
alert.messageText = title
|
||||||
if let text = text { alert.informativeText = text }
|
if let text = text { alert.informativeText = text }
|
||||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||||
var result: NSApplication.ModalResponse = .alertFirstButtonReturn
|
var result: NSApplication.ModalResponse = .alertFirstButtonReturn
|
||||||
|
guard let self = self else { return alert.runModal() }
|
||||||
alert.beginSheetModal(for: self) { theResponce in
|
alert.beginSheetModal(for: self) { theResponce in
|
||||||
result = theResponce
|
result = theResponce
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ public enum UserDef: String, CaseIterable, Identifiable {
|
||||||
public struct MetaData {
|
public struct MetaData {
|
||||||
public var userDef: UserDef
|
public var userDef: UserDef
|
||||||
public var shortTitle: String?
|
public var shortTitle: String?
|
||||||
public var control: AnyObject?
|
|
||||||
public var prompt: String?
|
public var prompt: String?
|
||||||
public var inlinePrompt: String?
|
public var inlinePrompt: String?
|
||||||
public var popupPrompt: 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