Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customize right click highlight on view-based NSTableView

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.

like image 666
Alex Avatar asked Mar 08 '12 14:03

Alex


2 Answers

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.

like image 68
Ben Stock Avatar answered Nov 10 '22 02:11

Ben Stock


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:

  1. In my case, I wanted to remove the lines completely
  2. Lines are not "Focus" rings, they are some stuff Apple is doing in undocument API
  3. The ONLY way I found to remove them (Without using Undocumented API) is by opening NSMenu programmatically, without Interface Builder.
  4. 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() {

    }
}
like image 32
MCMatan Avatar answered Nov 10 '22 02:11

MCMatan