fix: issues with preferences window on mojave (closes #233)

This commit is contained in:
Louis Pontoise authored and lwouis committed 2020-04-19 17:53:15 +09:00
1 parent 0d27cf77a4
commit 8d7f121f3d
39 files changed
+2154 -731

No files matched your search

+5 -1
View File
@@ -6,12 +6,16 @@ target 'alt-tab-macos' do
pod 'Sparkle', '1.23.0'
pod 'ShortcutRecorder', :git => 'https://github.com/Kentzo/ShortcutRecorder.git', :branch => 'issue-114'
pod 'SwiftyMarkdown', '1.1.0'
pod 'Preferences', '1.0.1'
post_install do |installer|
# remove incorrect whitespace from ShortcutRecorder license
# remote first 2 lines (i.e. title)
# force wrap text as some licenses are and some are not
system('cat -s Pods/Target\\ Support\\ Files/Pods-alt-tab-macos/Pods-alt-tab-macos-acknowledgements.markdown'\
'| tail -n +2 '\
'| sed -e "s/^ \{12\}/ /" '\
'| sed -e "s/^ \{7\}/ /" > docs/ACKNOWLEDGMENTS.md')
'| sed -e "s/^ \{7\}/ /" '\
'| fold -w 80 -s > docs/ACKNOWLEDGMENTS.md')
end
end
+5 -1
View File
@@ -1,11 +1,13 @@
PODS:
- LetsMove (1.24)
- Preferences (1.0.1)
- ShortcutRecorder (3.1)
- Sparkle (1.23.0)
- SwiftyMarkdown (1.1.0)
DEPENDENCIES:
- LetsMove (= 1.24)
- Preferences (= 1.0.1)
- ShortcutRecorder (from `https://github.com/Kentzo/ShortcutRecorder.git`, branch `issue-114`)
- Sparkle (= 1.23.0)
- SwiftyMarkdown (= 1.1.0)
@@ -13,6 +15,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- LetsMove
- Preferences
- Sparkle
- SwiftyMarkdown
@@ -28,10 +31,11 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5
Preferences: a5afbe0ae3e6bd886328b1960f4998e65d58f2c4
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
Sparkle: 55b1a87ba69d56913375a281546b7c82dec95bb0
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
PODFILE CHECKSUM: f27dbca47faec4b1a3a1a150ba2947bb14c72be4
PODFILE CHECKSUM: 59c4f773e0cca4647e11f7a5113c033bfdd5c0ff
COCOAPODS: 1.8.4
+5 -1
View File
@@ -1,11 +1,13 @@
PODS:
- LetsMove (1.24)
- Preferences (1.0.1)
- ShortcutRecorder (3.1)
- Sparkle (1.23.0)
- SwiftyMarkdown (1.1.0)
DEPENDENCIES:
- LetsMove (= 1.24)
- Preferences (= 1.0.1)
- ShortcutRecorder (from `https://github.com/Kentzo/ShortcutRecorder.git`, branch `issue-114`)
- Sparkle (= 1.23.0)
- SwiftyMarkdown (= 1.1.0)
@@ -13,6 +15,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- LetsMove
- Preferences
- Sparkle
- SwiftyMarkdown
@@ -28,10 +31,11 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5
Preferences: a5afbe0ae3e6bd886328b1960f4998e65d58f2c4
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
Sparkle: 55b1a87ba69d56913375a281546b7c82dec95bb0
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
PODFILE CHECKSUM: f27dbca47faec4b1a3a1a150ba2947bb14c72be4
PODFILE CHECKSUM: 59c4f773e0cca4647e11f7a5113c033bfdd5c0ff
COCOAPODS: 1.8.4
+795 -579
View File
File diff suppressed because it is too large. Load diff
+134
View File
@@ -0,0 +1,134 @@
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 }
?? Locale.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
}
}
@@ -0,0 +1,41 @@
import Cocoa
public struct PreferencePaneIdentifier: Hashable, RawRepresentable, Codable {
public typealias Identifier = PreferencePaneIdentifier
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
public protocol PreferencePane: NSViewController {
typealias Identifier = PreferencePaneIdentifier
var preferencePaneIdentifier: Identifier { get }
var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get }
}
extension PreferencePane {
public var toolbarItemIdentifier: NSToolbarItem.Identifier {
preferencePaneIdentifier.toolbarItemIdentifier
}
public var toolbarItemIcon: NSImage { .empty }
}
extension PreferencePane.Identifier {
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)
}
}
@@ -0,0 +1,6 @@
import Cocoa
public enum PreferencesStyle {
case toolbarItems
case segmentedControl
}
@@ -0,0 +1,15 @@
import Cocoa
protocol PreferencesStyleController: AnyObject {
var delegate: PreferencesStyleControllerDelegate? { get set }
var isKeepingWindowCentered: Bool { get }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
func toolbarItem(preferenceIdentifier: PreferencePane.Identifier) -> NSToolbarItem?
func selectTab(index: Int)
}
protocol PreferencesStyleControllerDelegate: AnyObject {
func activateTab(preferenceIdentifier: PreferencePane.Identifier, animated: Bool)
func activateTab(index: Int, animated: Bool)
}
@@ -0,0 +1,224 @@
import Cocoa
final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate {
private var activeTab: Int?
private var preferencePanes = [PreferencePane]()
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: Bool = true
override func loadView() {
self.view = NSView()
self.view.translatesAutoresizingMaskIntoConstraints = false
}
func configure(preferencePanes: [PreferencePane], style: PreferencesStyle) {
self.preferencePanes = preferencePanes
self.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: PreferencePane.Identifier, 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) {
self.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.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] {
toolbarItemIdentifiers
}
public func toolbar(
_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
if itemIdentifier == .flexibleSpace {
return nil
}
return preferencesStyleController.toolbarItem(preferenceIdentifier: .Identifier(fromToolbarItemIdentifier: itemIdentifier))
}
}
@@ -0,0 +1,114 @@
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: PreferencesStyle = .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
}
}()
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 `PreferencePane.Identifier`, 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: PreferencePane.Identifier? = 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 = self.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)
}
}
@@ -0,0 +1,143 @@
import Cocoa
extension NSToolbarItem.Identifier {
static var toolbarSegmentedControlItem: NSToolbarItem.Identifier {
NSToolbarItem.Identifier("toolbarSegmentedControlItem")
}
}
extension NSUserInterfaceItemIdentifier {
static var toolbarSegmentedControl: NSUserInterfaceItemIdentifier {
NSUserInterfaceItemIdentifier("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: PreferencePane.Identifier) -> 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)
}
}
@@ -0,0 +1,57 @@
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: PreferencePane.Identifier) -> 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: PreferencePane.Identifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
animated: true
)
}
func selectTab(index: Int) {
toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier
}
}
@@ -0,0 +1,27 @@
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 = 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)
}
}
+75
View File
@@ -0,0 +1,75 @@
import Cocoa
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")
?? "<Unknown App Name>"
}
private func string(forInfoDictionaryKey key: String) -> String? {
// `object(forInfoDictionaryKey:)` prefers localized info dictionary over the regular one automatically
object(forInfoDictionaryKey: key) as? String
}
}
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
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.
+251
View File
@@ -0,0 +1,251 @@
# Preferences
> Add a preferences window to your macOS app in minutes
<img src="screenshot.gif" width="628" height="416">
Just pass in some view controllers and this package will take care of the rest.
## Requirements
- macOS 10.10+
- Xcode 11+
- Swift 5.1+
## Install
#### SwiftPM
```swift
.package(url: "https://github.com/sindresorhus/Preferences", from: "1.0.1")
```
#### Carthage
```
github "sindresorhus/Preferences"
```
#### CocoaPods
```ruby
pod 'Preferences'
```
## Usage
*Run the `PreferencesExample` target in Xcode to try a live example.*
First, create some preference pane identifiers:
```swift
import Preferences
extension PreferencePane.Identifier {
static let general = Identifier("general")
static let advanced = Identifier("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 `PreferencePane` protocol and implement the `preferencePaneIdentifier`, `toolbarItemTitle`, and `toolbarItemIcon` properties, as shown below. You can leave out `toolbarItemIcon` if you're using the `.segmentedControl` style.
`GeneralPreferenceViewController.swift`
```swift
import Cocoa
import Preferences
final class GeneralPreferenceViewController: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier.general
let preferencePaneTitle = "General"
let toolbarItemIcon = NSImage(named: NSImage.preferencesGeneralName)!
override var nibName: NSNib.Name? { "GeneralPreferenceViewController" }
override func viewDidLoad() {
super.viewDidLoad()
// Setup stuff here
}
}
```
`AdvancedPreferenceViewController.swift`
```swift
import Cocoa
import Preferences
final class AdvancedPreferenceViewController: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier.advanced
let preferencePaneTitle = "Advanced"
let toolbarItemIcon = NSImage(named: NSImage.advancedName)!
override var nibName: NSNib.Name? { "AdvancedPreferenceViewController" }
override func viewDidLoad() {
super.viewDidLoad()
// Setup stuff here
}
}
```
In the `AppDelegate`, initialize a new `PreferencesWindowController` and pass it the view controllers. Then add an action outlet for the `Preferences…` menu item to show the preferences window.
`AppDelegate.swift`
```swift
import Cocoa
import Preferences
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet private var window: NSWindow!
lazy var preferencesWindowController = PreferencesWindowController(
preferencePanes: [
GeneralPreferenceViewController(),
AdvancedPreferenceViewController()
]
)
func applicationDidFinishLaunching(_ notification: Notification) {}
@IBAction
func preferencesMenuItemActionHandler(_ sender: NSMenuItem) {
preferencesWindowController.show()
}
}
```
### Preferences Tab Styles
When you create the `PreferencesWindowController`, you can choose between the `NSToolbarItem`-based style (default) and the `NSSegmentedControl`:
```swift
// …
lazy var preferencesWindowController = PreferencesWindowController(
preferencePanes: [
GeneralPreferenceViewController(),
AdvancedPreferenceViewController()
],
style: .segmentedControl
)
// …
```
`.toolbarItem` style:
![NSToolbarItem based (default)](toolbar-item.png)
`.segmentedControl` style:
![NSSegmentedControl based](segmented-control.png)
## API
```swift
public protocol PreferencePane: NSViewController {
var preferencePaneIdentifier: PreferencePane.Identifier { get }
var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get } // Not required when using the .`segmentedControl` style
}
public enum PreferencesStyle {
case toolbarItems
case segmentedControl
}
public final class PreferencesWindowController: NSWindowController {
init(
preferencePanes: [PreferencePane],
style: PreferencesStyle = .toolbarItems,
animated: Bool = true,
hidesToolbarForSingleItem: Bool = true
)
func show(preferencePane: PreferencePane.Identifier? = nil)
}
```
As with any `NSWindowController`, call `NSWindowController#close()` to close the preferences window.
## Recommendation
The easiest way to create the user interface within each pane is to use a [`NSGridView`](https://developer.apple.com/documentation/appkit/nsgridview) in Interface Builder. See the example project in this repo for a demo.
## Known issues
### The preferences window doesn't show
This can happen when you are not using auto-layout or have not set a size for the view controller. You can fix this by either using auto-layout or setting an explicit size, for example, `preferredContentSize` in `viewDidLoad()`. [We intend to fix this.](https://github.com/sindresorhus/Preferences/pull/28)
### There are no animations on macOS 10.13 and earlier
The `animated` parameter of `PreferencesWindowController.init` has no effect on macOS 10.13 or earlier as those versions don't support `NSViewController.TransitionOptions.crossfade`.
## FAQ
### How can I localize the window title?
The `PreferencesWindowController` adheres to the [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/macos/app-architecture/preferences/) and uses this set of rules to determine the window title:
- **Multiple preference panes:** Uses the currently selected `preferencePaneTitle` as the window title. Localize your `preferencePaneTitle`s to get localized window titles.
- **Single preference pane:** Sets the window title to `APPNAME Preferences`. The app name is obtained from your app's bundle. You can localize its `Info.plist` to customize the title. The `Preferences` part is taken from the "Preferences…" menu item, see #12. The order of lookup for the app name from your bundle:
1. `CFBundleDisplayName`
2. `CFBundleName`
3. `CFBundleExecutable`
4. Fall back to `"<Unknown App Name>"` to show you're missing some settings.
### Why should I use this instead of just manually implementing it myself?
It can't be that hard right? Well, turns out it is:
- The recommended way is to implement it using storyboards. [But storyboards...](https://gist.github.com/iraycd/01b45c5e1be7ef6957b7) And if you want the segmented control style, you have to implement it programmatically, [which is quite complex](https://github.com/sindresorhus/Preferences/blob/85f8d793050004fc0154c7f6a061412e00d13fa3/Sources/Preferences/SegmentedControlStyleViewController.swift).
- [Even Apple gets it wrong, a lot.](https://twitter.com/sindresorhus/status/1113382212584464384)
- You have to correctly handle [window](https://github.com/sindresorhus/Preferences/commit/cc25d58a9ec379812fc8f2fd7ba48f3d35b4cbff) and [tab restoration](https://github.com/sindresorhus/Preferences/commit/2bb3fc7418f3dc49b534fab986807c4e70ba78c3).
- [The window title format depends on whether you have a single or multiple panes.](https://developer.apple.com/design/human-interface-guidelines/macos/app-architecture/preferences/)
- It's difficult to get the transition animation right. A lot of apps have flaky animation between panes.
- You end up having to deal with a lot of gnarly auto-layout complexities.
### How is it better than [`MASPreferences`](https://github.com/shpakovski/MASPreferences)?
- Written in Swift. *(No bridging header!)*
- Swifty API using a protocol.
- Supports segmented control style tabs.
- Fully documented.
- Adheres to the [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/macos/app-architecture/preferences/).
- The window title is automatically localized by using the system string.
## Related
- [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
- [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift)
You might also like Sindre's [apps](https://sindresorhus.com/apps).
## Used in these apps
- [TableFlip](https://tableflipapp.com) - Visual Markdown table editor by [Christian Tietze](https://github.com/DivineDominion)
- [The Archive](https://zettelkasten.de/the-archive/) - Note-taking app by [Christian Tietze](https://github.com/DivineDominion)
- [Word Counter](https://wordcounterapp.com) - Measuring writer's productivity by [Christian Tietze](https://github.com/DivineDominion)
Want to tell the world about your app that is using Preferences? Open a PR!
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Christian Tietze](https://github.com/DivineDominion)
@@ -1,6 +1,19 @@
# Acknowledgements
This application makes use of the following third party libraries:
## Preferences
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
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.
## ShortcutRecorder
Creative Commons Attribution 4.0 International Public License
@@ -12,6 +12,25 @@
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>MIT License
Copyright (c) Sindre Sorhus &lt;sindresorhus@gmail.com&gt; (sindresorhus.com)
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.
</string>
<key>License</key>
<string>MIT</string>
<key>Title</key>
<string>Preferences</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Creative Commons Attribution 4.0 International Public License
@@ -162,6 +162,7 @@ strip_invalid_archs() {
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Preferences/Preferences.framework"
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
install_framework "${PODS_ROOT}/Sparkle/Sparkle.framework"
install_dsym "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM"
@@ -169,6 +170,7 @@ if [[ "$CONFIGURATION" == "Debug" ]]; then
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Preferences/Preferences.framework"
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
install_framework "${PODS_ROOT}/Sparkle/Sparkle.framework"
install_dsym "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM"
@@ -1,9 +1,9 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/Sparkle"
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/Preferences" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/Sparkle"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Preferences/Preferences.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' @loader_path/../Frameworks
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyMarkdown"
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "LetsMove" -framework "Preferences" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyMarkdown"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
@@ -1,9 +1,9 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/Sparkle"
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/Preferences" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/Sparkle"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Preferences/Preferences.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' @loader_path/../Frameworks
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyMarkdown"
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "LetsMove" -framework "Preferences" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyMarkdown"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
@@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
@interface PodsDummy_Preferences : NSObject
@end
@implementation PodsDummy_Preferences
@end
@@ -0,0 +1,12 @@
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
@@ -0,0 +1,16 @@
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double PreferencesVersionNumber;
FOUNDATION_EXPORT const unsigned char PreferencesVersionString[];
@@ -0,0 +1,6 @@
framework module Preferences {
umbrella header "Preferences-umbrella.h"
export *
module * { export * }
}
@@ -0,0 +1,11 @@
CODE_SIGN_IDENTITY =
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Preferences
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Preferences
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
+2 -12
View File
@@ -29,7 +29,6 @@
D04BA2E64C59D96F6EB27D9D /* FeedbackWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA26C75F76C277653C932 /* FeedbackWindow.swift */; };
D04BA34AC850A273AB288B1E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA3B51D05213404938366 /* Localizable.strings */; };
D04BA3744F48116DF4252B19 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA02355EB28D639F854DF /* Localizable.strings */; };
D04BA3BFB0CDF4ED343914B2 /* PreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA47FF1B7838CF4814538 /* PreferencesWindow.swift */; };
D04BA3C24F4F644EA91DE38C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA717693DA18CB74BAED1 /* Localizable.strings */; };
D04BA3CF766857381519B892 /* DispatchQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAB74451B79FE18B8BEDF /* DispatchQueues.swift */; };
D04BA40CC1415DA69CCE5D89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA17FC84640580894400E /* InfoPlist.strings */; };
@@ -37,7 +36,6 @@
D04BA5863E8AD08C36B52F80 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAC86E60B1B850059BC54 /* Localizable.strings */; };
D04BA6187A91A847844B6ABB /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA015A45DE7AFDC9794FE /* Window.swift */; };
D04BA69012A483201B375D46 /* ACKNOWLEDGMENTS.md in Resources */ = {isa = PBXBuildFile; fileRef = D04BAADF01EB8FF8A15AECC6 /* ACKNOWLEDGMENTS.md */; };
D04BA691CB6082A3C39CBC89 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE757BB2B605234FBF58 /* TabViewController.swift */; };
D04BA69D47B5E60A6AD9CBD9 /* ThumbnailTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1297730B191E96E7FE /* ThumbnailTitleView.swift */; };
D04BA737008AA2CD4E230A21 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA10777505D8A67ABD186 /* Application.swift */; };
D04BA73E90EFEF8247A5105D /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAC34CFD42A7F6F1F01C0 /* CGWindow.swift */; };
@@ -72,7 +70,6 @@
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C836A8CE8C0B8D128B /* TextArea.swift */; };
D04BAD451966B43720120D2E /* Menubar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD905546AA93E5117B0A /* Menubar.swift */; };
D04BADADBE35CA2E78483F72 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA6EC52A66DFFD4DF79C1 /* Localizable.strings */; };
D04BADBCF20CD72057E7CF09 /* TabViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA27C87B86C4484A5B15B /* TabViewItem.swift */; };
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */; };
D04BAE6A13DBAE85B8F7CB6A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BABF92E92F093C18BB1A1 /* Localizable.strings */; };
D04BAEE31B6FFCDC779E6C17 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D04BAC2FF99F629CD4ED20FC /* MainMenu.xib */; };
@@ -112,7 +109,6 @@
D04BA258B56193958D60978A /* ThumbnailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailView.swift; sourceTree = "<group>"; };
D04BA26154AB2A2897E08CAF /* windows-theme.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "windows-theme.jpg"; sourceTree = "<group>"; };
D04BA26C75F76C277653C932 /* FeedbackWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackWindow.swift; sourceTree = "<group>"; };
D04BA27C87B86C4484A5B15B /* TabViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewItem.swift; sourceTree = "<group>"; };
D04BA282BB16C1554595A968 /* Applications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Applications.swift; sourceTree = "<group>"; };
D04BA2C7B51F68651B3C60E2 /* 6 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "6 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA2E723062A1DAC58618F /* setup_ci_pr.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = setup_ci_pr.sh; sourceTree = "<group>"; };
@@ -122,7 +118,6 @@
D04BA4135D97F9E784709DA8 /* ko */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ko; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BA4336B6004A0A99849AD /* package.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = package.json; sourceTree = "<group>"; };
D04BA459034C1885CA43A807 /* LICENCE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENCE.md; sourceTree = "<group>"; };
D04BA47FF1B7838CF4814538 /* PreferencesWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindow.swift; sourceTree = "<group>"; };
D04BA49E45BFFF3D9FC60E43 /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
D04BA4A26987F67DD94C827F /* AboutTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutTab.swift; sourceTree = "<group>"; };
D04BA4B5292629AA6B560216 /* package_and_notarize_release.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = package_and_notarize_release.sh; sourceTree = "<group>"; };
@@ -207,7 +202,6 @@
D04BAE2DC036FD84446E1AE6 /* menubar-icon.svg */ = {isa = PBXFileReference; lastKnownFileType = file.svg; path = "menubar-icon.svg"; sourceTree = "<group>"; };
D04BAE333F7170E87C5AC0EF /* CustomRecorderControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRecorderControl.swift; sourceTree = "<group>"; };
D04BAE5D665680CB4B13CA26 /* app-icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "app-icon.icns"; sourceTree = "<group>"; };
D04BAE757BB2B605234FBF58 /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = "<group>"; };
D04BAE93A5854C501639C640 /* update_homebrew_cask.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = update_homebrew_cask.sh; sourceTree = "<group>"; };
D04BAEF1D194C2379C9970F1 /* setup_ci_master.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = setup_ci_master.sh; sourceTree = "<group>"; };
D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
@@ -604,10 +598,7 @@
D04BABCD12CA72B06290C248 /* preferences-window */ = {
isa = PBXGroup;
children = (
D04BA47FF1B7838CF4814538 /* PreferencesWindow.swift */,
D04BAE757BB2B605234FBF58 /* TabViewController.swift */,
D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */,
D04BA27C87B86C4484A5B15B /* TabViewItem.swift */,
D04BA3FA52AA40B827A72F10 /* tabs */,
);
path = "preferences-window";
@@ -796,6 +787,7 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-alt-tab-macos/Pods-alt-tab-macos-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework",
"${BUILT_PRODUCTS_DIR}/Preferences/Preferences.framework",
"${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework",
"${PODS_ROOT}/Sparkle/Sparkle.framework",
"${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM",
@@ -804,6 +796,7 @@
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LetsMove.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Preferences.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShortcutRecorder.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework",
"${DWARF_DSYM_FOLDER_PATH}/Sparkle.framework.dSYM",
@@ -843,10 +836,7 @@
D04BA69D47B5E60A6AD9CBD9 /* ThumbnailTitleView.swift in Sources */,
D04BA1B133D53572D7B312C2 /* ThumbnailFontIconView.swift in Sources */,
D04BA1CEC6B9C8945FEC8740 /* ThumbnailView.swift in Sources */,
D04BA3BFB0CDF4ED343914B2 /* PreferencesWindow.swift in Sources */,
D04BA691CB6082A3C39CBC89 /* TabViewController.swift in Sources */,
D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */,
D04BADBCF20CD72057E7CF09 /* TabViewItem.swift in Sources */,
D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */,
D04BAD1BE9DC22C48C53D195 /* AboutTab.swift in Sources */,
D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */,
+26 -1
View File
@@ -1,5 +1,29 @@
This application makes use of the following third party libraries:
## Preferences
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
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.
## ShortcutRecorder
Creative Commons Attribution 4.0 International Public License
@@ -366,7 +390,8 @@ EXTERNAL LICENSES
bspatch.c and bsdiff.c, from bsdiff 4.3 <http://www.daemonology.net/bsdiff/>:
Copyright (c) 2003-2005 Colin Percival.
sais.c and sais.c, from sais-lite (2010/08/07) <https://sites.google.com/site/yuta256/sais>:
sais.c and sais.c, from sais-lite (2010/08/07)
<https://sites.google.com/site/yuta256/sais>:
Copyright (c) 2008-2010 Yuta Mori.
SUDSAVerifier.m:
+21 -10
View File
@@ -2,6 +2,7 @@ import Cocoa
import Darwin
import LetsMove
import ShortcutRecorder
import Preferences
let cgsMainConnectionId = CGSMainConnectionID()
@@ -14,7 +15,7 @@ class App: NSApplication, NSApplicationDelegate {
static let shortcutMonitor = LocalShortcutMonitor()
var statusItem: NSStatusItem?
var thumbnailsPanel: ThumbnailsPanel?
var preferencesWindow: PreferencesWindow?
var preferencesWindowController: PreferencesWindowController!
var feedbackWindow: FeedbackWindow?
var uiWorkShouldBeDone = true
var isFirstSummon = true
@@ -44,12 +45,25 @@ class App: NSApplication, NSApplicationDelegate {
Spaces.initialDiscovery()
Applications.initialDiscovery()
Keyboard.listenToGlobalEvents()
preferencesWindow = PreferencesWindow()
loadPreferencesWindow()
UpdatesTab.observeUserDefaults()
// TODO: undeterministic; events in the queue may still be processing; good enough for now
DispatchQueue.main.async { () -> () in Windows.sortByLevel() }
}
private func loadPreferencesWindow() {
let tabs = [
GeneralTab(),
UpdatesTab(),
AppearanceTab(),
AboutTab(),
AcknowledgmentsTab(),
]
// pre-load tabs so we can interact with them before the user opens the preferences window
tabs.forEach { (tab: NSViewController) in tab.loadView() }
preferencesWindowController = PreferencesWindowController(preferencePanes: tabs as! [PreferencePane])
}
// keyboard shortcuts are broken without a menu. We generated the default menu from XCode and load it
// see https://stackoverflow.com/a/3746058/2249756
private func loadMainMenuXib() {
@@ -78,19 +92,16 @@ class App: NSApplication, NSApplicationDelegate {
focusSelectedWindow(Windows.focusedWindow())
}
@objc
func checkForUpdatesNow(_ sender: NSMenuItem) {
@objc func checkForUpdatesNow(_ sender: NSMenuItem) {
UpdatesTab.checkForUpdatesNow(sender)
}
@objc
func showPreferencesPanel() {
Screen.repositionPanel(preferencesWindow!, Screen.preferred(), .appleCentered)
preferencesWindow?.show()
@objc func showPreferencesPanel() {
Screen.repositionPanel(preferencesWindowController.window!, Screen.preferred(), .appleCentered)
preferencesWindowController.show()
}
@objc
func showFeedbackPanel() {
@objc func showFeedbackPanel() {
if feedbackWindow == nil {
feedbackWindow = FeedbackWindow()
}
@@ -69,12 +69,22 @@ class LabelAndControl: NSObject {
static func setupControl(_ control: NSControl, _ rawName: String, _ extraAction: ActionClosure? = nil) -> NSControl {
control.identifier = NSUserInterfaceItemIdentifier(rawName)
control.onAction = {
PreferencesWindow.controlWasChanged($0)
controlWasChanged($0)
extraAction?($0)
}
return control
}
static func controlWasChanged(_ senderControl: NSControl) {
let newValue = LabelAndControl.getControlValue(senderControl)
LabelAndControl.updateControlExtras(senderControl, newValue)
Preferences.set(senderControl.identifier!.rawValue, newValue)
// some preferences require re-creating some components
if ["iconSize", "fontHeight", "theme"].contains(where: { (pref: String) -> Bool in pref == senderControl.identifier!.rawValue }) {
(App.shared as! App).resetPreferencesDependentComponents()
}
}
static func makeLabel(_ labelText: String, _ labelPosition: LabelPosition = .leftWithoutSeparator) -> NSTextField {
let labelText_ = labelPosition == .leftWithSeparator ? labelText + ":" : labelText
let label = NSTextField(wrappingLabelWithString: labelText_)
@@ -1,41 +0,0 @@
import Cocoa
class PreferencesWindow: NSWindow {
let tabViewController = TabViewController()
convenience init() {
self.init(contentRect: .zero, styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false, screen: NSScreen.main)
setupWindow()
setupTabViews()
}
private func setupWindow() {
hidesOnDeactivate = false
isReleasedWhenClosed = false
}
private func setupTabViews() {
contentViewController = tabViewController
tabViewController.tabStyle = .toolbar
tabViewController.addTabViewItem(TabViewItem(NSLocalizedString("General", comment: ""), NSImage.preferencesGeneralName, GeneralTab.makeView()))
tabViewController.addTabViewItem(TabViewItem(NSLocalizedString("Appearance", comment: ""), NSImage.colorPanelName, AppearanceTab.makeView()))
tabViewController.addTabViewItem(TabViewItem(NSLocalizedString("Updates", comment: ""), NSImage.refreshTemplateName, UpdatesTab.makeView()))
tabViewController.addTabViewItem(TabViewItem(NSLocalizedString("About", comment: ""), NSImage.infoName, AboutTab.makeView()))
tabViewController.addTabViewItem(TabViewItem(NSLocalizedString("Acknowledgments", comment: ""), NSImage.userAccountsName, AcknowledgmentsTab.makeView()))
}
func show() {
App.shared.activate(ignoringOtherApps: true)
makeKeyAndOrderFront(nil)
}
static func controlWasChanged(_ senderControl: NSControl) {
let newValue = LabelAndControl.getControlValue(senderControl)
LabelAndControl.updateControlExtras(senderControl, newValue)
Preferences.set(senderControl.identifier!.rawValue, newValue)
// some preferences require re-creating some components
if ["iconSize", "fontHeight", "theme"].contains(where: { (pref: String) -> Bool in pref == senderControl.identifier!.rawValue }) {
(App.shared as! App).resetPreferencesDependentComponents()
}
}
}
@@ -1,18 +0,0 @@
import Cocoa
class TabViewController: NSTabViewController {
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
super.tabView(tabView, didSelect: tabViewItem)
guard let tabViewItem = tabViewItem, let window = view.window else { return }
window.title = tabViewItem.label
resizeWindowToFit(tabViewItem, window)
}
private func resizeWindowToFit(_ tabViewItem: NSTabViewItem, _ window: NSWindow) {
let container = window.frame
let containee = tabView.subviews.first!.frame
window.setFrame(NSRect(origin: CGPoint(x: container.origin.x, y: container.origin.y + (container.height - containee.height)),
size: containee.size),
display: false, animate: true)
}
}
@@ -1,11 +0,0 @@
import Cocoa
class TabViewItem: NSTabViewItem {
convenience init(_ label: String, _ imageName: NSImage.Name, _ view: NSView) {
let viewController = NSViewController()
self.init(viewController: viewController)
viewController.view = view
self.label = label
image = NSImage(named: imageName)
}
}
@@ -1,7 +1,12 @@
import Cocoa
import Preferences
class AboutTab: NSObject {
static func makeView() -> NSGridView {
class AboutTab: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier("About")
let preferencePaneTitle = NSLocalizedString("About", comment: "")
let toolbarItemIcon = NSImage(named: NSImage.infoName)!
override func loadView() {
let appIcon = NSImageView(image: NSImage(named: "app-icon")!.resizedCopy(150, 150))
appIcon.imageScaling = .scaleNone
let appText = StackView([
@@ -17,7 +22,7 @@ class AboutTab: NSObject {
let appInfo = StackView([appIcon, appText])
appInfo.spacing = GridView.interPadding
appInfo.alignment = .centerY
let sendFeedback = NSButton(title: NSLocalizedString("Send feedback…", comment: ""), target: self, action: #selector(feedbackCallback))
let sendFeedback = NSButton(title: NSLocalizedString("Send feedback…", comment: ""), target: nil, action: #selector(App.app.showFeedbackPanel))
let grid = GridView([
[appInfo],
[sendFeedback],
@@ -26,11 +31,6 @@ class AboutTab: NSObject {
sendFeedbackCell.xPlacement = .center
sendFeedbackCell.row!.topPadding = GridView.interPadding
grid.fit()
return grid
}
@objc
static func feedbackCallback() {
App.app.showFeedbackPanel()
view = grid
}
}
@@ -1,8 +1,13 @@
import Cocoa
import SwiftyMarkdown
import Preferences
class AcknowledgmentsTab: NSObject {
static func makeView() -> NSGridView {
class AcknowledgmentsTab: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier("Acknowledgments")
let preferencePaneTitle = NSLocalizedString("Acknowledgments", comment: "")
let toolbarItemIcon = NSImage(named: NSImage.userAccountsName)!
override func loadView() {
let markdownFileUrl = Bundle.main.url(forResource: "ACKNOWLEDGMENTS", withExtension: "md")!
let md = SwiftyMarkdown(url: markdownFileUrl)!
md.h2.fontSize = 20
@@ -18,6 +23,6 @@ class AcknowledgmentsTab: NSObject {
let grid = GridView([[scrollView]])
grid.fit()
return grid
view = grid
}
}
@@ -1,9 +1,12 @@
import Cocoa
import Preferences
class AppearanceTab {
private static let rowHeight = CGFloat(20)
class AppearanceTab: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier("Appearance")
let preferencePaneTitle = NSLocalizedString("Appearance", comment: "")
let toolbarItemIcon = NSImage(named: NSImage.colorPanelName)!
static func makeView() -> NSGridView {
override func loadView() {
let grid = GridView([
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Theme", comment: ""), "theme", ThemePreference.allCases),
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Align windows", comment: ""), "alignThumbnails", AlignThumbnailsPreference.allCases),
@@ -20,6 +23,7 @@ class AppearanceTab {
grid.column(at: 0).xPlacement = .trailing
grid.rowAlignment = .lastBaseline
grid.fit()
return grid
view = grid
}
}
+15 -11
View File
@@ -1,19 +1,23 @@
import Cocoa
import ShortcutRecorder
import Preferences
class GeneralTab: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier("General")
let preferencePaneTitle = NSLocalizedString("General", comment: "")
let toolbarItemIcon = NSImage(named: NSImage.preferencesGeneralName)!
class GeneralTab {
private static let rowHeight = CGFloat(22) // height of the "Tab key" input
static var shortcutActions = [String: ShortcutAction]()
static var shortcutsDependentOnHoldShortcut = [NSControl]()
static func makeView() -> NSView {
override func loadView() {
let startAtLogin = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Start at login", comment: ""), "startAtLogin", extraAction: startAtLoginCallback)
var holdShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Hold", comment: ""), "holdShortcut", Preferences.holdShortcut, false, labelPosition: .leftWithoutSeparator)
holdShortcut.append(LabelAndControl.makeLabel(NSLocalizedString("then press:", comment: "")))
let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), "nextWindowShortcut", Preferences.nextWindowShortcut, labelPosition: .right)
let previousWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select previous window", comment: ""), "previousWindowShortcut", Preferences.previousWindowShortcut, labelPosition: .right)
let cancelShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Cancel and hide", comment: ""), "cancelShortcut", Preferences.cancelShortcut, labelPosition: .right)
let enableArrows = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Arrow keys", comment: ""), "arrowKeysEnabled", extraAction: arrowKeysEnabledCallback, labelPosition: .right)
let enableArrows = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Arrow keys", comment: ""), "arrowKeysEnabled", extraAction: GeneralTab.arrowKeysEnabledCallback, labelPosition: .right)
let enableMouse = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Mouse hover", comment: ""), "mouseHoverEnabled", labelPosition: .right)
let holdAndPress = StackView(holdShortcut)
let checkboxesExplanations = LabelAndControl.makeLabel(NSLocalizedString("Select windows using:", comment: ""))
@@ -39,14 +43,14 @@ class GeneralTab {
[1, 2, 3].forEach { grid.row(at: $0).topPadding = GridView.interPadding }
grid.fit()
shortcutsDependentOnHoldShortcut.append(contentsOf: [enableArrows[0] as! NSControl] + [nextWindowShortcut, previousWindowShortcut, cancelShortcut].map { $0[0] as! NSControl })
arrowKeysEnabledCallback(enableArrows[0] as! NSControl)
GeneralTab.shortcutsDependentOnHoldShortcut.append(contentsOf: [enableArrows[0] as! NSControl] + [nextWindowShortcut, previousWindowShortcut, cancelShortcut].map { $0[0] as! NSControl })
GeneralTab.arrowKeysEnabledCallback(enableArrows[0] as! NSControl)
startAtLoginCallback(startAtLogin[1] as! NSControl)
return grid
view = grid
}
private static func dropdown(_ rawName: String, _ macroPreferences: [MacroPreference]) -> NSControl {
private func dropdown(_ rawName: String, _ macroPreferences: [MacroPreference]) -> NSControl {
let dropdown = LabelAndControl.makeDropDown(rawName, macroPreferences)
return LabelAndControl.setupControl(dropdown, rawName)
}
@@ -108,14 +112,14 @@ class GeneralTab {
// adding/removing login item depending on the checkbox state
@available(OSX, deprecated: 10.11)
static func startAtLoginCallback(_ sender: NSControl) {
func startAtLoginCallback(_ sender: NSControl) {
let loginItems = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue()
let loginItemsSnapshot = LSSharedFileListCopySnapshot(loginItems, nil).takeRetainedValue() as! [LSSharedFileListItem]
let itemName = Bundle.main.bundleURL.lastPathComponent as CFString
let itemUrl = URL(fileURLWithPath: Bundle.main.bundlePath) as CFURL
loginItemsSnapshot.forEach {
if (LSSharedFileListItemCopyDisplayName($0)?.takeRetainedValue() == itemName) ||
(LSSharedFileListItemCopyResolvedURL($0, 0, nil)?.takeRetainedValue() == itemUrl) {
(LSSharedFileListItemCopyResolvedURL($0, 0, nil)?.takeRetainedValue() == itemUrl) {
LSSharedFileListItemRemove(loginItems, $0)
}
}
@@ -123,4 +127,4 @@ class GeneralTab {
LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst.takeRetainedValue(), nil, nil, itemUrl, nil, nil)
}
}
}
}
+32 -22
View File
@@ -1,20 +1,21 @@
import Cocoa
import Sparkle
import Preferences
class UpdatesTab: NSObject {
static var dontPeriodicallyCheck: NSButton!
static var periodicallyCheck: NSButton!
static var periodicallyInstall: NSButton!
static var policyObserver = PolicyObserver()
class UpdatesTab: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier("Updates")
let preferencePaneTitle = NSLocalizedString("Updates", comment: "")
let toolbarItemIcon = NSImage(named: NSImage.refreshTemplateName)!
static var policyObserver: PolicyObserver!
var dontPeriodicallyCheck: NSButton!
var periodicallyCheck: NSButton!
var periodicallyInstall: NSButton!
// this helps prevent double-dipping (i.e. user updates the UI > changes the preference > updates the UI)
static var policyLock = false
var policyLock = false
static func observeUserDefaults() {
UserDefaults.standard.addObserver(UpdatesTab.policyObserver, forKeyPath: "SUAutomaticallyUpdate", options: [.initial, .new], context: nil)
UserDefaults.standard.addObserver(UpdatesTab.policyObserver, forKeyPath: "SUEnableAutomaticChecks", options: [.initial, .new], context: nil)
}
static func makeView() -> NSGridView {
override func loadView() {
UpdatesTab.policyObserver = PolicyObserver(self)
dontPeriodicallyCheck = NSButton(radioButtonWithTitle: NSLocalizedString("Don't check for updates periodically", comment: ""), target: self, action: #selector(updatePolicyCallback))
dontPeriodicallyCheck.fit()
periodicallyCheck = NSButton(radioButtonWithTitle: NSLocalizedString("Check for updates periodically", comment: ""), target: self, action: #selector(updatePolicyCallback))
@@ -28,7 +29,7 @@ class UpdatesTab: NSObject {
policies.spacing = GridView.interPadding / 2
let grid = GridView([
[policyLabel, policies],
[NSButton(title: NSLocalizedString("Check for updates now…", comment: ""), target: self, action: #selector(checkForUpdatesNow))],
[NSButton(title: NSLocalizedString("Check for updates now…", comment: ""), target: nil, action: #selector(UpdatesTab.checkForUpdatesNow))],
])
grid.cell(atColumnIndex: 0, rowIndex: 0).xPlacement = .trailing
let row1 = grid.row(at: 1)
@@ -36,16 +37,19 @@ class UpdatesTab: NSObject {
row1.topPadding = GridView.interPadding
row1.cell(at: 0).xPlacement = .center
grid.fit()
return grid
view = grid
}
@objc
static func checkForUpdatesNow(_ sender: Any) {
static func observeUserDefaults() {
UserDefaults.standard.addObserver(UpdatesTab.policyObserver, forKeyPath: "SUAutomaticallyUpdate", options: [.initial, .new], context: nil)
UserDefaults.standard.addObserver(UpdatesTab.policyObserver, forKeyPath: "SUEnableAutomaticChecks", options: [.initial, .new], context: nil)
}
@objc static func checkForUpdatesNow(_ sender: Any) {
SUUpdater.shared().checkForUpdates(sender)
}
@objc
static func updatePolicyCallback() {
@objc func updatePolicyCallback() {
policyLock = true
SUUpdater.shared().automaticallyDownloadsUpdates = periodicallyInstall.state == .on
SUUpdater.shared().automaticallyChecksForUpdates = periodicallyInstall.state == .on || periodicallyCheck.state == .on
@@ -54,16 +58,22 @@ class UpdatesTab: NSObject {
}
class PolicyObserver: NSObject {
var updatesTab: UpdatesTab
init(_ updatesTab: UpdatesTab) {
self.updatesTab = updatesTab
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard !UpdatesTab.policyLock else { return }
guard !updatesTab.policyLock else { return }
if SUUpdater.shared().automaticallyDownloadsUpdates {
UpdatesTab.periodicallyInstall.state = .on
updatesTab.periodicallyInstall.state = .on
// Sparkle UI "Automatically download and install updates in the future" doesn't activate periodical checks; we do it manually
SUUpdater.shared().automaticallyChecksForUpdates = true
} else if SUUpdater.shared().automaticallyChecksForUpdates {
UpdatesTab.periodicallyCheck.state = .on
updatesTab.periodicallyCheck.state = .on
} else {
UpdatesTab.dontPeriodicallyCheck.state = .on
updatesTab.dontPeriodicallyCheck.state = .on
}
}
}