I have a NSTableView with cells that contain NSCheckBox. I'd like to change the behaviour of it so that any row in the table can be clicked and the checkbox in that row toggles on/off accordingly.
I figured it can be done with
func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
}
This method provides the index of the clicked row and I could use this to toggle the checkboxes from there on programmatically. But the problem is that the NSCheckBox intercepts mouse clicks on its row. Is there any way to disable this so that the rows can be clicked fully? Setting the enabled state of the NSCheckBox to false would allow this but it also greys out the checkbox and its title.
EDIT:
To clarify what I need: The (view-based) table cell view contains a checkbox. If a user clicks the checkbox, it toggles but when the row is clicked anywhere where no checkbox is, nothing happens. I want the row to be clicked and the checkbox toggles accordingly. So essentially I want the checkbox to be non-clickable and the table row to notify the checkbox inside it when the row is clicked.
In a cell-based table view, you can implement -selectionShouldChangeInTableView:. I assume this will also work for a view-based table view.
- (BOOL)selectionShouldChangeInTableView:(NSTableView *)tableView
{
NSInteger clickedColumn = tableView.clickedColumn;
NSInteger attributeEnabledColumn = [tableView columnWithIdentifier:#"attributeEnabled"];
if (clickedColumn == attributeEnabledColumn) {
NSInteger clickedRow = tableView.clickedRow;
if (clickedRow >= 0) {
NSArrayController *attributesArrayController = self.attributesArrayController;
NSMutableDictionary *attributeRow = [attributesArrayController.arrangedObjects objectAtIndex:clickedRow];
BOOL attributeEnabled = [attributeRow[kAttributeSelectionAttributeEnabledKey] boolValue];
attributeRow[kAttributeSelectionAttributeEnabledKey] = #(!attributeEnabled);
}
return NO;
}
return YES;
}
Apple provide you with the opportunity to intercept a number of NSEvent types via the following NSEvent class method:
Any time an event whose type property matches the mask you passed in to the first argument of the above method, the block (second argument) executes. This block gives you the opportunity to do a number of things: you can carry out additional processing then let the event carry on as usual, you can modify the event, or you cancel the event altogether. Crucially anything you put in this block happens before any other event processing.
In the snippet below any time a checkbox is clicked, the incoming event is doctored to make the event behave as it the click took place outside of the checkbox, but inside the checkbox's NSTableCellView superview.
func applicationDidFinishLaunching(aNotification: NSNotification) {
NSEvent.addLocalMonitorForEventsMatchingMask(.LeftMouseDownMask,
handler: { (theEvent) -> NSEvent! in
var retval: NSEvent? = theEvent
let winPoint = theEvent.locationInWindow
// Did the mouse-down event take place on a row in the table?
if let row = self.tableView.rowContainingWindowPoint(winPoint) {
// Get the view on which the mouse down event took place
let view = self.tableView.viewAtColumn(0,
row: row,
makeIfNecessary: true) as! NSTableCellView
// In my demo-app the checkbox is the NSTableCellView's last subview
var cb = view.subviews.last! as! NSButton
let cbBoundsInWindowCoords = cb.convertRect(cb.bounds, toView: nil)
// Did the click occur on the checkbox part of the view?
if CGRectContainsPoint(cbBoundsInWindowCoords, theEvent.locationInWindow) {
// Create a modified event, where the <location> property has been
// altered so that it looks like the click took place in the
// NSTableCellView itself.
let newLocation = view.convertPoint(view.bounds.origin, toView: nil)
retval = theEvent.cloneEventButUseAdjustedWindowLocation(newLocation)
}
}
return retval
})
}
func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
if let view = self.tableView.viewAtColumn(0,
row: row,
makeIfNecessary: true) as? NSTableCellView {
var checkbox = view.subviews.last! as! NSButton
checkbox.state = (checkbox.state == NSOnState) ? NSOffState : NSOnState
}
return true
}
////////////////////////////////////////////////////////////////
extension NSTableView {
// Get the row number (if any) that coincides with a specific
// point - where the point is in window coordinates.
func rowContainingWindowPoint(windowPoint: CGPoint) -> Int? {
var rowNum: Int?
var tableRectInWindowCoords = convertRect(bounds, toView: nil)
if CGRectContainsPoint(tableRectInWindowCoords, windowPoint) {
let tabPt = convertPoint(windowPoint, fromView: nil)
let indexOfClickedRow = rowAtPoint(tabPt)
if indexOfClickedRow > -1 && indexOfClickedRow < numberOfRows {
rowNum = indexOfClickedRow
}
}
return rowNum
}
}
extension NSEvent {
// Create an event based on another event. The created event is identical to the
// original except for its <location> property.
func cloneEventButUseAdjustedWindowLocation(windowLocation: CGPoint) -> NSEvent {
return NSEvent.mouseEventWithType(type,
location: windowLocation,
modifierFlags: modifierFlags,
timestamp: timestamp,
windowNumber: windowNumber,
context: context,
eventNumber: eventNumber,
clickCount: clickCount,
pressure: pressure)!
}
}
Your approach is fine so far, you can click on the table row and this toggles the checkbox state.
As you said, the checkbox can be clicked on its own which doesn't trigger the table row selection. You need to subclass the NSTableCellView and assign this subclass to the cell's class property. Within that custom class file you can react on the checkbox toggle and change the underlying datasource of your table.
import Cocoa
class MyTableCellView: NSTableCellView {
#IBOutlet weak var checkbox: NSButton! // the checkbox
#IBAction func checkboxToggle(sender: AnyObject) {
// modify the datasource here
}
}
EDIT:
Here is a code snippet where a checkbox is toggled when the user clicks anywhere on the table cell:
func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
if let cell = tableView.viewAtColumn(0, row: row, makeIfNecessary: false) as? MyTableCellView {
cell.checkbox.state = cell.checkbox.state == NSOnState ? NSOffState : NSOnState
}
return false
}
Note that it still needs a subclass for the table cell where you place an #IBOutlet to your checkbox to make it accessible in code.
Related
I am able to reorder my UICollectionViewCells on iOS 9 by dragging it using a gesture recognizer and simplementing newly iOS 9 support for reordering.
public func beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool // returns NO if reordering was prevented from beginning - otherwise YES
public func updateInteractiveMovementTargetPosition(targetPosition: CGPoint)
public func endInteractiveMovement()
public func cancelInteractiveMovement()
I noticed that when I start dragging a cell, its center changes to be the touch location, I don't like that.
I would like to be able to drag my cell by it's corner if I want.
Do you know how to do this?
Thanks a lot.
(Written in Swift 3.1)
For the targetPosition parameter of the updateInteractiveMovementTargetPosition function, instead of using the gesture recognizer's location directly like this...
var location = recognizer.location(in: collectionView)
collectionView.updateInteractiveMovementTargetPosition(location)
... I created a function that takes the center of the cell to be dragged (The location that the collectionView updateInteractiveMovementTargetPosition would use, and then takes the location of the gesture recognizer's touch in the cell, and subtracts that from the center of the cell.
func offsetOfTouchFrom(recognizer: UIGestureRecognizer, inCell cell: UICollectionViewCell) -> CGPoint {
let locationOfTouchInCell = recognizer.location(in: cell)
let cellCenterX = cell.frame.width / 2
let cellCenterY = cell.frame.height / 2
let cellCenter = CGPoint(x: cellCenterX, y: cellCenterY)
var offSetPoint = CGPoint.zero
offSetPoint.y = cellCenter.y - locationOfTouchInCell.y
offSetPoint.x = cellCenter.x - locationOfTouchInCell.x
return offSetPoint
}
I have a simple var offsetForCollectionViewCellBeingMoved: CGPoint = .zero in my view controller that will store that offset so function above doesn't need to be called every time the gesture recognizer's location changes.
So the target of my gesture recognizer would look like this:
func collectionViewLongPressGestureRecognizerWasTriggered(recognizer: UILongPressGestureRecognizer) {
guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)),
let cell = collectionView.cellForItem(at: indexPath), indexPath.item != 0 else { return }
switch recognizer.state {
case .began:
collectionView.beginInteractiveMovementForItem(at: indexPath)
// This is the class variable I mentioned above
offsetForCollectionViewCellBeingMoved = offsetOfTouchFrom(recognizer: recognizer, inCell: cell)
// This is the vanilla location of the touch that alone would make the cell's center snap to your touch location
var location = recognizer.location(in: collectionView)
/* These two lines add the offset calculated a couple lines up to
the normal location to make it so you can drag from any part of the
cell and have it stay where your finger is. */
location.x += offsetForCollectionViewCellBeingMoved.x
location.y += offsetForCollectionViewCellBeingMoved.y
collectionView.updateInteractiveMovementTargetPosition(location)
case .changed:
var location = recognizer.location(in: collectionView)
location.x += offsetForCollectionViewCellBeingMoved.x
location.y += offsetForCollectionViewCellBeingMoved.y
collectionView.updateInteractiveMovementTargetPosition(location)
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
If your collection view is only scrolling in once direction then the easiest way to achieve this is to simple lock the axis which isnt scrolling to something hardcoded, this means your cell will only move in the axis that you can scroll. Here is the code, see the changed case...
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let selectedIndexPath = self.collectionView
.indexPathForItem(at: gesture
.location(in: self.collectionView)) else { break }
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
var gesturePosition = gesture.location(in: gesture.view!)
gesturePosition.x = (self.collectionView.frame.width / 2) - 20
collectionView.updateInteractiveMovementTargetPosition(gesturePosition)
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
I'm new to tvOS. I have created UICollectionView using a XIB file in Objective-C. How can I focus UICollectionViewCell?
I have a label and an image view in the cell, and I want to focus both the label and the image view.
UICollectionViewCell are not focus appearance by default, but you can achive this by adding one yourself.
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
if (self.focused)
{
// Apply focused appearence,
// e.g scale both of them using transform or apply background color
}
else
{
// Apply normal appearance
}
}
OR
If you just want to focus ImageView like scaling it up when collection view cell get focus you can do like this in awakeFromNib method
self.imageView.adjustsImageWhenAncestorFocused = YES;
self.clipToBounds = NO;
Add this in custom class of collectionview:
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if (self.focused) {
collectionView.image.adjustsImageWhenAncestorFocused = true
} else {
collectionView.image.adjustsImageWhenAncestorFocused = false
}
}
Add below methods to your collectionview cell.It will show title of focussed cell only giving complete look and feel of focus.
override func awakeFromNib() {
super.awakeFromNib()
// These properties are also exposed in Interface Builder.
imageView.adjustsImageWhenAncestorFocused = true
imageView.clipsToBounds = false
title.alpha = 0.0
}
// MARK: UICollectionReusableView
override func prepareForReuse() {
super.prepareForReuse()
// Reset the label's alpha value so it's initially hidden.
title.alpha = 0.0
}
// MARK: UIFocusEnvironment
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
/*
Update the label's alpha value using the `UIFocusAnimationCoordinator`.
This will ensure all animations run alongside each other when the focus
changes.
*/
coordinator.addCoordinatedAnimations({
if self.focused {
self.title.alpha = 1.0
}
else {
self.title.alpha = 0.0
}
}, completion: nil)
}
I'm using a horizontally, paging UICollectionView to display a variable number of collection view cells. The size of each collection view cell needs to be equal to that of the collection view and whenever the size of the collection view changes, the size of the collection view cells need to update accordingly. The latter is causing issues. The size of the collection view cells is not updated when the size of the collection view changes.
Invalidating the layout doesn't seem to do the trick. Subclassing UICollectionViewFlowLayout and overriding shouldInvalidateLayoutForBoundsChange: doesn't work either.
For your information, I'm using an instance of UICollectionViewFlowLayout as the collection view's layout object.
I think solution below is much cleaner. You only need to override one of UICollectionViewLayout's method like:
- (void)invalidateLayoutWithContext:(UICollectionViewFlowLayoutInvalidationContext *)context
{
context.invalidateFlowLayoutAttributes = YES;
context.invalidateFlowLayoutDelegateMetrics = YES;
[super invalidateLayoutWithContext:context];
}
and
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
if(!CGSizeEqualToSize(self.collectionView.bounds.size, newBounds.size))
{
return YES;
}
return NO;
}
as well.
I have similar behavior in my app: UICollectionView with cells that should have the same width as collection view at all time. Just returning true from shouldInvalidateLayoutForBoundsChange: didn't work for me either, but I managed to make it work in this way:
class AdaptableFlowLayout: UICollectionViewFlowLayout {
var previousWidth: CGFloat?
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let newWidth = newBounds.width
let shouldIvalidate = newWidth != self.previousWidth
if shouldIvalidate {
collectionView?.collectionViewLayout.invalidateLayout()
}
self.previousWidth = newWidth
return false
}
}
In documentation it is stated that when shouldInvalidateLayoutForBoundsChange returns true then invalidateLayoutWithContext: will be called. I don't know why invalidateLayout works and invalidateLayoutWithContext: doesn't.
Swift 4 Xcode 9 implementation for height changes:
final class AdaptableHeightFlowLayout: UICollectionViewFlowLayout {
var previousHeight: CGFloat?
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
let newHeight = newBounds.height
let shouldIvalidate = newHeight != self.previousHeight
if shouldIvalidate {
collectionView?.collectionViewLayout.invalidateLayout()
}
self.previousHeight = newHeight
return false
}
}
While studying iOS6 new features I got a question about UICollectionView.
I am currently testing it with Flow layout and the scroll direction set to horizontal, scrolling and paging enabled. I've set its size to exactly the same as my custom's cells, so it can show one at a time, and by scrollig it sideways, the user would see the other existing cells.
It works perfectly.
Now I want to add and UIPageControl to the collection view I made, so it can show up which cell is visible and how many cells are there.
Building up the page control was rather simple, frame and numberOfPages defined.
The problem I am having, as the question titles marks, is how to get which cell is currently visible in the collection view, so it can change the currentPage of the page control.
I've tried delegate methods, like cellForItemAtIndexPath, but it is made to load cells, not show them. didEndDisplayingCell triggers when a cell its not displayed anymore, the opposite event of what I need.
Its seems that -visibleCells and -indexPathsForVisibleItems, collection view methods, are the correct choice for me, but I bumped into another problem. When to trigger them?
Thanks in advance, hope I made myself clear enough so you guys can understand me!
You must setup yourself as UIScrollViewDelegate and implement the scrollViewDidEndDecelerating:method like so:
Objective-C
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = self.collectionView.frame.size.width;
self.pageControl.currentPage = self.collectionView.contentOffset.x / pageWidth;
}
Swift
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = self.collectionView.frame.size.width
pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth)
}
I struggled with this for a while as well, then I was advised to check out the parent classes of UICollectionView. One of them happens to be UIScrollView and if you set yourself up as a UIScrollViewDelegate, you get access to very helpful methods such as scrollViewDidEndDecelerating, a great place to update the UIPageControl.
I would recommend a little tuned calculation and handling as it will update page control immediately in any scroll position with better accuracy.
The solution below works with any scroll view or it subclass (UITableView UICollectionView and others)
in viewDidLoad method write this
scrollView.delegate = self
then use code for your language:
Swift 3
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let pageWidth = scrollView.frame.width
pageControl.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
}
Swift 2:
func scrollViewDidScroll(scrollView: UIScrollView)
{
let pageWidth = CGRectGetWidth(scrollView.frame)
pageControl.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
}
Objective C
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat pageWidth = self.collectionView.frame.size.width;
self.pageControl.currentPage = (self.collectionView.contentOffset.x + pageWidth / 2) / pageWidth;
}
Another option with less code is to use visible item index path and set the page control.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.pageControl.currentPage = [[[self.collectionView indexPathsForVisibleItems] firstObject] row];
}
Place PageControl in your view or set by Code.
Set UIScrollViewDelegate
In Collectionview-> cellForItemAtIndexPath (Method) add the below
code for calculate the Number of pages,
int pages
=floor(ImageCollectionView.contentSize.width/ImageCollectionView.frame.size.width);
[pageControl setNumberOfPages:pages];
Add the ScrollView Delegate method,
pragma mark - UIScrollVewDelegate for UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = ImageCollectionView.frame.size.width;
float currentPage = ImageCollectionView.contentOffset.x / pageWidth;
if (0.0f != fmodf(currentPage, 1.0f))
{
pageControl.currentPage = currentPage + 1;
}
else
{
pageControl.currentPage = currentPage;
}
NSLog(#"finishPage: %ld", (long)pageControl.currentPage);
}
I know this is an old one but I've just needed to implement this sort of feature again and have a bit to add which gives a more complete answer.
Firstly: Using scrollViewDidEndDecelerating assumes that the user lifted their finger while dragging (more like a flick action) and therefore there is a deceleration phase. If the user drags without lifting the finger the UIPageControl will still indicate the old page from before the drag began. Instead using the scrollViewDidScroll callback means that the view is updated both after dragging and flicking and also during dragging and scrolling so it feels much more responsive and accurate for the user.
Secondly: Relying on the pagewidth for calculating the selected index assumes all the cells have the same width and that there is one cell per screen. taking advantage of the indexPathForItemAtPoint method on UICollectionView gives a more resilient result which will work for different layouts and cell sizes. The implementation below assumes the centre of the frame is the desired cell to be represented in the pagecontrol. Also if there are intercell spacings there will times during scrolling when the selectedIndex could be nil or optional so this needs to be checked and unwrapped before setting on the pageControl.
func scrollViewDidScroll(scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset
let centrePoint = CGPointMake(
contentOffset.x + CGRectGetMidX(scrollView.frame),
contentOffset.y + CGRectGetMidY(scrollView.frame)
)
if let index = self.collectionView.indexPathForItemAtPoint(centrePoint){
self.pageControl.currentPage = index.row
}
}
One more thing - set the number of pages on the UIPageControl with something like this:
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
self.pageControl.numberOfPages = 20
return self.pageControl.numberOfPages
}
Simple Swift
public func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
pageControl.currentPage = (collectionView.indexPathsForVisibleItems().first?.row)!
}
UIScrollViewDelegate is already implemented if you implement UICollectionViewDelegate
If using scrollViewDidScroll, updating the page control should be done manually to ⚠️ avoid the flickering dots when you tap on the page control.
Setup the UIPageControl.
let pageControl = UIPageControl()
pageControl.pageIndicatorTintColor = .label
pageControl.defersCurrentPageDisplay = true // Opt-out from automatic display
pageControl.numberOfPages = viewModel.items.count
pageControl.addTarget(self, action: #selector(pageControlValueChanged), for: .valueChanged)
Implement the action (using the extensions below).
#objc func pageControlValueChanged(_ sender: UIPageControl) {
collectionView.scroll(to: sender.currentPage)
}
Update UIPageControl manually on every scroll.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
pageControl.currentPage = collectionView.currentPage
pageControl.updateCurrentPageDisplay() // Display only here
}
}
Convinient UICollectionView extensions.
extension CGRect {
var middle: CGPoint {
CGPoint(x: midX, y: midY)
}
}
extension UICollectionView {
var visibleArea: CGRect {
CGRect(origin: contentOffset, size: bounds.size)
}
var currentPage: Int {
indexPathForItem(at: visibleArea.middle)?.row ?? 0
}
func scroll(to page: Int) {
scrollToItem(
at: IndexPath(row: page, section: 0),
at: .centeredHorizontally,
animated: true
)
}
}
I have a view-based NSTableView with a custom NSTableCellView and a custom NSTableRowView. I customized both of those classes because I want to change the appearance of each row. By implementing the [NSTableRowView draw...] methods I can change the background, the selection, the separator and the drag destination highlight.
My question is: how can I change the highlight that appears when the row is right clicked and a menu appears?
For example, this is the norm:
And I want to change the square highlight to a round one, like this:
I'd imagine this would be done in NSTableRowView by calling a method like drawMenuHighlightInRect: or something, but I can't find it. Also, how can the NSTableRowView class be doing this if I customized, in my subclass, all of the drawing methods, and I don't call the superclass? Is this drawn by the table itself?
EDIT:
After some more experimenting I found out that the round highlight can be achieved by setting the tableview as a source list. Nonetheless, I want to know how to customize it if possible.
I know I'm a bit late to offer any help to the OP, but hopefully this can spare some other folks a little bit of time. I subclassed NSTableRowView to achieve the right-click contextual menu highlight (why Apple doesn't have a public drawing method to override this is beyond me). Here it is in all its glory:
BSDSourceListRowView.h
#import <Cocoa/Cocoa.h>
#interface BSDSourceListRowView : NSTableRowView
// This needs to be set when a context menu is shown.
#property (nonatomic, assign, getter = isShowingMenu) BOOL showingMenu;
#end
BSDSourceListRowView.m
#import "BSDSourceListRowView.h"
#implementation BSDSourceListRowView
- (void)drawBackgroundInRect:(NSRect)dirtyRect
{
[super drawBackgroundInRect:dirtyRect];
// Context menu highlight:
if ( self.isShowingMenu ) {
[self drawContextMenuHighlight];
}
}
- (void)drawContextMenuHighlight
{
BOOL selected = self.isSelected;
CGFloat insetY = ( selected ) ? 2.f : 1.f;
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds, 2.f, insetY) xRadius:6.f yRadius:6.f];
NSColor *fillColor, *strokeColor;
if ( selected ) {
fillColor = [NSColor clearColor];
strokeColor = [NSColor whiteColor];
} else {
fillColor = [NSColor colorWithCalibratedRed:95.f/255.f green:159.f/255.f blue:1.f alpha:0.12f];
strokeColor = [NSColor alternateSelectedControlColor];
}
[fillColor setFill];
[strokeColor setStroke];
[path setLineWidth:2.f];
[path fill];
[path stroke];
}
- (void)drawSelectionInRect:(NSRect)dirtyRect
{
[super drawSelectionInRect:dirtyRect];
if ( self.isShowingMenu ) {
[self drawContextMenuHighlight];
}
}
- (void)setShowingMenu:(BOOL)showingMenu
{
if ( showingMenu == _showingMenu )
return;
_showingMenu = showingMenu;
[self setNeedsDisplay:YES];
}
#end
Feel free to use any of it, change any of it, or do whatever you want with any of it. Have fun!
Updated for Swift 3.x:
SourceListRowView.swift
import Cocoa
open class SourceListRowView : NSTableRowView {
open var isShowingMenu: Bool = false {
didSet {
if isShowingMenu != oldValue {
needsDisplay = true
}
}
}
override open func drawBackground(in dirtyRect: NSRect) {
super.drawBackground(in: dirtyRect)
if isShowingMenu {
drawContextMenuHighlight()
}
}
override open func drawSelection(in dirtyRect: NSRect) {
super.drawSelection(in: dirtyRect)
if isShowingMenu {
drawContextMenuHighlight()
}
}
private func drawContextMenuHighlight() {
let insetY: CGFloat = isSelected ? 2 : 1
let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: insetY), xRadius: 6, yRadius: 6)
let fillColor, strokeColor: NSColor
if isSelected {
fillColor = .clear
strokeColor = .white
} else {
fillColor = NSColor(calibratedRed: 95/255, green: 159/255, blue: 1, alpha: 0.12)
strokeColor = .alternateSelectedControlColor
}
fillColor.setFill()
strokeColor.setStroke()
path.lineWidth = 2
path.fill()
path.stroke()
}
}
Note: I haven't actually run this, but I'm pretty sure this should do the trick in Swift.
Stop Default Drawing
Several answers describe how to draw a custom contextual-click highlight. However, AppKit will continue to draw the default one. There is an easy trick to stop that and I didn't want it to get lost in a comment: subclass NSTableView and override -menuForEvent:
// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
// DO NOT call super's implementation.
return self.menu
}
Here, I assume that you've assigned a menu to the tableView in IB or have set the tableView's menu property programatically. NSTableView's default implementation of -menuForEvent: is what draws the contextual menu highlight.
Solve Bad Apple Engineering
Now that we're not calling super's implementation of menuForEvent:, the clickedRow property of our tableView will always be -1 when we right-click, which means our menuItems won't target the correct row of our tableView.
But fear not, we can do Apple Engineering's job for them. On our custom NSTableView subclass, we override the clickedRow property:
class MyTableView: NSTableView
{
private var _clickedRow: Int = -1
override var clickedRow: Int {
get { return _clickedRow }
set { _clickedRow = newValue }
}
}
Now we update the -menuForEvent: method:
// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
let location: CGPoint = convert(event.locationInWindow, from: nil)
clickedRow = row(at: location)
return self.menu
}
Great. We solved that problem. Onwards to the next thing:
Tell Your RowView To Do Custom Drawing
As others have suggested, add a custom Bool property to your NSTableRowView subclass. Then, in your drawing code, inspect that value to decide whether to draw your custom contextual highlight. However, the correct place to set that value is in the same NSTableView method:
// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
let location: CGPoint = convert(event.locationInWindow, from: nil)
clickedRow = row(at: location)
if clickedRow > 0,
let rowView: MyCustomRowView = rowView(atRow: tableRow, makeIfNecessary: false) as? MyCustomRowView
{
rowView.isContextualMenuTarget = true
}
return self.menu
}
Above, I've created MyCustomRowView (a subclass of NSTableRowView) and have added a custom property: isContextualMenuTarget. That custom property looks like this:
// NSTableRowView subclass
var isContextualMenuTarget: Bool = false {
didSet {
needsDisplay = true
}
}
In my drawing method, I inspect the value of that property and, if it's true, draw my custom highlight.
Clean Up When The Menu Closes
You have a controller that implements the datasource and delegate methods for your tableView. That controller is also likely the delegate for the tableView's menu. (You can assign that in IB or programatically.)
Whatever object is your menu's delegate, implement the menuDidClose: method. Here, I'm working in Objective-C because my controller is still ObjC:
// NSMenuDelegate object
- (void) menuDidClose:(NSMenu *)menu
{
// We use a custom flag on our rowViews to draw our own contextual menu highlight, so we need to reset that.
[_outlineView enumerateAvailableRowViewsUsingBlock:^(__kindof MyCustomRowView * _Nonnull rowView, NSInteger row) {
rowView.isContextualMenuTarget = NO;
}];
}
Performance Note: My tableView will never have more than about 50 entries. If you have a table with THOUSANDS of visible rows, you would be better served to save the rowView that you set isContextualMenuTarget=true on, then access that rowView directly in -menuDidClose: so you don't have to enumerate all rowViews.
Single-Column: This example assumes a single column tableView that has the same NSMenu for each row. You could adapt the same technique for multi-column and/or varying NSMenus per row.
And that's how you beat AppKit in the face until it does what you want.
This is already a bit old, but I've wasted on it quite a bit of time, so posting my solution in case it could help anyone:
In my case, I wanted to remove the lines completely
Lines are not "Focus" rings, they are some stuff Apple is doing in undocument API
The ONLY way I found to remove them (Without using Undocumented API) is by opening NSMenu programmatically, without Interface Builder.
For that, I had to cache "right-click" event on TableViewRow, which has some issue since not always called, so I've dealt with that issue too.
A. Subclass NSTableView:
Overriding right click event, calculating the location of click to get a correct row, and transferring it to my custom NSTableRowView!
class TableView: NSTableView {
override func rightMouseDown(with event: NSEvent) {
let location = event.locationInWindow
let toMyOrigin = self.superview?.convert(location, from: nil)
let rowIndex = self.row(at: toMyOrigin!)
if (rowIndex < 0 || self.numberOfRows < rowIndex) {
return
}
if let isRowExists = self.rowView(atRow: rowIndex, makeIfNecessary: false) {
if let isMyTypeRow = isRowExists as? MyNSTableRowView {
isMyTypeRow.costumRightMouseDown(with: event)
}
}
}
}
B. Subclass MyNSTableRowView
Presenting NSMenu programmatically
class MyNSTableRowView: NSTableRowView {
//My custom selection colors, don't have to implement this if you are ok with the default system highlighted background color
override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none {
let selectionRect = NSInsetRect(self.bounds, 0, 0)
Colors.tabSelectedBackground.setStroke()
Colors.tabSelectedBackground.setFill()
let selectionPath = NSBezierPath.init(roundedRect: selectionRect, xRadius: 0, yRadius: 0)
selectionPath.fill()
selectionPath.stroke()
}
}
func costumRightMouseDown(with event: NSEvent) {
let menu = NSMenu.init(title: "Actions:")
menu.addItem(NSMenuItem.init(title: "Some", action: #selector(foo), keyEquivalent: "a"))
NSMenu.popUpContextMenu(menu, with: event, for: self)
}
#objc func foo() {
}
}
I agree with MCMatan that this is not something you can tweak by changing any draw calls. The box will remain.
I took his approach of bypassing the default menu launch, but left the context menu setup as default in my NSTableView. I think this is a simpler way.
I derive from NSTableView and add the following:
public private(set) var rightClickedRow: Int = -1
override func rightMouseDown(with event: NSEvent)
{
guard let menu = self.menu else { return }
let windowClickLocation = event.locationInWindow
let outlineClickLocation = convert(windowClickLocation, from: nil)
rightClickedRow = row(at: outlineClickLocation)
menu.popUp(positioning: nil, at: outlineClickLocation, in: self)
}
override func rightMouseUp(with event: NSEvent) {
rightClickedRow = -1
}
My rightClickedRow is analogous to clickedRow for the table view. I have an NSViewController that looks after my table, and it is set as the table's menu delegate. I can use rightClickedRow in the delegate calls, such as menuNeedsUpdate().
I'd take a look at the NSTableRowView documentation. It's the class that is responsible for drawing selection and drag feedback in a view-based NSTableView.