commit be59936601ff12b966d1432609bf7fec0a275634 Author: Louis Pontoise Date: Tue Aug 27 19:46:53 2019 +0900 feat: mvp diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..89e235e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +xcuserdata/ diff --git a/alt-tab-macos.xcodeproj/project.pbxproj b/alt-tab-macos.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4cbeb351 --- /dev/null +++ b/alt-tab-macos.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D04BA0219AC4C1B8444B469F /* 4 windows - 1 line.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA996DB03B329C6B36A39 /* 4 windows - 1 line.jpg */; }; + D04BA10F27D14F5B90AA1910 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7F8D6FBE1C6FDA7BC72 /* Application.swift */; }; + D04BA1BEB40295C4DC25F637 /* 1 window - 1 line.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA58701B5A47B6A2953E1 /* 1 window - 1 line.jpg */; }; + D04BA235E34B5120E2F75F34 /* 3 windows - 1 line.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA04504BB9802F6E0B889 /* 3 windows - 1 line.jpg */; }; + D04BA2FB9D4E2CE6E0B49F10 /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAECA9B67E89686393364 /* WindowManager.swift */; }; + D04BA3999A3D6A308EA41039 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = D04BAD40CE2D3A8AAC3819D0 /* .gitignore */; }; + D04BA486DF30D7169D4B69BA /* 3 windows - 1 line - wide window.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA8298AF4D8ED7827ACDC /* 3 windows - 1 line - wide window.jpg */; }; + D04BA4D8D74841AEC2601C80 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D04BA262E9A663B3A85A2A95 /* Assets.xcassets */; }; + D04BA5CB7B1B84BDC99E5C3C /* 7 windows - 2 lines - tall window.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA041E420F065EF8DC9F4 /* 7 windows - 2 lines - tall window.jpg */; }; + D04BA5F23B9618C2C526DA1E /* 5 windows - 1 line.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA4AF59F978749D697493 /* 5 windows - 1 line.jpg */; }; + D04BA7A29152DDF0BDCAC4AF /* 7 windows - 2 lines - wide window.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BADF2A78C91437E1E4C42 /* 7 windows - 2 lines - wide window.jpg */; }; + D04BA8877A8149B1439B1F04 /* 5 windows - 2 lines.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA5349996324809EA7208 /* 5 windows - 2 lines.jpg */; }; + D04BA958877CFEC80F5E883A /* 2 windows - 1 line.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BADB4ABF73F979B9FC0BA /* 2 windows - 1 line.jpg */; }; + D04BA960DDD1D32A3019C835 /* UiSubclasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3202A2C22C347E849B3 /* UiSubclasses.swift */; }; + D04BAAACE304CA2BCB1DC1BE /* too many windows - 4 lines - paginated.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BAC503A122CC0068BB84E /* too many windows - 4 lines - paginated.jpg */; }; + D04BAAB78E71A923B63A9DFF /* 14 windows - 3 lines.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA75BFFC94D1E33047688 /* 14 windows - 3 lines.jpg */; }; + D04BAB37A8033F8D4309889F /* 3 windows - 1 line - tall window.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA685BCDB60B9FEA57438 /* 3 windows - 1 line - tall window.jpg */; }; + D04BAB6A446F35FA221E1831 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA0E21840B63A2BC23253 /* Keyboard.swift */; }; + D04BAB8555DC582A5BA5F8EE /* 6 windows - 1 line.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA9EBCA307524DBE48DCF /* 6 windows - 1 line.jpg */; }; + D04BAE3835B9FBA218664C74 /* 7 windows - 2 lines - extra wide window.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D04BA9B41D7EA9F1B6290C91 /* 7 windows - 2 lines - extra wide window.jpg */; }; + D04BAE5CC40654B1F96E8DE9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D04BA7B6AAB0812631BBC7A2 /* Info.plist */; }; + D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D04BA041E420F065EF8DC9F4 /* 7 windows - 2 lines - tall window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - tall window.jpg"; sourceTree = ""; }; + D04BA04504BB9802F6E0B889 /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = ""; }; + D04BA0E21840B63A2BC23253 /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; + D04BA262E9A663B3A85A2A95 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D04BA3202A2C22C347E849B3 /* UiSubclasses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiSubclasses.swift; sourceTree = ""; }; + D04BA4AF59F978749D697493 /* 5 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 1 line.jpg"; sourceTree = ""; }; + D04BA5349996324809EA7208 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = ""; }; + D04BA58701B5A47B6A2953E1 /* 1 window - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1 window - 1 line.jpg"; sourceTree = ""; }; + D04BA685BCDB60B9FEA57438 /* 3 windows - 1 line - tall window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - tall window.jpg"; sourceTree = ""; }; + D04BA75BFFC94D1E33047688 /* 14 windows - 3 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "14 windows - 3 lines.jpg"; sourceTree = ""; }; + D04BA7B6AAB0812631BBC7A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + D04BA7F8D6FBE1C6FDA7BC72 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + D04BA8298AF4D8ED7827ACDC /* 3 windows - 1 line - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - wide window.jpg"; sourceTree = ""; }; + D04BA82F792DF53958D92572 /* alt-tab-macos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "alt-tab-macos.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D04BA996DB03B329C6B36A39 /* 4 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "4 windows - 1 line.jpg"; sourceTree = ""; }; + D04BA9B41D7EA9F1B6290C91 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = ""; }; + D04BA9EBCA307524DBE48DCF /* 6 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "6 windows - 1 line.jpg"; sourceTree = ""; }; + D04BAA44C837F3A67403B9DB /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + D04BAC503A122CC0068BB84E /* too many windows - 4 lines - paginated.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "too many windows - 4 lines - paginated.jpg"; sourceTree = ""; }; + D04BAD1C9F215BCCD3B620AC /* alt_tab_macos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = alt_tab_macos.entitlements; sourceTree = ""; }; + D04BAD40CE2D3A8AAC3819D0 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = file.gitignore; path = .gitignore; sourceTree = ""; }; + D04BADB4ABF73F979B9FC0BA /* 2 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2 windows - 1 line.jpg"; sourceTree = ""; }; + D04BADF2A78C91437E1E4C42 /* 7 windows - 2 lines - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - wide window.jpg"; sourceTree = ""; }; + D04BAECA9B67E89686393364 /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D04BA82F32FB183F65DC3E42 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D04BA036CA8BDCF4608909C4 /* docs */ = { + isa = PBXGroup; + children = ( + D04BA9280DD11C32934568FC /* img */, + ); + path = docs; + sourceTree = ""; + }; + D04BA1463D2A17038222BB84 = { + isa = PBXGroup; + children = ( + D04BA1A461BBB5A9BE15E015 /* Products */, + D04BAD48B702C4B1DC543557 /* alt-tab-macos */, + D04BAD40CE2D3A8AAC3819D0 /* .gitignore */, + ); + sourceTree = ""; + }; + D04BA1A461BBB5A9BE15E015 /* Products */ = { + isa = PBXGroup; + children = ( + D04BA82F792DF53958D92572 /* alt-tab-macos.app */, + ); + name = Products; + sourceTree = ""; + }; + D04BA9280DD11C32934568FC /* img */ = { + isa = PBXGroup; + children = ( + D04BA58701B5A47B6A2953E1 /* 1 window - 1 line.jpg */, + D04BADB4ABF73F979B9FC0BA /* 2 windows - 1 line.jpg */, + D04BA04504BB9802F6E0B889 /* 3 windows - 1 line.jpg */, + D04BA996DB03B329C6B36A39 /* 4 windows - 1 line.jpg */, + D04BA4AF59F978749D697493 /* 5 windows - 1 line.jpg */, + D04BA9EBCA307524DBE48DCF /* 6 windows - 1 line.jpg */, + D04BA5349996324809EA7208 /* 5 windows - 2 lines.jpg */, + D04BA75BFFC94D1E33047688 /* 14 windows - 3 lines.jpg */, + D04BA685BCDB60B9FEA57438 /* 3 windows - 1 line - tall window.jpg */, + D04BA8298AF4D8ED7827ACDC /* 3 windows - 1 line - wide window.jpg */, + D04BA041E420F065EF8DC9F4 /* 7 windows - 2 lines - tall window.jpg */, + D04BADF2A78C91437E1E4C42 /* 7 windows - 2 lines - wide window.jpg */, + D04BAC503A122CC0068BB84E /* too many windows - 4 lines - paginated.jpg */, + D04BA9B41D7EA9F1B6290C91 /* 7 windows - 2 lines - extra wide window.jpg */, + ); + path = img; + sourceTree = ""; + }; + D04BAAA28315301A906DD201 /* ui */ = { + isa = PBXGroup; + children = ( + D04BA3202A2C22C347E849B3 /* UiSubclasses.swift */, + ); + path = ui; + sourceTree = ""; + }; + D04BAD48B702C4B1DC543557 /* alt-tab-macos */ = { + isa = PBXGroup; + children = ( + D04BA262E9A663B3A85A2A95 /* Assets.xcassets */, + D04BAD1C9F215BCCD3B620AC /* alt_tab_macos.entitlements */, + D04BAAA28315301A906DD201 /* ui */, + D04BA7F8D6FBE1C6FDA7BC72 /* Application.swift */, + D04BA0E21840B63A2BC23253 /* Keyboard.swift */, + D04BAECA9B67E89686393364 /* WindowManager.swift */, + D04BA7B6AAB0812631BBC7A2 /* Info.plist */, + D04BAA44C837F3A67403B9DB /* main.swift */, + D04BA036CA8BDCF4608909C4 /* docs */, + ); + path = "alt-tab-macos"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D04BA5E73DAF232ADE11C97A /* alt-tab-macos */ = { + isa = PBXNativeTarget; + buildConfigurationList = D04BA4D71CBB2FA4B9947B10 /* Build configuration list for PBXNativeTarget "alt-tab-macos" */; + buildPhases = ( + D04BAD01F4BCEDF8B539AFD2 /* Sources */, + D04BA82F32FB183F65DC3E42 /* Frameworks */, + D04BA96F3DC99263120BCD21 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "alt-tab-macos"; + productName = "alt-tab-macos"; + productReference = D04BA82F792DF53958D92572 /* alt-tab-macos.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D04BA59EB5ED6F005E8F4407 /* Project object */ = { + isa = PBXProject; + attributes = { + ORGANIZATIONNAME = lwouis; + }; + buildConfigurationList = D04BA83C9495F252D87CDF0F /* Build configuration list for PBXProject "alt-tab-macos" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D04BA1463D2A17038222BB84; + productRefGroup = D04BA1A461BBB5A9BE15E015 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D04BA5E73DAF232ADE11C97A /* alt-tab-macos */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D04BA96F3DC99263120BCD21 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D04BA4D8D74841AEC2601C80 /* Assets.xcassets in Resources */, + D04BAE5CC40654B1F96E8DE9 /* Info.plist in Resources */, + D04BA1BEB40295C4DC25F637 /* 1 window - 1 line.jpg in Resources */, + D04BA958877CFEC80F5E883A /* 2 windows - 1 line.jpg in Resources */, + D04BA235E34B5120E2F75F34 /* 3 windows - 1 line.jpg in Resources */, + D04BA0219AC4C1B8444B469F /* 4 windows - 1 line.jpg in Resources */, + D04BA5F23B9618C2C526DA1E /* 5 windows - 1 line.jpg in Resources */, + D04BAB8555DC582A5BA5F8EE /* 6 windows - 1 line.jpg in Resources */, + D04BA8877A8149B1439B1F04 /* 5 windows - 2 lines.jpg in Resources */, + D04BAAB78E71A923B63A9DFF /* 14 windows - 3 lines.jpg in Resources */, + D04BAB37A8033F8D4309889F /* 3 windows - 1 line - tall window.jpg in Resources */, + D04BA486DF30D7169D4B69BA /* 3 windows - 1 line - wide window.jpg in Resources */, + D04BA5CB7B1B84BDC99E5C3C /* 7 windows - 2 lines - tall window.jpg in Resources */, + D04BA7A29152DDF0BDCAC4AF /* 7 windows - 2 lines - wide window.jpg in Resources */, + D04BAAACE304CA2BCB1DC1BE /* too many windows - 4 lines - paginated.jpg in Resources */, + D04BAE3835B9FBA218664C74 /* 7 windows - 2 lines - extra wide window.jpg in Resources */, + D04BA3999A3D6A308EA41039 /* .gitignore in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D04BAD01F4BCEDF8B539AFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D04BA960DDD1D32A3019C835 /* UiSubclasses.swift in Sources */, + D04BA10F27D14F5B90AA1910 /* Application.swift in Sources */, + D04BAB6A446F35FA221E1831 /* Keyboard.swift in Sources */, + D04BA2FB9D4E2CE6E0B49F10 /* WindowManager.swift in Sources */, + D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D04BA49BCED00029C5289244 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "alt-tab-macos/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + }; + name = Release; + }; + D04BA6FB4EC72C6A126E86D7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "alt-tab-macos/alt_tab_macos.entitlements"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "alt-tab-macos/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + D04BA801581D700C583B53FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D04BA9150F87B08AB0ACE4AE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D04BA4D71CBB2FA4B9947B10 /* Build configuration list for PBXNativeTarget "alt-tab-macos" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D04BA6FB4EC72C6A126E86D7 /* Debug */, + D04BA49BCED00029C5289244 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + D04BA83C9495F252D87CDF0F /* Build configuration list for PBXProject "alt-tab-macos" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D04BA801581D700C583B53FB /* Debug */, + D04BA9150F87B08AB0ACE4AE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D04BA59EB5ED6F005E8F4407 /* Project object */; +} diff --git a/alt-tab-macos.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/alt-tab-macos.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/alt-tab-macos.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/alt-tab-macos/Application.swift b/alt-tab-macos/Application.swift new file mode 100644 index 00000000..bf4eec63 --- /dev/null +++ b/alt-tab-macos/Application.swift @@ -0,0 +1,227 @@ +import Cocoa +import Foundation + +let maxScreenUsage: CGFloat = 0.8 +let windowPadding: CGFloat = 20 +let cellPadding: CGFloat = 6 +let cellBorderWidth: CGFloat = 2 +let maxThumbnailsPerRow: CGFloat = 4 +var thumbnailMaxWidth: CGFloat = 0 +var thumbnailMaxHeight: CGFloat = 0 +let iconSize: CGFloat = 32 +let fontHeight: CGFloat = 15 +let font: NSFont = .systemFont(ofSize: fontHeight) +let interItemPadding: CGFloat = 4 +let tabKey = 48 +let metaKey = 0x3B +let metaModifierFlag: NSEvent.ModifierFlags = .control +let cellId = NSUserInterfaceItemIdentifier("Cell") +let highlightColor: NSColor = .white + +func updateThumbnailMaxSize() -> Void { + let main = NSScreen.main!.frame + thumbnailMaxWidth = (NSScreen.main!.frame.size.width * maxScreenUsage - windowPadding * 2) / maxThumbnailsPerRow - interItemPadding + thumbnailMaxHeight = thumbnailMaxWidth * (main.height / main.width) +} + +class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate, NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout { + var item: NSStatusItem? + var backgroundView: NSVisualEffectView? + var collectionView_: NSCollectionView? + var window: NSWindow? + var selectedOpenWindow: Int = 0 + var numberOfColumns: Int = 0 + var openWindows: [OpenWindow] = [] + var workItems: [DispatchWorkItem] = [] + var isFirstSummon: Bool = true + var appIsBeingUsed: Bool = false + + func applicationDidFinishLaunching(_ aNotification: Notification) { + ensureAccessibilityCheckboxIsChecked() + updateThumbnailMaxSize() + makeStatusBarItem() + makeWindow() + makeCollectionView() + backgroundView = NSVisualEffectView() + backgroundView!.blendingMode = .behindWindow + backgroundView!.material = .dark + backgroundView!.state = .active + backgroundView!.addSubview(collectionView_!) + window!.contentView = backgroundView + listenToGlobalKeyboardEvents(self) + listenToScreenChanges() + } + + func listenToScreenChanges() { + NotificationCenter.default.addObserver( + forName: NSApplication.didChangeScreenParametersNotification, + object: NSApplication.shared, + queue: OperationQueue.main + ) { notification -> Void in + updateThumbnailMaxSize() + } + } + + func applicationDidBecomeActive(_ notification: Notification) { + if !appIsBeingUsed { + focusSelectedWindow(currentlySelectedWindow()) + } + } + + func makeCollectionView() { + let layout = CollectionViewCenterFlowLayout() + layout.estimatedItemSize = NSSize(width: thumbnailMaxWidth, height: thumbnailMaxHeight) + layout.minimumInteritemSpacing = 5 + layout.minimumLineSpacing = 5 + collectionView_ = NSCollectionView() + collectionView_!.dataSource = self + collectionView_!.delegate = self + collectionView_!.collectionViewLayout = layout + collectionView_!.backgroundColors = [.clear] + collectionView_!.isSelectable = true + collectionView_!.allowsMultipleSelection = false + collectionView_!.register(Cell.self, forItemWithIdentifier: cellId) + } + + func makeWindow() { + window = NSWindow() + window!.styleMask = [.borderless] + window!.level = .floating + window!.animationBehavior = NSWindow.AnimationBehavior.none + window!.backgroundColor = .clear + window!.hidesOnDeactivate = true + window!.delegate = self + } + + func makeStatusBarItem() { + item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + item!.button!.title = "AltTab" + item!.menu = NSMenu() + item!.menu!.addItem( + withTitle: "Quit \(ProcessInfo.processInfo.processName)", + action: #selector(NSApplication.terminate(_:)), + keyEquivalent: "q") + } + + func ensureAccessibilityCheckboxIsChecked() { + if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) { + debugPrint("Not trusted as an AX process; please authorize and re-launch") + NSApp.terminate(self) + } + } + + func computeOpenWindows() { + openWindows.removeAll() + for cgWindow in cgWindows() { + let (cgMakeshiftId_, cgOwnerPid, cgId, cgTitle) = cgMakeshiftId(cgWindow) + for axWindow in axWindows(cgOwnerPid) { + if axMakeshiftId(axWindow).starts(with: cgMakeshiftId_) { + openWindows.append(OpenWindow(target: axWindow, ownerPid: cgOwnerPid, cgId: cgId, cgTitle: cgTitle)) + break + } + } + } + } + + func highlightThumbnail(_ step: Int) { + collectionView_!.selectItems(at: [IndexPath(item: selectedOpenWindow, section: 0)], scrollPosition: .top) + collectionView_!.deselectItems(at: [IndexPath(item: cellWithStep(-step), section: 0)]) + } + + func computeThumbnails() { + let maxSize = NSSize(width: NSScreen.main!.frame.width * maxScreenUsage, height: NSScreen.main!.frame.height * maxScreenUsage) + collectionView_!.setFrameSize(maxSize) + collectionView_!.collectionViewLayout!.invalidateLayout() + collectionView_!.reloadData() + collectionView_!.layoutSubtreeIfNeeded() + window!.setContentSize(NSSize(width: collectionView_!.frame.size.width + windowPadding * 2, height: collectionView_!.frame.size.height + windowPadding * 2)) + backgroundView!.setFrameSize(window!.frame.size) + collectionView_!.setFrameOrigin(NSPoint(x: windowPadding, y: windowPadding)) + window!.center() + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + debugPrint("collectionView: count items", openWindows.count) + return openWindows.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + debugPrint("collectionView: make item", indexPath.item) + let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell + item.updateWithNewContent(openWindows[indexPath.item], self.focusSelectedWindow) + return item + } + + func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { + debugPrint("collectionView: item size") + if indexPath.item < openWindows.count { + let (width, height) = computeDownscaledSize(openWindows[indexPath.item].thumbnail) + return NSSize(width: CGFloat(width) + cellPadding * 2, height: CGFloat(height) + max(fontHeight, iconSize) + interItemPadding + cellPadding * 2) + } + return .zero + } + + func cellWithStep(_ step: Int) -> Int { + return selectedOpenWindow + step < 0 ? openWindows.count - 1 : (selectedOpenWindow + step) % openWindows.count + } + + func selectOtherCell(_ step: Int) { + if openWindows.count > 0 { + selectedOpenWindow = cellWithStep(step) + if isFirstSummon { + isFirstSummon = false + let workItem = DispatchWorkItem { + self.computeOpenWindows() + self.computeThumbnails() + self.highlightThumbnail(step) + self.showWindow() + } + workItems.append(workItem) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: workItem) + } else { + highlightThumbnail(step) + } + } + } + + func currentlySelectedWindow() -> OpenWindow? { + return openWindows.count > selectedOpenWindow ? openWindows[selectedOpenWindow] : nil + } + + func showWindow() { + window!.makeKeyAndOrderFront(nil) + NSRunningApplication.current.activate(options: [.activateIgnoringOtherApps]) + } + + func focusSelectedWindow(_ window: OpenWindow?) { + workItems.forEach({ $0.cancel() }) + workItems.removeAll() + window?.focus() + appIsBeingUsed = false + isFirstSummon = true + } + + func keyDownMeta() { + debugPrint("meta down") + computeOpenWindows() + selectedOpenWindow = 0 + } + + func keyDownMetaTab() { + debugPrint("meta+tab down") + appIsBeingUsed = true + selectOtherCell(1) + } + + func keyDownMetaShiftTab() { + debugPrint("meta+shift+tab down") + selectOtherCell(-1) + } + + func keyUpMeta() { + debugPrint("meta up") + if appIsBeingUsed { + focusSelectedWindow(currentlySelectedWindow()) + } + } +} diff --git a/alt-tab-macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/alt-tab-macos/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2db2b1c7 --- /dev/null +++ b/alt-tab-macos/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/alt-tab-macos/Assets.xcassets/Contents.json b/alt-tab-macos/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/alt-tab-macos/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/alt-tab-macos/Info.plist b/alt-tab-macos/Info.plist new file mode 100644 index 00000000..0996ff88 --- /dev/null +++ b/alt-tab-macos/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2019 Pontoise, Louis. All rights reserved. + NSPrincipalClass + NSApplication + LSUIElement + 1 + + diff --git a/alt-tab-macos/Keyboard.swift b/alt-tab-macos/Keyboard.swift new file mode 100644 index 00000000..45cfd964 --- /dev/null +++ b/alt-tab-macos/Keyboard.swift @@ -0,0 +1,47 @@ +import Cocoa + +func listenToGlobalKeyboardEvents(_ delegate: Application) { + let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) }) + let eventTap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: CGEventMask(eventMask), + callback: { (_, _, event, delegate_) -> Unmanaged? in + let d = Unmanaged.fromOpaque(delegate_!).takeUnretainedValue() + return keyboardHandler(event, d) + }, + userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(delegate).toOpaque())) + let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) + CGEvent.tapEnable(tap: eventTap!, enable: true) + CFRunLoopRun() +} + +func keyboardHandler(_ cgEvent: CGEvent, _ delegate: Application) -> Unmanaged? { + let event = NSEvent(cgEvent: cgEvent)! + let keyDown = event.type == .keyDown + let optionKeyEvent = event.keyCode == metaKey + let tabKeyEvent = event.keyCode == tabKey + if optionKeyEvent && event.modifiersDown([metaModifierFlag]) { + delegate.keyDownMeta() + } else if tabKeyEvent && event.modifiersDown([metaModifierFlag]) && keyDown { + delegate.keyDownMetaTab() + // focused app will not receive the event (will not press tab key in that app) + return nil + } else if tabKeyEvent && event.modifiersDown([metaModifierFlag, .shift]) && keyDown { + delegate.keyDownMetaShiftTab() + // focused app will not receive the event (will not press tab key in that app) + return nil + } else if optionKeyEvent && !keyDown { + delegate.keyUpMeta() + } + // focused app will receive the event + return Unmanaged.passRetained(cgEvent) +} + +extension NSEvent { + func modifiersDown(_ modifiers: NSEvent.ModifierFlags) -> Bool { + return self.modifierFlags.intersection(.deviceIndependentFlagsMask) == modifiers + } +} diff --git a/alt-tab-macos/WindowManager.swift b/alt-tab-macos/WindowManager.swift new file mode 100644 index 00000000..95adc450 --- /dev/null +++ b/alt-tab-macos/WindowManager.swift @@ -0,0 +1,99 @@ +import Cocoa +import Foundation + +class OpenWindow { + var target: AXUIElement? + var ownerPid: pid_t? + var cgId: CGWindowID + var cgTitle: String + lazy var thumbnail: NSImage = computeThumbnail() + lazy var icon: NSImage = computeIcon() + + init(target: AXUIElement?, ownerPid: pid_t?, cgId: CGWindowID, cgTitle: String) { + self.target = target + self.ownerPid = ownerPid + self.cgId = cgId + self.cgTitle = cgTitle + } + + func computeIcon() -> NSImage { + return NSRunningApplication(processIdentifier: ownerPid!)!.icon! + } + + func computeThumbnail() -> NSImage { + let windowImage = CGWindowListCreateImage(.null, .optionIncludingWindow, cgId, [.boundsIgnoreFraming, .bestResolution]) + return NSImage(cgImage: windowImage!, size: NSSize(width: windowImage!.width, height: windowImage!.height)) + } + + func focus() { + NSRunningApplication(processIdentifier: ownerPid!)!.activate(options: [.activateIgnoringOtherApps]) + AXUIElementPerformAction(target!, kAXRaiseAction as CFString) + } +} + +func computeDownscaledSize(_ image: NSImage) -> (Int, Int) { + let imageRatio = image.size.width / image.size.height + let thumbnailWidth = Int(floor(thumbnailMaxHeight * imageRatio)) + if thumbnailWidth <= Int(thumbnailMaxWidth) { + return (thumbnailWidth, Int(thumbnailMaxHeight)) + } else { + return (Int(thumbnailMaxWidth), Int(floor(thumbnailMaxWidth / imageRatio))) + } +} + +func cgMakeshiftId(_ window: NSDictionary) -> (String, pid_t, CGWindowID, String) { + let cgId = window[kCGWindowNumber] as! CGWindowID + let cgOwnerPid = window[kCGWindowOwnerPID] as! pid_t + let cgBounds = window[kCGWindowBounds] as! [String: Int] + let cgTitle = String(window[kCGWindowName] as! NSString) + let cgMakeshiftId = String(cgBounds["X"]!) + String(cgBounds["Y"]!) + + String(cgBounds["Width"]!) + String(cgBounds["Height"]!) + + cgTitle + return (cgMakeshiftId, cgOwnerPid, cgId, cgTitle) +} + +func axMakeshiftId(_ element: AXUIElement) -> String { + let titleAttribute = AXUIElementCopyAttributeValue(element, kAXTitleAttribute, String.self)! + let pos = AXValueGetValue(element, kAXPositionAttribute, NSPoint(), AXValueType.cgPoint)! + let size = AXValueGetValue(element, kAXSizeAttribute, NSSize(), AXValueType.cgSize)! + return String(Int(pos.x)) + String(Int(pos.y)) + + String(Int(size.width)) + String(Int(size.height)) + + titleAttribute +} + +func axWindows(_ cgOwnerPid: pid_t) -> [AXUIElement] { + if let windows = AXUIElementCopyAttributeValue(AXUIElementCreateApplication(cgOwnerPid), kAXWindowsAttribute, [AXUIElement].self) { + return windows.filter { + let hasTitle = !(AXUIElementCopyAttributeValue($0, kAXTitleAttribute, String.self) ?? "").isEmpty + return hasTitle + } + } + return [] +} + +func cgWindows() -> [NSDictionary] { + let windows = CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionOnScreenOnly], kCGNullWindowID) as! [NSDictionary] + return windows.filter { + let isWindowNotMenubarOrOthers = $0[kCGWindowLayer] as? Int == 0 + let hasTitle = !(($0[kCGWindowName] as? String ?? "").isEmpty) + return isWindowNotMenubarOrOthers && hasTitle + } +} + +func AXUIElementCopyAttributeValue(_ element: AXUIElement, _ attribute: String, _ type: T.Type) -> T? { + var value: AnyObject? + let result = AXUIElementCopyAttributeValue(element, attribute as CFString, &value) + if result == .success, let typedValue = value as? T { + return typedValue + } + return nil +} + +func AXValueGetValue(_ element: AXUIElement, _ attribute: String, _ target: T, _ type: AXValueType) -> T? { + if let a = AXUIElementCopyAttributeValue(element, attribute, AXValue.self) { + var value = target + AXValueGetValue(a, type, &value) + return value + } + return nil +} diff --git a/alt-tab-macos/alt_tab_macos.entitlements b/alt-tab-macos/alt_tab_macos.entitlements new file mode 100644 index 00000000..b7d85c34 --- /dev/null +++ b/alt-tab-macos/alt_tab_macos.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/alt-tab-macos/docs/img/1 window - 1 line.jpg b/alt-tab-macos/docs/img/1 window - 1 line.jpg new file mode 100644 index 00000000..87781eaf Binary files /dev/null and b/alt-tab-macos/docs/img/1 window - 1 line.jpg differ diff --git a/alt-tab-macos/docs/img/14 windows - 3 lines.jpg b/alt-tab-macos/docs/img/14 windows - 3 lines.jpg new file mode 100644 index 00000000..6b494845 Binary files /dev/null and b/alt-tab-macos/docs/img/14 windows - 3 lines.jpg differ diff --git a/alt-tab-macos/docs/img/2 windows - 1 line.jpg b/alt-tab-macos/docs/img/2 windows - 1 line.jpg new file mode 100644 index 00000000..b1dc3bff Binary files /dev/null and b/alt-tab-macos/docs/img/2 windows - 1 line.jpg differ diff --git a/alt-tab-macos/docs/img/3 windows - 1 line - tall window.jpg b/alt-tab-macos/docs/img/3 windows - 1 line - tall window.jpg new file mode 100644 index 00000000..b361d18d Binary files /dev/null and b/alt-tab-macos/docs/img/3 windows - 1 line - tall window.jpg differ diff --git a/alt-tab-macos/docs/img/3 windows - 1 line - wide window.jpg b/alt-tab-macos/docs/img/3 windows - 1 line - wide window.jpg new file mode 100644 index 00000000..3e864936 Binary files /dev/null and b/alt-tab-macos/docs/img/3 windows - 1 line - wide window.jpg differ diff --git a/alt-tab-macos/docs/img/3 windows - 1 line.jpg b/alt-tab-macos/docs/img/3 windows - 1 line.jpg new file mode 100644 index 00000000..51c82784 Binary files /dev/null and b/alt-tab-macos/docs/img/3 windows - 1 line.jpg differ diff --git a/alt-tab-macos/docs/img/4 windows - 1 line.jpg b/alt-tab-macos/docs/img/4 windows - 1 line.jpg new file mode 100644 index 00000000..28b81cba Binary files /dev/null and b/alt-tab-macos/docs/img/4 windows - 1 line.jpg differ diff --git a/alt-tab-macos/docs/img/5 windows - 1 line.jpg b/alt-tab-macos/docs/img/5 windows - 1 line.jpg new file mode 100644 index 00000000..7af929db Binary files /dev/null and b/alt-tab-macos/docs/img/5 windows - 1 line.jpg differ diff --git a/alt-tab-macos/docs/img/5 windows - 2 lines.jpg b/alt-tab-macos/docs/img/5 windows - 2 lines.jpg new file mode 100644 index 00000000..dd3dd213 Binary files /dev/null and b/alt-tab-macos/docs/img/5 windows - 2 lines.jpg differ diff --git a/alt-tab-macos/docs/img/6 windows - 1 line.jpg b/alt-tab-macos/docs/img/6 windows - 1 line.jpg new file mode 100644 index 00000000..fdaefc08 Binary files /dev/null and b/alt-tab-macos/docs/img/6 windows - 1 line.jpg differ diff --git a/alt-tab-macos/docs/img/7 windows - 2 lines - extra wide window.jpg b/alt-tab-macos/docs/img/7 windows - 2 lines - extra wide window.jpg new file mode 100644 index 00000000..4e236375 Binary files /dev/null and b/alt-tab-macos/docs/img/7 windows - 2 lines - extra wide window.jpg differ diff --git a/alt-tab-macos/docs/img/7 windows - 2 lines - tall window.jpg b/alt-tab-macos/docs/img/7 windows - 2 lines - tall window.jpg new file mode 100644 index 00000000..c5167e2d Binary files /dev/null and b/alt-tab-macos/docs/img/7 windows - 2 lines - tall window.jpg differ diff --git a/alt-tab-macos/docs/img/7 windows - 2 lines - wide window.jpg b/alt-tab-macos/docs/img/7 windows - 2 lines - wide window.jpg new file mode 100644 index 00000000..cabe179b Binary files /dev/null and b/alt-tab-macos/docs/img/7 windows - 2 lines - wide window.jpg differ diff --git a/alt-tab-macos/docs/img/too many windows - 4 lines - paginated.jpg b/alt-tab-macos/docs/img/too many windows - 4 lines - paginated.jpg new file mode 100644 index 00000000..11394ee7 Binary files /dev/null and b/alt-tab-macos/docs/img/too many windows - 4 lines - paginated.jpg differ diff --git a/alt-tab-macos/main.swift b/alt-tab-macos/main.swift new file mode 100644 index 00000000..ecf3b208 --- /dev/null +++ b/alt-tab-macos/main.swift @@ -0,0 +1,8 @@ +import Foundation +import AppKit + +autoreleasepool { + let app = Application.shared + app.delegate = app as? NSApplicationDelegate + app.run() +} diff --git a/alt-tab-macos/ui/UiSubclasses.swift b/alt-tab-macos/ui/UiSubclasses.swift new file mode 100644 index 00000000..b2801c66 --- /dev/null +++ b/alt-tab-macos/ui/UiSubclasses.swift @@ -0,0 +1,138 @@ +import Cocoa + +class HighInterpolationImageView: NSImageView { + override func draw(_ dirtyRect: NSRect) { + NSGraphicsContext.current!.imageInterpolation = .high + super.draw(dirtyRect) + } +} + +class Cell: NSCollectionViewItem { + var thumbnail = NSImageView() + var icon = NSImageView() + var text = NSTextView() + var openWindow: OpenWindow? + var mouseDownCallback: ((OpenWindow) -> Void)? + + override func mouseDown(with theEvent: NSEvent) { + mouseDownCallback!(openWindow!) + } + + override func loadView() { + let vStackView = NSStackView() + vStackView.wantsLayer = true + vStackView.layer!.borderWidth = cellBorderWidth + vStackView.layer!.borderColor = .clear + vStackView.edgeInsets = NSEdgeInsets(top: cellPadding, left: cellPadding, bottom: cellPadding, right: cellPadding) + vStackView.orientation = .vertical + vStackView.spacing = interItemPadding + let hStackView = NSStackView() + hStackView.spacing = interItemPadding + let textStorage = NSTextStorage() + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + let textContainer = NSTextContainer() + textContainer.maximumNumberOfLines = 1 + textContainer.lineFragmentPadding = 0 + layoutManager.addTextContainer(textContainer) + text = NSTextView.init(frame: NSRect.zero, textContainer: textContainer) + text.drawsBackground = true + text.backgroundColor = .clear + text.isSelectable = false + text.isEditable = false + text.font = font + text.textColor = highlightColor + let shadow = NSShadow() + shadow.shadowColor = .darkGray + shadow.shadowOffset = NSMakeSize(0, 0) + shadow.shadowBlurRadius = 1 + text.shadow = shadow + text.enabledTextCheckingTypes = 0 + let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle + paragraphStyle.lineBreakMode = .byTruncatingTail + paragraphStyle.maximumLineHeight = fontHeight + paragraphStyle.minimumLineHeight = fontHeight + paragraphStyle.allowsDefaultTighteningForTruncation = false + text.defaultParagraphStyle = paragraphStyle + text.heightAnchor.constraint(equalToConstant: fontHeight).isActive = true + let shadow2 = NSShadow() + shadow2.shadowColor = .gray + shadow2.shadowOffset = NSMakeSize(0, 0) + shadow2.shadowBlurRadius = 1 + thumbnail.shadow = shadow2 + icon.shadow = shadow2 + hStackView.addView(icon, in: .leading) + hStackView.addView(text, in: .leading) + vStackView.addView(hStackView, in: .leading) + vStackView.addView(thumbnail, in: .leading) + view = vStackView + } + + override var isSelected: Bool { + didSet { + view.layer!.borderColor = isSelected ? highlightColor.cgColor : .clear + } + } + + func updateWithNewContent(_ element: OpenWindow, _ fn: @escaping (OpenWindow) -> Void) { + openWindow = element + thumbnail.image = element.thumbnail + let (width, height) = computeDownscaledSize(element.thumbnail) + thumbnail.image!.size = NSSize(width: width, height: height) + thumbnail.frame.size = NSSize(width: width, height: height) + icon.image = element.icon + icon.image!.size = NSSize(width: iconSize, height: iconSize) + icon.frame.size = NSSize(width: iconSize, height: iconSize) + text.string = element.cgTitle + // workaround: setting string on NSTextView change the font (most likely a Cocoa bug) + text.font = font + text.textContainer!.size.width = thumbnail.frame.size.width - iconSize - interItemPadding + mouseDownCallback = fn + } +} + +class CollectionViewCenterFlowLayout: NSCollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [NSCollectionViewLayoutAttributes] { + let attributes = super.layoutAttributesForElements(in: rect) + if attributes.isEmpty { + return attributes + } + var currentRow: [NSCollectionViewLayoutAttributes] = [] + var currentRowY = CGFloat(0) + var currentRowWidth = CGFloat(0) + var previousRowMaxY = CGFloat(0) + var currentRowMaxY = CGFloat(0) + var widestRow = CGFloat(0) + var totalHeight = CGFloat(0) + attributes.enumerated().forEach { + let isNewRow = abs($1.frame.origin.y - currentRowY) > thumbnailMaxHeight + if isNewRow { + computeOriginXForAllItems(currentRowWidth - minimumInteritemSpacing, previousRowMaxY, currentRow) + currentRow.removeAll() + currentRowY = $1.frame.origin.y + currentRowWidth = 0 + previousRowMaxY += currentRowMaxY + minimumLineSpacing + currentRowMaxY = 0 + } + currentRow.append($1) + currentRowWidth += $1.frame.size.width + minimumInteritemSpacing + widestRow = max(widestRow, currentRowWidth) + currentRowMaxY = max(currentRowMaxY, $1.frame.size.height) + if $0 == attributes.count - 1 { + computeOriginXForAllItems(currentRowWidth - minimumInteritemSpacing, previousRowMaxY, currentRow) + totalHeight = previousRowMaxY + currentRowMaxY + } + } + collectionView!.setFrameSize(NSSize(width: widestRow - minimumInteritemSpacing, height: totalHeight)) + return attributes + } + + func computeOriginXForAllItems(_ currentRowWidth: CGFloat, _ previousRowMaxHeight: CGFloat, _ currentRow: [NSCollectionViewLayoutAttributes]) { + var marginLeft = floor((collectionView!.frame.size.width - currentRowWidth) / 2) + currentRow.forEach { + $0.frame.origin.x = marginLeft + $0.frame.origin.y = previousRowMaxHeight + marginLeft += $0.frame.size.width + minimumInteritemSpacing + } + } +}