├── .gitignore ├── Icon@2x.png ├── Icon@3x.png ├── Icon_512.png ├── SwipeSelection.plist ├── README.md ├── control ├── Makefile └── Tweak.xm /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .theos/ 3 | packages/ 4 | -------------------------------------------------------------------------------- /Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendonjkding/SwipeSelection/HEAD/Icon@2x.png -------------------------------------------------------------------------------- /Icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendonjkding/SwipeSelection/HEAD/Icon@3x.png -------------------------------------------------------------------------------- /Icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendonjkding/SwipeSelection/HEAD/Icon_512.png -------------------------------------------------------------------------------- /SwipeSelection.plist: -------------------------------------------------------------------------------- 1 | Filter = {Bundles = ("com.apple.UIKit");}; 2 | SupportsRoot = 1; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SwipeSelection 2 | ============== 3 | 4 | An improvement to iOS's text editing that allows you to move the cursor and select text using gestures on the keyboard itself. 5 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.iky1e.swipeselection 2 | Name: SwipeSelection 3 | Depends: mobilesubstrate 4 | Version: 1.5.3 5 | Architecture: iphoneos-arm 6 | Description: Swipe along the keyboard to move the cursor and select text. 7 | Maintainer: Kyle Howells 8 | Author: Kyle Howells 9 | Section: Tweaks 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifdef SIMULATOR 2 | TARGET = simulator:clang:latest:8.0 3 | else 4 | TARGET = iphone:clang:latest:7.0 5 | ifeq ($(debug),0) 6 | ARCHS= armv7 arm64 arm64e 7 | else 8 | ARCHS= arm64 arm64e 9 | endif 10 | endif 11 | 12 | TWEAK_NAME = SwipeSelection 13 | SwipeSelection_CFLAGS = -fobjc-arc 14 | SwipeSelection_FILES = Tweak.xm 15 | SwipeSelection_FRAMEWORKS = UIKit Foundation CoreGraphics 16 | 17 | ADDITIONAL_CFLAGS += -Wno-error=unused-variable -Wno-error=unused-function 18 | 19 | include $(THEOS)/makefiles/common.mk 20 | include $(THEOS_MAKE_PATH)/tweak.mk 21 | 22 | 23 | after-install:: 24 | install.exec "killall -9 SpringBoard" 25 | -------------------------------------------------------------------------------- /Tweak.xm: -------------------------------------------------------------------------------- 1 | // **************************************************** // 2 | // **************************************************** // 3 | // ********** Design outline ********** // 4 | // **************************************************** // 5 | // **************************************************** // 6 | // 7 | // 1 finger moves the cursour 8 | // 2 fingers moves it one word at a time 9 | // 10 | // Should be able to move between 1 and 2 fingers without lifting your hand. 11 | // If a selection has been made and you move right the selection starts moving from the end. 12 | // - else it starts at the beginning. 13 | // 14 | // Holding shift selects text between the starting point and the destination. 15 | // - the starting point is the reverse of the non selection movement. 16 | // - - movement to the right starts at the start of existing selections. 17 | // 18 | // Movement upwards when in 2 finger mode should jump to the nearest word in the new line. 19 | // - But another movement up again (without sideways movement) will jump to the nearest word to the originals x location, 20 | // - - this ensures that the cursour doesn't jump about moving far away from it's start point. 21 | // 22 | 23 | 24 | #import 25 | #import 26 | #import 27 | #import 28 | #import 29 | #import 30 | 31 | 32 | #pragma mark - Headers 33 | 34 | /// iOS 7 Task Execution 35 | @class UIKeyboardTaskExecutionContext; 36 | 37 | @interface UIKeyboardTaskQueue : NSObject 38 | @property(retain, nonatomic) UIKeyboardTaskExecutionContext *executionContext; 39 | 40 | -(BOOL)isMainThreadExecutingTask; 41 | -(void)performTask:(id)arg1; 42 | -(void)waitUntilAllTasksAreFinished; 43 | -(void)addDeferredTask:(id)arg1; 44 | -(void)addTask:(id)arg1; 45 | -(void)promoteDeferredTaskIfIdle; 46 | -(void)performDeferredTaskIfIdle; 47 | -(void)performTaskOnMainThread:(id)arg1 waitUntilDone:(void)arg2; 48 | -(void)finishExecution; 49 | -(void)continueExecutionOnMainThread; 50 | -(void)unlock; 51 | -(BOOL)tryLockWhenReadyForMainThread; 52 | -(void)lockWhenReadyForMainThread; 53 | -(void)lock; 54 | @end 55 | 56 | @interface UIKeyboardTaskExecutionContext : NSObject 57 | @property(readonly, nonatomic) UIKeyboardTaskQueue *executionQueue; 58 | 59 | -(void)transferExecutionToMainThreadWithTask:(id)arg1; 60 | -(void)returnExecutionToParent; 61 | -(id)childWithContinuation:(id)arg1; 62 | -(id)initWithParentContext:(id)arg1 continuation:(id)arg2; 63 | -(id)initWithExecutionQueue:(id)arg1; 64 | @end 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | @protocol UITextInputPrivate //, UITextInputTraits_Private, UITextSelectingContainer> 73 | -(BOOL)shouldEnableAutoShift; 74 | -(NSRange)selectionRange; 75 | -(CGRect)rectForNSRange:(NSRange)nsrange; 76 | -(NSRange)_markedTextNSRange; 77 | //-(id)selectedDOMRange; 78 | //-(id)wordInRange:(id)range; 79 | //-(void)setSelectedDOMRange:(id)range affinityDownstream:(BOOL)downstream; 80 | //-(void)replaceRangeWithTextWithoutClosingTyping:(id)textWithoutClosingTyping replacementText:(id)text; 81 | //-(CGRect)rectContainingCaretSelection; 82 | -(void)moveBackward:(unsigned)backward; 83 | -(void)moveForward:(unsigned)forward; 84 | -(unsigned short)characterBeforeCaretSelection; 85 | -(id)wordContainingCaretSelection; 86 | -(id)wordRangeContainingCaretSelection; 87 | -(id)markedText; 88 | -(void)setMarkedText:(id)text; 89 | -(BOOL)hasContent; 90 | -(void)selectAll; 91 | -(id)textColorForCaretSelection; 92 | -(id)fontForCaretSelection; 93 | -(BOOL)hasSelection; 94 | @end 95 | 96 | 97 | 98 | /** iOS 5-6 **/ 99 | @interface UIKBShape : NSObject 100 | @end 101 | 102 | @interface UIKBKey : UIKBShape 103 | @property(copy) NSString * name; 104 | @property(copy) NSString * representedString; 105 | @property(copy) NSString * displayString; 106 | @property(copy) NSString * displayType; 107 | @property(copy) NSString * interactionType; 108 | @property(copy) NSString * variantType; 109 | //@property(copy) UIKBAttributeList * attributes; 110 | @property(copy) NSString * overrideDisplayString; 111 | @property(copy) NSString * clientVariantRepresentedString; 112 | @property(copy) NSString * clientVariantActionName; 113 | @property BOOL visible; 114 | @property BOOL hidden; 115 | @property BOOL disabled; 116 | @property BOOL isGhost; 117 | @property int splitMode; 118 | @end 119 | 120 | 121 | /** iOS 7 **/ 122 | @interface UIKBTree : NSObject 123 | +(id)keyboard; 124 | +(id)key; 125 | +(id)shapesForControlKeyShapes:(id)arg1 options:(int)arg2; 126 | +(id)mergeStringForKeyName:(id)arg1; 127 | +(BOOL)shouldSkipCacheString:(id)arg1; 128 | +(id)stringForType:(int)arg1; 129 | +(id)treeOfType:(int)arg1; 130 | +(id)uniqueName; 131 | 132 | @property(retain, nonatomic) NSString *layoutTag; 133 | @property(retain, nonatomic) NSMutableDictionary *cache; 134 | @property(retain, nonatomic) NSMutableArray *subtrees; 135 | @property(retain, nonatomic) NSMutableDictionary *properties; 136 | @property(retain, nonatomic) NSString *name; 137 | @property(nonatomic) int type; 138 | 139 | -(int)flickDirection; 140 | 141 | - (BOOL)isLeafType; 142 | - (BOOL)usesKeyCharging; 143 | - (BOOL)usesAdaptiveKeys; 144 | - (BOOL)modifiesKeyplane; 145 | - (BOOL)avoidsLanguageIndicator; 146 | - (BOOL)isAlphabeticPlane; 147 | - (BOOL)noLanguageIndicator; 148 | - (BOOL)isLetters; 149 | - (BOOL)subtreesAreOrdered; 150 | 151 | @end 152 | 153 | 154 | @interface UIKeyboardLayout : UIView 155 | -(UIKBKey*)keyHitTest:(CGPoint)point; 156 | -(long long)idiom; 157 | @end 158 | 159 | @interface UIKeyboardLayoutStar : UIKeyboardLayout 160 | // iOS 7 161 | -(id)keyHitTest:(CGPoint)arg1; 162 | -(id)keyHitTestWithoutCharging:(CGPoint)arg1; 163 | -(id)keyHitTestClosestToPoint:(CGPoint)arg1; 164 | -(id)keyHitTestContainingPoint:(CGPoint)arg1; 165 | 166 | -(BOOL)SS_shouldSelect; 167 | -(BOOL)SS_disableSwipes; 168 | -(BOOL)SS_isKanaKey; 169 | -(BOOL)isShiftKeyBeingHeld; 170 | -(void)deleteAction; 171 | @end 172 | 173 | 174 | @interface UIKeyboardImpl : UIView 175 | +(UIKeyboardImpl*)sharedInstance; 176 | +(UIKeyboardImpl*)activeInstance; 177 | @property (readonly, assign, nonatomic) UIResponder *privateInputDelegate; 178 | @property (readonly, assign, nonatomic) UIResponder *inputDelegate; 179 | -(BOOL)isLongPress; 180 | -(id)_layout; 181 | -(BOOL)callLayoutIsShiftKeyBeingHeld; 182 | -(void)handleDelete; 183 | -(void)handleDeleteAsRepeat:(BOOL)repeat; 184 | -(void)handleDeleteWithNonZeroInputCount; 185 | -(void)stopAutoDelete; 186 | -(BOOL)handwritingPlane; 187 | 188 | -(void)updateForChangedSelection; 189 | 190 | // SwipeSelection 191 | -(void)_KHKeyboardGestureDidPan:(UIPanGestureRecognizer*)gesture; 192 | -(void)SS_revealSelection:(UIView*)inputView; 193 | @property (nonatomic,strong) UIPanGestureRecognizer *SS_pan; 194 | 195 | @property (nonatomic,retain) id feedbackBehavior;//iOS 10 196 | @property (nonatomic,retain) id feedbackGenerator;//iOS11 12 197 | -(void)playKeyClickSound:(BOOL)arg1 ;// iOS 13 198 | -(void)playDeleteKeyFeedback:(BOOL)arg1 ;//iOS 14 199 | @end 200 | 201 | 202 | @interface UIFieldEditor : NSObject 203 | +(UIFieldEditor*)sharedFieldEditor; 204 | -(void)revealSelection; 205 | @end 206 | 207 | 208 | //@interface UIWebDocumentView : UIView 209 | //-(void)_scrollRectToVisible:(CGRect)visible animated:(BOOL)animated; 210 | //-(void)scrollSelectionToVisible:(BOOL)visible; 211 | //@end 212 | 213 | 214 | @interface UIView(Private_text) 215 | // UIWebDocumentView 216 | -(void)_scrollRectToVisible:(CGRect)visible animated:(BOOL)animated; 217 | -(void)scrollSelectionToVisible:(BOOL)visible; 218 | 219 | // UITextInputPrivate 220 | -(CGRect)caretRect; 221 | -(void)_scrollRectToVisible:(CGRect)visible animated:(BOOL)animated; 222 | 223 | -(NSRange)selectedRange; 224 | -(NSRange)selectionRange; 225 | -(void)setSelectedRange:(NSRange)range; 226 | -(void)setSelectionRange:(NSRange)range; 227 | -(void)scrollSelectionToVisible:(BOOL)arg1; 228 | -(CGRect)rectForSelection:(NSRange)range; 229 | -(CGRect)textRectForBounds:(CGRect)rect; 230 | @end 231 | 232 | 233 | // Safari webview 234 | @interface WKContentView : UIView 235 | -(void)moveByOffset:(NSInteger)offset; 236 | -(id)_moveLeft:(BOOL)arg1 withHistory:(id)arg2 ; 237 | -(id)_moveRight:(BOOL)arg1 withHistory:(id)arg2 ; 238 | @end 239 | 240 | @interface UIResponder() 241 | -(id)interactionAssistant; 242 | @end 243 | 244 | @interface UITextInteractionAssistant : NSObject 245 | -(id)selectionView; 246 | @end 247 | 248 | @interface UITextSelectionView 249 | - (void)showCalloutBarAfterDelay:(double)arg1; 250 | @end 251 | 252 | 253 | @interface UIDevice() 254 | -(void)_playSystemSound:(unsigned)arg1 ; 255 | @end 256 | 257 | @interface _UIKeyboardFeedbackGenerator : UIFeedbackGenerator 258 | -(void)_playFeedbackForActionType:(long long)arg1 withCustomization:(/*^block*/id)arg2 ; 259 | @end 260 | 261 | @interface _UIFeedbackKeyboardBehavior : UIFeedbackGenerator 262 | -(void)_playFeedbackForActionType:(long long)arg1 withCustomization:(/*^block*/id)arg2 ; 263 | @end 264 | 265 | #pragma mark - Helper functions 266 | 267 | UITextPosition *KH_MovePositionDirection(id tokenizer, UITextPosition *startPosition, UITextDirection direction){ 268 | if (tokenizer && startPosition) { 269 | return [tokenizer positionFromPosition:startPosition inDirection:direction offset:1]; 270 | } 271 | return nil; 272 | } 273 | 274 | UITextPosition *KH_tokenizerMovePositionWithGranularitInDirection(id tokenizer, UITextPosition *startPosition, UITextGranularity granularity, UITextDirection direction){ 275 | 276 | if (tokenizer && startPosition) { 277 | return [tokenizer positionFromPosition:startPosition toBoundary:granularity inDirection:direction]; 278 | } 279 | 280 | return nil; 281 | } 282 | 283 | BOOL KH_positionsSame(id tokenizer, UITextPosition *position1, UITextPosition *position2){ 284 | if([tokenizer isKindOfClass:[%c(WKContentView) class]]) return position1==position2; 285 | return ([tokenizer comparePosition:position1 toPosition:position2] == NSOrderedSame); 286 | } 287 | 288 | 289 | 290 | 291 | 292 | // AltKeyboard2 compatibility 293 | Class AKFlickGestureRecognizer(){ 294 | static Class AKFlickGestureRecognizer_Class = nil; 295 | static BOOL checked = NO; 296 | 297 | if (!checked) { 298 | AKFlickGestureRecognizer_Class = objc_getClass("AKFlickGestureRecognizer"); 299 | } 300 | 301 | return AKFlickGestureRecognizer_Class; 302 | } 303 | 304 | 305 | #pragma mark - GestureRecognizer 306 | @interface SSPanGestureRecognizer : UIPanGestureRecognizer 307 | @end 308 | 309 | #pragma mark - GestureRecognizer implementation 310 | @implementation SSPanGestureRecognizer 311 | -(BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer{ 312 | 313 | if ([preventingGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && 314 | ([preventingGestureRecognizer isKindOfClass:AKFlickGestureRecognizer()] == NO)) 315 | { 316 | return YES; 317 | } 318 | 319 | return NO; 320 | } 321 | 322 | - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer{ 323 | return NO; 324 | } 325 | @end 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | #pragma mark - Hooks 334 | %hook UIKeyboardImpl 335 | 336 | %property (nonatomic,strong) UIPanGestureRecognizer *SS_pan; 337 | 338 | -(id)initWithFrame:(CGRect)rect{ 339 | id orig = %orig; 340 | 341 | if (orig){ 342 | SSPanGestureRecognizer *pan = [[SSPanGestureRecognizer alloc] initWithTarget:self action:@selector(SS_KeyboardGestureDidPan:)]; 343 | pan.cancelsTouchesInView = NO; 344 | [self addGestureRecognizer:pan]; 345 | [self setSS_pan:pan]; 346 | } 347 | 348 | return orig; 349 | } 350 | -(instancetype)initWithFrame:(CGRect)arg1 forCustomInputView:(BOOL)arg2 { 351 | id orig = %orig; 352 | 353 | if (orig){ 354 | SSPanGestureRecognizer *pan = [[SSPanGestureRecognizer alloc] initWithTarget:self action:@selector(SS_KeyboardGestureDidPan:)]; 355 | pan.cancelsTouchesInView = NO; 356 | [self addGestureRecognizer:pan]; 357 | [self setSS_pan:pan]; 358 | } 359 | 360 | return orig; 361 | } 362 | 363 | %new 364 | -(void)SS_KeyboardGestureDidPan:(UIPanGestureRecognizer*)gesture{ 365 | // Location info (may change) 366 | static UITextRange *startingtextRange = nil; 367 | static CGPoint previousPosition; 368 | 369 | // Webview fix 370 | static CGFloat xOffset = 0; 371 | static CGPoint realPreviousPosition; 372 | 373 | // Basic info 374 | static BOOL shiftHeldDown = NO; 375 | static BOOL hasStarted = NO; 376 | static BOOL longPress = NO; 377 | static BOOL handWriting = NO; 378 | static BOOL haveCheckedHand = NO; 379 | static BOOL isFirstShiftDown = NO; // = first run of the code shift is held, then pick the pivot point 380 | static BOOL isMoreKey = NO; 381 | static BOOL isKanaKey = NO; 382 | static int touchesWhenShiting = 0; 383 | static BOOL cancelled = NO; 384 | 385 | int touchesCount = [gesture numberOfTouches]; 386 | 387 | UIKeyboardImpl *keyboardImpl = self; 388 | 389 | if ([keyboardImpl respondsToSelector:@selector(isLongPress)]) { 390 | BOOL nLongTouch = [keyboardImpl isLongPress]; 391 | if (nLongTouch) { 392 | longPress = nLongTouch; 393 | } 394 | } 395 | 396 | // Get current layout 397 | id currentLayout = nil; 398 | if ([keyboardImpl respondsToSelector:@selector(_layout)]) { 399 | currentLayout = [keyboardImpl _layout]; 400 | } 401 | 402 | // Check more key, unless it's already ues 403 | if ([currentLayout respondsToSelector:@selector(SS_disableSwipes)] && !isMoreKey) { 404 | isMoreKey = [currentLayout SS_disableSwipes]; 405 | } 406 | 407 | // Hand writing recognition 408 | if ([currentLayout respondsToSelector:@selector(handwritingPlane)] && !haveCheckedHand) { 409 | handWriting = [currentLayout handwritingPlane]; 410 | } 411 | else if ([currentLayout respondsToSelector:@selector(subviews)] && !handWriting && !haveCheckedHand) { 412 | NSArray *subviews = [((UIView*)currentLayout) subviews]; 413 | for (UIView *subview in subviews) { 414 | 415 | if ([subview respondsToSelector:@selector(subviews)]) { 416 | NSArray *arrayToCheck = [subview subviews]; 417 | 418 | for (id view in arrayToCheck) { 419 | NSString *classString = [NSStringFromClass([view class]) lowercaseString]; 420 | NSString *substring = [@"Handwriting" lowercaseString]; 421 | 422 | if ([classString rangeOfString:substring].location != NSNotFound) { 423 | handWriting = YES; 424 | break; 425 | } 426 | } 427 | } 428 | } 429 | haveCheckedHand = YES; 430 | } 431 | haveCheckedHand = YES; 432 | 433 | 434 | 435 | // Check for shift key being pressed 436 | if ([currentLayout respondsToSelector:@selector(SS_shouldSelect)] && !shiftHeldDown) { 437 | shiftHeldDown = [currentLayout SS_shouldSelect]; 438 | isFirstShiftDown = YES; 439 | touchesWhenShiting = touchesCount; 440 | } 441 | 442 | if ([currentLayout respondsToSelector:@selector(SS_isKanaKey)]) { 443 | isKanaKey = [currentLayout SS_isKanaKey]; 444 | } 445 | 446 | 447 | // Get the text input 448 | id privateInputDelegate = nil; 449 | if ([keyboardImpl respondsToSelector:@selector(privateInputDelegate)]) { 450 | privateInputDelegate = (id)keyboardImpl.privateInputDelegate; 451 | } 452 | if (!privateInputDelegate && [keyboardImpl respondsToSelector:@selector(inputDelegate)]) { 453 | privateInputDelegate = (id)keyboardImpl.inputDelegate; 454 | } 455 | 456 | // Viber custom text view, which is super buggy with the tockenizer stuff. 457 | if (privateInputDelegate != nil && [NSStringFromClass([privateInputDelegate class]) isEqualToString:@"VBEmoticonsContentTextView"]) { 458 | privateInputDelegate = nil; 459 | cancelled = YES; // Try disabling it 460 | } 461 | 462 | 463 | 464 | 465 | // 466 | // Start Gesture stuff 467 | // 468 | if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) { 469 | 470 | if (hasStarted) 471 | { 472 | if ([privateInputDelegate respondsToSelector:@selector(selectedTextRange)]) { 473 | UITextRange *range = [privateInputDelegate selectedTextRange]; 474 | if (range && !range.empty) { 475 | UITextInteractionAssistant *assistant = [(UIResponder*)privateInputDelegate interactionAssistant]; 476 | if(assistant){ 477 | [[assistant selectionView] showCalloutBarAfterDelay:0]; 478 | } 479 | else{ 480 | CGRect screenBounds = [UIScreen mainScreen].bounds; 481 | CGRect rect = CGRectMake(screenBounds.size.width * 0.5, screenBounds.size.height * 0.5, 1, 1); 482 | 483 | if ([privateInputDelegate respondsToSelector:@selector(firstRectForRange:)]) { 484 | rect = [privateInputDelegate firstRectForRange:range]; 485 | } 486 | 487 | UIView *view = nil; 488 | if ([privateInputDelegate isKindOfClass:[UIView class]]) { 489 | view = (UIView*)privateInputDelegate; 490 | } 491 | else if ([privateInputDelegate respondsToSelector:@selector(inputDelegate)]) { 492 | id v = [keyboardImpl inputDelegate]; 493 | if (v != privateInputDelegate) { 494 | if ([v isKindOfClass:[UIView class]]) { 495 | view = (UIView*)v; 496 | } 497 | } 498 | } 499 | // Should fix this to actually get the onscreen rect 500 | UIMenuController *menu = [UIMenuController sharedMenuController]; 501 | [menu setTargetRect:rect inView:view]; 502 | [menu setMenuVisible:YES animated:YES]; 503 | } 504 | } 505 | } 506 | 507 | // Tell auto correct/suggestions the cursor has moved 508 | if ([keyboardImpl respondsToSelector:@selector(updateForChangedSelection)]) { 509 | [keyboardImpl updateForChangedSelection]; 510 | } 511 | } 512 | 513 | 514 | shiftHeldDown = NO; 515 | isMoreKey = NO; 516 | longPress = NO; 517 | hasStarted = NO; 518 | handWriting = NO; 519 | haveCheckedHand = NO; 520 | cancelled = NO; 521 | 522 | touchesCount = 0; 523 | touchesWhenShiting = 0; 524 | gesture.cancelsTouchesInView = NO; 525 | } 526 | else if (longPress || handWriting || !privateInputDelegate || isMoreKey || isKanaKey || cancelled) { 527 | return; 528 | } 529 | else if (gesture.state == UIGestureRecognizerStateBegan) { 530 | xOffset = 0; 531 | 532 | previousPosition = [gesture locationInView:self]; 533 | realPreviousPosition = previousPosition; 534 | 535 | if ([privateInputDelegate respondsToSelector:@selector(selectedTextRange)]) { 536 | startingtextRange = [privateInputDelegate selectedTextRange]; 537 | } 538 | } 539 | else if (gesture.state == UIGestureRecognizerStateChanged) { 540 | UITextRange *currentRange = startingtextRange; 541 | if ([privateInputDelegate respondsToSelector:@selector(selectedTextRange)]) { 542 | currentRange = nil; 543 | currentRange = [privateInputDelegate selectedTextRange]; 544 | } 545 | 546 | CGPoint position = [gesture locationInView:self]; 547 | CGPoint delta = CGPointMake(position.x - previousPosition.x, position.y - previousPosition.y); 548 | 549 | // Should we even run? 550 | CGFloat deadZone = 18; 551 | if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { 552 | deadZone = 30; 553 | } 554 | 555 | // If hasn't started, and it's either moved to little or the user swiped up (accents) kill it. 556 | if (hasStarted == NO && ABS(delta.y) > deadZone) { 557 | if (ABS(delta.y) > ABS(delta.x)) { 558 | cancelled = YES; 559 | } 560 | } 561 | if ((hasStarted == NO && delta.x < deadZone && delta.x > (-deadZone)) || cancelled) { 562 | return; 563 | } 564 | 565 | // We are running so shut other things off/down 566 | gesture.cancelsTouchesInView = YES; 567 | hasStarted = YES; 568 | 569 | // Make x & y positive for comparision 570 | CGFloat positiveX = ABS(delta.x); 571 | // CGFloat positiveY = ((delta.y >= 0) ? delta.y : (-delta.y)); 572 | 573 | // Determine the direction it should be going in 574 | UITextDirection textDirection; 575 | if (delta.x < 0) { 576 | textDirection = UITextStorageDirectionBackward; 577 | } 578 | else { 579 | textDirection = UITextStorageDirectionForward; 580 | } 581 | 582 | 583 | // Only do these new big 'jumps' if we've moved far enough 584 | CGFloat xMinimum = 10; 585 | // CGFloat yMinimum = 1; 586 | 587 | CGFloat neededTouches = 2; 588 | if (shiftHeldDown && (touchesWhenShiting >= 2)) { 589 | neededTouches = 3; 590 | } 591 | 592 | UITextGranularity granularity = UITextGranularityCharacter; 593 | // Handle different touches 594 | if (touchesCount >= neededTouches) { 595 | // make it skip words 596 | granularity = UITextGranularityWord; 597 | xMinimum = 20; 598 | } 599 | 600 | // Should we move the cusour or extend the current range. 601 | BOOL extendRange = shiftHeldDown; 602 | 603 | static UITextPosition *pivotPoint = nil; 604 | 605 | // Get the new range 606 | UITextPosition *positionStart = currentRange.start; 607 | UITextPosition *positionEnd = currentRange.end; 608 | 609 | // The moving position is 610 | UITextPosition *_position = nil; 611 | 612 | // If this is the first run we are selecting then pick our pivot point 613 | if (isFirstShiftDown) { 614 | if (delta.x > 0 || delta.y < -20) { 615 | pivotPoint = positionStart; 616 | } 617 | else { 618 | pivotPoint = positionEnd; 619 | } 620 | } 621 | if (extendRange && pivotPoint) { 622 | // Find which position isn't our pivot and move that. 623 | BOOL startIsPivot = KH_positionsSame(privateInputDelegate, pivotPoint, positionStart); 624 | if (startIsPivot) { 625 | _position = positionEnd; 626 | } 627 | else { 628 | _position = positionStart; 629 | } 630 | } 631 | else { 632 | _position = (delta.x > 0) ? positionEnd : positionStart; 633 | 634 | if (!pivotPoint) { 635 | pivotPoint = _position; 636 | } 637 | } 638 | 639 | 640 | // Is it right to left at the current selection point? 641 | if ([privateInputDelegate baseWritingDirectionForPosition:_position inDirection:UITextStorageDirectionForward] == UITextWritingDirectionRightToLeft) { 642 | if (textDirection == UITextStorageDirectionForward){ 643 | textDirection = UITextStorageDirectionBackward; 644 | } 645 | else { 646 | textDirection = UITextStorageDirectionForward; 647 | } 648 | } 649 | 650 | 651 | // Try and get the tockenizer 652 | id tokenizer = nil; 653 | if ([privateInputDelegate respondsToSelector:@selector(positionFromPosition:toBoundary:inDirection:)]) { 654 | tokenizer = privateInputDelegate; 655 | } 656 | else if ([privateInputDelegate respondsToSelector:@selector(tokenizer)]) { 657 | tokenizer = (id )privateInputDelegate.tokenizer; 658 | } 659 | 660 | if (tokenizer) { 661 | // Move X 662 | if (positiveX >= 1) { 663 | UITextPosition *_position_old = _position; 664 | 665 | if (granularity == UITextGranularityCharacter && 666 | [tokenizer respondsToSelector:@selector(positionFromPosition:inDirection:offset:)] && 667 | NO) { 668 | _position = KH_MovePositionDirection(tokenizer, _position, textDirection); 669 | } 670 | else { 671 | _position = KH_tokenizerMovePositionWithGranularitInDirection(tokenizer, _position, granularity, textDirection); 672 | } 673 | 674 | 675 | // If I tried to move it and got nothing back reset it to what I had. 676 | if (!_position){ _position = _position_old; } 677 | 678 | // If I tried to move it a word at a time and nothing happened 679 | if (granularity == UITextGranularityWord && (KH_positionsSame(privateInputDelegate, currentRange.start, _position) && 680 | !KH_positionsSame(privateInputDelegate, privateInputDelegate.beginningOfDocument, _position))) { 681 | 682 | _position = KH_tokenizerMovePositionWithGranularitInDirection(tokenizer, _position, UITextGranularityCharacter, textDirection); 683 | xMinimum = 4; 684 | } 685 | 686 | // Another sanity check 687 | if (!_position || positiveX < xMinimum){ 688 | _position = _position_old; 689 | } 690 | } 691 | 692 | // Move Y 693 | // if (positiveY >= yMinimum) { 694 | // UITextPosition *_position_old = _position; 695 | 696 | // 697 | // CGRect caretRect = [privateInputDelegate caretRectForPosition:_position]; 698 | // 699 | // CGFloat yDiff = delta.y * 0.8; 700 | // 701 | // CGPoint newLinePoint = CGPointMake(caretRect.origin.x + (caretRect.size.width * 0.5), caretRect.origin.y + (caretRect.size.height * 0.5) + yDiff); 702 | // newLinePoint = [[privateInputDelegate textInputView] convertPoint:newLinePoint toView:nil]; 703 | // _position = [privateInputDelegate closestPositionToPoint:newLinePoint]; 704 | // 705 | // if (!_position){ _position = _position_old; } 706 | // } 707 | } 708 | 709 | if (!extendRange && _position) { 710 | pivotPoint = _position; 711 | } 712 | 713 | // Get a new text range 714 | UITextRange *textRange = startingtextRange = nil; 715 | if ([privateInputDelegate respondsToSelector:@selector(textRangeFromPosition:toPosition:)]) { 716 | if([privateInputDelegate comparePosition:_position toPosition:pivotPoint] == NSOrderedAscending){ 717 | textRange = [privateInputDelegate textRangeFromPosition:_position toPosition:pivotPoint]; 718 | } 719 | else{ 720 | textRange = [privateInputDelegate textRangeFromPosition:pivotPoint toPosition:_position]; 721 | } 722 | } 723 | 724 | CGPoint oldPrevious = previousPosition; 725 | // Should I change X? 726 | if (positiveX > xMinimum) { //|| positiveY > yMinimum) { 727 | //CGFloat xDiff = ((delta.x < 0) ? (delta.x + xMinimum) : (delta.x - xMinimum)); 728 | //CGPoint accountForLeftOver = CGPointMake(position.x - xDiff, position.y); 729 | previousPosition = position; 730 | } 731 | 732 | isFirstShiftDown = NO; 733 | 734 | 735 | 736 | // 737 | // Handle Safari's broken UITextInput support 738 | // 739 | BOOL webView = [NSStringFromClass([privateInputDelegate class]) isEqualToString:@"WKContentView"]; 740 | if (webView) { 741 | xOffset += (position.x - realPreviousPosition.x); 742 | 743 | if (ABS(xOffset) >= xMinimum) { 744 | BOOL positive = (xOffset > 0); 745 | int offset = (ABS(xOffset) / xMinimum); 746 | BOOL isSelecting = pivotPoint!=_position; 747 | 748 | for (int i = 0; i < offset; i++) { 749 | if(positive){ 750 | [(WKContentView*)privateInputDelegate _moveRight:isSelecting withHistory:nil]; 751 | } 752 | else{ 753 | [(WKContentView*)privateInputDelegate _moveLeft:isSelecting withHistory:nil]; 754 | } 755 | } 756 | 757 | xOffset += (positive ? -(offset * xMinimum) : (offset * xMinimum)); 758 | } 759 | [self SS_revealSelection:(UIView*)privateInputDelegate]; 760 | } 761 | 762 | 763 | // 764 | // Normal text input 765 | // 766 | if (textRange && (oldPrevious.x != previousPosition.x || oldPrevious.y != previousPosition.y)) { 767 | [privateInputDelegate setSelectedTextRange:textRange]; 768 | [self SS_revealSelection:(UIView*)privateInputDelegate]; 769 | } 770 | 771 | realPreviousPosition = position; 772 | } 773 | } 774 | 775 | %new 776 | -(void)SS_revealSelection:(UIView*)inputView{ 777 | UIFieldEditor *fieldEditor = [objc_getClass("UIFieldEditor") sharedFieldEditor]; 778 | if (fieldEditor && [fieldEditor respondsToSelector:@selector(revealSelection)]) { 779 | [fieldEditor revealSelection]; 780 | } 781 | 782 | if ([inputView respondsToSelector:@selector(_scrollRectToVisible:animated:)]) { 783 | if ([inputView respondsToSelector:@selector(caretRect)]) { 784 | CGRect caretRect = [inputView caretRect]; 785 | [inputView _scrollRectToVisible:caretRect animated:NO]; 786 | } 787 | } 788 | else if ([inputView respondsToSelector:@selector(scrollSelectionToVisible:)]) { 789 | [inputView scrollSelectionToVisible:YES]; 790 | } 791 | } 792 | 793 | %end 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | // 805 | // Code from : @iamharicc 806 | // 807 | // iAmharic 808 | // 809 | 810 | 811 | static BOOL shiftByOtherKey = NO; 812 | static BOOL isLongPressed = NO; 813 | static BOOL isDeleteKey = NO; 814 | static BOOL isMoreKey = NO; 815 | static BOOL isKanaKey = NO; 816 | static BOOL g_deleteOnlyOnce; 817 | static int g_availableDeleteTimes; 818 | static NSSet *kanaKeys; 819 | 820 | %hook UIKeyboardLayoutStar 821 | /*==============touchesBegan================*/ 822 | -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 823 | UITouch *touch = [touches anyObject]; 824 | 825 | UIKBKey *keyObject = [self keyHitTest:[touch locationInView:touch.view]]; 826 | NSString *key = [[keyObject representedString] lowercaseString]; 827 | // NSLog(@"key=[%@] - keyObject=%@ - flickDirection = %d", key, keyObject, [(UIKBTree*)keyObject flickDirection]); 828 | 829 | 830 | // Delete key 831 | if ([key isEqualToString:@"delete"]) { 832 | isDeleteKey = YES; 833 | } 834 | else { 835 | isDeleteKey = NO; 836 | } 837 | 838 | 839 | // More key 840 | if ([key isEqualToString:@"more"]) { 841 | isMoreKey = YES; 842 | } 843 | else { 844 | isMoreKey = NO; 845 | } 846 | 847 | if ([kanaKeys containsObject:key]) { 848 | isKanaKey = YES; 849 | } 850 | else { 851 | isKanaKey = NO; 852 | } 853 | 854 | g_deleteOnlyOnce=NO; 855 | 856 | 857 | %orig; 858 | } 859 | 860 | /*==============touchesMoved================*/ 861 | -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 862 | UITouch *touch = [touches anyObject]; 863 | 864 | UIKBKey *keyObject = [self keyHitTest:[touch locationInView:touch.view]]; 865 | NSString *key = [[keyObject representedString] lowercaseString]; 866 | 867 | 868 | // Delete key (or the arabic key which is where the shift key would be) 869 | if ([key isEqualToString:@"delete"] || 870 | [key isEqualToString:@"ء"]) { 871 | shiftByOtherKey = YES; 872 | } 873 | 874 | // More key 875 | if ([key isEqualToString:@"more"]) { 876 | isMoreKey = YES; 877 | } 878 | else { 879 | isMoreKey = NO; 880 | } 881 | 882 | 883 | %orig; 884 | } 885 | 886 | -(void)touchesCancelled:(id)arg1 withEvent:(id)arg2 { 887 | %orig(arg1, arg2); 888 | 889 | shiftByOtherKey = NO; 890 | isLongPressed = NO; 891 | isMoreKey = NO; 892 | } 893 | 894 | /*==============touchesEnded================*/ 895 | -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 896 | %orig; 897 | 898 | isDeleteKey = NO; 899 | 900 | UITouch *touch = [touches anyObject]; 901 | NSString *key = [[[self keyHitTest:[touch locationInView:touch.view]] representedString] lowercaseString]; 902 | 903 | 904 | // Delete key 905 | if ([key isEqualToString:@"delete"] && !isLongPressed && !isKanaKey) { 906 | g_deleteOnlyOnce = YES; 907 | g_availableDeleteTimes = 1; 908 | UIKeyboardImpl *kb = [UIKeyboardImpl activeInstance]; 909 | if ([kb respondsToSelector:@selector(handleDelete)]) { 910 | [kb handleDelete]; 911 | } 912 | else if ([kb respondsToSelector:@selector(handleDeleteAsRepeat:)]) { 913 | [kb handleDeleteAsRepeat:NO]; 914 | } 915 | else if ([kb respondsToSelector:@selector(handleDeleteWithNonZeroInputCount)]) { 916 | [kb handleDeleteWithNonZeroInputCount]; 917 | } 918 | } 919 | 920 | 921 | shiftByOtherKey = NO; 922 | isLongPressed = NO; 923 | isMoreKey = NO; 924 | } 925 | 926 | 927 | 928 | // Old approach, keep incase the next one breaks anything 929 | //-(BOOL)isShiftKeyBeingHeld { 930 | // if (shiftByOtherKey) { 931 | // return YES; 932 | // } 933 | // 934 | // return %orig; 935 | //} 936 | 937 | %new 938 | -(BOOL)SS_shouldSelect{ 939 | return ([self isShiftKeyBeingHeld] || shiftByOtherKey); 940 | } 941 | 942 | %new 943 | -(BOOL)SS_disableSwipes{ 944 | return isMoreKey; 945 | } 946 | 947 | %new 948 | -(BOOL)SS_isKanaKey{ 949 | return isKanaKey; 950 | } 951 | %end 952 | 953 | 954 | 955 | 956 | 957 | /*==============UIKeyboardImpl================*/ 958 | %hook UIKeyboardImpl 959 | 960 | // Doesn't work to get long press on delete key but does for other keys. 961 | -(BOOL)isLongPress { 962 | isLongPressed = %orig; 963 | return isLongPressed; 964 | } 965 | 966 | // Legacy support (doesn't effect iOS 7 + so harmless leaving in & helps iOS 6) 967 | -(void)handleDelete { 968 | if (!isLongPressed && isDeleteKey) { 969 | 970 | } 971 | else { 972 | %orig; 973 | } 974 | } 975 | 976 | -(void)handleDeleteAsRepeat:(BOOL)repeat executionContext:(UIKeyboardTaskExecutionContext*)executionContext{ 977 | // Long press is simply meant to indicate if it's should repeat delete so repeat will do. 978 | isLongPressed = repeat; 979 | if ((!isLongPressed && isDeleteKey) 980 | || (g_deleteOnlyOnce && g_availableDeleteTimes<=0)) { 981 | if([[self _layout] respondsToSelector:@selector(idiom)]) 982 | { 983 | if([(UIKeyboardLayout *)[self _layout] idiom] == 2){ 984 | [[UIDevice currentDevice] _playSystemSound:1123LL]; 985 | } 986 | else{ 987 | if(IS_IOS_OR_NEWER(iOS_16_0)) {} 988 | else if(IS_IOS_OR_NEWER(iOS_14_0)) [self playDeleteKeyFeedback:repeat]; 989 | else if(IS_IOS_OR_NEWER(iOS_13_0)) [self playKeyClickSound:repeat]; 990 | else if(IS_IOS_OR_NEWER(iOS_11_0)) [[self feedbackGenerator] _playFeedbackForActionType:3 withCustomization:nil]; 991 | else if(IS_IOS_OR_NEWER(iOS_10_0)){ 992 | [[self feedbackBehavior] _playFeedbackForActionType:3 withCustomization:nil]; 993 | } 994 | } 995 | } 996 | [[executionContext executionQueue] finishExecution]; 997 | return; 998 | } 999 | 1000 | if(g_deleteOnlyOnce) g_availableDeleteTimes--; 1001 | 1002 | %orig; 1003 | } 1004 | 1005 | 1006 | //-(BOOL)handleKeyCommand:(id)arg1 repeatOkay:(BOOL*)arg2{ %log; return %orig; } 1007 | %end 1008 | 1009 | @interface _UIKeyboardTextSelectionInteraction 1010 | - (id)owner; 1011 | @end 1012 | %hook _UIKeyboardTextSelectionInteraction 1013 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ 1014 | id delegate = [[self owner] delegate]; 1015 | if([delegate respondsToSelector:@selector(SS_pan)] && [[delegate SS_pan] state] == UIGestureRecognizerStateChanged) return NO; 1016 | return %orig; 1017 | } 1018 | %end //_UIKeyboardTextSelectionInteraction 1019 | 1020 | %ctor{ 1021 | kanaKeys = [NSSet setWithArray:@[@"あ",@"か",@"さ",@"た",@"な",@"は",@"ま",@"や",@"ら",@"わ",@"、"]]; 1022 | } 1023 | --------------------------------------------------------------------------------