For a personal project, I'm currently trying to replicate the visual stylings of the toolbar in Automator for OS X. I have tried just about everything to get my NSButtons inside of the NSToolbar to look visually similar, but can't seem to figure out the delicate UI components to figure it out, so I'm turning to the brilliant minds on Stack Overflow.
What I'm trying to do: I'm trying to copy the visual stylings of the Automator toolbar buttons:
The Setup: Currently I have tiff images for active button state, inactive button state, and pressed button state. I want to use these images as the background for the NSButtonCell. Right now, I've subclassed NSButtonCell (code below), and set the NSButtonCell class to be TFToolbarButtonCell in the XIB file in Interface Builder. In the subclass of NSButtonCell, I'm overriding -drawWithFrame:inView to draw the appropriate state image in the frame; I'm also overriding -highlight:withFrame:inView to draw the pressed image when the button is clicked.
Any direction into what I might be doing wrong here would be greatly appreciated!
TFToolbarButtonCell.h
#import <Cocoa/Cocoa.h>
#interface TFToolbarButtonCell : NSButtonCell
#property (nonatomic, strong) NSImage *onImage;
#property (nonatomic, strong) NSImage *offImage;
#property (nonatomic, strong) NSImage *highlightImage;
#end
TFToolbarButtonCell.m
#import "TFToolbarButtonCell.h"
#implementation TFToolbarButtonCell
- (id)init
{
self = [super init];
if(self) {
//initialize here
}
return self;
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
if([self state]){
[self.onImage drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
} else {
[self.offImage drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
}
- (void)highlight:(BOOL)flag withFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
if(flag){
[self.highlightImage drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
}
#end
I think you can accomplish what you want by subclassing NSButton and implementing
- (void)mouseDown:(NSEvent *)theEvent
- (void)mouseUp:(NSEvent *)theEvent
and your own method to set the inactive state.
These methods would call
- (void)setImage:(NSImage *)anImage
To change from active to pressed to inactive states using the different images.
You also have to uncheck "Bordered" in Interface Builder on your NSButton to stop the button background from showing.
Also, calling
- (void)setEnabled:(BOOL)enabled
on the NSToolbarItem will change the palette label to active/inactive (grey the text below the button).
Related
I'm trying to make a configure sheet appear for a ScreenSaverView subclass. After a long battle with Xcode, I'm finally getting the configure sheet to appear when "Screen Saver Options" is clicked in System Preferences (and my screen saver is selected), and the sheet behaves normally with one exception: the background is black, rendering text invisible (see image).
This occurs regardless of whether the sheet is an NSPanel or NSWindow class, and whether the panel is a Regular Panel, Utility Panel, or HUD Panel. The only thing I can seem to do is change is the alpha value of the panel, which as expected makes everything more transparent (but the text is still not visible). Interestingly, calling setOpaque or setBackgroundColor on the NSPanel or NSWindow don't seem to have any effect.
To figure out why it's showing up as black, we'd really need to see code for how you're creating the window.
I just did a quick test project and it seems to work OK here. IMO, the easiest solution for creating the window to return in the configureSheet method is to use an NSWindowController subclass to load a nib file in which you've configured the window ahead of time.
So in your ScreenSaverView subclass, you'd define an interface something like the following:
#interface MDScreenSaverFinaglerView : ScreenSaverView {
MDScreenSaverOptionsWindowController *optionsWindowController;
NSInteger screenSaverViewMode;
}
#property (nonatomic, retain) MDScreenSaverOptionsWindowController
*optionsWindowController;
#property (nonatomic, assign) NSInteger screenSaverViewMode;
#end
Your implementation would then look like this for the configureSheet method:
- (NSWindow *)configureSheet {
if (optionsWindowController == nil) {
optionsWindowController = [[MDScreenSaverOptionsWindowController alloc]
initWithScreenSaverView:self];
}
return optionsWindowController.window;
}
Basically, you check to see if the optionsWindowController instance exists, creating it if necessary, then return its window.
The interface for the custom NSWindowController subclass would look like the following:
#interface MDScreenSaverOptionsWindowController : NSWindowController {
IBOutlet NSMatrix *optionsMatrix;
MDScreenSaverFinaglerView *screenSaverView; // non-retained/weak reference
}
- (id)initWithScreenSaverView:(MDScreenSaverFinaglerView *)aView;
#property (nonatomic, assign) MDScreenSaverFinaglerView *screenSaverView;
- (IBAction)ok:(id)sender;
#end
There's a screenSaverView property which will allow communication back with the ScreenSaverView subclass once the user has clicked the OK button.
The nib file for the MDScreenSaverOptionsWindowController class (named "MDScreenSaverOptionsWindowController.xib") is set up like shown below:
The implementation of the MDScreenSaverOptionsWindowController looks like the following:
#implementation MDScreenSaverOptionsWindowController
#synthesize screenSaverView;
- (id)initWithScreenSaverView:(MDScreenSaverFinaglerView *)aView {
NSParameterAssert(aView != nil);
if ((self = [super initWithWindowNibName:NSStringFromClass([self class])])) {
self.screenSaverView = aView;
}
return self;
}
- (void)windowDidLoad {
[super windowDidLoad];
[optionsMatrix selectCellWithTag:screenSaverView.screenSaverViewMode];
}
- (IBAction)ok:(id)sender {
NSInteger viewMode = [optionsMatrix selectedTag];
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber
numberWithInteger:viewMode] forKey:MDScreenSaverViewModeKey];
screenSaverView.screenSaverViewMode = viewMode;
[NSApp endSheet:self.window];
}
#end
The end result:
Sample project: ScreenSaverFinagler.zip
I'm writing an iPad app and one of my screens has lots of small buttons that when pressed will display one sentence of text in a popover originating from that button. Currently all popovers are created using the storyboard and I store the popover controller in my UIViewController as such:
#property (nonatomic, strong) UIPopoverController *myPopoverController;
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue isKindOfClass:[UIStoryboardPopoverSegue class]])
{
UIStoryboardPopoverSegue *popoverSegue = (UIStoryboardPopoverSegue *)segue;
self.myPopoverController = popoverSegue.popoverController;
}
}
However, I can't figure out a good way to deal with rotation. Right my didRotate method looks like so:
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
if (self.myPopoverController)
{
[self.myPopoverController dismissPopoverAnimated: NO];
[self.myPopoverController presentPopoverFromRect:?????? inView:self.view permittedArrowDirections:UIPopoverArrowDirectionDown animated:NO];
}
}
However, I don't know where to present the popovers from given that they could have originated from any of the small buttons on my screen. Any suggestions? Remember that these are VERY simple popovers, thus a whole bunch of new code is not ideal.
Your best bet may be to make another property in your main view controller that keeps a reference to the button pressed. Something like:
#property (nonatomic, strong) UIPopoverController *myPopoverController;
#property (nonatomic, weak) UIView *popoverButton;
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue isKindOfClass:[UIStoryboardPopoverSegue class]])
{
UIStoryboardPopoverSegue *popoverSegue = (UIStoryboardPopoverSegue *)segue;
self.myPopoverController = popoverSegue.popoverController;
//The sender in prepareForSegue should be the view used to initiate the segue.
popoverButton = (UIView *)sender;
}
}
That done, you can modify your rotation code thusly:
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
if (self.myPopoverController)
{
[self.myPopoverController dismissPopoverAnimated: NO];
[self.myPopoverController presentPopoverFromRect:popoverButton.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionDown animated:NO];
}
}
Keeping a reference to the pressed button takes up no more resources that storing a pointer, and keeping the reference weak should avoid retain cycles (after all, your view controller does not own the button, the button's superview owns it).
IS it possible to cut out parts of a NSWindow or an NSView and make them see through? I have an NSWindow with an NSView and I want to either:
A) make a hole in the NSWindow to be able to see through it or
B) set my NSWindow background to have a clear color and then make an NSView on top and set a certain part of my NSViews opacity to be able to see through to the desktop.
This is the effect I am trying to create:
Yes, it's possible, and actually not that hard.
In your window subclass, you need to set the window background color to transparent
self.backgroundColor = NSColor.clearColor;
and tell the compositing engine that parts of your window are transparent and need to be redrawn when the window moves
[self setOpaque:NO];
Setting the background color was not necessary in early versions of macOS and many answers still do not mention that fact. I've verified that at least since macOS 10.11 it is necessary.
In your NSView subclass, you must render the new background with a color of your choice (otherwise the window is entirely transparent and only the title bar will show) and then render a hole in the view with
NSRectFillUsingOperation(NSMakeRect(50, 50, 100, 100), NSCompositingOperationClear);
This gives the desired effect and also works in Mojave's dark mode etc.
Full code:
#interface MyWindow : NSWindow
- (id)initWithContentRect:(NSRect)contentRect styleMask:( unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag;
#end
#implementation MyWindow
- (id)initWithContentRect:(NSRect)contentRect styleMask:( unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag {
self = [super initWithContentRect:contentRect styleMask : aStyle backing :bufferingType defer:flag ];
if (self)
{
self.backgroundColor = NSColor.clearColor;
[self setOpaque:NO];
[self setHasShadow:NO];
}
return self;
}
#end
#interface MyView : NSView
- (void)drawRect:(NSRect)rect;
#end
#implementation MyView
- (void)drawRect:(NSRect)rect
{
[[NSColor windowBackgroundColor] set];
NSRectFill(self.bounds);
NSRectFillUsingOperation(NSMakeRect(50, 50, 100, 100), NSCompositingOperationClear);
}
#end
Is there any way to color NSPopover? Ive seen apps like facetab etc that have cool colors and resizeable popovers, how is this done?
Ay guides, hints?
Thanks.
Set popover.contentViewController.view as a subclass of NSView with a custom background drawing (i.e. override drawRect: and fill a rect with your custom background color).
Then set the popover.appearance = NSPopoverAppearanceHUD to remove the default border around the view.
Note that there will still be a very thin border around the view, so if you want to remove it completely, you may want to use MAAttachedWindow or a similar solution.
In Swift 4:
Go to File->New File->Cocoa Class
Name your class. eg. PopColor. Make sure it is a subclass of NSView
Set the contents of the file to:
import Cocoa
class PopoverContentView:NSView {
var backgroundView:PopoverBackgroundView?
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
if let frameView = self.window?.contentView?.superview {
if backgroundView == nil {
backgroundView = PopoverBackgroundView(frame: frameView.bounds)
backgroundView!.autoresizingMask = NSView.AutoresizingMask([.width, .height]);
frameView.addSubview(backgroundView!, positioned: NSWindow.OrderingMode.below, relativeTo: frameView)
}
}
}
}
class PopoverBackgroundView:NSView {
override func draw(_ dirtyRect: NSRect) {
NSColor.green.set()
self.bounds.fill()
}
}
In your storyboard, select the view which has your popover content and go to the Identity Inspector
Set the Class to PopoverContentView
Your popover and its triangle will now be green.
You can use MAAttachedWindow instead.
You can subclass NSView and set it as the NSPopover's view controller's view.
Yes and no. Unfortunately NSPopover isn't designed to be customisable. You can use some simple hacks for adding additional background view behind contentViewController's view and colorise or customise it as you want. In this case, you can get the customisable background that will be masked the same as generic NSPopover border and tail.
For more details you can take a look at the code of NSPopover+MISSINGBackgroundView category that implements this approach or just use this piece of code as CocoaPod library.
The complete code to change the color of NSPopover including the triangle is here:
I assume people have hooked the popover outlets and methods
#import "AppDelegate.h"
#interface MyPopoverBackgroundView : NSView
#end
#implementation MyPopoverBackgroundView
-(void)drawRect:(NSRect)dirtyRect
{
[[NSColor redColor] set];
NSRectFill(self.bounds);
}
#end
//===============================================================================================
#interface MyPopView : NSView
#end
#implementation MyPopView
-(void)viewDidMoveToWindow{
NSView *aFrameView = [[self.window contentView] superview];
MyPopoverBackgroundView * aBGView =[[MyPopoverBackgroundView alloc] initWithFrame:aFrameView.bounds];
aBGView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[aFrameView addSubview:aBGView positioned:NSWindowBelow relativeTo:aFrameView];
[super viewDidMoveToWindow];
}
#end
//-----------------------------------------------------------------------------------------
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
//close when clicked outside
[self.popover setBehavior:NSPopoverBehaviorTransient];
//change its color
MyPopView *myPopview = [MyPopView new];
[self.popover.contentViewController.view addSubview:myPopview];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (IBAction)closePopover:(id)sender {
[self.popover close];
}
- (IBAction)showPopover:(id)sender {
[self.popover showRelativeToRect:[sender bounds]
ofView:sender
preferredEdge:NSMaxYEdge];
}
#end
This is what I did to change the popover color.
Assuming that you have properly defined your NSPopover:
//AppController.h
#import <Foundation/Foundation.h>
#interface AppController : NSObject
#property (weak) IBOutlet NSPopover errorPopover;
// whatever else you have ...
#end
//AppController.m
#import "AppController.h"
#implementation AppController
#synthesize errorPopover = _errorPopover;
// whatever else you have ...
-(IBAction)doSomethingThatCallsPopover:(id)sender {
_errorPopover.appearance = NSPopoverAppearanceHUD; //set color of error popup
[[self errorPopover] showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxXEdge];
}
#end
NSPopover Class Reference - I really wish they would provide usage code in the developer docs.
I've only programmed on the iPhone so far, so Cocoa is sort of confusing in certain ways for me. Here's where I've hit a snag. I wanted my window so that the background was invisible, and without a title-bar. Something like this:
Here's how I'm doing it:
I set my window's class to a custom window, which I've created like this:
CustomWindow.h
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#interface CustomWindow : NSWindow {
#private
NSPoint initialLocation;
}
#property(assign)NSPoint initialLocation;
#end
CustomWindow.m
//trimmed to show important part
#import "CustomWindow.h"
#implementation CustomWindow
#synthesize initialLocation;
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag {
// Removes the window title bar
self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
if (self != nil) {
[self setAlphaValue:1.0];
[self setOpaque:NO];
}
return self;
}
#end
Now, in my .xib file for this window I've added a custom view onto the window. I've set the view class to a custom class I've created that inherits from NSView. Here's how I'm setting that up:
MainView.h
#import <Cocoa/Cocoa.h>
#interface MainView : NSView {
#private
//nothing to see here, add later
}
#end
MainView.m
//trimmed greatly again to show important part
#import "MainView.h"
#implementation MainView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(NSRect)rect {
// Clear the drawing rect.
[[NSColor clearColor] set];
NSRectFill([self frame]);
}
#end
So here's my question. I've added a NSImageView to my custom view (MainView) in Interface Builder. However, for some reason I can't figure out how to connect this image view to an instance variable in my custom view. They seem like they can't be connected like I normally would if I was creating an iPhone app. Any ideas how this would be done?
You connect objects created in your XIB in Mac OS X the same way you do for iOS programs. Just add an NSImageView property to your main view, mark it as an IBOutlet and connect it up.
For example,
In MainView.h create a property for your NSImageView and make it an IBOutlet:
#import <Cocoa/Cocoa.h>
#interface MainView : NSView {
NSImageView *imageView;
}
#property(retain) IBOutlet NSImageView *imageView;
#end
In interface builder, make sure the class for the custom view is set to MainView, to do this click on the File's Owner object in the custom view XIB and then select the identity option in the inspector and enter MainView as the class type.
Next, CTRL+click File's owner and drag the arrow to the NSImageView and select the imageView outlet.
That's all there is to it. You should be able to reference the image view from code now.