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