Is there a way to implement a popup menu from NSToolBarItem? - cocoa

I am trying to implement a pop-up menu (something that can be seen when in Chrome I press right mouse button when the cursor is over the left arrow).
I have a class derived from NSToolBarItem and I have another class derived from NSToolBar. In the toolbar I call setAllowsUserCustomization. So my right click anywhere on the toolbar brings up the customization menu for the toolbar.
Thank you for any pointer you can give.

You don't need to subclass NSToolbarItem. Just give one toolbar item its own view (in code or in IB). In that view, you can use a standard control like NSPopUpButton, or a custom view with whatever event handling logic you like.

If you want your NSToolbarItem custom views to receive mouseDown events, you can follow this pattern: use a custom NSWindow subclass (or swizzle the -[NSWindow hitTest:] method) and forward events to your views yourself.
// MyWindow.h
#interface MyWindow : NSWindow
#end
#interface NSView (MyWindow)
- (BOOL)interceptsToolbarRightMouseDownEvents;
#end
// MyWindow.m
#implementation NSView (MyWindow)
- (BOOL)interceptsToolbarRightMouseDownEvents { // overload in your custom toolbar item view return YES
return NO;
}
#end
#interface NSToolbarView : NSView /* this class is hidden in AppKit */ #end
#implementation MyWindow
- (void)sendEvent:(NSEvent*)event {
if (event.type == NSRightMouseDown) {
NSView* frameView = [self.contentView superview];
NSView* view = [frameView hitTest:[frameView convertPoint:event.locationInWindow fromView:nil]];
if ([view isKindOfClass:NSToolbarView.class])
for (NSView* subview in view.subviews) {
NSView* view = [subview hitTest:[subview convertPoint:event.locationInWindow fromView:nil]];
if (view.interceptsToolbarRightMouseDownEvents)
return [view rightMouseDown:event];
}
}
[super sendEvent:event];
}
#end

Related

"Bottom NSView's" button border line still appears in the "top NSView" after top View is presented

I'm working on a "Mac OSX App" that has two view controllers, the app starts from the first view (bottom view) and later "Canvas view controller (top view)" is presented from the first view controller.
After presenting "CanvasViewController (top view)",
CanvasView appears correctly with its buttons on the top of bottom view however the border line of the button of the bottom view still appears. I have attached the images of the bottom and top views below.
It doesn't look like alpha value issue as it is set to 1.0, instead it is related to some things else in the viewController settings (ie. first responder or button state, etc).
How can I remove this bottom view's button border line when the top view is presented on the top?
Any solutions or helpful comments will be much appreciated. Thanks in advance.
// From bottom view's viewController
#interface MainMenuViewController : NSViewController
#end
#implementation MainMenuViewController
...
-(IBAction)presentNextViewWithAnimator:(id)sender{
id animator = [[MyCustomAnimator alloc] init];
CanvasViewController* viewController = [[CanvasViewController alloc] initWithNibName:#"CanvasViewController" bundle:nil];
// presenting "topView" with custom animator
[self presentViewController:viewController animator:animator];
}
...
#end
// From "top view's viewController"
#interface CanvasViewController : NSViewController
#end
#implementation CanvasViewController
...
// "close" button
-(IBAction)closeCanvas:(id)sender{
[self dismissViewController:self];
}
// "refresh" button
-(IBAction)refreshCanvas:(id)sender{
}
...
#end
// From the view presentation's custom animator
#interface MyCustomAnimator : NSObject <NSViewControllerPresentationAnimator>
#end
#implementation MyCustomAnimator
-(void) animatePresentationOfViewController:(NSViewController *)viewController
fromViewController:(NSViewController *)fromViewController
{
NSViewController* bottomVC = fromViewController;
NSViewController* topVC = viewController;
topVC.view.wantsLayer = YES;
topVC.view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
topVC.view.alphaValue = 1.0f;
[bottomVC.view addSubview:topVC.view];
CGRect frame = NSRectToCGRect(bottomVC.view.frame);
[topVC.view setFrame:NSRectFromCGRect(frame)];
topVC.view.layer.backgroundColor = [NSColor whiteColor].CGColor;
[NSAnimationContext
runAnimationGroup:^(NSAnimationContext * context){
context.duration = 0.5f;
topVC.view.animator.alphaValue = 1.0f;
} completionHandler:nil];
}
...
#end

How do I validate an NSButton in an NSToolbar?

I have a document-based app with a tool bar containing several NSButton which I need to validate. Base on other code here, I have subclassed NSToolbar:
#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
MyDocument (the File's owner) is set as the delegate of the toolbar. However
-(BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
is never called. The buttons have an action set on them, so not sure why [responder respondsToSelector:toolbarItem.action] is always false.
I have tried subclassing the NSButton items:
#interface DocumentToolbarActionItem : NSToolbarItem
#implementation DocumentToolbarActionItem
-(void)validate
{
Document* document = [[self toolbar] delegate];
[self setEnabled:[document validateUserInterfaceItem:self]];
}
#end
But this results in an endless loop.
The document's validateUserInterfaceItem: method works for all other items in the app and I need to have my toolbar button call it to determine if they should be enabled or not.
My guess is that you're not calling through [super validateVisibleItems] and, so, losing the superclass behaviour of validation through the responder chain.

cocoa: how to create a custom view by IB?

I want to create a custom view, say MyView, which is only containing a button and print out "hello my view" when I click it. When next time I want to use it, I just need to add a custom view to window from IB, add MyView.m into project and set the class of custom view to MyView.
My question is :
I want to use IB to set the appearance of MyView, but I don't know how to get the corresponding view file : MyView.m in order to use the MyView.m file next time.
You need to create the subclassed view using code; for example:
MyView.h:
#import <Cocoa/Cocoa.h>
#implementation MyView : NSView
{
IBOutlet NSTextField *_label;
}
- (IBAction)buttonPressed:(id)sender;
#end
MyView.m:
#import "MyView.h"
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
// Init here
}
return self;
}
- (void)awakeFromNib:
{
// Init here
}
- (IBAction)buttonPressed:(id)sender
{
[_label setStringValue:#"hello my view"];
}
#end
You need to create your custom view layout using IB and change the owning class from NSView to MyView (using the 3rd tab if I recall).
Then connect the _label from the owning object on the left pane (Ctrl-drag) and connect the button action to the buttonPressed: method (Ctrl-drag).

How to use Storyboard to make popover that can be used in code?

I'm building a collection of forms each of which contains several fields. Some of the fields are UITextFields that will display a date. I've created a new class called DatePickerTextField, a descendant of UITextField. When a DatePickerTextField is tapped I'd like for a UIDatePicker control to appear in a popover.
My question is how do I use the storyboard to implement the popover? I can do a segue when there is a specific, visible control in the scene. But how do I represent a generic popover in the scene that I can attach to any instantiated DatePickerTextField that becomes active?
You can create segue that is not connected to any control but I don't think that there would be way to specify anchor point for popover from code. Another option is to create ViewController that is not connected with any segue. When editing storyboard, create ViewController which will be placed in popover, select it and navigate to Utilities pane->Attributes Inspector. Set Size to Freeform, Status Bar to None, specify unique Identifier that will be used to instantiate ViewController from code. Now you can change the size of ViewController by selecting its View and navigating to Utilities pane->Size Inspector.
After that you can create popover from code:
- (IBAction)buttonPressed:(id)sender {
UIView *anchor = sender;
UIViewController *viewControllerForPopover =
[self.storyboard instantiateViewControllerWithIdentifier:#"yourIdentifier"];
popover = [[UIPopoverController alloc]
initWithContentViewController:viewControllerForPopover];
[popover presentPopoverFromRect:anchor.frame
inView:anchor.superview
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
One caveat is that you need to hold reference to popover as ivar of your class, otherwise it'll crash because UIPopoverController would be released and deallocated after buttonPressed returns:
#interface MyViewController : UIViewController {
// ...
UIPopoverController *popover;
// ...
}
So, I had a similar issue, and in case others might benefit, I figured I'd share it, since I benefit so much from stackoverflow.
This solution allows you to set the anchor of a customizable popover segue. It also allows you to configure the segue to be modal or not (I could not find a way to prevent the segue by dimming the exterior context, so if someone knows how to do that, I would be interested in hearing it); this is accomplished by setting the passthrough views for the popover controller. I also added the capacity to specify a custom view, rather than the view of the source viewcontroller (since I needed this capacity); this portion is not critical to the solution.
DynamicPopoverSegue.h
#interface DynamicPopoverSegue : UIStoryboardPopoverSegue
#property BOOL isModal;
#property UIView* sourceView;
#property CGRect anchor;
#end
DynamicPopoverSegue.m
#implementation DynamicPopoverSegue
- (void)perform
{
if (!self.popoverController.popoverVisible)
{
UIViewController* dst = (UIViewController*)self.destinationViewController;
UIViewController* src = (UIViewController*)self.sourceViewController;
UIView* inView = _sourceView ? _sourceView : src.view;
self.popoverController.contentViewController = dst;
if (!_isModal)
{
[self.popoverController setPassthroughViews:[[NSArray alloc] initWithObjects:inView, nil]];
}
[self.popoverController presentPopoverFromRect:_anchor
inView:inView
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
}
#end
Then you just set your segue to "Custom" in the storyboard, and set the segue class to "DynamicPopoverSegue". In my case, since I wanted to associate it with dynamic layers in a view, I could not set the anchor, so I created the segue by control clicking from the view controller icon in the bar beneath my view controller to the view controller I was using to present the popupover.
To call the popover segue:
[self performSegueWithIdentifier:#"MyPopoverSegue" sender:self];
And to configure the popover segue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"MyPopoverSegue"])
{
DynamicPopoverSegue* popoverSegue = (DynamicPopoverSegue*)segue;
// set the anchor to wherever you want it to be
popoverSegue.anchor = _destinationFrame;
}
}
- (IBAction)pressItemChooseOprateRoom:(id)sender {
if (isPad){
// UIView *anchor = sender;
UIViewController *viewControllerForPopover =
[self.storyboard instantiateViewControllerWithIdentifier:#"OperateRoomList"];
_myPopover = [[UIPopoverController alloc]
initWithContentViewController:viewControllerForPopover];
CGRect rc=[self getBarItemRc:sender];
[_myPopover presentPopoverFromRect:rc
inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
[MLControl shared].popover =self;
// [self perfformSegueWithIdentifier:SEGUE_POP_OPERATEROOM sender:self];
}else{
[self iphoneOpenOperateRoomList];
/* [self performSegueWithIdentifier:#"iPhonePushOperateRoom" sender:self];
*/
}
}
-(void)iphoneOpenOperateRoomList{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"OperateRoomList"];
// if (!index.showTabBar) {
// vc.hidesBottomBarWhenPushed = YES;
// }
[self.navigationController pushViewController:vc animated:YES];
}
Just used the answer from Jonnywho for my SWIFT project. In case you need it:
Here's the SWIFT version:
let anchor: UIView = sender
var viewControllerForPopover = self.storyboard?.instantiateViewControllerWithIdentifier("GameAboutViewController") as! UIViewController?
let popover = UIPopoverController(contentViewController: viewControllerForPopover!)
popover.presentPopoverFromRect(anchor.frame, inView: anchor, permittedArrowDirections: UIPopoverArrowDirection.Any, animated: true)
Add a UIView in the scene dock.
You can add it as a subview to any existing view on the view controller.
You can then toggle it's isHidden property as you require.
You can add multiple such subviews and create many such popups.
This technique will save you from setting up a new View Controller and using segues.

Custom Sheet : can't click buttons

I used this source http://www.cats.rwth-aachen.de/library/programming/cocoa
to create my custom sheet.
I created a NSPanel object in existing .xib file and connected with IBOutlet
My source code:
.h
#interface MainDreamer : NSWindow <NSWindowDelegate>
{
...
NSPanel *newPanel;
}
...
#property (assign) IBOutlet NSPanel *newPanel;
.m
#dynamic newPanel;
...
//this method is wired with button on main window and calls a sheet
- (IBAction)callPanel:(id)sender
{
[NSApp beginSheet:newPanel
modalForWindow:[self window] modalDelegate:self
didEndSelector:#selector(myPanelDidEnd:returnCode:contextInfo:)
contextInfo: nil]; //(__bridge void *)[NSNumber numberWithFloat: 0]
}
//this method is wired with cancel and ok buttons on the panel
- (IBAction)endWorkPanel:(id)sender
{
[newPanel orderOut:self];
[NSApp endSheet:newPanel returnCode:([sender tag] == 9) ? NSOKButton : NSCancelButton];
}
//closing a sheet
- (void)myPanelDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == NSCancelButton) return;
else{
return;
}
}
So callPanel works fine, sheet appears but I can't interact with controls on the sheet (with buttons). They don't react on a click (even visually).
Where the problem lies?
Heh, I forgot about
[newDreamPanel close];
in applicationDidFinishLaunching method. I wrote it because I wanted the panel to not appear when main window launches.
In fact, the Visible At Launch panel's property should be activated in IB. The close method works too, but side effect is that all controls become unable on the panel.

Resources