Dont deselect a selected row in view based NStableview when clicked outside - cocoa

In my view based NSTableView i don't want to deselect the selected cell, when clicking in an empty part of the NSTableView, how to obey this default behavior?

Improved version from Neha's answer (this obeys the select / deselect)
Subclass NSTableView and implement :
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint globalLocation = [theEvent locationInWindow];
NSPoint localLocation = [self convertPoint:globalLocation fromView:nil];
NSInteger clickedRow = [self rowAtPoint:localLocation];
if(clickedRow != -1) {
[super mouseDown:theEvent];
}
}
We just ignore the event, when we don't hit a row...

Implement the mouse down event of your NSTableView by subclassing it. Inside it check if the clicked point is a row or empty area. If it is an empty area, then again select the previously selected rows of your table view.
- (void)mouseDown:(NSEvent *)theEvent
{
NSPoint globalLocation = [theEvent locationInWindow];
NSPoint localLocation = [self convertPoint:globalLocation fromView:nil];
NSInteger clickedRow = [self rowAtPoint:localLocation];
NSIndexSet* selectedRows = [self selectedRowIndexes];
NSLog(#"%ld",clickedRow);
[super mouseDown:theEvent];
if(clickedRow == -1)
{
[self selectRowIndexes:selectedRows byExtendingSelection:NO];
}
}

This the Swift4 version if anyone needs it:
override func mouseDown(with event: NSEvent) {
let globalLocation = event.locationInWindow
let localLocation = convert(globalLocation, from: nil)
let clickedRow = row(at: localLocation)
if clickedRow != -1 {
super.mouseDown(with: event)
}
}

Related

NSOutlineView show indentation marker

How do I generate indentation marker for NSOutlineView?
I am not sure if this is an inbuilt functionality because it appears in other apps like Instruments
Update
I tried solving the problem by iterating all the children of the item that the row represents and show the marker on all children rows based on indentation level, but I faced a few problems
How to handle the case where the item has thousands of children. One simply cannot draw marker to every row as NSOutlineView would draw rows as they are displayed
When I scroll the NSOutlineView, the mouse moves out of the specified row but mouseExited is not being called. Thus the user has to manually move the mouse to reload the highlighting.
I had solved this problem but my solution looks hacky hence wanted to know if there is a better solution. And hence the question
First to receive mouseEntered: and mouseExited: events you need to setup a tracking rect using NSTrackingArea.
I would start with a subclass of NSTableRowView thats overwrites setFrame: making sure the tracking rect gets updated when the view is resize:
#interface TableRowView : NSTableRowView {
NSBox *_box;
NSTrackingArea *_trackingArea;
}
#property (weak) id owner;
#property (copy) NSDictionary<id, id> *userInfo;
#property (nonatomic) CGFloat indentation;
#property (nonatomic) BOOL indentationMarkerHidden;
#end
#implementation TableRowView
- (void)setFrame:(NSRect)frame
{
[super setFrame:frame];
if (_trackingArea) {
[self removeTrackingArea:_trackingArea];
}
_trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInKeyWindow owner:[self owner] userInfo:[self userInfo]];
[self addTrackingArea:_trackingArea];
}
#end
To use the NSTableRowView subclass, implement the NSOutlineViewDelegate messages like this:
- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
TableRowView *view = [[TableRowView alloc] init];
view.owner = self;
view.userInfo = item;
return view;
}
With this in place you're ready to receive mouseEntered: and mouseExited: events. Use NSOutlineView levelForItem: together with indentationPerLevel to calculate the position of the marker NSBox.:
- (void)mouseEntered:(NSEvent *)event
{
id item = [event userData];
CGFloat indentation = [_outlineView levelForItem:item] * [_outlineView indentationPerLevel];
[self setIndentationMarker:indentation hidden:NO item:item];
}
- (void)mouseExited:(NSEvent *)event
{
id item = [event userData];
[self setIndentationMarker:0.0 hidden:YES item:item];
}
Now you get the NSTableRowView subclass by rowViewAtRow:makeIfNecessary: and recursively do the same for all children in your data:
- (void)setIndentationMarker:(CGFloat)indentation hidden:(BOOL)hidden item:(NSDictionary *)item
{
TableRowView *view = [_outlineView rowViewAtRow:[_outlineView rowForItem:item] makeIfNecessary:NO];
view.indentationMarkerHidden = hidden;
view.indentation = indentation;
for (NSMutableDictionary *child in [item objectForKey:#"children"]) {
[self setIndentationMarker:indentation hidden:hidden item:child];
}
}
Now layout the NSBox the NSTableRowView subclass:
#implementation TableRowView
- (instancetype)init
{
self = [super init];
if (self) {
_indentationMarkerHidden = YES;
_box = [[NSBox alloc] init];
_box.boxType = NSBoxCustom;
_box.borderWidth = 0.0;
_box.fillColor = [NSColor tertiaryLabelColor];
_box.hidden = _indentationMarkerHidden;
[self addSubview:_box];
}
return self;
}
- (void)layout
{
[super layout];
NSRect rect = [self bounds];
rect.origin.x = _indentation + 7;
rect.size.width = 10;
_box.frame = rect;
}
- (void)setIndentation:(CGFloat)indentation
{
_indentation = indentation;
[self setNeedsLayout:YES];
}
- (void)setIndentationMarkerHidden:(BOOL)indentationMarkerHidden
{
if (_indentationMarkerHidden != indentationMarkerHidden) {
_indentationMarkerHidden = indentationMarkerHidden;
_box.hidden = indentationMarkerHidden;
}
}
#end
This enough to make a basic version like here:

Button to Select Random Cell in tableView

How could I create a button on the Table View that when touched randomly selects one of the cells?
I know how to add Round Rect Button and all, just not the code part of selecting a random cell.
Thanks!
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"showRecipeDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
RecipeDetailViewController *destViewController = segue.destinationViewController;
destViewController.recipe = [recipes objectAtIndex:indexPath.row];
// Hide bottom tab bar in the detail view
// destViewController.hidesBottomBarWhenPushed = YES;
}
Hello as you have mentioned in your comment that you have only one section with 36 cells. find the rendom row using arc4random.
Then fetch your indexPath from section and row.
then call didSelectRowAtIndexPath
Whole code of your button click method will look like....
int section = 0;
int row = arc4random() %35;
NSIndexPath * path = [NSIndexPath indexPathForRow:row inSection:section];
if ([self.yourTableView.delegate respondsToSelector:#selector(tableView:willSelectRowAtIndexPath:)]) {
[self.yourTableView.delegate tableView:self.yourTableView willSelectRowAtIndexPath:path];
}
[self.yourTableView selectRowAtIndexPath:path animated:YES scrollPosition: UITableViewScrollPositionNone];
if ([self.yourTableView.delegate respondsToSelector:#selector(tableView:didSelectRowAtIndexPath:)]) {
[self.yourTableView.delegate tableView:self.yourTableView didSelectRowAtIndexPath:path];
}
Then implement didSelectRowAtIndexPath
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.tag = indexPath.row;
[self performSegueWithIdentifier:#"showRecipeDetail" sender:btn];
}
Make sure that you have written following code in viewDidLoad
self.yourTableView.delegate = self;
self.yourTableView.dataSource = self;
And change prepareForSegue method as below
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"showRecipeDetail"]) {
RecipeDetailViewController *destViewController = [segue destinationViewController];
NSInteger tagIndex = [(UIButton *)sender tag];
destViewController.recipe = [recipes objectAtIndex:tagIndex];
}
}
This will surely help you.....

NSTableView hit tab to jump from row to row while editing

I've got an NSTableView. While editing, if I hit tab it automatically jumps me to the next column. This is fantastic, but when I'm editing the field in the last column and I hit tab, I'd like focus to jump to the first column of the NEXT row.
Any suggestions?
Thanks to Michael for the starting code, it was very close to what ended up working! Here is the final code that I used, hope it will be helpful to someone else:
- (void) textDidEndEditing: (NSNotification *) notification {
NSInteger editedColumn = [self editedColumn];
NSInteger editedRow = [self editedRow];
NSInteger lastColumn = [[self tableColumns] count] - 1;
NSDictionary *userInfo = [notification userInfo];
int textMovement = [[userInfo valueForKey:#"NSTextMovement"] intValue];
[super textDidEndEditing: notification];
if ( (editedColumn == lastColumn)
&& (textMovement == NSTabTextMovement)
&& editedRow < ([self numberOfRows] - 1)
)
{
// the tab key was hit while in the last column,
// so go to the left most cell in the next row
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:(editedRow+1)] byExtendingSelection:NO];
[self editColumn: 0 row: (editedRow + 1) withEvent: nil select: YES];
}
}
You can do this without subclassing by implementing control:textView:doCommandBySelector:
-(BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
if(commandSelector == #selector(insertTab:) ) {
// Do your thing
return YES;
} else {
return NO;
}
}
(The NSTableViewDelegate implements the NSControlTextEditingDelegate protocol, which is where this method is defined)
This method responds to the actual keypress, so you're not constrained to the textDidEndEditing method, which only works for text cells.
Subclass UITableView and add code to catch the textDidEndEditing call.
You can then decide what to do based on something like this:
- (void) textDidEndEditing: (NSNotification *) notification
{
NSDictionary *userInfo = [notification userInfo];
int textMovement = [[userInfo valueForKey:#"NSTextMovement"] intValue];
if ([self selectedColumn] == ([[self tableColumns] count] - 1))
(textMovement == NSTabTextMovement)
{
// the tab key was hit while in the last column,
// so go to the left most cell in the next row
[yourTableView editColumn: 0 row: ([self selectedRow] + 1) withEvent: nil select: YES];
}
[super textDidEndEditing: notification];
[[self window] makeFirstResponder:self];
} // textDidEndEditing
This code isn't tested... no warranties... etc. And you might need to move that [super textDidEndEditing:] call for the tab-in-the-right-most-cell case. But hopefully this will help you to the finish line. Let me know!

Respond to mouse events in text field in view-based table view

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)
}
}
}
}

Open an NSWindow by clicking NSRect in cocoa

In my program I am able to determine whether a mouseclick was made within a certain NSRect. How can I open a new NSWindow by clicking this NSRect?
If you want to display an existing window (which you created with Interface Builder) you just call makeKeyAndOrderFront on your window object.
If you want to create a new window programmatically you find an answer here.
To handle events, you'd implement the relevant methods of NSResponder in your NSView or NSViewController subclass. For instance, you could implement mouseDown: and -mouseUp: to handle mouse clicks (in a fairly simplistic manner), like so:
- (void) mouseDown: (NSEvent *) event
{
if ( [event type] != NSLeftMouseDown )
{
// not the left button, let other things handle it
[super mouseDown: event];
return;
}
NSPoint location = [self convertPoint: [event locationInWindow] fromView: nil];
if ( !NSPointInRect(location, self.theRect) )
{
[super mouseDown: event];
return;
}
self.hasMouseDown = YES;
}
- (void) mouseUp: (NSEvent *) event
{
if ( (!self.hasMouseDown) || ([event type] != NSLeftMouseUp) )
{
[super mouseUp: event];
return;
}
NSPoint location = [self convertPoint: [event locationInWindow] fromView: nil];
if ( !NSPointInRect(location, self.theRect) )
{
[super mouseDown: event];
return;
}
self.hasMouseDown = NO;
// mouse went down and up within the target rect, so you can do stuff now
}

Resources