Skip to content

Commit

Permalink
Support control bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
Mx-Iris committed Oct 11, 2024
1 parent 8485f16 commit 593cae8
Show file tree
Hide file tree
Showing 23 changed files with 1,238 additions and 19 deletions.
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -82,9 +83,14 @@ let package = Package(
.target(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation"
"SwiftNavigation",
"AppKitNavigationShim",
.product(name: "IdentifiedCollections", package: "swift-identified-collections")
]
),
.target(
name: "AppKitNavigationShim"
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
4 changes: 4 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ let package = Package(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation",
"AppKitNavigationShim",
]
),
.target(
name: "AppKitNavigationShim"
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppKitNavigation/AppKitAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
return try result!._rethrowGet()

case let .swiftUI(animation):
var result: Swift.Result<Result, Error>?
#if swift(>=6)
var result: Swift.Result<Result, Error>?
if #available(macOS 15, *) {
NSAnimationContext.animate(animation) {
result = Swift.Result(catching: body)
Expand Down
48 changes: 48 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSAlert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

extension NSAlert {
/// Creates and returns a alert for displaying an alert using a data description.
///
/// - Parameters:
/// - state: A data description of the alert.
/// - handler: A closure that is invoked with an action held in `state`.
public convenience init<Action>(
state: AlertState<Action>,
handler: @escaping (_ action: Action?) -> Void
) {
self.init()
self.messageText = String(state: state.title)
state.message.map { self.informativeText = String(state: $0) }

for button in state.buttons {
addButton(button, action: handler)
}
}
}

extension NSAlert {
public func addButton<Action>(
_ buttonState: ButtonState<Action>,
action handler: @escaping (_ action: Action?) -> Void = { (_: Never?) in }
) {
let button = addButton(withTitle: String(state: buttonState.label))

button.createActionProxyIfNeeded().addBindingAction { _ in
buttonState.withAction(handler)
}

if buttonState.role == .destructive, #available(macOS 11.0, *) {
button.hasDestructiveAction = true
}

if buttonState.role == .cancel {
button.keyEquivalent = "\u{1b}"
}

if #available(macOS 12, *) {
button.setAccessibilityLabel(buttonState.label.accessibilityLabel.map { String(state: $0) })
}
}
}
#endif
41 changes: 41 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSColorPanel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

extension NSColorPanel: NSTargetActionProtocol {
public var appkitNavigationTarget: AnyObject? {
set { setTarget(newValue) }
get { value(forKeyPath: "target") as? AnyObject }
}

public var appkitNavigationAction: Selector? {
set { setAction(newValue) }
get { value(forKeyPath: "action") as? Selector }
}
}

extension NSColorPanel {
/// Creates a new color panel and registers the binding against the
/// selected color.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - color: The binding to read from for the selected color, and write to when the
/// selected color is changes.
public convenience init(color: UIBinding<NSColor>) {
self.init()
bind(color: color)
}

/// Establishes a two-way connection between a binding and the color panel's selected color.
///
/// - Parameter color: The binding to read from for the selected color, and write to
/// when the selected color changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(color: UIBinding<NSColor>) -> ObserveToken {
bind(color, to: \.color)
}
}

#endif
27 changes: 27 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSColorWell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

extension NSColorWell {
/// Creates a new color well with the specified frame and registers the binding against the
/// selected color.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - color: The binding to read from for the selected color, and write to when the
/// selected color is changes.
public convenience init(frame: CGRect = .zero, color: UIBinding<NSColor>) {
self.init(frame: frame)
bind(color: color)
}

/// Establishes a two-way connection between a binding and the color well's selected color.
///
/// - Parameter color: The binding to read from for the selected color, and write to
/// when the selected color changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(color: UIBinding<NSColor>) -> ObserveToken {
bind(color, to: \.color)
}
}
#endif
43 changes: 43 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

extension NSControl: NSTargetActionProtocol {
public var appkitNavigationTarget: AnyObject? {
set { target = newValue }
get { target }
}

public var appkitNavigationAction: Selector? {
set { action = newValue }
get { action }
}
}

extension NSControl {
public convenience init(action: @escaping (Self) -> Void) {
self.init(frame: .zero)
createActionProxyIfNeeded().addAction { [weak self] _ in
guard let self else { return }
action(self)
}
}

@discardableResult
public func addAction(_ action: @escaping (NSControl) -> Void) -> UUID {
createActionProxyIfNeeded().addAction { [weak self] _ in
guard let self else { return }
action(self)
}
}

public func removeAction(for id: UUID) {
createActionProxyIfNeeded().removeAction(for: id)
}

public func removeAllActions() {
createActionProxyIfNeeded().removeAllActions()
}
}

#endif
27 changes: 27 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSDatePicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

extension NSDatePicker {
/// Creates a new date picker with the specified frame and registers the binding against the
/// selected date.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - date: The binding to read from for the selected date, and write to when the selected
/// date changes.
public convenience init(frame: CGRect = .zero, date: UIBinding<Date>) {
self.init(frame: frame)
bind(date: date)
}

/// Establishes a two-way connection between a binding and the date picker's selected date.
///
/// - Parameter date: The binding to read from for the selected date, and write to when the
/// selected date changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(date: UIBinding<Date>) -> ObserveToken {
bind(date, to: \.dateValue)
}
}
#endif
77 changes: 77 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSFontManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit
import SwiftNavigation

extension NSFontManager: NSTargetActionProtocol, @unchecked Sendable {
public var appkitNavigationTarget: AnyObject? {
set { appkitNavigationDelegate.target = newValue }
get { appkitNavigationDelegate.target }
}

public var appkitNavigationAction: Selector? {
set { appkitNavigationDelegate.action = newValue }
get { appkitNavigationDelegate.action }
}

private static let appkitNavigationDelegateKey = malloc(1)!

private var appkitNavigationDelegate: Delegate {
set {
objc_setAssociatedObject(self, Self.appkitNavigationDelegateKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
if let delegate = objc_getAssociatedObject(self, Self.appkitNavigationDelegateKey) as? Delegate {
return delegate
} else {
let delegate = Delegate()
target = delegate
self.appkitNavigationDelegate = delegate
return delegate
}
}
}

private class Delegate: NSObject, NSFontChanging {
var target: AnyObject?
var action: Selector?

func changeFont(_ sender: NSFontManager?) {
if let action {
NSApplication.shared.sendAction(action, to: target, from: sender)
}
}
}
}

@MainActor
extension NSFontManager {
/// Creates a new date picker with the specified frame and registers the binding against the
/// selected date.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - date: The binding to read from for the selected date, and write to when the selected
/// date changes.
public convenience init(font: UIBinding<NSFont>) {
self.init()
bind(font: font)
}

/// Establishes a two-way connection between a binding and the date picker's selected date.
///
/// - Parameter date: The binding to read from for the selected date, and write to when the
/// selected date changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(font: UIBinding<NSFont>) -> ObserveToken {
bind(font, to: \._selectedFont)
}

@objc private var _selectedFont: NSFont {
set { setSelectedFont(newValue, isMultiple: false) }
get { convert(.systemFont(ofSize: 0)) }
}
}

#endif
17 changes: 17 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSMenuItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

extension NSMenuItem: NSTargetActionProtocol, @unchecked Sendable {
public var appkitNavigationTarget: AnyObject? {
set { target = newValue }
get { target }
}

public var appkitNavigationAction: Selector? {
set { action = newValue }
get { action }
}
}

#endif
29 changes: 29 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSPathControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

extension NSPathControl {
/// Creates a new path control with the specified frame and registers the binding against the
/// selected url.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - date: The binding to read from for the selected url, and write to when the selected
/// url changes.
public convenience init(frame: CGRect = .zero, date: UIBinding<URL?>) {
self.init(frame: frame)
bind(url: date)
}

/// Establishes a two-way connection between a binding and the path control's selected url.
///
/// - Parameter url: The binding to read from for the selected url, and write to when the
/// selected url changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(url: UIBinding<URL?>) -> ObserveToken {
bind(url, to: \.url)
}
}

#endif
Loading

0 comments on commit 593cae8

Please sign in to comment.