How can I implement a tab bar like the ones in Xcode? - macos

There does not seem to be any standard AppKit control for creating a tab bar similar to the ones found in Xcode.
Any idea on whether this is possible or would I need to use a custom control of some sort, and if so any suggestions on whether any are available.

OK I figured it out by subclassing NSSegementedControl to get the desired behaviours.
import Cocoa
class OSSegmentedCell: NSSegmentedCell {
override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
// Do not call super to prevent the border from being draw
//super.draw(withFrame: cellFrame, in: controlView)
// Call this to ensure the overridden drawSegment() method gets called for each segment
super.drawInterior(withFrame: cellFrame, in: controlView)
}
override func drawSegment(_ segment: Int, inFrame frame: NSRect, with controlView: NSView) {
// Resize the view to leave a small gap
let d:CGFloat = 0.0
let width = frame.height - 2.0*d
let dx = (frame.width - width) / 2.0
let newRect = NSRect(x: frame.minX+dx, y: frame.minY, width: width, height: width)
if let image = self.image(forSegment: segment) {
if self.selectedSegment == segment {
let tintedImage = image.tintedImageWithColor(color: NSColor.blue)
tintedImage.draw(in: newRect)
} else {
image.draw(in: newRect)
}
}
}
}
extension NSImage {
func tintedImageWithColor(color:NSColor) -> NSImage {
let size = self.size
let imageBounds = NSMakeRect(0, 0, size.width, size.height)
let copiedImage = self.copy() as! NSImage
copiedImage.lockFocus()
color.set()
__NSRectFillUsingOperation(imageBounds, NSCompositingOperation.sourceIn)
copiedImage.unlockFocus()
return copiedImage
}
}

Related

How to efficiently draw a Core Image with a Filter into an NSView?

I am applying a perspective Core Image filter to transform and draw a CIImage into a custom NSView and it seems slower than I expected (e.g, I drag a slider that alters the perspective transformation and the drawing lags behind the slider value). Here is my custom drawRect method where self.mySourceImage is a CIImage:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
if (self.perspectiveFilter == nil)
self.perspectiveFilter = [CIFilter filterWithName:#"CIPerspectiveTransform"];
[self.perspectiveFilter setValue:self.mySourceImage
forKey:#"inputImage"];
[self.perspectiveFilter setValue: [CIVector vectorWithX:0 Y:0]
forKey:#"inputBottomLeft"];
// ... set other vector parameters based on slider value
CIImage *outputImage = [self.perspectiveFilter outputImage];
[outputImage drawInRect:dstrect
fromRect:srcRect
operation:NSCompositingOperationSourceOver
fraction:0.8];
}
Here is an example output:
My experience with image filters tells me that this should be much faster. Is there some "best practice" that I am missing to speed this up?
Note that I only create the filter once (stored as a property).
I did make sure the view has a CALayer for a backing store. Should I be adding the filter to a CALayer somehow?
Note that I never create a CIContext -- I assume there is an implicit context used by NSView? Should I create a CIContext and render to an image and draw the image?
Here's how I use a GLKView in UIKit:
I prefer subclassing GLKView to allow for a few things:
initializing from code
overriding draw(rect:) for the UIImageView equivalence of contentMode (aspect fit in particular)
when using scaleAspectFit, creating a "clear color" for the background color to match the surrounding superviews
That said, here's what I have:
import GLKit
class ImageView: GLKView {
var renderContext: CIContext
var rgb:(Int?,Int?,Int?)!
var myClearColor:UIColor!
var clearColor: UIColor! {
didSet {
myClearColor = clearColor
}
}
var image: CIImage! {
didSet {
setNeedsDisplay()
}
}
var uiImage:UIImage? {
get {
let final = renderContext.createCGImage(self.image, from: self.image.extent)
return UIImage(cgImage: final!)
}
}
init() {
let eaglContext = EAGLContext(api: .openGLES2)
renderContext = CIContext(eaglContext: eaglContext!)
super.init(frame: CGRect.zero)
context = eaglContext!
self.translatesAutoresizingMaskIntoConstraints = false
}
override init(frame: CGRect, context: EAGLContext) {
renderContext = CIContext(eaglContext: context)
super.init(frame: frame, context: context)
enableSetNeedsDisplay = true
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
let eaglContext = EAGLContext(api: .openGLES2)
renderContext = CIContext(eaglContext: eaglContext!)
super.init(coder: aDecoder)
context = eaglContext!
self.translatesAutoresizingMaskIntoConstraints = false
}
override func draw(_ rect: CGRect) {
if let image = image {
let imageSize = image.extent.size
var drawFrame = CGRect(x: 0, y: 0, width: CGFloat(drawableWidth), height: CGFloat(drawableHeight))
let imageAR = imageSize.width / imageSize.height
let viewAR = drawFrame.width / drawFrame.height
if imageAR > viewAR {
drawFrame.origin.y += (drawFrame.height - drawFrame.width / imageAR) / 2.0
drawFrame.size.height = drawFrame.width / imageAR
} else {
drawFrame.origin.x += (drawFrame.width - drawFrame.height * imageAR) / 2.0
drawFrame.size.width = drawFrame.height * imageAR
}
rgb = myClearColor.rgb()
glClearColor(Float(rgb.0!)/256.0, Float(rgb.1!)/256.0, Float(rgb.2!)/256.0, 0.0);
glClear(0x00004000)
// set the blend mode to "source over" so that CI will use that
glEnable(0x0BE2);
glBlendFunc(1, 0x0303);
renderContext.draw(image, in: drawFrame, from: image.extent)
}
}
}
A few notes:
The vast majority of this was taken from something written a few years back (in Swift 2 I think) from objc.io with the associated GitHub project. In particular, check out their GLKView subclass that has code for scaleAspectFill and other content modes.
Note the usage of a single CIContext called renderContext. I use it to create a UIImage when needed (in iOS you "share" a UIImage).
I use a didSet with the image property to automatically call setNeedsDisplay when the image changes. (I also call this explicitly when an iOS device changes orientation.) I do not know the macOS equivalent of this call.
I hope this gives you a good start for using OpenGL in macOS. If it's anything like UIKit, trying to put a CIImage in an NSView doesn't involve the GPU, which is a bad thing.

Change NSSlider bar color

I've got an NSWindow and an horizontal NSSlider.
I'd like to change the color of the right part of the slider bar when the window background color changes.
Currently, the right part of the bar isn't visible anymore when the window background is dark.
Note: the window background is actually a gradient I'm drawing by overriding drawRect in a subclass of the window's view.
I thought I could change the slider bar fill color by subclassing NSSliderCell and overriding some method like drawBarInside but I don't understand how it works: should I make a little square image and draw it repeatedly in the rect after the knob? Or maybe just draw a colored line? I've never done this before.
I've looked at this question and it's interesting but it's about drawing the knob and I don't need that for now.
I also had a look at this one which seemed very promising, but when I try to mimic this code my slider bar just disappears...
In this question they use drawWithFrame and it looks interesting but again I'm not sure how it works.
I would like to do this with my own code instead of using a library. Could somebody give me a hint about how to do this please? :)
I'm doing this in Swift but I can read/use Objective-C if necessary.
First, I created an image of a slider bar and copied it in my project.
Then I used this image in the drawBarInside method to draw in the bar rect before the normal one, so we'll see only the remainder part (I wanted to keep the blue part intact).
This has to be done in a subclass of NSSliderCell:
class CustomSliderCell: NSSliderCell {
let bar: NSImage
required init(coder aDecoder: NSCoder) {
self.bar = NSImage(named: "bar")!
super.init(coder: aDecoder)
}
override func drawBarInside(aRect: NSRect, flipped: Bool) {
var rect = aRect
rect.size = NSSize(width: rect.width, height: 3)
self.bar.drawInRect(rect)
super.drawBarInside(rect, flipped: flipped)
}
}
Pro: it works. :)
Con: it removes the rounded edges of the bar and I haven't found a way to redraw this yet.
UPDATE:
I made a Swift version of the accepted answer, it works very well:
class CustomSliderCell: NSSliderCell {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawBarInside(aRect: NSRect, flipped: Bool) {
var rect = aRect
rect.size.height = CGFloat(5)
let barRadius = CGFloat(2.5)
let value = CGFloat((self.doubleValue - self.minValue) / (self.maxValue - self.minValue))
let finalWidth = CGFloat(value * (self.controlView!.frame.size.width - 8))
var leftRect = rect
leftRect.size.width = finalWidth
let bg = NSBezierPath(roundedRect: rect, xRadius: barRadius, yRadius: barRadius)
NSColor.orangeColor().setFill()
bg.fill()
let active = NSBezierPath(roundedRect: leftRect, xRadius: barRadius, yRadius: barRadius)
NSColor.purpleColor().setFill()
active.fill()
}
}
This is correct, you have to subclass the NSSliderCell class to redraw the bar or the knob.
NSRect is just a rectangular container, you have to draw inside this container. I made an example based on an custom NSLevelIndicator that I have in one of my program.
First you need to calculate the position of the knob. You must pay attention to the control minimum and maximum value.
Next you draw a NSBezierPath for the background and another for the left part.
#import "MyCustomSlider.h"
#implementation MyCustomSlider
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
// [super drawBarInside:rect flipped:flipped];
rect.size.height = 5.0;
// Bar radius
CGFloat barRadius = 2.5;
// Knob position depending on control min/max value and current control value.
CGFloat value = ([self doubleValue] - [self minValue]) / ([self maxValue] - [self minValue]);
// Final Left Part Width
CGFloat finalWidth = value * ([[self controlView] frame].size.width - 8);
// Left Part Rect
NSRect leftRect = rect;
leftRect.size.width = finalWidth;
NSLog(#"- Current Rect:%# \n- Value:%f \n- Final Width:%f", NSStringFromRect(rect), value, finalWidth);
// Draw Left Part
NSBezierPath* bg = [NSBezierPath bezierPathWithRoundedRect: rect xRadius: barRadius yRadius: barRadius];
[NSColor.orangeColor setFill];
[bg fill];
// Draw Right Part
NSBezierPath* active = [NSBezierPath bezierPathWithRoundedRect: leftRect xRadius: barRadius yRadius: barRadius];
[NSColor.purpleColor setFill];
[active fill];
}
#end
Swift 5
class CustomSliderCell: NSSliderCell {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawBar(inside aRect: NSRect, flipped: Bool) {
var rect = aRect
rect.size.height = CGFloat(5)
let barRadius = CGFloat(2.5)
let value = CGFloat((self.doubleValue - self.minValue) / (self.maxValue - self.minValue))
let finalWidth = CGFloat(value * (self.controlView!.frame.size.width - 8))
var leftRect = rect
leftRect.size.width = finalWidth
let bg = NSBezierPath(roundedRect: rect, xRadius: barRadius, yRadius: barRadius)
NSColor.orange.setFill()
bg.fill()
let active = NSBezierPath(roundedRect: leftRect, xRadius: barRadius, yRadius: barRadius)
NSColor.purple.setFill()
active.fill()
}
}
I have achieved this without redraw or override cell at all. Using "False color" filter seems work very well and it is only a few codes!
class RLSlider: NSSlider {
init() {
super.init(frame: NSZeroRect)
addFilter()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addFilter()
}
func addFilter() {
let colorFilter = CIFilter(name: "CIFalseColor")!
colorFilter.setDefaults()
colorFilter.setValue(CIColor(cgColor: NSColor.white.cgColor), forKey: "inputColor0")
colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor1")
// colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor0")
// colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor1")
self.contentFilters = [colorFilter]
}
}

How to make a UIImageView tappable and cause it to do something? (Swift)

(I just started using Swift a few days ago and am relatively new to programming, so please bear with me.) I am trying to make random blocks appear on the screen, and the user must tap them to make them disappear. I have been able to create the blocks, but I have no idea how to actually make them tappable. Can someone please help me? This is my code so far:
func createBlock(){
let imageName = "block.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: xPosition, y: -50, width: size, height: size)
self.view.addSubview(imageView)
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
imageView.backgroundColor = UIColor.redColor()
imageView.frame = CGRect(x: self.xPosition, y: 590, width: self.size, height: self.size)
}, completion: { animationFinished in
imageView.removeFromSuperview()
})
}
Edit: This is the new code I am trying:
func createBlock(){
let imageName = "block.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: xPosition, y: -50, width: size, height: size)
self.view.addSubview(imageView)
imageView.userInteractionEnabled = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("imageTapped"))
imageView.addGestureRecognizer(tapRecognizer)
func imageTapped(gestureRecognizer: UITapGestureRecognizer) {
let tappedImageView = gestureRecognizer.view!
tappedImageView.removeFromSuperview()
score += 10
}
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
imageView.backgroundColor = UIColor.redColor()
imageView.frame = CGRect(x: self.xPosition, y: 590, width: self.size, height: self.size)
}, completion: { animationFinished in
imageView.removeFromSuperview()
})
}
Once you've create the view, you need to set it's userInteractionEnabled property to true. Then you need to attach a gesture to it.
imageView.userInteractionEnabled = true
//now you need a tap gesture recognizer
//note that target and action point to what happens when the action is recognized.
let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("imageTapped:"))
//Add the recognizer to your view.
imageView.addGestureRecognizer(tapRecognizer)
Now you still need the function, in this case imageTapped:, which is where the action happens when the gesture is recognized. The gesture that was recognized will be sent as an argument, and you can find out which imageView was tapped from they gestures view property.
func imageTapped(gestureRecognizer: UITapGestureRecognizer) {
//tappedImageView will be the image view that was tapped.
//dismiss it, animate it off screen, whatever.
let tappedImageView = gestureRecognizer.view!
}
In swift 3.0:
imvEditName.isUserInteractionEnabled = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
imvEditName.addGestureRecognizer(tapRecognizer)
And the target method:
func imageTapped(sender: UIImageView) {
print("image tapped")
}
My previous answer is still accurate for adding a tap recognizer to a UIImageView, however it is not sufficient for the purposes of a view that is animating. If you are actually creating a game, you'll likely want to look into SpriteKit, as it is literally tailor made for this. However, this can be accomplished with normal UIKit. I've tested two approaches, which both work.
Both approaches worked far better on an actual device than in the simulator
Approach 1, Simpler yet slightly less precise up to a tenth of second slower in my tests.
Instead of adding a tap gesture to the UIImageView, add the gesture to the superview. Keep a reference to your blocks in an array property, and check if any blocks are intercepted by the tap.
class ViewController: UIViewController {
let size = CGFloat(40)
let xPosition = CGFloat(14)
let options = UIViewAnimationOptions.Autoreverse
var blocks = [UIImageView]()
override func viewDidLoad() {
super.viewDidLoad()
// Adding recognizer to the whole view.
let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("screenTapped:"))
view.addGestureRecognizer(tapRecognizer)
blocks.append(createBlock())
}
//changed to return the created block so it can be stored in an array.
func createBlock() -> UIImageView {
let imageName = "block.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: xPosition, y: -40, width: size, height: size)
self.view.addSubview(imageView)
UIView.animateWithDuration(2, delay: 0.0, options: options, animations: {
imageView.backgroundColor = UIColor.redColor()
imageView.frame = CGRect(x: self.xPosition, y: 590, width: self.size, height: self.size)
}, completion: { animationFinished in
imageView.removeFromSuperview()
self.blocks.append(self.createBlock())
})
return imageView
}
func screenTapped(gestureRecognizer: UITapGestureRecognizer) {
let tapLocation = gestureRecognizer.locationInView(view)
//This is to keep track of the blocks to remove from the array.
//If you remove them as you iterate the array, and you have more than
//one block to remove, you may end up removing the wrong block
//or with an index out of bounds.
var removeBlocks = [Int]()
for (index, block) in enumerate(blocks) {
//this is the frame of the view as we see it on screen.
//unfortunately block.frame is not where we see it making a recognizer
//on the block itself not work.
if block.layer.presentationLayer().frame.contains(tapLocation) {
//Then this block was clicked.
block.removeFromSuperview()
removeBlocks.append(index)
}
}
//get the indexes ro remove backwards so we are removing from
//back to front.
for index in removeBlocks.reverse() {
blocks.removeAtIndex(index)
}
}
}
Approach 2, Best Performance outside of SpriteKit
In approach two, you subclass UIView, and set your main view to it instead of the UIView. You only need to override a single method. I'm storing the block array in the view for convenience.
First the TouchView.
import UIKit
class TouchView: UIView {
var blocks = [UIImageView]()
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// ignoring multiple touches for now
if touches.count == 1 {
if let touch = touches.first as? UITouch {
let touchLocation = touch.locationInView(self)
//This is to keep track of the blocks to remove from the array.
//If you remove them as you iterate the array, and you have more than
//one block to remove, you may end up removing the wrong block
//or with an index out of bounds.
var removeBlocks = [Int]()
for (index, block) in enumerate(blocks) {
//this is the frame of the view as we see it on screen.
//unfortunately block.frame is not where we see it making a recognizer
//on the block itself not work.
if block.layer.presentationLayer().frame.contains(touchLocation) {
//Then this block was clicked.
block.removeFromSuperview()
removeBlocks.append(index)
}
}
//get the indexes ro remove backwards so we are removing from
//back to front.
for index in removeBlocks.reverse() {
blocks.removeAtIndex(index)
}
}
}
}
}
Now the ViewController would look like:
import UIKit
class ViewController: UIViewController {
let size = CGFloat(40)
let xPosition = CGFloat(14)
let options = UIViewAnimationOptions.Autoreverse
var blocks = [UIImageView]()
// computed get only property for conveniece of accessing block array
var touchView:TouchView {
get {
return self.view as! TouchView
}
}
override func viewDidLoad() {
super.viewDidLoad()
touchView.blocks.append(createBlock())
// Do any additional setup after loading the view, typically from a nib.
}
func createBlock() -> UIImageView {
let imageName = "block.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: xPosition, y: -40, width: size, height: size)
self.view.addSubview(imageView)
UIView.animateWithDuration(2, delay: 0.0, options: options, animations: {
imageView.backgroundColor = UIColor.redColor()
imageView.frame = CGRect(x: self.xPosition, y: 590, width: self.size, height: self.size)
}, completion: { animationFinished in
imageView.removeFromSuperview()
self.touchView.blocks.append(self.createBlock())
})
return imageView
}
}
In my tests, this seems to work pretty good, even with quickly animated "blocks". Again, if you are planning on making a full fledged game, you should really look at SpriteKit. There is a really simple tutorial here that should get you started. If you can remove the accepted answer from my previous answer, that would help others not be lead down the wrong path, unless they only are looking to add a gesture to a non-moving UIImageView.
Instead using a UIImageView, you could try use a Custome UIButton and replace the default button image with yours. Hence, you do not need to add UITapGestureRecognizer for each UIImageView. #JeremyPope 's approach is actually more efficient compare to this approach.
Handling UIButton is pretty simple, its subclass of UIControl so there are properties like hide and enabled and you can manipulate them in the way you want.
What you need to do is to create UIButton programmaticly, just like you they way you creating UIImageView. The way to create them is similar to UIImageView, read more on create UIButton programmaticly in Swift.
The image below is how a custom UIButton looks like.
Leave comments if you need more info.
By Default, image view user interaction is not enabled, so be sure to enable it by setting it to true.
let myImageView = UIImageView()
myImageView.isUserInteractionEnabled = true

NSPopUpButton arrow color

Is there a way to customize the color of a NSPopUpButton arrow? I've looked around but I've not found an answer yet
I really dont think there is an "easy" way to do this. If you look at the API description, it even states that it doesnt respond to the setImage routine. I have done quite a bit of work sub-classing button objects, etc... and I think this is where you would have to go in order to do what you are asking.
Like too many of these controls, I did it by subclassing NSPopupButton(Cell) and then doing all my own drawing in drawRect...I cheated a little though, and used an image do the actual triangle rather than trying to do it via primitives.
- (void)drawRect:(NSRect)dirtyRect
{
//...Insert button draw code here...
//Admittedly the above statement includes more work than we probably want to do.
//Assumes triangleIcon is a cached NSImage...I also make assumptions about location
CGFloat iconSize = 6.0;
CGFloat iconYLoc = (dirtyRect.size.height - iconSize) / 2.0;
CGFloat iconXLoc = (dirtyRect.size.width - (iconSize + 8));
CGRect triRect = {iconXLoc, iconYLoc, iconSize, iconSize};
[triangleIcon drawInRect:triRect];
}
i did this and its worked for me.
(void)drawImageWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSPopUpButton *temp = (NSPopUpButton*)controlView;
NSString *strtile = temp.title;
AppDelegate *appdel = (AppDelegate*)[NSApplication sharedApplication].delegate;
NSFont *font = [NSFont systemFontOfSize:13.5];
NSSize size = NSMakeSize(40, 10);// string size
CGRect rect = controlView.frame;
rect = CGRectMake((size.width + temp.frame.size.width)/2, rect.origin.y, 8, 17);
[self drawImage:[NSImage imageNamed:#"icon_downArrow_white.png"] withFrame:rect inView:self.
}
I have changed arrow color by using "False Color" filter without using any image. So far it is the easiest way to change cocoa control to me.
class RLPopUpButton: NSPopUpButton {
init() {
super.init(frame: NSZeroRect, pullsDown: false)
addFilter()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addFilter()
}
func addFilter() {
let colorFilter = CIFilter(name: "CIFalseColor")!
colorFilter.setDefaults()
colorFilter.setValue(CIColor(cgColor: NSColor.black.cgColor), forKey: "inputColor0")
colorFilter.setValue(CIColor(cgColor: NSColor.white.cgColor), forKey: "inputColor1")
// colorFilter.setValue(CIColor(cgColor: NSColor.yellow.cgColor), forKey: "inputColor0")
// colorFilter.setValue(CIColor(cgColor: NSColor.property.cgColor), forKey: "inputColor1")
self.contentFilters = [colorFilter]
}
}
Swift 5
In interface builder, remove default arrow setting.
Then, apply this subclass for cell, which will add an NSImageView to the right side of the NSPopUpButton.
This way you have complete control over what you set as your custom button and how you position it.
import Cocoa
#IBDesignable class NSPopUpButtonCellBase: NSPopUpButtonCell {
let textColor = NSColor(named: "white")!
let leftPadding: CGFloat = 16
let rightPadding: CGFloat = 30
override func awakeFromNib() {
super.awakeFromNib()
let imageView = NSImageView()
imageView.image = NSImage(named: "ic_chevron_down")!
controlView!.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: CGFloat(20)).isActive = true
imageView.heightAnchor.constraint(equalToConstant: CGFloat(20)).isActive = true
imageView.trailingAnchor.constraint(equalTo: controlView!.trailingAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: controlView!.centerYAnchor).isActive = true
}
// overriding this removes the white container
override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {
}
// overriding this allows us to modify paddings to text
override func titleRect(forBounds cellFrame: NSRect) -> NSRect {
// this gets rect, which has title's height, not the whole control's height
// also, it's origin.y is such that it centers title
let processedTitleFrame = super.titleRect(forBounds: cellFrame)
let paddedFrame = NSRect(
x: cellFrame.origin.x + leftPadding,
y: processedTitleFrame.origin.y,
width: cellFrame.size.width - leftPadding - rightPadding,
height: processedTitleFrame.size.height
)
return paddedFrame
}
// overriding this allows us to style text
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
let attributedTitle = NSMutableAttributedString.init(attributedString: title)
let range = NSMakeRange(0, attributedTitle.length)
attributedTitle.addAttributes([NSAttributedString.Key.foregroundColor : textColor], range: range)
return super.drawTitle(attributedTitle, withFrame: frame, in: controlView)
}
}

How to fill the right bottom corner of NSScrollView?

I want to change its color, but I'm not sure wether to subclass NSScrollView or NSClipView. Or if the corner can be inserted as a regular NSView.
(source: flickr.com)
I don't need code. Just a hint at how to do it.
Already answered elsewhere on stackoverflow by mekentosj. The class to subclass is NSScrollView.
#interface MyScrollView : NSScrollView {
}
#end
#implementation MyScrollView
- (void)drawRect:(NSRect)rect{
[super drawRect: rect];
if([self hasVerticalScroller] && [self hasHorizontalScroller]){
NSRect vframe = [[self verticalScroller]frame];
NSRect hframe = [[self horizontalScroller]frame];
NSRect corner;
corner.origin.x = NSMaxX(hframe);
corner.origin.y = NSMinY(hframe);
corner.size.width = NSWidth(vframe);
corner.size.height = NSHeight(hframe);
// your custom drawing in the corner rect here
[[NSColor redColor] set];
NSRectFill(corner);
}
}
#end
Kind of odd but just subclassing NSScrollView and overriding draw with super.drawRect() made my NSScrollView (not) fill in that corner with white. I tested it 2x to make sure, since it doesn't make much sense.
import Cocoa
class ThemedScrollView: NSScrollView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}
}
Here's a Swift variation of the original answer as well:
import Cocoa
class ThemedScrollView: NSScrollView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
if hasVerticalScroller && hasHorizontalScroller {
guard verticalScroller != nil && horizontalScroller != nil else { return }
let vFrame = verticalScroller!.frame
let hFrame = horizontalScroller!.frame
let square = NSRect(origin: CGPoint(x: hFrame.maxX, y: vFrame.maxY), size: CGSize(width: vFrame.width, height: hFrame.height))
let path = NSBezierPath(rect: square)
let fillColor = NSColor.redColor()
fillColor.set()
path.fill()
}
}
}
Updated Swift 5.2 version of Austin's answer
class Themed: NSScrollView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if hasVerticalScroller && hasHorizontalScroller {
guard verticalScroller != nil && horizontalScroller != nil else { return }
let vFrame = verticalScroller!.frame
let hFrame = horizontalScroller!.frame
let square = NSRect(origin: CGPoint(x: hFrame.maxX, y: vFrame.maxY), size: CGSize(width: vFrame.width, height: hFrame.height))
let path = NSBezierPath(rect: square)
let fillColor = NSColor.red
fillColor.set()
path.fill()
}
}
}

Resources