Change NSTextField text color when in edit mode - appkit

Using NSTextField I'd like to change the text color from black to white when going into edit mode.
Right now, it:
Defaults to black when not selected (GOOD)
Defaults to white when selected (GOOD)
Changes color to black when editing starts (NOT GOOD), would like for it to be white.
I know I could set the textColor property to .white but that would also set the color to white when the row is not selected (see point 1.), which I don't want. Huge thanks in advance!
Code I'm using now for reference, note is part of a NSViewRepresentable struct.
func makeNSView(context:Context) -> NSTextField{
let osTextView = NSTextField(string: inputText)
osTextView.maximumNumberOfLines = 1
osTextView.isBordered = false
osTextView.delegate = context.coordinator
osTextView.drawsBackground = true
osTextView.backgroundColor = .none
//osTextView.textColor = .white
return osTextView
}
I've tried adding the following and it almost gets the job done, however it only triggers when edit begins as opposed to when NSTextField becomes editable.
func control(_ control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
fieldEditor.textColor = .white
return true
}

So the way I managed to solve it is by subclassing NSTextField and overriding becomeFirstResponder. As that only triggers when the field goes into edit mode I can set there the color to white.
import AppKit
final class NSTextFieldFirstResponderAware: NSTextField{
override func becomeFirstResponder() -> Bool {
self.textColor = .white
return super.becomeFirstResponder()
}
}
Once the user finishes I reset the color at
func controlTextDidEndEditing(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else {
return
}
textField.textColor = nil
}
You may wonder why I don't reset it at resignsFirstResponder, the reason is this (TLDR -> would not work).
And here is the end result:

Related

How to draw NSPopupButtonCell highlighted image

I have NSPopupButton inside NSToolbar that has borderder = false which is having some color burn blend when highlighted. If I use bordered = true the image is drawn with nice dark overlay.
I am trying to achieve to draw highlighted state the same way as it is in bordered=true
PS: NSButtonCell with bordered = false works out of the box.
I can achieve the behaviour by having bordered = true and overriding drawBezel and do nothing there but I want to know
Things I tried:
highlightsBy
interiorBackgroundStyle
setCellAttribute
-
class ToolbarPopUpButtonCell : NSPopUpButtonCell {
override var isHighlighted: Bool {
get { return true }
set { super.isHighlighted = newValue }
}
override func drawImage(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawImage(withFrame: cellFrame, in: controlView)
}
//used in case bordered = true so we do nothing
override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {
}
//doesn't work
override var interiorBackgroundStyle: NSView.BackgroundStyle
{
return .raised
}
}
class ToolbarPopUpButton: NSPopUpButton {
override func awakeFromNib() {
cell?.setCellAttribute(.cellLightsByBackground, to: 1)
}
override var intrinsicContentSize: NSSize {
return NSMakeSize(32 + 5, 32)
}
}
Notice the image on right which works for bordered = false (NSButtonCell)
The only hack I found is to draw a subsitute cell. Still seeking a better solution.
- (void)drawImageWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSImage *image = [self image];
NSButtonCell *swapCell = [[NSButtonCell alloc] initImageCell:image];
[swapCell setHighlighted:[self isHighlighted]];
[swapCell drawImage:image withFrame:cellFrame inView:controlView];
}

Setting Background Color of NSTableCellView in Swift

After searching through SO and online, I'm struggling to figure out a concept that I thought would be relatively simple. Essentially, I have a table in an OS X Swift app, with several columns, and it is currently populating data. I am trying to discern how I can set the background color of each "row" (ideally with alternating colors, but I'll start with just one color). My MasterViewController file is like so;
import Cocoa
class MasterViewController: NSViewController {
var minions = [Minion]()
func setupSampleMinion() {
minions = Minion.fetchMinionData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
// MARK: - NSTableViewDataSource extension MasterViewController: NSTableViewDataSource {
func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
return self.minions.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
// 1
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
let minion = self.minions[row]
// 2
if tableColumn!.identifier == "MyColumn" {
// 3
cellView.imageView!.image = NSImage(named: "minion.name!")
cellView.textField!.stringValue = minion.name!
return cellView
}
return cellView
}
}
func tableView(tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let myCustomView = MyRowView()
return myCustomView
}
class MyRowView: NSTableRowView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
self.backgroundColor = NSColor(red: 0.76, green: 0.82, blue: 0.92, alpha: 1)
NSRectFill(dirtyRect)
}
}
// MARK: - NSTableViewDelegate extension MasterViewController: NSTableViewDelegate {
}
While I THINK I have some of the coding right, this does not seem to set the background color if the row in any way. Any thoughts or overall guidance would be most appreciated. Thank you!
If you just want the rows to use the standard alternating colors for rows, there's a simple checkbox in the Attributes inspector for the table view in IB to enable that.
To use a non-standard background color, you want to set the row view's backgroundColor, but not inside of drawRect(). If you change properties of a view that affect how it draws inside of drawRect(), that will probably mark the view as needing display, which will provoke another call to drawRect(), etc.
It should work to just set it in the delegate's tableView(_:didAddRowView:forRow:) method. That's documented in the description of the backgroundColor property.
With regard to your attempt at overriding drawRect(): setting the row view's backgroundColor will presumably affect how the superclass draws. So, setting it after calling through to super is unlikely to help. It definitely won't affect the subsequent NSRectFill() call. That function relies on the fill color set for the current graphics context, which is implicit. You would change that by calling someColor.set().
Buy, anyway, there should be no need to override drawRect() given that you can set the backgroundColor. If you want to achieve some background drawing beyond what's possible by just setting a color, you should override drawBackgroundInRect() and not drawRect(), anyway.
Finally, your implementation of tableView(tableView:rowViewForRow:) should call the table view's makeViewWithIdentifier(_:owner:) method first, before creating a new view. And it should set the identifier on any new view it does create. That allows the table view to maintain a reuse queue of views, to avoid constantly destroying and recreating views.

Change NSTextField border and BG color while editing

I have a NSTextField that uses no border and window background color while it is displayed but I want it to change to have the default border and white BG color when being edited. I know I can change these properties with:
nameTextField.bezeled = true
nameTextField.backgroundColor = NSColor.textBackgroundColor()
What I don't know is how to be notified when the textfield is started to be edited and when it's ended. There seem to be no actions for this. Is there any other way how could this behaviour be achieved?
EDIT:
Actually, edit end can be detected via the textfield's change action when setting Action to "Send on End Editing" so that's solved but still how do I detect start editing?
You can set the delegate of NSTextField:
nameTextField.delegate = self
then you can set a different state:
func control(control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
nameTextField.bezeled = true
nameTextField.backgroundColor = NSColor.textBackgroundColor()
return true
}
func control(control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool {
nameTextField.bezeled = false
nameTextField.backgroundColor = NSColor.windowBackgroundColor()
return true
}
EDIT:
I think you can subclass NSTextField and override the becomeFirstResponder and the resignFirstResponder, then you know the NSTextField has the focus or not.

Resizing NSWindow to match view controller size in storyboard

I am working on Xcode 6.1.1 on OSX 10.10. I am trying out storyboards for Mac apps. I have a NSTabViewController using the new NSTabViewControllerTabStyleToolbar tabStyle and it is set as the default view controller for the window controller. How do I make my window resize according to the current selected view controller?
Is it possible to do entirely in Interface Builder?
Here is what my storyboard looks like:
The auto layout answer is half of it. You need to set the preferredContentSize in your ViewController for each tab to the fitting size (if you wanted the tab to size to the smallest size satisfying all constraints).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = view.fittingSize
}
If your constraints are causing an issue below try first with a fixed size, the example below sets this in the tab item's view controller's viewWillAppear function (Swift used here, but the Objective-C version works just as well).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
If that works, fiddle with your constraints to figure out what's going on
This solution for 'toolbar style' tab view controllers does animate and supports the nice crossfade effect. In the storyboard designer, add 'TabViewController' in the custom class name field of the NSTabViewController. Don't forget to assign a title to each viewController, this is used as a key value.
import Cocoa
class TabViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
// Add size of first tab to tabViewSizes
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
super.viewDidLoad()
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(from: fromViewController, to: toViewController, options: [.crossfade, .allowUserInteraction], completionHandler: completion)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title, let window = view.window else {
return
}
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
}
else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}
The window containing a toolbar style tab view controller does resize without any code if you have auto layout constraints in your storyboard tab views (macOS 11.1, Xcode 12.3). I haven't tried other style tab view controllers.
If you want to resize with animation as in Finder, it is sufficient to add one override in your tab view controller. It will resize the window with system-calculated resize animation time and will hide the tab view during resize animation:
class PreferencesTabViewController: NSTabViewController {
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
guard let window = view.window else {
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
return
}
let fromSize = window.frame.size
let toSize = window.frameRect(forContentRect: toViewController.view.frame).size
let widthDelta = toSize.width - fromSize.width
let heightDelta = toSize.height - fromSize.height
var toOrigin = window.frame.origin
toOrigin.x += widthDelta / 2
toOrigin.y -= heightDelta
let toFrame = NSRect(origin: toOrigin, size: toSize)
NSAnimationContext.runAnimationGroup { context in
context.duration = window.animationResizeTime(toFrame)
view.isHidden = true
window.animator().setFrame(toFrame, display: false)
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
} completionHandler: { [weak self] in
self?.view.isHidden = false
}
}
}
Please adjust closure syntax if you are using Swift versions older than 5.3.
Use autolayout. Set explicit size constraints on you views. Or once you have entered the UI into each tab view item's view set up the internal constraints such that they force view to be the size you want.

Growing NSTableView Row Height

I have a view-based NSTableView in a MacOSX app that structures data nicely. I would like to implement the NSTableView to have row heights which grow with the content of the data entered into one of the NSTextViews. I've subclassed an NSTextView to "grow" with the user text but the issue is that having the field embedded in the TableView causes the field to be clipped.
Does anyone have any suggestions as to how to go about implementing a growing row size?
You need to implement -tableView:heightOfRow: in your table view delegate and return the appropriate height for the row. Furthermore, you need to monitor the text views for changes in their height and call -noteHeightOfRowsWithIndexesChanged: on the table view when any of them changes. To monitor the height of the text views, it should suffice to observe the NSViewFrameDidChangeNotification that they will post.
(If you're using auto layout generally in your UI, I think you will have to leave the text views with translatesAutoresizingMaskIntoConstraints on, place them manually, and set their autoresizing masks as appropriate. Then, you would avoid setting any other constraints on them. This is because you need the frame to be set by the text layout manager but not by auto layout.)
I've managed to implement this in Swift 3 (with the help of this great tip and this and this SO answer):
Make sure the table view cell in the NSTableView has a delegate connection to your subclass/view controller which adopts the NSTextFieldDelegate protocol.
Also give it these constraints to make sure it resizes according to the height of the row:
In the delegate use this code:
var editedString: String? = nil
var textCellForHeight: NSTextFieldCell = NSTextFieldCell.init()
func control(_ control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
editedString = fieldEditor.string ?? ""
return true
}
func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool {
editedString = nil
return true
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(insertNewline(_:)) {
textView.insertNewlineIgnoringFieldEditor(self)
editedString = textView.string ?? ""
//The NSAnimationContext lines get rid of the animation that normally happens when the height changes
NSAnimationContext.beginGrouping()
NSAnimationContext.current().duration = 0
myTableView.noteHeightOfRows(withIndexesChanged: IndexSet.init(integer: selected))
NSAnimationContext.endGrouping()
myTable.needsDisplay = true
return true
}
return false
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
if let temp = editedString { //we know it’s currently being edited if it’s not nil
textCellForHeight.stringValue = temp
//in my case, there was a specific table column that I knew would be edited
//you might need to decide on the column width another way.
let column = myTable.tableColumns[myTable.column(withIdentifier: “TheColumnThatsBeingEdited”)]
let frame: NSRect = NSMakeRect(0, 0, column.width, CGFloat.greatestFiniteMagnitude)
return textCellForHeight.cellSize(forBounds: frame).height
}
return yourStandardHeightCGFloat
}

Resources