// // KBKeyboardView.m // BasiliskII // // Created by Jesús A. Álvarez on 22/03/2014. // Copyright (c) 2014-2018 namedfork. All rights reserved. // #import "KBKeyboardView.h" #import "KBKey.h" @implementation KBKeyboardView { NSMutableArray *keyPlanes, *emptyKeyPlanes; NSMutableSet *modifiers; NSMutableIndexSet *keysDown; CGAffineTransform defaultKeyTransform; CGFloat fontSize; CGSize selectedSize; UIEdgeInsets safeAreaInsets; CGSize intrinsicSize; } - (instancetype)initWithFrame:(CGRect)frame safeAreaInsets:(UIEdgeInsets)insets { self = [super initWithFrame:frame]; if (self) { safeAreaInsets = insets; intrinsicSize = frame.size; #if !defined(TARGET_OS_VISION) || TARGET_OS_VISION == 0 if (@available(iOS 13.0, *)) { self.backgroundColor = [UIColor clearColor]; UIVisualEffectView *backgroundView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemThickMaterial]]; backgroundView.frame = self.bounds; [self addSubview: backgroundView]; } else { self.backgroundColor = [UIColor colorWithRed:0xEB / 255.0 green:0xF0 / 255.0 blue:0xF7 / 255.0 alpha:0.9]; } #endif modifiers = [NSMutableSet setWithCapacity:4]; keysDown = [NSMutableIndexSet indexSet]; self.autoresizesSubviews = NO; } return self; } - (instancetype)initWithFrame:(CGRect)frame { return [self initWithFrame:frame safeAreaInsets:UIEdgeInsetsZero]; } - (CGSize)intrinsicContentSize { return intrinsicSize; } - (BOOL)isCompactKeyboardSize:(CGSize)size { return size.width < 768.0; } #if defined(TARGET_OS_VISION) && TARGET_OS_VISION == 1 - (void)layoutSubviews { // TODO: optimize this if needed CGSize size = self.bounds.size; CGFloat scale = size.width / intrinsicSize.width; CGAffineTransform transform = CGAffineTransformMakeScale(scale, scale); if (!CGAffineTransformEqualToTransform(transform, defaultKeyTransform)) { defaultKeyTransform = transform; keyPlanes = emptyKeyPlanes.mutableCopy; fontSize = 30.0 * scale; [self switchToKeyPlane:0]; } } #endif - (CGSize)findBestSizeForWidth:(CGFloat)preferredWidth inArray:(NSArray*)sizes { CGSize selectedSize = CGSizeZero; for (NSValue *key in sizes) { CGSize size = key.CGSizeValue; if (size.width > selectedSize.width && size.width <= preferredWidth) { selectedSize = size; if (size.width == preferredWidth) { // exact match break; } } } // still not found, use smallest width if (CGSizeEqualToSize(selectedSize, CGSizeZero)) { selectedSize = sizes.firstObject.CGSizeValue; } return selectedSize; } - (void)setLayout:(KBKeyboardLayout *)layout { if ([_layout isEqual:layout]) { return; } _layout = layout; // find preferred size (same width or smaller) CGRect safeFrame = UIEdgeInsetsInsetRect(self.frame, safeAreaInsets); CGFloat frameWidth = safeFrame.size.width; CGFloat preferredWidth = frameWidth; // find best size in class BOOL isCompactSize = self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact; NSArray *sizes = [[layout.availableSizes filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSValue *size, NSDictionary * _Nullable bindings) { return [self isCompactKeyboardSize:size.CGSizeValue] == isCompactSize; }]] sortedArrayUsingComparator:^NSComparisonResult(NSValue * _Nonnull size1, NSValue * _Nonnull size2) { return size1.CGSizeValue.width - size2.CGSizeValue.width; }]; selectedSize = [self findBestSizeForWidth:preferredWidth inArray:sizes]; defaultKeyTransform = CGAffineTransformIdentity; fontSize = [self isCompactKeyboardSize:selectedSize] ? 22.0 : 30.0; if (preferredWidth != selectedSize.width || preferredWidth != frameWidth) { // adjust width if (frameWidth / selectedSize.width > 2 || frameWidth / selectedSize.width < 0.5) { // iphone keyboard on ipad? defaultKeyTransform = CGAffineTransformMakeScale(frameWidth / selectedSize.width, frameWidth / selectedSize.width); } else if (frameWidth < selectedSize.width) { // shrink keyboard defaultKeyTransform = CGAffineTransformMakeScale(frameWidth / selectedSize.width, 1.0); } else { // iPhone keyboard on bigger phone CGFloat wScale = safeFrame.size.width / selectedSize.width; defaultKeyTransform = CGAffineTransformMakeScale(wScale, 1.0); } } self.frame = CGRectMake(0, 0, safeFrame.size.width + safeAreaInsets.right + safeAreaInsets.left, selectedSize.height * defaultKeyTransform.d + safeAreaInsets.bottom); // init keyplanes array NSUInteger numberOfKeyPlanes = layout.numberOfKeyPlanes; keyPlanes = [NSMutableArray arrayWithCapacity:numberOfKeyPlanes]; for (int i = 0; i < numberOfKeyPlanes; i++) { [keyPlanes addObject:[NSNull null]]; } emptyKeyPlanes = keyPlanes.mutableCopy; [self switchToKeyPlane:0]; } - (void)_addKeyWithFrame:(CGRect)keyFrame scanCode:(int8_t)scancode fontScale:(CGFloat)fontScale dark:(BOOL)dark sticky:(BOOL)sticky toKeyPlane:(NSMutableArray*)keyPlane { KBKey *key = nil; if (@available(iOS 11, *)) { keyFrame.origin.x += safeAreaInsets.left; } if (scancode == VKC_HIDE) { key = [[KBHideKey alloc] initWithFrame:keyFrame]; #if defined(TARGET_OS_VISION) && TARGET_OS_VISION == 1 if (self.layoutMenu != nil) { key.showsMenuAsPrimaryAction = YES; key.menu = self.layoutMenu; } else { return; } #else [key addTarget:self action:@selector(hideKeyboard:) forControlEvents:UIControlEventTouchUpInside]; #endif } else if (scancode == VKC_SHIFT_CAPS) { key = [[KBShiftCapsKey alloc] initWithFrame:keyFrame]; key.scancode = KC_SHIFT; [key addTarget:self action:@selector(capsKey:) forControlEvents:KBKeyEventStickyKey]; } else if (sticky) { key = [[KBStickyKey alloc] initWithFrame:keyFrame]; key.scancode = scancode; [key addTarget:self action:@selector(stickyKey:) forControlEvents:KBKeyEventStickyKey]; } else { key = [[KBKey alloc] initWithFrame:keyFrame]; key.scancode = scancode; [key addTarget:self action:@selector(keyDown:) forControlEvents:UIControlEventTouchDown]; [key addTarget:self action:@selector(keyUp:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchDragExit | UIControlEventTouchCancel]; } key.dark = dark; NSString *label = [_layout labelForScanCode:scancode]; if ([label containsString:@"\n"]) { fontScale *= [self isCompactKeyboardSize:selectedSize] ? 0.6 : 0.65; } key.label = label; key.titleLabel.font = [UIFont systemFontOfSize:self->fontSize * fontScale weight:UIFontWeightRegular]; [keyPlane addObject:key]; } - (NSArray *)_loadKeyPlane:(NSUInteger)plane { NSMutableArray *keyPlane = [NSMutableArray arrayWithCapacity:64]; [_layout enumerateKeysForSize:selectedSize plane:plane transform:defaultKeyTransform usingBlock:^(int8_t scancode, CGRect keyFrame, CGFloat fontScale, BOOL dark, BOOL sticky) { [self _addKeyWithFrame:keyFrame scanCode:scancode fontScale:fontScale dark:dark sticky:sticky toKeyPlane:keyPlane]; }]; return keyPlane; } - (NSArray *)keys { return [self.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [KBKey class]]]; } - (NSArray *)stickyKeys { return [self.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [KBStickyKey class]]]; } - (void)switchToKeyPlane:(NSInteger)idx { if (idx < 0 || idx >= keyPlanes.count) { return; } // remove previous keys [self.keys makeObjectsPerformSelector:@selector(removeFromSuperview)]; // load keyplane if (keyPlanes[idx] == [NSNull null]) { keyPlanes[idx] = [self _loadKeyPlane:idx]; } // add keys for (KBKey *key in keyPlanes[idx]) { if ([key isKindOfClass:[KBStickyKey class]]) { // match modifiers KBStickyKey *stickyKey = (KBStickyKey *)key; stickyKey.down = [keysDown containsIndex:key.scancode]; if ([key isKindOfClass:[KBShiftCapsKey class]]) { KBShiftCapsKey *shiftCapsKey = (KBShiftCapsKey *)key; shiftCapsKey.capsLocked = [keysDown containsIndex:KC_CAPSLOCK]; } } [self addSubview:key]; } } - (void)keyDown:(KBKey *)key { int16_t scancode = key.scancode; if (scancode >= 0 && ![keysDown containsIndex:scancode]) { [keysDown addIndex:scancode]; [self.delegate keyDown:scancode]; } else if (scancode < 0) { [self switchToKeyPlane:(-scancode) - 1]; } } - (void)keyUp:(KBKey *)key { int16_t scancode = key.scancode; if (scancode >= 0 && [keysDown containsIndex:scancode]) { [keysDown removeIndex:scancode]; [self.delegate keyUp:scancode]; // up sticky keys [self.stickyKeys setValue:@NO forKey:@"down"]; } } - (void)hideKeyboard:(KBHideKey *)key { if ([self.delegate respondsToSelector:@selector(hideKeyboard:)]) { [self.delegate hideKeyboard:key]; } else { self.hidden = YES; } } - (void)stickyKey:(KBStickyKey *)key { if (key.down && ![keysDown containsIndex:key.scancode]) { [keysDown addIndex:key.scancode]; [self.delegate keyDown:key.scancode]; } else if (!key.down && [keysDown containsIndex:key.scancode]) { [keysDown removeIndex:key.scancode]; [self.delegate keyUp:key.scancode]; } } - (void)capsKey:(KBShiftCapsKey *)key { if (key.capsLocked && ![keysDown containsIndex:KC_CAPSLOCK]) { [keysDown addIndex:KC_CAPSLOCK]; [self.delegate keyDown:KC_CAPSLOCK]; } else if ([keysDown containsIndex:KC_CAPSLOCK]) { [keysDown removeIndex:KC_CAPSLOCK]; [self.delegate keyUp:KC_CAPSLOCK]; } else { [self stickyKey:key]; } } @end