NSMenu Subclass does not respond to mouse click - cocoa

I am subclassing an NSPopUpButton with the purpose of having control over the drawing methods of the button itself, but also the NSMenu that will pop up. Therefore I am also subclassing NSMenu and - most importantly - setting the view of each menu item to a custom NSView.
So far I have managed to come very close to the appearance of the original NSPopupButton and its menu. In the code, I provide a small window that will display an original NSButton on the left side and an instance of my custom version on the right.
However, the custom menu does not function properly. The following issues occur:
The button can be clicked and the menu will pop up. When the mouse is moved inside the menu, the item on which the pointer is hovering will highlight properly, except for the item that is selected: when the mouse exits its tracking area to the neighboring item, this one will be highlighted, but the first one will not lose highlight color. Only when entering the selected item again and then exiting it a second time it will lose the highlight properly.
Clicking an item will NOT dismiss the menu, the menu does not respond to any click within one of its items. The menu will however be dismissed when a click outside the menu occurs.
The button and the menu are fully functional when using the keyboard: Tab switches between the standard and the custom PopUpButton, space will summon the menu, the arrow buttons move the selection, and space or return will make a selection and dismiss the menu.
The first menu entry (Item 1) can not be selected, when dismissing the menu with enter or space when Item 1 is highlighted the Item that was selected before will stay selected.
Problem 4 is possibly unrelated, my main question is:
Why do the CustomMenuItemViews not respond to mouse events the way a stock Menu does? I assume that there is either a method that I have to override, a delegate that has to be set somewhere, or both, but I have not yet managed to find the part of the code where I have to hook in.
I was at least able to pinpoint the problem to the overridden method willOpenMenu - if I do not override, I get normal behavior, but of course, the menu will then be drawn by the cocoa method.
import Cocoa
import AppKit
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
window.contentViewController = MyViewController(size: NSSize(width: 200, height: 80))
}
}
class MyViewController: NSViewController {
public init(size: NSSize) {
super.init(nibName: nil, bundle: nil)
self.view = MyInnerView(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyInnerView: NSView, NSMenuDelegate {
public override init(frame: NSRect) {
super.init(frame: frame)
let standardPopUp = NSPopUpButton(title: "Switch", target: nil, action: nil)
standardPopUp.frame = NSRect(x: 10, y: constant.buttonFrameY, width: 80, height: constant.buttonFrameHeigth)
standardPopUp.addItems(withTitles: ["Item 1", "Item 2", "Item 3"])
let popUpCell = CustomPopUpButtonCell()
let customPopUp = CustomPopUpButton(title: "Switch", target: nil, action: nil)
customPopUp.cell = popUpCell
customPopUp.menu = CustomPopUpMenu()
customPopUp.menu?.delegate = self
customPopUp.frame = NSRect(x: 90, y: constant.buttonFrameY, width: 80, height: constant.buttonFrameHeigth)
customPopUp.addItems(withTitles: ["Item 1", "Item 2", "Item 3"])
self.addSubview(standardPopUp)
self.addSubview(customPopUp)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class CustomPopUpButton: NSPopUpButton {
override func drawFocusRingMask() {
// prevent focus ring drawing
}
override func becomeFirstResponder() -> Bool {
(self.cell as! CustomPopUpButtonCell).hasFocus = true
self.needsDisplay = true
return true
}
override func resignFirstResponder() -> Bool {
(self.cell as! CustomPopUpButtonCell).hasFocus = false
self.needsDisplay = true
return true
}
// this function breaks the intended behaviour
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for (index,item) in self.menu!.items.enumerated() {
item.view = MenuItemCustomView(frame: NSRect(x: 0, y: 0, width: 150, height: constant.popUpMenuCellHeigth))
item.view?.menu = menu
(item.view as! MenuItemCustomView).text = item.title
if self.indexOfSelectedItem == index {
(item.view as! MenuItemCustomView).selected = true
}
}
}
}
class CustomPopUpMenu: NSMenu {
}
class CustomPopUpButtonCell: NSPopUpButtonCell {
var hasFocus = false
override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
let context = NSGraphicsContext.current!.cgContext
// calculate width
let buttonWidth = CGFloat(60)
// draw rounded rect with shadow
let buttonRect = CGRect(x: constant.popUpButtonInset, y: (cellFrame.height/2 - constant.popUpButtonHeigth/2) - constant.popUpButtonVerticalOffset, width: buttonWidth, height: constant.popUpButtonHeigth)
let roundedRect = CGPath.init(roundedRect: buttonRect, cornerWidth: constant.popUpButtonCornerRadius, cornerHeight: constant.popUpButtonCornerRadius, transform: nil)
let shadowColor = CGColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5)
context.setShadow(
offset: CGSize(width: 0, height: 0),
blur: 3.0,
color: shadowColor)
context.setLineWidth(3)
context.setFillColor(.white)
context.addPath(roundedRect)
context.fillPath()
context.setShadow(offset: CGSize(), blur: 0)
// draw arrow rect
let arrowRect = CGRect(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth - constant.popUpButtonArrowRectGap, y: (cellFrame.height/2 - constant.popUpButtonArrowRectWidth/2 - constant.popUpButtonVerticalOffset), width: constant.popUpButtonArrowRectWidth, height: constant.popUpButtonArrowRectWidth)
let arrowRoundedRect = CGPath.init(roundedRect: arrowRect, cornerWidth: constant.popUpButtonArrowRectCornerRadius, cornerHeight: constant.popUpButtonArrowRectCornerRadius, transform: nil)
context.setFillColor(NSColor.controlAccentColor.cgColor)
context.addPath(arrowRoundedRect)
context.fillPath()
// draw arrows
context.setStrokeColor(.white)
context.setLineWidth(1.5)
context.setLineCap(.round)
context.move(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth - constant.popUpButtonArrowRectGap + 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset + 2)))
context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth/2 - constant.popUpButtonArrowRectGap, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset + constant.popUpButtonArrowRectWidth/2 - 3)))
context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectGap - 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset + 2)))
context.strokePath()
context.move(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth - constant.popUpButtonArrowRectGap + 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset - 2)))
context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth/2 - constant.popUpButtonArrowRectGap, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset - constant.popUpButtonArrowRectWidth/2 + 3)))
context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectGap - 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset - 2)))
context.strokePath()
// draw text
let textColor: NSColor = .black
let attributes = [
NSAttributedString.Key.font : NSFont(name: "Lucida Grande", size: CGFloat(12)),
NSAttributedString.Key.foregroundColor : textColor
]
let textPosition = NSPoint(x: constant.popUpButtonInset + constant.popUpButtonArrowRectGap, y: constant.popUpButtonVerticalOffset + 8 - constant.popUpButtonArrowRectGap)
NSAttributedString(string: self.selectedItem!.title, attributes: attributes as [NSAttributedString.Key : Any]).draw(at: textPosition)
if hasFocus {
let buttonRect = CGRect(x: constant.popUpButtonInset - constant.popUpButtonFocusRingThickness/4, y: (cellFrame.height/2 - constant.popUpButtonHeigth/2) - constant.popUpButtonVerticalOffset - constant.popUpButtonFocusRingThickness/4, width: buttonWidth + constant.popUpButtonFocusRingThickness*0.5, height: constant.popUpButtonHeigth + constant.popUpButtonFocusRingThickness*0.5)
let roundedRect = CGPath.init(roundedRect: buttonRect, cornerWidth: constant.popUpButtonFocusRingCornerRadius, cornerHeight: constant.popUpButtonFocusRingCornerRadius, transform: nil)
context.setLineWidth(constant.popUpButtonFocusRingThickness)
context.setStrokeColor((NSColor.keyboardFocusIndicatorColor).cgColor)
context.addPath(roundedRect)
context.strokePath()
}
}
}
class MenuItemCustomView: NSView {
var text: String = ""
var scaleFactor: CGFloat = 1
var selected = false
override func draw(_ dirtyRect: NSRect) {
let context = NSGraphicsContext.current!.cgContext
context.setLineWidth(1)
var textColor: NSColor
if self.enclosingMenuItem!.isHighlighted {
textColor = .white
context.setStrokeColor(.white
)
// draw selection frame
let arrowRect = CGRect(x: constant.popUpMenuSelectionInset, y: 0, width: (self.frame.width - constant.popUpMenuSelectionInset*2), height: self.frame.height)
let arrowRoundedRect = CGPath.init(roundedRect: arrowRect, cornerWidth: constant.popUpButtonArrowRectCornerRadius, cornerHeight: constant.popUpButtonArrowRectCornerRadius, transform: nil)
context.setFillColor(NSColor.controlAccentColor.cgColor)
context.addPath(arrowRoundedRect)
context.fillPath()
} else {
textColor = .black
context.setStrokeColor(.black)
}
let attributes = [
NSAttributedString.Key.font : NSFont(name: "Lucida Grande", size: CGFloat(12*scaleFactor)),
NSAttributedString.Key.foregroundColor : textColor
]
let textPosition = NSPoint(x: constant.popUpMenuTextX*scaleFactor, y: constant.popUpMenuTextY*scaleFactor)
NSAttributedString(string: self.text, attributes: attributes as [NSAttributedString.Key : Any]).draw(at: textPosition)
if selected {
// draw checkmark
context.setLineWidth(2*scaleFactor)
let inset = constant.popUpMenuSelectionInset
context.move(to: CGPoint(x: (inset + 3)*scaleFactor, y: (self.frame.height/2)))
context.addLine(to: CGPoint(x: (inset + 7)*scaleFactor, y: self.frame.height*0.3))
context.addLine(to: CGPoint(x: (inset + 13)*scaleFactor, y: (self.frame.height*0.7)))
context.strokePath()
}
}
}
struct constant {
static let popUpButtonHeigth = CGFloat(20)
static let popUpButtonInset = CGFloat(4)
static let popUpButtonCornerRadius = CGFloat(5)
static let popUpButtonVerticalOffset = CGFloat(1.5)
static let popUpButtonFocusRingThickness = CGFloat(4)
static let popUpButtonFocusRingCornerRadius = CGFloat(6)
static let popUpButtonArrowRectWidth = CGFloat(15)
static let popUpButtonArrowRectGap = CGFloat(2)
static let popUpButtonArrowRectCornerRadius = CGFloat(3)
static let popUpMenuCellHeigth = CGFloat(24)
static let popUpMenuTextX = CGFloat(25)
static let popUpMenuTextY = CGFloat(4)
static let popUpMenuSelectionInset = CGFloat(5)
static let buttonFrameY = CGFloat(10)
static let buttonFrameHeigth = CGFloat(35)
}

Related

Cocoa FocusRing gets distorted when its view gets resized

I display an NSWindow that contains an NSbutton. The button can be focussed with the tab key for keyboard interaction, the standard focus ring is shown around the button.
The window has a fixed aspect ratio and can be resized as a whole. When this happens, all content should resize, i.e. the button gets larger or smaller.
This works fine so far. However, the focus ring does only kind of resize properly: the general position and its width are fine, but its heigth does not match the button’s heigth anymore.
Here is how the window initially looks like:
...and here after the window is resized:
You'll probably notice that the text gets slightly disproportional as well, but to no such extent as the focus ring is misplaced.
What steps do I have to take to have the focus ring match its corresponding button at all zoom levels?
Here is my code:
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
window.contentViewController = MyViewController(size: NSSize(width: 200, height: 72))
window.minSize = CGSize(width: 200, height: 100)
window.aspectRatio = NSMakeSize(2,1)
}
}
class MyViewController: NSViewController {
public init(size: NSSize) {
super.init(nibName: nil, bundle: nil)
self.view = MyView(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyView: NSView {
let initialFirstResponder: NSView
public override init(frame: NSRect) {
let textField = NSTextField(labelWithString: "Textfield")
textField.isEditable = false
textField.frame = NSRect(x: 20, y: 36, width: 100, height: 15)
let switchButton = NSButton(title: "Switch", target: nil, action: nil)
switchButton.frame = NSRect(x: 100, y: 31, width: 80, height: 20)
self.initialFirstResponder = switchButton
super.init(frame: frame)
self.bounds = frame
self.postsBoundsChangedNotifications = true
NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: self, queue: OperationQueue.main, using: {(note) in
self.setBoundsSize(NSSize(width: 200, height: 72)) // 28px for titlebar
print("Frame nun \(self.frame.width) x \(self.frame.height)")
})
self.addSubview(textField)
self.addSubview(switchButton)
self.autoresizesSubviews = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to update different views using SwiftUI?

I am aiming to make a program in which I am using using SwiftUI buttons to update by SCNView in SceneKit. I have a cylinder as a SCNCylinder in my SCNView inside a frame in SwiftUI. I want my cylinder to rotate about 180° after I press the button. In my current code I have used #State and #Binding to update the view. But somehow the cylinder rotates as soon as I run the code, not waiting for me to touch the button. Not sure why this happens
Here is my code :
struct ContentView: View {
#State var rotationAngle: Float = 180
var body: some View {
VStack{
Button(action: {
// What to perform
self.rotationAngle = 180
}) {
// How the button looks like
Text("180°")
.frame(width: 100, height: 100)
.position(x: 225, y: 500)
}
SceneKitView(angle: self.$rotationAngle)
.frame(width: 300, height: 300)
.position(x: 225, y: 0)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Float
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let rotation = SCNAction.rotate(by: self.degreesToRadians(self.angle), around: SCNVector3(1, 0, 0), duration: 5)
sceneView.scene?.rootNode.addChildNode(cylindernode)
cylindernode.runAction(rotation)
}
typealias UIViewType = SCNView
}
I want the cylinder to rotate after I press the button. Please help me with this problem.
just set startingAngle to 0
#State var rotationAngle: Float = 0

How to make the keyboard same size as the view

In the following code, the UIKeyboard is bigger than the view.
Question: How to make the keyboard same size as the view?
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController, UITextFieldDelegate {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.backgroundColor = .white
let label = UITextField()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
label.addTarget(self, action: #selector(myTargetFunction), for: .touchDown)
view.addSubview(label)
self.view = view
}
#objc func myTargetFunction() {
print("It works!")
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
The only "solution" I discovered so far is to set the preferredContentSize of your UIViewController to the size of the UIScreen.main of the current Xcode Playground environment:
let vc = MyViewController()
vc.preferredContentSize = UIScreen.main.bounds.size
PlaygroundPage.current.liveView = vc

Migration from Swift 3 to Swift 4 Navigation Bar broken interface

I have migrated my app from Swift 3.1 to Swift 4.0 (Xcode 8.3.3 to Xcode 9.0) and some part of my interface is broken now. Navigation Bar of Navigation controller is complete mess. Please, look at screenshot:
There are 3 elements:
left Netfnet logo (image)
right Signal strength (image)
right QR Code button
As you can see, two images are too big and not in center and button was deformed (it should be perfect square, all images too).
There is code which generated navigation controller:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
settings()
}
func settings() {
let color = UIColor(red: 81 / 255, green: 155 / 255, blue: 22 / 255, alpha: 1.0)
self.navigationController?.navigationBar.barTintColor = color
let logoImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
logoImageView.contentMode = .scaleAspectFit
let logo = UIImage(named: "littleLogoImage")
logoImageView.image = logo
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: logoImageView)
let signalStengthImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
signalStengthImageView.contentMode = .scaleAspectFit
signalStengthImageView.image = UIImage(named: "signalStrength4")
let signalStengthImageItem = UIBarButtonItem(customView: signalStengthImageView)
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "qrCodeButton"), for: .normal)
button.frame = CGRect(x: 0, y: 0, width: 35, height: 35)
let qrCodeButtonItem = UIBarButtonItem(customView: button)
navigationItem.rightBarButtonItems = [qrCodeButtonItem, signalStengthImageItem] //
}
}
I can decrease resolution of images directly myself, but I just don't get why everting was fine in Swift 3.1 and in Swift 4.0 is broken.
I will be thankful for any help or advice.
You have to add width and height constraints.
Your barImageView and barButton in CustomNavigationController should be like below :
func barImageView(imageName: String) -> UIBarButtonItem {
let imgView = imageView(imageName: imageName)
let widthConstraint = imgView.widthAnchor.constraint(equalToConstant: 35)
let heightConstraint = imgView.heightAnchor.constraint(equalToConstant: 35)
heightConstraint.isActive = true
widthConstraint.isActive = true
return UIBarButtonItem(customView: imgView)
}
func barButton(imageName: String, selector: Selector) -> UIBarButtonItem {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: imageName), for: .normal)
button.frame = CGRect(x: 0, y: 0, width: 35, height: 35)
button.addTarget(self, action: selector, for: .touchUpInside)
let widthConstraint = button.widthAnchor.constraint(equalToConstant: 35)
let heightConstraint = button.heightAnchor.constraint(equalToConstant: 35)
heightConstraint.isActive = true
widthConstraint.isActive = true
return UIBarButtonItem(customView: button)
}
Your signalStengthImageView in LogoWithSignalStrength:
signalStengthImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
signalStengthImageView.contentMode = .scaleAspectFit
let widthConstraint = signalStengthImageView.widthAnchor.constraint(equalToConstant: 35)
let heightConstraint = signalStengthImageView.heightAnchor.constraint(equalToConstant: 35)
heightConstraint.isActive = true
widthConstraint.isActive = true
In Xcode 9, Navigation bar items are constraints base, Add this:
if #available(iOS 11.0, *) {
logoImageView.widthAnchor.constraint(equalToConstant: 35).isActive = true
logoImageView.heightAnchor.constraint(equalToConstant: 35).isActive = true
} else {
//set frames
}

How to make the frame of a button custom shape in Swift 2

I want the button to be reactive to tap only in the custom polygonal shape that I create and not in the CGRect frame.
button.frame only supports CGRect.
Here is an example of a button that only responds to touches within a certain area.
class MyButton: UIButton {
var path: UIBezierPath!
override func awakeFromNib() {
backgroundColor = UIColor.greenColor()
addTarget(self, action: #selector(touchDown), forControlEvents: .TouchDown)
}
override func drawRect(rect: CGRect) {
path = UIBezierPath()
path.moveToPoint(CGPointMake(150, 10))
path.addLineToPoint(CGPointMake(200, 10))
path.addLineToPoint(CGPointMake(150, 100))
path.addLineToPoint(CGPointMake(100, 100))
path.closePath()
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.fillColor = UIColor.blueColor().CGColor
shapeLayer.path = path.CGPath
layer.addSublayer(shapeLayer)
}
func touchDown(button: MyButton, event: UIEvent) {
if let touch = event.touchesForView(button)?.first {
let location = touch.locationInView(button)
if path.containsPoint(location) == false {
button.cancelTrackingWithEvent(nil)
}
}
}
}
If you want to do it in Swift 3/4:
class MyButton: UIButton {
var path: UIBezierPath!
override func awakeFromNib() {
backgroundColor = UIColor.green
addTarget(self, action: #selector(touchDown), for: .touchDown)
}
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path.move(to: CGPoint(x: 150, y: 10))
path.addLine(to: CGPoint(x: 200, y: 10))
path.addLine(to: CGPoint(x: 150, y: 100))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
}
func touchDown(button: MyButton, event: UIEvent) {
if let touch = event.touches(for: button)?.first {
let location = touch.location(in: button)
if path.contains(location) == false {
button.cancelTracking(with: nil)
}
}
}
}

Resources