From d63e4f8021c3125f17923812c743f98d224e0822 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Thu, 21 Oct 2010 00:47:58 -0300 Subject: [PATCH 01/21] Remove some placeholder comments for methods which now exist. --- AppKit/CPTokenField.j | 5 ----- 1 file changed, 5 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 9945d6479..0d626c383 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -769,11 +769,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self setNeedsLayout]; } - -// - (void)setTokenizingCharacterSet:(NSCharacterSet *)characterSet; -// - (NSCharacterSet *)tokenizingCharacterSet; -// + (NSCharacterSet *)defaultTokenizingCharacterSet - // ========== // = LAYOUT = // ========== From 2210c1f4c59b14ab9c10575c6603fffefa20341b Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Thu, 21 Oct 2010 15:56:17 -0300 Subject: [PATCH 02/21] Implemented token field overflow support as in Cocoa: if more tokens than can fit are entered, they wrap to a new line. If the new line does not fit within the height of the token field, the previous line is scrolled out of view. Note: this replaces the previous non Cocoa behaviour where the token field would grow height-wise with new tokens. This behaviour can still be useful, as seen in Apple Mail, but it will now require the user to subclass CPTokenField, override layoutSubview and set the frame size to the size of the scroll view's document view. --- AppKit/CPTokenField.j | 90 +++++++++++++------ AppKit/Themes/Aristo/ThemeDescriptors.j | 4 +- Tests/Manual/CPTokenFieldTest/AppController.j | 14 +++ 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 0d626c383..7f6c3b531 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -58,6 +58,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", @implementation CPTokenField : CPTextField { + CPScrollView _tokenScrollView; + BOOL _scrollToLastToken; + CPView _autocompleteContainer; CPScrollView _autocompleteScrollView; CPTableView _autocompleteView; @@ -85,6 +88,13 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { if (self = [super initWithFrame:frame]) { + _tokenScrollView = [[CPScrollView alloc] initWithFrame:CGRectMakeZero()]; + [_tokenScrollView setHasHorizontalScroller:NO]; + [_tokenScrollView setHasVerticalScroller:NO]; + var contentView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + [_tokenScrollView setDocumentView:contentView]; + [self addSubview:_tokenScrollView]; + _tokenIndex = 0; _selectedTokenIndexes = [CPIndexSet indexSet]; @@ -185,6 +195,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [objectValue addObject:token]; [self setObjectValue:objectValue]; + _scrollToLastToken = NO; [self _inputElement].value = @""; [self setNeedsLayout]; @@ -288,7 +299,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", element.style.width = CGRectGetWidth(contentRect) + "px"; element.style.height = CGRectGetHeight(contentRect) + "px"; - _DOMElement.appendChild(element); + [_tokenScrollView documentView]._DOMElement.appendChild(element); window.setTimeout(function() { @@ -340,7 +351,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPTokenFieldInputDidBlur = NO; CPTokenFieldInputResigning = NO; - if (element.parentNode == _DOMElement) + if (element.parentNode == [_tokenScrollView documentView]._DOMElement) element.parentNode.removeChild(element); CPTokenFieldInputIsActive = NO; @@ -439,8 +450,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView setTokenField:self]; [tokenView setStringValue:token]; [objectValue addObject:tokenView]; - - [self addSubview:tokenView]; } } @@ -456,6 +465,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self _updatePlaceholderState]; + _scrollToLastToken = YES; [self setNeedsLayout]; [self setNeedsDisplay:YES]; } @@ -500,7 +510,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPTokenFieldBlurFunction = function(anEvent) { - if (CPTokenFieldInputOwner && CPTokenFieldInputOwner._DOMElement != CPTokenFieldDOMInputElement.parentNode) + if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner._tokenScrollView documentView]._DOMElement != CPTokenFieldDOMInputElement.parentNode) return; if (!CPTokenFieldInputResigning && !CPTokenFieldFocusInput) @@ -776,14 +786,17 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { [super layoutSubviews]; + [_tokenScrollView setFrame:[self rectForEphemeralSubviewNamed:"content-view"]]; + + var textFieldContentView = [self layoutEphemeralSubviewNamed:@"content-view" + positioned:CPWindowAbove + relativeToEphemeralSubviewNamed:@"bezel-view"]; + + if (textFieldContentView) + [textFieldContentView setHidden:[self stringValue] !== @""]; + var frame = [self frame], - - contentView = [self layoutEphemeralSubviewNamed:@"content-view" - positioned:CPWindowAbove - relativeToEphemeralSubviewNamed:@"bezel-view"]; - - if (contentView) - [contentView setHidden:[self stringValue] !== @""]; + contentView = [_tokenScrollView documentView]; // Correctly size the tableview // FIXME Horizontal scrolling will not work because we are not actually looking at the content to set the width for the table column @@ -802,7 +815,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [_autocompleteContainer setHidden:YES]; // Add every token as a seperate view - var contentRect = [self contentRectForBounds:[self bounds]], + var contentRect = CGRectMakeCopy([contentView bounds]), contentOrigin = contentRect.origin, contentSize = contentRect.size, offset = CPPointMake(contentOrigin.x, contentOrigin.y), @@ -812,6 +825,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (![[self _tokens] isKindOfClass:[CPArray class]]) return; + [contentView setSubviews:[]]; for (var i = 0; i < [[self _tokens] count]; i++) { var tokenView = [[self _tokens] objectAtIndex:i]; @@ -820,11 +834,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if ([tokenView isKindOfClass:[CPString class]]) continue; + [contentView addSubview:tokenView]; + [tokenView setHighlighted:[_selectedTokenIndexes containsIndex:i]]; [tokenView sizeToFit]; - // Increase the token fields height if the token view is outside of the bounds - var size = [self bounds].size, + var size = [contentView bounds].size, tokenViewSize = [tokenView bounds].size; if (contentSize.width < offset.x + tokenViewSize.width) @@ -834,27 +849,48 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", // Increase the y offset to fall below the current tokens offset.y += tokenViewSize.height + spaceBetweenTokens.height; - - if (offset.y + tokenViewSize.height > contentSize.height) - { - size.height += offset.y + tokenViewSize.height; - [self setFrameSize:size]; - } } + // Shrink to fit. + [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenViewSize.height)]; + [tokenView setFrameOrigin:offset]; offset.x += [tokenView bounds].size.width + spaceBetweenTokens.width; } + if (_scrollToLastToken) + { + // This code is responsible for showing the end of the token list + // by default when a new object value is set. + if ([[self window] firstResponder] != self) + [self _scrollTokenViewToVisible:[[self _tokens] lastObject]]; + _scrollToLastToken = NO; + } + if ([[self window] firstResponder] != self) return; - var element = [self _inputElement]; + var element = [self _inputElement], + tokenToken = [_CPTokenFieldToken new]; - element.style.left = offset.x + @"px"; - element.style.top = offset.y + @"px"; - element.style.width = [self bounds].size.width - offset.x - 8.0 + "px"; - element.style.height = contentRect.size.height; + // Get the height of a typical token, or a token token if you will, and make the editor that tall. + [tokenToken sizeToFit]; + + var inputFrame = CGRectMake(offset.x, offset.y, [self bounds].size.width - offset.x - 8.0, CGRectGetHeight([tokenToken bounds])); + element.style.left = inputFrame.origin.x + "px"; + element.style.top = inputFrame.origin.y + "px"; + element.style.width = inputFrame.size.width + "px"; + element.style.height = inputFrame.size.height + "px"; + + // When editing, always show the cursor. + [[_tokenScrollView documentView] scrollRectToVisible:inputFrame]; +} + +- (BOOL)_scrollTokenViewToVisible:(_CPTokenFieldToken)aToken +{ + if (!aToken) + return; + return [[_tokenScrollView documentView] scrollRectToVisible:[aToken frame]]; } // ====================== @@ -967,7 +1003,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (CGSize)_minimumFrameSize { - var size = CGRectMakeZero(), + var size = CGSizeMakeZero(), minSize = [self currentValueForThemeAttribute:@"min-size"], contentInset = [self currentValueForThemeAttribute:@"content-inset"]; diff --git a/AppKit/Themes/Aristo/ThemeDescriptors.j b/AppKit/Themes/Aristo/ThemeDescriptors.j index 23f4b7e1a..391f5b3f2 100755 --- a/AppKit/Themes/Aristo/ThemeDescriptors.j +++ b/AppKit/Themes/Aristo/ThemeDescriptors.j @@ -821,10 +821,10 @@ var themedButtonValues = nil, overrides = [ - [@"content-inset", CGInsetMake(7.0, 0.0, 7.0, 0.0)], + [@"content-inset", CGInsetMake(7.0, 0.0, 4.0, 0.0)], // Placeholder is displayed as regular text, not tokens; requires a different inset. [@"content-inset", CGInsetMake(9.0, 0.0, 5.0, 2.0), CPTextFieldStatePlaceholder], - [@"content-inset", CGInsetMake(7.0, 5.0, 7.0, 6.0), CPThemeStateBezeled], + [@"content-inset", CGInsetMake(7.0, 5.0, 4.0, 6.0), CPThemeStateBezeled], [@"content-inset", CGInsetMake(9.0, 7.0, 5.0, 8.0), CPThemeStateBezeled | CPTextFieldStatePlaceholder], ]; diff --git a/Tests/Manual/CPTokenFieldTest/AppController.j b/Tests/Manual/CPTokenFieldTest/AppController.j index 2441d942d..f6721e6c9 100644 --- a/Tests/Manual/CPTokenFieldTest/AppController.j +++ b/Tests/Manual/CPTokenFieldTest/AppController.j @@ -47,6 +47,20 @@ var STATES = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorad [contentView addSubview:tokenFieldB]; + var tokenFieldC = [[CPTokenField alloc] initWithFrame:CGRectMake(15, 170, 500, 30)], + labelC = [[CPTextField alloc] initWithFrame:CGRectMake(15, 150, 500, 24)]; + + [labelC setStringValue:"This token field can't fit all its tokens."]; + [contentView addSubview:labelC]; + + [tokenFieldC setEditable:YES]; + [tokenFieldC setPlaceholderString:"Edit me!"]; + + [tokenFieldC setObjectValue:['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado']]; + [tokenFieldC setDelegate:self]; + + [contentView addSubview:tokenFieldC]; + [theWindow orderFront:self]; } From b4f2f156981ce127ece344651543f174cdcb3969 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Thu, 21 Oct 2010 17:04:09 -0300 Subject: [PATCH 03/21] Fixed: if a token field had many tokens and a long token name was entered, that token name had to be edited in the small amount of space left on the right hand side of the token field. Now the editor wraps to a new line as needed. --- AppKit/CPTokenField.j | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 7f6c3b531..bfca2eb23 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -667,6 +667,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [CPTokenFieldInputOwner _delayedShowCompletions]; _selectedTokenIndexes = [CPIndexSet indexSet]; + // Force immediate layout in case word wrapping is now necessary. + [owner setNeedsLayout]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; } @@ -873,10 +875,30 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", var element = [self _inputElement], tokenToken = [_CPTokenFieldToken new]; - // Get the height of a typical token, or a token token if you will, and make the editor that tall. + // Get the height of a typical token, or a token token if you will. [tokenToken sizeToFit]; - var inputFrame = CGRectMake(offset.x, offset.y, [self bounds].size.width - offset.x - 8.0, CGRectGetHeight([tokenToken bounds])); + var tokenHeight = CGRectGetHeight([tokenToken bounds]); + + // Do we need to line wrap the editor element? + if (offset.x > contentOrigin.x) + { + // XXX The "X" here is used to estimate the space needed for one more character, + // so that we can wrap before we're out of bounds. Since different fonts + // might have different sizes of "X" this solution is not ideal, but it works. + var textWidth = [(element.value || @" ") + "X" sizeWithFont:[self font]].width; + + if (offset.x + textWidth >= contentSize.width) + { + offset.x = contentOrigin.x; + offset.y += tokenHeight + spaceBetweenTokens.height; + } + } + + // Make the content view just the right size. + [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)]; + + var inputFrame = CGRectMake(offset.x, offset.y, [contentView bounds].size.width - offset.x, tokenHeight); element.style.left = inputFrame.origin.x + "px"; element.style.top = inputFrame.origin.y + "px"; element.style.width = inputFrame.size.width + "px"; From 28378ff19b00ff8034277bc59e57efb055706dc7 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Thu, 21 Oct 2010 17:11:47 -0300 Subject: [PATCH 04/21] Minor performance tweaks. --- AppKit/CPTokenField.j | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index bfca2eb23..dfa561b70 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -823,12 +823,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", offset = CPPointMake(contentOrigin.x, contentOrigin.y), spaceBetweenTokens = CPSizeMake(2.0, 2.0); - // Hack to make sure we are handling with an array + // Hack to make sure we are handling an array if (![[self _tokens] isKindOfClass:[CPArray class]]) return; [contentView setSubviews:[]]; - for (var i = 0; i < [[self _tokens] count]; i++) + for (var i = 0, count = [[self _tokens] count]; i < count; i++) { var tokenView = [[self _tokens] objectAtIndex:i]; @@ -853,7 +853,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", offset.y += tokenViewSize.height + spaceBetweenTokens.height; } - // Shrink to fit. + // Make sure there is enough space to hold the new token. [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenViewSize.height)]; [tokenView setFrameOrigin:offset]; @@ -892,12 +892,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { offset.x = contentOrigin.x; offset.y += tokenHeight + spaceBetweenTokens.height; + [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)]; } } - // Make the content view just the right size. - [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)]; - var inputFrame = CGRectMake(offset.x, offset.y, [contentView bounds].size.width - offset.x, tokenHeight); element.style.left = inputFrame.origin.x + "px"; element.style.top = inputFrame.origin.y + "px"; From d0b29fd38c3e55c9a64a4c0e261f5163285ffae3 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Thu, 21 Oct 2010 17:49:41 -0300 Subject: [PATCH 05/21] Fix resizing and initial sizing of token fields. --- AppKit/CPTokenField.j | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index dfa561b70..bf85e8d0d 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -93,6 +93,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [_tokenScrollView setHasVerticalScroller:NO]; var contentView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; [_tokenScrollView setDocumentView:contentView]; + [contentView setAutoresizingMask:CPViewWidthSizable]; + [_tokenScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [self addSubview:_tokenScrollView]; _tokenIndex = 0; @@ -132,6 +134,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self setBezeled:YES]; [self setObjectValue:[]]; + [self setNeedsLayout]; } return self; @@ -431,7 +434,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (aValue === superValue || [aValue isEqualToArray:superValue]) return; - var objectValue = [aValue copy]; + var objectValue = [aValue copy], + contentView = [_tokenScrollView documentView]; // Because we do not know for sure which tokens are removed we remove them all for (var i = 0; i < [[self _tokens] count]; i++) @@ -450,6 +454,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView setTokenField:self]; [tokenView setStringValue:token]; [objectValue addObject:tokenView]; + + [contentView addSubview:tokenView]; } } @@ -827,7 +833,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (![[self _tokens] isKindOfClass:[CPArray class]]) return; - [contentView setSubviews:[]]; for (var i = 0, count = [[self _tokens] count]; i < count; i++) { var tokenView = [[self _tokens] objectAtIndex:i]; @@ -836,8 +841,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if ([tokenView isKindOfClass:[CPString class]]) continue; - [contentView addSubview:tokenView]; - [tokenView setHighlighted:[_selectedTokenIndexes containsIndex:i]]; [tokenView sizeToFit]; @@ -862,8 +865,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (_scrollToLastToken) { - // This code is responsible for showing the end of the token list - // by default when a new object value is set. + // This code is responsible for showing the last line of tokens + // initially. if ([[self window] firstResponder] != self) [self _scrollTokenViewToVisible:[[self _tokens] lastObject]]; _scrollToLastToken = NO; From e66f22d094c180d1cc24de7b857f4ea82fcb2ee0 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Thu, 21 Oct 2010 23:46:03 -0300 Subject: [PATCH 06/21] Allow the editor to be placed anywhere based on a selection range. --- AppKit/CPTokenField.j | 115 +++++++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index bf85e8d0d..8522bf816 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -61,6 +61,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPScrollView _tokenScrollView; BOOL _scrollToLastToken; + CPRange _selectedRange; + CPView _autocompleteContainer; CPScrollView _autocompleteScrollView; CPTableView _autocompleteView; @@ -88,6 +90,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { if (self = [super initWithFrame:frame]) { + _selectedRange = CPMakeRange(0, 0); + _tokenScrollView = [[CPScrollView alloc] initWithFrame:CGRectMakeZero()]; [_tokenScrollView setHasHorizontalScroller:NO]; [_tokenScrollView setHasVerticalScroller:NO]; @@ -469,6 +473,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", */ _value = objectValue; + // Place the cursor at the end + _selectedRange = CPMakeRange([[self _tokens] count], 0); + [self _updatePlaceholderState]; _scrollToLastToken = YES; @@ -827,14 +834,63 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", contentOrigin = contentRect.origin, contentSize = contentRect.size, offset = CPPointMake(contentOrigin.x, contentOrigin.y), - spaceBetweenTokens = CPSizeMake(2.0, 2.0); + spaceBetweenTokens = CPSizeMake(2.0, 2.0), + isEditing = [[self window] firstResponder] == self; // Hack to make sure we are handling an array if (![[self _tokens] isKindOfClass:[CPArray class]]) return; + var fitAndFrame = function(width, height) + { + var r = CGRectMake(0, 0, width, height); + + if (offset.x + width >= contentSize.width && offset.x > contentOrigin.x) + { + offset.x = contentOrigin.x; + offset.y += height + spaceBetweenTokens.height; + } + + r.origin.x = offset.x; + r.origin.y = offset.y; + + // Make sure the frame fits. + [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + height)]; + + offset.x += width + spaceBetweenTokens.width; + + return r; + } + + var placeEditor = function() + { + var element = [self _inputElement], + tokenToken = [_CPTokenFieldToken new]; + + // Get the height of a typical token, or a token token if you will. + [tokenToken sizeToFit]; + + // XXX The "X" here is used to estimate the space needed for one more character, + // so that we can wrap before we're out of bounds. Since different fonts + // might have different sizes of "X" this solution is not ideal, but it works. + var textWidth = [(element.value || @"") + "X" sizeWithFont:[self font]].width, + tokenHeight = CGRectGetHeight([tokenToken bounds]), + inputFrame = fitAndFrame(textWidth, tokenHeight); + + element.style.left = inputFrame.origin.x + "px"; + element.style.top = inputFrame.origin.y + "px"; + element.style.width = inputFrame.size.width + "px"; + element.style.height = inputFrame.size.height + "px"; + + // When editing, always show the cursor. + [[_tokenScrollView documentView] scrollRectToVisible:inputFrame]; + } + for (var i = 0, count = [[self _tokens] count]; i < count; i++) { + if (isEditing && i == _selectedRange.location) + placeEditor(i); + var tokenView = [[self _tokens] objectAtIndex:i]; // Make sure we are only changing completed tokens @@ -845,24 +901,15 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView sizeToFit]; var size = [contentView bounds].size, - tokenViewSize = [tokenView bounds].size; + tokenViewSize = [tokenView bounds].size, + tokenFrame = fitAndFrame(tokenViewSize.width, tokenViewSize.height); - if (contentSize.width < offset.x + tokenViewSize.width) - { - // Reset the x coordinate to the beginnning of the field - offset.x = contentOrigin.x; - - // Increase the y offset to fall below the current tokens - offset.y += tokenViewSize.height + spaceBetweenTokens.height; - } - - // Make sure there is enough space to hold the new token. - [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenViewSize.height)]; - - [tokenView setFrameOrigin:offset]; - offset.x += [tokenView bounds].size.width + spaceBetweenTokens.width; + [tokenView setFrame:tokenFrame]; } + if (isEditing && _selectedRange.location >= [[self _tokens] count]) + placeEditor(); + if (_scrollToLastToken) { // This code is responsible for showing the last line of tokens @@ -871,42 +918,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self _scrollTokenViewToVisible:[[self _tokens] lastObject]]; _scrollToLastToken = NO; } - - if ([[self window] firstResponder] != self) - return; - - var element = [self _inputElement], - tokenToken = [_CPTokenFieldToken new]; - - // Get the height of a typical token, or a token token if you will. - [tokenToken sizeToFit]; - - var tokenHeight = CGRectGetHeight([tokenToken bounds]); - - // Do we need to line wrap the editor element? - if (offset.x > contentOrigin.x) - { - // XXX The "X" here is used to estimate the space needed for one more character, - // so that we can wrap before we're out of bounds. Since different fonts - // might have different sizes of "X" this solution is not ideal, but it works. - var textWidth = [(element.value || @" ") + "X" sizeWithFont:[self font]].width; - - if (offset.x + textWidth >= contentSize.width) - { - offset.x = contentOrigin.x; - offset.y += tokenHeight + spaceBetweenTokens.height; - [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)]; - } - } - - var inputFrame = CGRectMake(offset.x, offset.y, [contentView bounds].size.width - offset.x, tokenHeight); - element.style.left = inputFrame.origin.x + "px"; - element.style.top = inputFrame.origin.y + "px"; - element.style.width = inputFrame.size.width + "px"; - element.style.height = inputFrame.size.height + "px"; - - // When editing, always show the cursor. - [[_tokenScrollView documentView] scrollRectToVisible:inputFrame]; } - (BOOL)_scrollTokenViewToVisible:(_CPTokenFieldToken)aToken From 85683d8f18419aa7a973e9ed00c932b7f4e87153 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Fri, 22 Oct 2010 20:43:40 -0300 Subject: [PATCH 07/21] Navigate among tokens with the arrow keys. Insert and delete tokens relative to the cursor. --- AppKit/CPTokenField.j | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 8522bf816..42662a9d7 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -192,15 +192,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", // Remove the uncompleted token and add the token string. // Explicitely remove the last object because the array contains strings and removeObject uses isEqual to compare objects if (shouldRemoveLastObject) - { - var indexOfLastObject = [objectValue count] - 1; - if (!indexOfLastObject) - indexOfLastObject = 0; + [objectValue removeObjectAtIndex:_selectedRange.location]; - [objectValue removeObjectAtIndex:indexOfLastObject]; - } - - [objectValue addObject:token]; + [objectValue insertObject:token atIndex:_selectedRange.location]; [self setObjectValue:objectValue]; _scrollToLastToken = NO; @@ -419,7 +413,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", #if PLATFORM(DOM) if ([self _inputElement].value != @"") - [objectValue addObject:[self _inputElement].value]; + [objectValue insertObject:[self _inputElement].value atIndex:_selectedRange.location]; #endif @@ -550,7 +544,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (aDOMEvent.keyCode === CPUpArrowKeyCode) index -= 1; - else if (aDOMEvent.keyCode === CPDownArrowKeyCode) index += 1; @@ -564,7 +557,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", var autocompleteView = [CPTokenFieldInputOwner autocompleteView], clipView = [[autocompleteView enclosingScrollView] contentView], - rowRect = [autocompleteView rectOfRow:index]; + rowRect = [autocompleteView rectOfRow:index], + owner = CPTokenFieldInputOwner; // The clipview's and row it's width are equal, this makes sure the clipview can contain the row rect // rowRect.size.width -= 2.0; @@ -579,8 +573,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", aDOMEvent.stopPropagation(); aDOMEvent.cancelBubble = true; - var owner = CPTokenFieldInputOwner; - if (aDOMEvent && aDOMEvent.keyCode === CPReturnKeyCode) { // Only resign first responder if we weren't autocompleting @@ -617,10 +609,26 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", aDOMEvent.stopPropagation(); aDOMEvent.cancelBubble = true; } + else if (aDOMEvent.keyCode == CPLeftArrowKeyCode && owner._selectedRange.location > 0 && CPTokenFieldDOMInputElement.value == "") + { + // Move the cursor back one token if the input is empty and the left arrow key is pressed. + owner._selectedRange.location--; + // Collapse the range. + owner._selectedRange.length = 0; + [owner setNeedsLayout]; + } + else if (aDOMEvent.keyCode == CPRightArrowKeyCode && owner._selectedRange.location < [[owner _tokens] count] && CPTokenFieldDOMInputElement.value == "") + { + // Move the cursor forward one token if the input is empty and the right arrow key is pressed. + owner._selectedRange.location = MIN([[owner _tokens] count], owner._selectedRange.location + owner._selectedRange.length + 1); + // Collapse the range. + owner._selectedRange.length = 0; + [owner setNeedsLayout]; + } else if (aDOMEvent.keyCode === CPDeleteKeyCode) { // Highlight the previous token if backspace was pressed in an empty input element or re-show the completions view - if (CPTokenFieldDOMInputElement.value == @"") + if (CPTokenFieldDOMInputElement.value == @"" && CPTokenFieldInputOwner._selectedRange.location > 0) { [self _hideCompletions]; @@ -629,7 +637,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (![[CPTokenFieldInputOwner _selectedTokenIndexes] count]) { - var tokenView = [tokens lastObject]; + var tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)]; [CPTokenFieldInputOwner _setSelectedTokenIndexes:[CPIndexSet indexSetWithIndex:[tokens indexOfObject:tokenView]]]; [CPTokenFieldInputOwner _hideCompletions]; } From 2912acb4f5649cde661bf38e47b004de95f2647d Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Fri, 22 Oct 2010 23:45:48 -0300 Subject: [PATCH 08/21] Simplify: remove _selectedTokenIndexes since we now have _selectedRange. Hide the editor when tokens are selected. Preserve cursor position when tokens are deleted. --- AppKit/CPTokenField.j | 91 ++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 42662a9d7..4aa93f28b 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -71,8 +71,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPArray _cachedCompletions; - CPIndexSet _selectedTokenIndexes; - CPCharacterSet _tokenizingCharacterSet @accessors(property=tokenizingCharacterSet); } @@ -102,7 +100,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self addSubview:_tokenScrollView]; _tokenIndex = 0; - _selectedTokenIndexes = [CPIndexSet indexSet]; _cachedCompletions = []; _completionDelay = [CPTokenField defaultCompletionDelay]; @@ -216,10 +213,15 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { var indexOfToken = [[self _tokens] indexOfObject:token]; - if (extend) - [_selectedTokenIndexes addIndex:indexOfToken]; + if (indexOfToken == CPNotFound) + { + if (!extend) + _selectedRange = CPMakeRange([[self _tokens] count], 0); + } + else if (extend) + _selectedRange = CPUnionRange(_selectedRange, CPMakeRange(indexOfToken, 1)); else - _selectedTokenIndexes = [CPIndexSet indexSetWithIndex:indexOfToken]; + _selectedRange = CPMakeRange(indexOfToken, 1); [self setNeedsLayout]; } @@ -227,7 +229,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)_deselectToken:(_CPTokenFieldToken)token { var indexOfToken = [[self _tokens] indexOfObject:token]; - [_selectedTokenIndexes removeIndex:indexOfToken]; + + if (CPLocationInRange(indexOfToken, _selectedRange)) + _selectedRange = CPMakeRange(MAX(indexOfToken, _selectedRange.location), MIN(_selectedRange.length, indexOfToken - _selectedRange.location)); [self setNeedsLayout]; } @@ -240,6 +244,11 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [objectValue removeObjectAtIndex:indexOfToken]; [self setObjectValue:objectValue]; + [self _controlTextDidChange]; +} + +- (void)_controlTextDidChange +{ var theBinding = [CPKeyValueBinding getBinding:CPValueBinding forObject:self]; if (theBinding) @@ -248,15 +257,20 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]]; } -- (CPIndexSet)_selectedTokenIndexes +- (void)_removeSelectedTokens:(id)sender { - return _selectedTokenIndexes; -} + var tokens = [self objectValue]; -- (void)_setSelectedTokenIndexes:(CPIndexSet)selectedIndexes -{ - _selectedTokenIndexes = selectedIndexes; - [self setNeedsLayout]; + for (var i = 0; i < _selectedRange.length; i++) + [tokens removeObjectAtIndex:_selectedRange.location + i]; + + var collapsedSelection = _selectedRange.location; + [self setObjectValue:tokens]; + // setObjectValue moves the cursor to the end of the selection. We want it to stay + // where the selected tokens were. + _selectedRange = CPMakeRange(collapsedSelection, 0); + + [self _controlTextDidChange]; } // ============= @@ -367,7 +381,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", } #endif - _selectedTokenIndexes = [CPIndexSet indexSet]; [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; @@ -376,7 +389,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)mouseDown:(CPEvent)anEvent { - _selectedTokenIndexes = [CPIndexSet indexSet]; + [self _selectToken:nil byExtendingSelection:NO]; // CPTokenFieldFocusInput = YES; [super mouseDown:anEvent]; @@ -467,8 +480,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", */ _value = objectValue; - // Place the cursor at the end - _selectedRange = CPMakeRange([[self _tokens] count], 0); + // Reset the selection. + [self _selectToken:nil byExtendingSelection:NO]; [self _updatePlaceholderState]; @@ -628,34 +641,24 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", else if (aDOMEvent.keyCode === CPDeleteKeyCode) { // Highlight the previous token if backspace was pressed in an empty input element or re-show the completions view - if (CPTokenFieldDOMInputElement.value == @"" && CPTokenFieldInputOwner._selectedRange.location > 0) + if (CPTokenFieldDOMInputElement.value == @"") { [self _hideCompletions]; // var tokenViews = [[CPTokenFieldInputOwner _tokens] lastObject]; var tokens = [CPTokenFieldInputOwner _tokens]; - if (![[CPTokenFieldInputOwner _selectedTokenIndexes] count]) + if (CPEmptyRange(CPTokenFieldInputOwner._selectedRange)) { - var tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)]; - [CPTokenFieldInputOwner _setSelectedTokenIndexes:[CPIndexSet indexSetWithIndex:[tokens indexOfObject:tokenView]]]; - [CPTokenFieldInputOwner _hideCompletions]; + if (CPTokenFieldInputOwner._selectedRange.location > 0) + { + var tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)]; + [CPTokenFieldInputOwner _selectToken:tokenView byExtendingSelection:NO]; + [CPTokenFieldInputOwner _hideCompletions]; + } } else - { - var tokenViews = [tokens objectsAtIndexes:[CPTokenFieldInputOwner _selectedTokenIndexes]]; - - for (var i = 0; i < [tokenViews count]; i++) - { - var tokenView = [tokenViews objectAtIndex:i]; - - [tokenView removeFromSuperview]; - [[CPTokenFieldInputOwner _tokens] removeObject:tokenView]; - } - - [CPTokenFieldInputOwner _setSelectedTokenIndexes:[CPIndexSet indexSet]]; - } - + [CPTokenFieldInputOwner _removeSelectedTokens:nil]; } else [CPTokenFieldInputOwner _delayedShowCompletions]; @@ -686,7 +689,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", } [CPTokenFieldInputOwner _delayedShowCompletions]; - _selectedTokenIndexes = [CPIndexSet indexSet]; + // If there was a selection, collapse it now since we're typing in a new token. + owner._selectedRange.length = 0; // Force immediate layout in case word wrapping is now necessary. [owner setNeedsLayout]; @@ -896,7 +900,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", for (var i = 0, count = [[self _tokens] count]; i < count; i++) { - if (isEditing && i == _selectedRange.location) + if (isEditing && i == _selectedRange.location && !_selectedRange.length) placeEditor(i); var tokenView = [[self _tokens] objectAtIndex:i]; @@ -905,7 +909,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if ([tokenView isKindOfClass:[CPString class]]) continue; - [tokenView setHighlighted:[_selectedTokenIndexes containsIndex:i]]; + [tokenView setHighlighted:CPLocationInRange(i, _selectedRange)]; [tokenView sizeToFit]; var size = [contentView bounds].size, @@ -915,9 +919,14 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView setFrame:tokenFrame]; } - if (isEditing && _selectedRange.location >= [[self _tokens] count]) + if (isEditing && _selectedRange.location >= [[self _tokens] count] && !_selectedRange.length) placeEditor(); + // Hide the editor if there are selected tokens, but still keep it active + // so we can continue using our standard keyboard handling events. + if (_selectedRange.length) + [self _inputElement].style.left = "-10000px"; + if (_scrollToLastToken) { // This code is responsible for showing the last line of tokens From d4cfa4b24f713d587b5bac060fa2f81414dcef8c Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 00:00:00 -0300 Subject: [PATCH 09/21] Minor optimisations and cleanup. --- AppKit/CPTokenField.j | 51 +++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 4aa93f28b..76be283d1 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -93,10 +93,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", _tokenScrollView = [[CPScrollView alloc] initWithFrame:CGRectMakeZero()]; [_tokenScrollView setHasHorizontalScroller:NO]; [_tokenScrollView setHasVerticalScroller:NO]; - var contentView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; - [_tokenScrollView setDocumentView:contentView]; - [contentView setAutoresizingMask:CPViewWidthSizable]; [_tokenScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + var contentView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + [contentView setAutoresizingMask:CPViewWidthSizable]; + [_tokenScrollView setDocumentView:contentView]; + [self addSubview:_tokenScrollView]; _tokenIndex = 0; @@ -413,7 +415,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (id)objectValue { var objectValue = []; - for (var i = 0; i < [[self _tokens] count]; i++) + for (var i = 0, count = [[self _tokens] count]; i < count; i++) { var token = [[self _tokens] objectAtIndex:i]; @@ -449,7 +451,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", contentView = [_tokenScrollView documentView]; // Because we do not know for sure which tokens are removed we remove them all - for (var i = 0; i < [[self _tokens] count]; i++) + for (var i = 0, count = [[self _tokens] count]; i < count; i++) [[[self _tokens] objectAtIndex:i] removeFromSuperview]; objectValue = []; @@ -457,7 +459,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (aValue !== nil) { // Re-add all tokens - for (var i = 0; i < [aValue count]; i++) + for (var i = 0, count = [aValue count]; i < count; i++) { var token = [aValue objectAtIndex:i], tokenView = [[_CPTokenFieldToken alloc] init]; @@ -547,11 +549,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPTokenFieldKeyDownFunction = function(aDOMEvent) { - CPTokenFieldTextDidChangeValue = [CPTokenFieldInputOwner stringValue]; - - // CPTokenFieldKeyPressFunction(anEvent); aDOMEvent = aDOMEvent || window.event + CPTokenFieldTextDidChangeValue = [CPTokenFieldInputOwner stringValue]; + // Update the selectedIndex if necesary var index = [[CPTokenFieldInputOwner autocompleteView] selectedRow]; @@ -573,8 +574,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", rowRect = [autocompleteView rectOfRow:index], owner = CPTokenFieldInputOwner; - // The clipview's and row it's width are equal, this makes sure the clipview can contain the row rect - // rowRect.size.width -= 2.0; if (rowRect && !CPRectContainsRect([clipView bounds], rowRect)) [clipView scrollToPoint:[autocompleteView rectOfRow:index].origin]; @@ -586,19 +585,15 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", aDOMEvent.stopPropagation(); aDOMEvent.cancelBubble = true; - if (aDOMEvent && aDOMEvent.keyCode === CPReturnKeyCode) + // Only resign first responder if we weren't autocompleting + if (![CPTokenFieldInputOwner hasThemeState:CPThemeStateAutoCompleting]) { - // Only resign first responder if we weren't autocompleting - if (![CPTokenFieldInputOwner hasThemeState:CPThemeStateAutoCompleting]) + if (aDOMEvent && aDOMEvent.keyCode === CPReturnKeyCode) { [owner sendAction:[owner action] to:[owner target]]; [[owner window] makeFirstResponder:nil]; } - } - else if (aDOMEvent && aDOMEvent.keyCode === CPTabKeyCode) - { - // Only resign first responder if we weren't autocompleting - if (![CPTokenFieldInputOwner hasThemeState:CPThemeStateAutoCompleting]) + else if (aDOMEvent && aDOMEvent.keyCode === CPTabKeyCode) { if (!aDOMEvent.shiftKey) [[owner window] selectNextKeyView:owner]; @@ -645,14 +640,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { [self _hideCompletions]; - // var tokenViews = [[CPTokenFieldInputOwner _tokens] lastObject]; - var tokens = [CPTokenFieldInputOwner _tokens]; - if (CPEmptyRange(CPTokenFieldInputOwner._selectedRange)) { if (CPTokenFieldInputOwner._selectedRange.location > 0) { - var tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)]; + var tokens = [CPTokenFieldInputOwner _tokens], + tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)]; [CPTokenFieldInputOwner _selectToken:tokenView byExtendingSelection:NO]; [CPTokenFieldInputOwner _hideCompletions]; } @@ -712,7 +705,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPTokenFieldHandleBlur = function(anEvent) { - var owner = CPTokenFieldInputOwner; CPTokenFieldInputOwner = nil; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; @@ -793,9 +785,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)_cancelShowCompletions { if ([_showCompletionsTimer isValid]) - { [_showCompletionsTimer invalidate]; - } } - (void)_hideCompletions @@ -927,10 +917,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (_selectedRange.length) [self _inputElement].style.left = "-10000px"; + // This code is responsible for showing the last line of tokens + // initially. if (_scrollToLastToken) { - // This code is responsible for showing the last line of tokens - // initially. if ([[self window] firstResponder] != self) [self _scrollTokenViewToVisible:[[self _tokens] lastObject]]; _scrollToLastToken = NO; @@ -960,7 +950,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)tableViewSelectionDidChange:(CPNotification)notification { // make sure a mouse click in the tableview doesn't steal first responder state - window.setTimeout(function() { [[self window] makeFirstResponder:self]; }, 2.0); + window.setTimeout(function() + { + [[self window] makeFirstResponder:self]; + }, 2.0); } // ============= From d68f4359ac387453851ac95048277084303da430 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 00:22:40 -0300 Subject: [PATCH 10/21] Reduce token shifting when a token is selected. Ideally there shouldn't be any shifting at all, but placing a tiny editor at the right spot simplifies scroll handling. --- AppKit/CPTokenField.j | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 76be283d1..0c0fbdbdd 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -872,10 +872,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", // Get the height of a typical token, or a token token if you will. [tokenToken sizeToFit]; - // XXX The "X" here is used to estimate the space needed for one more character, - // so that we can wrap before we're out of bounds. Since different fonts - // might have different sizes of "X" this solution is not ideal, but it works. - var textWidth = [(element.value || @"") + "X" sizeWithFont:[self font]].width, + // XXX The "X" here is used to estimate the space needed to fit the next character + // without clipping. Since different fonts might have different sizes of "X" this + // solution is not ideal, but it works. Also, if tokens are selected, make a small + // 1 pixel editor as to not displace the tokens too much. We still want >0px because + // it makes an easy scroll to target. + var textWidth = _selectedRange.length ? 1 : [(element.value || @"") + "X" sizeWithFont:[self font]].width, tokenHeight = CGRectGetHeight([tokenToken bounds]), inputFrame = fitAndFrame(textWidth, tokenHeight); @@ -890,7 +892,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", for (var i = 0, count = [[self _tokens] count]; i < count; i++) { - if (isEditing && i == _selectedRange.location && !_selectedRange.length) + if (isEditing && i == _selectedRange.location) placeEditor(i); var tokenView = [[self _tokens] objectAtIndex:i]; @@ -909,7 +911,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView setFrame:tokenFrame]; } - if (isEditing && _selectedRange.location >= [[self _tokens] count] && !_selectedRange.length) + if (isEditing && _selectedRange.location >= [[self _tokens] count]) placeEditor(); // Hide the editor if there are selected tokens, but still keep it active From 07330a0ca83a13732f22bb7f98147c42cb7d9631 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 13:49:36 -0300 Subject: [PATCH 11/21] Select tokens by clicking on them. Select multiple tokens by holding down shift. Also fixed a bug when deleting multiple tokens at a time. --- AppKit/CPTokenField.j | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 0c0fbdbdd..c63d0a779 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -72,6 +72,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPArray _cachedCompletions; CPCharacterSet _tokenizingCharacterSet @accessors(property=tokenizingCharacterSet); + + CPEvent _mouseDownEvent; } + (CPCharacterSet)defaultTokenizingCharacterSet @@ -263,7 +265,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { var tokens = [self objectValue]; - for (var i = 0; i < _selectedRange.length; i++) + for (var i = _selectedRange.length - 1; i >= 0; i--) [tokens removeObjectAtIndex:_selectedRange.location + i]; var collapsedSelection = _selectedRange.location; @@ -391,12 +393,32 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)mouseDown:(CPEvent)anEvent { + _mouseDownEvent = anEvent; + [self _selectToken:nil byExtendingSelection:NO]; - // CPTokenFieldFocusInput = YES; [super mouseDown:anEvent]; } +- (void)mouseUp:(CPEvent)anEvent +{ + _mouseDownEvent = nil; +} + +- (void)mouseDownOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent +{ + _mouseDownEvent = anEvent; +} + +- (void)mouseUpOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent +{ + if (_mouseDownEvent && CGPointEqualToPoint([_mouseDownEvent locationInWindow], [anEvent locationInWindow])) + { + [[self window] makeFirstResponder:self]; + [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask]; + } +} + // =========== // = CONTROL = // =========== @@ -1083,7 +1105,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)mouseDown:(CPEvent)anEvent { - [_tokenField mouseDown:anEvent]; + [_tokenField mouseDownOnToken:self withEvent:anEvent]; +} + +- (void)mouseUp:(CPEvent)anEvent +{ + [_tokenField mouseUpOnToken:self withEvent:anEvent]; } - (void)_delete:(id)sender From 80b16ae9aba86a905c350c19f04d68ac0f83df41 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 14:07:46 -0300 Subject: [PATCH 12/21] Bluer selected tokens in token field. --- .../Resources/token-highlighted-center.png | Bin 150 -> 158 bytes .../Aristo/Resources/token-highlighted-left.png | Bin 511 -> 530 bytes .../Resources/token-highlighted-right.png | Bin 511 -> 537 bytes AppKit/Themes/Aristo/ThemeDescriptors.j | 6 +++++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/AppKit/Themes/Aristo/Resources/token-highlighted-center.png b/AppKit/Themes/Aristo/Resources/token-highlighted-center.png index ac45c130b1c5cb7623f5d2e8128aba74c3eed409..ce60ddbf0c5c36876cdeab5aa5d1b6932cd241a0 100644 GIT binary patch delta 92 zcmV-i0Hgnw0iFSnNmW2eL_t(|+G5I^v-Lldv4t%I)A48D7?}S4{Rbre{e!Xo{$*hL ygT#Vy|NQw2RRR`6==cX>$-h0#00K$?0R{kxIWe#!t~IFu0000}*li(^Q|t)!^RN6+fjgn3ojma+HC85{il|9^ht%lq|g8Xv_E{pvq+ oNRLtZ09#D$pIMA7Qy7?d7>o~UsY;ulUC00gp00i_>zopr0Ie1vM*si- diff --git a/AppKit/Themes/Aristo/Resources/token-highlighted-left.png b/AppKit/Themes/Aristo/Resources/token-highlighted-left.png index 4408affbf22f8d43a27ee6186b6319c13f1281ca..30f2cc1b95774886c609b2603471aeb8e81e5525 100644 GIT binary patch delta 467 zcmV;^0WALi1Cj)gNq@9SL_t(|+G70w|33pGBO^oJoUIHnFe$~G5lDeBbLxcUZfxuv z!HmqzhAeOuKuutUsXkyuF^$vI1tg?r2ndSU2nvfZaPjamuprd_W0;)g2UeKSG1q}t zP-K;iqKY7guoT1F?|&Jdef!722y+I^+OVo#O)*)egVHKmf`4zA`58X_{0Fxj;S7jk z79mO58A7s(g7^RMF#P%ThXEO|ASq;yZk+1EBOq+`ltGx`&+oqs=m14AI~PyzH&#A| zpTGYwU<1gCS(unv96m5`GyMDekDy{EMuz{=zZsYr82ptQZsoKkr>QQOx`K;b)HTKpX!3L&4~Zf1TL4^!l?~ zSCRxj-2TY@^*M?&FcK0FgM7zj;u)%~WN6v@n_I;A8%&!Z!%t=|gyP8hNes&y^N=-w z!jIE2Cd*ksT*`}uokItqxNaiYJT`i{2BlN002ov JPDHLkV1g+V?rH!4 delta 448 zcmV;x0YCnd1pfn&Nq?b9L_t(|+G70w|33pGBO}Ag6E_)PAlyQo5lDeB^PC+g+&H+n zf|;0E3|U|b{{4s0k(OYE%-zejs!Pf#&Jg0`u@U6sX5i*vXFw9aNDP6My{w`zOPXA3x#FgPROh z%p$3vIzvW8Q1JVgFF@P=F(3mLB!$fF%XYX3^6^^z`}LFIFPcIO#aw)R!OTF`ufLcI z(G{~WvamZa{QnCK876{?nf@^_Ni#4qG5r0{Kv?nLe}8`f-O9=Y2^|c8V&AXde_s3o z8ViabY#62(WPkC$uV25M`1lMlT6@bT1} zFFy#H{1=FSUO2d;_{ICrpFe*4iPy~_#lN=A?7V*a{E4K;Z$5r}`}sSHVvK|Y#B4y! z<&s#at?TI1E5OHV%*V;jz`@3XP+YS72t!Y#4YCGK5IGL1>~j{CQ}JTspjgCL56wg>f35D$X=4}^kv z@gyQW2;RJir}pBpwX}FkFNy~h6a=Yg!4H&p2oXgn3MM9O+1QP{+1;64$7GvM;)(;$ zVcvP(-#+v1NFTnfCtSu7mRVf=K0Ue(fN{B=D$8|UNQ*~lSAQ>PQ2!{%tIDtZ-R)$2 zZt6m4s+i>~o!~wcL+hJ`kAGSk;^zS_Nu5WF3 zT9)6>F7(wl5au|QPStRr$q}@5c1J0-zw**Bm-8=<%$NgFrp<7R7$VKFfwB3|??gH` zGt|noDfGHVgnz=35z$O2bKtk~s7E+Jo-Rtb>~dzte9eplCAb|=Ul+*!WT zu%)6wyVkDG4rln*6dzPojK*RX(ZbDr=J(~q;o6ULUQ@EF%)}}n#Pkxe?m;=dNF2?^IbYdJ4`Dxm;#hayFbt4n84AS@0Ly^x_OQ^}7TH?Z*zWTW1SykD z(+!+9p))K9g!HarX&6O+0bEDC-0YC0V{bO~vtOun)=umlPd}qFl~p zuBVf0OAlBg9#|(PREPx Date: Sat, 23 Oct 2010 14:36:42 -0300 Subject: [PATCH 13/21] Improve token field scrolling behaviour and performance: only resize the document view frame when necessary to prevent the loss of scroll position and display jumps. Fixed: if a token on a new line was clicked, the view would scroll up to the first line. --- AppKit/CPTokenField.j | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index c63d0a779..5d729bfbb 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -853,17 +853,23 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", else [_autocompleteContainer setHidden:YES]; - // Add every token as a seperate view + // Hack to make sure we are handling an array + if (![[self _tokens] isKindOfClass:[CPArray class]]) + return; + + // Move each token into the right position. var contentRect = CGRectMakeCopy([contentView bounds]), contentOrigin = contentRect.origin, contentSize = contentRect.size, offset = CPPointMake(contentOrigin.x, contentOrigin.y), spaceBetweenTokens = CPSizeMake(2.0, 2.0), - isEditing = [[self window] firstResponder] == self; + isEditing = [[self window] firstResponder] == self, + tokenToken = [_CPTokenFieldToken new]; - // Hack to make sure we are handling an array - if (![[self _tokens] isKindOfClass:[CPArray class]]) - return; + // Get the height of a typical token, or a token token if you will. + [tokenToken sizeToFit]; + + var tokenHeight = CGRectGetHeight([tokenToken bounds]); var fitAndFrame = function(width, height) { @@ -879,7 +885,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", r.origin.y = offset.y; // Make sure the frame fits. - [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + height)]; + if (CGRectGetHeight([contentView bounds]) < offset.y + height) + [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + height)]; offset.x += width + spaceBetweenTokens.width; @@ -888,19 +895,13 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", var placeEditor = function() { - var element = [self _inputElement], - tokenToken = [_CPTokenFieldToken new]; + var element = [self _inputElement]; - // Get the height of a typical token, or a token token if you will. - [tokenToken sizeToFit]; // XXX The "X" here is used to estimate the space needed to fit the next character // without clipping. Since different fonts might have different sizes of "X" this - // solution is not ideal, but it works. Also, if tokens are selected, make a small - // 1 pixel editor as to not displace the tokens too much. We still want >0px because - // it makes an easy scroll to target. + // solution is not ideal, but it works. var textWidth = _selectedRange.length ? 1 : [(element.value || @"") + "X" sizeWithFont:[self font]].width, - tokenHeight = CGRectGetHeight([tokenToken bounds]), inputFrame = fitAndFrame(textWidth, tokenHeight); element.style.left = inputFrame.origin.x + "px"; @@ -908,13 +909,14 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", element.style.width = inputFrame.size.width + "px"; element.style.height = inputFrame.size.height + "px"; - // When editing, always show the cursor. - [[_tokenScrollView documentView] scrollRectToVisible:inputFrame]; + // When editing, always scroll to the cursor. + if (_selectedRange.length == 0) + [[_tokenScrollView documentView] scrollRectToVisible:inputFrame]; } for (var i = 0, count = [[self _tokens] count]; i < count; i++) { - if (isEditing && i == _selectedRange.location) + if (isEditing && i == CPMaxRange(_selectedRange)) placeEditor(i); var tokenView = [[self _tokens] objectAtIndex:i]; @@ -933,7 +935,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView setFrame:tokenFrame]; } - if (isEditing && _selectedRange.location >= [[self _tokens] count]) + if (isEditing && CPMaxRange(_selectedRange) >= [[self _tokens] count]) placeEditor(); // Hide the editor if there are selected tokens, but still keep it active @@ -941,6 +943,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (_selectedRange.length) [self _inputElement].style.left = "-10000px"; + // Trim off any excess height downwards. + if (CGRectGetHeight([contentView bounds]) > offset.y + tokenHeight) + [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)]; + // This code is responsible for showing the last line of tokens // initially. if (_scrollToLastToken) @@ -955,6 +961,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { if (!aToken) return; + return [[_tokenScrollView documentView] scrollRectToVisible:[aToken frame]]; } From ba26b19567a68adccc75a2d99e5d5bb379d78388 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 15:41:41 -0300 Subject: [PATCH 14/21] Fixed: if the token field was scrolled up and a token was clicked, the scroll position would jump back down. --- AppKit/CPTokenField.j | 81 ++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 5d729bfbb..547c0382e 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -53,13 +53,17 @@ var CPTokenFieldDOMInputElement = nil, #endif -var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - CPTokenFieldTableColumnIdentifier = @"CPTokenFieldTableColumnIdentifier"; +var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", + CPTokenFieldTableColumnIdentifier = @"CPTokenFieldTableColumnIdentifier", + + CPScrollDestinationNone = 0, + CPScrollDestinationLeft = 1, + CPScrollDestinationRight = 2; @implementation CPTokenField : CPTextField { CPScrollView _tokenScrollView; - BOOL _scrollToLastToken; + int _shouldScrollTo; CPRange _selectedRange; @@ -197,7 +201,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [objectValue insertObject:token atIndex:_selectedRange.location]; [self setObjectValue:objectValue]; - _scrollToLastToken = NO; + _shouldScrollTo = CPScrollDestinationNone; [self _inputElement].value = @""; [self setNeedsLayout]; @@ -414,8 +418,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { if (_mouseDownEvent && CGPointEqualToPoint([_mouseDownEvent locationInWindow], [anEvent locationInWindow])) { - [[self window] makeFirstResponder:self]; [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask]; + [[self window] makeFirstResponder:self]; + // When a token is clicked on and the editor was active, it blurs and causes the field to + // resign first responder. The line above restores first responder status in that case, + // but the scroll position will be lost. So scroll to the just clicked token. + _shouldScrollTo = aToken; } } @@ -509,7 +517,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self _updatePlaceholderState]; - _scrollToLastToken = YES; + _shouldScrollTo = CPScrollDestinationRight; [self setNeedsLayout]; [self setNeedsDisplay:YES]; } @@ -643,16 +651,34 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { // Move the cursor back one token if the input is empty and the left arrow key is pressed. owner._selectedRange.location--; - // Collapse the range. - owner._selectedRange.length = 0; + if (!aDOMEvent.shiftKey) + { + // Collapse the range. + owner._selectedRange.length = 0; + } + else + { + // When shift is depressed, select the next token backwards. + owner._selectedRange.length++; + } + owner._shouldScrollTo = CPScrollDestinationLeft; [owner setNeedsLayout]; } else if (aDOMEvent.keyCode == CPRightArrowKeyCode && owner._selectedRange.location < [[owner _tokens] count] && CPTokenFieldDOMInputElement.value == "") { - // Move the cursor forward one token if the input is empty and the right arrow key is pressed. - owner._selectedRange.location = MIN([[owner _tokens] count], owner._selectedRange.location + owner._selectedRange.length + 1); - // Collapse the range. - owner._selectedRange.length = 0; + if (!aDOMEvent.shiftKey) + { + // Move the cursor forward one token if the input is empty and the right arrow key is pressed. + owner._selectedRange.location = MIN([[owner _tokens] count], owner._selectedRange.location + owner._selectedRange.length + 1); + // Collapse the range. + owner._selectedRange.length = 0; + } + else + { + // Leave the selection location in place but include the next token to the right. + owner._selectedRange.length++; + } + owner._shouldScrollTo = CPScrollDestinationRight; [owner setNeedsLayout]; } else if (aDOMEvent.keyCode === CPDeleteKeyCode) @@ -835,7 +861,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [textFieldContentView setHidden:[self stringValue] !== @""]; var frame = [self frame], - contentView = [_tokenScrollView documentView]; + contentView = [_tokenScrollView documentView], + tokens = [self _tokens]; // Correctly size the tableview // FIXME Horizontal scrolling will not work because we are not actually looking at the content to set the width for the table column @@ -854,7 +881,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [_autocompleteContainer setHidden:YES]; // Hack to make sure we are handling an array - if (![[self _tokens] isKindOfClass:[CPArray class]]) + if (![tokens isKindOfClass:[CPArray class]]) return; // Move each token into the right position. @@ -897,7 +924,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { var element = [self _inputElement]; - // XXX The "X" here is used to estimate the space needed to fit the next character // without clipping. Since different fonts might have different sizes of "X" this // solution is not ideal, but it works. @@ -914,12 +940,12 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [[_tokenScrollView documentView] scrollRectToVisible:inputFrame]; } - for (var i = 0, count = [[self _tokens] count]; i < count; i++) + for (var i = 0, count = [tokens count]; i < count; i++) { if (isEditing && i == CPMaxRange(_selectedRange)) placeEditor(i); - var tokenView = [[self _tokens] objectAtIndex:i]; + var tokenView = [tokens objectAtIndex:i]; // Make sure we are only changing completed tokens if ([tokenView isKindOfClass:[CPString class]]) @@ -935,7 +961,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [tokenView setFrame:tokenFrame]; } - if (isEditing && CPMaxRange(_selectedRange) >= [[self _tokens] count]) + if (isEditing && CPMaxRange(_selectedRange) >= [tokens count]) placeEditor(); // Hide the editor if there are selected tokens, but still keep it active @@ -947,13 +973,20 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (CGRectGetHeight([contentView bounds]) > offset.y + tokenHeight) [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)]; - // This code is responsible for showing the last line of tokens - // initially. - if (_scrollToLastToken) + if (_shouldScrollTo !== CPScrollDestinationNone) { - if ([[self window] firstResponder] != self) - [self _scrollTokenViewToVisible:[[self _tokens] lastObject]]; - _scrollToLastToken = NO; + // Only carry out the scroll if the cursor isn't visible. + if (!(isEditing && _selectedRange.length == 0)) + { + + var scrollToToken = _shouldScrollTo; + if (scrollToToken === CPScrollDestinationLeft) + scrollToToken = tokens[_selectedRange.location] + else if (scrollToToken === CPScrollDestinationRight) + scrollToToken = tokens[MAX(0, CPMaxRange(_selectedRange) - 1)]; + [self _scrollTokenViewToVisible:scrollToToken]; + } + _shouldScrollTo = CPScrollDestinationNone; } } From 7037151bfc75ae4e08643e1bf8ca803a4a191537 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 16:00:45 -0300 Subject: [PATCH 15/21] Fixed: clicking a token would cause the token field to resign first responder status, only to reclaim it on mouse up. This was caused by the framework's stop DOM event code blurring the field. Now simply refuse to resign first responder status between mouse down and mouse up. Note that this fixes the bug the previous commit did too, but that commit improves other scrolling behaviour so it gets to stay. --- AppKit/CPTokenField.j | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 547c0382e..cefa84aa2 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -78,6 +78,8 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", CPCharacterSet _tokenizingCharacterSet @accessors(property=tokenizingCharacterSet); CPEvent _mouseDownEvent; + + BOOL _preventResign; } + (CPCharacterSet)defaultTokenizingCharacterSet @@ -353,6 +355,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (BOOL)resignFirstResponder { + if (_preventResign) + return NO; + [self unsetThemeState:CPThemeStateEditing]; [self _updatePlaceholderState]; @@ -397,6 +402,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)mouseDown:(CPEvent)anEvent { + _preventResign = YES; _mouseDownEvent = anEvent; [self _selectToken:nil byExtendingSelection:NO]; @@ -406,11 +412,13 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", - (void)mouseUp:(CPEvent)anEvent { + _preventResign = NO; _mouseDownEvent = nil; } - (void)mouseDownOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent { + _preventResign = YES; _mouseDownEvent = anEvent; } @@ -420,11 +428,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask]; [[self window] makeFirstResponder:self]; - // When a token is clicked on and the editor was active, it blurs and causes the field to - // resign first responder. The line above restores first responder status in that case, - // but the scroll position will be lost. So scroll to the just clicked token. + // Snap to the token if it's only half visible due to mouse wheel scrolling. _shouldScrollTo = aToken; } + _preventResign = NO; } // =========== @@ -565,6 +572,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner._tokenScrollView documentView]._DOMElement != CPTokenFieldDOMInputElement.parentNode) return; + if (CPTokenFieldInputOwner && CPTokenFieldInputOwner._preventResign) + return false; + if (!CPTokenFieldInputResigning && !CPTokenFieldFocusInput) { [[CPTokenFieldInputOwner window] makeFirstResponder:nil]; From 8a7b6054ef0941979246c3b9b229ee69435f353a Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 16:58:02 -0300 Subject: [PATCH 16/21] If the text editing part of the token field is the last in the line, make it take up the remaining field width so that it captures clicks there. --- AppKit/CPTokenField.j | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index cefa84aa2..22145a0c9 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -930,15 +930,20 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", return r; } - var placeEditor = function() + var placeEditor = function(useRemainingWidth) { var element = [self _inputElement]; - // XXX The "X" here is used to estimate the space needed to fit the next character - // without clipping. Since different fonts might have different sizes of "X" this - // solution is not ideal, but it works. - var textWidth = _selectedRange.length ? 1 : [(element.value || @"") + "X" sizeWithFont:[self font]].width, - inputFrame = fitAndFrame(textWidth, tokenHeight); + var textWidth = 1; + if (_selectedRange.length === 0) + { + // XXX The "X" here is used to estimate the space needed to fit the next character + // without clipping. Since different fonts might have different sizes of "X" this + // solution is not ideal, but it works. + textWidth = useRemainingWidth ? contentSize.width - offset.x - 1 : [(element.value || @"") + "X" sizeWithFont:[self font]].width; + } + + var inputFrame = fitAndFrame(textWidth, tokenHeight); element.style.left = inputFrame.origin.x + "px"; element.style.top = inputFrame.origin.y + "px"; @@ -953,7 +958,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", for (var i = 0, count = [tokens count]; i < count; i++) { if (isEditing && i == CPMaxRange(_selectedRange)) - placeEditor(i); + placeEditor(false); var tokenView = [tokens objectAtIndex:i]; @@ -972,7 +977,7 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", } if (isEditing && CPMaxRange(_selectedRange) >= [tokens count]) - placeEditor(); + placeEditor(true); // Hide the editor if there are selected tokens, but still keep it active // so we can continue using our standard keyboard handling events. From 0c2301766ae07bb30fd9223d4c6beb58ee5c9e29 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 17:09:46 -0300 Subject: [PATCH 17/21] Place the cursor after the last inserted token when editing. --- AppKit/CPTokenField.j | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 22145a0c9..f90015e4f 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -202,8 +202,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [objectValue removeObjectAtIndex:_selectedRange.location]; [objectValue insertObject:token atIndex:_selectedRange.location]; + var location = _selectedRange.location; [self setObjectValue:objectValue]; - _shouldScrollTo = CPScrollDestinationNone; + _selectedRange = CPMakeRange(location + 1, 0); [self _inputElement].value = @""; [self setNeedsLayout]; From 90d12ef77e5b4b9202b5ef5e43929aee97757f41 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 19:31:08 -0300 Subject: [PATCH 18/21] Place the cursor correctly when arrow keys are used with one or more tokens selected. Fixed: the layout would not update to remove the gap for the editor if between tokens when resigning first responders. --- AppKit/CPTokenField.j | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index f90015e4f..c5f08a05a 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -361,10 +361,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", [self unsetThemeState:CPThemeStateEditing]; - [self _updatePlaceholderState]; - - [self setNeedsLayout]; - [self _autocomplete]; #if PLATFORM(DOM) @@ -396,6 +392,10 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", #endif + [self _updatePlaceholderState]; + + [self setNeedsLayout]; + [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; return YES; @@ -661,14 +661,17 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", else if (aDOMEvent.keyCode == CPLeftArrowKeyCode && owner._selectedRange.location > 0 && CPTokenFieldDOMInputElement.value == "") { // Move the cursor back one token if the input is empty and the left arrow key is pressed. - owner._selectedRange.location--; if (!aDOMEvent.shiftKey) { - // Collapse the range. - owner._selectedRange.length = 0; + if (owner._selectedRange.length) + // Simply collapse the range. + owner._selectedRange.length = 0; + else + owner._selectedRange.location--; } else { + owner._selectedRange.location--; // When shift is depressed, select the next token backwards. owner._selectedRange.length++; } @@ -679,10 +682,17 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", { if (!aDOMEvent.shiftKey) { - // Move the cursor forward one token if the input is empty and the right arrow key is pressed. - owner._selectedRange.location = MIN([[owner _tokens] count], owner._selectedRange.location + owner._selectedRange.length + 1); - // Collapse the range. - owner._selectedRange.length = 0; + if (owner._selectedRange.length) + { + // Place the cursor at the end of the selection and collapse. + owner._selectedRange.location = CPMaxRange(owner._selectedRange); + owner._selectedRange.length = 0; + } + else + { + // Move the cursor forward one token if the input is empty and the right arrow key is pressed. + owner._selectedRange.location = MIN([[owner _tokens] count], owner._selectedRange.location + owner._selectedRange.length + 1); + } } else { From 9e235f72369ad9be949b07e8d4f136cbf417c1ed Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 20:21:02 -0300 Subject: [PATCH 19/21] Fixed: clicking tokens in a token field could disable its keyboard handling due to the input field losing focus. --- AppKit/CPTokenField.j | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index c5f08a05a..877424ed1 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -992,8 +992,11 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", // Hide the editor if there are selected tokens, but still keep it active // so we can continue using our standard keyboard handling events. - if (_selectedRange.length) + if (isEditing && _selectedRange.length) + { [self _inputElement].style.left = "-10000px"; + [self _inputElement].focus(); + } // Trim off any excess height downwards. if (CGRectGetHeight([contentView bounds]) > offset.y + tokenHeight) From 6fd5f66a15653e8b365b5cb9fe95dc2d7e3ce661 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 20:42:01 -0300 Subject: [PATCH 20/21] Support forward delete in token fields. --- AppKit/CPTokenField.j | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 877424ed1..d979f0e7e 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -252,9 +252,15 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", var indexOfToken = [[self _tokens] indexOfObject:token], objectValue = [self objectValue]; + // If the token was selected, deselect it for selection preservation. + [self _deselectToken:token]; + // Preserve selection. + var selection = CPCopyRange(_selectedRange); [objectValue removeObjectAtIndex:indexOfToken]; [self setObjectValue:objectValue]; + _selectedRange = selection; + [self setNeedsLayout]; [self _controlTextDidChange]; } @@ -716,7 +722,6 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", var tokens = [CPTokenFieldInputOwner _tokens], tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)]; [CPTokenFieldInputOwner _selectToken:tokenView byExtendingSelection:NO]; - [CPTokenFieldInputOwner _hideCompletions]; } } else @@ -725,6 +730,20 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", else [CPTokenFieldInputOwner _delayedShowCompletions]; } + else if (aDOMEvent.keyCode === CPDeleteForwardKeyCode && CPTokenFieldDOMInputElement.value == @"") + { + // Delete forward if nothing is selected, else delete all selected. + [self _hideCompletions]; + + if (CPEmptyRange(CPTokenFieldInputOwner._selectedRange)) + { + var tokens = [CPTokenFieldInputOwner _tokens]; + if (CPTokenFieldInputOwner._selectedRange.location < [tokens count]) + [CPTokenFieldInputOwner _deleteToken:tokens[CPTokenFieldInputOwner._selectedRange.location]]; + } + else + [CPTokenFieldInputOwner _removeSelectedTokens:nil]; + } return true; } @@ -951,7 +970,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", // XXX The "X" here is used to estimate the space needed to fit the next character // without clipping. Since different fonts might have different sizes of "X" this // solution is not ideal, but it works. - textWidth = useRemainingWidth ? contentSize.width - offset.x - 1 : [(element.value || @"") + "X" sizeWithFont:[self font]].width; + textWidth = [(element.value || @"") + "X" sizeWithFont:[self font]].width; + if (useRemainingWidth) + textWidth = MAX(contentSize.width - offset.x - 1, textWidth); } var inputFrame = fitAndFrame(textWidth, tokenHeight); From b95bdca2a3badcad887df9105630dc8521e7e8a0 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Sat, 23 Oct 2010 20:47:11 -0300 Subject: [PATCH 21/21] Lint cleanup. --- AppKit/CPTokenField.j | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index d979f0e7e..85379894d 100755 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -962,9 +962,9 @@ var CPThemeStateAutoCompleting = @"CPThemeStateAutoCompleting", var placeEditor = function(useRemainingWidth) { - var element = [self _inputElement]; + var element = [self _inputElement], + textWidth = 1; - var textWidth = 1; if (_selectedRange.length === 0) { // XXX The "X" here is used to estimate the space needed to fit the next character