How to force NSToolBar validation? - cocoa

I'm geting this strange behavior. I'm using a panel with text to show to the user when the app is waiting for some info. This panel is show modally to prevent the user to click something.
When the loading panel is hidden all the items on the toolbar are disabled and the validateToolbarItem method is not called.
I'm showing the panel in this way:
- (void)showInWindow:(NSWindow *)mainWindow {
sheetWindow = [self window];
[self sheetWillShow];
[NSApp beginSheet:sheetWindow modalForWindow:mainWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
[NSApp runModalForWindow:sheetWindow];
[NSApp endSheet:sheetWindow];
[sheetWindow orderOut:self];
}
- (void)dismissModal {
[sheetWindow close];
[NSApp stopModal];
}
How can I force the toolbar to validate in this case?
Edit after comment:
I have already tried:
[[[NSApp mainWindow] toolbar] validateVisibleItems]
[[NSApp mainWindow] update];
[NSApp updateWindows];
[NSApp setWindowsNeedUpdate:YES];
All after call dismissModal. I'm thinking that the problem is elsewhere....

The problem is that NSToolbar only sends validation messages to NSToolbarItem's that are of Image type, which none of mine were. In order to validate any or all NSToolbarItems's, create a custom subclass of NSToolBar and override the validateVisibleItems: method. This will send validation messages to ALL visible NSToolbarItem's. The only real difference is that instead of having the Toolbar class enable or disable the item with the returned BOOL, you need to enable or disable the item in the validation method itself.
#interface CustomToolbar : NSToolbar
#end
#implementation CustomToolbar
-(void)validateVisibleItems
{
for (NSToolbarItem *toolbarItem in self.visibleItems)
{
NSResponder *responder = toolbarItem.view;
while ((responder = [responder nextResponder]))
{
if ([responder respondsToSelector:toolbarItem.action])
{
[responder performSelector:#selector(validateToolbarItem:) withObject:toolbarItem];
}
}
}
}
#end
Now, assume you have a controller with an IBAction method that handles Actions for a NSSegmentedControl in your toolbar:
- (IBAction)backButton:(NSSegmentedControl*)sender
{
NSInteger segment = sender.selectedSegment;
if (segment == 0)
{
// Action for first button segment
}
else if (segment == 1)
{
// Action for second button segment
}
}
Place the following in the same controller that handles the toolbar item's Action:
-(BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
{
SEL theAction = [toolbarItem action];
if (theAction == #selector(backButton:))
{
[toolbarItem setEnabled:YES];
NSSegmentedControl *backToolbarButton = (NSSegmentedControl *)toolbarItem.view;
[backToolbarButton setEnabled:YES forSegment:0];
[backToolbarButton setEnabled:NO forSegment:1];
}
return NO;
}
The result is that you have complete control over which segments are enabled or disabled.
This technique should be applicable to almost any other type of NSToolbarItem as long as the item's Received Action is being handled by a controller in the responder chain.
I hope this helps.

NSToolbar *toolbar; //Get this somewhere. If you have the window it is in, call [window toolbar];
[toolbar validateVisibleItems];

Related

NSButton with Mouse Down/Up and Key Down/Up

I need a NSButton that gives me 2 events, one when the button is pressed down (NSOnState) and one when the button is released (NSOffState) and so far i've got it working with the mouse (intercepting the mouseDown: event). But using a keyboard shortcut doesn't work, it fires a NSOnState once and then after a delay really often. Is there any way to get a button that fires NSOnState when pressed and NSOffState when released?
My current subclass of NSButton looks like this and unfortunately works using a delegate:
-(void)awakeFromNib {
[self setTarget:self];
[self setAction:#selector(buttonAction:)];
}
-(void)mouseDown:(NSEvent *)theEvent {
[_delegate button:self isPressed:YES];
[super mouseDown:theEvent];
}
-(void)buttonAction:(id)sender {
[_delegate button:self isPressed:NO];
}
The button can be set to send mouse events at both times by using -sendActionOn::
[self.button sendActionOn: NSLeftMouseDownMask | NSLeftMouseUpMask];
Handling keyboard events similarly seems more difficult. If you don't need the event exactly at the same time the highlight is removed from the button, you could override NSButton's -performKeyEquivalent: so that it will e.g. send the action twice.
- (BOOL) performKeyEquivalent: (NSEvent *) anEvent
{
if ([super performKeyEquivalent: anEvent])
{
[self sendAction: self.action to: self.target];
return YES;
}
return NO;
}
If you do need the event at the same time, I think you need to use a custom button cell (by creating a subclass of NSButtonCell and setting the button's cell in the initializer) and override its -highlight:withFrame:inView::
- (void)highlight:(BOOL)flag
withFrame:(NSRect)cellFrame
inView:(NSView *)controlView
{
[super highlight: flag withFrame:cellFrame inView:controlView];
if (flag)
{
// Action hasn't been sent yet.
}
else
{
// Action has been sent.
}
}

Prevent QLPreviewView from grabbing focus

I have a list of files. Next to it I have a QLPreviewView which shows the currently selected file.
Unfortunately QLPreviewView loads a web view to preview bookmark files. Some web pages can grab keyboard focus. E.g. the Gmail login form places the insertion point into the user name field.
This breaks the flow of my application. I want to navigate my list using arrow keys. This is disrupted when keyboard focus is taken away from the table view.
So far the best I could come up with is to override - [NSWindow makeFirstResponder:] and not call super for instances of classes named with a QL prefix. Yuck.
Is there a more reasonable way to
Prevent unwanted changes of first responder?
or prevent user interaction on QLPreviewView and its subviews?
I ended up using a NSWindow subclass that allows QLPreviewViews and its private subviews to become first responder on user interaction, but prevents these views from simply stealing focus.
- (BOOL)makeFirstResponder:(NSResponder *)aResponder
{
NSString *classname = NSStringFromClass([aResponder class]);
// This is a hack to prevent Quick Look from stealing first responder
if ([classname hasPrefix:#"QL"]) {
BOOL shouldMakeFirstRespnder = NO;
NSEvent *currentEvent = [[NSApplication sharedApplication] currentEvent] ;
NSEventType eventType = currentEvent.type;
if ((eventType == NSLeftMouseDown) || (eventType == NSRightMouseDown) || (eventType == NSMouseEntered)) {
if ([aResponder isKindOfClass:[NSView class]]) {
NSView *view = (NSView *)aResponder;
NSPoint locationInWindow = currentEvent.locationInWindow;
NSPoint locationInView = [view convertPoint:locationInWindow fromView:nil];
BOOL pointInRect = NSPointInRect(locationInView, [view bounds]);
shouldMakeFirstRespnder = pointInRect;
}
}
if (!shouldMakeFirstRespnder) {
return NO;
}
}
return [super makeFirstResponder:aResponder];
}
Maybe you can subclass QLPreviewView and override its becomeFirstResponder so that you can either enable or disable it when your application should allow it to accept focus.
Header
#interface MyQLPreviewView : QLPreviewView
#end
Implementation
#implementation
- (BOOL)becomeFirstResponder
{
return NO;
}
#end

Cocoa sheet shows up in a random place

I'm trying to create a sheet that I'm loading from a custom nib file and has it's own Window Controller. In my app delegate upon a button press, I call
- (IBAction)loginLogout:(id)sender {
if (![self isLoggedIn]) {
// need to login
LoginManager *manager = [[LoginManager alloc] initWithWindowNibName:#"LoginSheet"];
[manager presentLoginWithWindow:self.window];
}
}
Then in the window controller (the LoginManager class), I have this
- (void)presentLoginWithWindow:(NSWindow *)window {
if (!self.window) {
[NSBundle loadNibNamed:#"LoginSheet" owner:self];
}
[NSApp beginSheet:self.window modalForWindow:window modalDelegate:self didEndSelector:#selector(didEndSheet:returnCode:contextInfo:) contextInfo:nil];
}
But I end up with this.
Perhaps you left the sheet window's "Visible At Launch" option checked in Interface Builder?

NSAlert Close Alert Programmatically when using beginSheetModalForWindow:

I'm trying to close a modal alert sheet programmatically when called like this:
[alert beginSheetModalForWindow:contacts modalDelegate:self didEndSelector:#selector(myAlertEnded:code:context:) contextInfo:NULL];
I had the same issue and I solved this way.
To launch the sheet:
[myAlertSheet beginSheetModalForWindow:self.view.window modalDelegate:self didEndSelector:#selector(showAlertDidEnd: returnCode: contextInfo:) contextInfo:nil];
To programmatically close the modal sheet:
[NSApp endSheet:[myAlertSheet window]];
myAlertSheet is an NSAlert instance variable to keep track of the modal sheet on-screen. The endSheet message then calls the selector:
- (void)showAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{...}
I hope the above is useful
You'd have a method like this in your controller (the modalDelegate):
- (IBAction) cancelClicked: (id) sender {
// Cancel the sheet and close.
[NSApp endSheet: [self window]];
}
... which would be wired to a Cancel button in the modal sheet (or to an OK button for that matter but that would probably invoke some spin-off processing).
You also need to implement this didEndSelector to actually remove the sheet:
- (void) didEndSheet: (id) modalSheet returnCode: (NSInteger) returnCode contextInfo: (void*) contextInfo {
// Remove the sheet.
[modalSheet orderOut: nil];
}
If I remember correctly I scooped this from an example in the Apple docs.
In NSWindow you'll find a method called attachedSheet. It yields a reference to a sheet attached to this window, if any. The sheet itself is also simply a NSWindow. Therefore, you might want to try this:
NSWindow *window = [NSApp mainWindow];
[[window attachedSheet] close];
The way I found it we need to both endSheet(alert.window) and alert.window.close().
let hostWindow = contacts
// Dismiss the sheet. Yet its window will stay on screen.
// .endSheet(:) will yield NSApplication.ModalResponse.stop
hostWindow.endSheet(alert.window)
// Close the alert window.
alert.window.close()
OR
let hostWindow = contacts
alert.beginSheetModal(for: hostWindow) { response in
switch response {
case .stop: alert.window.close()
default: fatalError("TODO")
}
}
// Later... Dismiss the sheet.
// .endSheet(:) will yield NSApplication.ModalResponse.stop
hostWindow.endSheet(alert.window)
Using NSAlert.runModal() we need instead:
let response = alert.runModal()
switch response {
case .stop: break
default: fatalError("TODO")
}
// Later... Dismiss the NSApp.modalWindow
// stopModal() will yield NSApplication.ModalResponse.stop
NSApp.stopModal()

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

Resources