diff --git a/alt-tab-macos.xcodeproj/project.pbxproj b/alt-tab-macos.xcodeproj/project.pbxproj index 4861c5cb0..a64ca210b 100644 --- a/alt-tab-macos.xcodeproj/project.pbxproj +++ b/alt-tab-macos.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ D04BA084CD1236EC78D90A01 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BACCBE5F97BE9B6CA645B /* Localizable.strings */; }; D04BA100BD0F47828EB649FF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAAEC2847830A3991F8D1 /* InfoPlist.strings */; }; D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */; }; - D04BA15A1B0C4871EA7CB899 /* ShortcutsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACE22DC907F03D193075 /* ShortcutsTab.swift */; }; + D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACE22DC907F03D193075 /* GeneralTab.swift */; }; D04BA1B133D53572D7B312C2 /* CollectionViewItemFontIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA1DF8CAB2FAB7FE9244B /* CollectionViewItemFontIcon.swift */; }; D04BA1CEC6B9C8945FEC8740 /* CollectionViewItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA258B56193958D60978A /* CollectionViewItemView.swift */; }; D04BA26A691D56031FCCF00C /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8DB8AA7E5570DAC568A /* Sysctl.swift */; }; @@ -148,7 +148,7 @@ D04BACABD048E62EBE4576CC /* DebugProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugProfile.swift; sourceTree = ""; }; D04BACB97A5895839BCB14BD /* es */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = es; path = InfoPlist.strings; sourceTree = ""; }; D04BACD976030676FD0761D5 /* Windows.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = ""; }; - D04BACE22DC907F03D193075 /* ShortcutsTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsTab.swift; sourceTree = ""; }; + D04BACE22DC907F03D193075 /* GeneralTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralTab.swift; sourceTree = ""; }; D04BACEE8D430B8CAAD8C4CD /* BoldLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoldLabel.swift; sourceTree = ""; }; D04BAD1297730B191E96E7FE /* CollectionViewItemTitle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItemTitle.swift; sourceTree = ""; }; D04BAD241A6928F45355B315 /* es */ = {isa = PBXFileReference; fileEncoding = 2483028224; lastKnownFileType = text.plist.strings; name = es; path = Localizable.strings; sourceTree = ""; }; @@ -310,7 +310,7 @@ children = ( D04BA64F1F344007EA13BA05 /* AppearanceTab.swift */, D04BA4A26987F67DD94C827F /* AboutTab.swift */, - D04BACE22DC907F03D193075 /* ShortcutsTab.swift */, + D04BACE22DC907F03D193075 /* GeneralTab.swift */, D04BAD60C97E609A759E721E /* UpdatesTab.swift */, ); path = tabs; @@ -685,7 +685,7 @@ D04BADBCF20CD72057E7CF09 /* TabViewItem.swift in Sources */, D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */, D04BAD1BE9DC22C48C53D195 /* AboutTab.swift in Sources */, - D04BA15A1B0C4871EA7CB899 /* ShortcutsTab.swift in Sources */, + D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */, D04BA8092885B40CE3527370 /* UpdatesTab.swift in Sources */, D04BAD451966B43720120D2E /* Menubar.swift in Sources */, D04BA2E64C59D96F6EB27D9D /* FeedbackWindow.swift in Sources */, diff --git a/src/api-wrappers/HelperExtensions.swift b/src/api-wrappers/HelperExtensions.swift index 2a1eb5e18..b12001dfa 100644 --- a/src/api-wrappers/HelperExtensions.swift +++ b/src/api-wrappers/HelperExtensions.swift @@ -122,3 +122,42 @@ extension Array { group.wait() } } + +// allow using a closure for NSControl action, instead of selector +class SelectorWrapper { + let selector: Selector + let closure: (T) -> Void + + init(withClosure closure: @escaping (T) -> Void) { + self.selector = #selector(callClosure) + self.closure = closure + } + + @objc + private func callClosure(sender: AnyObject) { + closure(sender as! T) + } +} + +fileprivate var handle: Int = 0 + +typealias ActionClosure = (NSControl) -> Void + +extension NSControl { + var onAction: ActionClosure? { + get { + return nil + } + set { + if let newValue = newValue { + let selectorWrapper = SelectorWrapper(withClosure: newValue) + objc_setAssociatedObject(self, &handle, selectorWrapper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + action = selectorWrapper.selector + target = selectorWrapper + } else { + action = nil + target = nil + } + } + } +} diff --git a/src/logic/Preferences.swift b/src/logic/Preferences.swift index ce15129a7..48729e835 100644 --- a/src/logic/Preferences.swift +++ b/src/logic/Preferences.swift @@ -18,6 +18,7 @@ class Preferences { "theme": MacroPreferences.themeList.keys.first!, "showOnScreen": MacroPreferences.showOnScreenList.keys.first!, "hideSpaceNumberLabels": false, + "startAtLogin": true, ] // constant values @@ -39,6 +40,7 @@ class Preferences { static var tabKeyCode: UInt16 { UInt16(defaults.integer(forKey: "tabKeyCode")) } static var windowDisplayDelay: DispatchTimeInterval { DispatchTimeInterval.milliseconds(defaults.integer(forKey: "windowDisplayDelay")) } static var hideSpaceNumberLabels: Bool { defaults.bool(forKey: "hideSpaceNumberLabels") } + static var startAtLogin: Bool { defaults.bool(forKey: "startAtLogin") } // macro values static var theme: Theme { MacroPreferences.themeList[defaults.string(forKey: "theme")!]! } diff --git a/src/ui/App.swift b/src/ui/App.swift index 27962b1a2..5ef32d6cf 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -9,6 +9,7 @@ class App: NSApplication, NSApplicationDelegate { static let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String static let licence = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") as! String static let repository = "https://github.com/lwouis/alt-tab-macos" + static let url = URL(fileURLWithPath: Bundle.main.bundlePath) as CFURL var statusItem: NSStatusItem? var thumbnailsPanel: ThumbnailsPanel? var preferencesWindow: PreferencesWindow? diff --git a/src/ui/preferences-window/LabelAndControl.swift b/src/ui/preferences-window/LabelAndControl.swift index ed71bb54c..1884870a9 100644 --- a/src/ui/preferences-window/LabelAndControl.swift +++ b/src/ui/preferences-window/LabelAndControl.swift @@ -1,8 +1,6 @@ import Cocoa class LabelAndControl: NSObject { - static var callbackTarget: PreferencesWindow! - static func makeLabelWithInput(_ labelText: String, _ rawName: String, _ width: CGFloat, _ suffixText: String? = nil, _ suffixUrl: String? = nil, _ validator: ((String) -> Bool)? = nil) -> [NSView] { let input = TextField(Preferences.getAsString(rawName)!) input.validationHandler = validator @@ -13,10 +11,10 @@ class LabelAndControl: NSObject { return [views[0], NSStackView(views: [views[1], makeSuffix(rawName, suffixText!, suffixUrl)])] } - static func makeLabelWithCheckbox(_ labelText: String, _ rawName: String) -> [NSView] { + static func makeLabelWithCheckbox(_ labelText: String, _ rawName: String, extraAction: ActionClosure? = nil) -> [NSView] { let checkbox = NSButton(checkboxWithTitle: "", target: nil, action: nil) setControlValue(checkbox, Preferences.getAsString(rawName)!) - return makeLabelWithProvidedControl(labelText, rawName, checkbox) + return makeLabelWithProvidedControl(labelText, rawName, checkbox, extraAction: extraAction) } static func makeLabelWithDropdown(_ labelText: String, _ rawName: String, _ values: [String], _ suffixText: String? = nil) -> [NSView] { @@ -40,19 +38,16 @@ class LabelAndControl: NSObject { return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText) } - static func makeLabelWithProvidedControl(_ labelText: String?, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil, _ suffixUrl: String? = nil) -> [NSView] { + static func makeLabelWithProvidedControl(_ labelText: String?, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil, _ suffixUrl: String? = nil, extraAction: ActionClosure? = nil) -> [NSView] { let label = makeLabel(labelText, rawName) control.identifier = NSUserInterfaceItemIdentifier(rawName) - control.target = self - control.action = #selector(controlWasChanged) + control.onAction = { + PreferencesWindow.controlWasChanged($0) + extraAction?($0) + } return [label, control, suffixText != nil ? makeSuffix(rawName, suffixText!, suffixUrl) : NSView()] } - @objc - static func controlWasChanged(senderControl: NSControl) { - callbackTarget.controlWasChanged(senderControl) - } - private static func makeLabel(_ labelText: String?, _ rawName: String) -> NSTextField { let label = NSTextField(wrappingLabelWithString: labelText != nil ? labelText! + ": " : "") label.fit() diff --git a/src/ui/preferences-window/PreferencesWindow.swift b/src/ui/preferences-window/PreferencesWindow.swift index fdab18b29..102920904 100644 --- a/src/ui/preferences-window/PreferencesWindow.swift +++ b/src/ui/preferences-window/PreferencesWindow.swift @@ -5,7 +5,6 @@ class PreferencesWindow: NSWindow { override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) { super.init(contentRect: .zero, styleMask: style, backing: backingStoreType, defer: flag) - LabelAndControl.callbackTarget = self setupWindow() setupTabViews() } @@ -15,7 +14,7 @@ class PreferencesWindow: NSWindow { makeKeyAndOrderFront(nil) } - func controlWasChanged(_ senderControl: NSControl) { + @objc static func controlWasChanged(_ senderControl: NSControl) { let newValue = LabelAndControl.getControlValue(senderControl) LabelAndControl.updateControlExtras(senderControl, newValue) Preferences.set(senderControl.identifier!.rawValue, newValue) @@ -31,7 +30,7 @@ class PreferencesWindow: NSWindow { private func setupTabViews() { contentViewController = tabViewController tabViewController.tabStyle = .toolbar - tabViewController.addTabViewItem(ShortcutsTab.make()) + tabViewController.addTabViewItem(GeneralTab.make()) tabViewController.addTabViewItem(AppearanceTab.make()) tabViewController.addTabViewItem(UpdatesTab.make()) tabViewController.addTabViewItem(AboutTab.make()) diff --git a/src/ui/preferences-window/tabs/GeneralTab.swift b/src/ui/preferences-window/tabs/GeneralTab.swift new file mode 100644 index 000000000..155b24c2f --- /dev/null +++ b/src/ui/preferences-window/tabs/GeneralTab.swift @@ -0,0 +1,60 @@ +import Cocoa + +class GeneralTab { + private static let rowHeight = CGFloat(22) // height of the "Tab key" input + + static func make() -> NSTabViewItem { + return TabViewItem.make(NSLocalizedString("General", comment: ""), NSImage.preferencesGeneralName, makeView()) + } + + private static func makeView() -> NSGridView { + // TODO: make the validators be a part of each Preference + let tabKeyCodeValidator: ((String) -> Bool) = { + guard let int = Int($0) else { + return false + } + // non-special keys (mac & pc keyboards): https://eastmanreference.com/complete-list-of-applescript-key-codes + var whitelistedKeycodes: [Int] = Array(0...53) + whitelistedKeycodes.append(contentsOf: [65, 67, 69, 75, 76, 78, ]) + whitelistedKeycodes.append(contentsOf: Array(81...89)) + whitelistedKeycodes.append(contentsOf: [91, 92, 115, 116, 117, 119, 121]) + whitelistedKeycodes.append(contentsOf: Array(123...126)) + return whitelistedKeycodes.contains(int) + } + + let startAtLogin = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Start at login", comment: ""), "startAtLogin", extraAction: startAtLoginCallback) + let view = GridView.make([ + startAtLogin, + LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Alt key", comment: ""), "metaKey", MacroPreferences.metaKeyList.values.map { $0.label }), + LabelAndControl.makeLabelWithInput(NSLocalizedString("Tab key", comment: ""), "tabKeyCode", 33, NSLocalizedString("KeyCodes Reference", comment: ""), "https://eastmanreference.com/complete-list-of-applescript-key-codes", tabKeyCodeValidator), + ]) + view.column(at: 0).xPlacement = .trailing + view.rowAlignment = .lastBaseline + view.setRowsHeight(rowHeight) + view.fit() + setLoginItemIfCheckboxIsOn(startAtLogin[1] as! NSButton) + return view + } + + private static func setLoginItemIfCheckboxIsOn(_ startAtLoginCheckbox: NSButton) { + if startAtLoginCheckbox.state == .on { + startAtLoginCallback(startAtLoginCheckbox) + } + } + + // adding/removing login item depending on the checkbox state + @available(OSX, deprecated: 10.11) + @objc static func startAtLoginCallback(_ sender: NSControl) { + let loginItems = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() + let loginItemsSnapshot = LSSharedFileListCopySnapshot(loginItems, nil).takeRetainedValue() as! [LSSharedFileListItem] + if (sender as! NSButton).state == .on { + LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst.takeRetainedValue(), nil, nil, App.url, nil, nil) + } else { + loginItemsSnapshot.forEach { + if CFEqual(LSSharedFileListItemCopyResolvedURL($0, 0, nil).takeRetainedValue(), App.url) { + LSSharedFileListItemRemove(loginItems, $0) + } + } + } + } +} diff --git a/src/ui/preferences-window/tabs/ShortcutsTab.swift b/src/ui/preferences-window/tabs/ShortcutsTab.swift deleted file mode 100644 index 2a016322e..000000000 --- a/src/ui/preferences-window/tabs/ShortcutsTab.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Cocoa - -class ShortcutsTab { - private static let rowHeight = CGFloat(22) // height of the "Tab key" input - - static func make() -> NSTabViewItem { - return TabViewItem.make(NSLocalizedString("Shortcuts", comment: ""), NSImage.preferencesGeneralName, makeView()) - } - - private static func makeView() -> NSGridView { // TODO: make the validators be a part of each Preference - let tabKeyCodeValidator: ((String) -> Bool) = { - guard let int = Int($0) else { - return false - } - // non-special keys (mac & pc keyboards): https://eastmanreference.com/complete-list-of-applescript-key-codes - var whitelistedKeycodes: [Int] = Array(0...53) - whitelistedKeycodes.append(contentsOf: [65, 67, 69, 75, 76, 78, ]) - whitelistedKeycodes.append(contentsOf: Array(81...89)) - whitelistedKeycodes.append(contentsOf: [91, 92, 115, 116, 117, 119, 121]) - whitelistedKeycodes.append(contentsOf: Array(123...126)) - return whitelistedKeycodes.contains(int) - } - - let view = GridView.make([ - LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Alt key", comment: ""), "metaKey", MacroPreferences.metaKeyList.values.map { $0.label }), - LabelAndControl.makeLabelWithInput(NSLocalizedString("Tab key", comment: ""), "tabKeyCode", 33, NSLocalizedString("KeyCodes Reference", comment: ""), "https://eastmanreference.com/complete-list-of-applescript-key-codes", tabKeyCodeValidator), - ]) - view.column(at: 0).xPlacement = .trailing - view.rowAlignment = .lastBaseline - view.setRowsHeight(rowHeight) - view.fit() - return view - } -}