I'm trying to create a simple data entry application for the Mac (not iPhone).
It has an NSTableView (inside an NSScrollView) which contains an NSTextField into which the user keys data, line by line.
What I have so far is working okay, except that in order to actually be able to type anything into the text field, you have to press the 'Enter' key first. Then when you press the 'Enter' key at the end, the field leaves 'edit' mode, but the highlight just sits on the same row. Having to press 'Enter', 'down arrow', 'Enter' between each row of data entered is not very efficient.
What I would prefer is that when you press the 'Enter' key, the cursor moves down to the next row and the field is automatically placed into 'edit' mode.
I had thought that such a thing should be fairly straightforward, but I have been tearing my hair out all evening without any progress, and I still don't even know which way I should be heading.
I tried looking for some attribute settings in the 'inspectors' in Xcode, but couldn't see anything.
Searching online, suggestions seemed to keep coming back to somehow using First Responder. Playing around with this, I've only managed to get the blue highlight ring to move down around my TableView on demand, but this still doesn't seem to put the text field into 'edit' mode nor move to another line.
Finally I found some information about subclassing and implementing your own textfields, but this seems like a very long and convoluted way to achieve this.
Can someone point me in the right direction as to how to do this?
Am I maybe using the wrong kind of control? Perhaps something else is more appropriate. Note that each line is distinct, so I don't want things like word wrap etc.
You were on the right track with the First Responder, but it's important to not that text editing is handled by the window's Field Editor (an instance of NSTextView), which becomes the first responder when text editing begins. The field editor informs its client (in this case your Table View) during editing.
The easiest way to handle this would be to subclass NSTableView and override its textDidEndEditing: method. Something like this should get you started:
- (void) textDidEndEditing:(NSNotification *)notification {
[super textDidEndEditing:notification];
int textMovement = [[notification.userInfo valueForKey:#"NSTextMovement"] intValue];
if (NSReturnTextMovement == textMovement) {
NSText *fieldEditor = notification.object;
// The row and column for the cell that just ended editing
NSInteger row = [self rowAtPoint:fieldEditor.frame.origin];
NSInteger col = [self columnAtPoint:fieldEditor.frame.origin];
if (++col >= self.numberOfColumns) {
col = 0;
if (++row >= self.numberOfRows) return;
}
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:row]
byExtendingSelection:NO];
[self editColumn:col row:row withEvent:nil select:YES];
}
}
This will progress left-to-right, top-to-bottom through the table's columns and rows. Though you said you were using a NSTextField, I presume you meant you were using a NSTextFieldCell in a cell-based table. That's another important distinction. If you're not familiar with the concept of the field editor, I suggest you read up.
Related
I've programmed iOS since the beginning, but am new to Cocoa so please be gentle!
I have an NSOutlineView and have implemented cut/copy/paste from the main menu to cut/copy/paste the selected rows.
Now I also want to allow the user to select some text within an NSTextFieldCell, copy it, place the cursor in a different NSTextFieldCell, and paste it.
I have managed to discover that the user is working inside a cell, by setting a BOOL in:
- (BOOL)outlineView:(NSOutlineView *)anOutlineView shouldEditTableColumn:(NSTableColumn*)aTableColumn item:(id)anItem
and unsetting it in my notification for the end of editing, set up like this:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(editingDidEnd:)
name:NSControlTextDidEndEditingNotification object:nil];
This seems to work fine.
Then, in my cut, copy, paste actions, I can check the BOOL and do the right thing - copy whole rows if the user is not working within a cell, and copy text if the user is working within a cell.
However, I just can't find out how to get at what I need when the user is working within the cell.
I've considered using the NSText cut:, copy: paste: methods, as they should handle selection for me. But I don't think I have an NSText object anywhere!
Alternatively, I need to be able to read off the selected text from my NSTextFieldCell, save it, then discover the insertion point in the cell to be pasted into, and paste.
Or is there much better built-in support that I'm missing?
Any help gratefully received - particular solutions of course, but also links to background primers on text / cell / field handling in Cocoa.
As is always the way, I find a solution just after posting!
The answer is that the field editor, an NSTextView, is the first responder when the cell is being edited. Its superclass, NSText, is the one that supports cut:, copy: and paste:.
So my "cell edit" versions of the cut copy paste commands look like this:
-(void)pasteTextToCell:(id)sender {
NSTextView* fieldEditor = (NSTextView*)[[appDelegate mainWindow] firstResponder];
[fieldEditor paste:sender];
}
-(void)copyTextFromCell:(id)sender {
NSTextView* fieldEditor = (NSTextView*)[[appDelegate mainWindow] firstResponder];
[fieldEditor copy:sender];
}
-(void)deleteTextFromCell:(id)sender {
NSTextView* fieldEditor = (NSTextView*)[[appDelegate mainWindow] firstResponder];
[fieldEditor cut:sender];
}
I have a Core Data project.
Basically I have an NSTableView where I add some entities (using the "add:" selector), double clicking on the TableView opens a new NSWindow where is possible to edit the entity using some NSTextFields.
Each text field is binded to an attribute of the entity.
Everything is working fine, except the fact that the entity's attributes are updated only when a textfield lose the focus.
If I write on the first text field and then I move to the second one my entry is saved, but if I write on the first text field and I close the window I lose my changes.
How can I update my core data entity as soon as I write something in the text field? Should I use textDidChange:?
--- UPDATE ---
Unfortunately [context save] doesn't work. If I understand correctly the entity is not modified until the NSTextField resign first responder.
The only working solution for now is something like:
(void)controlTextDidChange:(NSNotification *)aNotification
{
NSTextField *tf = [aNotification object];
[self.window makeFirstResponder:tf];
}
but this is quite inelegant, and in any case I also still need to re-set the cursor at the end of the NSTextField.
Setting NSContinuouslyUpdatesValueBindingOption will cause the model to update every time the text field changes, which sets the dirty flag properly and causes the document to save on quit.
I think you could use DidEndEditing or TextDidChange, another way of doing this is handeling in the window close event, but I would not recommend it.
If you don't have one already, you can set a delegate on the window and use -windowWillClose: or observe the NSWindowWillCloseNotification. You can then call [[notification object] makeFirstResponder:[window initialFirstResponder]] to set the window's first responder to its initial first responder as the window is closing. This will cause the control that is first responder (e.g. NSTextField) to resign the first responder status and the binding will save the changes.
I have a custom view in a .xib file, which I use as the contentViewController for an MAAttachedWindow. The view has several NSTextFields in it.
When I open the MAAttachedWindow first time, everything is fine. Text shows up in all relevant text fields. Then, if I close the window (which sets it to nil) and then call it again (which reinitializes, using the same custom view as the contentViewController), the last firstResponder text field is now blank.
The strange thing is that if I click the "empty" text field, it shows the correct text. This can be edited, and behaves appropriately as long as this text field has focus. As soon as something else becomes firstResponder, the text vanishes again.
Updates:
Changing the color did not change the aforementioned behavior.
The text color does not change at any time during this process.
Placeholder text also is subject to the aforementioned behavior.
No errors are occurring at any time during this process.
This does not happen to NSSecureTextFields.
I first encountered this problem about 5 years ago with accessory view of a NSSavePanel.
The solution that I've found was to move the first responder to the panel itself, before it's closed. Here's my exact method:
- (void)windowDidEndSheet:(NSNotification *)notification
NSSavePanel *savePanel = [(XSDocument *)[self document] savePanel];
if (!savePanel)
return;
// this fixes a bug where on next opening one of accessory view's text field will be blank and behave strangely
[savePanel makeFirstResponder:savePanel];
}
Try changing color of textfield text to red color (or any other color) you may get what happens here.
I got it!
I simply needed to explicitly remove the viewController from its superview before closing (and subsequently deallocating) the MAAttachedWindow.
Try resigning all first responders before setting the window to nil.
How can I navigate through my table view with the arrow keys. Much like setAction: or setDoubleAction, but instead of reacting to clicks, react with the arrow keys moving up or down through the table.
In your table view delegate, implement tableView:shouldSelectRow:. Do whatever you want, then return YES. It'll get triggered as you select items in the table view.
I'm not sure what you mean because when I select something in a table I can move up and down in the table using the arrow keys. But if you want to customize the behavior more I have a solution. In one of my apps I wanted to detect when the return or enter key was pressed and then perform some action accordingly. I created a new class and made it a subclass of NSWindow. In interface builder I set the main window to be this class. Then I override the keyDown: method of NSWindow in that subclass. So whenever my main window is frontmost (first responder) then key presses are detected and filtered through the method. I'm sure you could do something similar for arrow presses. You might want to make your class a subclass of NSTableView instead of NSWindow depending on how you want to catch the key presses. I wanted it to work for the entire application but you may want it to work only when the table view is first responder.
- (void)keyDown:(NSEvent *)theEvent {
if ([theEvent type] == NSKeyDown) {
NSString* characters = [theEvent characters];
if (([characters length] > 0) && (([characters characterAtIndex:0] == NSCarriageReturnCharacter) || ([characters characterAtIndex:0] == NSEnterCharacter))) {
// do something here when return or enter is pressed
}
}
}
aha! Did you accidentally BREAK NSTableView by doing this?
#implementation NSTableView ( DeleteKeyCategory )
-( void ) keyDown: ( NSEvent * ) event
{
// ... do something ...
[super keyDown:event];
}
#end
For me, this had the nefarious side effect of REPLACING NSTableView's keyDown: routine, and it broke the cursor keys. (kind of like swizzling)
Lessons I've learned:
- avoid the keyDown: routine altogether.
- subclassing Apple NSControls will save work in the long run.
This is the type of mistake that makes using NSTableView very frustrating.
Maybe Apple could detect this kind of thing in the static analyzer?
Arrows are used for selection, not for performing any Action. The action that will be applied to the selected item will usually be set by the "action" or "doubleAction" property of the TableView.
Clicking on a table-row does two different things.
TRIES to select the table row (sometimes the table-row can REFUSE to be selected, that's why there is a "shouldSelect" delegate method).
If new selection took place, then the action is performed (with the tableView as the sender). There you can ask the table about the current selection and do whatever you need with it.
Please consider the situation when there are SEVERAL selected rows, or selected columns, or many other complicated situations.
In your case --- what I would recommend, is that you implement the
selectionDidChange:(NSNotigivation)notification;
NSTableView delegate call. This is called AFTER selection has changed, and you know by then the new current selection, and do whatever you want with the selected items.
I want to disallow dropping anything into my NSTextField. In my app, users can drag and drop iCal events into a different part of the GUI. Now I've had a test user who accidentally dropped the iCal event into the text field – but he didn't realize this because the text is inserted in the lines above the one that I see in my one-line text field.
(You can reveal the inserted text by clicking into the text field and using the keyboard to go one line up – but a normal user wouldn't do this because he/she wouldn't even have realized that something got inserted in the first place!)
I tried registerForDraggedTypes:[NSArray array]] (doesn't seem to have any effect) as well as implementing the draggingEntered: delegate method returning NSDragOperationNone (the delegate method isn't even invoked).
Any ideas?
EDIT: Of course dropping something onto an NSTextField only works when it has focus, as described by ssp in his blog and in the comments to a blog entry by Daniel Jalkut.
I am glad you discovered the comments in my blog post. I think they are the tip of the iceberg to discovering how to achieve what you're looking for.
You need to keep in mind that the reason dragging to an NSTextField works when it has focus, is that the NSTextField has itself been temporarily obscured by a richer, more powerful view (an NSTextView), which is called the "Field Editor."
Check out this section of Apple's documentation on the field editor:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/FieldEditor.html
To achieve what you're striving for, I think you might need to intercept the standard provision of the field editor for your NSTextFields, by implementing the window delegate method:
windowWillReturnFieldEditor:toObject:
This gives you the opportunity to either tweak the configuration on the NSTextView, or provide a completely new field editor object.
In the worst case scenario, you could provide your own NSTextView subclass as the field editor, which was designed to reject all drags.
This might work: If you subclass NSTextView and implement -acceptableDragTypes to return nil, then the text view will be disabled as a drag destination. I also had to implement the NSDraggingDestination methods -draggingEntered: and -draggingUpdated: to return NSDragOperationNone.
#implementation NoDragTextView
- (NSArray *)acceptableDragTypes
{
return nil;
}
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
return NSDragOperationNone;
}
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
{
return NSDragOperationNone;
}
#end
I was able to solve this problem by creating a custom NSTextView and implementing the enter and exit NSDraggingDestination protocol methods to set the NSTextView to hidden. Once the text field is hidden the superview will be able to catch the drag/drop events, or if the superview doesn't implement or want the drag/drop they are discarded
For example:
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
//hide so that the drop event falls through into superview's drag/drop view
[self setHidden:YES];
return NSDragOperationNone;
}
- (void)draggingExited:(id<NSDraggingInfo>)sender {
//show our field editor again since dragging is all over with
[self setHidden:NO];
}
Have you tried - (void)unregisterDraggedTypes from NSView?