How to add multi-line text using NSAttributedString to a NSButton? - swift

My application supports multiple languages. I have a Translation object which sets string on NSButton Title. How can I use multiline to set text inside my Button?
I used self.lineBreakMode = .ByWordWrapping but it does not work.
class CustomNSButton: NSButton {
override func viewWillDraw() {
let currentText = Translations.shared.current?[self.identifier ?? ""]?.string ?? self.stringValue
self.lineBreakMode = .byWordWrapping
let size = calculateIdealFontSize(min: 5, max: 16)
let translatedString = CustomFormatter.string(for: currentText)
let pstyle = NSMutableParagraphStyle()
pstyle.alignment = .center
let translatedAttributedString = CustomFormatter.attributedString(for: translatedString ?? "", withDefaultAttributes:[NSFontAttributeName : NSFont(name: (self.font?.fontName)!, size: CGFloat(size))!, NSParagraphStyleAttributeName : pstyle])!
attributedTitle = translatedAttributedString
}
}

I created a multiline text by using let textLabel = NSTextField() and a let textFieldCell = CustomNSTextFieldCell() subclass. Add subview in CustomNSButton class addSubview(textLabel)
class CustomNSButton: NSButton {
let textLabel = NSTextField()
let textFieldCell = CustomNSTextFieldCell()
override func viewWillDraw() {
textLabel.frame = CGRect(x:0,y:0, width: frame.width - 2, height: frame.height - 2)
let pstyle = NSMutableParagraphStyle()
pstyle.alignment = .center
textLabel.attributedStringValue = CustomFormatter.attributedString(for: translatedString ?? "", withDefaultAttributes:[NSFontAttributeName : NSFont(name: (self.font?.fontName)!, size: CGFloat(size))!, NSParagraphStyleAttributeName : pstyle, NSForegroundColorAttributeName : NSColor.white])!
textLabel.isEditable = false
textLabel.isBezeled = false
textLabel.backgroundColor = NSColor.clear
textLabel.cell = textFieldCell
addSubview(textLabel)
}
}
class CustomNSTextFieldCell: NSTextFieldCell {
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
let attrString: NSAttributedString? = attributedStringValue
attrString?.draw(with: titleRect(forBounds: cellFrame), options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin])
}
override func titleRect(forBounds theRect: NSRect) -> NSRect {
var tFrame: NSRect = super.titleRect(forBounds: theRect)
let attrString: NSAttributedString? = attributedStringValue
let tRect: NSRect? = attrString?.boundingRect(with: tFrame.size, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin])
if (textRect?.size.height)! < tFrame.size.height {
tFrame.origin.y = theRect.origin.y + (theRect.size.height - (textRect?.size.height)!) / 2.0
tFrame.size.height = (textRect?.size.height)!
}
return tFrame
}
}

Related

NSLayoutManager for Line Numbering in UITextVIew

I am attempting to create an iOS LineNumberLayoutManger in Swift to be used in a TextKit 2 UITextView. My problem is the line number variable is being overwritten when I enter a new line with the return key. The revised code sample below can be copied and pasted into an Xcode project in a ViewController to run. Any suggestions very much appreciated.
// ViewController.swift
// tester11
//
// Created by ianshortreed on 2022/07/15.
//
import UIKit
var currentRect:CGRect!
var cgPoint:CGPoint!
extension String {
func substring(with range: NSRange) -> String {
let startIndex = index(self.startIndex, offsetBy: range.location)
let endIndex = index(startIndex, offsetBy: range.length)
//return substring(with: startIndex ..< endIndex)
return String(self[startIndex..<endIndex])
}
}
extension Collection {
var enumerated: Zip2Sequence<PartialRangeFrom<Int32>, Self> { zip(1..., self) }
}
class ViewController: UIViewController, UITextViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
createTextView()
}
func createTextView() {
var textView: UITextView!
var textStorage: NSTextStorage!
// 1
let attrs = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body), .foregroundColor: UIColor.label]
let attrString = NSAttributedString(string: "Press the return key to add a new line.", attributes: attrs)
textStorage = NSTextStorage()
textStorage.append(attrString)
let newTextViewRect = view.bounds
// 2
//let layoutManager = LineNumberLayoutManager()
let layoutManager = LineNumberLayoutManager()
// 3
let containerSize = CGSize(width: newTextViewRect.width, height: .greatestFiniteMagnitude)
let container = NSTextContainer(size: containerSize)
container.widthTracksTextView = true
layoutManager.addTextContainer(container)
textStorage.addLayoutManager(layoutManager)
// 4
textView = UITextView(frame: newTextViewRect, textContainer: container)
textView.delegate = self
textView.isScrollEnabled = true
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 9.5
//textView.layoutManager.usesFontLeading = false
textView.keyboardDismissMode = .interactive
textView.textContainerInset = UIEdgeInsets(top: 50, left: 10, bottom: 0, right: 10)
textView.typingAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17)]
textView.textAlignment = NSTextAlignment.justified
textView.allowsEditingTextAttributes = true
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.dataDetectorTypes = .all
view.addSubview(textView)
// 5
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
class LineNumberLayoutManager: NSLayoutManager {
let lineSize = CGSize(width: 8, height: 8)
var lineColor = UIColor(red:1.00, green:1.00, blue:1.00, alpha:1.0)
override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
var linenumber = 0
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
guard let textStorage = self.textStorage else { return }
enumerateLineFragments(forGlyphRange: glyphsToShow) { [self] (rect, usedRect, textContainer, glyphRange, _) in
linenumber += 1
let origin = CGPoint(x: 10, y: usedRect.origin.y + 48 + (usedRect.size.height - self.lineSize.height) / 2)
var newLineRange = NSRange(location: 0, length: 0)
if glyphRange.location > 0 {
newLineRange.location = glyphRange.location - 1
newLineRange.length = 1
}
var isNewLine = true
if newLineRange.length > 0 {
isNewLine = textStorage.string.substring(with: newLineRange) == "\n"
}
if isNewLine {
let str = textStorage.string.components(separatedBy: .newlines)
"\(linenumber)\(str)".draw(in:CGRect(origin: origin, size: lineSize))
}
}
}
}
}

Adding padding to a UILabel with a background colour

I have a multiline UILabel that contains an NSAttributedString, and this has a background colour applied to give the above effect.
This much is fine but I need padding within the label to give a bit of space on the left. There are other posts on SO addressing this issue, such as by subclassing UILabel to add UIEdgeInsets. However, this merely added padding to the outside of the label for me.
Any suggestions on how padding can be added to this label?
EDIT: Apologies if this has been confusing, but the end goal is something like this...
Based on the answer provided here: https://stackoverflow.com/a/59216224/6257435
Just to demonstrate (several hard-coded values which would, ideally, be dynamic / calculated):
class ViewController: UIViewController, NSLayoutManagerDelegate {
var myTextView: UITextView!
let textStorage = MyTextStorage()
let layoutManager = MyLayoutManager()
override func viewDidLoad() {
super.viewDidLoad()
myTextView = UITextView()
myTextView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myTextView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
myTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
myTextView.widthAnchor.constraint(equalToConstant: 248.0),
myTextView.heightAnchor.constraint(equalToConstant: 300.0),
])
self.layoutManager.delegate = self
self.textStorage.addLayoutManager(self.layoutManager)
self.layoutManager.addTextContainer(myTextView.textContainer)
let quote = "This is just some sample text to demonstrate the word wrapping with padding at the beginning (leading) and ending (trailing) of the lines of text."
self.textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: quote)
guard let font = UIFont(name: "TimesNewRomanPSMT", size: 24.0) else {
fatalError("Could not instantiate font!")
}
let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: font]
self.textStorage.setAttributes(attributes, range: NSRange(location: 0, length: quote.count))
myTextView.isUserInteractionEnabled = false
// so we can see the frame of the textView
myTextView.backgroundColor = .systemTeal
}
func layoutManager(_ layoutManager: NSLayoutManager,
lineSpacingAfterGlyphAt glyphIndex: Int,
withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 14.0 }
func layoutManager(_ layoutManager: NSLayoutManager,
paragraphSpacingAfterGlyphAt glyphIndex: Int,
withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 14.0 }
}
class MyTextStorage: NSTextStorage {
var backingStorage: NSMutableAttributedString
override init() {
backingStorage = NSMutableAttributedString()
super.init()
}
required init?(coder: NSCoder) {
backingStorage = NSMutableAttributedString()
super.init(coder: coder)
}
// Overriden GETTERS
override var string: String {
get { return self.backingStorage.string }
}
override func attributes(at location: Int,
effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
return backingStorage.attributes(at: location, effectiveRange: range)
}
// Overriden SETTERS
override func replaceCharacters(in range: NSRange, with str: String) {
backingStorage.replaceCharacters(in: range, with: str)
self.edited(.editedCharacters,
range: range,
changeInLength: str.count - range.length)
}
override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
backingStorage.setAttributes(attrs, range: range)
self.edited(.editedAttributes,
range: range,
changeInLength: 0)
}
}
import CoreGraphics //Important to draw the rectangles
class MyLayoutManager: NSLayoutManager {
override init() { super.init() }
required init?(coder: NSCoder) { super.init(coder: coder) }
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
self.enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, textContainer, glyphRange, stop) in
var lineRect = usedRect
lineRect.size.height = 41.0
let currentContext = UIGraphicsGetCurrentContext()
currentContext?.saveGState()
// outline rectangles
//currentContext?.setStrokeColor(UIColor.red.cgColor)
//currentContext?.setLineWidth(1.0)
//currentContext?.stroke(lineRect)
// filled rectangles
currentContext?.setFillColor(UIColor.orange.cgColor)
currentContext?.fill(lineRect)
currentContext?.restoreGState()
}
}
}
Output (teal-background shows the frame):
I used one different way. First I get all lines from the UILabel and add extra blank space at the starting position of every line. To getting all line from the UILabel I just modify code from this link (https://stackoverflow.com/a/55156954/14733292)
Final extension UILabel code:
extension UILabel {
var addWhiteSpace: String {
guard let text = text, let font = font else { return "" }
let ctFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
let attStr = NSMutableAttributedString(string: text)
attStr.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value: ctFont, range: NSRange(location: 0, length: attStr.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude), transform: .identity)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
guard let lines = CTFrameGetLines(frame) as? [Any] else { return "" }
return lines.map { line in
let lineRef = line as! CTLine
let lineRange: CFRange = CTLineGetStringRange(lineRef)
let range = NSRange(location: lineRange.location, length: lineRange.length)
return " " + (text as NSString).substring(with: range)
}.joined(separator: "\n")
}
}
Use:
let labelText = yourLabel.addWhiteSpace
let attributedString = NSMutableAttributedString(string: labelText)
attributedString.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.red, range: NSRange(location: 0, length: labelText.count))
yourLabel.attributedText = attributedString
yourLabel.backgroundColor = .yellow
Edited:
The above code is worked at some point but it's not sufficient. So I created one class and added padding and one shape rect layer.
class AttributedPaddingLabel: UILabel {
private let leftShapeLayer = CAShapeLayer()
var leftPadding: CGFloat = 5
var attributedTextColor: UIColor = .red
override func awakeFromNib() {
super.awakeFromNib()
self.addLeftSpaceLayer()
self.addAttributed()
}
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: 0, left: leftPadding, bottom: 0, right: 0)
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + leftPadding, height: size.height)
}
override var bounds: CGRect {
didSet {
preferredMaxLayoutWidth = bounds.width - leftPadding
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
leftShapeLayer.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: leftPadding, height: rect.height)).cgPath
}
private func addLeftSpaceLayer() {
leftShapeLayer.fillColor = attributedTextColor.cgColor
self.layer.addSublayer(leftShapeLayer)
}
private func addAttributed() {
let lblText = self.text ?? ""
let attributedString = NSMutableAttributedString(string: lblText)
attributedString.addAttribute(NSAttributedString.Key.backgroundColor, value: attributedTextColor, range: NSRange(location: 0, length: lblText.count))
self.attributedText = attributedString
}
}
How to use:
class ViewController: UIViewController {
#IBOutlet weak var lblText: AttributedPaddingLabel!
override func viewDidLoad() {
super.viewDidLoad()
lblText.attributedTextColor = .green
lblText.leftPadding = 10
}
}
SwiftUI
This is just a suggestion in SwiftUI. I hope it will be useful.
Step 1: Create the content
let string =
"""
Lorem ipsum
dolor sit amet,
consectetur
adipiscing elit.
"""
Step 2: Apply attributes
let attributed: NSMutableAttributedString = .init(string: string)
attributed.addAttribute(.backgroundColor, value: UIColor.orange, range: NSRange(location: 0, length: attributed.length))
attributed.addAttribute(.font, value: UIFont(name: "Times New Roman", size: 22)!, range: NSRange(location: 0, length: attributed.length))
Step 3: Create AttributedLabel in SwiftUI using a UILabel()
struct AttributedLabel: UIViewRepresentable {
let attributedString: NSAttributedString
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.lineBreakMode = .byClipping
label.numberOfLines = 0
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.attributedText = attributedString
}
}
Final step: Use it and add .padding()
struct ContentView: View {
var body: some View {
AttributedLabel(attributedString: attributed)
.padding()
}
}
This is the result:

NSMenuItem with custom view disappears while scrolling

I implement a NSMenu with NSMenuItem and set custom view to it. When menu is scrollable, mouse hovering on ▼ button to scroll will cause some menuItem disappear (or not draw correctly). Hope someone give me some help. I will appreciate that.
Here's video about this issue:
https://streamable.com/obrbon
Here's my code:
private func setupMenuItemView(_ menu: NSMenu) {
let menuItemHeight: CGFloat = 20
let menuWidth = frame.width
let textFieldPadding: CGFloat = 10
for menuItem in menu.items {
guard !menuItem.title.isEmpty else { continue }
let menuItemView = MenuItemView(frame: NSRect(x: 0, y: 0, width: Int(menuWidth), height: menuItemHeight))
let textField = MenuItemTextField(labelWithString: menuItem.title)
textField.frame = NSRect(
x: textFieldPadding,
y: (menuItemView.frame.height-textField.frame.height)/2,
width: menuWidth-textFieldPadding*2,
height: textField.frame.height
)
textField.lineBreakMode = .byTruncatingTail
menuItemView.addSubview(textField)
menuItemView.toolTip = menuItem.title
menuItem.view = menuItemView
menuItem.target = self
menuItem.action = #selector(onMenuItemClicked(_:))
}
}
fileprivate class MenuItemView: NSView {
override func mouseUp(with event: NSEvent) {
guard let menuItem = enclosingMenuItem else { return }
guard let action = menuItem.action else { return }
NSApp.sendAction(action, to: menuItem.target, from: menuItem)
menuItem.menu?.cancelTracking()
}
override func draw(_ dirtyRect: NSRect) {
guard let menuItem = enclosingMenuItem else { return }
if menuItem.isHighlighted {
NSColor.alternateSelectedControlColor.set()
} else {
NSColor.clear.set()
}
NSBezierPath.fill(dirtyRect)
super.draw(dirtyRect)
}
}
fileprivate class MenuItemTextField: NSTextField {
override var allowsVibrancy: Bool {
return false
}
}
After calling setupMenuItemView(), i call menu.popup().
Hope this information helps.
I was unable to get your posted code to work correctly. The demo below is an alternative which uses a popUpContextual menu with subclassed text fields embedded in the menuItem views (note that the view associated with each menuItem is used and a custom view class is not created). Text alignment and truncation is functional. Menu width is also flexible and may be set to match width of the menu title field. The demo may be run in Xcode by copy/pasting source code into a newly added ‘main.swift’ file and additionally deleting Apple’s AppDelegate class.
import Cocoa
var view = [NSTextField]()
class TextField: NSTextField {
override func mouseDown(with event: NSEvent) {
print("selected = \(self.tag)")
for i:Int in 0..<view.count {
if(self.tag == i) {
view[i].backgroundColor = .lightGray
} else {
view[i].backgroundColor = .clear
}
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var menu = NSMenu()
let _menuLeft : CGFloat = 40
let _menuTop : CGFloat = 70
let _menuWidth : CGFloat = 70
let _menuItemH : CGFloat = 20
#objc func menuBtnAction(_ sender:AnyObject ) {
let menuOrigin = NSMakePoint(_menuLeft, sender.frame.origin.y - 5)
let wNum : Int = sender.window.windowNumber
let event = NSEvent.mouseEvent(with:.leftMouseDown, location:menuOrigin, modifierFlags:[], timestamp:0, windowNumber:wNum, context:nil, eventNumber:0, clickCount:1, pressure:1.0)
NSMenu.popUpContextMenu(menu, with: event!, for: window.contentView!)
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
let menuItems = ["10%","25%","50%","75%","100%","Longer text."]
menu.title = "Fit"
var count:Int = 0
for mItem in menuItems{
let menuItem = NSMenuItem()
menu.addItem(menuItem)
let textField = TextField(frame:NSMakeRect(0,0,_menuWidth, _menuItemH))
menuItem.view = textField
textField.alignment = .left // .left, .center, .right
textField.stringValue = mItem
textField.lineBreakMode = .byTruncatingTail
textField.backgroundColor = .clear
textField.isEditable = false
textField.tag = count
textField.isBordered = false
textField.font = NSFont( name:"Menlo bold", size:14 )
count = count + 1
view.append(textField)
}
// **** Menu title **** //
let label = NSTextField (frame:NSMakeRect( _menuLeft, _wndH - 50, _menuWidth, 24 ))
window.contentView!.addSubview (label)
label.autoresizingMask = [.maxXMargin,.minYMargin]
label.backgroundColor = .clear
label.lineBreakMode = .byTruncatingTail
label.isSelectable = false
label.isBordered = true
label.font = NSFont( name:"Menlo bold", size:14 )
label.stringValue = menu.title
// **** Menu Disclosure Button **** //
let menuBtn = NSButton (frame:NSMakeRect( (_menuLeft + _menuWidth) - 20, _wndH - 50, 20, 24 ))
menuBtn.bezelStyle = .disclosure
menuBtn.autoresizingMask = [.maxXMargin,.minYMargin]
menuBtn.title = ""
menuBtn.action = #selector(self.menuBtnAction(_:))
window.contentView!.addSubview (menuBtn)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()
Second possible alternative utilizes an NSPopUpButton with drop down menu which accommodates DarkMode by changing text color. As before, a subclassed text field is embedded into each menuItem.view to support text truncation and alignment. May be run in Xcode with instructions given previously.
import Cocoa
var view = [NSTextField]()
class TextField: NSTextField {
override func mouseDown(with event: NSEvent) {
print("selected = \(self.tag)")
for i:Int in 0..<view.count {
if(self.tag == i) {
view[i].backgroundColor = .lightGray
} else {
view[i].backgroundColor = .windowBackgroundColor
}
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var menu:NSMenu!
var pullDwn:NSPopUpButton!
var count:Int = 0
let _menuItemH : CGFloat = 20
func isDarkMode(view: NSView) -> Bool {
if #available(OSX 10.14, *) {
return view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
return false
}
#objc func myBtnAction(_ sender:Any ) {
print(pullDwn.index(of:sender as! NSMenuItem))
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
// **** NSPopUpButton with Menu **** //
let menuItems = ["10%","25%","50%","75%","100%","200%","300%","400%","800%","longertext"]
pullDwn = NSPopUpButton(frame:NSMakeRect(80, _wndH - 50, 80, 30), pullsDown:true)
pullDwn.autoresizingMask = [.maxXMargin,.minYMargin]
let menu = pullDwn.menu
for mItem in menuItems{
let menuItem = NSMenuItem()
menu?.addItem(menuItem)
menuItem.title = "Fit"
let textField = TextField(frame:NSMakeRect( 0, 0, pullDwn.frame.size.width, _menuItemH))
menuItem.view = textField
textField.alignment = .left // .left, .center, .right
textField.stringValue = mItem
textField.lineBreakMode = .byTruncatingTail
if (isDarkMode(view: textField)){
textField.textColor = .white
} else {
textField.textColor = .black
}
textField.backgroundColor = .windowBackgroundColor
textField.isEditable = false
textField.tag = count
textField.isBordered = false
textField.font = NSFont( name:"Menlo", size:14 )
count = count + 1
view.append(textField)
}
window.contentView!.addSubview (pullDwn)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()

How to print entire NSCollectionView

I'm trying to print entire NSCollectionView. It prints only displayed items, moreover it doesn't render NSCollectionViewItem properly
Print is populated by bindings using first responder. Method:
#IBAction func printEquations(_ sender: Any) {
let op = NSPrintOperation(view: collectionView)
op.canSpawnSeparateThread = true
let pi = op.printInfo
pi.horizontalPagination = NSPrintingPaginationMode.fitPagination
pi.verticalPagination = NSPrintingPaginationMode.fitPagination
pi.isHorizontallyCentered = true
pi.isVerticallyCentered = true
pi.isSelectionOnly = false
pi.orientation = NSPaperOrientation.portrait
pi.leftMargin = 10
pi.rightMargin = 10
op.run()
}
NSCollectionView subclass:
class EQCollectionView: NSCollectionView {
var printRect: NSRect?
override func draw(_ dirtyRect: NSRect) {
if (NSGraphicsContext.currentContextDrawingToScreen()) {
self.layer!.borderWidth = 0
self.layer!.borderColor = nil
self.backgroundColors = []
super.draw(dirtyRect)
} else {
self.wantsLayer = true
let printWidth = Double(calculatePrintWidth())
let printHeight = Double(calculatePrintHeight())
printRect = NSRect(x: 0, y: 0, width: printWidth, height: printHeight)
super.draw(printRect!)
}
}
func calculatePrintHeight() -> Float {
let pi = NSPrintOperation.current()!.printInfo
let paperSize = pi.paperSize
let pageHeight = Float(paperSize.height - pi.topMargin - pi.bottomMargin)
let scale = Float(pi.dictionary().object(forKey: NSPrintScalingFactor) as! CGFloat)
return pageHeight / scale
}
func calculatePrintWidth() -> Float {
let pi = NSPrintOperation.current()!.printInfo
let paperSize = pi.paperSize
let pageWidth = Float(paperSize.width - pi.leftMargin - pi.rightMargin)
let scale = Float(pi.dictionary().object(forKey: NSPrintScalingFactor) as! CGFloat)
return pageWidth / scale
}
override func knowsPageRange(_ range: NSRangePointer) -> Bool {
range.pointee.location = 1
range.pointee.length = 1
return true
}
override func rectForPage(_ page: Int) -> NSRect {
let bounds = self.bounds
let pageHeight = calculatePrintHeight()
let rect = NSMakeRect( NSMinX(bounds), NSMinY(bounds), frame.width, CGFloat(pageHeight));
return rect;
}
}
How to print NSCollectionView with all available content?

How to use a NSNumberFormatter with an AttributedString?

I have been unable to find anything that works on the subject of using an attributed text in a NSTextField with a NumberFormatter. What I want to accomplish is very simple. I would like to use a NumberFormatter on an editable NSTextField with attributed text and keep the text attributed.
Currently, I have subclassed NSTextFieldCell and implemented it as so:
class AdjustTextFieldCell: NSTextFieldCell {
required init(coder: NSCoder) {
super.init(coder: coder)
let attributes = makeAttributes()
allowsEditingTextAttributes = true
attributedStringValue = AttributedString(string: stringValue, attributes: attributes)
//formatter = TwoDigitFormatter()
}
func makeAttributes() -> [String: AnyObject] {
let style = NSMutableParagraphStyle()
style.minimumLineHeight = 100
style.maximumLineHeight = 100
style.paragraphSpacingBefore = 0
style.paragraphSpacing = 0
style.alignment = .center
style.lineHeightMultiple = 1.0
style.lineBreakMode = .byTruncatingTail
let droidSansMono = NSFont(name: "DroidSansMono", size: 70)!
return [NSParagraphStyleAttributeName: style, NSFontAttributeName: droidSansMono, NSBaselineOffsetAttributeName: -60]
}
}
This implementation adjusts the text in the NSTextField instance to have the shown attributes. When I uncomment the line that sets the formatter property the NSTextField loses its attributes. My NumberFormatter is as follows:
class TwoDigitFormatter: NumberFormatter {
override init() {
super.init()
let customAttribs = makeAttributes()
textAttributesForNegativeValues = customAttribs.attribs
textAttributesForPositiveValues = customAttribs.attribs
textAttributesForZero = customAttribs.attribs
textAttributesForNil = customAttribs.attribs
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let maxLength = 2
let wrongCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
override func isPartialStringValid(_ partialString: String, newEditingString newString: AutoreleasingUnsafeMutablePointer<NSString?>?, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
if partialString.characters.count > maxLength {
return false
}
if partialString.rangeOfCharacter(from: wrongCharacterSet) != nil {
return false
}
return true
}
override func attributedString(for obj: AnyObject, withDefaultAttributes attrs: [String : AnyObject]? = [:]) -> AttributedString? {
let stringVal = string(for: obj)
guard let string = stringVal else { return nil }
let customAttribs = makeAttributes()
var attributes = attrs
attributes?[NSFontAttributeName] = customAttribs.font
attributes?[NSParagraphStyleAttributeName] = customAttribs.style
attributes?[NSBaselineOffsetAttributeName] = customAttribs.baselineOffset
return AttributedString(string: string, attributes: attributes)
}
func makeAttributes() -> (font: NSFont, style: NSMutableParagraphStyle, baselineOffset: CGFloat, attribs: [String: AnyObject]) {
let style = NSMutableParagraphStyle()
style.minimumLineHeight = 100
style.maximumLineHeight = 100
style.paragraphSpacingBefore = 0
style.paragraphSpacing = 0
style.alignment = .center
style.lineHeightMultiple = 1.0
style.lineBreakMode = .byTruncatingTail
let droidSansMono = NSFont(name: "DroidSansMono", size: 70)!
return (droidSansMono, style, -60, [NSParagraphStyleAttributeName: style, NSFontAttributeName: droidSansMono, NSBaselineOffsetAttributeName: -60])
}
}
As you can see from the code directly above I have tried:
Setting the textAttributesFor... properties.
Overriding attributedString(for obj: AnyObject, withDefaultAttributes attrs: [String : AnyObject]? = [:])
I have tried both these solution separately from each other and together, of which none of the attempts worked.
TLDR: Is it possible to use attributed text and a NumberFormatter at the same time? If so, how? If not, how can I limit a NSTextField with attributed text to digits only and two characters without using a NumberFormatter?
For anyone in the future who wants to achieve the behavior I was able to do it by subclassing NSTextView and essentially faking a NSTextField like so:
class FakeTextField: NSTextView {
required init?(coder: NSCoder) {
super.init(coder: coder)
delegate = self
let droidSansMono = NSFont(name: "DroidSansMono", size: 70)!
configure(lineHeight: frame.size.height, alignment: .center, font: droidSansMono)
}
func configure(lineHeight: CGFloat, alignment: NSTextAlignment, font: NSFont) {
//Other Configuration
//Define and set typing attributes
let style = NSMutableParagraphStyle()
style.minimumLineHeight = lineHeight
style.maximumLineHeight = lineHeight
style.alignment = alignment
style.lineHeightMultiple = 1.0
typingAttributes = [NSParagraphStyleAttributeName: style, NSFontAttributeName: font]
}
}
extension TextField: NSTextViewDelegate {
func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
if let oldText = textView.string, let replacement = replacementString {
let newText = (oldText as NSString).replacingCharacters(in: affectedCharRange, with: replacement)
let numberOfChars = newText.characters.count
let wrongCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
let containsWrongCharacters = newText.rangeOfCharacter(from: wrongCharacterSet) != nil
return numberOfChars <= 2 && !containsWrongCharacters
}
return false
}
}
With this class, all you need to do is set the NSTextView in your storyboard to this class and change the attributes and shouldChangeTextIn to suit your needs. In this implementation, the "TextField" has various attributes set and is limited to two characters and only digits.
You subclass NSNumberFormatter with the following:
final class ChargiePercentageNumberFormatter: NumberFormatter {
override func attributedString(for obj: Any, withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString? {
guard let obj = obj as? NSNumber else {
return nil;
}
guard let numberString = self.string(from: obj) else {
return nil
}
let font = NSFont(name: "Poppins-Regular", size: 16)!
let attrs: [NSAttributedString.Key: Any] = [.font: font, .kern: 1.2]
let result = NSAttributedString(string: numberString, attributes: attrs)
return result
}
}