I have a NSTextField with a NSNumberFormatter. I set the formatter with a min of 0 (because I couldn't set it to 0.01), and the style to decimal. The NSTextField has a binding on its value with a float ivar, and the action is set to "Send On Enter Only". This works just fine.
What I'd like to do is if the user tries to erase the value and either clicks off, or presses enter, I want to restore the original value before editing.
I tried:
-(void) setNilValueForKey:(NSString*) key {
if ([key compare:#"valX"] == NSOrderedSame) {
self.valX = valX;
}
}
But this doesn't set the NSTextField. I'm at a loss, any help is appreciated.
Thanks
GW
For case of erasing, Implement the following delegate method as
- (void)controlTextDidChange:(NSNotification *)notification
{
NSTextField* textfield = [notification object];
if([textfield intValue] == 0)
{
[textfield setValue:valX];
}
}
For case of pressing Enter and clicking off, implement
- (void)controlTextDidEndEditing:(NSNotification *)obj
{
NSTextField* textfield = [notification object];
[textfield setValue:valX];
}
Related
I'm developing an app for MacOSX in Xcode
which is a screen with a group of NSTextFields selectable and editable all of them
Every time I press tab this changes from NSTextField to NSTextField in ordered way...
Now I want to remove textfield's first responder by clicking outside of any NSTextField or when I go to last NSTextField of the NSWindowController, lets call it descripcionBitacoraTextField
as far as I know there's a method called
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
and I added there next code
- (void)controlTextDidEndEditing:(NSNotification *)aNotification{
NSTextField *textField = (NSTextField *)[aNotification object];
if (textField == self.descripcionBitacoraTextField) {
[[textField window] makeFirstResponder:nil];
}
}
which triggers when I finish editing any textField but when I press Tab button for finishing last NSTextField, it starts focusing my first NSTextField like an endless carrousel
for clicking outside:
-(void)mouseDown:(NSEvent *)event {
[self.window makeFirstResponder:nil];
}
if you want to exit when you tab last NSTextField you can:
-(BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector{
NSTextField *textField = (NSTextField *) control;
if (commandSelector == #selector(insertTab:) && (textField == self.descripcionBitacoraTextField)) {
[self.window makeFirstResponder:[[textView window]nextResponder]];
return YES;
}
else if (commandSelector == #selector(insertTab:)){
[self.window makeFirstResponder:[self.contentView viewWithTag:(textField.tag +1)]];
return YES;
}
return NO;
}
In xcode last version, I am trying to get the keyDown event of an NSTextField.
However, despite following multiple tutorials on the internet (delegates, controllers...), I still can't receive it.
Any easy hint for me ?
Thanks !
I got sick of all the non answers to do it some other way people so I put my nose down and figured out a way to make this work. This isn't using keydown event directly but it is using the keydown in the block. And the behavior is exactly what I wanted.
Subclass the text field
.h
#interface LQRestrictedInputTextField : NSTextField
.m
In the become first responder setup a local event
static id eventMonitor = nil;
- (BOOL)becomeFirstResponder {
BOOL okToChange = [super becomeFirstResponder];
if (okToChange) {
[self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]];
if (!eventMonitor) {
eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^(NSEvent *event) {
NSString *characters = [event characters];
unichar character = [characters characterAtIndex:0];
NSString *characterString=[NSString stringWithFormat:#"%c",character];
NSArray *validNonAlphaNumericArray = #[#" ",#"(",#")",#"[",#"]",#":",#";",#"\'",#"\"",#".",#"<",#">",#",",#"{",#"}",#"|",#"=",#"+",#"-",#"_",#"?",#"#",
#(NSDownArrowFunctionKey),#(NSUpArrowFunctionKey),#(NSLeftArrowFunctionKey),#(NSRightArrowFunctionKey)];
if([[NSCharacterSet alphanumericCharacterSet] characterIsMember:character] || character == NSCarriageReturnCharacter || character == NSTabCharacter || character == NSDeleteCharacter || [validNonAlphaNumericArray containsObject:characterString ] ) { //[NSCharacterSet alphanumericCharacterSet]
} else {
NSBeep();
event=nil;
}
return event;
} ];
}
}
NSLog(#"become first responder");
return okToChange;
}
remove the event once the textfield editing ends
Also if you're using ARC I noticed you might need to assign the textview string to the stringValue. I nslog'd the stringValue and the value was retained. Without the nslog I had to assign the notification object string to the stringValue to keep it from getting released.
-(void) textDidEndEditing:(NSNotification *)notification {
[NSEvent removeMonitor:eventMonitor];
eventMonitor = nil;
NSTextView *textView=[notification object];
self.stringValue=textView.string;
}
You can subclass NStextField and use keyUp that works for NSTextField.
in .h
#interface MyTextField : NSTextField <NSTextFieldDelegate>
in .m
-(void)keyUp:(NSEvent *)theEvent {
NSLog(#"Pressed key in NStextField!");
}
Add UITextFieldDelegate to your .h like this
#interface ViewController : UIViewController <UITextFieldDelegate> {
Then you can use this to detect every key press in a text field
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
Return YES to allow the character that was pressed to be inserted into the field but you can add whatever code you need in here.
I have created my UITextField by code, without InterfaceBuilder. I want the keyboard to disappear when the button "Done" is pushed. How does the code know that I am referending to an UITextField and no to other one
First, thanks a lot.
My code is like this:
-(void)viewDidLoad {
[super viewDidLoad];
field = [[UITextField alloc] initWithFrame:CGRectMake(30, 100, 185, 30)];
field.adjustsFontSizeToFitWidth = YES;
field.textColor = [UIColor blackColor];
field.placeholder = #"Text";
field.keyboardType = UIKeyboardTypeDefault;
field.returnKeyType = UIReturnKeyDone;
field.backgroundColor = [UIColor blueColor];
field.delegate = self;
[self.view addSubview:field];
}
......
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
}
With this code I push the button Done and nothing happen. Is like that how you say?
Edit:
I've created two UITextField how I did with the previous one. But now, for each row I do this:
if ([indexPath row] == 0) {
[cell.contentView addSubview:pTextField];
}
else {
[cell.contentView addSubview:pTextField];
}
So with this code, the program received signal "EXC_BAD_ACCESS". Any idea why happen this?
How does the code know that I am referending to an UITextField and no to other one
Your textFieldShouldReturn: method's textField parameter will always be the text field that is currently active.
The method has to return a BOOL, you should be getting compiler warnings with it as it stands. Try
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
return YES;
}
Note that you are also currently leaking memory in the way you add the text field. You should set it as a property as per WrightCS's answer so that you can refer to it later on. So at the end of your viewDidLoad:
self.myTextField = field;
[field release];
Define your textField in your header, then you can use the following:
.h
#interface MyeViewController : UIViewController <UITextFieldDelegate>
{
UITextField * myTextField;
}
#end
.m
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
[myTextField resignFirstResponder];
/* textField here is referenced from
textFieldShouldReturn:(UITextField *)textField
*/
}
Make sure you set the delegate of your programatically created UITextField to self (the view controller that created the object) and implement the appropriate UITextFieldDelegate method (I think its textFieldShouldReturn:) and call in that method resignFirstResponder on the textField argument passed to the delegate method (which will be your UITextField).
I have text fields inside a custom view inside an NSOutlineView. Editing one of these cells requires a single click, a pause, and another single click. The first single click selects the table view row, and the second single click draws the cursor in the field. Double-clicking the cell, which lets you edit in a cell-based table view, only selects the row.
The behavior I want: one click to change the selection and edit.
What do I need to override to obtain this behavior?
I've read some other posts:
The NSTextField flyweight pattern wouldn't seem to apply to view-based table views, where the cell views are all instantiated from nibs.
I tried subclassing NSTextField like this solution describes, but my overridden mouseDown method is not called. Overridden awakeFromNib and viewWillDraw (mentioned in this post) are called. Of course mouseDown is called if I put the text field somewhere outside a table view.
By comparison, a NSSegmentedControl in my cell view changes its value without first selecting the row.
Here's the working solution adapted from the accepted response:
In outline view subclass:
-(void)mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
// Forward the click to the row's cell view
NSPoint selfPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
NSInteger row = [self rowAtPoint:selfPoint];
if (row>=0) [(CellViewSubclass *)[self viewAtColumn:0 row:row makeIfNecessary:NO]
mouseDownForTextFields:theEvent];
}
In table cell view subclass:
// Respond to clicks within text fields only, because other clicks will be duplicates of events passed to mouseDown
- (void)mouseDownForTextFields:(NSEvent *)theEvent {
// If shift or command are being held, we're selecting rows, so ignore
if ((NSCommandKeyMask | NSShiftKeyMask) & [theEvent modifierFlags]) return;
NSPoint selfPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
for (NSView *subview in [self subviews])
if ([subview isKindOfClass:[NSTextField class]])
if (NSPointInRect(selfPoint, [subview frame]))
[[self window] makeFirstResponder:subview];
}
Had the same problem. After much struggle, it magically worked when I selected None as against the default Regular (other option is Source List) for the Highlight option of the table view in IB!
Another option is the solution at https://stackoverflow.com/a/13579469/804616, which appears to be more specific but a little hacky compared to this.
I'll try to return the favor... Subclass NSOutlineView and override -mouseDown: like so:
- (void)mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
// Only take effect for double clicks; remove to allow for single clicks
if (theEvent.clickCount < 2) {
return;
}
// Get the row on which the user clicked
NSPoint localPoint = [self convertPoint:theEvent.locationInWindow
fromView:nil];
NSInteger row = [self rowAtPoint:localPoint];
// If the user didn't click on a row, we're done
if (row < 0) {
return;
}
// Get the view clicked on
NSTableCellView *view = [self viewAtColumn:0 row:row makeIfNecessary:NO];
// If the field can be edited, pop the editor into edit mode
if (view.textField.isEditable) {
[[view window] makeFirstResponder:view.textField];
}
}
You really want to override validateProposedFirstResponder and allow a particular first responder to be made (or not) depending on your logic. The implementation in NSTableView is (sort of) like this (I'm re-writing it to be pseudo code):
- (BOOL)validateProposedFirstResponder:(NSResponder *)responder forEvent:(NSEvent *)event {
// We want to not do anything for the following conditions:
// 1. We aren't view based (sometimes people have subviews in tables when they aren't view based)
// 2. The responder to valididate is ourselves (we send this up the chain, in case we are in another tableview)
// 3. We don't have a selection highlight style; in that case, we just let things go through, since the user can't appear to select anything anyways.
if (!isViewBased || responder == self || [self selectionHighlightStyle] == NSTableViewSelectionHighlightStyleNone) {
return [super validateProposedFirstResponder:responder forEvent:event];
}
if (![responder isKindOfClass:[NSControl class]]) {
// Let any non-control become first responder whenever it wants
result = YES;
// Exclude NSTableCellView.
if ([responder isKindOfClass:[NSTableCellView class]]) {
result = NO;
}
} else if ([responder isKindOfClass:[NSButton class]]) {
// Let all buttons go through; this would be caught later on in our hit testing, but we also do it here to make it cleaner and easier to read what we want. We want buttons to track at anytime without any restrictions. They are always valid to become the first responder. Text editing isn't.
result = YES;
} else if (event == nil) {
// If we don't have any event, then we will consider it valid only if it is already the first responder
NSResponder *currentResponder = self.window.firstResponder;
if (currentResponder != nil && [currentResponder isKindOfClass:[NSView class]] && [(NSView *)currentResponder isDescendantOf:(NSView *)responder]) {
result = YES;
}
} else {
if ([event type] == NSEventTypeLeftMouseDown || [event type] == NSEventTypeRightMouseDown) {
// If it was a double click, and we have a double action, then send that to the table
if ([self doubleAction] != NULL && [event clickCount] > 1) {
[cancel the first responder delay];
}
...
The code here checks to see if the text field
cell had text hit. If it did, it attempts to edit it on a delay.
Editing is simply making that NSTextField the first responder.
...
}
I wrote the following to support the case for when you have a more complex NSTableViewCell with multiple text fields or where the text field doesn't occupy the whole cell. There a trick in here for flipping y values because when you switch between the NSOutlineView or NSTableView and it's NSTableCellViews the coordinate system gets flipped.
- (void)mouseDown:(NSEvent *)theEvent
{
[super mouseDown: theEvent];
NSPoint thePoint = [self.window.contentView convertPoint: theEvent.locationInWindow
toView: self];
NSInteger row = [self rowAtPoint: thePoint];
if (row != -1) {
NSView *view = [self viewAtColumn: 0
row: row
makeIfNecessary: NO];
thePoint = [view convertPoint: thePoint
fromView: self];
if ([view isFlipped] != [self isFlipped])
thePoint.y = RectGetHeight(view.bounds) - thePoint.y;
view = [view hitTest: thePoint];
if ([view isKindOfClass: [NSTextField class]]) {
NSTextField *textField = (NSTextField *)view;
if (textField.isEnabled && textField.window.firstResponder != textField)
dispatch_async(dispatch_get_main_queue(), ^{
[textField selectText: nil];
});
}
}
}
Just want to point out that if all that you want is editing only (i.e. in a table without selection), overriding -hitTest: seems to be simpler and a more Cocoa-like:
- (NSView *)hitTest:(NSPoint)aPoint
{
NSInteger column = [self columnAtPoint: aPoint];
NSInteger row = [self rowAtPoint: aPoint];
// Give cell view a chance to override table hit testing
if (row != -1 && column != -1) {
NSView *cell = [self viewAtColumn:column row:row makeIfNecessary:NO];
// Use cell frame, since convertPoint: doesn't always seem to work.
NSRect frame = [self frameOfCellAtColumn:column row:row];
NSView *hit = [cell hitTest: NSMakePoint(aPoint.x + frame.origin.x, aPoint.y + frame.origin.y)];
if (hit)
return hit;
}
// Default implementation
return [super hitTest: aPoint];
}
Here is a swift 4.2 version of #Dov answer:
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if (event.clickCount < 2) {
return;
}
// Get the row on which the user clicked
let localPoint = self.convert(event.locationInWindow, from: nil)
let row = self.row(at: localPoint)
// If the user didn't click on a row, we're done
if (row < 0) {
return
}
DispatchQueue.main.async {[weak self] in
guard let self = self else {return}
// Get the view clicked on
if let clickedCell = self.view(atColumn: 0, row: row, makeIfNecessary: false) as? YourOutlineViewCellClass{
let pointInCell = clickedCell.convert(localPoint, from: self)
if (clickedCell.txtField.isEditable && clickedCell.txtField.hitTest(pointInCell) != nil){
clickedCell.window?.makeFirstResponder(clickedCell.txtField)
}
}
}
}
I need to have a NSTextField working with a NSStepper as being one control so that I can edit an integer value either by changing it directly on the text field or using the stepper up/down arrows.
In IB I've added both of these controls then connected NSStepper's takeIntValueFrom to NSTextField and that makes the text value to change whenever I click the stepper arrows. Problem is that if I edit the text field then click the stepper again it will forget about the value I manually edited and use the stepper's internal value.
What's the best/easiest way to have the stepper's value be updated whenever the text field value is changed?
Skip the takeIntValueFrom: method. Instead, bind both views to the same property in your controller. You may also want to create a formatter and hook up the text field's formatter outlet to it.
I would have a model with one integer variable, which represents the value of both controls.
In my controller, I would use one IBAction, connected to both controls, and two IBOutlets, one for each control. then I would have a method for updating outlets from model value.
IBOutlet NSStepper * stepper;
IBOutlet NSTextField * textField;
- (IBAction) controlDidChange: (id) sender
{
[model setValue:[sender integerValue]];
[self updateControls];
}
- (void) updateControls
{
[stepper setIntegerValue:[model value]];
[textField setIntegerValue:[model value]];
}
This is the principle. As said by Peter Hosey, a formatter may be useful on your text field, at least to take min and max values of stepper into account.
I found easy way is to bind stepper value to input and input value to stepper
#property (strong) IBOutlet NSTextField *timeInput;
#property (strong) IBOutlet NSStepper *timeStepper;
If one is keeping track of the value of a field in one's model, such as a current page number, then there's no need to keep another copy in the stepper control. I just configure the control to have an initial value of 0, and a range from -1 to 1. In the IBAction method for the stepper control, which gets called for any click (or for auto-repeat) on the control, ask for its current value, which will be 1 if the up-arrow was clicked, or -1 for the down-arrow. Immediately reset the control's current value to 0, and then update the model and anything else (the associated text field, or a new page view, etc.) with a new value based on the direction 1 or -1. E.g.,
- (IBAction) bumpPageNum:(id)sender
{
int whichWay = [sender intValue]; // Either 1 or -1
[sender setIntValue:0]; // Same behavior next time
[model changePageBy:whichWay];
}
This way, the stepper control doesn't have to be linked to any values in the model at all.
I did as Peter Hosey suggested as it seems to me the cleanest approach. Created a property in my controller:
int editValue_;
...
#property (nonatomic, readwrite) int editValue;
...
#synthesize editValue = editValue_;
then in IB for both controls in the Bindings tab I've set the "Bind to:" check box and selected my controller, then on the "Model Key Path" field set "editValue" and voilá, it worked! With just 3 lines of code and some IB editing. And if I need to change the value on my controller I use setEditValue: and the text field gets updated.
This is for people who care about Cocoa.
The only reason to use NSStepper together with NSTextField is because there is some number in the textfield.
Steps for complete advanced Cocoa solution (which is sadly missing here):
Step 1: add number formatters to your textfields and format as you wish.
Step 2: add NSObjectController and glue your textfields/steppers to it. This is a common mistake when people do direct bindings. Meh. Add respective keyPaths as you have in your model.
Step 3: make sure your textfields react to key events. Always missing by newbies. Hook textfield delegate to our controller and add code.
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
{
if (commandSelector == #selector(moveUp:) || commandSelector == #selector(moveDown:)) {
if (control == [self minAgeTextField]) {
return [[self minAgeStepper] sendAction:commandSelector to:[self minAgeStepper]];
}
if (control == [self maxAgeTextField]) {
return [[self maxAgeStepper] sendAction:commandSelector to:[self maxAgeStepper]];
}
}
return NO;
}
Step 4: Some glue code. This is also the place where we set content of our objectController.
#property (weak) IBOutlet NSObjectController *profilesFilterObjectController;
#property (weak) IBOutlet NSTextField *minAgeTextField;
#property (weak) IBOutlet NSTextField *maxAgeTextField;
#property (weak) IBOutlet NSStepper *minAgeStepper;
#property (weak) IBOutlet NSStepper *maxAgeStepper;
#property (nonatomic) ProfilesFilter *filter;
- (void)awakeFromNib //or viewDidLoad...
{
[self setFilter:[ProfilesFilter new]];
[[self profilesFilterObjectController] setContent:[self filter]];
}
Step 5: Validate your values (KVC validation)
#implementation ProfilesFilter
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError
{
if ([inKey isEqualToString:#"minAge"] || [inKey isEqualToString:#"maxAge"]) {
if (*ioValue == nil) {
return YES;
}
NSInteger minAge = [[self minAge] integerValue];
NSInteger maxAge = [[self maxAge] integerValue];
if ([inKey isEqualToString:#"minAge"]) {
if (maxAge != 0) {
*ioValue = #(MAX(18, MIN([*ioValue integerValue], maxAge)));
}
}
if ([inKey isEqualToString:#"maxAge"]) {
if (minAge != 0) {
*ioValue = #(MIN(99, MAX([*ioValue integerValue], minAge)));
}
}
}
return YES;
}
#end
Notes: Wrong values? NSNumberFormatter will show error. Max age lower than min age? We use KVC validation (step 5). Eureka!
BONUS: What if user holds CTRL or SHIFT or both (user wants slower or faster increment)? We can modify increment based on key pressed (subclass NSStepper and overrider increment getter and check e.g. NSEvent.modifierFlags.contains(.shift)).
- (double)increment
{
BOOL isShiftDown = ([NSEvent modifierFlags] & NSEventModifierFlagShift) ? YES : NO;
//BOOL isOptionDown = ([NSEvent modifierFlags] & NSEventModifierFlagOption) ? YES : NO;
double increment = ([self defaultIncrement] - 0.001 > 0) ? [self defaultIncrement] : 1.0;
if (isShiftDown) {
increment = increment * 5;
}
return increment;
}
Add this to - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
//JUST AN ILLUSTRATION, holding shift + key up doesn't send moveUp but moveUpAndModifySelection (be careful of crash, just modify the command to moveUp; if not `NSStepper` doesn't know `moveUpAndModifySelection`)
if (commandSelector == #selector(moveUpAndModifySelection:)) {
commandSelector = #selector(moveUp:);
}
if (commandSelector == #selector(moveToEndOfDocument:) || commandSelector == #selector(moveDownAndModifySelection:)) {
commandSelector = #selector(moveDown:);
}
PS: The best solution is to use custom NSTextField that has and draws stepper and controls all events. You end up with a smaller controller!