I am implementing touchbar functionalities.
I want a specific touchbar to be displayed when editing an NSTextField.
I have tried to both methods :
set a touchbar using touchbar property :
field.touchBar = myTouchBar
and subclassing NSTextField to override makeTouchBar() function :
class MyTextField: NSTextField
{
override func makeTouchBar(){return myTouchBar}
}
Both methods show an empty touchbar when editing the field. Changing the isAutomaticTextCompletionEnabled and allowsCharacterPickerTouchBarItem properties does not change it - just making the corresponding buttons appear.
Doing exactly the same thing with an NSTextView - or many other type of NSView, however, works perfectly well.
Do you know if it is possible to have a custom toolbar when editing an NSTextField?
Thanks to #Willeke's answer, I have been able to find the solution. It is quite tricky, however :
First, subclass NSTextField to keep another NSTouchBar :
class MyTextField: NSTextField
{
private var innerTouchBar: Any?
var editor: NSText?
#available(OSX 10.12.2, *)
func setTouchBar(_ touchBar: NSTouchBar?)
{
innerTouchBar = touchBar
}
#available(OSX 10.12.2, *)
func getTouchBar() -> NSTouchBar?
{
innerTouchBar as? NSTouchBar
}
}
Then, subclass NSTextView to use a provided NSTouchBar :
#available(OSX 10.12.2, *)
class MyTextView: NSTextView
{
private var innerTouchBar: NSTouchBar?
convenience init(touchBar: NSTouchBar?)
{
self.init()
innerTouchBar = touchBar
}
override func makeTouchBar() -> NSTouchBar?
{
innerTouchBar
}
}
When the NSWindowController gets asked for the NSTextView of the NSTextField, then create a custom NSTextView with the innerTouchBar of the NSTextField :
extension MyWindowController
{
func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any?
{
if #available(OSX 10.12.2, *)
{
if let field = client as? MyTextField
{
if field.editor == nil
{
field.editor = SFTextView(touchBar: field.getTouchBar())
field.editor?.isFieldEditor = true
}
return field.editor
}
}
return nil;
}
}
Of course, do not forget to use MyTextField instead of NSTextField in the XIB or in your code, and to call the setTouchBar(_:) function first thing after creation.
Explanation
Every NSTextField has an underlying NSTextView, which is in fact the firstResponder, and the object whose touchBar is displayed. We cannot access the underlying NSTextView directly from NSTextField. Instead, the NSTextField asks the NWindowController which NSTextView to use. So when this happens, in windowWillReturnFieldEditor(_:,to:) of NSWindowController, we have to return a custom NSTextViewwith the correct touchBar.
I think this can apply to other things than touchBar...
Related
While AVPlayerView Watching,
Sliding Mouse Wheel makes pause movie then fast forward or rewind.
this makes process error.
I set AVPlayerView Mode as NONE, btw.
let me know how to disable mouse wheel event on AVPlayerView.
Swift 3 (forwards scrolling to the view underneath it):
class NonScrollableAVPlayerView : AVPlayerView
{
override func scrollWheel(with event: NSEvent)
{
if self.superview != nil
{
self.nextResponder?.scrollWheel(with: event)
}
}
}
Override hitTest:
#interface MyAVPlayerView : AVPlayerView
#end
#implementation MyAVPlayerView
- (NSView *)hitTest:(NSPoint)aPoint
{
if (prohibitScrolling) {
return nil;
} else {
return [super hitTest:aPoint];
}
}
#end
I very rarely override drawRect in my UIView subclasses, usually preferring to set layer.contents with pre-rendering images and often employing multiple sublayers or subviews and manipulating these based on input parameters. Is there a way for IB to render these more complex view stacks?
Thanks, #zisoft for the clueing me in on prepareForInterfaceBuilder. There a few nuances with Interface Builder's render cycle which were the source of my issues and are worth noting...
Confirmed: You don't need to use -drawRect.
Setting images on UIButton control states works. Arbitrary layer stacks seem to work if a few things are kept in mind...
IB uses initWithFrame:
..not initWithCoder. awakeFromNib is also NOT called.
init... is only called once per session
I.e. once per re-compile whenever you make a change in the file. When you change IBInspectable properties, init is NOT called again. However...
prepareForInterfaceBuilder is called on every property change
It's like having KVO on all your IBInspectables as well as other built-in properties. You can test this yourself by having the your _setup method called, first only from your init.. method. Changing an IBInspectable will have no effect. Then add the call as well to prepareForInterfaceBuilder. Whahla! Note, your runtime code will probably need some additional KVO since it won't be calling the prepareForIB method. More on this below...
init... is too soon to draw, set layer content, etc.
At least with my UIButton subclass, calling [self setImage:img forState:UIControlStateNormal] has no effect in IB. You need to call it from prepareForInterfaceBuilder or via a KVO hook.
When IB fails to render, it doesn't blank our your component but rather keeps the last successful version.
Can be confusing at times when you are making changes that have no effect. Check the build logs.
Tip: Keep Activity Monitor nearby
I get hangs all the time on a couple different support processes and they take the whole machine down with them. Apply Force Quit liberally.
(UPDATE: This hasn't really been true since XCode6 came out of beta. It seldom hangs anymore)
UPDATE
6.3.1 seems to not like KVO in the IB version. Now you seem to need a flag to catch Interface Builder and not set up the KVOs. This is ok as the prepareForInterfaceBuilder method effectively KVOs all the IBInspectable properties. It's unfortunate that this behaviour isn't mirrored somehow at runtime thus requiring the manual KVO. See the updated sample code below.
UIButton subclass example
Below is some example code of a working IBDesignable UIButton subclass. ~~Note, prepareForInterfaceBuilder isn't actually required as KVO listens for changes to our relevant properties and triggers a redraw.~~ UPDATE: See point 8 above.
IB_DESIGNABLE
#interface SBR_InstrumentLeftHUDBigButton : UIButton
#property (nonatomic, strong) IBInspectable NSString *topText;
#property (nonatomic) IBInspectable CGFloat topTextSize;
#property (nonatomic, strong) IBInspectable NSString *bottomText;
#property (nonatomic) IBInspectable CGFloat bottomTextSize;
#property (nonatomic, strong) IBInspectable UIColor *borderColor;
#property (nonatomic, strong) IBInspectable UIColor *textColor;
#end
#implementation HUDBigButton
{
BOOL _isInterfaceBuilder;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self _setup];
}
return self;
}
//---------------------------------------------------------------------
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self _setup];
}
return self;
}
//---------------------------------------------------------------------
- (void)_setup
{
// Defaults.
_topTextSize = 11.5;
_bottomTextSize = 18;
_borderColor = UIColor.whiteColor;
_textColor = UIColor.whiteColor;
}
//---------------------------------------------------------------------
- (void)prepareForInterfaceBuilder
{
[super prepareForInterfaceBuilder];
_isInterfaceBuilder = YES;
[self _render];
}
//---------------------------------------------------------------------
- (void)awakeFromNib
{
[super awakeFromNib];
if (!_isInterfaceBuilder) { // shouldn't be required but jic...
// KVO to update the visuals
#weakify(self);
[self
bk_addObserverForKeyPaths:#[#"topText",
#"topTextSize",
#"bottomText",
#"bottomTextSize",
#"borderColor",
#"textColor"]
task:^(id obj, NSDictionary *keyPath) {
#strongify(self);
[self _render];
}];
}
}
//---------------------------------------------------------------------
- (void)dealloc
{
if (!_isInterfaceBuilder) {
[self bk_removeAllBlockObservers];
}
}
//---------------------------------------------------------------------
- (void)_render
{
UIImage *img = [SBR_Drawing imageOfHUDButtonWithFrame:self.bounds
edgeColor:_borderColor
buttonTextColor:_textColor
topText:_topText
topTextSize:_topTextSize
bottomText:_bottomText
bottomTextSize:_bottomTextSize];
[self setImage:img forState:UIControlStateNormal];
}
#end
This answer is related to overriding drawRect, but maybe it can give some ideas:
I have a custom UIView class which has complex drawings in drawRect. You have to take care about references which are not available during design time, i.e. UIApplication. For that, I override prepareForInterfaceBuilder where I set a boolean flag which I use in drawRect to distinguish between runtime and design time:
#IBDesignable class myView: UIView {
// Flag for InterfaceBuilder
var isInterfaceBuilder: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
// Initialization code
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForInterfaceBuilder() {
self.isInterfaceBuilder = true
}
override func drawRect(rect: CGRect)
{
// rounded cornders
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
// your drawing stuff here
if !self.isInterfaceBuilder {
// code for runtime
...
}
}
}
An here is how it looks in InterfaceBuilder:
You do not have to use drawRect, instead you can create your custom interface in a xib file, load it in initWithCoder and initWithFrame and it will be live rendering in IB after adding IBDesignable. Check this short tutorial: https://www.youtube.com/watch?v=L97MdpaF3Xg
I think layoutSubviews is the simplest mechanism.
Here is a (much) simpler example in Swift:
#IBDesignable
class LiveLayers : UIView {
var circle:UIBezierPath {
return UIBezierPath(ovalInRect: self.bounds)
}
var newLayer:CAShapeLayer {
let shape = CAShapeLayer()
self.layer.addSublayer(shape)
return shape
}
lazy var myLayer:CAShapeLayer = self.newLayer
// IBInspectable proeprties here...
#IBInspectable var pathLength:CGFloat = 0.0 { didSet {
self.setNeedsLayout()
}}
override func layoutSubviews() {
myLayer.frame = self.bounds // etc
myLayer.path = self.circle.CGPath
myLayer.strokeEnd = self.pathLength
}
}
I haven't tested this snippet, but have used patterns like this before. Note the use of the lazy property delegating to a computed property to simplify initial configuration.
To elaborate upon Hari Karam Singh's answer, this slideshow explains further:
http://www.splinter.com.au/presentations/ibdesignable/
Then if you aren't seeing your changes show up in Interface Builder, try these menus:
Xcode->Editor->Automatically Refresh Views
Xcode->Editor->Refresh All Views
Xcode->Editor->Debug Selected Views
Unfortunately, debugging my view froze Xcode, but it should work for small projects (YMMV).
In my case, there were two problems:
I did not implement initWithFrame in custom view: (Usually initWithCoder: is called when you initialize via IB, but for some reason initWithFrame: is needed for IBDesignable only. Is not called during runtime when you implement via IB)
My custom view's nib was loading from mainBundle: [NSBundle bundleForClass:[self class]] was needed.
I believe you can implement prepareForInterfaceBuilder and do your core animation work in there to get it to show up in IB.
I've done some fancy things with subclasses of UIButton that do their own core animation layer work to draw borders or backgrounds, and they live render in interface builder just fine, so i imagine if you're subclassing UIView directly, then prepareForInterfaceBuilder is all you'll need to do differently. Keep in mind though that the method is only ever executed by IB
Edited to include code as requested
I have something similar to, but not exactly like this (sorry I can't give you what I really do, but it's a work thing)
class BorderButton: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit(){
layer.borderWidth = 1
layer.borderColor = self.tintColor?.CGColor
layer.cornerRadius = 5
}
override func tintColorDidChange() {
layer.borderColor = self.tintColor?.CGColor
}
override var highlighted: Bool {
willSet {
if(newValue){
layer.backgroundColor = UIColor(white: 100, alpha: 1).CGColor
} else {
layer.backgroundColor = UIColor.clearColor().CGColor
}
}
}
}
I override both initWithCoder and initWithFrame because I want to be able to use the component in code or in IB (and as other answers state, you have to implement initWithFrame to make IB happy.
Then in commonInit I set up the core animation stuff to draw a border and make it pretty.
I also implement a willSet for the highlighted variable to change the background color because I hate when buttons draw borders, but don't provide feedback when pressed (i hate it when the pressed button looks like the unpressed button)
Swift 3 macro
#if TARGET_INTERFACE_BUILDER
#else
#endif
and class with function which is called when IB renders storyboard
#IBDesignable
class CustomView: UIView
{
#IBInspectable
public var isCool: Bool = true {
didSet {
#if TARGET_INTERFACE_BUILDER
#else
#endif
}
}
override func prepareForInterfaceBuilder() {
// code
}
}
IBInspectable can be used with types below
Int, CGFloat, Double, String, Bool, CGPoint, CGSize, CGRect, UIColor, UIImage
I'm looking for a way to get right-clicked row index from NSTableView but I can't find any delegate methods or class attributes for it. Any suggestion is appreciated.
Use the NSTableView method - (NSInteger)clickedRow to get the index of the last clicked row. The returned NSInteger will be the index of the right clicked row.
You do not need to subclass NSTableView for this solution. clickedRow is also available on NSOutlineView.
While I haven't done this, I am pretty sure you can by overriding NSView's - (NSMenu*)menuForEvent:(NSEvent*)theEvent. The example in this link does a point conversion to determine the index.
-(NSMenu*)menuForEvent:(NSEvent*)theEvent
{
NSPoint mousePoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
int row = [self rowAtPoint:mousePoint];
// Produce the menu here or perform an action like selection of the row.
}
Updated Answer
If you want to get clicked row index on menu opening, the answer is NSTableView.clickedRow. Anyway this property is available only in specific moments, and usually just -1.
When is this index to be available? That's in NSMenuDelegate.menuWillOpen method. So you conform the delegate and implement the method on your class, and access the clickedRow property. It's done.
final class FileNavigatorViewController: NSViewController, NSMenuDelegate {
let ov = NSOutlineView() // Assumed you setup this properly.
let ctxm = NSMenu()
override func viewDidLoad() {
super.viewDidLoad()
ov.menu = ctxm
ctxm.delegate = self
}
func menuWillOpen(_ menu: NSMenu) {
print(outlineView.clickedRow)
}
}
Clicked row index is available until you click an item in the menu. So this also works.
final class FileNavigatorViewController: NSViewController {
let ov = NSOutlineView() // Assumed you setup this properly.
let ctxm = NSMenu()
let item1 = NSMenuItem()
override func viewDidLoad() {
super.viewDidLoad()
ov.menu = ctxm
ov.addItem(item1)
ov.target = self
ov.action = #selector(onClickItem1(_:))
}
#objc
func onClickItem1(_: NSObject?) {
print(outlineView.clickedRow)
}
}
I tested this on macOS Sierra (10.12.5).
Old Answer
Starting from OS X 10.11, Apple finally added a method to access clickedRow easily. Just subclass NSTableView and override this method and you'll get the clickedRow as far as I experienced.
func willOpenMenu(menu: NSMenu, withEvent event: NSEvent)
This needs subclassing, but anyway, the cleanest and simplest way to access clickedRow.
Also, there's a pairing method.
func didCloseMenu(menu: NSMenu, withEvent event: NSEvent?)
Just select row on right-click by implementing menuForEvent: in NSTableView subclass:
#implementation MyTableView
- (NSMenu *)menuForEvent:(NSEvent *)theEvent
{
int row = [self rowAtPoint:[self convertPoint:theEvent.locationInWindow fromView:nil]];
if (row == -1)
return nil;
if (row != self.selectedRow)
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
return self.menu;
}
#end
if you dont need to open NSMenu but need to know "right click action with row number", i think most simple way is below. (Swift4 Code) Don't need any other connected Outer NSMenu class.
class SomeViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
...
override func viewDidLoad() {
...
tableView.action = #selector(some method()) // left single click action
tableView.doubleAction = #selector(someMethod()) // left double click action
}
// right click action
override func rightMouseDown(with theEvent: NSEvent) {
let point = tableView.convert(theEvent.locationInWindow, from: nil)
let row = tableView.row(at: point)
print("right click")
print(row)
}
I had the same question but I also needed a solution that would work with multiple selected rows (because when multiple rows are selected and you right-click on one of them, NSTableView highlights all of them). Here's the property I added for this in a subclass of NSTableView:
var rightClickRowIndexes: IndexSet {
if clickedRow >= 0 {
return selectedRowIndexes.contains(clickedRow) ? selectedRowIndexes : IndexSet(integer: clickedRow)
} else {
return IndexSet()
}
}
I'm searching for an elegant way to detect a right-click/ctrl-click on the header of an NSTableView.
When the right click occurs, I want to display an contextual menu.
- (NSMenu *)menuForEvent:(NSEvent *)
detects only right clicks in the table - not in the header of the table.
thanks for your help.
Sometimes a picture explains a 1000 words.
You do not need to subclass your table view.
On any tableView you can select the TableView and connect the menu outlet to a menu.
Now you can wire the selector of the menu (on the right) to your code .
To figure out what row in the table was clicked use
[yourTableView clickedRow]
Done. Like a boss.
Get the NSTableHeaderView from the NSTableView and set it's menu.
[[myTableView headerView] setMenu:aMenu];
You need to subclass NSTableHeaderView. While it is possible to make a menu show up without subclassing, it is not possible to find out which table column was clicked without subclassing (making the context menu useless).
I wrote my own sublcass of the table header view, and added a delegate. In interface builder, find the NSTableHeaderView, assign it your custom subclass, and connect its new delegate outlet. Additionally, create a menu and assign it to the menu outlet.
Then implement the -validateMenu:forTableColumn: method in the delegate. Enable/disable menu items as apropriate (make sure that the menu doesn't autovalidate in IB). Store the clicked column somewhere in an instance variable, so you know which column to act on when the user selects an action.
PGETableViewTableHeaderView.h
#import <Cocoa/Cocoa.h>
#protocol PGETableViewTableHeaderViewDelegate <NSObject>
-(void)validateMenu:(NSMenu*)menu forTableColumn:(NSTableColumn*)tableColumn;
#end
#interface PGETableViewTableHeaderView : NSTableHeaderView
#property(weak) IBOutlet id<PGETableViewTableHeaderViewDelegate> delegate;
#end
PGETableViewTableHeaderView.m
#import "PGETableViewTableHeaderView.h"
#implementation PGETableViewTableHeaderView
-(NSMenu *)menuForEvent:(NSEvent *)event {
NSInteger columnForMenu = [self columnAtPoint:[self convertPoint:event.locationInWindow fromView:nil]];
NSTableColumn *tableColumn = nil;
if (columnForMenu >= 0) tableColumn = self.tableView.tableColumns[columnForMenu];
NSMenu *menu = self.menu;
[self.delegate validateMenu:menu forTableColumn:tableColumn];
return menu;
}
#end
Thanks Jakob Egger for his precise answer.
I come up with Swift version of this approach. I changed the delegate method signature a little bit, to give more flexibility in case of more then one TableView in ViewController.
protocol IMenuTableHeaderViewDelegate: class {
func menuForTableHeader(inTableView tableView: NSTableView, forTableColumn tableColumn: NSTableColumn) -> NSMenu?
}
class MenuTableHeaderView: NSTableHeaderView {
weak var menuDelegate: IMenuTableHeaderViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
guard tableView != nil else {
return nil
}
let columnForMenu = column(at: convert(event.locationInWindow, from: nil))
if columnForMenu >= 0, tableView!.tableColumns.count > columnForMenu {
if let tableColumn = tableView?.tableColumns[columnForMenu] {
return menuDelegate?.menuForTableHeader(inTableView: tableView!, forTableColumn: tableColumn)
}
}
return self.menu;
}
}
To use this custom class, find NSTableHeaderView in the interface builder and change the class to MenuTableHeaderView
Window where you have to enter custom class name
Example of this approach usage in a ViewController
class ExampleViewController: NSViewController, IMenuTableHeaderViewDelegate {
#IBOutlet weak var tableView: NSTableView!
#IBOutlet var tableHeaderMenu: NSMenu!
var lastColumnForMenu: HeaderColumnForMenu?
struct HeaderColumnForMenu {
let tableView: NSTableView
let tableColumn: NSTableColumn
}
override func viewDidLoad() {
super.viewDidLoad()
if let tableHeaderWithMenu = tableView.headerView as? MenuTableHeaderView {
tableHeaderWithMenu.menuDelegate = self
}
}
func menuForTableHeader(inTableView tableView: NSTableView, forTableColumn tableColumn: NSTableColumn) -> NSMenu? {
//Save column to wich we are going to show menu
lastColumnForMenu = HeaderColumnForMenu(tableView: tableView, tableColumn: tableColumn)
if needShowMenu {
return tableHeaderMenu
}
return nil
}
}
How to make a Cocoa application quit when the main window is closed? Without that you have to click on the app icon and click quit in the menu.
You can implement applicationShouldTerminateAfterLastWindowClosed: to return YES in your app's delegate. But I would think twice before doing this, as it's really unusual on the Mac outside of small "utility" applications like Calculator and most Mac users will not appreciate your app behaving so strangely.
Add this code snippet to your app's delegate:
-(BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app {
return YES;
}
As the question is mainly about Cocoa programming and not about a specific
language (Objective-C), here is the Swift version of Chuck's and Steve's
answer:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool {
return true
}
// Your other application delegate methods ...
}
For Swift 3 change the method definition to
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
You should have an IBOutlet to your main window. For Example: IBOutlet NSWindow *mainWindow;
- (void)awakeFromWindow {
[mainWindow setDelegate: self];
}
- (void)windowWillClose:(NSNotification *)notification {
[NSApp terminate:self];
}
If this does not work you should add an observer to your NSNotificationCenter for the Notification NSWindowWillCloseNotification. Don't forget to check if the right window is closing.
This works for me.
extension MainWindowController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
if let window = notification.object as? NSWindow, let controller = window.windowController {
if window == self.window {
for window in self.childWindows {
print(" Closing \(window)")
window.close()
}
}
}
}
}