Skip to content

Commit 7a67c4e

Browse files
BohdanBohdan
authored andcommitted
Added custom image upload for keyboard layout
1 parent 5eba934 commit 7a67c4e

23 files changed

+1205
-280
lines changed

.swiftlint.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ custom_rules:
102102
regex: '^\s*(?:if|guard)[ \t]+(?:let|var)\s+\w[^{\n]*,\s*$'
103103
message: "When if/guard has multiple conditions, the keyword should be alone on its line and the opening brace on its own line"
104104
severity: warning
105+
# Multiline guard: last condition should not have 'else {' on the same line
106+
# Bad: guard let x = foo,
107+
# let y = bar else { return }
108+
# Good: guard
109+
# let x = foo,
110+
# let y = bar
111+
# else { return }
112+
guard_multiline_else_same_line:
113+
name: "Guard Multiline Else Same Line"
114+
regex: '^[ \t]+(?=[^ \t])(?!guard)(?!\})(?!//)(?!else)[^\n]*\belse\s*\{'
115+
message: "In a multiline guard, 'else {' should be on its own line"
116+
severity: warning
117+
105118
# One blank line after class/struct/enum/protocol declaration
106119
vertical_whitespace_after_type_declaration:
107120
name: "Vertical Whitespace After Type Declaration"

LanguageFlag.xcodeproj/project.pbxproj

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,28 @@
3939
/* Begin PBXFileSystemSynchronizedRootGroup section */
4040
B45A6DEC2F54C0170003B6DD /* LanguageFlagTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LanguageFlagTests; sourceTree = "<group>"; };
4141
B45A6E172F54CD620003B6DD /* LanguageFlagUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LanguageFlagUITests; sourceTree = "<group>"; };
42-
B47643282F37B7DE00719179 /* LanguageFlag */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LanguageFlag; sourceTree = "<group>"; };
42+
B47643282F37B7DE00719179 /* LanguageFlag */ = {
43+
isa = PBXFileSystemSynchronizedRootGroup;
44+
exceptions = (
45+
B4AA00002F000000AA000000 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
46+
);
47+
explicitFileTypes = {};
48+
explicitFolders = ();
49+
path = LanguageFlag;
50+
sourceTree = "<group>";
51+
};
4352
/* End PBXFileSystemSynchronizedRootGroup section */
4453

54+
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
55+
B4AA00002F000000AA000000 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
56+
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
57+
membershipExceptions = (
58+
"Supporting files/Info.plist",
59+
);
60+
target = 40E0C9A5244B7EF50012F409 /* LanguageFlag */;
61+
};
62+
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
63+
4564
/* Begin PBXFrameworksBuildPhase section */
4665
40E0C9A3244B7EF50012F409 /* Frameworks */ = {
4766
isa = PBXFrameworksBuildPhase;

LanguageFlag/Animations/Basic/ScaleAnimation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class ScaleAnimation: BaseWindowAnimation, WindowAnimation {
2727
scaleAnim.fillMode = .forwards
2828
scaleAnim.isRemovedOnCompletion = false
2929

30-
scaleAnim.delegate = AnimationCompletionDelegate { [weak layer] finished in
30+
scaleAnim.delegate = AnimationCompletionDelegate { [weak layer] _ in
3131
layer?.transform = CATransform3DIdentity
3232
layer?.anchorPoint = oldAnchor
3333
layer?.position = oldPosition
@@ -63,7 +63,7 @@ class ScaleAnimation: BaseWindowAnimation, WindowAnimation {
6363
scaleAnim.fillMode = .forwards
6464
scaleAnim.isRemovedOnCompletion = false
6565

66-
scaleAnim.delegate = AnimationCompletionDelegate { [weak layer] finished in
66+
scaleAnim.delegate = AnimationCompletionDelegate { [weak layer] _ in
6767
layer?.transform = CATransform3DIdentity
6868
layer?.anchorPoint = oldAnchor
6969
layer?.position = oldPosition

LanguageFlag/Animations/FilterBased/VHSGlitchAnimation.swift

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,12 @@ class VHSGlitchAnimation: BaseWindowAnimation, WindowAnimation {
4747
}
4848
layer.add(blurAnim, forKey: "blur")
4949

50-
let posterAnim = createAnimation(keyPath: "filters.CIColorPosterize.inputLevels", from: 8.0, to: 256.0, duration: duration)
51-
posterAnim.fillMode = .forwards
52-
posterAnim.isRemovedOnCompletion = false
53-
layer.add(posterAnim, forKey: "level")
54-
55-
let motionAnim = createAnimation(keyPath: "filters.CIMotionBlur.inputRadius", from: 10.0, to: 0.0, duration: duration)
56-
motionAnim.fillMode = .forwards
57-
motionAnim.isRemovedOnCompletion = false
58-
layer.add(motionAnim, forKey: "motion")
59-
50+
addForwardAnimation(keyPath: "filters.CIColorPosterize.inputLevels", from: 8.0, to: 256.0, duration: duration, to: layer)
51+
addForwardAnimation(keyPath: "filters.CIMotionBlur.inputRadius", from: 10.0, to: 0.0, duration: duration, to: layer)
52+
6053
// Animate Overlays Opacity
61-
let scanlineAnim = createAnimation(keyPath: "opacity", from: 0.3, to: 0.0, duration: duration)
62-
scanlineAnim.fillMode = .forwards
63-
scanlineAnim.isRemovedOnCompletion = false
64-
scanline.add(scanlineAnim, forKey: "opacity")
65-
66-
let noiseAnim = createAnimation(keyPath: "opacity", from: 0.15, to: 0.0, duration: duration)
67-
noiseAnim.fillMode = .forwards
68-
noiseAnim.isRemovedOnCompletion = false
69-
noise.add(noiseAnim, forKey: "opacity")
54+
addForwardAnimation(keyPath: "opacity", from: 0.3, to: 0.0, duration: duration, to: scanline)
55+
addForwardAnimation(keyPath: "opacity", from: 0.15, to: 0.0, duration: duration, to: noise)
7056

7157
scanline.opacity = 0.0
7258
noise.opacity = 0.0
@@ -115,28 +101,15 @@ class VHSGlitchAnimation: BaseWindowAnimation, WindowAnimation {
115101
noise.removeFromSuperlayer()
116102
completion?()
117103
}
104+
118105
layer.add(blurAnim, forKey: "blur")
119106

120-
let posterAnim = createAnimation(keyPath: "filters.CIColorPosterize.inputLevels", from: 256.0, to: 8.0, duration: duration)
121-
posterAnim.fillMode = .forwards
122-
posterAnim.isRemovedOnCompletion = false
123-
layer.add(posterAnim, forKey: "level")
124-
125-
let motionAnim = createAnimation(keyPath: "filters.CIMotionBlur.inputRadius", from: 0.0, to: 10.0, duration: duration)
126-
motionAnim.fillMode = .forwards
127-
motionAnim.isRemovedOnCompletion = false
128-
layer.add(motionAnim, forKey: "motion")
129-
107+
addForwardAnimation(keyPath: "filters.CIColorPosterize.inputLevels", from: 256.0, to: 8.0, duration: duration, to: layer)
108+
addForwardAnimation(keyPath: "filters.CIMotionBlur.inputRadius", from: 0.0, to: 10.0, duration: duration, to: layer)
109+
130110
// Animate Overlays Opacity
131-
let scanlineAnim = createAnimation(keyPath: "opacity", from: 0.0, to: 0.3, duration: duration)
132-
scanlineAnim.fillMode = .forwards
133-
scanlineAnim.isRemovedOnCompletion = false
134-
scanline.add(scanlineAnim, forKey: "opacity")
135-
136-
let noiseAnim = createAnimation(keyPath: "opacity", from: 0.0, to: 0.15, duration: duration)
137-
noiseAnim.fillMode = .forwards
138-
noiseAnim.isRemovedOnCompletion = false
139-
noise.add(noiseAnim, forKey: "opacity")
111+
addForwardAnimation(keyPath: "opacity", from: 0.0, to: 0.3, duration: duration, to: scanline)
112+
addForwardAnimation(keyPath: "opacity", from: 0.0, to: 0.15, duration: duration, to: noise)
140113

141114
scanline.opacity = 0.3
142115
noise.opacity = 0.15
@@ -145,4 +118,21 @@ class VHSGlitchAnimation: BaseWindowAnimation, WindowAnimation {
145118

146119
animateAlpha(contentView: contentView, from: 1.0, to: 0.0, duration: duration)
147120
}
121+
122+
// MARK: - Private
123+
124+
private func addForwardAnimation(keyPath: String,
125+
from fromValue: Any,
126+
to toValue: Any,
127+
duration: TimeInterval,
128+
to targetLayer: CALayer) {
129+
let anim = createAnimation(keyPath: keyPath,
130+
from: fromValue,
131+
to: toValue,
132+
duration: duration)
133+
134+
anim.fillMode = .forwards
135+
anim.isRemovedOnCompletion = false
136+
targetLayer.add(anim, forKey: nil)
137+
}
148138
}

LanguageFlag/Animations/Helpers/VHSOverlayManager.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import Cocoa
77
class VHSOverlayManager {
88

99
// MARK: - Cache
10+
private struct SizeKey: Hashable {
1011

11-
private var scanlineCache: [CGSize: CGImage] = [:]
12-
private var noiseCache: [CGSize: CGImage] = [:]
12+
let width: CGFloat
13+
let height: CGFloat
14+
15+
init(_ size: CGSize) { width = size.width; height = size.height }
16+
}
17+
18+
private var scanlineCache: [SizeKey: CGImage] = [:]
19+
private var noiseCache: [SizeKey: CGImage] = [:]
1320

1421
// MARK: - Scanline Layer
1522

@@ -19,16 +26,20 @@ class VHSOverlayManager {
1926
/// - opacity: Initial opacity of the layer
2027
/// - Returns: CALayer with scanline pattern
2128
func createScanlineLayer(size: CGSize, opacity: Float = 0.3) -> CALayer {
22-
let image = scanlineCache[size] ?? {
29+
let key = SizeKey(size)
30+
let image = scanlineCache[key] ?? {
2331
let img = AnimationEffectHelpers.createScanlinePattern(size: size)
24-
scanlineCache[size] = img
32+
scanlineCache[key] = img
33+
2534
return img
2635
}()
36+
2737
let layer = CALayer()
2838
layer.name = "vhsScanline"
2939
layer.frame = CGRect(origin: .zero, size: size)
3040
layer.contents = image
3141
layer.opacity = opacity
42+
3243
return layer
3344
}
3445

@@ -40,16 +51,20 @@ class VHSOverlayManager {
4051
/// - opacity: Initial opacity of the layer
4152
/// - Returns: CALayer with noise pattern
4253
func createNoiseLayer(size: CGSize, opacity: Float = 0.15) -> CALayer {
43-
let image = noiseCache[size] ?? {
54+
let key = SizeKey(size)
55+
let image = noiseCache[key] ?? {
4456
let img = AnimationEffectHelpers.createNoisePattern(size: size)
45-
noiseCache[size] = img
57+
noiseCache[key] = img
58+
4659
return img
4760
}()
61+
4862
let layer = CALayer()
4963
layer.name = "vhsNoise"
5064
layer.frame = CGRect(origin: .zero, size: size)
5165
layer.contents = image
5266
layer.opacity = opacity
67+
5368
return layer
5469
}
5570

@@ -61,6 +76,7 @@ class VHSOverlayManager {
6176
func createVHSOverlays(size: CGSize) -> (scanline: CALayer, noise: CALayer) {
6277
let scanline = createScanlineLayer(size: size)
6378
let noise = createNoiseLayer(size: size)
79+
6480
return (scanline, noise)
6581
}
6682
}

LanguageFlag/Animations/Transform/RotateAnimation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class RotateAnimation: BaseWindowAnimation, WindowAnimation {
4343
group.fillMode = .forwards
4444
group.isRemovedOnCompletion = false
4545

46-
group.delegate = AnimationCompletionDelegate { [weak layer] finished in
46+
group.delegate = AnimationCompletionDelegate { [weak layer] _ in
4747
layer?.transform = CATransform3DIdentity
4848
layer?.anchorPoint = oldAnchor
4949
layer?.position = oldPosition
@@ -90,7 +90,7 @@ class RotateAnimation: BaseWindowAnimation, WindowAnimation {
9090
group.fillMode = .forwards
9191
group.isRemovedOnCompletion = false
9292

93-
group.delegate = AnimationCompletionDelegate { [weak layer] finished in
93+
group.delegate = AnimationCompletionDelegate { [weak layer] _ in
9494
layer?.transform = CATransform3DIdentity
9595
layer?.anchorPoint = oldAnchor
9696
layer?.position = oldPosition

LanguageFlag/Animations/Transform/SwingAnimation.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ class SwingAnimation: BaseWindowAnimation, WindowAnimation {
4242
AnimationTiming.easeInOut
4343
]
4444

45-
animation.delegate = AnimationCompletionDelegate { [weak self] finished in
46-
guard let self, finished else { return }
45+
animation.delegate = AnimationCompletionDelegate { finished in
46+
guard finished else { return }
4747

4848
layer.transform = CATransform3DIdentity
4949
layer.anchorPoint = CGPoint(x: 0, y: 0)
@@ -83,8 +83,8 @@ class SwingAnimation: BaseWindowAnimation, WindowAnimation {
8383
animation.duration = duration
8484
animation.timingFunction = AnimationTiming.easeIn
8585

86-
animation.delegate = AnimationCompletionDelegate { [weak self] finished in
87-
guard let self, finished else { return }
86+
animation.delegate = AnimationCompletionDelegate { finished in
87+
guard finished else { return }
8888

8989
layer.transform = CATransform3DIdentity
9090
layer.anchorPoint = CGPoint(x: 0, y: 0)

LanguageFlag/Application/AppDelegate.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3333
// Disable window restoration for menu bar app
3434
UserDefaults.standard.set(false, forKey: "NSQuitAlwaysKeepsWindows")
3535

36+
// ⚠️ Run once to regenerate Layout.json with stable source IDs.
37+
// Copy the Xcode console output to Layout.json, then delete this line.
38+
#if DEBUG
39+
LayoutMappingGenerator.printMapping()
40+
#endif
41+
3642
// Hide dock icon (menu bar app only)
3743
NSApp.setActivationPolicy(.accessory)
3844
}

LanguageFlag/Helpers/AnimationEffectHelpers.swift

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@ enum AnimationEffectHelpers {
1616
let colorSpace = CGColorSpaceCreateDeviceGray()
1717
let bitmapInfo = CGImageAlphaInfo.none.rawValue
1818

19-
guard let context = CGContext(
20-
data: nil,
21-
width: width,
22-
height: height,
23-
bitsPerComponent: 8,
24-
bytesPerRow: width,
25-
space: colorSpace,
26-
bitmapInfo: bitmapInfo
27-
) else { return nil }
28-
19+
guard
20+
let context = CGContext(
21+
data: nil,
22+
width: width,
23+
height: height,
24+
bitsPerComponent: 8,
25+
bytesPerRow: width,
26+
space: colorSpace,
27+
bitmapInfo: bitmapInfo
28+
)
29+
else {
30+
return nil
31+
}
32+
2933
// Fill with transparent
3034
context.setFillColor(gray: 1.0, alpha: 1.0)
3135
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
@@ -75,15 +79,20 @@ enum AnimationEffectHelpers {
7579
let bitmapInfo = CGImageAlphaInfo.none.rawValue
7680

7781
return pixels.withUnsafeMutableBytes { ptr in
78-
guard let context = CGContext(
79-
data: ptr.baseAddress,
80-
width: width,
81-
height: height,
82-
bitsPerComponent: 8,
83-
bytesPerRow: width,
84-
space: colorSpace,
85-
bitmapInfo: bitmapInfo
86-
) else { return nil }
82+
guard
83+
let context = CGContext(
84+
data: ptr.baseAddress,
85+
width: width,
86+
height: height,
87+
bitsPerComponent: 8,
88+
bytesPerRow: width,
89+
space: colorSpace,
90+
bitmapInfo: bitmapInfo
91+
)
92+
else {
93+
return nil
94+
}
95+
8796
return context.makeImage()
8897
}
8998
}

0 commit comments

Comments
 (0)