Is Empty selection in an NSTableView with Source List Highlighting not allowed? - objective-c

I feel like I may be missing something obvious here, but if I have an NSTableView with it's Highlight set to Source List and with Empty selection enabled, I don't seem to be able to click on a blank row in the table to clear the selection.
Changing the Highlight to regular fixes the problem, but of course doesn't draw in the manner I'd like.
The table has no bindings and uses a custom data source. Is there a way to work around this limitation?

For now, I've ended up adding the following to my NSTableView subclass:
- (void)mouseDown:(NSEvent *)theEvent
{
[super mouseDown:theEvent];
if ( [self allowsEmptySelection] && [self selectionHighlightStyle] == NSTableViewSelectionHighlightStyleSourceList )
{
NSInteger row = [self rowAtPoint:[self convertPoint:[theEvent locationInWindow] fromView:nil]];
if ( row == -1 )
{
[self deselectAll:nil];
}
}
}

You can use target actions to accomplish this. During initialization do the following:
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.target = self;
self.tableView.action = #selector(singleClickAction:);
self.tableView.allowsEmptySelection = YES;
Then add a method to your class:
- (void)singleClickAction:(id)sender
{
NSInteger clickedRow = [sender clickedRow];
if (clickedRow < 0) {
[self.tableView deselectAll:self];
}
}

Related

Input Accessory View Realization

Absolutely beginner obj-c question.
I am totally frustrated last few days about to do my task, but it seems like I have a problem bigger than my level of knowledges now. Before posting this question I asked few times about this in different forms, but so far I haven't understanding of how to do it, so I search for turnkey solution.
Task:
Plain UITableView with two sections. I am interested only in first section to improve Input Accessory View to switch between four textFields in cells.
http://uaimage.com/image/62f08045
Custom cells are inherited from UITableViewCell and have a UITextField's in them as a property. So my task is to set first responder to different textFields.
Ideas:
to set textFields as a delegate for ViewController and then resign and set first responder in input acessory view methods
to tag textFields, set NSMutableArray filled with this textFields and then in -inputAccessoryViewNext and in -inputAccesoryViewPrev change responder
to tag cells by indexPath and get textFields from cell
But I'm unable to realize any of this advances correctly and nothing works for me yet, so very need help.
I'm attached UITableViewController.m and FDTextFieldCell.m (custom cell) above:
https://docs.google.com/open?id=0B5rYA7McNhFlSXBrLTFVU2hVd1U
Will be glad to reward anyone who can help by a modest bounty of reputation.
I think your second idea is the right approach. Add an instance variable to your table view controller like NSMutableArray *_textFields and initialize it in viewDidLoad.
Then, in your tableView:cellForRowAtIndexPath: method, add something like this every time you :
if ([indexPath row] == 0) {
FDTextFieldCell *cell = [self textFieldCell];
[[cell textLabel] setText:#"Ваше Имя"];
[[cell textField] setPlaceholder:#"Обязательно"];
[[cell textField] setText:[profile name]];
[[cell textField] setReturnKeyType:UIReturnKeyNext];
[[cell textField] setKeyboardType:UIKeyboardTypeDefault];
// ADD THIS
[[cell textField] setTag:[indexPath row]];
if (![_textFields containsObject:[cell textField]]) {
[_textFields addObject:[cell textField]];
[_textFields sortUsingDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"tag" ascending:YES]]];
}
return cell;
}
From there, you have a sorted array of your text fields, so you can implement your accessory methods like so:
- (void) inputAccessoryViewDidSelectNext:(FDInputAccessoryView *)view {
UITextField *textField = nil;
for (textField in _textFields) {
if ([textField isFirstResponder])
break;
}
NSInteger indexOfFirstResponder = [_textFields indexOfObject:textField];
NSInteger nextIndex = indexOfFirstResponder + 1;
if (nextIndex == [_textFields count])
nextIndex = 0;
UITextField *nextField = [_textFields objectAtIndex:nextIndex];
[nextField becomeFirstResponder];
}
- (void) inputAccessoryViewDidSelectPrev:(FDInputAccessoryView *)view {
UITextField *textField = nil;
for (textField in _textFields) {
if ([textField isFirstResponder])
break;
}
NSInteger indexOfFirstResponder = [_textFields indexOfObject:textField];
NSInteger previousIndex = indexOfFirstResponder - 1;
if (previousIndex < 0)
previousIndex = [_textFields count] - 1;
UITextField *previousField = [_textFields objectAtIndex:previousIndex];
[previousField becomeFirstResponder];
}

navigate through textfields

I'm trying to use the #PeyloW code that I found here How to navigate through textfields (Next / Done Buttons) but when I press the keyboard return button nothing happens. My tags are ok.
Header:
- (BOOL)textFieldShouldReturn:(UITextField*)textField;
Implementation:
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
NSInteger nextTag = textField.tag + 1;
UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
if (nextResponder) {
[nextResponder becomeFirstResponder];
} else {
[textField resignFirstResponder];
}
return NO;
}
What am I missing? My keyboard doesn't have next done, only return. Keep in mind that I'm very new to iOS.
EDIT:
I tried to debug the code by adding a breakpoint to it and the code isn't being triggered.
I don't like solutions that incorporate the tag. Instead I would put all inputfileds in the desired order into an array and in -textFieldShouldReturn: use the given textfield to get it's index from in the array. Then I would get the object at that index.
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
NSUInteger nextIndex = [arrayWithResponders indexOfObject:textField]+1 % [arrayWithResponders count];
UIResponder* nextResponder = [arrayWithTextFields objectAtIndex: nextIndex];
if (nextResponder) {
[nextResponder becomeFirstResponder];
} else {
[textField resignFirstResponder];
}
return NO;
}
You just added, that the breakpoints aren't triggered, so most likely you didn't set up the delegate.

Clickable Text with Hover Effect in NSTableView

I'm trying to create a column of clickable URL-type text (NOT URLs like this, but essentially a borderless, title button or text field cell with a tracking area for a hover effect) within an NSTableView.
1.) When the user hovers over a particular cell the text in that cell should draw an underline below the text (hover/trackable area effect).
2.) When the user clicks the text it should perform an action.
I've subclassed NSCell and NSTableView and added a tracking area within the custom tableview to try and track the mouse location of the individual cell of the table to notify the cell when to redraw itself. I can get the current row and column of the mouse location, but can't seem to get the right cell in my custom tableview's mouseMoved: method
-(void)mouseMoved:(NSEvent *)theEvent {
[super mouseMoved:theEvent];
NSPoint p = [self convertPoint:[theEvent locationInWindow] fromView:nil];
long column = [self columnAtPoint:p];
long row = [self rowAtPoint:p];
id cell = [[self.tableColumns objectAtIndex:column] dataCellForRow:row];
}
It gets the cell for the column, but doesn't get the right cell for that particular row. Perhaps I'm not fully understanding the dataCellForRow: function for NSTableColumn?
I know you can't quite add a tracking area for cells, but instead you must create the hit test for mouse clicks and then begin tracking once the hit test is successful (meaning the mouse is already down) and then use startTracking:, continueTracking:, and stopTracking: to get the mouse's position. The idea though is that it has a hover effect before any mouseDown: action.
Also, I can't just use a view-based tableview (which would be incredible) because my app must be 10.6 compatible.
I'm not sure what's wrong with your method of getting the cell, but you don't really need to get that to do what you want. I tested a way to do this that entailed creating a table view subclass to do the tracking in the mouse moved method. Here is the code for that subclass:
-(void)awakeFromNib {
NSTrackingArea *tracker = [[NSTrackingArea alloc] initWithRect:self.bounds options:NSTrackingMouseEnteredAndExited|NSTrackingMouseMoved|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:tracker];
self.rowNum = -1;
}
-(void)mouseMoved:(NSEvent *)theEvent {
NSPoint p = theEvent.locationInWindow;
NSPoint tablePoint = [self convertPoint:p fromView:nil];
NSInteger newRowNum = [self rowAtPoint:tablePoint];
NSInteger newColNum = [self columnAtPoint:tablePoint];
if (newColNum != self.colNum || newRowNum != self.rowNum) {
self.rowNum = newRowNum;
self.colNum = newColNum;
[self reloadData];
}
}
-(void)mouseEntered:(NSEvent *)theEvent {
[self reloadData];
}
-(void)mouseExited:(NSEvent *)theEvent {
self.rowNum = -1;
[self reloadData];
}
I put the array and table delegate and data source code in the app delegate (probably not the best place, but ok for testing).
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = #[#{#"name":#"Tom",#"age":#"47"},#{#"name":#"Dick",#"age":#"21"},#{#"name":#"Harry",#"age":#"27"}];
[self.table reloadData];
self.dict = [NSDictionary dictionaryWithObjectsAndKeys:#2,NSUnderlineStyleAttributeName,[NSColor redColor],NSForegroundColorAttributeName,nil];
}
- (NSInteger)numberOfRowsInTableView:(RDTableView *)aTableView {
return self.theData.count;
}
- (id)tableView:(RDTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
if (self.table.colNum == 0 && rowIndex == self.table.rowNum && [aTableColumn.identifier isEqualToString:#"Link"]) {
NSString *theName = [[self.theData objectAtIndex:rowIndex] valueForKey:#"name"];
return [[NSAttributedString alloc] initWithString:theName attributes:self.dict];
}else if ([aTableColumn.identifier isEqualToString:#"Link"]){
return [[self.theData objectAtIndex:rowIndex] valueForKey:#"name"];
}else{
return [[self.theData objectAtIndex:rowIndex] valueForKey:#"age"];
}
}
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification {
if (self.table.colNum == 0)
NSLog(#"%ld",[aNotification.object selectedRow]);
}
I use the delegate method tableViewSelectionDidChange: to implement the action if you click on a cell in the first column (which has the identifier "Link" set in IB).

Creating No Empty Selections in NSCollectionView

I have set up an NSCollectionView in a cocoa application. I have subclassed the collection view's NSCollectionViewItem to send me a custom NSNotification when one of its views it selected / deselected. I register to receive a notification within my controller object when this notification is posted. Within this method I tell the view that has just been selected that it is selected and tell it to redraw, which makes it shade itself grey.
The NSCollectionViewItem Subclass:
-(void)setSelected:(BOOL)flag {
[super setSelected:flag];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ASCollectionViewItemSetSelected"
object:nil
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:(ASListView *)self.view, #"view",
[NSNumber numberWithBool:flag], #"flag", nil]];}
The Controller Class (in the -(void)awakeFromNib Method):
//Register for selection changed notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(selectionChanged:)
name:#"ASCollectionViewItemSetSelected"
object:nil];
And the -(void)selectionChanged:(NSNotification *)notification method:
- (void)selectionChanged:(NSNotification *)notification {
// * * Must get the selected item and set its properties accordingly
//Get the flag
NSNumber *flagNumber = [notification.userInfo objectForKey:#"flag"];
BOOL flag = flagNumber.boolValue;
//Get the view
ASListView *listView = [notification.userInfo objectForKey:#"view"];
//Set the view's selected property
[listView setIsSelected:flag];
[listView setNeedsDisplay:YES];
//Log for testing
NSLog(#"SelectionChanged to: %d on view: %#", flag, listView);}
The application that contains this code requires there to be no empty selection within the collection view at any time. This is where i get my problem. I've tried checking when a view's selection is changed and reselecting it if there is no selection, and manually selecting the views using NSCollectionView's
-(void)setSelectionIndexes:(NSIndexSet *)indexes
But there is always a situation which occurs that causes there to be an empty selection in the collection view.
So I was wondering if there is an easier way to prevent an empty selection occurring in an NSCollectionView? I see no checkbox in interface builder.
Thanks in advance!
Ben
Update
I ended up just subclassing my NSCollectionView, and overriding the - (void)mouseDown:(NSEvent *)theEvent method. I only then sent the method [super mouseDown:theEvent]; if the click was in one of the subviews. Code:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint clickPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
int i = 0;
for (NSView *view in self.subviews) {
if (NSPointInRect(clickPoint, view.frame)) {
//Click is in rect
i = 1;
}
}
//The click wasnt in any of the rects
if (i != 0) {
[super mouseDown:theEvent];
}}
I ended up just subclassing my NSCollectionView, and overriding the - (void)mouseDown:(NSEvent *)theEvent method. I only then sent the method [super mouseDown:theEvent]; if the click was in one of the subviews. Code:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint clickPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
int i = 0;
for (NSView *view in self.subviews) {
if (NSPointInRect(clickPoint, view.frame)) {
//Click is in rect
i = 1;
}
}
//The click wasnt in any of the rects
if (i != 0) {
[super mouseDown:theEvent];
}}
I also wanted to avoid empty selection in my collection view.
The way I did it is also by subclassing, but I overrode -hitTest: instead of -mouseDown: to return nil in case the click wasn't on an item :
-(NSView *)hitTest:(NSPoint)aPoint {
// convert aPoint in self coordinate system
NSPoint localPoint = [self convertPoint:aPoint fromView:[self superview]];
// get the item count
NSUInteger itemCount = [[self content] count];
for(NSUInteger itemIndex = 0; itemIndex < itemCount; itemIndex += 1) {
// test the point in each item frame
NSRect itemFrame = [self frameForItemAtIndex:itemIndex];
if(NSPointInRect(localPoint, itemFrame)) {
return [[self itemAtIndex:itemIndex] view];
}
}
// not on an item
return nil;
}
Although I'm late to this thread, I thought I'd just chime in because I've had the same problem recently. I got around it using the following line of code:
[_collectionView setValue:#NO forKey:#"avoidsEmptySelection"];
There is one caveat: the avoidsEmptySelection property is not part of the official API although I think that it's pretty safe to assume that its the type of property that will stick around for a while.

Equivalent to a "ListBox" in XCode?

You know Visual Studio, that awesome element called "ListBox"? Just a box that would list a bunch of strings.
I am now working with XCode, and I found this class in the interface builder "NSScrollView". It seems to be able to list me a couple strings. It says it got a NSTextView inside, but, how do I access it?
I am not even sure if NSScrollView is the correct solution I need, but if I could simply access the NSTextView inside it, I think it would be enough.
See NSTableView.
As for getting to a text view inside a scroll view, create an Interface Builder outlet (IBOutlet) and connect it to the text view itself, rather than the scroll view.
To get to a text view inside a scroll view; you need to select the controller with your outlet defined; click and hold control and then drag the blue connection line from your controller to the top line of the scroll view; then just wait for a blue line to appear; this will then prompt to let you link your outlet to the text view.
Josh's answer above to use NSTableView is correct. For those not that familiar with it, it can seem like a much bigger task than it actually turns out to be. Hopefully this saves people some time.
Rather than fight with NSTableCellView assumptions, you can create any type of simple view you want and use auto layout (or even return a simple NSTextView. This is what I did to get more control over layout of my text strings:
#interface PreferenceTableViewCell : NSView
#property (nonnull, strong, readonly) NSTextField *tf;
#end
#implementation PreferenceTableViewCell
-(id)init
{
self = [super init];
if(self) {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.autoresizesSubviews = YES;
_tf = [NSTextField labelWithString:#""];
_ tf.translatesAutoresizingMaskIntoConstraints = NO;
_tf.autoresizesSubviews = YES;
[self addSubview:_tf];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(10)-[_tf]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_tf)]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:_tf attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
}
return self;
}
#end
Then put this whereever you need the list of strings (or controls, or whatever):
_tv = [NSTableView new];
_tv.translatesAutoresizingMaskIntoConstraints = NO;
_tv.autoresizesSubviews = YES;
_tv.focusRingType = NSFocusRingTypeNone;
_tv.delegate = self;
_tv.dataSource = self;
_tv.rowHeight = 40; // Use this to adjust the height of your cell or do it in cell.
_tv.headerView = nil;
_tv.selectionHighlightStyle = NSTableViewSelectionHighlightStyleRegular;
_tv.allowsColumnReordering = NO;
_tv.allowsColumnResizing = NO;
_tv.allowsEmptySelection = NO;
_tv.allowsTypeSelect = NO;
_tv.gridStyleMask = NSTableViewGridNone;
[panel addSubview:_tv];
// TableView Column
NSTableColumn *col1 = [[NSTableColumn alloc] initWithIdentifier:#"c1"];
col1.resizingMask = NSTableColumnAutoresizingMask;
[_tv addTableColumn:col1];
Then in whatever is set as the delegate and datasource for the NSTableView add these methods:
-(NSInteger)numberOfRowsInTableView:(NSTableView *)tv
{
return stringArray.count;
}
-(NSView *)tableView:(NSTableView *)tv viewForTableColumn:(NSTableColumn *)tc row:(NSInteger)row
{
// This can be ANY NSView based control built as shown above.
PreferenceTableViewCell *cell = [PreferenceTableViewCell new];
cell.tf.stringValue = stringArray[row];
return cell;
}
-(void)tableViewSelectionDidChange:(NSNotification *)notification
{
// Code to do whatever when a list item is selected.
}
That is basically it for a simple list. See the Apple Docs on NSTableView for more details on how to bind the table to a data sources and more complicated problems.