I created a local event loop and showed up a borderless window (derived from NSPanel),
I found in the event loop there's no NSMouseMoved event received, although I can receive Mouse button down/up events.
What should I do to get the NSMouseMoved events? I found making the float window as key window can receive the NSMouseMoved events, but I don't want to change key window. And it appears this is possible, because I found after clicking the test App Icon in System Dock Bar, I can receive the mousemoved events, and the key window/mainwindow are unchanged.
Here's the my test code: (Create a Cocoa App project names FloatWindowTest, and put a button to link with the onClick: IBAction).
Thanks in advance!
-Jonny
#import "FloatWindowTestAppDelegate.h"
#interface FloatWindow : NSPanel
#end
#interface FloatWindowContentView : NSView
#end
#implementation FloatWindowTestAppDelegate
#synthesize window;
- (void)delayedAction:(id)sender
{
// What does this function do?
// 1. create a float window
// 2. create a local event loop
// 3. print out the events got from nextEventMatchingMask.
// 4. send it to float window.
// What is the problem?
// In local event loop, althrough the event mask has set NSMouseMovedMask
// there's no mouse moved messages received.
//
FloatWindow* floatWindow = [[FloatWindow alloc] init];
NSEvent* event = [NSApp currentEvent];
NSPoint screenOrigin = [[self window] convertBaseToScreen:[event locationInWindow]];
[floatWindow setFrameTopLeftPoint:screenOrigin];
[floatWindow orderFront:nil];
//Making the float window as Key window will work, however
//change active window is not anticipated.
//[floatWindow makeKeyAndOrderFront:nil];
BOOL done = NO;
while (!done)
{
NSAutoreleasePool* pool = [NSAutoreleasePool new];
NSUInteger eventMask = NSLeftMouseDownMask|
NSLeftMouseUpMask|
NSMouseMovedMask|
NSMouseEnteredMask|
NSMouseExitedMask|
NSLeftMouseDraggedMask;
NSEvent* event = [NSApp nextEventMatchingMask:eventMask
untilDate:[NSDate distantFuture]
inMode:NSDefaultRunLoopMode
dequeue:YES];
//why I cannot get NSMouseMoved event??
NSLog(#"new event %#", [event description]);
[floatWindow sendEvent:event];
[pool drain];
}
[floatWindow release];
return;
}
-(IBAction)onClick:(id)sender
{
//Tried to postpone the local event loop
//after return from button's mouse tracking loop.
//but not fixes this problem.
[[NSRunLoop currentRunLoop]
performSelector:#selector(delayedAction:)
target:self
argument:nil
order:0
modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
}
#end
#implementation FloatWindow
- (id)init
{
NSRect contentRect = NSMakeRect(200,300,200,300);
self = [super initWithContentRect:contentRect
styleMask:NSTitledWindowMask
backing:NSBackingStoreBuffered
defer:YES];
if (self) {
[self setLevel:NSFloatingWindowLevel];
NSRect frameRect = [self frameRectForContentRect:contentRect];
NSView* view = [[[FloatWindowContentView alloc]
initWithFrame:frameRect] autorelease];
[self setContentView:view];
[self setAcceptsMouseMovedEvents:YES];
[self setIgnoresMouseEvents:NO];
}
return self;
}
- (BOOL)becomesKeyOnlyIfNeeded
{
return YES;
}
- (void)becomeMainWindow
{
NSLog(#"becomeMainWindow");
[super becomeMainWindow];
}
- (void)becomeKeyWindow
{
NSLog(#"becomeKeyWindow");
[super becomeKeyWindow];
}
#end
#implementation FloatWindowContentView
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (id)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
if (self) {
NSTrackingArea* area;
area = [[NSTrackingArea alloc] initWithRect:frameRect
options:NSTrackingActiveAlways|
NSTrackingMouseMoved|
NSTrackingMouseEnteredAndExited
owner:self
userInfo:nil];
[self addTrackingArea:area];
[area release];
}
return self;
}
- (void)drawRect:(NSRect)rect
{
[[NSColor redColor] set];
NSRectFill([self bounds]);
}
- (BOOL)becomeFirstResponder
{
NSLog(#"becomeFirstResponder");
return [super becomeFirstResponder];
}
#end
I think I find the right answer. This is somehow related to tell NSWindow to accept MouseMoved event. but what to my surprise is, The window should accept mouseMoved events isn't the Floating Window, but the current Key window. So the solution is simple, just enable key window to accept mouse moved events before starting tracking, and revert the switch after ending tracking.
Related
In building a custom NSButton, I've run into a problem handling highlight behavior. After clicking down on the button, holding, and dragging the cursor outside the button's bounds, mouseExited: and mouseEntered: events are not delivered. I understand the reason why, because in mouseDown: calling [super mouseDown:event]; will block until the click is released.
In researching this I came across this Stack Overflow post which describes the same problem. The solution noted is to add NSTrackingEnabledDuringMouseDrag to the NSTrackingArea options, which I have done, yet I continue to see this problem. I tried the other proposed solution with handling the next events in a loop, but this resulted in odd behavior. The button text color turns black on mouse down instead of highlighting the dimmed color, and it doesn't unhighlight upon releasing the mouse, it remains black.
I am using Xcode 9.3, running on macOS 10.13.4.
Here is my NSButton subclass:
#interface BorderlessButton : NSButton {
NSColor *_tempColor;
}
#property (strong, nonatomic) NSColor *color;
#end
#interface BorderlessButton ()
#property (nonatomic) BOOL pressed;
#end
#implementation BorderlessButton
- (id)init {
if (self = [super init]) {
[self setUp];
}
return self;
}
- (id)initWithFrame:(NSRect)frameRect {
if (self = [super initWithFrame:frameRect]) {
[self setUp];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self setUp];
}
return self;
}
- (void)setUp {
_color = [NSColor redColor];
[self setTitle:self.title];
[self setButtonType:NSButtonTypeMomentaryChange];
[self setBordered:NO];
NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:self.bounds
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag
owner:self
userInfo:nil];
[self addTrackingArea:area];
}
- (void)setTitle:(NSString *)title {
[super setTitle:title];
NSMutableAttributedString *colorTitle = [[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTitle]];
[colorTitle addAttributes:#{NSFontAttributeName: self.font, NSForegroundColorAttributeName: self.color} range:NSMakeRange(0, [colorTitle length])];
[self setAttributedTitle:colorTitle];
}
- (void)setColor:(NSColor *)color {
_color = color;
[self setTitle:self.title];
}
- (void)mouseDown:(NSEvent *)event {
self.pressed = YES;
[self highlight:YES];
[super mouseDown:event]; // this blocks until released
[self mouseUp:event];
}
- (void)mouseUp:(NSEvent *)event {
self.pressed = NO;
[self highlight:NO];
[super mouseUp:event];
}
//FIXME: Not called after mouse press down and hold then exit
- (void)mouseExited:(NSEvent *)event {
if (self.pressed) {
[self highlight:NO];
}
[super mouseExited:event];
}
- (void)mouseEntered:(NSEvent *)event {
if (self.pressed) {
[self highlight:YES];
}
[super mouseEntered:event];
}
- (void)highlight:(BOOL)flag {
if (flag) {
if (self.isEnabled) {
NSColor *dimmedColor = [self dimmedColor];
_tempColor = _color;
self.color = dimmedColor;
[self setTitle:self.title];
}
} else {
if (self.isEnabled) {
self.color = _tempColor;
[self setTitle:self.title];
}
}
}
- (NSColor *)dimmedColor {
return [self.color colorWithAlphaComponent:0.5];
}
#end
I have an auto-repeating NSStepper that I want to stop tracking when I receive a certain NSNotification.
One idea was to just send [_stepper setAutorepeat: NO] from the method that receives the notification. That doesn't work. I suppose that the stepper only checks the autorepeat flag when tracking starts.
Then I thought I could subclass NSStepperCell, and use an override of-[NSCell continueTracking:at:inView:] to abort tracking. However, apparently that method does not get called when a stepper is auto-repeating without the mouse moving.
Do I need to completely rewrite trackMouse:inRect:ofView:untilMouseUp:? I suppose then I'd have to handle highlighting parts of the stepper as the mouse moves in or out, and I don't see any public API to even find out which part is highlighted.
I ended up posting a mouse up event. I subclassed NSStepperCell just as a convenient place to watch for the notification and post the event.
#interface AbortableStepperCell : NSStepperCell
{
BOOL _isTracking;
BOOL _isObserverInstalled;
}
#implementation AbortableStepperCell
- (void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
[super dealloc];
}
- (BOOL)startTrackingAt: (NSPoint) startPoint
inView: (NSView *) controlView
{
_isTracking = YES;
if ( ! _isObserverInstalled )
{
[[NSNotificationCenter defaultCenter]
addObserver: self
selector: #selector(abortTracking:)
name: #"AbortMouseTracking"
object: nil];
_isObserverInstalled = YES;
}
return [super startTrackingAt: startPoint inView: controlView];
}
- (void) abortTracking: (NSNotification*) note
{
if (_isTracking)
{
NSWindow* myWindow = self.controlView.window;
NSGraphicsContext* gc =
[NSGraphicsContext graphicsContextWithWindow: myWindow];
NSEvent* upEvent = [NSEvent
mouseEventWithType: NSLeftMouseUp
location: NSZeroPoint
modifierFlags: 0
timestamp: 0.0
windowNumber: myWindow.windowNumber
context: gc
eventNumber: 0
clickCount: 1
pressure: 0.0f ];
if (upEvent)
{
[NSApp postEvent: upEvent atStart: YES];
}
}
}
- (void)stopTracking:(NSPoint)lastPoint
at:(NSPoint)stopPoint
inView:(NSView *)controlView
mouseIsUp:(BOOL)flag
{
_isTracking = NO;
[super stopTracking: lastPoint
at: stopPoint
inView: controlView
mouseIsUp: flag];
}
#end
I am creating an application on MacOS and I am having this problem. I am using custom traffic buttons. When the window is not active if I click they do not respond, what I get is the window becoming active.
How could I fix that so the buttons will respond even if the window is not the active one.
I have them added to a custom view, here is what I have:
My View:
#implementation TrafficView
- (id)init {
if ((self = [super init])) {
trackingArea_.reset([[NSTrackingArea alloc]
initWithRect:[self bounds]
options:(NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways |
NSTrackingInVisibleRect)
owner:self
userInfo:nil]);
[self addTrackingArea:trackingArea_.get()];
}
return self;
}
- (void)dealloc {
[super dealloc];
}
- (BOOL)acceptsFirstResponder {
return YES;
}
- (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
return YES;
}
- (void)mouseEntered:(NSEvent*)event {
mouseInside_ = YES;
for (NSView* view in self.subviews)
[view setNeedsDisplay:YES];
}
- (void)mouseExited:(NSEvent*)event {
mouseInside_ = NO;
for (NSView* view in self.subviews)
[view setNeedsDisplay:YES];
}
- (BOOL)_mouseInGroup:(NSButton *)button {
return mouseInside_;
}
#end
Now to add the buttons I do this in the window implementation (I will post the code for only one button)
trafficView_.reset([[TrafficView alloc] init]);
[self.frameView addSubview:trafficView_];
// close button is declared as NSButton
closeButton_ =
[NSWindow standardWindowButton:NSWindowCloseButton
forStyleMask:NSTitledWindowMask];
[trafficView_ addSubview:closeButton_];
Note that my custom view (trafficView_) is added to the NSThemeFrame class, self.frameView
- (NSView*)frameView {
return [[self contentView] superview];
}
I do not why the buttons do not respond.
Could someone help on this?
Thanks in advance
I'm making a little server app for OS X and I'm using an NSTextView to log some info about connected clients.
Whenever I need to log something I'm appending the new message to the text of the NSTextView this way:
- (void)logMessage:(NSString *)message
{
if (message) {
self.textView.string = [self.textView.string stringByAppendingFormat:#"%#\n",message];
}
}
After this I'd like the NSTextField (or maybe I should say the NSClipView that contains it) to scroll down to show the last line of its text (obviously it should scroll only if the last line is not visible yet, in fact if then new line is the first line I log it is already on the screen so there is no need to scroll down).
How can I do that programmatically?
Found solution:
- (void)logMessage:(NSString *)message
{
if (message) {
[self appendMessage:message];
}
}
- (void)appendMessage:(NSString *)message
{
NSString *messageWithNewLine = [message stringByAppendingString:#"\n"];
// Smart Scrolling
BOOL scroll = (NSMaxY(self.textView.visibleRect) == NSMaxY(self.textView.bounds));
// Append string to textview
[self.textView.textStorage appendAttributedString:[[NSAttributedString alloc]initWithString:messageWithNewLine]];
if (scroll) // Scroll to end of the textview contents
[self.textView scrollRangeToVisible: NSMakeRange(self.textView.string.length, 0)];
}
As of OS 10.6 it's as simple as nsTextView.scrollToEndOfDocument(self).
Swift 4 + 5
let smartScroll = self.textView.visibleRect.maxY == self.textView.bounds.maxY
self.textView.textStorage?.append("new text")
if smartScroll{
self.textView.scrollToEndOfDocument(self)
}
I've been messing with this for a while, because I couldn't get it to work reliably. I've finally gotten my code working, so I'd like to post it as a reply.
My solution allows you to scroll manually, while output is being added to the view. As soon as you scroll to the absolute bottom of the NSTextView, the automatic scrolling will resume (if enabled, that is).
First a category to #import this only when needed...
FSScrollToBottomExtensions.h:
#interface NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom;
- (BOOL)isAtBottom;
- (void)scrollToBottom;
#end
FSScrollToBottomExtensions.m:
#implementation NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom
{
NSRect visRect;
NSRect boundsRect;
visRect = [self visibleRect];
boundsRect = [self bounds];
return(NSMaxY(visRect) - NSMaxY(boundsRect));
}
// Apple's suggestion did not work for me.
- (BOOL)isAtBottom
{
return([self distanceToBottom] == 0.0);
}
// The scrollToBottom method provided by Apple seems unreliable, so I wrote this one
- (void)scrollToBottom
{
NSPoint pt;
id scrollView;
id clipView;
pt.x = 0;
pt.y = 100000000000.0;
scrollView = [self enclosingScrollView];
clipView = [scrollView contentView];
pt = [clipView constrainScrollPoint:pt];
[clipView scrollToPoint:pt];
[scrollView reflectScrolledClipView:clipView];
}
#end
... create yourself an "OutputView", which is a subclass of NSTextView:
FSOutputView.h:
#interface FSOutputView : NSTextView
{
BOOL scrollToBottomPending;
}
FSOutputView.m:
#implementation FSOutputView
- (id)setup
{
...
return(self);
}
- (id)initWithCoder:(NSCoder *)aCoder
{
return([[super initWithCoder:aCoder] setup]);
}
- (id)initWithFrame:(NSRect)aFrame textContainer:(NSTextContainer *)aTextContainer
{
return([[super initWithFrame:aFrame textContainer:aTextContainer] setup]);
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (void)awakeFromNib
{
NSNotificationCenter *notificationCenter;
NSView *view;
// viewBoundsDidChange catches scrolling that happens when the caret
// moves, and scrolling caused by pressing the scrollbar arrows.
view = [self superview];
[notificationCenter addObserver:self
selector:#selector(viewBoundsDidChangeNotification:)
name:NSViewBoundsDidChangeNotification object:view];
[view setPostsBoundsChangedNotifications:YES];
// viewFrameDidChange catches scrolling that happens because text
// is inserted or deleted.
// it also catches situations, where window resizing causes changes.
[notificationCenter addObserver:self
selector:#selector(viewFrameDidChangeNotification:)
name:NSViewFrameDidChangeNotification object:self];
[self setPostsFrameChangedNotifications:YES];
}
- (void)handleScrollToBottom
{
if(scrollToBottomPending)
{
scrollToBottomPending = NO;
[self scrollToBottom];
}
}
- (void)viewBoundsDidChangeNotification:(NSNotification *)aNotification
{
[self handleScrollToBottom];
}
- (void)viewFrameDidChangeNotification:(NSNotification *)aNotification
{
[self handleScrollToBottom];
}
- (void)outputAttributedString:(NSAttributedString *)aAttributedString
flags:(int)aFlags
{
NSRange range;
BOOL wasAtBottom;
if(aAttributedString)
{
wasAtBottom = [self isAtBottom];
range = [self selectedRange];
if(aFlags & FSAppendString)
{
range = NSMakeRange([[self textStorage] length], 0);
}
if([self shouldChangeTextInRange:range
replacementString:[aAttributedString string]])
{
[[self textStorage] beginEditing];
[[self textStorage] replaceCharactersInRange:range
withAttributedString:aAttributedString];
[[self textStorage] endEditing];
}
range.location += [aAttributedString length];
range.length = 0;
if(!(aFlags & FSAppendString))
{
[self setSelectedRange:range];
}
if(wasAtBottom || (aFlags & FSForceScroll))
{
scrollToBottomPending = YES;
}
}
}
#end
... You can add a few more convenience methods to this class (I've stripped it down), so that you can output a formatted string.
- (void)outputString:(NSString *)aFormatString arguments:(va_list)aArguments attributeKey:(NSString *)aKey flags:(int)aFlags
{
NSMutableAttributedString *str;
str = [... generate attributed string from parameters ...];
[self outputAttributedString:str flags:aFlags];
}
- (void)outputLineWithFormat:(NSString *)aFormatString, ...
{
va_list args;
va_start(args, aFormatString);
[self outputString:aFormatString arguments:args attributeKey:NULL flags:FSAddNewLine];
va_end(args);
}
I have some customised NSTextView and custom input method so my option was to use:
self.scrollView.contentView.scroll(NSPoint(x: 1, y: self.textView.frame.size.height))
I am a .Net developer who need to port a small project into Mac, so I know next to nothing about Objective C. In fact the code below was just a bunch of grasping at straws and shooting in the dark.
Trying to build a Status Menu program that opens one or another window depending on if it is a left-click or a right-click/ctrl+click. Here is what I have, and it works for left-click only (obviously):
-(void) awakeFromNib{
NSBundle *bundle = [NSbundle mainBundle];
statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
[statusImage = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:#"icon" ofType:#"png"]];
[statusItem setImage:statusImage];
[statusItem setToolTip:#"Program Name"];
[statusItem setHighlightMode:YES];
[statusItem setAction:#selector(openWin:)];
[statusItem setTarget: self];
}
-(void)openWin:(id)sender{
[self openLeftWindow:sender];
}
-(IBAction)openLeftWindow:(id)sender{
//Code to populate Left Click Window
[leftWindow makeKeyAndorderFront:nil];
}
-(IBAction)openRightWindow:(id)sender{
//Code to populate Right Click Window
[rightWindow makeKeyAndorderFront:nil];
}
It seems to me that the solution would be either an if statement in the openWin() function to determine which button is clicked (or if ctrl was held down) then run the appropriate code or to make the menu aware of both the left and right clicks. But neither of these have worked when I attempted to do so.
Thanks In Advance.
If you will be satisfied with detecting control-click and not right click, then the first block of code will do what you want. If you really need the right click detection, you will have to use a custom view instead of an image in your NSStatusItem, and the second block of code will work.
Simple Method:
- (void)openWin:(id)sender {
NSEvent *event = [NSApp currentEvent];
if([event modifierFlags] & NSControlKeyMask) {
[self openRightWindow:nil];
} else {
[self openLeftWindow:nil];
}
}
Custom view method:
- (void)awakeFromNib {
...
statusImage = ...
MyView *view = [MyView new];
view.image = statusImage;
[statusItem setView:view];
[statusItem setToolTip:#"Program Name"];
view target = self;
view action = #selector(openLeftWindow:);
view rightAction = #selector(openRightWindow:);
[view release];
//[statusImage release]; //If you are not using it anymore, you should release it.
}
MyView.h
#import <Cocoa/Cocoa.h>
#interface MyView : NSControl {
NSImage *image;
id target;
SEL action, rightAction;
}
#property (retain) NSImage *image;
#property (assign) id target;
#property (assign) SEL action, rightAction;
#end
MyView.m
#import "MyView.h"
#implementation MyView
#synthesize image, target, action, rightAction;
- (void)mouseUp:(NSEvent *)event {
if([event modifierFlags] & NSControlKeyMask) {
[NSApp sendAction:self.rightAction to:self.target from:self];
} else {
[NSApp sendAction:self.action to:self.target from:self];
}
}
- (void)rightMouseUp:(NSEvent *)event {
[NSApp sendAction:self.rightAction to:self.target from:self];
}
- (void)dealloc {
self.image = nil;
[super dealloc];
}
- (void)drawRect:(NSRect)rect {
[self.image drawInRect:self.bounds fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}
#end
I would create a view and use the status items method.
-setView:
Then in the subclassed view you can detect ctrl+LMB using the following
- (void)mouseDown:(NSEvent *)theEvent
{
[super mouseDown:theEvent];
//Respond to the mouse click
if ([theEvent modifierFlags] & NSCommandKeyMask) //Command + LMB
{
//Do something
}
}
I think you can figure out the rest.
A more simplified response (Note, only works with control + click)
Properties:
#property (strong, nonatomic) NSStatusItem *statusItem;
#property (weak) IBOutlet NSMenu *statusMenu;
In Your Application Did Load:
[self.statusItem setAction:#selector(itemClicked:)];
Clicked Function:
- (void)itemClicked:(id)sender
{
NSEvent *event = [NSApp currentEvent];
if([event modifierFlags] & NSControlKeyMask) {
NSLog(#"Right Click Pressed");
[self.statusItem popUpStatusItemMenu:self.statusMenu];
} else {
// Do Nothing
NSLog(#"Left Click Pressed");
}
}