I have a "sheet" as NSWindowController in Swift, for OS X. I placed an NSView on the sheet, with a custom controller in a class called CustomView. Everything I do on the sheet and in the custom view controller works fine.
class MySheet: NSWindowController {
//...
}
class CustomView: NSView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect) {
// Draw some things...
}
}
override func mouseDown(theEvent: NSEvent) {
// Change text on a label (or whatever) in MySheet()
}
}
Now I need to respond to user input in the CustomView, specifically, a mouse click on the view, and modify a label (or other control) back in MySheet(). This has to be very simple, but I don't know how to do it. The biggest stumbling block I have is that I don't have an instance of MySheet(); it is hidden in the way the system works. I can call back to a static func in MySheet(), but that won't let me change a label or other control through an #IBOutlet.
I know the above is a most likely a naive question, but would appreciate any help toward a solution.
Of course (!), this problem is supposed to be solved using a delegate, and when it is set up correctly it works fine. So I have like this:
protocol ClickableCurves {
func clickedOnCurve(index: Int)
}
class MySheet: NSWindowController, ClickableCurves {
private let lineColorCount = 16
func clickedOnCurve(index: Int) {
Swift.print("came back as \(index)")
}
override func windowDidLoad() {
super.windowDidLoad()
(curveView as! DefaultCurveAttrView).delegate = self
}
#IBOutlet weak var curveView: NSView!
}
class DefaultCurveAttrView: NSView {
var delegate: ClickableCurves? = nil
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}
override func mouseDown(theEvent: NSEvent) {
let frameRelWindow = self.convertRect(bounds, toView: nil)
let x = theEvent.locationInWindow.x - frameRelWindow.origin.x
let y = theEvent.locationInWindow.y - frameRelWindow.origin.y
let xm = bounds.width / 10
let dy = bounds.height / CGFloat(lineColorCount + 1)
var yc: CGFloat = dy / 2
if x >= xm && x <= 9 * xm {
for i in 1 ... lineColorCount {
yc += dy
if yc > y || i == lineColorCount {
if delegate != nil {
delegate!.clickedOnCurve(i - 1)
}
break
}
}
}
}
}
Related
I have been experimenting with mouse clicks. I am ok with left mouse clicks getting raw values etc etc. I now want to add right mouse clicks. I have setup a basic example. What i would like to achieve if there is one mouse right click it performs one function, and if there is two mouse right clicks it performs a different function. The problem is if you do two mouse clicks it obviously cannot differentiate between the two and so fire the one mouse click function before performing the second mouse function.
I was thinking of maybe using a timer of some sort to record the number of click. But i end up going round in circles as i just seem to start the timers over and over. I'm hoping some one might help. thanks for reading here is the code.
Xcode 8 Swift 3 Mac OSX Sierra NOT IOS.. NOT IOS
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var MyView: NSView!
override func windowDidLoad() {
super.windowDidLoad()
//Initialize mouse for Right Click numberOfClicksRequired = 1
let recogRightClick1 = NSClickGestureRecognizer(target: self, action: #selector(oneMouseClick))
recogRightClick1.buttonMask = 0x2
recogRightClick1.numberOfClicksRequired = 1
MyView.addGestureRecognizer(recogRightClick1)
//Initialize mouse for Right ClicknumberOfClicksRequired = 2
let recogRightClick2 = NSClickGestureRecognizer(target: self, action: #selector(twoMouseClick(myrec:myRightClick:)))
recogRightClick2.buttonMask = 0x2
recogRightClick2.numberOfClicksRequired = 2
MyView.addGestureRecognizer(recogRightClick2)
}//EO Overide
func oneMouseClick(myrec: NSPanGestureRecognizer,myRightClick:NSClickGestureRecognizer){
let rightClick = myRightClick.state.rawValue
print("oneMouseClick",rightClick)
}
func twoMouseClick(myrec: NSPanGestureRecognizer,myRightClick:NSClickGestureRecognizer){
let rightClick = myRightClick.state.rawValue
print("twoMouseClick",rightClick)
}
}//EnD oF thE wORld
UPDATE
I have re generated the code following the advice given. Now the code reflects more of what i wanted to do. My only problem is that I would like all the mouse operations to be triggered only inside 'myView' rather than within the main window. I thought it might have something to do with first responder but that doesn't seem to work. Again any thought would be appreciated. Please excuse any bad code i'm self taught.
Xcode 8 Swift 3 Mac OSX Sierra NOT IOS.. NOT IOS
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
var mouseX:CGFloat = 0
var mouseY:CGFloat = 0
override func windowDidLoad() {
myView.window?.becomeFirstResponder()
myView.window?.acceptsMouseMovedEvents = true
super.windowDidLoad()
myView.wantsLayer = true
myView.layer?.backgroundColor = CGColor(red: 0.05, green: 0.57, blue: 0.80, alpha: 0.6)
NSEvent.addLocalMonitorForEvents(matching:.leftMouseDown){
self.mouseEventFunction(data: 1)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.leftMouseUp){
self.mouseEventFunction(data:2)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.rightMouseDown){
self.mouseEventFunction(data: 3)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.rightMouseUp){
self.mouseEventFunction(data: 4)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.mouseMoved) {
self.mouseX = NSEvent.mouseLocation().x
self.mouseY = NSEvent.mouseLocation().y
return $0 }
}//EO Overide
func mouseEventFunction (data: Int){
switch data{
case 1 :
print("LeftMouseDown")
case 2 :
print("LeftMouseUp")
case 3 :
print("RightMouseDown")
case 3 :
print("RightMouseUP")
default: break
}
if data == 1 {print("mouseX",mouseX,"mouseY",mouseY)}
}//eo mouseEvent
}//EnD oF thE wORld
UPDATE 2
I have now updated subClassing the view controller, so the mouse clicks are now only working in myView. I'm still having problems with 'func mouseDragged' What i need to achieve is the bottom left of my view is x = 0 and Y = 0. I had a try with converting but thats not working. hoping someone might guide me. thanks for reading here is the updated code.
Xcode 8 Swift 3 Mac OSX Sierra NOT IOS.. NOT IOS
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
override func windowDidLoad() {
myView.window?.becomeFirstResponder()
myView.window?.acceptsMouseMovedEvents = true
window?.contentView?.addSubview(myView)
super.windowDidLoad()
}//EO Overide
}//EnD oF thE wORld
class testView: NSView {
override func draw(_ dirtyRect: NSRect) {
let backgroundColor = NSColor.lightGray
backgroundColor.set()
NSBezierPath.fill(bounds)
}
override func mouseDragged(with theEvent: NSEvent) {
let myLocationInWindow = theEvent.locationInWindow
let location = convert(myLocationInWindow, to: self)
Swift.print("myLocationInWindow",myLocationInWindow,"location",location)
}
override func mouseDown(with theEvent: NSEvent) {
Swift.print("mouseDown")
}
override func mouseUp(with theEvent: NSEvent) {
Swift.print("mouseUp clickCount: \(theEvent.clickCount)")
}
}//eo testView
To define mouse inside view you use
let myLocationInWindow = theEvent.locationInWindow
let location = convert(myLocationInWindow, from: nil)
where nil is the window
here is the final code
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
override func windowDidLoad() {
super.windowDidLoad()
}//EO Overide
}//EnD oF thE wORld
class testView: NSView {
override func draw(_ dirtyRect: NSRect) {
let backgroundColor = NSColor.lightGray
backgroundColor.set()
NSBezierPath.fill(bounds)
}
override func mouseDragged(with theEvent: NSEvent) {
let myLocationInWindow = theEvent.locationInWindow
let location = convert(myLocationInWindow, from: nil)
Swift.print("location",location)
}
override func mouseDown(with theEvent: NSEvent) {
Swift.print("mouseDown")
}
override func mouseUp(with theEvent: NSEvent) {
Swift.print("mouseUp clickCount: \(theEvent.clickCount)")
}
}//eo testView
I'm trying to pass the contents of an array to an NSView to display rectangles x positions.
I have set up a version using random numbers and a button and reduced the code to a minimum so hopefully it's more readable . I have tried creating the array as a Global Variable. I have also tried reversing the code from this answer.Pass data between view controllers.I have inserted a comment: "This is what I would like to achieve" which explain what i'm trying to achieve.
I'm hoping someone might help ? thanks
Swift 3 Xcode 8.1 OSX Mac OS not iOS
//
// MainWindowController.swift
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
var myArray = [Int]()
override func windowDidLoad() {
super.windowDidLoad()
//just for testing
for i in 0..<6{
myArray.append(i*10)
}
}//EO Overide
//just for testing
#IBAction func myButton(_ sender: AnyObject) {
myFunc()
}
func myFunc(){
for i in 0..<6{
let diceRoll = Int(arc4random_uniform(6) + 1)
myArray[i]=diceRoll * 10
}
myView.needsDisplay = true
}//EO myFunc
}//EnD oF thE wORld
class myGUI: NSView {
override func draw(_ dirtyRect: NSRect) {
for i in..<6{
let fromArray = myArray[i]
let box1 = NSMakeRect(CGFloat(fromArray),0,4,60)//This is what i want to do
let box1Color = NSColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0)
let box1Path: NSBezierPath = NSBezierPath(rect: box1)
box1Color.set()
box1Path.fill()
}//EO For
}}
Since you have a strong reference to the subclass of NSView just add a property:
class myGUI: NSView {
var xPosArray = [Int]()
override func draw(_ dirtyRect: NSRect) {
for xPos in xPosArray { // please avoid ugly C-style index based loops
let box1 = NSMakeRect(CGFloat(xPos), 0, 4, 60)
...
and simply set it in the view controller
func myFunc(){
for i in 0..<6 {
let diceRoll = Int(arc4random_uniform(6) + 1)
myArray[i]=diceRoll * 10
}
myView.xPosArray = myArray
myView.needsDisplay = true
}
Consider that class names are supposed to start with an uppercase letter.
Trying to recognize a right click on a NSStatusItem I got a suggestion ( Thanks to Zoff Dino ) to use a NSClickGestureRecognizer for that. But for some bizarre reason it isn't working as it should be. I am able to recognize a left click (buttonMask = 0x1) but not a right-click (buttonMask = 0x2). This is how I would like it to work but it isn't:
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
if let button = statusItem.button {
// Add right click functionality
let gesture = NSClickGestureRecognizer()
gesture.buttonMask = 0x2 // right mouse
gesture.target = self
gesture.action = "rightClickAction:"
button.addGestureRecognizer(gesture)
}}
func rightClickAction(sender: NSGestureRecognizer) {
if let button = sender.view as? NSButton {
NSLog("rightClick")
}
}
UPDATE:
I still did not manage to gets to work. Somehow it doesn't react on a right click (but changing the code on a left click) does. I guess some really simple issues are occurring that seem to block it from working. Even stranger is the fact that gesture.buttonMask = 0x1 works on the left click.
An alternative solution rather than NSClickGestureRecognizer is to attach a custom view to the status bar and handle the event from there.
The small disadvantage is you have to take care of the drawing and menu delegate methods.
Here a simple example:
Create a file StatusItemView a subclass of NSView
import Cocoa
class StatusItemView: NSView, NSMenuDelegate {
//MARK: - Variables
weak var statusItem : NSStatusItem!
var menuVisible = false
var image : NSImage! {
didSet {
if image != nil {
statusItem.length = image.size.width
needsDisplay = true
}
}
}
//MARK: - Override functions
override func mouseDown(theEvent: NSEvent) {
if let hasMenu = menu {
hasMenu.delegate = self
statusItem.popUpStatusItemMenu(hasMenu)
needsDisplay = true
}
}
override func rightMouseDown(theEvent: NSEvent) {
Swift.print(theEvent)
}
//MARK: - NSMenuDelegate
func menuWillOpen(menu: NSMenu) {
menuVisible = true
needsDisplay = true
}
func menuDidClose(menu: NSMenu) {
menuVisible = false
menu.delegate = nil
needsDisplay = true
}
//MARK: - DrawRect
override func drawRect(dirtyRect: NSRect) {
statusItem.drawStatusBarBackgroundInRect(bounds, withHighlight:menuVisible)
let origin = NSMakePoint(2.0, 3.0) // adjust origin if necessary
image?.drawAtPoint(origin, fromRect: dirtyRect, operation: .CompositeSourceOver, fraction: 1.0)
}
}
In AppDelegate you need a reference to the custom menu and an instance variable for the NSStatusItem instance
#IBOutlet weak var menu : NSMenu!
var statusItem : NSStatusItem!
In applicationDidFinishLaunching create the view and attach it to the status item. Be aware to set the image of the view after attaching it to make sure the width is considered.
func applicationDidFinishLaunching(aNotification: NSNotification) {
statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) // NSVariableStatusItemLength)
let statusItemView = StatusItemView(frame: NSRect(x: 0.0, y: 0.0, width: statusItem.length, height: 22.0))
statusItemView.statusItem = statusItem;
statusItemView.menu = menu
statusItem.view = statusItemView
statusItemView.image = NSImage(named: NSImageNameStatusAvailable)
}
The special case control-click to trigger the right-click function is not implemented.
File GameScene have class GameScene, class GameScene have CirclePhysicsDefault:
func circlePhysicsDefault() {
var Circle = SKShapeNode(circleOfRadius: 40)
Circle.position = CGPointMake(500, 500)
Circle.name = "defaultCircle"
Circle.strokeColor = SKColor.blackColor()
Circle.glowWidth = 10.0
Circle.fillColor = SKColor.yellowColor()
Circle.physicsBody = SKPhysicsBody(circleOfRadius: 40)
Circle.physicsBody.dynamic = true
self.addChild(Circle)
}
In file GameViewController i type:
#IBAction func addOneCircle(sender: AnyObject) {
GameScene().circlePhysicsDefault()
}
I linked this to button.
Run application, push the button - nothing happens.
If in GameScene function didMoveToView i call function circlePhysicsDefault, then app launch circle will be placed.
File GameViewController:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
/* Pick a size for the scene */
let scene = GameScene(fileNamed:"GameScene")
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
#IBAction func addOneCircle(sender: AnyObject) {
GameScene(fileNamed:"GameScene").circlePhysicsDefault()
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.toRaw())
} else {
return Int(UIInterfaceOrientationMask.All.toRaw())
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return false }
}
File GameScene:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
circlePhysicsDefault()
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
func sceneSetting() {
self.backgroundColor = SKColor.grayColor()
self.physicsWorld.gravity = CGVectorMake(0.01, -2)
}
func circlePhysicsDefault() {
var Circle = SKShapeNode(circleOfRadius: 40)
Circle.position = CGPointMake(500, 500)
Circle.name = "defaultCircle"
Circle.strokeColor = SKColor.blackColor()
Circle.glowWidth = 10.0
Circle.fillColor = SKColor.yellowColor()
Circle.physicsBody = SKPhysicsBody(circleOfRadius: 40)
Circle.physicsBody.dynamic = true
self.addChild(Circle)
}
}
you are creating a new instance on key press, store a weak reference to the scene and call the method from the wished instance.
#IBAction func addOneCircle(sender: AnyObject) {
myGameSceneRef.circlePhysicsDefault()
}
So now please try the following:
#IBAction func addOneCircle(sender: AnyObject) {
let scene = ((self.view as SKScene).scene as GameScene)
scene.circlePhysicsDefault()
}
Or maybe change "let" to "var"
I have set the intercell spacing in my NSTableView to 0 by sending:
[self.tableView setIntercellSpacing:NSMakeSize(0, 0)];
in the window controller's awakeFromNib but there is still an (possibly 1 pixel wide) empty space between the rows, which I think is where the grid lines are drawn although I'm not using the grid lines. How can I get rid of this space between the rows?
update:
The NSTableView documentation seems to say that this 1 pixel separation should go away when the intercell separation is set to 0, 0. In my case, its not. Maybe it's a bug?
As suggested by trudyscousin, I'll post how I fixed my problem:
As it turns out, the empty space does in fact disappear when you set the intercell spacing to 0 as I did. My problem was that the drawing code in my NSTableCellView subclass wasn't drawing all the way to the edge of the view. The gap I was seeing wasn't the intercell separation, it was the border of my NSTableCellView subclass.
In my case the task was to have 0.5 pt (1 px on Retina display) separator. Seems even when intercellSpacing set to .zero (or 0.5 pt in my case) AppKit still preserving 1 pt space between rows when drawing selection.
I ended up by subclassing NSTableRowView. With custom row I can set separator to any height. Here is a Swift 4.2 example:
class WelcomeRecentDocumentsView: View {
private lazy var tableView = TableView().autolayoutView()
private lazy var scrollView = ScrollView(tableView: tableView).autolayoutView()
override var intrinsicContentSize: NSSize {
return CGSize(width: 240, height: 400)
}
private var recentDocumentsURLs = (0 ..< 10).map { $0 }
}
extension WelcomeRecentDocumentsView: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return recentDocumentsURLs.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
return tableView.makeView(ItemView.self)
}
}
extension WelcomeRecentDocumentsView: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
// Returning custom RowView.
return RowView(separatorHeight: separatorHeight, separatorColor: tableView.gridColor)
}
}
extension WelcomeRecentDocumentsView {
private var separatorHeight: CGFloat {
return convertFromBacking(1)
}
override func setupUI() {
addSubview(scrollView)
let column = NSTableColumn(identifier: "com.example.column", resizingMask: .autoresizingMask)
tableView.addTableColumn(column)
tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle
tableView.headerView = nil
tableView.setAutomaticRowHeight(estimatedHeight: ItemView.defaultHeight)
tableView.intercellSpacing = CGSize(height: separatorHeight)
tableView.gridStyleMask = .solidHorizontalGridLineMask
}
override func setupHandlers() {
tableView.delegate = self
tableView.dataSource = self
}
override func setupLayout() {
LayoutConstraint.pin(to: .bounds, scrollView).activate()
}
override func setupDefaults() {
tableView.sizeLastColumnToFit()
}
}
extension WelcomeRecentDocumentsView {
class RowView: NSTableRowView {
let separatorHeight: CGFloat
let separatorColor: NSColor
init(separatorHeight: CGFloat = 1, separatorColor: NSColor = .gridColor) {
self.separatorHeight = separatorHeight
self.separatorColor = separatorColor
super.init(frame: .zero)
}
required init?(coder decoder: NSCoder) {
fatalError()
}
override func drawSelection(in dirtyRect: NSRect) {
let rect = bounds.insetBottom(by: -separatorHeight)
NSColor.alternateSelectedControlColor.setFill()
rect.fill()
}
override func drawSeparator(in dirtyRect: NSRect) {
let rect = bounds.insetTop(by: bounds.height - separatorHeight)
separatorColor.setFill()
rect.fill()
}
}
class ItemView: View {
static let defaultHeight: CGFloat = 52
override var intrinsicContentSize: NSSize {
return CGSize(intrinsicHeight: type(of: self).defaultHeight)
}
}
}