/** NSSpellChecker Class to provide the graphical interface to the spell checking service. Copyright (C) 2001, 1996 Free Software Foundation, Inc. Author: Gregory John Casamento Date: 2001,2003 Author: Scott Christley Date: 1996 This file is part of the GNUstep GUI Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, see or write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #import "config.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import "AppKit/NSBrowser.h" #import "AppKit/NSBrowserCell.h" #import "AppKit/NSEvent.h" #import "AppKit/NSGraphics.h" #import "AppKit/NSImage.h" #import "AppKit/NSMatrix.h" #import "AppKit/NSNib.h" #import "AppKit/NSNibLoading.h" #import "AppKit/NSPanel.h" #import "AppKit/NSPopUpButton.h" #import "AppKit/NSSpellChecker.h" #import "AppKit/NSApplication.h" #import "AppKit/NSTextField.h" #import "AppKit/NSWindow.h" #import "GSGuiPrivate.h" #import "GNUstepBase/NSDebug+GNUstepBase.h" #import "GNUstepGUI/GSServicesManager.h" // prototype for function to create name for server extern NSString *GSSpellServerName(NSString *checkerDictionary, NSString *language); // These are methods which we only want the NSSpellChecker to call. // The protocol is defined here so that the outside world does not // have access to these internal methods. @protocol NSSpellServerPrivateProtocol - (NSRange) _findMisspelledWordInString: (NSString *)stringToCheck language: (NSString *)language ignoredWords: (NSArray *)ignoredWords wordCount: (int *)wordCount countOnly: (BOOL)countOnly; - (BOOL) _learnWord: (NSString *)word inDictionary: (NSString *)language; - (BOOL) _forgetWord: (NSString *)word inDictionary: (NSString *)language; - (NSArray *) _suggestGuessesForWord: (NSString *)word inLanguage: (NSString *)language; @end // Methods needed to get the GSServicesManager @interface NSApplication(NSSpellCheckerMethods) - (GSServicesManager *)_listener; @end @implementation NSApplication(NSSpellCheckerMethods) - (GSServicesManager *)_listener { return _listener; } @end // Methods in the GSServicesManager to launch the spell server. @interface GSServicesManager(NSSpellCheckerMethods) - (id)_launchSpellCheckerForLanguage: (NSString *)language; - (NSArray *)_languagesForPopUpButton; @end @implementation GSServicesManager(NSSpellCheckerMethods) - (id)_launchSpellCheckerForLanguage: (NSString *)language { if ([[NSUserDefaults standardUserDefaults] boolForKey: @"GSDisableSpellCheckerServer"]) { GSOnceMLog(@"WARNING: spell checker disabled - reset 'GSDisableSpellCheckerServer' to NO in defaults to re-enable"); return nil; } id proxy = nil; NSDictionary *spellCheckers = [_allServices objectForKey: @"BySpell"]; NSDictionary *checkerDictionary = [spellCheckers objectForKey: language]; NSString *spellServicePath = [checkerDictionary objectForKey: @"ServicePath"]; NSString *vendor = [checkerDictionary objectForKey: @"NSSpellChecker"]; NSDate *finishBy; NSString *port = GSSpellServerName(vendor, language); double seconds = 30.0; NSLog(@"Language: %@", language); NSLog(@"Service to start: %@", spellServicePath); NSLog(@"Port: %@",port); finishBy = [NSDate dateWithTimeIntervalSinceNow: seconds]; proxy = GSContactApplication(spellServicePath, port, finishBy); if (proxy == nil) { NSLog(@"Failed to contact spell checker for language '%@'", language); } else { NSLog(@"Set proxy"); [(NSDistantObject *)proxy setProtocolForProxy: @protocol(NSSpellServerPrivateProtocol)]; } return proxy; } - (NSArray *)_languagesForPopUpButton { NSDictionary *spellCheckers = [_allServices objectForKey: @"BySpell"]; NSArray *allKeys = [spellCheckers allKeys]; return allKeys; } @end // Shared spell checker instance.... static NSSpellChecker *__sharedSpellChecker = nil; static int __documentTag = 0; // Implementation of spell checker class @implementation NSSpellChecker // // Class methods // + (void)initialize { if (self == [NSSpellChecker class]) { // Initial version [self setVersion:1]; } } + (BOOL)isAutomaticTextReplacementEnabled { return NO; } + (BOOL)isAutomaticDashSubstitutionEnabled { return NO; } + (BOOL)isAutomaticQuoteSubstitutionEnabled { return NO; } // // Making a Checker available // + (NSSpellChecker *)sharedSpellChecker { // Create the shared instance. if (__sharedSpellChecker == nil) { __sharedSpellChecker = [[NSSpellChecker alloc] init]; } return __sharedSpellChecker; } + (BOOL)sharedSpellCheckerExists { // If the spell checker has been created, the // variable will not be nil. return (__sharedSpellChecker != nil); } // // Managing the Spelling Process // + (int)uniqueSpellDocumentTag { return ++__documentTag; } // // Internal methods for use by the spellChecker GUI // // Support function to start the spell server - (id)_startServerForLanguage: (NSString *)language { id proxy = nil; // Start the service for this language proxy = [[NSApp _listener] _launchSpellCheckerForLanguage: language]; if (proxy == nil) { NSLog(@"Failed to get the spellserver"); } else { // remove any previous notifications we are observing. [[NSNotificationCenter defaultCenter] removeObserver: self]; // Make sure that we handle the death of the server correctly. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_handleServerDeath:) name: NSConnectionDidDieNotification object: [(NSDistantObject *)proxy connectionForProxy]]; } return proxy; } - (id)_serverProxy { if (_serverProxy == nil) { // Start the server and retain the reference to the // proxy. id proxy = [self _startServerForLanguage: _language]; if (proxy != nil) { _serverProxy = proxy; RETAIN(_serverProxy); } } return _serverProxy; } - (void)_populateDictionaryPulldown: (NSArray *)dictionaries { [_dictionaryPulldown removeAllItems]; [_dictionaryPulldown addItemsWithTitles: [dictionaries sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]]; [_dictionaryPulldown selectItemWithTitle: _language]; } - (void)_populateAccessoryView { // refresh the columns in the browser [_accessoryView reloadColumn: 0]; } - (void)_handleServerDeath: (NSNotification *)notification { NSLog(@"Spell server died"); DESTROY(_serverProxy); } // // Instance methods // - init { NSArray *languages; self = [super init]; if (self == nil) return nil; languages = [[NSUserDefaults standardUserDefaults] stringArrayForKey: @"NSLanguages"]; // Set the language to the default for the user. _language = RETAIN([languages objectAtIndex: 0]); _wrapFlag = NO; _position = 0; _spellPanel = nil; _serverProxy = nil; _currentTag = 0; _ignoredWords = [NSMutableDictionary new]; // Load the NIB file from the GUI bundle if (![GSGuiBundle() loadNibFile: @"GSSpellPanel" externalNameTable: [NSDictionary dictionaryWithObject: self forKey: NSNibOwner] withZone: [self zone]]) { NSLog(@"Model file load failed for GSSpellPanel"); return nil; } return self; } - (void)dealloc { RELEASE(_language); RELEASE(_ignoredWords); RELEASE(_serverProxy); [super dealloc]; } // // Managing the Spelling Panel // - (NSView *)accessoryView { return _accessoryView; } - (void)setAccessoryView:(NSView *)aView { _accessoryView = aView; } - (NSPanel *)spellingPanel { return _spellPanel; } // // Checking Spelling // - (int)countWordsInString:(NSString *)aString language:(NSString *)language { int count = 0; id proxy = [self _serverProxy]; if (proxy != nil) [proxy _findMisspelledWordInString: aString language: _language ignoredWords: nil wordCount: &count countOnly: YES]; return count; } - (NSRange)checkSpellingOfString:(NSString *)stringToCheck startingAt:(int)startingOffset { int wordCount = 0; NSRange r; r = [self checkSpellingOfString: stringToCheck startingAt: startingOffset language: _language wrap: NO inSpellDocumentWithTag: _currentTag wordCount: &wordCount]; return r; } - (NSRange)checkSpellingOfString:(NSString *)stringToCheck startingAt:(int)startingOffset language:(NSString *)language wrap:(BOOL)wrapFlag inSpellDocumentWithTag:(int)tag wordCount:(int *)wordCount { NSRange r; NSArray *dictForTag = [self ignoredWordsInSpellDocumentWithTag: tag]; _currentTag = tag; // We have no string to work with if (stringToCheck == nil) { return NSMakeRange(0,0); } else // The string is zero length if ([stringToCheck length] == 0) { return NSMakeRange(0,0); } // Do this in an exception handling block in ensure that a failure of the // spellserver does not bring down the application. NS_DURING { id proxy = [self _serverProxy]; // Get the substring and check it. NSString *substringToCheck = [stringToCheck substringFromIndex: startingOffset]; if (proxy == nil) NS_VALUERETURN(NSMakeRange(0,0), NSRange); r = [proxy _findMisspelledWordInString: substringToCheck language: _language ignoredWords: dictForTag wordCount: wordCount countOnly: NO]; if (r.length != 0) { // Adjust results relative to the original string r.location += startingOffset; } else { if (wrapFlag) { // Check the second half of the string NSString *firstHalfOfString = [stringToCheck substringToIndex: startingOffset]; r = [proxy _findMisspelledWordInString: firstHalfOfString language: _language ignoredWords: dictForTag wordCount: wordCount countOnly: NO]; } } NS_VALUERETURN(r, NSRange); } NS_HANDLER { NSLog(@"%@",[localException reason]); } NS_ENDHANDLER return NSMakeRange(0,0); } - (NSArray *)guessesForWord:(NSString *)word { NSArray *guesses; // Make the call to the server to get the guesses. NS_DURING { guesses = [[self _serverProxy] _suggestGuessesForWord: word inLanguage: _language]; NS_VALUERETURN(guesses, id); } NS_HANDLER { NSLog(@"%@",[localException reason]); } NS_ENDHANDLER return nil; } - (NSRange)checkGrammarOfString:(NSString *)stringToCheck startingAt:(NSInteger)startingOffset language:(NSString *)language wrap:(BOOL)wrapFlag inSpellDocumentWithTag:(NSInteger)tag details:(NSArray **)details { return NSMakeRange(NSNotFound, 0); } // // Setting the Language // - (NSString *)language { return _language; } - (BOOL)setLanguage:(NSString *)aLanguage { int index = 0; BOOL result = NO; index = [_dictionaryPulldown indexOfItemWithTitle: aLanguage]; if (index != -1) { [_dictionaryPulldown selectItemAtIndex: index]; result = YES; } return result; } // // Managing the Spelling Process // // Remove the ignored word list for this // document from the dictionary - (void)closeSpellDocumentWithTag:(int)tag { NSNumber *key = [NSNumber numberWithInt: tag]; [_ignoredWords removeObjectForKey: key]; } // Add a word to the ignored list. - (void) ignoreWord:(NSString *)wordToIgnore inSpellDocumentWithTag:(int)tag { NSNumber *key = [NSNumber numberWithInt: tag]; NSMutableSet *words = [_ignoredWords objectForKey: key]; if (![wordToIgnore isEqualToString: @""]) { // If there is a dictionary add to it, if not create one. if (words == nil) { words = [NSMutableSet setWithObject: wordToIgnore]; [_ignoredWords setObject: words forKey: key]; } else { [words addObject: wordToIgnore]; } } } // get the list of ignored words. - (NSArray *)ignoredWordsInSpellDocumentWithTag:(int)tag { NSNumber *key = [NSNumber numberWithInt: tag]; NSSet *words = [_ignoredWords objectForKey: key]; return [words allObjects]; } // set the list of ignored words for a given document - (void)setIgnoredWords:(NSArray *)someWords inSpellDocumentWithTag:(int)tag { NSNumber *key = [NSNumber numberWithInt: tag]; NSSet *words = [NSSet setWithArray: someWords]; [_ignoredWords setObject: words forKey: key]; } - (void)setWordFieldStringValue:(NSString *)aString { [_wordField setStringValue: aString]; } - (void)updateSpellingPanelWithMisspelledWord:(NSString *)word { if ((word == nil) || ([word isEqualToString: @""])) { [_ignoreButton setEnabled: NO]; [_guessButton setEnabled: NO]; NSBeep(); return; } [_ignoreButton setEnabled: YES]; [_guessButton setEnabled: NO]; [self setWordFieldStringValue: word]; [self _populateAccessoryView]; } - (void) _findNext: (id)sender { BOOL processed = [[[NSApp mainWindow] firstResponder] tryToPerform: @selector(checkSpelling:) with: _spellPanel]; if (!processed) { NSLog(@"No responder found"); } } - (void) _learn: (id)sender { NSString *word = [_wordField stringValue]; // Call server and record the learned word. NS_DURING { [[self _serverProxy] _learnWord: word inDictionary: _language]; } NS_HANDLER { NSLog(@"%@",[localException reason]); } NS_ENDHANDLER [self _findNext: sender]; } - (void) _forget: (id)sender { NSString *word = [_wordField stringValue]; // Call the server and remove the word from the learned // list. NS_DURING { [[self _serverProxy] _forgetWord: word inDictionary: _language]; } NS_HANDLER { NSLog(@"%@",[localException reason]); } NS_ENDHANDLER [self _findNext: sender]; } - (void) _ignore: (id)sender { BOOL processed = [[[NSApp mainWindow] firstResponder] tryToPerform: @selector(ignoreSpelling:) with: _wordField]; if (!processed) { NSLog(@"_ignore: No responder found"); } [self _findNext: sender]; } - (void) _guess: (id)sender { // Fill in the view... [self _populateAccessoryView]; } - (void) _correct: (id)sender { BOOL processed = [[[NSApp mainWindow] firstResponder] tryToPerform: @selector(changeSpelling:) with: _wordField]; if (!processed) { NSLog(@"No responder found"); } [self _findNext: sender]; } - (void) _switchDictionary: (id)sender { id proxy = nil; NSString *language = nil; // Start the service for this language language = [_dictionaryPulldown stringValue]; if (![language isEqualToString: _language]) { NSLog(@"Language = %@",language); proxy = [self _startServerForLanguage: language]; if (proxy != nil) { ASSIGN(_language, language); ASSIGN(_serverProxy, (id)proxy); } else { // Reset the pulldown to the proper language. [_dictionaryPulldown selectItemWithTitle: _language]; } } } - (void) _highlightGuess: (id)sender { NSString *selectedGuess = nil; selectedGuess = [[_accessoryView selectedCell] stringValue]; [_ignoreButton setEnabled: NO]; [_guessButton setEnabled: YES]; [_wordField setStringValue: selectedGuess]; } - (void) awakeFromNib { [self _populateDictionaryPulldown: [[NSApp _listener] _languagesForPopUpButton]]; [_accessoryView setDelegate: self]; [_accessoryView setDoubleAction: @selector(_correct:)]; [_findNextButton setKeyEquivalent: @"n"]; [_findNextButton setKeyEquivalentModifierMask: NSCommandKeyMask]; [_ignoreButton setKeyEquivalent: @"i"]; [_ignoreButton setKeyEquivalentModifierMask: NSCommandKeyMask]; [_learnButton setKeyEquivalent: @"l"]; [_learnButton setKeyEquivalentModifierMask: NSCommandKeyMask]; [_forgetButton setKeyEquivalent: @"f"]; [_forgetButton setKeyEquivalentModifierMask: NSCommandKeyMask]; [_guessButton setKeyEquivalent: @"g"]; [_guessButton setKeyEquivalentModifierMask: NSCommandKeyMask]; [_correctButton setKeyEquivalent: @"c"]; [_correctButton setKeyEquivalentModifierMask: NSCommandKeyMask]; [_correctButton setImagePosition: NSImageRight]; [_correctButton setImage: [NSImage imageNamed: @"common_ret"]]; [_correctButton setAlternateImage: [NSImage imageNamed: @"common_retH"]]; [_spellPanel makeFirstResponder: _correctButton]; [_spellPanel setBecomesKeyOnlyIfNeeded: YES]; [_spellPanel setFloatingPanel: YES]; } @end @interface NSSpellChecker(SpellBrowserDelegate) - (BOOL) browser: (NSBrowser*)sender selectRow: (NSInteger)row inColumn: (NSInteger)column; - (void) browser: (NSBrowser *)sender createRowsForColumn: (NSInteger)column inMatrix: (NSMatrix *)matrix; - (NSString*) browser: (NSBrowser*)sender titleOfColumn: (NSInteger)column; - (void) browser: (NSBrowser *)sender willDisplayCell: (id)cell atRow: (NSInteger)row column: (NSInteger)column; - (BOOL) browser: (NSBrowser *)sender isColumnValid: (NSInteger)column; @end @implementation NSSpellChecker(SpellBrowserDelegate) - (BOOL) browser: (NSBrowser*)sender selectRow: (NSInteger)row inColumn: (NSInteger)column { return YES; } - (void) browser: (NSBrowser *)sender createRowsForColumn: (NSInteger)column inMatrix: (NSMatrix *)matrix { NSArray *guesses = [self guessesForWord: [_wordField stringValue]]; NSEnumerator *e = [guesses objectEnumerator]; NSString *word = nil; NSBrowserCell *cell= nil; NSInteger i = 0; while ((word = [e nextObject]) != nil) { [matrix insertRow: i withCells: nil]; cell = [matrix cellAtRow: i column: 0]; [cell setLeaf: YES]; i++; [cell setStringValue: word]; } } - (NSString*) browser: (NSBrowser*)sender titleOfColumn: (NSInteger)column { return _(@"Guess"); } - (void) browser: (NSBrowser *)sender willDisplayCell: (id)cell atRow: (NSInteger)row column: (NSInteger)column { } - (BOOL) browser: (NSBrowser *)sender isColumnValid: (NSInteger)column { return NO; } @end