Hiding the dock icon without hiding the menu bar - cocoa

I use the ideas in this thread to hide the dock icon of my app optionally. If the dock icon is shown after all, the menu bar should be shown too. Only with Jiulong's answer I haven't been able to make this work. The menu bar is still hidden.
So basically 'Application is agent' is set to '1' in the InfoPList, and this code is used :
if (![[NSUserDefaults standardUserDefaults] boolForKey:#"LaunchAsAgentApp"]) {
ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
SetSystemUIMode(kUIModeNormal, 0);
[[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:#"com.apple.dock" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifier:nil];
[[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE];
}
So why doesn't the menu bar show up, until I hide and refocus the app? Is there any fix for this? I saw that the 'Quick Search Box' for mac app doesn't show the menu bar upon launching either...
EDIT : I contacted Apple, and they gave me a carbon and a non-carbon solution. Given a new project with 'Application is Agent' set to 'YES' in the Plist file, then this code could be used in the AppDelegate class :
#define USE_CARBON 0
//
// Note: NSLogDebug is defined in the projects pre-compiled (.pch) file
//
#implementation AppDelegate
{
BOOL show_icon;
}
// Application will finish launching
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
NSLogDebug();
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
if (![[NSFileManager defaultManager] fileExistsAtPath:[receiptUrl path]]) {
// exit(173);
}
#if 1
show_icon = YES;
#else
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *hasDockIconDefaultsKey = #"Has Dock Icon?";
// note: toggles value on each run (normally set from user pref pannel)
show_icon = [userDefaults boolForKey:hasDockIconDefaultsKey];
[userDefaults setBool:!show_icon forKey:hasDockIconDefaultsKey];
#endif // if 1
if (show_icon) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp setPresentationOptions:NSApplicationPresentationDefault];
[NSMenu setMenuBarVisible:NO];
[NSMenu setMenuBarVisible:YES];
}
[NSApp activateIgnoringOtherApps:YES];
} // applicationWillFinishLaunching
// Application did finish launching
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSLogDebug();
// Insert code here to initialize your application
if (show_icon) {
#if USE_CARBON
ProcessSerialNumber psn = {0, kCurrentProcess};
OSStatus returnCode = TransformProcessType(&psn, kProcessTransformToForegroundApplication);
if (noErr != returnCode) {
NSLog(#"TransformProcessType error: %d (0x%0X)", returnCode, returnCode);
}
ProcessSerialNumber psnx = {0, kNoProcess};
GetNextProcess(&psnx);
SetFrontProcess(&psnx);
#else // if 0
NSWorkspace *sharedWorkspace = [NSWorkspace sharedWorkspace];
NSRunningApplication * menuBarOwningApplication = [sharedWorkspace menuBarOwningApplication];
(void) [menuBarOwningApplication activateWithOptions:NSApplicationActivateIgnoringOtherApps];
#endif
[self performSelector:#selector(setFront) withObject:nil afterDelay:0.];
}
} // applicationDidFinishLaunching
// Close app when main window is closed
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
return (YES);
}
- (void)setFront;
{
#if USE_CARBON
ProcessSerialNumber psn = {0, kCurrentProcess};
SetFrontProcess(&psn);
#else // if USE_CARBON
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
#endif // USE_CARBON
}
#end
Note that I filed a bug report too.
Here's a Swift version of the non-carbon solution :
func applicationWillFinishLaunching(_ notification: Notification) {
if showIcon {
NSApp.setActivationPolicy(.regular)
NSApp.presentationOptions = []
NSMenu.setMenuBarVisible(false)
NSMenu.setMenuBarVisible(true)
}
NSApp.activate(ignoringOtherApps: true)
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSApplication.shared.activate(ignoringOtherApps: true)
if showIcon {
let workspace = NSWorkspace.shared
let application = workspace.menuBarOwningApplication
application?.activate(options: .activateIgnoringOtherApps)
self.perform(#selector(activate), with: nil, afterDelay: 0.0)
}
}
#objc private func activate() {
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
}

First, you should use -[NSApplication setActivationPolicy:] rather than TransformProcessType() and -[NSApplication setPresentationOptions:] rather than SetSystemUIMode() in modern code. If switching to those is not enough to fix the problem with the menu bar not updating, I recommend that you try using -setPresentationOptions: or +[NSMenu setMenuBarVisible:] to hide the menu bar and then immediately reverse that operation.
Also, drop that business with trying to activate the Dock.
So, something like:
if (![[NSUserDefaults standardUserDefaults] boolForKey:#"LaunchAsAgentApp"]) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp setPresentationOptions:NSApplicationPresentationDefault]; // probably not necessary since it's the default
[NSMenu setMenuBarVisible:NO]; // these two lines may not be necessary, either; using -setActivationPolicy: instead of TransformProcessType() may be enough
[NSMenu setMenuBarVisible:YES];
}

Related

How to intercept toggling fullscreen mode

I'm creating Mac OS plugin(bundle) for Unity3D. How can I intercept entering cmd-f combination (toggling full screen mode)? I can't create my own window, I can only use default (mainWindow). I've tried to use NSNotificationCenter, but I need to stop event, I don't need just a notification. I've tried to create NSResponder and add it to capture input events, but something don't work. Any ideas how to do it?
NSWindow* window = [[NSApplication sharedApplication] mainWindow];
NSView* view = [window contentView];
NSResponder* oldresp = [view nextResponder];
MyResponder* myres = [MyResponder alloc];
[myres retain];
[view setNextResponder:myres];
and
#interface MyResponder : NSResponder
{
}
- (void)keyDown:(NSEvent *)theEvent;
#end
#implementation MyResponder
- (void)keyDown:(NSEvent *)theEvent
{
NSLog(#"%#",#"!KeyDown Event");
NSString *theArrow = [theEvent charactersIgnoringModifiers];
unichar keyChar = 0;
if ( [theArrow length] == 1 )
{
keyChar = [theArrow characterAtIndex:0];
if ( keyChar == NSModeSwitchFunctionKey )
{
NSLog(#"%#",#"!!!___!!! GOT NSModeSwitchFunctionKey !!!");
return;
}
NSLog(#"%# %d",#"! Key:",keyChar);
}
[super keyDown:theEvent];
}
#end
One solution is to check constantly if the user has switched to fullscreen mode, and if he has, toggle fullscreen off from your program. This may cause some brief lag or graphics sketchiness momentarily, but it should work.
function Update ()
{
if (Screen.fullScreen) {
Screen.fullScreen = false;
}
}
Im not sure of a way to intercept the key press and ignore the command before the program switches though.
See this related post on unity answers about dealing with this on windows:
http://answers.unity3d.com/questions/544183/block-or-override-alt-enter-fullscreen.html

NSPasteBoard get file from the Download (Grid View) in dock via DragAndDrop to App Statusbar Icon

I Implemented drag and drop functionality in a cocoa app that I am working on.
When I drag and drop files, from a folder or from the desktop or from any specific location on the hard drive, onto the status bar icon of my app, the app appropriately detects it and I am able to work with the file without any issues at all.
The problem that I encounter is that if I click the Downloads icon in the dock (it opens in grid view) and from there I select a file and drag and drop it to the status bar, then the app does not detect it at all. The "performDragOperation" is not even called in this scenario. I think this is because the Downloads Grid is not a specific location, instead it is a representation of the Downloads folder presented on the desktop.
This is where I register for the Drag types:
- (id)initWithStatusItem:(NSStatusItem *)statusItem {
CGFloat itemWidth = [statusItem length];
CGFloat itemHeight = [[NSStatusBar systemStatusBar] thickness];
NSRect itemRect = NSMakeRect(0.0, 0.0, itemWidth, itemHeight);
self = [super initWithFrame:itemRect];
if (self != nil) {
_statusItem = statusItem;
_statusItem.view = self;
}
NSArray *dragTypes = [NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil];
[self registerForDraggedTypes:dragTypes];
return self;
}
Here is the draggingEntered method:
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
ApplicationDelegate *appdelegate = (ApplicationDelegate *)[[NSApplication sharedApplication] delegate];
if (!appdelegate.isApplicationLoggedIn) {
return NSDragOperationNone;
}
if ([sender draggingSourceOperationMask] & NSDragOperationCopy) {
return NSDragOperationCopy;
}
if ([[sender draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil]]) {
return NSDragOperationCopy;
}
return NSDragOperationNone;
}
Any help would be appreciated.
Thank You.

Scrolling NSTextView to bottom

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))

NSButton with delayed NSMenu - Objective-C/Cocoa

I want to create an NSButton that sends an action when it is clicked, but when it is pressed for 1 or two seconds it show a NSMenu. Exactly the same as this question here, but since that answer doesn't solve my problem, I decided to ask again.
As an example, go to Finder, open a new window, navigate through some folders and then click the back button: you go to the previous folder. Now click and hold the back button: a menu is displayed. I don't know how to do this with a NSPopUpButton.
Use NSSegmentedControl.
Add a menu by sending setMenu:forSegment: to the control (connecting anything to the menu outlet in IB won't do the trick). Have an action connected to the control (this is important).
Should work exactly as you described.
Create a subclass of NSPopUpButton and override the mouseDown/mouseUp events.
Have the mouseDown event delay for a moment before calling super's implementation and only if the mouse is still being held down.
Have the mouseUp event set the selectedMenuItem to nil (and therefore selectedMenuItemIndex will be -1) before firing the button's target/action.
The only other issue is to handle rapid clicks, where the timer for one click might fire at the moment when the mouse is down for some future click. Instead of using an NSTimer and invalidating it, I chose to have a simple counter for mouseDown events and bail out if the counter has changed.
Here's the code I'm using in my subclass:
// MyClickAndHoldPopUpButton.h
#interface MyClickAndHoldPopUpButton : NSPopUpButton
#end
// MyClickAndHoldPopUpButton.m
#interface MyClickAndHoldPopUpButton ()
#property BOOL mouseIsDown;
#property BOOL menuWasShownForLastMouseDown;
#property int mouseDownUniquenessCounter;
#end
#implementation MyClickAndHoldPopUpButton
// highlight the button immediately but wait a moment before calling the super method (which will show our popup menu) if the mouse comes up
// in that moment, don't tell the super method about the mousedown at all.
- (void)mouseDown:(NSEvent *)theEvent
{
self.mouseIsDown = YES;
self.menuWasShownForLastMouseDown = NO;
self.mouseDownUniquenessCounter++;
int mouseDownUniquenessCounterCopy = self.mouseDownUniquenessCounter;
[self highlight:YES];
float delayInSeconds = [NSEvent doubleClickInterval];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if (self.mouseIsDown && mouseDownUniquenessCounterCopy == self.mouseDownUniquenessCounter) {
self.menuWasShownForLastMouseDown = YES;
[super mouseDown:theEvent];
}
});
}
// if the mouse was down for a short enough period to avoid showing a popup menu, fire our target/action with no selected menu item, then
// remove the button highlight.
- (void)mouseUp:(NSEvent *)theEvent
{
self.mouseIsDown = NO;
if (!self.menuWasShownForLastMouseDown) {
[self selectItem:nil];
[self sendAction:self.action to:self.target];
}
[self highlight:NO];
}
#end
If anybody still needs this, here's my solution based on a plain NSButton, not a segmented control.
Subclass NSButton and implement a custom mouseDown that starts a timer within the current run loop. In mouseUp, check if the timer has not fired. In that case, cancel it and perform the default action.
This is a very simple approach, it works with any NSButton you can use in IB.
Code below:
- (void)mouseDown:(NSEvent *)theEvent {
[self setHighlighted:YES];
[self setNeedsDisplay:YES];
_menuShown = NO;
_timer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(showContextMenu:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}
- (void)mouseUp:(NSEvent *)theEvent {
[self setHighlighted:NO];
[self setNeedsDisplay:YES];
[_timer invalidate];
_timer = nil;
if(!_menuShown) {
[NSApp sendAction:[self action] to:[self target] from:self];
}
_menuShown = NO;
}
- (void)showContextMenu:(NSTimer*)timer {
if(!_timer) {
return;
}
_timer = nil;
_menuShown = YES;
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:#"Contextual Menu"];
[[theMenu addItemWithTitle:#"Beep" action:#selector(beep:) keyEquivalent:#""] setTarget:self];
[[theMenu addItemWithTitle:#"Honk" action:#selector(honk:) keyEquivalent:#""] setTarget:self];
[theMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(self.bounds.size.width-8, self.bounds.size.height-1) inView:self];
NSWindow* window = [self window];
NSEvent* fakeMouseUp = [NSEvent mouseEventWithType:NSLeftMouseUp
location:self.bounds.origin
modifierFlags:0
timestamp:[NSDate timeIntervalSinceReferenceDate]
windowNumber:[window windowNumber]
context:[NSGraphicsContext currentContext]
eventNumber:0
clickCount:1
pressure:0.0];
[window postEvent:fakeMouseUp atStart:YES];
[self setState:NSOnState];
}
I've posted a working sample on my GitHub.
Late to the party but here is a bit different approach, also subclassing NSButton:
///
/// #copyright © 2018 Vadim Shpakovski. All rights reserved.
///
import AppKit
/// Button with a delayed menu like Safari Go Back & Forward buttons.
public class DelayedMenuButton: NSButton {
/// Click & Hold menu, appears after `NSEvent.doubleClickInterval` seconds.
public var delayedMenu: NSMenu?
}
// MARK: -
extension DelayedMenuButton {
public override func mouseDown(with event: NSEvent) {
// Run default implementation if delayed menu is not assigned
guard delayedMenu != nil, isEnabled else {
super.mouseDown(with: event)
return
}
/// Run the popup menu if the mouse is down during `doubleClickInterval` seconds
let delayedItem = DispatchWorkItem { [weak self] in
self?.showDelayedMenu()
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(NSEvent.doubleClickInterval * 1000)), execute: delayedItem)
/// Action will be set to nil if the popup menu runs during `super.mouseDown`
let defaultAction = self.action
// Run standard tracking
super.mouseDown(with: event)
// Restore default action if popup menu assigned it to nil
self.action = defaultAction
// Cancel popup menu once tracking is over
delayedItem.cancel()
}
}
// MARK: - Private API
private extension DelayedMenuButton {
/// Cancels current tracking and runs the popup menu
func showDelayedMenu() {
// Simulate mouse up to stop native tracking
guard
let delayedMenu = delayedMenu, delayedMenu.numberOfItems > 0, let window = window, let location = NSApp.currentEvent?.locationInWindow,
let mouseUp = NSEvent.mouseEvent(
with: .leftMouseUp, location: location, modifierFlags: [], timestamp: Date.timeIntervalSinceReferenceDate,
windowNumber: window.windowNumber, context: NSGraphicsContext.current, eventNumber: 0, clickCount: 1, pressure: 0
)
else {
return
}
// Cancel default action
action = nil
// Show the default menu
delayedMenu.popUp(positioning: nil, at: .init(x: -4, y: bounds.height + 2), in: self)
// Send mouse up when the menu is on screen
window.postEvent(mouseUp, atStart: false)
}
}

Hide a child window from parent window hide all app

I have 2 windows one is the header [mainWindow] (-setMovableByWindowBackground:YES) and the other is the content [secWindow], child of the header, the header have a button to hide the content.
[mainWindow addChildWindow:secWindow ordered:NSWindowBelow];
[mainWindow setMovableByWindowBackground:YES];
code to hide secWindow:
(IBAction) toggleSecondary: (id) sender;
{
if ([secWindow isVisible]) {
[secWindow orderOut:self];
} else {
[secWindow orderFront:self];
}
}
The problem is when a push the button, all app hide, Main and Sec windows and only need to hide the [secWindow].
Weel, I found a solution, I don't know if a correct way, but works for me. ^_^
//get the mainWindow cordinates
NSRect theFrame = [mainWindow frame];
NSPoint theOrigin = theFrame.origin;
NSPoint pSecWin = theFrame.origin;
//put secWin below mainWindow
pSecWin.y = theOrigin.y - secHeight;
(IBAction) toggleSecondary: (id) sender;
{
if ([secWindow isVisible]) {
[mainWindow removeChildWindow:secWindow];
[secWindow orderOut:self];
} else {
[secWindow setFrameOrigin:pSecWin];
[mainWindow addChildWindow:secWindow ordered:NSWindowBelow];
[secWindow orderFront:self];
}
}
so thats it, thanks anyway

Resources