UPDATE:Solved issue, see below!
The situation: I have several dynamically loaded UIViews on a UIScrollView in a nib.
Expected behavior: I want to single TAP any one of the UIViews and it will change background color to indicate it was tapped. If it was already tapped it should then change back to its initial look.
I have set up a UITapGesture recognizer on each of the UIViews and here is the selector method where I am doing the behavior. I have confused myself. I apologize for the sketchy logic here (it is a ruff draft). I have set up a isTapped BOOL set to "NO" initially in the init in the file.
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
isTapped = !isTapped;
UIView *v = gestureRecognizer.view;
NSInteger currentIndex = [studentCellArray indexOfObjectIdenticalTo:v];
if (oldIndex != currentIndex) {
isTapped = YES;
}
//check to see if obj in array then switch on/off
if ([tappedViewArray indexOfObjectIdenticalTo:v] != NSNotFound) {
oldIndex = currentIndex;
}
if (currentIndex == v.tag) {
isTapped = !isTapped;
}
if (isTapped) {
[tappedViewArray addObject:v];
[super formatViewTouchedNiceGrey:v];
}else{
[tappedViewArray removeObject:v];
[super formatViewBorder:v];
}
if (currentIndex == oldIndex) {
isTapped = !isTapped;
}
}
Actual Behavior: After Tapping the First UIView it selects fine and changes, a second tap will change it back, however after successive taps it stays selected. Also, if you select a UIView and go to another view - you have to double tap the successive views.
I would like to just tap once to turn off or on any of the UIViews in the scrollview.
UPDATE: Well, after some Hand writing and other vain attempts at trying to focus on this issue ---- I have solved it this way and it BEHAVES properly!
here is my solution:
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
isTapped = !isTapped;
UIView *v = gestureRecognizer.view;
NSInteger currentIndex = [studentCellArray indexOfObjectIdenticalTo:v];
if (((isTapped && currentIndex != oldIndex) || (!isTapped && currentIndex != oldIndex)) && [tappedViewArray indexOfObject:v] == NSNotFound) {
oldIndex = currentIndex;
[tappedViewArray addObject:v];
[super formatCornerRadiusWithGreyBackgrnd:v];
} else {
[super formatViewBorder:v];
[tappedViewArray removeObject:v];
}
}
So I hope this helps someone with this issue.
The key was to check for the isTapped and indexes being not equal AND the view object NOT being in the array I was assembling to indicate items touched/Tapped....
Related
Note: I have searched Stack Overflow for similar problems, and none of the questions I found seem to address this particular problem.
I've written a small sample app (complete Xcode project with source code available here: http://jollyroger.kicks-ass.org/stackoverflow/FlickeringTableView.zip) that plays all of the sounds in /System/Library/Sounds/ sequentially and displays the sounds in a window as they are played to show the issue I am seeing. The window in MainMenu.xib has a single-column NSTableView with one row defined as a cell template with three items in it:
an NSTextField to hold the sound name
another NSTextField to hold the sound details
a NSProgressIndicator to show play progress while the sound is playing
I have subclassed NSTableCellView (SoundsTableCellView.h) to define each of the items in the cell view so that I can access and set them when the time arises.
I have defined a MySound class that encapsulates properties and methods needed to handle the playing of sound files via AVAudioPlayer APIs. This class defines a MySoundDelegate protocol to allow the app delegate to receive events whenever sounds start or finish playing.
The application delegate adheres to the NSTableViewDelegate and NSTableViewDataSource protocols to allow it to store the table data as an array of MySound objects and update the table with relevant information when needed. It also adheres to the MySoundDelegate protocol to receive events when sounds start or finish playing. The delegate also has an NSTimer task that periodically calls a refreshWindow method to update the progress indicator for the currently playing sound.
The app delegate's refreshWindow method displays and resizes the window if needed based on the number of sounds in the list, and updates the stored reference to the associated NSProgressIndicator for the sound that is playing.
The app delegate's tableView: viewForTableColumn (NSTableViewDelegate protocol) method gets called to populate the table cells. In it, I use Apple's standard "Populating a Table View Programmatically" advice to:
check the table column identifier to ensure it matches the
identifier (sound column) I set in Interface Builder (Xcode) for the table column,
get the corresponding table cell with identifier (sound cell) by calling thisTableView makeViewWithIdentifier,
use the incoming row parameter to locate the matching array element
of the data source (app delegate sounds array), then
set the string values of NSTextFields and set the maxValue and doubleValue of the NSProgressIndicator in the cell to corresponding details of the associated sound object,
store a reference to the associated NSProgressIndicator control in the associated sound object for later updating
Here's the viewForTableColumn method:
- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow
{
SoundsTableCellView *cellView = nil;
// get the table column identifier
NSString *columnID = [thisTableColumn identifier];
if ([columnID isEqualToString:#"sound column"])
{
// get the sound corresponding to the specified row (sounds array index)
MySound *sound = [sounds objectAtIndex:thisRow];
// get an existing cell from IB with our hard-coded identifier
cellView = [thisTableView makeViewWithIdentifier:#"sound cell" owner:self];
// display sound name
[cellView.soundName setStringValue:[sound name]];
[cellView.soundName setLineBreakMode:NSLineBreakByTruncatingMiddle];
// display sound details (source URL)
NSString *details = [NSString stringWithFormat:#"%#", [sound sourceURL]];
[cellView.soundDetails setStringValue:details];
[cellView.soundDetails setLineBreakMode:NSLineBreakByTruncatingMiddle];
// update progress indicators
switch ([sound state])
{
case kMySoundStateQueued:
break;
case kMySoundStateReadyToPlay:
break;
case kMySoundStatePlaying:
if (sound.playProgress == nil)
{
sound.playProgress = cellView.playProgress;
}
NSTimeInterval duration = [sound duration];
NSTimeInterval position = [sound position];
NSLog(#"row %ld: %# (%f / %f)", (long)thisRow, [sound name], position, duration);
NSLog(#" %#: %#", [sound name], sound.playProgress);
[cellView.playProgress setMaxValue:duration];
[cellView.playProgress setDoubleValue:position];
break;
case kMySoundStatePaused:
break;
case kMySoundStateFinishedPlaying:
break;
default:
break;
}
}
return cellView;
}
And here's the refreshWindow method:
- (void) refreshWindow
{
if ([sounds count] > 0)
{
// show window if needed
if ([window isVisible] == false)
{
[window makeKeyAndOrderFront:self];
}
// resize window to fit all sounds in the list if needed
NSRect frame = [self.window frame];
int screenHeight = self.window.screen.frame.size.height;
long maxRows = ((screenHeight - 22) / 82) - 1;
long displayedRows = ([sounds count] > maxRows ? maxRows : [sounds count]);
long actualHeight = frame.size.height;
long desiredHeight = 22 + (82 * displayedRows);
long delta = desiredHeight - actualHeight;
if (delta != 0)
{
frame.size.height += delta;
frame.origin.y -= delta;
[self.window setFrame:frame display:YES];
}
// update play position of progress indicator for all sounds in the list
for (MySound *nextSound in sounds)
{
switch ([nextSound state])
{
case kMySoundStatePlaying:
if (nextSound.playProgress != nil)
{
[nextSound.playProgress setDoubleValue:[nextSound position]];
NSLog(#" %#: %# position: %f", [nextSound name], nextSound.playProgress, [nextSound position]);
}
break;
case kMySoundStateQueued:
case kMySoundStateReadyToPlay:
case kMySoundStatePaused:
case kMySoundStateFinishedPlaying:
default:
break;
}
}
}
else
{
// hide window
if ([window isVisible])
{
[window orderOut:self];
}
}
// reload window table view
[tableView reloadData];
}
During init, the application delegate scans the /System/Library/Sounds/ folder to get a list of AIFF sound files in that folder, and creates a sounds array holding sound objects for each of the sounds in that folder. The applicationDidFinishLaunching method then starts playing the first sound in the list sequentially.
The problem (which you can see by running the sample project) is that rather than only updating the top table row for the sound that is currently playing, the progress indicators in all of the following rows seem to update and flicker as well. The way it displays is somewhat inconsistent (sometimes they all flicker, and sometimes they are all blank as expected); but when they do update and flicker the progress indicators do seem to roughly correspond to the sound that is currently playing. So I am pretty sure the issue must be somehow related to the way I am updating the table; I'm just not sure where the problem is or how to solve it.
Here's a screen shot of what the window looks like to give you an idea:
Table View Screen Shot
Any ideas or guidance would be greatly appreciated!
Here's what I changed.
tableView:viewForTableColumn:row: returns "a view to display the specified row and column". The value of the progress bar is always set.
- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow
{
SoundsTableCellView *cellView = nil;
// get the table column identifier
NSString *columnID = [thisTableColumn identifier];
if ([columnID isEqualToString:#"sound column"])
{
// get the sound corresponding to the specified row (sounds array index)
MySound *sound = [sounds objectAtIndex:thisRow];
// get an existing cell from IB with our hard-coded identifier
cellView = [thisTableView makeViewWithIdentifier:#"sound cell" owner:self];
// display sound name
[cellView.soundName setStringValue:[sound name]];
// display sound details (source URL)
NSString *details = [NSString stringWithFormat:#"%#", [sound sourceURL]];
[cellView.soundDetails setStringValue:details];
// update progress indicators
// [cellView.playProgress setUsesThreadedAnimation:NO];
NSTimeInterval duration = [sound duration];
NSTimeInterval position = [sound position];
[cellView.playProgress setMaxValue:duration];
[cellView.playProgress setDoubleValue:position];
}
// end updates
// [thisTableView endUpdates];
return cellView;
}
refreshWindow is split into refreshProgress and refreshWindow. refreshProgress refreshes the row of the playing sound and is called on a timer.
- (void)refreshProgress
{
if ([sounds count] > 0)
{
[sounds enumerateObjectsUsingBlock:^(MySound *nextSound, NSUInteger rowNr, BOOL *stop)
{
switch ([nextSound state])
{
case kMySoundStatePlaying:
// refresh row
[tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:rowNr]
columnIndexes:[NSIndexSet indexSetWithIndex:0]];
break;
case kMySoundStateQueued:
case kMySoundStateReadyToPlay:
case kMySoundStatePaused:
case kMySoundStateFinishedPlaying:
default:
break;
}
}];
}
}
refreshWindow refreshes the size and visibility of the window and is called when the number of sounds changes.
- (void) refreshWindow
{
if ([sounds count] > 0)
{
// show window if needed
if ([window isVisible] == false)
{
[window makeKeyAndOrderFront:self];
}
// resize window to fit all sounds in the list if needed
... calculate new window frame
}
else
{
// hide window
if ([window isVisible])
{
[window orderOut:self];
}
}
}
When a sound is removed, the row is also removed so the other rows still display the same sound and don't need an update.
- (void) soundFinishedPlaying:(MySound *)sound encounteredError:(NSError *)error
{
if (error != NULL)
{
// display an error dialog box to the user
[NSApp presentError:error];
}
else
{
// remove sound from array
NSLog(#"deleting: [%#|%#]", [sound truncatedID], [sound name]);
NSUInteger index = [sounds indexOfObject:sound];
[sounds removeObject:sound];
[tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withAnimation:NSTableViewAnimationEffectNone];
}
// refresh window
[self refreshWindow];
// play the next sound in the queue
[self play];
}
[tableView reloadData] isn't called. sound.playProgress isn't used.
When touches on the keyboard area, the root view's method be triggered:
(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
I am very confused,anyone can help me?
In your applicationWillEnterForeground in AppDelegate, put this code.
It works for me, specially with KLCPopup
- (void)applicationWillEnterForeground:(UIApplication *)application{
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
if (!IS_OS_8_OR_LATER) return;
[UIApplication.sharedApplication.windows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIWindow *w, NSUInteger idx, BOOL *stop) {
if (!w.opaque && [NSStringFromClass(w.class) hasPrefix:#"UIText"]) {
// The keyboard sometimes disables interaction. This brings it back to normal.
BOOL wasHidden = w.hidden;
w.hidden = YES;
w.hidden = wasHidden;
*stop = YES;
}
}];}
This problem will appear in:
1、you changed keywindow‘s rootViewController;
2、enter background and return to the foreground;
So,restore UITextEffectsWindow can fixed it after your change everytime.
void TSRestoreKeyboardWindow(void)
{
if (!TSSystemVersionGreaterThanIOS8()) return;
[UIApplication.sharedApplication.windows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIWindow *w, NSUInteger idx, BOOL *stop) {
if (!w.opaque && [NSStringFromClass(w.class) hasPrefix:#"UIText"]) {
// The keyboard sometimes disables interaction. This brings it back to normal.
BOOL wasHidden = w.hidden;
w.hidden = YES;
w.hidden = wasHidden;
*stop = YES;
}
}];
}
In my OSX app I have a collection view which is a subclass of NSCollectionView.
I'm all satisfied with how the things are except the contextual menu, which I can't figure out yet.
So what I want is:
right-click on a collection view item brings up the contextual menu
the options picked from the menu (delete, edit, etc) are applied to the item that the click was performed on.
I know how to do it for NSOutlineView or NSTableView, but not for collection view.
I can't figure out how to get the index of the item clicked.
Does anyone have any ideas on how I can implement this?
Any kind of help is highly appreciated!
Basically, all of our solutions are capable of addressing requirements, but I would like to make a supplement to swift3+, which I think is a complete solution.
/// 扩展NSCollectionView功能,增加常用委托
class ANCollectionView: NSCollectionView {
// 扩展委托方式
weak open var ANDelegate: ANCollectionViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
var menu = super.menu(for: event);
let point = self.convert(event.locationInWindow, from: nil)
let indexPath = self.indexPathForItem(at: point);
if ANDelegate != nil{
menu = ANDelegate?.collectionView(self, menu: menu, at: indexPath);
}
return menu;
}
}
/// 扩展NSCollectionView的委托
protocol ANCollectionViewDelegate : NSObjectProtocol {
func collectionView(_ collectionView:NSCollectionView, menu:NSMenu?, at indexPath: IndexPath?) -> NSMenu?
}
This is what I wrote an extension, and I hope to help everyone.
One approach I've used is to not try to apply the contextual menu actions to the one specific item that was clicked on but to the selected items. And I make the clicked-on item add itself to the selection.
I used a custom view for the collection item view. The custom view class has an outlet, item, to its owning collection view item, which I connect in the NIB. It also overrides -rightMouseDown: to have the item add itself to the selection:
- (void) rightMouseDown:(NSEvent*)event
{
NSCollectionView* parent = self.item.collectionView;
NSUInteger index = NSNotFound;
NSUInteger count = parent.content.count;
for (NSUInteger i = 0; i < count; i++)
{
if ([parent itemAtIndex:i] == self.item)
{
index = i;
break;
}
}
NSMutableIndexSet* selectionIndexes = [[parent.selectionIndexes mutableCopy] autorelease];
if (index != NSNotFound && ![selectionIndexes containsIndex:index])
{
[selectionIndexes addIndex:index];
parent.selectionIndexes = selectionIndexes;
}
return [super rightMouseDown:event];
}
If you prefer, rather than adding the item to the selection, you can check if it's already in the selection. If it is, don't modify the selection. If it's not, replace the selection with just the item (making it the only selected item).
Alternatively, you could set a contextual menu on the item views rather than on the collection view. Then, the menu items could target either the item view or the collection view item.
Lastly, you could subclass NSCollectionView and override -menuForEvent:. You would still call through to super and return the menu it returns, but you could take the opportunity to record the event and/or the item at its location. To determine that, you'd do something like:
- (NSMenu*) menuForEvent:(NSEvent*)event
{
_clickedItemIndex = NSNotFound;
NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
NSUInteger count = self.content.count;
for (NSUInteger i = 0; i < count; i++)
{
NSRect itemFrame = [self frameForItemAtIndex:i];
if (NSMouseInRect(point, itemFrame, self.isFlipped))
{
_clickedItemIndex = i;
break;
}
}
return [super menuForEvent:event];
}
Here's Ken's idea to override menuForEvent: in an NSCollectionView subclass implemented in Swift:
// MARK: - Properties
/**
The index of the item the user clicked.
*/
var clickedItemIndex: Int = NSNotFound
// MARK: - Menu override methods
override func menuForEvent(event: NSEvent) -> NSMenu?
{
self.clickedItemIndex = NSNotFound
let point = self.convertPoint(event.locationInWindow, fromView:nil)
let count = self.content.count
for index in 0 ..< count
{
let itemFrame = self.frameForItemAtIndex(index)
if NSMouseInRect(point, itemFrame, self.flipped)
{
self.clickedItemIndex = index
break
}
}
return super.menuForEvent(event)
}
In Swift 5, you can use
class ClickedCollectionView: NSCollectionView {
var clickedIndex: Int?
override func menu(for event: NSEvent) -> NSMenu? {
clickedIndex = nil
let point = convert(event.locationInWindow, from: nil)
for index in 0..<numberOfItems(inSection: 0) {
let frame = frameForItem(at: index)
if NSMouseInRect(point, frame, isFlipped) {
clickedIndex = index
break
}
}
return super.menu(for: event)
}
}
Thanks for this solution. I wrapped it into a NSCollectionView subclass:
#import <Cocoa/Cocoa.h>
#interface TAClickableCollectionViewItem : NSCollectionViewItem
#property (nonatomic, assign) BOOL isClicked;
#end
#interface TAClickableCollectionView : NSCollectionView <NSMenuDelegate>
#property (nonatomic, readonly) id clickedObject;
#property (nonatomic, readonly) TAClickableCollectionViewItem *clickedItem;
#end
So you can use bindings in Interface Builder to highlight clicked items as well.
#import "TAClickableCollectionView.h"
#implementation TAClickableCollectionViewItem
#end
#implementation TAClickableCollectionView
- (NSMenu*) menuForEvent:(NSEvent*)event
{
NSInteger _clickedItemIndex = NSNotFound;
NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
NSUInteger count = self.content.count;
for (NSUInteger i = 0; i < count; i++)
{
NSRect itemFrame = [self frameForItemAtIndex:i];
if (NSMouseInRect(point, itemFrame, self.isFlipped))
{
_clickedItemIndex = i;
break;
}
}
if(_clickedItemIndex < self.content.count) {
id obj = [self.content objectAtIndex:_clickedItemIndex];
TAClickableCollectionViewItem *item = (TAClickableCollectionViewItem *)[self itemAtIndex:_clickedItemIndex];
if(item != _clickedItem) {
[self willChangeValueForKey:#"clickedObject"];
_clickedItem.isClicked = NO;
_clickedItem = item;
[self didChangeValueForKey:#"clickedObject"];
}
item.isClicked = YES;
if(obj != _clickedObject) {
[self willChangeValueForKey:#"clickedObject"];
_clickedObject = obj;
[self didChangeValueForKey:#"clickedObject"];
}
}
return [super menuForEvent:event];
}
- (void)menuDidClose:(NSMenu *)menu {
_clickedItem.isClicked = NO;
}
#end
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)
}
}
}
}
This is a really strange problem I'm seeing in my app. I have an NSTextField bound to an attribute of an NSManagedObject, but whenever the object is saved the textfield loses focus. I'm continuously updating the value of the binding, so this is far from ideal.
Has anyone seen anything like this before, and (hopefully) found a solution?
I encountered the issue recently and fixed it by changing the way the NSTextField was bound to the NSManagedObject attribute. Instead of binding the value of the text field to the selection.[attribute] key path of the NSArrayController, I bound the arrayController.selection.[attribute] keyPath of the view controller that had a proper outlet pointing to the controller.
For some reason, the NSTextField doesn't loose focus when the NSManagedObjectContext is saved if bound this way.
I want to share my solution. It will work for all fields without modification.
I have optimized it for this posting and removed some error checking, logging and thread safety.
- (BOOL)saveChanges:(NSError **)outError {
BOOL result = YES;
#try {
NSError *error = nil;
if ([self hasChanges]) {
// Get field editor
NSResponder *responder = [[NSApp keyWindow] firstResponder];
NSText *editor = [[NSApp keyWindow] fieldEditor: NO forObject: nil];
id editingObject = [editor delegate];
BOOL isEditing = (responder == editor);
NSRange range;
NSInteger editedRow, editedColumn;
// End editing to commit the last changes
if (isEditing) {
// Special case for tables
if ([editingObject isKindOfClass: [NSTableView class]]) {
editedRow = [editingObject editedRow];
editedColumn = [editingObject editedColumn];
}
range = [editor selectedRange];
[[NSApp keyWindow] endEditingFor: nil];
}
// The actual save operation
if (![self save: &error]) {
if (outError != nil)
*outError = error;
result = NO;
} else {
result = YES;
}
// Now restore the field editor, if any.
if (isEditing) {
[[NSApp keyWindow] makeFirstResponder: editingObject];
if ([editingObject isKindOfClass: [NSTableView class]])
[editingObject editColumn: editedColumn row: editedRow withEvent: nil select: NO];
[editor setSelectedRange: range];
}
}
} #catch (id exception) {
result = NO;
}
return result;
}
OK, so thanks to Martin for pointing out that I should read the docs a little more closely. This is expected behaviour, and here's what I did to get around it (use your judgement as to whether this is appropriate for you):
I save my context once every 3 seconds, checking at the start if the context has any changes before I bother executing the actual save: method on my NSManagedObjectContext. I added a simple incrementing/decrementing NSUInteger (_saveDisabler) to my Core Data controller class that is modified via the following methods:
- (void)enableSaves {
if (_saveDisabler > 0) {
_saveDisabler -= 1;
}
}
- (void)disableSaves {
_saveDisabler += 1;
}
Then all I do in my custom saveContext method is do a simple check at the top:
if (([moc hasChanges] == NO) || (_saveDisabler > 0)) {
return YES;
}
This prevents the save from occurring, and means that the focus is not stolen from any of my custom textfield subclasses. For completeness, I also subclassed NSTextField and enable/disable saves in my Core Data controller from the following methods:
- (void)textDidBeginEditing:(NSNotification *)notification;
- (void)textDidEndEditing:(NSNotification *)notification;
It might be a little messy, but it works for me. I'm keen to hear of cleaner/less convoluted methods if anyone has done this successfully in another way.