Wrap NSButton title - objective-c

Any way to have a NSButton title to wrap when it's width is longer than the button width, instead of getting clipped?
I'm trying to have a radio button with a text that can be long and have multiple lines. One way I thought about having it work is to have an NSButton of type NSRadioButton but can't get multiple lines of text to work.
Maybe my best alternative is to have an NSButton followed by an NSTextView with the mouseDown delegate function on it triggering the NSButton state?

I don't believe you can. You'd have to subclass NSButtonCell to add support for this.
That said, it's typically a bad idea to have multiple lines of text on a button. A button label should concisely represent the action performed:
The label on a push button should be a verb or verb phrase that describes the action it performs—Save, Close, Print, Delete, Change Password, and so on. If a push button acts on a single setting, label the button as specifically as possible; “Choose Picture…,” for example, is more helpful than “Choose…” Because buttons initiate an immediate action, it shouldn’t be necessary to use “now” (Scan Now, for example) in the label.
What are you trying to do?

I`m incredibly late, but I still feel obliged to share what I`ve found.
Just add a newline character before and after the button title before you assign it to the actual button — and voilà! It now wraps automatically.
The downside of this approach is that, for reasons unknown to me, apps compiled on a certain version of OS X shift button titles one line down when run on newer versions.

Well here's my excuse for needing multiline buttons: I'm writing an emulator for an IBM 701, complete with front panel, and, bless their hearts, the designers of that front panel used multi-line labels. Here's my code. You only have to subclass NSButtonCell (not NSButton), and only one method needs to be overridden.
// In Xcode 4.6 (don't know about earlier versions): Place NSButton, then double-click it
// and change class NSButtonCell to ButtonMultiLineCell.
#interface ButtonMultiLineCell : NSButtonCell
#end
#implementation ButtonMultiLineCell
- (NSRect)drawTitle:(NSAttributedString *)title withFrame:(NSRect)frame inView:(NSView *)controlView
{
NSAttributedString *as = [[NSAttributedString alloc] initWithString:[title.string stringByReplacingOccurrencesOfString:#" " withString:#"\n"]];
NSFont *sysFont = [NSFont systemFontOfSize:10];
NSMutableParagraphStyle *paragraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
[paragraphStyle setAlignment:NSCenterTextAlignment];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
sysFont, NSFontAttributeName,
paragraphStyle, NSParagraphStyleAttributeName,
nil];
NSSize textSize = [as.string sizeWithAttributes:attributes];
NSRect textBounds = NSMakeRect(0, 0, textSize.width, textSize.height);
// using frame argument seems to produce text in wrong place
NSRect f = NSMakeRect(0, (controlView.frame.size.height - textSize.height) / 2, controlView.frame.size.width, textSize.height);
[as.string drawInRect:f withAttributes:attributes];
return textBounds; // not sure what rectangle to return or what is done with it
}
#end

Even later, but I also feel obliged to share. You can set the attributedTitle property of NSButton to achieve manual wrapping.
In my case, I wanted the button title to wrap if it was greater than 6 characters (Swift 3):
if button.title.characters.count > 6 {
var wrappedTitle = button.title
wrappedTitle.insert("\n", at: wrappedTitle.index(wrappedTitle.startIndex, offsetBy: 6))
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes = [NSFontAttributeName: NSFont.systemFont(ofSize: 19), NSParagraphStyleAttributeName: style] as [String : Any]
button.attributedTitle = NSAttributedString(string: wrappedTitle, attributes: attributes)
}

I'm with Sören; If you need a longer description, think about using a tool tip or placing descriptive text in a wrapped text field using the small system font below the radio choices if the descriptive text is only a few lines. Otherwise, you could provide more information in a help document.
Figuring out a way to say what you need to say in a concise way is your best bet, though.

As of today, I'm seeing this can be done simply with a property on the cell of NSButton:
myButton.cell?.wraps = true

I had the same problem and tried, with a sinking heart, the solutions in this post. (While I appreciate advice that one generally should keep button titles short, I'm writing a game, and I want multi-line answers to behave like buttons).
Sometimes, you don't get there from here. My ideal was an NSButton with a multi-line label, but since I can't get that without considerable hassle, I have created a PseudoButton: an NSControl subclass that behaves like a button. It has a hand cursor to indicate 'you can click here' and it gives feedback: when you click the mouse, it changes to selectedControlColor, when you release the mouse, it returns to normal. And unlike solutions that try to stack buttons and labels, there is no problem with having labels and images on top of the view: the whole of the view is the clickable area.
import Cocoa
#IBDesignable
class PseudoButton: NSControl {
#IBInspectable var backgroundColor: NSColor = NSColor.white{
didSet{
self.needsDisplay = true
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = NSBezierPath(rect: dirtyRect)
backgroundColor.setFill()
path.fill()
NSColor.black.setStroke()
path.lineWidth = 2
path.stroke()
}
override func mouseDown(with event: NSEvent) {
self.backgroundColor = NSColor.selectedControlColor
}
override func mouseUp(with event: NSEvent) {
self.backgroundColor = NSColor.clear
guard let action = action else {return}
tryToPerform(action, with: self)
//#IBAction func pseudobuttonClicked(_ sender: PseudoButton) in the ViewController class
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .pointingHand)
}
}
You use this like any other control in the storyboard: drag a Pseudobutton in, decorate it at will, and connect it to an appropriate IBAction in your viewController class.
I like this better than meddling with NSCell. (On past experience, NSCell-based hacks are more likely to break).

A little bit late here, here's my code to insert new line in title:
private func calculateMultipleLineTitle(_ title: String) -> String {
guard !title.isEmpty else { return title }
guard let cell = cell as? NSButtonCell else { return title }
let titleRect = cell.titleRect(forBounds: bounds)
let attr = attributedTitle.attributes(at: 0, effectiveRange: nil)
let indent = (attr[.paragraphStyle] as? NSMutableParagraphStyle)?.firstLineHeadIndent ?? 0
let titleTokenArray = title.components(separatedBy: " ") // word wrap break mode
guard !titleTokenArray.isEmpty else { return title }
var multipleLineTitle = titleTokenArray[0]
var multipleLineAttrTitle = NSMutableAttributedString(string: multipleLineTitle, attributes: attr)
var index = 1
while index < titleTokenArray.count {
multipleLineAttrTitle = NSMutableAttributedString(
string: multipleLineTitle + " " + titleTokenArray[index],
attributes: attr
)
if titleRect.minX+indent+multipleLineAttrTitle.size().width > bounds.width {
multipleLineTitle += " \n" + titleTokenArray[index]
} else {
multipleLineTitle += " " + titleTokenArray[index]
}
index += 1
}
return multipleLineTitle
}
Just pass the original title as parameter, it will return multiple line title.

I added an "\n" at the end of the title and I am setting the title using the NSAttributedString. this fixed the problem for me.
I am on MacOS Big Sur 11.7.2, Xcode 13.12.1
private NSAttributedString GetAttributedString(string text)
{
var paragraph = new NSMutableParagraphStyle();
paragraph.Alignment = NSTextAlignment.Center;
paragraph.LineBreakMode = NSLineBreakMode.ByWordWrapping;
var attrString = new NSAttributedString
(
text + "\n",
font: NSFont.FromFontName("Arial", 50.0f),
foregroundColor: NSColor.White,
backgroundColor: NSColor.FromCalibratedRgba(0, 0, 0, 0.0f),
paragraphStyle: paragraph
);
return attrString;
}
textButton.AttributedTitle = GetAttributedString("some text");

Related

Has anyone found **legal** overrides to customize drawing of NSTabView?

BGHUDAppKit BGHUDTabView _drawThemeTab private API override now broken
For years, I have been using code originally based off of BGHUDAppKit, and found replacements for all of the private API that BGHUDAppKit overrides.
Except for one that I could not find a way to replace...
-[NSTabView _drawThemeTab:withState:inRect:]
(Note: I also use venerable PSMTabBarControl in many circumstances, so if all else fails I'll convert all my tab views to PSMTabBarControl)
Apple has now added the dark NSAppearance in 10.14 Mojave (so in ~10 years I can use it once we stop supporting High Sierra).
Whichever selfish dev at Apple writes NSTabView does not believe in making his view customizable, unlike all of the other NSControls which are customizable.
Here is part of the hackish overrides for custom drawing of NSTabView:
// until we can eliminate private API _drawThemeTab:, return nil for new NSAppearance
- (id) appearance { return nil; }
- (id) effectiveAppearance { return nil; }
-(void)_drawThemeTab:(id) tabItem withState:(NSUInteger) state inRect:(NSRect) aRect {
NSInteger idx = [self indexOfTabViewItem: tabItem];
int gradientAngle = 90;
NSBezierPath *path = nil;
aRect = NSInsetRect(aRect, 0.5f, 0.5f);
if([self tabViewType] == NSLeftTabsBezelBorder) {
gradientAngle = 0;
} else if([self tabViewType] == NSRightTabsBezelBorder) {
gradientAngle = 180;
}
NSColor *specialFillColor = [tabItem color];
NSColor *outlineColor = nil;
NSString *name = [specialFillColor description];
// MEC - added new prefix 12/15/17 to fix white border around last segment in High Sierra
if ( [name hasPrefix:#"NSNamedColorSpace System"] || [name hasPrefix:#"Catalog color: System controlColor"])
specialFillColor = nil;
else if ( [name isEqualToString: #"NSCalibratedWhiteColorSpace 0 1"] )
[specialFillColor set];
else
{
outlineColor = specialFillColor;
specialFillColor = nil;
}
... etc ...
It's probably preferrable to completely disable NSTabView's drawing (setting its tabViewType to NSNoTabsNoBorder), and create a custom segmented bar view to draw the selection separately (as a sibling view). This allows you to completely control the appearance, layout, and sizing of that custom implementation rather than relying on any details of NSTabView.
Looking at the view hierarchy of an NSTabViewController, you can see that it has this same approach by using an NSSegmentedControl as a separate subview managing selection from the NSTabView.

Showing a button when all 'enemy' ccsprites have been removed from scene

I am using SpriteBuilder to make a game. The objective is to destroy some CCSprites. I have 3 sprites on screen and are destroyed by another sprite, so the code must have something to do with when there are no more 'enemy' sprites remaining a next button must show. I have looked on the internet and are inexperienced with Cocos2D coding. Here is the code I have used to get rid of the 'enemy'
-(void)ccPhysicsCollisionPostSolve:(CCPhysicsCollisionPair *)pair danald:(CCNode *)nodeA wildcard:(CCNode *)nodeB {
float energy = [pair totalKineticEnergy];
if (energy > 5000.f) {
[self danaldRemoved:nodeA];
}
}
If the object is hit with a certain speed it will call the method below
- (void)danaldRemoved:(CCNode *)Danald {
CCParticleSystem *explosion = (CCParticleSystem *)[CCBReader load:#"Explosion"];
explosion.autoRemoveOnFinish = TRUE;
explosion.position = Danald.position;
[Danald.parent addChild:explosion];
[Danald removeFromParent];
}
Thanks in an advanced, sorry if this question has been asked before but I cannot find it
Well I would suggest this method:
Create a variable where you store the number of sprites left. For example:
int spritesLeft;
And then initialize it to 0:
-(void) didLoadFromCCB{
//REST OF CODE
spritesLeft=3; //3 because you said there are only 3.
}
Now when you call danaldRemoved: method, just subtract 1 to spritesLeft, and check if spritesLeft is equal to 0. If it's true, just call your method to make a button appear:
- (void)danaldRemoved:(CCNode *)Danald {
spritesLeft--; //substract 1
CCParticleSystem *explosion = (CCParticleSystem *)[CCBReader load:#"Explosion"];
explosion.autoRemoveOnFinish = TRUE;
explosion.position = Danald.position;
[Danald.parent addChild:explosion];
[Danald removeFromParent];
//check if game is over.
if (spritesLeft == 0){
[self printButton];
}
}
Now create the method printButton, but before go to SpriteBuilder, create the button and place it where you want. Now uncheck 'Visible' value, and then go to code connections, and select 'Doc root var' (under custom class) and write a name for the button, for example: nextButton. At the selector value write: changeLevel and target: document root
Now declare it at the top of your .m file as you did with any other objects:
CCButton *nextButton;
Method for button (just set visibility ON)
-(void) printButton{
nextButton.visible = YES;
}
And now your method to change level:
-(void) changeLevel{
CCScene *nextLevel = [CCBReader loadAsScene:#"YOUR LEVEL"];
[[CCDirector sharedDirector] replaceScene:nextLevel];
}
Hope this helps!
EDIT: HOW TO DETECT WHEN A SPRITE GOES OFF THE SCREEN
As I said, create any kind of physic object in spritebuilder. For example, I use CCNodeColor. Then make it a rectangle and place it at left of the screen. Now go to physics, enable physics, polygon type and static. Now in connections, select doc root var and call it _leftNode. Now repeat with top,right and bottom and call them _topNode, etc.
Now go to code, declare your new nodes: CCNode *_leftNode; and so...
Now let's make a collision type:
_bottomNode.physicsBody.collisionType = #"_bound";
_leftNode.physicsBody.collisionType = #"_bound";
_rightNode.physicsBody.collisionType = #"_bound";
_topNode.physicsBody.collisionType = #"_bound";
And do the same with your sprite, but I think you have done that before. Let's make an example:
spritename.physicsBody.collisionType = #"_sprite";
So now implement the method:
-(void)ccPhysicsCollisionPostSolve:(CCPhysicsCollisionPair *)pair _sprite:(CCNode *)nodeA _bound:(CCNode *)nodeB {
[_physicsNode removeChild:nodeA cleanup:YES];
}
And that's all.

Change the text of a NSTextView programmatically

I can't find any parameter that seems to be related with the text value showed in a NSTextView. I understood that a NSTextView uses a complex structure (with NSLayoutManager etc...), but I can't find a valid way to modify the current text value.
The right method is "setString" [textView setString:#"the string"];
Setting text on outlet textView Swift Xcode 8.0
textView.string = newString
If you want to set attributed text (formatted text) them try
[myTextView setAttributedString:(NSAttributedString*)attrString].
NSTextView contains a NSTextStorage object that actually holds the text...
Something like this:
[textView setString:#"new value"];
Objective C / Xcode 9
[myTextView setString:#"The string"];
self.myTextView.string = #"My String";
Swift / Xcode 9
self.myTextView.setString("MyString")
self.myTextView.string = "My String"
Almost there - the program is below - this almost works. There are two
outstanding problems:
1) It takes two mouse click to set the correct selection point in the text
the first always goes to the end of the text. The second to the required
position
2) A strange error is printed in the shell - Assertion failure in -[LUPresenter animationControllerForTerm:atLocation:options:], /SourceCache/Lookup/Lookup-160/Framework/Classes/LUPresenter.m:
import Cocoa
class MyAppDelegate: NSObject, NSApplicationDelegate {
let window = NSWindow()
func applicationDidFinishLaunching(aNotification: NSNotification) {
window.setContentSize(NSSize(width:600, height:200))
window.styleMask = NSTitledWindowMask | NSClosableWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask
window.opaque = false
window.center();
window.title = "My window"
let ed = NSTextView(frame: NSMakeRect(20, 10, 180, 160))
ed.font = NSFont(name:"Helvetica Bold", size:20)
ed.string = "edit me"
ed.editable = true
ed.selectable = true
window.contentView!.addSubview(ed)
window.makeKeyAndOrderFront(window)
window.level = 1
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
let app = NSApplication.sharedApplication()
app.setActivationPolicy(.Regular)
let obj = MyAppDelegate()
app.delegate = obj
app.run()

Moving the cursor in an UITextView

I try to move the cursor when a UITextView is selected(touched) to simulate kind of a UITextField placeholder thing.
I'd like the cursor to be at the beginning of the first line. My problem is, that [someTextField setSelectedRange]is not working reliably. When I call it in textView:shouldChangeTextInRange:replacementText: it works as it should. But this method is only called when the user starts typing. I'm using textViewDidBeginEditing: to move the cursor when the UITextView becomes the first responder:
- (void)textViewDidBeginEditing:(UITextView *)textView
{
if (textView == self.descriptionText) {
CustomTextView* customTV = (CustomTextView *)textView;
if ([customTV.text isEqualToString:customTV.placeholder]) {
// text in text view is still the placeholder -> move cursor to the beginning
customTV.text = customTV.placeholder;
customTV.textColor = [UIColor lightGrayColor];
customTV.selectedRange = NSMakeRange(0, 0);
}
}
}
Any ideas why customTV.selectedRange = NSMakeRange(0, 0); isn't working correctly in textViewDidBeginEditing: ?
Thanks for your help!
Actually there's a very simple way of accomplishing this.
// Count the characters on screen
NSMutableString *numOfChar = [self.myTextField.text mutableCopy];
// Skip that many characters to the left
self.myTextField.selectedRange = NSMakeRange(self.myTextField.selectedRange.location-[numOfChar length], 0);
Hope this helps.

Create UITextRange from NSRange

I need to find the pixel-frame for different ranges in a textview. I'm using the - (CGRect)firstRectForRange:(UITextRange *)range; to do it. However I can't find out how to actually create a UITextRange.
Basically this is what I'm looking for:
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
UITextRange*range2 = [UITextRange rangeWithNSRange:range]; //DOES NOT EXIST
CGRect rect = [textView firstRectForRange:range2];
return rect;
}
Apple says one has to subclass UITextRange and UITextPosition in order to adopt the UITextInput protocol. I don't do that, but I tried anyway, following the doc's example code and passing the subclass to firstRectForRange which resulted in crashing.
If there is a easier way of adding different colored UILables to a textview, please tell me. I have tried using UIWebView with content editable set to TRUE, but I'm not fond of communicating with JS, and coloring is the only thing I need.
Thanks in advance.
You can create a text range with the method textRangeFromPosition:toPosition. This method requires two positions, so you need to compute the positions for the start and the end of your range. That is done with the method positionFromPosition:offset, which returns a position from another position and a character offset.
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
CGRect rect = [textView firstRectForRange:textRange];
return [textView convertRect:rect fromView:textView.textInputView];
}
It is a bit ridiculous that seems to be so complicated.
A simple "workaround" would be to select the range (accepts NSRange) and then read the selectedTextRange (returns UITextRange):
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
textView.selectedRange = range;
UITextRange *textRange = [textView selectedTextRange];
CGRect rect = [textView firstRectForRange:textRange];
return rect;
}
This worked for me even if the textView is not first responder.
If you don't want the selection to persist, you can either reset the selectedRange:
textView.selectedRange = NSMakeRange(0, 0);
...or save the current selection and restore it afterwards
NSRange oldRange = textView.selectedRange;
// do something
// then check if the range is still valid and
textView.selectedRange = oldRange;
Swift 4 of Andrew Schreiber's answer for easy copy/paste
extension NSRange {
func toTextRange(textInput:UITextInput) -> UITextRange? {
if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location),
let rangeEnd = textInput.position(from: rangeStart, offset: length) {
return textInput.textRange(from: rangeStart, to: rangeEnd)
}
return nil
}
}
To the title question, here is a Swift 2 extension that creates a UITextRange from an NSRange.
The only initializer for UITextRange is a instance method on the UITextInput protocol, thus the extension also requires you pass in UITextInput such as UITextField or UITextView.
extension NSRange {
func toTextRange(textInput textInput:UITextInput) -> UITextRange? {
if let rangeStart = textInput.positionFromPosition(textInput.beginningOfDocument, offset: location),
rangeEnd = textInput.positionFromPosition(rangeStart, offset: length) {
return textInput.textRangeFromPosition(rangeStart, toPosition: rangeEnd)
}
return nil
}
}
Swift 4 of Nicolas Bachschmidt's answer as an UITextView extension using swifty Range<String.Index> instead of NSRange:
extension UITextView {
func frame(ofTextRange range: Range<String.Index>?) -> CGRect? {
guard let range = range else { return nil }
let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
guard
let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
let end = position(from: start, offset: length),
let txtRange = textRange(from: start, to: end)
else { return nil }
let rect = self.firstRect(for: txtRange)
return self.convert(rect, to: textInputView)
}
}
Possible use:
guard let rect = textView.frame(ofTextRange: text.range(of: "awesome")) else { return }
let awesomeView = UIView()
awesomeView.frame = rect.insetBy(dx: -3.0, dy: 0)
awesomeView.layer.borderColor = UIColor.black.cgColor
awesomeView.layer.borderWidth = 1.0
awesomeView.layer.cornerRadius = 3
self.view.insertSubview(awesomeView, belowSubview: textView)
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
UITextRange *textRange = [[textView _inputController] _textRangeFromNSRange:range]; // Private
CGRect rect = [textView firstRectForRange:textRange];
return rect;
}
Here is explain.
A UITextRange object represents a range of characters in a text
container; in other words, it identifies a starting index and an
ending index in string backing a text-entry object.
Classes that adopt the UITextInput protocol must create custom
UITextRange objects for representing ranges within the text managed by
the class. The starting and ending indexes of the range are
represented by UITextPosition objects. The text system uses both
UITextRange and UITextPosition objects for communicating text-layout
information. There are two reasons for using objects for text ranges
rather than primitive types such as NSRange:
Some documents contain nested elements (for example, HTML tags and
embedded objects) and you need to track both absolute position and
position in the visible text.
The WebKit framework, which the iPhone text system is based on,
requires that text indexes and offsets be represented by objects.
If you adopt the UITextInput protocol, you must create a custom
UITextRange subclass as well as a custom UITextPosition subclass.
For example like in those sources