feat: display other spaces/minimized windows (closes #14)

Also closes #11 closes #45 closes #62

BREAKING CHANGE: this brings huge changes to core parts of the codebase. It introduces the use of private APIs that hopefully are should be compatible from macOS 10.12+, but I couldn't test them. I reviewed the whole codebase to clean and improve on performance and readability
This commit is contained in:
louis.pontoise authored and lwouis committed 2019-12-27 14:47:51 +09:00
1 parent b50fa5bd92
commit 3f5ea25154
24 files changed
+858 -297

No files matched your search

+71 -17
View File
@@ -7,27 +7,35 @@
objects = {
/* Begin PBXBuildFile section */
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4807A6C523A9CD190052A53E /* SkyLight.framework */; };
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */; };
D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA86768C6503A11ED81FC /* Extensions.swift */; };
D04BA0496ACF1427B6E9D369 /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA78E3B4E73B40DB77174 /* CGWindow.swift */; };
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56355579F78776E6D51 /* Cell.swift */; };
D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */; };
D04BA278D9EFA568C8D18A4C /* TrackedWindows.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */; };
D04BA2CBF0EFA04CC80EC1BC /* TrackedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE80772D25834E440975 /* TrackedWindow.swift */; };
D04BA308162F8043F8561D03 /* AXUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA40A4291E4F310527DBF /* AXUIElement.swift */; };
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA90C6C36DB1D65BC2B66 /* Application.swift */; };
D04BA4D356055A39B97712DE /* PrivateApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */; };
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA35456DA0DDA74F9687E /* Keyboard.swift */; };
D04BA57FB9EF1373D59A1AA7 /* CGWindowID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */; };
D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA02F476DE30C4647886C /* PreferencesPanel.swift */; };
D04BA6B6B703DCEFE892D5A4 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA5EB5ED248C8C22CC672 /* Spaces.swift */; };
D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */; };
D04BA89A77CFDC4A3DF30487 /* CoreGraphicsApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7AC3D9153353CE0A76D /* CoreGraphicsApis.swift */; };
D04BA8373D4DE452C0C081ED /* SF-Pro-Text-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = D04BABC654F40BE74DA25BC7 /* SF-Pro-Text-Regular.otf */; };
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */; };
D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */; };
D04BA960DDD1D32A3019C835 /* CollectionViewCenterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */; };
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; };
D04BACD2FFB12589F9286B47 /* AccessibilityApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE965ED779513FDEA0CD /* AccessibilityApis.swift */; };
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD32E130E4A061DC8332 /* Labels.swift */; };
D04BAE2E8E9B9898A4DF9B3B /* FontIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAED53465957807CBF8B2 /* FontIcon.swift */; };
D04BAE369A14C3126A1606FE /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */; };
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; };
F029861A378EC1417106FEC3 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298E42A818112B290FF6C7 /* TextField.swift */; };
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
4807A6C523A9CD190052A53E /* SkyLight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SkyLight.framework; path = ../../../../System/Library/PrivateFrameworks/SkyLight.framework; sourceTree = "<group>"; };
D04BA02F476DE30C4647886C /* PreferencesPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPanel.swift; sourceTree = "<group>"; };
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = "<group>"; };
D04BA0CE87BE264C52987ED1 /* 7 windows - 2 lines - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - wide window.jpg"; sourceTree = "<group>"; };
@@ -43,6 +51,7 @@
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA35456DA0DDA74F9687E /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
D04BA40A4291E4F310527DBF /* AXUIElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXUIElement.swift; sourceTree = "<group>"; };
D04BA4336B6004A0A99849AD /* package.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = package.json; sourceTree = "<group>"; };
D04BA459034C1885CA43A807 /* LICENCE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENCE.md; sourceTree = "<group>"; };
D04BA4B5292629AA6B560216 /* package_release.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = package_release.sh; sourceTree = "<group>"; };
@@ -50,21 +59,23 @@
D04BA51D43775E57CE91154A /* 3 windows - 1 line - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - wide window.jpg"; sourceTree = "<group>"; };
D04BA56355579F78776E6D51 /* Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = "<group>"; };
D04BA5ABFA5457A86536E2E4 /* 5 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA7AC3D9153353CE0A76D /* CoreGraphicsApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreGraphicsApis.swift; sourceTree = "<group>"; };
D04BA5EB5ED248C8C22CC672 /* Spaces.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = "<group>"; };
D04BA78E3B4E73B40DB77174 /* CGWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGWindow.swift; sourceTree = "<group>"; };
D04BA7B6AAB0812631BBC7A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = "<group>"; };
D04BA7ECCE728582D9ECA613 /* determine_version.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version.sh; sourceTree = "<group>"; };
D04BA82F792DF53958D92572 /* alt-tab-macos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "alt-tab-macos.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D04BA86768C6503A11ED81FC /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperExtensions.swift; sourceTree = "<group>"; };
D04BA90C6C36DB1D65BC2B66 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D04BA92541D46EA4F6943A72 /* package-lock.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "package-lock.json"; 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>"; };
D04BAA44C837F3A67403B9DB /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
D04BAB6652494D7575057E86 /* 14 windows - 3 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "14 windows - 3 lines.jpg"; sourceTree = "<group>"; };
D04BABC654F40BE74DA25BC7 /* SF-Pro-Text-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "SF-Pro-Text-Regular.otf"; sourceTree = "<group>"; };
D04BAC02D60EF22D9CC7D969 /* commitlint.config.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = commitlint.config.js; sourceTree = "<group>"; };
D04BAC159731F80FDAF4EA6C /* 1-row.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1-row.jpg"; sourceTree = "<group>"; };
D04BAC6AFC7F06D1A567F27A /* set_version_in_app.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = set_version_in_app.sh; sourceTree = "<group>"; };
D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackedWindows.swift; sourceTree = "<group>"; };
D04BAD1C9F215BCCD3B620AC /* alt_tab_macos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = alt_tab_macos.entitlements; sourceTree = "<group>"; };
D04BAD32E130E4A061DC8332 /* Labels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Labels.swift; sourceTree = "<group>"; };
D04BAD40CE2D3A8AAC3819D0 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = file.gitignore; path = .gitignore; sourceTree = "<group>"; };
@@ -74,8 +85,11 @@
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; 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>"; };
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = "<group>"; };
D04BAE965ED779513FDEA0CD /* AccessibilityApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityApis.swift; sourceTree = "<group>"; };
D04BAE80772D25834E440975 /* TrackedWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackedWindow.swift; sourceTree = "<group>"; };
D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGWindowID.swift; sourceTree = "<group>"; };
D04BAED53465957807CBF8B2 /* FontIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontIcon.swift; sourceTree = "<group>"; };
D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateApis.swift; sourceTree = "<group>"; };
D04BAF249324297C07E31164 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = "<group>"; };
D04BAFA277EAE3BDDDB61110 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
@@ -87,12 +101,21 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
4807A6C423A9CD190052A53E /* Frameworks */ = {
isa = PBXGroup;
children = (
4807A6C523A9CD190052A53E /* SkyLight.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
D04BA1463D2A17038222BB84 = {
isa = PBXGroup;
children = (
@@ -110,6 +133,7 @@
D04BA703DCD38D9757093312 /* ci */,
D04BA459034C1885CA43A807 /* LICENCE.md */,
D04BA2C9EF33A646D0977195 /* .github */,
4807A6C423A9CD190052A53E /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -121,6 +145,18 @@
name = Products;
sourceTree = "<group>";
};
D04BA22D2CA2755FA5902C34 /* api-wrappers */ = {
isa = PBXGroup;
children = (
D04BA40A4291E4F310527DBF /* AXUIElement.swift */,
D04BA78E3B4E73B40DB77174 /* CGWindow.swift */,
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */,
D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */,
D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */,
);
path = "api-wrappers";
sourceTree = "<group>";
};
D04BA2C9EF33A646D0977195 /* .github */ = {
isa = PBXGroup;
children = (
@@ -150,6 +186,14 @@
path = "windows-10";
sourceTree = "<group>";
};
D04BA5A0E9C82F7579CD2B78 /* resources */ = {
isa = PBXGroup;
children = (
D04BABC654F40BE74DA25BC7 /* SF-Pro-Text-Regular.otf */,
);
path = resources;
sourceTree = "<group>";
};
D04BA63877FC8FB11C43C3D2 /* alt-tab-macos */ = {
isa = PBXGroup;
children = (
@@ -175,13 +219,12 @@
isa = PBXGroup;
children = (
D04BA35456DA0DDA74F9687E /* Keyboard.swift */,
D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */,
D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */,
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */,
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA86768C6503A11ED81FC /* Extensions.swift */,
D04BAE965ED779513FDEA0CD /* AccessibilityApis.swift */,
D04BA7AC3D9153353CE0A76D /* CoreGraphicsApis.swift */,
D04BA5EB5ED248C8C22CC672 /* Spaces.swift */,
D04BAE80772D25834E440975 /* TrackedWindow.swift */,
);
path = logic;
sourceTree = "<group>";
@@ -198,6 +241,7 @@
D04BAD32E130E4A061DC8332 /* Labels.swift */,
F0298E42A818112B290FF6C7 /* TextField.swift */,
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */,
D04BAED53465957807CBF8B2 /* FontIcon.swift */,
);
path = ui;
sourceTree = "<group>";
@@ -227,6 +271,8 @@
D04BA7B6AAB0812631BBC7A2 /* Info.plist */,
D04BAA44C837F3A67403B9DB /* main.swift */,
D04BAA1C553891551B903DA7 /* logic */,
D04BA22D2CA2755FA5902C34 /* api-wrappers */,
D04BA5A0E9C82F7579CD2B78 /* resources */,
);
path = "alt-tab-macos";
sourceTree = "<group>";
@@ -290,6 +336,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D04BA8373D4DE452C0C081ED /* SF-Pro-Text-Regular.otf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -304,7 +351,7 @@
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */,
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */,
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */,
D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */,
D04BA278D9EFA568C8D18A4C /* TrackedWindows.swift in Sources */,
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */,
D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */,
D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */,
@@ -312,12 +359,17 @@
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */,
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */,
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */,
D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */,
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */,
F029861A378EC1417106FEC3 /* TextField.swift in Sources */,
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */,
D04BACD2FFB12589F9286B47 /* AccessibilityApis.swift in Sources */,
D04BA89A77CFDC4A3DF30487 /* CoreGraphicsApis.swift in Sources */,
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */,
D04BA308162F8043F8561D03 /* AXUIElement.swift in Sources */,
D04BA0496ACF1427B6E9D369 /* CGWindow.swift in Sources */,
D04BAE369A14C3126A1606FE /* HelperExtensions.swift in Sources */,
D04BAE2E8E9B9898A4DF9B3B /* FontIcon.swift in Sources */,
D04BA4D356055A39B97712DE /* PrivateApis.swift in Sources */,
D04BA6B6B703DCEFE892D5A4 /* Spaces.swift in Sources */,
D04BA2CBF0EFA04CC80EC1BC /* TrackedWindow.swift in Sources */,
D04BA57FB9EF1373D59A1AA7 /* CGWindowID.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -330,6 +382,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements";
COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = /System/Library/PrivateFrameworks;
INFOPLIST_FILE = "alt-tab-macos/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12;
@@ -345,6 +398,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements";
COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = /System/Library/PrivateFrameworks;
INFOPLIST_FILE = "alt-tab-macos/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12;
+2
View File
@@ -28,5 +28,7 @@
<string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
<key>ATSApplicationFontsPath</key>
<string></string>
</dict>
</plist>
@@ -0,0 +1,83 @@
import Cocoa
import Foundation
// This list of keys is not exhaustive; it contains only the values used by this app
// full public list: ApplicationServices.HIServices.AXAttributeConstants.swift
// Note that the String value is transformed by the getters (e.g. kAXWindowsAttribute -> AXWindows)
enum AXAttributeKey: String {
case windows = "AXWindows"
case minimized = "AXMinimized"
case focusedWindow = "AXFocusedWindow"
}
extension AXUIElement {
func value<T>(_ key: AXAttributeKey, _ target: T, _ type: AXValueType) -> T? {
if let a = attribute(key, AXValue.self) {
var value = target
AXValueGetValue(a, type, &value)
return value
}
return nil
}
func attribute<T>(_ key: AXAttributeKey, _ type: T.Type) -> T? {
var value: AnyObject?
let result = AXUIElementCopyAttributeValue(self, key.rawValue as CFString, &value)
if result == .success, let value = value as? T {
return value
}
return nil
}
func cgId() -> CGWindowID {
var id = CGWindowID(0)
_AXUIElementGetWindow(self, &id)
return id
}
func focusedWindow() -> AXUIElement? {
return attribute(.focusedWindow, AXUIElement.self)
}
func windows() -> [AXUIElement]? {
return attribute(.windows, [AXUIElement].self)
}
func isMinimized() -> Bool {
return attribute(.minimized, Bool.self) == true
}
func focus(_ id: CGWindowID) {
var elementConnection = UInt32(0)
CGSGetWindowOwner(cgsMainConnectionId, id, &elementConnection)
var psn = ProcessSerialNumber()
CGSGetConnectionPSN(elementConnection, &psn)
_SLPSSetFrontProcessWithOptions(&psn, id, .userGenerated)
makeKeyWindow(psn, id)
AXUIElementPerformAction(self, kAXRaiseAction as CFString)
}
// The following function was ported from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
func makeKeyWindow(_ psn: ProcessSerialNumber, _ wid: CGWindowID) -> Void {
var wid_ = wid
var psn_ = psn
var bytes1 = [UInt8](repeating: 0, count: 0xf8)
bytes1[0x04] = 0xF8
bytes1[0x08] = 0x01
bytes1[0x3a] = 0x10
var bytes2 = [UInt8](repeating: 0, count: 0xf8)
bytes2[0x04] = 0xF8
bytes2[0x08] = 0x02
bytes2[0x3a] = 0x10
memcpy(&bytes1[0x3c], &wid_, MemoryLayout<UInt32>.size)
memset(&bytes1[0x20], 0xFF, 0x10)
memcpy(&bytes2[0x3c], &wid_, MemoryLayout<UInt32>.size)
memset(&bytes2[0x20], 0xFF, 0x10)
SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes1)).pointee))
SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes2)).pointee))
}
}
+36
View File
@@ -0,0 +1,36 @@
import Cocoa
import Foundation
typealias CGWindow = [CGWindowKey.RawValue: Any]
extension CGWindow {
static func windows(_ option: CGWindowListOption) -> [CGWindow] {
return CGWindowListCopyWindowInfo([.excludeDesktopElements, option], kCGNullWindowID) as! [CGWindow]
}
func value<T>(_ key: CGWindowKey, _ type: T.Type) -> T? {
return self[key.rawValue] as? T
}
// workaround: filtering this criteria seems to remove non-windows UI elements
func isNotMenubarOrOthers() -> Bool {
return value(.layer, Int.self) == 0
}
// workaround: some apps like chrome use a window to implement the search popover
func isReasonablyBig() -> Bool {
let windowBounds = CGRect(dictionaryRepresentation: value(.bounds, CFDictionary.self)!)!
return windowBounds.width > Preferences.minimumWindowSize && windowBounds.height > Preferences.minimumWindowSize
}
}
// This list of keys is not exhaustive; it contains only the values used by this app
// full public list: CoreGraphics.CGWindow.swift
enum CGWindowKey: String {
case number = "kCGWindowNumber"
case layer = "kCGWindowLayer"
case bounds = "kCGWindowBounds"
case ownerPID = "kCGWindowOwnerPID"
case ownerName = "kCGWindowOwnerName"
case name = "kCGWindowName"
}
@@ -0,0 +1,32 @@
import Cocoa
import Foundation
extension CGWindowID {
func AXUIElement(_ ownerPid: pid_t) -> AXUIElement? {
return AXUIElementCreateApplication(ownerPid).windows()?.first(where: { return $0.cgId() == self })
}
func AXUIElementOfOtherSpaceWindow(_ ownerPid: pid_t) -> AXUIElement? {
CGSAddWindowsToSpaces(cgsMainConnectionId, [self], [Spaces.currentSpaceId])
let axWindow = AXUIElement(ownerPid)
CGSRemoveWindowsFromSpaces(cgsMainConnectionId, [self], [Spaces.currentSpaceId])
return axWindow
}
func screenshot() -> CGImage? {
// CGSHWCaptureWindowList
var windowId_ = self
let options: CGSWindowCaptureOptions = [.captureIgnoreGlobalClipShape, .windowCaptureNominalResolution]
let list = CGSHWCaptureWindowList(cgsMainConnectionId, &windowId_, 1, options) as! [CGImage]
return list.first
// // CGWindowListCreateImage
// return CGWindowListCreateImage(.null, .optionIncludingWindow, self, [.boundsIgnoreFraming, .bestResolution])
// // CGSCaptureWindowsContentsToRectWithOptions
// var windowId_ = self
// var windowImage = CIContext(options: nil).createCGImage(CIImage.empty(), from: CIImage.empty().extent)!
// CGSCaptureWindowsContentsToRectWithOptions(cgsMainConnectionId, &windowId_, true, .zero, [.windowCaptureNominalResolution, .captureIgnoreGlobalClipShape], &windowImage)
// return windowImage
}
}
@@ -1,8 +1,8 @@
import Foundation
import Cocoa
// add CGFloat constructor from String
extension CGFloat {
// add CGFloat constructor from String
init?(_ string: String) {
guard let number = NumberFormatter().number(from: string) else {
return nil
@@ -11,8 +11,8 @@ extension CGFloat {
}
}
// add throw-on-nil method on Optional
extension Optional {
// add throw-on-nil method on Optional
func orThrow() throws -> Wrapped {
switch self {
case .some(let value):
@@ -24,8 +24,8 @@ extension Optional {
}
}
// add String constructor from CGFloat that round up at 1 decimal
extension String {
// add String constructor from CGFloat that round up at 1 decimal
init?(_ cgFloat: CGFloat) {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 1
@@ -36,8 +36,8 @@ extension String {
}
}
// add recursive lookup in subviews for specific type
extension NSView {
// add recursive lookup in subviews for specific type
func findNestedViews<T: NSView>(subclassOf: T.Type) -> [T] {
return recursiveSubviews.compactMap { $0 as? T }
}
@@ -47,8 +47,8 @@ extension NSView {
}
}
// add convenience to NSError
extension NSError {
// add convenience to NSError
class func make(domain: String, message: String, code: Int = 9999) -> NSError {
return NSError(
domain: domain,
@@ -57,3 +57,10 @@ extension NSError {
)
}
}
extension Collection {
// recursive flatMap
func joined() -> [Any] {
return flatMap { ($0 as? [Any])?.joined() ?? [$0] }
}
}
@@ -0,0 +1,286 @@
import Cocoa
import Foundation
// Private APIs are APIs that we can build the app against, but they are not supported or documented by Apple
// We can see their names as symbols in the SDK (see https://github.com/lwouis/MacOSX-SDKs)
// However their full signature is a best-effort of retro-engineering
// Very little information is available about private APIs. I tried to document them as much as possible here
// Some links:
// * Webkit repo: https://github.com/WebKit/webkit/blob/master/Source/WebCore/PAL/pal/spi/cg/CoreGraphicsSPI.h
// * Alt-tab-macos issue: https://github.com/lwouis/alt-tab-macos/pull/87#issuecomment-558624755
typealias CGSConnectionID = UInt32
typealias CGSSpaceID = UInt64
struct CGSWindowCaptureOptions: OptionSet {
let rawValue: UInt32
static let windowCaptureNominalResolution = CGSWindowCaptureOptions(rawValue: 1 << 0x0200)
static let captureIgnoreGlobalClipShape = CGSWindowCaptureOptions(rawValue: 1 << 0x0800)
}
enum SLPSMode: UInt32 {
case allWindows = 0x100
case userGenerated = 0x200
case noWindows = 0x400
}
// returns the connection to the WindowServer. This connection ID is required when calling other APIs
// * macOS 10.10+
@_silgen_name("CGSMainConnectionID")
func CGSMainConnectionID() -> CGSConnectionID
// returns an array of CGImage of the windows which ID is given as `windowList`. `windowList` is supposed to be an array of IDs but in my test on High Sierra, the function ignores other IDs than the first, and always returns the screenshot of the first window in the array
// * performance: the `HW` in the name seems to imply better performance, and it was observed by some contributors that it seems to be faster (see https://github.com/lwouis/alt-tab-macos/issues/45) than other methods
// * quality: medium
// * minimized windows: yes
// * windows in other spaces: yes
// * macOS 10.10+
@_silgen_name("CGSHWCaptureWindowList")
func CGSHWCaptureWindowList(_ cid: CGSConnectionID, _ windowList: inout CGWindowID, _ windowCount: UInt32, _ options: CGSWindowCaptureOptions) -> CFArray
// returns the CGImage of the window which ID is given in `wid`
// * performance: it seems that this function performs similarly to public API `CGWindowListCreateImage`
// * quality: low
// * minimized windows: yes
// * windows in other spaces: yes
// * macOS 10.10+
@_silgen_name("CGSCaptureWindowsContentsToRectWithOptions") @discardableResult
func CGSCaptureWindowsContentsToRectWithOptions(_ cid: CGSConnectionID, _ wid: inout CGWindowID, _ windowOnly: Bool, _ rect: CGRect, _ options: CGSWindowCaptureOptions, _ image: inout CGImage) -> CGError
// returns the connection ID for the provided window
// * macOS 10.10+
@_silgen_name("CGSGetWindowOwner") @discardableResult
func CGSGetWindowOwner(_ cid: CGSConnectionID, _ wid: CGWindowID, _ windowCid: inout CGSConnectionID) -> CGError
// returns the PSN for the provided connection ID
// * macOS 10.10+
@_silgen_name("CGSGetConnectionPSN") @discardableResult
func CGSGetConnectionPSN(_ cid: CGSConnectionID, _ psn: inout ProcessSerialNumber) -> CGError
// returns an array of displays (as NSDictionary) -> each having an array of spaces (as NSDictionary) at the "Spaces" key; each having a space ID (as UInt64) at the "id64" key
// /!\ only returns correct values if the user has checked the checkbox in Preferences > Mission Control > "Displays have separate Spaces"
// * macOS 10.10+
@_silgen_name("CGSCopyManagedDisplaySpaces")
func CGSCopyManagedDisplaySpaces(_ cid: CGSConnectionID) -> CFArray
// returns an array of window IDs (as UInt32) for the space(s) provided as `spaces`
// the elements of the array are ordered by the z-index order of the windows in each space, with some exceptions where spaces mix
// * macOS 10.10+
@_silgen_name("CGSCopyWindowsWithOptionsAndTags")
func CGSCopyWindowsWithOptionsAndTags(_ cid: CGSConnectionID, _ owner: UInt32, _ spaces: CFArray, _ options: UInt32, _ setTags: inout UInt64, _ clearTags: inout UInt64) -> CFArray
// returns the current space ID on the provided display UUID
// * macOS 10.10+
@_silgen_name("CGSManagedDisplayGetCurrentSpace")
func CGSManagedDisplayGetCurrentSpace(_ cid: CGSConnectionID, _ displayUuid: CFString) -> CGSSpaceID
// adds the provided windows to the provided spaces
// * macOS 10.10+
@_silgen_name("CGSAddWindowsToSpaces")
func CGSAddWindowsToSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray) -> Void
// remove the provided windows from the provided spaces
// * macOS 10.10+
@_silgen_name("CGSRemoveWindowsFromSpaces")
func CGSRemoveWindowsFromSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray) -> Void
// returns the front process PSN
// * macOS 10.12+
@_silgen_name("_SLPSGetFrontProcess") @discardableResult
func _SLPSGetFrontProcess(_ psn: inout ProcessSerialNumber) -> OSStatus
// focuses the front process
// * macOS 10.12+
@_silgen_name("_SLPSSetFrontProcessWithOptions") @discardableResult
func _SLPSSetFrontProcessWithOptions(_ psn: inout ProcessSerialNumber, _ wid: CGWindowID, _ mode: SLPSMode) -> CGError
// sends bytes to the WindowServer
// more context: https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
// * macOS 10.12+
@_silgen_name("SLPSPostEventRecordTo") @discardableResult
func SLPSPostEventRecordTo(_ psn: inout ProcessSerialNumber, _ bytes: inout UInt8) -> CGError
// returns the CGWindowID of the provided AXUIElement
// * macOS 10.10+
@_silgen_name("_AXUIElementGetWindow") @discardableResult
func _AXUIElementGetWindow(_ axUiElement: AXUIElement, _ wid: inout CGWindowID) -> AXError
// ------------------------------------------------------------
// below are some notes on some private APIs I experimented with
// ------------------------------------------------------------
//// returns true is the PSNs are the same
//// * deprecated in macOS 10.9, so we have to declare it to use it in Swift
//@_silgen_name("SameProcess")
//func SameProcess(_ psn1: inout ProcessSerialNumber, _ psn2: inout ProcessSerialNumber, _ same: inout DarwinBoolean) -> Void
//
//// returns the CGRect of a window
//// * performance: it seems that this function is faster than the public API AX calls to get a window bounds
//// * minimized windows: ?
//// * windows in other spaces: ?
//// * macOS 10.12+
//@_silgen_name("CGSGetWindowBounds") @discardableResult
//func CGSGetWindowBounds(_ cid: CGSConnectionID, _ wid: inout CGWindowID, _ frame: inout CGRect) -> CGError
//
//// * deprecated in macOS 10.9, so we have to declare it to use it in Swift
//@_silgen_name("GetProcessPID")
//func GetProcessPID(_ psn: inout ProcessSerialNumber, _ pid: inout pid_t) -> Void
//
//// seems like it takes the normal CG keys, so might as well use the public API
//// * macOS 10.10+
//@_silgen_name("CGSCopyWindowProperty") @discardableResult
//func CGSCopyWindowProperty(_ cid: CGSConnectionID, _ wid: CGWindowID, _ key: CFString, _ output: inout CFString) -> CGError
//
//// crashed the app with SIGSEGV
//// * macOS 10.10+
//@_silgen_name("CGSGetWindowType") @discardableResult
//func CGSGetWindowType(_ wid: CGWindowID, _ type: inout UInt32) -> CGError
//
//// seems to always return 0, for windows in the same space, other space, minimized, etc
//// * macOS 10.10+
//@_silgen_name("CGSGetWindowLevel") @discardableResult
//func CGSGetWindowLevel(_ cid: CGSConnectionID, _ wid: CGWindowID, _ level: inout UInt32) -> CGError
//
//// * macOS 10.12+
//@_silgen_name("CGSProcessAssignToSpace") @discardableResult
//func CGSProcessAssignToSpace(_ cid: CGSConnectionID, _ pid: pid_t, _ sid: CGSSpaceID) -> CGError
//
//// changes the active space for the display_ref (e.g. "Main"). This doesn't actually trigger the UI animation and switch to the space. It allows windows from that space to be manipulated (e.g. focused) from the current space. Very weird behaviour and graphical glitch will happen when triggering Mission Control
//// * macOS 10.10+
//@_silgen_name("CGSManagedDisplaySetCurrentSpace")
//func CGSManagedDisplaySetCurrentSpace(_ cid: CGSConnectionID, _ display: CFString, _ sid: CGSSpaceID) -> Void
//
//// get space for window
//// * macOS 10.10+
//@_silgen_name("CGSGetWindowWorkspace") @discardableResult
//func CGSGetWindowWorkspace(_ cid: CGSConnectionID, _ wid: CGWindowID, _ workspace: [Int]) -> OSStatus
//
//// returns the space uuid. Not very useful
//// * macOS 10.10+
//@_silgen_name("CGSSpaceCopyName")
//func CGSSpaceCopyName(_ cid: CGSConnectionID, _ sid: CGSSpaceID) -> CFString
//
//enum CGSWindowOrderingMode: Int {
// case orderAbove = 1 // Window is ordered above target.
// case orderBelow = -1 // Window is ordered below target.
// case orderOut = 0 // Window is removed from the on-screen window list.
//}
//
//// change window order. I tried with relativeToWindow=0, and place=.orderAbove, and it does nothing
//// * macOS 10.10+
//@_silgen_name("CGSOrderWindow") @discardableResult
//func CGSOrderWindow(_ cid: CGSConnectionID, _ win: CGWindowID, _ place: CGSWindowOrderingMode, relativeTo: CGWindowID /* can be NULL */) -> OSStatus
//
//// Get on-screen window counts and lists. With targetCID=1 -> returns []. With targetCID=0 -> crashes, with targetCID=cid -> crashes
//// * macOS 10.10+
//@_silgen_name("CGSGetWindowList") @discardableResult
//func CGSGetWindowList(_ cid: CGSConnectionID, _ targetCID: CGSConnectionID, _ count: Int, _ list: [Int], _ outCount: [Int]) -> OSStatus
//
//// per-workspace window counts and lists. Can't compile on macOS 10.14 ("Undefined symbol: _CGSGetWorkspaceWindowList"). There are references of this API on the internet, but it doesn't seem to appear in any SDK though
//// * macOS 10.10+
//@_silgen_name("CGSGetWorkspaceWindowList") @discardableResult
//func CGSGetWorkspaceWindowList(_ cid: CGSConnectionID, _ workspaceNumber: CGSSpaceID, _ count: Int, _ list: [Int], _ outCount: [Int]) -> OSStatus
//
//struct CGSSpaceMask: OptionSet {
// let rawValue: UInt32
// static let includesCurrent = CGSSpaceMask(rawValue: 1 << 0)
// static let includesOthers = CGSSpaceMask(rawValue: 1 << 1)
// static let includesUser = CGSSpaceMask(rawValue: 1 << 2)
//}
//
//// get the CGSSpaceIDs for the given windows (CGWindowIDs). It's more efficient (i.e. fewer calls to the OS) to get windows of each space instead of space of each window
//// * macOS 10.10+
//@_silgen_name("CGSCopySpacesForWindows")
//func CGSCopySpacesForWindows(_ connection: CGSConnectionID, _ mask: CGSSpaceMask, _ wids: CFArray) -> CFArray
//
//enum CGSSpaceType {
// case user
// case fullscreen
// case system
// case unknown
//}
//
//// get the CGSSpaceType for a given space. Maybe useful for fullscreen windows
//// * macOS 10.10+
//@_silgen_name("CGSSpaceGetType")
//func CGSSpaceGetType(_ connection: CGSConnectionID, _ space: CGSSpaceID) -> CGSSpaceType
//
//// assigns a process to all spaces. This creates weird behaviours where its windows are available from all spaces
//// * macOS 10.10+
//@_silgen_name("CGSProcessAssignToAllSpaces") @discardableResult
//func CGSProcessAssignToAllSpaces(_ cid: CGSConnectionID, _ pid: pid_t) -> CGError
//
//enum SpaceManagementMode: Int {
// case separate = 1
// case notSeparate = 0
//}
//
//// returns the status of the "Displays have separate Spaces" system Preference
//// * macOS 10.10+
//@_silgen_name("CGSGetSpaceManagementMode")
//func CGSGetSpaceManagementMode(_ cid: CGSConnectionID) -> SpaceManagementMode
//
//// get spaces for the provided window_list
//// * macOS 10.10+
//@_silgen_name("CGSCopySpacesForWindows")
//func CGSCopySpacesForWindows(_ cid: CGSConnectionID, _ selector: Int, _ window_list: CFArray) -> CFArray
//
//// The following function was ported from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
//func windowManagerDeferWindowRaise(_ psn: ProcessSerialNumber, _ wid: CGWindowID) -> Void {
// var wid_ = wid
// var psn_ = psn
//
// var bytes = [UInt8](repeating: 0, count: 0xf8)
// bytes[0x04] = 0xf8
// bytes[0x08] = 0x0d
// bytes[0x8a] = 0x09
//
// memcpy(&bytes[0x3c], &wid_, MemoryLayout<UInt32>.size)
// SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes)).pointee))
//}
//
//func windowManagerDeactivateWindow(_ psn: ProcessSerialNumber, _ wid: CGWindowID) -> Void {
// var wid_ = wid
// var psn_ = psn
//
// var bytes = [UInt8](repeating: 0, count: 0xf8)
// bytes[0x04] = 0xf8
// bytes[0x08] = 0x0d
// bytes[0x8a] = 0x02
//
// memcpy(&bytes[0x3c], &wid_, MemoryLayout<UInt32>.size)
// SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes)).pointee))
//}
//
//func windowManagerActivateWindow(_ psn: ProcessSerialNumber, _ wid: CGWindowID) -> Void {
// var wid_ = wid
// var psn_ = psn
//
// var bytes = [UInt8](repeating: 0, count: 0xf8)
// bytes[0x04] = 0xf8
// bytes[0x08] = 0x0d
// bytes[0x8a] = 0x01
//
// memcpy(&bytes[0x3c], &wid_, MemoryLayout<UInt32>.size)
// SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes)).pointee))
//}
//
//func psnEqual(_ psn1: ProcessSerialNumber, _ psn2: ProcessSerialNumber) -> Bool {
// var psn1_ = psn1
// var psn2_ = psn2
//
// var same = DarwinBoolean(false)
// SameProcess(&psn1_, &psn2_, &same)
// return same == DarwinBoolean(true)
//}
//
//func windowIdToPsn(_ wid: CGWindowID) -> ProcessSerialNumber {
// var elementConnection = UInt32(0)
// CGSGetWindowOwner(cgsMainConnectionId, wid, &elementConnection)
// var psn = ProcessSerialNumber()
// CGSGetConnectionPSN(elementConnection, &psn)
// return psn
//}
@@ -1,49 +0,0 @@
import Cocoa
import Foundation
class AccessibilityApis {
static func windows(_ cgOwnerPid: pid_t) -> [AXUIElement] {
if let windows = attribute(AXUIElementCreateApplication(cgOwnerPid), kAXWindowsAttribute, [AXUIElement].self) {
return windows.filter {
// workaround: some apps like chrome use a window to implement the search popover
let windowBounds = value($0, kAXSizeAttribute, NSSize(), .cgSize)!
let isReasonablyBig = windowBounds.width > Preferences.minimumWindowSize && windowBounds.height > Preferences.minimumWindowSize
return isReasonablyBig
}
}
return []
}
static func rect(_ element: AXUIElement) -> CGRect {
let sizeBefore = value(element, kAXSizeAttribute, NSSize(), .cgSize)!
let positionBefore = value(element, kAXPositionAttribute, NSPoint(), .cgPoint)!
return CGRect(x: positionBefore.x, y: positionBefore.y, width: sizeBefore.width, height: sizeBefore.height)
}
static func focus(_ element: AXUIElement) {
AXUIElementPerformAction(element, kAXRaiseAction as CFString)
}
static func setAttribute<T>(_ element: AXUIElement, _ value: T, _ attribute: String, _ type: AXValueType) {
var v = value
AXUIElementSetAttributeValue(element, attribute as CFString, AXValueCreate(type, &v)!)
}
private static func value<T>(_ element: AXUIElement, _ key: String, _ target: T, _ type: AXValueType) -> T? {
if let a = attribute(element, key, AXValue.self) {
var value = target
AXValueGetValue(a, type, &value)
return value
}
return nil
}
private static func attribute<T>(_ element: AXUIElement, _ key: String, _ type: T.Type) -> T? {
var value: AnyObject?
let result = AXUIElementCopyAttributeValue(element, key as CFString, &value)
if result == .success, let typedValue = value as? T {
return typedValue
}
return nil
}
}
@@ -1,24 +0,0 @@
import Cocoa
import Foundation
class CoreGraphicsApis {
static func windows() -> [NSDictionary] {
return (CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionOnScreenOnly], kCGNullWindowID) as! [NSDictionary])
.filter {
// workaround: filtering this criteria seems to remove non-windows UI elements
let isWindowNotMenubarOrOthers = value($0, kCGWindowLayer, Int(0)) == 0
let windowBounds = CGRect(dictionaryRepresentation: value($0, kCGWindowBounds, [:] as CFDictionary))!
// workaround: some apps like chrome use a window to implement the search popover
let isReasonablyBig = windowBounds.width > Preferences.minimumWindowSize && windowBounds.height > Preferences.minimumWindowSize
return isWindowNotMenubarOrOthers && isReasonablyBig
}
}
static func value<T>(_ cgWindow: NSDictionary, _ key: CFString, _ fallback: T) -> T {
return cgWindow[key] as? T ?? fallback
}
static func image(_ windowNumber: CGWindowID) -> CGImage? {
return CGWindowListCreateImage(.null, .optionIncludingWindow, windowNumber, [.boundsIgnoreFraming, .bestResolution])
}
}
+35 -33
View File
@@ -2,6 +2,8 @@ import Cocoa
import Carbon.HIToolbox.Events
class Keyboard {
static let backgroundQueue = DispatchQueue(label: "uiQueue", qos: .userInteractive, autoreleaseFrequency: .never)
static func listenToGlobalEvents(_ delegate: Application) {
listenToGlobalKeyboardEvents(delegate)
}
@@ -10,17 +12,16 @@ class Keyboard {
var eventTap: CFMachPort?
func listenToGlobalKeyboardEvents(_ delegate: Application) {
DispatchQueue.global(qos: .userInteractive).async {
Keyboard.backgroundQueue.async {
Thread.current.name = "uiQueue-thread"
let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: { (_, _, event, delegate_) -> Unmanaged<CGEvent>? in
let d = Unmanaged<Application>.fromOpaque(delegate_!).takeUnretainedValue()
return keyboardHandler(event, d)
},
callback: keyboardHandler,
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(delegate).toOpaque()))
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
@@ -29,42 +30,43 @@ func listenToGlobalKeyboardEvents(_ delegate: Application) {
}
}
func keyboardHandler(_ cgEvent: CGEvent, _ delegate: Application) -> Unmanaged<CGEvent>? {
if cgEvent.type == .keyDown || cgEvent.type == .keyUp || cgEvent.type == .flagsChanged {
if let event = NSEvent(cgEvent: cgEvent) {
let keyDown = event.type == .keyDown
func dispatchWork(_ application: Application, _ uiWorkShouldBeDone: Bool, _ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
application.uiWorkShouldBeDone = uiWorkShouldBeDone
DispatchQueue.main.async {
fn()
}
return nil // previously focused app should not receive keys
}
func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent, delegate_: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let application = Unmanaged<Application>.fromOpaque(delegate_!).takeUnretainedValue()
if type == .keyDown || type == .keyUp || type == .flagsChanged {
if let event = NSEvent(cgEvent: event_) {
let isTab = event.keyCode == Preferences.tabKeyCode
let isMeta = Preferences.metaKeyCodes!.contains(event.keyCode)
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 event.modifierFlags.contains(Preferences.metaModifierFlag!) {
if keyDown {
if isTab && event.modifierFlags.contains(.shift) {
delegate.showUiOrSelectPrevious()
return nil // previously focused app should not receive keys
} else if isTab {
delegate.showUiOrSelectNext()
return nil // previously focused app should not receive keys
} else if isRightArrow && delegate.appIsBeingUsed {
delegate.cycleSelection(1)
return nil // previously focused app should not receive keys
} else if isLeftArrow && delegate.appIsBeingUsed {
delegate.cycleSelection(-1)
return nil // previously focused app should not receive keys
} else if keyDown && isEscape {
delegate.hideUi()
return nil // previously focused app should not receive keys
}
if isMetaDown && type == .keyDown {
if isTab && event.modifierFlags.contains(.shift) {
return dispatchWork(application, true, { application.showUiOrCycleSelection(-1) })
} else if isTab {
return dispatchWork(application, true, { application.showUiOrCycleSelection(1) })
} else if isRightArrow && application.appIsBeingUsed {
return dispatchWork(application, true, { application.cycleSelection(1) })
} else if isLeftArrow && application.appIsBeingUsed {
return dispatchWork(application, true, { application.cycleSelection(-1) })
} else if type == .keyDown && isEscape {
return dispatchWork(application, false, { application.hideUi() })
}
} else if isMeta && !keyDown {
delegate.focusTarget()
return nil // previously focused app should not receive keys
} else if isMetaChanged && !isMetaDown {
return dispatchWork(application, false, { application.focusTarget() })
}
}
} else if cgEvent.type == .tapDisabledByUserInput || cgEvent.type == .tapDisabledByTimeout {
} else if type == .tapDisabledByUserInput || type == .tapDisabledByTimeout {
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
// focused app will receive the event
return Unmanaged.passRetained(cgEvent)
return Unmanaged.passRetained(event_)
}
+14 -11
View File
@@ -2,11 +2,6 @@ import Foundation
import Cocoa
import Carbon.HIToolbox.Events
enum ShowOnScreenPreference {
case MAIN
case MOUSE
}
class Preferences {
static var defaults: [String: String] = [
"maxScreenUsage": "80",
@@ -20,12 +15,15 @@ class Preferences {
"showOnScreen": showOnScreenMacro.macros[0].label
]
static var rawValues = [String: String]()
static var minimumWindowSize: CGFloat = 200
static var fontColor: NSColor = .white
static var windowMaterial: NSVisualEffectView.Material = .dark
static var windowPadding: CGFloat = 23
static var interItemPadding: CGFloat = 4
static var cellPadding: CGFloat = 6
static var minimumWindowSize = CGFloat(200)
static var emptyThumbnailWidth = CGFloat(200)
static var emptyThumbnailHeight = CGFloat(emptyThumbnailWidth * 9 / 16)
static var fontColor = NSColor.white
static var windowMaterial = NSVisualEffectView.Material.dark
static var windowPadding = CGFloat(23)
static var interItemPadding = CGFloat(4)
static var fontIconSize = CGFloat(20)
static var cellPadding = CGFloat(6)
static var cellBorderWidth: CGFloat?
static var cellCornerRadius: CGFloat?
static var maxScreenUsage: CGFloat?
@@ -174,3 +172,8 @@ class MacroPreferenceHelper<T> {
}
}
}
enum ShowOnScreenPreference {
case MAIN
case MOUSE
}
+11 -3
View File
@@ -2,16 +2,16 @@ import Foundation
import Cocoa
class Screen {
static func preferredScreen() -> NSScreen {
static func preferred() -> NSScreen {
switch Preferences.showOnScreen! {
case .MOUSE:
return screenWithMouse() ?? NSScreen.main!; // .main as fall-back
return withMouse() ?? NSScreen.main!; // .main as fall-back
case .MAIN:
return NSScreen.main!;
}
}
private static func screenWithMouse() -> NSScreen? {
private static func withMouse() -> NSScreen? {
return NSScreen.screens.first { NSMouseInRect(NSEvent.mouseLocation, $0.frame, false) }
}
@@ -36,6 +36,14 @@ class Screen {
panel.makeKeyAndOrderFront(nil)
Application.shared.arrangeInFront(nil)
}
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())!
}
}
enum VerticalAlignment: CGFloat {
+22
View File
@@ -0,0 +1,22 @@
import Cocoa
import Foundation
class Spaces {
static var singleSpace = true
static var currentSpaceId = CGSSpaceID(1)
static var currentSpaceIndex = SpaceIndex(1)
static func allIdsAndIndexes() -> [(CGSSpaceID, SpaceIndex)] {
return (CGSCopyManagedDisplaySpaces(cgsMainConnectionId) as! [NSDictionary])
.map { return $0["Spaces"] }.joined().enumerated()
.map { (($0.element as! NSDictionary)["id64"]! as! CGSSpaceID, $0.offset + 1) }
}
static func windowsInSpaces(_ spaceIds: [CGSSpaceID]) -> [CGWindowID] {
var set_tags = UInt64(0)
var clear_tags = UInt64(0)
return CGSCopyWindowsWithOptionsAndTags(cgsMainConnectionId, 0, spaceIds as CFArray, 2, &set_tags, &clear_tags) as! [CGWindowID]
}
}
typealias SpaceIndex = Int
+5 -3
View File
@@ -1,7 +1,9 @@
import Foundation
import Cocoa
// macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy > Privacy
class SystemPermissions {
// macOS 10.9+
static func ensureAccessibilityCheckboxIsChecked() {
if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) {
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Accessibility.",
@@ -12,10 +14,10 @@ class SystemPermissions {
}
}
// macOS 10.15+
static func ensureScreenRecordingCheckboxIsChecked() {
let firstWindow = CoreGraphicsApis.windows()[0]
let windowNumber = CoreGraphicsApis.value(firstWindow, kCGWindowNumber, UInt32(0))
if CoreGraphicsApis.image(windowNumber) == nil {
let firstWindow = CGWindow.windows(.optionOnScreenOnly)[0]
if let cgId = firstWindow.value(.number, CGWindowID.self), cgId.screenshot() == nil {
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Screen Recording.",
"Please authorize and re-launch.",
"See https://dropshare.zendesk.com/hc/en-us/articles/360033453434-Enabling-Screen-Recording-Permission-on-macOS-Catalina-10-15-",
+45
View File
@@ -0,0 +1,45 @@
import Cocoa
import Foundation
class TrackedWindow {
var cgWindow: CGWindow
var ownerPid: pid_t
var id: CGWindowID
var title: String
var thumbnail: NSImage?
var icon: NSImage?
var app: NSRunningApplication?
var axWindow: AXUIElement?
var isMinimized: Bool
var spaceId: CGSSpaceID?
var spaceIndex: SpaceIndex?
var rank: WindowRank?
init(_ cgWindow: CGWindow, _ cgId: CGWindowID, _ ownerPid: pid_t, _ isMinimized: Bool, _ axWindow: AXUIElement?, _ spaceId: CGSSpaceID?, _ spaceIndex: SpaceIndex?, _ rank: WindowRank?) {
self.cgWindow = cgWindow
self.id = cgId
self.ownerPid = ownerPid
let cgTitle = cgWindow.value(.name, String.self)
let cgOwnerName = cgWindow.value(.ownerName, String.self)
// for some reason Google Chrome uses a unicode 0-width no-break space character in their empty window title
self.title = cgTitle != nil && cgTitle != "" && cgTitle != "" ? cgTitle! : cgOwnerName ?? ""
self.app = NSRunningApplication(processIdentifier: ownerPid)
self.icon = self.app?.icon
if let cgImage = cgId.screenshot() {
self.thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
}
self.axWindow = axWindow
self.isMinimized = isMinimized
self.spaceId = spaceId
// System Preferences windows appear on all spaces, so we make them the current space
self.spaceIndex = app?.bundleIdentifier == "com.apple.systempreferences" ? Spaces.currentSpaceIndex : spaceIndex
self.rank = rank
}
func focus() {
if axWindow == nil {
axWindow = id.AXUIElementOfOtherSpaceWindow(ownerPid)
}
axWindow?.focus(id)
}
}
+77
View File
@@ -0,0 +1,77 @@
import Cocoa
import Foundation
class TrackedWindows {
static var list = [TrackedWindow]()
static var focusedWindowIndex = Int(0)
static func focusedWindow() -> TrackedWindow? {
return list.count > focusedWindowIndex ? list[focusedWindowIndex] : nil
}
static func moveFocusedWindowIndex(_ step: Int) -> Int {
return focusedWindowIndex + step < 0 ? list.count - 1 : (focusedWindowIndex + step) % list.count
}
static func refreshList(_ step: Int) {
list.removeAll()
focusedWindowIndex = 0
let spaces = Spaces.allIdsAndIndexes()
Spaces.singleSpace = spaces.count == 1
Spaces.currentSpaceId = CGSManagedDisplayGetCurrentSpace(cgsMainConnectionId, Screen.mainUuid())
Spaces.currentSpaceIndex = spaces.first { $0.0 == Spaces.currentSpaceId }!.1
filterAndAddToList(mapWindowsWithRankAndSpace(spaces))
sortList()
TrackedWindows.focusedWindowIndex = TrackedWindows.moveFocusedWindowIndex(step)
}
private static func mapWindowsWithRankAndSpace(_ spaces: [(CGSSpaceID, SpaceIndex)]) -> [CGWindowID: (CGSSpaceID, SpaceIndex, WindowRank)] {
var windowSpaceMap: [CGWindowID: (CGSSpaceID, SpaceIndex, WindowRank?)] = [:]
for (spaceId, spaceIndex) in spaces {
Spaces.windowsInSpaces([spaceId]).forEach {
windowSpaceMap[$0] = (spaceId, spaceIndex, nil)
}
}
Spaces.windowsInSpaces(spaces.map { $0.0 }).enumerated().forEach {
windowSpaceMap[$0.element]!.2 = $0.offset
}
return windowSpaceMap as! [CGWindowID : (CGSSpaceID, SpaceIndex, WindowRank)]
}
private static func sortList() {
list.sort(by: {
if $0.rank == nil {
return false
}
if $1.rank == nil {
return true
}
return $0.rank! < $1.rank!
})
}
private static func filterAndAddToList(_ windowsMap: [CGWindowID: (CGSSpaceID, SpaceIndex, WindowRank)]) {
for cgWindow in CGWindow.windows(.optionAll) {
guard let cgId = cgWindow.value(.number, CGWindowID.self),
let ownerPid = cgWindow.value(.ownerPID, pid_t.self),
cgWindow.isNotMenubarOrOthers(),
cgWindow.isReasonablyBig() else {
continue
}
let (spaceId, spaceIndex, rank) = windowsMap[cgId] ?? (nil, nil, nil)
if let axWindow = cgId.AXUIElement(ownerPid) {
if spaceId != nil {
list.append(TrackedWindow(cgWindow, cgId, ownerPid, false, axWindow, spaceId, spaceIndex, rank))
} else if axWindow.isMinimized() {
list.append(TrackedWindow(cgWindow, cgId, ownerPid, true, axWindow, spaceId, spaceIndex, rank))
}
} else {
// window is on another space
guard spaceId != nil else { continue }
list.append(TrackedWindow(cgWindow, cgId, ownerPid, false, nil, spaceId, spaceIndex, rank))
}
}
}
}
typealias WindowRank = Int
-45
View File
@@ -1,45 +0,0 @@
import Cocoa
import Foundation
class OpenWindow {
var target: AXUIElement?
var ownerPid: pid_t?
var cgId: CGWindowID
var cgTitle: String
lazy var thumbnail: NSImage = computeThumbnail()
lazy var icon: NSImage? = computeIcon()
init(_ target: AXUIElement?, _ ownerPid: pid_t?, _ cgId: CGWindowID, _ cgTitle: String) {
self.target = target
self.ownerPid = ownerPid
self.cgId = cgId
self.cgTitle = cgTitle
}
func computeIcon() -> NSImage? {
return NSRunningApplication(processIdentifier: ownerPid!)?.icon
}
func computeThumbnail() -> NSImage {
let windowImage = CoreGraphicsApis.image(cgId)
return NSImage(cgImage: windowImage!, size: NSSize(width: windowImage!.width, height: windowImage!.height))
}
func focus() {
if let app = NSRunningApplication(processIdentifier: ownerPid!) {
app.activate(options: [.activateIgnoringOtherApps])
AccessibilityApis.focus(target!)
}
}
}
func computeDownscaledSize(_ image: NSImage, _ screen: NSScreen) -> (Int, Int) {
let imageRatio = image.size.width / image.size.height
let thumbnailMaxSize = Screen.thumbnailMaxSize(screen)
let thumbnailWidth = Int(floor(thumbnailMaxSize.height * imageRatio))
if thumbnailWidth <= Int(thumbnailMaxSize.width) {
return (thumbnailWidth, Int(thumbnailMaxSize.height))
} else {
return (Int(thumbnailMaxSize.width), Int(floor(thumbnailMaxSize.width / imageRatio)))
}
}
Binary file not shown.
+29 -67
View File
@@ -1,17 +1,16 @@
import Foundation
import Cocoa
let cgsMainConnectionId = CGSMainConnectionID()
class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
static let name = "AltTab"
var statusItem: NSStatusItem?
var thumbnailsPanel: ThumbnailsPanel?
var preferencesPanel: PreferencesPanel?
var selectedOpenWindow: Int = 0
var numberOfColumns: Int = 0
var openWindows: [OpenWindow] = []
var workItems: [DispatchWorkItem] = []
var isFirstSummon: Bool = true
var appIsBeingUsed: Bool = false
var uiWorkShouldBeDone = true
var isFirstSummon = true
var appIsBeingUsed = false
override init() {
super.init()
@@ -29,6 +28,12 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
statusItem = StatusItem.make(self)
initPreferencesDependentComponents()
Keyboard.listenToGlobalEvents(self)
warmUpThumbnailPanel()
}
// running this code on startup avoid having the very first invocation be slow for the user
private func warmUpThumbnailPanel() {
thumbnailsPanel!.computeThumbnails(Screen.preferred())
}
// we put application code here which should be executed on init() and Preferences change
@@ -36,19 +41,9 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
thumbnailsPanel = ThumbnailsPanel(self)
}
func showUiOrSelectNext() {
debugPrint("showUiOrSelectNext")
showUiOrCycleSelection(1)
}
func showUiOrSelectPrevious() {
debugPrint("showUiOrSelectPrevious")
showUiOrCycleSelection(-1)
}
func hideUi() {
debugPrint("hideUi")
DispatchQueue.main.async(execute: { self.thumbnailsPanel!.orderOut(nil) })
thumbnailsPanel!.orderOut(nil)
appIsBeingUsed = false
isFirstSummon = true
}
@@ -56,8 +51,8 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
func focusTarget() {
debugPrint("focusTarget")
if appIsBeingUsed {
focusSelectedWindow(currentlySelectedWindow())
hideUi()
debugPrint("focusTarget: appIsBeingUsed")
focusSelectedWindow(TrackedWindows.focusedWindow())
}
}
@@ -66,69 +61,36 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
if preferencesPanel == nil {
preferencesPanel = PreferencesPanel()
}
Screen.showPanel(preferencesPanel!, Screen.preferredScreen(), .appleCentered)
}
func computeOpenWindows() {
openWindows.removeAll()
// we rely on the fact that CG and AX APIs arrays follow the same order to match objects from both APIs
var pidAndCurrentIndex: [pid_t: Int] = [:]
for cgWindow in CoreGraphicsApis.windows() {
let cgId = CoreGraphicsApis.value(cgWindow, kCGWindowNumber, UInt32(0))
let cgTitle = CoreGraphicsApis.value(cgWindow, kCGWindowName, "")
let cgOwnerName = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerName, "")
let cgOwnerPid = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerPID, Int32(0))
let i = pidAndCurrentIndex.index(forKey: cgOwnerPid)
pidAndCurrentIndex[cgOwnerPid] = (i == nil ? 0 : pidAndCurrentIndex[i!].value + 1)
let axWindows_ = AccessibilityApis.windows(cgOwnerPid)
// windows may have changed between the CG and the AX calls
if axWindows_.count > pidAndCurrentIndex[cgOwnerPid]! {
openWindows.append(OpenWindow(axWindows_[pidAndCurrentIndex[cgOwnerPid]!], cgOwnerPid, cgId, cgTitle.isEmpty ? cgOwnerName : cgTitle))
}
}
}
func cellWithStep(_ step: Int) -> Int {
return selectedOpenWindow + step < 0 ? openWindows.count - 1 : (selectedOpenWindow + step) % openWindows.count
Screen.showPanel(preferencesPanel!, Screen.preferred(), .appleCentered)
}
func cycleSelection(_ step: Int) {
selectedOpenWindow = cellWithStep(step)
DispatchQueue.main.async(execute: { self.thumbnailsPanel!.highlightCellAt(step) })
TrackedWindows.focusedWindowIndex = TrackedWindows.moveFocusedWindowIndex(step)
self.thumbnailsPanel!.highlightCellAt(step)
}
func showUiOrCycleSelection(_ step: Int) {
debugPrint("showUiOrCycleSelection", step)
appIsBeingUsed = true
if isFirstSummon {
debugPrint("showUiOrCycleSelection: isFirstSummon")
isFirstSummon = false
selectedOpenWindow = 0
computeOpenWindows()
if openWindows.count <= 0 {
TrackedWindows.refreshList(step)
if TrackedWindows.list.count == 0 {
return
}
selectedOpenWindow = cellWithStep(step)
var workItem: DispatchWorkItem!
workItem = DispatchWorkItem {
let currentScreen = Screen.preferredScreen() // fix screen between steps since it could change (e.g. mouse moved to another screen)
if !workItem.isCancelled { self.thumbnailsPanel!.computeThumbnails(currentScreen) }
if !workItem.isCancelled { self.thumbnailsPanel!.highlightCellAt(step) }
if !workItem.isCancelled { Screen.showPanel(self.thumbnailsPanel!, currentScreen, .appleCentered) }
}
workItems.append(workItem)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay!, execute: workItem)
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
if uiWorkShouldBeDone { self.thumbnailsPanel!.computeThumbnails(currentScreen); debugPrint("computeThumbnails") }
if uiWorkShouldBeDone { self.thumbnailsPanel!.highlightCellAt(step); debugPrint("highlightCellAt") }
if uiWorkShouldBeDone { Screen.showPanel(self.thumbnailsPanel!, currentScreen, .appleCentered); debugPrint("showPanel") }
} else {
debugPrint("showUiOrCycleSelection: !isFirstSummon")
cycleSelection(step)
}
}
func focusSelectedWindow(_ window: OpenWindow?) {
workItems.forEach({ $0.cancel() })
workItems.removeAll()
window?.focus()
func focusSelectedWindow(_ window: TrackedWindow?) {
hideUi()
DispatchQueue.global(qos: .userInteractive).async { window?.focus() }
}
func currentlySelectedWindow() -> OpenWindow? {
return openWindows.count > selectedOpenWindow ? openWindows[selectedOpenWindow] : nil
}
}
+39 -14
View File
@@ -1,13 +1,16 @@
import Cocoa
import WebKit
typealias MouseDownCallback = (OpenWindow) -> Void
typealias MouseDownCallback = (TrackedWindow) -> Void
typealias MouseMovedCallback = (Cell) -> Void
class Cell: NSCollectionViewItem {
var thumbnail = NSImageView()
var icon = NSImageView()
var label = CellTitle()
var openWindow: OpenWindow?
var appIcon = NSImageView()
var label = CellTitle(Preferences.fontHeight!)
var minimizedIcon = FontIcon(FontIcon.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white)
var spaceIcon = FontIcon(FontIcon.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
var openWindow: TrackedWindow?
var mouseDownCallback: MouseDownCallback?
var mouseMovedCallback: MouseMovedCallback?
@@ -16,7 +19,7 @@ class Cell: NSCollectionViewItem {
let vStackView = makeVStackView(hStackView)
let shadow = Cell.makeShadow(.gray)
thumbnail.shadow = shadow
icon.shadow = shadow
appIcon.shadow = shadow
view = vStackView
}
@@ -35,19 +38,25 @@ class Cell: NSCollectionViewItem {
}
}
func updateWithNewContent(_ element: OpenWindow, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
func updateWithNewContent(_ element: TrackedWindow, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
openWindow = element
thumbnail.image = element.thumbnail
let (width, height) = computeDownscaledSize(element.thumbnail, screen)
thumbnail.image!.size = NSSize(width: width, height: height)
let (width, height) = Cell.computeDownscaledSize(element.thumbnail, screen)
thumbnail.image?.size = NSSize(width: width, height: height)
thumbnail.frame.size = NSSize(width: width, height: height)
icon.image = element.icon
icon.image?.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
icon.frame.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
label.string = element.cgTitle
appIcon.image = element.icon
appIcon.image?.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
appIcon.frame.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
label.string = element.title
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
label.font = Preferences.font!
label.textContainer!.size.width = thumbnail.frame.size.width - Preferences.iconSize! - Preferences.interItemPadding
let fontIconWidth = Spaces.singleSpace && !openWindow!.isMinimized ? 0 : Preferences.fontIconSize + Preferences.interItemPadding
label.textContainer!.size.width = thumbnail.frame.width - Preferences.iconSize! - Preferences.interItemPadding - fontIconWidth
minimizedIcon.isHidden = !openWindow!.isMinimized
spaceIcon.isHidden = openWindow!.isMinimized || Spaces.singleSpace
if !spaceIcon.isHidden {
spaceIcon.setNumber(UInt32(element.spaceIndex!))
}
self.mouseDownCallback = mouseDownCallback
self.mouseMovedCallback = mouseMovedCallback
if view.trackingAreas.count > 0 {
@@ -56,6 +65,20 @@ class Cell: NSCollectionViewItem {
view.addTrackingArea(NSTrackingArea(rect: view.bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil))
}
static func computeDownscaledSize(_ image: NSImage?, _ screen: NSScreen) -> (Int, Int) {
if let image_ = image {
let imageRatio = image_.size.width / image_.size.height
let thumbnailMaxSize = Screen.thumbnailMaxSize(screen)
let thumbnailWidth = Int(floor(thumbnailMaxSize.height * imageRatio))
if thumbnailWidth <= Int(thumbnailMaxSize.width) {
return (thumbnailWidth, Int(thumbnailMaxSize.height))
} else {
return (Int(thumbnailMaxSize.width), Int(floor(thumbnailMaxSize.width / imageRatio)))
}
}
return (Int(Preferences.emptyThumbnailWidth), Int((Preferences.emptyThumbnailHeight)))
}
static func makeShadow(_ color: NSColor) -> NSShadow {
let shadow = NSShadow()
shadow.shadowColor = color
@@ -67,8 +90,10 @@ class Cell: NSCollectionViewItem {
private func makeHStackView() -> NSStackView {
let hStackView = NSStackView()
hStackView.spacing = Preferences.interItemPadding
hStackView.addView(icon, in: .leading)
hStackView.addView(appIcon, in: .leading)
hStackView.addView(label, in: .leading)
hStackView.addView(minimizedIcon, in: .leading)
hStackView.addView(spaceIcon, in: .leading)
return hStackView
}
+38
View File
@@ -0,0 +1,38 @@
import Cocoa
// Font icon using SF Symbols from the SF Pro font from Apple
// see https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
class FontIcon: CellTitle {
static let sfSymbolCircledMinusSign = "􀁎"
static let sfSymbolCircledNumber0 = "􀀸"
static let sfSymbolCircledNumber10 = "􀓵"
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(_ text: String, _ size: CGFloat, _ color: NSColor) {
// This helps SF symbols display vertically centered and not clipped at the bottom
super.init(size, 3)
string = text
font = NSFont(name: "SF Pro Text", size: size)
textColor = color
// This helps SF symbols not be clipped on the right
widthAnchor.constraint(equalToConstant: size * 1.15).isActive = true
}
// number should be in the interval [0-50]
func setNumber(_ number: UInt32) {
let (baseCharacter, offset) = baseCharacterAndOffset(number)
string = String(UnicodeScalar(baseCharacter.unicodeScalars.first!.value + offset)!)
}
private func baseCharacterAndOffset(_ number: UInt32) -> (String, UInt32) {
if number <= 9 {
// numbers alternate between empty and full circles; we skip the full circles
return (FontIcon.sfSymbolCircledNumber0, number * UInt32(2))
} else {
return (FontIcon.sfSymbolCircledNumber10, number - 10)
}
}
}
+9 -8
View File
@@ -7,8 +7,6 @@ class BaseLabel: NSTextView {
init(_ text: String) {
super.init(frame: .zero)
_init()
heightAnchor.constraint(greaterThanOrEqualToConstant: Preferences.fontHeight! + Preferences.interItemPadding).isActive = true
string = text
}
@@ -28,11 +26,14 @@ class BaseLabel: NSTextView {
}
class CellTitle: BaseLabel {
let magicOffset: CGFloat
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
init(_ size: CGFloat, _ magicOffset: CGFloat = 0) {
self.magicOffset = magicOffset
let textStorage = NSTextStorage()
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
@@ -43,15 +44,15 @@ class CellTitle: BaseLabel {
super.init(frame: .zero, textContainer: textContainer)
textColor = Preferences.fontColor
shadow = Cell.makeShadow(.darkGray)
defaultParagraphStyle = makeParagraphStyle()
heightAnchor.constraint(equalToConstant: Preferences.fontHeight!).isActive = true
defaultParagraphStyle = makeParagraphStyle(size)
heightAnchor.constraint(equalToConstant: size + magicOffset).isActive = true
}
private func makeParagraphStyle() -> NSMutableParagraphStyle {
private func makeParagraphStyle(_ size: CGFloat) -> NSMutableParagraphStyle {
let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.maximumLineHeight = Preferences.fontHeight!
paragraphStyle.minimumLineHeight = Preferences.fontHeight!
paragraphStyle.maximumLineHeight = size + magicOffset
paragraphStyle.minimumLineHeight = size + magicOffset
paragraphStyle.allowsDefaultTighteningForTruncation = false
return paragraphStyle
}
-5
View File
@@ -18,11 +18,6 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
contentView = makeContentView()
}
override func close() {
(NSApp as! Application).preferencesPanel = nil
super.close()
}
public func windowShouldClose(_ sender: NSWindow) -> Bool {
windowCloseRequested = true
challengeNextInvalidEditableTextField()
+12 -13
View File
@@ -24,6 +24,8 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
collectionView_ = makeCollectionView()
backgroundView = makeBackgroundView()
contentView!.addSubview(backgroundView!)
// highest level possible; this allows the app to go on top of context menus
level = .screenSaver
}
private func makeBackgroundView() -> NSVisualEffectView {
@@ -51,44 +53,41 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
func makeLayout() -> CollectionViewCenterFlowLayout {
let layout = CollectionViewCenterFlowLayout()
layout.estimatedItemSize = NSSize(width: 200, height: 200)
layout.estimatedItemSize = NSSize(width: Preferences.emptyThumbnailWidth, height: Preferences.emptyThumbnailHeight)
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 5
return layout
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
// debugPrint("collectionView: count items", application!.openWindows.count)
return application!.openWindows.count
return TrackedWindows.list.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
// debugPrint("collectionView: make item", indexPath.item)
let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell
item.updateWithNewContent(application!.openWindows[indexPath.item], application!.focusSelectedWindow, application!.thumbnailsPanel!.highlightCell, currentScreen!)
item.updateWithNewContent(TrackedWindows.list[indexPath.item], application!.focusSelectedWindow, application!.thumbnailsPanel!.highlightCell, currentScreen!)
return item
}
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
// debugPrint("collectionView: item size")
if indexPath.item < application!.openWindows.count {
let (width, height) = computeDownscaledSize(application!.openWindows[indexPath.item].thumbnail, currentScreen!)
if indexPath.item < TrackedWindows.list.count {
let (width, height) = Cell.computeDownscaledSize(TrackedWindows.list[indexPath.item].thumbnail, currentScreen!)
return NSSize(width: CGFloat(width) + Preferences.cellPadding * 2, height: CGFloat(height) + max(Preferences.fontHeight!, Preferences.iconSize!) + Preferences.interItemPadding + Preferences.cellPadding * 2)
}
return .zero
}
func highlightCellAt(_ step: Int) {
collectionView_!.selectItems(at: [IndexPath(item: application!.selectedOpenWindow, section: 0)], scrollPosition: .top)
collectionView_!.deselectItems(at: [IndexPath(item: application!.cellWithStep(-step), section: 0)])
collectionView_!.selectItems(at: [IndexPath(item: TrackedWindows.focusedWindowIndex, section: 0)], scrollPosition: .top)
collectionView_!.deselectItems(at: [IndexPath(item: TrackedWindows.moveFocusedWindowIndex(-step), section: 0)])
}
func highlightCell(_ cell: Cell) {
let newIndex = collectionView_.indexPath(for: cell)!
if application!.selectedOpenWindow != newIndex.item {
if TrackedWindows.focusedWindowIndex != newIndex.item {
collectionView_!.selectItems(at: [newIndex], scrollPosition: .top)
collectionView_!.deselectItems(at: [IndexPath(item: application!.selectedOpenWindow, section: 0)])
application!.selectedOpenWindow = newIndex.item
collectionView_!.deselectItems(at: [IndexPath(item: TrackedWindows.focusedWindowIndex, section: 0)])
TrackedWindows.focusedWindowIndex = newIndex.item
}
}