feat: more flexible shortcuts (closes #72)
closes #50, closes #125, closes #133
This commit is contained in:
1 parent
48bb3df240
commit
5eade75983
26 files changed
+479
-274
No files matched your search
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,7 +102,9 @@ class ThumbnailView: NSStackView {
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
mouseMovedCallback()
|
||||
if Preferences.mouseHoverEnabled {
|
||||
mouseMovedCallback()
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseDown(with theEvent: NSEvent) {
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user