diff --git a/alt-tab-macos.xcodeproj/project.pbxproj b/alt-tab-macos.xcodeproj/project.pbxproj index 317cd4538..b3b8c13ba 100644 --- a/alt-tab-macos.xcodeproj/project.pbxproj +++ b/alt-tab-macos.xcodeproj/project.pbxproj @@ -7,27 +7,32 @@ 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 /* CoreGraphicsApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA78E3B4E73B40DB77174 /* CoreGraphicsApis.swift */; }; D04BA20D4A240843293B3B52 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56355579F78776E6D51 /* Cell.swift */; }; - D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */; }; + D04BA278D9EFA568C8D18A4C /* OpenWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* OpenWindow.swift */; }; + D04BA308162F8043F8561D03 /* AccessibilityApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA40A4291E4F310527DBF /* AccessibilityApis.swift */; }; D04BA3261C7DA5F48310E654 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA90C6C36DB1D65BC2B66 /* Application.swift */; }; + D04BA374F668C84491E1A64A /* PreferredApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA6B2B9BA0A84866157FC /* PreferredApis.swift */; }; D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA35456DA0DDA74F9687E /* Keyboard.swift */; }; D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA02F476DE30C4647886C /* PreferencesPanel.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 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8F1AA48A323EE5638DC /* Extensions.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 = ""; }; D04BA02F476DE30C4647886C /* PreferencesPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPanel.swift; sourceTree = ""; }; D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = ""; }; D04BA0CE87BE264C52987ED1 /* 7 windows - 2 lines - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - wide window.jpg"; sourceTree = ""; }; @@ -43,6 +48,7 @@ D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = ""; }; D04BA35456DA0DDA74F9687E /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; + D04BA40A4291E4F310527DBF /* AccessibilityApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityApis.swift; sourceTree = ""; }; D04BA4336B6004A0A99849AD /* package.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = package.json; sourceTree = ""; }; D04BA459034C1885CA43A807 /* LICENCE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENCE.md; sourceTree = ""; }; D04BA4B5292629AA6B560216 /* package_release.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = package_release.sh; sourceTree = ""; }; @@ -50,21 +56,23 @@ D04BA51D43775E57CE91154A /* 3 windows - 1 line - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - wide window.jpg"; sourceTree = ""; }; D04BA56355579F78776E6D51 /* Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; D04BA5ABFA5457A86536E2E4 /* 5 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 1 line.jpg"; sourceTree = ""; }; - D04BA7AC3D9153353CE0A76D /* CoreGraphicsApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreGraphicsApis.swift; sourceTree = ""; }; + D04BA6B2B9BA0A84866157FC /* PreferredApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferredApis.swift; sourceTree = ""; }; + D04BA78E3B4E73B40DB77174 /* CoreGraphicsApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreGraphicsApis.swift; sourceTree = ""; }; D04BA7B6AAB0812631BBC7A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; D04BA7ECCE728582D9ECA613 /* determine_version.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version.sh; sourceTree = ""; }; 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 = ""; }; + D04BA8F1AA48A323EE5638DC /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; D04BA90C6C36DB1D65BC2B66 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; D04BA92541D46EA4F6943A72 /* package-lock.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "package-lock.json"; sourceTree = ""; }; D04BA9EF65B2E7AF9E3ADCA3 /* 2 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2 windows - 1 line.jpg"; sourceTree = ""; }; D04BAA34E0CB00DED7C04B4F /* 2-rows.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2-rows.jpg"; sourceTree = ""; }; D04BAA44C837F3A67403B9DB /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; D04BAB6652494D7575057E86 /* 14 windows - 3 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "14 windows - 3 lines.jpg"; sourceTree = ""; }; + D04BABC654F40BE74DA25BC7 /* SF-Pro-Text-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "SF-Pro-Text-Regular.otf"; sourceTree = ""; }; D04BAC02D60EF22D9CC7D969 /* commitlint.config.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = commitlint.config.js; sourceTree = ""; }; D04BAC159731F80FDAF4EA6C /* 1-row.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1-row.jpg"; sourceTree = ""; }; D04BAC6AFC7F06D1A567F27A /* set_version_in_app.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = set_version_in_app.sh; sourceTree = ""; }; - D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; + D04BAD1BED44EAEB77FED8A4 /* OpenWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenWindow.swift; sourceTree = ""; }; D04BAD1C9F215BCCD3B620AC /* alt_tab_macos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = alt_tab_macos.entitlements; sourceTree = ""; }; D04BAD32E130E4A061DC8332 /* Labels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Labels.swift; sourceTree = ""; }; D04BAD40CE2D3A8AAC3819D0 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = file.gitignore; path = .gitignore; sourceTree = ""; }; @@ -74,10 +82,11 @@ D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; D04BAE1243C9B4BE3ED1B524 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = ""; }; D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = ""; }; - D04BAE965ED779513FDEA0CD /* AccessibilityApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityApis.swift; sourceTree = ""; }; + D04BAED53465957807CBF8B2 /* FontIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontIcon.swift; sourceTree = ""; }; D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = ""; }; D04BAF249324297C07E31164 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = ""; }; D04BAFA277EAE3BDDDB61110 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + D04BAFFC95D3A5BE76E3E653 /* PrivateApisBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivateApisBridge.h; sourceTree = ""; }; F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = ""; }; F0298E42A818112B290FF6C7 /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -87,12 +96,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 = ""; + }; D04BA1463D2A17038222BB84 = { isa = PBXGroup; children = ( @@ -110,6 +128,7 @@ D04BA703DCD38D9757093312 /* ci */, D04BA459034C1885CA43A807 /* LICENCE.md */, D04BA2C9EF33A646D0977195 /* .github */, + 4807A6C423A9CD190052A53E /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +140,17 @@ name = Products; sourceTree = ""; }; + D04BA22D2CA2755FA5902C34 /* api-wrappers */ = { + isa = PBXGroup; + children = ( + D04BA40A4291E4F310527DBF /* AccessibilityApis.swift */, + D04BA78E3B4E73B40DB77174 /* CoreGraphicsApis.swift */, + D04BA8F1AA48A323EE5638DC /* Extensions.swift */, + D04BA6B2B9BA0A84866157FC /* PreferredApis.swift */, + ); + path = "api-wrappers"; + sourceTree = ""; + }; D04BA2C9EF33A646D0977195 /* .github */ = { isa = PBXGroup; children = ( @@ -150,6 +180,14 @@ path = "windows-10"; sourceTree = ""; }; + D04BA5A0E9C82F7579CD2B78 /* resources */ = { + isa = PBXGroup; + children = ( + D04BABC654F40BE74DA25BC7 /* SF-Pro-Text-Regular.otf */, + ); + path = resources; + sourceTree = ""; + }; D04BA63877FC8FB11C43C3D2 /* alt-tab-macos */ = { isa = PBXGroup; children = ( @@ -175,13 +213,10 @@ isa = PBXGroup; children = ( D04BA35456DA0DDA74F9687E /* Keyboard.swift */, - D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */, + D04BAD1BED44EAEB77FED8A4 /* OpenWindow.swift */, D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */, D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */, D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */, - D04BA86768C6503A11ED81FC /* Extensions.swift */, - D04BAE965ED779513FDEA0CD /* AccessibilityApis.swift */, - D04BA7AC3D9153353CE0A76D /* CoreGraphicsApis.swift */, ); path = logic; sourceTree = ""; @@ -198,6 +233,7 @@ D04BAD32E130E4A061DC8332 /* Labels.swift */, F0298E42A818112B290FF6C7 /* TextField.swift */, F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */, + D04BAED53465957807CBF8B2 /* FontIcon.swift */, ); path = ui; sourceTree = ""; @@ -227,6 +263,9 @@ D04BA7B6AAB0812631BBC7A2 /* Info.plist */, D04BAA44C837F3A67403B9DB /* main.swift */, D04BAA1C553891551B903DA7 /* logic */, + D04BAFFC95D3A5BE76E3E653 /* PrivateApisBridge.h */, + D04BA22D2CA2755FA5902C34 /* api-wrappers */, + D04BA5A0E9C82F7579CD2B78 /* resources */, ); path = "alt-tab-macos"; sourceTree = ""; @@ -290,6 +329,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D04BA8373D4DE452C0C081ED /* SF-Pro-Text-Regular.otf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -304,7 +344,7 @@ D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */, D04BA20D4A240843293B3B52 /* Cell.swift in Sources */, D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */, - D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */, + D04BA278D9EFA568C8D18A4C /* OpenWindow.swift in Sources */, D04BA3261C7DA5F48310E654 /* Application.swift in Sources */, D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */, D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */, @@ -312,12 +352,14 @@ 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 /* AccessibilityApis.swift in Sources */, + D04BA0496ACF1427B6E9D369 /* CoreGraphicsApis.swift in Sources */, + D04BAE369A14C3126A1606FE /* Extensions.swift in Sources */, + D04BA374F668C84491E1A64A /* PreferredApis.swift in Sources */, + D04BAE2E8E9B9898A4DF9B3B /* FontIcon.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -330,11 +372,13 @@ 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; PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "alt-tab-macos/PrivateApisBridge.h"; SWIFT_VERSION = 4.2; }; name = Release; @@ -345,11 +389,13 @@ 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; PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "alt-tab-macos/PrivateApisBridge.h"; SWIFT_VERSION = 4.2; }; name = Debug; diff --git a/alt-tab-macos/Info.plist b/alt-tab-macos/Info.plist index f07df1631..dbdf52ee4 100644 --- a/alt-tab-macos/Info.plist +++ b/alt-tab-macos/Info.plist @@ -28,5 +28,7 @@ NSApplication LSUIElement 1 + ATSApplicationFontsPath + diff --git a/alt-tab-macos/PrivateApisBridge.h b/alt-tab-macos/PrivateApisBridge.h new file mode 100644 index 000000000..414fea8ae --- /dev/null +++ b/alt-tab-macos/PrivateApisBridge.h @@ -0,0 +1,81 @@ +// In this .h file, we define symbols of private APIs so we can use these APIs in the project +// see Webkit repo: https://github.com/WebKit/webkit/blob/master/Source/WebCore/PAL/pal/spi/cg/CoreGraphicsSPI.h +// see Hammerspoon issue: https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468 +// see Alt-tab-macos issue: https://github.com/lwouis/alt-tab-macos/pull/87#issuecomment-558624755 + +#import +#import + +typedef uint32_t CGSConnectionID; +typedef uint32_t CGSWindowID; +typedef uint32_t CGSWindowCount; +typedef uint32_t CGSWindowCaptureOptions; +enum { + kCGSWindowCaptureNominalResolution = 0x0200, + kCGSCaptureIgnoreGlobalClipShape = 0x0800, +}; + +extern CGSConnectionID CGSMainConnectionID(void); + +// 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 +// * minimized windows: the function can return screenshots of minimized windows +// * windows in other spaces: ? +extern CFArrayRef CGSHWCaptureWindowList(CGSConnectionID connectionId, CGSWindowID *windowList, CGSWindowCount windowCount, CGSWindowCaptureOptions options); + +// returns the CGImage of the window which ID is given in `wid` +// * performance: it seems that this function performs similarly to public API `CGWindowListCreateImage` +// * minimized windows: the function can return screenshots of minimized windows +// * windows in other spaces: ? +extern CGError CGSCaptureWindowsContentsToRectWithOptions(CGSConnectionID connectionId, CGSWindowID *windowId, bool windowOnly, CGRect rect, CGSWindowCaptureOptions options, CGImageRef *image); + +// returns the size 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: ? +extern CGError SLSGetWindowBounds(CGSConnectionID connectionId, CGSWindowID *windowId, CGRect *frame); + +typedef uint32_t SLPSMode; +enum { + kCPSAllWindows = 0x100, + kCPSUserGenerated = 0x200, + kCPSNoWindows = 0x400, +}; + +// focuses a window +// * performance: faster than AXUIElementPerformAction(kAXRaiseAction) +// * minimized windows: yes +// * windows in other spaces: ? +extern CGError _SLPSSetFrontProcessWithOptions(ProcessSerialNumber *psn, CGSWindowID windowId, SLPSMode mode); + +extern CGError SLSGetWindowOwner(CGSConnectionID connectionId, CGSWindowID windowId, CGSConnectionID *windowConnectionId); + +extern CGError SLSGetConnectionPSN(CGSConnectionID connectionId, ProcessSerialNumber *psn); + +extern CGError SLPSPostEventRecordTo(ProcessSerialNumber *psn, uint8_t *bytes); + +// The following function was taken from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468 +static void window_manager_make_key_window(ProcessSerialNumber *psn, uint32_t windowId) { + // the information specified in the events below consists of the "special" category, event type, and modifiers, + // basically synthesizing a mouse-down and up event targetted at a specific window of the application, + // but it doesn't actually get treated as a mouse-click normally would. + uint8_t bytes1[0xf8] = { + [0x04] = 0xF8, + [0x08] = 0x01, + [0x3a] = 0x10 + }; + uint8_t bytes2[0xf8] = { + [0x04] = 0xF8, + [0x08] = 0x02, + [0x3a] = 0x10 + }; + memcpy(bytes1 + 0x3c, &windowId, sizeof(uint32_t)); + memset(bytes1 + 0x20, 0xFF, 0x10); + memcpy(bytes2 + 0x3c, &windowId, sizeof(uint32_t)); + memset(bytes2 + 0x20, 0xFF, 0x10); + SLPSPostEventRecordTo(psn, bytes1); + SLPSPostEventRecordTo(psn, bytes2); +} + +// returns the window ID of the provided AXUIElement +extern AXError _AXUIElementGetWindow(AXUIElementRef axUiElement, uint32_t *windowId); diff --git a/alt-tab-macos/logic/AccessibilityApis.swift b/alt-tab-macos/api-wrappers/AccessibilityApis.swift similarity index 62% rename from alt-tab-macos/logic/AccessibilityApis.swift rename to alt-tab-macos/api-wrappers/AccessibilityApis.swift index 3aa4b02f6..2d37a6c03 100644 --- a/alt-tab-macos/logic/AccessibilityApis.swift +++ b/alt-tab-macos/api-wrappers/AccessibilityApis.swift @@ -3,15 +3,17 @@ 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 [] + return attribute(AXUIElementCreateApplication(cgOwnerPid), kAXWindowsAttribute, [AXUIElement].self) ?? [] + } + + static func windowThatMatchCgWindow(_ ownerPid: pid_t, _ cgId: CGWindowID) -> AXUIElement? { + return AccessibilityApis.windows(ownerPid).first(where: { return windowId($0) == cgId }) + } + + private static func windowId(_ window: AXUIElement) -> CGWindowID { + var id = UInt32(0) + _AXUIElementGetWindow(window, &id) + return id } static func rect(_ element: AXUIElement) -> CGRect { @@ -29,7 +31,7 @@ class AccessibilityApis { AXUIElementSetAttributeValue(element, attribute as CFString, AXValueCreate(type, &v)!) } - private static func value(_ element: AXUIElement, _ key: String, _ target: T, _ type: AXValueType) -> T? { + static func value(_ element: AXUIElement, _ key: String, _ target: T, _ type: AXValueType) -> T? { if let a = attribute(element, key, AXValue.self) { var value = target AXValueGetValue(a, type, &value) @@ -38,7 +40,7 @@ class AccessibilityApis { return nil } - private static func attribute(_ element: AXUIElement, _ key: String, _ type: T.Type) -> T? { + static func attribute(_ 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 { diff --git a/alt-tab-macos/api-wrappers/CoreGraphicsApis.swift b/alt-tab-macos/api-wrappers/CoreGraphicsApis.swift new file mode 100644 index 000000000..deab88ba5 --- /dev/null +++ b/alt-tab-macos/api-wrappers/CoreGraphicsApis.swift @@ -0,0 +1,28 @@ +import Cocoa +import Foundation + +class CoreGraphicsApis { + static func windows(_ option: CGWindowListOption) -> [NSDictionary] { + return (CGWindowListCopyWindowInfo([.excludeDesktopElements, option], kCGNullWindowID) as! [NSDictionary]) + .filter { return windowIsNotMenubarOrOthers($0) && windowIsReasonablyBig($0) } + } + + // workaround: filtering this criteria seems to remove non-windows UI elements + private static func windowIsNotMenubarOrOthers(_ window: NSDictionary) -> Bool { + return value(window, kCGWindowLayer, Int(0)) == 0 + } + + // workaround: some apps like chrome use a window to implement the search popover + private static func windowIsReasonablyBig(_ window: NSDictionary) -> Bool { + let windowBounds = CGRect(dictionaryRepresentation: value(window, kCGWindowBounds, [:] as CFDictionary))! + return windowBounds.width > Preferences.minimumWindowSize && windowBounds.height > Preferences.minimumWindowSize + } + + static func value(_ 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]) + } +} diff --git a/alt-tab-macos/logic/Extensions.swift b/alt-tab-macos/api-wrappers/Extensions.swift similarity index 100% rename from alt-tab-macos/logic/Extensions.swift rename to alt-tab-macos/api-wrappers/Extensions.swift diff --git a/alt-tab-macos/api-wrappers/PreferredApis.swift b/alt-tab-macos/api-wrappers/PreferredApis.swift new file mode 100644 index 000000000..795a9a4fd --- /dev/null +++ b/alt-tab-macos/api-wrappers/PreferredApis.swift @@ -0,0 +1,70 @@ +import Cocoa +import Foundation + +enum WindowScreenshotApi { + case CGWindowListCreateImage + case CGSHWCaptureWindowList + case CGSCaptureWindowsContentsToRectWithOptions +} + +enum WindowDimensionsApi { + case AXValueGetValue + case SLSGetWindowBounds +} + +enum FocusWindowApi { + case AXUIElementPerformAction + case _SLPSSetFrontProcessWithOptions +} + +let cgsMainConnectionId = CGSMainConnectionID() + +// This class wraps different public and private APIs that achieve similar functionality. +// It lets the user pick the API as a parameter, and thus the level of service they want +class PreferredApis { + static func windowScreenshot(_ windowId: CGSWindowID, _ api: WindowScreenshotApi) -> CGImage { + switch api { + case .CGWindowListCreateImage: + return CGWindowListCreateImage(.null, .optionIncludingWindow, windowId, [.boundsIgnoreFraming, .bestResolution])! + case .CGSHWCaptureWindowList: + var windowId_ = windowId + let options = CGSWindowCaptureOptions(kCGSCaptureIgnoreGlobalClipShape | kCGSWindowCaptureNominalResolution) + return (CGSHWCaptureWindowList(cgsMainConnectionId, &windowId_, 1, options)!.takeRetainedValue() as! Array).first! + case .CGSCaptureWindowsContentsToRectWithOptions: + var windowId_ = windowId + var windowImage: Unmanaged? + CGSCaptureWindowsContentsToRectWithOptions(cgsMainConnectionId, &windowId_, true, .zero, (1 << 8), &windowImage) + return windowImage!.takeRetainedValue() + } + } + + static func windowDimensions(_ windowId: CGSWindowID?, _ axUiElement: AXUIElement?, _ api: WindowDimensionsApi) -> CGSize { + switch api { + case .AXValueGetValue: + return AccessibilityApis.value(axUiElement!, kAXSizeAttribute, CGSize(), .cgSize)! + case .SLSGetWindowBounds: + var windowId_ = windowId! + var frame = CGRect() + SLSGetWindowBounds(cgsMainConnectionId, &windowId_, &frame); + return frame.size + } + } + + static func focusWindow(_ axUiElement: AXUIElement, _ windowId: CGSWindowID?, _ ownerPid: Int32?, _ api: FocusWindowApi) -> Void { + DispatchQueue.global(qos: .userInteractive).async { + switch api { + case .AXUIElementPerformAction: + NSRunningApplication(processIdentifier: ownerPid!)?.activate(options: [.activateIgnoringOtherApps]) + AccessibilityApis.focus(axUiElement) + case ._SLPSSetFrontProcessWithOptions: + var elementConnection = UInt32.zero + SLSGetWindowOwner(cgsMainConnectionId, windowId!, &elementConnection) + var psn = ProcessSerialNumber() + SLSGetConnectionPSN(elementConnection, &psn) + _SLPSSetFrontProcessWithOptions(&psn, windowId!, SLPSMode(kCPSUserGenerated)) + window_manager_make_key_window(&psn, windowId!) + AccessibilityApis.focus(axUiElement) + } + } + } +} diff --git a/alt-tab-macos/logic/CoreGraphicsApis.swift b/alt-tab-macos/logic/CoreGraphicsApis.swift deleted file mode 100644 index 7d4eb5fb5..000000000 --- a/alt-tab-macos/logic/CoreGraphicsApis.swift +++ /dev/null @@ -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(_ 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]) - } -} diff --git a/alt-tab-macos/logic/Keyboard.swift b/alt-tab-macos/logic/Keyboard.swift index fff39efaf..e78faf34e 100644 --- a/alt-tab-macos/logic/Keyboard.swift +++ b/alt-tab-macos/logic/Keyboard.swift @@ -17,10 +17,7 @@ func listenToGlobalKeyboardEvents(_ delegate: Application) { place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: eventMask, - callback: { (_, _, event, delegate_) -> Unmanaged? in - let d = Unmanaged.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 +26,44 @@ func listenToGlobalKeyboardEvents(_ delegate: Application) { } } -func keyboardHandler(_ cgEvent: CGEvent, _ delegate: Application) -> Unmanaged? { - if cgEvent.type == .keyDown || cgEvent.type == .keyUp || cgEvent.type == .flagsChanged { - if let event = NSEvent(cgEvent: cgEvent) { +func consumeEvent(_ fn: @escaping () -> Void) -> Unmanaged? { + // run app logic on main thread + DispatchQueue.main.async { + fn() + } + // previously focused app should not receive keys + return nil +} + +func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent, delegate_: UnsafeMutableRawPointer?) -> Unmanaged? { + let delegate = Unmanaged.fromOpaque(delegate_!).takeUnretainedValue() + if type == .keyDown || type == .keyUp || type == .flagsChanged { + if let event = NSEvent(cgEvent: event_) { let keyDown = event.type == .keyDown let isTab = event.keyCode == Preferences.tabKeyCode let isMeta = Preferences.metaKeyCodes!.contains(event.keyCode) 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 event.modifierFlags.contains(Preferences.metaModifierFlag!) && keyDown { + if isTab && event.modifierFlags.contains(.shift) { + return consumeEvent { delegate.showUiOrSelectPrevious() } + } else if isTab { + return consumeEvent { delegate.showUiOrSelectNext() } + } else if isRightArrow && delegate.appIsBeingUsed { + return consumeEvent { delegate.cycleSelection(1) } + } else if isLeftArrow && delegate.appIsBeingUsed { + return consumeEvent { delegate.cycleSelection(-1) } + } else if keyDown && isEscape { + return consumeEvent { delegate.hideUi() } } } else if isMeta && !keyDown { - delegate.focusTarget() - return nil // previously focused app should not receive keys + return consumeEvent { delegate.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_) } diff --git a/alt-tab-macos/logic/OpenWindow.swift b/alt-tab-macos/logic/OpenWindow.swift new file mode 100644 index 000000000..86862d6b4 --- /dev/null +++ b/alt-tab-macos/logic/OpenWindow.swift @@ -0,0 +1,40 @@ +import Cocoa +import Foundation + +class OpenWindow { + var cgWindow: NSDictionary + var ownerPid: Int32 + var cgId: CGWindowID + var cgTitle: String + var cgRect: CGRect + var thumbnail: NSImage? + var icon: NSImage? + var app: NSRunningApplication? + var axWindow: AXUIElement? + var isMinimized: Bool + + init(_ cgWindow: NSDictionary, _ cgId: CGWindowID, _ isMinimized: Bool, _ axWindow: AXUIElement?) { + self.cgWindow = cgWindow + self.cgId = cgId + self.ownerPid = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerPID, Int32(0)) + let cgTitle = CoreGraphicsApis.value(cgWindow, kCGWindowName, "") + let cgOwnerName = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerName, "") + self.cgTitle = cgTitle.isEmpty ? cgOwnerName : cgTitle + self.app = NSRunningApplication(processIdentifier: ownerPid) + self.icon = self.app?.icon + self.cgRect = CGRect(dictionaryRepresentation: cgWindow[kCGWindowBounds] as! NSDictionary)! + let cgImage = PreferredApis.windowScreenshot(cgId, .CGSHWCaptureWindowList) + self.thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) + self.axWindow = axWindow + self.isMinimized = isMinimized + } + + func focus() { + if axWindow == nil { + axWindow = AccessibilityApis.windowThatMatchCgWindow(ownerPid, cgId) + } + if axWindow != nil { + PreferredApis.focusWindow(axWindow!, cgId, nil, ._SLPSSetFrontProcessWithOptions) + } + } +} diff --git a/alt-tab-macos/logic/Preferences.swift b/alt-tab-macos/logic/Preferences.swift index 7fd7b87a7..411b97e08 100644 --- a/alt-tab-macos/logic/Preferences.swift +++ b/alt-tab-macos/logic/Preferences.swift @@ -25,6 +25,7 @@ class Preferences { static var windowMaterial: NSVisualEffectView.Material = .dark static var windowPadding: CGFloat = 23 static var interItemPadding: CGFloat = 4 + static var minimizedIconSize: CGFloat = 15 static var cellPadding: CGFloat = 6 static var cellBorderWidth: CGFloat? static var cellCornerRadius: CGFloat? diff --git a/alt-tab-macos/logic/SystemPermissions.swift b/alt-tab-macos/logic/SystemPermissions.swift index 55c8a2801..9f536f52f 100644 --- a/alt-tab-macos/logic/SystemPermissions.swift +++ b/alt-tab-macos/logic/SystemPermissions.swift @@ -13,7 +13,7 @@ class SystemPermissions { } static func ensureScreenRecordingCheckboxIsChecked() { - let firstWindow = CoreGraphicsApis.windows()[0] + let firstWindow = CoreGraphicsApis.windows(.optionOnScreenOnly)[0] let windowNumber = CoreGraphicsApis.value(firstWindow, kCGWindowNumber, UInt32(0)) if CoreGraphicsApis.image(windowNumber) == nil { debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Screen Recording.", diff --git a/alt-tab-macos/logic/WindowManager.swift b/alt-tab-macos/logic/WindowManager.swift deleted file mode 100644 index 0ab6897af..000000000 --- a/alt-tab-macos/logic/WindowManager.swift +++ /dev/null @@ -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))) - } -} diff --git a/alt-tab-macos/resources/SF-Pro-Text-Regular.otf b/alt-tab-macos/resources/SF-Pro-Text-Regular.otf new file mode 100644 index 000000000..3f23f8fa3 Binary files /dev/null and b/alt-tab-macos/resources/SF-Pro-Text-Regular.otf differ diff --git a/alt-tab-macos/ui/Application.swift b/alt-tab-macos/ui/Application.swift index 6405cf7b0..7a50a7290 100644 --- a/alt-tab-macos/ui/Application.swift +++ b/alt-tab-macos/ui/Application.swift @@ -48,7 +48,7 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate { func hideUi() { debugPrint("hideUi") - DispatchQueue.main.async(execute: { self.thumbnailsPanel!.orderOut(nil) }) + self.thumbnailsPanel!.orderOut(nil) appIsBeingUsed = false isFirstSummon = true } @@ -71,19 +71,27 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate { 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() { + // first pass: get all visible windows, in recently-used order + computeOpenWindows_(.optionOnScreenOnly) + // second pass: get all minimized windows, in fixed order + computeOpenWindows_(.optionAll) + } + + private func computeOpenWindows_(_ option: CGWindowListOption) { + for cgWindow in CoreGraphicsApis.windows(option) { 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)) + if option == .optionOnScreenOnly { + openWindows.append(OpenWindow(cgWindow, cgId, false, nil)) + } else { + // not already there from the visible-windows first pass + if openWindows.first(where: { $0.cgId == cgId }) == nil { + let ownerPid = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerPID, Int32(0)) + if let axWindow = AccessibilityApis.windowThatMatchCgWindow(ownerPid, cgId) { + if AccessibilityApis.attribute(axWindow, kAXMinimizedAttribute, Bool.self)! { + openWindows.append(OpenWindow(cgWindow, cgId, true, axWindow)) + } + } + } } } } @@ -94,7 +102,7 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate { func cycleSelection(_ step: Int) { selectedOpenWindow = cellWithStep(step) - DispatchQueue.main.async(execute: { self.thumbnailsPanel!.highlightCellAt(step) }) + self.thumbnailsPanel!.highlightCellAt(step) } func showUiOrCycleSelection(_ step: Int) { diff --git a/alt-tab-macos/ui/Cell.swift b/alt-tab-macos/ui/Cell.swift index 754e656be..5286f55e6 100644 --- a/alt-tab-macos/ui/Cell.swift +++ b/alt-tab-macos/ui/Cell.swift @@ -1,12 +1,14 @@ import Cocoa +import WebKit typealias MouseDownCallback = (OpenWindow) -> Void typealias MouseMovedCallback = (Cell) -> Void class Cell: NSCollectionViewItem { var thumbnail = NSImageView() - var icon = NSImageView() + var appIcon = NSImageView() var label = CellTitle() + var minimizedIcon = FontIcon("􀁎", Preferences.minimizedIconSize, .white) var openWindow: OpenWindow? var mouseDownCallback: MouseDownCallback? var mouseMovedCallback: MouseMovedCallback? @@ -16,7 +18,7 @@ class Cell: NSCollectionViewItem { let vStackView = makeVStackView(hStackView) let shadow = Cell.makeShadow(.gray) thumbnail.shadow = shadow - icon.shadow = shadow + appIcon.shadow = shadow view = vStackView } @@ -38,16 +40,22 @@ class Cell: NSCollectionViewItem { func updateWithNewContent(_ element: OpenWindow, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) { openWindow = element 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.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!) + 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.cgTitle // 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 + if openWindow!.isMinimized { + minimizedIcon.isHidden = false + label.textContainer!.size.width -= Preferences.minimizedIconSize + Preferences.interItemPadding + } else { + minimizedIcon.isHidden = true + } self.mouseDownCallback = mouseDownCallback self.mouseMovedCallback = mouseMovedCallback if view.trackingAreas.count > 0 { @@ -56,6 +64,17 @@ 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) { + 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))) + } + } + static func makeShadow(_ color: NSColor) -> NSShadow { let shadow = NSShadow() shadow.shadowColor = color @@ -67,8 +86,9 @@ 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) return hStackView } diff --git a/alt-tab-macos/ui/FontIcon.swift b/alt-tab-macos/ui/FontIcon.swift new file mode 100644 index 000000000..a9906de29 --- /dev/null +++ b/alt-tab-macos/ui/FontIcon.swift @@ -0,0 +1,20 @@ +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 { + 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(4) + string = text + font = NSFont(name: "SF Pro Text", size: size) + textColor = color + heightAnchor.constraint(equalToConstant: size + magicOffset).isActive = true + // This helps SF symbols not be clipped on the right + widthAnchor.constraint(equalToConstant: size * 1.15).isActive = true + } +} diff --git a/alt-tab-macos/ui/Labels.swift b/alt-tab-macos/ui/Labels.swift index 2263d8b5d..632466f85 100644 --- a/alt-tab-macos/ui/Labels.swift +++ b/alt-tab-macos/ui/Labels.swift @@ -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(_ magicOffset: CGFloat = 0) { + self.magicOffset = magicOffset let textStorage = NSTextStorage() let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) @@ -44,14 +45,14 @@ class CellTitle: BaseLabel { textColor = Preferences.fontColor shadow = Cell.makeShadow(.darkGray) defaultParagraphStyle = makeParagraphStyle() - heightAnchor.constraint(equalToConstant: Preferences.fontHeight!).isActive = true + heightAnchor.constraint(equalToConstant: Preferences.fontHeight! + magicOffset).isActive = true } private func makeParagraphStyle() -> NSMutableParagraphStyle { let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle paragraphStyle.lineBreakMode = .byTruncatingTail - paragraphStyle.maximumLineHeight = Preferences.fontHeight! - paragraphStyle.minimumLineHeight = Preferences.fontHeight! + paragraphStyle.maximumLineHeight = Preferences.fontHeight! + magicOffset + paragraphStyle.minimumLineHeight = Preferences.fontHeight! + magicOffset paragraphStyle.allowsDefaultTighteningForTruncation = false return paragraphStyle } diff --git a/alt-tab-macos/ui/ThumbnailsPanel.swift b/alt-tab-macos/ui/ThumbnailsPanel.swift index f04ca4c63..b3a6f9421 100644 --- a/alt-tab-macos/ui/ThumbnailsPanel.swift +++ b/alt-tab-macos/ui/ThumbnailsPanel.swift @@ -72,7 +72,7 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele 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!) + let (width, height) = Cell.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 .zero