diff --git a/Source/3rdParty/SindreSorhus/Preferences/Container.swift b/Source/3rdParty/SindreSorhus/Preferences/Container.swift new file mode 100755 index 00000000..7ccba756 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/Container.swift @@ -0,0 +1,103 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import SwiftUI + +@available(macOS 10.15, *) +extension Preferences { + /** + Function builder for `Preferences` components used in order to restrict types of child views to be of type `Section`. + */ + @resultBuilder + public struct SectionBuilder { + public static func buildBlock(_ sections: Section...) -> [Section] { + sections + } + } + + /** + A view which holds `Preferences.Section` views and does all the alignment magic similar to `NSGridView` from AppKit. + */ + public struct Container: View { + private let sectionBuilder: () -> [Section] + private let contentWidth: Double + private let minimumLabelWidth: Double + @State private var maximumLabelWidth = 0.0 + + /** + Creates an instance of container component, which handles layout of stacked `Preferences.Section` views. + + Custom alignment requires content width to be specified beforehand. + + - Parameters: + - contentWidth: A fixed width of the container's content (excluding paddings). + - minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label. + - builder: A view builder that creates `Preferences.Section`'s of this container. + */ + public init( + contentWidth: Double, + minimumLabelWidth: Double = 0, + @SectionBuilder builder: @escaping () -> [Section] + ) { + self.sectionBuilder = builder + self.contentWidth = contentWidth + self.minimumLabelWidth = minimumLabelWidth + } + + public var body: some View { + let sections = sectionBuilder() + + return VStack(alignment: .preferenceSectionLabel) { + ForEach(0.. some View { + sections[index] + if index != sections.count - 1 && sections[index].bottomDivider { + Divider() + // Strangely doesn't work without width being specified. Probably because of custom alignment. + .frame(width: CGFloat(contentWidth), height: 20) + .alignmentGuide(.preferenceSectionLabel) { + $0[.leading] + CGFloat(max(minimumLabelWidth, maximumLabelWidth)) + } + } + } + } +} + +/// Extension with custom alignment guide for section title labels. +@available(macOS 10.15, *) +extension HorizontalAlignment { + private enum PreferenceSectionLabelAlignment: AlignmentID { + static func defaultValue(in context: ViewDimensions) -> CGFloat { + context[HorizontalAlignment.leading] + } + } + + static let preferenceSectionLabel = HorizontalAlignment(PreferenceSectionLabelAlignment.self) +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/Localization.swift b/Source/3rdParty/SindreSorhus/Preferences/Localization.swift new file mode 100755 index 00000000..9d19e190 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/Localization.swift @@ -0,0 +1,155 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Foundation + +struct Localization { + enum Identifier { + case preferences + case preferencesEllipsized + } + + private static let localizedStrings: [Identifier: [String: String]] = [ + .preferences: [ + "ar": "تفضيلات", + "ca": "Preferències", + "cs": "Předvolby", + "da": "Indstillinger", + "de": "Einstellungen", + "el": "Προτιμήσεις", + "en": "Preferences", + "en-AU": "Preferences", + "en-GB": "Preferences", + "es": "Preferencias", + "es-419": "Preferencias", + "fi": "Asetukset", + "fr": "Préférences", + "fr-CA": "Préférences", + "he": "העדפות", + "hi": "प्राथमिकता", + "hr": "Postavke", + "hu": "Beállítások", + "id": "Preferensi", + "it": "Preferenze", + "ja": "環境設定", + "ko": "환경설정", + "ms": "Keutamaan", + "nl": "Voorkeuren", + "no": "Valg", + "pl": "Preferencje", + "pt": "Preferências", + "pt-PT": "Preferências", + "ro": "Preferințe", + "ru": "Настройки", + "sk": "Nastavenia", + "sv": "Inställningar", + "th": "การตั้งค่า", + "tr": "Tercihler", + "uk": "Параметри", + "vi": "Tùy chọn", + "zh-CN": "偏好设置", + "zh-HK": "偏好設定", + "zh-TW": "偏好設定", + ], + .preferencesEllipsized: [ + "ar": "تفضيلات…", + "ca": "Preferències…", + "cs": "Předvolby…", + "da": "Indstillinger…", + "de": "Einstellungen…", + "el": "Προτιμήσεις…", + "en": "Preferences…", + "en-AU": "Preferences…", + "en-GB": "Preferences…", + "es": "Preferencias…", + "es-419": "Preferencias…", + "fi": "Asetukset…", + "fr": "Préférences…", + "fr-CA": "Préférences…", + "he": "העדפות…", + "hi": "प्राथमिकता…", + "hr": "Postavke…", + "hu": "Beállítások…", + "id": "Preferensi…", + "it": "Preferenze…", + "ja": "環境設定…", + "ko": "환경설정...", + "ms": "Keutamaan…", + "nl": "Voorkeuren…", + "no": "Valg…", + "pl": "Preferencje…", + "pt": "Preferências…", + "pt-PT": "Preferências…", + "ro": "Preferințe…", + "ru": "Настройки…", + "sk": "Nastavenia…", + "sv": "Inställningar…", + "th": "การตั้งค่า…", + "tr": "Tercihler…", + "uk": "Параметри…", + "vi": "Tùy chọn…", + "zh-CN": "偏好设置…", + "zh-HK": "偏好設定⋯", + "zh-TW": "偏好設定⋯", + ], + ] + + /** + Returns the localized version of the given string. + + - Parameter identifier: Identifier of the string to localize. + + - Note: If the system's locale can't be determined, the English localization of the string will be returned. + */ + static subscript(identifier: Identifier) -> String { + // Force-unwrapped since all of the involved code is under our control. + let localizedDict = Localization.localizedStrings[identifier]! + let defaultLocalizedString = localizedDict["en"]! + + // Iterate through all user-preferred languages until we find one that has a valid language code. + let preferredLocale = + Locale.preferredLanguages + .lazy + .map { Locale(identifier: $0) } + .first { $0.languageCode != nil } + ?? .current + + guard let languageCode = preferredLocale.languageCode else { + return defaultLocalizedString + } + + // Chinese is the only language where different region codes result in different translations. + if languageCode == "zh" { + let regionCode = preferredLocale.regionCode ?? "" + if regionCode == "HK" || regionCode == "TW" { + return localizedDict["\(languageCode)-\(regionCode)"]! + } else { + // Fall back to "regular" zh-CN if neither the HK or TW region codes are found. + return localizedDict["\(languageCode)-CN"]! + } + } else { + if let localizedString = localizedDict[languageCode] { + return localizedString + } + } + + return defaultLocalizedString + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/Pane.swift b/Source/3rdParty/SindreSorhus/Preferences/Pane.swift new file mode 100755 index 00000000..31f5e391 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/Pane.swift @@ -0,0 +1,112 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import SwiftUI + +/// Represents a type that can be converted to `PreferencePane`. +/// +/// Acts as type-eraser for `Preferences.Pane`. +public protocol PreferencePaneConvertible { + /** + Convert `self` to equivalent `PreferencePane`. + */ + func asPreferencePane() -> PreferencePane +} + +@available(macOS 10.15, *) +extension Preferences { + /** + Create a SwiftUI-based preference pane. + + SwiftUI equivalent of the `PreferencePane` protocol. + */ + public struct Pane: View, PreferencePaneConvertible { + let identifier: PaneIdentifier + let title: String + let toolbarIcon: NSImage + let content: Content + + public init( + identifier: PaneIdentifier, + title: String, + toolbarIcon: NSImage, + contentView: () -> Content + ) { + self.identifier = identifier + self.title = title + self.toolbarIcon = toolbarIcon + self.content = contentView() + } + + public var body: some View { content } + + public func asPreferencePane() -> PreferencePane { + PaneHostingController(pane: self) + } + } + + /** + Hosting controller enabling `Preferences.Pane` to be used alongside AppKit `NSViewController`'s. + */ + public final class PaneHostingController: NSHostingController, PreferencePane { + public let preferencePaneIdentifier: PaneIdentifier + public let preferencePaneTitle: String + public let toolbarItemIcon: NSImage + + init( + identifier: PaneIdentifier, + title: String, + toolbarIcon: NSImage, + content: Content + ) { + self.preferencePaneIdentifier = identifier + self.preferencePaneTitle = title + self.toolbarItemIcon = toolbarIcon + super.init(rootView: content) + } + + public convenience init(pane: Pane) { + self.init( + identifier: pane.identifier, + title: pane.title, + toolbarIcon: pane.toolbarIcon, + content: pane.content + ) + } + + @available(*, unavailable) + @objc + dynamic required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } +} + +@available(macOS 10.15, *) +extension View { + /** + Applies font and color for a label used for describing a preference. + */ + public func preferenceDescription() -> some View { + font(.system(size: 11.0)) + // TODO: Use `.foregroundStyle` when targeting macOS 12. + .foregroundColor(.secondary) + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/PreferencePane.swift b/Source/3rdParty/SindreSorhus/Preferences/PreferencePane.swift new file mode 100755 index 00000000..a62a0743 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/PreferencePane.swift @@ -0,0 +1,59 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +extension Preferences { + public struct PaneIdentifier: Hashable, RawRepresentable, Codable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } +} + +public protocol PreferencePane: NSViewController { + var preferencePaneIdentifier: Preferences.PaneIdentifier { get } + var preferencePaneTitle: String { get } + var toolbarItemIcon: NSImage { get } +} + +extension PreferencePane { + public var toolbarItemIdentifier: NSToolbarItem.Identifier { + preferencePaneIdentifier.toolbarItemIdentifier + } + + public var toolbarItemIcon: NSImage { .empty } +} + +extension Preferences.PaneIdentifier { + public init(_ rawValue: String) { + self.init(rawValue: rawValue) + } + + public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) { + self.init(rawValue: itemIdentifier.rawValue) + } + + public var toolbarItemIdentifier: NSToolbarItem.Identifier { + NSToolbarItem.Identifier(rawValue) + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/Preferences.swift b/Source/3rdParty/SindreSorhus/Preferences/Preferences.swift new file mode 100755 index 00000000..de297837 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/Preferences.swift @@ -0,0 +1,22 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/// The namespace for this package. +public enum Preferences {} diff --git a/Source/3rdParty/SindreSorhus/Preferences/PreferencesStyle.swift b/Source/3rdParty/SindreSorhus/Preferences/PreferencesStyle.swift new file mode 100755 index 00000000..c8c7130c --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/PreferencesStyle.swift @@ -0,0 +1,28 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +extension Preferences { + public enum Style { + case toolbarItems + case segmentedControl + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/PreferencesStyleController.swift b/Source/3rdParty/SindreSorhus/Preferences/PreferencesStyleController.swift new file mode 100755 index 00000000..b696a53d --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/PreferencesStyleController.swift @@ -0,0 +1,35 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +protocol PreferencesStyleController: AnyObject { + var delegate: PreferencesStyleControllerDelegate? { get set } + var isKeepingWindowCentered: Bool { get } + + func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] + func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? + func selectTab(index: Int) +} + +protocol PreferencesStyleControllerDelegate: AnyObject { + func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool) + func activateTab(index: Int, animated: Bool) +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/PreferencesTabViewController.swift b/Source/3rdParty/SindreSorhus/Preferences/PreferencesTabViewController.swift new file mode 100755 index 00000000..8d7a9f43 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/PreferencesTabViewController.swift @@ -0,0 +1,258 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate { + private var activeTab: Int? + private var preferencePanes = [PreferencePane]() + private var style: Preferences.Style? + internal var preferencePanesCount: Int { preferencePanes.count } + private var preferencesStyleController: PreferencesStyleController! + private var isKeepingWindowCentered: Bool { preferencesStyleController.isKeepingWindowCentered } + + private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] { + preferencesStyleController?.toolbarItemIdentifiers() ?? [] + } + + var window: NSWindow! { view.window } + + var isAnimated = true + + var activeViewController: NSViewController? { + guard let activeTab = activeTab else { + return nil + } + + return preferencePanes[activeTab] + } + + override func loadView() { + view = NSView() + view.translatesAutoresizingMaskIntoConstraints = false + } + + func configure(preferencePanes: [PreferencePane], style: Preferences.Style) { + self.preferencePanes = preferencePanes + self.style = style + children = preferencePanes + + let toolbar = NSToolbar(identifier: "PreferencesToolbar") + toolbar.allowsUserCustomization = false + toolbar.displayMode = .iconAndLabel + toolbar.showsBaselineSeparator = true + toolbar.delegate = self + + switch style { + case .segmentedControl: + preferencesStyleController = SegmentedControlStyleViewController(preferencePanes: preferencePanes) + case .toolbarItems: + preferencesStyleController = ToolbarItemStyleViewController( + preferencePanes: preferencePanes, + toolbar: toolbar, + centerToolbarItems: false + ) + } + preferencesStyleController.delegate = self + + // Called last so that `preferencesStyleController` can be asked for items. + window.toolbar = toolbar + } + + func activateTab(preferencePane: PreferencePane, animated: Bool) { + activateTab(preferenceIdentifier: preferencePane.preferencePaneIdentifier, animated: animated) + } + + func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool) { + guard let index = (preferencePanes.firstIndex { $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) + updateWindowTitle(tabIndex: index) + } + + if activeTab == nil { + immediatelyDisplayTab(index: index) + } else { + guard index != activeTab else { + return + } + + animateTabTransition(index: index, animated: animated) + } + } + + func restoreInitialTab() { + if activeTab == nil { + activateTab(index: 0, animated: false) + } + } + + private func updateWindowTitle(tabIndex: Int) { + window.title = { + if preferencePanes.count > 1 { + return preferencePanes[tabIndex].preferencePaneTitle + } else { + let preferences = Localization[.preferences] + let appName = Bundle.main.appName + return "\(appName) \(preferences)" + } + }() + } + + /// Cached constraints that pin `childViewController` views to the content view. + private var activeChildViewConstraints = [NSLayoutConstraint]() + + private func immediatelyDisplayTab(index: Int) { + let toViewController = preferencePanes[index] + view.addSubview(toViewController.view) + activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds() + setWindowFrame(for: toViewController, animated: false) + } + + private func animateTabTransition(index: Int, animated: Bool) { + guard let activeTab = activeTab else { + assertionFailure( + "animateTabTransition called before a tab was displayed; transition only works from one tab to another") + immediatelyDisplayTab(index: index) + return + } + + let fromViewController = preferencePanes[activeTab] + let toViewController = preferencePanes[index] + + // View controller animations only work on macOS 10.14 and newer. + let options: NSViewController.TransitionOptions + if #available(macOS 10.14, *) { + options = animated && isAnimated ? [.crossfade] : [] + } else { + options = [] + } + + view.removeConstraints(activeChildViewConstraints) + + transition( + from: fromViewController, + to: toViewController, + options: options + ) { [self] in + 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 + frame.origin.x += horizontalDiff + } + + let animatableWindow = animated ? window.animator() : window + animatableWindow.setFrame(frame, display: false) + } +} + +extension PreferencesTabViewController: NSToolbarDelegate { + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + toolbarItemIdentifiers + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + toolbarItemIdentifiers + } + + func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + style == .segmentedControl ? [] : toolbarItemIdentifiers + } + + public func toolbar( + _ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool + ) -> NSToolbarItem? { + if itemIdentifier == .flexibleSpace { + return nil + } + + return preferencesStyleController.toolbarItem( + preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: itemIdentifier)) + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/PreferencesWindowController.swift b/Source/3rdParty/SindreSorhus/Preferences/PreferencesWindowController.swift new file mode 100755 index 00000000..2ada7810 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/PreferencesWindowController.swift @@ -0,0 +1,188 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +extension NSWindow.FrameAutosaveName { + static let preferences: NSWindow.FrameAutosaveName = "com.sindresorhus.Preferences.FrameAutosaveName" +} + +public final class PreferencesWindowController: NSWindowController { + private let tabViewController = PreferencesTabViewController() + + public var isAnimated: Bool { + get { tabViewController.isAnimated } + set { + tabViewController.isAnimated = newValue + } + } + + public var hidesToolbarForSingleItem: Bool { + didSet { + updateToolbarVisibility() + } + } + + private func updateToolbarVisibility() { + window?.toolbar?.isVisible = + (hidesToolbarForSingleItem == false) + || (tabViewController.preferencePanesCount > 1) + } + + public init( + preferencePanes: [PreferencePane], + style: Preferences.Style = .toolbarItems, + animated: Bool = true, + hidesToolbarForSingleItem: Bool = true + ) { + precondition(!preferencePanes.isEmpty, "You need to set at least one view controller") + + let window = UserInteractionPausableWindow( + contentRect: preferencePanes[0].view.bounds, + styleMask: [ + .titled, + .closable, + ], + backing: .buffered, + defer: true + ) + self.hidesToolbarForSingleItem = hidesToolbarForSingleItem + super.init(window: window) + + window.contentViewController = tabViewController + + window.titleVisibility = { + switch style { + case .toolbarItems: + return .visible + case .segmentedControl: + return preferencePanes.count <= 1 ? .visible : .hidden + } + }() + + if #available(macOS 11.0, *), style == .toolbarItems { + window.toolbarStyle = .preference + } + + tabViewController.isAnimated = animated + tabViewController.configure(preferencePanes: preferencePanes, style: style) + updateToolbarVisibility() + } + + @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:)") + } + + /** + Show the preferences window and brings it to front. + + If you pass a `Preferences.PaneIdentifier`, the window will activate the corresponding tab. + + - Parameter preferencePane: Identifier of the preference pane to display, or `nil` to show the tab that was open when the user last closed the window. + + - Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`. + + - See `close()` to close the window again. + - See `showWindow(_:)` to show the window without the convenience of activating the app. + */ + public func show(preferencePane preferenceIdentifier: Preferences.PaneIdentifier? = nil) { + if let preferenceIdentifier = preferenceIdentifier { + tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false) + } else { + tabViewController.restoreInitialTab() + } + + showWindow(self) + restoreWindowPosition() + NSApp.activate(ignoringOtherApps: true) + } + + private func restoreWindowPosition() { + guard + let window = window, + let screenContainingWindow = window.screen + else { + return + } + + window.setFrameOrigin( + CGPoint( + x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2, + y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2 + )) + window.setFrameUsingName(.preferences) + window.setFrameAutosaveName(.preferences) + } +} + +extension PreferencesWindowController { + /// Returns the active pane if it responds to the given action. + override public func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? { + if let target = super.supplementalTarget(forAction: action, sender: sender) { + return target + } + + guard let activeViewController = tabViewController.activeViewController else { + return nil + } + + if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder, + target.responds(to: action) + { + return target + } + + if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder, + target.responds(to: action) + { + return target + } + + return nil + } +} + +@available(macOS 10.15, *) +extension PreferencesWindowController { + /** + Create a preferences window from only SwiftUI-based preference panes. + */ + public convenience init( + panes: [PreferencePaneConvertible], + style: Preferences.Style = .toolbarItems, + animated: Bool = true, + hidesToolbarForSingleItem: Bool = true + ) { + let preferencePanes = panes.map { $0.asPreferencePane() } + + self.init( + preferencePanes: preferencePanes, + style: style, + animated: animated, + hidesToolbarForSingleItem: hidesToolbarForSingleItem + ) + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/Section.swift b/Source/3rdParty/SindreSorhus/Preferences/Section.swift new file mode 100755 index 00000000..15ad0400 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/Section.swift @@ -0,0 +1,138 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import SwiftUI + +@available(macOS 10.15, *) +extension Preferences { + /** + Represents a section with right-aligned title and optional bottom divider. + */ + @available(macOS 10.15, *) + public struct Section: View { + /** + Preference key holding max width of section labels. + */ + private struct LabelWidthPreferenceKey: PreferenceKey { + typealias Value = Double + + static var defaultValue = 0.0 + + static func reduce(value: inout Double, nextValue: () -> Double) { + let next = nextValue() + value = next > value ? next : value + } + } + + /** + Convenience overlay for finding a label's dimensions using `GeometryReader`. + */ + private struct LabelOverlay: View { + var body: some View { + GeometryReader { geometry in + Color.clear + .preference(key: LabelWidthPreferenceKey.self, value: Double(geometry.size.width)) + } + } + } + + /** + Convenience modifier for applying `LabelWidthPreferenceKey`. + */ + struct LabelWidthModifier: ViewModifier { + @Binding var maximumWidth: Double + + func body(content: Content) -> some View { + content + .onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in + maximumWidth = Double(newMaximumWidth) + } + } + } + + public let label: AnyView + public let content: AnyView + public let bottomDivider: Bool + public let verticalAlignment: VerticalAlignment + + /** + A section is responsible for controlling a single preference. + + - Parameters: + - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`. + - verticalAlignement: The vertical alignment of the section content. + - label: A view describing preference handled by this section. + - content: A content view. + */ + public init( + bottomDivider: Bool = false, + verticalAlignment: VerticalAlignment = .firstTextBaseline, + label: @escaping () -> Label, + @ViewBuilder content: @escaping () -> Content + ) { + self.label = label() + .overlay(LabelOverlay()) + .eraseToAnyView() // TODO: Remove use of `AnyView`. + self.bottomDivider = bottomDivider + self.verticalAlignment = verticalAlignment + let stack = VStack(alignment: .leading) { content() } + self.content = stack.eraseToAnyView() + } + + /** + Creates instance of section, responsible for controling single preference with `Text` as a `Label`. + + - Parameters: + - title: A string describing preference handled by this section. + - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`. + - verticalAlignement: The vertical alignment of the section content. + - content: A content view. + */ + public init( + title: String, + bottomDivider: Bool = false, + verticalAlignment: VerticalAlignment = .firstTextBaseline, + @ViewBuilder content: @escaping () -> Content + ) { + let textLabel = { + Text(title) + .font(.system(size: 13.0)) + .overlay(LabelOverlay()) + .eraseToAnyView() + } + + self.init( + bottomDivider: bottomDivider, + verticalAlignment: verticalAlignment, + label: textLabel, + content: content + ) + } + + public var body: some View { + HStack(alignment: verticalAlignment) { + label + .alignmentGuide(.preferenceSectionLabel) { $0[.trailing] } + content + Spacer() + } + } + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/SegmentedControlStyleViewController.swift b/Source/3rdParty/SindreSorhus/Preferences/SegmentedControlStyleViewController.swift new file mode 100755 index 00000000..d16bd748 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/SegmentedControlStyleViewController.swift @@ -0,0 +1,159 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +extension NSToolbarItem.Identifier { + static let toolbarSegmentedControlItem = Self("toolbarSegmentedControlItem") +} + +extension NSUserInterfaceItemIdentifier { + static let toolbarSegmentedControl = Self("toolbarSegmentedControl") +} + +final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController { + var segmentedControl: NSSegmentedControl! { + get { view as? NSSegmentedControl } + set { + view = newValue + } + } + + var isKeepingWindowCentered: Bool { true } + + weak var delegate: PreferencesStyleControllerDelegate? + + private var preferencePanes: [PreferencePane]! + + required init(preferencePanes: [PreferencePane]) { + super.init(nibName: nil, bundle: nil) + self.preferencePanes = preferencePanes + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = createSegmentedControl(preferencePanes: preferencePanes) + } + + fileprivate func createSegmentedControl(preferencePanes: [PreferencePane]) -> NSSegmentedControl { + let segmentedControl = NSSegmentedControl() + segmentedControl.segmentCount = preferencePanes.count + segmentedControl.segmentStyle = .texturedSquare + segmentedControl.target = self + segmentedControl.action = #selector(segmentedControlAction) + segmentedControl.identifier = .toolbarSegmentedControl + + if let cell = segmentedControl.cell as? NSSegmentedCell { + cell.controlSize = .regular + cell.trackingMode = .selectOne + } + + let segmentSize: CGSize = { + let insets = CGSize(width: 36, height: 12) + var maxSize = CGSize.zero + + for preferencePane in preferencePanes { + let title = preferencePane.preferencePaneTitle + let titleSize = title.size( + withAttributes: [ + .font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)) + ] + ) + + maxSize = CGSize( + width: max(titleSize.width, maxSize.width), + height: max(titleSize.height, maxSize.height) + ) + } + + return CGSize( + width: maxSize.width + insets.width, + height: maxSize.height + insets.height + ) + }() + + let segmentBorderWidth = CGFloat(preferencePanes.count) + 1 + let segmentWidth = segmentSize.width * CGFloat(preferencePanes.count) + segmentBorderWidth + let segmentHeight = segmentSize.height + segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight) + + for (index, preferencePane) in preferencePanes.enumerated() { + segmentedControl.setLabel(preferencePane.preferencePaneTitle, forSegment: index) + 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] { + [ + .flexibleSpace, + .toolbarSegmentedControlItem, + .flexibleSpace, + ] + } + + func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> 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 = preferencePanes.enumerated().map { index, preferenceable -> NSToolbarItem in + let item = NSToolbarItem(itemIdentifier: .init("segment-\(preferenceable.preferencePaneTitle)")) + item.label = preferenceable.preferencePaneTitle + + let menuItem = NSMenuItem( + title: preferenceable.preferencePaneTitle, + 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/Source/3rdParty/SindreSorhus/Preferences/ToolbarItemStyleViewController.swift b/Source/3rdParty/SindreSorhus/Preferences/ToolbarItemStyleViewController.swift new file mode 100755 index 00000000..34cdf5c1 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/ToolbarItemStyleViewController.swift @@ -0,0 +1,77 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController { + let toolbar: NSToolbar + let centerToolbarItems: Bool + let preferencePanes: [PreferencePane] + var isKeepingWindowCentered: Bool { centerToolbarItems } + weak var delegate: PreferencesStyleControllerDelegate? + + init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) { + self.preferencePanes = preferencePanes + self.toolbar = toolbar + self.centerToolbarItems = centerToolbarItems + } + + func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] { + var toolbarItemIdentifiers = [NSToolbarItem.Identifier]() + + if centerToolbarItems { + toolbarItemIdentifiers.append(.flexibleSpace) + } + + for preferencePane in preferencePanes { + toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier) + } + + if centerToolbarItems { + toolbarItemIdentifiers.append(.flexibleSpace) + } + + return toolbarItemIdentifiers + } + + func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? { + guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else { + preconditionFailure() + } + + let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier) + toolbarItem.label = preference.preferencePaneTitle + toolbarItem.image = preference.toolbarItemIcon + toolbarItem.target = self + toolbarItem.action = #selector(toolbarItemSelected) + return toolbarItem + } + + @IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) { + delegate?.activateTab( + preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier), + animated: true + ) + } + + func selectTab(index: Int) { + toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier + } +} diff --git a/Source/3rdParty/SindreSorhus/Preferences/Utilities.swift b/Source/3rdParty/SindreSorhus/Preferences/Utilities.swift new file mode 100755 index 00000000..849e65b4 --- /dev/null +++ b/Source/3rdParty/SindreSorhus/Preferences/Utilities.swift @@ -0,0 +1,136 @@ +// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa +import SwiftUI + +extension NSImage { + static var empty: NSImage { NSImage(size: .zero) } +} + +extension NSView { + @discardableResult + func constrainToSuperviewBounds() -> [NSLayoutConstraint] { + guard let superview = 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])) + translatesAutoresizingMaskIntoConstraints = false + superview.addConstraints(result) + + return result + } +} + +extension NSEvent { + /// Events triggered by user interaction. + static let userInteractionEvents: [NSEvent.EventType] = { + var events: [NSEvent.EventType] = [ + .leftMouseDown, + .leftMouseUp, + .rightMouseDown, + .rightMouseUp, + .leftMouseDragged, + .rightMouseDragged, + .keyDown, + .keyUp, + .scrollWheel, + .tabletPoint, + .otherMouseDown, + .otherMouseUp, + .otherMouseDragged, + .gesture, + .magnify, + .swipe, + .rotate, + .beginGesture, + .endGesture, + .smartMagnify, + .quickLook, + .directTouch, + ] + + if #available(macOS 10.10.3, *) { + events.append(.pressure) + } + + return events + }() + + /// Whether the event was triggered by user interaction. + var isUserInteraction: Bool { NSEvent.userInteractionEvents.contains(type) } +} + +extension Bundle { + var appName: String { + string(forInfoDictionaryKey: "CFBundleDisplayName") + ?? string(forInfoDictionaryKey: "CFBundleName") + ?? string(forInfoDictionaryKey: "CFBundleExecutable") + ?? "" + } + + private func string(forInfoDictionaryKey key: String) -> String? { + // `object(forInfoDictionaryKey:)` prefers localized info dictionary over the regular one automatically + object(forInfoDictionaryKey: key) as? String + } +} + +/// A window that allows you to disable all user interactions via `isUserInteractionEnabled`. +/// +/// Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set. +class UserInteractionPausableWindow: NSWindow { // swiftlint:disable:this final_class + var isUserInteractionEnabled = true + + override func sendEvent(_ event: NSEvent) { + guard isUserInteractionEnabled || !event.isUserInteraction else { + return + } + + super.sendEvent(event) + } + + override func responds(to selector: Selector!) -> Bool { + // Deactivate toolbar interactions from the Main Menu. + if selector == #selector(NSWindow.toggleToolbarShown(_:)) { + return false + } + + return super.responds(to: selector) + } +} + +@available(macOS 10.15, *) +extension View { + /** + Equivalent to `.eraseToAnyPublisher()` from the Combine framework. + */ + func eraseToAnyView() -> AnyView { + AnyView(self) + } +} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 1181b2e5..ce3b8e7d 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -30,6 +30,19 @@ 5B707CEC27D9F4870099EF99 /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = 5B707CEB27D9F4870099EF99 /* OpenCC */; }; 5B73FB5E27B2BE1300E9BF49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B73FB6027B2BE1300E9BF49 /* InfoPlist.strings */; }; 5B7BC4B027AFFBE800F66C24 /* frmPrefWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B7BC4AE27AFFBE800F66C24 /* frmPrefWindow.xib */; }; + 5BA9FD2327FEF39C002DE248 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1627FEF39C002DE248 /* Utilities.swift */; }; + 5BA9FD2427FEF39C002DE248 /* Pane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1727FEF39C002DE248 /* Pane.swift */; }; + 5BA9FD2527FEF39C002DE248 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1827FEF39C002DE248 /* Localization.swift */; }; + 5BA9FD2627FEF39C002DE248 /* PreferencesStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1927FEF39C002DE248 /* PreferencesStyle.swift */; }; + 5BA9FD2727FEF39C002DE248 /* PreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1A27FEF39C002DE248 /* PreferencePane.swift */; }; + 5BA9FD2827FEF39C002DE248 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1B27FEF39C002DE248 /* Preferences.swift */; }; + 5BA9FD2927FEF39C002DE248 /* SegmentedControlStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1C27FEF39C002DE248 /* SegmentedControlStyleViewController.swift */; }; + 5BA9FD2A27FEF39C002DE248 /* ToolbarItemStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1D27FEF39C002DE248 /* ToolbarItemStyleViewController.swift */; }; + 5BA9FD2B27FEF39C002DE248 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1E27FEF39C002DE248 /* Container.swift */; }; + 5BA9FD2C27FEF39C002DE248 /* PreferencesStyleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD1F27FEF39C002DE248 /* PreferencesStyleController.swift */; }; + 5BA9FD2D27FEF39C002DE248 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD2027FEF39C002DE248 /* PreferencesWindowController.swift */; }; + 5BA9FD2E27FEF39C002DE248 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD2127FEF39C002DE248 /* Section.swift */; }; + 5BA9FD2F27FEF39C002DE248 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD2227FEF39C002DE248 /* PreferencesTabViewController.swift */; }; 5BAD0CD527D701F6003D127F /* vChewingKeyLayout.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */; }; 5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */; }; 5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; }; @@ -189,6 +202,19 @@ 5B7BC4AF27AFFBE800F66C24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmPrefWindow.xib; sourceTree = ""; }; 5B7BC4B227AFFC0B00F66C24 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/frmPrefWindow.strings; sourceTree = ""; }; 5B8F43ED27C9BC220069AC27 /* SymbolLM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SymbolLM.h; sourceTree = ""; }; + 5BA9FD1627FEF39C002DE248 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; + 5BA9FD1727FEF39C002DE248 /* Pane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pane.swift; sourceTree = ""; }; + 5BA9FD1827FEF39C002DE248 /* Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; + 5BA9FD1927FEF39C002DE248 /* PreferencesStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesStyle.swift; sourceTree = ""; }; + 5BA9FD1A27FEF39C002DE248 /* PreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencePane.swift; sourceTree = ""; }; + 5BA9FD1B27FEF39C002DE248 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + 5BA9FD1C27FEF39C002DE248 /* SegmentedControlStyleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlStyleViewController.swift; sourceTree = ""; }; + 5BA9FD1D27FEF39C002DE248 /* ToolbarItemStyleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarItemStyleViewController.swift; sourceTree = ""; }; + 5BA9FD1E27FEF39C002DE248 /* Container.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + 5BA9FD1F27FEF39C002DE248 /* PreferencesStyleController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesStyleController.swift; sourceTree = ""; }; + 5BA9FD2027FEF39C002DE248 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; + 5BA9FD2127FEF39C002DE248 /* Section.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; + 5BA9FD2227FEF39C002DE248 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = ""; }; 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_Menu.swift; sourceTree = ""; }; 5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = ""; }; 5BBBB75E27AED54C0023B93A /* Fart.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.m4a; sourceTree = ""; }; @@ -353,6 +379,7 @@ children = ( 5B707CE627D9F43E0099EF99 /* OpenCCBridge */, 5B62A30227AE733500A19448 /* OVMandarin */, + 5BA9FCEA27FED652002DE248 /* SindreSorhus */, ); path = 3rdParty; sourceTree = ""; @@ -564,6 +591,35 @@ path = OpenCCBridge; sourceTree = ""; }; + 5BA9FCEA27FED652002DE248 /* SindreSorhus */ = { + isa = PBXGroup; + children = ( + 5BA9FD1527FEF39C002DE248 /* Preferences */, + ); + name = SindreSorhus; + path = Source/3rdParty/SindreSorhus; + sourceTree = SOURCE_ROOT; + }; + 5BA9FD1527FEF39C002DE248 /* Preferences */ = { + isa = PBXGroup; + children = ( + 5BA9FD1E27FEF39C002DE248 /* Container.swift */, + 5BA9FD1827FEF39C002DE248 /* Localization.swift */, + 5BA9FD1727FEF39C002DE248 /* Pane.swift */, + 5BA9FD1A27FEF39C002DE248 /* PreferencePane.swift */, + 5BA9FD1B27FEF39C002DE248 /* Preferences.swift */, + 5BA9FD1927FEF39C002DE248 /* PreferencesStyle.swift */, + 5BA9FD1F27FEF39C002DE248 /* PreferencesStyleController.swift */, + 5BA9FD2227FEF39C002DE248 /* PreferencesTabViewController.swift */, + 5BA9FD2027FEF39C002DE248 /* PreferencesWindowController.swift */, + 5BA9FD2127FEF39C002DE248 /* Section.swift */, + 5BA9FD1C27FEF39C002DE248 /* SegmentedControlStyleViewController.swift */, + 5BA9FD1D27FEF39C002DE248 /* ToolbarItemStyleViewController.swift */, + 5BA9FD1627FEF39C002DE248 /* Utilities.swift */, + ); + path = Preferences; + sourceTree = ""; + }; 5BBBB75C27AED54C0023B93A /* SoundFiles */ = { isa = PBXGroup; children = ( @@ -970,7 +1026,9 @@ D461B792279DAC010070E734 /* InputState.swift in Sources */, 5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */, D47B92C027972AD100458394 /* main.swift in Sources */, + 5BA9FD2527FEF39C002DE248 /* Localization.swift in Sources */, D44FB74D2792189A003C80A6 /* PhraseReplacementMap.mm in Sources */, + 5BA9FD2E27FEF39C002DE248 /* Section.swift in Sources */, D4A13D5A27A59F0B003BE359 /* ctlInputMethod.swift in Sources */, D4E569DC27A34D0E00AC2CEF /* KeyHandler.mm in Sources */, D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */, @@ -978,16 +1036,26 @@ 5BE78BE027B38804005EA1BE /* LMConsolidator.mm in Sources */, D456576E279E4F7B00DF6BC9 /* KeyParser.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */, + 5BA9FD2927FEF39C002DE248 /* SegmentedControlStyleViewController.swift in Sources */, D47D73AC27A6CAE600255A50 /* AssociatedPhrases.mm in Sources */, 5B62A34A27AE7CD900A19448 /* NotifierController.swift in Sources */, + 5BA9FD2827FEF39C002DE248 /* Preferences.swift in Sources */, + 5BA9FD2D27FEF39C002DE248 /* PreferencesWindowController.swift in Sources */, + 5BA9FD2B27FEF39C002DE248 /* Container.swift in Sources */, + 5BA9FD2427FEF39C002DE248 /* Pane.swift in Sources */, 5B11328927B94CFB00E58451 /* AppleKeyboardConverter.swift in Sources */, D41355DB278E6D17005E5CBD /* LMInstantiator.mm in Sources */, 5B62A32927AE77D100A19448 /* FSEventStreamHelper.swift in Sources */, D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, 5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */, 5B62A33827AE79CD00A19448 /* NSStringUtils.swift in Sources */, + 5BA9FD2327FEF39C002DE248 /* Utilities.swift in Sources */, 5B62A33227AE792F00A19448 /* InputSourceHelper.swift in Sources */, + 5BA9FD2C27FEF39C002DE248 /* PreferencesStyleController.swift in Sources */, + 5BA9FD2A27FEF39C002DE248 /* ToolbarItemStyleViewController.swift in Sources */, + 5BA9FD2F27FEF39C002DE248 /* PreferencesTabViewController.swift in Sources */, 5B5E535227EF261400C6AA1E /* IME.swift in Sources */, + 5BA9FD2627FEF39C002DE248 /* PreferencesStyle.swift in Sources */, 5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */, 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, 5B62A34827AE7CD900A19448 /* ctlCandidateVertical.swift in Sources */, @@ -999,6 +1067,7 @@ 5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */, D41355DE278EA3ED005E5CBD /* UserPhrasesLM.mm in Sources */, 6ACC3D3F27914F2400F1B140 /* KeyValueBlobReader.cpp in Sources */, + 5BA9FD2727FEF39C002DE248 /* PreferencePane.swift in Sources */, D41355D8278D74B5005E5CBD /* mgrLangModel.mm in Sources */, 5BDC1CFA27FDF1310052C2B9 /* apiUpdate.swift in Sources */, );