feat: mvp

This commit is contained in:
Louis Pontoise committed 2019-08-27 19:46:53 +09:00
commit be59936601
26 files changed
+1030

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
/.idea/
xcuserdata/
+396
View File
@@ -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 = "<group>"; };
D04BA04504BB9802F6E0B889 /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA0E21840B63A2BC23253 /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
D04BA262E9A663B3A85A2A95 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D04BA3202A2C22C347E849B3 /* UiSubclasses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiSubclasses.swift; sourceTree = "<group>"; };
D04BA4AF59F978749D697493 /* 5 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA5349996324809EA7208 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
D04BA58701B5A47B6A2953E1 /* 1 window - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1 window - 1 line.jpg"; sourceTree = "<group>"; };
D04BA685BCDB60B9FEA57438 /* 3 windows - 1 line - tall window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - tall window.jpg"; sourceTree = "<group>"; };
D04BA75BFFC94D1E33047688 /* 14 windows - 3 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "14 windows - 3 lines.jpg"; sourceTree = "<group>"; };
D04BA7B6AAB0812631BBC7A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = "<group>"; };
D04BA7F8D6FBE1C6FDA7BC72 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D04BA8298AF4D8ED7827ACDC /* 3 windows - 1 line - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - wide window.jpg"; sourceTree = "<group>"; };
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 = "<group>"; };
D04BA9B41D7EA9F1B6290C91 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = "<group>"; };
D04BA9EBCA307524DBE48DCF /* 6 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "6 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BAA44C837F3A67403B9DB /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
D04BAC503A122CC0068BB84E /* too many windows - 4 lines - paginated.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "too many windows - 4 lines - paginated.jpg"; sourceTree = "<group>"; };
D04BAD1C9F215BCCD3B620AC /* alt_tab_macos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = alt_tab_macos.entitlements; sourceTree = "<group>"; };
D04BAD40CE2D3A8AAC3819D0 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = file.gitignore; path = .gitignore; sourceTree = "<group>"; };
D04BADB4ABF73F979B9FC0BA /* 2 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BADF2A78C91437E1E4C42 /* 7 windows - 2 lines - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - wide window.jpg"; sourceTree = "<group>"; };
D04BAECA9B67E89686393364 /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
D04BA1463D2A17038222BB84 = {
isa = PBXGroup;
children = (
D04BA1A461BBB5A9BE15E015 /* Products */,
D04BAD48B702C4B1DC543557 /* alt-tab-macos */,
D04BAD40CE2D3A8AAC3819D0 /* .gitignore */,
);
sourceTree = "<group>";
};
D04BA1A461BBB5A9BE15E015 /* Products */ = {
isa = PBXGroup;
children = (
D04BA82F792DF53958D92572 /* alt-tab-macos.app */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
D04BAAA28315301A906DD201 /* ui */ = {
isa = PBXGroup;
children = (
D04BA3202A2C22C347E849B3 /* UiSubclasses.swift */,
);
path = ui;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
+227
View File
@@ -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())
}
}
}
@@ -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"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 Pontoise, Louis. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
</dict>
</plist>
+47
View File
@@ -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<CGEvent>? in
let d = Unmanaged<Application>.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<CGEvent>? {
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
}
}
+99
View File
@@ -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<T>(_ 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<T>(_ 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
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

+8
View File
@@ -0,0 +1,8 @@
import Foundation
import AppKit
autoreleasepool {
let app = Application.shared
app.delegate = app as? NSApplicationDelegate
app.run()
}
+138
View File
@@ -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
}
}
}