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