feat: more flexible shortcuts (closes #72)

closes #50, closes #125, closes #133
This commit is contained in:
Louis Pontoise authored and lwouis committed 2020-04-02 19:39:39 +09:00
1 parent 48bb3df240
commit 5eade75983
26 files changed
+479 -274

No files matched your search

+1 -1
View File
@@ -4,5 +4,5 @@ target 'alt-tab-macos' do
use_frameworks!
pod 'LetsMove', '1.24'
pod 'Sparkle', '1.23.0'
pod 'ShortcutRecorder', :git => 'https://github.com/Kentzo/ShortcutRecorder.git', :branch => 'issue-114'
end
+14 -1
View File
@@ -1,9 +1,11 @@
PODS:
- LetsMove (1.24)
- ShortcutRecorder (3.1)
- Sparkle (1.23.0)
DEPENDENCIES:
- LetsMove (= 1.24)
- ShortcutRecorder (from `https://github.com/Kentzo/ShortcutRecorder.git`, branch `issue-114`)
- Sparkle (= 1.23.0)
SPEC REPOS:
@@ -11,10 +13,21 @@ SPEC REPOS:
- LetsMove
- Sparkle
EXTERNAL SOURCES:
ShortcutRecorder:
:branch: issue-114
:git: https://github.com/Kentzo/ShortcutRecorder.git
CHECKOUT OPTIONS:
ShortcutRecorder:
:commit: 4aebe01e1ec9eebb520ef50e42bc321be473c580
:git: https://github.com/Kentzo/ShortcutRecorder.git
SPEC CHECKSUMS:
LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
Sparkle: 55b1a87ba69d56913375a281546b7c82dec95bb0
PODFILE CHECKSUM: 465451026269525f0f1d2dc7053cf0b789a35421
PODFILE CHECKSUM: 7a88a7e0e87002ed5ea9c531fdea213ac6657496
COCOAPODS: 1.8.4
+14 -8
View File
@@ -17,6 +17,8 @@
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */; };
D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */; };
D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACE22DC907F03D193075 /* GeneralTab.swift */; };
D04BA1621718FF26F8D5E75D /* UnclearableRecorderControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE333F7170E87C5AC0EF /* UnclearableRecorderControl.swift */; };
D04BA1637E125D38546C26E2 /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8AC77465E1B2BC947CF /* StackView.swift */; };
D04BA1B133D53572D7B312C2 /* ThumbnailFontIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA1DF8CAB2FAB7FE9244B /* ThumbnailFontIconView.swift */; };
D04BA1C85041FB043FD43E65 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA05006D56C7AA011C0CE /* Localizable.strings */; };
D04BA1CEC6B9C8945FEC8740 /* ThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA258B56193958D60978A /* ThumbnailView.swift */; };
@@ -33,7 +35,6 @@
D04BA40CC1415DA69CCE5D89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA17FC84640580894400E /* InfoPlist.strings */; };
D04BA48B00B4211A465C7337 /* DebugProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACABD048E62EBE4576CC /* DebugProfile.swift */; };
D04BA570E7806F28741B1472 /* SF-Pro-Text-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = D04BA0CDCFF1F0B1A77E3E86 /* SF-Pro-Text-Regular.otf */; };
D04BA5F99B45DC13B9E9DD91 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8276B3D3905E80B1739 /* Keyboard.swift */; };
D04BA6187A91A847844B6ABB /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA015A45DE7AFDC9794FE /* Window.swift */; };
D04BA691CB6082A3C39CBC89 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE757BB2B605234FBF58 /* TabViewController.swift */; };
D04BA69D47B5E60A6AD9CBD9 /* ThumbnailTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1297730B191E96E7FE /* ThumbnailTitleView.swift */; };
@@ -42,6 +43,7 @@
D04BA748B2BECEDF57750E5F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA447E4AB0750BA7857FE /* InfoPlist.strings */; };
D04BA76A74267B1346D23687 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA6D57A1456C07318B8EA /* GridView.swift */; };
D04BA775CF3F8D9394A1E256 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA68C2561D9EE4FD851B8 /* Screen.swift */; };
D04BA78B5D288BF072050C43 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA857B1AB33FBCC59A86 /* Keyboard.swift */; };
D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA64F1F344007EA13BA05 /* AppearanceTab.swift */; };
D04BA7F86F1926FBE31F44BF /* BaseLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA53992F116E5E704CAB3 /* BaseLabel.swift */; };
D04BA8092885B40CE3527370 /* UpdatesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD60C97E609A759E721E /* UpdatesTab.swift */; };
@@ -70,7 +72,6 @@
D04BAE6A13DBAE85B8F7CB6A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BABF92E92F093C18BB1A1 /* Localizable.strings */; };
D04BAEE31B6FFCDC779E6C17 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D04BAC2FF99F629CD4ED20FC /* MainMenu.xib */; };
D04BAF12DF5D15B9D7D316A4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA61693F710CD7BD054D7 /* InfoPlist.strings */; };
D04BAF25E67A5B31CF7676DB /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA44F7B5E58A08416706B /* TextField.swift */; };
D04BAFB973C3D28718FAEB87 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACD976030676FD0761D5 /* Windows.swift */; };
D04BAFBC862BA5FE0294EA7A /* AXUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA6F823BC0EDA9AA4B80A /* AXUIElement.swift */; };
/* End PBXBuildFile section */
@@ -113,7 +114,6 @@
D04BA399F1DF2C61FC2C9599 /* menubar-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-icon@2x.png"; sourceTree = "<group>"; };
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>"; };
D04BA44F7B5E58A08416706B /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; 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>"; };
@@ -145,13 +145,14 @@
D04BA7C836A8CE8C0B8D128B /* TextArea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextArea.swift; sourceTree = "<group>"; };
D04BA7CF9C2D1BEC7C05AB24 /* Spaces.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = "<group>"; };
D04BA7ECCE728582D9ECA613 /* determine_version_and_changelog.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version_and_changelog.sh; sourceTree = "<group>"; };
D04BA8276B3D3905E80B1739 /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
D04BA82F792DF53958D92572 /* AltTab.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltTab.app; sourceTree = BUILT_PRODUCTS_DIR; };
D04BA8AC77465E1B2BC947CF /* StackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = "<group>"; };
D04BA8DB8AA7E5570DAC568A /* Sysctl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sysctl.swift; sourceTree = "<group>"; };
D04BA93F02C9B26B05032B7A /* ja */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ja; path = Localizable.strings; sourceTree = "<group>"; };
D04BA9B93823398A542FF7A0 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
D04BA9EF65B2E7AF9E3ADCA3 /* 2 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BAA34E0CB00DED7C04B4F /* 2-rows.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2-rows.jpg"; sourceTree = "<group>"; };
D04BAA857B1AB33FBCC59A86 /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsView.swift; sourceTree = "<group>"; };
D04BAA9E0539EE620D08F63F /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = fr; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAAB92261FC04854FDDE9 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
@@ -185,6 +186,7 @@
D04BADB20AB31BF83593E0BE /* greetings.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = greetings.yml; sourceTree = "<group>"; };
D04BAE1243C9B4BE3ED1B524 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = "<group>"; };
D04BAE2DC036FD84446E1AE6 /* menubar-icon.svg */ = {isa = PBXFileReference; lastKnownFileType = file.svg; path = "menubar-icon.svg"; sourceTree = "<group>"; };
D04BAE333F7170E87C5AC0EF /* UnclearableRecorderControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnclearableRecorderControl.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>"; };
@@ -239,7 +241,6 @@
D04BA0D80B24E72B9B981A1D /* logic */ = {
isa = PBXGroup;
children = (
D04BA8276B3D3905E80B1739 /* Keyboard.swift */,
D04BACD976030676FD0761D5 /* Windows.swift */,
D04BA9B93823398A542FF7A0 /* Preferences.swift */,
D04BA68C2561D9EE4FD851B8 /* Screen.swift */,
@@ -250,6 +251,7 @@
D04BA282BB16C1554595A968 /* Applications.swift */,
D04BAB74451B79FE18B8BEDF /* DispatchQueues.swift */,
D04BACABD048E62EBE4576CC /* DebugProfile.swift */,
D04BAA857B1AB33FBCC59A86 /* Keyboard.swift */,
);
path = logic;
sourceTree = "<group>";
@@ -335,6 +337,8 @@
children = (
D04BA6D57A1456C07318B8EA /* GridView.swift */,
D04BAA41B9A74189B065D856 /* text */,
D04BAE333F7170E87C5AC0EF /* UnclearableRecorderControl.swift */,
D04BA8AC77465E1B2BC947CF /* StackView.swift */,
);
path = "generic-components";
sourceTree = "<group>";
@@ -498,7 +502,6 @@
children = (
D04BACEE8D430B8CAAD8C4CD /* BoldLabel.swift */,
D04BA49E45BFFF3D9FC60E43 /* HyperlinkLabel.swift */,
D04BA44F7B5E58A08416706B /* TextField.swift */,
D04BA7C836A8CE8C0B8D128B /* TextArea.swift */,
D04BA53992F116E5E704CAB3 /* BaseLabel.swift */,
);
@@ -738,12 +741,14 @@
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}/ShortcutRecorder/ShortcutRecorder.framework",
"${PODS_ROOT}/Sparkle/Sparkle.framework",
"${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LetsMove.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShortcutRecorder.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework",
"${DWARF_DSYM_FOLDER_PATH}/Sparkle.framework.dSYM",
);
@@ -766,7 +771,6 @@
D04BABED81800E18732912CC /* CGWindowID.swift in Sources */,
D04BA26A691D56031FCCF00C /* Sysctl.swift in Sources */,
D04BA8480A8FF466CA89DA5B /* main.swift in Sources */,
D04BA5F99B45DC13B9E9DD91 /* Keyboard.swift in Sources */,
D04BAFB973C3D28718FAEB87 /* Windows.swift in Sources */,
D04BAC011A71E0418154F8CD /* Preferences.swift in Sources */,
D04BA775CF3F8D9394A1E256 /* Screen.swift in Sources */,
@@ -795,10 +799,12 @@
D04BA76A74267B1346D23687 /* GridView.swift in Sources */,
D04BAB4EB890853B5B9B2C61 /* BoldLabel.swift in Sources */,
D04BAABE804F3769CE22BEB6 /* HyperlinkLabel.swift in Sources */,
D04BAF25E67A5B31CF7676DB /* TextField.swift in Sources */,
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */,
D04BA7F86F1926FBE31F44BF /* BaseLabel.swift in Sources */,
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */,
D04BA1621718FF26F8D5E75D /* UnclearableRecorderControl.swift in Sources */,
D04BA78B5D288BF072050C43 /* Keyboard.swift in Sources */,
D04BA1637E125D38546C26E2 /* StackView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+4
View File
@@ -27,6 +27,10 @@ extension AXUIElement {
return cgWindowId().level() == CGWindowLevelForKey(.normalWindow)
}
func position() -> CGPoint? {
return value(kAXPositionAttribute, CGPoint.zero, .cgPoint)
}
func title() -> String? {
return attribute(kAXTitleAttribute, String.self)
}
+1
View File
@@ -235,6 +235,7 @@ func SLSRequestScreenCaptureAccess() -> UInt8
//}
//
//// returns the status of the "Displays have separate Spaces" system Preference
//// there is a public API for that: NSScreen.screensHaveSeparateSpaces
//// * macOS 10.10+
//@_silgen_name("CGSGetSpaceManagementMode")
//func CGSGetSpaceManagementMode(_ cid: CGSConnectionID) -> SpaceManagementMode
+9 -4
View File
@@ -48,8 +48,13 @@ class Application: NSObject {
}
func observeNewWindows() {
if let windows = axUiElement!.windows(), windows.count > 0 {
addWindows(windows.filter { $0.isActualWindow(runningApplication) && Windows.list.firstIndexThatMatches($0) == nil })
if let windows = axUiElement!.windows() {
let actualWindows = windows.filter {
$0.isActualWindow(runningApplication) && Windows.list.firstIndexThatMatches($0) == nil
}
if actualWindows.count > 0 {
addWindows(actualWindows)
}
}
}
@@ -62,7 +67,7 @@ class Application: NSObject {
private func addWindows(_ axWindows: [AXUIElement]) {
let windows = axWindows.map { Window($0, self) }
Windows.list.insertAndScaleRecycledPool(windows, at: 0)
Windows.moveFocusedWindowIndexAfterWindowCreatedInBackground(windows.count)
Windows.cycleFocusedWindowIndex(windows.count)
App.app.refreshOpenUi(windows)
}
@@ -121,7 +126,7 @@ private func eventWindowCreated(_ app: App, _ element: AXUIElement, _ applicatio
guard Windows.list.firstIndexThatMatches(element) == nil else { return }
let window = Window(element, application)
Windows.list.insertAndScaleRecycledPool([window], at: 0)
Windows.moveFocusedWindowIndexAfterWindowCreatedInBackground(1)
Windows.cycleFocusedWindowIndex(1)
app.refreshOpenUi([window])
}
+16 -43
View File
@@ -2,14 +2,14 @@ import Cocoa
import Carbon.HIToolbox.Events
class Keyboard {
static func listenToGlobalEvents(_ delegate: App) {
listenToGlobalKeyboardEvents(delegate)
static func listenToGlobalEvents() {
listenToGlobalKeyboardEvents()
}
}
var eventTap: CFMachPort?
func listenToGlobalKeyboardEvents(_ app: App) {
func listenToGlobalKeyboardEvents() {
DispatchQueues.keyboardEvents.async {
let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
@@ -19,7 +19,7 @@ func listenToGlobalKeyboardEvents(_ app: App) {
options: .defaultTap,
eventsOfInterest: eventMask,
callback: keyboardHandler,
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(app).toOpaque()))
userInfo: nil)
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CGEvent.tapEnable(tap: eventTap!, enable: true)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
@@ -27,49 +27,22 @@ func listenToGlobalKeyboardEvents(_ app: App) {
}
}
func dispatchWork(_ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
(App.shared as! App).uiWorkShouldBeDone = true
DispatchQueue.main.async {
fn()
}
return nil // previously focused app should not receive keys
}
func dispatchWorkCurrentThread(_ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
(App.shared as! App).uiWorkShouldBeDone = false
fn()
return nil // previously focused app should not receive keys
}
func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent, appPointer: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let app = Unmanaged<App>.fromOpaque(appPointer!).takeUnretainedValue()
func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
if type == .keyDown || type == .keyUp || type == .flagsChanged {
if let event = NSEvent(cgEvent: event_) {
let isTab = event.keyCode == Preferences.tabKeyCode
let isMetaChanged = Preferences.metaKeyCodes.contains(event.keyCode)
let isMetaDown = event.modifierFlags.contains(Preferences.metaModifierFlag)
let isRightArrow = event.keyCode == kVK_RightArrow
let isLeftArrow = event.keyCode == kVK_LeftArrow
let isEscape = event.keyCode == kVK_Escape
if type == .keyDown && isEscape && app.appIsBeingUsed {
return dispatchWorkCurrentThread { app.hideUi() }
} else if isMetaDown && type == .keyDown {
if isTab && event.modifierFlags.contains(.shift) {
return dispatchWork { app.showUiOrCycleSelection(-1) }
} else if isTab {
return dispatchWork { app.showUiOrCycleSelection(1) }
} else if isRightArrow && app.appIsBeingUsed {
return dispatchWork { app.cycleSelection(1) }
} else if isLeftArrow && app.appIsBeingUsed {
return dispatchWork { app.cycleSelection(-1) }
}
} else if isMetaChanged && !isMetaDown {
return dispatchWorkCurrentThread { app.focusTarget() }
if let event_ = NSEvent(cgEvent: cgEvent),
// workaround: NSEvent.characters is not safe outside of the main thread; this is not documented by Apple
// see https://github.com/Kentzo/ShortcutRecorder/issues/114#issuecomment-606465340
let event = NSEvent.keyEvent(with: event_.type, location: event_.locationInWindow, modifierFlags: event_.modifierFlags,
timestamp: event_.timestamp, windowNumber: event_.windowNumber, context: event_.context, characters: "",
charactersIgnoringModifiers: "", isARepeat: type == .flagsChanged ? false : event_.isARepeat, keyCode: event_.keyCode) {
let appWasBeingUsed = App.app.appIsBeingUsed
App.shortcutMonitor.handle(event, withTarget: nil)
if appWasBeingUsed || App.app.appIsBeingUsed {
return nil // focused app won't receive the event
}
}
} else if type == .tapDisabledByUserInput || type == .tapDisabledByTimeout {
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
// focused app will receive the event
return Unmanaged.passRetained(event_)
return Unmanaged.passRetained(cgEvent) // focused app will receive the event
}
+112 -48
View File
@@ -12,12 +12,21 @@ class Preferences {
"rowsCount": Float(3),
"iconSize": Float(32),
"fontHeight": Float(15),
"tabKeyCode": kVK_Tab,
"holdShortcut": "⌥",
"nextWindowShortcut": "⇥",
"previousWindowShortcut": "⇧⇥",
"cancelShortcut": "⎋",
"arrowKeysEnabled": true,
"mouseHoverEnabled": true,
"showMinimizedWindows": true,
"showHiddenWindows": true,
"windowDisplayDelay": 0,
"metaKey": MacroPreferences.metaKeyList.keys.first!,
"theme": MacroPreferences.themeList.keys.first!,
"showOnScreen": MacroPreferences.showOnScreenList.keys.first!,
"alignThumbnails": MacroPreferences.alignThumbnailsList.keys.first!,
"theme": ThemePreference.macOs.rawValue,
"showOnScreen": ShowOnScreenPreference.active.rawValue,
"alignThumbnails": AlignThumbnailsPreference.left.rawValue,
"appsToShow": AppsToShowPreference.all.rawValue,
"spacesToShow": SpacesToShowPreference.all.rawValue,
"screensToShow": ScreensToShowPreference.all.rawValue,
"hideSpaceNumberLabels": false,
"startAtLogin": true,
]
@@ -38,25 +47,32 @@ class Preferences {
static var rowsCount: CGFloat { CGFloat(defaults.float(forKey: "rowsCount")) }
static var iconSize: CGFloat { CGFloat(defaults.float(forKey: "iconSize")) }
static var fontHeight: CGFloat { CGFloat(defaults.float(forKey: "fontHeight")) }
static var tabKeyCode: UInt16 { UInt16(defaults.integer(forKey: "tabKeyCode")) }
static var holdShortcut: String { defaults.string(forKey: "holdShortcut")! }
static var nextWindowShortcut: String { defaults.string(forKey: "nextWindowShortcut")! }
static var previousWindowShortcut: String { defaults.string(forKey: "previousWindowShortcut")! }
static var cancelShortcut: String { defaults.string(forKey: "cancelShortcut")! }
static var arrowKeysEnabled: Bool { defaults.bool(forKey: "arrowKeysEnabled") }
static var mouseHoverEnabled: Bool { defaults.bool(forKey: "mouseHoverEnabled") }
static var showMinimizedWindows: Bool { defaults.bool(forKey: "showMinimizedWindows") }
static var showHiddenWindows: Bool { defaults.bool(forKey: "showHiddenWindows") }
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")!]! }
static var metaKey: MetaKey { MacroPreferences.metaKeyList[defaults.string(forKey: "metaKey")!]! }
static var showOnScreen: ShowOnScreenPreference { MacroPreferences.showOnScreenList[defaults.string(forKey: "showOnScreen")!]! }
static var alignThumbnails: AlignThumbnailsPreference { MacroPreferences.alignThumbnailsList[defaults.string(forKey: "alignThumbnails")!]! }
static var theme: ThemePreference { ThemePreference(rawValue: defaults.string(forKey: "theme")!)! }
static var showOnScreen: ShowOnScreenPreference { ShowOnScreenPreference(rawValue: defaults.string(forKey: "showOnScreen")!)! }
static var alignThumbnails: AlignThumbnailsPreference { AlignThumbnailsPreference(rawValue: defaults.string(forKey: "alignThumbnails")!)! }
static var appsToShow: AppsToShowPreference { AppsToShowPreference(rawValue: defaults.string(forKey: "appsToShow")!)! }
static var spacesToShow: SpacesToShowPreference { SpacesToShowPreference(rawValue: defaults.string(forKey: "spacesToShow")!)! }
static var screensToShow: ScreensToShowPreference { .showingAltTab }//ScreensToShowPreference(rawValue: defaults.string(forKey: "screensToShow")!)! }
// derived values
static var cellBorderWidth: CGFloat { theme.cellBorderWidth }
static var cellCornerRadius: CGFloat { theme.cellCornerRadius }
static var windowCornerRadius: CGFloat { theme.windowCornerRadius }
static var highlightBorderColor: NSColor { theme.highlightBorderColor }
static var highlightBackgroundColor: NSColor { theme.highlightBackgroundColor }
static var metaKeyCodes: [UInt16] { metaKey.keyCodes.map { UInt16($0) } }
static var metaModifierFlag: NSEvent.ModifierFlags { metaKey.modifierFlag }
static var cellBorderWidth: CGFloat { theme.themeParameters.cellBorderWidth }
static var cellCornerRadius: CGFloat { theme.themeParameters.cellCornerRadius }
static var windowCornerRadius: CGFloat { theme.themeParameters.windowCornerRadius }
static var highlightBorderColor: NSColor { theme.themeParameters.highlightBorderColor }
static var highlightBackgroundColor: NSColor { theme.themeParameters.highlightBackgroundColor }
static var font: NSFont { NSFont.systemFont(ofSize: fontHeight) }
static func registerDefaults() {
@@ -80,9 +96,21 @@ class Preferences {
}
static var all: [String: Any] { defaults.persistentDomain(forName: NSRunningApplication.current.bundleIdentifier!)! }
static func migrateOldPreferences() {
// "Main screen" was renamed to "Active screen"
if defaults.string(forKey: "showOnScreen") == "Main screen" {
defaults.removeObject(forKey: "showOnScreen")
}
}
}
struct Theme {
// MacroPreference are collection of values derived from a single key
// we don't want to store every value in UserDefaults as the user could change them and contradict the macro
protocol MacroPreference {
}
struct ThemeParameters {
let label: String
let cellBorderWidth: CGFloat
let cellCornerRadius: CGFloat
@@ -91,40 +119,76 @@ struct Theme {
let highlightBackgroundColor: NSColor
}
struct MetaKey {
let label: String
let keyCodes: [Int]
let modifierFlag: NSEvent.ModifierFlags
typealias LocalizedString = String
enum AppsToShowPreference: String, CaseIterable, MacroPreference {
case all = "All apps"
case active = "Active app"
var localizedString: LocalizedString {
switch self {
case .all: return NSLocalizedString("All apps", comment: "")
case .active: return NSLocalizedString("Active app", comment: "")
}
}
}
enum ShowOnScreenPreference {
case main
case mouse
enum SpacesToShowPreference: String, CaseIterable, MacroPreference {
case all = "All spaces"
case active = "Active space"
var localizedString: LocalizedString {
switch self {
case .all: return NSLocalizedString("All spaces", comment: "")
case .active: return NSLocalizedString("Active space", comment: "")
}
}
}
enum AlignThumbnailsPreference {
case left
case center
enum ScreensToShowPreference: String, CaseIterable, MacroPreference {
case all = "All screens"
case showingAltTab = "Screen showing AltTab"
var localizedString: LocalizedString {
switch self {
case .all: return NSLocalizedString("All screens", comment: "")
case .showingAltTab: return NSLocalizedString("Screen showing AltTab", comment: "")
}
}
}
// macros are collection of values derived from a single key
// we don't want to store every value in UserDefaults as the user could change them and contradict the macro
class MacroPreferences {
static let themeList = [
" macOS": Theme(label: " macOS", cellBorderWidth: 0, cellCornerRadius: 5, windowCornerRadius: 20, highlightBorderColor: .clear, highlightBackgroundColor: NSColor(red: 0, green: 0, blue: 0, alpha: 0.4)),
"❖ Windows 10": Theme(label: "❖ Windows 10", cellBorderWidth: 2, cellCornerRadius: 0, windowCornerRadius: 0, highlightBorderColor: .white, highlightBackgroundColor: .clear),
]
static let metaKeyList = [
"⌥ option": MetaKey(label: "⌥ option", keyCodes: [kVK_Option, kVK_RightOption], modifierFlag: .option),
"⌃ control": MetaKey(label: "⌃ control", keyCodes: [kVK_Control, kVK_RightControl], modifierFlag: .control),
"⌘ command": MetaKey(label: "⌘ command", keyCodes: [kVK_Command, kVK_RightCommand], modifierFlag: .command)
]
static let showOnScreenList = [
"Main screen": ShowOnScreenPreference.main,
"Screen including mouse": ShowOnScreenPreference.mouse,
]
static let alignThumbnailsList = [
"Center": AlignThumbnailsPreference.center,
"Left": AlignThumbnailsPreference.left,
]
enum ShowOnScreenPreference: String, CaseIterable, MacroPreference {
case active = "Active screen"
case includingMouse = "Screen including mouse"
var localizedString: LocalizedString {
switch self {
case .active: return NSLocalizedString("Active screen", comment: "")
case .includingMouse: return NSLocalizedString("Screen including mouse", comment: "")
}
}
}
enum AlignThumbnailsPreference: String, CaseIterable, MacroPreference {
case left = "Left"
case center = "Center"
var localizedString: LocalizedString {
switch self {
case .left: return NSLocalizedString("Left", comment: "")
case .center: return NSLocalizedString("Center", comment: "")
}
}
}
enum ThemePreference: String, CaseIterable, MacroPreference {
case macOs = " macOS"
case windows10 = "❖ Windows 10"
var themeParameters: ThemeParameters {
switch self {
case .macOs: return ThemeParameters(label: NSLocalizedString(" macOS", comment: ""), cellBorderWidth: 0, cellCornerRadius: 5, windowCornerRadius: 20, highlightBorderColor: .clear, highlightBackgroundColor: NSColor(red: 0, green: 0, blue: 0, alpha: 0.4))
case .windows10: return ThemeParameters(label: NSLocalizedString("❖ Windows 10", comment: ""), cellBorderWidth: 2, cellCornerRadius: 0, windowCornerRadius: 0, highlightBorderColor: .white, highlightBackgroundColor: .clear)
}
}
}
+8 -8
View File
@@ -3,8 +3,8 @@ import Cocoa
class Screen {
static func preferred() -> NSScreen {
switch Preferences.showOnScreen {
case .mouse: return withMouse() ?? NSScreen.main!; // .main as fall-back
case .main: return NSScreen.main!;
case .includingMouse: return withMouse() ?? NSScreen.main!; // .main as fall-back
case .active: return NSScreen.main!;
}
}
@@ -20,12 +20,10 @@ class Screen {
window.setFrameOrigin(NSPoint(x: x, y: y))
}
static func mainUuid() -> CFString {
return "Main" as CFString
// the bellow code gets the actual main screen, but in our case we seem to be fine with sending "Main"
// our only need for this is for the System Preferences panel which has incorrect space with or without this
//let mainScreenId = NSScreen.main!.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! UInt32
//return CFUUIDCreateString(nil, CGDisplayCreateUUIDFromDisplayID(mainScreenId).takeRetainedValue())!
static func uuid(_ screen: NSScreen) -> ScreenUuid {
let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! UInt32
let screenUuid = CGDisplayCreateUUIDFromDisplayID(screenNumber).takeRetainedValue()
return CFUUIDCreateString(nil, screenUuid)!
}
}
@@ -34,3 +32,5 @@ enum VerticalAlignment: CGFloat {
// vertically centered but with an upward offset, similar to a book title; mimics NSView.center()
case appleCentered = 0.75
}
typealias ScreenUuid = CFString
+11 -4
View File
@@ -5,10 +5,12 @@ class Spaces {
static var currentSpaceIndex = SpaceIndex(1)
static var visitedSpaces = [CGSSpaceID: Bool]()
static var isSingleSpace = true
static var idsAndIndexes = allIdsAndIndexes()
static func observeSpaceChanges() {
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.activeSpaceDidChangeNotification, object: nil, queue: nil, using: { _ in
debugPrint("OS event", "activeSpaceDidChangeNotification")
idsAndIndexes = allIdsAndIndexes()
updateCurrentSpace()
Applications.observeNewWindows()
guard App.app.appIsBeingUsed else { return }
@@ -16,6 +18,11 @@ class Spaces {
})
}
static func refreshCurrentSpaceId() {
currentSpaceId = CGSManagedDisplayGetCurrentSpace(cgsMainConnectionId, Screen.uuid(NSScreen.main!))
debugPrint("currentSpaceId", currentSpaceId)
}
static func initialDiscovery() {
updateCurrentSpace()
updateIsSingleSpace()
@@ -23,8 +30,8 @@ class Spaces {
}
static func updateCurrentSpace() {
currentSpaceId = CGSManagedDisplayGetCurrentSpace(cgsMainConnectionId, Screen.mainUuid())
currentSpaceIndex = allIdsAndIndexes().first { $0.0 == currentSpaceId }!.1
refreshCurrentSpaceId()
currentSpaceIndex = idsAndIndexes.first { $0.0 == currentSpaceId }!.1
debugPrint("Current space", currentSpaceId)
}
@@ -35,7 +42,7 @@ class Spaces {
}
static func otherSpaces() -> [CGSSpaceID] {
return allIdsAndIndexes().filter { $0.0 != currentSpaceId }.map { $0.0 }
return idsAndIndexes.filter { $0.0 != currentSpaceId }.map { $0.0 }
}
static func windowsInSpaces(_ spaceIds: [CGSSpaceID]) -> [CGWindowID] {
@@ -45,7 +52,7 @@ class Spaces {
}
static func updateIsSingleSpace() {
isSingleSpace = allIdsAndIndexes().count == 1
isSingleSpace = idsAndIndexes.count == 1
}
}
+1
View File
@@ -6,6 +6,7 @@ class Window {
var title: String
var thumbnail: NSImage?
var icon: NSImage?
var shouldShowTheUser = true
var isHidden: Bool
var isMinimized: Bool
var isOnAllSpaces: Bool
+34 -14
View File
@@ -3,13 +3,13 @@ import Cocoa
class Windows {
// order in the array is important: most-recently-used elements are first
static var list = [Window]()
static var previousFocusedWindowIndex = Array<Window>.Index(0)
static var focusedWindowIndex = Array<Window>.Index(0)
static var previousFocusedWindowIndex = Int(0)
static var focusedWindowIndex = Int(0)
static var windowsInSubscriptionRetryLoop = [String]()
static func updateFocusedWindowIndex(_ newValue: Array<Window>.Index) {
static func updateFocusedWindowIndex(_ newIndex: Int) {
previousFocusedWindowIndex = focusedWindowIndex
focusedWindowIndex = newValue
focusedWindowIndex = newIndex
let focusedView = ThumbnailsView.recycledViews[focusedWindowIndex]
ThumbnailsPanel.highlightCell(ThumbnailsView.recycledViews[previousFocusedWindowIndex], focusedView)
App.app.thumbnailsPanel!.thumbnailsView.scrollView.contentView.scrollToVisible(focusedView.frame)
@@ -19,22 +19,29 @@ class Windows {
return list.count > focusedWindowIndex ? list[focusedWindowIndex] : nil
}
static func cycleFocusedWindowIndex(_ step: Array<Window>.Index) {
updateFocusedWindowIndex(focusedWindowIndex + step < 0 ? list.count - 1 : (focusedWindowIndex + step) % list.count)
static func cycleFocusedWindowIndex(_ step: Int) {
var iterations = 0
var targetIndex = focusedWindowIndex
repeat {
let next = (targetIndex + step) % list.count
targetIndex = next < 0 ? list.count + next : next
iterations += 1
} while !list[targetIndex].shouldShowTheUser && iterations <= list.count
updateFocusedWindowIndex(targetIndex)
}
static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ destroyedWindowIndex: Array<Window>.Index) {
if focusedWindowIndex <= destroyedWindowIndex {
updateFocusedWindowIndex(max(focusedWindowIndex - 1, 0))
static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ destroyedWindowIndex: Int) {
if focusedWindowIndex >= destroyedWindowIndex {
cycleFocusedWindowIndex(-1)
}
}
static func moveFocusedWindowIndexAfterWindowCreatedInBackground(_ step: Int) {
updateFocusedWindowIndex(focusedWindowIndex + step)
}
static func updateSpaces() {
let spacesMap = Spaces.allIdsAndIndexes()
Spaces.updateIsSingleSpace()
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
Spaces.refreshCurrentSpaceId()
let spacesMap = Spaces.idsAndIndexes
list.forEachAsync { window in
let spaceIds = window.cgWindowId.spaces()
if spaceIds.count == 1 {
@@ -72,6 +79,19 @@ class Windows {
}
}
static func refreshWhichWindowsToShowTheUser(_ screen: NSScreen) {
var screenFrameInQuartzCoordinates = screen.frame
screenFrameInQuartzCoordinates.origin.y = NSMaxY(NSScreen.screens[0].frame) - NSMaxY(screen.frame)
let activeApp = NSWorkspace.shared.frontmostApplication
Windows.list.forEach {
$0.shouldShowTheUser = !(!Preferences.showMinimizedWindows && $0.isMinimized) &&
!(!Preferences.showHiddenWindows && $0.isHidden) &&
!(Preferences.appsToShow == .active && $0.application.runningApplication != activeApp) &&
!(Preferences.spacesToShow == .active && $0.spaceId != Spaces.currentSpaceId) &&
!(Preferences.screensToShow == .showingAltTab && $0.axUiElement.position().map { p in !screenFrameInQuartzCoordinates.contains(p) } ?? true)
}
}
static func refreshAllExistingThumbnails() {
refreshAllThumbnails()
guard App.app.uiWorkShouldBeDone else { return }
+18 -13
View File
@@ -1,6 +1,7 @@
import Cocoa
import Darwin
import LetsMove
import ShortcutRecorder
let cgsMainConnectionId = CGSMainConnectionID()
@@ -11,6 +12,7 @@ class App: NSApplication, NSApplicationDelegate {
static let repository = "https://github.com/lwouis/alt-tab-macos"
static let url = URL(fileURLWithPath: Bundle.main.bundlePath) as CFURL
static var app: App!
static let shortcutMonitor = LocalShortcutMonitor()
var statusItem: NSStatusItem?
var thumbnailsPanel: ThumbnailsPanel?
var preferencesWindow: PreferencesWindow?
@@ -35,6 +37,7 @@ class App: NSApplication, NSApplicationDelegate {
#endif
SystemPermissions.ensureAccessibilityCheckboxIsChecked()
SystemPermissions.ensureScreenRecordingCheckboxIsChecked()
Preferences.migrateOldPreferences()
Preferences.registerDefaults()
statusItem = Menubar.make()
loadMainMenuXib()
@@ -71,10 +74,7 @@ class App: NSApplication, NSApplicationDelegate {
func focusTarget() {
debugPrint("focusTarget")
if appIsBeingUsed {
let window = Windows.focusedWindow()
focusSelectedWindow(window)
}
focusSelectedWindow(Windows.focusedWindow())
}
@objc
@@ -99,7 +99,9 @@ class App: NSApplication, NSApplicationDelegate {
@objc
func showUi() {
_ = dispatchWork { self.showUiOrCycleSelection(0) }
uiWorkShouldBeDone = true
appIsBeingUsed = true
DispatchQueue.main.async { self.showUiOrCycleSelection(0) }
}
func cycleSelection(_ step: Int) {
@@ -120,9 +122,14 @@ class App: NSApplication, NSApplicationDelegate {
func refreshOpenUi(_ windowsToRefresh: [Window]? = nil) {
guard appIsBeingUsed else { return }
windowsToRefresh?.forEach { $0.refreshThumbnail() }
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
guard uiWorkShouldBeDone else { return }
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
Spaces.refreshCurrentSpaceId()
guard uiWorkShouldBeDone else { return }
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
thumbnailsPanel!.thumbnailsView.updateItems(currentScreen)
guard uiWorkShouldBeDone else { return }
thumbnailsPanel!.setFrame(thumbnailsPanel!.thumbnailsView.frame, display: false)
guard uiWorkShouldBeDone else { return }
Screen.repositionPanel(thumbnailsPanel!, currentScreen, .appleCentered)
@@ -130,19 +137,17 @@ class App: NSApplication, NSApplicationDelegate {
func showUiOrCycleSelection(_ step: Int) {
debugPrint("showUiOrCycleSelection", step)
appIsBeingUsed = true
if isFirstSummon {
debugPrint("showUiOrCycleSelection: isFirstSummon")
isFirstSummon = false
if Windows.list.count == 0 || CGWindow.isMissionControlActive() {
appIsBeingUsed = false
isFirstSummon = true
return
}
if Windows.list.count == 0 || CGWindow.isMissionControlActive() { hideUi(); return }
// TODO: find a way to update isSingleSpace by listening to space creation, instead of on every trigger
Spaces.updateIsSingleSpace()
Spaces.idsAndIndexes = Spaces.allIdsAndIndexes()
// TODO: find a way to update space index when windows are moved to another space, instead of on every trigger
Windows.updateSpaces()
let screen = Screen.preferred()
Windows.refreshWhichWindowsToShowTheUser(screen)
if Windows.list.first(where: { $0.shouldShowTheUser }) == nil { hideUi(); return }
Windows.updateFocusedWindowIndex(0)
Windows.cycleFocusedWindowIndex(step)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay) {
+3
View File
@@ -3,10 +3,13 @@ import Cocoa
class GridView {
static let padding = CGFloat(20)
static let interPadding = CGFloat(10)
static let indentPadding = padding * 2
static func make(_ controls: [[NSView]]) -> NSGridView {
let gridView = NSGridView(views: controls)
gridView.translatesAutoresizingMaskIntoConstraints = false
gridView.yPlacement = .fill
gridView.rowAlignment = .firstBaseline
gridView.columnSpacing = interPadding
gridView.rowSpacing = interPadding
gridView.column(at: 0).leadingPadding = padding
+19
View File
@@ -0,0 +1,19 @@
import Cocoa
class StackView: NSStackView {
var alignmentRectInsets_: NSEdgeInsets!
convenience init(_ views: [NSView], _ orientation: NSUserInterfaceLayoutOrientation = .horizontal, top: CGFloat = 0, right: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0) {
self.init(views: views)
alignmentRectInsets_ = NSEdgeInsets(top: top, left: left, bottom: bottom, right: right)
alignment = orientation == .horizontal ? .firstBaseline : .left
// workaround: for some reason, horizontal stackviews with a RecorderControl have extra fittingSize.height
if orientation == .horizontal && views.first(where: { $0 is UnclearableRecorderControl }) != nil {
fit(fittingSize.width, fittingSize.height - 7)
} else {
fit()
}
}
override var alignmentRectInsets: NSEdgeInsets { alignmentRectInsets_ }
}
@@ -0,0 +1,29 @@
import Cocoa
import ShortcutRecorder
class UnclearableRecorderControl: RecorderControl {
convenience init(_ shortcutString: String, _ modifierFlagsOnly: Bool) {
self.init(frame: .zero)
allowsDeleteToClearShortcutAndEndRecording = false
set(allowedModifierFlags: CocoaModifierFlagsMask, requiredModifierFlags: [], allowsEmptyModifierFlags: false)
if modifierFlagsOnly {
allowsModifierFlagsOnlyShortcut = true
}
objectValue = Shortcut(keyEquivalent: shortcutString)
// TODO: doesn't seem to work; handle width of the control better
allowsExpansionToolTips = true
widthAnchor.constraint(equalToConstant: 80).isActive = true
}
override func drawClearButton(_ aDirtyRect: NSRect) {
// don't draw the clear button
}
override func areModifierFlagsValid(_ aModifierFlags: NSEvent.ModifierFlags, for aKeyCode: KeyCode) -> Bool {
// only allow modifiers: ⌥ -> valid, e -> invalid, ⌥e -> invalid
if allowsModifierFlagsOnlyShortcut && aKeyCode != .none {
return false
}
return super.areModifierFlagsValid(aModifierFlags, for: aKeyCode)
}
}
@@ -1,36 +0,0 @@
import Cocoa
class TextField: NSTextField, NSTextFieldDelegate {
var validationHandler: ((String)->Bool)?
convenience init(_ value: String) {
self.init(string: value)
usesSingleLineMode = true
font = .labelFont(ofSize: NSFont.systemFontSize)
wantsLayer = true
layer?.borderWidth = 1
}
func controlTextDidChange(_ obj: Notification) {
visualizeValidationState()
let textField = obj.object as! TextField
sendAction(textField.action, to: textField.target)
}
func visualizeValidationState() -> Void {
if !isValid() {
layer?.borderColor = NSColor.systemRed.cgColor
} else {
layer?.borderColor = .clear
}
}
func isValid() -> Bool {
if let handler = validationHandler {
return handler(stringValue)
}
return true
}
}
+3 -1
View File
@@ -102,7 +102,9 @@ class ThumbnailView: NSStackView {
}
override func mouseMoved(with event: NSEvent) {
mouseMovedCallback()
if Preferences.mouseHoverEnabled {
mouseMovedCallback()
}
}
override func mouseDown(with theEvent: NSEvent) {
+2
View File
@@ -25,6 +25,8 @@ class ThumbnailsView: NSVisualEffectView {
var maxY = height
var newViews = [ThumbnailView]()
for (index, window) in Windows.list.enumerated() {
guard App.app.uiWorkShouldBeDone else { return }
guard window.shouldShowTheUser else { continue }
let view = ThumbnailsView.recycledViews[index]
view.updateRecycledCellWithNewContent(window,
{ App.app.focusSelectedWindow(window) },
+48 -20
View File
@@ -1,27 +1,37 @@
import Cocoa
import ShortcutRecorder
enum LabelPosition {
case leftWithSeparator
case leftWithoutSeparator
case right
}
class LabelAndControl: NSObject {
static func makeLabelWithInput(_ labelText: String, _ rawName: String, _ width: CGFloat, _ suffixText: String? = nil, _ suffixUrl: String? = nil, _ validator: ((String) -> Bool)? = nil) -> [NSView] {
let input = TextField(Preferences.getString(rawName)!)
input.validationHandler = validator
input.delegate = input
input.visualizeValidationState()
input.fit(width, input.fittingSize.height)
let views = makeLabelWithProvidedControl(labelText, rawName, input)
return [views[0], NSStackView(views: [views[1], makeSuffix(rawName, suffixText!, suffixUrl)])]
static func makeLabelWithRecorder(_ labelText: String, _ rawName: String, _ shortcutString: String, _ modifierFlagsOnly: Bool = false, labelPosition: LabelPosition = .leftWithSeparator) -> [NSView] {
let input = UnclearableRecorderControl(shortcutString, modifierFlagsOnly)
let views = makeLabelWithProvidedControl(labelText, rawName, input, labelPosition: labelPosition, extraAction: GeneralTab.shortcutChangedCallback)
input.sendAction(input.action, to: input.target)
return views
}
static func makeLabelWithCheckbox(_ labelText: String, _ rawName: String, extraAction: ActionClosure? = nil) -> [NSView] {
let checkbox = NSButton(checkboxWithTitle: "", target: nil, action: nil)
static func makeLabelWithCheckbox(_ labelText: String, _ rawName: String, extraAction: ActionClosure? = nil, labelPosition: LabelPosition = .leftWithSeparator) -> [NSView] {
let checkbox = NSButton(checkboxWithTitle: labelPosition == .right ? labelText : "", target: nil, action: nil)
let views = makeLabelWithProvidedControl(labelText, rawName, checkbox, labelPosition: labelPosition, extraAction: extraAction)
setControlValue(checkbox, Preferences.getString(rawName)!)
return makeLabelWithProvidedControl(labelText, rawName, checkbox, extraAction: extraAction)
checkbox.sendAction(checkbox.action, to: checkbox.target)
return views
}
static func makeLabelWithDropdown(_ labelText: String, _ rawName: String, _ values: [String], _ suffixText: String? = nil) -> [NSView] {
return makeLabelWithProvidedControl(labelText, rawName, makeDropDown(rawName, values), suffixText)
}
static func makeDropDown(_ rawName: String, _ values: [String]) -> NSPopUpButton {
let popUp = NSPopUpButton()
popUp.addItems(withTitles: values)
popUp.selectItem(withTitle: Preferences.getString(rawName)!)
return makeLabelWithProvidedControl(labelText, rawName, popUp, suffixText)
return popUp
}
static func makeLabelWithSlider(_ labelText: String, _ rawName: String, _ minValue: Double, _ maxValue: Double, _ numberOfTickMarks: Int, _ allowsTickMarkValuesOnly: Bool, _ unitText: String = "") -> [NSView] {
@@ -34,25 +44,44 @@ class LabelAndControl: NSObject {
// slider.numberOfTickMarks = numberOfTickMarks
// slider.allowsTickMarkValuesOnly = allowsTickMarkValuesOnly
// slider.tickMarkPosition = .below
// TODO: update suffix continuously, but only trigger action on mouse release
slider.isContinuous = true
return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText)
}
static func makeLabelWithProvidedControl(_ labelText: String?, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil, _ suffixUrl: String? = nil, extraAction: ActionClosure? = nil) -> [NSView] {
let label = makeLabel(labelText, rawName)
static func makeLabelWithProvidedControl(_ labelText: String, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil, _ suffixUrl: String? = nil, labelPosition: LabelPosition = .leftWithSeparator, extraAction: ActionClosure? = nil) -> [NSView] {
_ = setupControl(control, rawName, extraAction)
if labelPosition == .right && control is NSButton {
return [control]
}
let label = makeLabel(labelText, labelPosition)
if labelPosition == .right {
if let suffixText = suffixText {
return [control, label, makeSuffix(rawName, suffixText, suffixUrl)]
}
return [control, label]
}
if let suffixText = suffixText {
return [label, control, makeSuffix(rawName, suffixText, suffixUrl)]
}
return [label, control]
}
static func setupControl(_ control: NSControl, _ rawName: String, _ extraAction: ActionClosure? = nil) -> NSControl {
control.identifier = NSUserInterfaceItemIdentifier(rawName)
control.onAction = {
PreferencesWindow.controlWasChanged($0)
extraAction?($0)
}
return [label, control, suffixText != nil ? makeSuffix(rawName, suffixText!, suffixUrl) : NSView()]
return control
}
private static func makeLabel(_ labelText: String?, _ rawName: String) -> NSTextField {
let label = NSTextField(wrappingLabelWithString: labelText != nil ? labelText! + ": " : "")
label.fit()
static func makeLabel(_ labelText: String, _ labelPosition: LabelPosition = .leftWithoutSeparator) -> NSTextField {
let labelText_ = labelPosition == .leftWithSeparator ? labelText + ":" : labelText
let label = NSTextField(wrappingLabelWithString: labelText_)
label.isSelectable = false
label.usesSingleLineMode = true
label.alignment = .right
label.identifier = NSUserInterfaceItemIdentifier(rawName + ControlIdentifierDiscriminator.LABEL.rawValue)
return label
}
@@ -113,6 +142,5 @@ class LabelAndControl: NSObject {
}
enum ControlIdentifierDiscriminator: String {
case LABEL = "_label"
case SUFFIX = "_suffix"
}
@@ -3,12 +3,26 @@ import Cocoa
class PreferencesWindow: NSWindow {
let tabViewController = TabViewController()
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
super.init(contentRect: .zero, styleMask: style, backing: backingStoreType, defer: flag)
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(GeneralTab.make())
tabViewController.addTabViewItem(AppearanceTab.make())
tabViewController.addTabViewItem(UpdatesTab.make())
tabViewController.addTabViewItem(AboutTab.make())
}
func show() {
App.shared.activate(ignoringOtherApps: true)
makeKeyAndOrderFront(nil)
@@ -23,19 +37,4 @@ class PreferencesWindow: NSWindow {
(App.shared as! App).resetPreferencesDependentComponents()
}
}
private func setupWindow() {
hidesOnDeactivate = false
isReleasedWhenClosed = false
styleMask.insert([.miniaturizable, .closable])
}
private func setupTabViews() {
contentViewController = tabViewController
tabViewController.tabStyle = .toolbar
tabViewController.addTabViewItem(GeneralTab.make())
tabViewController.addTabViewItem(AppearanceTab.make())
tabViewController.addTabViewItem(UpdatesTab.make())
tabViewController.addTabViewItem(AboutTab.make())
}
}
@@ -9,10 +9,10 @@ class TabViewController: NSTabViewController {
}
private func resizeWindowToFit(_ tabViewItem: NSTabViewItem, _ window: NSWindow) {
let contentFrame = window.frameRect(forContentRect: NSRect(origin: .zero, size: tabViewItem.view!.frame.size))
let toolbarHeight = window.frame.size.height - contentFrame.size.height
let newOrigin = NSPoint(x: window.frame.origin.x, y: window.frame.origin.y + toolbarHeight)
let newFrame = NSRect(origin: newOrigin, size: contentFrame.size)
window.setFrame(newFrame, display: false, animate: true)
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)
}
}
}
+11 -11
View File
@@ -8,33 +8,33 @@ class AboutTab: NSObject {
static func makeView() -> NSGridView {
let appIcon = NSImageView(image: NSImage(named: "app-icon")!.resizedCopy(150, 150))
appIcon.imageScaling = .scaleNone
let appText = NSStackView(views: [
let appText = StackView([
BoldLabel(App.name),
NSTextField(wrappingLabelWithString: NSLocalizedString("Version", comment: "") + " " + App.version),
NSTextField(wrappingLabelWithString: App.licence),
HyperlinkLabel(NSLocalizedString("Source code repository", comment: ""), App.repository),
HyperlinkLabel(NSLocalizedString("Latest releases", comment: ""), App.repository + "/releases"),
])
appText.orientation = .vertical
appText.alignment = .left
], .vertical)
appText.spacing = GridView.interPadding / 2
let rowToSeparate = 3
appText.views[rowToSeparate].topAnchor.constraint(equalTo: appText.views[rowToSeparate - 1].bottomAnchor, constant: GridView.interPadding).isActive = true
let appInfo = NSStackView(views: [appIcon, appText])
let appInfo = StackView([appIcon, appText])
appInfo.spacing = GridView.interPadding
let view = GridView.make([
appInfo.alignment = .centerY
let sendFeedback = NSButton(title: NSLocalizedString("Send feedback…", comment: ""), target: self, action: #selector(feedbackCallback))
let grid = GridView.make([
[appInfo],
[NSButton(title: NSLocalizedString("Send feedback…", comment: ""), target: self, action: #selector(feedbackCallback))],
[sendFeedback],
])
let sendFeedbackCell = view.cell(atColumnIndex: 0, rowIndex: 1)
let sendFeedbackCell = grid.cell(atColumnIndex: 0, rowIndex: 1)
sendFeedbackCell.xPlacement = .center
sendFeedbackCell.row!.topPadding = GridView.interPadding
view.fit()
return view
grid.fit()
return grid
}
@objc
static func feedbackCallback() {
(App.shared as! App).showFeedbackPanel()
App.app.showFeedbackPanel()
}
}
@@ -8,22 +8,22 @@ class AppearanceTab {
}
private static func makeView() -> NSGridView {
let view = GridView.make([
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Theme", comment: ""), "theme", MacroPreferences.themeList.values.map { $0.label }),
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Align windows", comment: ""), "alignThumbnails", Array(MacroPreferences.alignThumbnailsList.keys)),
let grid = GridView.make([
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Theme", comment: ""), "theme", ThemePreference.allCases.map { $0.themeParameters.label }),
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Align windows", comment: ""), "alignThumbnails", AlignThumbnailsPreference.allCases.map { $0.localizedString }),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Max size on screen", comment: ""), "maxScreenUsage", 10, 100, 10, true, "%"),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Min windows per row", comment: ""), "minCellsPerRow", 1, 20, 20, true),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Max windows per row", comment: ""), "maxCellsPerRow", 1, 40, 20, true),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Rows of windows", comment: ""), "rowsCount", 1, 20, 20, true),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Window app icon size", comment: ""), "iconSize", 0, 64, 11, false, "px"),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Window title font size", comment: ""), "fontHeight", 0, 64, 11, false, "px"),
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show on", comment: ""), "showOnScreen", Array(MacroPreferences.showOnScreenList.keys)),
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show on", comment: ""), "showOnScreen", ShowOnScreenPreference.allCases.map { $0.localizedString }),
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Apparition delay", comment: ""), "windowDisplayDelay", 0, 2000, 11, false, "ms"),
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide space number labels", comment: ""), "hideSpaceNumberLabels"),
])
view.column(at: 0).xPlacement = .trailing
view.rowAlignment = .lastBaseline
view.fit()
return view
grid.column(at: 0).xPlacement = .trailing
grid.rowAlignment = .lastBaseline
grid.fit()
return grid
}
}
+86 -26
View File
@@ -1,43 +1,103 @@
import Cocoa
import ShortcutRecorder
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 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)
}
private static func makeView() -> NSView {
let startAtLogin = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Start at login", comment: ""), "startAtLogin", extraAction: startAtLoginCallback)
let view = GridView.make([
var holdShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Hold", comment: ""), "holdShortcut", Preferences.holdShortcut, true, 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 enableMouse = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Mouse hover", comment: ""), "mouseHoverEnabled", labelPosition: .right)
let holdAndPress = StackView(holdShortcut)
let checkboxesExplanations = LabelAndControl.makeLabel(NSLocalizedString("Select windows using:", comment: ""))
let checkboxes = StackView([StackView(enableArrows), StackView(enableMouse)], .vertical)
let appsToShow = dropdown("appsToShow", AppsToShowPreference.allCases.map { $0.localizedString })
let spacesToShow = dropdown("spacesToShow", SpacesToShowPreference.allCases.map { $0.localizedString })
let screensToShow = dropdown("screensToShow", ScreensToShowPreference.allCases.map { $0.localizedString })
let showMinimizedWindows = StackView(LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Minimized", comment: ""), "showMinimizedWindows", labelPosition: .right))
let showHiddenWindows = StackView(LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hidden", comment: ""), "showHiddenWindows", labelPosition: .right))
let shortcuts = StackView([nextWindowShortcut, previousWindowShortcut, cancelShortcut].map { StackView($0) }, .vertical)
let toShowDropdowns = StackView([appsToShow, spacesToShow, screensToShow], .vertical)
let toShowCheckboxes = StackView([showMinimizedWindows, showHiddenWindows], .vertical)
let toShowExplanations = LabelAndControl.makeLabel(NSLocalizedString("Show the following windows:", comment: ""))
let toShow = StackView([toShowDropdowns, toShowCheckboxes], .vertical)
let grid = 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),
[holdAndPress, shortcuts],
[checkboxesExplanations, checkboxes],
[toShowExplanations, toShow],
])
view.column(at: 0).xPlacement = .trailing
view.rowAlignment = .lastBaseline
view.fit()
setLoginItemIfCheckboxIsOn(startAtLogin[1] as! NSButton)
return view
grid.column(at: 0).xPlacement = .trailing
[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 })
return grid
}
private static func setLoginItemIfCheckboxIsOn(_ startAtLoginCheckbox: NSButton) {
if startAtLoginCheckbox.state == .on {
startAtLoginCallback(startAtLoginCheckbox)
private static func dropdown(_ rawName: String, _ values: [String]) -> NSControl {
let dropdown = LabelAndControl.makeDropDown(rawName, values)
return LabelAndControl.setupControl(dropdown, rawName)
}
private static func addShortcut(_ fn: @escaping () -> Void, _ type: KeyEventType, _ shortcut: Shortcut, _ controlId: String) {
removeShortcutIfExists(controlId, type) // remove the previous shortcut
shortcutActions[controlId] = ShortcutAction(shortcut: shortcut, actionHandler: { _ in
let shortcutThatInitiatesTheApp = controlId == "previousWindowShortcut" || controlId == "nextWindowShortcut"
App.app.uiWorkShouldBeDone = shortcutThatInitiatesTheApp
if shortcutThatInitiatesTheApp {
App.app.appIsBeingUsed = true
DispatchQueue.main.async { fn() }
} else if App.app.appIsBeingUsed {
fn()
}
return true
})
App.shortcutMonitor.addAction(shortcutActions[controlId]!, forKeyEvent: type)
}
@objc static func shortcutChangedCallback(_ sender: NSControl) {
let controlId = sender.identifier!.rawValue
// remove the holdShortcut character in case they also use it in the other shortcuts
let newValue = (sender as! RecorderControl).stringValue.replacingOccurrences(of: Preferences.holdShortcut, with: "")
switch controlId {
case "holdShortcut":
addShortcut({ App.app.focusTarget() }, .up, Shortcut(keyEquivalent: Preferences.holdShortcut)!, controlId)
shortcutsDependentOnHoldShortcut.forEach { $0.sendAction($0.action, to: $0.target) }
case "nextWindowShortcut": addShortcut({ App.app.showUiOrCycleSelection(1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
case "previousWindowShortcut": addShortcut({ App.app.showUiOrCycleSelection(-1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
case "cancelShortcut": addShortcut({ App.app.hideUi() }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
default: return
}
}
@objc static func arrowKeysEnabledCallback(_ sender: NSControl) {
if (sender as! NSButton).state == .on {
addShortcut({ App.app.cycleSelection(1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + "→")!, "→")
addShortcut({ App.app.cycleSelection(-1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + "←")!, "←")
} else {
removeShortcutIfExists("→", .down)
removeShortcutIfExists("←", .down)
}
}
private static func removeShortcutIfExists(_ controlId: String, _ type: KeyEventType) {
if let a = shortcutActions[controlId] {
App.shortcutMonitor.removeAction(_: a, forKeyEvent: type)
shortcutActions.removeValue(forKey: controlId)
}
}
@@ -30,17 +30,17 @@ class UpdatesTab: NSObject {
policies.alignment = .left
policies.orientation = .vertical
policies.spacing = GridView.interPadding / 2
let view = GridView.make([
let grid = GridView.make([
[policyLabel, policies],
[NSButton(title: NSLocalizedString("Check for updates now…", comment: ""), target: self, action: #selector(checkForUpdatesNow))],
])
view.cell(atColumnIndex: 0, rowIndex: 0).xPlacement = .trailing
let row1 = view.row(at: 1)
grid.cell(atColumnIndex: 0, rowIndex: 0).xPlacement = .trailing
let row1 = grid.row(at: 1)
row1.mergeCells(in: NSRange(location: 0, length: 2))
row1.topPadding = GridView.interPadding
row1.cell(at: 0).xPlacement = .center
view.fit()
return view
grid.fit()
return grid
}
@objc