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 -![NSToolbarItem based (default)](toolbar-item.png) +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: ![NSSegmentedControl based](segmented-control.png) +>>>>>>>-main +NSToolba![NSToolbarItem based (default)](images/toolbar-item.png) -## API +`.segmentedControl` style: -```swift -public enum Settings {} +![NSSegmentedControl based](images/segmented-control.png) +>>>>>>>-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![NSToolbarIt +[Defaults](https://github.com/sindresorhus/Defaults) - 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