Highlighting selected rows in an NSPredicateEditor

131 views Asked by At

The NSPredicateEditor's superclass NSRuleEditor exposes a selectedRowIndexes: IndexSet property (and corresponding selectRowIndexes(IndexSet, byExtendingSelection: Bool) setter). Adding an observer on this property shows that it is indeed changed whenever a row in the predicate editor is clicked on. However, there is no visual indication that the row is selected or deselected.

I would like to visually highlight the selected rows in my predicate editor, but there are almost no view drawing methods to subclass or delegate methods to implement to customize the appearance of the editor. Can anyone suggest some way to convey to the use that rows of the rule editor have been selected?

3

There are 3 answers

1
Marc T. On

In the NSPredicateEditorRowTemplate class you will find a property named templateViews As mentioned in the documentation from Apple you can override this to return additional views. Return an additional view and use it as an indicator for the selection.

/* returns the list of views that are placed in the row. NSPopUpButtons are treated specially in that the items of multiple templates are merged together; other views are added as-is. Developers can override this to return views in addition to or instead of the default views. */

I customised NSPredicateEditorRowTemplate years ago to implement a Star rating control but would have to look for the code and how it was exactly done.

Hope this helps.

3
marcprux On

Thanks for all the answers so far. This is what I've settled on, but I'm concerned about the fragility. Any suggestions for improvement would be appreciated…

class RowHighlightingRuleEdtitor : NSRuleEditor {
    var highlightsSelectedRow: Bool = true

    override func didChangeValue(forKey key: String) {
        super.didChangeValue(forKey: key)
        if key == "selectedRowIndexes" {
            // Whenever we change the selection, re-layout the view.
            self.needsLayout = true
        }
    }

    override func layout() {
        super.layout()
        if !highlightsSelectedRow { return }
        let selected = self.selectedRowIndexes

        // Of all the fragile ways to check if the view is a row, checking the class name is the easiest.
        // Possible alternative solution: check for the existence of the add-new-row button.
        func viewIsEditorRow(_ view: NSView) -> Bool { return view.className == "NSRuleEditorViewSliceRow" }

        // Sorting must be done because the order of the rows does not necessarily correspond to the order of the subviews.
        // NSRuleEditorViewSliceRow's superclass NSRuleEditorViewSlice has a rowIndex property, but it is private
        // Possible alternative solution: use valueForKey("rowIndex") to get the index
        func rowOrder(lhs: NSView, rhs: NSView) -> Bool { return lhs.frame.origin.y < rhs.frame.origin.y }

        // subview is NSRuleEditorViewSliceHolder and holds NSBannerView and NSRuleEditorViewSliceRow instances
        for (index, rowView) in subviews.flatMap({ $0.subviews }).filter(viewIsEditorRow).sorted(by: rowOrder).enumerated() {
            if selected.contains(index) {
                // we use *secondary*SelectedControlColor because the rule editor comonent is not actually focusable
                rowView.layer?.backgroundColor = NSColor.secondarySelectedControlColor.cgColor
            } else {
                rowView.layer?.backgroundColor = nil
            }
        }
    }
}
1
Willeke On

Undocumented and hacky, tested with NSRuleEditor: Observe selection changes and set the background color of the row slices. Create a category on NSRuleEditorViewSliceRow and implement drawRect:. For example

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
    change:(NSDictionary *)change context:(void *)context {
    if (context != &myRuleEditorObservingContext)
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    else
    {
        // backgroundColor is a property or ivar of NSRuleEditorViewSlice and subclass NSRuleEditorViewSliceRow
        NSArray *slices = [ruleEditor valueForKey:@"slices"];
        [slices makeObjectsPerformSelector:@selector(setBackgroundColor:) withObject:nil];
        NSArray *selectedSlices = [slices objectsAtIndexes:[ruleEditor selectedRowIndexes]];
        [selectedSlices makeObjectsPerformSelector:@selector(setBackgroundColor:) withObject:[NSColor selectedControlColor]];
    }
}


@interface NSRuleEditorViewSliceRow : NSObject

@end

@interface NSRuleEditorViewSliceRow(Draw)

@end

@implementation NSRuleEditorViewSliceRow(Draw)

- (void)drawRect:(NSRect)dirtyRect {
    NSColor *aColor = [(id)self backgroundColor];
    if (aColor) {
        [aColor set];
        NSRectFill(dirtyRect);
    }
}

@end