Simple mouseover effect on NSButton - cocoa

I am creating a custom NSButtonCell for a custom rendering.
Now, I want to have different aspect depending if the mouse is over the button or not. How can I get this information?
Thanks and regards,

Here is i have created and worked for me perfectly...
Step 1: Create the Button with tracking area
NSButton *myButton = [[NSButton alloc] initWithFrame:NSMakeRect(100, 7, 100, 50)];
[myButton setTitle:#"sample"];
[self.window.contentView addSubview:myButton];
// Insert code here to initialize your application
NSTrackingArea* trackingArea = [[NSTrackingArea alloc]
initWithRect:[myButton bounds]
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
owner:self userInfo:nil];
[myButton addTrackingArea:trackingArea];
Step: 2 Implement the following methods
- (void)mouseEntered:(NSEvent *)theEvent{
NSLog(#"entered");
[[myButton cell] setBackgroundColor:[NSColor blueColor]];
}
- (void)mouseExited:(NSEvent *)theEvent{
[[myButton cell] setBackgroundColor:[NSColor redColor]];
NSLog(#"exited");
}

Swift 3:
Create the button with code or just use it's #IBOutlet.
Then define the button's tracking area for mouse over (hover):
let area = NSTrackingArea.init(rect: yourButtonName.bounds,
options: [.mouseEnteredAndExited, .activeAlways],
owner: self,
userInfo: nil)
yourButtonName.addTrackingArea(area)
Then override mouseEntered and mouseExited, set whatever you want to change (button color, button image, button text,..) in these functions:
override func mouseEntered(with event: NSEvent) {
print("Entered: \(event)")
}
override func mouseExited(with event: NSEvent) {
print("Exited: \(event)")
}
If you have multiple buttons (with tracking area added for each) and you need to identify which button triggered the mouseEntered event, you can add some userInfo information for this purpose, so instead of:
userInfo: nil
Add your custom button name in userInfo for each button, for example:
userInfo: ["btnName": "yourButtonName"]
Then you can write a switch-case or if statements in your mouseEntered and mouseExited functions like this:
override func mouseEntered(with event: NSEvent) {
// Identify which button triggered the mouseEntered event
if let buttonName = event.trackingArea?.userInfo?.values.first as? String {
switch (buttonName) {
case "yourButtonName":
//do whatever you want for this button..
case "anotherButtonName":
//do whatever you want for this button..
default:
print("The given button name: \"\(buttonName)\" is unknown!")
}
}
}

You need to Subclass the NSButton class (or even better the NSButtonCell class).
As Justin said if you put the two method
- (void)mouseEntered:(NSEvent *)theEvent;
- (void)mouseExited:(NSEvent *)theEvent;
They should get called when the mouse enter and exit the area. You may also need to re create the tracking area, look here:
- (void)updateTrackingAreas
For fade in and fade out effect I played with animator and alpha value for example:
[self animator]setAlphaValue:0.5];

Swift 5 version with callback, super easy to use:
final class HoverButton: NSButton {
private let callback: (HoverButton, Bool) -> Void
init(callback: #escaping (HoverButton, Bool) -> Void) {
self.callback = callback
super.init(frame: .zero)
let area = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil)
addTrackingArea(area)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
callback(self, true)
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
callback(self, false)
}
}
Usage:
let button = HoverButton { button, isHovered in
button.animator().alphaValue = isHovered ? 1 : 0.5
}

a good starting point, declared in NSResponder:
- (void)mouseEntered:(NSEvent *)theEvent;
- (void)mouseExited:(NSEvent *)theEvent;
specifically, the button cell's container (not the cell itself) is the NSResponder.

For those who prefer subclassing, you can also make your own NSButton and assigning the NSTrackingArea in it.
Here is a really simple and elegant way to do it, thanks to Joey Zhou : https://github.com/Swift-Kit/JZHoverNSButton
It is written in Swift 2 but XCode will automatically translate it in Swift 3 - 4 without any issue.
Hope it can help someone

This is a simple code to track mouse enter and mouse exit events on some controls (Images, Label, etc...):
#IBOutlet weak var myImage: NSImageView!
#IBOutlet weak var myLabel: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
let area1 = myTrakingArea(control: self.myImage)
let area2 = myTrakingArea(control: self.myLabel)
self.myImage.addTrackingArea(area1)
self.myLabel.addTrackingArea(area2)
}
func myTrakingArea(control: NSControl) -> NSTrackingArea {
return NSTrackingArea.init(rect: control.bounds,
options: [.mouseEnteredAndExited, .activeAlways],
owner: control,
userInfo: nil)
}
override func mouseEntered(with event: NSEvent) {
if let owner = event.trackingArea?.owner as? NSControl {
let id : String = owner.identifier!.rawValue
switch id {
case self.myLabel.identifier!.rawValue:
print("Entered Quit Label")
case self.myImage.identifier!.rawValue:
print("Entered Quit Image")
default:
print("Entered ???")
}
}
}
override func mouseExited(with event: NSEvent) {
if let owner = event.trackingArea?.owner as? NSControl {
let id : String = owner.identifier!.rawValue
switch id {
case self.myLabel.identifier!.rawValue:
print("Exited Quit Label")
case self.myImage.identifier!.rawValue:
print("Exited Quit Image")
default:
print("Exited ???")
}
}
}

Related

I want to use it as a UIButton when I tap UITextField

I created a custom keyboard screen on tvOS.
If possible, tap on UITextField as it is, I want to transition to the custom keyboard view.
But tapping the UITextField always displays the system keyboard.
What should I do now?
1) Make the view controller implement this delegate: UITextFieldDelegate
class YourViewController: UIViewController, UITextFieldDelegate {
// ...
yourTextField.delegate = self
// ...
}
2) Return false in textFieldShouldBeginEditing, so the text field doesn't respond and the keyboard doesn't open. Instead, open yours or do whatever you want.
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
// HERE, open your keyboard or do whatever you want
return false
}
textField.inputView = UIView()
class YourViewController: UIViewController,UITextFieldDelegate { }
First set your delegate for textfieldtextField.delegate = self, Then
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.addTarget(self, action: #selector(gettextFieldFunction), for: UIControlEvents.touchDown)
}

NSView overlaying NSPageController disappearing on transition

I have an NSPageController containing 8 or so NSViewControllers. I want to have a semi transparent bottom bar when the mouse is inside of the window, and a semi transparent top bar that persists no matter where the mouse is.
I add the top bar and bottom bar to the view, along with constraints in NSPageControllers viewDidLoad() method.
They show up fine on the first page, but when I start to transition from one page to another, the new NSViewController is redrawn over the overlaying views and they disappear. I can verify that they are under the NSViewControllers because then I drag all the way to a specific side I can see them underneath.
Any ideas why this is happening / how I can avoid it?
Code:
class MyPageController: NSPageController {
// MARK: - Properties
fileprivate var mouseIsInside = false
fileprivate var tabBar: TabBar!
// MARK: - NSViewController
override func viewDidLoad() {
super.viewDidLoad()
// add tab bar, then hide it (mouse in or outside of window will restore current state)
tabBar = TabBar(frame: NSRect(x: 0, y:0, width: view.frame.size.width, height: 40))
addTabBar(withAnimation: false)
removeTabBar(withAnimation: false)
NSLayoutConstraint.activate([
tabBar.heightAnchor.constraint(equalToConstant: 40),
tabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tabBar.centerXAnchor.constraint(equalTo: view.centerXAnchor),
tabBar.widthAnchor.constraint(equalTo: view.widthAnchor)
])
delegate = self
transitionStyle = .horizontalStrip
arrangedObjects = ["0","1","2","3","4","5","6","7"]
selectedIndex = 0
view.wantsLayer = true
// register for mouse events
let trackingArea = NSTrackingArea(rect: view.bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .inVisibleRect], owner: self, userInfo: nil)
view.addTrackingArea(trackingArea)
}
}
// NSPageController Delegate
extension PageController: NSPageControllerDelegate {
func pageController(_ pageController: NSPageController, frameFor object: Any?) -> NSRect {
return NSInsetRect(view.frame, -1, 0)
}
func pageController(_ pageController: NSPageController, identifierFor object: Any) -> String {
return (object as? String)!
}
func pageController(_ pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
return ViewController(id: identifier)
}
func pageControllerWillStartLiveTransition(_ pageController: NSPageController) {
Swift.print("pageControllerWillStartLiveTransition")
addTabBar(withAnimation: false)
}
func pageControllerDidEndLiveTransition(_ pageController: NSPageController) {
pageController.completeTransition()
addTabBar(withAnimation: false)
Swift.print("pageControllerWillEndLiveTransition")
}
}
// tabBar functions
extension PageController {
func addTabBar(withAnimation shouldAnimate: Bool) {
view.addSubview(tabBar)
tabBar.frame.size.width = view.frame.size.width
if mouseIsInside {
tabBar.showWithAnimation(shouldAnimate)
}
}
func removeTabBar(withAnimation shouldAnimate: Bool) {
tabBar.hideWithAnimation(shouldAnimate)
}
}
// Mouse Movements
extension PageController {
override func mouseEntered(with event: NSEvent) {
mouseIsInside = true
addTabBar(withAnimation: true)
}
override func mouseExited(with event: NSEvent) {
mouseIsInside = false
removeTabBar(withAnimation: true)
}
}
Thanks in advance!
This appear to be a not resolved bug. This fixed for me:
If the pageController is fullfilling the window's view, set to nil the contentView's background. This way the background we'll see is always the background of the pageController.
Sort views in this method:
func pageControllerDidEndLiveTransition(_ pageController: NSPageController) {
let controller = selectedViewController as! (YOUR_NSVIEWCONTROLLER_CLASS)
controller.view.superview?.addSubview(controller.view, positioned: .below, relativeTo: TOOLBAR)
}
Replace YOUR_NSVIEWCONTROLLER_CLASS for your ViewControllerclass name, and then replace TOOLBAR for the view that you want to see on top.

NSTableView pretend to be active

I have NSTextView and I want to show autocompletion options using NSTableView (like Xcode autocompletion). The problem is that when textView is the first responder, tableView is shown as unfocused (which is true), but I want to pretend that it's also active. Is there an easy way to achieve this (having firstResponder textView and tableView with active cell selection color)?
So I managed to resolve the issue with lots of experiments.
Here are my solution:
setup NSTableView:
tableView.refusesFirstResponder = true
tableView.selectionHighlightStyle = .none
on NSTableCellView subclass implement following code:
override func awakeFromNib() {
super.awakeFromNib()
registerNotifications()
}
deinit {
unregisterNotifications()
}
private func registerNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(selectionIsChanging),
name: NSNotification.Name.NSTableViewSelectionIsChanging,
object: nil)
}
private func unregisterNotifications() {
NotificationCenter.default.removeObserver(self)
}
#objc private func selectionIsChanging() {
if let row = superview as? NSTableRowView, row.isSelected == true {
self.backgroundColor = NSColor.alternateSelectedControlColor
} else {
self.backgroundColor = NSColor.clear
}
}

Mouse events (NSTrackingArea) in Playground not working

I have a a subclass of NSView which contains the following NSTrackingArea code. But for some reason the mouse events won't trigger in Playground.
The viewWillMoveToWindowis being called, but nothing else seems to fire. Does anyone have a clue to what is missing?
class MyView: NSView {
private var trackingArea: NSTrackingArea = NSTrackingArea()
// Other stuff omitted here...
// ...
override func viewWillMoveToWindow(newWindow: NSWindow?) {
// Setup a new tracking area when the view is added to the window.
trackingArea = NSTrackingArea(rect: self.bounds, options: [.MouseEnteredAndExited, .ActiveAlways], owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
override func updateTrackingAreas() {
self.removeTrackingArea(trackingArea)
trackingArea = NSTrackingArea(rect: self.bounds, options: [.MouseEnteredAndExited, .ActiveAlways], owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
// Mouse events
override func mouseEntered(theEvent: NSEvent) {
NSLog("MouseEntered")
}
override func mouseExited(theEvent: NSEvent) {
NSLog("MouseExited")
}
override func mouseDown(theEvent: NSEvent) {
NSLog("MouseDown")
}
}
According to this 2014 WWDC session:
There are few more limitations with Playgrounds.
Playgrounds cannot be used for things which require user interaction.
So we have great support for showing live views but you can only see them, you can't touch them.
You can find the original video here

NSTableView: detecting a mouse click together with the row and column

I'm trying to detect when a mouse click occurs in an NSTableView, and when it does, to determine the row and column of the cell that was clicked.
So far I've tried to use NSTableViewSelectionDidChangeNotification, but there are two problems:
It only triggers when the selection changes, whereas I want every mouse click, even if it is on the currently selected row.
The clickedRow and clickedColumn properties of NSTableView are both -1 when my delegate is called.
Is there a better (and correct) way of doing this?
There is a simple way.
Tested with Swift 3.0.2 on macOS 10.12.2 and Xcode 8.2.1
Let
tableView.action = #selector(onItemClicked)
Then
#objc private func onItemClicked() {
print("row \(tableView.clickedRow), col \(tableView.clickedColumn) clicked")
}
To catch the user clicking a row (only, when the user clicks a row, not when it is selected programmatically) :
Subclass your NSTableView and declare a protocol
MyTableView.h
#protocol ExtendedTableViewDelegate <NSObject>
- (void)tableView:(NSTableView *)tableView didClickedRow:(NSInteger)row;
#end
#interface MyTableView : NSTableView
#property (nonatomic, weak) id<ExtendedTableViewDelegate> extendedDelegate;
#end
MyTableView.m
Handle the mouse down event (note, the delegate callback is not called when the user clicks outside, maybe you want to handle that too, in that case, just comment out the condition "if (clickedRow != -1)")
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint globalLocation = [theEvent locationInWindow];
NSPoint localLocation = [self convertPoint:globalLocation fromView:nil];
NSInteger clickedRow = [self rowAtPoint:localLocation];
[super mouseDown:theEvent];
if (clickedRow != -1) {
[self.extendedDelegate tableView:self didClickedRow:clickedRow];
}
}
Make your WC, VC conform to ExtendedTableViewDelegate.
#interface MyViewController : DocumentBaseViewController<ExtendedTableViewDelegate, NSTableViewDelegate, NSTableViewDataSource>
set the extendedDelegate of the MyTableView to your WC, VC (MyViewController)
somewhere in MyTableView.m
self.myTableView.extendedDelegate = self
Implement the callback in delegate (MyViewController.m)
- (void)tableView:(NSTableView *)tableView didClickedRow:(NSInteger)row {
// have fun
}
I would prefer doing as follows.
Override
-(BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row;
Provide super implementation;
RequiredRow = row;
RequiredColumn = [tableView clickedColumn];
Hope this helps.
If someone is looking for a Swift 3/4/5 version of Peter Lapisu's answer:
Add an extension for the NSTableView (NSTableView+Clickable.swift):
import Foundation
import Cocoa
extension NSTableView {
open override func mouseDown(with event: NSEvent) {
let globalLocation = event.locationInWindow
let localLocation = self.convert(globalLocation, from: nil)
let clickedRow = self.row(at: localLocation)
super.mouseDown(with: event)
if (clickedRow != -1) {
(self.delegate as? NSTableViewClickableDelegate)?.tableView(self, didClickRow: clickedRow)
}
}
}
protocol NSTableViewClickableDelegate: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, didClickRow row: Int)
}
Then to use it, make sure you implement the new delegate protocol:
extension MyViewController: NSTableViewClickableDelegate {
#nonobjc func tableView(_ tableView: NSTableView, didClickRow row: Int) {
Swift.print("Clicked row \(row)")
}
}
The #nonobjc attribute silences the warning about it being close to didClick.
Just in case someone was looking for it in SWIFT and / or for NSOutlineView.
Based on #Peter Lapisu instructions.
class MYOutlineViewDelegate: NSOutlineView, NSOutlineViewDelegate,NSOutlineViewDataSource{
//....
}
extension MYOutlineViewDelegate{
func outlineView(outlineView: NSOutlineView, didClickTableRow item: AnyObject?) {
//Click stuff
}
override func mouseDown(theEvent: NSEvent) {
let globalLocation:NSPoint = theEvent.locationInWindow
let localLocation:NSPoint = self.convertPoint(globalLocation, fromView: nil)
let clickedRow:Int = self.rowAtPoint(localLocation)
super.mouseDown(theEvent)
if (clickedRow != -1) {
self.outlineView(self, didClickTableRow: self.itemAtRow(clickedRow))
}
}}
see the tableViewSelectionIsChanging notification, here are the the comments from NSTableView.h
/*
Optional - Called when the selection is about to be changed, but note, tableViewSelectionIsChanging: is only called when mouse events are changing the selection and not keyboard events.
*/
I concede that this might not be the surest way to correlate your mouse clicks, but it is another area to investigate, seeing that you are interested in mouse clicks.

Resources