diff --git a/TwUI.xcodeproj/project.pbxproj b/TwUI.xcodeproj/project.pbxproj index 44c81c24..530df537 100644 --- a/TwUI.xcodeproj/project.pbxproj +++ b/TwUI.xcodeproj/project.pbxproj @@ -78,6 +78,12 @@ 886EBA8313D64393006DE018 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 886EBA7E13D64393006DE018 /* TUIControl+Private.m */; }; 886EBA8413D64393006DE018 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 886EBA7E13D64393006DE018 /* TUIControl+Private.m */; }; 886EBA8513D64393006DE018 /* TUIControl+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 886EBA7E13D64393006DE018 /* TUIControl+Private.m */; }; + 887F272C13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 887F272D13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */; }; + 887F272E13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */; }; + 887F272F13F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */; }; + 887F273013F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */; }; + 887F273113F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */; }; 88CC1F2F13E365B600827793 /* TUIControl+Accessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */; }; 88CC1F3013E365B600827793 /* TUIControl+Accessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */; }; 88CC1F3113E365B600827793 /* TUIControl+Accessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */; }; @@ -266,6 +272,8 @@ 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUINSView+Accessibility.m"; sourceTree = ""; }; 886EBA7D13D64393006DE018 /* TUIControl+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Private.h"; sourceTree = ""; }; 886EBA7E13D64393006DE018 /* TUIControl+Private.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Private.m"; sourceTree = ""; }; + 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUITableViewSectionHeader.h; sourceTree = ""; }; + 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TUITableViewSectionHeader.m; sourceTree = ""; }; 88CC1F2D13E365B500827793 /* TUIControl+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Accessibility.h"; sourceTree = ""; }; 88CC1F2E13E365B500827793 /* TUIControl+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Accessibility.m"; sourceTree = ""; }; 88CC1F3513E3684400827793 /* TUIButton+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIButton+Accessibility.h"; sourceTree = ""; }; @@ -593,6 +601,8 @@ CBB74C7413BE6E1900C85CB5 /* TUITableViewCell.m */, 88D25F5313F5D96500CFAAA9 /* TUITableView+Cell.h */, 88D25F5413F5D96500CFAAA9 /* TUITableView+Cell.m */, + 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */, + 887F272B13F9969800D75DE6 /* TUITableViewSectionHeader.m */, CBB74C7513BE6E1900C85CB5 /* TUITextEditor.h */, CBB74C7613BE6E1900C85CB5 /* TUITextEditor.m */, CBB74C7713BE6E1900C85CB5 /* TUITextField.h */, @@ -644,6 +654,7 @@ 88CC1F3913E3684700827793 /* TUIButton+Accessibility.h in Headers */, 88EFFB5313F417E200CF91A9 /* TUITextViewEditor.h in Headers */, 88D25F5713F5D96500CFAAA9 /* TUITableView+Cell.h in Headers */, + 887F272E13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -692,6 +703,7 @@ CBB74CE213BE6E1900C85CB5 /* TUIView.h in Headers */, CBB74CE413BE6E1900C85CB5 /* TUIViewController.h in Headers */, CBB74CE613BE6E1900C85CB5 /* TUIViewNSViewContainer.h in Headers */, + 887F272C13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, 886EBA7F13D64393006DE018 /* TUIControl+Private.h in Headers */, 8819794413E26E0200AA39EB /* TUIView+Accessibility.h in Headers */, 8819794C13E26E5800AA39EB /* TUINSView+Accessibility.h in Headers */, @@ -713,6 +725,7 @@ 88CC1F3813E3684700827793 /* TUIButton+Accessibility.h in Headers */, 88EFFB5213F417E200CF91A9 /* TUITextViewEditor.h in Headers */, 88D25F5613F5D96500CFAAA9 /* TUITableView+Cell.h in Headers */, + 887F272D13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -912,6 +925,7 @@ 88CC1F3C13E3684700827793 /* TUIButton+Accessibility.m in Sources */, 88EFFB5613F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5A13F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, + 887F273113F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -972,6 +986,7 @@ 88CC1F3A13E3684700827793 /* TUIButton+Accessibility.m in Sources */, 88EFFB5413F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5813F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, + 887F272F13F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1041,6 +1056,7 @@ 88CC1F3B13E3684700827793 /* TUIButton+Accessibility.m in Sources */, 88EFFB5513F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5913F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, + 887F273013F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/lib/UIKit/TUIButton.m b/lib/UIKit/TUIButton.m index 7cc17788..6360c2eb 100644 --- a/lib/UIKit/TUIButton.m +++ b/lib/UIKit/TUIButton.m @@ -215,12 +215,7 @@ - (void)drawRect:(CGRect)r - (void)mouseDown:(NSEvent *)event { [super mouseDown:event]; - if([event clickCount] < 2) { - [self sendActionsForControlEvents:TUIControlEventTouchDown]; - } else { - [self sendActionsForControlEvents:TUIControlEventTouchDownRepeat]; - } - + if(popUpMenu) { // happens even if clickCount is big NSMenu *menu = popUpMenu; NSPoint p = [self frameInNSView].origin; @@ -240,20 +235,6 @@ - (void)mouseDown:(NSEvent *)event } } -- (void)mouseUp:(NSEvent *)event -{ - [super mouseUp:event]; -// if([event clickCount] < 2) { - if([self eventInside:event]) { - if(![self didDrag]) { - [self sendActionsForControlEvents:TUIControlEventTouchUpInside]; - } - } else { - [self sendActionsForControlEvents:TUIControlEventTouchUpOutside]; - } -// } -} - - (void)_update { _titleView.text = self.currentTitle; _titleView.textColor = self.currentTitleColor; diff --git a/lib/UIKit/TUIControl.m b/lib/UIKit/TUIControl.m index 9b9d3197..9be222ff 100644 --- a/lib/UIKit/TUIControl.m +++ b/lib/UIKit/TUIControl.m @@ -95,9 +95,9 @@ -(BOOL)selected { */ -(void)setSelected:(BOOL)selected { [self _stateWillChange]; - _controlFlags.selected = selected; + _controlFlags.selected = selected; [self _stateDidChange]; - [self setNeedsDisplay]; + [self setNeedsDisplay]; } - (BOOL)acceptsFirstMouse @@ -124,12 +124,15 @@ - (void)mouseDown:(NSEvent *)event _controlFlags.tracking = 1; [self _stateDidChange]; - // handle touch down - [self sendActionsForControlEvents:TUIControlEventTouchDown]; + // handle touch down + if([event clickCount] < 2) { + [self sendActionsForControlEvents:TUIControlEventTouchDown]; + } else { + [self sendActionsForControlEvents:TUIControlEventTouchDownRepeat]; + } // needs display [self setNeedsDisplay]; - } - (void)mouseUp:(NSEvent *)event @@ -141,16 +144,16 @@ - (void)mouseUp:(NSEvent *)event _controlFlags.tracking = 0; [self _stateDidChange]; - // handle touch up - if([self pointInside:[self localPointForEvent:event] withEvent:event]){ - [self sendActionsForControlEvents:TUIControlEventTouchUpInside]; - }else{ - [self sendActionsForControlEvents:TUIControlEventTouchUpOutside]; - } + if([self eventInside:event]) { + if(![self didDrag]) { + [self sendActionsForControlEvents:TUIControlEventTouchUpInside]; + } + } else { + [self sendActionsForControlEvents:TUIControlEventTouchUpOutside]; + } - // needs display + // needs display [self setNeedsDisplay]; - } @end diff --git a/lib/UIKit/TUITextRenderer+Event.h b/lib/UIKit/TUITextRenderer+Event.h index abfcf28a..cb04984a 100644 --- a/lib/UIKit/TUITextRenderer+Event.h +++ b/lib/UIKit/TUITextRenderer+Event.h @@ -19,6 +19,7 @@ @interface TUITextRenderer (Event) - (CFIndex)stringIndexForPoint:(CGPoint)p; +- (CFIndex)stringIndexForEvent:(NSEvent *)event; - (void)resetSelection; - (CGRect)rectForCurrentSelection; diff --git a/lib/UIKit/TUITextView.h b/lib/UIKit/TUITextView.h index be2c8bb8..4221a399 100644 --- a/lib/UIKit/TUITextView.h +++ b/lib/UIKit/TUITextView.h @@ -35,6 +35,12 @@ TUIColor *textColor; TUITextAlignment textAlignment; BOOL editable; + + BOOL spellCheckingEnabled; + NSInteger lastCheckToken; + NSArray *lastCheckResults; + NSTextCheckingResult *selectedTextCheckingResult; + BOOL autocorrectionEnabled; TUIEdgeInsets contentInset; @@ -62,6 +68,8 @@ @property (nonatomic, assign) NSRange selectedRange; @property (nonatomic, assign, getter=isEditable) BOOL editable; +@property (nonatomic, assign, getter=isSpellCheckingEnabled) BOOL spellCheckingEnabled; +@property (nonatomic, assign, getter=isAutocorrectionEnabled) BOOL autocorrectionEnabled; @property (nonatomic, copy) TUIViewDrawRect drawFrame; diff --git a/lib/UIKit/TUITextView.m b/lib/UIKit/TUITextView.m index 4c1e6163..826426a4 100644 --- a/lib/UIKit/TUITextView.m +++ b/lib/UIKit/TUITextView.m @@ -17,6 +17,15 @@ #import "TUIKit.h" #import "TUITextView.h" #import "TUITextViewEditor.h" +#import "TUITextRenderer+Event.h" + +@interface TUITextView () +- (void)_checkSpelling; +- (void)_replaceMisspelledWord:(NSMenuItem *)menuItem; + +@property (nonatomic, retain) NSArray *lastCheckResults; +@property (nonatomic, retain) NSTextCheckingResult *selectedTextCheckingResult; +@end @implementation TUITextView @@ -28,6 +37,10 @@ @implementation TUITextView @synthesize editable; @synthesize contentInset; @synthesize placeholder; +@synthesize spellCheckingEnabled; +@synthesize lastCheckResults; +@synthesize selectedTextCheckingResult; +@synthesize autocorrectionEnabled; - (void)_updateDefaultAttributes { @@ -76,6 +89,8 @@ - (void)dealloc [font release]; [textColor release]; [placeholder release]; + [lastCheckResults release]; + [selectedTextCheckingResult release]; [super dealloc]; } @@ -259,6 +274,90 @@ - (void)_textDidChange { if(_textViewFlags.delegateTextViewDidChange) [delegate textViewDidChange:self]; + + if(spellCheckingEnabled) { + [self _checkSpelling]; + } +} + +- (void)_checkSpelling +{ + lastCheckToken = [[NSSpellChecker sharedSpellChecker] requestCheckingOfString:self.text range:NSMakeRange(0, [self.text length]) types:NSTextCheckingTypeSpelling options:nil inSpellDocumentWithTag:0 completionHandler:^(NSInteger sequenceNumber, NSArray *results, NSOrthography *orthography, NSInteger wordCount) { + // This needs to happen on the main thread so that the user doesn't enter more text while we're changing the attributed string. + dispatch_async(dispatch_get_main_queue(), ^{ + // we only care about the most recent results, ignore anything older + if(sequenceNumber != lastCheckToken) return; + + [[renderer backingStore] beginEditing]; + + NSRange wholeStringRange = NSMakeRange(0, [self.text length]); + [[renderer backingStore] removeAttribute:(id)kCTUnderlineColorAttributeName range:wholeStringRange]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineStyleAttributeName range:wholeStringRange]; + + NSRange selectionRange = [self selectedRange]; + for(NSTextCheckingResult *result in results) { + // Don't spell check the word they're typing, otherwise we're constantly marking it as misspelled and that's lame. + BOOL isActiveWord = (result.range.location + result.range.length == selectionRange.location) && selectionRange.length == 0; + if(isActiveWord) continue; + + [[renderer backingStore] addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[TUIColor redColor].CGColor range:result.range]; + [[renderer backingStore] addAttribute:(id)kCTUnderlineStyleAttributeName value:[NSNumber numberWithInteger:kCTUnderlineStyleThick | kCTUnderlinePatternDot] range:result.range]; + } + + [[renderer backingStore] endEditing]; + [renderer reset]; // make sure we reset so that the renderer uses our new attributes + + [self setNeedsDisplay]; + + self.lastCheckResults = results; + }); + }]; +} + +- (NSMenu *)menuForEvent:(NSEvent *)event +{ + CFIndex stringIndex = [renderer stringIndexForEvent:event]; + for(NSTextCheckingResult *result in lastCheckResults) { + if(stringIndex >= result.range.location && stringIndex <= result.range.location + result.range.length) { + self.selectedTextCheckingResult = result; + break; + } + } + + if(selectedTextCheckingResult == nil) return nil; + + NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; + NSArray *guesses = [[NSSpellChecker sharedSpellChecker] guessesForWordRange:selectedTextCheckingResult.range inString:[self text] language:nil inSpellDocumentWithTag:0]; + if(guesses.count > 0) { + for(NSString *guess in guesses) { + NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:guess action:@selector(_replaceMisspelledWord:) keyEquivalent:@""]; + [menuItem setTarget:self]; + [menuItem setRepresentedObject:guess]; + [menu addItem:menuItem]; + [menuItem release]; + } + } else { + NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"No guesses" action:NULL keyEquivalent:@""]; + [menu addItem:menuItem]; + [menuItem release]; + } + + return [menu autorelease]; +} + +- (void)_replaceMisspelledWord:(NSMenuItem *)menuItem +{ + NSString *replacement = [menuItem representedObject]; + [[renderer backingStore] beginEditing]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineColorAttributeName range:selectedTextCheckingResult.range]; + [[renderer backingStore] removeAttribute:(id)kCTUnderlineStyleAttributeName range:selectedTextCheckingResult.range]; + [[renderer backingStore] replaceCharactersInRange:self.selectedTextCheckingResult.range withString:replacement]; + [[renderer backingStore] endEditing]; + [renderer reset]; + + [self _textDidChange]; + + self.selectedTextCheckingResult = nil; } - (NSRange)selectedRange