Tony
Tony

Reputation: 13

getting position of right click in macos app

I'm working on a macOS app with SwiftUI that should be able to create, arrange and delete geometric shapes on screen. The creation and dragging of shapes already works pretty well using a context menu.

import SwiftUI

class Canvas: ObservableObject {
    
    @Published var nodes: [Node] = []
    
    func addNode(position: CGPoint) -> Void {
        nodes.append(Node(id: UUID(), position: position))
    }
}

struct CanvasView: View {
    
    @ObservedObject var canvas = Canvas()
    
    var body: some View {
        ZStack {
            Color(red: 0.9, green: 0.9, blue: 0.8)
                .contextMenu {
                    Button( action: {
                        self.canvas.addNode(position: CGPoint(x: 400, y: 400))
                        
                    } )
                        { Text("Add Node ...") }
                }
            ForEach(canvas.nodes) {node in
                NodeView(node: node)
            }
        }
    }
}

class Node: Identifiable, ObservableObject {
    @Published var id: UUID
    @Published var position: CGPoint
    @Published var positionProxy: CGPoint
    
    init (id: UUID, position: CGPoint) {
        self.id = id
        self.position = position
        self.positionProxy = position
    }
}

struct NodeView: View {
    
    @ObservedObject var node: Node
    
    init(node: Node) {
        self.node = node
    }
    
    var draggingNode: some Gesture {
        DragGesture(coordinateSpace: .global)
            .onChanged { value in
                self.node.position.x = value.translation.width + self.node.positionProxy.x;
                self.node.position.y = -value.translation.height + self.node.positionProxy.y
            }
            .onEnded { value in
                self.node.position.x = value.translation.width + self.node.positionProxy.x;
                self.node.position.y = -value.translation.height + self.node.positionProxy.y;
                self.node.positionProxy = self.node.position
            }
    }
    
    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(Color.white)
            .frame(width: 100, height: 100)
            .overlay(
                RoundedRectangle(cornerRadius: 20)
                    .stroke(lineWidth: 1)
                    .fill(Color.gray)
            )
            .position(node.position)
            .gesture(draggingNode)
    }
}

My problem is that all created shapes appear at the same predefined location

position: CGPoint(x: 400, y: 400)

on screen and I have to move each of them manually to its intended position. I'm looking for a way to track the cursor position during right click or the context menu position to use it as node position and be able to write something like

self.canvas.addNode(position: cursorPosition)

instead of

self.canvas.addNode(position: CGPoint(x: 400, y: 400))

Is there any functionality in Swift, preferably in SwiftUI that solves my issue?

Upvotes: 0

Views: 350

Answers (1)

Louis Lac
Louis Lac

Reputation: 6436

It is currently not possible to detect mouse click location in SwiftUI. A workaround for your issue may be to use a composition of SwiftUI Gesture's.

Here is code that you can use to detect a (simple or double) left-click location:

import SwiftUI

struct ClickGesture: Gesture {
    let count: Int
    let coordinateSpace: CoordinateSpace
    
    typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
    
    init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
        precondition(count > 0, "Count must be greater than or equal to 1.")
        self.count = count
        self.coordinateSpace = coordinateSpace
    }
    
    var body: SimultaneousGesture<TapGesture, DragGesture> {
        TapGesture(count: count)
            .simultaneously(with: DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace))
    }
    
    func onEnded(perform action: @escaping (CGPoint) -> Void) -> some Gesture {
        ClickGesture(count: count, coordinateSpace: coordinateSpace)
            .onEnded { (value: Value) -> Void in
                guard value.first != nil else { return }
                guard let startLocation = value.second?.startLocation else { return }
                guard let endLocation = value.second?.location else { return }
                guard ((startLocation.x-1)...(startLocation.x+1)).contains(endLocation.x),
                      ((startLocation.y-1)...(startLocation.y+1)).contains(endLocation.y) else { return }
                
                action(startLocation)
            }
    }
}

extension View {
    func onClickGesture(
        count: Int,
        coordinateSpace: CoordinateSpace = .local,
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
            .onEnded(perform: action)
        )
    }
    
    func onClickGesture(
        count: Int,
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        onClickGesture(count: count, coordinateSpace: .local, perform: action)
    }
    
    func onClickGesture(
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        onClickGesture(count: 1, coordinateSpace: .local, perform: action)
    }
}

You can use it in a very similar fashion as onTapGesture() or TapGesture:

struct MyView: View {
  var body: some View {
    Rectangle()
      .frame(width: 600, height: 400)
      .onClickGesture(count: 2) { location in 
        print("Double tap at location \(location)")
      }
  }
}

You can additionally specify a CoordinateSpace.

Upvotes: 0

Related Questions