1/** 2 * Yudit Unicode Editor Source File 3 * 4 * GNU Copyright (C) 1997-2020 Gaspar Sinai <gaspar@yudit.org> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License, version 2, 8 * dated June 1991. See file COPYYING for details. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program; if not, write to the Free Software 17 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 18 */ 19 20// https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html 21// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/TextViewTask.html 22 23#import <objc/objc-runtime.h> 24#import <AppKit/Appkit.h> 25#import <Foundation/Foundation.h> 26#import <swindow/sosx/SCocoaInput.h> 27 28#define DEBUG 0 29 30/* 31 Normal japanese input example: 32 selectedRange 9223372036854775807,0 33 hasMarkedText 34 firstRectForCharacterRange helper 35 validAttributesForMarkedText 36 setMarkedText s{ 37 NSUnderline = 1; 38 NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1"; 39 } 40 calling replaementMarkedRange 41 selectedRange 1,0 42 hasMarkedText 43 setMarkedText し{ 44 NSUnderline = 1; 45 NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1"; 46 } 47 selectedRange 1,0 48 hasMarkedText 49 setMarkedText しn{ 50 NSUnderline = 1; 51 NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1"; 52} 53 selectedRange 2,0 54*/ 55 56/* 57 Moved selection to suru 58 59replaementMarkedRange short 60replaceCharactersInRange 品川に行って買いもの{ 61 NSUnderline = 1; 62 NSUnderlineColor = "NSCalibratedWhiteColorSpace 0.17 1"; 63}する{ 64 NSUnderline = 2; 65 NSUnderlineColor = "Generic Gray Gamma 2.2 Profile colorspace 0 1"; 66} 67selectedRange 13,2 68*/ 69 70/* 71 * For Alt-latin 2019 72 NSTextInputClient hasMarkedText 73 NSTextInputClient validAttributesForMarkedText 74 setMarkedText ˝ 75 replaementMarkedRange short 76 replaceCharactersInRange ˝ 77 NSTextInputClient hasMarkedText 78 NSTextInputClient insertText ő 79 replaceCharactersInRange ő 80 */ 81 82// From https://gist.github.com/ishikawa/23049 83static const NSRange kEmptyRange = {NSNotFound, 0}; 84 85@implementation CocoaInput { 86 NSMutableAttributedString *_text; // NSTextInputClient 87 88 // [oldText][selectedRange[markedRange]..] 89 NSRange _selectedRange; 90 NSRange _markedRange; 91 92 SPreEditor* preEditor; 93 bool cocoaActive; 94} 95-(id) initWithFrame:(NSRect)frameRect { 96 self = [super initWithFrame:frameRect]; 97 _text = [[NSMutableAttributedString alloc] init]; 98 _selectedRange = _markedRange = kEmptyRange; 99 cocoaActive = false; 100 preEditor = nil; 101 return self; 102} 103 104- (void) appendCharacters: (id) aString { 105 // FIXME 106 if ([aString isKindOfClass: [NSAttributedString class]]) { 107 [_text appendAttributedString: aString]; 108 } else { 109 [[_text mutableString] appendString: aString]; 110 } 111#if DEBUG 112 NSLog(@"appendCharacters %@", aString); 113#endif 114 [self cocoaFireTextChanged]; 115} 116 117- (void) removeMarkedText { 118 if (_markedRange.location != NSNotFound) { 119 if (NSMaxRange(_markedRange) <= [_text length]) 120 [_text deleteCharactersInRange: _markedRange]; 121 _markedRange = _selectedRange = kEmptyRange; 122 } 123} 124 125- (void) replaceCharactersInRange: (NSRange) aRange 126 withText: (id) aString 127 effectiveRange: (NSRangePointer) effectiveRange 128{ 129#if DEBUG 130 NSLog(@"replaceCharactersInRange %@", aString); 131#endif 132 NSRange replacementRange = aRange; 133 134 if (replacementRange.location == NSNotFound) { 135 replacementRange.location = [_text length]; 136 replacementRange.length = 0; 137 } 138 // FIXME 139 if (NSMaxRange(replacementRange) > [_text length]) { 140 NSLog (@"Out of bounds: %@ for length %lu", 141 NSStringFromRange(replacementRange), 142 (unsigned long) [_text length]); 143 return; 144 } 145 if ([aString isKindOfClass: [NSAttributedString class]]) { 146 [_text replaceCharactersInRange: replacementRange 147 withAttributedString: aString]; 148 } else { 149 [_text replaceCharactersInRange: replacementRange 150 withString: aString]; 151 } 152 153 if (effectiveRange != NULL) { 154 *effectiveRange = NSMakeRange(replacementRange.location, [aString length]); 155 } 156} 157 158- (void) setMarkedText: (id) aString 159 selectedRange: (NSRange) selectedRange 160 replacementRange: (NSRange) replacementRange 161{ 162#if DEBUG 163 NSLog(@"setMarkedText %@", aString); 164#endif 165 NSRange effectiveRange; 166 167 [self replaceCharactersInRange: 168 [self replacementMarkedRange: replacementRange] 169 withText: aString 170 effectiveRange: &effectiveRange]; 171 if (selectedRange.location != NSNotFound) selectedRange.location += effectiveRange.location; 172 _selectedRange = selectedRange; 173 _markedRange = effectiveRange; 174 if ([aString length] == 0) [self removeMarkedText]; 175 [self cocoaFireTextChanged]; 176} 177 178- (NSRange) replacementMarkedRange: (NSRange) replacementRange { 179 NSRange markedRange = _markedRange; 180 181#if DEBUG 182 NSLog(@"replacementMarkedRange short"); 183#endif 184 if (markedRange.location == NSNotFound) markedRange = _selectedRange; 185 if (replacementRange.location != NSNotFound) { 186 NSRange newRange = markedRange; 187 newRange.location += replacementRange.location; 188 newRange.length += replacementRange.length; 189 if (NSMaxRange(newRange) <= NSMaxRange(markedRange)) { 190 markedRange = newRange; 191 } 192 } 193 194 return markedRange; 195} 196//----------------------------- NSResponder 197//X1 198- (void) deleteBackward: (id) sender { 199 const NSUInteger length = [_text length]; 200 if (length > 0) { 201 [_text deleteCharactersInRange: NSMakeRange(length - 1, 1)]; 202 [self cocoaFireTextChanged]; 203 } else { 204 self.cppWindow->listener->keyPressed (self.cppWindow, 205 SWindowListener::Key_BackSpace, "", false, false, false); 206 } 207} 208 209- (void) insertNewline: (id) sender { 210 const NSUInteger length = [_text length]; 211 if (length > 0) { 212 [self appendCharacters: @"\n"]; 213 [self cocoaFireTextChanged]; 214 } else { 215 self.cppWindow->listener->keyPressed (self.cppWindow, 216 SWindowListener::Key_Enter, "", false, false, false); 217 } 218} 219 220- (void) insertTab: (id) sender { 221 const NSUInteger length = [_text length]; 222// FIMXE 223 if (length > 0) { 224 [self appendCharacters: @"\t"]; 225 } else { 226 self.cppWindow->listener->keyPressed (self.cppWindow, 227 SWindowListener::Key_Tab, "\t", false, false, false); 228 } 229} 230 231- (void) insertText: (id) aString { 232 const NSUInteger length = [_text length]; 233// FIMXE 234 if (length > 0) { 235 [self appendCharacters: aString]; 236 } else { 237 //preEditor->preEditClearMarkedText(); 238 self.cppWindow->listener->keyPressed (self.cppWindow, 239 SWindowListener::Key_Undefined, [aString UTF8String], 240 false, false, false); 241 } 242} 243 244// http://mirror.informatimago.com/next/developer.apple.com/documentation/Cocoa/Reference/ApplicationKit/Java/Protocols/NSTextInput.html#//apple_ref/doc/uid/20000610/BAJBDHAD 245 246// ---------------------------- NSTextInputClient Begin --------------------- 247 248/* 249Returns attributed string at theRange. This method allows input mangers to query any range in text storage. 250An implementation of this method should be prepared theRange to be out-of-bounds. The InkWell text input service can ask for the contents of the text input client that extends beyond the document’s range. In this case, you should return the intersection of the document’s range and theRange. If the location of theRange is completely outside of the document’s range, return null. 251*/ 252- (NSAttributedString *) attributedSubstringFromRange: (NSRange) theRange { 253#if DEBUG 254// NSLog(@"NSTextInputClient attributedSubstringFromRange in"); 255#endif 256 NSAttributedString* ret = [self attributedSubstringForProposedRange:theRange actualRange:NULL]; 257#if DEBUG 258 NSLog(@"NSTextInputClient attributedSubstringFromRange ret %@", ret); 259#endif 260 return ret; 261} 262/* 263 Could not find doc, but needed. 264 Seriously: this is a security risk, return nothing. 265 */ 266- (NSAttributedString *) attributedSubstringForProposedRange: (NSRange) aRange 267 actualRange: (NSRangePointer) actualRange 268{ 269 NSAttributedString * ret = [[[NSAttributedString alloc] init] autorelease]; 270 271#if DEBUG 272 NSLog (@"NSTextInputClient attributedSubstringForProposedRange replacementRange -> %@", ret); 273#endif 274 return ret; 275} 276 277/* 278 Returns the index of the character whose frame rectangle includes thePoint. The returned index measures from the start of the receiver’s text storage. thePoint is in the screen coordinate system. Returns NSArray.NotFound if the cursor is not within a character. 279*/ 280- (NSUInteger) characterIndexForPoint: (NSPoint) thePoint { 281#if DEBUG 282 NSLog (@"NSTextInputClient characterIndexForPoint"); 283#endif 284 return 0; 285} 286 287/* 288Returns a number used to identify the receiver’s context to the input server. Each text view within an application should return a unique identifier (typically its address). However, multiple text views sharing the same text storage must all return the same identifier. 289*/ 290- (NSInteger) conversationIdentifier { 291#if DEBUG 292 NSLog (@"NSTextInputClient conversationIdentifier"); 293#endif 294 return (NSInteger) self; 295} 296/* 297Invokes aSelector if possible. If aSelector cannot be invoked, then doCommandBySelector should not pass this message up the responder chain. NSResponder also implements this method, and it does forward uninvokable commands up the responder chain, but a text view should not. A text view implementing the NSTextInput interface will inherit from NSView, which inherits from NSResponder, so your implementation of this method will override the one in NSResponder. It should not call super. 298See Also: interpretKeyEvents (NSResponder), doCommandBySelector (NSKeyBindingResponder) 299*/ 300// FIXME 301- (void) doCommandBySelector: (SEL) aSelector { 302#if DEBUG 303 NSLog (@"NSTextInputClient doCommandBySelector %@", 304 NSStringFromSelector(aSelector)); 305#endif 306 [super doCommandBySelector: aSelector]; 307} 308 309/* 310Returns the first frame rectangle for characters in theRange, in screen coordinates. If theRange spans multiple lines of text in the text view, the rectangle returned is the one for the characters in the first line. If the length of theRange is 0 (as it would be if there is nothing selected at the insertion point), the rectangle will coincide with the insertion point, and its width will be 0. 311*/ 312- (NSRect) firstRectForCharacterRange: (NSRange) theRange { 313#if DEBUG 314 NSLog (@"NSTextInputClient firstRectForCharacterRange actual"); 315#endif 316 return [self firstRectForCharacterRange:theRange actualRange:NULL]; 317} 318 319/* 320 * This is what is called. 321*/ 322- (NSRect) firstRectForCharacterRange: (NSRange) aRange 323 actualRange: (NSRangePointer) actualRange 324{ 325#if DEBUG 326 NSLog (@"NSTextInputClient firstRectForCharacterRange"); 327#endif 328//SGC1 329 if ( _markedRange.location == NSNotFound) { 330 return NSZeroRect; 331 } 332 if ( aRange.location == NSNotFound) { 333 return NSZeroRect; 334 } 335 if (aRange.location < _markedRange.location) { 336 return NSZeroRect; 337 } 338 SRectangle rect = preEditor->preEditGlyphRectangleUTF16( 339 aRange.location - _markedRange.location); 340 341//NSLog (@"arange.location=%lu markedrane.location=%lu arange.length=%lu", 342// aRange.location, _markedRange.location, aRange.length); 343 344 NSRect rectView = NSMakeRect((float)rect.originX, (float)rect.originY, 345 (float) rect.width, (float) rect.height); 346 NSRect rectWindow = [self convertRect:rectView toView: nil]; 347 NSRect rectScreen = [[self window] convertRectToScreen: rectWindow]; 348 // FIXME 349 //if (actualRange != nil) *actualRange = aRange; 350 return rectScreen; 351 352} 353 354/* 355Returns true if the receiver has marked text, false if it doesn’t. Unlike other methods in this protocol, this one is not called by an input server. The text view itself may call this method to determine whether there currently is marked text. NSTextView, for example, disables the Edit>Copy menu item when this method returns true. 356See Also: markedRange 357 */ 358- (BOOL) hasMarkedText { 359#if DEBUG 360 BOOL ret = _markedRange.location != NSNotFound; 361 NSLog (@"NSTextInputClient hasMarkedText %d", ret); 362#endif 363 return _markedRange.location != NSNotFound; 364} 365 366/* 367Returns the range of the marked text. The returned range measures from the start of the receiver’s text storage. The return value’s location is NSArray.NotFound, and its length is 0 if and only if hasMarkedText returns false. 368See Also: setMarkedTextAndSelectedRange, unmarkText, hasMarkedText 369*/ 370- (NSRange) markedRange { 371#if DEBUG 372 NSLog (@"NSTextInputClient markedRange"); 373#endif 374 return _markedRange; 375} 376 377/* 378 Returns the range of selected text. The returned range measures from the start of the receiver’s text storage. If there is no selection, the return value’s location is NSArray.NotFound, and its length is 0. 379See Also: setMarkedTextAndSelectedRange 380 */ 381- (NSRange) selectedRange { 382#if DEBUG 383 NSLog (@"NSTextInputClient selectedRange %lu,%lu", _selectedRange.location, _selectedRange.length); 384#endif 385 return _selectedRange; 386} 387 388/* 389 Replaces text in selRange within receiver’s text storage with the contents of aString, which the receiver must display distinctively to indicate that it is marked text. aString must be either a String or an NSAttributedString and not null. 390See Also: selectedRange, unmarkText 391*/ 392- (void) setMarkedText: (id) aString selectedRange: (NSRange) selRange { 393#if DEBUG 394 NSLog (@"NSTextInputClient setMarketText %@", aString); 395#endif 396 [self setMarkedText: aString 397 selectedRange: selRange 398 replacementRange: kEmptyRange]; 399} 400 401/* 402 Removes any marking from pending input text, and disposes of the marked text as it wishes. The text view should accept the marked text as if it had been inserted normally. 403See Also: selectedRange, setMarkedTextAndSelectedRange 404*/ 405- (void) unmarkText { 406#if DEBUG 407 NSLog (@"NSTextInputClient unmarkText"); 408#endif 409} 410 411/* 412Returns an array of String names for the attributes supported by the receiver. The input server may choose to use some of these attributes in the text it inserts or in marked text. Returns an empty array if no attributes are supported. See NSAttributedString for the set of string constants that you could return in the array. 413*/ 414- (NSArray *) validAttributesForMarkedText { 415#if DEBUG 416 NSLog(@"NSTextInputClient validAttributesForMarkedText"); 417#endif 418 const NSRange entireRange = NSMakeRange(0, [_text length]); 419 [_text addAttribute: NSFontAttributeName 420 value: [NSFont userFontOfSize: 18.0f] 421 range: entireRange]; 422 NSArray* ret = [NSArray arrayWithObjects: 423 NSUnderlineStyleAttributeName, 424 nil 425 ]; 426 return ret; 427 428} 429 430/* 431 Could not find doc, but needed. 432 */ 433- (void) insertText: (id) aString 434 replacementRange: (NSRange) replacementRange 435{ 436#if DEBUG 437 NSLog (@"NSTextInputClient insertText %@ replacementRange %lu,%lu", aString, 438 replacementRange.location, replacementRange.length); 439#endif 440 441 preEditor->preEditClearMarkedText(); 442 self.cppWindow->listener->keyPressed (self.cppWindow, 443 SWindowListener::Key_Undefined, [aString UTF8String], 444 false, false, false); 445 446 [self removeMarkedText]; 447 [self replaceCharactersInRange: replacementRange 448 withText: aString 449 effectiveRange: NULL]; 450 451} 452 453 454// Returns the baseline position of a given character relative 455// to the origin of rectangle returned by 456// firstRect(forCharacterRange:actualRange:) 457// not required. 458#if 0 459- (float) baselineDeltaForCharacterAt:(int) aAt { 460 // FIXME 461#if DEBUG 462 NSLog(@"baselineDeltaForCharacterAt %d", aAt); 463#endif 464 return 0.0; 465} 466#endif 467 468#if 0 469-(NSInteger) windowLevel { 470 // FIXME 471 NSLog(@"windowLevel"); 472 return 0; 473} 474#endif 475 476#if 0 477- (NSAttributedString *) attributedString { 478 return _text; 479} 480#endif 481 482-(BOOL) drawsVerticallyForCharacterAt: (int) aAt { 483#if DEBUG 484 NSLog(@"NSTextInputClient drawsVerticallyForCharacterAt"); 485#endif 486 return false; 487} 488 489- (NSAttributedString *) attributedString { 490 return _text; 491} 492 493// 494// ---------------------------- NSTextInputClient End ------------------------- 495- (BOOL) cocoaInputEnabled { 496 return cocoaActive; 497} 498 499- (void) cocoaClearInput { 500 if (![self cocoaInputEnabled]) return; 501 if ([_text length]==0) return; 502 if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) { 503#if DEBUG 504 NSLog(@"cocoaClearInput"); 505#endif 506 [ctxt discardMarkedText]; 507 _selectedRange = _markedRange = kEmptyRange; 508 [_text release]; 509 _text = [[NSMutableAttributedString alloc] init]; 510 [self cocoaFireTextChanged]; 511 } 512} 513 514- (void) cocoaStartMacKeyboard: (SPreEditor*) aPreEditor { 515 preEditor = aPreEditor; 516 if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) { 517#if DEBUG 518 NSLog(@"cocoaStartMacKeyboard: unmark text"); 519#endif 520 [ctxt discardMarkedText]; 521 // does not work. 522 //[ctxt invalidateCharacterCoordinates]; 523 [ctxt activate]; 524 //[view unmarkText]; 525 } 526 _selectedRange = _markedRange = kEmptyRange; 527 [_text release]; 528 _text = [[NSMutableAttributedString alloc] init]; 529 530#if DEBUG 531 NSLog(@"cocoaStartMacKeyboard"); 532#endif 533 cocoaActive = true; 534 preEditor->preEditClearMarkedText(); 535} 536 537- (BOOL) cocoaCommitInput { 538 if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) { 539 if (_markedRange.location == NSNotFound) { 540 return false; 541 } 542 [ctxt discardMarkedText]; 543 NSAttributedString* marked = [_text attributedSubstringFromRange:_markedRange]; 544 [_text release]; 545 _text = [[NSMutableAttributedString alloc] init]; 546 _selectedRange = _markedRange = kEmptyRange; 547 if ([marked length] > 0) { 548 preEditor->preEditClearMarkedText(); 549 self.cppWindow->listener->keyPressed (self.cppWindow, 550 SWindowListener::Key_Undefined, [[marked string] UTF8String], 551 false, false, false); 552 return true; 553 } 554 } 555 return false; 556} 557 558// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/TextViewTask.html 559- (void) cocoaInvalidateCoordinates { 560 if (![self cocoaInputEnabled]) return; 561#if 0 562 // Does not work 563 if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) { 564 NSLog (@"cocoaInvalidateCoordinates"); 565 [ctxt invalidateCharacterCoordinates]; 566 } 567 // 568#endif 569 [self cocoaCommitInput]; 570} 571 572- (void) cocoaStopMacKeyboard { 573#if DEBUG 574 // NSLog(@"cocoaStopMacKeyboard"); 575#endif 576 // previously it was: NSInputManager 577 if (NSTextInputContext *ctxt = [NSTextInputContext currentInputContext]) { 578 [ctxt invalidateCharacterCoordinates]; 579 [ctxt discardMarkedText]; 580 [ctxt deactivate]; 581 } 582 [_text release]; 583 _text = [[NSMutableAttributedString alloc] init]; 584 _selectedRange = _markedRange = kEmptyRange; 585 586 cocoaActive = false; 587 preEditor->preEditClearMarkedText(); 588} 589 590- (void) cocoaFireTextChanged { 591 preEditor->preEditClearMarkedText(); 592 if (_markedRange.location == NSNotFound) { 593 return; 594 } 595 NSAttributedString* marked = [_text attributedSubstringFromRange:_markedRange]; 596 unsigned int length = [_text length]; 597 NSRange effectiveRange = NSMakeRange(0, 0); 598 while (NSMaxRange(effectiveRange) < length) { 599 id attr = [marked attribute:NSUnderlineStyleAttributeName 600 atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange]; 601 NSAttributedString* subs = [marked attributedSubstringFromRange:effectiveRange]; 602 int n = 1; 603 if (attr != nil && [attr isKindOfClass: [NSNumber class]]) { 604 n = [(NSNumber*) attr intValue]; 605 //NSLog(@"Underline is %d", n); 606 } 607 if (n == 1) { 608 preEditor->preEditInsertMarkedText([[subs string] UTF8String], SPreEditor::Style_Default); 609 } 610 if (n == 2) { 611 preEditor->preEditInsertMarkedText([[subs string] UTF8String], SPreEditor::Style_Selected); 612 } 613 } 614} 615 616@end 617