Can't change the mouse cursor of a NSTextField

烂漫一生 提交于 2019-11-29 05:08:19
Girish Kolari

Clickable links with NSTextField

I got this working after subclassing NSTextField as mentioned :

- (void)resetCursorRects {
    [self addCursorRect:[self bounds] cursor:[NSCursor pointingHandCursor]];
}

I wrote a Swift version 2.0 of the answers above and made it available as a GitHub sample project found here.

And even though it's set for NSTextField, the concepts should work with anything that subclasses from NSResponder (which is where mouseEntered and becomeFirstResponder come from).

Here's the guts of the code I wrote:

import Cocoa

class CCTextField: NSTextField {

    var myColorCursor : NSCursor?

    var mouseIn : Bool = false

    var trackingArea : NSTrackingArea?

    override func awakeFromNib()
    {
        myColorCursor = NSCursor.init(image: NSImage(named:"heart")!, hotSpot: NSMakePoint(0.0, 0.0))
    }

    override func resetCursorRects() {
        if let colorCursor = myColorCursor {
            self.addCursorRect(self.bounds, cursor: colorCursor)
        }
    }

    override func mouseEntered(theEvent: NSEvent) {
        super.mouseEntered(theEvent)
        self.mouseIn = true
    }

    override func mouseExited(theEvent: NSEvent) {
        super.mouseExited(theEvent)
        self.mouseIn = false
    }

    override func mouseMoved(theEvent: NSEvent) {
        if self.mouseIn {
            myColorCursor?.set()
        }
        super.mouseMoved(theEvent)
    }

    func setArea(areaToSet: NSTrackingArea?)
    {
        if let formerArea = trackingArea {
            self.removeTrackingArea(formerArea)
        }

        if let newArea = areaToSet {
            self.addTrackingArea(newArea)
        }
        trackingArea = areaToSet
    }

    override func becomeFirstResponder() -> Bool {
        let rect = self.bounds
        let trackingArea = NSTrackingArea.init(rect: rect, options: [NSTrackingAreaOptions.MouseEnteredAndExited, NSTrackingAreaOptions.ActiveAlways], owner: self, userInfo: nil)

        // keep track of where the mouse is within our text field
        self.setArea(trackingArea)

        if let ev = NSApp.currentEvent {
            if NSPointInRect(self.convertPoint(ev.locationInWindow, fromView: nil), self.bounds) {
                self.mouseIn = true
                myColorCursor?.set()
            }
        }
        return true
    }
}

I had the same issue, the cursorUpdate method was called each time the cursor entered the tracking area, but the cursor was set back somewhere else, probably its superview.

I managed to solve it by overriding the mouseMoved method.

// In the textfield subclass:
- (void)mouseEntered:(NSEvent *)theEvent {
    [super mouseEntered:theEvent];
    self.isMouseIn = YES;
}

- (void)mouseExited:(NSEvent *)theEvent {
    [super mouseExited:theEvent];
    self.isMouseIn = NO;
}


//In the superview of the textfield:
- (void)mouseMoved:(NSEvent *)theEvent {
    if (self.hoverButton.isMouseIn) {
        [[NSCursor pointingHandCursor] set];
    }
    [super mouseMoved:theEvent];
}

I overrided the mouseMoved method in my windowController class, but overriding the superview should work.

NSTextView seems to be handling the cursor through its -(void)mouseMoved:(NSEvent*)theEvent method. Also, when the NSTextView becomes first responder, some private code in the RunLoop seems to force the cursor to the IBeamCursor without giving us a choice. Here is a subclass that works around those limitations :

@interface MyTextView : NSTextView {
    NSTrackingArea* area;
    BOOL mouseInside;
}

@property(nonatomic, retain) NSTrackingArea* area;

@end


@implementation MyTextView

@synthesize area;

- (void)setArea:(NSTrackingArea *)newArea
{
    [newArea retain];
    if (area) {
        [self removeTrackingArea:area];
        [area release];
    }
    if (newArea) {
        [self addTrackingArea:newArea];
    }
    area = newArea;
}

- (BOOL)becomeFirstResponder
{
    NSRect rect = <insert the tracking rect where you want to have a special cursor>;
    self.area = [[[NSTrackingArea alloc] initWithRect:rect options:NSTrackingMouseEnteredAndExited | NSTrackingActiveWhenFirstResponder owner:self userInfo:nil] autorelease];
    NSEvent* ev = [NSApp currentEvent];
    if (NSPointInRect([self convertPoint:ev.locationInWindow fromView:nil], self.bounds)) {
        mouseInside = YES;
        // This is a workaround for the private call that resets the IBeamCursor
        [[NSCursor arrowCursor] performSelector:@selector(set) withObject:nil afterDelay:0];
    }
}

- (void)mouseEntered:(NSEvent *)theEvent
{
    [super mouseEntered:theEvent];
    [[NSCursor arrowCursor] set];
    mouseInside = YES;
}

- (void)mouseExited:(NSEvent *)theEvent
{
    [super mouseExited:theEvent];
    mouseInside = NO;
}

- (void)mouseMoved:(NSEvent *)theEvent
{
    si (!mouseInside) {
        // We only forward the mouseMoved event when the mouse is outside the zone for which we control the cursor
        [super mouseMoved:theEvent];
    }
}

- (oneway void)dealloc
{
    [area release];
    [super dealloc];
}

@end

This works for me:

class TextField: NSTextField {
    override func becomeFirstResponder() -> Bool {
        addTrackingAreaIfNeeded()
        return super.becomeFirstResponder()
    }
    override func mouseMoved(with event: NSEvent) {
        super.mouseMoved(with: event)
        NSCursor.pointingHand.set()
    }
    private func addTrackingAreaIfNeeded() {
        if trackingAreas.isEmpty {
            let area = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)
            addTrackingArea(area)
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!