Compare commits

...
Author SHA1 Message Date
lwouis ad7000fc33 fix: improve performance when many windows are open (closes #5177) 2026-02-02 21:30:51 +01:00
lwouis 08cf150bf8 fix for 10.13 2026-01-31 21:54:57 +01:00
lwouis 7d03c26e9a feat: update cs/el/fr/ru localizations 2026-01-31 21:54:47 +01:00
lwouis 21aeafd8dd fix: quitting apps by pressing q multiple times (closes #5247) 2026-01-31 18:42:45 +01:00
lwouis ab704dc4be fix: app could sometimes crash in the background
closes #5260

appcenter showed that access to debugId() from main and non-main threa could crash the app. Making debugId a string that's set once at init simplifies everything and allows concurrent access from threads
2026-01-31 18:42:42 +01:00
lwouis 42c0a03ddc docs: improve website title 2026-01-25 22:34:53 +01:00
semantic-release-bot 550529f1fa chore(release): 8.3.3 [skip ci]
## [8.3.3](https://github.com/lwouis/alt-tab-macos/compare/v8.3.2...v8.3.3) (2026-01-25)

### Bug Fixes

* app icons could sometimes be invisible (closes [#5226](https://github.com/lwouis/alt-tab-macos/issues/5226)) ([23fbc01](https://github.com/lwouis/alt-tab-macos/commit/23fbc01))
* improve trackpad gesture detection (closes [#5203](https://github.com/lwouis/alt-tab-macos/issues/5203)) ([9b5e43e](https://github.com/lwouis/alt-tab-macos/commit/9b5e43e))
* switcher could sometimes show with no window pre-selected ([3e95f77](https://github.com/lwouis/alt-tab-macos/commit/3e95f77))
* title could be off-center in app-icons appearance (closes [#5235](https://github.com/lwouis/alt-tab-macos/issues/5235)) ([7fdfe4a](https://github.com/lwouis/alt-tab-macos/commit/7fdfe4a))
2026-01-25 21:24:33 +00:00
lwouis 23fbc01a0e fix: app icons could sometimes be invisible (closes #5226) 2026-01-25 22:19:02 +01:00
lwouis 56546571b6 docs: add frequently asked questions on website 2026-01-24 20:12:45 +01:00
lwouis e5c634e672 refactor: simplify v-center logic
no need for the complex bias thing. We round in the end. And up or down each have downsides, so let's pick the default without doing extra logic
2026-01-24 14:07:38 +01:00
lwouis 7fdfe4a9ee fix: title could be off-center in app-icons appearance (closes #5235) 2026-01-24 13:39:25 +01:00
lwouis 3e95f77cc0 fix: switcher could sometimes show with no window pre-selected 2026-01-24 13:39:24 +01:00
lwouis 9b5e43eec5 fix: improve trackpad gesture detection (closes #5203) 2026-01-23 18:27:42 +01:00
lwouis ac3c3740c2 chore: improve log 2026-01-23 18:18:27 +01:00
semantic-release-bot 35a67ddf16 chore(release): 8.3.2 [skip ci]
## [8.3.2](https://github.com/lwouis/alt-tab-macos/compare/v8.3.1...v8.3.2) (2026-01-22)

### Bug Fixes

* blacklisting fullscreen windows was not working (closes [#5228](https://github.com/lwouis/alt-tab-macos/issues/5228)) ([53815e0](https://github.com/lwouis/alt-tab-macos/commit/53815e0))
2026-01-22 23:34:13 +00:00
lwouis 53815e0f28 fix: blacklisting fullscreen windows was not working (closes #5228) 2026-01-23 00:24:53 +01:00
semantic-release-bot 7b7a73bd46 chore(release): 8.3.1 [skip ci]
## [8.3.1](https://github.com/lwouis/alt-tab-macos/compare/v8.3.0...v8.3.1) (2026-01-22)

### Bug Fixes

* chrome apps windows would not show in the switcher (closes [#5227](https://github.com/lwouis/alt-tab-macos/issues/5227)) ([bf3fa67](https://github.com/lwouis/alt-tab-macos/commit/bf3fa67))
2026-01-22 21:47:45 +00:00
lwouis bf3fa6734e fix: chrome apps windows would not show in the switcher (closes #5227) 2026-01-22 22:28:46 +01:00
semantic-release-bot be06aef6d7 chore(release): 8.3.0 [skip ci]
# [8.3.0](https://github.com/lwouis/alt-tab-macos/compare/v8.2.0...v8.3.0) (2026-01-20)

### Bug Fixes

* position of windowless indicator in thumbnails appearance ([d1fd1fd](https://github.com/lwouis/alt-tab-macos/commit/d1fd1fd))
* small memory leak when opening the switcher (closes [#4255](https://github.com/lwouis/alt-tab-macos/issues/4255)) ([0a6485e](https://github.com/lwouis/alt-tab-macos/commit/0a6485e))
* windows would sometimes not update their order (closes [#5207](https://github.com/lwouis/alt-tab-macos/issues/5207)) ([a3f2a54](https://github.com/lwouis/alt-tab-macos/commit/a3f2a54))

### Features

* increase apparition delay granularity (closes [#5210](https://github.com/lwouis/alt-tab-macos/issues/5210)) ([5b0df6b](https://github.com/lwouis/alt-tab-macos/commit/5b0df6b))
2026-01-20 17:59:06 +00:00
lwouis 3e27abd1e8 chore: improve log levels 2026-01-19 19:35:15 +01:00
lwouis 0a6485e024 fix: small memory leak when opening the switcher (closes #4255) 2026-01-19 19:35:14 +01:00
lwouis 5b0df6bd4c feat: increase apparition delay granularity (closes #5210) 2026-01-19 19:35:14 +01:00
lwouis a3f2a54f20 fix: windows would sometimes not update their order (closes #5207) 2026-01-19 19:35:13 +01:00
lwouis d1fd1fd9d0 fix: position of windowless indicator in thumbnails appearance 2026-01-17 00:06:10 +01:00
lwouis cd5fb46963 chore: silence build warnings from pods 2026-01-15 19:21:32 +01:00
semantic-release-bot 09ebff06ee chore(release): 8.2.0 [skip ci]
# [8.2.0](https://github.com/lwouis/alt-tab-macos/compare/v8.1.0...v8.2.0) (2026-01-15)

### Bug Fixes

* thumbnails could sometimes be stretched (closes [#5204](https://github.com/lwouis/alt-tab-macos/issues/5204)) ([ac24d6d](https://github.com/lwouis/alt-tab-macos/commit/ac24d6d))

### Features

* stop checking screen-recording permission after launch ([#5202](https://github.com/lwouis/alt-tab-macos/issues/5202)) ([f481d22](https://github.com/lwouis/alt-tab-macos/commit/f481d22))
2026-01-15 18:02:05 +00:00
lwouis ac24d6d148 fix: thumbnails could sometimes be stretched (closes #5204) 2026-01-15 18:01:08 +01:00
lwouis f481d22b75 feat: stop checking screen-recording permission after launch (#5202)
closes #5202

the permission check was expensive for the os, leading to higher energy usage, thus more battery drain
2026-01-15 15:47:33 +01:00
lwouis 149e3a5026 docs: improve changelog design 2026-01-15 14:42:42 +01:00
lwouis ef83525e9a docs: improve website on mobile 2026-01-15 14:42:41 +01:00
semantic-release-bot 56a042716e chore(release): 8.1.0 [skip ci]
# [8.1.0](https://github.com/lwouis/alt-tab-macos/compare/v8.0.0...v8.1.0) (2026-01-14)

### Bug Fixes

* allow f-keys as shortcuts-when-active (closes [#5166](https://github.com/lwouis/alt-tab-macos/issues/5166)) ([a0c9fee](https://github.com/lwouis/alt-tab-macos/commit/a0c9fee))
* avoid unintentional trackpad trigger (closes [#4278](https://github.com/lwouis/alt-tab-macos/issues/4278)) ([0539a3a](https://github.com/lwouis/alt-tab-macos/commit/0539a3a))
* better handle palm/fingers resting on the trackpad (closes [#5191](https://github.com/lwouis/alt-tab-macos/issues/5191)) ([9ed8a19](https://github.com/lwouis/alt-tab-macos/commit/9ed8a19))
* mitigate macos 15 bugs with screencapture-kit (closes [#5190](https://github.com/lwouis/alt-tab-macos/issues/5190)) ([b2f5c6c](https://github.com/lwouis/alt-tab-macos/commit/b2f5c6c))
* mouse hover detection for hidden/windowless apps ([7c64864](https://github.com/lwouis/alt-tab-macos/commit/7c64864))
* thumbnails could sometimes be the wrong size ([9d0b947](https://github.com/lwouis/alt-tab-macos/commit/9d0b947))

### Features

* add belarusian localization ([0fe722b](https://github.com/lwouis/alt-tab-macos/commit/0fe722b))
* improve accidental cursor movement ([74b5a11](https://github.com/lwouis/alt-tab-macos/commit/74b5a11)), closes [/github.com/lwouis/alt-tab-macos/issues/4278#issuecomment-3733280737](https://github.com//github.com/lwouis/alt-tab-macos/issues/4278/issues/issuecomment-3733280737)
* improve pl localization ([3b53ce7](https://github.com/lwouis/alt-tab-macos/commit/3b53ce7))
* show outline of windows hidden for privacy reasons (closes [#5172](https://github.com/lwouis/alt-tab-macos/issues/5172)) ([1090acf](https://github.com/lwouis/alt-tab-macos/commit/1090acf))
2026-01-14 23:19:39 +00:00
lwouis 0fe722bfba feat: add belarusian localization 2026-01-15 00:08:36 +01:00
lwouis 3b53ce77bb feat: improve pl localization 2026-01-15 00:08:36 +01:00
lwouis b2f5c6c4d5 fix: mitigate macos 15 bugs with screencapture-kit (closes #5190) 2026-01-15 00:08:35 +01:00
emptiestplace 7c64864c2f fix: mouse hover detection for hidden/windowless apps 2026-01-15 00:08:35 +01:00
lwouis 9d0b947a48 fix: thumbnails could sometimes be the wrong size
* fixes issues when the user has monitors with different DPI
* fixes issues where screenshots after a window is resized could be wrong (because of cached SCWindow having stale frame.size)
2026-01-15 00:08:34 +01:00
lwouis 9ed8a199b0 fix: better handle palm/fingers resting on the trackpad (closes #5191) 2026-01-15 00:08:34 +01:00
lwouis a0c9feefca fix: allow f-keys as shortcuts-when-active (closes #5166) 2026-01-15 00:08:33 +01:00
lwouis 1a2822d130 docs: website redesign 2026-01-14 00:09:51 +01:00
lwouis 44007195b7 docs: improve website and readme 2026-01-12 17:15:25 +01:00
lwouis 74b5a119d3 feat: improve accidental cursor movement
see https://github.com/lwouis/alt-tab-macos/issues/4278#issuecomment-3733280737
2026-01-11 14:55:26 +01:00
lwouis 0539a3ad49 fix: avoid unintentional trackpad trigger (closes #4278) 2026-01-11 14:55:26 +01:00
lwouis a06afd4bd2 refactor: remove unneeded views; simplify views 2026-01-11 14:55:25 +01:00
lwouis 1090acf3a5 feat: show outline of windows hidden for privacy reasons (closes #5172) 2026-01-07 19:16:14 +01:00
semantic-release-bot b8ba0f0888 chore(release): 8.0.0 [skip ci]
# [8.0.0](https://github.com/lwouis/alt-tab-macos/compare/v7.39.0...v8.0.0) (2026-01-06)

### Bug Fixes

* avoid macos bug where many permission dialogs pop (closes [#5106](https://github.com/lwouis/alt-tab-macos/issues/5106)) ([5c4199e](https://github.com/lwouis/alt-tab-macos/commit/5c4199e))

### Features

* improve memory image of showing app icons (closes [#5144](https://github.com/lwouis/alt-tab-macos/issues/5144)) ([bf58335](https://github.com/lwouis/alt-tab-macos/commit/bf58335))
* improve ta localization ([989b344](https://github.com/lwouis/alt-tab-macos/commit/989b344))
* improve thumbnails with screen-capture kit ([7821d7c](https://github.com/lwouis/alt-tab-macos/commit/7821d7c)), closes [#4255](https://github.com/lwouis/alt-tab-macos/issues/4255) [#3652](https://github.com/lwouis/alt-tab-macos/issues/3652)

### Performance Improvements

* debounce screen/space events to minimize work ([e2b4779](https://github.com/lwouis/alt-tab-macos/commit/e2b4779))

### BREAKING CHANGES

* Better thumbnails with ScreenCaptureKit for users on macOS >= 14
2026-01-06 18:36:28 +00:00
lwouis 989b344b1d feat: improve ta localization 2026-01-06 19:27:16 +01:00
lwouis bf58335bbe feat: improve memory image of showing app icons (closes #5144) 2026-01-05 23:12:36 +01:00
lwouis 5c4199ec74 fix: avoid macos bug where many permission dialogs pop (closes #5106) 2026-01-04 22:53:54 +01:00
lwouis 7821d7c979 feat: improve thumbnails with screen-capture kit
BREAKING CHANGE: Better thumbnails with ScreenCaptureKit for users on macOS >= 14

* No more WindowServer memory leaks (closes #4255)
* No more purple UI from macOS bothering users
* Fullscreen thumbnails contain top part (closes #3652)
* Thumbnails contain dialogs (e.g. Save/Print panel)
* Show thumbnails for off-screen windows
* Memory usage drastically reduced (e.g. don't save full-resolution if Preview feature is not used)
* Limitations of ScreenCaptureKit compared to private API: can't capture minimized/hidden/tabbed windows
2026-01-04 22:49:41 +01:00
lwouis e2b47790a9 perf: debounce screen/space events to minimize work 2026-01-04 14:18:17 +01:00
lwouis ff66a0664d chore: reduce link font size 2026-01-04 12:08:44 +01:00
semantic-release-bot 6d42bef7ec chore(release): 7.39.0 [skip ci]
# [7.39.0](https://github.com/lwouis/alt-tab-macos/compare/v7.38.1...v7.39.0) (2026-01-03)

### Bug Fixes

* avoid showing closed windows (closes [#3589](https://github.com/lwouis/alt-tab-macos/issues/3589)) ([894a69d](https://github.com/lwouis/alt-tab-macos/commit/894a69d)), closes [#4924](https://github.com/lwouis/alt-tab-macos/issues/4924)
* permission checks might freeze or restart the app (closes [#5112](https://github.com/lwouis/alt-tab-macos/issues/5112)) ([7906d63](https://github.com/lwouis/alt-tab-macos/commit/7906d63))
* permission window could steal focus (see [#3577](https://github.com/lwouis/alt-tab-macos/issues/3577)) ([2f875b3](https://github.com/lwouis/alt-tab-macos/commit/2f875b3))
* permissions window could look wrong in some languages like chinese ([914f91f](https://github.com/lwouis/alt-tab-macos/commit/914f91f)), closes [/github.com/lwouis/alt-tab-macos/issues/5112#issuecomment-3692769341](https://github.com//github.com/lwouis/alt-tab-macos/issues/5112/issues/issuecomment-3692769341)
* preview could sometimes be incorrectly positioned ([edc2f00](https://github.com/lwouis/alt-tab-macos/commit/edc2f00))
* show some hovering windows (e.g. zoom.app) (closes [#5140](https://github.com/lwouis/alt-tab-macos/issues/5140)) ([fcc6651](https://github.com/lwouis/alt-tab-macos/commit/fcc6651))
* thumbnails could sometimes have the wrong size ([55f3f59](https://github.com/lwouis/alt-tab-macos/commit/55f3f59))

### Features

* avoid occasional delays in showing the switcher (closes [#5109](https://github.com/lwouis/alt-tab-macos/issues/5109)) ([3c04a4c](https://github.com/lwouis/alt-tab-macos/commit/3c04a4c))
* improve nb/tr localizations ([5e842d5](https://github.com/lwouis/alt-tab-macos/commit/5e842d5))
* remove shadow around app icon appearance ([dda612c](https://github.com/lwouis/alt-tab-macos/commit/dda612c))

### Performance Improvements

* do login item and plist updates later to accelerate launch ([2f72c5d](https://github.com/lwouis/alt-tab-macos/commit/2f72c5d))
* don't compute diffs of running-apps; directly get new/old ([7ded01c](https://github.com/lwouis/alt-tab-macos/commit/7ded01c))
* improve mouse hover algo and core-animation side ([6103785](https://github.com/lwouis/alt-tab-macos/commit/6103785))
2026-01-03 18:23:00 +00:00
lwouis cca3423a59 ci: github action somehow misses 2 commits; adding more leeway 2026-01-03 19:18:44 +01:00
lwouis 5e842d5850 feat: improve nb/tr localizations 2026-01-03 19:18:43 +01:00
lwouis 0c724f3c7b refactor: various small improvements 2026-01-03 19:18:43 +01:00
lwouis 55f3f59208 fix: thumbnails could sometimes have the wrong size 2026-01-03 19:18:43 +01:00
lwouis edc2f00603 fix: preview could sometimes be incorrectly positioned 2026-01-03 19:18:42 +01:00
lwouis bcb130e7eb refactor: clarify names and organize tidier 2026-01-03 19:18:42 +01:00
lwouis d8de1e80b8 refactor: logs overall: lazy + reviewed them 1 by 1 for better dx 2026-01-03 19:18:41 +01:00
lwouis 610378525a perf: improve mouse hover algo and core-animation side 2026-01-03 18:09:19 +01:00
lwouis 2f72c5d6ae perf: do login item and plist updates later to accelerate launch 2026-01-03 18:09:19 +01:00
lwouis a62949f0dc refactor: add logs 2026-01-03 18:09:18 +01:00
lwouis 7ded01c277 perf: don't compute diffs of running-apps; directly get new/old 2026-01-03 18:09:18 +01:00
lwouis a45361321d chore: document why we check mouse events on main-thread 2026-01-03 18:09:17 +01:00
lwouis b7204f9694 refactor: repeatingKeyThread -> repeatingKeyQueue 2026-01-03 18:09:17 +01:00
lwouis b8a3b779e1 chore: document why we use kvo for processes diffing 2026-01-03 18:09:16 +01:00
lwouis 7906d630be fix: permission checks might freeze or restart the app (closes #5112) 2026-01-03 18:09:16 +01:00
lwouis 3c04a4ce15 feat: avoid occasional delays in showing the switcher (closes #5109) 2026-01-03 17:36:23 +01:00
lwouis 2f875b3bcb fix: permission window could steal focus (see #3577) 2025-12-28 18:47:02 +01:00
lwouis 914f91f9f7 fix: permissions window could look wrong in some languages like chinese
see https://github.com/lwouis/alt-tab-macos/issues/5112#issuecomment-3692769341
2025-12-28 18:47:01 +01:00
lwouis fcc6651b6a fix: show some hovering windows (e.g. zoom.app) (closes #5140) 2025-12-28 18:47:01 +01:00
lwouis dda612c15e feat: remove shadow around app icon appearance 2025-12-28 18:47:00 +01:00
110 changed files with 3764 additions and 2448 deletions

No files matched your search

+2 -1
View File
@@ -32,7 +32,7 @@ jobs:
COMMITS: ${{ toJson(github.event.commits) }}
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
COUNT=$(( $(echo "$COMMITS" | jq length) + 1 ))
COUNT=$(( $(echo "$COMMITS" | jq length) + 5 ))
else
COUNT=1
fi
@@ -57,6 +57,7 @@ jobs:
- run: scripts/package_and_notarize_release.sh
- run: scripts/upload_symbols_to_appcenter.sh
- run: scripts/update_appcast.sh
- run: scripts/update_readme_and_website.sh
- run: npx semantic-release
- id: set_tag_name
run: echo "tag_name=v"$(cat $VERSION_FILE)"" >> "$GITHUB_OUTPUT"
+3
View File
@@ -1,3 +1,6 @@
# disable warnings coming from pods, which are always noise from our perspective
inhibit_all_warnings!
def deployment_target_from_xcconfig()
xcconfig_path = 'config/base.xcconfig'
File.foreach(xcconfig_path) do |line|
+1 -1
View File
@@ -44,6 +44,6 @@ SPEC CHECKSUMS:
Sparkle: 1fa490bb4ab2a9b37cfb9ea4feec19dd33b81cd2
SwiftyBeaver: d39e7c376bf15e9fffca8263b8fd1f42ea56aa2d
PODFILE CHECKSUM: c3fc354fa5ccad08f1b9f7302205cc969b4b0ef0
PODFILE CHECKSUM: c20cb0e8753361ba1c1daea73624f879862271ed
COCOAPODS: 1.16.2
+1 -1
View File
@@ -44,6 +44,6 @@ SPEC CHECKSUMS:
Sparkle: 1fa490bb4ab2a9b37cfb9ea4feec19dd33b81cd2
SwiftyBeaver: d39e7c376bf15e9fffca8263b8fd1f42ea56aa2d
PODFILE CHECKSUM: c3fc354fa5ccad08f1b9f7302205cc969b4b0ef0
PODFILE CHECKSUM: c20cb0e8753361ba1c1daea73624f879862271ed
COCOAPODS: 1.16.2
+38 -38
View File
@@ -37,6 +37,7 @@
0765C83AC970A3DF92240522AA840B8D /* mk.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 0F6C871680F7ADB5F2FC20CC7503E1D8 /* mk.lproj */; };
09FFDD6CA7A2ABBF7B9FFFE19225E928 /* ru.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 7425B4F0B8D7AE68419113CF81BA0C7B /* ru.lproj */; };
0C31E740CC9DB225A896A552381ECE04 /* de.lproj in Resources */ = {isa = PBXBuildFile; fileRef = E78A0125E6667191F7F73CFBF20D66DF /* de.lproj */; };
13C9B11E8154877B29450AF358FB693F /* SRModifierFlagsTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34AFF6C2A3A41CF8FA59DE072FA9DEE9 /* SRModifierFlagsTransformer.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
19141E2D6ADC5ECB21C8117627E8996F /* sv.lproj in Resources */ = {isa = PBXBuildFile; fileRef = D30717BA0B03330C1EFCBE8ABC28853D /* sv.lproj */; };
1AFBAB297C62C0075822E90ABF72C2FB /* es.lproj in Resources */ = {isa = PBXBuildFile; fileRef = B23657FAF453892E8512C341C06F535A /* es.lproj */; };
1AFCEC66ACC98F5DBE56CC5EF1D977EE /* SRCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DF73A3728A24BBEB0E57795E54BF515 /* SRCommon.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -44,8 +45,6 @@
1D60678C50E742175FCDD7932A31EEDB /* AES256CBC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143CC11B54B607623E08EB6D8AC9CB2F /* AES256CBC.swift */; };
1DD93AEF2D487A349DB2564E5C7DEB0F /* sk.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 5CDACA1C9F549AD7106D831671F01405 /* sk.lproj */; };
1E4357F20077F9939C6D8CB274A0926A /* sk.lproj in Resources */ = {isa = PBXBuildFile; fileRef = F81CAA272C54ABD1FD722A84E2C57CDA /* sk.lproj */; };
1F149276AF739AC833BCADDCC1C83B73 /* SRShortcut.m in Sources */ = {isa = PBXBuildFile; fileRef = 908D3BC562CDE59ABCC3A47E25C395D0 /* SRShortcut.m */; };
22D0C539333677DC4DD8C357A2EE8074 /* SRModifierFlagsTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34AFF6C2A3A41CF8FA59DE072FA9DEE9 /* SRModifierFlagsTransformer.m */; };
24B9F193FBF42D709E80CEEC2C57038E /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586D767FDAC3D2ABE60B85F8C0D6C2F0 /* Extensions.swift */; };
258995C74CA2831F16B4059B139C361F /* zh_CN.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 63B668B3C596AA84093C7CCD7E13A74C /* zh_CN.lproj */; };
267D03ED297081CA66DBE1C422624F1D /* Pods-alt-tab-macos-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 616E070B17DAA048BA8AC1C696E23614 /* Pods-alt-tab-macos-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -55,28 +54,26 @@
2D35EBA08F0AFBB42E94C2C03474881E /* LetsMove-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7555526F49F9B017E9091D0D41EDE96F /* LetsMove-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
2F01365A2E8F2D3651C994BFA7A35849 /* nl.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 8A91DE636206786A97ADB7217687D550 /* nl.lproj */; };
323571A800C800D57D1D344EE6B4A076 /* LetsMove.h in Headers */ = {isa = PBXBuildFile; fileRef = 6CA84714B5B5E3373CEE2C55DED79A6C /* LetsMove.h */; settings = {ATTRIBUTES = (Project, ); }; };
343BFE43B781B05ED85224B009844203 /* SRShortcutValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = FBA566C5D6C5E62075BCD60635153279 /* SRShortcutValidator.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
35919AACC1E0D4C9C83E73D991F74EB6 /* GoogleCloudDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224927D3D690751292E77B2DA55FE5A /* GoogleCloudDestination.swift */; };
3ADF27FE54F59EC60116432676BA1937 /* SRShortcutFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 279E7D7B1123E54F78F297ECC787C32E /* SRShortcutFormatter.m */; };
3E030C055335A4F9895BB69099B86180 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BA604A31A1DD4CA42C4B531C2266FC9 /* Cocoa.framework */; };
474FFA8B855EC6CDC9168F3AD7467275 /* SRKeyCodeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = AE895BA41B15EEB276D5D22AA642B863 /* SRKeyCodeTransformer.m */; };
3EC20EE1C2D46CECA7FFDA0BE8CBA7DC /* SRShortcut.m in Sources */ = {isa = PBXBuildFile; fileRef = 908D3BC562CDE59ABCC3A47E25C395D0 /* SRShortcut.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
418DED74DF783AA497A3DA8BD8185C7F /* SRRecorderControlStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E6927C6D93BFE217C63FF9775CF2AD4 /* SRRecorderControlStyle.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
4AC61BCCEB37DAFC0A3FDCCCAFF5D908 /* ko.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 5FFFBEF12F4BC80B11118348EA112DC5 /* ko.lproj */; };
4C69AC49D9DED6174293A5D534AB2C86 /* el.lproj in Resources */ = {isa = PBXBuildFile; fileRef = F74E63D29FB691AA5276039BE623459B /* el.lproj */; };
4EA71F45A1D8B5523FD6C4F329E78E9D /* pl.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 3CE082BB2F892678911A89F5229D18BF /* pl.lproj */; };
521D5A75EF858C43B907C85DB81DF9D2 /* ru.lproj in Resources */ = {isa = PBXBuildFile; fileRef = F60833BA468BF0982A7757A0B4C7B6FA /* ru.lproj */; };
52266B6AB44FB51066899ADB78B40E98 /* SRModifierFlagsTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = D2AA7143845FAE5DA9AF2C73B7FA4935 /* SRModifierFlagsTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; };
56FD19D6AAFDA1359D7BEC1C08BF5A36 /* SRKeyCodeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = AE895BA41B15EEB276D5D22AA642B863 /* SRKeyCodeTransformer.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
574E2E99EDB4E5B0EF66E82D94307529 /* SRRecorderControl.h in Headers */ = {isa = PBXBuildFile; fileRef = AFD2D0990D11CBEC7920984F43CF4E47 /* SRRecorderControl.h */; settings = {ATTRIBUTES = (Public, ); }; };
59432AF7193CDF335E064696E90D0385 /* sr.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 9C5DA155F26AE9B5C6A9C2702A876C89 /* sr.lproj */; };
5A2989F6DBAC28DD784739513E3C8652 /* tr.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 1C1F51902DE7F002D230A8A831954814 /* tr.lproj */; };
5C79484B0FE7E3B5624E5269DF989261 /* SRShortcutValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = FBA566C5D6C5E62075BCD60635153279 /* SRShortcutValidator.m */; };
5D6E0B408CE31ADD19BED5F3500BF70C /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46D5BE035F0ABA1DC114CF2B9C7CE0F8 /* Carbon.framework */; };
5DE2ACCCCDBE0B5F9615B8A6D4884C99 /* ShortcutRecorder-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C77518D43E11C5EAE891E833481E5B16 /* ShortcutRecorder-dummy.m */; };
5ECC5662B52C59C90487CFEE4446BAC2 /* SRShortcutController.m in Sources */ = {isa = PBXBuildFile; fileRef = 442A770F956E37A443249CEA3A9210F7 /* SRShortcutController.m */; };
63A7D9C525A17174AB336A3C0A7BB20C /* pt.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 87A36116B795431CF9FC0EA190EEF84A /* pt.lproj */; };
67FFF76E5A537563D5953849AD7A5CA7 /* fr.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 7C08865CA7260B55C8CC908004086026 /* fr.lproj */; };
68D0E7A7A86B678B37F1756DA6EF5C91 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BA604A31A1DD4CA42C4B531C2266FC9 /* Cocoa.framework */; };
6EF965BFBEC5C5151F6642E2A12B62B9 /* SwiftyBeaver-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A6BC13B92452B467C398EABB021F3EC /* SwiftyBeaver-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
6FDA577C500F9FA1F3D63B2407CBEAE5 /* cs.lproj in Resources */ = {isa = PBXBuildFile; fileRef = CA601F9CF83636BE52B583599FA5209B /* cs.lproj */; };
74114B8B8983E90BDC9C85B231D00E40 /* SRKeyBindingTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 160A98F0E3604055FD8181545A88D923 /* SRKeyBindingTransformer.m */; };
744027C06DF5E758A00BAD444EE8C07E /* cs.lproj in Resources */ = {isa = PBXBuildFile; fileRef = B68B6A2B5480C2F24C2275C21DC7DDA8 /* cs.lproj */; };
746FCD6781093D2F95C59845CD7442E7 /* pt.lproj in Resources */ = {isa = PBXBuildFile; fileRef = D036B90B8FF6FF26C12246D02A9098BB /* pt.lproj */; };
7488632181922E3AA679BE0F3EDA29BD /* ko.lproj in Resources */ = {isa = PBXBuildFile; fileRef = E981E3BC91278FFE08924186992EF0D3 /* ko.lproj */; };
@@ -96,43 +93,46 @@
8F7F7232F41DDBD65894ED524B29866C /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8147D04AB10DF0A4759F373F4845F263 /* Filter.swift */; };
9074A4EFDC6E60EC0AFDDF30087D12AF /* ja.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 980E66E6BD817DEE7FACEC63303C3480 /* ja.lproj */; };
9632B9EDC92F450C8D59F640C7914E31 /* nb.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 97196A76CE2F7069C712AA7090F2C480 /* nb.lproj */; };
99BF20014D3DBC64D0D3D83C349C8635 /* SRKeyEquivalentModifierMaskTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB7EC660F798E3AAB637764C0D80F415 /* SRKeyEquivalentModifierMaskTransformer.m */; };
9A788410B4054A20EDA58C44A66CD231 /* pt_BR.lproj in Resources */ = {isa = PBXBuildFile; fileRef = E8E8B1EAA027DF908391EFA8F2BC95A3 /* pt_BR.lproj */; };
9C2FB5039E7EE9D8718A5A7BE69B0C73 /* SRShortcutAction.h in Headers */ = {isa = PBXBuildFile; fileRef = E6CD62F0F895E48DFEB9BFEC03F80A36 /* SRShortcutAction.h */; settings = {ATTRIBUTES = (Public, ); }; };
9D008D1F91FF0793D96DF23969F1C833 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = DBBE4379BC8B189957E290B799916676 /* it.lproj */; };
A7201CEEAA2E87A8AD27D46A4B641F9F /* ShortcutRecorder-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C77518D43E11C5EAE891E833481E5B16 /* ShortcutRecorder-dummy.m */; };
AAD8450861DBF14FBF5F6B50182C2009 /* FilterValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45791B624ED5DF1E5AB8B421356D1951 /* FilterValidator.swift */; };
B26825BD19B6771D0E83F6BEF2B63E4E /* SRRecorderControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 36A80746C85C0961ECD2D88522052778 /* SRRecorderControl.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
B2D04BB67E61139E7A776D62C9A11517 /* zh_TW.lproj in Resources */ = {isa = PBXBuildFile; fileRef = BB3B637A46A573FE010E685A71372472 /* zh_TW.lproj */; };
B3DEE2DF2FB4D54D798B2B700191B437 /* nb.lproj in Resources */ = {isa = PBXBuildFile; fileRef = BCEB7130F82E5D0B9B5C083745D98000 /* nb.lproj */; };
B8272B8C758AC4B34320429669CAD237 /* pl.lproj in Resources */ = {isa = PBXBuildFile; fileRef = ABB8A0EACBC6538EB722D52A448D0116 /* pl.lproj */; };
BB5D9FAD90FCE79DACC52E74E32ADFB2 /* SRShortcutAction.m in Sources */ = {isa = PBXBuildFile; fileRef = FDF5D98729528C41061C6F934C7C0046 /* SRShortcutAction.m */; };
BBE98B64AA4DA59E5D9B8B1D791BE4A2 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BA604A31A1DD4CA42C4B531C2266FC9 /* Cocoa.framework */; };
BC38EDEAA1B42FB0F78A453C7E0BD2AF /* SRRecorderControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 36A80746C85C0961ECD2D88522052778 /* SRRecorderControl.m */; };
BD49046DC2930FC0EECD106C89AF5ABF /* LetsMove-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C76BFF42A2A39F6CF4CD48868B2DD498 /* LetsMove-dummy.m */; };
BEA14B70BC975F119FCB2FBE4318B584 /* SRKeyCodeTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E73F446254094F81DB66CBA926FD579 /* SRKeyCodeTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; };
BFC163D8D2E3122425E885875D72BE33 /* SRShortcutController.m in Sources */ = {isa = PBXBuildFile; fileRef = 442A770F956E37A443249CEA3A9210F7 /* SRShortcutController.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
C0BA6ED3823D0D086EC874E635FB52D5 /* SRShortcutFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 49914EEFD471ABD74DAC3FFCC9000CCF /* SRShortcutFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; };
C12B5C5AF97E56A60349FBE6BBD539E3 /* Pods-unit-tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 30C8F24C8CB39D43FB35BD742199C674 /* Pods-unit-tests-dummy.m */; };
CB49894234FDAA4BFA548BB1B6E61815 /* ShortcutRecorder-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A06D0E4AF7999EC0D2E1CC2C8229546A /* ShortcutRecorder-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
CDCD6BC054C680BFE72F539C97B92091 /* Pods-alt-tab-macos-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B1A96DE61E902885AB0F28DD9789DF0B /* Pods-alt-tab-macos-dummy.m */; };
D1F4B0F6593581EA53A8A02CD81ECD8C /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = A4C0A84ADBFBC853AF7F2CB1F53D3321 /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
CEF0371E3359B0951DD138807A8A1F5A /* SRKeyEquivalentModifierMaskTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB7EC660F798E3AAB637764C0D80F415 /* SRKeyEquivalentModifierMaskTransformer.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
D191964986E1403DF882312E694517C4 /* SRKeyEquivalentTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 20E4DADA6B920C1BA2FFE9869EFD356E /* SRKeyEquivalentTransformer.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
D1D8A8FCB583AE7AB5E03DA82B27EDF5 /* SRKeyBindingTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 160A98F0E3604055FD8181545A88D923 /* SRKeyBindingTransformer.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
D44087AED8D32C993A585D77D7F6CC33 /* Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = E064E22C9DC958413827A901AB895A60 /* Base64.swift */; };
D837B3034B324427E867B12A52843A34 /* SRCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = B996471D454304C221B2F40EB33DFCF5 /* SRCommon.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
D99528B10FAEEBCA5033785E2A30B958 /* sv.lproj in Resources */ = {isa = PBXBuildFile; fileRef = E493CCB665E33B784972BCBD33FB6A35 /* sv.lproj */; };
D9FDDFBDA583D8A2618B67A54667322C /* SRShortcutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 216167185C2F81F90E2B9E2E672F2D25 /* SRShortcutController.h */; settings = {ATTRIBUTES = (Public, ); }; };
DAC7D497B1BB4561414F87631FE581D2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ED3F4BE3AEC041EEB45B74377D4425C8 /* Images.xcassets */; };
DB23A943B5830ACA4DB0CC3CEE229DBF /* es.lproj in Resources */ = {isa = PBXBuildFile; fileRef = BE424034018D08C7AC53E6FC58317FE3 /* es.lproj */; };
E3AD888D6605C568E027C3339B7B6AC5 /* SRCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = B996471D454304C221B2F40EB33DFCF5 /* SRCommon.m */; };
E123BC90074F47220454CAE5500627BF /* SRShortcutFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 279E7D7B1123E54F78F297ECC787C32E /* SRShortcutFormatter.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
E73C5479E237E5996D71307E266ECA9A /* pt-BR.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 7708AB164DB7C805FFB9869A1729C9BD /* pt-BR.lproj */; };
EA4B63319C3F12B1B7E62F5F6D0868DF /* ca.lproj in Resources */ = {isa = PBXBuildFile; fileRef = C627C17301E88898D6958A0729815ABC /* ca.lproj */; };
EC01EB47CD745508C1061BC9C41160F1 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 468F310276D58AB2873A36636350DC85 /* it.lproj */; };
EC03CCA55DAFD9C9A13533FBAC2398DE /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = A4C0A84ADBFBC853AF7F2CB1F53D3321 /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc -w -Xanalyzer -analyzer-disable-all-checks"; }; };
EC4AB36F93A3DABDBDAA6EF533FEA96F /* BaseDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B80718C9FB63C4AC0F29FD9E47875D /* BaseDestination.swift */; };
EC71569499666FA7370A8EA237225223 /* ca.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 5CBA66F8E4E81C9EB0BDA858C07B7DD1 /* ca.lproj */; };
EF18844124567DCD55BCF37B4CFE0A3E /* ja.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 8A92863FB68A70A86C9B7717DBD53F27 /* ja.lproj */; };
F2E8E8C95F044A9704E291C1CB23C65E /* FileDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 330A6BF3DA1253629D58E09A1534E182 /* FileDestination.swift */; };
F40E6412D0E5D977D56EF8AFEE78164A /* SRRecorderControlStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E6927C6D93BFE217C63FF9775CF2AD4 /* SRRecorderControlStyle.m */; };
F4158BBFB86D066D1244630E551CF6A5 /* SRShortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = BFA51136A7D709B4E5DE7ED9252EC299 /* SRShortcut.h */; settings = {ATTRIBUTES = (Public, ); }; };
F60389C5482DB5DD534EBCEB7F43082E /* en.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 5B09AE646A0A349EF211067540E1E9FA /* en.lproj */; };
F669B18B45B50886227DA3B2D844D49F /* Pods-unit-tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 34F38470D7148741213714EAF323F61B /* Pods-unit-tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
FA6B63611FDBBD5042B0D1F5903835F4 /* SRShortcutAction.m in Sources */ = {isa = PBXBuildFile; fileRef = FDF5D98729528C41061C6F934C7C0046 /* SRShortcutAction.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
FB2A76B1CAA1ED20A642A39817C561D0 /* SRKeyEquivalentModifierMaskTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0C575DF53390930D949CD9061E019A /* SRKeyEquivalentModifierMaskTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; };
FD8983B259D0C9AEB1AAFB23870FC33E /* SRKeyEquivalentTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 20E4DADA6B920C1BA2FFE9869EFD356E /* SRKeyEquivalentTransformer.m */; };
FE9CED4F39EDD76BE679F222881F2D45 /* zh-Hans.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 99951189D3BB8AE762F1DA57E8847F56 /* zh-Hans.lproj */; };
/* End PBXBuildFile section */
@@ -848,7 +848,7 @@
buildConfigurationList = 16D998FF7F1FCC3F400F5F965C90BCBB /* Build configuration list for PBXNativeTarget "ShortcutRecorder" */;
buildPhases = (
B2746300F1D3CD842DDE7285E5CD1159 /* Headers */,
CFB5E26132F12D60F10D08809CDB86F4 /* Sources */,
315C29AFB6DB056FE4A7360297E0CAE5 /* Sources */,
257D5576E6D30FB5B6FB248AF13C95B9 /* Frameworks */,
A97F80695A1C0E1C629EA33CAD10C904 /* Resources */,
);
@@ -1130,6 +1130,27 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
315C29AFB6DB056FE4A7360297E0CAE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A7201CEEAA2E87A8AD27D46A4B641F9F /* ShortcutRecorder-dummy.m in Sources */,
D837B3034B324427E867B12A52843A34 /* SRCommon.m in Sources */,
D1D8A8FCB583AE7AB5E03DA82B27EDF5 /* SRKeyBindingTransformer.m in Sources */,
56FD19D6AAFDA1359D7BEC1C08BF5A36 /* SRKeyCodeTransformer.m in Sources */,
CEF0371E3359B0951DD138807A8A1F5A /* SRKeyEquivalentModifierMaskTransformer.m in Sources */,
D191964986E1403DF882312E694517C4 /* SRKeyEquivalentTransformer.m in Sources */,
13C9B11E8154877B29450AF358FB693F /* SRModifierFlagsTransformer.m in Sources */,
B26825BD19B6771D0E83F6BEF2B63E4E /* SRRecorderControl.m in Sources */,
418DED74DF783AA497A3DA8BD8185C7F /* SRRecorderControlStyle.m in Sources */,
3EC20EE1C2D46CECA7FFDA0BE8CBA7DC /* SRShortcut.m in Sources */,
FA6B63611FDBBD5042B0D1F5903835F4 /* SRShortcutAction.m in Sources */,
BFC163D8D2E3122425E885875D72BE33 /* SRShortcutController.m in Sources */,
E123BC90074F47220454CAE5500627BF /* SRShortcutFormatter.m in Sources */,
343BFE43B781B05ED85224B009844203 /* SRShortcutValidator.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
630948035BBAB26A1B98C9EAB2A4862E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1151,28 +1172,7 @@
buildActionMask = 2147483647;
files = (
BD49046DC2930FC0EECD106C89AF5ABF /* LetsMove-dummy.m in Sources */,
D1F4B0F6593581EA53A8A02CD81ECD8C /* PFMoveApplication.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CFB5E26132F12D60F10D08809CDB86F4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5DE2ACCCCDBE0B5F9615B8A6D4884C99 /* ShortcutRecorder-dummy.m in Sources */,
E3AD888D6605C568E027C3339B7B6AC5 /* SRCommon.m in Sources */,
74114B8B8983E90BDC9C85B231D00E40 /* SRKeyBindingTransformer.m in Sources */,
474FFA8B855EC6CDC9168F3AD7467275 /* SRKeyCodeTransformer.m in Sources */,
99BF20014D3DBC64D0D3D83C349C8635 /* SRKeyEquivalentModifierMaskTransformer.m in Sources */,
FD8983B259D0C9AEB1AAFB23870FC33E /* SRKeyEquivalentTransformer.m in Sources */,
22D0C539333677DC4DD8C357A2EE8074 /* SRModifierFlagsTransformer.m in Sources */,
BC38EDEAA1B42FB0F78A453C7E0BD2AF /* SRRecorderControl.m in Sources */,
F40E6412D0E5D977D56EF8AFEE78164A /* SRRecorderControlStyle.m in Sources */,
1F149276AF739AC833BCADDCC1C83B73 /* SRShortcut.m in Sources */,
BB5D9FAD90FCE79DACC52E74E32ADFB2 /* SRShortcutAction.m in Sources */,
5ECC5662B52C59C90487CFEE4446BAC2 /* SRShortcutController.m in Sources */,
3ADF27FE54F59EC60116432676BA1937 /* SRShortcutFormatter.m in Sources */,
5C79484B0FE7E3B5624E5269DF989261 /* SRShortcutValidator.m in Sources */,
EC03CCA55DAFD9C9A13533FBAC2398DE /* PFMoveApplication.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -5,6 +5,7 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" @loader_path/../Frameworks
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers" -iframework "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" -iframework "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" -iframework "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" -iframework "${PODS_ROOT}/Sparkle" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SystemConfiguration"
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
@@ -5,6 +5,7 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" @loader_path/../Frameworks
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers" -iframework "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" -iframework "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" -iframework "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" -iframework "${PODS_ROOT}/Sparkle" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SystemConfiguration"
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
@@ -3,6 +3,7 @@ FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutR
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks'
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "ShortcutRecorder"
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
PODS_BUILD_DIR = ${BUILD_DIR}
@@ -3,6 +3,7 @@ FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutR
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks'
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "ShortcutRecorder"
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
PODS_BUILD_DIR = ${BUILD_DIR}
@@ -3,7 +3,7 @@ CODE_SIGN_IDENTITY =
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
@@ -3,7 +3,7 @@ CODE_SIGN_IDENTITY =
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
+6 -1
View File
@@ -1,10 +1,13 @@
<div align = center>
# AltTab
[![Screenshot](docs/public/demo/frontpage.jpg)](docs/public/demo/frontpage.jpg)
**AltTab** brings the power of Windows alt-tab to macOS
[Find out more on the official website](https://alt-tab-macos.netlify.app/)
[Official website](https://alt-tab-macos.netlify.app/)<br/><sub>14K stars</sub> | [Download](https://github.com/lwouis/alt-tab-macos/releases/download/v8.3.3/AltTab-8.3.3.zip)<br/><sub>6.6M downloads</sub>
-|-
<div align="right">
<p>Project supported by</p>
@@ -12,3 +15,5 @@
<img src="docs/public/demo/jetbrains.svg" alt="Jetbrains" width="149" height="32">
</a>
</div>
</div>
+87 -8
View File
@@ -63,6 +63,8 @@
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4807A6C523A9CD190052A53E /* SkyLight.framework */; };
48F3E16224EC0D8800A1C64B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BABDA79F8EF76E3ACDD5F /* Localizable.strings */; };
5883F0A829A5182C0071DB65 /* TrackpadEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5883F0A729A5182C0071DB65 /* TrackpadEvents.swift */; };
5F04852E2F09AC9700235D4A /* WindowCaptureEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F04852D2F09AC9700235D4A /* WindowCaptureEvents.swift */; };
5F17452E2F1A908D00128EF8 /* WindowDiscriminator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17452D2F1A907800128EF8 /* WindowDiscriminator.swift */; };
5F21C9E02C6E94240091F72F /* ShortcutsWhenActiveSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21C9DF2C6E94240091F72F /* ShortcutsWhenActiveSheet.swift */; };
5F21C9E22C6E94410091F72F /* AdditionalControlsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21C9E12C6E94410091F72F /* AdditionalControlsSheet.swift */; };
5F21C9E42C6E94700091F72F /* CustomizeStyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21C9E32C6E94700091F72F /* CustomizeStyleSheet.swift */; };
@@ -71,7 +73,11 @@
5F4753072CEE5A2E002C6C5E /* AppearanceTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8D57C86AF492D43A576D /* AppearanceTestable.swift */; };
5F706EC02BA448B1004D542D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5F706EBE2BA448B1004D542D /* Localizable.strings */; };
5F706EC32BA448D8004D542D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5F706EC12BA448D8004D542D /* InfoPlist.strings */; };
5F791E4B2F1C65E100B6419C /* ApplicationDiscriminator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F791E4A2F1C65E100B6419C /* ApplicationDiscriminator.swift */; };
5F882F382EC3C63400110A9F /* CustomRecorderControlTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8DC2A355E29516DD2FAF /* CustomRecorderControlTestable.swift */; };
5F906A342F16A18500B99924 /* Screens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F906A332F16A18000B99924 /* Screens.swift */; };
5FEFDF2F2F181E120065BAA3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5FEFDF2D2F181E120065BAA3 /* Localizable.strings */; };
5FEFDF322F181E250065BAA3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5FEFDF312F181E250065BAA3 /* InfoPlist.strings */; };
76D02BB22BFE7C9E0056008D /* Pods_alt_tab_macos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0712B3BEA2B3780398C0999 /* Pods_alt_tab_macos.framework */; };
AA974BA929B7D84C0099A29E /* PreviewPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA974BA829B7D84C0099A29E /* PreviewPanel.swift */; };
BF0C803A13C409E7F01FBCAB /* SkyLight.framework.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B94D7994AB9007F6391 /* SkyLight.framework.swift */; };
@@ -100,6 +106,7 @@
BF0C827C61136E83F43C6436 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8913DCD0FE4C494055B6 /* InfoPlist.strings */; };
BF0C828D53D429C2B8B22391 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C883EBA8FDA9CB61C91FD /* Localizable.strings */; };
BF0C829583ED47E458528F3A /* accessibility@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = BF0C889529FF19620EDAC295 /* accessibility@2x.png */; };
BF0C82E9AB495AAAA182764A /* CursorEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C81DD40D5AAD9B7C09028 /* CursorEvents.swift */; };
BF0C832C5BF4C8E9C95F7767 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C82E4DE5A28800F1DA6E6 /* MissionControl.swift */; };
BF0C83578F7292F307E30751 /* PopupButtonLikeSystemSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8A479D2B7FB80531F037 /* PopupButtonLikeSystemSettings.swift */; };
BF0C836CC0D9CEE5026B8EF4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C83121AAE4437993B4FF3 /* InfoPlist.strings */; };
@@ -237,7 +244,6 @@
D04BA737008AA2CD4E230A21 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA10777505D8A67ABD186 /* Application.swift */; };
D04BA73E90EFEF8247A5105D /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAC34CFD42A7F6F1F01C0 /* CGWindow.swift */; };
D04BA76A74267B1346D23687 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA6D57A1456C07318B8EA /* GridView.swift */; };
D04BA775CF3F8D9394A1E256 /* NSScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA68C2561D9EE4FD851B8 /* NSScreen.swift */; };
D04BA7B8D599E1A7A27FF5AE /* AcknowledgmentsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA788228BA86D9EFBD1ED /* AcknowledgmentsTab.swift */; };
D04BA7B9421871EDBD58F4E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BABA255CD25D8724A06CD /* Localizable.strings */; };
D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA64F1F344007EA13BA05 /* AppearanceTab.swift */; };
@@ -353,16 +359,31 @@
481FE54624D2D387001032F1 /* alt-tab-macos-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "alt-tab-macos-Bridging-Header.h"; sourceTree = "<group>"; };
5883F0A729A5182C0071DB65 /* TrackpadEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadEvents.swift; sourceTree = "<group>"; };
59DD4BE63D42EEFF1182BA7F /* Pods-alt-tab-macos.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-alt-tab-macos.release.xcconfig"; path = "Target Support Files/Pods-alt-tab-macos/Pods-alt-tab-macos.release.xcconfig"; sourceTree = "<group>"; };
5F04852D2F09AC9700235D4A /* WindowCaptureEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCaptureEvents.swift; sourceTree = "<group>"; };
5F084EAF2EF15E6B00CD212A /* TabbedWindowDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedWindowDetection.swift; sourceTree = "<group>"; };
5F17452D2F1A907800128EF8 /* WindowDiscriminator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDiscriminator.swift; sourceTree = "<group>"; };
5F21C9DF2C6E94240091F72F /* ShortcutsWhenActiveSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsWhenActiveSheet.swift; sourceTree = "<group>"; };
5F21C9E12C6E94410091F72F /* AdditionalControlsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalControlsSheet.swift; sourceTree = "<group>"; };
5F21C9E32C6E94700091F72F /* CustomizeStyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeStyleSheet.swift; sourceTree = "<group>"; };
5F21C9E52C6E94920091F72F /* AnimationsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationsSheet.swift; sourceTree = "<group>"; };
5F21C9E72C6E96A00091F72F /* SheetWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetWindow.swift; sourceTree = "<group>"; };
5F2218A92F156BC6005769F9 /* logo-github-sponsors.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-github-sponsors.png"; sourceTree = "<group>"; };
5F2218AA2F156BC6005769F9 /* logo-patreon.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = "logo-patreon.svg"; sourceTree = "<group>"; };
5F2218AB2F156BC6005769F9 /* logo-paypal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-paypal.png"; sourceTree = "<group>"; };
5F2218AC2F156BC6005769F9 /* logo-stripe.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = "logo-stripe.svg"; sourceTree = "<group>"; };
5F2218AD2F156BC6005769F9 /* mascot-cat.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mascot-cat.png"; sourceTree = "<group>"; };
5F2218AE2F156BC6005769F9 /* mascot-dog.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mascot-dog.png"; sourceTree = "<group>"; };
5F2218AF2F156BC6005769F9 /* mascot-rabbit.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mascot-rabbit.png"; sourceTree = "<group>"; };
5F2218B02F156BC6005769F9 /* mascot-squirrel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mascot-squirrel.png"; sourceTree = "<group>"; };
5F706EBD2BA448B1004D542D /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = Localizable.strings; sourceTree = "<group>"; };
5F706EC22BA448D8004D542D /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = InfoPlist.strings; sourceTree = "<group>"; };
5F791E4A2F1C65E100B6419C /* ApplicationDiscriminator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDiscriminator.swift; sourceTree = "<group>"; };
5F906A332F16A18000B99924 /* Screens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screens.swift; sourceTree = "<group>"; };
5F9F965C2EAE731200E25498 /* jetbrains.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = jetbrains.svg; sourceTree = "<group>"; };
5FAE000A2F13B49800FE6246 /* update_readme_and_website.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = update_readme_and_website.sh; sourceTree = "<group>"; };
5FE2AF752EAE849F004A4AC5 /* xcbeautify */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = xcbeautify; sourceTree = "<group>"; };
5FEFDF2C2F181E120065BAA3 /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = Localizable.strings; sourceTree = "<group>"; };
5FEFDF302F181E250065BAA3 /* InfoPlist.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = InfoPlist.strings; sourceTree = "<group>"; };
672F94799CC7C90282AEB3EA /* Pods-alt-tab-macos.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-alt-tab-macos.debug.xcconfig"; path = "Target Support Files/Pods-alt-tab-macos/Pods-alt-tab-macos.debug.xcconfig"; sourceTree = "<group>"; };
7451B8433E47C444A6F489FF /* Pods-unit-tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-unit-tests.debug.xcconfig"; path = "Target Support Files/Pods-unit-tests/Pods-unit-tests.debug.xcconfig"; sourceTree = "<group>"; };
AA974BA829B7D84C0099A29E /* PreviewPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewPanel.swift; sourceTree = "<group>"; };
@@ -385,6 +406,7 @@
BF0C81BB57B297B2432C4824 /* test-release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "test-release.xcconfig"; sourceTree = "<group>"; };
BF0C81BF699036148BA17B7C /* jv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = jv; path = Localizable.strings; sourceTree = "<group>"; };
BF0C81D602ED642A48D50B78 /* notarytool */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = notarytool; sourceTree = "<group>"; };
BF0C81DD40D5AAD9B7C09028 /* CursorEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorEvents.swift; sourceTree = "<group>"; };
BF0C82009DCCA86287730F13 /* th */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = th; path = InfoPlist.strings; sourceTree = "<group>"; };
BF0C821029C1CBAC09232B0B /* nn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nn; path = InfoPlist.strings; sourceTree = "<group>"; };
BF0C822D3BEDAB1F1C9F775E /* ScreensEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreensEvents.swift; sourceTree = "<group>"; };
@@ -472,6 +494,7 @@
BF0C89B4DEC3CBB8ECD29E1B /* import_l10n_strings_from_poeditor.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = import_l10n_strings_from_poeditor.sh; sourceTree = "<group>"; };
BF0C89D7F23D82A3FF442E0B /* build_app.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = build_app.sh; sourceTree = "<group>"; };
BF0C89F6EA16DD7322626E20 /* FUNDING.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = FUNDING.yml; sourceTree = "<group>"; };
BF0C89F77BFA42510A8FFDAB /* FAQ.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = FAQ.md; sourceTree = "<group>"; };
BF0C8A261AAA42191426A587 /* run_tests.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = run_tests.sh; sourceTree = "<group>"; };
BF0C8A3DAEA5B956593AF97A /* kn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = kn; path = InfoPlist.strings; sourceTree = "<group>"; };
BF0C8A479D2B7FB80531F037 /* PopupButtonLikeSystemSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopupButtonLikeSystemSettings.swift; sourceTree = "<group>"; };
@@ -524,6 +547,8 @@
BF0C8E9D0B36A8872D5B7653 /* nb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nb; path = InfoPlist.strings; sourceTree = "<group>"; };
BF0C8EAF02309B9C489E65F4 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
BF0C8EC400A560FDF99F8C35 /* el */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = el; path = Localizable.strings; sourceTree = "<group>"; };
BF0C8EC6F81536045569B1CF /* turbolinks.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = turbolinks.js; sourceTree = "<group>"; };
BF0C8EE00994AC6C8323EE12 /* table-of-contents.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "table-of-contents.js"; sourceTree = "<group>"; };
BF0C8EE17F3ABD7A6D44BFFE /* pl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = pl; path = Localizable.strings; sourceTree = "<group>"; };
BF0C8F58B38F29B2662E6C2D /* PreferencesMigrations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesMigrations.swift; sourceTree = "<group>"; };
BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollwheelEvents.swift; sourceTree = "<group>"; };
@@ -566,7 +591,7 @@
D04BA2E8D5D5CC23456BC8F7 /* tr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = tr; path = Localizable.strings; sourceTree = "<group>"; };
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA3632A2EB5C64A3FEE59 /* hu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hu; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BA369B4D9FD6A13936F53 /* bare.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = bare.html; sourceTree = "<group>"; };
D04BA369B4D9FD6A13936F53 /* changelog-bare.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "changelog-bare.html"; sourceTree = "<group>"; };
D04BA3C951614ADC408312CB /* pt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = pt; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BA3EF6656955B0B29C2CD /* other.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = other.md; sourceTree = "<group>"; };
D04BA3FF376BC2757D25A2AD /* controls@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "controls@2x.png"; sourceTree = "<group>"; };
@@ -603,7 +628,6 @@
D04BA653BD073CB58E2CFC93 /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = "<group>"; };
D04BA662DF59DF9A97FDA598 /* de */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = de; path = Localizable.strings; sourceTree = "<group>"; };
D04BA67D904743A293768CB1 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = "<group>"; };
D04BA68C2561D9EE4FD851B8 /* NSScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSScreen.swift; sourceTree = "<group>"; };
D04BA6BBFD0E87B8458E91D3 /* feature-suggestion.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "feature-suggestion.md"; sourceTree = "<group>"; };
D04BA6D57A1456C07318B8EA /* GridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = "<group>"; };
D04BA6ED38651224DB117A5A /* upload_symbols_to_appcenter.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = upload_symbols_to_appcenter.sh; sourceTree = "<group>"; };
@@ -642,7 +666,6 @@
D04BAA1A00449C0C6434AB95 /* id */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = id; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAA265462999602D1DDE7 /* id */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = id; path = Localizable.strings; sourceTree = "<group>"; };
D04BAA4442D0CE3A4972937F /* nl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nl; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAA5ECAE8BB8F8656DEDF /* update_contributors.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = update_contributors.sh; sourceTree = "<group>"; };
D04BAA6E43F1E6EB81F953AF /* ru */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ru; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsView.swift; sourceTree = "<group>"; };
D04BAA9E0539EE620D08F63F /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = fr; path = InfoPlist.strings; sourceTree = "<group>"; };
@@ -819,6 +842,21 @@
path = appearance;
sourceTree = "<group>";
};
5F2218B12F156BC6005769F9 /* support */ = {
isa = PBXGroup;
children = (
5F2218A92F156BC6005769F9 /* logo-github-sponsors.png */,
5F2218AA2F156BC6005769F9 /* logo-patreon.svg */,
5F2218AB2F156BC6005769F9 /* logo-paypal.png */,
5F2218AC2F156BC6005769F9 /* logo-stripe.svg */,
5F2218AD2F156BC6005769F9 /* mascot-cat.png */,
5F2218AE2F156BC6005769F9 /* mascot-dog.png */,
5F2218AF2F156BC6005769F9 /* mascot-rabbit.png */,
5F2218B02F156BC6005769F9 /* mascot-squirrel.png */,
);
path = support;
sourceTree = "<group>";
};
5F706EBF2BA448B1004D542D /* is.lproj */ = {
isa = PBXGroup;
children = (
@@ -828,6 +866,15 @@
path = is.lproj;
sourceTree = "<group>";
};
5FEFDF2E2F181E120065BAA3 /* be.lproj */ = {
isa = PBXGroup;
children = (
5FEFDF312F181E250065BAA3 /* InfoPlist.strings */,
5FEFDF2D2F181E120065BAA3 /* Localizable.strings */,
);
path = be.lproj;
sourceTree = "<group>";
};
A638921971E99EC1D9EC383D /* Pods */ = {
isa = PBXGroup;
children = (
@@ -1287,9 +1334,11 @@
D04BA0D80B24E72B9B981A1D /* logic */ = {
isa = PBXGroup;
children = (
5F17452D2F1A907800128EF8 /* WindowDiscriminator.swift */,
5F791E4A2F1C65E100B6419C /* ApplicationDiscriminator.swift */,
5F906A332F16A18000B99924 /* Screens.swift */,
D04BACD976030676FD0761D5 /* Windows.swift */,
D04BA9B93823398A542FF7A0 /* Preferences.swift */,
D04BA68C2561D9EE4FD851B8 /* NSScreen.swift */,
D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */,
D04BA7CF9C2D1BEC7C05AB24 /* Spaces.swift */,
D04BA015A45DE7AFDC9794FE /* Window.swift */,
@@ -1357,6 +1406,8 @@
isa = PBXGroup;
children = (
D04BAB7714DEDEA0A53AC3ED /* main.scss */,
BF0C8EC6F81536045569B1CF /* turbolinks.js */,
BF0C8EE00994AC6C8323EE12 /* table-of-contents.js */,
);
path = _includes;
sourceTree = "<group>";
@@ -1609,12 +1660,12 @@
D04BA703DCD38D9757093312 /* scripts */ = {
isa = PBXGroup;
children = (
5FAE000A2F13B49800FE6246 /* update_readme_and_website.sh */,
5FE2AF752EAE849F004A4AC5 /* xcbeautify */,
D04BA4B5292629AA6B560216 /* package_and_notarize_release.sh */,
D04BAC6AFC7F06D1A567F27A /* replace_environment_variables_in_app.sh */,
D04BA0AAAE82C72855DBBA26 /* update_appcast.sh */,
D04BA4FB0047656BEA21F1BE /* codesign */,
D04BAA5ECAE8BB8F8656DEDF /* update_contributors.sh */,
D04BAD65AA870A49D2F89DBC /* ensure_generated_files_are_up_to_date.sh */,
D04BA4D5DF349429527824A2 /* determine_next_version.sh */,
D04BAAE6E6823304D66B74F8 /* update_website.sh */,
@@ -1715,7 +1766,7 @@
isa = PBXGroup;
children = (
D04BAE298D0D87E4E69A2054 /* default.html */,
D04BA369B4D9FD6A13936F53 /* bare.html */,
D04BA369B4D9FD6A13936F53 /* changelog-bare.html */,
);
path = _layouts;
sourceTree = "<group>";
@@ -1757,6 +1808,7 @@
D04BAA4B2A70759DD5128364 /* public */ = {
isa = PBXGroup;
children = (
5F2218B12F156BC6005769F9 /* support */,
D04BA954319227829B646315 /* android-chrome-192x192.png */,
D04BA72AC9B57A1673C28B2E /* android-chrome-256x256.png */,
D04BA2D022B64A876CCA6BB7 /* apple-touch-icon.png */,
@@ -1819,6 +1871,7 @@
D04BAC2B70BC798C57B0B492 /* l10n */ = {
isa = PBXGroup;
children = (
5FEFDF2E2F181E120065BAA3 /* be.lproj */,
5F706EBF2BA448B1004D542D /* is.lproj */,
D04BA2641C1A385861945B7C /* en.lproj */,
D04BA61DD16A622D398FED3E /* fr.lproj */,
@@ -1881,6 +1934,7 @@
D04BAC8857A527C2E15D6598 /* events */ = {
isa = PBXGroup;
children = (
5F04852D2F09AC9700235D4A /* WindowCaptureEvents.swift */,
D04BA8FDCF137D892114F5F3 /* AccessibilityEvents.swift */,
D04BA4A621A8A15660F973C1 /* KeyboardEvents.swift */,
D04BA76AEE37D6656EB80126 /* RunningApplicationsEvents.swift */,
@@ -1895,6 +1949,7 @@
BF0C8643F524515224BF1618 /* KeyboardEventsTestable.swift */,
BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */,
BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */,
BF0C81DD40D5AAD9B7C09028 /* CursorEvents.swift */,
);
path = events;
sourceTree = "<group>";
@@ -1931,6 +1986,7 @@
D04BADB5AA570748C717D31A /* Changelog-bare.md */,
BF0C815B4782DEEEDB2C76C5 /* Support.md */,
BF0C8880DFDFAC86CE1E2E31 /* Gemfile.lock */,
BF0C89F77BFA42510A8FFDAB /* FAQ.md */,
);
path = docs;
sourceTree = "<group>";
@@ -2045,6 +2101,7 @@
lt,
gu,
"zh-HK",
be,
);
mainGroup = D04BA1463D2A17038222BB84;
productRefGroup = D04BA1A461BBB5A9BE15E015 /* Products */;
@@ -2080,6 +2137,7 @@
D04BAC3169D54014CBA9ACE5 /* Localizable.strings in Resources */,
D04BA100BD0F47828EB649FF /* InfoPlist.strings in Resources */,
D04BA3C24F4F644EA91DE38C /* Localizable.strings in Resources */,
5FEFDF2F2F181E120065BAA3 /* Localizable.strings in Resources */,
D04BA03B560B681AF96E4610 /* InfoPlist.strings in Resources */,
D04BA1C85041FB043FD43E65 /* Localizable.strings in Resources */,
D04BA9557A6C9E192D228150 /* InfoPlist.strings in Resources */,
@@ -2238,6 +2296,7 @@
BF0C836CC0D9CEE5026B8EF4 /* InfoPlist.strings in Resources */,
BF0C8BD4410A3D597409C328 /* InfoPlist.strings in Resources */,
BF0C8167D415F503D2390065 /* InfoPlist.strings in Resources */,
5FEFDF322F181E250065BAA3 /* InfoPlist.strings in Resources */,
BF0C8CE3AE04A98400B9962F /* InfoPlist.strings in Resources */,
BF0C8610E3416890DED16C36 /* Localizable.strings in Resources */,
BF0C827C61136E83F43C6436 /* InfoPlist.strings in Resources */,
@@ -2389,7 +2448,6 @@
D04BA8480A8FF466CA89DA5B /* main.swift in Sources */,
D04BAFB973C3D28718FAEB87 /* Windows.swift in Sources */,
D04BAC011A71E0418154F8CD /* Preferences.swift in Sources */,
D04BA775CF3F8D9394A1E256 /* NSScreen.swift in Sources */,
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */,
D04BABEECBC6D922298BC93A /* Spaces.swift in Sources */,
D04BA6187A91A847844B6ABB /* Window.swift in Sources */,
@@ -2405,6 +2463,7 @@
D04BA1B133D53572D7B312C2 /* ThumbnailFontIconView.swift in Sources */,
D04BA1CEC6B9C8945FEC8740 /* ThumbnailView.swift in Sources */,
D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */,
5F791E4B2F1C65E100B6419C /* ApplicationDiscriminator.swift in Sources */,
D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */,
D04BAD1BE9DC22C48C53D195 /* AboutTab.swift in Sources */,
D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */,
@@ -2415,6 +2474,7 @@
D04BAB4EB890853B5B9B2C61 /* BoldLabel.swift in Sources */,
D04BAABE804F3769CE22BEB6 /* HyperlinkLabel.swift in Sources */,
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */,
5F17452E2F1A908D00128EF8 /* WindowDiscriminator.swift in Sources */,
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */,
5F21C9E42C6E94700091F72F /* CustomizeStyleSheet.swift in Sources */,
D04BA1621718FF26F8D5E75D /* CustomRecorderControl.swift in Sources */,
@@ -2426,6 +2486,7 @@
5883F0A829A5182C0071DB65 /* TrackpadEvents.swift in Sources */,
D04BAAAF5CFA991D3B078DB8 /* KeyboardEvents.swift in Sources */,
D04BA1766FBCB2E941D081A5 /* RunningApplicationsEvents.swift in Sources */,
5F906A342F16A18500B99924 /* Screens.swift in Sources */,
D04BA0AE9865276FF8EF5DEB /* UserDefaultsEvents.swift in Sources */,
D04BA4A11F821548EE3C5E95 /* Bash.swift in Sources */,
5F21C9E62C6E94920091F72F /* AnimationsSheet.swift in Sources */,
@@ -2446,6 +2507,7 @@
1C96108A0CB2565233FE100B /* TableGroupView.swift in Sources */,
1C9618872DD3863A8EFE8414 /* Appearance.swift in Sources */,
BF0C83578F7292F307E30751 /* PopupButtonLikeSystemSettings.swift in Sources */,
5F04852E2F09AC9700235D4A /* WindowCaptureEvents.swift in Sources */,
1C961F9CF4F1BEDF04ACAEE0 /* Switch.swift in Sources */,
1C96182D2ECA735740ED474E /* ImageTextButtonView.swift in Sources */,
BF0C860F1779DDE8C0171EA0 /* WindowlessAppIndicator.swift in Sources */,
@@ -2472,6 +2534,7 @@
BF0C8E5F4E4D5F7DAEE07C20 /* ThumbnailsPanelBackgroundView.swift in Sources */,
BF0C8AD478399D2E97AE3FB4 /* CustomRecorderControlTestable.swift in Sources */,
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */,
BF0C82E9AB495AAAA182764A /* CursorEvents.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2494,6 +2557,22 @@
name = InfoPlist.strings;
sourceTree = "<group>";
};
5FEFDF2D2F181E120065BAA3 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
5FEFDF2C2F181E120065BAA3 /* be */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
5FEFDF312F181E250065BAA3 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
5FEFDF302F181E250065BAA3 /* InfoPlist.strings */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
BF0C803A0202BA8D9397E0CA /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
+112
View File
@@ -4,6 +4,118 @@
<title>alt-tab-macos</title>
<language>en</language>
<item>
<title>Version 8.3.3</title>
<pubDate>Sun, 25 Jan 2026 21:23:56 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.3.3/AltTab-8.3.3.zip"
sparkle:version="8.3.3"
sparkle:shortVersionString="8.3.3"
sparkle:edSignature="56edeQ0CPAyClzRH9vK+xnwn79CvRH8w4tEsVMRNT8Qjja8cv/Q9aYqkPXRFaXR74wY9ogFqzPFK+NZoVf8gAg==" length="12107335"
type="application/octet-stream"/>
</item>
<item>
<title>Version 8.3.2</title>
<pubDate>Thu, 22 Jan 2026 23:33:49 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.3.2/AltTab-8.3.2.zip"
sparkle:version="8.3.2"
sparkle:shortVersionString="8.3.2"
sparkle:edSignature="ojwCSKUiwD/Xk4DL/1bFn8TEJahuvjfGJ5aALCUx/DDvwUl8313ECXaNTHKZ58W/RA1QYrk8jjv3kLl4JqiYCQ==" length="12103858"
type="application/octet-stream"/>
</item>
<item>
<title>Version 8.3.1</title>
<pubDate>Thu, 22 Jan 2026 21:47:18 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.3.1/AltTab-8.3.1.zip"
sparkle:version="8.3.1"
sparkle:shortVersionString="8.3.1"
sparkle:edSignature="4Usbj7WgaPmzkW9vj4rOAQLxcXymN8sOrQnJJgvh9xahyFi3EBS1R/jrA/FQx6R9YFbebzMkZMVnyhrdVg7/Bw==" length="12102360"
type="application/octet-stream"/>
</item>
<item>
<title>Version 8.3.0</title>
<pubDate>Tue, 20 Jan 2026 17:58:52 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.3.0/AltTab-8.3.0.zip"
sparkle:version="8.3.0"
sparkle:shortVersionString="8.3.0"
sparkle:edSignature="1fb/o7iUv3OCQxm+BLlq7mZGXhwCv4/xDmR6RmPLfJ47S4Cy8ui32VQEnePfA7JtHP6z/ZLGVvC3aETKbG/QCA==" length="12095858"
type="application/octet-stream"/>
</item>
<item>
<title>Version 8.2.0</title>
<pubDate>Thu, 15 Jan 2026 18:01:36 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.2.0/AltTab-8.2.0.zip"
sparkle:version="8.2.0"
sparkle:shortVersionString="8.2.0"
sparkle:edSignature="k3DXj6SOQDYxMFOqVzwWC4pwJOdEd2/GOe8wzOi6z4ITkKIviEGm3o+5w0e6ECR3Gd4eAC0+N5NzItupXClIDg==" length="12102733"
type="application/octet-stream"/>
</item>
<item>
<title>Version 8.1.0</title>
<pubDate>Wed, 14 Jan 2026 23:19:00 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.1.0/AltTab-8.1.0.zip"
sparkle:version="8.1.0"
sparkle:shortVersionString="8.1.0"
sparkle:edSignature="PeL1FVtC8wV+tB4hl8mSKn96EYCls/c+iyb1bYGDm8EcJl/acFtTyMaAMq8LzIOtLv3Q6C4aNPqyXWTSIGRyDg==" length="12103907"
type="application/octet-stream"/>
</item>
<item>
<title>Version 8.0.0</title>
<pubDate>Tue, 06 Jan 2026 18:36:14 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v8.0.0/AltTab-8.0.0.zip"
sparkle:version="8.0.0"
sparkle:shortVersionString="8.0.0"
sparkle:edSignature="d4RDsNE+p13oq+BWEsuylv1T1wsIR36e72uTT4ge4ynaNX4pD9jUY1QQCUNe1HK3Dg78vl8gvjQHcriZFaGjBQ==" length="12095023"
type="application/octet-stream"/>
</item>
<item>
<title>Version 7.39.0</title>
<pubDate>Sat, 03 Jan 2026 18:22:52 +0000</pubDate>
<sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://alt-tab-macos.netlify.app/changelog-bare</sparkle:releaseNotesLink>
<enclosure
url="https://github.com/lwouis/alt-tab-macos/releases/download/v7.39.0/AltTab-7.39.0.zip"
sparkle:version="7.39.0"
sparkle:shortVersionString="7.39.0"
sparkle:edSignature="Cq61dj1IouJja0FIWp1F6HFBXYg4Efsij0Xq75G5iFY/o6j02mGOtnIFFx0bWuLjt5I9PzsD7dN9izsoi72gAw==" length="12054195"
type="application/octet-stream"/>
</item>
<item>
<title>Version 7.38.1</title>
<pubDate>Sat, 13 Dec 2025 19:15:05 +0000</pubDate>
+1 -1
View File
@@ -1,4 +1,4 @@
# Acknowledgments
## Acknowledgments
These third-party libraries are used:
+1 -15
View File
@@ -1,19 +1,5 @@
---
layout: bare
layout: changelog-bare
---
<h1><a style="
position: fixed;
top: 1.5em;
right: 10px;
border-radius: 0.5em;
padding: 0.15em 0.9em;
font-size: 0.7em;
background: #ffdbd5;
/*background: #f91675;*/
color: #950020;
/*color: white;*/
font-weight: normal;
" href="https://alt-tab-macos.netlify.app/support">Support this project ❤️</a></h1>
{% include_relative Changelog.md %}
+121
View File
@@ -1,3 +1,124 @@
## [8.3.3](https://github.com/lwouis/alt-tab-macos/compare/v8.3.2...v8.3.3) (2026-01-25)
### Bug Fixes
* app icons could sometimes be invisible (closes [#5226](https://github.com/lwouis/alt-tab-macos/issues/5226)) ([23fbc01](https://github.com/lwouis/alt-tab-macos/commit/23fbc01))
* improve trackpad gesture detection (closes [#5203](https://github.com/lwouis/alt-tab-macos/issues/5203)) ([9b5e43e](https://github.com/lwouis/alt-tab-macos/commit/9b5e43e))
* switcher could sometimes show with no window pre-selected ([3e95f77](https://github.com/lwouis/alt-tab-macos/commit/3e95f77))
* title could be off-center in app-icons appearance (closes [#5235](https://github.com/lwouis/alt-tab-macos/issues/5235)) ([7fdfe4a](https://github.com/lwouis/alt-tab-macos/commit/7fdfe4a))
## [8.3.2](https://github.com/lwouis/alt-tab-macos/compare/v8.3.1...v8.3.2) (2026-01-22)
### Bug Fixes
* blacklisting fullscreen windows was not working (closes [#5228](https://github.com/lwouis/alt-tab-macos/issues/5228)) ([53815e0](https://github.com/lwouis/alt-tab-macos/commit/53815e0))
## [8.3.1](https://github.com/lwouis/alt-tab-macos/compare/v8.3.0...v8.3.1) (2026-01-22)
### Bug Fixes
* chrome apps windows would not show in the switcher (closes [#5227](https://github.com/lwouis/alt-tab-macos/issues/5227)) ([bf3fa67](https://github.com/lwouis/alt-tab-macos/commit/bf3fa67))
# [8.3.0](https://github.com/lwouis/alt-tab-macos/compare/v8.2.0...v8.3.0) (2026-01-20)
### Bug Fixes
* position of windowless indicator in thumbnails appearance ([d1fd1fd](https://github.com/lwouis/alt-tab-macos/commit/d1fd1fd))
* small memory leak when opening the switcher (closes [#4255](https://github.com/lwouis/alt-tab-macos/issues/4255)) ([0a6485e](https://github.com/lwouis/alt-tab-macos/commit/0a6485e))
* windows would sometimes not update their order (closes [#5207](https://github.com/lwouis/alt-tab-macos/issues/5207)) ([a3f2a54](https://github.com/lwouis/alt-tab-macos/commit/a3f2a54))
### Features
* increase apparition delay granularity (closes [#5210](https://github.com/lwouis/alt-tab-macos/issues/5210)) ([5b0df6b](https://github.com/lwouis/alt-tab-macos/commit/5b0df6b))
# [8.2.0](https://github.com/lwouis/alt-tab-macos/compare/v8.1.0...v8.2.0) (2026-01-15)
### Bug Fixes
* thumbnails could sometimes be stretched (closes [#5204](https://github.com/lwouis/alt-tab-macos/issues/5204)) ([ac24d6d](https://github.com/lwouis/alt-tab-macos/commit/ac24d6d))
### Features
* stop checking screen-recording permission after launch ([#5202](https://github.com/lwouis/alt-tab-macos/issues/5202)) ([f481d22](https://github.com/lwouis/alt-tab-macos/commit/f481d22))
# [8.1.0](https://github.com/lwouis/alt-tab-macos/compare/v8.0.0...v8.1.0) (2026-01-14)
### Bug Fixes
* allow f-keys as shortcuts-when-active (closes [#5166](https://github.com/lwouis/alt-tab-macos/issues/5166)) ([a0c9fee](https://github.com/lwouis/alt-tab-macos/commit/a0c9fee))
* avoid unintentional trackpad trigger (closes [#4278](https://github.com/lwouis/alt-tab-macos/issues/4278)) ([0539a3a](https://github.com/lwouis/alt-tab-macos/commit/0539a3a))
* better handle palm/fingers resting on the trackpad (closes [#5191](https://github.com/lwouis/alt-tab-macos/issues/5191)) ([9ed8a19](https://github.com/lwouis/alt-tab-macos/commit/9ed8a19))
* mitigate macos 15 bugs with screencapture-kit (closes [#5190](https://github.com/lwouis/alt-tab-macos/issues/5190)) ([b2f5c6c](https://github.com/lwouis/alt-tab-macos/commit/b2f5c6c))
* mouse hover detection for hidden/windowless apps ([7c64864](https://github.com/lwouis/alt-tab-macos/commit/7c64864))
* thumbnails could sometimes be the wrong size ([9d0b947](https://github.com/lwouis/alt-tab-macos/commit/9d0b947))
### Features
* add belarusian localization ([0fe722b](https://github.com/lwouis/alt-tab-macos/commit/0fe722b))
* improve accidental cursor movement ([74b5a11](https://github.com/lwouis/alt-tab-macos/commit/74b5a11)), closes [/github.com/lwouis/alt-tab-macos/issues/4278#issuecomment-3733280737](https://github.com//github.com/lwouis/alt-tab-macos/issues/4278/issues/issuecomment-3733280737)
* improve pl localization ([3b53ce7](https://github.com/lwouis/alt-tab-macos/commit/3b53ce7))
* show outline of windows hidden for privacy reasons (closes [#5172](https://github.com/lwouis/alt-tab-macos/issues/5172)) ([1090acf](https://github.com/lwouis/alt-tab-macos/commit/1090acf))
# [8.0.0](https://github.com/lwouis/alt-tab-macos/compare/v7.39.0...v8.0.0) (2026-01-06)
### Bug Fixes
* avoid macos bug where many permission dialogs pop (closes [#5106](https://github.com/lwouis/alt-tab-macos/issues/5106)) ([5c4199e](https://github.com/lwouis/alt-tab-macos/commit/5c4199e))
### Features
* improve memory image of showing app icons (closes [#5144](https://github.com/lwouis/alt-tab-macos/issues/5144)) ([bf58335](https://github.com/lwouis/alt-tab-macos/commit/bf58335))
* improve ta localization ([989b344](https://github.com/lwouis/alt-tab-macos/commit/989b344))
* improve thumbnails with screen-capture kit ([7821d7c](https://github.com/lwouis/alt-tab-macos/commit/7821d7c)), closes [#4255](https://github.com/lwouis/alt-tab-macos/issues/4255) [#3652](https://github.com/lwouis/alt-tab-macos/issues/3652)
### Performance Improvements
* debounce screen/space events to minimize work ([e2b4779](https://github.com/lwouis/alt-tab-macos/commit/e2b4779))
### BREAKING CHANGES
* Better thumbnails with ScreenCaptureKit for users on macOS >= 14
# [7.39.0](https://github.com/lwouis/alt-tab-macos/compare/v7.38.1...v7.39.0) (2026-01-03)
### Bug Fixes
* avoid showing closed windows (closes [#3589](https://github.com/lwouis/alt-tab-macos/issues/3589)) ([894a69d](https://github.com/lwouis/alt-tab-macos/commit/894a69d)), closes [#4924](https://github.com/lwouis/alt-tab-macos/issues/4924)
* permission checks might freeze or restart the app (closes [#5112](https://github.com/lwouis/alt-tab-macos/issues/5112)) ([7906d63](https://github.com/lwouis/alt-tab-macos/commit/7906d63))
* permission window could steal focus (see [#3577](https://github.com/lwouis/alt-tab-macos/issues/3577)) ([2f875b3](https://github.com/lwouis/alt-tab-macos/commit/2f875b3))
* permissions window could look wrong in some languages like chinese ([914f91f](https://github.com/lwouis/alt-tab-macos/commit/914f91f)), closes [/github.com/lwouis/alt-tab-macos/issues/5112#issuecomment-3692769341](https://github.com//github.com/lwouis/alt-tab-macos/issues/5112/issues/issuecomment-3692769341)
* preview could sometimes be incorrectly positioned ([edc2f00](https://github.com/lwouis/alt-tab-macos/commit/edc2f00))
* show some hovering windows (e.g. zoom.app) (closes [#5140](https://github.com/lwouis/alt-tab-macos/issues/5140)) ([fcc6651](https://github.com/lwouis/alt-tab-macos/commit/fcc6651))
* thumbnails could sometimes have the wrong size ([55f3f59](https://github.com/lwouis/alt-tab-macos/commit/55f3f59))
### Features
* avoid occasional delays in showing the switcher (closes [#5109](https://github.com/lwouis/alt-tab-macos/issues/5109)) ([3c04a4c](https://github.com/lwouis/alt-tab-macos/commit/3c04a4c))
* improve nb/tr localizations ([5e842d5](https://github.com/lwouis/alt-tab-macos/commit/5e842d5))
* remove shadow around app icon appearance ([dda612c](https://github.com/lwouis/alt-tab-macos/commit/dda612c))
### Performance Improvements
* do login item and plist updates later to accelerate launch ([2f72c5d](https://github.com/lwouis/alt-tab-macos/commit/2f72c5d))
* don't compute diffs of running-apps; directly get new/old ([7ded01c](https://github.com/lwouis/alt-tab-macos/commit/7ded01c))
* improve mouse hover algo and core-animation side ([6103785](https://github.com/lwouis/alt-tab-macos/commit/6103785))
## [7.38.1](https://github.com/lwouis/alt-tab-macos/compare/v7.38.0...v7.38.1) (2025-12-13)
+3 -9
View File
@@ -1,5 +1,3 @@
# Contributing
There are many ways to contribute:
* [Suggest an enhancement or discuss an issue on github](https://github.com/lwouis/alt-tab-macos/issues), or use the feedback form in the app.
@@ -9,18 +7,14 @@ There are many ways to contribute:
This document gives a technical overview of the project, for newcomers who want to contribute.
## Building the project locally
## Building the project
This project has minimal dependency on Xcode-only features (e.g. InterfaceBuilder, Playgrounds). You can build it by doing:
* `scripts/codesign/setup_local.sh` to generate a local self-signed certificate, to avoid having to re-check the `System Preferences > Security & Privacy` permissions on every build
* Either open `alt-tab-macos.xcworkspace` with XCode, or use the cli: `xcodebuild -workspace alt-tab-macos.xcworkspace -scheme Debug` to build the .app with the `Debug` build configuration
## Raising a pull-request
If you want to contribute a PR, please run `npm install` once. It will add the pre-commit hook to ensure that your commits follow the convention and will pass the PR.
## Mac development ecosystem
## Mac development
Mac development ecosystem is pretty terrible in general. They keep piling on the tech stacks on top of each other, so you have C APIs, ObjC APIs, Swift APIs, Interface builder, Playgrounds, Swift UI, Mac Catalyst. All these are bridging with each other with a bunch of macros, SDKs glue, compiler flags, compatibility mode, XCode legacy build system, etc. For alt-tab, we are on Swift 5.0. Note that swift just recently started being stable, but overall any change of version breaks a lot of stuff. Swift itself is the mainstream language with the worst governance I’ve seen in modern times.
@@ -32,7 +26,7 @@ Dependencies were historically never handled by Apple. The community came up wit
OS APIs are quite limited for the kind of low-level, system-wide app AltTab is. This means often we just don’t have an API to do something. For instance, there is no API to ask the OS “how many Spaces does the user have?” or “Can you focus the window on Space 2?”. There are however, retro-engineered private APIs which you can call. These are not documented at all, not guaranteed to be there in future macOS releases, and prevent us from releasing AltTab on the Mac AppStore. We have tried our best to [document the ones we are using](https://github.com/lwouis/alt-tab-macos/blob/master/src/api-wrappers/private-apis/README.md), as well as [the ones we investigated](https://github.com/lwouis/alt-tab-macos/blob/master/src/experimentations/PrivateApis.swift) in the past.
## This project specifically
## Project architecture
To mitigate the issues listed above, we took some measures.
+4 -291
View File
@@ -1,294 +1,7 @@
# Contributors
## [Developed the app](https://github.com/lwouis/alt-tab-macos/graphs/contributors)
They helped [develop the app](https://github.com/lwouis/alt-tab-macos/graphs/contributors):
[adamnemecek](https://github.com/adamnemecek), [AfzalivE](https://github.com/AfzalivE), [akx](https://github.com/akx), [Allsochen](https://github.com/Allsochen), [ayroblu](https://github.com/ayroblu), [britalmeida](https://github.com/britalmeida), [Calinou](https://github.com/Calinou), [damonpam](https://github.com/damonpam), [decodism](https://github.com/decodism), [delaneyb](https://github.com/delaneyb), [gingerr](https://github.com/gingerr), [GrzegorzKazana](https://github.com/GrzegorzKazana), [hughlilly](https://github.com/hughlilly), [karbassi](https://github.com/karbassi), [L1cardo](https://github.com/L1cardo), [lwouis](https://github.com/lwouis), [metacodes](https://github.com/metacodes), [mnin](https://github.com/mnin), [MrPaschenko](https://github.com/MrPaschenko), [nella17](https://github.com/nella17), [notlmn](https://github.com/notlmn), [phungtuanhoang1996](https://github.com/phungtuanhoang1996), [rbnis](https://github.com/rbnis), [Rjevski](https://github.com/Rjevski), [ShlomoCode](https://github.com/ShlomoCode), [skolj](https://github.com/skolj), [stopdesign](https://github.com/stopdesign), [xanathar](https://github.com/xanathar), [zacharee](https://github.com/zacharee)
* [adamnemecek](https://github.com/adamnemecek)
* [AfzalivE](https://github.com/AfzalivE)
* [akx](https://github.com/akx)
* [Allsochen](https://github.com/Allsochen)
* [ayroblu](https://github.com/ayroblu)
* [britalmeida](https://github.com/britalmeida)
* [Calinou](https://github.com/Calinou)
* [damonpam](https://github.com/damonpam)
* [decodism](https://github.com/decodism)
* [delaneyb](https://github.com/delaneyb)
* [gingerr](https://github.com/gingerr)
* [GrzegorzKazana](https://github.com/GrzegorzKazana)
* [hughlilly](https://github.com/hughlilly)
* [karbassi](https://github.com/karbassi)
* [L1cardo](https://github.com/L1cardo)
* [lwouis](https://github.com/lwouis)
* [metacodes](https://github.com/metacodes)
* [mnin](https://github.com/mnin)
* [MrPaschenko](https://github.com/MrPaschenko)
* [nella17](https://github.com/nella17)
* [notlmn](https://github.com/notlmn)
* [phungtuanhoang1996](https://github.com/phungtuanhoang1996)
* [rbnis](https://github.com/rbnis)
* [Rjevski](https://github.com/Rjevski)
* [ShlomoCode](https://github.com/ShlomoCode)
* [skolj](https://github.com/skolj)
* [stopdesign](https://github.com/stopdesign)
* [xanathar](https://github.com/xanathar)
* [zacharee](https://github.com/zacharee)
## [Localized the app](https://poeditor.com/join/project/8AOEZ0eAZE)
They helped [localize the app](https://poeditor.com/join/project/8AOEZ0eAZE):
* 73
* A
* a774 nn
* Aamirsuleman
* Aarni Koskela
* Abdulelah
* Abdur-Rahman Bilal
* Aden Aziz
* Admin
* Aindriú Mac Giolla Eoin
* Ajaykumarmkaju
* Aku
* Albert Abdilim
* Aleksander Bang-Larsen
* Alex
* Alexander Kilian
* Alexey Eremeev
* Ali
* Ali Gokmen
* Ali. tas103
* Allen Guan
* Alonso Chen
* Ameng
* Anders
* Andreas Wagner
* Andrej Koman
* Andrew Vader
* antoon334
* Anurag Roy
* anushree b
* Arman
* Artem Stankov
* Arthur
* Ash
* blanorama
* bluefirex
* BO the Man
* bovirus
* Bufo
* BV Joshi
* caduellery
* Caner İlhan
* catcore
* changanmoon
* Christian Keilmann
* Chun Fei Lung
* codecantata
* codingalone
* Cong Tri
* Dan
* Dan84
* Darius Rosendahl
* Darko
* David R
* Denis
* Dékány Martin
* dibas-np
* Didier Deschrijver
* Dzhuneyt
* edu_sombra
* EDUARDO
* Eimantas
* Eliezer Shpigelman
* Emir H. KAYNAK
* EP45
* Eric WANTZ
* Ersagun Kuruca
* EUKA
* Eukarya
* Eyelly wu
* fil
* Filipe
* Frangarciasalomon
* Frank
* Friendship1
* Fukuda
* Ganesh Sawant
* Gezimos
* Giacomo Volpi
* Giang
* Github
* Gkostov
* Grzegorz Kazana
* Guillaume
* Gumpanath Puttarassu
* hann-solo
* Haoshuai Xu
* Hao豪
* Heramb Lonkar
* Hjörtur Hjartarson
* Hokuto Kato
* Huandngoc
* Huynh Sang
* Ialiendeg
* Ida Bomholt Dyrholm Jacobsen
* Igor Aradski
* igorpocta
* Indexerrowaty
* Inês Almeida
* isametry
* Ismatulla
* Isthereanybody
* Jaeyong Sung
* Jakob
* Jakob Licina
* Jakub Bartušek
* Jesus Angel Villafuerte
* jiho4
* Jisan
* Joao Vitor Casarin
* Jord Nijhuis
* Jui Yuan Liu
* Jules Beckers
* Julian Nowaczyk
* Julio
* justcontributor
* Kagurazaka Tsuki
* kal
* kant
* Kawarimidoll+git
* kazuki sakamoto
* Kevinsevinche
* KillianG
* Kinyagulovrr
* Klara
* Kostiantyn Ko
* Kristófer Fannar Björnsson
* Kushnee5
* Kuuchi
* Lakshman Kolappan
* Lars
* Lasse Mattila
* Lcwhhh
* Leo
* Lester
* Logan Boissy
* Loic. peron
* Loïc
* LostInCompilation
* Lumaxis
* lwouis
* M
* Malo
* Maplevantablack
* Marc Pla
* Marcus Billman
* Marekscholle
* Marko McLion
* Martin Mitka
* Martin. mitka
* Masayoshi Motojima
* Masih
* Matheus
* Max
* Maximilian Falk
* MaximilianFreitag
* Mehmet Ali Ertorer
* Meriç Bağlayan
* Michael
* Milos M
* Mohammad Al Zouabi
* mohma
* Mohsin Rahman
* Mr. axel. bock
* mrtbts
* MuDraconis
* Muzhenstudent
* Mwolfinspace
* Nathancodes
* Navekar Vishal
* NIKHIL S
* Nikola Rajić
* Nils Fahldieck
* Nilton Souza
* Nmolham
* Nuriddin Islamov
* Ori
* Paul Pichaureau
* Paul Przybyszewski
* Paul. pichaureau
* Pehovorka
* Pendolino
* Petar Shomov
* Peterkim0620
* Petr Kolář
* Petter Johan Olsen
* Piotr Budny
* Quentin. douarre
* Rajesh Kaswala
* rajuneon
* Rami Elsawy
* Raphaël
* Rasmus
* Raymonf
* raz
* Razvan
* rbnis
* rei fujise
* Roccobot
* Roccobot
* Rodrigo Schneider
* Ron Nuss
* rPaasch
* Ryan Nardi
* sa-ha
* Sai Khant Zay Lynn Yaung
* Sami Suleiman
* Saransh
* sawtooth
* Sebastian
* Selcuk Dursun
* Sergey
* Serhii Alekseev
* Seyedparsa Mirtaheri
* Shameem Reza
* SheNeVmerla
* Shivam Bansal
* shlomo
* Sippawit Thammawiset
* Sjur Moshagen
* Sjur.n. moshagen
* Stan Smits
* star-affinity
* Stefan
* Stefan Lange
* Svetoslav Stefanov
* Thebennue
* Thomas Wölk
* Thorsten
* Thorsten
* Tomoa Nozawa
* Trevor
* Tsilimpotis Dimitrios
* Umutakkaya1996
* Vadym
* Vijayant Saini
* Vincent Orback
* Vlad
* Webmaster
* Wesley Matos
* Whatsmine-HaoshuaiXu
* White Vogel
* Wilhelm Wolfgang Gärtner
* Yongsung Yoon
* Yorben
* Yossi Zahn
* ysaito
* Yukai
* yumechan
* Yusuf Caliskan
* Zamaialexander
* Zuhaib Syed
* Τσιλιμπότης Δημ.
* Андрій Кирбаба
* Ивайло
* Пустой
* 林清
* 橙梓
73, A, a774 nn, Aamirsuleman, Aarni Koskela, Abdulelah, Abdur-Rahman Bilal, Aden Aziz, Admin, Aindriú Mac Giolla Eoin, Ajaykumarmkaju, Aku, Albert Abdilim, Aleksander Bang-Larsen, Alex, Alexander Kilian, Alexey Eremeev, Ali, Ali Gokmen, Ali. tas103, Allen Guan, Alonso Chen, Ameng, Anders, Andreas Wagner, Andrej Koman, Andrew Vader, antoon334, Anurag Roy, anushree b, Arman, Artem Stankov, Arthur, Ash, blanorama, bluefirex, BO the Man, bovirus, Bufo, BV Joshi, caduellery, Caner İlhan, catcore, changanmoon, Christian Keilmann, Chun Fei Lung, codecantata, codingalone, Cong Tri, Dan, Dan84, Darius Rosendahl, Darko, David R, Denis, Dékány Martin, dibas-np, Didier Deschrijver, Dzhuneyt, edu_sombra, EDUARDO, Eimantas, Eliezer Shpigelman, Emir H. KAYNAK, EP45, Eric WANTZ, Ersagun Kuruca, EUKA, Eukarya, Eyelly wu, fil, Filipe, Frangarciasalomon, Frank, Friendship1, Fukuda, Ganesh Sawant, Gezimos, Giacomo Volpi, Giang, Github, Gkostov, Grzegorz Kazana, Guillaume, Gumpanath Puttarassu, hann-solo, Haoshuai Xu, Hao豪, Heramb Lonkar, Hjörtur Hjartarson, Hokuto Kato, Huandngoc, Huynh Sang, Ialiendeg, Ida Bomholt Dyrholm Jacobsen, Igor Aradski, igorpocta, Indexerrowaty, Inês Almeida, isametry, Ismatulla, Isthereanybody, Jaeyong Sung, Jakob, Jakob Licina, Jakub Bartušek, Jesus Angel Villafuerte, jiho4, Jisan, Joao Vitor Casarin, Jord Nijhuis, Jui Yuan Liu, Jules Beckers, Julian Nowaczyk, Julio, justcontributor, Kagurazaka Tsuki, kal, kant, Kawarimidoll+git, kazuki sakamoto, Kevinsevinche, KillianG, Kinyagulovrr, Klara, Kostiantyn Ko, Kristófer Fannar Björnsson, Kushnee5, Kuuchi, Lakshman Kolappan, Lars, Lasse Mattila, Lcwhhh, Leo, Lester, Logan Boissy, Loic. peron, Loïc, LostInCompilation, Lumaxis, lwouis, M, Malo, Maplevantablack, Marc Pla, Marcus Billman, Marekscholle, Marko McLion, Martin Mitka, Martin. mitka, Masayoshi Motojima, Masih, Matheus, Max, Maximilian Falk, MaximilianFreitag, Mehmet Ali Ertorer, Meriç Bağlayan, Michael, Milos M, Mohammad Al Zouabi, mohma, Mohsin Rahman, Mr. axel. bock, mrtbts, MuDraconis, Muzhenstudent, Mwolfinspace, Nathancodes, NIKHIL S, Nikola Rajić, Nils Fahldieck, Nilton Souza, Nmolham, Nuriddin Islamov, Ori, Paul Pichaureau, Paul Przybyszewski, Paul. pichaureau, Pehovorka, Pendolino, Petar Shomov, Peterkim0620, Petr Kolář, Petter Johan Olsen, Piotr Budny, Quentin. douarre, Rajesh Kaswala, rajuneon, Rami Elsawy, Raphaël, Rasmus, Raymonf, raz, Razvan, rbnis, rei fujise, Roccobot, Roccobot, Rodrigo Schneider, Ron Nuss, rPaasch, Ryan Nardi, sa-ha, Sai Khant Zay Lynn Yaung, Sami Suleiman, Samimkel00, Saransh, sawtooth, Sebastian, Selcuk Dursun, Sergey, Serhii Alekseev, Seyedparsa Mirtaheri, Shameem Reza, SheNeVmerla, Shivam Bansal, shlomo, Sippawit Thammawiset, Sjur Moshagen, Sjur.n. moshagen, Stan Smits, star-affinity, Stefan, Stefan Lange, Svetoslav Stefanov, Tamilselvan0x0, Thebennue, Thomas Wölk, Thorsten, Thorsten, Tomoa Nozawa, Trevor, Tsilimpotis Dimitrios, Umutakkaya1996, Vadym, Vijayant Saini, Vincent Orback, Vlad, Webmaster, Wesley Matos, Whatsmine-HaoshuaiXu, White Vogel, Wilhelm Wolfgang Gärtner, Yongsung Yoon, Yorben, Yossi Zahn, ysaito, Yukai, yumechan, Yusuf Caliskan, Zamaialexander, Zuhaib Syed, Τσιλιμπότης Δημ., Андрій Кирбаба, Ивайло, Пустой, 林清, 橙梓
+36
View File
@@ -0,0 +1,36 @@
## Can it show, or hide, this window?
It's sometimes hard to say that some dialog, HUD, UI element, is a window or not. Not everyone will agree that a particular window is a window. AltTab does its best to decide what a window is, and show it to the user.
Some apps are not native macOS apps, or poorly programmed. Such apps will use "windows" to code things like menus, and play tricks like hidding a window instead of closing it. Notorious examples: Electron apps (e.g. Slack, Discord, etc) and apps using cross-platform SDKs (e.g. Adobe apps, Steam, IntelliJ, etc).
If a window is showing, and you think it shouldn't, please contact the app maker so they can fix their product.
## Can it show individual browser tabs?
Browser windows are not standard windows or tabs. They are custom UI painted by each browser, in their own unique way. There is no macOS API to screenshot a browser tab. Some browser may not even render a tab that's not active, so there are no pixels to screenshot.
Browser may offer custom integration to know about tabs, screenshot, active them. However, each browser is unique, and it would require to write these integrations for each one, as well as to maintain them. Browsers evolve fast, so maintaining multiple fast-changing integrations is not attainable.
## Can we add window-management features?
AltTab is not intended to be a window manager. It will not add features such as: resizing windows, organizing them into tiles, Dock enhancements.
[Other apps](http://localhost:4000/#alternatives) offer such experiences.
AltTab aims at being the best at the Windows-style alt+tab experience.
## Can it be on the App Store?
AltTab uses **many** private APIs. This prevents us from publishing on the App Store. It's a shame. Many more casual users would benefit from it. However, without these private APIs, the user experience would be crippled. We use private APIs for refined details, but also for basic functionality.
## Where are settings stored?
The app uses the standard macOS API to store settings. You can interact with them from the Terminal, using the `defaults` command:
* List current settings: `defaults read com.lwouis.alt-tab-macos.plist`
* Update a setting: `defaults write com.lwouis.alt-tab-macos.plist AppleAccentColor -int 4`
* Export settings to a file: `defaults export com.lwouis.alt-tab-macos.plist /tmp/my-export.plist`
## Can I share an idea or report a bug?
Please open an issue on [GitHub](https://github.com/lwouis/alt-tab-macos/issues)
+25 -12
View File
@@ -2,13 +2,18 @@
permalink: /
---
# Introduction
[<img src="public/demo/frontpage.jpg" alt="Screenshot showing AltTab" width="1920" height="1080" style="width:100%; height:auto;" />](public/demo/frontpage.jpg)
[<img src="public/demo/frontpage.jpg" alt="Screenshot showing AltTab" width="1920" height="1080" style="width:100%; height:auto;" class="can-extend" />](public/demo/frontpage.jpg)
Alternative Styles
| [<img loading="lazy" src="public/demo/app-icons-style.jpg" alt="Screenshot: App Icons Style" width="513" height="241" style="width:100%; height:auto;" />](public/demo/app-icons-style.jpg) | [<img loading="lazy" src="public/demo/titles-style.jpg" alt="Screenshot: Titles Style" width="766" height="248" style="width:100%; height:auto;" />](public/demo/titles-style.jpg) |
<div class="can-extend" style="display:flex; align-items:center; gap:10px;">
<a href="public/demo/app-icons-style.jpg">
<img loading="lazy" src="public/demo/app-icons-style.jpg" alt="Screenshot: App Icons Style" width="513" height="241" style="width:100%; height:auto;" />
</a>
<a href="public/demo/titles-style.jpg">
<img loading="lazy" src="public/demo/titles-style.jpg" alt="Screenshot: Titles Style" width="766" height="248" style="width:100%; height:auto;" />
</a>
</div>
## Installation
@@ -33,22 +38,30 @@ Alternative Styles
## Localization
AltTab is available in many languages: Bahasa Indonesia, Basa Jawa, Català, Čeština, Dansk, Deutsch, Eesti keel, English, Español, Français, Gaeilge, Galego, Hrvatski, Íslenska, Italiano, Kurdî, Lietuvių, Lëtzebuergesch, Magyar, Nederlands, Norsk Bokmål, Norsk Nynorsk, Oʻzbekcha, Polski, Português, Português (Brasil), Română, Shqip, Slovenčina, Slovenščina, Suomi, Svenska, Tiếng Việt, Türkçe, Български, Русский, Српски, Українська, Ελληνικά, עִבְרִית, العربية, فارسی, हिन्दी, বাংলা, ગુજરાતી, தமிழ், ಕನ್ನಡ, മലയാളം, ภาษาไทย, 日本語, 简体中文, 繁體中文, 繁體中文 (香港), 한국어
AltTab is available in many languages: Bahasa Indonesia, Basa Jawa, Català, Čeština, Dansk, Deutsch, Eesti keel, English, Español, Français, Gaeilge, Galego, Hrvatski, Íslenska, Italiano, Kurdî, Lietuvių, Lëtzebuergesch, Magyar, Nederlands, Norsk Bokmål, Norsk Nynorsk, Oʻzbekcha, Polski, Português, Português (Brasil), Română, Shqip, Slovenčina, Slovenščina, Suomi, Svenska, Tiếng Việt, Türkçe, Беларуская мова, Български, Русский, Српски, Українська, Ελληνικά, עִבְרִית, العربية, فارسی, हिन्दी, বাংলা, ગુજરાતી, தமிழ், ಕನ್ನಡ, മലയാളം, ภาษาไทย, 日本語, 简体中文, 繁體中文, 繁體中文 (香港), 한국어
[Contribute your own language easily!](https://poeditor.com/join/project/8AOEZ0eAZE)
## Privacy and respecting the user
## Privacy
* AltTab doesn’t upload or download any data without the user explicitly asking for it (e.g. asking to update the app, sending a crash report, etc)
* AltTab uses as few resources as possible: CPU, memory, disk, etc. The app footprint is small (e.g. small disk size, minimal CPU use in the background, etc)
AltTab doesn’t communicate to any server for so-called telemetry. AltTab will only use the network if it's absolutely required (e.g. checking for updates).
## Performance
AltTab has been designed to take as few resources as possible on the user computer. The app itself is kept as small as possible. CPU usage is kept to a minimum, especially while in the background. The goal is to respect the user and try to have no negative impact on their experience.
## Configuration
Change the shortcut keys, switch to a Windows theme and more, using the Preferences window:
Change the shortcut keys, switch to a Windows theme and more, using the Settings window:
| [<img loading="lazy" src="public/demo/preferences-general.jpg" alt="Screenshot: General preferences" width="672" height="354" style="width:100%; height:auto;" />](public/demo/preferences-general.jpg) | [<img loading="lazy" src="public/demo/preferences-controls.jpg" alt="Screenshot: Controls preferences" width="677" height="688" style="width:100%; height:auto;" />](public/demo/preferences-controls.jpg) |
| [<img loading="lazy" src="public/demo/preferences-appearance.jpg" alt="Screenshot: Appearance preferences" width="672" height="735" style="width:100%; height:auto;" />](public/demo/preferences-appearance.jpg) | [<img loading="lazy" src="public/demo/preferences-appearance-details.jpg" alt="Screenshot: Appearance preferences (details)" width="672" height="934" style="width:100%; height:auto;" />](public/demo/preferences-appearance-details.jpg) |
| [<img loading="lazy" src="public/demo/preferences-blacklist.jpg" alt="Screenshot: Blacklist preferences" width="672" height="655" style="width:100%; height:auto;" />](public/demo/preferences-blacklist.jpg) | [<img loading="lazy" src="public/demo/preferences-policies.jpg" alt="Screenshot: Policies preferences" width="672" height="353" style="width:100%; height:auto;" />](public/demo/preferences-policies.jpg) |
<div class="can-extend" style="display:flex; align-items:center; flex-wrap: wrap;">
<a href="public/demo/preferences-general.jpg"><img loading="lazy" src="public/demo/preferences-general.jpg" alt="Screenshot: General preferences" width="672" height="354" style="width:100%; height:auto; max-width:330px;" /></a>
<a href="public/demo/preferences-controls.jpg"><img loading="lazy" src="public/demo/preferences-controls.jpg" alt="Screenshot: Controls preferences" width="677" height="688" style="width:100%; height:auto; max-width:330px;" /></a>
<a href="public/demo/preferences-appearance.jpg"><img loading="lazy" src="public/demo/preferences-appearance.jpg" alt="Screenshot: Appearance preferences" width="672" height="735" style="width:100%; height:auto; max-width:330px;" /></a>
<a href="public/demo/preferences-appearance-details.jpg"><img loading="lazy" src="public/demo/preferences-appearance-details.jpg" alt="Screenshot: Appearance preferences (details)" width="672" height="934" style="width:100%; height:auto; max-width:330px;" /></a>
<a href="public/demo/preferences-blacklist.jpg"><img loading="lazy" src="public/demo/preferences-blacklist.jpg" alt="Screenshot: Blacklist preferences" width="672" height="655" style="width:100%; height:auto; max-width:330px;" /></a>
<a href="public/demo/preferences-policies.jpg"><img loading="lazy" src="public/demo/preferences-policies.jpg" alt="Screenshot: Policies preferences" width="672" height="353" style="width:100%; height:auto; max-width:330px;" /></a>
</div>
## CLI usage
+1 -1
View File
@@ -1,4 +1,4 @@
# Support this project ❤️
<h1 style="text-align:center">Support this project ❤️</h1>
<div class="support-buttons">
<div class="support-button-wrapper">
+526 -300
View File
@@ -1,343 +1,569 @@
$width: 700px;
html, body {
width: 100%;
line-height: 1.6;
margin: 0;
}
* {
box-sizing: border-box;
//margin: 0;
//padding: 0;
}
body {
display: flex;
flex-direction: column;
align-items: center;
html, body {
margin: 0;
height: 100%;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
//background-color: #a9e7da;
overflow-wrap: anywhere;
}
header {
width: 100%;
:root {
--sidebar-bg: #f1f7ff;
--content-bg: #ffffff;
--item-bg: #cfe0ff;
--subitem-bg: #b9d0ff;
--border: #c0d3ff;
}
a {
color: #0366d6;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
code, pre, tt {
padding: .2em .4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, .05);
border-radius: 3px;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: normal;
}
h1 {
margin-top: 0.2em;
}
h2 {
font-size: 1.3em;
}
h3 {
font-size: 1.1em;
font-weight: 500;
}
table {
border-collapse: collapse;
thead {
text-align: left;
background-color: #eee;
}
thead {
border-bottom: 7px solid #fff;
}
tr {
border-bottom: 7px solid #fff;
border-top: 7px solid #fff;
vertical-align: baseline;
}
tr:first-child {
border-bottom: none;
}
tr:last-child {
border-top: none;
}
td {
padding: 0;
}
td, th {
border-left: 5px solid #fff;
border-right: 5px solid #fff;
}
td:first-child, th:first-child {
border-left: none;
}
td:last-child, th:last-child {
border-right: none;
}
}
li + li {
margin-top: .25em;
}
/* ---------- Root ---------- */
.layout {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
margin: 20px 0 5px 10px;
line-height: 1;
/* ---------- Desktop layout ---------- */
.app {
display: flex;
justify-content: center;
height: 100%;
overflow: hidden;
background-color: #dccb921c;
}
/* ---------- Sidebar ---------- */
.sidebar {
flex-shrink: 0;
width: 262px;
background: var(--sidebar-bg);
//border-right: 2px solid var(--border);
box-shadow: rgb(216, 231, 251) 0px 2px 4px 1px;
overflow-y: auto;
padding: 16px;
#turbolinks-header {
#logo {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 10px;
margin-bottom: 25px;
}
a {
color: #000;
text-decoration: none;
}
a.button.rainbow {
position: relative;
display: inline-block;
z-index: 0;
background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
background-size: 300% 300%;
animation: animatedgradient 5s linear infinite;
color: #fff;
font-size: 120%;
padding: 0.4em 0.9em;
border-radius: 3em;
margin-right: 0;
&::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
z-index: -1;
background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
background-size: 300% 300%;
animation: animatedgradient 5s linear infinite;
filter: blur(8px);
opacity: 0;
}
&:hover::before {
opacity: 0.9;
}
&:active::before {
opacity: 0;
}
}
h1 {
margin: 0;
padding-bottom: 5px;
line-height: 1;
a {
color: #000;
}
}
p {
margin: 0;
color: #666;
}
#title-and-subtitle {
display: flex;
flex-direction: column;
padding-left: 10px;
}
#download-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 25px;
div {
font-size: 0.7em;
margin-top: 5px;
color: #646464;
}
}
@keyframes animatedgradient {
0% {
background-position: 0% 0%;
}
25% {
background-position: 50% 50%;
}
50% {
background-position: 100% 10%;
}
75% {
background-position: 50% 50%;
}
100% {
background-position: 0% 0%;
}
}
}
}
p {
margin: 0 0 0 10px;
color: #666;
}
/* ---------- Menu ---------- */
.menu,
.menu ul {
list-style: none;
padding: 0;
margin: 0;
padding-left: 10px;
}
.menu-item {
margin-bottom: 8px;
a {
text-decoration: none;
}
& > div {
display: flex;
margin-top: 5px;
color: #468ed8;
}
}
nav {
position: sticky;
z-index: 1000000;
top: 0;
width: 100%;
//max-width: $width;
//background-color: rgba(255, 255, 255, 1);
//background-color: #dff8ff;
background-color: #f1f7ff;
.menu-item > input {
display: none;
}
.menu-label {
display: flex;
align-items: center;
flex-direction: column;
//background-color: rgba(255, 255, 255, 0.9);
//backdrop-filter: blur(10px);
gap: 6px;
//background: var(--item-bg);
padding: 6px 0;
cursor: pointer;
font-size: 16px;
ul {
list-style: none;
margin: 0;
padding: 10px;
display: flex;
flex-wrap: wrap;
a {
display: block;
text-decoration: none;
padding: 5px 10px;
border-radius: 15px;
color: #468ed8;
margin-right: 5px;
span {
padding-bottom: 1px;
border-bottom: #468ed8 solid 1px;
}
&:hover, &.active {
text-decoration: none;
background-color: #d5e6ff;
//color: #222;
//box-shadow: #222 0px 1px;
span {
border-bottom: none;
}
}
}
a.support {
color: #c10029;
span {
border-bottom: #c10029 solid 1px;
}
&:hover, &.active {
background: #ffd5ce;
color: #c10029;
span {
border-bottom: none;
}
}
}
a.rainbow {
background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
animation: animatedgradient 5s linear alternate infinite;
background-size: 300% 300%;
text-decoration: none;
color: #fff;
//box-shadow: #222 0px 1px;
&:hover {
//color: #000;
animation-play-state: paused;
}
}
&.active, &:hover {
text-decoration: none;
font-weight: bold;
//color: #0052a7;
}
@keyframes animatedgradient {
0% {
background-position: 0% 0%;
}
25% {
background-position: 50% 50%;
}
50% {
background-position: 100% 10%;
}
75% {
background-position: 50% 50%;
}
100% {
background-position: 0% 0%;
&.support {
color: #c10029;
}
}
ul.submenu {
padding-left: 28px;
border-left: 1px solid var(--border);
margin-left: 1px;
font-size: 15px;
max-height: 300px;
overflow: auto;
a.active, a:hover {
text-decoration: none;
font-weight: bold;
//color: #0052a7;
}
}
/* ---------- Content ---------- */
.content {
max-width: 1048px;
flex-shrink: 1;
flex-grow: 1;
background: var(--content-bg);
overflow-y: auto;
padding: 24px;
& > div {
max-width: 700px;
.can-extend {
width: calc(100vw - 310px) !important;
max-width: 1000px;
}
}
}
.page {
width: 100%;
max-width: $width;
padding-left: 10px;
padding-right: 10px;
word-break: break-word;
/* ---------- Mobile ---------- */
#menu-toggle {
display: none;
}
img {
.hamburger-bar {
display: none;
height: 60px;
background: var(--sidebar-bg);
//border-bottom: 2px solid var(--border);
box-shadow: rgb(216, 231, 251) 0px 2px 4px 1px;
padding: 8px;
align-items: center;
#mobile-logo {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
gap: 10px;
& > div {
display: flex;
flex-direction: column;
a {
text-decoration: none;
color: black;
font-weight: bold;
}
& > span:last-child {
margin-top: 4px;
font-size: 13px;
color: #666666;
}
}
}
}
.hamburger-label {
background: var(--item-bg);
padding: 10px 15px;
border-radius: 15px;
cursor: pointer;
font-size: 13px;
}
/* ---------- Mobile behavior ---------- */
@media (max-width: 800px) {
#turbolinks-header {
display: none;
}
.hamburger-bar {
display: flex;
}
.app {
position: relative;
}
.sidebar {
position: fixed;
top: 55px;
//left: 0;
right: 0;
//bottom: 0;
width: auto;
transform: translateY(-100%);
transition: transform 0.2s ease;
z-index: 10;
padding: 14px 24px 0 14px;
}
/* FIX: checkbox now controls sidebar */
#menu-toggle:checked ~ .app .sidebar {
transform: translateY(0);
}
.content {
padding-top: 24px;
& > div {
max-width: unset;
.can-extend {
width: 100% !important;
}
}
}
/* Toggle text */
.close {
display: none;
}
#menu-toggle:checked + .hamburger-bar .open {
display: none;
}
#menu-toggle:checked + .hamburger-bar .close {
display: inline;
}
.sidebar {
pointer-events: none; /* closed = not clickable */
}
#menu-toggle:checked ~ .app .sidebar {
transform: translateY(0);
pointer-events: auto; /* open = clickable */
}
.hamburger-bar {
position: relative;
z-index: 20; /* always above sidebar */
}
}
/* ---------- Support Page ------------- */
.support-buttons {
display: flex;
justify-content: center;
column-gap: 40px;
row-gap: 55px;
flex-wrap: wrap;
margin-bottom: 70px;
/* Wrapper: each button keeps its mascot */
.support-button-wrapper {
position: relative;
display: inline-block;
height: 120px; /* ensures space for mascot below */
}
/* Button shape + style */
.support-button {
width: 130px;
height: 50px;
border-radius: 22px;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: linear-gradient(145deg, #ffffff, #dce8ff);
box-shadow:
4px 4px 10px rgba(0,0,0,0.15),
-4px -4px 10px rgba(255,255,255,0.7);
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
/* Shine streak */
.support-button::before {
content: "";
position: absolute;
top: 0;
left: -150%;
width: 50%;
height: 100%;
background: linear-gradient(
120deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0.55) 50%,
rgba(255,255,255,0) 100%
);
transform: skewX(-20deg);
opacity: 0;
}
.support-button:hover::before {
animation: shine-sweep 0.8s forwards;
opacity: 1;
}
@keyframes shine-sweep {
0% { left: -150%; }
100% { left: 200%; }
}
/* Button image */
.support-button img {
width: 100%;
height: auto;
height: 100%;
object-fit: contain;
object-position: center;
}
code, pre, tt {
padding: .2em .4em;
/* Hover lift */
.support-button:hover {
transform: scale(1.08);
box-shadow:
6px 6px 15px rgba(0,0,0,0.18),
-6px -6px 15px rgba(255,255,255,0.8);
}
/* Mascots */
.mascot {
position: absolute;
width: 60px;
bottom: -20px;
left: 50%;
transform: translateX(-50%) translateY(0);
transition: transform 0.3s;
pointer-events: none;
}
.rabbit { width: 50px } /* long ears '^^ /*
/* Hover reactions */
.support-button-wrapper:hover .cat { transform: translateX(-50%) translateY(-20px) rotate(-15deg); }
.support-button-wrapper:hover .dog { transform: translateX(-50%) translateY(-20px) scaleX(-1); }
.support-button-wrapper:hover .squirrel { transform: translateX(-50%) translateY(-20px) rotate(15deg); }
.support-button-wrapper:hover .rabbit { transform: translateX(-50%) translateY(-20px) rotate(-10deg); }
.support-button img.paypal { width: 60%; margin-top: 6px; }
.support-button img.github-sponsors { width: 110%; margin-top: 6px; }
.support-button img.stripe { width: 51%; margin-top: 4px; }
}
#changelog {
margin-top: -20px;
padding: 15px;
#support-link {
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, .05);
border-radius: 3px;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: normal;
position: fixed;
top: 15px;
right: 10px;
border-radius: 0.5em;
padding: 0.15em 0.9em;
font-size: 13px;
background: #ffdbd5;
/*background: #f91675;*/
color: #950020;
/*color: white;*/
font-weight: normal;
}
h2 {
font-size: 1.3em;
h1, h2 {
margin-top: 20px;
margin-bottom: 10px;
font-size: 20px;
background-color: #f1f7ff;
display: inline-block;
}
h3 {
font-size: 1.1em;
font-weight: 500;
font-size: 13px;
font-weight: bold;
margin: 0;
}
table {
border-collapse: collapse;
thead {
text-align: left;
background-color: #eee;
}
thead {
border-bottom: 7px solid #fff;
}
tr {
border-bottom: 7px solid #fff;
border-top: 7px solid #fff;
vertical-align: baseline;
}
tr:first-child {
border-bottom: none;
}
tr:last-child {
border-top: none;
}
td {
padding: 0;
}
td, th {
border-left: 5px solid #fff;
border-right: 5px solid #fff;
}
td:first-child, th:first-child {
border-left: none;
}
td:last-child, th:last-child {
border-right: none;
}
}
li + li {
margin-top: .25em;
}
a {
color: #0366d6;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
/* Button row */
.support-buttons {
display: flex;
justify-content: center;
column-gap: 40px;
row-gap: 55px;
flex-wrap: wrap;
margin-bottom: 70px;
/* Wrapper: each button keeps its mascot */
.support-button-wrapper {
position: relative;
display: inline-block;
height: 120px; /* ensures space for mascot below */
}
/* Button shape + style */
.support-button {
width: 130px;
height: 50px;
border-radius: 22px;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: linear-gradient(145deg, #ffffff, #dce8ff);
box-shadow:
4px 4px 10px rgba(0,0,0,0.15),
-4px -4px 10px rgba(255,255,255,0.7);
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
/* Shine streak */
.support-button::before {
content: "";
position: absolute;
top: 0;
left: -150%;
width: 50%;
height: 100%;
background: linear-gradient(
120deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0.55) 50%,
rgba(255,255,255,0) 100%
);
transform: skewX(-20deg);
opacity: 0;
}
.support-button:hover::before {
animation: shine-sweep 0.8s forwards;
opacity: 1;
}
@keyframes shine-sweep {
0% { left: -150%; }
100% { left: 200%; }
}
/* Button image */
.support-button img {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
/* Hover lift */
.support-button:hover {
transform: scale(1.08);
box-shadow:
6px 6px 15px rgba(0,0,0,0.18),
-6px -6px 15px rgba(255,255,255,0.8);
}
/* Mascots */
.mascot {
position: absolute;
width: 60px;
bottom: -20px;
left: 50%;
transform: translateX(-50%) translateY(0);
transition: transform 0.3s;
pointer-events: none;
}
.rabbit { width: 50px } /* long ears '^^ /*
/* Hover reactions */
.support-button-wrapper:hover .cat { transform: translateX(-50%) translateY(-20px) rotate(-15deg); }
.support-button-wrapper:hover .dog { transform: translateX(-50%) translateY(-20px) scaleX(-1); }
.support-button-wrapper:hover .squirrel { transform: translateX(-50%) translateY(-20px) rotate(15deg); }
.support-button-wrapper:hover .rabbit { transform: translateX(-50%) translateY(-20px) rotate(-10deg); }
.support-button img.paypal { width: 60%; margin-top: 6px; }
.support-button img.github-sponsors { width: 110%; margin-top: 6px; }
.support-button img.stripe { width: 51%; margin-top: 4px; }
ul {
margin: 0 0 5px 0;
}
}
+55
View File
@@ -0,0 +1,55 @@
function slugify(text) {
return text.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-');
}
function buildTOC() {
const urlParts = window.location.pathname.split('/');
const currentPage = urlParts[urlParts.length - 1] || ""; // fallback
const tocContainer = currentPage === "" ? document.querySelector('.submenu') : document.querySelector(`.submenu.${currentPage}`);
if (!tocContainer) return;
tocContainer.innerHTML = ''; // clear old TOC
const headings = document.querySelectorAll('.content h1, .content h2');
if (!headings.length || headings.length === 1) {
tocContainer.style.display = 'none';
return;
}
const usedIds = new Set();
headings.forEach((heading) => {
let id = heading.id;
if (!id) {
id = slugify(heading.textContent);
let uniqueId = id;
let i = 1;
while (usedIds.has(uniqueId) || document.getElementById(uniqueId)) {
uniqueId = `${id}-${i++}`;
}
id = uniqueId;
heading.id = id;
}
usedIds.add(id);
const li = document.createElement('li');
li.className = `toc-${heading.tagName.toLowerCase()}`;
const a = document.createElement('a');
a.href = `#${id}`;
a.textContent = heading.textContent;
a.setAttribute('data-turbolinks', 'false');
li.appendChild(a);
tocContainer.appendChild(li);
});
const hash = window.location.hash;
if (!hash) return;
// remove existing .active, if exists
const existingLink = document.querySelector(`.submenu a.active`);
if (existingLink) {
existingLink.classList.remove('active');
}
// add new .active, if exists
const link = document.querySelector(`a[href="${hash}"]`);
if (link) {
link.classList.add('active');
}
}
document.addEventListener('turbolinks:load', buildTOC);
window.addEventListener('hashchange', buildTOC);
+2
View File
@@ -0,0 +1,2 @@
(function(){var t=this;(function(){(function(){this.Turbolinks={supported:function(){return null!=window.history.pushState&&null!=window.requestAnimationFrame&&null!=window.addEventListener}(),visit:function(t,r){return e.controller.visit(t,r)},clearCache:function(){return e.controller.clearCache()},setProgressBarDelay:function(t){return e.controller.setProgressBarDelay(t)}}}).call(this)}).call(t);var e=t.Turbolinks;(function(){(function(){var t,r,n,o=[].slice;e.copyObject=function(t){var e,r,n;r={};for(e in t)n=t[e],r[e]=n;return r},e.closest=function(e,r){return t.call(e,r)},t=function(){var t,e;return t=document.documentElement,null!=(e=t.closest)?e:function(t){var e;for(e=this;e;){if(e.nodeType===Node.ELEMENT_NODE&&r.call(e,t))return e;e=e.parentNode}}}(),e.defer=function(t){return setTimeout(t,1)},e.throttle=function(t){var e;return e=null,function(){var r;return r=1<=arguments.length?o.call(arguments,0):[],null!=e?e:e=requestAnimationFrame(function(n){return function(){return e=null,t.apply(n,r)}}(this))}},e.dispatch=function(t,e){var r,o,i,s,a,u;return a=null!=e?e:{},u=a.target,r=a.cancelable,o=a.data,i=document.createEvent("Events"),i.initEvent(t,!0,r===!0),i.data=null!=o?o:{},i.cancelable&&!n&&(s=i.preventDefault,i.preventDefault=function(){return this.defaultPrevented||Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}}),s.call(this)}),(null!=u?u:document).dispatchEvent(i),i},n=function(){var t;return t=document.createEvent("Events"),t.initEvent("test",!0,!0),t.preventDefault(),t.defaultPrevented}(),e.match=function(t,e){return r.call(t,e)},r=function(){var t,e,r,n;return t=document.documentElement,null!=(e=null!=(r=null!=(n=t.matchesSelector)?n:t.webkitMatchesSelector)?r:t.msMatchesSelector)?e:t.mozMatchesSelector}(),e.uuid=function(){var t,e,r;for(r="",t=e=1;36>=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){e.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.requestURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=t(this.requestCanceled,this),this.requestTimedOut=t(this.requestTimedOut,this),this.requestFailed=t(this.requestFailed,this),this.requestLoaded=t(this.requestLoaded,this),this.requestProgressed=t(this.requestProgressed,this),this.url=e.Location.wrap(n).requestURL,this.referrer=e.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){return t.Line truncated
},r.prototype.cacheSnapshot=function(){var t,r;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),r=this.view.getSnapshot(),t=this.lastRenderedLocation,e.defer(function(e){return function(){return e.cache.put(t,r.clone())}}(this))):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=this.view.getElementForAnchor(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r),this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(t,r){return e.dispatch("turbolinks:click",{target:t,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(t){return e.dispatch("turbolinks:before-visit",{data:{url:t.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(t){return e.dispatch("turbolinks:visit",{data:{url:t.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return e.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(t){return e.dispatch("turbolinks:before-render",{data:{newBody:t}})},r.prototype.notifyApplicationAfterRender=function(){return e.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(t){return null==t&&(t={}),e.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:t}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(t,r,n){var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new e.Visit(this,t,r),u.restorationIdentifier=null!=a?a:e.uuid(),u.restorationData=e.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(t){return this.nodeIsVisitable(t)?e.closest(t,"a[href]:not([target]):not([download])"):void 0},r.prototype.getVisitableLocationForLink=function(t){var r;return r=new e.Location(t.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(t){var r;return(r=e.closest(t,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){!function(){var t,e;if((t=e=document.currentScript)&&!e.hasAttribute("data-turbolinks-suppress-warning"))for(;t=t.parentNode;)if(t===document.body)return console.warn("You are loading Turbolinks from a <script> element inside the <body> element. This is probably not what you meant to do!\n\nLoad your application\u2019s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\nFor more information,Line truncated
@@ -16,9 +16,10 @@
<body>
<section class="page">
<div id="changelog">
<a id="support-link" href="https://alt-tab-macos.netlify.app/support">Support this project ❤️</a>
{{ content }}
</section>
</div>
</body>
</html>
+85 -40
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en-us">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
@@ -7,18 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Windows alt-tab on macOS">
<title>
{% if page.title == "Introduction" %} {{ site.title }} - {{ site.description }}
{% else %} {{ site.title }} - {{ page.title }} {% endif %}
</title>
<style type="text/css">
{% capture main %}
{% include main.scss %}
{% endcapture %}
{{ main | scssify }}
</style>
<!-- Generated with https://realfavicongenerator.net/ -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ site.baseurl }}public/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ site.baseurl }}public/favicon-32x32.png">
@@ -28,38 +16,95 @@
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<script>
(function(){var t=this;(function(){(function(){this.Turbolinks={supported:function(){return null!=window.history.pushState&&null!=window.requestAnimationFrame&&null!=window.addEventListener}(),visit:function(t,r){return e.controller.visit(t,r)},clearCache:function(){return e.controller.clearCache()},setProgressBarDelay:function(t){return e.controller.setProgressBarDelay(t)}}}).call(this)}).call(t);var e=t.Turbolinks;(function(){(function(){var t,r,n,o=[].slice;e.copyObject=function(t){var e,r,n;r={};for(e in t)n=t[e],r[e]=n;return r},e.closest=function(e,r){return t.call(e,r)},t=function(){var t,e;return t=document.documentElement,null!=(e=t.closest)?e:function(t){var e;for(e=this;e;){if(e.nodeType===Node.ELEMENT_NODE&&r.call(e,t))return e;e=e.parentNode}}}(),e.defer=function(t){return setTimeout(t,1)},e.throttle=function(t){var e;return e=null,function(){var r;return r=1<=arguments.length?o.call(arguments,0):[],null!=e?e:e=requestAnimationFrame(function(n){return function(){return e=null,t.apply(n,r)}}(this))}},e.dispatch=function(t,e){var r,o,i,s,a,u;return a=null!=e?e:{},u=a.target,r=a.cancelable,o=a.data,i=document.createEvent("Events"),i.initEvent(t,!0,r===!0),i.data=null!=o?o:{},i.cancelable&&!n&&(s=i.preventDefault,i.preventDefault=function(){return this.defaultPrevented||Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}}),s.call(this)}),(null!=u?u:document).dispatchEvent(i),i},n=function(){var t;return t=document.createEvent("Events"),t.initEvent("test",!0,!0),t.preventDefault(),t.defaultPrevented}(),e.match=function(t,e){return r.call(t,e)},r=function(){var t,e,r,n;return t=document.documentElement,null!=(e=null!=(r=null!=(n=t.matchesSelector)?n:t.webkitMatchesSelector)?r:t.msMatchesSelector)?e:t.mozMatchesSelector}(),e.uuid=function(){var t,e,r;for(r="",t=e=1;36>=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){e.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.requestURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=t(this.requestCanceled,this),this.requestTimedOut=t(this.requestTimedOut,this),this.requestFailed=t(this.requestFailed,this),this.requestLoaded=t(this.requestLoaded,this),this.requestProgressed=t(this.requestProgressed,this),this.url=e.Location.wrap(n).requestURL,this.referrer=e.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){rLine truncated
},r.prototype.cacheSnapshot=function(){var t,r;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),r=this.view.getSnapshot(),t=this.lastRenderedLocation,e.defer(function(e){return function(){return e.cache.put(t,r.clone())}}(this))):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=this.view.getElementForAnchor(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r),this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(t,r){return e.dispatch("turbolinks:click",{target:t,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(t){return e.dispatch("turbolinks:before-visit",{data:{url:t.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(t){return e.dispatch("turbolinks:visit",{data:{url:t.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return e.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(t){return e.dispatch("turbolinks:before-render",{data:{newBody:t}})},r.prototype.notifyApplicationAfterRender=function(){return e.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(t){return null==t&&(t={}),e.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:t}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(t,r,n){var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new e.Visit(this,t,r),u.restorationIdentifier=null!=a?a:e.uuid(),u.restorationData=e.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(t){return this.nodeIsVisitable(t)?e.closest(t,"a[href]:not([target]):not([download])"):void 0},r.prototype.getVisitableLocationForLink=function(t){var r;return r=new e.Location(t.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(t){var r;return(r=e.closest(t,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){!function(){var t,e;if((t=e=document.currentScript)&&!e.hasAttribute("data-turbolinks-suppress-warning"))for(;t=t.parentNode;)if(t===document.body)return console.warn("You are loading Turbolinks from a <script> element inside the <body> element. This is probably not what you meant to do!\n\nLoad your application\u2019s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\nFor more infoLine truncated
</script>
<title>
{% if page.name == "Introduction.md" %} {{ site.title }} - {{ site.description }}
{% else %} {{ site.title }} - {{ page.name | remove: ".md" }} {% endif %}
</title>
<style type="text/css">
{% capture main %}
{% include main.scss %}
{% endcapture %}
{{ main | scssify }}
</style>
<script>{% include turbolinks.js %}</script>
<script>{% include table-of-contents.js %}</script>
</head>
<body>
<header>
<div>
<a href="{{ site.baseurl }}" aria-label="Homepage"><img src="{{ site.baseurl }}public/android-chrome-256x256.png" width="100" height="100" alt="AltTab logo"/></a>
<div>
<h1><a href="{{ site.baseurl }}">{{ site.title }}</a></h1>
<p>{{ site.github.project_tagline }}</p>
</div>
</div>
</header>
<nav>
<ul>
<li><a class="rainbow" href="{{ site.github.latest_release.assets[0].browser_download_url }}">Download</a></li>
<li><a class="{% if page.name == 'Introduction.md' %}active{% endif %}" href="{{ site.baseurl }}"><span>Introduction</span></a></li>
<li><a class="{% if page.name == 'Changelog.md' %}active{% endif %}" href="{{ site.baseurl }}changelog"><span>Release notes</span></a></li>
<li><a class="{% if page.name == 'Contributing.md' %}active{% endif %}" href="{{ site.baseurl }}contributing"><span>Contributing</span></a></li>
<li><a class="{% if page.name == 'Thanks.md' %}active{% endif %}" href="{{ site.baseurl }}thanks"><span>Thanks</span></a></li>
<li><a href="{{ site.github.repository_url }}"><span>GitHub</span></a></li>
<li><a class="support {% if page.name == 'Support.md' %}active{% endif %}" href="{{ site.baseurl }}support"><span>Support this project ❤️</span></a></li>
</ul>
</nav>
<div class="layout">
<input type="checkbox" id="menu-toggle" />
<section class="page">
{{ content }}
</section>
<div class="hamburger-bar" style="line-height: 1">
<div id="mobile-logo">
<a href="{{ site.baseurl }}" aria-label="Homepage"><img src="{{ site.baseurl }}public/android-chrome-256x256.png" width="42" height="42" alt="AltTab logo" /></a>
<div>
<span><a href="{{ site.baseurl }}">{{ site.title }}</a></span>
<span>{{ site.github.project_tagline }}</span>
</div>
</div>
<label for="menu-toggle" class="hamburger-label">
<span class="open">Menu</span>
<span class="close">Close</span>
</label>
</div>
<div class="app">
<aside class="sidebar">
<div id="turbolinks-header" data-turbolinks-permanent>
<div id="logo">
<a href="{{ site.baseurl }}" aria-label="Homepage"><img src="{{ site.baseurl }}public/android-chrome-256x256.png" width="100" height="100" alt="AltTab logo" style="display:block" /></a>
<div id="title-and-subtitle">
<h1><a href="{{ site.baseurl }}">{{ site.title }}</a></h1>
<p>{{ site.github.project_tagline }}</p>
</div>
</div>
<div id="download-section">
<a class="button rainbow" href="{{ site.github.latest_release.assets[0].browser_download_url }}">Download</a>
<div id="downloads">6.6M downloads</div>
</div>
</div>
<ul class="menu">
<li class="menu-item">
<a class="menu-label button {% if page.name == 'Introduction.md' %}active{% endif %}" href="{{ site.baseurl }}"><span>Introduction</span></a>
<ul class="submenu"></ul>
</li>
<li class="menu-item">
<a class="menu-label button {% if page.name == 'FAQ.md' %}active{% endif %}" href="{{ site.baseurl }}faq"><span>FAQ</span></a>
<ul class="submenu faq"></ul>
</li>
<li class="menu-item">
<a class="menu-label button {% if page.name == 'Changelog.md' %}active{% endif %}" href="{{ site.baseurl }}changelog"><span>Release notes</span></a>
<ul class="submenu changelog"></ul>
</li>
<li class="menu-item">
<a class="menu-label button {% if page.name == 'Contributing.md' %}active{% endif %}" href="{{ site.baseurl }}contributing"><span>Contributing</span></a>
<ul class="submenu contributing"></ul>
</li>
<li class="menu-item">
<a class="menu-label button {% if page.name == 'Thanks.md' %}active{% endif %}" href="{{ site.baseurl }}thanks"><span>Thanks</span></a>
<ul class="submenu thanks"></ul>
</li>
<li class="menu-item">
<a class="menu-label button external" href="{{ site.github.repository_url }}"><span style="display:flex; gap:3px;">GitHub <img src="data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDEyIDEyIj4KCTx0aXRsZT4KCQlleHRlcm5hbCBsaW5rCgk8L3RpdGxlPgoJPHBhdGggZmlsbD0iIzM2YyIgZD0iTTYgMWg1djVMOC44NiAzLjg1IDQuNyA4IDQgNy4zbDQuMTUtNC4xNnpNMiAzaDJ2MUgydjZoNlY4aDF2MmExIDEgMCAwIDEtMSAxSDJhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTEiLz4KPC9zdmc+Cg==" /></span></a>
</li>
<li class="menu-item">
<a class="menu-label button support {% if page.name == 'Support.md' %}active{% endif %}" href="{{ site.baseurl }}support"><span>Support this project ❤️</span></a>
<ul class="submenu support"></ul>
</li>
</ul>
</aside>
<main class="content">
<div{% if page.name == 'Changelog.md' %} id="changelog"{% endif %}>
{{ content }}
</div>
</main>
</div>
</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+3 -1
View File
@@ -20,7 +20,9 @@ module.exports = {
['@semantic-release/git', {
'assets': [
'docs/Changelog.md',
'appcast.xml'
'appcast.xml',
'README.md',
'docs/_layouts/default.html'
],
}],
],
@@ -0,0 +1,2 @@
/*Copyright (human-readable)*/
"NSHumanReadableCopyright" = "Ліцэнзія GPL-3.0";
@@ -0,0 +1,31 @@
"About" = "Аб праграме";
"Active app" = "Актыўнае прыкладанне";
"Active screen" = "Актыўны экран";
"Apparition delay:" = "Затрымка з’яўлення:";
"Appearance" = "Выгляд";
"Auto-install updates periodically" = "Аўтаматычна ўсталёўваць абнаўленні";
"Cancel" = "Адмяніць";
"Check for updates now…" = "Праверыць абнаўленні цяпер…";
"Check for updates periodically" = "Перыядычна правяраць абнаўленні";
"Check for updates…" = "Праверыць абнаўленні…";
"Don’t check for updates periodically" = "Не правяраць абнаўленні";
"Hide Space number labels:" = "Схаваць нумары прастораў:";
"I think the app could be improved with…" = "Я лічу, што праграму можна ўдасканаліць…";
"Latest releases" = "Апошнія рэлізы";
"NSHumanReadableCopyright" = "Ліцэнзія GPL-3.0";
"Optional: email (if you want a reply)" = "Неабавязкова: эл. пошта (калі вы хочаце адказ)";
"Preferences…" = "Налады…";
"Quit %@" = "Выйсці з %@";
"Send" = "Адправіць";
"Send debug profile (CPU, memory, etc)" = "Адправіць профіль адладкі (ЦП, памяць і г.д.)";
"Send feedback" = "Адправіць водгук";
"Send feedback…" = "Адправіць водгук";
"Share improvement ideas, or report bugs" = "Падзяліцеся ідэямі па паляпшэньні або паведаміце пра памылкі";
"Show" = "Паказаць";
"Show on:" = "Паказваць на:";
"Source code repository" = "Рэпазытар зыходнага коду";
"Theme:" = "Тэма:";
"Updates policy:" = "Правіла абнаўленняў:";
"View existing discussions" = "Паглядзець існуючыя абмеркаванні";
"Window app icon size:" = "Памер значка вакна:";
"Window title font size:" = "Памер шрыфту загалоўка вокна:";
@@ -124,16 +124,24 @@
"Miscellaneous:" = "Různé:";
"Mouse hover" = "Najetí myši";
"Multiple screens" = "Více obrazovek";
"Never" = "Nikdy";
"Never send crash reports" = "Nikdy neposílat hlášení o pádu";
"Non-active apps" = "Neaktivní appky";
"Normal" = "Normální";
"Not allowed" = "Nepovoleno";
"NSHumanReadableCopyright" = "Licence GPL-3.0";
"Only if the window is fullscreen" = "Jen pokud je okno na celou obrazovku";
"Only on different screen" = "Pouze na jiné obrazovce";
"Open Accessibility Preferences…" = "Otevřít volby Zpřístupnění…";
"Open Accessibility Settings…" = "Otevřít nastavení přístupnosti…";
"Open Screen Recording Preferences…" = "Otevřít nastavení oprávnění nahrávání obrazovky…";
"Open Screen Recording Settings…" = "Otevřít nastavení nahrávání obrazovky…";
"Open System Settings to confirm, and continue" = "Otevřít Systémová Nastavení pro potvrzení, a pokračovat";
"Open Trackpad Preferences…" = "Otevřít předvolby trackpadu…";
"Open Trackpad Settings…" = "Otevřít nastavení trackpadu…";
"Optional: email (if you want a reply)" = "Volitelně: e-mail (pokud chcete odpověď)";
"Order windows by" = "Seřadit okna podle";
"Please fill in the form" = "Prosím vyplňte formulář";
"Policies" = "Zásady";
"Preferences…" = "Nastavení…";
"Preview selected window" = "Zobrazit náhled vybraného okna";
@@ -149,6 +157,8 @@
"Remember my choice" = "Zapamatovat si mou volbu";
"Reset preferences and restart" = "Obnovit předvolby a restartovat";
"Reset preferences and restart…" = "Obnovit předvolby a restartovat…";
"Reset settings and restart" = "Obnovit nastavení a restartovat";
"Reset settings and restart…" = "Obnovit nastavení a restartovat…";
"Restart Now" = "Restartovat nyní";
"Rows of thumbnails:" = "Řádky náhledů:";
"Screen including menu bar" = "Obrazovka včetně panelu nabídek";
@@ -166,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Odeslat diagnostická data (CPU, paměť atd.)";
"Send feedback" = "Odeslat zpětnou vazbu";
"Send feedback…" = "Odeslat zpětnou vazbu…";
"Settings…" = "Nastavení";
"Share improvement ideas, or report bugs" = "Navrhněte vylepšení nebo nahlaste chybu";
"Shortcut 1" = "Zkratka 1";
"Shortcut 2" = "Zkratka 2";
@@ -178,6 +189,7 @@
"Show" = "Zobrazit";
"Show & Hide" = "Zobrazit a skrýt";
"Show an item in the switcher for each window, or for each application. Windows will be focused, whereas applications will be activated." = "Zobrazit položku v přepínači pro každé okno, nebo pro každou aplikaci. Okna budou aktivována jako zaměřená, zatímco aplikace budou spouštěny.";
"Show apps with no open window" = "Zobrazit appky bez otevřeného okna";
"Show at the end" = "Zobrazit na konci";
"Show fullscreen windows" = "Zobrazit okna na celé obrazovce";
"Show hidden windows" = "Zobrazit skrytá okna";
@@ -208,6 +220,9 @@
"Switch between 3 different styles. You can customize them." = "Přepínejte mezi 3 různými styly. Můžete si je upravit.";
"System" = "Systém";
"System Default" = "Výchozí nastavení systému";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "Zkratka ⌘⎋ (command+escape) může být použita macOS pro GameOverlay.\n\nVaŠe změna jí nastaví na : %@.\n\nPro pokračování, se ujistěte, že zkratka GameOverlay je vypnuta.\nDbejte také, že macOS hlásí chyby kdy zkratka GameOverlay může stále ovlivňovat chování i při jejím vypnutí.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Klávesa ⎋ (escape) je rezervována některými macOS zkratkami (např. ⌘⌥⎋ zobrazí okno vynuceného ukončení aplikací).\n\nPokud chcete použít ⎋, ujistěte se, že modifikátory Přidržení nejsou: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Klávesa ⎋ (escape) je rezervována některými macOS zkratkami (např. ⌘⌥⎋ zobrazí okno vynuceného ukončení aplikací).\n\nVaŠe změna jí nastaví na : %@.\n\nPokud chcete použít ⎋, ujistěte se, že modifikátory Přidržení nejsou: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃";
"The application needs to restart to apply the language change." = "Aplikace se musí restartovat, aby se změna jazyka projevila.";
"Theme" = "Motiv";
"Theme:" = "Motiv:";
@@ -219,6 +234,7 @@
"Title" = "Název";
"Title truncation" = "Zkrácení názvu";
"Titles" = "Názvy";
"Trackpad haptic feedback" = "Haptická zpětná vazba trackpadu";
"Trigger shortcut" = "Spouštěcí zkratka";
"Unassign existing shortcut and continue" = "Odebrat existující zkratku a pokračovat";
"Updates policy" = "Zásady aktualizací";
@@ -21,6 +21,7 @@
"Also select windows using:" = "Επιπλέον, επιλογή παραθύρων με:";
"AltTab crashed last time you used it. Sending a crash report will help get the issue fixed" = "Το Alttab συνετρίβη την τελευταία φορά που το χρησιμοποιήσατε. Η αποστολή μιας αναφοράς συντριβής θα βοηθήσει να διορθωθεί το ζήτημα";
"AltTab is currently set to show Applications. This setting is only available when AltTab is set to show Windows." = "Το Alttab έχει οριστεί επί του παρόντος να εμφανίζει Εφαρμογές. Αυτή η ρύθμιση είναι διαθέσιμη μόνο όταν το alttab έχει ρυθμιστεί να εμφανίζει Παράθυρα.";
"AltTab is running without Screen Recording permissions. Thumbnails won’t show." = "Το AltTab εκτελείται χωρίς δικαιώματα Εγγραφής Οθόνης. Οι μικρογραφίες δεν θα εμφανίζονται.";
"AltTab needs some permissions" = "Το AltTab χρειάζεται κάποιες άδειες";
"AltTab will show if the window is currently minimized or fullscreen with a status icon." = "Το alttab θα δείχνει εάν το παράθυρο είναι ελαχιστοποιημένο ή σε πλήρη οθόνη με εικονίδιο κατάστασης.";
"Always" = "Πάντοτε";
@@ -34,6 +35,7 @@
"App is hidden" = "Η εφαρμογή είναι κρυμμένη";
"App is running but has no open window" = "Η εφαρμογή εκτελείται αλλά δεν διαθέτει ανοιχτό παράθυρο";
"Apparition delay" = "Καθυστέρηση εμφάνισης";
"Apparition delay of Switcher" = "Καθυστέρηση εμφάνισης Switcher";
"Apparition delay:" = "Καθυστέρηση εμφάνισης";
"Appearance" = "Εμφάνιση";
"Application Name" = "Όνομα εφαρμογής";
@@ -61,19 +63,24 @@
"Customize Thumbnails style…" = "Προσαρμογή στυλ μικρογραφιών…";
"Customize Titles style…" = "Προσαρμογή στυλ τίτλων…";
"Dark" = "Σκοτεινό";
"Disabled" = "Απενεργοποιημένο";
"Do nothing" = "Μην κάνεις τίποτα";
"Don’t check for updates periodically" = "Να μη γίνεται έλεγχος για ενημερώσεις";
"Don’t send" = "Να μην σταλεί";
"Don’t show windows from these apps" = "Απόκρυψη παραθύρων από αυτές τις εφαρμογές";
"Done" = "Εντάξει";
"End" = "Τέλος";
"Fade in animation of Preview" = "Εμφάνιση με εξασθένηση της προεπισκόπησης";
"Fade out animation" = "Εφέ ξεθωριάσματος";
"Fade out animation of Switcher" = "Εξαφάνιση του Switcher με εξασθένηση";
"Fade out animation:" = "Εφέ ξεθωριάσματος:";
"Focus selected window" = "Εστίαση στο επιλεγμένο παράθυρο";
"Fullscreen window" = "Πλήρης οθόνη";
"Fullscreen windows:" = "Παράθυρα σε πλήρη οθόνη:";
"Fullscreen/Defullscreen window" = "Πλήρης οθόνη/επαναφορά μεγέθους";
"General" = "Γενικά";
"Gesture" = "Χειρονομία";
"Grant permission" = "Παραχώρησε άδεια";
"Hidden windows:" = "Κρυμμένα παράθυρα:";
"Hide" = "Απόκρυψη";
"Hide app badges" = "Απόκρυψη ενδείξεων ειδοποιήσεων εφαρμογών";
@@ -92,10 +99,15 @@
"High" = "Υψηλή";
"Highest" = "Μέγιστη";
"Hold" = "Κρατήστε πατημένο";
"Horizontal Swipe with Four Fingers" = "Οριζόντια σάρωση με τέσσερα δάχτυλα";
"Horizontal Swipe with Three Fingers" = "Οριζόντια σάρωση με τρία δάχτυλα";
"I think the app could be improved with…" = "Πιστεύω ότι η εφαρμογή θα μπορούσε να βελτιωθεί με...";
"Ignore shortcuts when active" = "Αγνόησε τις συντομεύσεις όταν ενεργό";
"Ignore shortcuts while a window from these apps is active" = "Να αγνοούνται οι συντομεύσεις ενόσω κάποιο παράθυρο από αυτές τις εφαρμογές είναι ενεργό";
"Language" = "Γλώσσα";
"Language Change" = "Αλλαγή γλώσσας";
"Large" = "Μεγάλο";
"Later" = "Αργότερα";
"Latest releases" = "Προηγούμενες εκδόσεις";
"Leading" = "Αριστερά";
"Left" = "Αριστερά";
@@ -108,18 +120,28 @@
"Middle" = "Μέσο";
"Minimize/Deminimize window" = "Ελαχιστοποίηση/Επαναφορά παραθύρου";
"Minimized windows:" = "Ελαχιστοποιημένα παράθυρα:";
"Miscellaneous" = "Διάφορα";
"Miscellaneous:" = "Διάφορα:";
"Mouse hover" = "Αιώρηση ποντικιού";
"Multiple screens" = "Πολλαπλές οθόνες";
"Never" = "Ποτέ";
"Never send crash reports" = "Να μην αποστέλλονται αναφορές συντριβής";
"Non-active apps" = "Εφαρμογές που δεν είναι ενεργές";
"Normal" = "Κανονική";
"Not allowed" = "Δεν επιτρέπεται";
"NSHumanReadableCopyright" = "Άδεια χρήσης GPL-3.0";
"Only if the window is fullscreen" = "Μόνο αν το παράθυρο βρίσκεται σε πλήρη οθόνη";
"Only on different screen" = "Μόνο σε διαφορετική οθόνη";
"Open Accessibility Preferences…" = "Άνοιγμα Επιλογών Προσβασιμότητας...";
"Open Accessibility Settings…" = "Ανοίξτε τις ρυθμίσεις προσβασιμότητας...";
"Open Screen Recording Preferences…" = "Άνοιγμα Επιλογών Καταγραφής Οθόνης...";
"Open Screen Recording Settings…" = "Ανοίξτε τις ρυθμίσεις εγγραφής οθόνης...";
"Open System Settings to confirm, and continue" = "Ανοίξτε τις ρυθμίσεις συστήματος για επιβεβαίωση, και συνεχίστε";
"Open Trackpad Preferences…" = "Άνοιγμα Ρυθμίσεων Trackpad…";
"Open Trackpad Settings…" = "Ανοίξτε τις ρυθμίσεις Trackpad...";
"Optional: email (if you want a reply)" = "Προαιρετικό: email (αν επιθυμείτε απάντηση)";
"Order windows by" = "Ταξινόμηση παραθύρων κατά";
"Please fill in the form" = "Παρακαλώ συμπληρώστε τη φόρμα";
"Policies" = "Πολιτικές";
"Preferences…" = "Προτιμήσεις...";
"Preview selected window" = "Προεπισκόπηση επιλεγμένου παραθύρου";
@@ -135,6 +157,9 @@
"Remember my choice" = "Θυμήσου την επιλογή μου";
"Reset preferences and restart" = "Επαναφορά προτιμήσεων και επανεκκίνηση";
"Reset preferences and restart…" = "Επαναφορά προτιμήσεων και επανεκκίνηση…";
"Reset settings and restart" = "Επαναφορά ρυθμίσεων και επανεκκίνηση";
"Reset settings and restart…" = "Επαναφορά ρυθμίσεων και επανεκκίνηση...";
"Restart Now" = "Επανεκκίνηση τώρα";
"Rows of thumbnails:" = "Σειρές μικρογραφιών:";
"Screen including menu bar" = "Οθόνη όπου βρίσκεται η γραμμή μενού";
"Screen including mouse" = "Οθόνη όπου βρίσκεται το ποντίκι";
@@ -151,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Αποστολή διαγνωστικού προφίλ (επεξεργαστής, μνήμη, κλπ)";
"Send feedback" = "Αποστολή σχολίων";
"Send feedback…" = "Αποστολή σχολίων...";
"Settings…" = "Ρυθμίσεις";
"Share improvement ideas, or report bugs" = "Μοιραστείτε ιδέες για βελτίωση ή αναφορά σφαλμάτων";
"Shortcut 1" = "Συντόμευση 1";
"Shortcut 2" = "Συντόμευση 2";
@@ -163,6 +189,7 @@
"Show" = "Εμφάνιση";
"Show & Hide" = "Εμφάνιση & Απόκρυψη";
"Show an item in the switcher for each window, or for each application. Windows will be focused, whereas applications will be activated." = "Εμφάνιση ενός στοιχείου για κάθε παράθυρο ή για κάθε εφαρμογή. Τα παράθυρα θα εστιάζονται, ενώ οι εφαρμογές θα ενεργοποιούνται.";
"Show apps with no open window" = "Εμφάνιση εφαρμογών χωρίς ανοικτό παράθυρο";
"Show at the end" = "Εμφάνιση στο τέλος";
"Show fullscreen windows" = "Εμφάνιση παραθύρων πλήρους οθόνης";
"Show hidden windows" = "Εμφάνιση κρυφών παραθύρων";
@@ -178,6 +205,7 @@
"Show windows from Spaces" = "Εμφάνιση παραθύρων από Γραφεία εργασίας";
"Show windows from:" = "Εμφάνιση παραθύρων από:";
"Size" = "Μέγεθος";
"Skipped" = "Παραλείφθηκε";
"Small" = "Μικρό";
"Some apps like Finder or Preview use standard tabs which act like independent windows. Some other apps like web browsers use custom tabs which act in unique ways and are not actual windows. AltTab can't list those separately." = "Ορισμένες εφαρμογές όπως ο Finder ή η Προεπισκόπηση χρησιμοποιούν τυπικές καρτέλες που λειτουργούν σαν ανεξάρτητα παράθυρα. Ορισμένες άλλες εφαρμογές όπως τα προγράμματα περιήγησης ιστού χρησιμοποιούν προσαρμοσμένες καρτέλες που λειτουργούν με μοναδικούς τρόπους και δεν είναι πραγματικά παράθυρα. Το Alttab δεν μπορεί να τα εμφανίσει χωριστά.";
"Source code repository" = "Αποθετήριο πηγαίου κώδικα";
@@ -187,21 +215,34 @@
"Start at login" = "Άνοιγμα κατά την είσοδο";
"Start at login:" = "Άνοιγμα κατά την είσοδο:";
"Support this project" = "Υποστηρίξτε αυτό το έργο";
"Swipe with Four Fingers" = "Σάρωση με τέσσερα δάχτυλα";
"Swipe with Three Fingers" = "Σάρωση με τρία δάχτυλα";
"Switch between 3 different styles. You can customize them." = "Εναλλαγή μεταξύ 3 διαφορετικών στυλ. Μπορείτε να τα προσαρμόσετε.";
"System" = "Σύστημα";
"System Default" = "Προεπιλογή συστήματος";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "Η συντόμευση ⌘⎋ (command+escape) μπορεί να χρησιμοποιείται από το macOS για το GameOverlay.\n\nΗ αλλαγή σας θα την αναθέσει σε: %@.\n\nΑν θέλετε να συνεχίσετε, βεβαιωθείτε ότι η συντόμευση του GameOverlay είναι απενεργοποιημένη.\nΣημειώστε επίσης ότι το macOS έχει αναφέρει σφάλματα όπου η συντόμευση μπορεί να επηρεάζει ακόμα και όταν είναι απενεργοποιημένη.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Το κλειδί ⎋ (escape) είναι δεσμευμένο από μερικά macOS shortcuts (π.χ. ⌘⌥⎋ θα εμφανίσει το Force Quit Applications window).\n\nΑν θέλετε να χρησιμοποιήσετε το ⎋, βεβαιωθείτε ότι τα Hold modifiers δεν είναι ούτε: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Το πλήκτρο ⎋ (escape) προορίζεται για ορισμένες συντομεύσεις του macOS (π.χ. ⌘⌥⎋ ανοίγει το παράθυρο Τερματισμός εφαρμογών).\n\nΗ αλλαγή σας θα το αναθέσει σε: %@.\n\nΑν θέλετε να χρησιμοποιήσετε το ⎋, βεβαιωθείτε ότι οι τροποποιητές Hold δεν είναι: ⌘⌥, ⌘⌥⇧ ή ⌘⌥⇧⌃.";
"The application needs to restart to apply the language change." = "Η εφαρμογή χρειάζεται επανεκκίνηση για να εφαρμοστεί η αλλαγή γλώσσας.";
"Theme" = "Θέμα";
"Theme:" = "Θέμα:";
"Then release:" = "Έπειτα αφήστε:";
"This permission is needed to focus windows after you release the shortcut" = "Αυτή η άδεια απαιτείται για να εστιάσετε τα παράθυρα μετά την απελευθέρωση της συντόμευσης";
"This permission is needed to show screenshots and titles of open windows" = "Αυτή η άδεια απαιτείται για την εμφάνιση στιγμιότυπων και τίτλων ανοιχτών παραθύρων";
"This permission is needed to show thumbnails and preview of open windows" = "Αυτή η άδεια είναι απαραίτητη για να εμφανιστούν πολλαπλές προεπισκοπήσεις παραθύρων";
"Thumbnails" = "Μικρογραφίες";
"Title" = "Τίτλος";
"Title truncation" = "Περικοπή τίτλου";
"Titles" = "Τίτλοι";
"Trackpad haptic feedback" = "Απτική ανάδραση του Trackpad";
"Trigger shortcut" = "Συντόμευση";
"Unassign existing shortcut and continue" = "Απόρριψη υπάρχουσας συντόμευσης και συνέχεια";
"Updates policy" = "Πολιτική ενημερώσεων";
"Updates policy:" = "Πολιτική ενημερώσεων:";
"Use the app without this permission. Thumbnails won’t show." = "Χρήση της εφαρμογής χωρίς αυτή την άδεια. Οι μικρογραφίες δεν θα εμφανίζονται.";
"Version" = "Έκδοση";
"Vertical Swipe with Four Fingers" = "Κάθετη σάρωση με τέσσερα δάχτυλα";
"Vertical Swipe with Three Fingers" = "Κάθετη σάρωση με τρία δάχτυλα";
"View existing discussions" = "Εμφάνιση υπαρχουσών συζητήσεων";
"Vim keys" = "Vim keys";
"Vim keys already assigned to other actions:\n%@" = "Vim keys που έχουν ήδη ανατεθεί σε άλλες ενέργειες: \\ n%@";
@@ -224,3 +265,4 @@
"Windows" = "Παράθυρα";
"You can’t undo this action." = "Αυτή η ενέργεια δεν είναι αναστρέψιμη.";
"You didn’t write your email, thus can’t receive any response." = "Δεν γράψατε το email σας, επομένως δεν μπορείτε να λάβετε καμία απάντηση.";
"You may need to disable some conflicting system gestures" = "Ίσως χρειαστεί να απενεργοποιήσετε ορισμένες χειρονομίες του συστήματος γιατί υπάρχει σύγκρουση";
@@ -124,17 +124,24 @@
"Miscellaneous:" = "Divers :";
"Mouse hover" = "Survol de la souris";
"Multiple screens" = "Plusieurs écrans";
"Never" = "Jamais";
"Never send crash reports" = "Ne jamais envoyer de rapport d’erreur";
"Non-active apps" = "App désactivée";
"Normal" = "Normale";
"Not allowed" = "Non autorisé";
"NSHumanReadableCopyright" = "Licence GPL-3.0";
"Only if the window is fullscreen" = "Seulement si la fenêtre est en mode plein écran";
"Only on different screen" = "Uniquement sur un écran différent…";
"Open Accessibility Preferences…" = "Ouvrir les Préférences d’Accessibilité…";
"Open Accessibility Settings…" = "Ouvrir les paramètres d’accessibilité…";
"Open Screen Recording Preferences…" = "Ouvrir les Préférences d’Enregistrement de l’écran…";
"Open Screen Recording Settings…" = "Ouvrir les paramètres d'enregistrement d'écran…";
"Open System Settings to confirm, and continue" = "Ouvrez les paramètres système pour confirmer, puis continuez";
"Open Trackpad Preferences…" = "Ouvrir les préférences du trackpad…";
"Open Trackpad Settings…" = "Ouvrir les réglages du trackpad…";
"Optional: email (if you want a reply)" = "Optionnel : email (si vous voulez une réponse)";
"Order windows by" = "Ranger les fenêtres par";
"Please fill in the form" = "Veuillez remplir le formulaire";
"Policies" = "Politiques";
"Preferences…" = "Préférences…";
"Preview selected window" = "Aperçu de la fenêtre sélectionnée";
@@ -150,6 +157,8 @@
"Remember my choice" = "Se souvenir de mon choix";
"Reset preferences and restart" = "Réinitialiser les préférences et redémarrer";
"Reset preferences and restart…" = "Réinitialiser les préférences et redémarrer…";
"Reset settings and restart" = "Réinitialiser les paramètres et redémarrer";
"Reset settings and restart…" = "Réinitialiser les paramètres et redémarrer…";
"Restart Now" = "Redémarrer maintenant";
"Rows of thumbnails:" = "Lignes d’aperçus :";
"Screen including menu bar" = "Écran contenant la barre de menu";
@@ -167,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Envoyer le profil technique (processeur, mémoire, etc)";
"Send feedback" = "Partager un retour d’expérience";
"Send feedback…" = "Partager un retour d’expérience…";
"Settings…" = "Paramètres";
"Share improvement ideas, or report bugs" = "Partagez des idées d’amélioration, ou signalez des bugs";
"Shortcut 1" = "Raccourci 1";
"Shortcut 2" = "Raccourci 2";
@@ -210,6 +220,9 @@
"Switch between 3 different styles. You can customize them." = "Choisissez et personnalisez entre ces 3 styles différents...";
"System" = "Système";
"System Default" = "Système";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "Le raccourci ⌘⎋ (commande + échap) peut être utilisé par macOS pour GameOverlay.\n\nVotre modification l’attribuerait à : %@.\n\nSi vous souhaitez continuer, assurez-vous que le raccourci GameOverlay est désactivé.\nNotez également que macOS a signalé des bugs où le raccourci peut encore interférer même lorsqu’il est désactivé.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "La touche ⎋ (escape) est réservée par certains raccourcis macOS (e.g. ⌘⌥⎋ affichera la fenêtre Force Quit Applications).\n\nSi vous souhaitez utiliser ⎋, assurez-vous que les modificateurs maintenus ne soient ni : ⌘⌥, ⌘⌥⇧, ou ⌘⌥⇧⌃";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "La touche ⎋ (escape) est réservée par certains raccourcis macOS (e.g. ⌘⌥⎋ affichera la fenêtre Force Quit Applications).\n\nVotre modification l’attribuerait à : %@.\n\nSi vous souhaitez utiliser ⎋, assurez-vous que les modificateurs maintenus ne soient ni : ⌘⌥, ⌘⌥⇧, ou ⌘⌥⇧⌃";
"The application needs to restart to apply the language change." = "Relancer l’application pour que le changment de langue soit effectif.";
"Theme" = "Thème";
"Theme:" = "Thème :";
@@ -221,6 +234,7 @@
"Title" = "Titre";
"Title truncation" = "Tronquer le titre par";
"Titles" = "Titres";
"Trackpad haptic feedback" = "Retour haptique du pavé tactile";
"Trigger shortcut" = "Raccourci déclencheur";
"Unassign existing shortcut and continue" = "Retirer le raccourci existant et continuer";
"Updates policy" = "Politique de mise à jour ";
+1 -1
View File
@@ -188,7 +188,7 @@
"Show standard tabs as windows" = "표준 탭을 윈도우로 보이기";
"Show standard tabs as windows:" = "표준 탭을 윈도우로 보여주기:";
"Show titles" = "제목 표시";
"Show windows from applications" = "윈도우을 표시할 앱";
"Show windows from applications" = "윈도우를 표시할 앱";
"Show windows from screens" = "윈도우를 표시할 화면";
"Show windows from Spaces" = "윈도우를 표시할 Space";
"Show windows from:" = "창을 표시할 항목:";
@@ -124,17 +124,24 @@
"Miscellaneous:" = "Diverse:";
"Mouse hover" = "Holde musa over";
"Multiple screens" = "Flere skjermer";
"Never" = "Aldri";
"Never send crash reports" = "Send aldri krasjrapporter";
"Non-active apps" = "Ikke-aktive apper";
"Normal" = "Normal";
"Not allowed" = "Ikke tillatt";
"NSHumanReadableCopyright" = "GPL-3.0-lisens";
"Only if the window is fullscreen" = "Bare hvis vinduet er i fullskjerm";
"Only on different screen" = "Bare på annen skjerm";
"Open Accessibility Preferences…" = "Åpne tilgjengelighetsinnstillinger…";
"Open Accessibility Settings…" = "Åpne tilgjengelighetsinnstillinger…";
"Open Screen Recording Preferences…" = "Åpne skjermopptaksinnstillinger…";
"Open Screen Recording Settings…" = "Åpne skjermopptakinnstillinger…";
"Open System Settings to confirm, and continue" = "Åpne systeminnstillinger for å bekrefte, og fortsette";
"Open Trackpad Preferences…" = "Åpne styreflateinnstillinger";
"Open Trackpad Settings…" = "Åpne styreflateinnstillinger…";
"Optional: email (if you want a reply)" = "Valgfritt: e-post (hvis du ønsker svar)";
"Order windows by" = "Sorter vinduer";
"Please fill in the form" = "Vennligst fyll inn skjema";
"Policies" = "Policyer";
"Preferences…" = "Valg…";
"Preview selected window" = "Forhåndsvis valgt vindu";
@@ -150,6 +157,8 @@
"Remember my choice" = "Husk valget mitt";
"Reset preferences and restart" = "Nullstill innstillingene og start på nytt";
"Reset preferences and restart…" = "Nullstill innstillingene og start på nytt…";
"Reset settings and restart" = "Tilbakestill innstillinger og start på nytt";
"Reset settings and restart…" = "Tilbakestill innstillinger og start på nytt…";
"Restart Now" = "Start på nytt nå";
"Rows of thumbnails:" = "Rader med miniatyrbilder:";
"Screen including menu bar" = "Skjerm med menylinje";
@@ -167,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Send debug-informasjon (CPU, minne, osv)";
"Send feedback" = "Send tilbakemelding";
"Send feedback…" = "Send tilbakemelding…";
"Settings…" = "Innstillinger…";
"Share improvement ideas, or report bugs" = "Del forbedringsideer, eller rapporter om feil";
"Shortcut 1" = "Hurtigtast 1";
"Shortcut 2" = "Hurtigtast 2";
@@ -210,6 +220,9 @@
"Switch between 3 different styles. You can customize them." = "Bytt mellom 3 forskjellige stiler. Du kan tilpasse dem.";
"System" = "System";
"System Default" = "Systemstandard";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "⌘⎋ (command+escape) kan være i bruk av macOS for GameOverlay.\n\nEndringen til tilordnes til: %@.\n\nHvis du vil bruke denne, sjekk at GameOverlay-hurtigtasten er deaktivert.\nDet har blitt rapportert bugs hvor hurtigtasten fremdeles har effekt selv om den har blitt deaktivert.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "⎋ (escape) er reservert for noen macOS-hurtigtaster (f.eks. ⌘⌥⎋ åpner Tvungen avslutning-vinduet).\n\nFor å bruke ⎋, unngå å bruke disse kombinasjonene: ⌘⌥, ⌘⌥⇧, eller ⌘⌥⇧⌃";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "⎋ (escape) er reservert for noen macOS-hurtigtaster (f.eks. ⌘⌥⎋ åpner Tvungen avslutning-vinduet).\n\nEndringen vil tilordnes til: %@.\n\nFor å bruke ⎋, unngå å bruke disse kombinasjonene: ⌘⌥, ⌘⌥⇧, eller ⌘⌥⇧⌃";
"The application needs to restart to apply the language change." = "Applikasjonen må starte på nytt for å sette nytt språk.";
"Theme" = "Tema";
"Theme:" = "Tema:";
@@ -221,6 +234,7 @@
"Title" = "Tittel";
"Title truncation" = "Tittelforkortelse";
"Titles" = "Titler";
"Trackpad haptic feedback" = "Styreflate følbar tilbakemelding";
"Trigger shortcut" = "Aktiver hurtigtast";
"Unassign existing shortcut and continue" = "Fjern eksisterende hurtigtast og fortsett";
"Updates policy" = "Oppdateringspolicy";
+17 -3
View File
@@ -68,7 +68,7 @@
"Don’t check for updates periodically" = "Nie sprawdzaj aktualizacji";
"Don’t send" = "Nie wysyłaj";
"Don’t show windows from these apps" = "Nie pokazuj okien z tych aplikacji";
"Done" = "Zrobione";
"Done" = "Gotowe";
"End" = "Na końcu";
"Fade in animation of Preview" = "Animacja pojawiania się";
"Fade out animation" = "Zniknąć animację";
@@ -124,17 +124,24 @@
"Miscellaneous:" = "Różne:";
"Mouse hover" = "Najechania myszką";
"Multiple screens" = "Wiele ekranów";
"Never" = "Nigdy";
"Never send crash reports" = "Nigdy nie wysyłaj raportów crashów";
"Non-active apps" = "Nieaktywne aplikacje";
"Normal" = "Normalna";
"Not allowed" = "Nie dozwolone";
"NSHumanReadableCopyright" = "Licencja GPL-3.0";
"Only if the window is fullscreen" = "Tylko jeśli okno jest pełnoekranowe";
"Only on different screen" = "Tylko na inny ekran";
"Open Accessibility Preferences…" = "Otwórz ustawienia dostępności…";
"Open Accessibility Settings…" = "Otwórz Ustawienia Dostępności...";
"Open Screen Recording Preferences…" = "Otwórz ustawienia nagrywania ekranu…";
"Open Screen Recording Settings…" = "Otwórz Ustawienia Nagrywania ekranu...";
"Open System Settings to confirm, and continue" = "Otwórz Ustawienia Systemowe aby potwierdzić i kontynuować";
"Open Trackpad Preferences…" = "Otwórz Ustawienia Gładzika...";
"Open Trackpad Settings…" = "Otwórz Ustawienia Gładzika...";
"Optional: email (if you want a reply)" = "Opcjonalne: email (jeśli chcesz odpowiedź)";
"Order windows by" = "Sortuj okna według";
"Please fill in the form" = "Proszę uzupełnić formularz";
"Policies" = "Polityki";
"Preferences…" = "Preferencje…";
"Preview selected window" = "Podświetlaj wybrane okno";
@@ -150,6 +157,8 @@
"Remember my choice" = "Pamiętaj mój wybór";
"Reset preferences and restart" = "Usuń ustawienia i zrestartuj";
"Reset preferences and restart…" = "Usuń ustawienia i zrestartuj...";
"Reset settings and restart" = "Wyczyść ustawienia i zrestartuj";
"Reset settings and restart…" = "Wyczyść ustawienia i zrestartuj...";
"Restart Now" = "Zrestartuj teraz";
"Rows of thumbnails:" = "Rzędy miniatur:";
"Screen including menu bar" = "Ekranie na którym znajduje się pasek menu";
@@ -167,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Wyślij profil debugowania (CPU, pamięć, itp.)";
"Send feedback" = "Wyślij feedback";
"Send feedback…" = "Wyślij feedback…";
"Settings…" = "Ustawienia";
"Share improvement ideas, or report bugs" = "Podziel się pomysłami na ulepszenia lub zgłoś błąd";
"Shortcut 1" = "Skrót 1";
"Shortcut 2" = "Skrót 2";
@@ -174,8 +184,8 @@
"Shortcut 4" = "Skrót 4";
"Shortcut 5" = "Skrót 5";
"Shortcut already assigned to another action: %@" = "Skrót już przypisany do innej akcji: %@";
"Shortcuts When Active" = "Skróty, gdy są aktywne";
"Shortcuts when active…" = "Skróty, gdy są aktywne…";
"Shortcuts When Active" = "Skróty, gdy aktywny";
"Shortcuts when active…" = "Skróty, gdy aktywny…";
"Show" = "Pokaż";
"Show & Hide" = "Pokaż i ukryj";
"Show an item in the switcher for each window, or for each application. Windows will be focused, whereas applications will be activated." = "Pokaż element w przełączniku dla każdego okna lub dla każdej aplikacji. Okna zostaną wybrane, podczas gdy aplikacje będą aktywowane.";
@@ -210,6 +220,9 @@
"Switch between 3 different styles. You can customize them." = "Przełącz między 3 różnymi stylami. Możesz je dostosować.";
"System" = "System";
"System Default" = "Domyślna Wartość Systemowa";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "Skrót ⌘⎋ (command+escape) może być używany przez macOS do Trybu Gry.\n\nTwoja zmiana zostanie zastosowana do: %@.\n\nJeżeli chcesz kontynuować, upewnij się, że skrót do Trybu Gry na Macu jest wyłączony.\nDodatkowo wiedz, że zostały zgłaszane błędy w działaniu tego skrótu nawet gdy zostanie wyłączony.\n";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Klawisz ⎋ (escape) jest zarezerwowany przez niektóre skróty macOS (np. ⌘⌥⎋ pokaże okno Wymuszenia zamknięcia aplikacji).\n\nJeżeli chcesz używać ⎋, upewnij się, że nie używasz modyfikatorów: ⌘⌥, ⌘⌥⇧, lub ⌘⌥⇧⌃";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Klawisz ⎋ (escape) jest zarezerwowany przez niektóre skróty macOS (np. ⌘⌥⎋ pokaże okno Wymuszenia zamknięcia aplikacji).\n\nTwoja zmiana zostanie zastosowana do: %@.\n\nJeżeli chcesz używać ⎋, upewnij się, że nie używasz modyfikatorów: ⌘⌥, ⌘⌥⇧, lub ⌘⌥⇧⌃";
"The application needs to restart to apply the language change." = "Aplikacja musi zostać uruchomiona ponownie, by zastosować zmianę języka.";
"Theme" = "Motyw";
"Theme:" = "Motyw:";
@@ -221,6 +234,7 @@
"Title" = "Tytuł";
"Title truncation" = "Obcięcie tytułu";
"Titles" = "Tytuły";
"Trackpad haptic feedback" = "Odpowiedzi haptyczne gładzika";
"Trigger shortcut" = "Skrót klawiszowy";
"Unassign existing shortcut and continue" = "Anuluj przypisanie istniejącego skrótu i kontynuuj";
"Updates policy" = "Zasady aktualizacji";
@@ -124,17 +124,24 @@
"Miscellaneous:" = "Разное:";
"Mouse hover" = "Наведение мышью";
"Multiple screens" = "Несколько экранов";
"Never" = "Никогда";
"Never send crash reports" = "Никогда не отправлять отчеты";
"Non-active apps" = "Неактивные приложения";
"Normal" = "Обычный";
"Not allowed" = "Не разрешено";
"NSHumanReadableCopyright" = "Лицензия GPL-3.0";
"Only if the window is fullscreen" = "Только если окно на весь экран";
"Only on different screen" = "Только на другом экране";
"Open Accessibility Preferences…" = "Открыть настройки Универсального доступа…";
"Open Accessibility Settings…" = "Открыть настройки универсального доступа";
"Open Screen Recording Preferences…" = "Открыть настройки Записи экрана…";
"Open Screen Recording Settings…" = "Открыть настройки записи экрана";
"Open System Settings to confirm, and continue" = "Откройте системные настройки для подтверждения и продолжите";
"Open Trackpad Preferences…" = "Открыть настройки трекпада…";
"Open Trackpad Settings…" = "Открыть настройки трекпада";
"Optional: email (if you want a reply)" = "Опционально: эл.почта (если вы хотите ответа)";
"Order windows by" = "Сортировка окон";
"Please fill in the form" = "Пожалуйста, заполните форму";
"Policies" = "Правила";
"Preferences…" = "Настройки…";
"Preview selected window" = "Миниатюра выбранного окна";
@@ -150,6 +157,8 @@
"Remember my choice" = "Запомнить мой выбор";
"Reset preferences and restart" = "Сброс настроек и перезапуск";
"Reset preferences and restart…" = "Сброс настроек и перезапуск…";
"Reset settings and restart" = "Сбросить настройки и перезапустить";
"Reset settings and restart…" = "Сбросить настройки и перезапустить";
"Restart Now" = "Перезагрузить сейчас";
"Rows of thumbnails:" = "Ряды с эскизами окон:";
"Screen including menu bar" = "Экран со строкой меню";
@@ -167,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Отправить отладочный профиль (CPU, память и т.д.)";
"Send feedback" = "Отправить отзыв";
"Send feedback…" = "Отправить отзыв…";
"Settings…" = "Настройки";
"Share improvement ideas, or report bugs" = "Поделитесь идеями по улучшению или сообщите об ошибках";
"Shortcut 1" = "Комбинация 1";
"Shortcut 2" = "Комбинация 2";
@@ -210,6 +220,9 @@
"Switch between 3 different styles. You can customize them." = "Переключайтесь между 3 разными стилями. Вы можете настраивать их.";
"System" = "Системная";
"System Default" = "По-умолчанию";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "Сочетание клавиш ⌘⎋ (command+escape) может использоваться macOS для GameOverlay.\n\nВаше изменение назначит это сочетание для действия: %@.\n\nЕсли вы хотите продолжить, убедитесь, что сочетание клавиш GameOverlay отключено.\nТакже учтите, что macOS сообщает об ошибках, при которых это сочетание может вызывать конфликты даже после отключения.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Клавиша ⎋ (escape) используется некоторыми системными сочетаниями macOS (например, ⌘⌥⎋ открывает окно «Принудительное завершение программ»).\n\nЧтобы использовать ⎋, убедитесь, что удерживаемые модификаторы не являются: ⌘⌥, ⌘⌥⇧ или ⌘⌥⇧⌃";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Клавиша ⎋ (escape) используется некоторыми системными сочетаниями macOS (например, ⌘⌥⎋ открывает окно «Принудительное завершение программ»).\n\nЭто изменение назначит её для действия: %@.\n\nЧтобы использовать ⎋, убедитесь, что удерживаемые клавиши-модификаторы не являются: ⌘⌥, ⌘⌥⇧ или ⌘⌥⇧⌃";
"The application needs to restart to apply the language change." = "Необходимо перезапустить приложение для смены языка.";
"Theme" = "Тема";
"Theme:" = "Тема:";
@@ -221,6 +234,7 @@
"Title" = "Заголовок";
"Title truncation" = "Обрезка заголовка";
"Titles" = "Заголовки";
"Trackpad haptic feedback" = "Тактильная обратная связь";
"Trigger shortcut" = "Комбинация срабатывания";
"Unassign existing shortcut and continue" = "Назначение отменить и дальше";
"Updates policy" = "Политика обновлений";
+20 -1
View File
@@ -1,5 +1,5 @@
"About" = "பற்றி";
"About %@" = "சுமார் %@";
"About %@" = "பற்றி %@";
"Accessibility" = "அணுகல்";
"Acknowledgments" = "ஒப்புதல்கள்";
"Active app" = "செயலில் உள்ள பயன்பாடு";
@@ -9,11 +9,13 @@
"Advanced" = "மேம்படுத்தபட்ட";
"After release" = "வெளியான பிறகு";
"Align windows" = "விண்டோஸை சீரமைக்கவும்";
"Align windows:" = "ஜன்னலை சீரமை:";
"All apps" = "அனைத்து பயன்பாடுகளும்";
"All data from this form will be made public, as a ticket on github.com" = "இந்த படிவத்திலிருந்து வரும் அனைத்து தரவும் Github.com இல் டிக்கெட்டாக பகிரங்கப்படுத்தப்படும்";
"All screens" = "அனைத்து திரைகளும்";
"All Spaces" = "அனைத்து இடங்களும்";
"Allowed" = "அனுமதிக்கப்படுகிறது";
"Alphabetical" = "அகரவரிசைப்படி";
"Alphabetical Order" = "அகரவரிசை வரிசை";
"AltTab crashed last time you used it. Sending a crash report will help get the issue fixed" = "கடைசியாக நீங்கள் அதைப் பயன்படுத்தியபோது ஆல்டாப் செயலிழந்தார்.செயலிழப்பு அறிக்கையை அனுப்புவது சிக்கலை சரிசெய்ய உதவும்";
"AltTab is currently set to show Applications. This setting is only available when AltTab is set to show Windows." = "பயன்பாடுகளைக் காண்பிப்பதற்காக அல்தாப் தற்போது அமைக்கப்பட்டுள்ளது.சாளரங்களைக் காண்பிக்க ஆல்டாப் அமைக்கப்படும்போது மட்டுமே இந்த அமைப்பு கிடைக்கும்.";
@@ -22,6 +24,7 @@
"Always" = "எப்போதும்";
"Always send crash reports" = "எப்போதும் செயலிழப்பு அறிக்கைகளை அனுப்பவும்";
"and press" = "மற்றும் அழுத்தவும்";
"and press:" = "மற்றும் அழுத்து:";
"Animations" = "அனிமேஷன்கள்";
"Animations…" = "அனிமேஷன்…";
"App (BundleID starting with)" = "பயன்பாடு (மூட்டை தொடங்கி)";
@@ -35,6 +38,7 @@
"Application Name - Window Title" = "பயன்பாட்டு பெயர் - சாளர தலைப்பு";
"Applications" = "பயன்பாடுகள்";
"Are you sure you don’t want a response?" = "நீங்கள் ஒரு பதிலை விரும்பவில்லை என்பதில் உறுதியாக இருக்கிறீர்களா?";
"Arrow keys" = "அம்புக்குறி விசைகள்";
"Ask whether to send crash reports" = "செயலிழப்பு அறிக்கைகளை அனுப்பலாமா என்று கேளுங்கள்";
"Auto-install updates periodically" = "அவ்வப்போது புதுப்பிப்புகளைத் தானாக நிறுவவும்";
"Blacklists" = "தடுப்புப்பட்டியல்கள்";
@@ -49,19 +53,25 @@
"Conflicting shortcut" = "முரண்பட்ட குறுக்குவழி";
"Controls" = "கட்டுப்பாடுகள்";
"Crash reports policy" = "க்ராஷ் அறிக்கைகள் கொள்கை";
"Crash reports policy:" = "விபத்து அறிக்கைக்கான கொள்கை:";
"Customize App Icons style…" = "பயன்பாட்டு சின்னங்கள் பாணியைத் தனிப்பயனாக்குங்கள்…";
"Customize Thumbnails style…" = "சிறுபட பாணியைத் தனிப்பயனாக்குங்கள்…";
"Customize Titles style…" = "தலைப்புகள் பாணியைத் தனிப்பயனாக்குங்கள்…";
"Dark" = "இருண்ட";
"Disabled" = "முடக்கப்பட்டது";
"Do nothing" = "எதுவும் செய்ய வேண்டாம்";
"Don’t check for updates periodically" = "அவ்வப்போது புதுப்பிப்புகளை சரிபார்க்க வேண்டாம்";
"Don’t send" = "அனுப்ப வேண்டாம்";
"Don’t show windows from these apps" = "ஆப்ஸ்";
"Done" = "முடிந்தது";
"End" = "முடிவு";
"Fade out animation" = "அனிமேஷனை மங்கச் செய்யுங்கள்";
"Fade out animation:" = "மறையும் அசைவூட்டம்:";
"Focus selected window" = "தேர்ந்தெடுக்கப்பட்ட சாளரத்தை கவனம் செலுத்துங்கள்";
"Fullscreen/Defullscreen window" = "முழுத்திரை/டெஃபுலெல்ஸ்கிரீன் சாளரம்";
"General" = "பொது";
"Gesture" = "சைகை";
"Grant permission" = "அனுமதி வழங்கு";
"Hide" = "மறை";
"Hide app badges" = "பயன்பாட்டு பேட்ஜ்களை மறைக்கவும்";
"Hide apps with no open window" = "திறந்த சாளரம் இல்லாத பயன்பாடுகளை மறைக்கவும்";
@@ -75,9 +85,11 @@
"Hold" = "பிடி";
"I think the app could be improved with…" = "பயன்பாட்டை மேம்படுத்த முடியும் என்று நினைக்கிறேன்…";
"Ignore shortcuts when active" = "செயலில் இருக்கும்போது குறுக்குவழிகளை புறக்கணிக்கவும்";
"Language" = "மொழி";
"Large" = "பெரிய";
"Latest releases" = "சமீபத்திய வெளியீடுகள்";
"Leading" = "முன்னணி";
"Left" = "இடது";
"Light" = "ஒளி";
"Medium" = "நடுத்தர";
"Menubar icon" = "மெனுபார் ஐகான்";
@@ -88,6 +100,7 @@
"Normal" = "சாதாரண";
"Not allowed" = "அனுமதிக்கப்படவில்லை";
"NSHumanReadableCopyright" = "GPL-3.0 உரிமம்";
"Only if the window is fullscreen" = "முழுத்திரையில் இருக்கும்போது மட்டும்";
"Open Accessibility Preferences…" = "திறந்த அணுகல் விருப்பத்தேர்வுகள்…";
"Open Screen Recording Preferences…" = "திறந்த திரை பதிவு விருப்பத்தேர்வுகள்…";
"Optional: email (if you want a reply)" = "விரும்பினால்: மின்னஞ்சல் (நீங்கள் பதில் விரும்பினால்)";
@@ -99,6 +112,7 @@
"Quit" = "வெளியேறு";
"Quit %@" = "வெளியேறுதல் %@";
"Quit app" = "பயன்பாட்டை விட்டு";
"Recently Created" = "சமீபத்தில் உருவாக்கப்பட்டது";
"Recently Created First" = "சமீபத்தில் முதலில் உருவாக்கப்பட்டது";
"Recently Focused First" = "சமீபத்தில் முதலில் கவனம் செலுத்தியது";
"Remember my choice" = "என் விருப்பத்தை நினைவில் கொள்ளுங்கள்";
@@ -119,6 +133,7 @@
"Send debug profile (CPU, memory, etc)" = "பிழைத்திருத்த சுயவிவரத்தை அனுப்பவும் (CPU, நினைவகம் போன்றவை)";
"Send feedback" = "கருத்துக்களை அனுப்பவும்";
"Send feedback…" = "கருத்துக்களை அனுப்பவும்…";
"Settings…" = "அமைப்புகள்";
"Share improvement ideas, or report bugs" = "மேம்பாட்டு யோசனைகளைப் பகிரவும் அல்லது பிழைகள் புகாரளிக்கவும்";
"Shortcut 1" = "குறுக்குவழி 1";
"Shortcut 2" = "குறுக்குவழி 2";
@@ -149,13 +164,17 @@
"Space Order" = "விண்வெளி வரிசை";
"Start" = "தொடக்க";
"Start at login" = "உள்நுழைவில் தொடங்கவும்";
"Start at login:" = "உள்நுழைவின் போது தொடங்கு:";
"Support this project" = "இந்த திட்டத்தை ஆதரிக்கவும்";
"Switch between 3 different styles. You can customize them." = "3 வெவ்வேறு பாணிகளுக்கு இடையில் மாறவும்.நீங்கள் அவற்றைத் தனிப்பயனாக்கலாம்.";
"System" = "அமைப்பு";
"Theme" = "தீம்";
"Theme:" = "தோற்றம்";
"Then release:" = "பிறகு விடு:";
"This permission is needed to focus windows after you release the shortcut" = "நீங்கள் குறுக்குவழியை வெளியிட்ட பிறகு சாளரங்களை மையப்படுத்த இந்த அனுமதி தேவை";
"This permission is needed to show screenshots and titles of open windows" = "திறந்த சாளரங்களின் ஸ்கிரீன் ஷாட்கள் மற்றும் தலைப்புகளைக் காட்ட இந்த அனுமதி தேவை";
"Thumbnails" = "சிறு உருவங்கள்";
"Title" = "தலைப்பு";
"Title truncation" = "தலைப்பு துண்டிப்பு";
"Titles" = "தலைப்புகள்";
"Trigger shortcut" = "குறுக்குவழியைத் தூண்டவும்";
@@ -124,17 +124,24 @@
"Miscellaneous:" = "Diğer:";
"Mouse hover" = "Fare ile üzerine gelince";
"Multiple screens" = "Birden fazla ekran";
"Never" = "Daha yeni";
"Never send crash reports" = "Çökme raporlarını asla gönderme";
"Non-active apps" = "Aktif Olmayan Uygulamalar";
"Normal" = "Normal";
"Not allowed" = "İzin verilmedi";
"NSHumanReadableCopyright" = "GPL-3.0 lisansı";
"Only if the window is fullscreen" = "Eğer pencere tam ekransa";
"Only on different screen" = "Sadece başka bir ekranda";
"Open Accessibility Preferences…" = "Erişilebilirlik Tercihlerini Aç…";
"Open Accessibility Settings…" = "Erişilebilirlik seçeneklerini aç…";
"Open Screen Recording Preferences…" = "Ekran Kaydı Tercihlerini Aç…";
"Open Screen Recording Settings…" = "Ekran kaydı ayarlarını aç…";
"Open System Settings to confirm, and continue" = "Onaylayıp devam etmek için sistem ayarlarını aç";
"Open Trackpad Preferences…" = "Trackpad Tercihlerini Aç…";
"Open Trackpad Settings…" = "Trackpad ayarlarını aç…";
"Optional: email (if you want a reply)" = "Opsiyonel: E-Posta (Eğer bir yanıt istiyorsanız)";
"Order windows by" = "Pencere sırası";
"Please fill in the form" = "Lütfen formu doldurun";
"Policies" = "Kurallar";
"Preferences…" = "Tercihler…";
"Preview selected window" = "Seçili pencereyi önizle";
@@ -150,6 +157,8 @@
"Remember my choice" = "Tercihimi hatırla";
"Reset preferences and restart" = "Tercihleri sıfırla ve yeniden başlat";
"Reset preferences and restart…" = "Tercihleri sıfırla ve yeniden başlat…";
"Reset settings and restart" = "Ayarları sıfırla ve yeniden başlat";
"Reset settings and restart…" = "Ayarları sıfırla ve yeniden başlat…";
"Restart Now" = "Yeniden Başlat";
"Rows of thumbnails:" = "Önizleme satırları:";
"Screen including menu bar" = "Menü çubuğunun olduğu monitör";
@@ -167,6 +176,7 @@
"Send debug profile (CPU, memory, etc)" = "Hata ayıklama profili gönder (İşlemci, bellek, vb)";
"Send feedback" = "Geri bildirim gönder";
"Send feedback…" = "Geri bildirim gönder…";
"Settings…" = "Ayarlar";
"Share improvement ideas, or report bugs" = "Geliştirme önerisi veya hata bilgisi paylaşın";
"Shortcut 1" = "Kestirme 1";
"Shortcut 2" = "Kestirme 2";
@@ -210,6 +220,9 @@
"Switch between 3 different styles. You can customize them." = "3 farklı stil arasında geçiş yapın.Onları özelleştirebilirsiniz.";
"System" = "Sistem";
"System Default" = "Sistem Varsayılanı";
"The ⌘⎋ (command+escape) shortcut may be used by macOS for GameOverlay.\n\nYour change would assign it to: %@.\n\nIf you want to continue, make sure that the GameOverlay shortcut is disabled.\nAlso note that macOS has reported bugs where the shortcut may still interfere while disabled." = "Command+escape (⌘⎋) kısayolu macOS tarafından GameOverlay için kullanılıyor olabilir.\n\nDeğişikliğiniz bunu şuraya atayacaktır: %@.\n\nDevam etmek istiyorsanız, GameOverlay kısayolunun kapalı olduğundan emin olun.\nAyrıca, macOS'ta kısayol devre dışı bırakıldığında bile hata oluşturabileceğini de unutmayın.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Escape (⎋) tuşu başka macOS kısayolları tarafından kullanılıyor (örn. ⌘⌥⎋ Uygulamaları zorla kapat penceresi).\n\n⎋ tuşunu kullanmak istiyorsanız, Hold değiştiricilerinin bunlardan biri olmadığından emin olun: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃.";
"The ⎋ (escape) key is reserved by some macOS shortcuts (e.g. ⌘⌥⎋ will show the Force Quit Applications window).\n\nYour change would assign it to: %@.\n\nIf you want to use ⎋, make sure that Hold modifiers are neither: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃" = "Escape (⎋) tuşu başka macOS kısayolları tarafından kullanılıyor (örn. ⌘⌥⎋ Uygulamaları zorla kapat penceresi).\n\nDeğişikliğiniz bunu şuraya atayacaktır: %@.\n\n⎋ tuşunu kullanmak istiyorsanız, Hold değiştiricilerinin bunlardan biri olmadığından emin olun: ⌘⌥, ⌘⌥⇧, or ⌘⌥⇧⌃.";
"The application needs to restart to apply the language change." = "Dil değişikliğini uygulamak için uygulamanın yeniden başlatılması gerekiyor.";
"Theme" = "Tema";
"Theme:" = "Tema:";
@@ -221,6 +234,7 @@
"Title" = "Başlık";
"Title truncation" = "Başlık kesilmesi";
"Titles" = "Başlıklar";
"Trackpad haptic feedback" = "Trackpad dokunma geri bildirimi";
"Trigger shortcut" = "Tetik kısayolu";
"Unassign existing shortcut and continue" = "Atanmış kestirmeyi kaldır ve devam et";
"Updates policy" = "Politikayı günceller";
@@ -1,6 +1,5 @@
set -ex
scripts/update_contributors.sh
#scripts/l10n/import_l10n_strings_from_poeditor.sh
scripts/l10n/extract_l10n_strings.sh
#pod install
+1 -1
View File
@@ -2,7 +2,7 @@
set -exu
version="$(cat $VERSION_FILE)"
version="$(cat "$VERSION_FILE")"
appFile="$APP_NAME.app"
zipName="$APP_NAME-$version.zip"
oldPwd="$PWD"
@@ -2,7 +2,7 @@
set -exu
version="$(cat $VERSION_FILE)"
version="$(cat "$VERSION_FILE")"
sed -i '' -e "s/#VERSION#/$version/" Info.plist
sed -i '' -e "s/#FEEDBACK_TOKEN#/$FEEDBACK_TOKEN/" Info.plist
+1 -1
View File
@@ -2,7 +2,7 @@
set -exu
version="$(cat $VERSION_FILE)"
version="$(cat "$VERSION_FILE")"
date="$(date +'%a, %d %b %Y %H:%M:%S %z')"
minimumSystemVersion="$(awk -F ' = ' '/MACOSX_DEPLOYMENT_TARGET/ { print $2; }' < config/base.xcconfig)"
zipName="$APP_NAME-$version.zip"
-37
View File
@@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -exu
readOnlyToken="8170d6b4f0531ffd7f52edea374a3689"
projectId="316051"
function unicode_sort() {
python3 -c 'import sys, unicodedata; print("".join(sorted((line for line in sys.stdin if line.strip()), key=lambda s: unicodedata.normalize("NFKD", s.casefold()))), end="")'
}
(
echo -e "# Contributors\n"
echo -e "They helped [develop the app](https://github.com/lwouis/alt-tab-macos/graphs/contributors):\n"
# this script is ran on shared CI machines which go over github's quota
# thus we use a token here to guaranty that the call will succeed
curl https://api.github.com/repos/lwouis/alt-tab-macos/contributors \
-H "Authorization: token $GITHUB_TOKEN" |
jq -r '.[]|("[" + .login + "](" + .html_url + ")")' |
sed -e '/semantic-release-bot/d' |
unicode_sort |
sed -e 's/^/* /'
echo -e "\nThey helped [localize the app](https://poeditor.com/join/project/8AOEZ0eAZE):\n"
(
echo -e "lwouis"
curl -s -X POST https://api.poeditor.com/v2/contributors/list \
-d api_token="$readOnlyToken" \
-d id="$projectId" |
jq -r '.result.contributors[].name'
) |
unicode_sort |
sed -e 's/^/* /'
) >docs/Contributors.md
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -exu
version="$(cat "$VERSION_FILE")"
readOnlyToken="8170d6b4f0531ffd7f52edea374a3689"
projectId="316051"
github_api_request() {
local url="$1"
curl -s \
-H "Accept: application/vnd.github+json" \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/lwouis/alt-tab-macos$url"
}
poeditor_api_request() {
local url="$1"
curl -s \
-X POST \
-d api_token="$readOnlyToken" \
-d id="$projectId" \
"https://api.poeditor.com/v2$url"
}
unicode_sort() {
python3 -c 'import sys, unicodedata; print("".join(sorted((line for line in sys.stdin if line.strip()), key=lambda s: unicodedata.normalize("NFKD", s.casefold()))), end="")'
}
github_contributors() {
github_api_request "/contributors" |
jq -r '.[]|("[" + .login + "](" + .html_url + ")")' |
sed -e '/semantic-release-bot/d' |
unicode_sort |
awk '{printf "%s%s", sep, $0; sep=", "} END{print ""}'
}
poeditor_contributors() {
(
echo "lwouis"
poeditor_api_request "/contributors/list" |
jq -r '.result.contributors[].name'
) |
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' |
unicode_sort |
awk '{printf "%s%s", sep, $0; sep=", "} END{print ""}'
}
generate_contributors() {
echo "# Contributors"
echo
echo "## [Developed the app](https://github.com/lwouis/alt-tab-macos/graphs/contributors)"
echo
github_contributors
echo
echo "## [Localized the app](https://poeditor.com/join/project/8AOEZ0eAZE)"
echo
poeditor_contributors
}
get_total_downloads() {
local downloads=0
local page=1
while true; do
local response
response=$(github_api_request "/releases?per_page=100&page=$page")
# Safely sum all download counts, even if assets are empty
local count
count=$(echo "$response" | jq '[.[]?.assets[]?.download_count // 0] | add // 0')
downloads=$((downloads + count))
# Stop if no more releases
if [[ $(echo "$response" | jq 'length') -lt 1 ]]; then
break
fi
page=$((page + 1))
done
echo "$downloads"
}
get_stars() {
github_api_request "" | jq '.stargazers_count'
}
format_number() {
local NUM=$1
if (( NUM >= 1000000 )); then
printf "%.1fM" "$(echo "$NUM/1000000" | bc -l)"
elif (( NUM >= 1000 )); then
printf "%.0fK" "$(echo "$NUM/1000" | bc -l)"
else
printf "%d" "$NUM"
fi
}
downloads=$(format_number "$(get_total_downloads)")
stars=$(format_number "$(get_stars)")
contributors=$(generate_contributors)
sed -i "" -E "s|(v)[^/]+(/AltTab-)[^/]+(\.zip)|\1${version}\2${version}\3|g" "README.md"
sed -i "" -E "s|(<sub>)[^ ]+( stars</sub>)|\1${stars}\2|g" "README.md"
sed -i "" -E "s|(<sub>)[^ ]+( downloads</sub>)|\1${downloads}\2|g" "README.md"
sed -i "" -E "s|(>)[^ ]+( downloads<)|\1${downloads}\2|g" "docs/_layouts/default.html"
echo "$contributors" > "docs/Contributors.md"
+1 -1
View File
@@ -5,7 +5,7 @@ set -exu
ownerName="alt-tab-macos"
appName="alt-tab-macos"
url="https://api.appcenter.ms/v0.1/apps/$ownerName/$appName"
version="$(cat $VERSION_FILE)"
version="$(cat "$VERSION_FILE")"
symbolFile="$APP_NAME.app.dSYM"
function firstCall() {
+139 -314
View File
@@ -6,6 +6,7 @@ import ApplicationServices.HIServices.AXRoleConstants
import ApplicationServices.HIServices.AXAttributeConstants
import ApplicationServices.HIServices.AXActionConstants
/// common, subscriptions, concurrency
extension AXUIElement {
// default timeout for AX calls is 6s
// we reduce to 1s to avoid AX calls blocking threads, thus too many threads getting created to make the next AX calls
@@ -21,14 +22,12 @@ extension AXUIElement {
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalMessagingTimeoutInSeconds)
}
enum AXCallType: Int {
case subscribeToAppNotification
case subscribeToWindowNotification
case subscribeToDockNotification
case updateWindow
case updateAppWindows
case updateDockBadges
case axEventEntrypoint
private func throwIfNotSuccess(_ result: AXError) throws -> Void {
// .cannotComplete can happen if the app is unresponsive
if result == .cannotComplete {
throw AxError.runtimeError
}
// for success or other errors we don't throw
}
/// if the window server is busy, it may not reply to AX calls. We retry right before the call times-out and returns a bogus value
@@ -64,9 +63,7 @@ extension AXUIElement {
// if their are common updates, we avoid congestion by only retrying the latest update
if let pid, callType == .updateWindow || callType == .updateAppWindows {
let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
axCallsRetriesQueueUnresponsiveAppsMap.lock.lock()
let time = axCallsRetriesQueueUnresponsiveAppsMap.map[unresponsiveAppsMapKey]
axCallsRetriesQueueUnresponsiveAppsMap.lock.unlock()
let time = axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[unresponsiveAppsMapKey] }
if let time {
if startTimeInNanoseconds > time {
// new most recent call; replace and retry
@@ -85,7 +82,7 @@ extension AXUIElement {
// should we give up?
let timePassedInSeconds = Float(DispatchTime.now().uptimeNanoseconds - startTimeInNanoseconds) / 1_000_000_000
if timePassedInSeconds >= axCallsRetriesQueueTimeoutInSeconds {
Logger.info("AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it", logFromContext(file, function, line, context, callType))
Logger.info { "AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it. \(logFromContext(file, function, line, context, callType))" }
if let pid, callType == .updateWindow || callType == .updateAppWindows {
let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0.removeValue(forKey: unresponsiveAppsMapKey) }
@@ -93,7 +90,7 @@ extension AXUIElement {
return
}
// retry
Logger.info(logFromContext(file, function, line, context, callType))
Logger.debug { "(pid:\(pid) wid:\(wid)) \(logFromContext(file, function, line, context, callType))" }
retryAxCallUntilTimeout(file: file, function: function, line: line, context: context, after: .now() + humanPerceptionDelay, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: true, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block)
}
@@ -101,14 +98,45 @@ extension AXUIElement {
return "Context: \((file as NSString).lastPathComponent):\(line) \(function) \(String(describing: callType)) \(context)"
}
func throwIfNotSuccess(_ result: AXError) throws -> Void {
// .cannotComplete can happen if the app is unresponsive
if result == .cannotComplete {
throw AxError.runtimeError
@discardableResult
func subscribeToNotification(_ axObserver: AXObserver, _ notification: String, _ callback: (() -> Void)? = nil) throws -> Bool {
let result = AXObserverAddNotification(axObserver, self, notification as CFString, nil)
if result == .success || result == .notificationAlreadyRegistered {
return true
}
// for success or other errors we don't throw
if result == .notificationUnsupported || result == .notImplemented {
// subscription will never succeed
return false
}
// temporary issue; subscription may succeed if retried
throw AxError.runtimeError
}
enum AXCallType: Int {
case subscribeToAppNotification
case subscribeToWindowNotification
case subscribeToDockNotification
case updateWindow
case updateAppWindows
case updateDockBadges
case axEventEntrypoint
}
final class ConcurrentMap<K: Hashable, V> {
private var map = [K: V]()
private let lock = NSLock()
@discardableResult
func withLock<T>(_ block: (inout [K: V]) -> T) -> T {
lock.lock()
defer { lock.unlock() }
return block(&map)
}
}
}
/// Attributes
extension AXUIElement {
// periphery:ignore
func id() -> AXUIElementID? {
let pointer = UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque()).advanced(by: 0x20)
@@ -130,94 +158,83 @@ extension AXUIElement {
return pid
}
func attribute<T>(_ key: String, _ _: T.Type) throws -> T? {
var attributeValue: AnyObject?
try throwIfNotSuccess(AXUIElementCopyAttributeValue(self, key as CFString, &attributeValue))
return attributeValue as? T
}
func windowAttributes() throws -> (String?, String?, String?, Bool, Bool)? {
let attributes = [
kAXTitleAttribute,
kAXRoleAttribute,
kAXSubroleAttribute,
kAXMinimizedAttribute,
kAXFullscreenAttribute,
]
func attributes(_ keys: [String]) throws -> AXAttributes {
var values: CFArray?
try throwIfNotSuccess(AXUIElementCopyMultipleAttributeValues(self, attributes as CFArray, [], &values))
if let array = (values as? Array<Any>) {
return (
array[0] as? String,
array[1] as? String,
array[2] as? String,
// if the value is nil, we return false. This avoid returning Bool?; simplifies things
(array[3] as? Bool) ?? false,
// if the value is nil, we return false. This avoid returning Bool?; simplifies things
(array[4] as? Bool) ?? false
)
}
return nil
}
private func value<T>(_ key: String, _ target: T, _ type: AXValueType) throws -> T? {
if let a = try attribute(key, AXValue.self) {
var value = target
_ = withUnsafePointer(to: &value) {
AXValueGetValue(a, type, UnsafeMutableRawPointer(mutating: $0))
try throwIfNotSuccess(AXUIElementCopyMultipleAttributeValues(self, keys as CFArray, [], &values))
let array = values as? [CFTypeRef] ?? []
var result = AXAttributes()
for (index, key) in keys.enumerated() {
guard index < array.count else { continue }
let value = array[index]
switch key {
case kAXTitleAttribute: result.title = castSafely(value)
case kAXRoleAttribute: result.role = castSafely(value)
case kAXSubroleAttribute: result.subrole = castSafely(value)
case kAXStatusLabelAttribute: result.statusLabel = castSafely(value)
case kAXMinimizedAttribute: result.isMinimized = castSafely(value)
case kAXFullscreenAttribute: result.isFullscreen = castSafely(value)
case kAXIsApplicationRunningAttribute: result.appIsRunning = castSafely(value)
case kAXURLAttribute: result.url = castSafely(value)
case kAXParentAttribute: result.parent = castSafely(value)
case kAXFocusedWindowAttribute: result.focusedWindow = castSafely(value)
case kAXMainWindowAttribute: result.mainWindow = castSafely(value)
case kAXCloseButtonAttribute: result.closeButton = castSafely(value)
case kAXChildrenAttribute: result.children = castSafely(value)
case kAXWindowsAttribute: result.windows = castSafely(value)
case kAXPositionAttribute: result.position = castSafely(value)
case kAXSizeAttribute: result.size = castSafely(value)
default: Logger.error { "key:\(key) value:\(value)" }
}
return value
}
return nil
return result
}
func title() throws -> String? {
return try attribute(kAXTitleAttribute, String.self)
func castSafely<T>(_ value: CFTypeRef) -> T? {
switch CFGetTypeID(value) {
case AXValueGetTypeID():
let axValue = value as! AXValue
switch AXValueGetType(axValue) {
case .axError:
// without .stopOnError, AXUIElementCopyMultipleAttributeValues always returns an array. it contains placeholder values.
// This makes it very hard to know what's real. For example, if an app has no MainWindow, it will return .axError. If we cast it to AXUIElement, it will succeed, but the object will have its attributes zero'd
// we have to check for .axError, which we map to nil values
return nil
case .cgSize:
var size = CGSize.zero
AXValueGetValue(axValue, .cgSize, &size)
return size as? T
case .cgPoint:
var point = CGPoint.zero
AXValueGetValue(axValue, .cgPoint, &point)
return point as? T
case let unknownAXValueType:
Logger.error { unknownAXValueType }
return nil
}
case AXUIElementGetTypeID(): return value as? T
case CFArrayGetTypeID(): return value as? T
case CFURLGetTypeID(): return value as? T
case CFStringGetTypeID(): return value as? T
case CFBooleanGetTypeID(): return value as? T
case let unknownCFTypeID:
Logger.error { unknownCFTypeID }
return nil
}
}
// periphery:ignore
func parent() throws -> AXUIElement? {
return try attribute(kAXParentAttribute, AXUIElement.self)
}
func children() throws -> [AXUIElement]? {
return try attribute(kAXChildrenAttribute, [AXUIElement].self)
}
func isMinimized() throws -> Bool {
// if the AX call doesn't return, we return false. This avoid returning Bool?; simplifies things
return try attribute(kAXMinimizedAttribute, Bool.self) == true
}
func isFullscreen() throws -> Bool {
// if the AX call doesn't return, we return false. This avoid returning Bool?; simplifies things
return try attribute(kAXFullscreenAttribute, Bool.self) == true
}
func focusedWindow() throws -> AXUIElement? {
return try attribute(kAXFocusedWindowAttribute, AXUIElement.self)
}
func role() throws -> String? {
return try attribute(kAXRoleAttribute, String.self)
}
func subrole() throws -> String? {
return try attribute(kAXSubroleAttribute, String.self)
}
func closeButton() throws -> AXUIElement? {
return try attribute(kAXCloseButtonAttribute, AXUIElement.self)
}
func appIsRunning() throws -> Bool? {
return try attribute(kAXIsApplicationRunningAttribute, Bool.self)
/// we combine both the normal approach and brute-force to get all possible windows
/// with only normal approach: we miss other-Spaces windows
/// with only brute-force approach: we miss windows when the app launches (e.g. launch Note.app: first window is not found by brute-force)
func allWindows(_ pid: pid_t) throws -> [AXUIElement] {
let aWindows = try windows()
let bWindows = Self.windowsByBruteForce(pid)
return Array(Set(aWindows + bWindows))
}
/// doesn't return windows on other Spaces
/// use windowsByBruteForce if you want those
func windows() throws -> [AXUIElement] {
let windows = try attribute(kAXWindowsAttribute, [AXUIElement].self)
private func windows() throws -> [AXUIElement] {
let windows = try attributes([kAXWindowsAttribute]).windows
if let windows,
!windows.isEmpty {
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
@@ -229,23 +246,6 @@ extension AXUIElement {
return []
}
func position() throws -> CGPoint? {
return try value(kAXPositionAttribute, CGPoint.zero, .cgPoint)
}
func size() throws -> CGSize? {
return try value(kAXSizeAttribute, CGSize.zero, .cgSize)
}
/// we combine both the normal approach and brute-force to get all possible windows
/// with only normal approach: we miss other-Spaces windows
/// with only brute-force approach: we miss windows when the app launches (e.g. launch Note.app: first window is not found by brute-force)
func allWindows(_ pid: pid_t) throws -> [AXUIElement] {
let aWindows = try windows()
let bWindows = AXUIElement.windowsByBruteForce(pid)
return Array(Set(aWindows + bWindows))
}
/// brute-force getting the windows of a process by iterating over AXUIElementID one by one
private static func windowsByBruteForce(_ pid: pid_t) -> [AXUIElement] {
// we use this to call _AXUIElementCreateWithRemoteToken; we reuse the object for performance
@@ -261,7 +261,7 @@ extension AXUIElement {
for axUiElementId: AXUIElementID in 0..<1000 {
remoteToken.replaceSubrange(12..<20, with: withUnsafeBytes(of: axUiElementId) { Data($0) })
if let axUiElement = _AXUIElementCreateWithRemoteToken(remoteToken as CFData)?.takeRetainedValue(),
let subrole = try? axUiElement.subrole(),
let subrole = try? axUiElement.attributes([kAXSubroleAttribute]).subrole,
[kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) {
axWindows.append(axUiElement)
}
@@ -271,178 +271,10 @@ extension AXUIElement {
}
return axWindows
}
}
static func isActualWindow(_ app: Application, _ wid: CGWindowID, _ level: CGWindowLevel, _ title: String?, _ subrole: String?, _ role: String?, _ size: CGSize?) -> Bool {
// Some non-windows have title: nil (e.g. some OS elements)
// Some non-windows have subrole: nil (e.g. some OS elements), "AXUnknown" (e.g. Bartender), "AXSystemDialog" (e.g. Intellij tooltips)
// Minimized windows or windows of a hidden app have subrole "AXDialog"
// Activity Monitor main window subrole is "AXDialog" for a brief moment at launch; it then becomes "AXStandardWindow"
// Some non-windows have cgWindowId == 0 (e.g. windows of apps starting at login with the checkbox "Hidden" checked)
return wid != 0
// Finder's file copy dialogs are wide but < 100 height (see https://github.com/lwouis/alt-tab-macos/issues/1466)
// Sonoma introduced a bug: a caps-lock & language indicators shows as a small window.
// We try to hide it by filtering out tiny windows
&& size != nil && (size!.width > 100 && size!.height > 50) && (
(
books(app) ||
keynote(app) ||
preview(app, subrole) ||
iina(app) ||
openFlStudio(app, title) ||
crossoverWindow(app, role, subrole, level) ||
isAlwaysOnTopScrcpy(app, level, role, subrole)
) || (
// CGWindowLevel == .normalWindow helps filter out iStats Pro and other top-level pop-overs, and floating windows
level == CGWindow.normalLevel && (
[kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) ||
openBoard(app) ||
adobeAudition(app, subrole) ||
adobeAfterEffects(app, subrole) ||
steam(app, title, role) ||
worldOfWarcraft(app, role) ||
battleNetBootstrapper(app, role) ||
firefox(app, role, size) ||
vlcFullscreenVideo(app, role) ||
sanGuoShaAirWD(app) ||
dvdFab(app) ||
drBetotte(app) ||
androidEmulator(app, title) ||
autocad(app, subrole)
) && (
mustHaveIfJetbrainApp(app, title, subrole, size!) &&
mustHaveIfSteam(app, title, role) &&
mustHaveIfFusion360(app, title, role) &&
mustHaveIfColorSlurp(app, subrole)
)
)
)
}
private static func mustHaveIfFusion360(_ app: Application, _ title: String?, _ role: String?) -> Bool {
// filter out Autodesk Fusion side panels "Browser" and "Comments" with subrole AXDialog but with no title
return app.bundleIdentifier != "com.autodesk.fusion360" || (title != nil && title != "")
}
private static func mustHaveIfJetbrainApp(_ app: Application, _ title: String?, _ subrole: String?, _ size: NSSize) -> Bool {
// jetbrain apps sometimes generate non-windows that pass all checks in isActualWindow
// they have no title, so we can filter them out based on that
// we also hide windows too small
return app.bundleIdentifier?.range(of: "^com\\.(jetbrains\\.|google\\.android\\.studio).*?$", options: .regularExpression) == nil || (
(subrole == kAXStandardWindowSubrole || (title != nil && title != "")) &&
size.width > 100 && size.height > 100
)
}
private static func mustHaveIfColorSlurp(_ app: Application, _ subrole: String?) -> Bool {
return app.bundleIdentifier != "com.IdeaPunch.ColorSlurp" || subrole == kAXStandardWindowSubrole
}
private static func iina(_ app: Application) -> Bool {
// IINA.app can have videos float (level == 2 instead of 0)
// there is also complex animations during which we may or may not consider the window not a window
return app.bundleIdentifier == "com.colliderli.iina"
}
private static func keynote(_ app: Application) -> Bool {
// apple Keynote has a fake fullscreen window when in presentation mode
// it covers the screen with a AXUnknown window instead of using standard fullscreen mode
return app.bundleIdentifier == "com.apple.iWork.Keynote"
}
private static func preview(_ app: Application, _ subrole: String?) -> Bool {
// when opening multiple documents at once with apple Preview,
// one of the window will have level == 1 for some reason
return app.bundleIdentifier == "com.apple.Preview" && [kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole)
}
private static func openFlStudio(_ app: Application, _ title: String?) -> Bool {
// OpenBoard is a ported app which doesn't use standard macOS windows
return app.bundleIdentifier == "com.image-line.flstudio" && (title != nil && title != "")
}
private static func openBoard(_ app: Application) -> Bool {
// OpenBoard is a ported app which doesn't use standard macOS windows
return app.bundleIdentifier == "org.oe-f.OpenBoard"
}
private static func adobeAudition(_ app: Application, _ subrole: String?) -> Bool {
return app.bundleIdentifier == "com.adobe.Audition" && subrole == kAXFloatingWindowSubrole
}
private static func adobeAfterEffects(_ app: Application, _ subrole: String?) -> Bool {
return app.bundleIdentifier == "com.adobe.AfterEffects" && subrole == kAXFloatingWindowSubrole
}
private static func books(_ app: Application) -> Bool {
// Books.app has animations on window creation. This means windows are originally created with subrole == AXUnknown or isOnNormalLevel == false
return app.bundleIdentifier == "com.apple.iBooksX"
}
private static func worldOfWarcraft(_ app: Application, _ role: String?) -> Bool {
return app.bundleIdentifier == "com.blizzard.worldofwarcraft" && role == kAXWindowRole
}
private static func battleNetBootstrapper(_ app: Application, _ role: String?) -> Bool {
// Battlenet bootstrapper windows have subrole == AXUnknown
return app.bundleIdentifier == "net.battle.bootstrapper" && role == kAXWindowRole
}
private static func drBetotte(_ app: Application) -> Bool {
return app.bundleIdentifier == "com.ssworks.drbetotte"
}
private static func dvdFab(_ app: Application) -> Bool {
return app.bundleIdentifier == "com.goland.dvdfab.macos"
}
private static func sanGuoShaAirWD(_ app: Application) -> Bool {
return app.bundleIdentifier == "SanGuoShaAirWD"
}
private static func steam(_ app: Application, _ title: String?, _ role: String?) -> Bool {
// All Steam windows have subrole == AXUnknown
// some dropdown menus are not desirable; they have title == "", or sometimes role == nil when switching between menus quickly
return app.bundleIdentifier == "com.valvesoftware.steam" && (title != nil && title != "" && role != nil)
}
private static func mustHaveIfSteam(_ app: Application, _ title: String?, _ role: String?) -> Bool {
// All Steam windows have subrole == AXUnknown
// some dropdown menus are not desirable; they have title == "", or sometimes role == nil when switching between menus quickly
return app.bundleIdentifier != "com.valvesoftware.steam" || (title != nil && title != "" && role != nil)
}
private static func firefox(_ app: Application, _ role: String?, _ size: CGSize?) -> Bool {
// Firefox fullscreen video have subrole == AXUnknown if fullscreen'ed when the base window is not fullscreen
// Firefox tooltips are implemented as windows with subrole == AXUnknown
return (app.bundleIdentifier?.hasPrefix("org.mozilla.firefox") ?? false) && role == kAXWindowRole && size?.height != nil && size!.height > 400
}
private static func vlcFullscreenVideo(_ app: Application, _ role: String?) -> Bool {
// VLC fullscreen video have subrole == AXUnknown if fullscreen'ed
return (app.bundleIdentifier?.hasPrefix("org.videolan.vlc") ?? false) && role == kAXWindowRole
}
private static func androidEmulator(_ app: Application, _ title: String?) -> Bool {
// android emulator small vertical menu is a "window" with empty title; we exclude it
return title != "" && Applications.isAndroidEmulator(app.bundleIdentifier, app.pid)
}
private static func crossoverWindow(_ app: Application, _ role: String?, _ subrole: String?, _ level: CGWindowLevel) -> Bool {
return app.bundleIdentifier == nil && role == kAXWindowRole && subrole == kAXUnknownSubrole && level == CGWindow.normalLevel
&& (app.localizedName == "wine64-preloader" || app.executableURL?.absoluteString.contains("/winetemp-") ?? false)
}
private static func isAlwaysOnTopScrcpy(_ app: Application, _ level: CGWindowLevel, _ role: String?, _ subrole: String?) -> Bool {
// scrcpy presents as a floating window when "Always on top" is enabled, so it doesn't get picked up normally.
// It also doesn't have a bundle ID, so we need to match using the localized name, which should always be the same.
return app.localizedName == "scrcpy" && level == CGWindow.floatingWindow && role == kAXWindowRole && subrole == kAXStandardWindowSubrole
}
private static func autocad(_ app: Application, _ subrole: String?) -> Bool {
// AutoCAD uses the undocumented "AXDocumentWindow" subrole
return (app.bundleIdentifier?.hasPrefix("com.autodesk.AutoCAD") ?? false) && subrole == kAXDocumentWindowSubrole
}
/// Actions
extension AXUIElement {
func focusWindow() throws {
try performAction(kAXRaiseAction)
}
@@ -454,24 +286,6 @@ extension AXUIElement {
func performAction(_ action: String) throws {
try throwIfNotSuccess(AXUIElementPerformAction(self, action as CFString))
}
@discardableResult
func subscribeToNotification(_ axObserver: AXObserver, _ notification: String, _ callback: (() -> Void)? = nil) throws -> Bool {
let result = AXObserverAddNotification(axObserver, self, notification as CFString, nil)
if result == .success || result == .notificationAlreadyRegistered {
return true
}
if result == .notificationUnsupported || result == .notImplemented {
// subscription will never succeed
return false
}
// temporary issue; subscription may succeed if retried
throw AxError.runtimeError
}
}
enum AxError: Error {
case runtimeError
}
/// tests have shown that this ID has a range going from 0 to probably UInt.MAX
@@ -480,19 +294,30 @@ enum AxError: Error {
/// we don't know how high it can go, and if it wraps around
typealias AXUIElementID = UInt64
enum AxError: Error {
case runtimeError
}
struct AXAttributes {
var title: String?
var role: String?
var subrole: String?
var isMinimized: Bool?
var isFullscreen: Bool?
var parent: AXUIElement?
var children: [AXUIElement]?
var focusedWindow: AXUIElement?
var mainWindow: AXUIElement?
var closeButton: AXUIElement?
var appIsRunning: Bool?
var url: URL?
var statusLabel: String?
var windows: [AXUIElement]?
var position: CGPoint?
var size: CGSize?
}
enum DebounceType: Int {
case windowResizedOrMoved
case windowTitleChanged
}
class ConcurrentMap<K: Hashable, V> {
var map = [K: V]()
let lock = NSLock()
@discardableResult
func withLock<T>(_ block: (inout [K: V]) -> T) -> T {
lock.lock()
defer { lock.unlock() }
return block(&map)
}
}
-18
View File
@@ -15,24 +15,6 @@ extension CGWindowID {
return CGSCopySpacesForWindows(CGS_CONNECTION, CGSSpaceMask.all.rawValue, [self] as CFArray) as! [CGSSpaceID]
}
func screenshot() -> CGImage? {
// we use CGSHWCaptureWindowList because it can screenshot minimized windows, which CGWindowListCreateImage can't
var windowId_ = self
let list = CGSHWCaptureWindowList(CGS_CONNECTION, &windowId_, 1, [.ignoreGlobalClipShape, .bestResolution, .fullSize]).takeRetainedValue() as! [CGImage]
return list.first
// // CGWindowListCreateImage
// return CGWindowListCreateImage(.null, .optionIncludingWindow, self, [.boundsIgnoreFraming, .bestResolution])
//
// // CGSCaptureWindowsContentsToRectWithOptions
// var windowId_ = self
// if Testt.sampleCgImage == nil {
// Testt.sampleCgImage = CGWindowListCreateImage(.null, .optionIncludingWindow, self, [.boundsIgnoreFraming, .bestResolution])!
// }
// var image = Testt.sampleCgImage!
// CGSCaptureWindowsContentsToRectWithOptions(cgsMainConnectionId, &windowId_, true, .zero, [.windowCaptureNominalResolution, .captureIgnoreGlobalClipShape], &image)
// return image
}
private func cgProperty<T>(_ key: String, _ type: T.Type) -> T? {
var value: AnyObject?
CGSCopyWindowProperty(CGS_CONNECTION, self, key as CFString, &value)
+70 -10
View File
@@ -1,5 +1,6 @@
import Cocoa
import Darwin
import Carbon.HIToolbox.Events
extension NSAppearance {
func getThemeName() -> AppearanceThemePreference {
@@ -99,9 +100,7 @@ extension NSView {
}
if (y) {
let diff = superviewSize.height - selfSize.height
// if there is no perfect centering, we biais top, as it's more aesthetic for ThumbnailView.label
let diffWithBiasTop = diff.truncatingRemainder(dividingBy: 2) == 0 ? diff : diff - 1
frame.origin.y = (diffWithBiasTop / 2).rounded()
frame.origin.y = (diff / 2).rounded()
}
}
@@ -194,20 +193,24 @@ extension DispatchQoS {
}
extension NSImage {
func appIconFixedSize(_ size: NSSize) -> CGImage? {
var rect = NSRect(origin: .zero, size: size)
return cgImage(forProposedRect: &rect, context: nil, hints: nil)
}
// NSImage(named) caches/reuses NSImage objects; we force separate instances of images by using copy()
static func initCopy(_ name: String) -> NSImage {
return NSImage(named: name)!.copy() as! NSImage
}
func tinted(_ color: NSColor) -> NSImage {
NSImage(size: size, flipped: false) { rect in
color.set()
rect.fill()
self.draw(in: rect, from: NSRect(origin: .zero, size: self.size), operation: .destinationIn, fraction: 1.0)
return true
}
}
}
extension CGImage {
func nsImage() -> NSImage {
return NSImage(cgImage: self, size: NSSize(width: width, height: height))
return NSImage(cgImage: self, size: size())
}
static func named(_ imageName: String) -> CGImage {
@@ -215,6 +218,36 @@ extension CGImage {
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, nil)!
return CGImageSourceCreateImageAtIndex(imageSource, 0, nil)!
}
func size() -> NSSize {
return NSSize(width: width, height: height)
}
func iFullyTransparent() -> Bool {
guard ![.none, .noneSkipFirst, .noneSkipLast].contains(alphaInfo),
let provider = dataProvider, let data = provider.data, let ptr = CFDataGetBytePtr(data)
else { return false }
// Assumes: kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Little
// Layout: [B, G, R, A]
let length = CFDataGetLength(data)
var i = 3
while i < length {
if ptr[i] != 0 {
return false
}
i += 4
}
return true
}
}
extension CVPixelBuffer {
func size() -> NSSize {
NSSize(
width: CVPixelBufferGetWidth(self),
height: CVPixelBufferGetHeight(self)
)
}
}
extension pid_t {
@@ -315,7 +348,7 @@ extension DispatchTimeInterval {
}
extension NSRunningApplication {
var id: String { "pid:\(String(describing: processIdentifier)) app:\(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName ?? "nil")" }
func debugId() -> String { "(pid:\(processIdentifier) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName))" }
}
// 250ms is similar to human delay in processing changes on screen
@@ -334,3 +367,30 @@ extension NSTouch.Phase {
}
}
}
/// this changes the behavior of interpolating optional values (e.g. "\(optionalValue)")
/// default is to return a compiler warning "string interpolation produces a debug description for an optional value; did you mean to make this explicit?"
/// instead, we either print the value, or print "nil"
extension String.StringInterpolation {
mutating func appendInterpolation<T>(_ value: T?) {
if let value {
appendInterpolation(value)
} else {
appendLiteral("nil")
}
}
}
extension CGEvent {
func toNSEvent() -> NSEvent? {
if Thread.isMainThread {
return NSEvent(cgEvent: self)
}
// conversion has to happen on the main-thread, or appkit will crash
var nsEvent: NSEvent?
DispatchQueue.main.sync {
nsEvent = NSEvent(cgEvent: self)
}
return nsEvent
}
}
+11 -11
View File
@@ -34,27 +34,27 @@ class Logger {
return .error
}
static func debug(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(.debug, items, file: file, function: function, line: line, context: context)
static func debug(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(level: .debug, file: file, function: function, line: line, context: context, message)
}
static func info(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(.info, items, file: file, function: function, line: line, context: context)
static func info(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(level: .info, file: file, function: function, line: line, context: context, message)
}
static func warning(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(.warning, items, file: file, function: function, line: line, context: context)
static func warning(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(level: .warning, file: file, function: function, line: line, context: context, message)
}
static func error(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(.error, items, file: file, function: function, line: line, context: context)
static func error(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
custom(level: .error, file: file, function: function, line: line, context: context, message)
}
private static func custom(_ level: SwiftyBeaver.Level, _ items: [Any?], file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
let message = items.map { "\($0 ?? "nil")" }.joined(separator: " ")
logger.custom(level: level, message: "[\(threadName())] \(message)", file: file, function: function, line: line, context: context)
private static func custom(level: SwiftyBeaver.Level, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil, _ message: @escaping () -> Any?) {
logger.custom(level: level, message: { "[\(threadName())] \(message())" }(), file: file, function: function, line: line, context: context)
}
private static func threadName() -> String {
if Thread.isMainThread {
return "main"
+17 -15
View File
@@ -32,9 +32,21 @@ class Markdown {
let attributedString = NSMutableAttributedString()
for line in markdown.split(separator: "\n", omittingEmptySubsequences: false) {
let processedLine = NSMutableAttributedString(string: String(line) + "\n", attributes: baseAttributes)
// links
let linkMatches = linkRegex.matches(in: processedLine.string, range: NSRange(location: 0, length: processedLine.string.utf16.count))
for match in linkMatches.reversed() {
let linkTextRange = match.range(at: 1)
let linkURLRange = match.range(at: 2)
let linkText = processedLine.attributedSubstring(from: linkTextRange).string
let linkURL = processedLine.attributedSubstring(from: linkURLRange).string
processedLine.replaceCharacters(in: match.range, with: linkText)
processedLine.addAttributes(linkAttributes, range: NSRange(location: match.range.location, length: linkText.utf16.count))
processedLine.addAttribute(.link, value: linkURL, range: NSRange(location: match.range.location, length: linkText.utf16.count))
}
// titles
if processedLine.string.starts(with: "# ") {
processedLine.replaceCharacters(in: NSRange(location: 0, length: 2), with: "")
let titlePattern = "## "
if processedLine.string.starts(with: titlePattern) {
processedLine.replaceCharacters(in: NSRange(location: 0, length: titlePattern.count), with: "")
processedLine.addAttributes(titleAttributes, range: NSRange(location: 0, length: processedLine.length - 1)) // Exclude the newline
attributedString.append(processedLine)
continue
@@ -48,21 +60,11 @@ class Markdown {
processedLine.addAttributes(boldAttributes, range: NSRange(location: match.range.location, length: boldText.utf16.count))
}
// list items
if processedLine.string.trimmingCharacters(in: .whitespaces).starts(with: "*") {
processedLine.replaceCharacters(in: NSRange(location: 0, length: 1), with: "•")
let listPattern = "*"
if processedLine.string.trimmingCharacters(in: .whitespaces).starts(with: listPattern) {
processedLine.replaceCharacters(in: NSRange(location: 0, length: listPattern.count), with: "•")
processedLine.addAttributes(listItemAttributes, range: NSRange(location: 0, length: processedLine.length - 1))
}
// links
let linkMatches = linkRegex.matches(in: processedLine.string, range: NSRange(location: 0, length: processedLine.string.utf16.count))
for match in linkMatches.reversed() {
let linkTextRange = match.range(at: 1)
let linkURLRange = match.range(at: 2)
let linkText = processedLine.attributedSubstring(from: linkTextRange).string
let linkURL = processedLine.attributedSubstring(from: linkURLRange).string
processedLine.replaceCharacters(in: match.range, with: linkText)
processedLine.addAttributes(linkAttributes, range: NSRange(location: match.range.location, length: linkText.utf16.count))
processedLine.addAttribute(.link, value: linkURL, range: NSRange(location: match.range.location, length: linkText.utf16.count))
}
attributedString.append(processedLine)
}
return attributedString
+1 -1
View File
@@ -18,7 +18,7 @@ class MissionControl {
stateLock.lock()
defer { stateLock.unlock() }
state_ = state
Logger.info(state)
Logger.info { state }
}
// on macOS < 12, this is the way we used to guess if Mission Control is active
+1 -1
View File
@@ -12,7 +12,7 @@ class IOKitPrototype {
kIOHIDDeviceUsageKey: kHIDUsage_GD_Keyboard
] as CFDictionary)
IOHIDManagerRegisterInputValueCallback(manager, keyboardEventHandler, nil)
IOHIDManagerScheduleWithRunLoop(manager, BackgroundWork.keyboardAndTrackpadEventsThread.runLoop!, CFRunLoopMode.commonModes.rawValue)
IOHIDManagerScheduleWithRunLoop(manager, BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop!, CFRunLoopMode.commonModes.rawValue)
IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
}
}
@@ -13,7 +13,7 @@ if #available(macOS 13.1, *) {
}
}
let windows = CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionOnScreenOnly], kCGNullWindowID) as! [CGWindow]
Logger.error("CGWindowListCopyWindowInfo extra", windows.sorted { $0.id()! < $1.id()! }.map { ($0.id()!, $0.title() ?? "nil") })
Logger.error("CGWindowListCopyWindowInfo extra", windows.sorted { $0.id()! < $1.id()! }.map { ($0.id()!, $0.title()) })
Logger.error("CGWindowListCopyWindowInfo", windows.sorted { $0.id()! < $1.id()! }.map { $0.id()! })
Spaces.refresh()
let spaceIdsAndIndexes = Spaces.idsAndIndexes.map { $0.0 }
+26 -6
View File
@@ -42,14 +42,16 @@ class ATShortcut {
return false
}
private func modifiersMatch(_ modifiers: UInt32) -> Bool {
private func modifiersMatch(_ modifiers: CarbonModifierFlags) -> Bool {
let modifiersCleaned = modifiers.cleaned()
let shortcutModifiersCleaned = shortcut.carbonModifierFlags.cleaned()
// holdShortcut: contains at least
if id.hasPrefix("holdShortcut") {
return modifiers == (modifiers | shortcut.carbonModifierFlags)
return modifiersCleaned == (modifiersCleaned | shortcutModifiersCleaned)
}
// other shortcuts: contains exactly or exactly + holdShortcut modifiers
let holdModifiers = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", App.app.shortcutIndex)]?.shortcut.carbonModifierFlags ?? 0
return modifiers == shortcut.carbonModifierFlags || modifiers == (shortcut.carbonModifierFlags | holdModifiers)
let holdModifiersCleaned = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", App.app.shortcutIndex)]?.shortcut.carbonModifierFlags.cleaned() ?? 0
return modifiersCleaned == shortcutModifiersCleaned || modifiersCleaned == (shortcutModifiersCleaned | holdModifiersCleaned)
}
func shouldTrigger() -> Bool {
@@ -70,7 +72,7 @@ class ATShortcut {
}
func executeAction(_ isARepeat: Bool) {
Logger.info("executeAction", id)
Logger.info { self.id }
ATShortcut.lastEventIsARepeat = isARepeat
ControlsTab.executeAction(id)
}
@@ -95,7 +97,7 @@ class ATShortcut {
}
if state == .up {
// ensure timers don't keep running if their shortcut is UP
KeyRepeatTimer.deactivateTimerForRepeatingKey(id)
KeyRepeatTimer.stopTimerForRepeatingKey(id)
}
}
}
@@ -114,3 +116,21 @@ enum ShortcutScope {
case global
case local
}
extension NSEvent.ModifierFlags {
// NSEvent.addLocalMonitorForEvents may return events with broken modifiers (e.g. [.NSEventModifierFlagOption, .NSEventModifierFlagFunction, 0x120])
// we filter modifiers to only include valid modifiers; which doesn't include fn as we don't support it as a modifier
func cleaned() -> Self {
return self.intersection([.command, .shift, .option, .control, .capsLock])
}
}
typealias CarbonModifierFlags = UInt32
extension CarbonModifierFlags {
// cocoaToCarbonFlags may remove NSEventModifierFlagFunction
// we filter modifiers to only include valid modifiers; which doesn't include fn as we don't support it as a modifier
func cleaned() -> Self {
return self & (UInt32(cmdKey) | UInt32(shiftKey) | UInt32(optionKey) | UInt32(controlKey) | UInt32(alphaLock))
}
}
+7 -5
View File
@@ -27,7 +27,7 @@ class Appearance {
static var highlightBorderWidth = CGFloat(3)
// theme: constants
static let enablePanelShadow = true
static var enablePanelShadow = true
static var highlightFocusedBackgroundColor: NSColor { get { NSColor.systemAccentColor.withAlphaComponent(0.2) } }
static var highlightHoveredBackgroundColor: NSColor { get { NSColor.systemAccentColor.withAlphaComponent(0.1) } }
static var highlightFocusedBorderColor: NSColor { get { NSColor.systemAccentColor } }
@@ -75,10 +75,12 @@ class Appearance {
} else {
lightTheme()
}
// // for Liquid Glass, we don't want a shadow around the panel
// if #available(macOS 26.0, *), currentStyle == .appIcons && LiquidGlassEffectView.canUsePrivateLiquidGlassLook() {
// enablePanelShadow = false
// }
// for Liquid Glass, we don't want a shadow around the panel
if #available(macOS 26.0, *), currentStyle == .appIcons && LiquidGlassEffectView.canUsePrivateLiquidGlassLook() {
enablePanelShadow = false
} else {
enablePanelShadow = true
}
}
private static func thumbnailsSize(_ isHorizontalScreen: Bool) {
+74 -86
View File
@@ -19,7 +19,7 @@ class Application: NSObject {
var dockLabel: String?
var focusedWindow: Window? = nil
var alreadyRequestedToQuit = false
var debugId: String { "(pid:\(String(describing: pid)) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName ?? "nil"))" }
var debugId: String
static let notifications = [
kAXApplicationActivatedNotification,
@@ -42,16 +42,30 @@ class Application: NSObject {
return 0
}()
private static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? {
/// Converting NSImage to CGImage may seem simple, but it's actually very tricky. Lots of time has been put to make it work robustly
/// The RunningApplication.icon can have store bitmaps or vectors. We have to rasterize into pixels. This is not easy as there are many APIs:
/// * icon.cgImage(forProposedRect:) > context.draw -> only API which works
/// * icon.cgImage(forProposedRect:) > cgImage.draw -> returns nil for some users (could never reproduce it locally)
/// * icon.draw() -> returns nil for some users (could never reproduce it locally)
/// * icon.bestRepresentation() > bestRep.draw(in:) -> returns nil for some users (could never reproduce it locally)
/// MacOS Big Sur also introduced a constant padding around app icons. It was later increased with Tahoe. We have to crop it
static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? {
guard let icon else { return nil }
// we can render the icon quite big (e.g. windowless app icon), so we store it high-res
let iconWidth = CGFloat(1024)
// NSRunningApplication.icon returns icons with padding; we remove it manually
let croppedSize = iconWidth - appIconPadding * 2
return icon
.appIconFixedSize(NSSize(width: iconWidth, height: iconWidth))?
.cropping(to: CGRect(x: appIconPadding, y: appIconPadding, width: croppedSize, height: croppedSize).integral)
let finalWidth = max(ThumbnailsPanel.maxPossibleAppIconSize.width, ThumbnailsPanel.maxPossibleAppIconSize.height)
// we hardcode cropping values based on a reference 1024 icon, and depending on the macOS version
let padding = appIconPadding * (finalWidth / (1024 - appIconPadding * 2))
// we need a bigger image size, since we'll crop to reach finalWidth
let sourceWidth = finalWidth + padding * 2
// we ask the NSImage for the closest image it has to our desired size. It's likely to return a 1024x1024 or 512x512 image; whichever is closest
var proposedRect = CGRect(origin: .zero, size: NSSize(width: sourceWidth, height: sourceWidth))
guard let cgImage = icon.cgImage(forProposedRect: &proposedRect, context: nil, hints: [.interpolation: NSImageInterpolation.high]) else { return nil }
// we have to crop this image; let's scale our intended padding, given the image size we got
let paddingScaled = padding * (CGFloat(cgImage.width) / sourceWidth)
guard let image = cgImage.cropping(to: CGRect(x: paddingScaled, y: paddingScaled, width: CGFloat(cgImage.width) - paddingScaled * 2, height: CGFloat(cgImage.height) - paddingScaled * 2).integral),
let context = CGContext(data: nil, width: Int(finalWidth), height: Int(finalWidth), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue).union(.byteOrder32Little).rawValue) else { return nil }
context.interpolationQuality = .high
context.draw(image, in: CGRect(origin: .zero, size: NSSize(width: finalWidth, height: finalWidth)))
return context.makeImage()
}
init(_ runningApplication: NSRunningApplication) {
@@ -64,8 +78,9 @@ class Application: NSObject {
bundleIdentifier = runningApplication.bundleIdentifier
bundleURL = runningApplication.bundleURL
executableURL = runningApplication.executableURL
debugId = "(pid:\(pid) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName))"
super.init()
Logger.debug(debugId)
Logger.info { self.debugId }
observeEventsIfEligible()
kvObservers = [
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in
@@ -75,7 +90,7 @@ class Application: NSObject {
runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in
guard let self else { return }
if self.runningApplication.activationPolicy != .regular {
self.removeWindowslessAppWindow()
self.removeWindowlessAppWindow()
}
self.observeEventsIfEligible()
},
@@ -83,15 +98,9 @@ class Application: NSObject {
}
deinit {
Logger.debug("Deinit app", debugId)
Logger.info { self.debugId }
}
func removeWindowslessAppWindow() {
if let windowlessAppWindow = (Windows.list.firstIndex { $0.isWindowlessApp == true && $0.application.pid == pid }) {
Windows.list.remove(at: windowlessAppWindow)
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
}
}
func observeEventsIfEligible() {
if runningApplication.activationPolicy != .prohibited && !isReallyFinishedLaunching {
@@ -105,36 +114,36 @@ class Application: NSObject {
}
}
func manuallyUpdateWindows() {
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .updateAppWindows) { [weak self] in
guard let self, let axUiElement = self.axUiElement else { return }
var atLeastOneActualWindow = false
let axWindows = try axUiElement.allWindows(self.pid)
for axWindow in axWindows {
let wid = try axWindow.cgWindowId()
if let (title, role, subrole, isMinimized, isFullscreen) = try axWindow.windowAttributes() {
let size = try axWindow.size()
let level = wid.level()
if AXUIElement.isActualWindow(self, wid, level, title, subrole, role, size) {
let position = try axWindow.position()
atLeastOneActualWindow = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if let window = (Windows.list.first { $0.isEqualRobust(axWindow, wid) }) {
window.title = window.bestEffortTitle(title)
window.size = size
window.isFullscreen = isFullscreen
window.isMinimized = isMinimized
window.position = position
} else {
let window = self.addWindow(axWindow, wid, title, isFullscreen, isMinimized, position, size)
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
private func observeEvents() {
guard let axObserver else { return }
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in
guard let self, !self.isReallyFinishedLaunching else { return }
if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) {
Logger.debug { "Subscribed to app: \(self.debugId)" }
if !self.isReallyFinishedLaunching {
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
// we consider them ready when the first subscription succeeds
// windows opened before that point won't send a notification, so check those windows manually here
self.isReallyFinishedLaunching = true
for notification in Application.notifications.dropFirst() {
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
}
}
DispatchQueue.main.async { [weak self] in
self?.manuallyUpdateWindows()
}
}
}
if (!atLeastOneActualWindow) {
}
CFRunLoopAddSource(BackgroundWork.accessibilityEventsThread.runLoop, AXObserverGetRunLoopSource(axObserver), .commonModes)
}
func manuallyUpdateWindows() {
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .updateAppWindows) { [weak self] in
guard let self, let axUiElement = self.axUiElement else { return }
let axWindows = try axUiElement.allWindows(self.pid)
guard !axWindows.isEmpty else {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if self.addWindowlessWindowIfNeeded() != nil {
@@ -149,18 +158,31 @@ class Application: NSObject {
if self.runningApplication.isActive && self.runningApplication.activationPolicy == .regular {
throw AxError.runtimeError
}
return
}
for axWindow in axWindows {
let wid = try axWindow.cgWindowId()
guard wid != 0 else { continue } // some bogus "windows" have wid 0
try AccessibilityEvents.handleEventWindow(kAXWindowCreatedNotification, wid, pid, axWindow)
}
}
}
@discardableResult
func addWindowlessWindowIfNeeded() -> Window? {
if runningApplication.activationPolicy == .regular && !runningApplication.isTerminated
&& (Windows.list.firstIndex { $0.application.pid == pid }) == nil {
let window = Window(self)
Windows.appendAndUpdateFocus(window)
return window
}
return nil
guard runningApplication.activationPolicy == .regular && !runningApplication.isTerminated
&& !(Windows.list.contains { $0.application.pid == pid }) else { return nil }
let window = Window(self)
Windows.appendWindow(window)
focusedWindow = nil
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
return window
}
func removeWindowlessAppWindow() {
guard let windowlessAppWindow = (Windows.list.first { $0.isWindowlessApp == true && $0.application.pid == pid }) else { return }
Windows.removeWindows([windowlessAppWindow], false)
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
}
func hideOrShow() {
@@ -188,38 +210,4 @@ class Application: NSObject {
alreadyRequestedToQuit = true
}
}
private func addWindow(_ axUiElement: AXUIElement, _ wid: CGWindowID, _ axTitle: String?, _ isFullscreen: Bool, _ isMinimized: Bool, _ position: CGPoint?, _ size: CGSize?) -> Window {
let window = Window(axUiElement, self, wid, axTitle, isFullscreen, isMinimized, position, size)
Windows.appendAndUpdateFocus(window)
if App.app.appIsBeingUsed {
Windows.cycleFocusedWindowIndex(1)
}
return window
}
private func observeEvents() {
guard let axObserver else { return }
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in
guard let self, !self.isReallyFinishedLaunching else { return }
if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) {
Logger.debug("Subscribed to app", self.debugId)
if !self.isReallyFinishedLaunching {
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
// we consider them ready when the first subscription succeeds
// windows opened before that point won't send a notification, so check those windows manually here
self.isReallyFinishedLaunching = true
for notification in Application.notifications.dropFirst() {
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
}
}
DispatchQueue.main.async { [weak self] in
self?.manuallyUpdateWindows()
}
}
}
}
CFRunLoopAddSource(BackgroundWork.accessibilityEventsThread.runLoop, AXObserverGetRunLoopSource(axObserver), .commonModes)
}
}
+30
View File
@@ -0,0 +1,30 @@
class ApplicationDiscriminator {
static func isActualApplication(_ processIdentifier: pid_t, _ bundleIdentifier: String?) -> Bool {
// an app can start with .activationPolicy == .prohibited, then transition to != .prohibited later
// an app can be both activationPolicy == .accessory and XPC (e.g. com.apple.dock.etci)
return (isNotXpc(processIdentifier) || isPasswords(bundleIdentifier) || isAndroidEmulator(bundleIdentifier, processIdentifier)) && !processIdentifier.isZombie()
}
private static func isNotXpc(_ processIdentifier: pid_t) -> Bool {
// these private APIs are more reliable than Bundle.init? as it can return nil (e.g. for com.apple.dock.etci)
var psn = ProcessSerialNumber()
GetProcessForPID(processIdentifier, &psn)
var info = ProcessInfoRec()
GetProcessInformation(&psn, &info)
return String(info.processType) != "XPC!"
}
private static func isPasswords(_ bundleIdentifier: String?) -> Bool {
return bundleIdentifier == "com.apple.Passwords"
}
static func isAndroidEmulator(_ bundleIdentifier: String?, _ processIdentifier: pid_t) -> Bool {
// NSRunningApplication provides no way to identify the emulator; we pattern match on its KERN_PROCARGS
if bundleIdentifier == nil,
let executablePath = Sysctl.run([CTL_KERN, KERN_PROCARGS, processIdentifier]) {
// example path: ~/Library/Android/sdk/emulator/qemu/darwin-x86_64/qemu-system-x86_64
return executablePath.range(of: "qemu-system[^/]*$", options: .regularExpression, range: nil, locale: nil) != nil
}
return false
}
}
+45 -67
View File
@@ -36,21 +36,19 @@ class Applications {
static func removeZombieWindows() {
let wIds = Windows.list.compactMap { $0.cgWindowId }
guard !wIds.isEmpty else { return }
let values = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: wIds.count)
for (i, id) in wIds.enumerated() {
values[i] = UnsafeRawPointer(bitPattern: UInt(id))
let rawIds: CFArray = wIds.map { UnsafeRawPointer(bitPattern: UInt($0)) }.withUnsafeBufferPointer {
CFArrayCreate(nil, UnsafeMutablePointer(mutating: $0.baseAddress), $0.count, nil)
}
let rawIds = CFArrayCreate(kCFAllocatorDefault, values, wIds.count, nil)
let descriptions = (CGWindowListCreateDescriptionFromArray(rawIds) as? [[CFString: Any]])
let descriptions = CGWindowListCreateDescriptionFromArray(rawIds) as? [[CFString: Any]]
let existingWids = descriptions?.compactMap { $0[kCGWindowNumber] } as? [CGWindowID]
guard let existingWids else { return }
let believedAlive = Set(wIds)
let confirmedAlive = Set(existingWids)
let zombies = believedAlive.subtracting(confirmedAlive)
for (index, window) in Windows.list.enumerated().reversed() {
for window in Windows.list.reversed() {
if let wid = window.cgWindowId, zombies.contains(wid) {
Logger.error("----", window.title, window.application.bundleIdentifier)
Windows.removeWindow(index, window.application.pid)
Logger.debug { window.debugId }
Windows.removeWindows([window], true)
}
}
}
@@ -63,8 +61,8 @@ class Applications {
DockEvents.observe(processIdentifier)
}
// com.apple.universalcontrol always fails subscribeToNotification. We blacklist it to save resources on everyone's machines
if bundleIdentifier != "com.apple.universalcontrol" && isActualApplication(processIdentifier, bundleIdentifier) {
Applications.list.append(Application($0))
if bundleIdentifier != "com.apple.universalcontrol" {
findOrCreate(processIdentifier)
}
}
}
@@ -73,49 +71,38 @@ class Applications {
let existingAppsToRemove = list.filter { app in terminatingApps.contains { tApp in app.runningApplication.isEqual(tApp) } }
let existingWindowstoRemove = Windows.list.filter { window in terminatingApps.contains { tApp in window.application.runningApplication.isEqual(tApp) } }
if existingAppsToRemove.isEmpty && existingWindowstoRemove.isEmpty { return }
var windowsOnTheLeftOfFocusedWindow = 0
for tApp in terminatingApps {
for (index, window) in Windows.list.enumerated() {
if window.application.runningApplication.isEqual(tApp)
&& index < Windows.focusedWindowIndex && window.shouldShowTheUser {
windowsOnTheLeftOfFocusedWindow += 1
}
}
Windows.removeWindows(Windows.list.filter { $0.application.runningApplication.isEqual(tApp) }, false)
// comparing pid here can fail here, as it can be already nil; we use isEqual here to avoid the issue
Applications.list.removeAll { $0.runningApplication.isEqual(tApp) }
Windows.list.removeAll { $0.application.runningApplication.isEqual(tApp) }
}
if Windows.list.count == 0 {
App.app.hideUi()
} else {
if windowsOnTheLeftOfFocusedWindow > 0 {
Windows.cycleFocusedWindowIndex(-windowsOnTheLeftOfFocusedWindow)
}
if !existingWindowstoRemove.isEmpty {
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
}
list.removeAll { $0.runningApplication.isEqual(tApp) }
}
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
}
static func refreshBadgesAsync() {
if !App.app.appIsBeingUsed || Preferences.hideAppBadges { return }
guard App.app.appIsBeingUsed && !Preferences.hideAppBadges else { return }
AXUIElement.retryAxCallUntilTimeout(callType: .updateDockBadges) {
if let dockPid = (list.first { $0.bundleIdentifier == "com.apple.dock" }?.pid),
let axList = (try AXUIElementCreateApplication(dockPid).children()?.first { try $0.role() == kAXListRole }),
let axAppDockItem = (try axList.children()?.filter { try $0.subrole() == kAXApplicationDockItemSubrole && ($0.appIsRunning() ?? false) }) {
let axAppDockItemUrlAndLabel = try axAppDockItem.map { try ($0.attribute(kAXURLAttribute, URL.self), $0.attribute(kAXStatusLabelAttribute, String.self)) }
DispatchQueue.main.async {
refreshBadges_(axAppDockItemUrlAndLabel)
}
guard let dockPid = (list.first { $0.bundleIdentifier == "com.apple.dock" }?.pid),
let axDockChildren = try AXUIElementCreateApplication(dockPid).attributes([kAXChildrenAttribute]).children,
let axList = try (axDockChildren.first { try $0.attributes([kAXRoleAttribute]).role == kAXListRole }),
let axListChildren = try axList.attributes([kAXChildrenAttribute]).children else { return }
let axAppDockItemUrlAndLabel: [(URL?, String?)] = try axListChildren.compactMap {
let a = try $0.attributes([kAXSubroleAttribute, kAXIsApplicationRunningAttribute, kAXURLAttribute, kAXStatusLabelAttribute])
guard a.subrole == kAXApplicationDockItemSubrole && (a.appIsRunning ?? false) else { return nil }
return (a.url, a.statusLabel)
}
guard !axAppDockItemUrlAndLabel.isEmpty else { return }
DispatchQueue.main.async {
guard App.app.appIsBeingUsed && !Preferences.hideAppBadges else { return }
refreshBadges_(axAppDockItemUrlAndLabel)
}
}
}
static func refreshBadges_(_ items: [(URL?, String?)]) {
Windows.list.enumerated().forEach { (i, window) in
if !App.app.appIsBeingUsed { return }
let view = ThumbnailsView.recycledViews[i]
if let app = Applications.find(window.application.pid) {
if let app = findOrCreate(window.application.pid) {
if app.runningApplication.activationPolicy == .regular,
let matchingItem = (items.first { $0.0 == app.bundleURL }),
let label = matchingItem.1 {
@@ -129,36 +116,27 @@ class Applications {
}
}
private static func isActualApplication(_ processIdentifier: pid_t, _ bundleIdentifier: String?) -> Bool {
// an app can start with .activationPolicy == .prohibited, then transition to != .prohibited later
// an app can be both activationPolicy == .accessory and XPC (e.g. com.apple.dock.etci)
return (isNotXpc(processIdentifier) || isPasswords(bundleIdentifier) || isAndroidEmulator(bundleIdentifier, processIdentifier)) && !processIdentifier.isZombie()
}
private static func isNotXpc(_ processIdentifier: pid_t) -> Bool {
// these private APIs are more reliable than Bundle.init? as it can return nil (e.g. for com.apple.dock.etci)
var psn = ProcessSerialNumber()
GetProcessForPID(processIdentifier, &psn)
var info = ProcessInfoRec()
GetProcessInformation(&psn, &info)
return String(info.processType) != "XPC!"
}
private static func isPasswords(_ bundleIdentifier: String?) -> Bool {
return bundleIdentifier == "com.apple.Passwords"
}
static func isAndroidEmulator(_ bundleIdentifier: String?, _ processIdentifier: pid_t) -> Bool {
// NSRunningApplication provides no way to identify the emulator; we pattern match on its KERN_PROCARGS
if bundleIdentifier == nil,
let executablePath = Sysctl.run([CTL_KERN, KERN_PROCARGS, processIdentifier]) {
// example path: ~/Library/Android/sdk/emulator/qemu/darwin-x86_64/qemu-system-x86_64
return executablePath.range(of: "qemu-system[^/]*$", options: .regularExpression, range: nil, locale: nil) != nil
@discardableResult
static func findOrCreate(_ pid: pid_t) -> Application? {
if let app = (list.first { $0.pid == pid }) {
return app
}
return false
guard let runningApp = NSRunningApplication(processIdentifier: pid) else { return nil }
guard ApplicationDiscriminator.isActualApplication(pid, runningApp.bundleIdentifier) else { return nil }
let app = Application(runningApp)
list.append(app)
return app
}
static func find(_ pid: pid_t?) -> Application? {
return list.first { $0.pid == pid }
static func updateAppIcons() {
for app in list {
BackgroundWork.screenshotsQueue.addOperation { [weak app] in
guard let app else { return }
let r = Application.appIconWithoutPadding(app.runningApplication.icon)
DispatchQueue.main.async { [weak app] in
app?.icon = r
}
}
}
}
}
+51 -33
View File
@@ -2,64 +2,69 @@ import Foundation
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
class BackgroundWork {
// we use an OperationQueue tied to the global DispatchQueue to call blocking APIs in parallel
// we use Threads when the APIs we observe require a RunLoop (e.g. CGEvent.tapCreate, AXObserverGetRunLoopSource, CFMessagePortCreateRunLoopSource)
// when possible, we prefer OperationQueues
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
static var keyboardAndMouseAndTrackpadEventsThread: BackgroundThreadWithRunLoop!
static var missionControlThread: BackgroundThreadWithRunLoop!
static var cliEventsThread: BackgroundThreadWithRunLoop!
// we use an OperationQueue for most tasks, especially when we need to call blocking APIs in parallel
static var repeatingKeyQueue: LabeledOperationQueue!
static var screenshotsQueue: LabeledOperationQueue!
static var accessibilityCommandsQueue: LabeledOperationQueue!
static var axCallsFirstAttemptQueue: LabeledOperationQueue!
static var axCallsRetriesQueue: LabeledOperationQueue!
static var axCallsManualDiscoveryQueue: LabeledOperationQueue!
static var crashReportsQueue: LabeledOperationQueue!
// we use Threads to observe events in sequence
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
static var keyboardAndTrackpadEventsThread: BackgroundThreadWithRunLoop!
static var systemPermissionsThread: BackgroundThreadWithRunLoop!
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
static var missionControlThread: BackgroundThreadWithRunLoop!
static var cliEventsThread: BackgroundThreadWithRunLoop!
static var debugMenu: DebugMenu!
static var permissionsCheckOnTimerQueue: LabeledOperationQueue!
static var permissionsSystemCallsQueue: LabeledOperationQueue!
private static var debugMenu: DebugMenu!
private static var totalPotentialThreadCount = 0
static func preStart() {
// we make calls to the system permissions API to know if permissions are granted. We do this on a timer
permissionsCheckOnTimerQueue = LabeledOperationQueue("permissionsCheckOnTimer", .userInteractive, 1)
// if macOS is overwhelmed, let's reduce the pressure on it by calling permission APIs one at a time
permissionsSystemCallsQueue = LabeledOperationQueue("permissionsSystemCalls", .userInteractive, 1)
}
static func start() {
screenshotsQueue = LabeledOperationQueue("screenshotsQueue", .userInteractive, 8)
screenshotsQueue = LabeledOperationQueue("screenshots", .userInteractive, 8)
// calls to focus/close/minimize/etc windows
// They are tried once and if they timeout we don't retry. The OS seems to still execute them even if the call timed out
accessibilityCommandsQueue = LabeledOperationQueue("accessibilityCommandsQueue", .userInteractive, 4)
accessibilityCommandsQueue = LabeledOperationQueue("axCommands", .userInteractive, 4)
// calls to the AX APIs can block for a long time (e.g. if an app is unresponsive)
// We first try the AX calls on axCallsFirstAttemptQueue. If we get a timeout, we move to axCallsRetriesQueue and retry there for a while
axCallsFirstAttemptQueue = LabeledOperationQueue("axCallsFirstAttemptQueue", .userInteractive, 8)
axCallsRetriesQueue = LabeledOperationQueue("axCallsRetriesQueue", .userInteractive, 8)
axCallsFirstAttemptQueue = LabeledOperationQueue("axCallsFirst", .userInteractive, 8)
axCallsRetriesQueue = LabeledOperationQueue("axCallsRetry", .userInteractive, 8)
// we separate calls to manuallyUpdateWindows since those can be very heavy and numerous
axCallsManualDiscoveryQueue = LabeledOperationQueue("axCallsManualDiscoveryQueue", .userInteractive, 8)
axCallsManualDiscoveryQueue = LabeledOperationQueue("axCallsManual", .userInteractive, 8)
// we time key repeat on a background queue. We handle their consequence on the main-thread
repeatingKeyQueue = LabeledOperationQueue("repeatingKey", .userInteractive, 1)
// we observe app and windows notifications. They arrive on this thread, and are handled off the main thread initially
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
accessibilityEventsThread = BackgroundThreadWithRunLoop("axEvents", .userInteractive)
// we listen to as any keyboard events as possible on a background thread, as it's more available/reliable than the main thread
keyboardAndTrackpadEventsThread = BackgroundThreadWithRunLoop("keyboardAndTrackpadEventsThread", .userInteractive)
// we time key repeat on a background thread for precision. We handle their consequence on the main-thread
repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive)
keyboardAndMouseAndTrackpadEventsThread = BackgroundThreadWithRunLoop("inputDevices", .userInteractive)
// we main Mission Control state on a background thread. We protect reads from main-thread with an NSLock
missionControlThread = BackgroundThreadWithRunLoop("missionControlThread", .userInteractive)
missionControlThread = BackgroundThreadWithRunLoop("missionControl", .userInteractive)
// we listen to CLI commands (CFMessagePort events)
cliEventsThread = BackgroundThreadWithRunLoop("cliEventsThread", .userInteractive)
// logThreadsAndQueuesOnRepeat()
cliEventsThread = BackgroundThreadWithRunLoop("cliMessages", .userInteractive)
// logThreadsAndQueuesOnRepeat()
}
static func startCrashReportsQueue() {
if crashReportsQueue == nil {
// crash reports can be sent off the main thread
crashReportsQueue = LabeledOperationQueue("crashReportsQueue", .utility, 1)
crashReportsQueue = LabeledOperationQueue("crashReports", .utility, 1)
}
}
static func startSystemPermissionThread() {
// not 100% sure this shouldn't be on the main-thread; it doesn't do anything except dispatch to main.async
systemPermissionsThread = BackgroundThreadWithRunLoop("systemPermissionsThread", .utility)
}
static func addPotentialThreadCount(_ additionalCount: Int) {
totalPotentialThreadCount += additionalCount
// a macos process has a soft limit of 64 threads. We need to be careful to don't spawn too many threads through DispatchQueues
assert(totalPotentialThreadCount <= 43)
assert(totalPotentialThreadCount <= 45)
}
// useful during development to inspect how many threads are used by AltTab
@@ -82,7 +87,7 @@ class BackgroundWork {
map[queue.underlyingQueue!.label] = queue.operations.reduce(0) { $1.isExecuting ? $0 + 1 : $0 }
}
let prettyPrintMap = map.sorted { $0.key < $1.key }.map { "\($0.key): \($0.value)" }.joined(separator: ", ")
Logger.debug(prettyPrintMap)
Logger.debug { prettyPrintMap }
}
private static func logThreads() -> Void {
@@ -109,8 +114,8 @@ class BackgroundWork {
vm_deallocate(mach_task_self_,
vm_address_t(bitPattern: threads),
vm_size_t(count) * vm_size_t(MemoryLayout<thread_t>.size))
Logger.info(namedThreads.count, "Named threads:", namedThreads.sorted())
Logger.info(unnamedThreadsCount, "Unnamed threads (e.g. from GCD queues)")
Logger.info { "\(namedThreads.count) named threads:\(namedThreads.sorted())" }
Logger.info { "\(unnamedThreadsCount) unnamed threads (e.g. from GCD queues)" }
}
class BackgroundThreadWithRunLoop: Thread {
@@ -128,8 +133,8 @@ class BackgroundWork {
}
override func main() {
Logger.debug(name)
// the RunLoop is lazy; calling this initialize it
Logger.debug { "Thread ready" }
// the RunLoop is lazy; calling this initializes it
runLoop = CFRunLoopGetCurrent()
addDummySourceToPreventRunLoopTermination()
threadStartSemaphore.signal()
@@ -149,6 +154,10 @@ class BackgroundWork {
class LabeledOperationQueue: OperationQueue, @unchecked Sendable {
let strongUnderlyingQueue: DispatchQueue
private var _activeCallbacks: Int32 = 0
var activeCallbacks: Int {
Int(OSAtomicAdd32(0, &_activeCallbacks))
}
init(_ label: String, _ qos: DispatchQoS, _ maxConcurrentOperationCount: Int) {
strongUnderlyingQueue = DispatchQueue(label: label, attributes: [.concurrent])
@@ -168,3 +177,12 @@ class LabeledOperationQueue: OperationQueue, @unchecked Sendable {
}
}
}
extension LabeledOperationQueue {
@inline(__always)
func trackCallbacks<T>(_ body: () throws -> T) rethrows -> T {
OSAtomicIncrement32(&_activeCallbacks)
defer { OSAtomicDecrement32(&_activeCallbacks) }
return try body()
}
}
+22 -20
View File
@@ -2,52 +2,54 @@ import Cocoa
import ShortcutRecorder
class KeyRepeatTimer {
static var timer: Timer?
static var timer = DispatchSource.makeTimerSource(queue: BackgroundWork.repeatingKeyQueue.strongUnderlyingQueue)
static var timerIsSuspended = true
static var currentTimerShortcutName: String?
static func toggleRepeatingKeyPreviousWindow() {
static func startRepeatingKeyPreviousWindow() {
if let shortcut = ControlsTab.shortcuts["previousWindowShortcut"],
// events already repeat when using a shortcut with a keycode; no need for artificial repeat
shortcut.shortcut.keyCode == .none {
activateTimerForRepeatingKey(shortcut) {
startTimerForRepeatingKey(shortcut) {
App.app.previousWindowShortcutWithRepeatingKey()
}
}
}
static func toggleRepeatingKeyNextWindow() {
static func startRepeatingKeyNextWindow() {
if let shortcut = ControlsTab.shortcuts[Preferences.indexToName("nextWindowShortcut", App.app.shortcutIndex)] {
activateTimerForRepeatingKey(shortcut) {
startTimerForRepeatingKey(shortcut) {
ControlsTab.executeAction(Preferences.indexToName("nextWindowShortcut", App.app.shortcutIndex))
}
}
}
static func deactivateTimerForRepeatingKey(_ shortcutName: String) {
static func stopTimerForRepeatingKey(_ shortcutName: String) {
if shortcutName == currentTimerShortcutName {
Logger.debug(shortcutName)
Logger.debug { shortcutName }
currentTimerShortcutName = nil
timer?.invalidate()
timer.suspend()
timerIsSuspended = true
}
}
private static func activateTimerForRepeatingKey(_ atShortcut: ATShortcut, _ block: @escaping () -> Void) {
if ((timer == nil || !timer!.isValid) && atShortcut.state != .up) {
let repeatRate = ticksToSeconds(CachedUserDefaults.globalString("KeyRepeat") ?? "6")
let initialDelay = ticksToSeconds(CachedUserDefaults.globalString("InitialKeyRepeat") ?? "25")
timer = Timer(fire: Date(timeIntervalSinceNow: initialDelay), interval: repeatRate, repeats: true) { _ in
handleEvent(atShortcut, block)
}
timer!.tolerance = repeatRate * 0.1
CFRunLoopAddTimer(BackgroundWork.repeatingKeyThread.runLoop, timer!, .commonModes)
currentTimerShortcutName = atShortcut.id
}
private static func startTimerForRepeatingKey(_ atShortcut: ATShortcut, _ block: @escaping () -> Void) {
guard timerIsSuspended && atShortcut.state != .up else { return }
currentTimerShortcutName = atShortcut.id
// reading these user defaults every time guarantees we have the latest value, if the user has updated those
let repeatRate = ticksToSeconds(CachedUserDefaults.globalString("KeyRepeat") ?? "6")
let initialDelay = ticksToSeconds(CachedUserDefaults.globalString("InitialKeyRepeat") ?? "25")
Logger.debug { "\(currentTimerShortcutName) repeatRate:\(repeatRate)s initialDelay:\(initialDelay)s" }
timer.schedule(deadline: .now() + initialDelay, repeating: repeatRate, leeway: .milliseconds(Int(repeatRate * 1000 / 10)))
timer.setEventHandler { handleEvent(atShortcut, block) }
timer.resume()
timerIsSuspended = false
}
private static func handleEvent(_ atShortcut: ATShortcut, _ block: @escaping () -> Void) {
DispatchQueue.main.async {
if atShortcut.state == .up {
deactivateTimerForRepeatingKey(atShortcut.id)
stopTimerForRepeatingKey(atShortcut.id)
} else {
block()
}
+3
View File
@@ -76,6 +76,7 @@ enum LanguagePreference: CaseIterable, MacroPreference {
case icelandic
case czech
case greek
case belarusian
case bulgarian
case russian
case serbian
@@ -99,6 +100,7 @@ enum LanguagePreference: CaseIterable, MacroPreference {
switch self {
case .systemDefault: return NSLocalizedString("System Default", comment: "")
case .arabic: return "العربية"
case .belarusian: return "Беларуская мова"
case .bulgarian: return "Български"
case .bengali: return "বাংলা"
case .catalan: return "Català"
@@ -158,6 +160,7 @@ enum LanguagePreference: CaseIterable, MacroPreference {
switch self {
case .systemDefault: return nil
case .arabic: return "ar"
case .belarusian: return "be"
case .bulgarian: return "bg"
case .bengali: return "bn"
case .catalan: return "ca"
+1 -1
View File
@@ -123,7 +123,7 @@ class Preferences {
// periphery:ignore
static var startAtLogin: Bool { CachedUserDefaults.bool("startAtLogin") }
static var blacklist: [BlacklistEntry] { CachedUserDefaults.json("blacklist", [BlacklistEntry].self) }
static var previewFocusedWindow: Bool { CachedUserDefaults.bool("previewFocusedWindow") }
static var previewSelectedWindow: Bool { CachedUserDefaults.bool("previewFocusedWindow") }
static var screenRecordingPermissionSkipped: Bool { CachedUserDefaults.bool("screenRecordingPermissionSkipped") }
// macro values
+1
View File
@@ -19,6 +19,7 @@ class PreferencesMigrations {
}
private static func updateToNewPreferences(_ versionInPlist: String) {
Logger.debug { "App-version:\(App.version), Plist-version:\(versionInPlist)" }
// x.compare(y) is .orderedDescending if x > y
if versionInPlist.compare("7.27.0", options: .numeric) != .orderedDescending {
migrateCursorFollowFocus()
@@ -1,7 +1,19 @@
import Cocoa
class Screens {
static var all = [ScreenUuid: NSScreen]()
static func refresh() {
all.removeAll()
for screen in NSScreen.screens {
guard let uuid = screen.uuid() else { continue }
all[uuid] = screen
}
}
}
extension NSScreen {
static var preferred: NSScreen = detectPreferred() ?? NSScreen.screens.first!
static var preferred = NSScreen.screens.first!
static func updatePreferred() {
preferred = detectPreferred() ?? NSScreen.screens.first!
@@ -22,15 +34,15 @@ extension NSScreen {
/// * if NSScreen.screensHaveSeparateSpaces == false, and key window is on another screen than screens[0], it still returns screens[0]
/// we find the screen with the key window ourselves manually
static func active() -> NSScreen? {
if let app = Applications.find(NSWorkspace.shared.frontmostApplication?.processIdentifier) {
if let focusedWindow = app.focusedWindow {
// on the very first summon, this window may not have its spaces updated, which may land the wrong active screen
Windows.updatesWindowSpace(focusedWindow)
return NSScreen.screens.first { focusedWindow.isOnScreen($0) }
}
return NSScreen.withActiveMenubar()
}
return nil
guard let frontmostPid = NSWorkspace.shared.frontmostApplication?.processIdentifier,
// we avoid Applications.findOrCreate() here, because it active() is called very early during launch
// we are not ready to create applications yet
let frontmostApp = (Applications.list.first { $0.pid == frontmostPid }) else { return nil }
guard let focusedWindow = frontmostApp.focusedWindow else { return NSScreen.withActiveMenubar() }
// on the very first summon, this window may not have its spaces updated, which may land the wrong active screen
focusedWindow.updateSpacesAndScreen()
guard let screenId = focusedWindow.screenId else { return nil }
return Screens.all[screenId]
}
/// there is only 1 active menubar. Other screens will show their menubar dimmed
@@ -55,13 +67,11 @@ extension NSScreen {
}
func uuid() -> ScreenUuid? {
if let screenNumber = number(),
// these APIs implicitly unwrap their return values, but it can actually be nil thus we check
let screenUuid = CGDisplayCreateUUIDFromDisplayID(screenNumber),
let uuid = CFUUIDCreateString(nil, screenUuid.takeRetainedValue()) {
return uuid
}
return nil
guard let screenNumber = number(),
// these APIs implicitly unwrap their return values, but it can actually be nil thus we check
let screenUuid = CGDisplayCreateUUIDFromDisplayID(screenNumber),
let uuid = CFUUIDCreateString(nil, screenUuid.takeRetainedValue()) else { return nil }
return uuid
}
// periphery:ignore
@@ -92,3 +102,4 @@ extension NSScreen {
}
typealias ScreenUuid = CFString
+60 -79
View File
@@ -4,91 +4,65 @@ import ScreenCaptureKit.SCShareableContent
// macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy
class SystemPermissions {
static var preStartupPermissionsPassed = false
static var timerPermissionsToUpdatePermissionsWindow: Timer?
static var timerPermissionsRemovedWhileAltTabIsRunning: Timer?
private static var timer: DispatchSourceTimer!
static func ensurePermissionsAreGranted(_ continueAppStartup: @escaping () -> Void) {
let startupBlock = {
pollPermissionsRemovedWhileAltTabIsRunning()
continueAppStartup()
}
if AccessibilityPermission.update() != .notGranted && ScreenRecordingPermission.update() != .notGranted {
preStartupPermissionsPassed = true
startupBlock()
} else {
App.app.permissionsWindow.show(startupBlock)
}
static func ensurePermissionsAreGranted() {
timer = DispatchSource.makeTimerSource(queue: BackgroundWork.permissionsCheckOnTimerQueue.strongUnderlyingQueue)
timer.setEventHandler(handler: checkPermissionsOnTimer)
setImmediateTimer()
timer.resume()
}
static func pollPermissionsToUpdatePermissionsWindow(_ startupBlock: @escaping () -> Void) {
timerPermissionsToUpdatePermissionsWindow = Timer(timeInterval: 0.1, repeats: true) { _ in
checkPermissionsToUpdatePermissionsWindow(startupBlock)
}
timerPermissionsToUpdatePermissionsWindow!.tolerance = 0.1
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsToUpdatePermissionsWindow!, .commonModes)
}
private static func pollPermissionsRemovedWhileAltTabIsRunning() {
timerPermissionsRemovedWhileAltTabIsRunning = Timer(timeInterval: 5, repeats: true) { _ in
DispatchQueue.main.async {
checkPermissionsWhileAltTabIsRunning()
}
}
timerPermissionsRemovedWhileAltTabIsRunning!.tolerance = 1
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsRemovedWhileAltTabIsRunning!, .commonModes)
}
private static func checkPermissionsWhileAltTabIsRunning() {
Logger.debug(AccessibilityPermission.status, ScreenRecordingPermission.status, preStartupPermissionsPassed, ScreenRecordingPermission.status, Appearance.hideThumbnails, Preferences.previewFocusedWindow)
private static func checkPermissionsOnTimer() {
AccessibilityPermission.update()
Logger.debug(AccessibilityPermission.status)
if AccessibilityPermission.status == .notGranted {
Logger.info("accessibilityPermission not granted; restarting")
App.app.restart()
if !preStartupPermissionsPassed || App.app.permissionsWindow.isVisible {
ScreenRecordingPermission.update()
}
if ScreenRecordingPermission.status == .skipped || (Appearance.hideThumbnails && !Preferences.previewFocusedWindow) { return }
ScreenRecordingPermission.update()
Logger.debug(ScreenRecordingPermission.status)
Menubar.togglePermissionCallout(ScreenRecordingPermission.status == .skipped)
if ScreenRecordingPermission.status == .notGranted {
// permission check may yield a false negative during wake-up
// we restart after 2 negative checks
if ScreenRecordingPermission.flakyCounter >= 2 {
Logger.info("screenRecordingPermission not granted 3 times; restarting")
App.app.restart()
} else {
ScreenRecordingPermission.flakyCounter += 1
}
Logger.debug { "accessibility:\(AccessibilityPermission.status) screenRecording:\(ScreenRecordingPermission.status)" }
if !preStartupPermissionsPassed {
checkPermissionsPreStartup()
} else {
ScreenRecordingPermission.flakyCounter = 0
checkPermissionsPostStartup()
}
}
private static func checkPermissionsToUpdatePermissionsWindow(_ startupBlock: @escaping () -> Void) {
AccessibilityPermission.update()
ScreenRecordingPermission.update()
Logger.debug(AccessibilityPermission.status, ScreenRecordingPermission.status, preStartupPermissionsPassed)
DispatchQueue.main.async {
Menubar.togglePermissionCallout(ScreenRecordingPermission.status == .skipped)
if AccessibilityPermission.status != App.app.permissionsWindow?.accessibilityView?.permissionStatus {
App.app.permissionsWindow?.accessibilityView.updatePermissionStatus(AccessibilityPermission.status)
Menubar.togglePermissionCallout(ScreenRecordingPermission.status != .granted)
App.app.permissionsWindow.updatePermissionViews()
}
}
private static func checkPermissionsPreStartup() {
if AccessibilityPermission.status != .notGranted && ScreenRecordingPermission.status != .notGranted {
DispatchQueue.main.async {
preStartupPermissionsPassed = true
App.app.permissionsWindow?.close()
setInfrequentTimer()
App.app.continueAppLaunchAfterPermissionsAreGranted()
}
if #available(macOS 10.15, *), ScreenRecordingPermission.status != App.app.permissionsWindow?.screenRecordingView?.permissionStatus {
App.app.permissionsWindow?.screenRecordingView?.updatePermissionStatus(ScreenRecordingPermission.status)
}
if !preStartupPermissionsPassed {
if AccessibilityPermission.status != .notGranted && ScreenRecordingPermission.status != .notGranted {
preStartupPermissionsPassed = true
App.app.permissionsWindow?.close()
startupBlock()
}
} else {
if AccessibilityPermission.status == .notGranted || ScreenRecordingPermission.status == .notGranted {
App.app.restart()
}
} else {
DispatchQueue.main.async {
App.app.permissionsWindow.show()
}
}
}
private static func checkPermissionsPostStartup() {
if AccessibilityPermission.status == .notGranted {
Logger.error { "Accessibility permission revoked while AltTab was running; restarting" }
DispatchQueue.main.async { App.app.restart() }
}
}
static func setInfrequentTimer() {
timer.schedule(deadline: .now() + 5, repeating: 5, leeway: .seconds(1))
}
static func setFrequentTimer() {
timer.schedule(deadline: .now(), repeating: 0.5, leeway: .milliseconds(500))
}
private static func setImmediateTimer() {
timer.schedule(deadline: .now(), repeating: .never, leeway: .never)
}
}
class AccessibilityPermission {
@@ -107,7 +81,6 @@ class AccessibilityPermission {
class ScreenRecordingPermission {
static var status = PermissionStatus.notGranted
static var flakyCounter = 0
@discardableResult
static func update() -> PermissionStatus {
@@ -150,8 +123,13 @@ class ScreenRecordingPermission {
@available(macOS 12.3, *)
private static func checkWithSCShareableContent() -> Bool {
return runWithTimeout { completion in
SCShareableContent.getWithCompletionHandler { shareableContent, error in
SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: false) { shareableContent, error in
// this callback runs on a GCD queue, not on the thread that called getWithCompletionHandler
if #available(macOS 14.0, *), let shareableContent, error == nil {
DispatchQueue.main.async {
WindowCaptureScreenshots.cachedSCWindows = shareableContent.windows
}
}
completion(error != nil ? false : (shareableContent != nil))
}
}
@@ -176,12 +154,15 @@ class ScreenRecordingPermission {
private static func runWithTimeout(_ block: @escaping (@escaping (Bool) -> Void) -> Void) -> Bool {
let semaphore = DispatchSemaphore(value: 0)
var result = false
block { r in
result = r
semaphore.signal()
BackgroundWork.permissionsSystemCallsQueue.addOperation {
block { r in
result = r
semaphore.signal()
}
}
let timeoutResult = semaphore.wait(timeout: .now() + 1)
let timeoutResult = semaphore.wait(timeout: .now() + 6)
if timeoutResult == .timedOut {
Logger.error { "Screen-recording permission call timed out after 6s" }
return false
}
return result
+71 -48
View File
@@ -11,11 +11,12 @@ class Window {
]
private static var globalCreationCounter = Int.zero
var id: String
var cgWindowId: CGWindowID?
var lastFocusOrder = Int.zero
var creationOrder = Int.zero
var title: String!
var thumbnail: CGImage?
var thumbnail: CALayerContents?
var icon: CGImage? { get { application.icon } }
var shouldShowTheUser = true
var isTabbed: Bool = false
@@ -29,43 +30,52 @@ class Window {
var size: CGSize?
var spaceIds = [CGSSpaceID.max]
var spaceIndexes = [SpaceIndex.max]
var screenId: ScreenUuid?
var axUiElement: AXUIElement?
var application: Application
var axObserver: AXObserver?
var rowIndex: Int?
var debugId: String { "(wid:\(cgWindowId.map { String(describing: $0) } ?? "nil")) \(title ?? "nil")) \(application.debugId)" }
var debugId: String!
init(_ axUiElement: AXUIElement, _ application: Application, _ wid: CGWindowID, _ axTitle: String?, _ isFullscreen: Bool, _ isMinimized: Bool, _ position: CGPoint?, _ size: CGSize?) {
init(_ axUiElement: AXUIElement, _ application: Application, _ wid: CGWindowID, _ title: String?, _ isFullscreen: Bool?, _ isMinimized: Bool?, _ position: CGPoint?, _ size: CGSize?) {
id = "\(wid)"
self.axUiElement = axUiElement
self.application = application
cgWindowId = wid
Windows.updatesWindowSpace(self)
self.isFullscreen = isFullscreen
self.isMinimized = isMinimized
self.position = position
self.size = size
title = bestEffortTitle(axTitle)
self.updateSpacesAndScreen()
updateFromAxAttributes(title, size, position, isFullscreen, isMinimized)
debugId = "\(self.application.debugId) (wid:\(cgWindowId) title:\(self.title))"
Window.globalCreationCounter += 1
creationOrder = Window.globalCreationCounter
application.removeWindowslessAppWindow()
application.removeWindowlessAppWindow()
// the app may have timed out trying to subscribe to app notifications
// It may be responsive now since it has a window; we attempt again
application.observeEventsIfEligible()
checkIfFocused(application, wid)
Logger.debug(debugId)
checkIfFocused()
Logger.info { self.debugId }
observeEvents()
}
init(_ application: Application) {
id = "\(application.pid)"
self.application = application
title = bestEffortTitle(nil)
Window.globalCreationCounter += 1
creationOrder = Window.globalCreationCounter
Logger.debug(debugId)
debugId = "\(application.debugId) (title:\(title))"
Logger.debug { self.debugId }
}
deinit {
Logger.debug(debugId)
Logger.info { self.debugId }
}
func updateFromAxAttributes(_ title: String?, _ size: CGSize?, _ position: CGPoint?, _ isFullscreen: Bool?, _ isMinimized: Bool?) {
self.title = bestEffortTitle(title)
self.size = size
self.position = position
self.isFullscreen = isFullscreen ?? false
self.isMinimized = isMinimized ?? false
}
func isEqualRobust(_ otherWindowAxUiElement: AXUIElement, _ otherWindowWid: CGWindowID?) -> Bool {
@@ -80,7 +90,7 @@ class Window {
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .subscribeToWindowNotification) { [weak self] in
guard let self else { return }
if try self.axUiElement!.subscribeToNotification(axObserver, Window.notifications.first!) {
Logger.debug("Subscribed to window", self.debugId)
Logger.debug { "Subscribed to window: \(self.debugId)" }
for notification in Window.notifications.dropFirst() {
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
@@ -91,16 +101,21 @@ class Window {
CFRunLoopAddSource(BackgroundWork.accessibilityEventsThread.runLoop, AXObserverGetRunLoopSource(axObserver), .commonModes)
}
func refreshThumbnail(_ screenshot: CGImage) {
func refreshThumbnail(_ screenshot: CALayerContents) {
thumbnail = screenshot
if !App.app.appIsBeingUsed || !shouldShowTheUser { return }
if let size,
if let position, let size,
let view = (ThumbnailsView.recycledViews.first { $0.window_?.cgWindowId == cgWindowId }) {
if !view.thumbnail.isHidden {
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot, false)
view.thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot.size(), false)
let newSize = thumbnailSize.width != view.thumbnail.frame.width || thumbnailSize.height != view.thumbnail.frame.height
view.thumbnail.updateContents(screenshot, thumbnailSize)
// if the thumbnail size has changed, we need to refresh the open UI
if newSize {
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
}
}
App.app.previewPanel.updateImageIfShowing(cgWindowId, screenshot, size)
App.app.previewPanel.updateIfShowing(cgWindowId, screenshot, position, size)
}
}
@@ -124,12 +139,12 @@ class Window {
// minimizing is ignored if sent immediatly; we wait for the de-fullscreen animation to be over
BackgroundWork.accessibilityCommandsQueue.addOperationAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self else { return }
if let closeButton_ = try? self.axUiElement!.closeButton() {
if let closeButton_ = try? self.axUiElement!.attributes([kAXCloseButtonAttribute]).closeButton {
try? closeButton_.performAction(kAXPressAction)
}
}
} else {
if let closeButton_ = try? self.axUiElement!.closeButton() {
if let closeButton_ = try? self.axUiElement!.attributes([kAXCloseButtonAttribute]).closeButton {
try? closeButton_.performAction(kAXPressAction)
}
}
@@ -183,7 +198,7 @@ class Window {
if let altTabWindow = altTabWindow() {
App.shared.activate(ignoringOtherApps: true)
altTabWindow.makeKeyAndOrderFront(nil)
Windows.previewFocusedWindowIfNeeded()
Windows.previewSelectedWindowIfNeeded()
} else if isWindowlessApp || cgWindowId == nil || Preferences.onlyShowApplications() {
if let bundleUrl = application.bundleURL, isWindowlessApp {
if (try? NSWorkspace.shared.launchApplication(at: bundleUrl, configuration: [:])) == nil {
@@ -192,7 +207,7 @@ class Window {
} else {
application.runningApplication.activate(options: .activateAllWindows)
}
Windows.previewFocusedWindowIfNeeded()
Windows.previewSelectedWindowIfNeeded()
} else {
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
// but quickly switches back to another window in that space
@@ -205,7 +220,7 @@ class Window {
self.makeKeyWindow(&psn)
try? self.axUiElement!.focusWindow()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
Windows.previewFocusedWindowIfNeeded()
Windows.previewSelectedWindowIfNeeded()
}
}
}
@@ -235,6 +250,26 @@ class Window {
return application.localizedName ?? ""
}
func updateSpacesAndScreen() {
// macOS bug: if you tab a window, then move the tab group to another space, other tabs from the tab group will stay on the current space
// you can use the Dock to focus one of the other tabs and it will teleport that tab in the current space, proving that it's a macOS bug
// note: for some reason, it behaves differently if you minimize the tab group after moving it to another space
updateSpaces()
updateScreenId()
}
private func updateSpaces() {
guard let cgWindowId else { return }
let spaceIds = cgWindowId.spaces()
self.spaceIds = spaceIds
self.spaceIndexes = spaceIds.compactMap { spaceId in Spaces.idsAndIndexes.first { $0.0 == spaceId }?.1 }
self.isOnAllSpaces = spaceIds.count > 1
}
private func updateScreenId() {
screenId = NSScreen.screens.first { isOnScreen($0) }?.uuid()
}
/// window may not be visible on that screen (e.g. the window is not on the current Space)
func isOnScreen(_ screen: NSScreen) -> Bool {
if NSScreen.screensHaveSeparateSpaces {
@@ -263,23 +298,11 @@ class Window {
// Determines if this window is the main application window
func isAppMainWindow() -> Bool {
if let element = application.axUiElement {
var mainWindow: AnyObject?
if AXUIElementCopyAttributeValue(element, kAXMainWindowAttribute as CFString, &mainWindow) == .success {
if let mainWin = mainWindow as! AXUIElement? {
do {
let w1 = try mainWin.cgWindowId()
let w2 = try axUiElement!.cgWindowId()
if w1 == w2 {
return true
}
} catch {
return false
}
}
}
}
return false
// AX calls done on main thread. They can block thus freeze the UI
// TODO: find a better approach
guard let appAxUiElement = application.axUiElement,
let mainWindow = try? appAxUiElement.attributes([kAXMainWindowAttribute]).mainWindow else { return false }
return (try? mainWindow.cgWindowId()) == cgWindowId
}
private func altTabWindow() -> NSWindow? {
@@ -291,12 +314,12 @@ class Window {
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
/// workaround: we check and possibly do it here
private func checkIfFocused(_ application: Application, _ wid: CGWindowID) {
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) {
let focusedWid = try application.axUiElement?.focusedWindow()?.cgWindowId()
if wid == focusedWid {
application.focusedWindow = self
}
private func checkIfFocused() {
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) { [weak self] in
guard let self,
let focusedWindow = try self.application.axUiElement?.attributes([kAXFocusedWindowAttribute]).focusedWindow,
let window = try (Windows.list.first { $0.isEqualRobust(focusedWindow, (try focusedWindow.cgWindowId())) }) else { return }
self.application.focusedWindow = window
}
}
}
+172
View File
@@ -0,0 +1,172 @@
class WindowDiscriminator {
static func isActualWindow(_ app: Application, _ wid: CGWindowID, _ level: CGWindowLevel, _ title: String?, _ subrole: String?, _ role: String?, _ size: CGSize?) -> Bool {
// Some non-windows have title: nil (e.g. some OS elements)
// Some non-windows have subrole: nil (e.g. some OS elements), "AXUnknown" (e.g. Bartender), "AXSystemDialog" (e.g. Intellij tooltips)
// Minimized windows or windows of a hidden app have subrole "AXDialog"
// Activity Monitor main window subrole is "AXDialog" for a brief moment at launch; it then becomes "AXStandardWindow"
// Some non-windows have cgWindowId == 0 (e.g. windows of apps starting at login with the checkbox "Hidden" checked)
// Logger.error { (app.bundleIdentifier, level, title, subrole, role, size) }
return wid != 0
// Finder's file copy dialogs are wide but < 100 height (see https://github.com/lwouis/alt-tab-macos/issues/1466)
// Sonoma introduced a bug: a caps-lock & language indicators shows as a small window.
// We try to hide it by filtering out tiny windows
&& size != nil && (size!.width > 100 && size!.height > 50) && (
(
books(app) ||
keynote(app) ||
preview(app, subrole) ||
iina(app) ||
openFlStudio(app, title) ||
crossoverWindow(app, role, subrole, level) ||
isAlwaysOnTopScrcpy(app, level, role, subrole)
) || (
(
[kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) ||
openBoard(app) ||
adobeAudition(app, subrole) ||
adobeAfterEffects(app, subrole) ||
steam(app, title, role) ||
worldOfWarcraft(app, role) ||
battleNetBootstrapper(app, role) ||
firefox(app, role, size) ||
vlcFullscreenVideo(app, role) ||
sanGuoShaAirWD(app) ||
dvdFab(app) ||
drBetotte(app) ||
androidEmulator(app, title) ||
autocad(app, subrole)
) && (
mustHaveIfJetbrainApp(app, title, subrole, size!) &&
mustHaveIfSteam(app, title, role) &&
mustHaveIfFusion360(app, title, role) &&
mustHaveIfColorSlurp(app, subrole)
)
)
)
}
private static func mustHaveIfFusion360(_ app: Application, _ title: String?, _ role: String?) -> Bool {
// filter out Autodesk Fusion side panels "Browser" and "Comments" with subrole AXDialog but with no title
return app.bundleIdentifier != "com.autodesk.fusion360" || (title != nil && title != "")
}
private static func mustHaveIfJetbrainApp(_ app: Application, _ title: String?, _ subrole: String?, _ size: NSSize) -> Bool {
// jetbrain apps sometimes generate non-windows that pass all checks in isActualWindow
// they have no title, so we can filter them out based on that
// we also hide windows too small
return app.bundleIdentifier?.range(of: "^com\\.(jetbrains\\.|google\\.android\\.studio).*?$", options: .regularExpression) == nil || (
(subrole == kAXStandardWindowSubrole || (title != nil && title != "")) &&
size.width > 100 && size.height > 100
)
}
private static func mustHaveIfColorSlurp(_ app: Application, _ subrole: String?) -> Bool {
return app.bundleIdentifier != "com.IdeaPunch.ColorSlurp" || subrole == kAXStandardWindowSubrole
}
private static func iina(_ app: Application) -> Bool {
// IINA.app can have videos float (level == 2 instead of 0)
// there is also complex animations during which we may or may not consider the window not a window
return app.bundleIdentifier == "com.colliderli.iina"
}
private static func keynote(_ app: Application) -> Bool {
// apple Keynote has a fake fullscreen window when in presentation mode
// it covers the screen with a AXUnknown window instead of using standard fullscreen mode
return app.bundleIdentifier == "com.apple.iWork.Keynote"
}
private static func preview(_ app: Application, _ subrole: String?) -> Bool {
// when opening multiple documents at once with apple Preview,
// one of the window will have level == 1 for some reason
return app.bundleIdentifier == "com.apple.Preview" && [kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole)
}
private static func openFlStudio(_ app: Application, _ title: String?) -> Bool {
// OpenBoard is a ported app which doesn't use standard macOS windows
return app.bundleIdentifier == "com.image-line.flstudio" && (title != nil && title != "")
}
private static func openBoard(_ app: Application) -> Bool {
// OpenBoard is a ported app which doesn't use standard macOS windows
return app.bundleIdentifier == "org.oe-f.OpenBoard"
}
private static func adobeAudition(_ app: Application, _ subrole: String?) -> Bool {
return app.bundleIdentifier == "com.adobe.Audition" && subrole == kAXFloatingWindowSubrole
}
private static func adobeAfterEffects(_ app: Application, _ subrole: String?) -> Bool {
return app.bundleIdentifier == "com.adobe.AfterEffects" && subrole == kAXFloatingWindowSubrole
}
private static func books(_ app: Application) -> Bool {
// Books.app has animations on window creation. This means windows are originally created with subrole == AXUnknown or isOnNormalLevel == false
return app.bundleIdentifier == "com.apple.iBooksX"
}
private static func worldOfWarcraft(_ app: Application, _ role: String?) -> Bool {
return app.bundleIdentifier == "com.blizzard.worldofwarcraft" && role == kAXWindowRole
}
private static func battleNetBootstrapper(_ app: Application, _ role: String?) -> Bool {
// Battlenet bootstrapper windows have subrole == AXUnknown
return app.bundleIdentifier == "net.battle.bootstrapper" && role == kAXWindowRole
}
private static func drBetotte(_ app: Application) -> Bool {
return app.bundleIdentifier == "com.ssworks.drbetotte"
}
private static func dvdFab(_ app: Application) -> Bool {
return app.bundleIdentifier == "com.goland.dvdfab.macos"
}
private static func sanGuoShaAirWD(_ app: Application) -> Bool {
return app.bundleIdentifier == "SanGuoShaAirWD"
}
private static func steam(_ app: Application, _ title: String?, _ role: String?) -> Bool {
// All Steam windows have subrole == AXUnknown
// some dropdown menus are not desirable; they have title == "", or sometimes role == nil when switching between menus quickly
return app.bundleIdentifier == "com.valvesoftware.steam" && (title != nil && title != "" && role != nil)
}
private static func mustHaveIfSteam(_ app: Application, _ title: String?, _ role: String?) -> Bool {
// All Steam windows have subrole == AXUnknown
// some dropdown menus are not desirable; they have title == "", or sometimes role == nil when switching between menus quickly
return app.bundleIdentifier != "com.valvesoftware.steam" || (title != nil && title != "" && role != nil)
}
private static func firefox(_ app: Application, _ role: String?, _ size: CGSize?) -> Bool {
// Firefox fullscreen video have subrole == AXUnknown if fullscreen'ed when the base window is not fullscreen
// Firefox tooltips are implemented as windows with subrole == AXUnknown
return (app.bundleIdentifier?.hasPrefix("org.mozilla.firefox") ?? false) && role == kAXWindowRole && size?.height != nil && size!.height > 400
}
private static func vlcFullscreenVideo(_ app: Application, _ role: String?) -> Bool {
// VLC fullscreen video have subrole == AXUnknown if fullscreen'ed
return (app.bundleIdentifier?.hasPrefix("org.videolan.vlc") ?? false) && role == kAXWindowRole
}
private static func androidEmulator(_ app: Application, _ title: String?) -> Bool {
// android emulator small vertical menu is a "window" with empty title; we exclude it
return title != "" && ApplicationDiscriminator.isAndroidEmulator(app.bundleIdentifier, app.pid)
}
private static func crossoverWindow(_ app: Application, _ role: String?, _ subrole: String?, _ level: CGWindowLevel) -> Bool {
return app.bundleIdentifier == nil && role == kAXWindowRole && subrole == kAXUnknownSubrole && level == CGWindow.normalLevel
&& (app.localizedName == "wine64-preloader" || app.executableURL?.absoluteString.contains("/winetemp-") ?? false)
}
private static func isAlwaysOnTopScrcpy(_ app: Application, _ level: CGWindowLevel, _ role: String?, _ subrole: String?) -> Bool {
// scrcpy presents as a floating window when "Always on top" is enabled, so it doesn't get picked up normally.
// It also doesn't have a bundle ID, so we need to match using the localized name, which should always be the same.
return app.localizedName == "scrcpy" && level == CGWindow.floatingWindow && role == kAXWindowRole && subrole == kAXStandardWindowSubrole
}
private static func autocad(_ app: Application, _ subrole: String?) -> Bool {
// AutoCAD uses the undocumented "AXDocumentWindow" subrole
return (app.bundleIdentifier?.hasPrefix("com.autodesk.AutoCAD") ?? false) && subrole == kAXDocumentWindowSubrole
}
}
+320 -335
View File
@@ -2,10 +2,278 @@ import Cocoa
class Windows {
static var list = [Window]()
static var focusedWindowIndex = Int(0)
static var selectedWindowIndex = Int(0)
static var selectedWindowTarget: String?
static var hoveredWindowIndex: Int?
private static var lastWindowActivityType = WindowActivityType.none
static func updateIsFullscreenOnCurrentSpace() {
let windowsOnCurrentSpace = list.filter { !$0.isWindowlessApp }
for window in windowsOnCurrentSpace {
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
guard let window else { return }
// we reuse existing code, to update .isFullscreen, as if there was a kAXWindowResizedNotification
try AccessibilityEvents.handleEventWindow(kAXWindowResizedNotification, window.cgWindowId!, window.application.pid, window.axUiElement!)
}
}
}
private static func compareByAppNameThenWindowTitle(_ w1: Window, _ w2: Window) -> ComparisonResult {
let order = w1.application.localizedName.localizedStandardCompare(w2.application.localizedName)
if order == .orderedSame {
return w1.title.localizedStandardCompare(w2.title)
}
return order
}
static func voiceOverWindow(_ windowIndex: Int = selectedWindowIndex) {
guard App.app.appIsBeingUsed && App.app.thumbnailsPanel.isKeyWindow else { return }
// it seems that sometimes makeFirstResponder is called before the view is visible
// and it creates a delay in showing the main window; calling it with some delay seems to work around this
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
let window = ThumbnailsView.recycledViews[windowIndex]
if window.window_ != nil && window.window != nil {
App.app.thumbnailsPanel.makeFirstResponder(window)
}
}
}
static func previewSelectedWindowIfNeeded() {
if App.app.appIsBeingUsed && ScreenRecordingPermission.status == .granted
&& Preferences.previewSelectedWindow && !Preferences.onlyShowApplications()
&& App.app.thumbnailsPanel.isKeyWindow,
let window = selectedWindow(),
let id = window.cgWindowId,
let thumbnail = window.thumbnail,
let position = window.position,
let size = window.size {
App.app.previewPanel.show(id, thumbnail, position, size)
} else {
App.app.previewPanel.orderOut(nil)
}
}
/// tabs detection is a flaky work-around the lack of public API to observe OS tabs
/// see: https://github.com/lwouis/alt-tab-macos/issues/1540
private static func detectTabbedWindows(_ window: Window, _ cgsWindowIds: [CGWindowID], _ visibleCgsWindowIds: [CGWindowID]) {
if let cgWindowId = window.cgWindowId {
if window.isMinimized || window.isHidden {
if #available(macOS 13.0, *) {
// not exact after window merging
window.isTabbed = !cgsWindowIds.contains(cgWindowId)
} else {
// not known
window.isTabbed = false
}
} else {
window.isTabbed = !visibleCgsWindowIds.contains(cgWindowId)
}
}
}
static func updatesBeforeShowing() -> Bool {
if list.count == 0 || MissionControl.state() == .showAllWindows || MissionControl.state() == .showFrontWindows { return false }
// TODO: find a way to update space info when spaces are changed, instead of on every trigger
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
Spaces.refresh()
let spaceIdsAndIndexes = Spaces.idsAndIndexes.map { $0.0 }
lazy var cgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes)
lazy var visibleCgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes, false)
for window in list {
detectTabbedWindows(window, cgsWindowIds, visibleCgsWindowIds)
window.updateSpacesAndScreen()
refreshIfWindowShouldBeShownToTheUser(window)
}
refreshWhichWindowsToShowTheUser()
sort()
if (!list.contains { $0.shouldShowTheUser }) { return false }
return true
}
// dispatch screenshot requests off the main-thread, then wait for completion
static func refreshThumbnailsAsync(_ windows: [Window], _ source: RefreshCausedBy, windowRemoved: Bool = false) {
guard (!windows.isEmpty || windowRemoved) && ScreenRecordingPermission.status == .granted
&& !Preferences.onlyShowApplications()
&& (!Appearance.hideThumbnails || Preferences.previewSelectedWindow) else { return }
var eligibleWindows = [Window]()
for window in windows {
if !window.isWindowlessApp, let cgWindowId = window.cgWindowId, cgWindowId != CGWindowID(bitPattern: -1) {
eligibleWindows.append(window)
}
}
guard (!eligibleWindows.isEmpty || windowRemoved) else { return }
if #available(macOS 14.0, *),
// mitigate macOS 15 bugs with ScreenCapture Kit (see https://github.com/lwouis/alt-tab-macos/issues/5190)
ProcessInfo.processInfo.operatingSystemVersion.majorVersion != 15 {
WindowCaptureScreenshots.oneTimeScreenshots(eligibleWindows, source)
} else {
WindowCaptureScreenshotsPrivateApi.oneTimeScreenshots(eligibleWindows, source)
}
}
static func refreshWhichWindowsToShowTheUser() {
if Preferences.onlyShowApplications() {
// Group windows by application and select the optimal main window
let windowsGroupedByApp = Dictionary(grouping: list) { $0.application.pid }
windowsGroupedByApp.forEach { (app, windows) in
if windows.count > 1, let mainWindow = findMainWindow(windows) {
windows.forEach { window in
if window.cgWindowId != mainWindow.cgWindowId {
window.shouldShowTheUser = false
}
}
}
}
}
}
private static func refreshIfWindowShouldBeShownToTheUser(_ window: Window) {
window.shouldShowTheUser =
!(window.application.bundleIdentifier.flatMap { id in
Preferences.blacklist.contains {
id.hasPrefix($0.bundleIdentifier) &&
($0.hide == .always || (window.isWindowlessApp && $0.hide != .none))
}
} ?? false) &&
!(Preferences.appsToShow[App.app.shortcutIndex] == .active && window.application.pid != NSWorkspace.shared.frontmostApplication?.processIdentifier) &&
!(Preferences.appsToShow[App.app.shortcutIndex] == .nonActive && window.application.pid == NSWorkspace.shared.frontmostApplication?.processIdentifier) &&
!(!(Preferences.showHiddenWindows[App.app.shortcutIndex] != .hide) && window.isHidden) &&
((Preferences.showWindowlessApps[App.app.shortcutIndex] != .hide && window.isWindowlessApp) ||
!window.isWindowlessApp &&
!(!(Preferences.showFullscreenWindows[App.app.shortcutIndex] != .hide) && window.isFullscreen) &&
!(!(Preferences.showMinimizedWindows[App.app.shortcutIndex] != .hide) && window.isMinimized) &&
!(Preferences.spacesToShow[App.app.shortcutIndex] == .visible && !Spaces.visibleSpaces.contains { visibleSpace in window.spaceIds.contains { $0 == visibleSpace } }) &&
!(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !window.isOnScreen(NSScreen.preferred)) &&
(Preferences.showTabsAsWindows || !window.isTabbed))
}
/// Selects the most appropriate main window from a given list of windows.
///
/// The selection criteria are as follows:
/// 1. Prefer the focused window if it exists.
/// 2. Prefer the main window of the application if the focused window is not found.
///
/// - Parameter windows: An array of `Window` objects to select from.
/// - Returns: The most appropriate `Window` object based on the selection criteria, or `nil` if the array is empty.
static func findMainWindow(_ windows: [Window]) -> Window? {
let sortedWindows = windows.sorted { (window1, window2) -> Bool in
// Prefer the focus window
if window1.application.focusedWindow?.cgWindowId == window1.cgWindowId {
return true
} else if window2.application.focusedWindow?.cgWindowId == window2.cgWindowId {
return false
}
// Prefer the main window
if window1.isAppMainWindow() && !window2.isAppMainWindow() {
return true
} else if !window1.isAppMainWindow() && window2.isAppMainWindow() {
return false
}
return true
}
return sortedWindows.first { $0.shouldShowTheUser }
}
/// selectedWindowIndex methods
//////////////////////////////
static func selectedWindow() -> Window? {
return list.count > selectedWindowIndex ? list[selectedWindowIndex] : nil
}
static func setInitialSelectedAndHoveredWindowIndex() {
let oldIndex = selectedWindowIndex
selectedWindowIndex = 0
selectedWindowTarget = nil
ThumbnailsView.highlight(oldIndex)
if let oldIndex = hoveredWindowIndex {
hoveredWindowIndex = nil
ThumbnailsView.highlight(oldIndex)
}
if let frontmostPid = NSWorkspace.shared.frontmostApplication?.processIdentifier,
let frontmostApp = Applications.findOrCreate(frontmostPid),
(frontmostApp.focusedWindow == nil || Preferences.windowOrder[App.app.shortcutIndex] != .recentlyFocused),
let lastFocusedOrderWindowIndex = getLastFocusedOrderWindowIndex() {
updateSelectedAndHoveredWindowIndex(lastFocusedOrderWindowIndex)
} else {
cycleSelectedWindowIndex(1)
if selectedWindowIndex == 0 {
updateSelectedAndHoveredWindowIndex(0)
}
}
}
static func updateSelectedWindow() {
// selectedWindowTarget still exists: we select its new index
if let index = (list.firstIndex { $0.id == selectedWindowTarget }) {
updateSelectedAndHoveredWindowIndex(index)
return
}
// list has reduced (e.g. app was quit). Selection should be at the end, so users can for example quit many apps in a sequence
let lastIndex = list.filter { $0.shouldShowTheUser }.count - 1
if selectedWindowIndex > lastIndex {
updateSelectedAndHoveredWindowIndex(lastIndex)
}
// selectedWindowIndex is undisturbed: do nothing
}
static func updateSelectedAndHoveredWindowIndex(_ newIndex: Int, _ fromMouse: Bool = false) {
var index: Int?
if fromMouse && (newIndex != hoveredWindowIndex || lastWindowActivityType == .focus) {
let oldIndex = hoveredWindowIndex
hoveredWindowIndex = newIndex
if let oldIndex {
ThumbnailsView.highlight(oldIndex)
}
index = hoveredWindowIndex
lastWindowActivityType = .hover
}
if (!fromMouse || Preferences.mouseHoverEnabled)
&& (newIndex != selectedWindowIndex || lastWindowActivityType == .hover) {
let oldIndex = selectedWindowIndex
selectedWindowIndex = newIndex
selectedWindowTarget = list[newIndex].id
ThumbnailsView.highlight(oldIndex)
previewSelectedWindowIfNeeded()
index = selectedWindowIndex
lastWindowActivityType = .focus
}
guard let index else { return }
ThumbnailsView.highlight(index)
let focusedView = ThumbnailsView.recycledViews[index]
App.app.thumbnailsPanel.thumbnailsView.scrollView.contentView.scrollToVisible(focusedView.frame)
voiceOverWindow(index)
}
static func cycleSelectedWindowIndex(_ step: Int, allowWrap: Bool = true) {
guard App.app.appIsBeingUsed else { return }
let nextIndex = selectedWindowIndexAfterCycling(step)
// don't wrap-around at the end, if key-repeat
if (((step > 0 && nextIndex < selectedWindowIndex) || (step < 0 && nextIndex > selectedWindowIndex)) &&
(!allowWrap || ATShortcut.lastEventIsARepeat || !KeyRepeatTimer.timerIsSuspended))
// don't cycle to another row, if !allowWrap
|| (!allowWrap && list[nextIndex].rowIndex != list[selectedWindowIndex].rowIndex) {
return
}
updateSelectedAndHoveredWindowIndex(nextIndex)
}
static func selectedWindowIndexAfterCycling(_ step: Int) -> Int {
if list.count == 0 { return 0 }
var iterations = 0
var targetIndex = selectedWindowIndex
repeat {
let next = (targetIndex + step) % list.count
targetIndex = next < 0 ? list.count + next : next
iterations += 1
} while !list[targetIndex].shouldShowTheUser && iterations <= list.count
return targetIndex
}
/// lastFocusOrder methods
//////////////////////////////
/// Updates windows "lastFocusOrder" to ensure unique values based on window z-order.
/// Windows are ordered by their position in Spaces.windowsInSpaces() results,
/// with topmost windows first.
@@ -15,14 +283,14 @@ class Windows {
windowLevelMap[cgWindowId] = index
}
list = list
.sorted { w1, w2 in
(windowLevelMap[w1.cgWindowId] ?? .max) < (windowLevelMap[w2.cgWindowId] ?? .max)
}
.enumerated()
.map { (index, window) -> Window in
window.lastFocusOrder = index
return window
}
.sorted { w1, w2 in
(windowLevelMap[w1.cgWindowId] ?? .max) < (windowLevelMap[w2.cgWindowId] ?? .max)
}
.enumerated()
.map { (index, window) -> Window in
window.lastFocusOrder = index
return window
}
}
/// reordered list based on preferences, keeping the original index
@@ -71,57 +339,47 @@ class Windows {
}
}
static func updateIsFullscreenOnCurrentSpace() {
let windowsOnCurrentSpace = Windows.list.filter { !$0.isWindowlessApp }
for window in windowsOnCurrentSpace {
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
guard let window else { return }
try AccessibilityEvents.updateWindowSizeAndPositionAndFullscreen(window.axUiElement!, window.cgWindowId!, window)
}
}
}
private static func compareByAppNameThenWindowTitle(_ w1: Window, _ w2: Window) -> ComparisonResult {
let order = w1.application.localizedName.localizedStandardCompare(w2.application.localizedName)
if order == .orderedSame {
return w1.title.localizedStandardCompare(w2.title)
}
return order
}
static func setInitialFocusedAndHoveredWindowIndex() {
let oldIndex = focusedWindowIndex
focusedWindowIndex = 0
ThumbnailsView.highlight(oldIndex)
if let oldIndex = hoveredWindowIndex {
hoveredWindowIndex = nil
ThumbnailsView.highlight(oldIndex)
}
if let app = Applications.find(NSWorkspace.shared.frontmostApplication?.processIdentifier),
(app.focusedWindow == nil || Preferences.windowOrder[App.app.shortcutIndex] != .recentlyFocused),
let lastFocusedWindowIndex = getLastFocusedWindowIndex() {
updateFocusedAndHoveredWindowIndex(lastFocusedWindowIndex)
} else {
cycleFocusedWindowIndex(1)
if focusedWindowIndex == 0 {
updateFocusedAndHoveredWindowIndex(0)
}
}
}
static func getLastFocusedWindowIndex() -> Int? {
static func getLastFocusedOrderWindowIndex() -> Int? {
var index: Int? = nil
var lastFocusOrderMin = Int.max
Windows.list.enumerated().forEach {
if !$0.element.isWindowlessApp && $0.element.lastFocusOrder < lastFocusOrderMin {
lastFocusOrderMin = $0.element.lastFocusOrder
index = $0.offset
for (offset, w) in list.enumerated() {
if !w.isWindowlessApp && w.shouldShowTheUser && w.lastFocusOrder < lastFocusOrderMin {
lastFocusOrderMin = w.lastFocusOrder
index = offset
}
}
return index
}
static func appendAndUpdateFocus(_ window: Window) {
static func updateLastFocusOrder(_ focusedWindow: Window) -> [Window]? {
// no need to update the list is the window is already lastFocusOrder 0
guard focusedWindow.lastFocusOrder != 0 && list.count > 1, let previousFocus = (list.first { $0.lastFocusOrder == 0 }) else { return [focusedWindow] }
// 2 windows have recently changed: the one which got focused, and the one who just lost focus
let windowsToRefresh = [focusedWindow, previousFocus]
let focusedWindowOldFocusOrder = focusedWindow.lastFocusOrder
list.forEach {
if $0.lastFocusOrder == focusedWindowOldFocusOrder {
$0.lastFocusOrder = 0
} else if $0.lastFocusOrder < focusedWindowOldFocusOrder {
$0.lastFocusOrder += 1
}
}
return windowsToRefresh
}
static func findOrCreate(_ windowAxUiElement: AXUIElement, _ wid: CGWindowID, _ app: Application, _ level: CGWindowLevel, _ title: String?, _ subrole: String?, _ role: String?, _ size: CGSize?, _ position: CGPoint?, _ isFullscreen: Bool?, _ isMinimized: Bool?) -> Window? {
if let window = (list.first { $0.isEqualRobust(windowAxUiElement, wid) }) {
// on any window event, we take the opportunity to refresh all window attributes
window.updateFromAxAttributes(title, size, position, isFullscreen, isMinimized)
return window
}
guard WindowDiscriminator.isActualWindow(app, wid, level, title, subrole, role, size) else { return nil }
let window = Window(windowAxUiElement, app, wid, title, isFullscreen, isMinimized, position, size)
appendWindow(window)
return window
}
static func appendWindow(_ window: Window) {
list.forEach {
$0.lastFocusOrder += 1
}
@@ -131,293 +389,20 @@ class Windows {
}
}
static func removeWindow(_ index: Int, _ pid: pid_t) {
let window = Windows.list[index]
removeAndUpdateFocus(window)
if window.application.addWindowlessWindowIfNeeded() != nil {
Applications.find(pid)?.focusedWindow = nil
}
if Windows.list.count > 0 {
moveFocusedWindowIndexAfterWindowDestroyedInBackground(index)
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
} else {
App.app.hideUi()
}
}
private static func removeAndUpdateFocus(_ window: Window) {
let removedWindowOldFocusOrder = window.lastFocusOrder
list.removeAll {
if $0.lastFocusOrder == removedWindowOldFocusOrder {
static func removeWindows(_ windows: [Window], _ addWindowlessWindowIfNeeded: Bool) {
let toRemove = windows.map { $0.lastFocusOrder }
list.removeAll { w in
if toRemove.contains(w.lastFocusOrder) {
return true
}
if $0.lastFocusOrder > removedWindowOldFocusOrder {
$0.lastFocusOrder -= 1
}
let howManyToShift = toRemove.reduce(0) { $1 < w.lastFocusOrder ? $0 + 1 : $0 }
w.lastFocusOrder -= howManyToShift
return false
}
}
static func updateLastFocus(_ otherWindowAxUiElement: AXUIElement, _ otherWindowWid: CGWindowID) -> [Window]? {
if let focusedWindow = (list.first { $0.isEqualRobust(otherWindowAxUiElement, otherWindowWid) }) {
let focusedWindowOldFocusOrder = focusedWindow.lastFocusOrder
var windowsToRefresh = [focusedWindow]
list.forEach {
if $0.lastFocusOrder == focusedWindowOldFocusOrder {
$0.lastFocusOrder = 0
} else if $0.lastFocusOrder < focusedWindowOldFocusOrder {
$0.lastFocusOrder += 1
}
if $0.lastFocusOrder == 0 {
windowsToRefresh.append($0)
}
}
return windowsToRefresh
if addWindowlessWindowIfNeeded {
windows.forEach { $0.application.addWindowlessWindowIfNeeded() }
}
return nil
}
static func updateFocusedAndHoveredWindowIndex(_ newIndex: Int, _ fromMouse: Bool = false) {
var index: Int?
if fromMouse && (newIndex != hoveredWindowIndex || lastWindowActivityType == .focus) {
let oldIndex = hoveredWindowIndex
hoveredWindowIndex = newIndex
if let oldIndex {
ThumbnailsView.highlight(oldIndex)
}
index = hoveredWindowIndex
lastWindowActivityType = .hover
}
if (!fromMouse || Preferences.mouseHoverEnabled)
&& (newIndex != focusedWindowIndex || lastWindowActivityType == .hover) {
let oldIndex = focusedWindowIndex
focusedWindowIndex = newIndex
ThumbnailsView.highlight(oldIndex)
previewFocusedWindowIfNeeded()
index = focusedWindowIndex
lastWindowActivityType = .focus
}
guard let index else { return }
ThumbnailsView.highlight(index)
let focusedView = ThumbnailsView.recycledViews[index]
App.app.thumbnailsPanel.thumbnailsView.scrollView.contentView.scrollToVisible(focusedView.frame)
voiceOverWindow(index)
}
static func previewFocusedWindowIfNeeded() {
if App.app.appIsBeingUsed && ScreenRecordingPermission.status == .granted
&& Preferences.previewFocusedWindow && !Preferences.onlyShowApplications()
&& App.app.thumbnailsPanel.isKeyWindow,
let window = focusedWindow(),
let id = window.cgWindowId,
let thumbnail = window.thumbnail,
let position = window.position,
let size = window.size {
App.app.previewPanel.show(id, thumbnail, position, size)
} else {
App.app.previewPanel.orderOut(nil)
}
}
static func voiceOverWindow(_ windowIndex: Int = focusedWindowIndex) {
guard App.app.appIsBeingUsed && App.app.thumbnailsPanel.isKeyWindow else { return }
// it seems that sometimes makeFirstResponder is called before the view is visible
// and it creates a delay in showing the main window; calling it with some delay seems to work around this
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
let window = ThumbnailsView.recycledViews[windowIndex]
if window.window_ != nil && window.window != nil {
App.app.thumbnailsPanel.makeFirstResponder(window)
}
}
}
static func focusedWindow() -> Window? {
return list.count > focusedWindowIndex ? list[focusedWindowIndex] : nil
}
static func cycleFocusedWindowIndex(_ step: Int, allowWrap: Bool = true) {
let nextIndex = windowIndexAfterCycling(step)
// don't wrap-around at the end, if key-repeat
if (((step > 0 && nextIndex < focusedWindowIndex) || (step < 0 && nextIndex > focusedWindowIndex)) &&
(!allowWrap || ATShortcut.lastEventIsARepeat || KeyRepeatTimer.timer?.isValid ?? false))
// don't cycle to another row, if !allowWrap
|| (!allowWrap && list[nextIndex].rowIndex != list[focusedWindowIndex].rowIndex) {
return
}
updateFocusedAndHoveredWindowIndex(nextIndex)
}
static func windowIndexAfterCycling(_ step: Int) -> Int {
if list.count == 0 { return 0 }
var iterations = 0
var targetIndex = focusedWindowIndex
repeat {
let next = (targetIndex + step) % list.count
targetIndex = next < 0 ? list.count + next : next
iterations += 1
} while !list[targetIndex].shouldShowTheUser && iterations <= list.count
return targetIndex
}
private static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ index: Int) {
if index < focusedWindowIndex {
cycleFocusedWindowIndex(-1)
}
}
static func updateFocusedWindowIndex() {
if let focusedWindow = focusedWindow() {
if !focusedWindow.shouldShowTheUser {
cycleFocusedWindowIndex(windowIndexAfterCycling(1) > focusedWindowIndex ? 1 : -1)
} else {
previewFocusedWindowIfNeeded()
}
} else {
cycleFocusedWindowIndex(-1)
}
}
/// tabs detection is a flaky work-around the lack of public API to observe OS tabs
/// see: https://github.com/lwouis/alt-tab-macos/issues/1540
private static func detectTabbedWindows(_ window: Window, _ cgsWindowIds: [CGWindowID], _ visibleCgsWindowIds: [CGWindowID]) {
if let cgWindowId = window.cgWindowId {
if window.isMinimized || window.isHidden {
if #available(macOS 13.0, *) {
// not exact after window merging
window.isTabbed = !cgsWindowIds.contains(cgWindowId)
} else {
// not known
window.isTabbed = false
}
} else {
window.isTabbed = !visibleCgsWindowIds.contains(cgWindowId)
}
}
}
static func updatesBeforeShowing() -> Bool {
if list.count == 0 || MissionControl.state() == .showAllWindows || MissionControl.state() == .showFrontWindows { return false }
// TODO: find a way to update space info when spaces are changed, instead of on every trigger
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
Spaces.refresh()
let spaceIdsAndIndexes = Spaces.idsAndIndexes.map { $0.0 }
lazy var cgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes)
lazy var visibleCgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes, false)
for window in list {
detectTabbedWindows(window, cgsWindowIds, visibleCgsWindowIds)
updatesWindowSpace(window)
refreshIfWindowShouldBeShownToTheUser(window)
}
refreshWhichWindowsToShowTheUser()
sort()
if (!list.contains { $0.shouldShowTheUser }) { return false }
return true
}
static func updatesWindowSpace(_ window: Window) {
// macOS bug: if you tab a window, then move the tab group to another space, other tabs from the tab group will stay on the current space
// you can use the Dock to focus one of the other tabs and it will teleport that tab in the current space, proving that it's a macOS bug
// note: for some reason, it behaves differently if you minimize the tab group after moving it to another space
if let cgWindowId = window.cgWindowId {
let spaceIds = cgWindowId.spaces()
window.spaceIds = spaceIds
window.spaceIndexes = spaceIds.compactMap { spaceId in Spaces.idsAndIndexes.first { $0.0 == spaceId }?.1 }
window.isOnAllSpaces = spaceIds.count > 1
}
}
// dispatch screenshot requests off the main-thread, then wait for completion
static func refreshThumbnailsAsync(_ windows: [Window], _ source: RefreshCausedBy) {
guard !windows.isEmpty && ScreenRecordingPermission.status == .granted
&& !Preferences.onlyShowApplications()
&& (!Appearance.hideThumbnails || Preferences.previewFocusedWindow) else { return }
var eligibleWindows = [Window]()
for window in windows {
if !window.isWindowlessApp, let cgWindowId = window.cgWindowId, cgWindowId != CGWindowID(bitPattern: -1) {
eligibleWindows.append(window)
}
}
if eligibleWindows.isEmpty { return }
screenshotEligibleWindowsAndUpdateUi(eligibleWindows, source)
}
private static func screenshotEligibleWindowsAndUpdateUi(_ eligibleWindows: [Window], _ source: RefreshCausedBy) {
for window in eligibleWindows {
BackgroundWork.screenshotsQueue.addOperation { [weak window] in
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
if let wid = window?.cgWindowId, let cgImage = wid.screenshot() {
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
DispatchQueue.main.async { [weak window] in
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
window?.refreshThumbnail(cgImage)
}
}
}
}
}
static func refreshWhichWindowsToShowTheUser() {
if Preferences.onlyShowApplications() {
// Group windows by application and select the optimal main window
let windowsGroupedByApp = Dictionary(grouping: list) { $0.application.pid }
windowsGroupedByApp.forEach { (app, windows) in
if windows.count > 1, let mainWindow = selectMainWindow(windows) {
windows.forEach { window in
if window.cgWindowId != mainWindow.cgWindowId {
window.shouldShowTheUser = false
}
}
}
}
}
}
private static func refreshIfWindowShouldBeShownToTheUser(_ window: Window) {
window.shouldShowTheUser =
!(window.application.bundleIdentifier.flatMap { id in
Preferences.blacklist.contains {
id.hasPrefix($0.bundleIdentifier) &&
($0.hide == .always || (window.isWindowlessApp && $0.hide != .none))
}
} ?? false) &&
!(Preferences.appsToShow[App.app.shortcutIndex] == .active && window.application.pid != NSWorkspace.shared.frontmostApplication?.processIdentifier) &&
!(Preferences.appsToShow[App.app.shortcutIndex] == .nonActive && window.application.pid == NSWorkspace.shared.frontmostApplication?.processIdentifier) &&
!(!(Preferences.showHiddenWindows[App.app.shortcutIndex] != .hide) && window.isHidden) &&
((Preferences.showWindowlessApps[App.app.shortcutIndex] != .hide && window.isWindowlessApp) ||
!window.isWindowlessApp &&
!(!(Preferences.showFullscreenWindows[App.app.shortcutIndex] != .hide) && window.isFullscreen) &&
!(!(Preferences.showMinimizedWindows[App.app.shortcutIndex] != .hide) && window.isMinimized) &&
!(Preferences.spacesToShow[App.app.shortcutIndex] == .visible && !Spaces.visibleSpaces.contains { visibleSpace in window.spaceIds.contains { $0 == visibleSpace } }) &&
!(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !window.isOnScreen(NSScreen.preferred)) &&
(Preferences.showTabsAsWindows || !window.isTabbed))
}
/// Selects the most appropriate main window from a given list of windows.
///
/// The selection criteria are as follows:
/// 1. Prefer the focused window if it exists.
/// 2. Prefer the main window of the application if the focused window is not found.
///
/// - Parameter windows: An array of `Window` objects to select from.
/// - Returns: The most appropriate `Window` object based on the selection criteria, or `nil` if the array is empty.
static func selectMainWindow(_ windows: [Window]) -> Window? {
let sortedWindows = windows.sorted { (window1, window2) -> Bool in
// Prefer the focus window
if window1.application.focusedWindow?.cgWindowId == window1.cgWindowId {
return true
} else if window2.application.focusedWindow?.cgWindowId == window2.cgWindowId {
return false
}
// Prefer the main window
if window1.isAppMainWindow() && !window2.isAppMainWindow() {
return true
} else if !window1.isAppMainWindow() && window2.isAppMainWindow() {
return false
}
return true
}
return sortedWindows.first { $0.shouldShowTheUser }
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent, windowRemoved: true)
}
}
+88 -145
View File
@@ -5,186 +5,129 @@ import ApplicationServices.HIServices.AXNotificationConstants
class AccessibilityEvents {
static let axObserverCallback: AXObserverCallback = { _, element, notificationName, _ in
let type = notificationName as String
Logger.debug(type)
AXUIElement.retryAxCallUntilTimeout(callType: .axEventEntrypoint) { try handleEvent(type, element) }
}
static func updateWindowSizeAndPositionAndFullscreen(_ element: AXUIElement, _ wid: CGWindowID, _ window: Window?) throws {
if let (title, _, _, isMinimized, isFullscreen) = try element.windowAttributes() {
let size = try element.size()
let position = try element.position()
DispatchQueue.main.async {
if let window = (window != nil ? window : (Windows.list.first { $0.isEqualRobust(element, wid) })) {
window.title = window.bestEffortTitle(title)
window.size = size
window.position = position
window.isMinimized = isMinimized
if window.isFullscreen != isFullscreen {
window.isFullscreen = isFullscreen
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
}
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
Logger.debug { type }
AXUIElement.retryAxCallUntilTimeout(context: "(type:\(type)", callType: .axEventEntrypoint) {
try handleEvent(type, element)
}
}
private static func handleEvent(_ type: String, _ element: AXUIElement) throws {
// events are handled concurrently, thus we check that the app is still running
let pid = try element.pid()
if try pid != ProcessInfo.processInfo.processIdentifier || (element.subrole() != kAXUnknownSubrole) {
Logger.info(type, pid, try element.title() ?? "nil")
switch type {
case kAXApplicationActivatedNotification: try applicationActivated(element, pid)
case kAXApplicationHiddenNotification,
kAXApplicationShownNotification: try applicationHiddenOrShown(pid, type)
case kAXWindowCreatedNotification: try windowCreated(element, pid)
case kAXMainWindowChangedNotification,
kAXFocusedWindowChangedNotification: try focusedWindowChanged(element, pid)
case kAXUIElementDestroyedNotification: try windowDestroyed(element, pid)
case kAXWindowMiniaturizedNotification,
kAXWindowDeminiaturizedNotification: try windowMiniaturizedOrDeminiaturized(element, type)
case kAXTitleChangedNotification: try windowTitleChanged(element, pid)
case kAXWindowResizedNotification,
kAXWindowMovedNotification: try windowResizedOrMoved(element, pid)
default: return
Logger.debug { "\(type) pid:\(pid)" }
if [kAXApplicationActivatedNotification, kAXApplicationHiddenNotification, kAXApplicationShownNotification].contains(type) {
try handleEventApp(type, pid, element)
} else {
try handleEventWindowWithDebounceIfNecessary(type, pid, element)
}
}
private static func handleEventApp(_ type: String, _ pid: pid_t, _ element: AXUIElement) throws {
let appFocusedWindow = try element.attributes([kAXFocusedWindowAttribute]).focusedWindow
DispatchQueue.main.async {
guard let app = Applications.findOrCreate(pid) else { return }
Logger.info { "\(type) app:\(app.debugId)" }
if type == kAXApplicationActivatedNotification {
applicationActivated(app, pid, type, appFocusedWindow)
} else if type == kAXApplicationHiddenNotification || type == kAXApplicationShownNotification {
applicationHiddenOrShown(app, pid, type)
}
}
}
private static func applicationActivated(_ element: AXUIElement, _ pid: pid_t) throws {
let appFocusedWindow = try element.focusedWindow()
let wid = try appFocusedWindow?.cgWindowId()
DispatchQueue.main.async {
if let app = Applications.find(pid) {
if app.hasBeenActiveOnce != true {
app.hasBeenActiveOnce = true
}
let window = (appFocusedWindow != nil && wid != nil) ? Windows.updateLastFocus(appFocusedWindow!, wid!)?.first : nil
app.focusedWindow = window
App.app.checkIfShortcutsShouldBeDisabled(window, app.runningApplication)
App.app.refreshOpenUi(window != nil ? [window!] : [], .refreshUiAfterExternalEvent)
}
private static func applicationActivated(_ app: Application, _ pid: pid_t, _ type: String, _ appFocusedWindow: AXUIElement?) {
if app.hasBeenActiveOnce != true {
app.hasBeenActiveOnce = true
}
}
private static func applicationHiddenOrShown(_ pid: pid_t, _ type: String) throws {
DispatchQueue.main.async {
if let app = Applications.find(pid) {
app.isHidden = type == kAXApplicationHiddenNotification
let windows = Windows.list.filter {
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
return $0.application.pid == pid
}
// if we process the "shown" event too fast, the window won't be listed by CGSCopyWindowsWithOptionsAndTags
// it will thus be detected as isTabbed. We add a delay to work around this scenario
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
if let appFocusedWindow {
// if there is a focusedWindow, we reuse existing code to process it as if it was a kAXFocusedWindowChangedNotification
AXUIElement.retryAxCallUntilTimeout(context: "(type:\(type)", callType: .axEventEntrypoint) {
try handleEventWindowWithDebounceIfNecessary(kAXFocusedWindowChangedNotification, pid, appFocusedWindow)
}
} else {
App.app.checkIfShortcutsShouldBeDisabled(nil, app)
if let windowless = (Windows.list.first { $0.isWindowlessApp && $0.application.pid == pid }) {
if let windows = Windows.updateLastFocusOrder(windowless) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
}
}
}
}
private static func windowCreated(_ element: AXUIElement, _ pid: pid_t) throws {
let wid = try element.cgWindowId()
if let (title, role, subrole, isMinimized, isFullscreen) = try element.windowAttributes() {
let position = try element.position()
let size = try element.size()
let level = wid.level()
DispatchQueue.main.async {
if let app = Applications.find(pid), NSRunningApplication(processIdentifier: pid) != nil {
if (!Windows.list.contains { $0.isEqualRobust(element, wid) }) &&
AXUIElement.isActualWindow(app, wid, level, title, subrole, role, size) {
let window = Window(element, app, wid, title, isFullscreen, isMinimized, position, size)
Windows.appendAndUpdateFocus(window)
Windows.cycleFocusedWindowIndex(1)
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
private static func applicationHiddenOrShown(_ app: Application, _ pid: pid_t, _ type: String) {
app.isHidden = type == kAXApplicationHiddenNotification
let windows = Windows.list.filter {
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
return $0.application.pid == pid
}
// if we process the "shown" event too fast, the window won't be listed by CGSCopyWindowsWithOptionsAndTags
// it will thus be detected as isTabbed. We add a delay to work around this scenario
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
}
}
private static func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) throws {
private static func handleEventWindowWithDebounceIfNecessary(_ type: String, _ pid: pid_t, _ element: AXUIElement) throws {
let wid = try element.cgWindowId()
if let runningApp = NSRunningApplication(processIdentifier: pid) {
// photoshop will focus a window *after* you focus another app
// we check that a focused window happens within an active app
if runningApp.isActive {
DispatchQueue.main.async {
guard let app = Applications.find(pid) else { return }
// if the window is shown by alt-tab, we mark it as focused for this app
// this avoids issues with dialogs, quicklook, etc (see scenarios from #1044 and #2003)
if let w = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
app.focusedWindow = w
}
if let windows = Windows.updateLastFocus(element, wid) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
} else {
AXUIElement.retryAxCallUntilTimeout(context: "wid:\(wid) pid:\(pid)", pid: pid, callType: .updateWindow) {
if let (title, role, subrole, isMinimized, isFullscreen) = try element.windowAttributes() {
let position = try element.position()
let size = try element.size()
let level = wid.level()
DispatchQueue.main.async {
if (!Windows.list.contains { $0.isEqualRobust(element, wid) }),
AXUIElement.isActualWindow(app, wid, level, title, subrole, role, size) {
let window = Window(element, app, wid, title, isFullscreen, isMinimized, position, size)
Windows.appendAndUpdateFocus(window)
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
}
}
}
guard wid != 0 || type == kAXUIElementDestroyedNotification else { return } // some bogus "windows" have wid 0
if type == kAXTitleChangedNotification || type == kAXWindowResizedNotification || type == kAXWindowMovedNotification {
AXUIElement.retryAxCallUntilTimeout(context: "(wid:\(wid) pid:\(pid))", debounceType: (type == kAXTitleChangedNotification ? .windowTitleChanged : .windowResizedOrMoved), pid: pid, wid: wid, callType: .updateWindow) {
try handleEventWindow(type, wid, pid, element)
}
} else {
DispatchQueue.main.async {
Applications.find(pid)?.focusedWindow = nil
}
try handleEventWindow(type, wid, pid, element)
}
}
private static func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
let wid = try element.cgWindowId()
static func handleEventWindow(_ type: String, _ wid: CGWindowID, _ pid: pid_t, _ element: AXUIElement) throws {
guard wid != App.app.thumbnailsPanel.windowNumber else { return } // don't process events for the thumbnails panel
let level = wid.level()
let a = try element.attributes([kAXTitleAttribute, kAXSubroleAttribute, kAXRoleAttribute, kAXSizeAttribute, kAXPositionAttribute, kAXFullscreenAttribute, kAXMinimizedAttribute])
DispatchQueue.main.async {
if let index = (Windows.list.firstIndex { $0.isEqualRobust(element, wid) }) {
Windows.removeWindow(index, pid)
guard let app = Applications.findOrCreate(pid) else { return }
Logger.info { "\(type) wid:\(wid) app:\(app.debugId)" }
if type == kAXUIElementDestroyedNotification {
windowDestroyed(element, pid, wid)
return
}
}
}
private static func windowMiniaturizedOrDeminiaturized(_ element: AXUIElement, _ type: String) throws {
let wid = try element.cgWindowId()
DispatchQueue.main.async {
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
window.isMinimized = type == kAXWindowMiniaturizedNotification
guard let window = Windows.findOrCreate(element, wid, app, level, a.title, a.subrole, a.role, a.size, a.position, a.isFullscreen, a.isMinimized) else {
// we don't know this window, but it got focused, so let's update app.focusedWindow with nil
if type == kAXFocusedWindowChangedNotification {
app.focusedWindow = nil
}
return
}
Logger.debug { "\(type) win:\(window.debugId)" }
if type == kAXMainWindowChangedNotification || type == kAXFocusedWindowChangedNotification {
focusedWindowChanged(window)
} else if type == kAXWindowResizedNotification || type == kAXWindowMovedNotification {
windowResizedOrMoved(window)
} else {
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
private static func windowTitleChanged(_ element: AXUIElement, _ pid: pid_t) throws {
let wid = try element.cgWindowId()
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowTitleChanged, pid: pid, wid: wid, callType: .updateWindow) {
if let (title, _, _, isMinimized, isFullscreen) = try element.windowAttributes() {
DispatchQueue.main.async {
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }), title != window.title {
window.title = window.bestEffortTitle(title)
window.isMinimized = isMinimized
window.isFullscreen = isFullscreen
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
private static func windowDestroyed(_ windowAxUiElement: AXUIElement, _ pid: pid_t, _ wid: CGWindowID) {
if let window = (Windows.list.first { $0.isEqualRobust(windowAxUiElement, wid) }) {
Windows.removeWindows([window], true)
}
}
private static func windowResizedOrMoved(_ element: AXUIElement, _ pid: pid_t) throws {
let wid = try element.cgWindowId()
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowResizedOrMoved, pid: pid, wid: wid, callType: .updateWindow) {
try updateWindowSizeAndPositionAndFullscreen(element, wid, nil)
private static func focusedWindowChanged(_ window: Window) {
// photoshop will focus a window *after* you focus another app
// we check that a focused window happens within an active app
guard window.application.runningApplication.isActive else { return }
// if the window is shown by alt-tab, we mark it as focused for this app
// this avoids issues with dialogs, quicklook, etc (see scenarios from #1044 and #2003)
window.application.focusedWindow = window
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
if let windows = Windows.updateLastFocusOrder(window) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
}
}
private static func windowResizedOrMoved(_ window: Window) {
window.updateSpacesAndScreen()
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
+4 -4
View File
@@ -7,7 +7,7 @@ class CliEvents {
let source = CFMessagePortCreateRunLoopSource(nil, messagePort, 0) {
CFRunLoopAddSource(BackgroundWork.cliEventsThread.runLoop, source, .commonModes)
} else {
Logger.error("Can't listen on message port. Is another AltTab already running?")
Logger.error { "Can't listen on message port. Is another AltTab already running?" }
// TODO: should we quit or restart here?
// It's complex since AltTab can be restarted sometimes,
// and the new instance may coexist with the old for some duration
@@ -16,16 +16,16 @@ class CliEvents {
}
private static let handleEvent: CFMessagePortCallBack = { (_: CFMessagePort?, _: Int32, _ data: CFData?, _: UnsafeMutableRawPointer?) in
Logger.debug()
Logger.debug { "" }
if let data,
let message = String(data: data as Data, encoding: .utf8) {
Logger.info(message)
Logger.info { message }
let output = CliServer.executeCommandAndSendReponse(message)
if let responseData = try? CliServer.jsonEncoder.encode(output) as CFData {
return Unmanaged.passRetained(responseData)
}
}
Logger.error("Failed to decode message")
Logger.error { "Failed to decode message" }
return nil
}
}
+68
View File
@@ -0,0 +1,68 @@
import Cocoa
import Carbon.HIToolbox.Events
class CursorEvents {
private static var eventTap: CFMachPort!
private static var shouldBeEnabled: Bool!
static var deadZoneInitialPosition: CGPoint?
static var isAllowedToMouseHover = true
static func observe() {
observe_()
}
static func toggle(_ enabled: Bool) {
guard enabled != shouldBeEnabled else { return }
shouldBeEnabled = enabled
if !enabled {
deadZoneInitialPosition = nil
}
if let eventTap {
CGEvent.tapEnable(tap: eventTap, enable: enabled)
}
}
private static func observe_() {
let eventMask = [CGEventType.mouseMoved].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
// CGEvent.tapCreate returns nil if ensureAccessibilityCheckboxIsChecked() didn't pass
eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: handleEvent,
userInfo: nil)
if let eventTap {
toggle(false)
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
// we run on main-thread directly since all we do is check NSEvent data, which we must do on main-thread
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes)
} else {
App.app.restart()
}
}
private static let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in
if type == .mouseMoved {
updateDeadzoneSituation(cgEvent)
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled {
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
return Unmanaged.passUnretained(cgEvent) // focused app will receive the event
}
/// when using the trackpad, the user may swipe with a slight mistake. This will create a small cursor movement
/// we ignore those, as they are not intended. Intended movements will be larger and not ignored
private static func updateDeadzoneSituation(_ cgEvent: CGEvent) {
guard let event = cgEvent.toNSEvent() else { return }
guard let deadZoneInitialPosition else {
deadZoneInitialPosition = event.locationInWindow
isAllowedToMouseHover = false
return
}
let deltaX = event.locationInWindow.x - deadZoneInitialPosition.x
let deltaY = event.locationInWindow.y - deadZoneInitialPosition.y
let d = hypot(deltaX, deltaY)
isAllowedToMouseHover = d > 25
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ class DockEvents {
AXUIElement.retryAxCallUntilTimeout(callType: .subscribeToDockNotification) {
if try axUiElement!.subscribeToNotification(axObserver!, notification.rawValue, nil) {
if notification == MissionControlState.showDesktop {
Logger.debug("Subscribed to Dock")
Logger.debug { "Subscribed to Dock" }
}
}
}
@@ -29,7 +29,7 @@ class DockEvents {
}
private static let handleEvent: AXObserverCallback = { _, _, notificationName, _ in
Logger.debug(notificationName)
Logger.debug { notificationName }
MissionControl.setState(MissionControlState(rawValue: notificationName as String)!)
}
}
+2 -2
View File
@@ -46,7 +46,7 @@ class KeyboardEvents {
fn(shortcutId, shortcut)
}
}
Logger.info("toggleGlobalShortcuts", shouldDisable)
Logger.info { "disabled:\(shouldDisable)" }
globalShortcutsAreDisabled = shouldDisable
}
}
@@ -97,7 +97,7 @@ class KeyboardEvents {
userInfo: nil)
if let eventTap {
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
CFRunLoopAddSource(BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
} else {
App.app.restart()
}
+13 -1
View File
@@ -13,7 +13,19 @@ class KeyboardEventsTestable {
@discardableResult
func handleKeyboardEvent(_ globalId: Int?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: NSEvent.ModifierFlags?, _ isARepeat: Bool) -> Bool {
Logger.debug(globalId, shortcutState, keyCode, modifiers, isARepeat, NSEvent.modifierFlags)
if let globalId, let shortcutState {
Logger.debug {
let shortcut = KeyboardEventsTestable.globalShortcutsIds.first { $0.value == globalId }
return "globalShortcut:\(shortcut?.key ?? "") state:\(shortcutState)"
}
} else {
// TODO: use proper pattern from SwiftBeaver to not compute SymbolicModifierFlagsTransformer when logs are off
Logger.debug {
let modifiersAsString = modifiers.flatMap { SymbolicModifierFlagsTransformer.shared.transformedValue(NSNumber(value: $0.rawValue)) }
let keyCodeAsString = keyCode.flatMap { SymbolicKeyCodeTransformer.shared.transformedValue(NSNumber(value: $0)) }
return "keys:\(modifiersAsString ?? "")\(keyCodeAsString ?? "") isARepeat:\(isARepeat)"
}
}
var someShortcutTriggered = false
for shortcut in ControlsTab.shortcuts.values {
if shortcut.matches(globalId, shortcutState, keyCode, modifiers) && shortcut.shouldTrigger() {
+2 -1
View File
@@ -28,8 +28,9 @@ class MouseEvents {
callback: handleEvent,
userInfo: nil)
if let eventTap {
MouseEvents.toggle(false)
toggle(false)
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
// we run on main-thread directly since all we do is check UI coordinates, which we must do on main-thread
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes)
} else {
App.app.restart()
@@ -2,24 +2,23 @@ import Cocoa
class RunningApplicationsEvents {
private static var appsObserver: NSKeyValueObservation!
private static var previousValueOfRunningApps: Set<NSRunningApplication>!
static func observe() {
previousValueOfRunningApps = Set(NSWorkspace.shared.runningApplications)
appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: { (_, _) in handleEvent() })
// we can't observe NSWorkspace.didLaunchApplicationNotification or NSWorkspace.didTerminateApplicationNotification
// these only trigger for some apps, mostly GUI app. We need to track all processes as any could spawn a window
appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: { (_, change) in handleEvent(change) })
}
private static func handleEvent() {
let workspaceApps = Set(NSWorkspace.shared.runningApplications)
let added = workspaceApps.subtracting(previousValueOfRunningApps)
let removed = previousValueOfRunningApps.subtracting(workspaceApps)
Logger.info("added:", added.map { $0.id }, "removed:", removed.map { $0.id })
if !added.isEmpty {
Applications.addRunningApplications(Array(added))
private static func handleEvent(_ change: NSKeyValueObservedChange<[NSRunningApplication]>) {
let launched = change.newValue
let quit = change.oldValue
if let launched {
Logger.debug { "launched:\(launched.map { $0.debugId() })" }
Applications.addRunningApplications(launched)
}
if !removed.isEmpty {
Applications.removeRunningApplications(Array(removed))
if let quit {
Logger.debug { "quit:\(quit.map { $0.debugId() })" }
Applications.removeRunningApplications(quit)
}
previousValueOfRunningApps = workspaceApps
}
}
+43 -9
View File
@@ -1,19 +1,53 @@
import Cocoa
class ScreensEvents {
static let debouncerScreenAndSpace = Debouncer(.main)
static func observe() {
NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: NSApplication.didChangeScreenParametersNotification, object: nil)
}
@objc private static func handleEvent(_ notification: Notification) {
Logger.debug(notification.name.rawValue)
Spaces.refresh()
// a screen added or removed, or screen resolution change can mess up layout; we reset components
App.app.resetPreferencesDependentComponents()
// a screen added or removed can shuffle windows around Spaces; we refresh them
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
Logger.info("screens", NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })
Logger.info("spaces", Spaces.screenSpacesMap)
Logger.info("current space", Spaces.currentSpaceIndex, Spaces.currentSpaceId)
// screen notifications often arrive in groups (e.g. 2 in a row in a short time)
// we debounce to minimize work
debouncerScreenAndSpace.debounce(.screenEvent) {
Logger.debug { notification.name.rawValue }
Spaces.refresh()
Screens.refresh()
// a screen added or removed, or screen resolution change can mess up layout; we reset components
App.app.resetPreferencesDependentComponents()
}
}
}
final class Debouncer {
private let queue: DispatchQueue
private var workItem: DispatchWorkItem?
private var contextsBlocks = [DebounceContext: () -> Void]()
init(_ queue: DispatchQueue) {
self.queue = queue
}
func debounce(_ context: DebounceContext, _ block: @escaping () -> Void) {
contextsBlocks[context] = block
workItem?.cancel()
let item = DispatchWorkItem(block: {
for block in self.contextsBlocks.values {
block()
}
// ScreensEvents: a screen added or removed can shuffle windows around Spaces; we refresh them
// SpacesEvents: if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" }
Logger.info { "currentSpace:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId)) spaces:\(Spaces.screenSpacesMap)" }
})
workItem = item
queue.asyncAfter(deadline: .now() + humanPerceptionDelay, execute: item)
}
}
enum DebounceContext {
case spaceEvent
case screenEvent
}
+1 -1
View File
@@ -28,7 +28,7 @@ class ScrollwheelEvents {
userInfo: nil)
if let eventTap {
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
CFRunLoopAddSource(BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
} else {
App.app.restart()
}
+13 -9
View File
@@ -6,14 +6,18 @@ class SpacesEvents {
}
@objc private static func handleEvent(_ notification: Notification) {
Logger.debug(notification.name.rawValue)
// if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
Logger.info("current space", Spaces.currentSpaceIndex, Spaces.currentSpaceId)
// Workaround for Safari full-screen videos
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
// for such cases, we refresh isFullscreen on Space change
Windows.updateIsFullscreenOnCurrentSpace()
ScreensEvents.debouncerScreenAndSpace.debounce(.spaceEvent) {
Logger.debug { notification.name.rawValue }
// Workaround for Safari full-screen videos
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
// for such cases, we refresh isFullscreen on Space change
Windows.updateIsFullscreenOnCurrentSpace()
if let frontmostPid = NSWorkspace.shared.frontmostApplication?.processIdentifier,
let frontmostApp = Applications.findOrCreate(frontmostPid),
let focusedWindow = frontmostApp.focusedWindow {
App.app.checkIfShortcutsShouldBeDisabled(focusedWindow, nil)
}
}
}
}
@@ -8,8 +8,8 @@ class SystemAppearanceEvents {
}
@objc private static func handleEvent(_ notification: Notification) {
Logger.debug(notification.name.rawValue)
Logger.info(UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light")
Logger.debug { notification.name.rawValue }
Logger.info { UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light" }
// fix layout issues by resetting components
App.app.resetPreferencesDependentComponents()
}
@@ -6,8 +6,8 @@ class SystemScrollerStyleEvents {
}
@objc private static func handleEvent(_ notification: Notification) {
Logger.debug(notification.name.rawValue)
Logger.info(App.app.thumbnailsPanel.thumbnailsView.scrollView.scrollerStyle == .overlay ? "overlay" : "legacy")
Logger.debug { notification.name.rawValue }
Logger.info { App.app.thumbnailsPanel.thumbnailsView.scrollView.scrollerStyle == .overlay ? "overlay" : "legacy" }
// force overlay style after a change in System Preference > General > Show scroll bars
App.app.thumbnailsPanel?.thumbnailsView.scrollView.scrollerStyle = .overlay
}
+140 -113
View File
@@ -4,6 +4,7 @@ import Cocoa
class TrackpadEvents {
private static var eventTap: CFMachPort!
private static var shouldBeEnabled: Bool!
private static var cursorMovedDistance = CGFloat(0.0)
static func observe() {
observe_()
@@ -19,6 +20,14 @@ class TrackpadEvents {
}
}
static func reset() {
ScrollwheelEvents.toggle(false)
NavigationSwipeDetector.reset()
NonFreshGestureDetector.reset()
// no need to call TriggerSwipeDetector.reset; it does it itself when triggering
TriggerSwipeDetector.maxFingersDownDuringTrigger = 0
}
private static func observe_() {
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
eventTap = CGEvent.tapCreate(
@@ -30,7 +39,7 @@ class TrackpadEvents {
userInfo: nil)
if let eventTap {
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
CFRunLoopAddSource(BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
} else {
App.app.restart()
}
@@ -48,88 +57,75 @@ class TrackpadEvents {
}
private static func touchEventHandler(_ cgEvent: CGEvent) -> Bool {
guard let nsEvent = GestureDetector.convertEvent(cgEvent) else { return false }
guard let nsEvent = cgEvent.toNSEvent() else { return false } // don't absorb the touch event
let touches = nsEvent.allTouches()
// sometimes the os sends events with no touches; we ignore these as they could break our gesture logic
if touches.count == 0 { return false }
let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved || $0.phase == .stationary) }
// Logger.error("---", "activeTouches:", activeTouches.count, "all:", touches.map { $0.phase.readable })
// Logger.error { (touches.count, touches.map { $0.phase.readable }) }
// macOS often sends faulty events with no touches between valid events; we ignore these as they would break our gesture logic
guard touches.count > 0 else { return false }
// isResting seems to always return false. It's not doing its job to detect resting thumb/palm/finger
// on macOS, the finger contact surface is not exposed in NSTouch, so we can't detect big contact == palm, for example
// the closest thing we can do to detect resting inputs is remove touches which have .phase == .stationary
let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved) }
let fingersDown = touches.count { $0.phase == .began || $0.phase == .moved || $0.phase == .stationary }
let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4
// not enough fingers are down
if (!App.app.appIsBeingUsed && activeTouches.count != requiredFingers) || (App.app.appIsBeingUsed && activeTouches.count < 2) {
DispatchQueue.main.async { ScrollwheelEvents.toggle(false) }
TriggerSwipeDetector.reset()
NavigationSwipeDetector.reset()
return GestureDetector.checkForFingersUp(activeTouches.count, requiredFingers)
}
// enough fingers are down
if App.app.appIsBeingUsed {
DispatchQueue.main.async { ScrollwheelEvents.toggle(true) }
if !GestureDetector.updateStartPositions(activeTouches, &NavigationSwipeDetector.startPositions) {
if let r = NavigationSwipeDetector.check(activeTouches) { return r }
handleEventIfAppIsBeingUsed(fingersDown, activeTouches, requiredFingers)
return true // absorb the touch event
}
return handleEventIfAppIsNotBeingUsed(fingersDown, activeTouches, requiredFingers) // absorb or not the touch event, depending on the situation
}
private static func handleEventIfAppIsBeingUsed(_ fingersDown: Int, _ activeTouches: Set<NSTouch>, _ requiredFingers: Int) {
if fingersDown <= TriggerSwipeDetector.maxFingersDownDuringTrigger - requiredFingers {
if App.app.shortcutIndex == Preferences.gestureIndex && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
DispatchQueue.main.async {
App.app.focusTarget()
}
}
return
}
if activeTouches.count > 1 {
CursorEvents.deadZoneInitialPosition = nil
NavigationSwipeDetector.hasDetected(activeTouches)
}
// if activeTouches.count == 1, ignore (finger is in pointer-mode)
}
private static func handleEventIfAppIsNotBeingUsed(_ fingersDown: Int, _ activeTouches: Set<NSTouch>, _ requiredFingers: Int) -> Bool {
if fingersDown <= 1 {
NonFreshGestureDetector.reset()
return false // don't absorb the touch event
}
if NonFreshGestureDetector.hasDetected(activeTouches, requiredFingers) {
return false // don't absorb the touch event
}
if activeTouches.count != requiredFingers {
TriggerSwipeDetector.reset()
return false // don't absorb the touch event
} else {
if !GestureDetector.updateStartPositions(activeTouches, &TriggerSwipeDetector.startPositions) {
if let r = TriggerSwipeDetector.check(activeTouches) { return r }
}
return TriggerSwipeDetector.hasDetected(fingersDown, activeTouches) // absorb or not the touch event, depending on the situation
}
return false
}
}
class GestureDetector {
static func convertEvent(_ cgEvent: CGEvent) -> NSEvent? {
var nsEvent: NSEvent?
// conversion has to happen on the main-thread, or appkit will crash
DispatchQueue.main.sync {
nsEvent = NSEvent(cgEvent: cgEvent)
}
return nsEvent
class NonFreshGestureDetector {
private static var userHasDoneAnotherGesture = false
private static var gestureTracker = GestureTracker()
/// if the user has already used a gesture-action (e.g. 2-finger scroll), we consider this "session" invalid, until all fingers are released
/// This prevents: 4->3 trigger (System swipe already happened), or 2->3 trigger (System scroll already happened)
static func hasDetected(_ activeTouches: Set<NSTouch>,_ requiredFingers: Int) -> Bool {
guard !userHasDoneAnotherGesture else { return true }
let new = gestureTracker.isNewGesture(activeTouches)
guard activeTouches.count > requiredFingers && !new else { return false }
let distances = gestureTracker.computeDistance(activeTouches)
userHasDoneAnotherGesture = distances.contains(where: { abs($0.x) >= TriggerSwipeDetector.MIN_SWIPE_DISTANCE || abs($0.y) >= TriggerSwipeDetector.MIN_SWIPE_DISTANCE })
return userHasDoneAnotherGesture
}
static func checkForFingersUp(_ fingersDown: Int, _ requiredFingers: Int) -> Bool {
if App.app.appIsBeingUsed && fingersDown < requiredFingers && App.app.shortcutIndex == Preferences.gestureIndex
&& !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
DispatchQueue.main.async { App.app.focusTarget() }
return true
}
return false
}
static func updateStartPositions(_ activeTouches: Set<NSTouch>, _ startPositions: inout [String: NSPoint]) -> Bool {
// if touches are new, record their startPositions
if (activeTouches.contains { startPositions["\($0.identity)"] == nil }) {
for touch in activeTouches {
startPositions["\(touch.identity)"] = touch.normalizedPosition
}
return true
}
return false
}
static func computeAverageDistance(_ activeTouches: Set<NSTouch>, _ startPositions: [String: NSPoint]) -> NSPoint {
var totalDelta = NSPoint(x: 0, y: 0)
for touch in activeTouches {
totalDelta = totalDelta + (touch.normalizedPosition - startPositions["\(touch.identity)"]!)
}
return totalDelta / activeTouches.count
}
static func computeDistance(_ activeTouches: Set<NSTouch>, _ startPositions: [String: NSPoint]) -> Array<NSPoint> {
var deltas: Array<NSPoint> = []
for touch in activeTouches {
deltas.append(touch.normalizedPosition - startPositions["\(touch.identity)"]!)
}
return deltas
}
static func blockOngoingScrolling() {
if #available(macOS 10.13, *) {
// simulate mouseWheel-stopped to end potential existing scrolling started before AltTab was opened
// this avoid the swipe to trigger a scroll on the active window and show AltTab, at the same time
CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 1, wheel1: 0, wheel2: 0, wheel3: 0)?
.post(tap: .cghidEventTap)
}
static func reset() {
userHasDoneAnotherGesture = false
gestureTracker.reset()
}
}
@@ -140,38 +136,35 @@ class TriggerSwipeDetector {
// when the native using 3-finger swipe to shift Space, macOS will prevent a swipe until the fingers are raised,
// if the user moves too much in the vertical direction. We imitate this behavior
static let MAX_SWIPE_DISTANCE_IN_WRONG_DIRECTION: Double = 0.1 // % of trackpad surface traveled
// if there are extra non-active fingers during the gesture trigger, we remember it so we can ignore those extra fingers to detect intent to focus on release
static var maxFingersDownDuringTrigger = 0
static var startPositions: [String: NSPoint] = [:]
static var swipeStillPossible = true
private static var gestureTracker = GestureTracker()
private static var swipeStillPossible = true
static func check(_ activeTouches: Set<NSTouch>) -> Bool? {
if !App.app.appIsBeingUsed && swipeStillPossible {
let distances = GestureDetector.computeDistance(activeTouches, startPositions)
for distance in distances {
let (absX, absY) = (abs(distance.x), abs(distance.y))
let horizontal = Preferences.nextWindowGesture.isHorizontal()
if (!(updateSwipeStillPossible(horizontal ? absY : absX) && (horizontal ? absX : absY) >= MIN_SWIPE_DISTANCE )) {
return false
}
}
reset()
DispatchQueue.main.async {
ScrollwheelEvents.toggle(true)
performHapticFeedback()
App.app.showUiOrCycleSelection(Preferences.gestureIndex, false)
}
return true
static func hasDetected(_ fingersDown: Int, _ activeTouches: Set<NSTouch>) -> Bool {
guard swipeStillPossible && !gestureTracker.isNewGesture(activeTouches) else { return false }
maxFingersDownDuringTrigger = max(maxFingersDownDuringTrigger, fingersDown)
let distances = gestureTracker.computeDistance(activeTouches)
for distance in distances {
let (absX, absY) = (abs(distance.x), abs(distance.y))
let horizontal = Preferences.nextWindowGesture.isHorizontal()
let distanceInRightDirection = horizontal ? absX : absY
let distanceInWrongDirection = horizontal ? absY : absX
swipeStillPossible = distanceInWrongDirection < MAX_SWIPE_DISTANCE_IN_WRONG_DIRECTION
guard swipeStillPossible && distanceInRightDirection >= MIN_SWIPE_DISTANCE else { return false }
}
return nil
}
static func updateSwipeStillPossible(_ distanceInWrongDirection: Double) -> Bool {
swipeStillPossible = distanceInWrongDirection < MAX_SWIPE_DISTANCE_IN_WRONG_DIRECTION
return swipeStillPossible
reset()
DispatchQueue.main.async {
ScrollwheelEvents.toggle(true)
performHapticFeedback()
App.app.showUiOrCycleSelection(Preferences.gestureIndex, false)
}
return true
}
static func reset() {
startPositions.removeAll(keepingCapacity: true)
gestureTracker.reset()
swipeStillPossible = true
}
}
@@ -179,42 +172,76 @@ class TriggerSwipeDetector {
class NavigationSwipeDetector {
static let MIN_SWIPE_DISTANCE: Double = 0.03 // % of trackpad surface traveled
static var startPositions: [String: NSPoint] = [:]
private static var gestureTracker = GestureTracker()
static func check(_ activeTouches: Set<NSTouch>) -> Bool? {
let averageDistance = GestureDetector.computeAverageDistance(activeTouches, startPositions)
static func hasDetected(_ activeTouches: Set<NSTouch>) {
guard !gestureTracker.isNewGesture(activeTouches) else { return }
let averageDistance = gestureTracker.computeAverageDistance(activeTouches)
let (absX, absY) = (abs(averageDistance.x), abs(averageDistance.y))
let maxIsX = absX >= absY
if (maxIsX ? absX : absY) > MIN_SWIPE_DISTANCE {
maxIsX ? resetX(activeTouches) : resetY(activeTouches)
let direction: Direction = maxIsX ? (averageDistance.x < 0 ? .left : .right) : (averageDistance.y < 0 ? .down : .up)
DispatchQueue.main.async {
performHapticFeedback()
App.app.cycleSelection(direction, allowWrap: false)
}
return true
guard (maxIsX ? absX : absY) > MIN_SWIPE_DISTANCE else { return }
maxIsX ? gestureTracker.resetX(activeTouches) : gestureTracker.resetY(activeTouches)
let direction: Direction = maxIsX ? (averageDistance.x < 0 ? .left : .right) : (averageDistance.y < 0 ? .down : .up)
DispatchQueue.main.async {
performHapticFeedback()
App.app.cycleSelection(direction, allowWrap: false)
}
return nil
}
static func reset() {
gestureTracker.reset()
}
}
class GestureTracker {
var startPositions = [String: NSPoint]()
@discardableResult
func isNewGesture(_ activeTouches: Set<NSTouch>) -> Bool {
// if touches are new, record their startPositions
if (activeTouches.contains { startPositions["\($0.identity)"] == nil }) {
for touch in activeTouches {
startPositions["\(touch.identity)"] = touch.normalizedPosition
}
return true
}
return false
}
func computeAverageDistance(_ activeTouches: Set<NSTouch>) -> NSPoint {
var totalDelta = NSPoint(x: 0, y: 0)
for touch in activeTouches {
totalDelta = totalDelta + (touch.normalizedPosition - startPositions["\(touch.identity)"]!)
}
return totalDelta / activeTouches.count
}
func computeDistance(_ activeTouches: Set<NSTouch>) -> Array<NSPoint> {
var deltas: Array<NSPoint> = []
for touch in activeTouches {
deltas.append(touch.normalizedPosition - startPositions["\(touch.identity)"]!)
}
return deltas
}
func reset() {
startPositions.removeAll(keepingCapacity: true)
}
static func resetX(_ activeTouches: Set<NSTouch>) {
func resetX(_ activeTouches: Set<NSTouch>) {
for touch in activeTouches {
startPositions["\(touch.identity)"]!.x = touch.normalizedPosition.x
}
}
static func resetY(_ activeTouches: Set<NSTouch>) {
func resetY(_ activeTouches: Set<NSTouch>) {
for touch in activeTouches {
startPositions["\(touch.identity)"]!.y = touch.normalizedPosition.y
}
}
}
func performHapticFeedback() {
fileprivate func performHapticFeedback() {
if Preferences.trackpadHapticFeedbackEnabled {
NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .default)
}
+1 -1
View File
@@ -14,7 +14,7 @@ class UserDefaultsEvents: NSObject {
}
private func handleEvent(_ keyPath: String?) {
Logger.debug(keyPath, PoliciesTab.policyLock, Preferences.updatePolicy)
Logger.debug { "\(keyPath ?? "keyPath:nil") updatePolicy:\(Preferences.updatePolicy) policyLock:\(PoliciesTab.policyLock)" }
guard !PoliciesTab.policyLock else { return }
let buttons = PoliciesTab.updatesPolicyDropdown!
let id = buttonIdToUpdate()
+324
View File
@@ -0,0 +1,324 @@
import Cocoa
import ScreenCaptureKit
@available(macOS 14.0, *)
class WindowCaptureScreenshots {
// SCShareableContent.getExcludingDesktopWindows is expensive for the OS; we cache as much as possible
static var cachedSCWindows = [SCWindow]()
static func oneTimeScreenshots(_ windowsToScreenshot: [Window], _ source: RefreshCausedBy) {
let windows = windowsToScreenshot.compactMap { $0.cgWindowId }
guard !windows.isEmpty else { return }
let (cachedWindows, notCachedWindows) = sortCachedAndNotCached(windows)
Logger.debug { "cached:\(cachedWindows.map { $0.windowID }) notCached:\(notCachedWindows)" }
handleCachedWindows(cachedWindows, source)
handleNotCachedWindows(notCachedWindows, source)
}
private static func handleCachedWindows(_ cachedWindows: [SCWindow], _ source: RefreshCausedBy) {
guard !cachedWindows.isEmpty else { return }
for cachedWindow in cachedWindows {
oneTimeCapture(cachedWindow, source)
}
}
private static func handleNotCachedWindows(_ notCachedWindows: [CGWindowID], _ source: RefreshCausedBy) {
guard !notCachedWindows.isEmpty else { return }
SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: false) { shareableContent, error in
guard let shareableContent, error == nil else { Logger.error { "\(shareableContent == nil) \(error)" }; return }
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
// this callback is executed on an undetermined queue; we move execution to main-thread
DispatchQueue.main.async {
cachedSCWindows = shareableContent.windows
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
for notCachedWindow in notCachedWindows {
if let cachedWindow = (cachedSCWindows.first { $0.windowID == notCachedWindow }) {
oneTimeCapture(cachedWindow, source)
} else {
Logger.error { "wid:\(notCachedWindow) was not found in SCShareableContent windows" }
}
}
}
}
}
private static func sortCachedAndNotCached(_ windows: [CGWindowID]) -> ([SCWindow], [CGWindowID]) {
var cachedWindows = [SCWindow]()
var notCachedWindows = [CGWindowID]()
for window in windows {
if let cachedWindow = (cachedSCWindows.first { $0.windowID == window }) {
cachedWindows.append(cachedWindow)
} else {
notCachedWindows.append(window)
}
}
return (cachedWindows, notCachedWindows)
}
private static func oneTimeCapture(_ scWindow: SCWindow, _ source: RefreshCausedBy) {
guard !App.app.isTerminating, let window = (Windows.list.first { $0.cgWindowId == scWindow.windowID }), window.size != nil else { return }
let config = SCStreamConfiguration.forWindow(scWindow, window, false)
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
ActiveWindowCaptures.increment()
SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) { cgImage, error in
ActiveWindowCaptures.decrement()
guard let cgImage, error == nil else { Logger.error { "\(window.debugId) \(cgImage == nil) \(error)" }; return }
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
DispatchQueue.main.async {
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
if let window = (Windows.list.first { $0.cgWindowId == scWindow.windowID }) {
window.refreshThumbnail(.cgImage(cgImage))
}
}
}
}
}
class WindowCaptureScreenshotsPrivateApi {
static func oneTimeScreenshots(_ eligibleWindows: [Window], _ source: RefreshCausedBy) {
for window in eligibleWindows {
BackgroundWork.screenshotsQueue.addOperation { [weak window] in
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
guard let wid = window?.cgWindowId, let cgImage = oneTimeCapture(wid) else { return }
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
DispatchQueue.main.async { [weak window] in
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
window?.refreshThumbnail(.cgImage(cgImage))
}
}
}
}
private static func oneTimeCapture(_ wid: CGWindowID) -> CGImage? {
guard !App.app.isTerminating else { return nil }
// we use CGSHWCaptureWindowList because it can screenshot minimized windows, which CGWindowListCreateImage can't
var windowId_ = wid
ActiveWindowCaptures.increment()
let list = CGSHWCaptureWindowList(CGS_CONNECTION, &windowId_, 1, [.ignoreGlobalClipShape, .bestResolution, .fullSize]).takeRetainedValue() as! [CGImage]
ActiveWindowCaptures.decrement()
return list.first
}
}
// @available(macOS 12.3, *)
// class WindowCaptureVideos {
// private static var streams = [CGWindowID: SCStream]()
// private static var streamOutputs = [CGWindowID: StreamOutput]()
// // SCStream.backgroundColor is [unowned], so we must keep own these variables
// static let scStreamBackgroundColorDark = NSColor(white: 0.23, alpha: 1).cgColor
// static let scStreamBackgroundColorLight = NSColor.white.cgColor
//
// static func startCapturing(_ windowsWhichMayHaveChanged: [Window]) {
// let windowsToShow = Set<CGWindowID>(Windows.list.filter { !$0.isWindowlessApp && $0.shouldShowTheUser }.compactMap { $0.cgWindowId })
// let windowsAlreadyStreaming = Set<CGWindowID>(streams.keys)
// let windowsToStop = windowsAlreadyStreaming.subtracting(windowsToShow)
// stopCaptures(windowsToStop)
// let windowsToStart = windowsToShow.subtracting(windowsAlreadyStreaming)
// let windowsWhichMayHaveChanged_ = windowsWhichMayHaveChanged.compactMap { $0.cgWindowId }
// if !windowsToStart.isEmpty || !windowsWhichMayHaveChanged_.isEmpty {
// SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: false) { shareableContent, error in
// guard let shareableContent, error == nil else {
// Logger.error { "\(shareableContent == nil) \(error)" }
// return
// }
// // this callback is executed on an undetermined queue
// // we move execution to main-thread to avoid races with starting/stopping streams and the app being shown/hidden
// DispatchQueue.main.async {
// guard App.app.appIsBeingUsed else { return }
// startCaptures(windowsToStart, shareableContent)
// updateCaptures(windowsWhichMayHaveChanged_, shareableContent)
// Logger.debug { streams.keys }
// }
// }
// }
// }
//
// static func stopCapturing() {
// Logger.debug { streams.keys }
// for stream in streams.values {
// stream.stopCapture()
// }
// streams.removeAll()
// streamOutputs.removeAll()
// }
//
// private static func updateCaptures(_ windowsWhichMayHaveChanged: [CGWindowID], _ shareableContent: SCShareableContent) {
// for wid in windowsWhichMayHaveChanged {
// if let stream = streams[wid],
// let scWindow = shareableContent.windows.first(where: { $0.windowID == wid }) {
// stream.updateConfiguration(SCStreamConfiguration.forWindow(scWindow, true)) { error in
// if let error { Logger.error { error } }
// }
// }
// }
// }
//
// private static func startCaptures(_ windowsToStart: Set<CGWindowID>, _ shareableContent: SCShareableContent) {
// for wid in windowsToStart {
// if let scWindow = shareableContent.windows.first(where: { $0.windowID == wid }) {
// startCapture(scWindow)
// }
// }
// }
//
//
// private static func startCapture(_ window: SCWindow) {
// let wid = window.windowID
// let output = StreamOutput(wid)
// let config = SCStreamConfiguration.forWindow(window, true)
// let filter = SCContentFilter(desktopIndependentWindow: window)
// let stream = SCStream(filter: filter, configuration: config, delegate: output)
// do {
// try stream.addStreamOutput(output, type: .screen, sampleHandlerQueue: BackgroundWork.screenshotsQueue.strongUnderlyingQueue)
// stream.startCapture { error in
// if let error { Logger.error { error } }
// }
// streams[wid] = stream
// streamOutputs[wid] = output
// } catch {
// Logger.error { error }
// }
// }
//
// private static func stopCaptures(_ windowsToStop: Set<CGWindowID>) {
// for wid in windowsToStop {
// stopCapture(wid)
// }
// }
//
// private static func stopCapture(_ wid: CGWindowID) {
// if let stream = streams[wid] {
// stream.stopCapture()
// streams.removeValue(forKey: wid)
// streamOutputs.removeValue(forKey: wid)
// }
// }
//
// class StreamOutput: NSObject, SCStreamOutput, SCStreamDelegate {
// let wid: CGWindowID
//
// init(_ wid: CGWindowID) {
// self.wid = wid
// }
//
// // from SCStreamOutput; handle captured samples
// func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
// BackgroundWork.screenshotsQueue.trackCallbacks {
// if sampleBuffer.isValid,
// let pixelBuffer = sampleBuffer.pixelBuffer() {
// DispatchQueue.main.async {
// if let window = (Windows.list.first { $0.cgWindowId == self.wid }) {
// window.refreshThumbnail(.pixelBuffer(pixelBuffer))
// }
// }
// }
// }
// }
//
// // from SCStreamDelegate; handle errors when opening a stream
// func stream(_ stream: SCStream, didStopWithError error: any Error) {
// BackgroundWork.screenshotsQueue.trackCallbacks {
// Logger.error { error }
// }
// }
// }
// }
@available(macOS 12.3, *)
extension SCStreamConfiguration {
static func forWindow(_ scWindow: SCWindow, _ window: Window, _ video: Bool) -> SCStreamConfiguration {
let config = SCStreamConfiguration()
config.setWindowSize(scWindow, window)
config.showsCursor = false
// if video {
// config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(60))
// config.queueDepth = 8
// // ~60% memory reduction compared to kCVPixelFormatType_32BGRA
// config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
// // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange doesn't have transparency, so we end up with an opaque background color around the window corners
// // we use a background color that try and hide these corners as much as possible
// config.backgroundColor = Appearance.currentTheme == .dark ? WindowCaptureVideos.scStreamBackgroundColorDark : WindowCaptureVideos.scStreamBackgroundColorLight
// }
// config.scalesToFit = true
return config
}
private func windowScaleFactor(_ window: Window) -> CGFloat {
if let screenId = window.screenId,
let screen = Screens.all[screenId] {
return screen.backingScaleFactor
}
return NSScreen.preferred.backingScaleFactor
}
private func setWindowSize(_ scWindow: SCWindow, _ window: Window) {
let scaleFactor = windowScaleFactor(window)
// we use window.size and not scWindow.frame, as scWindow is cached thus its size can be stale. window.size is always up-to-date
let size = window.size! // we checked non-nil earlier, up the stack
// window.size shows logical size. It doesn't change when the scaleFactor changes. We need to correct for this as we need to capture more and less pixels depending on DPI
let originalSize = NSSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
guard originalSize.width > 0, originalSize.height > 0 else { return }
if Preferences.previewSelectedWindow {
width = Int(originalSize.width)
height = Int(originalSize.height)
} else {
// capture screenshots as small as needed for the thumbnails
let maxSize = ThumbnailsPanel.maxPossibleThumbnailSize
guard maxSize.width > 0, maxSize.height > 0 else { return }
let scale = min(maxSize.width / originalSize.width, maxSize.height / originalSize.height)
width = Int((originalSize.width * scale).rounded())
height = Int((originalSize.height * scale).rounded())
}
}
}
extension CMSampleBuffer {
@available(macOS 12.3, *)
func pixelBuffer() -> CVPixelBuffer? {
if let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(self, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]],
let attachments = attachmentsArray.first,
let statusRawValue = attachments[SCStreamFrameInfo.status] as? Int,
let status = SCFrameStatus(rawValue: statusRawValue),
status == .complete || status == .started { // new frame was generated
return imageBuffer
}
return nil
}
@available(macOS 12.3, *)
func metalTexture(_ device: MTLDevice) -> MTLTexture? {
guard let pixelBuffer = pixelBuffer(),
let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else { return nil }
let desc = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: .bgra8Unorm,
width: CVPixelBufferGetWidth(pixelBuffer),
height: CVPixelBufferGetHeight(pixelBuffer),
mipmapped: false
)
return device.makeTexture(descriptor: desc, iosurface: surface, plane: 0)
}
}
class ActiveWindowCaptures {
private static var count: Int = 0
private static let semaphore = DispatchSemaphore(value: 1)
static func increment() {
semaphore.wait()
count += 1
semaphore.signal()
}
static func decrement() {
semaphore.wait()
count -= 1
semaphore.signal()
}
static func value() -> Int {
semaphore.wait()
let current = count
semaphore.signal()
return current
}
}
+16 -3
View File
@@ -5,9 +5,9 @@ if let command = CliClient.detectCommand() {
CliClient.sendCommandAndProcessResponse(command)
}
// - if the app is quit/force-quit from Activity Monitor, it will receive SIGTERM and applicationWillTerminate won't be called
// - if the app crashes in swift code (e.g. unexpected nil object), SIGTRAP is sent
// we intercept these signals, and do an emergency exit
// - SIGTERM: if the app is quit/force-quit from Activity Monitor, it will receive SIGTERM and applicationWillTerminate won't be called
// - SIGTRAP: if the app crashes in swift code (e.g. unexpected nil object), SIGTRAP is sent
// - SIGKILL: if we stop the app using SIGKILL (e.g. stopping from IntelliJ, or from the terminal), there is no chance to intercept it
[SIGTERM, SIGTRAP].forEach {
signal($0) { s in
emergencyExit("Exiting after receiving signal", s)
@@ -34,5 +34,18 @@ fileprivate func emergencyExit(_ logs: Any?...) {
setNativeCommandTabEnabled(true)
print(logs)
printStackTrace()
makeSureAllCapturesAreFinished()
exit(0)
}
func makeSureAllCapturesAreFinished() {
App.app.isTerminating = true
let timeout = 5.0
let startTime = DispatchTime.now()
var elapsedTime = 0.0
while ActiveWindowCaptures.value() > 0 && elapsedTime <= timeout {
Logger.warning { "There are \(ActiveWindowCaptures.value()) screenshots in progress. We need to wait for them to avoid a bug where macOS shows permission dialogs to the user for no reason." }
Thread.sleep(forTimeInterval: 0.1)
elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
}
}
+97 -65
View File
@@ -17,6 +17,7 @@ class App: AppCenterApplication {
static let website = "https://alt-tab-macos.netlify.app"
static let appIcon = CGImage.named("app.icns")
static var app: App!
var isTerminating = false
var thumbnailsPanel: ThumbnailsPanel!
var previewPanel: PreviewPanel!
var preferencesWindow: PreferencesWindow!
@@ -31,6 +32,8 @@ class App: AppCenterApplication {
private var appCenterDelegate: AppCenterCrash?
// don't queue multiple delayed rebuildUi() calls
private var delayedDisplayScheduled = 0
private var lastRefreshTimeInNanoseconds = DispatchTime.now().uptimeNanoseconds
private var nextRefreshScheduled = false
override init() {
super.init()
@@ -56,13 +59,14 @@ class App: AppCenterApplication {
}
func hideUi(_ keepPreview: Bool = false) {
Logger.info(appIsBeingUsed)
Logger.info { "appIsBeingUsed:\(self.appIsBeingUsed)" }
guard appIsBeingUsed else { return } // already hidden
appIsBeingUsed = false
isFirstSummon = true
forceDoNothingOnRelease = false
MouseEvents.toggle(false)
ScrollwheelEvents.toggle(false)
CursorEvents.toggle(false)
TrackpadEvents.reset()
hideThumbnailPanelWithoutChangingKeyWindow()
if !keepPreview {
previewPanel.orderOut(nil)
@@ -93,30 +97,30 @@ class App: AppCenterApplication {
}
func closeSelectedWindow() {
Windows.focusedWindow()?.close()
Windows.selectedWindow()?.close()
}
func minDeminSelectedWindow() {
Windows.focusedWindow()?.minDemin()
Windows.selectedWindow()?.minDemin()
}
func toggleFullscreenSelectedWindow() {
Windows.focusedWindow()?.toggleFullscreen()
Windows.selectedWindow()?.toggleFullscreen()
}
func quitSelectedApp() {
Windows.focusedWindow()?.application.quit()
Windows.selectedWindow()?.application.quit()
}
func hideShowSelectedApp() {
Windows.focusedWindow()?.application.hideOrShow()
Windows.selectedWindow()?.application.hideOrShow()
}
func focusTarget() {
guard appIsBeingUsed else { return } // already hidden
let focusedWindow = Windows.focusedWindow()
Logger.info(focusedWindow?.debugId)
focusSelectedWindow(focusedWindow)
let selectedWindow = Windows.selectedWindow()
Logger.info { selectedWindow?.debugId }
focusSelectedWindow(selectedWindow)
}
@objc func checkForUpdatesNow(_ sender: NSMenuItem) {
@@ -124,7 +128,7 @@ class App: AppCenterApplication {
}
@objc func checkPermissions(_ sender: NSMenuItem) {
permissionsWindow.show({})
permissionsWindow.show()
}
@objc func supportProject() {
@@ -167,13 +171,13 @@ class App: AppCenterApplication {
if direction == .up || direction == .down {
thumbnailsPanel.thumbnailsView.navigateUpOrDown(direction, allowWrap: allowWrap)
} else {
Windows.cycleFocusedWindowIndex(direction.step(), allowWrap: allowWrap)
Windows.cycleSelectedWindowIndex(direction.step(), allowWrap: allowWrap)
}
}
func previousWindowShortcutWithRepeatingKey() {
cycleSelection(.trailing)
KeyRepeatTimer.toggleRepeatingKeyPreviousWindow()
KeyRepeatTimer.startRepeatingKeyPreviousWindow()
}
func focusSelectedWindow(_ selectedWindow: Window?) {
@@ -197,30 +201,46 @@ class App: AppCenterApplication {
CGWarpMouseCursorPosition(point)
}
func refreshOpenUi(_ windowsToScreenshot: [Window], _ source: RefreshCausedBy) {
Windows.refreshThumbnailsAsync(windowsToScreenshot, source)
guard appIsBeingUsed else { return }
if source == .refreshUiAfterExternalEvent {
if !Windows.updatesBeforeShowing() { hideUi(); return }
func refreshOpenUi(_ windowsToScreenshot: [Window], _ source: RefreshCausedBy, windowRemoved: Bool = false) {
Windows.refreshThumbnailsAsync(windowsToScreenshot, source, windowRemoved: windowRemoved)
refreshOpenUiWithThrottling {
guard self.appIsBeingUsed else { return }
if source == .refreshUiAfterExternalEvent {
if !Windows.updatesBeforeShowing() { self.hideUi(); return }
}
guard self.appIsBeingUsed else { return }
Windows.updateSelectedWindow()
guard self.appIsBeingUsed else { return }
self.thumbnailsPanel.updateContents()
guard self.appIsBeingUsed else { return }
Windows.voiceOverWindow() // at this point ThumbnailViews are assigned to the window, and ready
guard self.appIsBeingUsed else { return }
Windows.previewSelectedWindowIfNeeded()
guard self.appIsBeingUsed else { return }
Applications.refreshBadgesAsync()
}
}
func refreshOpenUiWithThrottling( _ block: @escaping () -> Void) {
let timeSinceLastRefreshInSeconds = Float(DispatchTime.now().uptimeNanoseconds - lastRefreshTimeInNanoseconds) / 1_000_000
if timeSinceLastRefreshInSeconds >= 200 {
lastRefreshTimeInNanoseconds = DispatchTime.now().uptimeNanoseconds
block()
return
}
guard !nextRefreshScheduled else { return }
nextRefreshScheduled = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.210) {
self.nextRefreshScheduled = false
self.refreshOpenUiWithThrottling(block)
}
guard appIsBeingUsed else { return }
Windows.updateFocusedWindowIndex()
guard appIsBeingUsed else { return }
thumbnailsPanel.updateContents()
guard appIsBeingUsed else { return }
Windows.voiceOverWindow() // at this point ThumbnailViews are assigned to the window, and ready
guard appIsBeingUsed else { return }
Windows.previewFocusedWindowIfNeeded()
guard appIsBeingUsed else { return }
Applications.refreshBadgesAsync()
}
func showUiOrCycleSelection(_ shortcutIndex: Int, _ forceDoNothingOnRelease_: Bool) {
forceDoNothingOnRelease = forceDoNothingOnRelease_
Logger.debug(shortcutIndex, self.shortcutIndex, isFirstSummon)
Logger.debug { "isFirstSummon:\(self.isFirstSummon) shortcutIndex:\(shortcutIndex)" }
App.app.appIsBeingUsed = true
if isFirstSummon || shortcutIndex != self.shortcutIndex {
MainMenu.toggle(enabled: false)
NSScreen.updatePreferred()
if isVeryFirstSummon {
Windows.sortByLevel()
@@ -229,7 +249,7 @@ class App: AppCenterApplication {
isFirstSummon = false
self.shortcutIndex = shortcutIndex
if !Windows.updatesBeforeShowing() { hideUi(); return }
Windows.setInitialFocusedAndHoveredWindowIndex()
Windows.setInitialSelectedAndHoveredWindowIndex()
if Preferences.windowDisplayDelay == DispatchTimeInterval.milliseconds(0) {
buildUiAndShowPanel()
} else {
@@ -243,7 +263,7 @@ class App: AppCenterApplication {
}
} else {
cycleSelection(.leading)
KeyRepeatTimer.toggleRepeatingKeyNextWindow()
KeyRepeatTimer.startRepeatingKeyNextWindow()
}
}
@@ -254,14 +274,14 @@ class App: AppCenterApplication {
refreshOpenUi([], .showUi)
guard appIsBeingUsed else { return }
thumbnailsPanel.show()
KeyRepeatTimer.toggleRepeatingKeyNextWindow()
KeyRepeatTimer.startRepeatingKeyNextWindow()
Windows.refreshThumbnailsAsync(Windows.list, .refreshOnlyThumbnailsAfterShowUi)
}
func checkIfShortcutsShouldBeDisabled(_ activeWindow: Window?, _ activeApp: NSRunningApplication?) {
let app = activeWindow?.application.runningApplication ?? activeApp
func checkIfShortcutsShouldBeDisabled(_ activeWindow: Window?, _ activeApp: Application?) {
let app = activeWindow?.application ?? activeApp!
let shortcutsShouldBeDisabled = Preferences.blacklist.contains { blacklistedId in
if let id = app?.bundleIdentifier {
if let id = app.bundleIdentifier {
return id.hasPrefix(blacklistedId.bundleIdentifier) &&
(blacklistedId.ignore == .always || (blacklistedId.ignore == .whenFullscreen && (activeWindow?.isFullscreen ?? false)))
}
@@ -279,7 +299,7 @@ extension App: NSApplicationDelegate {
appCenterDelegate = AppCenterCrash()
App.shared.disableRelaunchOnLogin()
Logger.initialize()
Logger.info("Launching AltTab \(App.version)")
Logger.info { "Launching AltTab \(App.version)" }
#if DEBUG
UserDefaults.standard.set(true, forKey: "NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints")
#endif
@@ -288,35 +308,42 @@ extension App: NSApplicationDelegate {
#endif
AXUIElement.setGlobalTimeout()
Preferences.initialize()
BackgroundWork.startSystemPermissionThread()
permissionsWindow = PermissionsWindow()
SystemPermissions.ensurePermissionsAreGranted { [weak self] in
guard let self else { return }
Logger.info("System Permissions are granted")
BackgroundWork.start()
NSScreen.updatePreferred()
Appearance.update()
Menubar.initialize()
MainMenu.loadFromXib()
self.thumbnailsPanel = ThumbnailsPanel()
self.previewPanel = PreviewPanel()
Spaces.refresh()
SpacesEvents.observe()
ScreensEvents.observe()
SystemAppearanceEvents.observe()
SystemScrollerStyleEvents.observe()
Applications.initialDiscovery()
self.preferencesWindow = PreferencesWindow()
self.feedbackWindow = FeedbackWindow()
KeyboardEvents.addEventHandlers()
MouseEvents.observe()
TrackpadEvents.observe()
CliEvents.observe()
Logger.info("AltTab finished launching")
#if DEBUG
BackgroundWork.preStart()
SystemPermissions.ensurePermissionsAreGranted()
}
func continueAppLaunchAfterPermissionsAreGranted() {
Logger.info { "System permissions are granted; continuing launch" }
BackgroundWork.start()
NSScreen.updatePreferred()
Appearance.update()
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
ThumbnailsPanel.updateMaxPossibleAppIconSize()
Menubar.initialize()
MainMenu.loadFromXib()
self.thumbnailsPanel = ThumbnailsPanel()
self.previewPanel = PreviewPanel()
Spaces.refresh()
Screens.refresh()
SpacesEvents.observe()
ScreensEvents.observe()
SystemAppearanceEvents.observe()
SystemScrollerStyleEvents.observe()
Applications.initialDiscovery()
self.preferencesWindow = PreferencesWindow()
self.feedbackWindow = FeedbackWindow()
KeyboardEvents.addEventHandlers()
MouseEvents.observe()
CursorEvents.observe()
TrackpadEvents.observe()
CliEvents.observe()
// login item and plist updates can be done a bit later, to accelerate launch
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { GeneralTab.startAtLoginCallback() }
Logger.info { "Finished launching AltTab" }
#if DEBUG
// self.showPreferencesWindow()
#endif
}
#endif
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
@@ -328,11 +355,16 @@ extension App: NSApplicationDelegate {
// symbolic hotkeys state persist after the app is quit; we restore this shortcut before quitting
setNativeCommandTabEnabled(true)
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
Logger.info { "" }
makeSureAllCapturesAreFinished()
return .terminateNow
}
}
enum RefreshCausedBy {
case showUi
case refreshOnlyThumbnailsAfterShowUi
case refreshUiAfterThumbnailsHaveBeenRefreshed
case refreshUiAfterExternalEvent
}
+1 -1
View File
@@ -35,7 +35,7 @@ final class DebugMenu: NSPanel {
while self.running {
var sample: [String: Double] = [:]
for queue in self.queues {
sample[queue.strongUnderlyingQueue.label] = Double(queue.operationCount)
sample[queue.strongUnderlyingQueue.label] = Double(queue.operationCount + queue.activeCallbacks)
}
DispatchQueue.main.async { self.graphView.addData(sample) }
Thread.sleep(forTimeInterval: 0.1)
+2 -2
View File
@@ -29,7 +29,7 @@ class FeedbackWindow: NSWindow {
private func setupView() {
let appIcon = LightImageView()
appIcon.updateWithResizedCopy(App.appIcon, NSSize(width: 80, height: 80))
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
appIcon.fit(80, 80)
let appText = StackView([
BoldLabel(NSLocalizedString("Share improvement ideas, or report bugs", comment: "")),
@@ -88,7 +88,7 @@ class FeedbackWindow: NSWindow {
func openTicket() {
URLSession.shared.dataTask(with: prepareRequest(), completionHandler: { data, response, error in
if error != nil || response == nil || (response as! HTTPURLResponse).statusCode != 201 {
Logger.error("HTTP call failed:", response, error, data.flatMap { String(data: $0, encoding: .utf8) })
Logger.error { "HTTP call failed. response:\(response) error:\(error) data:\(data.flatMap { String(data: $0, encoding: .utf8) })" }
}
}).resume()
issueTitle.stringValue = ""
+17 -6
View File
@@ -1,5 +1,6 @@
class MainMenu {
private static var mainMenu: NSMenu!
private static var menuItemsWithShortcut = [NSMenuItem: String]()
/// classic keyboard shortcuts like copy-and-paste are missing without a MainMenu. We generated the default menu from XCode and load it
/// see https://stackoverflow.com/a/3746058/2249756
@@ -8,16 +9,26 @@ class MainMenu {
Bundle.main.loadNibNamed("MainMenu", owner: self, topLevelObjects: &menuObjects)
mainMenu = menuObjects?.first { $0 is NSMenu } as? NSMenu
App.shared.mainMenu = mainMenu
rememberMenuItemsWithShortcut()
}
/// we toggle the mainMenu off when showing the main window, and on when showing secondary windows
static func toggle(enabled: Bool) {
if enabled {
if App.shared.mainMenu == nil {
App.shared.mainMenu = mainMenu
for (item, keyEquivalent) in menuItemsWithShortcut {
item.keyEquivalent = enabled ? keyEquivalent : ""
}
}
private static func rememberMenuItemsWithShortcut() {
var stack: [NSMenu] = [mainMenu]
while let menu = stack.popLast() {
for item in menu.items {
if !item.keyEquivalent.isEmpty {
menuItemsWithShortcut[item] = item.keyEquivalent
}
if let submenu = item.submenu {
stack.append(submenu)
}
}
} else {
App.shared.mainMenu = nil
}
}
}
+82 -24
View File
@@ -3,7 +3,8 @@ import Cocoa
/// this is a lightweight view which displays an image using its CALayer
/// it is an alternative to NSImageView, which doesn't have internal complexities and performance costs
class LightImageView: NSView {
var image: CGImage? { get { layer!.contents as! CGImage? } }
private var outlineLayer: CAShapeLayer!
private var handRaisedLayer: CALayer!
required init?(coder: NSCoder) {
fatalError("Class only supports programmatic initialization")
@@ -26,37 +27,94 @@ class LightImageView: NSView {
"position": NSNull(),
"contentsScale": NSNull()
]
layerContentsRedrawPolicy = .never
setupOutlineLayer()
setupHandRaisedLayer()
}
func updateWithResizedCopy(_ image: CGImage?, _ size: NSSize) {
let scaleFactor = NSScreen.preferred.backingScaleFactor
if let image {
// alternatively, we could set layer!.contentsGravity to .center, and use the lines bellow to resize ourselves
// let scaledSize = NSSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
// layer!.contents = image.resizedCopyWithCoreGraphics(scaledSize, fixBitmapInfo)
// it would produce subjectively better quality, but the resizing would be done on the CPU so poor performance
private func setupOutlineLayer() {
outlineLayer = CAShapeLayer()
outlineLayer.name = "outline"
outlineLayer.fillColor = nil
outlineLayer.lineWidth = 1.0
outlineLayer.lineDashPattern = [4, 3]
outlineLayer.lineCap = .round
outlineLayer.lineJoin = .round
outlineLayer.isHidden = true
layer!.addSublayer(outlineLayer)
}
private func setupHandRaisedLayer() {
guard #available(macOS 11.0, *) else { return }
handRaisedLayer = CALayer()
handRaisedLayer.contentsGravity = .resizeAspect
handRaisedLayer.isHidden = true
layer!.addSublayer(handRaisedLayer)
}
private func updateOutlineLayer(_ fullyTransparent: Bool) {
outlineLayer.isHidden = !fullyTransparent
guard fullyTransparent else { return }
let inset: CGFloat = 0.5
let radius: CGFloat = 8.0
let bounds = outlineLayer.superlayer!.bounds
outlineLayer.frame = bounds
let rect = bounds.insetBy(dx: inset, dy: inset)
let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
outlineLayer.path = path
outlineLayer.strokeColor = Appearance.fontColor.cgColor
}
private func updateHandRaisedLayer(_ fullyTransparent: Bool) {
guard #available(macOS 11.0, *) else { return }
handRaisedLayer.isHidden = !fullyTransparent
guard fullyTransparent else { return }
// we want the icon to be large but not too large, and readable at small sizes
let minSize = min(Appearance.iconSize, bounds.width * 0.9, bounds.height * 0.9)
let maxSize = Appearance.iconSize * 3
let ratio = min(bounds.width / ThumbnailView.maxThumbnailWidth(), bounds.height / ThumbnailView.maxThumbnailHeight())
let pointSize = max(minSize, maxSize * ratio)
let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: .regular)
guard let image = NSImage(systemSymbolName: "hand.raised.fill", accessibilityDescription: nil)?.withSymbolConfiguration(config) else { return }
handRaisedLayer.contents = image.tinted(Appearance.fontColor)
handRaisedLayer.bounds = CGRect(x: 0, y: 0, width: pointSize, height: pointSize)
handRaisedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
}
func updateContents(_ caLayerContents: CALayerContents, _ size: NSSize) {
var fullyTransparent = false
switch caLayerContents {
case .cgImage(let image?):
layer!.contents = image
layer!.contentsScale = scaleFactor
fullyTransparent = image.iFullyTransparent()
case .pixelBuffer(let pixelBuffer?):
layer!.contents = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue()
default: break
}
frame.size = size
}
/// this schedules the image update for the next cycle
/// The Panel will show with previous pictures, then they each will get updated quickly. It's a trade-off of getting AltTab interactive faster at the price of wrong data being shown
/// I feel the UX is better without it. I may reconsider
func updateWithResizedCopyAsync(_ image: CGImage?, _ size: NSSize) {
layer!.contentsScale = NSScreen.preferred.backingScaleFactor
frame.size = size
if let image {
// set image on another cycle so it doesn't block initial rendering
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.layer!.contents = image
}
if frame.size != size {
frame.size = size
}
updateOutlineLayer(fullyTransparent)
updateHandRaisedLayer(fullyTransparent)
}
func releaseImage() {
layer!.contents = nil
}
}
enum CALayerContents {
case cgImage(CGImage?)
case pixelBuffer(CVPixelBuffer?)
func size() -> NSSize? {
switch self {
case .cgImage(let image):
return image?.size()
case .pixelBuffer(let pixelBuffer):
return pixelBuffer?.size()
}
}
}
+18 -13
View File
@@ -27,20 +27,10 @@ class PreviewPanel: NSPanel {
setAccessibilitySubrole(.unknown)
}
func updateImageIfShowing(_ id: CGWindowID?, _ preview: CGImage, _ size: CGSize) {
if isVisible && id == currentId {
previewView.updateWithResizedCopy(preview, size)
}
}
func show(_ id: CGWindowID, _ preview: CGImage, _ position: CGPoint, _ size: CGSize) {
func show(_ id: CGWindowID, _ preview: CALayerContents, _ position: CGPoint, _ size: CGSize) {
if id != currentId {
previewView.updateWithResizedCopy(preview, size)
var frame = NSRect(origin: position, size: size)
// Flip Y coordinate from Quartz (0,0 at bottom-left) to Cocoa coordinates (0,0 at top-left)
// Always use the primary screen as reference since all coordinates are relative to it
frame.origin.y = NSScreen.screens.first!.frame.maxY - frame.maxY
setFrame(frame, display: false)
repositionAndResize(position, size)
previewView.updateContents(preview, size)
}
if id != currentId || !isVisible {
if Preferences.previewFadeInAnimation {
@@ -60,6 +50,21 @@ class PreviewPanel: NSPanel {
level = NSWindow.Level(rawValue: App.app.thumbnailsPanel.level.rawValue - 1)
}
}
func updateIfShowing(_ id: CGWindowID?, _ preview: CALayerContents, _ position: CGPoint, _ size: CGSize) {
if isVisible && id == currentId {
repositionAndResize(position, size)
previewView.updateContents(preview, size)
}
}
private func repositionAndResize( _ position: CGPoint, _ size: CGSize) {
var frame = NSRect(origin: position, size: size)
// Flip Y coordinate from Quartz (0,0 at bottom-left) to Cocoa coordinates (0,0 at top-left)
// Always use the primary screen as reference since all coordinates are relative to it
frame.origin.y = NSScreen.screens.first!.frame.maxY - frame.maxY
setFrame(frame, display: false)
}
}
private class BorderView: NSView {
+87 -90
View File
@@ -6,8 +6,6 @@ class ThumbnailView: FlippedView {
static let extraTextForPadding = "lmnopqrstuvw"
var window_: Window?
var thumbnail = LightImageView()
var windowlessIcon = LightImageView()
let thumbnailContainer = FlippedView()
var appIcon = LightImageView()
var label = ThumbnailTitleView(font: Appearance.font)
var fullscreenIcon = ThumbnailFontIconView(symbol: .circledPlusSign, tooltip: NSLocalizedString("Window is fullscreen", comment: ""))
@@ -64,7 +62,7 @@ class ThumbnailView: FlippedView {
dragAndDropTimer?.invalidate()
dragAndDropTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in
// the user can tab to focus the next thumbnail, while still dragging. We don't want to perform drag then
if Windows.focusedWindowIndex == Windows.hoveredWindowIndex {
if Windows.selectedWindowIndex == Windows.hoveredWindowIndex {
self.mouseUpCallback()
}
})
@@ -132,7 +130,7 @@ class ThumbnailView: FlippedView {
}
func drawHighlight() {
let isFocused = indexInRecycledViews == Windows.focusedWindowIndex
let isFocused = indexInRecycledViews == Windows.selectedWindowIndex
let isHovered = indexInRecycledViews == Windows.hoveredWindowIndex
setBackground(isFocused: isFocused, isHovered: isHovered)
setBorder(isFocused: isFocused, isHovered: isHovered)
@@ -143,21 +141,27 @@ class ThumbnailView: FlippedView {
}
func showOrHideWindowControls(_ shouldShowWindowControls: Bool) {
guard Preferences.appearanceStyle == .thumbnails else { return }
let shouldShow = shouldShowWindowControls && !Preferences.hideColoredCircles && !Appearance.hideThumbnails
if Preferences.appearanceStyle == .thumbnails {
isShowingWindowControls = shouldShow
for icon in windowControlIcons {
icon.isHidden = !shouldShow
|| (icon.type == .quit && !(window_?.application.canBeQuit() ?? true))
|| (icon.type == .close && !(window_?.canBeClosed() ?? true))
|| ((icon.type == .miniaturize || icon.type == .fullscreen) && !(window_?.canBeMinDeminOrFullscreened() ?? true))
// Force the icons to redraw; otherwise after clicking the fullscreen button,
// it will still appear to be in fullscreen mode.
icon.display()
guard isShowingWindowControls != shouldShow else { return }
isShowingWindowControls = shouldShow
CATransaction.begin()
CATransaction.setDisableActions(true)
for icon in windowControlIcons {
let shouldHide = !shouldShow
|| (icon.type == .quit && !(window_?.application.canBeQuit() ?? true))
|| (icon.type == .close && !(window_?.canBeClosed() ?? true))
|| ((icon.type == .miniaturize || icon.type == .fullscreen) && !(window_?.canBeMinDeminOrFullscreened() ?? true))
if icon.isHidden != shouldHide {
icon.isHidden = shouldHide
icon.needsDisplay = true
}
}
CATransaction.commit()
}
func updateDockLabelIcon(_ dockLabel: String?) {
assignIfDifferent(&dockLabelIcon.isHidden, dockLabel == nil || Preferences.hideAppBadges || Appearance.iconSize == 0)
if !dockLabelIcon.isHidden, let dockLabel {
@@ -183,14 +187,10 @@ class ThumbnailView: FlippedView {
private func setupSharedSubviews() {
let shadow = ThumbnailView.makeShadow(Appearance.imagesShadowColor)
thumbnailContainer.wantsLayer = true
thumbnailContainer.layer!.masksToBounds = false // let thumbnail shadows show
thumbnail.layer!.masksToBounds = false // let thumbnail shadows show
thumbnail.shadow = shadow
windowlessIcon.shadow = shadow
appIcon.shadow = shadow
dockLabelIcon.shadow = shadow
windowlessIcon.translatesAutoresizingMaskIntoConstraints = false
windowlessIcon.toolTip = ThumbnailView.noOpenWindowToolTip
appIcon.translatesAutoresizingMaskIntoConstraints = false
appIcon.setSubviewAbove(dockLabelIcon)
label.fixHeight()
@@ -211,11 +211,10 @@ class ThumbnailView: FlippedView {
label.alignment = .center
label.isHidden = true
} else if Preferences.appearanceStyle == .thumbnails {
vStackView.addSubviews([thumbnailContainer])
thumbnailContainer.setSubviews([thumbnail, windowlessIcon])
thumbnailContainer.setSubviewAbove(windowlessAppIndicator)
vStackView.addSubviews([thumbnail])
thumbnail.setSubviewAbove(windowlessAppIndicator)
for icon in windowControlIcons {
thumbnailContainer.setSubviewAbove(icon)
thumbnail.setSubviewAbove(icon)
icon.isHidden = true
}
hStackView.addSubviews([label] + windowIndicatorIcons)
@@ -253,7 +252,7 @@ class ThumbnailView: FlippedView {
}
private func updateAppIconsLabel(isFocused: Bool, isHovered: Bool) {
let focusedView = ThumbnailsView.recycledViews[Windows.focusedWindowIndex]
let focusedView = ThumbnailsView.recycledViews[Windows.selectedWindowIndex]
var hoveredView: ThumbnailView? = nil
if Windows.hoveredWindowIndex != nil {
hoveredView = ThumbnailsView.recycledViews[Windows.hoveredWindowIndex!]
@@ -261,42 +260,47 @@ class ThumbnailView: FlippedView {
if isFocused || (!isFocused && !isHovered) {
hoveredView?.label.isHidden = true
focusedView.label.isHidden = false
updateAppIconsLabelFrame(focusedView)
focusedView.updateAppIconsLabelFrame()
} else if isHovered {
hoveredView?.label.isHidden = false
focusedView.label.isHidden = true
if let hoveredView {
updateAppIconsLabelFrame(hoveredView)
hoveredView.updateAppIconsLabelFrame()
}
}
}
private func getMaxAllowedLabelWidth(_ view: ThumbnailView) -> CGFloat {
let viewWidth = view.frame.width
private func getMaxAllowedLabelWidth() -> CGFloat {
let viewWidth = frame.width
let maxAllowedWidth = min(viewWidth * 2, ThumbnailsView.thumbnailsWidth)
let availableLeftWidth = view.isFirstInRow ? 0 : CGFloat(view.indexInRow) * viewWidth
let availableRightWidth = view.isLastInRow ? 0 : CGFloat(view.numberOfViewsInRow - 1 - view.indexInRow) * viewWidth
let availableLeftWidth = isFirstInRow ? 0 : CGFloat(indexInRow) * viewWidth
let availableRightWidth = isLastInRow ? 0 : CGFloat(numberOfViewsInRow - 1 - indexInRow) * viewWidth
let totalWidth = availableLeftWidth + availableRightWidth + viewWidth
let maxLabelWidth = min(totalWidth, maxAllowedWidth)
return maxLabelWidth - Appearance.intraCellPadding * 2
}
private func updateAppIconsLabelFrame(_ view: ThumbnailView) {
let viewWidth = view.frame.width
let labelWidth = view.label.cell!.cellSize.width
let maxAllowedLabelWidth = getMaxAllowedLabelWidth(view)
let effectiveLabelWidth = max(min(labelWidth, maxAllowedLabelWidth), viewWidth)
- (view.isFirstInRow && view.isLastInRow ? 4 : (view.isFirstInRow || view.isLastInRow ? 2 : 0)) * Appearance.intraCellPadding
var leftOffset = -Appearance.intraCellPadding * 2
if view.isFirstInRow && view.isLastInRow {
leftOffset = -Appearance.intraCellPadding * 2
} else if view.isLastInRow {
leftOffset = max(0, effectiveLabelWidth - viewWidth + Appearance.intraCellPadding * 2)
} else if !view.isFirstInRow && !view.isLastInRow {
private func updateAppIconsLabelFrame() {
let viewWidth = frame.width
let labelWidth = label.cell!.cellSize.width
let padding = (Preferences.appearanceSize == .small ? 0 : ( Preferences.appearanceSize == .medium ? 1 : 2)) * Appearance.intraCellPadding
let maxAllowedLabelWidth = getMaxAllowedLabelWidth()
let sidesToOffset: CGFloat = (isFirstInRow ? 1 : 0) + (isLastInRow ? 1 : 0)
let paddingForOffset = sidesToOffset * padding
var effectiveLabelWidth = max(min(labelWidth, maxAllowedLabelWidth), viewWidth) - paddingForOffset
// if the label is small, and with an offset only on one side, we reduce its width to center its text
if sidesToOffset == 1 && labelWidth <= (effectiveLabelWidth - paddingForOffset) {
effectiveLabelWidth -= paddingForOffset
}
var leftOffset = CGFloat(0)
if isFirstInRow {
leftOffset = -padding
} else if isLastInRow {
leftOffset = effectiveLabelWidth - viewWidth + padding
} else {
let halfNeededOffset = max(0, (effectiveLabelWidth - viewWidth) / 2)
let availableLeftWidth = view.isFirstInRow ? 0 : CGFloat(view.indexInRow) * viewWidth
let availableRightWidth = view.isLastInRow ? 0 : CGFloat(view.numberOfViewsInRow - 1 - view.indexInRow) * viewWidth
let availableLeftWidth = isFirstInRow ? 0 : CGFloat(indexInRow) * viewWidth
let availableRightWidth = isLastInRow ? 0 : CGFloat(numberOfViewsInRow - 1 - indexInRow) * viewWidth
if availableLeftWidth >= halfNeededOffset && availableRightWidth >= halfNeededOffset {
leftOffset = halfNeededOffset
} else if availableLeftWidth <= halfNeededOffset && availableRightWidth <= halfNeededOffset {
@@ -308,22 +312,20 @@ class ThumbnailView: FlippedView {
}
}
let xPosition = -leftOffset
let height = view.label.fittingSize.height
let yPosition = view.hStackView.frame.origin.y + view.hStackView.frame.height + Appearance.intraCellPadding * 2
view.label.frame = NSRect(x: xPosition, y: yPosition, width: effectiveLabelWidth, height: height)
view.label.setWidth(effectiveLabelWidth)
view.label.toolTip = view.label.cell!.cellSize.width >= view.label.frame.size.width ? view.label.stringValue : nil
let height = label.fittingSize.height
let yPosition = hStackView.frame.origin.y + hStackView.frame.height + Appearance.intraCellPadding * 2
label.frame = NSRect(x: xPosition, y: yPosition, width: effectiveLabelWidth, height: height)
label.setWidth(effectiveLabelWidth)
label.toolTip = label.cell!.cellSize.width >= label.frame.size.width ? label.stringValue : nil
}
private func updateAppIcon(_ element: Window, _ title: String) {
let appIconSize = ThumbnailView.iconSize()
appIcon.updateWithResizedCopy(element.icon, appIconSize)
appIcon.updateContents(.cgImage(element.icon), appIconSize)
appIcon.setAccessibilityLabel(title)
}
private func updateValues(_ element: Window, _ index: Int, _ newHeight: CGFloat) {
assignIfDifferent(&thumbnail.isHidden, Appearance.hideThumbnails || element.isWindowlessApp)
assignIfDifferent(&windowlessIcon.isHidden, !element.isWindowlessApp || Appearance.hideThumbnails)
assignIfDifferent(&windowlessAppIndicator.isHidden, !element.isWindowlessApp)
assignIfDifferent(&hiddenIcon.isHidden, !element.isHidden || Preferences.hideStatusIcons)
assignIfDifferent(&fullscreenIcon.isHidden, !element.isFullscreen || Preferences.hideStatusIcons)
@@ -333,14 +335,15 @@ class ThumbnailView: FlippedView {
NSScreen.screens.count < 2 || Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab
)
))
thumbnail.toolTip = element.isWindowlessApp ? ThumbnailView.noOpenWindowToolTip : nil
if !thumbnail.isHidden {
if let screenshot = element.thumbnail {
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot, false)
thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
let thumbnailSize = ThumbnailView.thumbnailSize(element.size, false)
thumbnail.updateContents(screenshot, thumbnailSize)
} else {
// if no thumbnail, show appIcon instead
let thumbnailSize = ThumbnailView.thumbnailSize(element.icon, true)
thumbnail.updateWithResizedCopy(element.icon, thumbnailSize)
let thumbnailSize = ThumbnailView.thumbnailSize(element.icon?.size(), true)
thumbnail.updateContents(.cgImage(element.icon), thumbnailSize)
}
// for Accessibility > "speak items under the pointer"
thumbnail.setAccessibilityLabel(element.title)
@@ -365,14 +368,10 @@ class ThumbnailView: FlippedView {
updateAppIcon(element, title)
updateDockLabelIcon(element.dockLabel)
setAccessibilityHelp(getAccessibilityHelp(element.application.localizedName, element.dockLabel))
if !windowlessIcon.isHidden {
let windowlessIconSize = ThumbnailView.thumbnailSize(element.icon, true)
windowlessIcon.updateWithResizedCopy(element.icon, windowlessIconSize)
}
windowControlIcons.forEach { $0.window_ = element }
showOrHideWindowControls(isShowingWindowControls)
mouseUpCallback = { () -> Void in App.app.focusSelectedWindow(element) }
mouseMovedCallback = { () -> Void in Windows.updateFocusedAndHoveredWindowIndex(index, true) }
mouseMovedCallback = { () -> Void in Windows.updateSelectedAndHoveredWindowIndex(index, true) }
}
private func updateSizes(_ newHeight: CGFloat) {
@@ -386,10 +385,6 @@ class ThumbnailView: FlippedView {
let labelWidth = hStackView.frame.width - appIcon.frame.width - Appearance.appIconLabelSpacing - indicatorsSpace()
label.setWidth(labelWidth)
}
if Preferences.appearanceStyle == .thumbnails {
let content = !windowlessIcon.isHidden ? windowlessIcon : thumbnail
thumbnailContainer.frame.size = NSSize(width: hStackView.frame.width, height: content.frame.height)
}
}
private func updatePositions(_ newHeight: CGFloat) {
@@ -416,17 +411,16 @@ class ThumbnailView: FlippedView {
label.centerFrameInParent(y: true)
}
if Preferences.appearanceStyle == .thumbnails {
let content = !windowlessIcon.isHidden ? windowlessIcon : thumbnail
thumbnailContainer.frame.origin = NSPoint(x: Appearance.edgeInsetsSize, y: hStackView.frame.maxY + Appearance.intraCellPadding)
content.centerFrameInParent(x: true)
thumbnail.frame.origin = NSPoint(x: Appearance.edgeInsetsSize, y: hStackView.frame.maxY + Appearance.intraCellPadding)
thumbnail.centerFrameInParent(x: true)
var xOffset = CGFloat(3)
var yOffset = CGFloat(2)
var yOffset = thumbnail.frame.height - CGFloat(2)
for icon in windowControlIcons {
icon.frame.origin = NSPoint(x: xOffset, y: yOffset)
icon.frame.origin = NSPoint(x: xOffset, y: yOffset - TrafficLightButton.size)
xOffset += TrafficLightButton.size + TrafficLightButton.spacing
if xOffset + TrafficLightButton.size > thumbnailContainer.frame.width {
xOffset = 3 + Appearance.edgeInsetsSize
yOffset += TrafficLightButton.size + TrafficLightButton.spacing
if xOffset + TrafficLightButton.size > thumbnail.frame.width {
xOffset = 3
yOffset -= TrafficLightButton.size + TrafficLightButton.spacing
}
}
}
@@ -437,7 +431,11 @@ class ThumbnailView: FlippedView {
windowlessAppIndicator.frame.origin.x = ((appIcon.frame.width / 2) - (windowlessAppIndicator.frame.width / 2)).rounded()
+ (App.shared.userInterfaceLayoutDirection == .leftToRight ? 0 : appIcon.frame.origin.x)
}
windowlessAppIndicator.frame.origin.y = windowlessAppIndicator.superview!.frame.height - windowlessAppIndicator.frame.height + 5
if Preferences.appearanceStyle != .thumbnails {
windowlessAppIndicator.frame.origin.y = windowlessAppIndicator.superview!.frame.height - windowlessAppIndicator.frame.height + 5
} else {
windowlessAppIndicator.frame.origin.y = -5
}
}
// we set dockLabelIcon origin, without checking if .isHidden
// This is because its updated async. We need it positioned correctly always
@@ -492,7 +490,7 @@ class ThumbnailView: FlippedView {
var contentWidth = CGFloat(0)
if Preferences.appearanceStyle == .thumbnails {
// Preferred to the width of the image, and the minimum width may be set to be large.
contentWidth = (!windowlessIcon.isHidden ? windowlessIcon : thumbnail).frame.size.width
contentWidth = thumbnail.frame.size.width
} else if Preferences.appearanceStyle == .titles {
contentWidth = ThumbnailView.maxThumbnailWidth() - Appearance.edgeInsetsSize * 2
} else {
@@ -522,7 +520,6 @@ class ThumbnailView: FlippedView {
// NSImageView instances are registered to drag-and-drop by default
thumbnail.unregisterDraggedTypes()
appIcon.unregisterDraggedTypes()
windowlessIcon.unregisterDraggedTypes()
// we only handle URLs (i.e. not text, image, or other draggable things)
registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeURL as String)])
}
@@ -544,8 +541,8 @@ class ThumbnailView: FlippedView {
return shadow
}
static func maxThumbnailWidth() -> CGFloat {
return ThumbnailsPanel.maxThumbnailsWidth() * Appearance.windowMaxWidthInRow - Appearance.interCellPadding * 2
static func maxThumbnailWidth(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
return ThumbnailsPanel.maxThumbnailsWidth(screen) * Appearance.windowMaxWidthInRow - Appearance.interCellPadding * 2
}
static func widthOfComfortableReadability() -> CGFloat? {
@@ -569,21 +566,21 @@ class ThumbnailView: FlippedView {
return maxWidth
}
static func minThumbnailWidth() -> CGFloat {
return ThumbnailsPanel.maxThumbnailsWidth() * Appearance.windowMinWidthInRow - Appearance.interCellPadding * 2
static func minThumbnailWidth(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
return ThumbnailsPanel.maxThumbnailsWidth(screen) * Appearance.windowMinWidthInRow - Appearance.interCellPadding * 2
}
/// The maximum height that a thumbnail can be drawn
/// maxThumbnailsHeight = maxThumbnailHeight * rowCount + interCellPadding * (rowCount - 1)
/// maxThumbnailHeight = (maxThumbnailsHeight - interCellPadding * (rowCount - 1)) / rowCount
static func maxThumbnailHeight() -> CGFloat {
return ((ThumbnailsPanel.maxThumbnailsHeight() - Appearance.interCellPadding) / Appearance.rowsCount - Appearance.interCellPadding).rounded()
static func maxThumbnailHeight(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
return ((ThumbnailsPanel.maxThumbnailsHeight(screen) - Appearance.interCellPadding) / Appearance.rowsCount - Appearance.interCellPadding).rounded()
}
static func thumbnailSize(_ image: CGImage?, _ isWindowlessApp: Bool) -> NSSize {
guard let image else { return NSSize(width: 0, height: 0) }
let imageWidth = CGFloat(image.width)
let imageHeight = CGFloat(image.height)
static func thumbnailSize(_ imageSize: NSSize?, _ isWindowlessApp: Bool) -> NSSize {
guard let imageSize else { return NSSize(width: 0, height: 0) }
let imageWidth = imageSize.width
let imageHeight = imageSize.height
let thumbnailHeightMax = ThumbnailView.maxThumbnailHeight()
- Appearance.edgeInsetsSize * 2
- Appearance.intraCellPadding
@@ -592,7 +589,7 @@ class ThumbnailView: FlippedView {
- Appearance.edgeInsetsSize * 2
// don't stretch very small windows; keep them 1:1 in the switcher
if !isWindowlessApp && imageWidth < thumbnailWidthMax && imageHeight < thumbnailHeightMax {
return NSSize(width: imageWidth, height: imageHeight)
return imageSize
}
let thumbnailHeight = min(imageHeight, thumbnailHeightMax)
let thumbnailWidth = min(imageWidth, thumbnailWidthMax)
@@ -616,9 +613,9 @@ class ThumbnailView: FlippedView {
return NSSize(width: width.rounded(), height: height.rounded())
}
static func iconSize() -> NSSize {
static func iconSize(_ screen: NSScreen = NSScreen.preferred) -> NSSize {
if Preferences.appearanceStyle == .appIcons {
let widthMin = ThumbnailView.minThumbnailWidth()
let widthMin = ThumbnailView.minThumbnailWidth(screen)
let contentWidth = Appearance.iconSize
let frameWidth = contentWidth + Appearance.edgeInsetsSize * 2
let width = max(frameWidth, widthMin).rounded()
+41 -17
View File
@@ -3,7 +3,8 @@ import Cocoa
class ThumbnailsPanel: NSPanel {
var thumbnailsView = ThumbnailsView()
override var canBecomeKey: Bool { true }
private var didDisplayOnce = false
static var maxPossibleThumbnailSize = NSSize.zero
static var maxPossibleAppIconSize = NSSize.zero
convenience init() {
self.init(contentRect: .zero, styleMask: .nonactivatingPanel, backing: .buffered, defer: false)
@@ -43,7 +44,6 @@ class ThumbnailsPanel: NSPanel {
}
override func orderOut(_ sender: Any?) {
didDisplayOnce = false
if Preferences.fadeOutAnimation {
NSAnimationContext.runAnimationGroup(
{ _ in animator().alphaValue = 0 },
@@ -54,40 +54,54 @@ class ThumbnailsPanel: NSPanel {
}
}
override func displayIfNeeded() {
super.displayIfNeeded()
if !didDisplayOnce {
didDisplayOnce = true
DispatchQueue.main.async {
Applications.manuallyRefreshAllWindows()
}
}
}
func show() {
updateAppearance()
alphaValue = 1
makeKeyAndOrderFront(nil)
MouseEvents.toggle(true)
CursorEvents.toggle(true)
thumbnailsView.scrollView.flashScrollers()
}
static func maxThumbnailsWidth() -> CGFloat {
static func maxThumbnailsWidth(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
if Preferences.appearanceStyle == .titles,
let readableWidth = ThumbnailView.widthOfComfortableReadability() {
return (
min(
NSScreen.preferred.frame.width * Appearance.maxWidthOnScreen,
screen.frame.width * Appearance.maxWidthOnScreen,
readableWidth + Appearance.intraCellPadding * 2 + Appearance.appIconLabelSpacing + Appearance.iconSize
// widthOfLongestTitle + Appearance.intraCellPadding * 2 + Appearance.appIconLabelSpacing + Appearance.iconSize
) - Appearance.windowPadding * 2
).rounded()
}
return (NSScreen.preferred.frame.width * Appearance.maxWidthOnScreen - Appearance.windowPadding * 2).rounded()
return (screen.frame.width * Appearance.maxWidthOnScreen - Appearance.windowPadding * 2).rounded()
}
static func maxThumbnailsHeight() -> CGFloat {
return (NSScreen.preferred.frame.height * Appearance.maxHeightOnScreen - Appearance.windowPadding * 2).rounded()
static func maxThumbnailsHeight(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
return (screen.frame.height * Appearance.maxHeightOnScreen - Appearance.windowPadding * 2).rounded()
}
static func updateMaxPossibleThumbnailSize() {
let (w, h) = NSScreen.screens.reduce((CGFloat.zero, CGFloat.zero)) { acc, screen in
(max(acc.0, ThumbnailView.maxThumbnailWidth(screen) * screen.backingScaleFactor),
max(acc.1, ThumbnailView.maxThumbnailHeight(screen) * screen.backingScaleFactor))
}
maxPossibleThumbnailSize = NSSize(width: w.rounded(), height: h.rounded())
}
static func updateMaxPossibleAppIconSize() {
let (w, h) = NSScreen.screens.reduce((CGFloat.zero, CGFloat.zero)) { acc, screen in
// in Thumbnails Appearance, AppIcons can be used for windowless apps, thus much bigger than the app icon near the title
if Preferences.appearanceStyle == .thumbnails {
return (max(acc.0, ThumbnailView.maxThumbnailWidth(screen) * screen.backingScaleFactor),
max(acc.1, ThumbnailView.maxThumbnailHeight(screen) * screen.backingScaleFactor))
} else {
let size = ThumbnailView.iconSize(screen)
return (max(acc.0, size.width * screen.backingScaleFactor),
max(acc.1, size.height * screen.backingScaleFactor))
}
}
maxPossibleAppIconSize = NSSize(width: w.rounded(), height: h.rounded())
}
}
@@ -99,6 +113,16 @@ extension ThumbnailsPanel: NSWindowDelegate {
if App.app.appIsBeingUsed {
App.app.thumbnailsPanel.makeKeyAndOrderFront(nil)
}
MainMenu.toggle(enabled: true)
}
}
func windowDidBecomeKey(_ notification: Notification) {
// we toggle the mainMenu off when showing the main window
// this avoids command+q from quitting AltTab itself, or command+p from printing
DispatchQueue.main.async {
MainMenu.toggle(enabled: false)
Applications.manuallyRefreshAllWindows()
}
}
}
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff. Show more