SSPreferences // Update to the upstream version (Mar. 5, 2023).

This commit is contained in:
ShikiSuen 2023-03-05 23:36:48 +08:00
parent 8541c3b9e6
commit 5fa96c7f64
15 changed files with 310 additions and 351 deletions

View File

@ -1,10 +1,10 @@
// swift-tools-version:5.3 // swift-tools-version:5.5
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "SSPreferences", name: "SSPreferences",
platforms: [ platforms: [
.macOS(.v10_11), .macOS(.v10_13),
], ],
products: [ products: [
.library( .library(

View File

@ -1,6 +0,0 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
/// The namespace for this package.
public enum SSPreferences {}

View File

@ -5,9 +5,9 @@
import SwiftUI import SwiftUI
@available(macOS 10.15, *) @available(macOS 10.15, *)
public extension SSPreferences { public extension Settings {
/** /**
Function builder for `Preferences` components used in order to restrict types of child views to be of type `Section`. Function builder for `Settings` components used in order to restrict types of child views to be of type `Section`.
*/ */
@resultBuilder @resultBuilder
enum SectionBuilder { enum SectionBuilder {
@ -17,7 +17,7 @@ public extension SSPreferences {
} }
/** /**
A view which holds `Preferences.Section` views and does all the alignment magic similar to `NSGridView` from AppKit. A view which holds `Settings.Section` views and does all the alignment magic similar to `NSGridView` from AppKit.
*/ */
struct Container: View { struct Container: View {
private let sectionBuilder: () -> [Section] private let sectionBuilder: () -> [Section]
@ -26,14 +26,14 @@ public extension SSPreferences {
@State private var maximumLabelWidth = 0.0 @State private var maximumLabelWidth = 0.0
/** /**
Creates an instance of container component, which handles layout of stacked `Preferences.Section` views. Creates an instance of container component, which handles layout of stacked `Settings.Section` views.
Custom alignment requires content width to be specified beforehand. Custom alignment requires content width to be specified beforehand.
- Parameters: - Parameters:
- contentWidth: A fixed width of the container's content (excluding paddings). - contentWidth: A fixed width of the container's content (excluding paddings).
- minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label. - minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label.
- builder: A view builder that creates `Preferences.Section`'s of this container. - builder: A view builder that creates `Settings.Section`'s of this container.
*/ */
public init( public init(
contentWidth: Double, contentWidth: Double,
@ -47,21 +47,10 @@ public extension SSPreferences {
public var body: some View { public var body: some View {
let sections = sectionBuilder() let sections = sectionBuilder()
let labelWidth = max(minimumLabelWidth, maximumLabelWidth)
return VStack(alignment: .preferenceSectionLabel) { return VStack(alignment: .settingsSectionLabel) {
ForEach(0 ..< sections.count, id: \.self) { index in ForEach(0 ..< sections.count, id: \.self) { index in
if sections[index].label != nil { viewForSection(sections, index: index)
sections[index].bodyLimited(rightPaneWidth: contentWidth - labelWidth)
} else {
sections[index]
.alignmentGuide(.preferenceSectionLabel) { $0[.leading] + labelWidth }
}
if sections[index].bottomDivider, index < sections.count - 1 {
Divider()
.frame(height: 10)
.alignmentGuide(.preferenceSectionLabel) { $0[.leading] + labelWidth }
}
} }
} }
.modifier(Section.LabelWidthModifier(maximumWidth: $maximumLabelWidth)) .modifier(Section.LabelWidthModifier(maximumWidth: $maximumLabelWidth))
@ -69,17 +58,31 @@ public extension SSPreferences {
.padding(.vertical, 20) .padding(.vertical, 20)
.padding(.horizontal, 30) .padding(.horizontal, 30)
} }
@ViewBuilder
private func viewForSection(_ sections: [Section], index: Int) -> some View {
sections[index]
if index != sections.count - 1, sections[index].bottomDivider {
Divider()
// Strangely doesn't work without width being specified. Probably because of custom alignment.
.frame(width: contentWidth, height: 20)
.alignmentGuide(.settingsSectionLabel) { $0[.leading] + max(minimumLabelWidth, maximumLabelWidth) }
}
}
} }
} }
/// Extension with custom alignment guide for section title labels. /**
Extension with custom alignment guide for section title labels.
*/
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension HorizontalAlignment { extension HorizontalAlignment {
private enum PreferenceSectionLabelAlignment: AlignmentID { private enum SettingsSectionLabelAlignment: AlignmentID {
// swiftlint:disable:next no_cgfloat
static func defaultValue(in context: ViewDimensions) -> CGFloat { static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading] context[HorizontalAlignment.leading]
} }
} }
static let preferenceSectionLabel = HorizontalAlignment(PreferenceSectionLabelAlignment.self) static let settingsSectionLabel = HorizontalAlignment(SettingsSectionLabelAlignment.self)
} }

View File

@ -7,7 +7,7 @@ import Foundation
struct Localization { struct Localization {
enum Identifier { enum Identifier {
case preferences case preferences
case preferencesEllipsized case settings
} }
private static let localizedStrings: [Identifier: [String: String]] = [ private static let localizedStrings: [Identifier: [String: String]] = [
@ -52,46 +52,46 @@ struct Localization {
"zh-HK": "偏好設定", "zh-HK": "偏好設定",
"zh-TW": "偏好設定", "zh-TW": "偏好設定",
], ],
.preferencesEllipsized: [ .settings: [
"ar": "تفضيلات…", "ar": "الإعدادات",
"ca": "Preferències…", "ca": "Configuració",
"cs": "Předvolby…", "cs": "Nastavení",
"da": "Indstillinger", "da": "Indstillinger",
"de": "Einstellungen", "de": "Einstellungen",
"el": "Προτιμήσεις…", "el": "Ρυθμίσεις",
"en": "Preferences…", "en": "Settings",
"en-AU": "Preferences…", "en-AU": "Settings",
"en-GB": "Preferences…", "en-GB": "Settings",
"es": "Preferencias…", "es": "Ajustes",
"es-419": "Preferencias…", "es-419": "Ajustes",
"fi": "Asetukset", "fi": "Asetukset",
"fr": "Préférences…", "fr": "Réglages",
"fr-CA": "Préférences…", "fr-CA": "Réglages",
"he": "העדפות…", "he": "הגדרות",
"hi": "प्राथमिकता…", "hi": "समायोजन",
"hr": "Postavke", "hr": "Postavke",
"hu": "Beállítások", "hu": "Beállítások",
"id": "Preferensi…", "id": "Pengaturan",
"it": "Preferenze…", "it": "Impostazioni",
"ja": "環境設定", "ja": "設定",
"ko": "환경설정...", "ko": "설정",
"ms": "Keutamaan…", "ms": "Tetapan",
"nl": "Voorkeuren…", "nl": "Instellingen",
"no": "Valg…", "no": "Innstillinger",
"pl": "Preferencje…", "pl": "Ustawienia",
"pt": "Preferências…", "pt": "Ajustes",
"pt-PT": "Preferências…", "pt-PT": "Definições",
"ro": "Preferințe…", "ro": "Configurări",
"ru": "Настройки", "ru": "Настройки",
"sk": "Nastavenia", "sk": "Nastavenia",
"sv": "Inställningar", "sv": "Inställningar",
"th": "การตั้งค่า…", "th": "ค่าติดตั้ง",
"tr": "Tercihler…", "tr": "Ayarlar",
"uk": "Параметри", "uk": "Параметри",
"vi": "Tùy chọn…", "vi": "Cài đặt",
"zh-CN": "偏好设置", "zh-CN": "设置",
"zh-HK": "偏好設定", "zh-HK": "設定",
"zh-TW": "偏好設定", "zh-TW": "設定",
], ],
] ]
@ -104,16 +104,16 @@ struct Localization {
*/ */
static subscript(identifier: Identifier) -> String { static subscript(identifier: Identifier) -> String {
// Force-unwrapped since all of the involved code is under our control. // Force-unwrapped since all of the involved code is under our control.
let localizedDict = Self.localizedStrings[identifier]! let localizedDict = Localization.localizedStrings[identifier]!
let defaultLocalizedString = localizedDict["en"]! let defaultLocalizedString = localizedDict["en"]!
// Iterate through all user-preferred languages until we find one that has a valid language code. // Iterate through all user-preferred languages until we find one that has a valid language code.
let preferredLocale = let preferredLocale = Locale.preferredLanguages
Locale.preferredLanguages // TODO: Use `.firstNonNil()` here when available.
.lazy .lazy
.map { Locale(identifier: $0) } .map { Locale(identifier: $0) }
.first { $0.languageCode != nil } .first { $0.languageCode != nil }
?? .current ?? .current
guard let languageCode = preferredLocale.languageCode else { guard let languageCode = preferredLocale.languageCode else {
return defaultLocalizedString return defaultLocalizedString

View File

@ -4,24 +4,26 @@
import SwiftUI import SwiftUI
/// Represents a type that can be converted to `PreferencePane`. /**
/// Represents a type that can be converted to `SettingsPane`.
/// Acts as type-eraser for `Preferences.Pane<T>`.
public protocol PreferencePaneConvertible { Acts as type-eraser for `Settings.Pane<T>`.
*/
public protocol SettingsPaneConvertible {
/** /**
Convert `self` to equivalent `PreferencePane`. Convert `self` to equivalent `SettingsPane`.
*/ */
func asPreferencePane() -> PreferencePane func asPreferencePane() -> SettingsPane
} }
@available(macOS 10.15, *) @available(macOS 10.15, *)
public extension SSPreferences { public extension Settings {
/** /**
Create a SwiftUI-based preference pane. Create a SwiftUI-based settings pane.
SwiftUI equivalent of the `PreferencePane` protocol. SwiftUI equivalent of the `SettingsPane` protocol.
*/ */
struct Pane<Content: View>: View, PreferencePaneConvertible { struct Pane<Content: View>: View, SettingsPaneConvertible {
let identifier: PaneIdentifier let identifier: PaneIdentifier
let title: String let title: String
let toolbarIcon: NSImage let toolbarIcon: NSImage
@ -41,15 +43,15 @@ public extension SSPreferences {
public var body: some View { content } public var body: some View { content }
public func asPreferencePane() -> PreferencePane { public func asPreferencePane() -> SettingsPane {
PaneHostingController(pane: self) PaneHostingController(pane: self)
} }
} }
/** /**
Hosting controller enabling `Preferences.Pane` to be used alongside AppKit `NSViewController`'s. Hosting controller enabling `Settings.Pane` to be used alongside AppKit `NSViewController`'s.
*/ */
final class PaneHostingController<Content: View>: NSHostingController<Content>, PreferencePane { final class PaneHostingController<Content: View>: NSHostingController<Content>, SettingsPane {
public let preferencePaneIdentifier: PaneIdentifier public let preferencePaneIdentifier: PaneIdentifier
public let preferencePaneTitle: String public let preferencePaneTitle: String
public let toolbarItemIcon: NSImage public let toolbarItemIcon: NSImage
@ -86,12 +88,11 @@ public extension SSPreferences {
@available(macOS 10.15, *) @available(macOS 10.15, *)
public extension View { public extension View {
/** /**
Applies font and color for a label used for describing a preference. Applies font and color for a label used for describing a setting.
*/ */
func preferenceDescription() -> some View { func preferenceDescription() -> some View {
font(.system(size: 11.0)) font(.system(size: 11.0))
// TODO: Use `.foregroundStyle` when targeting macOS 12. // TODO: Use `.foregroundStyle` when targeting macOS 12.
.foregroundColor(.secondary) .foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
} }
} }

View File

@ -5,7 +5,7 @@
import SwiftUI import SwiftUI
@available(macOS 10.15, *) @available(macOS 10.15, *)
public extension SSPreferences { public extension Settings {
/** /**
Represents a section with right-aligned title and optional bottom divider. Represents a section with right-aligned title and optional bottom divider.
*/ */
@ -32,7 +32,10 @@ public extension SSPreferences {
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
Color.clear Color.clear
.preference(key: LabelWidthPreferenceKey.self, value: Double(geometry.size.width)) .preference(
key: LabelWidthPreferenceKey.self,
value: geometry.size.width
)
} }
} }
} }
@ -46,53 +49,30 @@ public extension SSPreferences {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in .onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in
maximumWidth = Double(newMaximumWidth) maximumWidth = newMaximumWidth
} }
} }
} }
public private(set) var label: AnyView? public let label: AnyView
public let content: AnyView public let content: AnyView
public let bottomDivider: Bool public let bottomDivider: Bool
public let verticalAlignment: VerticalAlignment public let verticalAlignment: VerticalAlignment
/** /**
A section is responsible for controlling a single preference without Label. A section is responsible for controlling a single setting.
- Parameters:
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content.
- verticalAlignment:
- label: A view describing preference handled by this section.
- content: A content view.
*/
public init<Content: View>(
bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline,
@ViewBuilder content: @escaping () -> Content
) {
label = nil
self.bottomDivider = bottomDivider
self.verticalAlignment = verticalAlignment
let stack = VStack(alignment: .leading) { content() }
self.content = stack.eraseToAnyView()
}
/**
A section is responsible for controlling a single preference.
- Parameters: - Parameters:
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`. - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content. - verticalAlignement: The vertical alignment of the section content.
- verticalAlignment: - label: A view describing the setting handled by this section.
- label: A view describing preference handled by this section.
- content: A content view. - content: A content view.
*/ */
public init<Label: View, Content: View>( public init(
bottomDivider: Bool = false, bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline, verticalAlignment: VerticalAlignment = .firstTextBaseline,
label: @escaping () -> Label, label: @escaping () -> some View,
@ViewBuilder content: @escaping () -> Content @ViewBuilder content: @escaping () -> some View
) { ) {
self.label = label() self.label = label()
.overlay(LabelOverlay()) .overlay(LabelOverlay())
@ -104,57 +84,42 @@ public extension SSPreferences {
} }
/** /**
Creates instance of section, responsible for controling single preference with `Text` as a `Label`. Creates instance of section, responsible for controling a single setting with `Text` as a `Label`.
- Parameters: - Parameters:
- title: A string describing preference handled by this section. - title: A string describing the setting handled by this section.
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`. - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content. - verticalAlignement: The vertical alignment of the section content.
- verticalAlignment:
- content: A content view. - content: A content view.
*/ */
public init<Content: View>( public init(
title: String? = nil, title: String,
bottomDivider: Bool = false, bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline, verticalAlignment: VerticalAlignment = .firstTextBaseline,
@ViewBuilder content: @escaping () -> Content @ViewBuilder content: @escaping () -> some View
) { ) {
if let title = title { let textLabel = {
let textLabel = { Text(title)
Text(title) .font(.system(size: 13.0))
.font(.system(size: 13.0)) .overlay(LabelOverlay())
.overlay(LabelOverlay()) .eraseToAnyView()
.eraseToAnyView()
}
self.init(
bottomDivider: bottomDivider,
verticalAlignment: verticalAlignment,
label: textLabel,
content: content
)
return
} }
self.init( self.init(
bottomDivider: bottomDivider, bottomDivider: bottomDivider,
verticalAlignment: verticalAlignment, verticalAlignment: verticalAlignment,
label: textLabel,
content: content content: content
) )
} }
public func bodyLimited(rightPaneWidth: CGFloat? = nil) -> some View {
HStack(alignment: verticalAlignment) {
if let label = label {
label.alignmentGuide(.preferenceSectionLabel) { $0[.trailing] }
}
HStack {
content
Spacer()
}.frame(maxWidth: rightPaneWidth)
}
}
public var body: some View { public var body: some View {
bodyLimited() HStack(alignment: verticalAlignment) {
label
.alignmentGuide(.settingsSectionLabel) { $0[.trailing] }
content
Spacer()
}
} }
} }
} }

View File

@ -12,7 +12,7 @@ extension NSUserInterfaceItemIdentifier {
static let toolbarSegmentedControl = Self("toolbarSegmentedControl") static let toolbarSegmentedControl = Self("toolbarSegmentedControl")
} }
final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController { final class SegmentedControlStyleViewController: NSViewController, SettingsStyleController {
var segmentedControl: NSSegmentedControl! { var segmentedControl: NSSegmentedControl! {
get { view as? NSSegmentedControl } get { view as? NSSegmentedControl }
set { set {
@ -22,13 +22,13 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
var isKeepingWindowCentered: Bool { true } var isKeepingWindowCentered: Bool { true }
weak var delegate: PreferencesStyleControllerDelegate? weak var delegate: SettingsStyleControllerDelegate?
private var preferencePanes: [PreferencePane]! private var panes: [SettingsPane]!
required init(preferencePanes: [PreferencePane]) { required init(panes: [SettingsPane]) {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.preferencePanes = preferencePanes self.panes = panes
} }
@available(*, unavailable) @available(*, unavailable)
@ -37,12 +37,12 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
} }
override func loadView() { override func loadView() {
view = createSegmentedControl(preferencePanes: preferencePanes) view = createSegmentedControl(panes: panes)
} }
fileprivate func createSegmentedControl(preferencePanes: [PreferencePane]) -> NSSegmentedControl { fileprivate func createSegmentedControl(panes: [SettingsPane]) -> NSSegmentedControl {
let segmentedControl = NSSegmentedControl() let segmentedControl = NSSegmentedControl()
segmentedControl.segmentCount = preferencePanes.count segmentedControl.segmentCount = panes.count
segmentedControl.segmentStyle = .texturedSquare segmentedControl.segmentStyle = .texturedSquare
segmentedControl.target = self segmentedControl.target = self
segmentedControl.action = #selector(segmentedControlAction) segmentedControl.action = #selector(segmentedControlAction)
@ -57,8 +57,8 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
let insets = CGSize(width: 36, height: 12) let insets = CGSize(width: 36, height: 12)
var maxSize = CGSize.zero var maxSize = CGSize.zero
for preferencePane in preferencePanes { for pane in panes {
let title = preferencePane.preferencePaneTitle let title = pane.preferencePaneTitle
let titleSize = title.size( let titleSize = title.size(
withAttributes: [ withAttributes: [
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)), .font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)),
@ -77,13 +77,13 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
) )
}() }()
let segmentBorderWidth = Double(preferencePanes.count) + 1 let segmentBorderWidth = Double(panes.count) + 1
let segmentWidth = segmentSize.width * Double(preferencePanes.count) + segmentBorderWidth let segmentWidth = segmentSize.width * Double(panes.count) + segmentBorderWidth
let segmentHeight = segmentSize.height let segmentHeight = segmentSize.height
segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight) segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
for (index, preferencePane) in preferencePanes.enumerated() { for (index, pane) in panes.enumerated() {
segmentedControl.setLabel(preferencePane.preferencePaneTitle, forSegment: index) segmentedControl.setLabel(pane.preferencePaneTitle, forSegment: index)
segmentedControl.setWidth(segmentSize.width, forSegment: index) segmentedControl.setWidth(segmentSize.width, forSegment: index)
if let cell = segmentedControl.cell as? NSSegmentedCell { if let cell = segmentedControl.cell as? NSSegmentedCell {
cell.setTag(index, forSegment: index) cell.setTag(index, forSegment: index)
@ -109,8 +109,8 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
] ]
} }
func toolbarItem(preferenceIdentifier: SSPreferences.PaneIdentifier) -> NSToolbarItem? { func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem? {
let toolbarItemIdentifier = preferenceIdentifier.toolbarItemIdentifier let toolbarItemIdentifier = paneIdentifier.toolbarItemIdentifier
precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem) precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem)
// When the segments outgrow the window, we need to provide a group of // When the segments outgrow the window, we need to provide a group of
@ -118,12 +118,12 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
// context menu that pops up at the right edge of the window. // context menu that pops up at the right edge of the window.
let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier) let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier)
toolbarItemGroup.view = segmentedControl toolbarItemGroup.view = segmentedControl
toolbarItemGroup.subitems = preferencePanes.enumerated().map { index, preferenceable -> NSToolbarItem in toolbarItemGroup.subitems = panes.enumerated().map { index, settingsPane in
let item = NSToolbarItem(itemIdentifier: .init("segment-\(preferenceable.preferencePaneTitle)")) let item = NSToolbarItem(itemIdentifier: .init("segment-\(settingsPane.preferencePaneTitle)"))
item.label = preferenceable.preferencePaneTitle item.label = settingsPane.preferencePaneTitle
let menuItem = NSMenuItem( let menuItem = NSMenuItem(
title: preferenceable.preferencePaneTitle, title: settingsPane.preferencePaneTitle,
action: #selector(segmentedControlMenuAction), action: #selector(segmentedControlMenuAction),
keyEquivalent: "" keyEquivalent: ""
) )

View File

@ -0,0 +1,15 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
/**
The namespace for this package.
*/
public enum Settings {}
// TODO: Remove in the next major version.
// Preserve backwards compatibility.
public typealias Preferences = Settings
public typealias PreferencePane = SettingsPane
public typealias PreferencePaneConvertible = SettingsPaneConvertible
public typealias PreferencesWindowController = SettingsWindowController

View File

@ -4,7 +4,7 @@
import Cocoa import Cocoa
public extension SSPreferences { public extension Settings {
struct PaneIdentifier: Hashable, RawRepresentable, Codable { struct PaneIdentifier: Hashable, RawRepresentable, Codable {
public let rawValue: String public let rawValue: String
@ -14,13 +14,13 @@ public extension SSPreferences {
} }
} }
public protocol PreferencePane: NSViewController { public protocol SettingsPane: NSViewController {
var preferencePaneIdentifier: SSPreferences.PaneIdentifier { get } var preferencePaneIdentifier: Settings.PaneIdentifier { get }
var preferencePaneTitle: String { get } var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get } var toolbarItemIcon: NSImage { get }
} }
public extension PreferencePane { public extension SettingsPane {
var toolbarItemIdentifier: NSToolbarItem.Identifier { var toolbarItemIdentifier: NSToolbarItem.Identifier {
preferencePaneIdentifier.toolbarItemIdentifier preferencePaneIdentifier.toolbarItemIdentifier
} }
@ -28,7 +28,7 @@ public extension PreferencePane {
var toolbarItemIcon: NSImage { .empty } var toolbarItemIcon: NSImage { .empty }
} }
public extension SSPreferences.PaneIdentifier { public extension Settings.PaneIdentifier {
init(_ rawValue: String) { init(_ rawValue: String) {
self.init(rawValue: rawValue) self.init(rawValue: rawValue)
} }

View File

@ -4,16 +4,16 @@
import Cocoa import Cocoa
protocol PreferencesStyleController: AnyObject { protocol SettingsStyleController: AnyObject {
var delegate: PreferencesStyleControllerDelegate? { get set } var delegate: SettingsStyleControllerDelegate? { get set }
var isKeepingWindowCentered: Bool { get } var isKeepingWindowCentered: Bool { get }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
func toolbarItem(preferenceIdentifier: SSPreferences.PaneIdentifier) -> NSToolbarItem? func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem?
func selectTab(index: Int) func selectTab(index: Int)
} }
protocol PreferencesStyleControllerDelegate: AnyObject { protocol SettingsStyleControllerDelegate: AnyObject {
func activateTab(preferenceIdentifier: SSPreferences.PaneIdentifier, animated: Bool) func activateTab(paneIdentifier: Settings.PaneIdentifier, animated: Bool)
func activateTab(index: Int, animated: Bool) func activateTab(index: Int, animated: Bool)
} }

View File

@ -4,16 +4,16 @@
import Cocoa import Cocoa
final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate { final class SettingsTabViewController: NSViewController, SettingsStyleControllerDelegate {
private var activeTab: Int? private var activeTab: Int?
private var preferencePanes = [PreferencePane]() private var panes = [SettingsPane]()
private var style: SSPreferences.Style? private var style: Settings.Style?
internal var preferencePanesCount: Int { preferencePanes.count } internal var settingsPanesCount: Int { panes.count }
private var preferencesStyleController: PreferencesStyleController! private var settingsStyleController: SettingsStyleController!
private var isKeepingWindowCentered: Bool { preferencesStyleController.isKeepingWindowCentered } private var isKeepingWindowCentered: Bool { settingsStyleController.isKeepingWindowCentered }
private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] { private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] {
preferencesStyleController?.toolbarItemIdentifiers() ?? [] settingsStyleController?.toolbarItemIdentifiers() ?? []
} }
var window: NSWindow! { view.window } var window: NSWindow! { view.window }
@ -21,11 +21,11 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
var isAnimated = true var isAnimated = true
var activeViewController: NSViewController? { var activeViewController: NSViewController? {
guard let activeTab = activeTab else { guard let activeTab else {
return nil return nil
} }
return preferencePanes[activeTab] return panes[activeTab]
} }
override func loadView() { override func loadView() {
@ -33,12 +33,12 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
} }
func configure(preferencePanes: [PreferencePane], style: SSPreferences.Style) { func configure(panes: [SettingsPane], style: Settings.Style) {
self.preferencePanes = preferencePanes self.panes = panes
self.style = style self.style = style
children = preferencePanes children = panes
let toolbar = NSToolbar(identifier: "PreferencesToolbar") let toolbar = NSToolbar(identifier: "SettingsToolbar")
toolbar.allowsUserCustomization = false toolbar.allowsUserCustomization = false
toolbar.displayMode = .iconAndLabel toolbar.displayMode = .iconAndLabel
toolbar.showsBaselineSeparator = true toolbar.showsBaselineSeparator = true
@ -46,26 +46,22 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
switch style { switch style {
case .segmentedControl: case .segmentedControl:
preferencesStyleController = SegmentedControlStyleViewController(preferencePanes: preferencePanes) settingsStyleController = SegmentedControlStyleViewController(panes: panes)
case .toolbarItems: case .toolbarItems:
preferencesStyleController = ToolbarItemStyleViewController( settingsStyleController = ToolbarItemStyleViewController(
preferencePanes: preferencePanes, panes: panes,
toolbar: toolbar, toolbar: toolbar,
centerToolbarItems: false centerToolbarItems: false
) )
} }
preferencesStyleController.delegate = self settingsStyleController.delegate = self
// Called last so that `preferencesStyleController` can be asked for items. // Called last so that `settingsStyleController` can be asked for items.
window.toolbar = toolbar window.toolbar = toolbar
} }
func activateTab(preferencePane: PreferencePane, animated: Bool) { func activateTab(paneIdentifier: Settings.PaneIdentifier, animated: Bool) {
activateTab(preferenceIdentifier: preferencePane.preferencePaneIdentifier, animated: animated) guard let index = (panes.firstIndex { $0.preferencePaneIdentifier == paneIdentifier }) else {
}
func activateTab(preferenceIdentifier: SSPreferences.PaneIdentifier, animated: Bool) {
guard let index = (preferencePanes.firstIndex { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
return activateTab(index: 0, animated: animated) return activateTab(index: 0, animated: animated)
} }
@ -75,7 +71,7 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
func activateTab(index: Int, animated: Bool) { func activateTab(index: Int, animated: Bool) {
defer { defer {
activeTab = index activeTab = index
preferencesStyleController.selectTab(index: index) settingsStyleController.selectTab(index: index)
updateWindowTitle(tabIndex: index) updateWindowTitle(tabIndex: index)
} }
@ -96,46 +92,50 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
} }
} }
private func updateWindowTitle(tabIndex _: Int) { private func updateWindowTitle(tabIndex: Int) {
window.title = { window.title = {
// if preferencePanes.count > 1 { if panes.count > 1 {
// return preferencePanes[tabIndex].preferencePaneTitle return panes[tabIndex].preferencePaneTitle
// } else { } else {
// let preferences = Localization[.preferences] let settings: String
// let appName = Bundle.main.appName if #available(macOS 13, *) {
// return "\(appName) \(preferences)" settings = Localization[.settings]
// } } else {
var preferencesTitleName = NSLocalizedString("vChewing Preferences…", comment: "") settings = Localization[.preferences]
preferencesTitleName.removeLast() }
return preferencesTitleName
let appName = Bundle.main.appName
return "\(appName) \(settings)"
}
}() }()
} }
/// Cached constraints that pin `childViewController` views to the content view. /**
Cached constraints that pin `childViewController` views to the content view.
*/
private var activeChildViewConstraints = [NSLayoutConstraint]() private var activeChildViewConstraints = [NSLayoutConstraint]()
private func immediatelyDisplayTab(index: Int) { private func immediatelyDisplayTab(index: Int) {
let toViewController = preferencePanes[index] let toViewController = panes[index]
view.addSubview(toViewController.view) view.addSubview(toViewController.view)
activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds() activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
setWindowFrame(for: toViewController, animated: false) setWindowFrame(for: toViewController, animated: false)
} }
private func animateTabTransition(index: Int, animated: Bool) { private func animateTabTransition(index: Int, animated: Bool) {
guard let activeTab = activeTab else { guard let activeTab else {
assertionFailure( assertionFailure("animateTabTransition called before a tab was displayed; transition only works from one tab to another")
"animateTabTransition called before a tab was displayed; transition only works from one tab to another")
immediatelyDisplayTab(index: index) immediatelyDisplayTab(index: index)
return return
} }
let fromViewController = preferencePanes[activeTab] let fromViewController = panes[activeTab]
let toViewController = preferencePanes[index] let toViewController = panes[index]
// View controller animations only work on macOS 10.14 and newer. // View controller animations only work on macOS 10.14 and newer.
let options: NSViewController.TransitionOptions let options: NSViewController.TransitionOptions
if #available(macOS 10.14, *) { if #available(macOS 10.14, *) {
options = animated && isAnimated ? [.slideUp] : [] options = animated && isAnimated ? [.crossfade] : []
} else { } else {
options = [] options = []
} }
@ -157,18 +157,16 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
options: NSViewController.TransitionOptions = [], options: NSViewController.TransitionOptions = [],
completionHandler completion: (() -> Void)? = nil completionHandler completion: (() -> Void)? = nil
) { ) {
let isAnimated = let isAnimated = options
options .isDisjoint(with: [
.intersection([ .crossfade,
.crossfade, .slideUp,
.slideUp, .slideDown,
.slideDown, .slideForward,
.slideForward, .slideBackward,
.slideBackward, .slideLeft,
.slideLeft, .slideRight,
.slideRight, ])
])
.isEmpty == false
if isAnimated { if isAnimated {
NSAnimationContext.runAnimationGroup({ context in NSAnimationContext.runAnimationGroup({ context in
@ -195,7 +193,7 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
} }
private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) { private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) {
guard let window = window else { guard let window else {
preconditionFailure() preconditionFailure()
} }
@ -216,7 +214,7 @@ final class PreferencesTabViewController: NSViewController, PreferencesStyleCont
} }
} }
extension PreferencesTabViewController: NSToolbarDelegate { extension SettingsTabViewController: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarItemIdentifiers toolbarItemIdentifiers
} }
@ -238,7 +236,6 @@ extension PreferencesTabViewController: NSToolbarDelegate {
return nil return nil
} }
return preferencesStyleController.toolbarItem( return settingsStyleController.toolbarItem(paneIdentifier: .init(fromToolbarItemIdentifier: itemIdentifier))
preferenceIdentifier: SSPreferences.PaneIdentifier(fromToolbarItemIdentifier: itemIdentifier))
} }
} }

View File

@ -5,11 +5,11 @@
import Cocoa import Cocoa
extension NSWindow.FrameAutosaveName { extension NSWindow.FrameAutosaveName {
static let preferences: NSWindow.FrameAutosaveName = "com.sindresorhus.Preferences.FrameAutosaveName" static let settings: NSWindow.FrameAutosaveName = "com.sindresorhus.Preferences.FrameAutosaveName"
} }
public final class PreferencesWindowController: NSWindowController { public final class SettingsWindowController: NSWindowController {
private let tabViewController = PreferencesTabViewController() private let tabViewController = SettingsTabViewController()
public var isAnimated: Bool { public var isAnimated: Bool {
get { tabViewController.isAnimated } get { tabViewController.isAnimated }
@ -25,14 +25,13 @@ public final class PreferencesWindowController: NSWindowController {
} }
private func updateToolbarVisibility() { private func updateToolbarVisibility() {
window?.toolbar?.isVisible = window?.toolbar?.isVisible = (hidesToolbarForSingleItem == false)
(hidesToolbarForSingleItem == false) || (tabViewController.settingsPanesCount > 1)
|| (tabViewController.preferencePanesCount > 1)
} }
public init( public init(
preferencePanes: [PreferencePane], preferencePanes: [SettingsPane],
style: SSPreferences.Style = .toolbarItems, style: Settings.Style = .toolbarItems,
animated: Bool = true, animated: Bool = true,
hidesToolbarForSingleItem: Bool = true hidesToolbarForSingleItem: Bool = true
) { ) {
@ -66,7 +65,7 @@ public final class PreferencesWindowController: NSWindowController {
} }
tabViewController.isAnimated = animated tabViewController.isAnimated = animated
tabViewController.configure(preferencePanes: preferencePanes, style: style) tabViewController.configure(panes: preferencePanes, style: style)
updateToolbarVisibility() updateToolbarVisibility()
} }
@ -81,20 +80,20 @@ public final class PreferencesWindowController: NSWindowController {
} }
/** /**
Show the preferences window and brings it to front. Show the settings window and brings it to front.
If you pass a `SSPreferences.PaneIdentifier`, the window will activate the corresponding tab. If you pass a `Settings.PaneIdentifier`, the window will activate the corresponding tab.
- Parameter preferencePane: Identifier of the preference pane to display, or `nil` to show the tab that was open when the user last closed the window. - Parameter preferencePane: Identifier of the settings pane to display, or `nil` to show the tab that was open when the user last closed the window.
- Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`. - Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`.
- See `close()` to close the window again. - See `close()` to close the window again.
- See `showWindow(_:)` to show the window without the convenience of activating the app. - See `showWindow(_:)` to show the window without the convenience of activating the app.
*/ */
public func show(preferencePane preferenceIdentifier: SSPreferences.PaneIdentifier? = nil) { public func show(preferencePane paneIdentifier: Settings.PaneIdentifier? = nil) {
if let preferenceIdentifier = preferenceIdentifier { if let paneIdentifier {
tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false) tabViewController.activateTab(paneIdentifier: paneIdentifier, animated: false)
} else { } else {
tabViewController.restoreInitialTab() tabViewController.restoreInitialTab()
} }
@ -106,24 +105,25 @@ public final class PreferencesWindowController: NSWindowController {
private func restoreWindowPosition() { private func restoreWindowPosition() {
guard guard
let window = window, let window,
let screenContainingWindow = window.screen let screenContainingWindow = window.screen
else { else {
return return
} }
window.setFrameOrigin( window.setFrameOrigin(CGPoint(
CGPoint( x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2,
x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2, y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2
y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2 ))
)) window.setFrameUsingName(.settings)
window.setFrameUsingName(.preferences) window.setFrameAutosaveName(.settings)
window.setFrameAutosaveName(.preferences)
} }
} }
public extension PreferencesWindowController { public extension SettingsWindowController {
/// Returns the active pane if it responds to the given action. /**
Returns the active pane if it responds to the given action.
*/
override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? { override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
if let target = super.supplementalTarget(forAction: action, sender: sender) { if let target = super.supplementalTarget(forAction: action, sender: sender) {
return target return target
@ -133,15 +133,11 @@ public extension PreferencesWindowController {
return nil return nil
} }
if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder, if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder, target.responds(to: action) {
target.responds(to: action)
{
return target return target
} }
if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder, if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder, target.responds(to: action) {
target.responds(to: action)
{
return target return target
} }
@ -150,20 +146,18 @@ public extension PreferencesWindowController {
} }
@available(macOS 10.15, *) @available(macOS 10.15, *)
public extension PreferencesWindowController { public extension SettingsWindowController {
/** /**
Create a preferences window from only SwiftUI-based preference panes. Create a settings window from only SwiftUI-based settings panes.
*/ */
convenience init( convenience init(
panes: [PreferencePaneConvertible], panes: [SettingsPaneConvertible],
style: SSPreferences.Style = .toolbarItems, style: Settings.Style = .toolbarItems,
animated: Bool = true, animated: Bool = true,
hidesToolbarForSingleItem: Bool = true hidesToolbarForSingleItem: Bool = true
) { ) {
let preferencePanes = panes.map { $0.asPreferencePane() }
self.init( self.init(
preferencePanes: preferencePanes, preferencePanes: panes.map { $0.asPreferencePane() },
style: style, style: style,
animated: animated, animated: animated,
hidesToolbarForSingleItem: hidesToolbarForSingleItem hidesToolbarForSingleItem: hidesToolbarForSingleItem

View File

@ -4,7 +4,7 @@
import Cocoa import Cocoa
public extension SSPreferences { public extension Settings {
enum Style { enum Style {
case toolbarItems case toolbarItems
case segmentedControl case segmentedControl

View File

@ -4,15 +4,15 @@
import Cocoa import Cocoa
final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController { final class ToolbarItemStyleViewController: NSObject, SettingsStyleController {
let toolbar: NSToolbar let toolbar: NSToolbar
let centerToolbarItems: Bool let centerToolbarItems: Bool
let preferencePanes: [PreferencePane] let panes: [SettingsPane]
var isKeepingWindowCentered: Bool { centerToolbarItems } var isKeepingWindowCentered: Bool { centerToolbarItems }
weak var delegate: PreferencesStyleControllerDelegate? weak var delegate: SettingsStyleControllerDelegate?
init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) { init(panes: [SettingsPane], toolbar: NSToolbar, centerToolbarItems: Bool) {
self.preferencePanes = preferencePanes self.panes = panes
self.toolbar = toolbar self.toolbar = toolbar
self.centerToolbarItems = centerToolbarItems self.centerToolbarItems = centerToolbarItems
} }
@ -24,8 +24,8 @@ final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController
toolbarItemIdentifiers.append(.flexibleSpace) toolbarItemIdentifiers.append(.flexibleSpace)
} }
for preferencePane in preferencePanes { for pane in panes {
toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier) toolbarItemIdentifiers.append(pane.toolbarItemIdentifier)
} }
if centerToolbarItems { if centerToolbarItems {
@ -35,14 +35,14 @@ final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController
return toolbarItemIdentifiers return toolbarItemIdentifiers
} }
func toolbarItem(preferenceIdentifier: SSPreferences.PaneIdentifier) -> NSToolbarItem? { func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem? {
guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else { guard let pane = (panes.first { $0.preferencePaneIdentifier == paneIdentifier }) else {
preconditionFailure() preconditionFailure()
} }
let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier) let toolbarItem = NSToolbarItem(itemIdentifier: paneIdentifier.toolbarItemIdentifier)
toolbarItem.label = preference.preferencePaneTitle toolbarItem.label = pane.preferencePaneTitle
toolbarItem.image = preference.toolbarItemIcon toolbarItem.image = pane.toolbarItemIcon
toolbarItem.target = self toolbarItem.target = self
toolbarItem.action = #selector(toolbarItemSelected) toolbarItem.action = #selector(toolbarItemSelected)
return toolbarItem return toolbarItem
@ -50,12 +50,12 @@ final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController
@IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) { @IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
delegate?.activateTab( delegate?.activateTab(
preferenceIdentifier: SSPreferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier), paneIdentifier: .init(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
animated: true animated: true
) )
} }
func selectTab(index: Int) { func selectTab(index: Int) {
toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier toolbar.selectedItemIdentifier = panes[index].toolbarItemIdentifier
} }
} }

View File

@ -2,7 +2,6 @@
// ==================== // ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT) // This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Cocoa
import SwiftUI import SwiftUI
extension NSImage { extension NSImage {
@ -12,21 +11,13 @@ extension NSImage {
extension NSView { extension NSView {
@discardableResult @discardableResult
func constrainToSuperviewBounds() -> [NSLayoutConstraint] { func constrainToSuperviewBounds() -> [NSLayoutConstraint] {
guard let superview = superview else { guard let superview else {
preconditionFailure("superview has to be set first") preconditionFailure("superview has to be set first")
} }
var result = [NSLayoutConstraint]() var result = [NSLayoutConstraint]()
result.append( result.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self]))
contentsOf: NSLayoutConstraint.constraints( result.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self]))
withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil,
views: ["subview": self]
))
result.append(
contentsOf: NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil,
views: ["subview": self]
))
translatesAutoresizingMaskIntoConstraints = false translatesAutoresizingMaskIntoConstraints = false
superview.addConstraints(result) superview.addConstraints(result)
@ -35,42 +26,39 @@ extension NSView {
} }
extension NSEvent { extension NSEvent {
/// Events triggered by user interaction. /**
static let userInteractionEvents: [NSEvent.EventType] = { Events triggered by user interaction.
var events: [NSEvent.EventType] = [ */
.leftMouseDown, static let userInteractionEvents: [EventType] = [
.leftMouseUp, .leftMouseDown,
.rightMouseDown, .leftMouseUp,
.rightMouseUp, .rightMouseDown,
.leftMouseDragged, .rightMouseUp,
.rightMouseDragged, .leftMouseDragged,
.keyDown, .rightMouseDragged,
.keyUp, .keyDown,
.scrollWheel, .keyUp,
.tabletPoint, .scrollWheel,
.otherMouseDown, .tabletPoint,
.otherMouseUp, .otherMouseDown,
.otherMouseDragged, .otherMouseUp,
.gesture, .otherMouseDragged,
.magnify, .gesture,
.swipe, .magnify,
.rotate, .swipe,
.beginGesture, .rotate,
.endGesture, .beginGesture,
.smartMagnify, .endGesture,
.quickLook, .smartMagnify,
.directTouch, .pressure,
] .quickLook,
.directTouch,
]
if #available(macOS 10.10.3, *) { /**
events.append(.pressure) Whether the event was triggered by user interaction.
} */
var isUserInteraction: Bool { Self.userInteractionEvents.contains(type) }
return events
}()
/// Whether the event was triggered by user interaction.
var isUserInteraction: Bool { NSEvent.userInteractionEvents.contains(type) }
} }
extension Bundle { extension Bundle {
@ -87,10 +75,12 @@ extension Bundle {
} }
} }
/// A window that allows you to disable all user interactions via `isUserInteractionEnabled`. /**
/// A window that allows you to disable all user interactions via `isUserInteractionEnabled`.
/// Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set.
class UserInteractionPausableWindow: NSWindow { Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set.
*/
class UserInteractionPausableWindow: NSWindow { // swiftlint:disable:this final_class
var isUserInteractionEnabled = true var isUserInteractionEnabled = true
override func sendEvent(_ event: NSEvent) { override func sendEvent(_ event: NSEvent) {