Detecting the delete key being pressed - objective-c

I have looked around for ways to detect when the delete key is pressed. I came across Apple's key handling documentation and also some people trying this with work arounds. I am not sure which method to pursue. What I want to do is very simple:
-(void)deleteKeyWasPressed {
if (myTextField.text.length == 0) {
[previousTextField becomeFirstResponder];
}
}
But as far as I know this method does not exist.
What would be the best way of doing this?

iOS has no direct support for detecting the delete key (or any key other than Return). The best you can do is implement the textField:shouldChangeCharactersInRange:replacementString: delegate method. When the user taps the Delete key, the replacement string will be the empty string.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if (string.length == 0) {
// handle Delete (but this also handles the Cut menu as well)
} else {
// some other key or text is being pasted.
}
return YES;
}

The way that I have implemented it is to save the previous length of the textfield somewhere, and then compare the previous length to the current length. If the previous length is greater than the current length, then the delete key was pressed. Works well for me.

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.

shouldChangeCharactersInRange doesn't get the last number

I have this code that should catch exactly what the user types:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSLog(#"Catch -> %#",textfield.text);
}
But When the user digit the letter A, the console return me ('null'), When the user digit the letter AB, the console return me only the letter A, in this case the method shouldChangeCharactersInRange doesn't return the last letter that user types.
To change this I try to change my code to:
NSLog(#"Catch -> %#",string);
In this case, the console shows me only the last number. How can I solve this problem and pick the complete sentence that user digit?
This method is called BEFORE the text field has changed. If you speak English, you would have already known this just by reading the method name, one of the big advantages of Cocoa...
This method gives you a chance to say "NO, don't change the text in this text box!" or to change the text in the box before the change is made, or to change the text that will be added before it's added.
textfield.text won't reflect the change until after this method returns.
To catch the END of editing events for a textfield, put this in your viewDidLoad:
[yourTextField addTarget:self
action:#selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
Where yourTextField is a reference to the text field of interest.
Now write the method:
- (void)textFieldDidChange:(UITextfield *)textfield {
NSLog(#"Catch -> %#", textfield.text);
}
This delegate method for me usually starts out like this:
- (BOOL)textField:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
NSString *candidateString = [textView.text stringByReplacingCharactersInRange:range withString:text];
Now, candidateString will contain what you expect. It's done this way so that any manner of input (even a paste into the middle of the text) can be understood before and after the text field's text is updated

Switch UITextField keyboard from numbers to alphabet after user hits space

I want my Keyboard to change from UIKeyboardTypeNumbersAndPunctuation mode to normal text mode after the user hits space.
So he can easily type something like "34 loops".
Okay, you cannot get direct input notifications, but there's a solution I can think of.
Implement the following delegate method:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
The replacement string will, as its name states, contain the string that wants to replace the existing string in the text field. What you could do is check the last character of this string, and if it is indeed a space, then change the keyboard type:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
char lastCharacter = [string characterAtIndex:[string length] - 1]; //Get the last input character in the ext field.
if(lastCharacter == ' ')
{
//The last character is a space, so...
textField.keyboardType = UIKeyboardTypeDefault;
[textField resignFirstResponder];
[textField becomeFirstResponder];
}
return YES;
}
The answer given by Leonnears mostly answers the simple case, but there are many issues you need to consider. What happens if the user types 34, then a space, then delete? What happens when the user moves the caret from the end of loops to somewhere in the number part?
It all starts to get more difficult to cover each case. But more importantly, it starts to get annoying for the user as the keyboard starts changing on them. It makes it very difficult to just type in the text. I've never seen an app do what you propose and there is a good reason.
Let the user use the normal keyboard like they are used to. Everyone knows how to switch between letter and numbers. Having the keyboard automatically change will be unusual and confusing.

Want to make UITextView react to return key

I'm trying to implement a program in Xcode that's somewhat like a command line. I basically have a UITextView that can take multiple lines of text. Right now, I have a button that will make the necessary changes after the user is done entering commands, but I want to be able to have a method that gets called after the user hits the return key in the UITextView, so basically it makes changes after each command. Is it possible to do this?
The BOOL method mentioned above is a wrong answer... for one the person is checking the text from the TextView the moment before it is updated so they are viewing the old text... Also the methods are out of date.
This usage will work immediately once the return key is pressed (The current "answer" will not work until after the return key has been pressed and then ANOTHER key is pressed):
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([text isEqualToString:#"\n"]) {
NSLog(#"Return pressed");
} else {
NSLog(#"Other pressed");
}
return YES;
}
Don't forget to add the UITextViewDelegate to your .h file's protocols.
#interface ViewController : UIViewController <UITextViewDelegate> {
and set yourTextView.delegate = self; in .m file!
/*
note: This will also get called if a user copy-pastes just a line-break...
unlikely but possible. If you need to ignore pasted line-breaks for some
reason see here: http://stackoverflow.com/a/15933860/2057171 ...
Now for an unrelated-tip: If you want to accept pasted line breaks however
I suggest you add an "or" to the conditional statement and make it also
check if "text" isEqualToString #"\r" which is another form of line-break
not found on the iOS keyboard but it can, however, be found on a website
and copy-pasted into your textView. If you want to accept pasted text
with a line-break at the end you will need to change the
"isEqualToString" code above to say "hasSuffix", this will check for
any string %# with a "\n" at the end. (and again for "\r") but make
sure you don't call your "next" method until after `return YES;`
has been called and the text view has been updated, otherwise
you will get only the text that was there before the copy paste
since this is "shouldChangeTextInRange" method, not
"didChangeTextInRange", if you do this I suggest stripping the
"\n" or "\r" from the end of your final string after the copy-paste
was made and applied and the text was updated.
*/
If you set a delegate to the UITextView which implements
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
Then you can check if the last character is "\n", and get the text entered since the last command by doing
NSArray* components = [textView.text componentsSeparatedByString:#"\n"];
if ([components count] > 0) {
NSString* commandText = [components lastObject];
// and optionally clear the text view and hide the keyboard...
textView.text = #"";
[textView resignFirstResponder];
}
Note, I haven't tested this, just an idea:
- (void)textViewDidChange:(UITextView *)textView
{
if ([[textView.text substringWithRange:NSMakeRange(textView.text.length - 1, 1)] isEqualToString:#"\n"])
{
[textView resignFirstResponder];
[self methodYouWantToCall];
}
}
You can do this by setting up a delegate for the UITextView. See UITextViewDelegate Protocol Reference for details on what can be done.

How to validate all tokens are valid in an NSTokenField

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
}