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
let package = Package(
name: "SSPreferences",
platforms: [
.macOS(.v10_11),
.macOS(.v10_13),
],
products: [
.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
@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
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 {
private let sectionBuilder: () -> [Section]
@ -26,14 +26,14 @@ public extension SSPreferences {
@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.
- Parameters:
- 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.
- 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(
contentWidth: Double,
@ -47,21 +47,10 @@ public extension SSPreferences {
public var body: some View {
let sections = sectionBuilder()
let labelWidth = max(minimumLabelWidth, maximumLabelWidth)
return VStack(alignment: .preferenceSectionLabel) {
return VStack(alignment: .settingsSectionLabel) {
ForEach(0 ..< sections.count, id: \.self) { index in
if sections[index].label != nil {
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 }
}
viewForSection(sections, index: index)
}
}
.modifier(Section.LabelWidthModifier(maximumWidth: $maximumLabelWidth))
@ -69,17 +58,31 @@ public extension SSPreferences {
.padding(.vertical, 20)
.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, *)
extension HorizontalAlignment {
private enum PreferenceSectionLabelAlignment: AlignmentID {
private enum SettingsSectionLabelAlignment: AlignmentID {
// swiftlint:disable:next no_cgfloat
static func defaultValue(in context: ViewDimensions) -> CGFloat {
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 {
enum Identifier {
case preferences
case preferencesEllipsized
case settings
}
private static let localizedStrings: [Identifier: [String: String]] = [
@ -52,46 +52,46 @@ struct Localization {
"zh-HK": "偏好設定",
"zh-TW": "偏好設定",
],
.preferencesEllipsized: [
"ar": "تفضيلات…",
"ca": "Preferències…",
"cs": "Předvolby…",
"da": "Indstillinger",
"de": "Einstellungen",
"el": "Προτιμήσεις…",
"en": "Preferences…",
"en-AU": "Preferences…",
"en-GB": "Preferences…",
"es": "Preferencias…",
"es-419": "Preferencias…",
"fi": "Asetukset",
"fr": "Préférences…",
"fr-CA": "Préférences…",
"he": "העדפות…",
"hi": "प्राथमिकता…",
"hr": "Postavke",
"hu": "Beállítások",
"id": "Preferensi…",
"it": "Preferenze…",
"ja": "環境設定",
"ko": "환경설정...",
"ms": "Keutamaan…",
"nl": "Voorkeuren…",
"no": "Valg…",
"pl": "Preferencje…",
"pt": "Preferências…",
"pt-PT": "Preferências…",
"ro": "Preferințe…",
"ru": "Настройки",
"sk": "Nastavenia",
"sv": "Inställningar",
"th": "การตั้งค่า…",
"tr": "Tercihler…",
"uk": "Параметри",
"vi": "Tùy chọn…",
"zh-CN": "偏好设置",
"zh-HK": "偏好設定",
"zh-TW": "偏好設定",
.settings: [
"ar": "الإعدادات",
"ca": "Configuració",
"cs": "Nastavení",
"da": "Indstillinger",
"de": "Einstellungen",
"el": "Ρυθμίσεις",
"en": "Settings",
"en-AU": "Settings",
"en-GB": "Settings",
"es": "Ajustes",
"es-419": "Ajustes",
"fi": "Asetukset",
"fr": "Réglages",
"fr-CA": "Réglages",
"he": "הגדרות",
"hi": "समायोजन",
"hr": "Postavke",
"hu": "Beállítások",
"id": "Pengaturan",
"it": "Impostazioni",
"ja": "設定",
"ko": "설정",
"ms": "Tetapan",
"nl": "Instellingen",
"no": "Innstillinger",
"pl": "Ustawienia",
"pt": "Ajustes",
"pt-PT": "Definições",
"ro": "Configurări",
"ru": "Настройки",
"sk": "Nastavenia",
"sv": "Inställningar",
"th": "ค่าติดตั้ง",
"tr": "Ayarlar",
"uk": "Параметри",
"vi": "Cài đặt",
"zh-CN": "设置",
"zh-HK": "設定",
"zh-TW": "設定",
],
]
@ -104,16 +104,16 @@ struct Localization {
*/
static subscript(identifier: Identifier) -> String {
// 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"]!
// Iterate through all user-preferred languages until we find one that has a valid language code.
let preferredLocale =
Locale.preferredLanguages
.lazy
.map { Locale(identifier: $0) }
.first { $0.languageCode != nil }
?? .current
let preferredLocale = Locale.preferredLanguages
// TODO: Use `.firstNonNil()` here when available.
.lazy
.map { Locale(identifier: $0) }
.first { $0.languageCode != nil }
?? .current
guard let languageCode = preferredLocale.languageCode else {
return defaultLocalizedString

View File

@ -4,24 +4,26 @@
import SwiftUI
/// Represents a type that can be converted to `PreferencePane`.
///
/// Acts as type-eraser for `Preferences.Pane<T>`.
public protocol PreferencePaneConvertible {
/**
Represents a type that can be converted to `SettingsPane`.
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, *)
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 title: String
let toolbarIcon: NSImage
@ -41,15 +43,15 @@ public extension SSPreferences {
public var body: some View { content }
public func asPreferencePane() -> PreferencePane {
public func asPreferencePane() -> SettingsPane {
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 preferencePaneTitle: String
public let toolbarItemIcon: NSImage
@ -86,12 +88,11 @@ public extension SSPreferences {
@available(macOS 10.15, *)
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 {
font(.system(size: 11.0))
// TODO: Use `.foregroundStyle` when targeting macOS 12.
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}

View File

@ -5,7 +5,7 @@
import SwiftUI
@available(macOS 10.15, *)
public extension SSPreferences {
public extension Settings {
/**
Represents a section with right-aligned title and optional bottom divider.
*/
@ -32,7 +32,10 @@ public extension SSPreferences {
var body: some View {
GeometryReader { geometry in
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 {
content
.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 bottomDivider: Bool
public let verticalAlignment: VerticalAlignment
/**
A section is responsible for controlling a single preference without Label.
- 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.
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.
- label: A view describing the setting handled by this section.
- content: A content view.
*/
public init<Label: View, Content: View>(
public init(
bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline,
label: @escaping () -> Label,
@ViewBuilder content: @escaping () -> Content
label: @escaping () -> some View,
@ViewBuilder content: @escaping () -> some View
) {
self.label = label()
.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:
- 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`.
- verticalAlignement: The vertical alignment of the section content.
- verticalAlignment:
- content: A content view.
*/
public init<Content: View>(
title: String? = nil,
public init(
title: String,
bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline,
@ViewBuilder content: @escaping () -> Content
@ViewBuilder content: @escaping () -> some View
) {
if let title = title {
let textLabel = {
Text(title)
.font(.system(size: 13.0))
.overlay(LabelOverlay())
.eraseToAnyView()
}
self.init(
bottomDivider: bottomDivider,
verticalAlignment: verticalAlignment,
label: textLabel,
content: content
)
return
let textLabel = {
Text(title)
.font(.system(size: 13.0))
.overlay(LabelOverlay())
.eraseToAnyView()
}
self.init(
bottomDivider: bottomDivider,
verticalAlignment: verticalAlignment,
label: textLabel,
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 {
bodyLimited()
HStack(alignment: verticalAlignment) {
label
.alignmentGuide(.settingsSectionLabel) { $0[.trailing] }
content
Spacer()
}
}
}
}

View File

@ -12,7 +12,7 @@ extension NSUserInterfaceItemIdentifier {
static let toolbarSegmentedControl = Self("toolbarSegmentedControl")
}
final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController {
final class SegmentedControlStyleViewController: NSViewController, SettingsStyleController {
var segmentedControl: NSSegmentedControl! {
get { view as? NSSegmentedControl }
set {
@ -22,13 +22,13 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
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)
self.preferencePanes = preferencePanes
self.panes = panes
}
@available(*, unavailable)
@ -37,12 +37,12 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
}
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()
segmentedControl.segmentCount = preferencePanes.count
segmentedControl.segmentCount = panes.count
segmentedControl.segmentStyle = .texturedSquare
segmentedControl.target = self
segmentedControl.action = #selector(segmentedControlAction)
@ -57,8 +57,8 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
let insets = CGSize(width: 36, height: 12)
var maxSize = CGSize.zero
for preferencePane in preferencePanes {
let title = preferencePane.preferencePaneTitle
for pane in panes {
let title = pane.preferencePaneTitle
let titleSize = title.size(
withAttributes: [
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)),
@ -77,13 +77,13 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
)
}()
let segmentBorderWidth = Double(preferencePanes.count) + 1
let segmentWidth = segmentSize.width * Double(preferencePanes.count) + segmentBorderWidth
let segmentBorderWidth = Double(panes.count) + 1
let segmentWidth = segmentSize.width * Double(panes.count) + segmentBorderWidth
let segmentHeight = segmentSize.height
segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
for (index, preferencePane) in preferencePanes.enumerated() {
segmentedControl.setLabel(preferencePane.preferencePaneTitle, forSegment: index)
for (index, pane) in panes.enumerated() {
segmentedControl.setLabel(pane.preferencePaneTitle, forSegment: index)
segmentedControl.setWidth(segmentSize.width, forSegment: index)
if let cell = segmentedControl.cell as? NSSegmentedCell {
cell.setTag(index, forSegment: index)
@ -109,8 +109,8 @@ final class SegmentedControlStyleViewController: NSViewController, PreferencesSt
]
}
func toolbarItem(preferenceIdentifier: SSPreferences.PaneIdentifier) -> NSToolbarItem? {
let toolbarItemIdentifier = preferenceIdentifier.toolbarItemIdentifier
func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem? {
let toolbarItemIdentifier = paneIdentifier.toolbarItemIdentifier
precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem)
// 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.
let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier)
toolbarItemGroup.view = segmentedControl
toolbarItemGroup.subitems = preferencePanes.enumerated().map { index, preferenceable -> NSToolbarItem in
let item = NSToolbarItem(itemIdentifier: .init("segment-\(preferenceable.preferencePaneTitle)"))
item.label = preferenceable.preferencePaneTitle
toolbarItemGroup.subitems = panes.enumerated().map { index, settingsPane in
let item = NSToolbarItem(itemIdentifier: .init("segment-\(settingsPane.preferencePaneTitle)"))
item.label = settingsPane.preferencePaneTitle
let menuItem = NSMenuItem(
title: preferenceable.preferencePaneTitle,
title: settingsPane.preferencePaneTitle,
action: #selector(segmentedControlMenuAction),
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
public extension SSPreferences {
public extension Settings {
struct PaneIdentifier: Hashable, RawRepresentable, Codable {
public let rawValue: String
@ -14,13 +14,13 @@ public extension SSPreferences {
}
}
public protocol PreferencePane: NSViewController {
var preferencePaneIdentifier: SSPreferences.PaneIdentifier { get }
public protocol SettingsPane: NSViewController {
var preferencePaneIdentifier: Settings.PaneIdentifier { get }
var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get }
}
public extension PreferencePane {
public extension SettingsPane {
var toolbarItemIdentifier: NSToolbarItem.Identifier {
preferencePaneIdentifier.toolbarItemIdentifier
}
@ -28,7 +28,7 @@ public extension PreferencePane {
var toolbarItemIcon: NSImage { .empty }
}
public extension SSPreferences.PaneIdentifier {
public extension Settings.PaneIdentifier {
init(_ rawValue: String) {
self.init(rawValue: rawValue)
}

View File

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

View File

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

View File

@ -5,11 +5,11 @@
import Cocoa
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 {
private let tabViewController = PreferencesTabViewController()
public final class SettingsWindowController: NSWindowController {
private let tabViewController = SettingsTabViewController()
public var isAnimated: Bool {
get { tabViewController.isAnimated }
@ -25,14 +25,13 @@ public final class PreferencesWindowController: NSWindowController {
}
private func updateToolbarVisibility() {
window?.toolbar?.isVisible =
(hidesToolbarForSingleItem == false)
|| (tabViewController.preferencePanesCount > 1)
window?.toolbar?.isVisible = (hidesToolbarForSingleItem == false)
|| (tabViewController.settingsPanesCount > 1)
}
public init(
preferencePanes: [PreferencePane],
style: SSPreferences.Style = .toolbarItems,
preferencePanes: [SettingsPane],
style: Settings.Style = .toolbarItems,
animated: Bool = true,
hidesToolbarForSingleItem: Bool = true
) {
@ -66,7 +65,7 @@ public final class PreferencesWindowController: NSWindowController {
}
tabViewController.isAnimated = animated
tabViewController.configure(preferencePanes: preferencePanes, style: style)
tabViewController.configure(panes: preferencePanes, style: style)
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`.
- See `close()` to close the window again.
- See `showWindow(_:)` to show the window without the convenience of activating the app.
*/
public func show(preferencePane preferenceIdentifier: SSPreferences.PaneIdentifier? = nil) {
if let preferenceIdentifier = preferenceIdentifier {
tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false)
public func show(preferencePane paneIdentifier: Settings.PaneIdentifier? = nil) {
if let paneIdentifier {
tabViewController.activateTab(paneIdentifier: paneIdentifier, animated: false)
} else {
tabViewController.restoreInitialTab()
}
@ -106,24 +105,25 @@ public final class PreferencesWindowController: NSWindowController {
private func restoreWindowPosition() {
guard
let window = window,
let window,
let screenContainingWindow = window.screen
else {
return
}
window.setFrameOrigin(
CGPoint(
x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2,
y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2
))
window.setFrameUsingName(.preferences)
window.setFrameAutosaveName(.preferences)
window.setFrameOrigin(CGPoint(
x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2,
y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2
))
window.setFrameUsingName(.settings)
window.setFrameAutosaveName(.settings)
}
}
public extension PreferencesWindowController {
/// Returns the active pane if it responds to the given action.
public extension SettingsWindowController {
/**
Returns the active pane if it responds to the given action.
*/
override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
if let target = super.supplementalTarget(forAction: action, sender: sender) {
return target
@ -133,15 +133,11 @@ public extension PreferencesWindowController {
return nil
}
if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder,
target.responds(to: action)
{
if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder, target.responds(to: action) {
return target
}
if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder,
target.responds(to: action)
{
if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder, target.responds(to: action) {
return target
}
@ -150,20 +146,18 @@ public extension PreferencesWindowController {
}
@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(
panes: [PreferencePaneConvertible],
style: SSPreferences.Style = .toolbarItems,
panes: [SettingsPaneConvertible],
style: Settings.Style = .toolbarItems,
animated: Bool = true,
hidesToolbarForSingleItem: Bool = true
) {
let preferencePanes = panes.map { $0.asPreferencePane() }
self.init(
preferencePanes: preferencePanes,
preferencePanes: panes.map { $0.asPreferencePane() },
style: style,
animated: animated,
hidesToolbarForSingleItem: hidesToolbarForSingleItem

View File

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

View File

@ -4,15 +4,15 @@
import Cocoa
final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController {
final class ToolbarItemStyleViewController: NSObject, SettingsStyleController {
let toolbar: NSToolbar
let centerToolbarItems: Bool
let preferencePanes: [PreferencePane]
let panes: [SettingsPane]
var isKeepingWindowCentered: Bool { centerToolbarItems }
weak var delegate: PreferencesStyleControllerDelegate?
weak var delegate: SettingsStyleControllerDelegate?
init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) {
self.preferencePanes = preferencePanes
init(panes: [SettingsPane], toolbar: NSToolbar, centerToolbarItems: Bool) {
self.panes = panes
self.toolbar = toolbar
self.centerToolbarItems = centerToolbarItems
}
@ -24,8 +24,8 @@ final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController
toolbarItemIdentifiers.append(.flexibleSpace)
}
for preferencePane in preferencePanes {
toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier)
for pane in panes {
toolbarItemIdentifiers.append(pane.toolbarItemIdentifier)
}
if centerToolbarItems {
@ -35,14 +35,14 @@ final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController
return toolbarItemIdentifiers
}
func toolbarItem(preferenceIdentifier: SSPreferences.PaneIdentifier) -> NSToolbarItem? {
guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem? {
guard let pane = (panes.first { $0.preferencePaneIdentifier == paneIdentifier }) else {
preconditionFailure()
}
let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier)
toolbarItem.label = preference.preferencePaneTitle
toolbarItem.image = preference.toolbarItemIcon
let toolbarItem = NSToolbarItem(itemIdentifier: paneIdentifier.toolbarItemIdentifier)
toolbarItem.label = pane.preferencePaneTitle
toolbarItem.image = pane.toolbarItemIcon
toolbarItem.target = self
toolbarItem.action = #selector(toolbarItemSelected)
return toolbarItem
@ -50,12 +50,12 @@ final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController
@IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
delegate?.activateTab(
preferenceIdentifier: SSPreferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
paneIdentifier: .init(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
animated: true
)
}
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)
import Cocoa
import SwiftUI
extension NSImage {
@ -12,21 +11,13 @@ extension NSImage {
extension NSView {
@discardableResult
func constrainToSuperviewBounds() -> [NSLayoutConstraint] {
guard let superview = superview else {
guard let superview else {
preconditionFailure("superview has to be set first")
}
var result = [NSLayoutConstraint]()
result.append(
contentsOf: NSLayoutConstraint.constraints(
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]
))
result.append(contentsOf: NSLayoutConstraint.constraints(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
superview.addConstraints(result)
@ -35,42 +26,39 @@ extension NSView {
}
extension NSEvent {
/// Events triggered by user interaction.
static let userInteractionEvents: [NSEvent.EventType] = {
var events: [NSEvent.EventType] = [
.leftMouseDown,
.leftMouseUp,
.rightMouseDown,
.rightMouseUp,
.leftMouseDragged,
.rightMouseDragged,
.keyDown,
.keyUp,
.scrollWheel,
.tabletPoint,
.otherMouseDown,
.otherMouseUp,
.otherMouseDragged,
.gesture,
.magnify,
.swipe,
.rotate,
.beginGesture,
.endGesture,
.smartMagnify,
.quickLook,
.directTouch,
]
/**
Events triggered by user interaction.
*/
static let userInteractionEvents: [EventType] = [
.leftMouseDown,
.leftMouseUp,
.rightMouseDown,
.rightMouseUp,
.leftMouseDragged,
.rightMouseDragged,
.keyDown,
.keyUp,
.scrollWheel,
.tabletPoint,
.otherMouseDown,
.otherMouseUp,
.otherMouseDragged,
.gesture,
.magnify,
.swipe,
.rotate,
.beginGesture,
.endGesture,
.smartMagnify,
.pressure,
.quickLook,
.directTouch,
]
if #available(macOS 10.10.3, *) {
events.append(.pressure)
}
return events
}()
/// Whether the event was triggered by user interaction.
var isUserInteraction: Bool { NSEvent.userInteractionEvents.contains(type) }
/**
Whether the event was triggered by user interaction.
*/
var isUserInteraction: Bool { Self.userInteractionEvents.contains(type) }
}
extension Bundle {
@ -87,10 +75,12 @@ extension Bundle {
}
}
/// 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 {
/**
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 { // swiftlint:disable:this final_class
var isUserInteractionEnabled = true
override func sendEvent(_ event: NSEvent) {