NSTextInputプロトコルを実装するには、まず、@interfaceのところでNSTextInputをそのクラスに実装することを宣言します。NSTextInputは非公式ではないプロトコルで、実行時にconformsToProtocol:メソッドを使って実装の有無がチェックされるので、このプロトコル実装宣言は必須となります。
@interface MyTextView : NSView <NSTextInput> {
}
@end
つぎに、NSTextInputで定義されている全てのメソッドを以下のように実装していきます。
入力サーバーはこのメソッドを通して、マークされたテキストと選択されたテキストの情報を渡してきます。入力サーバーがことえりの場合、マークされたテキストとは入力中の未確定な日本語文字列のことです。マークされたテキストのうち変換中の文節が選択されたテキストとなります。
第一引数はid型となっていますが、状況に応じてNSStringまたはNSAttributedStringのインスタンスが渡されます。ことえりの場合は、ほとんどNSAttributedStringを渡してきます。
第二引数はマークされたテキストの中で、選択されたテキストの範囲を表しています。選択中のテキストがない場合は、長さ0の範囲が渡されます。
このメソッドの中では、マークされたテキストを一時的にテキストストレージに追加して、それを画面に表示させるコードを書きます。
- (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange
{
NSAttributedString* text;
if([aString isKindOfClass:[NSAttributedString class]])
{
text = aString;
}
else
{
text = [[NSAttributedString alloc] initWithString:aString];
[text autorelease];
}
[text setAttributes:markAttribute range:NSMakeRange(0, [text length])];
[text setAttributes:selAttribute range:selRange];
NSRange range = NSMakeRange(insertionPoint, markedRange.length);
[textStorage replaceCharactersInRange:range withAttributedString:text];
[self setNeedsDisplay:YES];
}
このメソッドが呼ばれたときは、マーク中のテキストを削除します。
- (void)unmarkText
{
[textStorage deleteCharactersInRange:markedRange];
[self setNeedsDisplay:YES];
}
入力サーバーはこのメソッドを通して、入力が確定した文字列を渡してきます。引数の型はidとなっており、入力サーバーの処理結果の応じてNSStringまたはNSAttributedStringのどちらかのインスタンスが渡されます。渡された文字列をテキストストレージなどに追加して、ビューの描画を更新すれば入力された文字列が画面に表示されます。
- (void)insertText:(id)aString
{
if([self hasMarkedText]) [self unmarkText];
NSAttributedString* text;
if([aString isKindOfClass:[NSAttributedString class]])
{
text = aString;
}
else
{
text = [[NSAttributedString alloc] initWithString:aString];
[text autorelease];
}
[textStorage appendAttributedString:text];
[self setNeedsDisplay:YES];
}
RETURNキーやDELETEキーなど文字でないキーを入力サーバーに送った場合は、このメソッドを通してコマンドメソッドが渡されます。渡されるメソッドはNSResponderに定義されていますが、実装はされていません。
このメソッドの中では、渡されたコマンドメソッドを呼び出すことで、それぞれのキーを処理します。
- (void)doCommandBySelector:(SEL)aSelector
{
[self tryToPerform:aSelector with:self];
}
// 改行
- (void)insertNewline:(id)sender
{
[self insertText:@"\n"];
}
テキストストレージに格納されているテキストのうち、マークされたテキストの範囲を返します。マーク中のテキストがない場合は、位置NSNotFound、長さ0を返します。
- (NSRange)markedRange
{
return markedRange;
}
現在マーク中のテキストがあればYESを返します。
- (BOOL)hasMarkedText
{
return markedRange.length != 0;
}
テキストストレージに格納されているテキストのうち、選択されたテキストの範囲を返します。選択中のテキストがない場合は、位置NSNotFound、長さ0を返します。
- (NSRange)selectedRange
{
return selectedRange;
}
このメソッドによってテキストストレージの指定範囲の内容を入力サーバーに返します。引数の範囲がテキストストレージ内文字列の範囲外にある場合は、nilを返します。
- (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange
{
if(NSMaxRange(theRange) > [textStorage length]) return nil;
return [textStorage attributedSubstringFromRange:theRange];
}
このビューでサポートしている文字の属性名の配列を返します。入力サーバーによっては、insertText:やsetMarkedText:selectedRange:で、ここで返した属性付きの文字列を渡してきます。ことえりでは、サポートしていないように思えます。このビューがどの文字属性もサポートしていない場合は、空のNSArrayを返します。
- (NSArray*)validAttributesForMarkedText
{
return [NSArray array];
}
ビューにテキストが表示されているとき、ある座標にある文字がテキストストレージに格納されている文字列の何文字目なのかを返します。引数で渡される座標は、スクリーンの原点を基点とした座標であるため、これを処理するうえでまず最初にスクリーン基点からビュー基点へ座標変換する必要があります。
渡された座標に文字がない場合は、NSNotFoundを返します。
- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint
{
NSPoint p1 = [[self window] convertScreenToBase:thePoint];
NSPoint p2 = [self convertPoint:p1 fromView:nil];
NSUInteger index = [layoutManager glyphIndexForPoint:p2
inTextContainer:textContainer];
NSRect rect = [layoutManager boundingRectForGlyphRange:NSMakeRange(index, 1)
inTextContainer:textContainer];
if(!NSMouseInRect(p2, rect, YES)) return NSNotFound;
return [layoutManager characterIndexForGlyphAtIndex:index];
}
テキストストレージ内の指定範囲の文字列がビューに表示されているとき、その文字列を囲む矩形を返します。文字列が複数行にわたっている場合は、最初の1行を囲む矩形を返します。矩形はスクリーンの原点を基点としたものでなければならないため、ビュー基点からスクリーン基点へ座標変換する処理が必要になります。
もし、長さ0の範囲を渡されたときは、幅0.0の文字挿入ポイントの矩形を返します。
- (NSRect)firstRectForCharacterRange:(NSRange)theRange
{
if(theRange.length == 0) return [self convertRectToScreen:insertionRect];
NSRange glyphRange;
glyphRange = [layoutManager glyphRangeForCharacterRange:theRange
actualCharacterRange:nil];
NSRange lineRange;
[layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
effectiveRange:&lineRange];
NSRange insersectionRange = NSIntersectionRange(glyphRange, lineRange);
NSRect glyphRect;
glyphRect = [layoutManager boundingRectForGlyphRange:insersectionRange
inTextContainer:textContainer];
return [self convertRectToScreen:glyphRect];
}
このメソッドでは、入力サーバーが個々のビューを識別するためのユニークな値を返します。Objective-Cなら自身のオブジェクトのポインタ値をそのまま返せばよいでしょう。
- (NSInteger)conversationIdentifier
{
return (NSInteger)self;
}
あまり知られていないかもしれませんが、マークされたテキストはマウスクリックに反応することができます。入力サーバーがことえりの場合、選択位置を変えたり、次の候補に変換したりできます。この機能を実装するためには、mouseDown:メソッド内に処理を書きます。
- (void)mouseDown:(NSEvent*)theEvent
{
if(![self hasMarkedText]) return;
NSInputManager* inputManager = [NSInputManager currentInputManager];
if([inputManager wantsToHandleMouseEvents])
{
[inputManager handleMouseEvent:theEvent];
}
}
まず、入力マネージャのインスタンスを取得し、wantsToHandleMouseEventsメソッドを呼びます。もしYESが返ってくれば、この入力サーバーはマウスイベントをサポートしています。入力サーバーがマウスイベントをサポートしていれば、次にhandleMouseEvent:を呼んで、マウスイベントを入力サーバーに送ります。