问题
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.
回答1:
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
.
回答2:
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.
回答3:
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() {
}
}
回答4:
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()
.
来源:https://stackoverflow.com/questions/9619664/customize-right-click-highlight-on-view-based-nstableview