How to validate all tokens are valid in an NSTokenField - objective-c

Apple have conveniently created a callback method that allows you to check that the new tokens that are being added to an NSTokenField are valid:
- (NSArray *)tokenField:(NSTokenField *)tokenField shouldAddObjects:(NSArray *)newTokens atIndex:(NSUInteger)index
I have implemented this, and it turns out that it works great except for in one case. If the user starts typing in a token, but has not yet completed typing the token, and the user presses the TAB key, the validation method is not called.
This means I am able to ensure that all tokens that are entered are valid unless the user works out they can press tab to bypass the validation.
Does anyone know what the correct way to handle this situation is?

I tried for a little while and I found that the token field calls control:isValidObject: of the NSControlTextEditingDelegate protocol when the Tab key is pressed. So you can implement a delegate method such as
- (BOOL)control:(NSControl *)control isValidObject:(id)object
{
NSLog(#"control:%#", control);
NSLog(#"object:%#", object);
return NO;
}
The 'object' parameter is the content of your incomplete token. If the method returns NO, the token will not be inserted to the array of valid tokens.

I'm also struggling with this problem and found that using control:isValidObject as suggested by zonble almost gets to the solution, but that it is difficult to determine whether to return NO or YES based on the object parameter. As far as I can tell this problem is only restricted to the tab key so I implemented a pair of methods as follows;
I realise that this is horribly ugly but it's the only way I could get the NSTokenField to avoid creating tokens on tab while not impinging on other NSTextField behaviours of NSTokenField (eg moving the cursor to a new position etc).
- (BOOL)control:(NSControl *)control isValidObject:(id)object
{
if (self.performingTab) {
self.performingTab=NO;
return NO;
} else {
return YES;
}
}
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor
doCommandBySelector:(SEL)commandSelector
{
if (commandSelector==#selector(insertTab:)) {
self.performingTab=YES;
}
return NO;
}

I've tried a slightly different approach and instead watch for the tab key, changing it to a return key. This delegate method first confirms it's the relevant token field and checks the command selector.)
Apologies for leaving this answer in Swift - hopefully allowable given the intervening 8.5 years.
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool
{
if control == tokenField, // my interested token field
commandSelector == #selector(insertTab(_:))
{
textView.insertNewline(self)
return true
}
return false
}

Related

How to make NSTextView balance delimiters with a double-click?

It's common to have a text editor for code or other structured content that balances delimiters of some sort; when you double click on a { it selects to the matching }, or similarly for ( ) pairs, [ ] pairs, etc. How can I implement this behavior in NSTextView in Cocoa/Obj-C?
(I will be posting an answer momentarily, since I found nothing on SO about this and spent today implementing a solution. Better answers are welcome.)
ADDENDUM:
This is not the same as this question, which is about NSTextField and is primarily concerned with NSTextField and field editor issues. If that question is solved by substituting a custom NSTextView subclass into the field editor, then that custom subclass could use the solution given here, of course; but there might be many other ways to solve the problem for NSTextField, and substituting a custom NSTextView subclass into the field editor is not obviously the right solution to that problem, and in any case a programmer concerned with delimiter balancing in NSTextView (which is presumably the more common problem) could care less about all of those NSTextField and field editor issues. So that is a different question – although I will add a link from that question to this one, as one possible direction it could go.
This is also not the same as this question, which is really about changing the definition of a "word" in NSTextView when a double-click occurs. As per Apple's documentation, these are different problems with different solutions; for delimiter-balancing (this question) Apple specifically recommends the use of NSTextView's selectionRangeForProposedRange:granularity: method, whereas for changing the definition of a word (that question) Apple specifically states that the selectionRangeForProposedRange:granularity: method should not be used.
In their Cocoa Text Architecture Guide (https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html), Apple suggests subclassing NSTextView and overriding selectionRangeForProposedRange:granularity: to achieve this sort of thing; they even say "For example, in a code editor you can provide a delegate that extends a double click on a brace or parenthesis character to its matching delimiter." However, it is not immediately clear how to achieve this, since you want the delimiter match to happen only at after a simple double-click on a delimiter, not after a double-click-drag or even a double-click-hold-release.
The best solution I could come up with involves overriding mouseDown: as well, and doing a little bookkeeping about the state of affairs. Maybe there is a simpler way. I've left out the core part of the code where the delimiter match actually gets calculated; that will depend on what delimiters you're matching, what syntactical complexities (strings, comments) might exist, and so forth. In my code I actually call a tokenizer to get a token stream, and I use that to find the matching delimiter. YMMV. So, here's what I've got:
In your NSTextView subclass interface (or class extension, better yet):
// these are used in selectionRangeForProposedRange:granularity:
// to balance delimiters properly
BOOL inEligibleDoubleClick;
NSTimeInterval doubleDownTime;
In your NSTextView subclass implementation:
- (void)mouseDown:(NSEvent *)theEvent
{
// Start out willing to work with a double-click for delimiter-balancing;
// see selectionRangeForProposedRange:proposedCharRange granularity: below
inEligibleDoubleClick = YES;
[super mouseDown:theEvent];
}
- (NSRange)selectionRangeForProposedRange:(NSRange)proposedCharRange
granularity:(NSSelectionGranularity)granularity
{
if ((granularity == NSSelectByWord) && inEligibleDoubleClick)
{
// The proposed range has to be zero-length to qualify
if (proposedCharRange.length == 0)
{
NSEvent *event = [NSApp currentEvent];
NSEventType eventType = [event type];
NSTimeInterval eventTime = [event timestamp];
if (eventType == NSLeftMouseDown)
{
// This is the mouseDown of the double-click; we do not want
// to modify the selection here, just log the time
doubleDownTime = eventTime;
}
else if (eventType == NSLeftMouseUp)
{
// After the double-click interval since the second mouseDown,
// the mouseUp is no longer eligible
if (eventTime - doubleDownTime <= [NSEvent doubleClickInterval])
{
NSString *scriptString = [[self textStorage] string];
...insert delimiter-finding code here...
...return the matched range, or NSBeep()...
}
else
{
inEligibleDoubleClick = false;
}
}
else
{
inEligibleDoubleClick = false;
}
}
else
{
inEligibleDoubleClick = false;
}
}
return [super selectionRangeForProposedRange:proposedCharRange
granularity:granularity];
}
It's a little fragile, because it relies on NSTextView's tracking working in a particular way and calling out to selectionRangeForProposedRange:granularity: in a particular way, but the assumptions are not large; I imagine it's pretty robust.

How to capture user input in real time in NSTextField?

I can capture string when user click the button.
and I also use the following method
- (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor
which declared in NSControlTextEditingDelegate Protocol.
And when user begin editing, the button will be available.
My Question is:
How to make the button disable when user delete all text (make the textField empty without clicking button)?
The above method seems can not do it...
If your NSText is a NSTextView you may also use NSTextDelegate Protocol and NSTextViewDelegate Protocol.
Among others, the NSTextDelegate Protocol declares this delegate method:
textDidChange:
Informs the delegate that the text object has changed its characters or formatting attributes.
- (void)textDidChange:(NSNotification *)aNotification
Discussion
The name of aNotification is NSTextDidChangeNotification.
The NSTextDidChangeNotification is documented here NSTextDidChangeNotification
Look at using the NSControlTextDidChangeNotification or controlTextDidChange: delegate method (from NSControl) which is posted by the text field. When you receive the callback you can examine the text currently in the field to decide what to do.
Many ways to do the same thing. This version lets you easily catch specific keystrokes of interest.
- (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString{
NSLog(#"last character entered: %#",replacementString);
}
for instance, if you were looking for the return key, you might then implement:
if ([replacementString characterAtIndex:0] == NSNewlineCharacter)
NSLog(#"return pressed");

Update property bound from text field without needing to press Enter

I have a text field and I bind it to an NSString instance variable.
When I type in the text field, it does not update the variable. It waits until I press the Enter key. I don't want to hit Enter every time.
What do I need to change in order to make the binding change value immediately?
By default, the value binding of an NSTextField does not update continuously. To fix this, you need, after selecting your text field, to check the "Continuously Updates Value" box in the Bindings Inspector under the Value heading:
However, most often, what you really want to do is update the property to which the text field is bound when the user has finished editing and presses a button ("Save" or "OK", for example). To do this, you needn't continuously update the property as described above, you just need to end editing. Daniel Jalkut provides an extremely useful implementation of just such a method:
#interface NSWindow (Editing)
- (void)endEditing;
#end
#implementation NSWindow (Editing)
- (void)endEditing
{
// Save the current first responder, respecting the fact
// that it might conceptually be the delegate of the
// field editor that is "first responder."
id oldFirstResponder = [oMainDocumentWindow firstResponder];
if ((oldFirstResponder != nil) &&
[oldFirstResponder isKindOfClass:[NSTextView class]] &&
[(NSTextView*)oldFirstResponder isFieldEditor])
{
// A field editor's delegate is the view we're editing
oldFirstResponder = [oldFirstResponder delegate];
if ([oldFirstResponder isKindOfClass:[NSResponder class]] == NO)
{
// Eh ... we'd better back off if
// this thing isn't a responder at all
oldFirstResponder = nil;
}
}
// Gracefully end all editing in our window (from Erik Buck).
// This will cause the user's changes to be committed.
if([oMainDocumentWindow makeFirstResponder:oMainDocumentWindow])
{
// All editing is now ended and delegate messages sent etc.
}
else
{
// For some reason the text object being edited will
// not resign first responder status so force an
/// end to editing anyway
[oMainDocumentWindow endEditingFor:nil];
}
// If we had a first responder before, restore it
if (oldFirstResponder != nil)
{
[oMainDocumentWindow makeFirstResponder:oldFirstResponder];
}
}
#end
So if for example you had a "Save" button targeting your view controller's method -save:, you would call
- (IBAction)save:(id)sender
{
[[[self view] window] endEditing];
//at this point, all properties bound to text fields have the same
//value as the contents of the text fields.
//save stuff...
}
The previous answer is beautiful, and I learned from it about tricking the Window/View/Document system to end-editing on everything at the programmer's will.
However, the default responder chain behavior (including the preservation of the first responder until the USER moved their focus to something else) is fundamental to the Mac's "look and feel" and I wouldn't mess with it lightly (I swear I did very powerful things in responder-chain manipulation, so I don't say that out of fear.)
In addition - there is even a simpler method - that does not require changing the binding. In the Interface-builder, select the text field, and select the "Attribute Inspector" tab. You'll see the following:
Checking the red-circled "continuous" will do the trick. This option is basic and older even than binding, and its main use is to allow validator object (a whole new story) to validate the text and change it on the fly, as the user types. When the text-field calls validator calls, it also updates bound values.

How do I handle a button tap according to Clean Code principles?

I have the following, seemingly simple piece of code handling button taps in an iOS application:
- (IBAction)tapKeypadButton:(UIButton *)sender {
NSString *buttonLabel = sender.titleLabel.text;
if ([buttonLabel isEqualToString:#"<"]) {
[self _tapBackButton];
} else {
[self _tapDigitButton:buttonLabel];
}
}
To completely follow the Clean Code principles by Robert C. Martin, would I need a ButtonTapFactory or something in the same line?
You have two types of buttons, with different behaviors (back button and digit button). To make this code clean, you should have two actions for each type. The type should not be determined by the contents of the text inside the button, but through a semantically meaningful way. (i.e. subclass).
Further, an action method should only contain a call to another method that does the actual logic. Everything else is not testable. In code:
- (IBAction) tapBackButton:(id) sender
{
[self _tapBackButton:sender];
}
- (IBAction) tapDigitButton:(id) sender
{
[self _tapDigitButton:sender];
}
This way you can have unit tests calling your methods without your UI code interfering. Please also note that I removed the label from the call to _tapDigitButton. The digit should not be parsed from the label, but be passed in a more semantically stable way, for example using the tag property.

NSTextField autocompletion delegate method not called

I implemented the following delegate method for NSTextField to add autocompletion support:
- (NSArray *)control:(NSControl *)control
textView:(NSTextView *)textView
completions:(NSArray *)words
forPartialWordRange:(NSRange)charRange
indexOfSelectedItem:(NSInteger *)index
The issue is that this method never gets called. I can verify that the delegate of the NSTextField is set properly because the other delegate methods function as they should.
You'll need to get complete: called on the text field's field editor at some point. That's what triggers the completions menu, but it doesn't get called automatically. If you don't have F5 bound to anything, try typing in your field and hit that. Completion should trigger then; Option-Esc may also work.
If you want auto completion, it takes some work. You could start with something like this:
- (void)controlTextDidChange:(NSNotification *)note {
if( amDoingAutoComplete ){
return;
} else {
amDoingAutoComplete = YES;
[[[note userInfo] objectForKey:#"NSFieldEditor"] complete:nil];
}
}
Some kind of flag is necessary because triggering completion will make NSControlTextDidChangeNotification be posted again, which causes this to be called, triggering completion, which changes the control text, which...
Obviously, you'll need to unset the flag at some point. This will depend on how you want to handle the user's interaction with autocompletion -- is there likely to only be one completion for a given start string, or will the user need to keep typing to narrow down possibilities (in which case you'll need to trigger autocompletion again)?
A simple flag might not quite do it, either; it seems that although the notification is re-posted, the field editor's string won't have changed -- it will only change in response to direct keyboard input. In my implementation of autocomplete, I found that I had to keep a copy of the "last typed string" and compare that each time to the field editor's contents.