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 = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */; };
D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */; }; D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */; };
D04BA960DDD1D32A3019C835 /* CollectionViewCenterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */; }; D04BA960DDD1D32A3019C835 /* CollectionViewCenterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */; };
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.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 */; }; 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 */; }; D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; };
F029861A378EC1417106FEC3 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298E42A818112B290FF6C7 /* TextField.swift */; }; F029861A378EC1417106FEC3 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298E42A818112B290FF6C7 /* TextField.swift */; };
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */; }; F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
@@ -87,12 +101,21 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
4807A6C423A9CD190052A53E /* Frameworks */ = {
isa = PBXGroup;
children = (
4807A6C523A9CD190052A53E /* SkyLight.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
D04BA1463D2A17038222BB84 = { D04BA1463D2A17038222BB84 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -110,6 +133,7 @@
D04BA703DCD38D9757093312 /* ci */, D04BA703DCD38D9757093312 /* ci */,
D04BA459034C1885CA43A807 /* LICENCE.md */, D04BA459034C1885CA43A807 /* LICENCE.md */,
D04BA2C9EF33A646D0977195 /* .github */, D04BA2C9EF33A646D0977195 /* .github */,
4807A6C423A9CD190052A53E /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -121,6 +145,18 @@
name = Products; name = Products;
sourceTree = "<group>"; 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 */ = { D04BA2C9EF33A646D0977195 /* .github */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -150,6 +186,14 @@
path = "windows-10"; path = "windows-10";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D04BA5A0E9C82F7579CD2B78 /* resources */ = {
isa = PBXGroup;
children = (
D04BABC654F40BE74DA25BC7 /* SF-Pro-Text-Regular.otf */,
);
path = resources;
sourceTree = "<group>";
};
D04BA63877FC8FB11C43C3D2 /* alt-tab-macos */ = { D04BA63877FC8FB11C43C3D2 /* alt-tab-macos */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -175,13 +219,12 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D04BA35456DA0DDA74F9687E /* Keyboard.swift */, D04BA35456DA0DDA74F9687E /* Keyboard.swift */,
D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */, D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */,
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */, D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */,
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */, D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */, D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA86768C6503A11ED81FC /* Extensions.swift */, D04BA5EB5ED248C8C22CC672 /* Spaces.swift */,
D04BAE965ED779513FDEA0CD /* AccessibilityApis.swift */, D04BAE80772D25834E440975 /* TrackedWindow.swift */,
D04BA7AC3D9153353CE0A76D /* CoreGraphicsApis.swift */,
); );
path = logic; path = logic;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -198,6 +241,7 @@
D04BAD32E130E4A061DC8332 /* Labels.swift */, D04BAD32E130E4A061DC8332 /* Labels.swift */,
F0298E42A818112B290FF6C7 /* TextField.swift */, F0298E42A818112B290FF6C7 /* TextField.swift */,
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */, F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */,
D04BAED53465957807CBF8B2 /* FontIcon.swift */,
); );
path = ui; path = ui;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -227,6 +271,8 @@
D04BA7B6AAB0812631BBC7A2 /* Info.plist */, D04BA7B6AAB0812631BBC7A2 /* Info.plist */,
D04BAA44C837F3A67403B9DB /* main.swift */, D04BAA44C837F3A67403B9DB /* main.swift */,
D04BAA1C553891551B903DA7 /* logic */, D04BAA1C553891551B903DA7 /* logic */,
D04BA22D2CA2755FA5902C34 /* api-wrappers */,
D04BA5A0E9C82F7579CD2B78 /* resources */,
); );
path = "alt-tab-macos"; path = "alt-tab-macos";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -290,6 +336,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D04BA8373D4DE452C0C081ED /* SF-Pro-Text-Regular.otf in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -304,7 +351,7 @@
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */, D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */,
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */, D04BA20D4A240843293B3B52 /* Cell.swift in Sources */,
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */, D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */,
D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */, D04BA278D9EFA568C8D18A4C /* TrackedWindows.swift in Sources */,
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */, D04BA3261C7DA5F48310E654 /* Application.swift in Sources */,
D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */, D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */,
D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */, D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */,
@@ -312,12 +359,17 @@
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */, D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */,
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */, D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */,
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */, D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */,
D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */,
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */, D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */,
F029861A378EC1417106FEC3 /* TextField.swift in Sources */, F029861A378EC1417106FEC3 /* TextField.swift in Sources */,
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */, F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */,
D04BACD2FFB12589F9286B47 /* AccessibilityApis.swift in Sources */, D04BA308162F8043F8561D03 /* AXUIElement.swift in Sources */,
D04BA89A77CFDC4A3DF30487 /* CoreGraphicsApis.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; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -330,6 +382,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements"; CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements";
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = /System/Library/PrivateFrameworks;
INFOPLIST_FILE = "alt-tab-macos/Info.plist"; INFOPLIST_FILE = "alt-tab-macos/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12; MACOSX_DEPLOYMENT_TARGET = 10.12;
@@ -345,6 +398,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements"; CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements";
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = /System/Library/PrivateFrameworks;
INFOPLIST_FILE = "alt-tab-macos/Info.plist"; INFOPLIST_FILE = "alt-tab-macos/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12; MACOSX_DEPLOYMENT_TARGET = 10.12;
+2
View File
@@ -28,5 +28,7 @@
<string>NSApplication</string> <string>NSApplication</string>
<key>LSUIElement</key> <key>LSUIElement</key>
<string>1</string> <string>1</string>
<key>ATSApplicationFontsPath</key>
<string></string>
</dict> </dict>
</plist> </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 Foundation
import Cocoa import Cocoa
// add CGFloat constructor from String
extension CGFloat { extension CGFloat {
// add CGFloat constructor from String
init?(_ string: String) { init?(_ string: String) {
guard let number = NumberFormatter().number(from: string) else { guard let number = NumberFormatter().number(from: string) else {
return nil return nil
@@ -11,8 +11,8 @@ extension CGFloat {
} }
} }
// add throw-on-nil method on Optional
extension Optional { extension Optional {
// add throw-on-nil method on Optional
func orThrow() throws -> Wrapped { func orThrow() throws -> Wrapped {
switch self { switch self {
case .some(let value): case .some(let value):
@@ -24,8 +24,8 @@ extension Optional {
} }
} }
// add String constructor from CGFloat that round up at 1 decimal
extension String { extension String {
// add String constructor from CGFloat that round up at 1 decimal
init?(_ cgFloat: CGFloat) { init?(_ cgFloat: CGFloat) {
let formatter = NumberFormatter() let formatter = NumberFormatter()
formatter.maximumFractionDigits = 1 formatter.maximumFractionDigits = 1
@@ -36,8 +36,8 @@ extension String {
} }
} }
// add recursive lookup in subviews for specific type
extension NSView { extension NSView {
// add recursive lookup in subviews for specific type
func findNestedViews<T: NSView>(subclassOf: T.Type) -> [T] { func findNestedViews<T: NSView>(subclassOf: T.Type) -> [T] {
return recursiveSubviews.compactMap { $0 as? T } return recursiveSubviews.compactMap { $0 as? T }
} }
@@ -47,8 +47,8 @@ extension NSView {
} }
} }
// add convenience to NSError
extension NSError { extension NSError {
// add convenience to NSError
class func make(domain: String, message: String, code: Int = 9999) -> NSError { class func make(domain: String, message: String, code: Int = 9999) -> NSError {
return NSError( return NSError(
domain: domain, 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 import Carbon.HIToolbox.Events
class Keyboard { class Keyboard {
static let backgroundQueue = DispatchQueue(label: "uiQueue", qos: .userInteractive, autoreleaseFrequency: .never)
static func listenToGlobalEvents(_ delegate: Application) { static func listenToGlobalEvents(_ delegate: Application) {
listenToGlobalKeyboardEvents(delegate) listenToGlobalKeyboardEvents(delegate)
} }
@@ -10,17 +12,16 @@ class Keyboard {
var eventTap: CFMachPort? var eventTap: CFMachPort?
func listenToGlobalKeyboardEvents(_ delegate: Application) { 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) }) 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( eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap, tap: .cgSessionEventTap,
place: .headInsertEventTap, place: .headInsertEventTap,
options: .defaultTap, options: .defaultTap,
eventsOfInterest: eventMask, eventsOfInterest: eventMask,
callback: { (_, _, event, delegate_) -> Unmanaged<CGEvent>? in callback: keyboardHandler,
let d = Unmanaged<Application>.fromOpaque(delegate_!).takeUnretainedValue()
return keyboardHandler(event, d)
},
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(delegate).toOpaque())) userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(delegate).toOpaque()))
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
@@ -29,42 +30,43 @@ func listenToGlobalKeyboardEvents(_ delegate: Application) {
} }
} }
func keyboardHandler(_ cgEvent: CGEvent, _ delegate: Application) -> Unmanaged<CGEvent>? { func dispatchWork(_ application: Application, _ uiWorkShouldBeDone: Bool, _ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
if cgEvent.type == .keyDown || cgEvent.type == .keyUp || cgEvent.type == .flagsChanged { application.uiWorkShouldBeDone = uiWorkShouldBeDone
if let event = NSEvent(cgEvent: cgEvent) { DispatchQueue.main.async {
let keyDown = event.type == .keyDown 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 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 isRightArrow = event.keyCode == kVK_RightArrow
let isLeftArrow = event.keyCode == kVK_LeftArrow let isLeftArrow = event.keyCode == kVK_LeftArrow
let isEscape = event.keyCode == kVK_Escape let isEscape = event.keyCode == kVK_Escape
if event.modifierFlags.contains(Preferences.metaModifierFlag!) { if isMetaDown && type == .keyDown {
if keyDown { if isTab && event.modifierFlags.contains(.shift) {
if isTab && event.modifierFlags.contains(.shift) { return dispatchWork(application, true, { application.showUiOrCycleSelection(-1) })
delegate.showUiOrSelectPrevious() } else if isTab {
return nil // previously focused app should not receive keys return dispatchWork(application, true, { application.showUiOrCycleSelection(1) })
} else if isTab { } else if isRightArrow && application.appIsBeingUsed {
delegate.showUiOrSelectNext() return dispatchWork(application, true, { application.cycleSelection(1) })
return nil // previously focused app should not receive keys } else if isLeftArrow && application.appIsBeingUsed {
} else if isRightArrow && delegate.appIsBeingUsed { return dispatchWork(application, true, { application.cycleSelection(-1) })
delegate.cycleSelection(1) } else if type == .keyDown && isEscape {
return nil // previously focused app should not receive keys return dispatchWork(application, false, { application.hideUi() })
} 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
}
} }
} else if isMeta && !keyDown { } else if isMetaChanged && !isMetaDown {
delegate.focusTarget() return dispatchWork(application, false, { application.focusTarget() })
return nil // previously focused app should not receive keys
} }
} }
} else if cgEvent.type == .tapDisabledByUserInput || cgEvent.type == .tapDisabledByTimeout { } else if type == .tapDisabledByUserInput || type == .tapDisabledByTimeout {
CGEvent.tapEnable(tap: eventTap!, enable: true) CGEvent.tapEnable(tap: eventTap!, enable: true)
} }
// focused app will receive the event // 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 Cocoa
import Carbon.HIToolbox.Events import Carbon.HIToolbox.Events
enum ShowOnScreenPreference {
case MAIN
case MOUSE
}
class Preferences { class Preferences {
static var defaults: [String: String] = [ static var defaults: [String: String] = [
"maxScreenUsage": "80", "maxScreenUsage": "80",
@@ -20,12 +15,15 @@ class Preferences {
"showOnScreen": showOnScreenMacro.macros[0].label "showOnScreen": showOnScreenMacro.macros[0].label
] ]
static var rawValues = [String: String]() static var rawValues = [String: String]()
static var minimumWindowSize: CGFloat = 200 static var minimumWindowSize = CGFloat(200)
static var fontColor: NSColor = .white static var emptyThumbnailWidth = CGFloat(200)
static var windowMaterial: NSVisualEffectView.Material = .dark static var emptyThumbnailHeight = CGFloat(emptyThumbnailWidth * 9 / 16)
static var windowPadding: CGFloat = 23 static var fontColor = NSColor.white
static var interItemPadding: CGFloat = 4 static var windowMaterial = NSVisualEffectView.Material.dark
static var cellPadding: CGFloat = 6 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 cellBorderWidth: CGFloat?
static var cellCornerRadius: CGFloat? static var cellCornerRadius: CGFloat?
static var maxScreenUsage: 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 import Cocoa
class Screen { class Screen {
static func preferredScreen() -> NSScreen { static func preferred() -> NSScreen {
switch Preferences.showOnScreen! { switch Preferences.showOnScreen! {
case .MOUSE: case .MOUSE:
return screenWithMouse() ?? NSScreen.main!; // .main as fall-back return withMouse() ?? NSScreen.main!; // .main as fall-back
case .MAIN: case .MAIN:
return NSScreen.main!; return NSScreen.main!;
} }
} }
private static func screenWithMouse() -> NSScreen? { private static func withMouse() -> NSScreen? {
return NSScreen.screens.first { NSMouseInRect(NSEvent.mouseLocation, $0.frame, false) } return NSScreen.screens.first { NSMouseInRect(NSEvent.mouseLocation, $0.frame, false) }
} }
@@ -36,6 +36,14 @@ class Screen {
panel.makeKeyAndOrderFront(nil) panel.makeKeyAndOrderFront(nil)
Application.shared.arrangeInFront(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 { 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 Foundation
import Cocoa 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 { class SystemPermissions {
// macOS 10.9+
static func ensureAccessibilityCheckboxIsChecked() { static func ensureAccessibilityCheckboxIsChecked() {
if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) { if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) {
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Accessibility.", 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() { static func ensureScreenRecordingCheckboxIsChecked() {
let firstWindow = CoreGraphicsApis.windows()[0] let firstWindow = CGWindow.windows(.optionOnScreenOnly)[0]
let windowNumber = CoreGraphicsApis.value(firstWindow, kCGWindowNumber, UInt32(0)) if let cgId = firstWindow.value(.number, CGWindowID.self), cgId.screenshot() == nil {
if CoreGraphicsApis.image(windowNumber) == nil {
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Screen Recording.", debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Screen Recording.",
"Please authorize and re-launch.", "Please authorize and re-launch.",
"See https://dropshare.zendesk.com/hc/en-us/articles/360033453434-Enabling-Screen-Recording-Permission-on-macOS-Catalina-10-15-", "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 Foundation
import Cocoa import Cocoa
let cgsMainConnectionId = CGSMainConnectionID()
class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate { class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
static let name = "AltTab" static let name = "AltTab"
var statusItem: NSStatusItem? var statusItem: NSStatusItem?
var thumbnailsPanel: ThumbnailsPanel? var thumbnailsPanel: ThumbnailsPanel?
var preferencesPanel: PreferencesPanel? var preferencesPanel: PreferencesPanel?
var selectedOpenWindow: Int = 0 var uiWorkShouldBeDone = true
var numberOfColumns: Int = 0 var isFirstSummon = true
var openWindows: [OpenWindow] = [] var appIsBeingUsed = false
var workItems: [DispatchWorkItem] = []
var isFirstSummon: Bool = true
var appIsBeingUsed: Bool = false
override init() { override init() {
super.init() super.init()
@@ -29,6 +28,12 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
statusItem = StatusItem.make(self) statusItem = StatusItem.make(self)
initPreferencesDependentComponents() initPreferencesDependentComponents()
Keyboard.listenToGlobalEvents(self) 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 // 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) thumbnailsPanel = ThumbnailsPanel(self)
} }
func showUiOrSelectNext() {
debugPrint("showUiOrSelectNext")
showUiOrCycleSelection(1)
}
func showUiOrSelectPrevious() {
debugPrint("showUiOrSelectPrevious")
showUiOrCycleSelection(-1)
}
func hideUi() { func hideUi() {
debugPrint("hideUi") debugPrint("hideUi")
DispatchQueue.main.async(execute: { self.thumbnailsPanel!.orderOut(nil) }) thumbnailsPanel!.orderOut(nil)
appIsBeingUsed = false appIsBeingUsed = false
isFirstSummon = true isFirstSummon = true
} }
@@ -56,8 +51,8 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
func focusTarget() { func focusTarget() {
debugPrint("focusTarget") debugPrint("focusTarget")
if appIsBeingUsed { if appIsBeingUsed {
focusSelectedWindow(currentlySelectedWindow()) debugPrint("focusTarget: appIsBeingUsed")
hideUi() focusSelectedWindow(TrackedWindows.focusedWindow())
} }
} }
@@ -66,69 +61,36 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
if preferencesPanel == nil { if preferencesPanel == nil {
preferencesPanel = PreferencesPanel() preferencesPanel = PreferencesPanel()
} }
Screen.showPanel(preferencesPanel!, Screen.preferredScreen(), .appleCentered) Screen.showPanel(preferencesPanel!, Screen.preferred(), .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
} }
func cycleSelection(_ step: Int) { func cycleSelection(_ step: Int) {
selectedOpenWindow = cellWithStep(step) TrackedWindows.focusedWindowIndex = TrackedWindows.moveFocusedWindowIndex(step)
DispatchQueue.main.async(execute: { self.thumbnailsPanel!.highlightCellAt(step) }) self.thumbnailsPanel!.highlightCellAt(step)
} }
func showUiOrCycleSelection(_ step: Int) { func showUiOrCycleSelection(_ step: Int) {
debugPrint("showUiOrCycleSelection", step)
appIsBeingUsed = true appIsBeingUsed = true
if isFirstSummon { if isFirstSummon {
debugPrint("showUiOrCycleSelection: isFirstSummon")
isFirstSummon = false isFirstSummon = false
selectedOpenWindow = 0 TrackedWindows.refreshList(step)
computeOpenWindows() if TrackedWindows.list.count == 0 {
if openWindows.count <= 0 {
return return
} }
selectedOpenWindow = cellWithStep(step) let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
var workItem: DispatchWorkItem! if uiWorkShouldBeDone { self.thumbnailsPanel!.computeThumbnails(currentScreen); debugPrint("computeThumbnails") }
workItem = DispatchWorkItem { if uiWorkShouldBeDone { self.thumbnailsPanel!.highlightCellAt(step); debugPrint("highlightCellAt") }
let currentScreen = Screen.preferredScreen() // fix screen between steps since it could change (e.g. mouse moved to another screen) if uiWorkShouldBeDone { Screen.showPanel(self.thumbnailsPanel!, currentScreen, .appleCentered); debugPrint("showPanel") }
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)
} else { } else {
debugPrint("showUiOrCycleSelection: !isFirstSummon")
cycleSelection(step) cycleSelection(step)
} }
} }
func focusSelectedWindow(_ window: OpenWindow?) { func focusSelectedWindow(_ window: TrackedWindow?) {
workItems.forEach({ $0.cancel() }) hideUi()
workItems.removeAll() DispatchQueue.global(qos: .userInteractive).async { window?.focus() }
window?.focus()
} }
func currentlySelectedWindow() -> OpenWindow? {
return openWindows.count > selectedOpenWindow ? openWindows[selectedOpenWindow] : nil
}
} }
+39 -14
View File
@@ -1,13 +1,16 @@
import Cocoa import Cocoa
import WebKit
typealias MouseDownCallback = (OpenWindow) -> Void typealias MouseDownCallback = (TrackedWindow) -> Void
typealias MouseMovedCallback = (Cell) -> Void typealias MouseMovedCallback = (Cell) -> Void
class Cell: NSCollectionViewItem { class Cell: NSCollectionViewItem {
var thumbnail = NSImageView() var thumbnail = NSImageView()
var icon = NSImageView() var appIcon = NSImageView()
var label = CellTitle() var label = CellTitle(Preferences.fontHeight!)
var openWindow: OpenWindow? var minimizedIcon = FontIcon(FontIcon.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white)
var spaceIcon = FontIcon(FontIcon.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
var openWindow: TrackedWindow?
var mouseDownCallback: MouseDownCallback? var mouseDownCallback: MouseDownCallback?
var mouseMovedCallback: MouseMovedCallback? var mouseMovedCallback: MouseMovedCallback?
@@ -16,7 +19,7 @@ class Cell: NSCollectionViewItem {
let vStackView = makeVStackView(hStackView) let vStackView = makeVStackView(hStackView)
let shadow = Cell.makeShadow(.gray) let shadow = Cell.makeShadow(.gray)
thumbnail.shadow = shadow thumbnail.shadow = shadow
icon.shadow = shadow appIcon.shadow = shadow
view = vStackView 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 openWindow = element
thumbnail.image = element.thumbnail thumbnail.image = element.thumbnail
let (width, height) = computeDownscaledSize(element.thumbnail, screen) let (width, height) = Cell.computeDownscaledSize(element.thumbnail, screen)
thumbnail.image!.size = NSSize(width: width, height: height) thumbnail.image?.size = NSSize(width: width, height: height)
thumbnail.frame.size = NSSize(width: width, height: height) thumbnail.frame.size = NSSize(width: width, height: height)
icon.image = element.icon appIcon.image = element.icon
icon.image?.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!) appIcon.image?.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
icon.frame.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!) appIcon.frame.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
label.string = element.cgTitle label.string = element.title
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug) // workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
label.font = Preferences.font! 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.mouseDownCallback = mouseDownCallback
self.mouseMovedCallback = mouseMovedCallback self.mouseMovedCallback = mouseMovedCallback
if view.trackingAreas.count > 0 { 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)) 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 { static func makeShadow(_ color: NSColor) -> NSShadow {
let shadow = NSShadow() let shadow = NSShadow()
shadow.shadowColor = color shadow.shadowColor = color
@@ -67,8 +90,10 @@ class Cell: NSCollectionViewItem {
private func makeHStackView() -> NSStackView { private func makeHStackView() -> NSStackView {
let hStackView = NSStackView() let hStackView = NSStackView()
hStackView.spacing = Preferences.interItemPadding hStackView.spacing = Preferences.interItemPadding
hStackView.addView(icon, in: .leading) hStackView.addView(appIcon, in: .leading)
hStackView.addView(label, in: .leading) hStackView.addView(label, in: .leading)
hStackView.addView(minimizedIcon, in: .leading)
hStackView.addView(spaceIcon, in: .leading)
return hStackView 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) { init(_ text: String) {
super.init(frame: .zero) super.init(frame: .zero)
_init()
heightAnchor.constraint(greaterThanOrEqualToConstant: Preferences.fontHeight! + Preferences.interItemPadding).isActive = true
string = text string = text
} }
@@ -28,11 +26,14 @@ class BaseLabel: NSTextView {
} }
class CellTitle: BaseLabel { class CellTitle: BaseLabel {
let magicOffset: CGFloat
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
init() { init(_ size: CGFloat, _ magicOffset: CGFloat = 0) {
self.magicOffset = magicOffset
let textStorage = NSTextStorage() let textStorage = NSTextStorage()
let layoutManager = NSLayoutManager() let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager) textStorage.addLayoutManager(layoutManager)
@@ -43,15 +44,15 @@ class CellTitle: BaseLabel {
super.init(frame: .zero, textContainer: textContainer) super.init(frame: .zero, textContainer: textContainer)
textColor = Preferences.fontColor textColor = Preferences.fontColor
shadow = Cell.makeShadow(.darkGray) shadow = Cell.makeShadow(.darkGray)
defaultParagraphStyle = makeParagraphStyle() defaultParagraphStyle = makeParagraphStyle(size)
heightAnchor.constraint(equalToConstant: Preferences.fontHeight!).isActive = true heightAnchor.constraint(equalToConstant: size + magicOffset).isActive = true
} }
private func makeParagraphStyle() -> NSMutableParagraphStyle { private func makeParagraphStyle(_ size: CGFloat) -> NSMutableParagraphStyle {
let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.lineBreakMode = .byTruncatingTail paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.maximumLineHeight = Preferences.fontHeight! paragraphStyle.maximumLineHeight = size + magicOffset
paragraphStyle.minimumLineHeight = Preferences.fontHeight! paragraphStyle.minimumLineHeight = size + magicOffset
paragraphStyle.allowsDefaultTighteningForTruncation = false paragraphStyle.allowsDefaultTighteningForTruncation = false
return paragraphStyle return paragraphStyle
} }
-5
View File
@@ -18,11 +18,6 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
contentView = makeContentView() contentView = makeContentView()
} }
override func close() {
(NSApp as! Application).preferencesPanel = nil
super.close()
}
public func windowShouldClose(_ sender: NSWindow) -> Bool { public func windowShouldClose(_ sender: NSWindow) -> Bool {
windowCloseRequested = true windowCloseRequested = true
challengeNextInvalidEditableTextField() challengeNextInvalidEditableTextField()
+12 -13
View File
@@ -24,6 +24,8 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
collectionView_ = makeCollectionView() collectionView_ = makeCollectionView()
backgroundView = makeBackgroundView() backgroundView = makeBackgroundView()
contentView!.addSubview(backgroundView!) contentView!.addSubview(backgroundView!)
// highest level possible; this allows the app to go on top of context menus
level = .screenSaver
} }
private func makeBackgroundView() -> NSVisualEffectView { private func makeBackgroundView() -> NSVisualEffectView {
@@ -51,44 +53,41 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
func makeLayout() -> CollectionViewCenterFlowLayout { func makeLayout() -> CollectionViewCenterFlowLayout {
let layout = CollectionViewCenterFlowLayout() let layout = CollectionViewCenterFlowLayout()
layout.estimatedItemSize = NSSize(width: 200, height: 200) layout.estimatedItemSize = NSSize(width: Preferences.emptyThumbnailWidth, height: Preferences.emptyThumbnailHeight)
layout.minimumInteritemSpacing = 5 layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 5 layout.minimumLineSpacing = 5
return layout return layout
} }
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
// debugPrint("collectionView: count items", application!.openWindows.count) return TrackedWindows.list.count
return application!.openWindows.count
} }
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
// debugPrint("collectionView: make item", indexPath.item)
let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell 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 return item
} }
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
// debugPrint("collectionView: item size") if indexPath.item < TrackedWindows.list.count {
if indexPath.item < application!.openWindows.count { let (width, height) = Cell.computeDownscaledSize(TrackedWindows.list[indexPath.item].thumbnail, currentScreen!)
let (width, height) = computeDownscaledSize(application!.openWindows[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 NSSize(width: CGFloat(width) + Preferences.cellPadding * 2, height: CGFloat(height) + max(Preferences.fontHeight!, Preferences.iconSize!) + Preferences.interItemPadding + Preferences.cellPadding * 2)
} }
return .zero return .zero
} }
func highlightCellAt(_ step: Int) { func highlightCellAt(_ step: Int) {
collectionView_!.selectItems(at: [IndexPath(item: application!.selectedOpenWindow, section: 0)], scrollPosition: .top) collectionView_!.selectItems(at: [IndexPath(item: TrackedWindows.focusedWindowIndex, section: 0)], scrollPosition: .top)
collectionView_!.deselectItems(at: [IndexPath(item: application!.cellWithStep(-step), section: 0)]) collectionView_!.deselectItems(at: [IndexPath(item: TrackedWindows.moveFocusedWindowIndex(-step), section: 0)])
} }
func highlightCell(_ cell: Cell) { func highlightCell(_ cell: Cell) {
let newIndex = collectionView_.indexPath(for: cell)! let newIndex = collectionView_.indexPath(for: cell)!
if application!.selectedOpenWindow != newIndex.item { if TrackedWindows.focusedWindowIndex != newIndex.item {
collectionView_!.selectItems(at: [newIndex], scrollPosition: .top) collectionView_!.selectItems(at: [newIndex], scrollPosition: .top)
collectionView_!.deselectItems(at: [IndexPath(item: application!.selectedOpenWindow, section: 0)]) collectionView_!.deselectItems(at: [IndexPath(item: TrackedWindows.focusedWindowIndex, section: 0)])
application!.selectedOpenWindow = newIndex.item TrackedWindows.focusedWindowIndex = newIndex.item
} }
} }