I am trying to set a label text with the text i selected from my tableview. Here are my codes
- (void)viewDidLoad
{
// Location *l = [[Location alloc]init];
// [l view];
[super viewDidLoad];
// Do any additional setup after loading the view.
[pLabel setText:[self selectedText]];
NSLog(#"%#1", [self selectedText]);
}
-(id) initWithTextSelected:(NSString *) text {
self.selectedText = text;
[pLabel setText:selectedText];
NSLog(#"%#2", [self selectedText]);
return self;
}
I can't seem to set my label. In addition, NSLog 1 prints out null while NSLog 2 displayed my selected value.
i think the selectedText at the viewDidLoad is null because it is not retained and instead of using setText: use pLabel.text = selectedText;
At NSLog 2 ou are setting self.selected text.
but in viewDidLoad there is any code that setting the selectedText. Be sure that you are setting your "selectedText".
Related
I have a NSTextView for which I want to use the find bar. The text is selectable, but not editable. I change the text in the text view programatically.
This setup can crash when NSTextFinder tries to select the next match after the text was changed. It seems NSTextFinder hold on to outdated ranges for incremental matches.
I tried several methods of changing the text:
[textView setString:#""];
or
NSTextStorage *newStorage = [[NSTextStorage alloc] initWithString:#""];
[textView.layoutManager replaceTextStorage:newStorage];
or
[textView.textStorage beginEditing];
[textView.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:#""]];
[textView.textStorage endEditing];
Only replaceTextStorage: calls -[NSTextFinder noteClientStringWillChange]. None of the above invokes -[NSTextFinder cancelFindIndicator].
Even with NSTextFinder notified about the text change it can crash on Find Next (command-G).
I have also tried creating my own NSTextFinder instance as suggested in this post. Even though NSTextView does not implement the NSTextFinderClient protocol this works and fails just the same as without the NSTextFinder instance.
What is the correct way to use NSTextFinder with NSTextView?
I had the same problem with the text view in my app, and what makes it even more annoying is that all "solutions" you find on the internet are either incorrect or at least incomplete. So here is my contribution.
When you set textView.useFindBar = YES in a NSTextView, this text view creates a NSTextFinder internally, and forwards the search/replace commands to it. Unfortunately, NSTextView does not seem to handle correctly the changes you make programmatically to its associated NSTextStorage, which causes the crashes you mention.
If you want to change this behavior, creating your private NSTextFinder is not enough: you also need to avoid the use by the text view of its default text finder, otherwise conflicts will occur and the new text finder won't be of much use.
To do this, you have to subclass NSTextView:
#interface MyTextView : NSTextView
- (void) resetTextFinder; // A method to reset the view's text finder when you change the text storage
#end
And in your text view, you have to override the responder methods used for controlling the text finder:
#interface MyTextView () <NSTextFinderClient>
{
NSTextFinder* _textFinder; // define your own text finder
}
#property (readonly) NSTextFinder* textFinder;
#end
#implementation MyTextView
// Text finder command validation (could also be done in method validateUserInterfaceItem: if you prefer)
- (BOOL) validateMenuItem:(NSMenuItem *)menuItem
{
BOOL isValidItem = NO;
if (menuItem.action == #selector(performTextFinderAction:)) {
isValidItem = [self.textFinder validateAction:menuItem.tag];
}
// validate other menu items if needed
// ...
// and don't forget to call the superclass
else {
isValidItem = [super validateMenuItem:menuItem];
}
return isValidItem;
}
// Text Finder
- (NSTextFinder*) textFinder
{
// Create the text finder on demand
if (_textFinder == nil) {
_textFinder = [[NSTextFinder alloc] init];
_textFinder.client = self;
_textFinder.findBarContainer = [self enclosingScrollView];
_textFinder.incrementalSearchingEnabled = YES;
_textFinder.incrementalSearchingShouldDimContentView = YES;
}
return _textFinder;
}
- (void) resetTextFinder
{
if (_textFinder != nil) {
// Hide the text finder
[_textFinder cancelFindIndicator];
[_textFinder performAction:NSTextFinderActionHideFindInterface];
// Clear its client and container properties
_textFinder.client = nil;
_textFinder.findBarContainer = nil;
// And delete it
_textFinder = nil;
}
}
// This is where the commands are actually sent to the text finder
- (void) performTextFinderAction:(id<NSValidatedUserInterfaceItem>)sender
{
[self.textFinder performAction:sender.tag];
}
#end
In your text view, you still need to set properties usesFindBar and incrementalSearchingEnabled to YES.
And before changing the view's text storage (or text storage contents) you just need to call [myTextView resetTextFinder]; to recreate a brand new text finder for your new content the next time you will do a search.
If you want more information about NSTextFinder, the best doc I have seen is in the AppKit Release Notes for OS X 10.7
The solution I had come up with seems rather similar to the one offered by #jlj. In both solutions NSTextView is used as client of NSTextFinder.
It seems that the main difference is that I don't hide the find bar on text change. I also hold onto my NSTextFinder instance. To do so I need to call [textFinder noteClientStringWillChange].
Changing text:
NSTextView *textView = self.textView;
NSTextFinder *textFinder = self.textFinder;
[textFinder cancelFindIndicator];
[textFinder noteClientStringWillChange];
[textView setString:#"New text"];
The rest of the view controller code looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
NSTextFinder *textFinder = [[NSTextFinder alloc] init];
[textFinder setClient:(id < NSTextFinderClient >)textView];
[textFinder setFindBarContainer:[textView enclosingScrollView]];
[textView setUsesFindBar:YES];
[textView setIncrementalSearchingEnabled:YES];
self.textFinder = textFinder;
}
- (void)viewWillDisappear
{
NSTextFinder *textFinder = self.textFinder;
[textFinder cancelFindIndicator];
[super viewWillDisappear];
}
- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
id target = [super supplementalTargetForAction:action sender:sender];
if (target != nil) {
return target;
}
if (action == #selector(performTextFinderAction:)) {
target = self.textView;
if (![target respondsToSelector:action]) {
target = [target supplementalTargetForAction:action sender:sender];
}
if ((target != self) && [target respondsToSelector:action]) {
return target;
}
}
return nil;
}
I have a dynamic NSTableView which can add a number of columns depending on the data provided. For each column I have set the header cell to be a NSPopUpButtonCell. (Side-note: I've had to use a custom subclass class for NSTableHeaderView otherwise the menu doesn't pop-up). All works well, apart from a duplicate or extra header button cell on the top right. It mirrors perfectly the previous column selection as shown in screenshots. My question is how do I stop the NSTableView from recycling the previous popup header cell? (By the way I have tried the setCornerView method but that only effects the header area above the vertical scrollbar.)
I came across the same problem this week. I went with the quick fix,
[_tableView sizeLastColumnToFit];
(However, after discussion with OP this requires that you use a subclass of NSPopUpButtonCell in the header and also NSTableHeaderView. I attach my solution below)
You can to this by combining the approaches outlined here,
PopUpTableHeaderCell
DataTableHeaderView
Here is a simplified snippet,
// PopUpTableHeaderCell.h
#import <Cocoa/Cocoa.h>
/* Credit: http://www.cocoabuilder.com/archive/cocoa/133285-placing-controls-inside-table-header-view-solution.html#133285 */
#interface PopUpTableHeaderCell : NSPopUpButtonCell
#property (strong) NSTableHeaderCell *tableHeaderCell; // Just used for drawing the background
#end
// PopUpTableHeaderCell.m
#implementation PopUpTableHeaderCell
- (id)init {
if (self = [super init]){
// Init our table header cell and set a blank title, ready for drawing
_tableHeaderCell = [[NSTableHeaderCell alloc] init];
[_tableHeaderCell setTitle:#""];
// Set up the popup cell attributes
[self setControlSize:NSMiniControlSize];
[self setArrowPosition:NSPopUpNoArrow];
[self setBordered:NO];
[self setBezeled:NO];
[self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
return self;
}
// We do all drawing ourselves to make our popup cell look like a header cell
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView{
[_tableHeaderCell drawWithFrame:cellFrame inView:controlView];
// Now draw the text and image over the top
[self drawInteriorWithFrame:cellFrame inView:controlView];
}
#end
Now for the NSTableViewHeader subclass.
//DataTableHeaderView.h
#import <Cocoa/Cocoa.h>
/* Credit: http://forums.macnn.com/79/developer-center/304072/problem-of-nspopupbuttoncell-within-nstableheaderview/ */
#interface DataTableHeaderView : NSTableHeaderView
#end
//DataTableHeaderView.m
#import "DataTableHeaderView.h"
/* Credit: http://forums.macnn.com/79/developer-center/304072/problem-of-nspopupbuttoncell-within-nstableheaderview/ */
#implementation DataTableHeaderView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)mouseDown:(NSEvent *)theEvent {
// Figure which column, if any, was clicked
NSPoint clickedPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
NSInteger columnIndex = [self columnAtPoint:clickedPoint];
if (columnIndex < 0) {
return [super mouseDown:theEvent];
}
NSRect columnRect = [self headerRectOfColumn:columnIndex];
// I want to preserve column resizing. If you do not, remove this
if (![self mouse:clickedPoint inRect:NSInsetRect(columnRect, 3, 0)]) {
return [super mouseDown:theEvent];
}
// Now, pop the cell's menu
[[[self.tableView.tableColumns objectAtIndex:columnIndex] headerCell] performClickWithFrame:columnRect inView:self];
[self setNeedsDisplay:YES];
}
- (BOOL)isOpaque {
return NO;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
#end
You can tie everything together in the AppDelegate -awakeFromNib or similar,
-(void) awakeFromNib {
/* NB the NSTableHeaderView class is changed to be an DataTableHeaderView in IB! */
NSUInteger numberOfColumnsWanted = 5;
for (NSUInteger i=0; i<numberOfColumnsWanted; i++) {
PopUpTableHeaderCell *headerCell;
headerCell = [[PopUpTableHeaderCell alloc] init];
[headerCell addItemWithTitle:#"item 1"];
[headerCell addItemWithTitle:#"item 2"];
[headerCell addItemWithTitle:#"item 3"];
NSTableColumn *column;
[column setHeaderCell:headerCell];
[column sizeToFit];
[_tableView addTableColumn:column];
}
/* If we don't do this we get a final (space filling) column with an unclickable (dummy) header */
[_tableView sizeLastColumnToFit];
}
Other than that I haven't figured out how to properly correct the drawing in that region.
It seems like it's the image of the last cell that is being duplicated. So I slightly more hack-ish approach would be to add a extra column to your table view with a blank name and which intentionally ignores the mouse clicks. Hopefully by setting the display properties of the last column you can make it look the way you want.
I couldn't find any NSTableView or NSTableViewDelegate method that allow control of this region, so may any other solution would be very complicated. I would be interested in a nice solution too, but I hope this gets you started!
I have this issue and i don't use NSPopUpButtonCell at all.
I just want to tell about other method how to hide this odd header. This methods will not remove an odd table column, i.e. if you have 2 'legal' columns and hide this extra 3rd column header, you will still be able to move separator between 2nd and 3rd column. But in this case you won't see redundant header even if you want to resize any column.
I still need solution how to completely remove the redundant column, and why this is happening. (and why Apple won't fix this bug?)
So... you can just calculate index of column which this header belongs to and according to this draw your header or don't. First, subclass NSTableHeaderCell and set it as a cell class for columns. Let assume your subclass named TableHeaderCell:
for column in self.tableView.tableColumns {
let col:NSTableColumn = column as! NSTableColumn
//you can operate with header cells even for view-based tableView's
//although the documentation says otherwise.
col.headerCell = TableHeaderCell(textCell: col.title)
//or what initialiser you will have
}
Then in TableHeaderCell's drawWithFrame method you should have:
override func drawWithFrame(cellFrame: NSRect, inView controlView: NSView) {
let headerView = controlView as! HashTableHeaderView
let columnIndex = headerView.columnAtPoint(cellFrame.origin)
if columnIndex == -1 {
return
}
//parent's drawWithFrame or your own draw logic:
super.drawWithFrame(cellFrame, inView: controlView)
}
After this you won't have redundant header drawn because it not belongs to any column and columnAtPoint method will return -1.
I am working on an app in which there are five UITextFields in one view controller, the user can fill the text fields they want and when they press a UIButton they'll get a randomized answer on a second view controller via a UILabel.
I got it to work so far, but let's say the user only fills the first two UITextFields and the random answer they get is from a blank, unfilled UITextField.
My question is: how do I make work so that the unfilled UITextFields are not part of the random count? Is this possible?
Here's the code:
FifthViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.choice1.delegate = self;
self.choice2.delegate = self;
self.choice3.delegate = self;
self.choice4.delegate = self;
self.choice5.delegate = self;
}
- (IBAction)choicebutton:(id)sender {
SixthViewController *SVC = [self.storyboard instantiateViewControllerWithIdentifier:#"SixthViewController"];
SVC.stringFromChoice1 = self.choice1.text;
SVC.stringFromChoice2 = self.choice2.text;
SVC.stringFromChoice3 = self.choice3.text;
SVC.stringFromChoice4 = self.choice4.text;
SVC.stringFromChoice5 = self.choice5.text;
[self presentViewController:SVC animated:YES completion:nil];
}
SixthViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.choiceAnswers = #[self.stringFromChoice1,
self.stringFromChoice2,
self.stringFromChoice3,
self.stringFromChoice4,
self.stringFromChoice5];
int index = arc4random() % [self.choiceAnswers count];
self.choiceanswer.text = self.choiceAnswers[index];
}
Thanks!
SixthViewController
You can check and see if a string is empty before adding it to self.choiceAnswers:
if(![self.stringFromChoice1 isEqualToString#""]);
{
[self.choiceAnswers addObject:self.stringFromChoice1];
}
Make sure that you are using an NSMutable array for self.choiceAnswers.
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)
}
}
}
}