diff --git a/keycastr/KCEventTap.m b/keycastr/KCEventTap.m
index c6ed8f1..873e12d 100644
--- a/keycastr/KCEventTap.m
+++ b/keycastr/KCEventTap.m
@@ -218,6 +218,9 @@ -(void) _noteFlagsChanged:(CGEventRef)event
if (f & kCGEventFlagMaskAlternate)
modifiers |= NSEventModifierFlagOption;
+
+ if (f & kCGEventFlagMaskSecondaryFn)
+ modifiers |= NSEventModifierFlagFunction;
[self noteFlagsChanged:modifiers];
}
diff --git a/keycastr/KeyCastr.xcodeproj/project.pbxproj b/keycastr/KeyCastr.xcodeproj/project.pbxproj
index 8b3e3bb..5c363ec 100644
--- a/keycastr/KeyCastr.xcodeproj/project.pbxproj
+++ b/keycastr/KeyCastr.xcodeproj/project.pbxproj
@@ -7,6 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
+ 1DB8252B2226E86500E123AA /* ModsVisualizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1DB825272226E86500E123AA /* ModsVisualizer.h */; };
+ 1DB8252C2226E86500E123AA /* Mods.nib in Resources */ = {isa = PBXBuildFile; fileRef = 1DB825282226E86500E123AA /* Mods.nib */; };
+ 1DB8252D2226E86500E123AA /* Mods-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1DB825292226E86500E123AA /* Mods-Info.plist */; };
+ 1DB8252E2226E86500E123AA /* ModsVisualizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1DB8252A2226E86500E123AA /* ModsVisualizer.m */; };
+ 1DB8253E2226E8D100E123AA /* ModsVisualizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1DB8252A2226E86500E123AA /* ModsVisualizer.m */; };
+ 1DB8253F2226E8DA00E123AA /* Mods.nib in Resources */ = {isa = PBXBuildFile; fileRef = 1DB825282226E86500E123AA /* Mods.nib */; };
+ 1DB825402226EC2100E123AA /* Mods.kcplugin in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1DB8253C2226E8A000E123AA /* Mods.kcplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
324AD25E40F9EFA6F54FA36A /* KCKeycastrEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 324AD6721F5E3C6D50AE104A /* KCKeycastrEvent.h */; };
324AD36C18A447EB8EDCE7B7 /* KCDefaultVisualizer.nib in Resources */ = {isa = PBXBuildFile; fileRef = 324AD93D2D9827CC6B6996A8 /* KCDefaultVisualizer.nib */; };
324AD7AC994A031550F38591 /* KCKeycastrEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 324ADBA0D37774E36EA35A2E /* KCKeycastrEvent.m */; };
@@ -71,6 +78,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
+ 1DB825412226EC3200E123AA /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 1DB8252F2226E8A000E123AA;
+ remoteInfo = Mods;
+ };
3D1646D40F43DCB200CA65AD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
@@ -297,6 +311,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
+ 1DB825402226EC2100E123AA /* Mods.kcplugin in CopyFiles */,
3D1648C20F451EDB00CA65AD /* Svelte.kcplugin in CopyFiles */,
3D1646690F43D76D00CA65AD /* KCDefaultVisualizer.kcplugin in CopyFiles */,
);
@@ -329,6 +344,11 @@
/* Begin PBXFileReference section */
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; };
+ 1DB825272226E86500E123AA /* ModsVisualizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModsVisualizer.h; sourceTree = ""; };
+ 1DB825282226E86500E123AA /* Mods.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; path = Mods.nib; sourceTree = ""; };
+ 1DB825292226E86500E123AA /* Mods-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Mods-Info.plist"; sourceTree = ""; };
+ 1DB8252A2226E86500E123AA /* ModsVisualizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModsVisualizer.m; sourceTree = ""; };
+ 1DB8253C2226E8A000E123AA /* Mods.kcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Mods.kcplugin; sourceTree = BUILT_PRODUCTS_DIR; };
29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; };
29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; };
@@ -471,10 +491,21 @@
3D1646B60F43DBD700CA65AD /* KCVisualizer.framework */,
3D1648AC0F451D4500CA65AD /* Svelte.kcplugin */,
AFC172192377CFF500292155 /* KCVisualizerTests.xctest */,
+ 1DB8253C2226E8A000E123AA /* Mods.kcplugin */,
);
name = Products;
sourceTree = "";
};
+ 1DB825262226E85800E123AA /* Mods */ = {
+ isa = PBXGroup;
+ children = (
+ 1DB825282226E86500E123AA /* Mods.nib */,
+ 1DB825272226E86500E123AA /* ModsVisualizer.h */,
+ 1DB8252A2226E86500E123AA /* ModsVisualizer.m */,
+ );
+ name = Mods;
+ sourceTree = "";
+ };
29B97314FDCFA39411CA2CEA /* KeyCastr */ = {
isa = PBXGroup;
children = (
@@ -486,6 +517,7 @@
AF6DD3FB2586E00500EDC9D4 /* Externals */,
29B97323FDCFA39411CA2CEA /* Frameworks */,
19C28FACFE9D520D11CA2CBB /* Products */,
+ 1DB825292226E86500E123AA /* Mods-Info.plist */,
);
name = KeyCastr;
sourceTree = "";
@@ -550,6 +582,7 @@
3D1646530F43D6B800CA65AD /* Visualizers */ = {
isa = PBXGroup;
children = (
+ 1DB825262226E85800E123AA /* Mods */,
3D1648A30F451CDA00CA65AD /* Svelte */,
324AD7A5D37DE702839F52C6 /* Mouse */,
324ADFFC6CB5410AEFDEF479 /* Default */,
@@ -674,12 +707,31 @@
324ADBC2EAD3FC5FD3C369FE /* KCMouseEventVisualizer.h in Headers */,
324ADBB15522719676E80D5F /* KCMouseEvent.h in Headers */,
324AD25E40F9EFA6F54FA36A /* KCKeycastrEvent.h in Headers */,
+ 1DB8252B2226E86500E123AA /* ModsVisualizer.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
+ 1DB8252F2226E8A000E123AA /* Mods */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1DB825382226E8A000E123AA /* Build configuration list for PBXNativeTarget "Mods" */;
+ buildPhases = (
+ 1DB825322226E8A000E123AA /* Resources */,
+ 1DB825342226E8A000E123AA /* Sources */,
+ 3D1646580F43D6E700CA65AD /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 1DB825302226E8A000E123AA /* PBXTargetDependency */,
+ );
+ name = Mods;
+ productName = Svelte;
+ productReference = 1DB8253C2226E8A000E123AA /* Mods.kcplugin */;
+ productType = "com.apple.product-type.bundle";
+ };
3D1646590F43D6E700CA65AD /* KCDefaultVisualizer */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3D16465E0F43D6E800CA65AD /* Build configuration list for PBXNativeTarget "KCDefaultVisualizer" */;
@@ -752,6 +804,7 @@
AF6DD4372586E0BA00EDC9D4 /* PBXTargetDependency */,
AF6DD4342586E0A700EDC9D4 /* PBXTargetDependency */,
3D1646D50F43DCB200CA65AD /* PBXTargetDependency */,
+ 1DB825422226EC3200E123AA /* PBXTargetDependency */,
);
name = KeyCastr;
productInstallPath = "$(HOME)/Applications";
@@ -828,6 +881,7 @@
projectRoot = "";
targets = (
3D1646B50F43DBD700CA65AD /* KCVisualizer */,
+ 1DB8252F2226E8A000E123AA /* Mods */,
3D1648AB0F451D4500CA65AD /* Svelte */,
3D1646590F43D6E700CA65AD /* KCDefaultVisualizer */,
8D1107260486CEB800E47090 /* KeyCastr */,
@@ -987,6 +1041,14 @@
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
+ 1DB825322226E8A000E123AA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1DB8253F2226E8DA00E123AA /* Mods.nib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3D1646560F43D6E700CA65AD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -999,6 +1061,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 1DB8252C2226E86500E123AA /* Mods.nib in Resources */,
+ 1DB8252D2226E86500E123AA /* Mods-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1041,6 +1105,14 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 1DB825342226E8A000E123AA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1DB8253E2226E8D100E123AA /* ModsVisualizer.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3D1646570F43D6E700CA65AD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1054,6 +1126,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 1DB8252E2226E86500E123AA /* ModsVisualizer.m in Sources */,
3D1646C20F43DC4700CA65AD /* KCVisualizer.m in Sources */,
AF5457292C0CDB2E00064C82 /* KCColorValueTransformer.m in Sources */,
AF7B1CF32C22376100C8C145 /* NSUserDefaults+Utility.m in Sources */,
@@ -1099,6 +1172,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
+ 1DB825422226EC3200E123AA /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 1DB8252F2226E8A000E123AA /* Mods */;
+ targetProxy = 1DB825412226EC3200E123AA /* PBXContainerItemProxy */;
+ };
3D1646D50F43DCB200CA65AD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3D1646B50F43DBD700CA65AD /* KCVisualizer */;
@@ -1171,6 +1249,88 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 1DB825392226E8A000E123AA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_FIX_AND_CONTINUE = YES;
+ GCC_MODEL_TUNING = G5;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h";
+ INFOPLIST_FILE = "Mods-Info.plist";
+ INSTALL_PATH = "$(HOME)/Library/Bundles";
+ OTHER_LDFLAGS = (
+ "-framework",
+ Foundation,
+ "-framework",
+ AppKit,
+ );
+ PREBINDING = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = net.stephendeken.KeyCastr.Mods;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = kcplugin;
+ ZERO_LINK = YES;
+ };
+ name = Debug;
+ };
+ 1DB8253A2226E8A000E123AA /* Code Coverage */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_FIX_AND_CONTINUE = YES;
+ GCC_GENERATE_TEST_COVERAGE_FILES = YES;
+ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES;
+ GCC_MODEL_TUNING = G5;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h";
+ INFOPLIST_FILE = "Mods-Info.plist";
+ INSTALL_PATH = "$(HOME)/Library/Bundles";
+ OTHER_LDFLAGS = (
+ "-framework",
+ Foundation,
+ "-framework",
+ AppKit,
+ );
+ PREBINDING = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = net.stephendeken.KeyCastr.Mods;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = kcplugin;
+ ZERO_LINK = NO;
+ };
+ name = "Code Coverage";
+ };
+ 1DB8253B2226E8A000E123AA /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ COPY_PHASE_STRIP = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ GCC_ENABLE_FIX_AND_CONTINUE = NO;
+ GCC_MODEL_TUNING = G5;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h";
+ INFOPLIST_FILE = "Mods-Info.plist";
+ INSTALL_PATH = "$(HOME)/Library/Bundles";
+ OTHER_LDFLAGS = (
+ "-framework",
+ Foundation,
+ "-framework",
+ AppKit,
+ );
+ PREBINDING = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = net.stephendeken.KeyCastr.Mods;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = kcplugin;
+ ZERO_LINK = NO;
+ };
+ name = Release;
+ };
3D16465C0F43D6E800CA65AD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1609,6 +1769,16 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ 1DB825382226E8A000E123AA /* Build configuration list for PBXNativeTarget "Mods" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1DB825392226E8A000E123AA /* Debug */,
+ 1DB8253A2226E8A000E123AA /* Code Coverage */,
+ 1DB8253B2226E8A000E123AA /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
3D16465E0F43D6E800CA65AD /* Build configuration list for PBXNativeTarget "KCDefaultVisualizer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/keycastr/Mods-Info.plist b/keycastr/Mods-Info.plist
new file mode 100644
index 0000000..5a6a411
--- /dev/null
+++ b/keycastr/Mods-Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${EXECUTABLE_NAME}
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ BNDL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ NSPrincipalClass
+ ModsVisualizerFactory
+
+
diff --git a/keycastr/Mods.nib/designable.nib b/keycastr/Mods.nib/designable.nib
new file mode 100644
index 0000000..98351db
--- /dev/null
+++ b/keycastr/Mods.nib/designable.nib
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keycastr/Mods.nib/keyedobjects.nib b/keycastr/Mods.nib/keyedobjects.nib
new file mode 100644
index 0000000..c6700cc
Binary files /dev/null and b/keycastr/Mods.nib/keyedobjects.nib differ
diff --git a/keycastr/ModsVisualizer.h b/keycastr/ModsVisualizer.h
new file mode 100644
index 0000000..5a5db10
--- /dev/null
+++ b/keycastr/ModsVisualizer.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2023 Colin Gray
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// * Neither the name KeyCastr nor the names of its contributors may be used to
+// endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+#import
+#import "KCVisualizer.h"
+
+@interface ModsVisualizerFactory : KCVisualizerFactory
+{
+}
+
+- (NSString*)visualizerNibName;
+- (Class)visualizerClass;
+- (NSString*)visualizerName;
+
+@end
+
+@interface ModsVisualizerView : NSView
+{
+ uint32_t _flags;
+}
+
+- (void)noteFlagsChanged:(uint32_t)flags;
+
+@end
+
+@interface ModsVisualizer : KCVisualizer
+{
+ NSWindow* _visualizerWindow;
+ ModsVisualizerView* _visualizerView;
+}
+
+- (NSString*)visualizerName;
+- (void)deactivateVisualizer:(id)sender;
+
+- (void)noteFlagsChanged:(uint32_t)flags;
+
+@end
diff --git a/keycastr/ModsVisualizer.m b/keycastr/ModsVisualizer.m
new file mode 100644
index 0000000..aac9bfc
--- /dev/null
+++ b/keycastr/ModsVisualizer.m
@@ -0,0 +1,254 @@
+// Copyright (c) 2023 Colin Gray
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// * Neither the name KeyCastr nor the names of its contributors may be used to
+// endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#define MODS_WIDTH 100
+
+#import "ModsVisualizer.h"
+#import "NSBezierPath+RoundedRect.h"
+#import "KCKeystroke.h"
+
+@implementation ModsVisualizerFactory
+
+- (NSString *)visualizerNibName {
+ return @"Mods";
+}
+
+- (Class)visualizerClass {
+ return [ModsVisualizer class];
+}
+
+- (NSString *)visualizerName {
+ return @"Mods";
+}
+
+@end
+
+@implementation ModsVisualizerView
+
+- (unsigned short)flagsCount {
+ unsigned short count = 0;
+
+ if (_flags & NSEventModifierFlagFunction) {
+ count += 1;
+ }
+
+ if (_flags & NSEventModifierFlagControl) {
+ count += 1;
+ }
+
+ if (_flags & NSEventModifierFlagOption) {
+ count += 1;
+ }
+
+ if (_flags & NSEventModifierFlagShift) {
+ count += 1;
+ }
+
+ if (_flags & NSEventModifierFlagCommand) {
+ count += 1;
+ }
+
+ return count;
+}
+
+- (void)drawRect:(NSRect)rect {
+ NSRect frame = [self frame];
+ NSRect bgFrame = [self frame];
+ float oneQuarter = floorf(MODS_WIDTH);
+
+ CGFloat x = frame.size.width, y;
+ NSSize size;
+
+ [[NSColor clearColor] setFill];
+ NSRectFill(frame);
+
+ if (bgFrame.size.width > 0) {
+ [[NSColor colorWithCalibratedWhite:0 alpha:0.75] setFill];
+ NSBezierPath* bp = [NSBezierPath bezierPath];
+ [bp appendRoundedRect:bgFrame radius:10];
+ [bp fill];
+ }
+
+ NSMutableParagraphStyle* ps = [[NSMutableParagraphStyle alloc] init];
+ [ps setAlignment:NSTextAlignmentCenter];
+
+ NSShadow* shadow = [[[NSShadow alloc] init] autorelease];
+ [shadow setShadowColor:[NSColor blackColor]];
+ [shadow setShadowBlurRadius:2];
+ [shadow setShadowOffset:NSMakeSize(2,-2)];
+
+ NSMutableDictionary* attr = [@{
+ NSFontAttributeName: [NSFont boldSystemFontOfSize:80],
+ NSForegroundColorAttributeName: [NSColor colorWithCalibratedWhite:1 alpha:0.8],
+ NSShadowAttributeName: shadow,
+ NSParagraphStyleAttributeName: [ps autorelease]
+ } mutableCopy];
+
+ if (_flags & NSEventModifierFlagCommand) {
+ NSString* commandKeyString = [NSString stringWithUTF8String:"\xe2\x8c\x98\x01"];
+ size = [commandKeyString sizeWithAttributes:attr];
+ y = (frame.size.height - size.height) / 2.0;
+ x -= oneQuarter;
+ [commandKeyString drawInRect:NSMakeRect(x, y, oneQuarter, size.height) withAttributes:attr];
+ }
+
+ if (_flags & NSEventModifierFlagShift) {
+ NSString* shiftKeyString = [NSString stringWithUTF8String:"\xe2\x87\xa7\x01"];
+ size = [shiftKeyString sizeWithAttributes:attr];
+ y = (frame.size.height - size.height) / 2.0;
+ x -= oneQuarter;
+ [shiftKeyString drawInRect:NSMakeRect(x, y, oneQuarter, size.height) withAttributes:attr];
+ }
+
+ if (_flags & NSEventModifierFlagOption) {
+ NSString* altKeyString = [NSString stringWithUTF8String:"\xe2\x8c\xa5\x01"];
+ size = [altKeyString sizeWithAttributes:attr];
+ y = (frame.size.height - size.height) / 2.0;
+ x -= oneQuarter;
+ [altKeyString drawInRect:NSMakeRect(x, y, oneQuarter, size.height) withAttributes:attr];
+ }
+
+ if (_flags & NSEventModifierFlagControl) {
+ NSString* controlKeyString = [NSString stringWithUTF8String:"\xe2\x8c\x83\x01"];
+ size = [controlKeyString sizeWithAttributes:attr];
+ y = (frame.size.height - size.height) / 2.0;
+ x -= oneQuarter;
+ [controlKeyString drawInRect:NSMakeRect(x, y, oneQuarter, size.height) withAttributes:attr];
+ }
+
+ if (_flags & NSEventModifierFlagFunction) {
+ NSString* controlKeyString = [NSString stringWithUTF8String:"fn"];
+ size = [controlKeyString sizeWithAttributes:attr];
+ y = (frame.size.height - size.height) / 2.0;
+ x -= oneQuarter;
+ [controlKeyString drawInRect:NSMakeRect(x, y, oneQuarter, size.height) withAttributes:attr];
+ }
+}
+
+- (void)noteFlagsChanged:(uint32_t)flags {
+ _flags = flags;
+ NSRect frame = self.frame;
+ frame.size.width = MODS_WIDTH * (CGFloat)[self flagsCount];
+ self.frame = frame;
+ [self setNeedsDisplay:YES];
+}
+
+@end
+
+@implementation ModsVisualizer
+
+- (NSString *)visualizerName {
+ return @"Mods";
+}
+
+- (id)init {
+ if (!(self = [super init]))
+ return nil;
+
+ // autosave frame was not working, despite best efforts. Easy workaround to use defaults instead.
+ // (and autosave frame _uses_ defaults anyway so same thing in the end?)
+ NSString *frameValue = [[NSUserDefaults standardUserDefaults] stringForKey:@"mods.savedFrame"];
+ NSRect windowFrame = { MODS_WIDTH, 100, 0, 100 };
+ if (frameValue) {
+ windowFrame = NSRectFromString(frameValue);
+ }
+
+ _visualizerWindow = [[NSWindow alloc]
+ initWithContentRect:windowFrame
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ [_visualizerWindow setLevel:NSScreenSaverWindowLevel];
+ [_visualizerWindow setBackgroundColor:[NSColor clearColor]];
+ [_visualizerWindow setMovableByWindowBackground:YES];
+ [_visualizerWindow setFrame:windowFrame display:NO];
+ [_visualizerWindow setOpaque:NO];
+ [_visualizerWindow setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
+
+ _visualizerView = [[ModsVisualizerView alloc] init];
+ [_visualizerView noteFlagsChanged:0];
+ [_visualizerWindow setContentView:_visualizerView];
+
+ return self;
+}
+
+- (void)dealloc {
+ [_visualizerWindow release];
+ [_visualizerView release];
+ [super dealloc];
+}
+
+- (void)showVisualizer:(id)sender {
+ [_visualizerWindow orderFront:self];
+}
+
+- (void)hideVisualizer:(id)sender {
+ [_visualizerWindow orderOut:self];
+}
+
+- (void)deactivateVisualizer:(id)sender {
+ [_visualizerWindow orderOut:self];
+}
+
+- (void)noteFlagsChanged:(uint32_t)flags {
+ [_visualizerView noteFlagsChanged:flags];
+ NSRect windowFrame = _visualizerWindow.frame;
+ NSScreen *screen = _visualizerWindow.screen;
+ if (!screen) {
+ for (NSScreen *s in NSScreen.screens) {
+ if (CGRectContainsPoint(s.frame, windowFrame.origin)) {
+ screen = s;
+ break;
+ }
+ }
+
+ if (!screen) {
+ screen = NSScreen.screens.firstObject;
+ }
+ }
+
+ NSRect screenFrame = screen.frame;
+ CGFloat screenX = windowFrame.origin.x - screenFrame.origin.x;
+ if (screenX > screenFrame.size.width / 2) {
+ CGFloat right = windowFrame.origin.x + windowFrame.size.width;
+ windowFrame.size.width = _visualizerView.frame.size.width;
+ windowFrame.origin.x = right - windowFrame.size.width;
+ } else {
+ windowFrame.size.width = _visualizerView.frame.size.width;
+ }
+ [_visualizerWindow setFrame:windowFrame display:NO];
+ [[NSUserDefaults standardUserDefaults] setValue:NSStringFromRect(_visualizerWindow.frame) forKey:@"mods.savedFrame"];
+}
+
+- (void)noteKeyEvent:(KCKeycastrEvent *)event {}
+
+- (void)noteMouseEvent:(KCMouseEvent *)mouseEvent {}
+
++ (NSDictionary *)visualizerDefaults {
+ return @{};
+}
+
+@end