NSTextFieldの補完

IE5:macっぽいアドレスバーのオートコンプリートを実装。NSComboBoxを使うのが普通っぽいのを、敢えてNSTextFieldのままCocoaの単語補完機構を利用してみた。
最初はDelegateをちょこっと書くだけでどうにかなるかと思っていたのだけど、所謂単語の補完とは勝手が違う訳で、結局NSTextViewをサブクラス化してフィールドエディタの置き換えで実装。
URLの補完だけならそれっぽく行けたのだが、IE5:macと同じページタイトルでの検索と置き換えを行おうとすると、検索結果が一つだけの場合だったり補完の終了がマウスクリックの場合だったりを細かく制御できなくて、結局ページタイトルでの検索は無理と判断した。
NSComboBoxならどうにかなるのかどうかは分からない。

ソースコード

- (void)complete:(id)sender {
	[self setSelectedRange:NSMakeRange(0, [[self string] length])];
	[privateProperties setValue:[NSString stringWithString:[self string]] forKey:@"completionSource"];
	[super complete:sender];
}

- (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index {
	NSMutableArray *result = [NSMutableArray arrayWithCapacity:100];
	NSString *preferredResult = nil;
	NSRange sourceRangeInPreferred = NSMakeRange(NSNotFound, 0);
	WebHistory *history = [WebHistory optionalSharedHistory];
	NSEnumerator *dateEnumerator = [[history orderedLastVisitedDays] objectEnumerator];
	id date;
	while ( (date = [dateEnumerator nextObject]) != nil ) {
		NSEnumerator *itemEnumerator = [[history orderedItemsLastVisitedOnDay:date] objectEnumerator];
		id item;
		while ( (item = [itemEnumerator nextObject]) != nil ) {
			NSString *completionResult = [item URLString];
			NSRange sourceRange = [completionResult
				rangeOfString:[privateProperties valueForKey:@"completionSource"]
				options:NSCaseInsensitiveSearch
			];
			if ( sourceRange.location != NSNotFound ) {
				[result addObject:completionResult];
				if (
					sourceRangeInPreferred.location == NSNotFound ||
					sourceRangeInPreferred.location > sourceRange.location || (
						sourceRangeInPreferred.location == sourceRange.location && (
							[preferredResult length] > [completionResult length] ||
							[preferredResult length] == [completionResult length] &&
							[preferredResult caseInsensitiveCompare:completionResult] == NSOrderedDescending
						)
					)
				) {
					preferredResult = completionResult;
					sourceRangeInPreferred = sourceRange;
				}
			}
		}
	}
	[result sortUsingSelector:@selector(caseInsensitiveCompare:)];
	*index = [result indexOfObject:preferredResult];
	return [NSArray arrayWithArray:result];
}

- (void)insertCompletion:(NSString *)word
	forPartialWordRange:(NSRange)charRange
	movement:(int)movement
	isFinal:(BOOL)flag
{
	[super
		insertCompletion:word
		forPartialWordRange:charRange
		movement:movement
		isFinal:flag
	];
	NSRange sourceRange = [word
		rangeOfString:[privateProperties valueForKey:@"completionSource"]
		options:NSCaseInsensitiveSearch
	];
	if ( sourceRange.location != NSNotFound ) {
		int location = sourceRange.location + sourceRange.length;
		int length = [word length] - location;
		[self setSelectedRange:NSMakeRange(location, length)];
	} else {
		[self selectAll:nil];
	}
	if ( flag ) {
		if ( movement == NSLeftTextMovement ) {
			int offset = 0;
			if ( [self selectedRange].length == 0 ) {
				offset = 1;
			}
			[self setSelectedRange:NSMakeRange([self selectedRange].location - offset, 0)];
		} else if ( movement == NSRightTextMovement ) {
			[self setSelectedRange:NSMakeRange([self selectedRange].location+[self selectedRange].length, 0)];
		} else if ( movement == NSReturnTextMovement && [self isFieldEditor] ) {
			[[self delegate] sendAction:[[self delegate] action] to:[[self delegate] target]];
		}
		[privateProperties setValue:nil forKey:@"completionSource"];
	}
}

- (NSRange)rangeForUserCompletion {
	return NSMakeRange(0, [[self string] length]);
}