How can I set [NSTextView selectedTextAttributes] on a background window? - objective-c

The default value for [NSTextView selectedTextAttributes] is unusable in my app, because i allow the user to select colors (syntax highlighting) that are almost exactly the same as the background color.
I have written some math to determine a suitable color and can use this to set it:
textView.selectedTextAttributes = #{
NSBackgroundColorAttributeName: [NSColor yellowColor],
NSForegroundColorAttributeName: [NSColor redColor]
};
But when the window is in the background, it still uses the system default light grey.
I've attached screenshots of the above code with active vs inactive window. — how can I change the selected text background colour of the inactive window?

You can override the colour by overriding drawing method of NSLayoutManager.
final class LayoutManager1: NSLayoutManager {
override func fillBackgroundRectArray(rectArray: UnsafePointer<NSRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: NSColor) {
let color1 = color == NSColor.secondarySelectedControlColor() ? NSColor.redColor() : color
color1.setFill()
super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color1)
color.setFill()
}
}
And replace NSTextView's layout manager to it.
textView.textContainer!.replaceLayoutManager(layoutManager1)
Here's full working example.
As #Kyle asks for reason of setFill, I add some update.
From Apple manual:
... the charRange and color parameters are passed in merely for informational purposes; the color is
already set in the graphics state. If for any reason you modify it, you must restore it before
returning from this method. ...
Which means passing-in other color into super call has no effect, and you just need to
NSColor.setFill to make it work with super call.
Also, the manual requires to set it back to original one.

It's not when the window is in the background it's when the NSTextView is not selected. I don't think you can change that behavior.
You could create an attributed string and add the NSBackgroundColorAttributeName attribute to the range of the selected text when it loses focus. The attributed string stays the same color even when the focus is lost.
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:#"hello world"];
[string addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:NSMakeRange(1, 7)];
[string addAttribute:NSBackgroundColorAttributeName value:[NSColor yellowColor] range:NSMakeRange(1, 7)];
[self.myTextView insertText:string];
EDIT by Abhi Beckert: this is how I implemented this answer (note I also had to disable the built in selected text attributes, or else they override the ones I'm setting):
#implementation MyTextView
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (!(self = [super initWithCoder:aDecoder]))
return nil;
// disable built in selected text attributes
self.selectedTextAttributes = #{};
return self;
}
- (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)container
{
if (!(self = [super initWithFrame:frameRect textContainer:container]))
return nil;
// disable built in selected text attributes
self.selectedTextAttributes = #{};
return self;
}
- (void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag
{
// remove from old ranges
for (NSValue *value in self.selectedRanges) {
if (value.rangeValue.length == 0)
continue;
[self.textStorage removeAttribute:NSBackgroundColorAttributeName range:value.rangeValue];
}
// apply to new ranges
for (NSValue *value in ranges) {
if (value.rangeValue.length == 0)
continue;
[self.textStorage addAttribute:NSBackgroundColorAttributeName value:[NSColor yellowColor] range:value.rangeValue];
}
[super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag];
}
#end

You can specify that your NSTextView should be treated as first responder by overriding layoutManagerOwnsFirstResponder(in:) from NSLayoutManager and the selection will use your defined attributes.
In Swift 5.1 would be:
override func layoutManagerOwnsFirstResponder(in window: NSWindow) -> Bool {
true
}

Related

How to check if the Button Shapes setting is enabled?

iOS 7.1 includes a new Accessibility setting calls Button Shapes that causes some button text to be automatically underlined. Is there a way to detect this mode, or customize it for individual UIButtons?
(This to allow changing button labels such as a dash or underscore so that when underlined, they don't look like an equals sign, etc.)
As of iOS 14, you can use UIAccessibility.buttonShapesEnabled or UIAccessibilityButtonShapesEnabled(), which will be true when the setting is enabled.
Old question, but hopefully this helps someone. There's still no built-in method for checking if Button Shapes is enabled on iOS, so we added this:
#pragma mark - Accessibility
/**
* There's currently no built-in way to ascertain whether button shapes is enabled.
* But we want to manually add shapes to certain buttons when it is.
*/
static BOOL _accessibilityButtonShapesEnabled = NO;
+ (BOOL)accessibilityButtonShapesEnabled {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self checkIfButtonShapesEnabled];
});
return _accessibilityButtonShapesEnabled;
}
+ (void)checkIfButtonShapesEnabled {
UIButton *testButton = [[UIButton alloc] init];
[testButton setTitle:#"Button Shapes" forState:UIControlStateNormal];
_accessibilityButtonShapesEnabled = (BOOL)[(NSDictionary *)[testButton.titleLabel.attributedText attributesAtIndex:0 effectiveRange:nil] valueForKey:NSUnderlineStyleAttributeName];
}
Because there's also no notification if Button Shapes is disabled/enabled whilst the app is running, we run checkIfButtonShapesEnabled in applicationDidBecomeActive:, and push our own notification if the value has changed. This should work in all cases, because it is not currently possible to add the Button Shapes toggle to the "Accessibility Shortcut".
It looks like you can request the button label's attributes and test to see if it contains an NSUnderlineStyleAttributeName attribute. If you remove the NSUnderlineStyleAttributeName attribute the system will put it right back so it seems the trick is to explicitly set the label's underline attribute to 0. I've added the following to my custom button:
- (void) adjustLabelProperties // override underline attribute
{
NSMutableAttributedString *attributedText = [self.titleLabel.attributedText mutableCopy];
[attributedText addAttribute: NSUnderlineStyleAttributeName value: #(0) range: NSMakeRange(0, [attributedText length])];
self.titleLabel.attributedText = attributedText;
}
I know it's an old question but this code works. Tested in iOS 9.3
NSMutableAttributedString *attrStr = [btn.titleLabel.attributedText mutableCopy];
[attrStr enumerateAttributesInRange:NSMakeRange(0, [attrStr length])
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes];
if([mutableAttributes objectForKey:NSUnderlineStyleAttributeName] != nil) {
//It's enabled for this button
}
}];
To disable button shapes for a specific button
[btn.titleLabel.attributedText addAttribute: NSUnderlineStyleAttributeName value: #(0) range: NSMakeRange(0, [attributedText length])];
I converted the code from this post to Swift (4.2):
import UIKit
public extension UIAccessibility {
public static var isButtonShapesEnabled: Bool {
let button = UIButton()
button.setTitle("Button Shapes", for: .normal)
return button.titleLabel?.attributedText?.attribute(NSAttributedString.Key.underlineStyle, at: 0, effectiveRange: nil) != nil
}
}
Usage:
if UIAccessibility.isButtonShapesEnabled {
// Apply button shapes style to custom button...
}
Tested and working in iOS 12.
Originally posted at my own question: Click
I had the same problem and I found no official solution. So the only workaround that I found until Apple releases a solution is to render a UIToolbar into an image and check if the button is underlined:
+ (BOOL)isUsesButtonShapes {
BOOL result = FALSE;
CGRect rect = CGRectMake(0, 0, 320, 44);
CGPoint point = CGPointMake(26, 33);
UIToolbar *toolbar = [[[UIToolbar alloc] initWithFrame:rect] autorelease];
toolbar.backgroundColor = [UIColor whiteColor];
toolbar.tintColor = [UIColor darkGrayColor];
toolbar.barTintColor = [UIColor whiteColor];
[toolbar setItems:#[[[[UIBarButtonItem alloc] initWithTitle:#"Test" style:UIBarButtonItemStyleBordered target:nil action:nil] autorelease]]];
toolbar.barStyle = UIBarStyleDefault;
toolbar.translucent = FALSE;
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[toolbar.layer renderInContext:context];
int bpr = CGBitmapContextGetBytesPerRow(context);
unsigned char *data = CGBitmapContextGetData(context);
if (data != NULL) {
int offset = (int) (bpr * point.y + 4 * point.x);
int blue = data[offset + 0];
result = blue < 250;
}
UIGraphicsEndImageContext();
return result;
}
It basically just renders the UIToolbar into an image:
Then it checks if there is an underline in the pixel under the "T". I know that this can easily break if Apple changes the way how the UIToolbar is rendered. But maybe this method can be improved and is at least better than nothing? Sorry, it isn't a good solution but I didn't find anything better yet.
This is only semi related, but I kind of "roll my own" for Button shapes and make the option available to the user via a settings menu.
I apologize for not being "spot on" regarding the question, but this is what I ended up with by thinking about the same question.
(The example is set up to always use a semi-circle for rounded corners regardless the size - please modify as you wish).
-(void)setBorderForButton:(UIButton*)theButton withSetting:(BOOL)theSetting{
if (theSetting == YES){
theButton.layer.cornerRadius = theButton.frame.size.height/2;
theButton.layer.borderWidth = 1;
theButton.layer.borderColor = [UIColor yourDesiredColor].CGColor;
theButton.clipsToBounds = YES;
}
}

Disabling NSView fade animation for NSView `setHidden:`

I am working on a project that has the concept of draggable controls, everything is working fine except that NSView seems to employ a fade in/out animation when calling setHidden:.
I have been able to work around the problem by changing the line session.animatesToStartingPositionsOnCancelOrFail = YES; to NO and implementing the image snapback myself with a custom animated NSWindow subclass. it looks great, but I know there must be an easier way.
I have tried:
using NSAnimationContext grouping with duration of 0 around the setHidden: calls
setting the view animations dictionary using various keys (alpha, hidden, isHidden) on the control and superview
overriding animationForKey: for both the control and its superview
I am not using CALayers and have even tried explicitly setting wantsLayer: to NO.
Does anybody know how to either disable this animation, or have a simpler solution then my animated NSWindow?
here is my stripped down altered code with the bare minimum to see what I'm talking about.
#implementation NSControl (DragControl)
- (NSDraggingSession*)beginDraggingSessionWithDraggingCell:(NSActionCell <NSDraggingSource> *)cell event:(NSEvent*) theEvent
{
NSImage* image = [self imageForCell:cell];
NSDraggingItem* di = [[NSDraggingItem alloc] initWithPasteboardWriter:image];
NSRect dragFrame = [self frameForCell:cell];
dragFrame.size = image.size;
[di setDraggingFrame:dragFrame contents:image];
NSArray* items = [NSArray arrayWithObject:di];
[self setHidden:YES];
return [self beginDraggingSessionWithItems:items event:theEvent source:cell];
}
- (NSRect)frameForCell:(NSCell*)cell
{
// override in multi-cell cubclasses!
return self.bounds;
}
- (NSImage*)imageForCell:(NSCell*)cell
{
return [self imageForCell:cell highlighted:[cell isHighlighted]];
}
- (NSImage*)imageForCell:(NSCell*)cell highlighted:(BOOL) highlight
{
// override in multicell cubclasses to just get an image of the dragged cell.
// for any single cell control we can just make sure that cell is the controls cell
if (cell == self.cell || cell == nil) { // nil signifies entire control
// basically a bitmap of the control
// NOTE: the cell is irrelevant when dealing with a single cell control
BOOL isHighlighted = [cell isHighlighted];
[cell setHighlighted:highlight];
NSRect cellFrame = [self frameForCell:cell];
// We COULD just draw the cell, to an NSImage, but button cells draw their content
// in a special way that would complicate that implementation (ex text alignment).
// subclasses that have multiple cells may wish to override this to only draw the cell
NSBitmapImageRep* rep = [self bitmapImageRepForCachingDisplayInRect:cellFrame];
NSImage* image = [[NSImage alloc] initWithSize:rep.size];
[self cacheDisplayInRect:cellFrame toBitmapImageRep:rep];
[image addRepresentation:rep];
// reset the original cell state
[cell setHighlighted:isHighlighted];
return image;
}
// cell doesnt belong to this control!
return nil;
}
#pragma mark NSDraggingDestination
- (void)draggingEnded:(id < NSDraggingInfo >)sender
{
[self setHidden:NO];
}
#end
#implementation NSActionCell (DragCell)
- (void)setControlView:(NSView *)view
{
// this is a bit of a hack, but the easiest way to make the control dragging work.
// force the control to accept image drags.
// the control will forward us the drag destination events via our DragControl category
[view registerForDraggedTypes:[NSImage imagePasteboardTypes]];
[super setControlView:view];
}
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
BOOL result = NO;
NSPoint currentPoint = theEvent.locationInWindow;
BOOL done = NO;
BOOL trackContinously = [self startTrackingAt:currentPoint inView:controlView];
BOOL mouseIsUp = NO;
NSEvent *event = nil;
while (!done)
{
NSPoint lastPoint = currentPoint;
event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSLeftMouseDraggedMask)
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (event)
{
currentPoint = event.locationInWindow;
// Send continueTracking.../stopTracking...
if (trackContinously)
{
if (![self continueTracking:lastPoint
at:currentPoint
inView:controlView])
{
done = YES;
[self stopTracking:lastPoint
at:currentPoint
inView:controlView
mouseIsUp:mouseIsUp];
}
if (self.isContinuous)
{
[NSApp sendAction:self.action
to:self.target
from:controlView];
}
}
mouseIsUp = (event.type == NSLeftMouseUp);
done = done || mouseIsUp;
if (untilMouseUp)
{
result = mouseIsUp;
} else {
// Check if the mouse left our cell rect
result = NSPointInRect([controlView
convertPoint:currentPoint
fromView:nil], cellFrame);
if (!result)
done = YES;
}
if (done && result && ![self isContinuous])
[NSApp sendAction:self.action
to:self.target
from:controlView];
else {
done = YES;
result = YES;
// this bit-o-magic executes on either a drag event or immidiately following timer expiration
// this initiates the control drag event using NSDragging protocols
NSControl* cv = (NSControl*)self.controlView;
NSDraggingSession* session = [cv beginDraggingSessionWithDraggingCell:self
event:theEvent];
// Note that you will get an ugly flash effect when the image returns if this is set to yes
// you can work around it by setting NO and faking the release by animating an NSWindowSubclass with the image as the content
// create the window in the drag ended method for NSDragOperationNone
// there is [probably a better and easier way around this behavior by playing with view animation properties.
session.animatesToStartingPositionsOnCancelOrFail = YES;
}
}
}
return result;
}
#pragma mark - NSDraggingSource Methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationNone;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationPrivate;
break;
}
}
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
// now tell the control view the drag ended so it can do any cleanup it needs
// this is somewhat hackish
[self.controlView draggingEnded:nil];
}
#end
There must be a layer enabled somewhere in your view hierarchy, otherwise there wouldn't be a fade animation. Here is my way of disabling such animations:
#interface NoAnimationImageView : NSImageView
#end
#implementation NoAnimationImageView
+ (id)defaultAnimationForKey: (NSString *)key
{
return nil;
}
#end
The solution you already tried by setting the view animations dictionary should work. But not for the keys you mention but for the following. Use it somewhere before the animation is triggered the first time. If you have to do it on the window or view or both, I don't know.
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderIn];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Or also just remove the keys if they are there (might not be the case as they are implicit / default):
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations removeObjectForKey:NSAnimationTriggerOrderIn];
[animations removeObjectForKey:NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Ok. I figured out that the animation I'm seeing is not the control, the superview, nor the control's window. It appears that animatesToStartingPositionsOnCancelOrFail causes NSDraggingSession to create a window (observed with QuartzDebug) and put the drag image in it and it is this window that animates back to the origin and fades out before the setHidden: call is executed (i.e. before the drag operation is concluded).
Unfortunately, the window that it creates is not an NSWindow so creating a category on NSWindow doesn't disable the fade animation.
Secondly, there is no public way that I know of to get a handle on the window, so I can't attempt directly manipulating the window instance.
It looks like maybe my workaround is the best way to do this, after all its not far from what AppKit does for you anyway.
If anybody knows how to get a handle on this window, or what class it is I would be interested to know.

How to add custom font to UILabel but have label retain its size from storyboard

I currently am subclassing a UILabel to change the font to a custom font, however I would also like it to retain the size that I have set in storyboard for each label. Is there a method of doing this and also detect the current chosen style bold etc and replacing it with the relevant custom font if possible?
Here is the code I use to set the current font.
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
self.font = [UIFont fontWithName:#"FrutigerLT-Roman" size:17.0];
}
return self;
}
To add a custom font, to your app, check the following link: http://shang-liang.com/blog/custom-fonts-in-ios4/
Now, to keep the size set in the storyboard, it should be fine:
self.font = [UIFont fontWithName:#"FrutigerLT-Roman" size:self.font.pointSize];
In the end I wrote my own solution.
https://stackoverflow.com/a/12281017/1565615 #Nathan R. helped me with getting the font size.
Then I extracted the font-weight component of the UIFont description and changed the font accordingly this is great with custom font's as now I can set the font size and style in storyboard and it will set that within the subclassed version of the UILabel.
I wish there was an easier way to identify the type of font-weight being used like font.fontWeight I realise that my solution is longwinded but it works any further ideas would be useful.
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
NSString *fontInfo = self.font.description;//Complete font description
NSArray *splitUpFontDescription = [fontInfo componentsSeparatedByString: #";"];//Split up
NSString *fontWeight = [[NSString alloc]init];
for (NSString *tempString in splitUpFontDescription)
{
if ([tempString rangeOfString:#"font-weight"].location != NSNotFound)//Font weight found
{
fontWeight = [tempString stringByReplacingOccurrencesOfString:#" "
withString:#""];//Remove whitespace
fontWeight = [fontWeight stringByReplacingOccurrencesOfString:#"font-weight:"
withString:#""];
}
}
NSLog(#"Font style (Weight) = *%#*",fontWeight);
if ([fontWeight isEqualToString:#"normal"])
{
//Set to custom font normal.
self.font = [UIFont fontWithName:#"FrutigerLT-Roman" size:self.font.pointSize];
}
else if([fontWeight isEqualToString:#"bold"])
{
//Set to custom font bold.
self.font = [UIFont fontWithName:#"FrutigerLT-Bold" size:self.font.pointSize];
}
}
return self;
}

Highlighting a NSMenuItem with a custom view?

I have created a simple NSStatusBar with a NSMenu set as the menu. I have also added a few NSMenuItems to this menu, which work fine (including selectors and highlighting) but as soon as I add a custom view (setView:) no highlighting occurs.
CustomMenuItem *menuItem = [[CustomMenuItem alloc] initWithTitle:#"" action:#selector(openPreferences:) keyEquivalent:#""];
[menuItem foo];
[menuItem setTarget:self];
[statusMenu insertItem:menuItem atIndex:0];
[menuItem release];
And my foo method is:
- (void)foo {
NSView *view = [[NSView alloc] initWithFrame:CGRectMake(5, 10, 100, 20)];
[self setView:view];
}
If I remove the setView method, it will highlight.
I have searched and searched and cannot find a way of implementing/enabling this.
Edit
I implemented highlight by following the code in this question in my NSView SubClass:
An NSMenuItem's view (instance of an NSView subclass) isn't highlighting on hover
#define menuItem ([self enclosingMenuItem])
- (void) drawRect: (NSRect) rect {
BOOL isHighlighted = [menuItem isHighlighted];
if (isHighlighted) {
[[NSColor selectedMenuItemColor] set];
[NSBezierPath fillRect:rect];
} else {
[super drawRect: rect];
}
}
Here's a rather less long-winded version of the above. It's worked well for me. (backgroundColour is an ivar.)
- (void)drawRect:(NSRect)rect
{
if ([[self enclosingMenuItem] isHighlighted]) {
[[NSColor selectedMenuItemColor] set];
} else if (backgroundColour) {
[backgroundColour set];
}
NSRectFill(rect);
}
Update for 2019:
class CustomMenuItemView: NSView {
private var effectView: NSVisualEffectView
override init(frame: NSRect) {
effectView = NSVisualEffectView()
effectView.state = .active
effectView.material = .selection
effectView.isEmphasized = true
effectView.blendingMode = .behindWindow
super.init(frame: frame)
addSubview(effectView)
effectView.frame = bounds
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
effectView.isHidden = !(enclosingMenuItem?.isHighlighted ?? false)
}
}
Set one of those to your menuItem.view.
(Credit belongs to Sam Soffes who helped me figure this out and sent me almost that code verbatim.)
If you're adding a view to a menu item, that view has to draw the highlight itself. You don't get that for free, I'm afraid. From the Menu Programming Topics:
A menu item with a view does not draw its title, state, font, or other standard drawing attributes, and assigns drawing responsibility entirely to the view.
Yes, as mentioned earlier you must draw it yourself. I use AppKit's NSDrawThreePartImage(…) to draw, and also include checks to use the user's control appearance (blue or graphite.) To get the images, I just took them from a screenshot (if anyone knows a better way, please add a comment.) Here's a piece of my MenuItemView's drawRect:
// draw the highlight gradient
if ([[self menuItem] isHighlighted]) {
NSInteger tint = [[NSUserDefaults standardUserDefaults] integerForKey:#"AppleAquaColorVariant"];
NSImage *image = (AppleAquaColorGraphite == tint) ? menuItemFillGray : menuItemFillBlue;
NSDrawThreePartImage(dirtyRect, nil, image, nil, NO,
NSCompositeSourceOver, 1.0, [self isFlipped]);
}
else if ([self backgroundColor]) {
[[self backgroundColor] set];
NSRectFill(dirtyRect);
}
EDIT
Should have defined these:
enum AppleAquaColorVariant {
AppleAquaColorBlue = 1,
AppleAquaColorGraphite = 6,
};
These correspond to the two appearance options in System Preferences. Also, menuItemFillGray & menuItemFillBlue are just NSImages of the standard menu item fill gradients.

Sample code for creating a NSTextField "label"?

In my desktop Mac OS X app, I'd like to programatically create a NSTextField "label" which has the same behavior and properties as a typical label created in Interface Builder.
I usually use (and very much like) IB, but in this case it must be done programatically.
Try as I might, I can't seem to find the combination of method calls that will programatically produce the same label-y behavior as a "Label" dragged from the IB View Library palette.
Can anyone provide or point out some example code of how to do this programatically? Thx.
A label is actually an instance of NSTextField, a subclass of NSView. So, since it is a NSView, it has to be added to another view.
Here's a working code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSTextField *textField;
textField = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 200, 17)];
[textField setStringValue:#"My Label"];
[textField setBezeled:NO];
[textField setDrawsBackground:NO];
[textField setEditable:NO];
[textField setSelectable:NO];
[view addSubview:textField];
}
macOS 10.12 and Later
Starting with macOS 10.12 (Sierra), there are three new NSTextField constructors:
NSTextField(labelWithString:), which the header file comment says “Creates a non-wrapping, non-editable, non-selectable text field that displays text in the default system font.”
NSTextField(wrappingLabelWithString:), which the header file comment says “Creates a wrapping, non-editable, selectable text field that displays text in the default system font.”
NSTextField(labelWithAttributedString:), which the header file comment says “Creates a non-editable, non-selectable text field that displays attributed text. The line break mode of this field is determined by the attributed string's NSParagraphStyle attribute.”
I tested the ones that take a plain (non-attributed string), and they create text fields that are similar to, but not precisely the same as, the text fields created in a storyboard or xib.
The important difference is that both constructors create a text field with textBackgroundColor (normally pure white) as its background color, while the storyboard text field uses controlColor (normally about 90% white).
Unimportantly, both constructors also set their fonts by calling NSFont.systemFont(ofSize: 0) (which produces a different NSFont object than my code below, but they wrap the same underlying Core Text font).
The wrappingLabelWithString: constructor sets the field's isSelectable to true. (This is documented in the header file.)
macOS 10.11 and Earlier
I compared four NSTextField instances: one created by dragging a “Label” to a storyboard, another created by dragging a “Wrapping Label” to a storyboard, and two in code. Then I carefully modified properties of the code-created labels until all their properties were exactly the same as the storyboard-created labels. These two methods are the result:
extension NSTextField {
/// Return an `NSTextField` configured exactly like one created by dragging a “Label” into a storyboard.
class func newLabel() -> NSTextField {
let label = NSTextField()
label.isEditable = false
label.isSelectable = false
label.textColor = .labelColor
label.backgroundColor = .controlColor
label.drawsBackground = false
label.isBezeled = false
label.alignment = .natural
label.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: label.controlSize))
label.lineBreakMode = .byClipping
label.cell?.isScrollable = true
label.cell?.wraps = false
return label
}
/// Return an `NSTextField` configured exactly like one created by dragging a “Wrapping Label” into a storyboard.
class func newWrappingLabel() -> NSTextField {
let label = newLabel()
label.lineBreakMode = .byWordWrapping
label.cell?.isScrollable = false
label.cell?.wraps = true
return label
}
}
If you use one of these methods, don't forget to set your field's frame, or turn off its translatesAutoresizingMaskIntoConstraints and add constraints.
Here is the code I used to compare the different text fields, in case you want to check:
import Cocoa
class ViewController: NSViewController {
#IBOutlet var label: NSTextField!
#IBOutlet var multilineLabel: NSTextField!
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
let codeLabel = NSTextField.newLabel()
let codeMultilineLabel = NSTextField.newWrappingLabel()
let labels = [label!, codeLabel, multilineLabel!, codeMultilineLabel]
for keyPath in [
"editable",
"selectable",
"allowsEditingTextAttributes",
"importsGraphics",
"textColor",
"preferredMaxLayoutWidth",
"backgroundColor",
"drawsBackground",
"bezeled",
"bezelStyle",
"bordered",
"enabled",
"alignment",
"font",
"lineBreakMode",
"usesSingleLineMode",
"formatter",
"baseWritingDirection",
"allowsExpansionToolTips",
"controlSize",
"highlighted",
"continuous",
"cell.opaque",
"cell.controlTint",
"cell.backgroundStyle",
"cell.interiorBackgroundStyle",
"cell.scrollable",
"cell.truncatesLastVisibleLine",
"cell.wraps",
"cell.userInterfaceLayoutDirection"
] {
Swift.print(keyPath + " " + labels.map({ ($0.value(forKeyPath: keyPath) as? NSObject)?.description ?? "nil" }).joined(separator: " "))
}
}
}
This can be tricky to get right. I don't have the recipe for an exact replica handy, but when I've been stuck in a similar situation, here's what I do:
Create a UI element in IB.
Add an outlet to it from my controller class.
Break in gdb in awakeFromNib or whatever.
From the gdb prompt, "p *whateverOutlet" ... this will show you the C struct contents of the label NSTextField that IB set up.
By looking at all the myriad values in there, you can get a lot of guesses about what you're neglecting to set. Usually it ends up being some magic combination of bezel and border settings, that gets you where you want to be.
You could try using nib2objc to get all the properties that IB sets
Disassembled AppKit in Objective-C:
BOOL TMPSierraOrLater() {
static BOOL result = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
result = [NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){ 10, 12, 0 }];
});
return result;
}
#implementation NSTextField (TMP)
+ (instancetype)TMP_labelWithString:(NSString *)stringValue {
if (TMPSierraOrLater()) {
return [self labelWithString:stringValue];
}
NSParameterAssert(stringValue);
NSTextField *label = [NSTextField TMP_newBaseLabelWithoutTitle];
label.lineBreakMode = NSLineBreakByClipping;
label.selectable = NO;
[label setContentHuggingPriority:(NSLayoutPriorityDefaultLow + 1) forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
[label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
label.stringValue = stringValue;
[label sizeToFit];
return label;
}
+ (instancetype)TMP_wrappingLabelWithString:(NSString *)stringValue {
if (TMPSierraOrLater()) {
return [self wrappingLabelWithString:stringValue];
}
NSParameterAssert(stringValue);
NSTextField *label = [NSTextField TMP_newBaseLabelWithoutTitle];
label.lineBreakMode = NSLineBreakByWordWrapping;
label.selectable = YES;
[label setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
[label setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
label.stringValue = stringValue;
label.preferredMaxLayoutWidth = 0;
[label sizeToFit];
return label;
}
+ (instancetype)TMP_labelWithAttributedString:(NSAttributedString *)attributedStringValue {
if (CRKSierraOrLater()) {
return [self labelWithAttributedString:attributedStringValue];
}
NSParameterAssert(attributedStringValue);
NSTextField *label = [NSTextField TMP_newBaseLabelWithoutTitle];
[label setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
[label setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
label.attributedStringValue = attributedStringValue;
[label sizeToFit];
return label;
}
#pragma mark - Private API
+ (instancetype)TMP_newBaseLabelWithoutTitle {
NSTextField *label = [[self alloc] initWithFrame:CGRectZero];
label.textColor = NSColor.labelColor;
label.font = [NSFont systemFontOfSize:0.0];
label.alignment = NSTextAlignmentNatural;
label.baseWritingDirection = NSWritingDirectionNatural;
label.userInterfaceLayoutDirection = NSApp.userInterfaceLayoutDirection;
label.enabled = YES;
label.bezeled = NO;
label.bordered = NO;
label.drawsBackground = NO;
label.continuous = NO;
label.editable = NO;
return label;
}
#end
Specifically, you will want to setBordered:NO, and set the bezel style to whatever that bezel style is which I forgot. Also setEditable:NO, and optionally setSelectable:NO. That should suffice.