diff --git a/Example/AdvancedPreferenceViewController.swift b/Example/AdvancedPreferenceViewController.swift
index 59b47f2..da26abb 100644
--- a/Example/AdvancedPreferenceViewController.swift
+++ b/Example/AdvancedPreferenceViewController.swift
@@ -1,10 +1,6 @@
import Cocoa
import Settings
-final class AdvancedSettingsViewController: NSViewController, SettingsPane {
- let paneIdentifier = Settings.PaneIdentifier.advanced
- let paneTitle = "Advanced"
- let toolbarItemIcon = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: "Advanced settings")!
@IBOutlet private var fontLabel: NSTextField!
private var font = NSFont.systemFont(ofSize: 14)
diff --git a/Example/AdvancedPreferenceViewController.xib b/Example/AdvancedPreferenceViewController.xib
new file mode 100644
index 0000000..bf28927
--- /dev/null
+++ b/Example/AdvancedPreferenceViewController.xib
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift
index a93e8a5..424c0b5 100644
--- a/Example/AppDelegate.swift
+++ b/Example/AppDelegate.swift
@@ -1,55 +1,28 @@
import Cocoa
import Settings
-extension Settings.PaneIdentifier {
- static let general = Self("general")
- static let accounts = Self("accounts")
- static let advanced = Self("advanced")
-}
-
-@main
-final class AppDelegate: NSObject, NSApplicationDelegate {
- @IBOutlet private var window: NSWindow!
-
- private var settingsStyle: Settings.Style {
- get { .settingsStyleFromUserDefaults() }
- set {
- newValue.storeInUserDefaults()
- }
- }
-
- private lazy var panes: [SettingsPane] = [
- GeneralSettingsViewController(),
- AccountsSettingsViewController(),
- AdvancedSettingsViewController()
- ]
-
- private lazy var settingsWindowController = SettingsWindowController(
- panes: panes,
- style: settingsStyle,
- animated: true,
- hidesToolbarForSingleItem: true
- )
func applicationWillFinishLaunching(_ notification: Notification) {
window.orderOut(self)
}
func applicationDidFinishLaunching(_ notification: Notification) {
- settingsWindowController.show(pane: .accounts)
- }
-
- @IBAction private func settingsMenuItemActionHandler(_ sender: NSMenuItem) {
- settingsWindowController.show()
}
@IBAction private func switchStyle(_ sender: Any) {
- settingsStyle = settingsStyle == .segmentedControl
- ? .toolbarItems
+ self.preferencesStyle = (self.preferencesStyle == .segmentedControl)
+ ? .toolbarItems
: .segmentedControl
-
- Task {
- try! await NSApp.relaunch() // swiftlint:disable:this force_try
- }
+ relaunch()
}
}
+
+private func relaunch() {
+ let appBundleIdentifier = Bundle.main.bundleIdentifier!
+ NSWorkspace.shared.launchApplication(
+ withBundleIdentifier: appBundleIdentifier,
+ options: NSWorkspace.LaunchOptions.newInstance,
+ additionalEventParamDescriptor: nil,
+ launchIdentifier: nil)
+ NSApp.terminate(nil)
+}
diff --git a/Example/GeneralPreferenceViewController.swift b/Example/GeneralPreferenceViewController.swift
new file mode 100644
index 0000000..39d7732
--- /dev/null
+++ b/Example/GeneralPreferenceViewController.swift
@@ -0,0 +1,18 @@
+import Cocoa
+import Preferences
+
+final class GeneralPreferenceViewController: NSViewController, PreferencePane {
+ let preferencePaneIdentifier: PreferencePaneIdentifier = .general
+ let toolbarItemTitle = "General"
+ let toolbarItemIcon = NSImage(named: NSImage.preferencesGeneralName)!
+
+ override var nibName: NSNib.Name? {
+ return "GeneralPreferenceViewController"
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Setup stuff here
+ }
+}
diff --git a/Example/GeneralPreferenceViewController.xib b/Example/GeneralPreferenceViewController.xib
new file mode 100644
index 0000000..a27daf9
--- /dev/null
+++ b/Example/GeneralPreferenceViewController.xib
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/PreferencesStyle+UserDefaults.swift b/Example/PreferencesStyle+UserDefaults.swift
new file mode 100644
index 0000000..dbc9ec2
--- /dev/null
+++ b/Example/PreferencesStyle+UserDefaults.swift
@@ -0,0 +1,39 @@
+import Foundation
+import Preferences
+
+// Helpers to write styles to and read them from UserDefaults.
+
+extension PreferencesStyle: RawRepresentable {
+ public var rawValue: Int {
+ switch self {
+ case .toolbarItems:
+ return 0
+ case .segmentedControl:
+ return 1
+ }
+ }
+
+ public init?(rawValue: Int) {
+ switch rawValue {
+ case 0:
+ self = .toolbarItems
+ case 1:
+ self = .segmentedControl
+ default:
+ return nil
+ }
+ }
+}
+
+extension PreferencesStyle {
+ static var userDefaultsKey: String { return "preferencesStyle" }
+
+ static func preferencesStyleFromUserDefaults(_ userDefaults: UserDefaults = .standard) -> PreferencesStyle {
+ return PreferencesStyle(rawValue: userDefaults.integer(forKey: PreferencesStyle.userDefaultsKey))
+ ?? .toolbarItems
+ }
+
+ func storeInUserDefaults(_ userDefaults: UserDefaults = .standard) {
+ userDefaults.set(self.rawValue, forKey: PreferencesStyle.userDefaultsKey)
+ }
+}
diff --git a/Preferences.xcodeproj/project.pbxproj b/Preferences.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..415ad6d
--- /dev/null
+++ b/Preferences.xcodeproj/project.pbxproj
@@ -0,0 +1,639 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 5006F25B22534BE9005F506C /* UserInteractionPausableWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5006F25A22534BE9005F506C /* UserInteractionPausableWindow.swift */; };
+ 502B68E72254947B00789D9F /* PreferencesStyle+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B68E52254944600789D9F /* PreferencesStyle+UserDefaults.swift */; };
+ 509EC81C22535E0A00760A3C /* PreferencesStyleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509EC81B22535E0A00760A3C /* PreferencesStyleController.swift */; };
+ 50A412F42196B70100E4A5A8 /* PreferencesStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A412F32196B70100E4A5A8 /* PreferencesStyle.swift */; };
+ 50A412F62196E87900E4A5A8 /* SegmentedControlStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A412F52196E87900E4A5A8 /* SegmentedControlStyleViewController.swift */; };
+ 50A412F82196EAF200E4A5A8 /* ToolbarItemStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A412F72196EAF200E4A5A8 /* ToolbarItemStyleViewController.swift */; };
+ E34E9EEA20E6149B002F8F86 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9EE920E6149B002F8F86 /* AppDelegate.swift */; };
+ E34E9EEC20E6149D002F8F86 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E34E9EEB20E6149D002F8F86 /* Assets.xcassets */; };
+ E34E9EEF20E6149D002F8F86 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = E34E9EED20E6149D002F8F86 /* MainMenu.xib */; };
+ E34E9EF520E61507002F8F86 /* Preferences.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Preferences::Preferences::Product" /* Preferences.framework */; };
+ E34E9EF620E61507002F8F86 /* Preferences.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = "Preferences::Preferences::Product" /* Preferences.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ E34E9EFB20E61557002F8F86 /* PreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9EFA20E61557002F8F86 /* PreferencePane.swift */; };
+ E34E9EFF20E617E7002F8F86 /* GeneralPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9EFE20E617E7002F8F86 /* GeneralPreferenceViewController.swift */; };
+ E34E9F0120E61A26002F8F86 /* GeneralPreferenceViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E34E9F0020E61A26002F8F86 /* GeneralPreferenceViewController.xib */; };
+ E34E9F0320E61BC0002F8F86 /* AdvancedPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9F0220E61BC0002F8F86 /* AdvancedPreferenceViewController.swift */; };
+ E34E9F0520E61C06002F8F86 /* AdvancedPreferenceViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E34E9F0420E61C06002F8F86 /* AdvancedPreferenceViewController.xib */; };
+ E34E9F0820E62743002F8F86 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9F0620E62627002F8F86 /* PreferencesTabViewController.swift */; };
+ OBJ_19 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* PreferencesWindowController.swift */; };
+ OBJ_20 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* util.swift */; };
+ OBJ_27 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ E34E9EF720E61507002F8F86 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = OBJ_1 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = "Preferences::Preferences";
+ remoteInfo = Preferences;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ E34E9EF920E61508002F8F86 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ E34E9EF620E61507002F8F86 /* Preferences.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 5006F25A22534BE9005F506C /* UserInteractionPausableWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInteractionPausableWindow.swift; sourceTree = ""; };
+ 502B68E52254944600789D9F /* PreferencesStyle+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesStyle+UserDefaults.swift"; sourceTree = ""; };
+ 509EC81B22535E0A00760A3C /* PreferencesStyleController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesStyleController.swift; sourceTree = ""; };
+ 50A412F32196B70100E4A5A8 /* PreferencesStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesStyle.swift; sourceTree = ""; };
+ 50A412F52196E87900E4A5A8 /* SegmentedControlStyleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlStyleViewController.swift; sourceTree = ""; };
+ 50A412F72196EAF200E4A5A8 /* ToolbarItemStyleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarItemStyleViewController.swift; sourceTree = ""; };
+ E34E9EE720E6149B002F8F86 /* PreferencesExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PreferencesExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ E34E9EE920E6149B002F8F86 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; usesTabs = 1; };
+ E34E9EEB20E6149D002F8F86 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ E34E9EEE20E6149D002F8F86 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ E34E9EF020E6149D002F8F86 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E34E9EFA20E61557002F8F86 /* PreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PreferencePane.swift; sourceTree = ""; usesTabs = 1; };
+ E34E9EFE20E617E7002F8F86 /* GeneralPreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferenceViewController.swift; sourceTree = ""; };
+ E34E9F0020E61A26002F8F86 /* GeneralPreferenceViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GeneralPreferenceViewController.xib; sourceTree = ""; };
+ E34E9F0220E61BC0002F8F86 /* AdvancedPreferenceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AdvancedPreferenceViewController.swift; sourceTree = ""; usesTabs = 1; };
+ E34E9F0420E61C06002F8F86 /* AdvancedPreferenceViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AdvancedPreferenceViewController.xib; sourceTree = ""; };
+ E34E9F0620E62627002F8F86 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PreferencesTabViewController.swift; sourceTree = ""; usesTabs = 1; };
+ OBJ_10 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.swift; sourceTree = ""; usesTabs = 1; };
+ OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; lineEnding = 0; path = Package.swift; sourceTree = ""; usesTabs = 1; };
+ OBJ_9 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PreferencesWindowController.swift; sourceTree = ""; usesTabs = 1; };
+ "Preferences::Preferences::Product" /* Preferences.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Preferences.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ E34E9EE420E6149B002F8F86 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E34E9EF520E61507002F8F86 /* Preferences.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ OBJ_21 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 0;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ E34E9EE820E6149B002F8F86 /* Example */ = {
+ isa = PBXGroup;
+ children = (
+ E34E9EE920E6149B002F8F86 /* AppDelegate.swift */,
+ 502B68E52254944600789D9F /* PreferencesStyle+UserDefaults.swift */,
+ E34E9EED20E6149D002F8F86 /* MainMenu.xib */,
+ E34E9EFE20E617E7002F8F86 /* GeneralPreferenceViewController.swift */,
+ E34E9F0020E61A26002F8F86 /* GeneralPreferenceViewController.xib */,
+ E34E9F0220E61BC0002F8F86 /* AdvancedPreferenceViewController.swift */,
+ E34E9F0420E61C06002F8F86 /* AdvancedPreferenceViewController.xib */,
+ E34E9EEB20E6149D002F8F86 /* Assets.xcassets */,
+ E34E9EF020E6149D002F8F86 /* Info.plist */,
+ );
+ path = Example;
+ sourceTree = "";
+ };
+ OBJ_12 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ "Preferences::Preferences::Product" /* Preferences.framework */,
+ E34E9EE720E6149B002F8F86 /* PreferencesExample.app */,
+ );
+ name = Products;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ OBJ_5 = {
+ isa = PBXGroup;
+ children = (
+ OBJ_6 /* Package.swift */,
+ OBJ_7 /* Sources */,
+ E34E9EE820E6149B002F8F86 /* Example */,
+ OBJ_12 /* Products */,
+ );
+ sourceTree = "";
+ };
+ OBJ_7 /* Sources */ = {
+ isa = PBXGroup;
+ children = (
+ OBJ_8 /* Preferences */,
+ );
+ name = Sources;
+ sourceTree = SOURCE_ROOT;
+ };
+ OBJ_8 /* Preferences */ = {
+ isa = PBXGroup;
+ children = (
+ E34E9EFA20E61557002F8F86 /* PreferencePane.swift */,
+ 50A412F32196B70100E4A5A8 /* PreferencesStyle.swift */,
+ OBJ_9 /* PreferencesWindowController.swift */,
+ 5006F25A22534BE9005F506C /* UserInteractionPausableWindow.swift */,
+ E34E9F0620E62627002F8F86 /* PreferencesTabViewController.swift */,
+ 509EC81B22535E0A00760A3C /* PreferencesStyleController.swift */,
+ 50A412F52196E87900E4A5A8 /* SegmentedControlStyleViewController.swift */,
+ 50A412F72196EAF200E4A5A8 /* ToolbarItemStyleViewController.swift */,
+ OBJ_10 /* util.swift */,
+ );
+ name = Preferences;
+ path = Sources/Preferences;
+ sourceTree = SOURCE_ROOT;
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ E34E9EE620E6149B002F8F86 /* PreferencesExample */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E34E9EF220E6149D002F8F86 /* Build configuration list for PBXNativeTarget "PreferencesExample" */;
+ buildPhases = (
+ E34E9EE320E6149B002F8F86 /* Sources */,
+ E34E9EE420E6149B002F8F86 /* Frameworks */,
+ E34E9EE520E6149B002F8F86 /* Resources */,
+ E34E9EF920E61508002F8F86 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ E34E9EF820E61507002F8F86 /* PBXTargetDependency */,
+ );
+ name = PreferencesExample;
+ productName = PreferencesExample;
+ productReference = E34E9EE720E6149B002F8F86 /* PreferencesExample.app */;
+ productType = "com.apple.product-type.application";
+ };
+ "Preferences::Preferences" /* Preferences */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = OBJ_15 /* Build configuration list for PBXNativeTarget "Preferences" */;
+ buildPhases = (
+ E3BFC67821FBBC8B00C16B1A /* SwiftLint */,
+ OBJ_18 /* Sources */,
+ OBJ_21 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Preferences;
+ productName = Preferences;
+ productReference = "Preferences::Preferences::Product" /* Preferences.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ "Preferences::SwiftPMPackageDescription" /* PreferencesPackageDescription */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = OBJ_23 /* Build configuration list for PBXNativeTarget "PreferencesPackageDescription" */;
+ buildPhases = (
+ OBJ_26 /* Sources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PreferencesPackageDescription;
+ productName = PreferencesPackageDescription;
+ productType = "com.apple.product-type.framework";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ OBJ_1 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0940;
+ LastUpgradeCheck = 0940;
+ TargetAttributes = {
+ E34E9EE620E6149B002F8F86 = {
+ CreatedOnToolsVersion = 9.4.1;
+ LastSwiftMigration = 1020;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 0;
+ };
+ };
+ };
+ "Preferences::Preferences" = {
+ LastSwiftMigration = 1020;
+ };
+ };
+ };
+ buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Preferences" */;
+ compatibilityVersion = "Xcode 10.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = OBJ_5;
+ productRefGroup = OBJ_12 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ "Preferences::Preferences" /* Preferences */,
+ "Preferences::SwiftPMPackageDescription" /* PreferencesPackageDescription */,
+ E34E9EE620E6149B002F8F86 /* PreferencesExample */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ E34E9EE520E6149B002F8F86 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E34E9EEC20E6149D002F8F86 /* Assets.xcassets in Resources */,
+ E34E9F0520E61C06002F8F86 /* AdvancedPreferenceViewController.xib in Resources */,
+ E34E9EEF20E6149D002F8F86 /* MainMenu.xib in Resources */,
+ E34E9F0120E61A26002F8F86 /* GeneralPreferenceViewController.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ E3BFC67821FBBC8B00C16B1A /* SwiftLint */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = SwiftLint;
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed\"\nfi\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ E34E9EE320E6149B002F8F86 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E34E9EFF20E617E7002F8F86 /* GeneralPreferenceViewController.swift in Sources */,
+ E34E9EEA20E6149B002F8F86 /* AppDelegate.swift in Sources */,
+ E34E9F0320E61BC0002F8F86 /* AdvancedPreferenceViewController.swift in Sources */,
+ 502B68E72254947B00789D9F /* PreferencesStyle+UserDefaults.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ OBJ_18 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 0;
+ files = (
+ 509EC81C22535E0A00760A3C /* PreferencesStyleController.swift in Sources */,
+ OBJ_19 /* PreferencesWindowController.swift in Sources */,
+ E34E9F0820E62743002F8F86 /* PreferencesTabViewController.swift in Sources */,
+ 5006F25B22534BE9005F506C /* UserInteractionPausableWindow.swift in Sources */,
+ 50A412F82196EAF200E4A5A8 /* ToolbarItemStyleViewController.swift in Sources */,
+ 50A412F62196E87900E4A5A8 /* SegmentedControlStyleViewController.swift in Sources */,
+ 50A412F42196B70100E4A5A8 /* PreferencesStyle.swift in Sources */,
+ E34E9EFB20E61557002F8F86 /* PreferencePane.swift in Sources */,
+ OBJ_20 /* util.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ OBJ_26 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 0;
+ files = (
+ OBJ_27 /* Package.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ E34E9EF820E61507002F8F86 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = "Preferences::Preferences" /* Preferences */;
+ targetProxy = E34E9EF720E61507002F8F86 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ E34E9EED20E6149D002F8F86 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E34E9EEE20E6149D002F8F86 /* Base */,
+ );
+ name = MainMenu.xib;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ E34E9EF320E6149D002F8F86 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Manual;
+ COMBINE_HIDPI_IMAGES = YES;
+ DEVELOPMENT_TEAM = "";
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ INFOPLIST_FILE = Example/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.PreferencesExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ E34E9EF420E6149D002F8F86 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Manual;
+ COMBINE_HIDPI_IMAGES = YES;
+ COPY_PHASE_STRIP = NO;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_NS_ASSERTIONS = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ INFOPLIST_FILE = Example/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.PreferencesExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ OBJ_16 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = YG56YK5RN5;
+ ENABLE_TESTABILITY = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PLATFORM_DIR)/Developer/Library/Frameworks",
+ );
+ HEADER_SEARCH_PATHS = "$(inherited)";
+ INFOPLIST_FILE = Preferences.xcodeproj/Preferences_Info.plist;
+ OTHER_CFLAGS = "$(inherited)";
+ OTHER_LDFLAGS = "$(inherited)";
+ OTHER_SWIFT_FLAGS = "$(inherited)";
+ PRODUCT_BUNDLE_IDENTIFIER = Preferences;
+ PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ TARGET_NAME = Preferences;
+ };
+ name = Debug;
+ };
+ OBJ_17 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = YG56YK5RN5;
+ ENABLE_TESTABILITY = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PLATFORM_DIR)/Developer/Library/Frameworks",
+ );
+ HEADER_SEARCH_PATHS = "$(inherited)";
+ INFOPLIST_FILE = Preferences.xcodeproj/Preferences_Info.plist;
+ OTHER_CFLAGS = "$(inherited)";
+ OTHER_LDFLAGS = "$(inherited)";
+ OTHER_SWIFT_FLAGS = "$(inherited)";
+ PRODUCT_BUNDLE_IDENTIFIER = Preferences;
+ PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ TARGET_NAME = Preferences;
+ };
+ name = Release;
+ };
+ OBJ_24 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ LD = /usr/bin/true;
+ OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk";
+ SWIFT_VERSION = 4.0;
+ };
+ name = Debug;
+ };
+ OBJ_25 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ LD = /usr/bin/true;
+ OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk";
+ SWIFT_VERSION = 4.0;
+ };
+ name = Release;
+ };
+ OBJ_3 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COMBINE_HIDPI_IMAGES = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_NS_ASSERTIONS = YES;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.10;
+ ONLY_ACTIVE_ARCH = YES;
+ OTHER_SWIFT_FLAGS = "-DXcode";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = macosx;
+ SUPPORTED_PLATFORMS = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ USE_HEADERMAP = NO;
+ VALID_ARCHS = x86_64;
+ };
+ name = Debug;
+ };
+ OBJ_4 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COMBINE_HIDPI_IMAGES = YES;
+ COPY_PHASE_STRIP = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = s;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.10;
+ OTHER_SWIFT_FLAGS = "-DXcode";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = macosx;
+ SUPPORTED_PLATFORMS = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ USE_HEADERMAP = NO;
+ VALID_ARCHS = x86_64;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ E34E9EF220E6149D002F8F86 /* Build configuration list for PBXNativeTarget "PreferencesExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E34E9EF320E6149D002F8F86 /* Debug */,
+ E34E9EF420E6149D002F8F86 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ OBJ_15 /* Build configuration list for PBXNativeTarget "Preferences" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ OBJ_16 /* Debug */,
+ OBJ_17 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ OBJ_2 /* Build configuration list for PBXProject "Preferences" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ OBJ_3 /* Debug */,
+ OBJ_4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ OBJ_23 /* Build configuration list for PBXNativeTarget "PreferencesPackageDescription" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ OBJ_24 /* Debug */,
+ OBJ_25 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = OBJ_1 /* Project object */;
+}
diff --git a/Sources/Preferences/PreferencePane.swift b/Sources/Preferences/PreferencePane.swift
new file mode 100644
index 0000000..357d5fb
--- /dev/null
+++ b/Sources/Preferences/PreferencePane.swift
@@ -0,0 +1,46 @@
+import Cocoa
+
+public struct PreferencePaneIdentifier: Equatable, RawRepresentable {
+ public let rawValue: String
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+}
+
+public protocol PreferencePane: AnyObject {
+ var preferencePaneIdentifier: PreferencePaneIdentifier { get }
+ var toolbarItemTitle: String { get }
+ var toolbarItemIcon: NSImage { get }
+ var viewController: NSViewController { get }
+}
+
+extension PreferencePane where Self: NSViewController {
+ public var viewController: NSViewController {
+ return self
+ }
+}
+
+extension PreferencePane {
+ public var toolbarItemIdentifier: NSToolbarItem.Identifier {
+ return preferencePaneIdentifier.toolbarItemIdentifier
+ }
+
+ public var toolbarItemIcon: NSImage {
+ return NSImage(size: .zero)
+ }
+}
+
+extension PreferencePaneIdentifier {
+ public init(_ rawValue: String) {
+ self.init(rawValue: rawValue)
+ }
+
+ public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) {
+ self.init(rawValue: itemIdentifier.rawValue)
+ }
+
+ public var toolbarItemIdentifier: NSToolbarItem.Identifier {
+ return NSToolbarItem.Identifier(rawValue)
+ }
+}
diff --git a/Sources/Preferences/PreferencesStyle.swift b/Sources/Preferences/PreferencesStyle.swift
new file mode 100644
index 0000000..6d33ec4
--- /dev/null
+++ b/Sources/Preferences/PreferencesStyle.swift
@@ -0,0 +1,17 @@
+import Cocoa
+
+public enum PreferencesStyle {
+ case toolbarItems
+ case segmentedControl
+}
+
+extension PreferencesStyle {
+ var windowTitleVisibility: NSWindow.TitleVisibility {
+ switch self {
+ case .toolbarItems:
+ return .visible
+ case .segmentedControl:
+ return .hidden
+ }
+ }
+}
diff --git a/Sources/Preferences/PreferencesStyleController.swift b/Sources/Preferences/PreferencesStyleController.swift
new file mode 100644
index 0000000..3342081
--- /dev/null
+++ b/Sources/Preferences/PreferencesStyleController.swift
@@ -0,0 +1,16 @@
+import Cocoa
+
+protocol PreferencesStyleController: AnyObject {
+ var delegate: PreferencesStyleControllerDelegate? { get set }
+ var isKeepingWindowCentered: Bool { get }
+
+ func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
+ func toolbarItem(preferenceIdentifier: PreferencePaneIdentifier) -> NSToolbarItem?
+
+ func selectTab(index: Int)
+}
+
+protocol PreferencesStyleControllerDelegate: AnyObject {
+ func activateTab(preferenceIdentifier: PreferencePaneIdentifier?, animated: Bool)
+ func activateTab(index: Int, animated: Bool)
+}
diff --git a/Sources/Preferences/PreferencesTabViewController.swift b/Sources/Preferences/PreferencesTabViewController.swift
new file mode 100644
index 0000000..58c6d73
--- /dev/null
+++ b/Sources/Preferences/PreferencesTabViewController.swift
@@ -0,0 +1,177 @@
+import Cocoa
+
+final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate {
+ private var activeTab: Int!
+ private var preferencePanes: [PreferencePane] = []
+
+ private var preferencesStyleController: PreferencesStyleController!
+
+ private var isKeepingWindowCentered: Bool {
+ return preferencesStyleController.isKeepingWindowCentered
+ }
+
+ private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] {
+ return preferencesStyleController?.toolbarItemIdentifiers() ?? []
+ }
+
+ var window: NSWindow! {
+ return view.window
+ }
+
+ var isAnimated: Bool = true
+
+ override func loadView() {
+ self.view = NSView()
+ self.view.translatesAutoresizingMaskIntoConstraints = false
+ }
+
+ func configure(preferencePanes: [PreferencePane]) {
+ self.preferencePanes = preferencePanes
+ self.children = preferencePanes.map { $0.viewController }
+ }
+
+ func changePreferencesStyle(to newStyle: PreferencesStyle) {
+ changePreferencesStyleController(preferences: self.preferencePanes, style: newStyle)
+ }
+
+ private func changePreferencesStyleController(preferences: [PreferencePane], style: PreferencesStyle) {
+ let toolbar = NSToolbar(identifier: "PreferencesToolbar")
+ toolbar.allowsUserCustomization = false
+ toolbar.displayMode = .iconAndLabel
+ toolbar.showsBaselineSeparator = true
+ toolbar.delegate = self
+
+ switch style {
+ case .segmentedControl:
+ self.preferencesStyleController = SegmentedControlStyleViewController(preferences: preferences)
+ case .toolbarItems:
+ self.preferencesStyleController = ToolbarItemStyleViewController(preferences: preferences, toolbar: toolbar, centerToolbarItems: false)
+ }
+ self.preferencesStyleController.delegate = self
+
+ self.window.toolbar = toolbar // Call latest so that preferencesStyleController can be asked for items
+ }
+
+ func activateTab(preference: PreferencePane?, animated: Bool) {
+ guard let preference = preference else {
+ return activateTab(index: 0, animated: animated)
+ }
+ activateTab(preferenceIdentifier: preference.preferencePaneIdentifier, animated: animated)
+ }
+
+ func activateTab(preferenceIdentifier: PreferencePaneIdentifier?, animated: Bool) {
+ guard let preferenceIdentifier = preferenceIdentifier,
+ let index = preferencePanes.firstIndex(where: { $0.preferencePaneIdentifier == preferenceIdentifier })
+ else { return activateTab(index: 0, animated: animated) }
+ activateTab(index: index, animated: animated)
+ }
+
+ func activateTab(index: Int, animated: Bool) {
+ defer {
+ activeTab = index
+ preferencesStyleController.selectTab(index: index)
+ }
+
+ if self.activeTab == nil {
+ immediatelyDisplayTab(index: index)
+ } else {
+ guard index != activeTab else {
+ return
+ }
+ animateTabTransition(index: index, animated: animated)
+ }
+ }
+
+ /// Cached constraints that pin childViewController views to the content view
+ private var activeChildViewConstraints: [NSLayoutConstraint] = []
+
+ private func immediatelyDisplayTab(index: Int) {
+ let toViewController = children[index]
+ view.addSubview(toViewController.view)
+ activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
+ setWindowFrame(for: toViewController, animated: false)
+ }
+
+ private func animateTabTransition(index: Int, animated: Bool) {
+ let fromViewController = children[activeTab]
+ let toViewController = children[index]
+ let options: NSViewController.TransitionOptions = animated && isAnimated
+ ? [.crossfade]
+ : []
+
+ view.removeConstraints(activeChildViewConstraints)
+
+ transition(
+ from: fromViewController,
+ to: toViewController,
+ options: options) {
+ self.activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
+ }
+ }
+
+ override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
+ let isAnimated = options
+ .intersection([.crossfade, .slideUp, .slideDown, .slideForward, .slideBackward, .slideLeft, .slideRight])
+ .isEmpty == false
+
+ if isAnimated {
+ NSAnimationContext.runAnimationGroup({ context in
+ context.allowsImplicitAnimation = true
+ context.duration = 0.25
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+ setWindowFrame(for: toViewController, animated: true)
+ super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
+ }, completionHandler: nil)
+ } else {
+ super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
+ }
+ }
+
+ private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) {
+ guard let window = window else { preconditionFailure() }
+ let contentSize = viewController.view.fittingSize
+
+ let newWindowSize = window.frameRect(forContentRect: CGRect(origin: .zero, size: contentSize)).size
+ var frame = window.frame
+ frame.origin.y += frame.height - newWindowSize.height
+ frame.size = newWindowSize
+
+ if isKeepingWindowCentered {
+ let horizontalDiff = (window.frame.width - newWindowSize.width) / 2.0
+ frame.origin.x += horizontalDiff
+ }
+
+ let animatableWindow = animated ? window.animator() : window
+ animatableWindow.setFrame(frame, display: false)
+ }
+}
+
+extension PreferencesTabViewController: NSToolbarDelegate {
+ func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
+ return toolbarItemIdentifiers
+ }
+
+ func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
+ return toolbarItemIdentifiers
+ }
+
+ func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
+ return toolbarItemIdentifiers
+ }
+
+ public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
+ if itemIdentifier == .flexibleSpace {
+ return nil
+ }
+
+ return preferencesStyleController.toolbarItem(preferenceIdentifier: PreferencePaneIdentifier(fromToolbarItemIdentifier: itemIdentifier))
+ }
+}
+
+extension NSWindow {
+ var effectiveMinSize: NSSize {
+ return (contentMinSize != .zero)
+ ? frameRect(forContentRect: NSRect(origin: .zero, size: contentMinSize)).size
+ : minSize
+ }
+}
diff --git a/Sources/Preferences/PreferencesWindowController.swift b/Sources/Preferences/PreferencesWindowController.swift
new file mode 100644
index 0000000..c291813
--- /dev/null
+++ b/Sources/Preferences/PreferencesWindowController.swift
@@ -0,0 +1,70 @@
+import Cocoa
+
+public final class PreferencesWindowController: NSWindowController {
+ private let tabViewController = PreferencesTabViewController()
+
+ public var isAnimated: Bool {
+ get {
+ return tabViewController.isAnimated
+ }
+ set {
+ tabViewController.isAnimated = newValue
+ }
+ }
+
+ public init(preferencePanes: [PreferencePane], style: PreferencesStyle = .toolbarItems, animated: Bool = true) {
+ precondition(!preferencePanes.isEmpty, "You need to set at least one view controller")
+
+ let window = UserInteractionPausableWindow(
+ contentRect: preferencePanes[0].viewController.view.bounds,
+ styleMask: [
+ .titled,
+ .closable
+ ],
+ backing: .buffered,
+ defer: true
+ )
+ super.init(window: window)
+
+ window.title = String(System.localizedString(forKey: "Preferences…").dropLast())
+ window.contentViewController = tabViewController
+ tabViewController.isAnimated = animated
+ tabViewController.configure(preferencePanes: preferencePanes)
+ changePreferencesStyle(to: style)
+ }
+
+ @available(*, unavailable)
+ override public init(window: NSWindow?) {
+ fatalError("init(window:) is not supported, use init(preferences:style:animated:)")
+ }
+
+ @available(*, unavailable)
+ public required init?(coder: NSCoder) {
+ fatalError("init(coder:) is not supported, use init(preferences:style:animated:)")
+ }
+
+ private func changePreferencesStyle(to newStyle: PreferencesStyle) {
+ window?.titleVisibility = newStyle.windowTitleVisibility
+ tabViewController.changePreferencesStyle(to: newStyle)
+ }
+
+ /// Show the preferences window and brings it to front.
+ ///
+ /// If you pass a `PreferencePaneIdentifier`, the window will activate the corresponding tab.
+ ///
+ /// - Note: Unless you need to open a specific pane, prefer not to pass a parameter at all
+ /// - Parameter preferencePane: Identifier of the preference pane to display.
+ public func show(preferencePane preferenceIdentifier: PreferencePaneIdentifier? = nil) {
+ if !window!.isVisible {
+ window?.center()
+ }
+
+ showWindow(self)
+ tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false)
+ NSApp.activate(ignoringOtherApps: true)
+ }
+
+ public func hideWindow() {
+ close()
+ }
+}
diff --git a/Sources/Preferences/SegmentedControlStyleViewController.swift b/Sources/Preferences/SegmentedControlStyleViewController.swift
new file mode 100644
index 0000000..e1f9361
--- /dev/null
+++ b/Sources/Preferences/SegmentedControlStyleViewController.swift
@@ -0,0 +1,137 @@
+import Cocoa
+
+extension NSToolbarItem.Identifier {
+ static var toolbarSegmentedControlItem: NSToolbarItem.Identifier {
+ return NSToolbarItem.Identifier(rawValue: "toolbarSegmentedControlItem")
+ }
+}
+
+extension NSUserInterfaceItemIdentifier {
+ static var toolbarSegmentedControl: NSUserInterfaceItemIdentifier {
+ return NSUserInterfaceItemIdentifier(rawValue: "toolbarSegmentedControl")
+ }
+}
+
+final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController {
+ var segmentedControl: NSSegmentedControl! {
+ get {
+ return view as? NSSegmentedControl
+ }
+ set {
+ view = newValue
+ }
+ }
+
+ var isKeepingWindowCentered: Bool {
+ return true
+ }
+
+ weak var delegate: PreferencesStyleControllerDelegate?
+
+ private var preferences: [PreferencePane]!
+
+ required init(preferences: [PreferencePane]) {
+ super.init(nibName: nil, bundle: nil)
+ self.preferences = preferences
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ self.view = createSegmentedControl(preferences: self.preferences)
+ }
+
+ fileprivate func createSegmentedControl(preferences: [PreferencePane]) -> NSSegmentedControl {
+ let segmentedControl = NSSegmentedControl()
+ segmentedControl.segmentCount = preferences.count
+ segmentedControl.segmentStyle = .texturedSquare
+ segmentedControl.target = self
+ segmentedControl.action = #selector(SegmentedControlStyleViewController.segmentedControlAction(_:))
+ segmentedControl.identifier = .toolbarSegmentedControl
+
+ if let cell = segmentedControl.cell as? NSSegmentedCell {
+ cell.controlSize = .regular
+ cell.trackingMode = .selectOne
+ }
+
+ let segmentSize: NSSize = {
+ let insets = NSSize(width: 36, height: 12)
+ var maxSize = NSSize(width: 0, height: 0)
+
+ for preference in preferences {
+ let title = preference.toolbarItemTitle
+ let titleSize = title.size(withAttributes: [.font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular))])
+
+ maxSize = NSSize(width: max(titleSize.width, maxSize.width),
+ height: max(titleSize.height, maxSize.height))
+ }
+
+ return NSSize(width: maxSize.width + insets.width,
+ height: maxSize.height + insets.height)
+ }()
+
+ let segmentBorderWidth = CGFloat(preferences.count) + 1.0
+ let segmentWidth = segmentSize.width * CGFloat(preferences.count) + segmentBorderWidth
+ let segmentHeight = segmentSize.height
+ segmentedControl.frame = NSRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
+
+ for (index, preference) in preferences.enumerated() {
+ segmentedControl.setLabel(preference.toolbarItemTitle, forSegment: index)
+ segmentedControl.setWidth(segmentSize.width, forSegment: index)
+ if let cell = segmentedControl.cell as? NSSegmentedCell {
+ cell.setTag(index, forSegment: index)
+ }
+ }
+
+ return segmentedControl
+ }
+
+ @IBAction private func segmentedControlAction(_ control: NSSegmentedControl) {
+ delegate?.activateTab(index: control.selectedSegment, animated: true)
+ }
+
+ func selectTab(index: Int) {
+ segmentedControl.selectedSegment = index
+ }
+
+ func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
+ return [
+ .flexibleSpace,
+ .toolbarSegmentedControlItem,
+ .flexibleSpace
+ ]
+ }
+
+ func toolbarItem(preferenceIdentifier: PreferencePaneIdentifier) -> NSToolbarItem? {
+ let toolbarItemIdentifier = preferenceIdentifier.toolbarItemIdentifier
+ precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem)
+
+ // When the segments outgrow the window, we need to provide a group of
+ // NSToolbarItems with custom menu item labels and action handling for the
+ // context menu that pops up at the right edge of the window.
+ let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier)
+ toolbarItemGroup.view = segmentedControl
+ toolbarItemGroup.subitems = preferences.enumerated().map { index, preferenceable -> NSToolbarItem in
+ let item = NSToolbarItem(itemIdentifier: .init(rawValue: "segment-\(preferenceable.toolbarItemTitle)"))
+ item.label = preferenceable.toolbarItemTitle
+
+ let menuItem = NSMenuItem(
+ title: preferenceable.toolbarItemTitle,
+ action: #selector(segmentedControlMenuAction(_:)),
+ keyEquivalent: "")
+ menuItem.tag = index
+ menuItem.target = self
+ item.menuFormRepresentation = menuItem
+
+ return item
+ }
+ return toolbarItemGroup
+ }
+
+ @IBAction private func segmentedControlMenuAction(_ menuItem: NSMenuItem) {
+ delegate?.activateTab(index: menuItem.tag, animated: true)
+ }
+}
diff --git a/Sources/Preferences/ToolbarItemStyleViewController.swift b/Sources/Preferences/ToolbarItemStyleViewController.swift
new file mode 100644
index 0000000..1d25f17
--- /dev/null
+++ b/Sources/Preferences/ToolbarItemStyleViewController.swift
@@ -0,0 +1,60 @@
+import Cocoa
+
+final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController {
+ let toolbar: NSToolbar
+ let centerToolbarItems: Bool
+ let preferences: [PreferencePane]
+
+ var isKeepingWindowCentered: Bool {
+ return centerToolbarItems
+ }
+
+ weak var delegate: PreferencesStyleControllerDelegate?
+
+ init(preferences: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) {
+ self.preferences = preferences
+ self.toolbar = toolbar
+ self.centerToolbarItems = centerToolbarItems
+ }
+
+ func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
+ var toolbarItemIdentifiers: [NSToolbarItem.Identifier] = []
+
+ if centerToolbarItems {
+ toolbarItemIdentifiers.append(.flexibleSpace)
+ }
+
+ for preference in preferences {
+ toolbarItemIdentifiers.append(preference.toolbarItemIdentifier)
+ }
+
+ if centerToolbarItems {
+ toolbarItemIdentifiers.append(.flexibleSpace)
+ }
+
+ return toolbarItemIdentifiers
+ }
+
+ func toolbarItem(preferenceIdentifier: PreferencePaneIdentifier) -> NSToolbarItem? {
+ guard let preference = preferences.first(where: { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
+ preconditionFailure()
+ }
+
+ let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier)
+ toolbarItem.label = preference.toolbarItemTitle
+ toolbarItem.image = preference.toolbarItemIcon
+ toolbarItem.target = self
+ toolbarItem.action = #selector(ToolbarItemStyleViewController.toolbarItemSelected(_:))
+ return toolbarItem
+ }
+
+ @IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
+ delegate?.activateTab(
+ preferenceIdentifier: PreferencePaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
+ animated: true)
+ }
+
+ func selectTab(index: Int) {
+ toolbar.selectedItemIdentifier = preferences[index].toolbarItemIdentifier
+ }
+}
diff --git a/Sources/Preferences/UserInteractionPausableWindow.swift b/Sources/Preferences/UserInteractionPausableWindow.swift
new file mode 100644
index 0000000..65c9659
--- /dev/null
+++ b/Sources/Preferences/UserInteractionPausableWindow.swift
@@ -0,0 +1,31 @@
+import Cocoa
+
+/// 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 {
+ var isUserInteractionEnabled: Bool = true
+
+ let pausableUserEventTypes: [NSEvent.EventType] = {
+ var result: [NSEvent.EventType] = [
+ .leftMouseDown, .leftMouseUp, .rightMouseDown, .rightMouseUp, .leftMouseDragged, .rightMouseDragged,
+ .keyDown, .keyUp, .scrollWheel, .tabletPoint, .otherMouseDown, .otherMouseUp, .otherMouseDragged,
+ .gesture, .magnify, .swipe, .rotate, .beginGesture, .endGesture, .smartMagnify, .quickLook, .directTouch
+ ]
+
+ if #available(macOS 10.10.3, *) {
+ result.append(.pressure)
+ }
+
+ return result
+ }()
+
+ override func sendEvent(_ event: NSEvent) {
+ if !isUserInteractionEnabled && pausableUserEventTypes.contains(event.type) {
+ return
+ }
+
+ super.sendEvent(event)
+ }
+}
diff --git a/Sources/Preferences/util.swift b/Sources/Preferences/util.swift
new file mode 100644
index 0000000..1f261ec
--- /dev/null
+++ b/Sources/Preferences/util.swift
@@ -0,0 +1,47 @@
+import Cocoa
+
+struct System {
+ /// Get a system localized string
+ /// Use https://itunes.apple.com/no/app/system-strings/id570467776 to find strings
+ static func localizedString(forKey key: String) -> String {
+ return Bundle(for: NSApplication.self).localizedString(forKey: key, value: nil, table: nil)
+ }
+}
+
+extension NSObject {
+ /// Returns the class name without module name
+ class var simpleClassName: String {
+ return String(describing: self)
+ }
+
+ /// Returns the class name of the instance without module name
+ var simpleClassName: String {
+ return type(of: self).simpleClassName
+ }
+}
+
+extension Collection {
+ func map(_ transform: (Element) throws -> (key: T, value: U)) rethrows -> [T: U] {
+ var result: [T: U] = [:]
+ for element in self {
+ let transformation = try transform(element)
+ result[transformation.key] = transformation.value
+ }
+ return result
+ }
+}
+
+extension NSView {
+ @discardableResult
+ func constrainToSuperviewBounds() -> [NSLayoutConstraint] {
+ guard let superview = self.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]))
+ self.translatesAutoresizingMaskIntoConstraints = false
+ superview.addConstraints(result)
+
+ return result
+ }
+}
diff --git a/screenshot.gif b/images/screenshot.gif
similarity index 99%
rename from screenshot.gif
rename to images/screenshot.gif
index 586897a..f89922b 100644
Binary files a/screenshot.gif and b/images/screenshot.gif differ
diff --git a/images/segmented-control.png b/images/segmented-control.png
new file mode 100644
index 0000000..32edc58
Binary files /dev/null and b/images/segmented-control.png differ
diff --git a/images/toolbar-item.png b/images/toolbar-item.png
new file mode 100644
index 0000000..2a9c0e4
Binary files /dev/null and b/images/toolbar-item.png differ
diff --git a/readme.md b/readme.md
index 46fcfcc..114d4e9 100644
--- a/readme.md
+++ b/readme.md
@@ -33,26 +33,61 @@ extension Settings.PaneIdentifier {
}
```
-Second, create a couple of view controllers for the settings panes you want. The only difference from implementing a normal view controller is that you have to add the `SettingsPane` protocol and implement the `paneIdentifier`, `toolbarItemTitle`, and `toolbarItemIcon` properties, as shown below. You can leave out `toolbarItemIcon` if you're using the `.segmentedControl` style.
+<<<>>>>>>-main
+===
+
+
+
+
+
+## Usage
+
+*Run the `PreferencesExample` target in Xcode to try a live example.*
+
+First, create a collection of preference pane identifiers:
```swift
+import Preferences
+
+extension PreferencePaneIdentifier {
+ static let general = PreferencePaneIdentifier("general")
+ static let advanced = PreferencePaneIdentifier("advanced")
+}
+```
+
+Second, create a couple of view controllers for the preference panes you want. The only difference from implementing a normal view controller is that you have to add the `Preferenceable` protocol and implement the `toolbarItemTitle` and `toolbarItemIcon` getters, as shown below.
+
+`GeneralPreferenceViewController.swift`
+>>>>>>>-3b62df8
+swift
import Cocoa
import Settings
-final class GeneralSettingsViewController: NSViewController, SettingsPane {
+<<<>>>>>>-main
+===
+finafinal class GeneralPreferenceViewController: NSViewController, PreferencePane {
+ let preferencePaneIdentifier: PreferencePaneIdentifier = .general
+ let toolbarItemTitle = "General"
+ let toolbarItemIcon = NSImage(named: NSImage.preferencesGeneralName)!
+
+ override var nibName: NSNib.Name? {
+ return "GeneralPreferenceViewController"
+ }
+>>>>>>>-3b62df8
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Setup stuff here
+ }
}
```
@@ -64,21 +99,240 @@ Note: If you need to support macOS versions older than macOS 11, you have to add
import Cocoa
import Settings
-final class AdvancedSettingsViewController: NSViewController, SettingsPane {
+<<<>>>>>>+main
+===
+finafinal class AdvancedPreferenceViewController: NSViewController, PreferencePane {
+ let preferencePaneIdentifier: PreferencePaneIdentifier = .advanced
+ let toolbarItemTitle = "Advanced"
+ let toolbarItemIcon = NSImage(named: NSImage.advancedName)!
+
+ override var nibName: NSNib.Name? {
+ return "AdvancedPreferenceViewController"
+ }
+>>>>>>>-3b62df8
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Setup stuff here
+ }
+}
+```
+
+If you need to respond actions indirectly, the settings window controller will forward responder chain actions to the active pane if it responds to that selector.
- override func viewDidLoad() {
- super.viewDidLoad()
+```swift
+final class AdvancedSettingsViewController: NSViewController, SettingsPane {
+ @IBOutlet private var fontLabel: NSTextField!
+ private var selectedFont = NSFont.systemFont(ofSize: 14)
- // Setup stuff here
+ @IBAction private func changeFont(_ sender: NSFontManager) {
+ font = sender.convert(font)
}
}
```
+In the `AppDelegate`, initialize a new `SettingsWindowController` and pass it the view controllers. Then add an action outlet for the `Settings…` menu item to show the settings window.
+
+`AppDelegate.swift`
+
+```swift
+import Cocoa
+import Settings
+
+@main
+final class AppDelegate: NSObject, NSApplicationDelegate {
+ @IBOutlet private var window: NSWindow!
+
+<<<<<< private lazy var settingsWindowController = SettingsWindowController(
+ panes: [
+ GeneralSettingsViewController(),
+ AdvancedSettingsViewController()
+ ]
+ )
+>>>>>>>-main
+=
+ le let preferencesWindowController = PreferencesWindowController(
+ preferences: [
+ GeneralPreferenceViewController(),
+ AdvancedPreferenceViewController()
+ ]
+ )
+>>>>>>>-3b62df8
+unc applicationDidFinishLaunching(_ notification: Notification) {}
+
+<<<<<< @IBAction
+ func settingsMenuItemActionHandler(_ sender: NSMenuItem) {
+ settingsWindowController.show()
+ }
+}
+```
+
+### Settings Tab Styles
+
+When you create the `SettingsWindowController`, you can choose between the `NSToolbarItem`-based style (default) and the `NSSegmentedControl`:
+
+```swift
+// …
+private lazy var settingsWindowController = SettingsWindowController(
+ panes: [
+ GeneralSettingsViewController(),
+ AdvancedSettingsViewController()
+ ],
+ style: .segmentedControl
+)
+// …
+>>>>>>>-main
+ @IBAct @IBAction
+ func preferencesMenuItemActionHandler(_ sender: NSMenuItem) {
+ preferencesWindowController.showWindow()
+ }
+}
+```
+
+### Preferences Tab Styles
+
+When you create the `PreferencesWindowController`, you can also switch between the `NSToolbarItem`-based style (default) and the `NSSegmentedControl`:
+
+```swift
+ // ...
+ let preferencesWindowController = PreferencesWindowController(
+ preferences: [
+ GeneralPreferenceViewController(),
+ AdvancedPreferenceViewController()
+ ],
+ style: .segmentedControl
+ )
+ // ...
+>>>>>>>-3b62df8
+lbarItem` style:
+
+
+Just pass in some view controllers and this package will take care of the rest. Built-in SwiftUI support.
+
+*This package is compatible with macOS 13 and automatically uses `Settings` instead of `Preferences` in the window title on macOS 13 and later.*
+
+*This project was previously known as `Preferences`.*
+
+## Requirements
+
+macOS 10.13 and later.
+
+## Install
+
+Add `https://github.com/sindresorhus/Settings` in the [“Swift Package Manager” tab in Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
+
+## Usage
+
+*Run the `Example` Xcode project to try a live example (requires macOS 11 or later).*
+
+First, create some settings pane identifiers:
+
+```swift
+import Settings
+
+extension Settings.PaneIdentifier {
+ static let general = Self("general")
+ static let advanced = Self("advanced")
+}
+```
+
+<<<>>>>>>-main
+===
+
+
+
+
+
+## Usage
+
+*Run the `PreferencesExample` target in Xcode to try a live example.*
+
+First, create a collection of preference pane identifiers:
+
+```swift
+import Preferences
+
+extension PreferencePaneIdentifier {
+ static let general = PreferencePaneIdentifier("general")
+ static let advanced = PreferencePaneIdentifier("advanced")
+}
+```
+
+Second, create a couple of view controllers for the preference panes you want. The only difference from implementing a normal view controller is that you have to add the `Preferenceable` protocol and implement the `toolbarItemTitle` and `toolbarItemIcon` getters, as shown below.
+
+`GeneralPreferenceViewController.swift`
+>>>>>>>-3b62df8
+swift
+import Cocoa
+import Settings
+
+<<<>>>>>>-main
+===
+finafinal class GeneralPreferenceViewController: NSViewController, PreferencePane {
+ let preferencePaneIdentifier: PreferencePaneIdentifier = .general
+ let toolbarItemTitle = "General"
+ let toolbarItemIcon = NSImage(named: NSImage.preferencesGeneralName)!
+
+ override var nibName: NSNib.Name? {
+ return "GeneralPreferenceViewController"
+ }
+>>>>>>>-3b62df8
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Setup stuff here
+ }
+}
+```
+
+Note: If you need to support macOS versions older than macOS 11, you have to add a [fallback for the `toolbarItemIcon`](#backwards-compatibility).
+
+`AdvancedSettingsViewController.swift`
+
+```swift
+import Cocoa
+import Settings
+
+<<<>>>>>>+main
+===
+finafinal class AdvancedPreferenceViewController: NSViewController, PreferencePane {
+ let preferencePaneIdentifier: PreferencePaneIdentifier = .advanced
+ let toolbarItemTitle = "Advanced"
+ let toolbarItemIcon = NSImage(named: NSImage.advancedName)!
+
+ override var nibName: NSNib.Name? {
+ return "AdvancedPreferenceViewController"
+ }
+>>>>>>>-3b62df8
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Setup stuff here
+ }
+}
+```
+
If you need to respond actions indirectly, the settings window controller will forward responder chain actions to the active pane if it responds to that selector.
```swift
@@ -102,18 +356,26 @@ import Settings
@main
final class AppDelegate: NSObject, NSApplicationDelegate {
- @IBOutlet private var window: NSWindow!
+ @IBOutlet private var window: NSWindow!
- private lazy var settingsWindowController = SettingsWindowController(
+<<<<<< private lazy var settingsWindowController = SettingsWindowController(
panes: [
GeneralSettingsViewController(),
AdvancedSettingsViewController()
]
)
-
- func applicationDidFinishLaunching(_ notification: Notification) {}
-
- @IBAction
+>>>>>>>-main
+=
+ le let preferencesWindowController = PreferencesWindowController(
+ preferences: [
+ GeneralPreferenceViewController(),
+ AdvancedPreferenceViewController()
+ ]
+ )
+>>>>>>>-3b62df8
+unc applicationDidFinishLaunching(_ notification: Notification) {}
+
+<<<<<< @IBAction
func settingsMenuItemActionHandler(_ sender: NSMenuItem) {
settingsWindowController.show()
}
@@ -134,20 +396,44 @@ private lazy var settingsWindowController = SettingsWindowController(
style: .segmentedControl
)
// …
+>>>>>>>-main
+ @IBAct @IBAction
+ func preferencesMenuItemActionHandler(_ sender: NSMenuItem) {
+ preferencesWindowController.showWindow()
+ }
+}
```
-`.toolbarItem` style:
+### Preferences Tab Styles
-
+When you create the `PreferencesWindowController`, you can also switch between the `NSToolbarItem`-based style (default) and the `NSSegmentedControl`:
+```swift
+ // ...
+ let preferencesWindowController = PreferencesWindowController(
+ preferences: [
+ GeneralPreferenceViewController(),
+ AdvancedPreferenceViewController()
+ ],
+ style: .segmentedControl
+ )
+ // ...
+>>>>>>>-3b62df8
+lbarItem` style:
+
+<<<<<<< ma
`.segmentedControl` style:

+>>>>>>>-main
+NSToolba
-## API
+`.segmentedControl` style:
-```swift
-public enum Settings {}
+
+>>>>>>>-3b62df8
+``swift
+<<<<<<< mapublic enum Settings {}
extension Settings {
public enum Style {
@@ -178,10 +464,28 @@ public final class SettingsWindowController: NSWindowController {
)
func show(pane: Settings.PaneIdentifier? = nil)
+>>>>>>>-main
+blic propublic protocol PreferencePane: AnyObject {
+ var preferencePaneIdentifier: PreferencePaneIdentifier { get }
+ var toolbarItemTitle: String { get }
+ // Defaults to an empty image
+ var toolbarItemIcon: NSImage { get }
+ var viewController: NSViewController { get }
}
-```
-As with any `NSWindowController`, call `NSWindowController#close()` to close the settings window.
+public enum PreferencesStyle {
+ case toolbarItems
+ case segmentedControl
+}
+
+class PreferencesWindowController: NSWindowController {
+ init(preferencePanes: [PreferencePane],
+ style: PreferencesStyle = .toolbarItems,
+ animated: Bool = true)
+ func show(preferencePane: PreferencePaneIdentifier? = nil)
+ func hideWindow()
+>>>>>>>-3b62df8
+with any `NSWindowController`, call `NSWindowController#close()` to close the settings window.
## Recommendation
@@ -322,10 +626,9 @@ It can't be that hard right? Well, turns out it is:
- Fully documented.
- Adheres to the [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/patterns/settings/).
- The window title is automatically localized by using the system string.
-
-## Related
-
-- [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults
+<<<<<<< main
+>>>>>>>-ma - Swifty and modern UserDefaults
- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app
- [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon