I'm trying to get the NSPoint cooridnates of an NSTextFieldCell, but NSTextFieldCell doesn't inherit from NSView, and therefore doesn't have the frame method. Any ideas on how to get around this?
I'm trying to use Matt Gemmel's MAAttachedWindow to attach little helper popups to certain elements. I want to attach it to an area on the window, and other than hacking it together by manually messing with the x and y coordinates, I'm not sure how to get the coordinates.
You can call [theCell controlView] to get a reference to the control that owns a particular cell. If the NSTextFieldCell object is part of a simple control, such as an NSTextField, the following should be sufficient:
NSRect cellFrame = [[theCell controlView] frame];
NSPoint origin = cellFrame.origin;
//..
If, however, the NSTextFieldCell is part of a more complex control, such as an NSTableView, where a single cell is used in multiple places, you will need more information in order to determine the proper rectangle. NSCell offers the method representedObject, which can help you to determine which object in the NSTableView is represented by the cell at that particular moment. Without knowing more about your specific case, I don't know how much more detail to provide in that regard.
Here is one possible solution, assuming you are able to discern the row and column information from the object stored in representedObject:
NSTableView * tableView = [theCell controlView];
id cellObject = [theCell representedObject];
NSInteger row = //... determine from representedObject
NSInteger col = //... determine from representedObject
NSRect cellFrame = [tableView frameOfCellAtColumn:col row:row];
A cell is a reusable object that's owned by an NSControl (an NSView subclass). It doesn't have an origin point because it doesn't actually represent a place on the screen, it's up to the control (which has a specific frame rectangle) to draw it where and when it's needed. Think about a table view for example, it might use a single cell that's re-drawn in several places for each row.
The cell is passed a frame rectangle by the control when it's drawn onto the screen, in most cases that should be all you need. You can also take advantage of NSCell's sizing methods, which can tell you how much room it needs to display its content.
For using MAAttachedWindow, could you instead use the control's frame (if it's a single-cell control), or in the case of a table view with a specific row one of the NSTableView layout methods? rectOfRow: or rectOfColumn:, for example. You could override NSTextFieldCell and position the window in one of the drawing methods, but I'd save that for my absolute last choice if possible.
It doesn't have co-ordinates. There are none to get. The way cells work is that the view tells the cell “draw here”.
So, if you're not in one of the cell's drawing methods and you really, really need co-ordinates, you need to ask the view that owns the cell. Usually, this is [cell controlView].
(Perhaps you're thinking of UIKit? UITableViewCells are very different from NSCells.)
Related
I have a custom UITableViewCell to implement swiping horizontally with UIAttachmentBehavior and a UIPanGestureRecognizer. Here's the relevant portion, in the UITableViewCell subclass:
- (void)panDidChange:(UIPanGestureRecognizer *)gesture {
CGPoint location = [gesture locationInView:[self tableView]];
location.y = CGRectGetMidY(self.frame);
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
self.panAttachment = [[UIAttachmentBehavior alloc] initWithItem:self attachedToAnchor:location];
[self.animator addBehavior:self.panAttachment];
break;
}
Then, when UIGestureRecognizerStateChanged, I just set the self.panAttachment.anchorPoint as the location.
Now, this works fine from startup, but as soon as I add a cell to the tableView or delete a cell from the tableView and try to swipe, the cell moves to it's previous position before the tableView change (one cell down if a cell was deleted, one cell up if a cell was added). As far, as I can tell, this is because the cell's frame isn't being updated when it's position in the tableView changes, so location's y coordinate is out of sync. I've tried [self setNeedsDisplay] and all other "Update view" methods I could find, to no avail.
I am creating the animator referenced above like so:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:[self tableView]];
It looks like the issue is in a couple different places:
Your animator is being initialized with your entire table view as the reference view.
You are then creating the attachment behavior with an anchor of midY in the tableView's coordinate system, meaning your attachment behavior is now anchored to a point in the tableView's reference coordinate system that is wherever the center point of the cell is located. I bet if you tried to scroll your tableView, weird things would also happen.
Depending on what your goal is for your dynamics behavior, you need to modify both your animator's reference frame and attachment's anchor point in order to fully define what physics simulation you are trying to achieve. Based on the above, it looks like you are trying to simply move the entire cell on the pan. I would recommend that you try moving only a specific container view inside your view hierarchy instead since the table view is going to be placing your cells directly using frames.
If you are looking to provide a pan with a snap-back effect on cancel, then this effect might be better implemented using a couple of UIAttachmentBehavior instances. One behavior instance to anchor the view you are wanting to swipe to it's resting position, and one to actually perform the panning and move the view like you are doing above during the pan. You can tweak the interaction between the two interactions by changing the damping and frequency. This question is a good reference for dragging in general using dynamic items.
In summary to fix your current issue:
Define your animator's reference bounds to the cell's coordinate system, preferably using cell.contentView
Have your cell manage the animator directly since the animator should only be concerned with a view in it's coordinate system. Your animator should have no knowledge of the table view's view hierarchy. Add/remove UIDynamicBehavior items from your cell's animator.
I have embedded UIButtons in my TableViewCells. In order to track which cell the button belongs to, I would like to add an NSIndexPath property to UIButton. I do not want to subclass a UIButton. Is there a way I can do this with categories?
EDIT: I believe the idea of setting tags will not work if I have multiple sections in the table view. Another approach of accessing the button's superview's superview to determine the cell seems more like a hack. Looking for a cleaner way of doing this.
There are several approaches. If you just really need a single number rather than a full index path, you can use setTag:. It's inflexible, but it's readily available.
The next best solution is associative references. These can hang data onto any object, with proper memory management. I usually create a category that adds a property implemented using objc_setAssociatedObject and objc_getAssociatedObject.
You can find out which cell a clicked button was in without having to associate tags or index paths with the buttons, which can get messy when re-using cells. Tags do work with sectioned tables but you have to mess about with numbers, e.g. 10001 is section 1, row 1, so you've got to convert at one end and convert back at the other, too fragile for my liking.
A nicer method is to use UITableView's indexPathForRowAtPoint: method. When your button's action is activated (assuming the target is in your table view controller:
CGPoint point = [sender convertPoint:sender.center toView:self.tableView];
NSIndexPath *path = [self.tableView indexPathForRowAtPoint:point];
sender here is cast to UIButton.
In order to track which cell the button belongs to
You already know which cell the button belongs to: it's the cell that the button is a subview of.
Start with a reference to the button. (If you are the button, this is self. If you're the button's target for its action, then when the button is tapped and emits an action message, it passes itself along as parameter to the action message, usually called sender.)
Look up the view hierarchy (superview) to find the UIViewTableCell containing the button.
UIView* v = theButton;
while (![v isKindOfClass: [UITableViewCell class]]) v = v.superview;
Now you can do whatever you like with that info. For example, you seem to want an NSIndexPath; then call -[UITableView indexPathForCell:].
I am trying to setup a NSTableView with a custom cell using an ArrayController and Bindings. To accomplish this I added a subview to the custom cell. The data connection seems to work somewhat. Though, there seems to be a redraw problem which I cannot fix. When I load the application only some of the cells are rendered. When I scroll through the rows or select one the rendering changes.
I created an example project on github to illustrate what the problem is.
The actual source code for the cell rendering can be found here:
// CustomCell.m
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
if (![m_view superview]) {
[controlView addSubview:m_view];
}
// The array controller only gets wrapped data items pack by the NSObjectTransformer.
// Therefore, objectValue returns a NSObjectWrapper.
// Unpack the wrapper to retreive the data item.
DataItem* dataItem = [(NSObjectWrapper*)[self objectValue] original];
[[m_view name] setStringValue:dataItem.name];
[[m_view occupation] setStringValue:dataItem.occupation];
[m_view setFrame:cellFrame];
}
It seems as if the parent controlView does not redraw. Can I force it somehow?
This is almost certainly not a best practice way of doing this, and I'll explain why afterwards: however, it does seem to work. Replace your cell class's drawInteriorWithFrame:inView: method with the following:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
DataItem* dataItem = [(NSObjectWrapper*)[self objectValue] original];
[[m_view name] setStringValue:dataItem.name];
[[m_view occupation] setStringValue:dataItem.occupation];
[m_view setFrame:cellFrame];
NSData *d = [m_view dataWithPDFInsideRect:[m_view bounds]];
NSImage *i = [[NSImage alloc] initWithData:d];
[i setFlipped:YES];
[i drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
The problem is that only one NSCell is created for the entire table. That's how cells are meant to work: the table view creates a cell, and calls setObject… followed by drawInterior… over and over again to get the cell to draw the whole table. That's great from an efficiency perspective (the NSCell class was designed back when 25mhz was a fast computer, so it aimed to minimise the number of object allocations), but causes problems here.
In your code, you populate a view with values, and set its frame, adding it as a subview of the table view if needed. However, since you've only got one instance of NSCell, there can only be one view: you took the single view that you had and merely moved it down the rows of the table.
To do this properly, you'd need some data structure to track all the views you added as subviews of your NSTableView, and when the cell is updating one in the drawInterior… method you'd need to look up which the correct one was and update that. You'd also need to allocate all these views in code (or at least move the view to a separate nib which you could load multiple copies of), because as it is you've only got one in your nib and copying a view is a pain.
The code I wrote is a kludge, since it's really inefficient. What I did was each time the view needs to draw, I drew the view into an off screen image buffer, and then drew the buffer into the correct place in the table view. In doing so, I avoided the problem of only having one view, since the code just takes and draws a new copy of its contents whenever it is needed.
EDIT: See my other answer for explanation
Have you implemented copyWithZone:? You'll need to ensure you either copy or recreate your view in that method, otherwise different cells will end up sharing a view (because NSTableView copies its cells).
I am wanting to change the text background color on a tableview's cell when it is hovered upon, similar to how AddressBook "highlights" the label of a contact's element when you mouseover the label names. However I cannot figure out how to accomplish...
detecting a mouseover on a particular NSCell and...
After detecting the cell his hovered upon, highlighting the text in that cell (not highlighting the entire row as if the user selected that row)
As NSCell is not a subclass of NSView this seems to be a very difficult task.
Any example of this or explanation on how this might be done would be greatly appreciated.
Thanks!
I actually got it working using another method. I got it from the example posted here... http://www.cocoadev.com/index.pl?NSTableViewRollover
https://web.archive.org/web/20111013060111/http://cocoadev.com/index.pl?NSTableViewRollover
Instead of using NSCell's tracking mechanism, I am tracking mouseEntered/mouseExited and mouseMoved within my subclassed NSTableView.
When the tableview awakeFromNib method is called, I create a trackingRect from the visible portion of the tableview
I have a BOOL ivar that is set to YES when the mouse is within the tracking area(mouseEntered) and NO when it is not (mouseExited)
Within the mouseMoved method, I determine the current row the mouse cursor is on and set it to an NSInteger ivar and then call the tableview's setNeedsDisplayInRect: passing the rect of the row that the mouse is on.
I also override resetCursorRects to remove the old tracking rect and add a new one...this method is called when the tableview is scrolled upon so that it's tracking the latest visible rect.
Finally in my tableview's delegate, I determine the selected row (by retrieving the row index from the NSInteger ivar of the table view and change the cell's text color (or anything you want) if the currently drawn cell matches the row the mouse cursor is on. All this is done in the delegate method: tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
I hope this helps others, as this was a bit tricky. It is also probably important to make sure that tableview is the firstResponder when the view loads, just makes things a bit more streamlined and cleaner.
Btw, is there a way to make a specific control in a view always be the firstResponder with nothing else possible as being the firstResponder? Even a method such as the iPhones... viewWillAppear method will help as I could set the first responder each time the view is visible...but i'm not aware of such a method on the Mac.
Overall, it's not a simple task as you noticed.
To track the mouse in an NSCell, subclass NSCell and override
-[NSCell startTrackingAt:inView:]
and
-[NSCell stopTracking:at:inView:mouseIsUp:]
Once you've detected the mouse is tracking inside a cell, you can find out which cell you are in the table with [tableView rowAtPoint:point] and [tableView columnAtPoint:point], and then find your frame with [tableView frameOfCellAtColumn:column row:row]
Then, you can change the way your cell is drawn by changing some property of the cell or changing the way it's drawn directly by overriding drawInteriorWithFrame:inView:.
Here's documentation on subclassing NSCell:
http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ControlCell/Tasks/SubclassingNSCell.html
I achieved something similar by making use of addGlobalMonitorForEventsMatchingMask: handler: of NSEvent within my NSTableView subclass for the NSMouseMovedMask. using this along with columnAtPoint and rowAtPoint of NSTableView I was able to figure out if the position of the mouse was within a given cell.
Using this information I was able to bring up a PopOver when the mouse was over a particular cell.
I am wanting something similar to how iWork has the template selection screen for Pages when you can select different templates, and each view contains different info has difference sizes etc.
I have tried subclassing NSCollectionView and determining which view to display using the newItemForRepresentedObject method (as opposed to using itemPrototype view Interface Builder), but it for some reason doesn't position the views correctly, and it does not show the correct number of views for the number of items present. Here is my code. I was hoping someone may have a better way to do this, or an example of how this is done.
personView and companyView are properties in the subclassed NSCollectionView, that are IBOutlets to views in IB.
-(NSCollectionViewItem *)newItemForRepresentedObject:(id)object{
NSCollectionViewItem *collectionViewItem = [[NSCollectionViewItem alloc] init];
[collectionViewItem setRepresentedObject:object];
if([[object valueForKey:#"company"] boolValue] == YES){
NSView *view = [companyView retain];
[collectionViewItem setView:companyView];
}else{
[collectionViewItem setView:personalView];
}
return collectionViewItem;
}
(It doesn't even seem possible to make an NSCollectionView with differently-sized item views; each size would need to be a multiple or integer divisor of some "main" size, and you'd need to do massive item-checking and -reordering to be sure it's even possible to render them in a grid. Are you sure you're asking the right question?)
Also, I don't see anything like this in iWork: all the views in its template chooser are the same. (Though their NSImageView subviews are of different sizes.) I'd recommend if at all possible using the same view and changing its subviews appropriately. It's easy to, for example, bind text fields' "hidden" property or change the width of an image view. Can't you make a single view that works for both classes, changing itself appropriately depending on the represented object?