// // KMDrawView.swift // PDF Reader Pro // // Created by lizhe on 2023/10/9. // import Cocoa private var _index: Int = 0 private var _points: [CGPoint] = [CGPoint](repeating: .zero, count: 5) @objc protocol KMDrawViewDelegate: NSObjectProtocol { func drawViewDidFinishTouchMode(_ drawView: KMDrawView) } @objcMembers class KMDrawView: NSView { private var drawImage: NSImage? var drawColor: NSColor = NSColor(red: 0, green: 0, blue: 0, alpha: 1) { didSet { self.needsDisplay = true } } var strokeRadius: CGFloat = 0.3 { didSet { self.needsDisplay = true } } private var bezierPath: NSBezierPath = NSBezierPath() var isAcceptsTouch: Bool = false { willSet{ } didSet{ self.acceptsTouchEvents = isAcceptsTouch let screenHeight = CGDisplayPixelsHigh(CGMainDisplayID()) let frameToWindow = self.convert(self.bounds, to: nil) var frameToScreen = self.window!.convertToScreen(frameToWindow) if isAcceptsTouch { // If the mouse cursor is not already hidden, if !self.cursorIsHidden { frameToScreen.origin.x += 5 frameToScreen.origin.y = CGFloat(screenHeight) - frameToScreen.origin.y - 5 CGWarpMouseCursorPosition(frameToScreen.origin) // Detach the mouse cursor from the mouse // hardware so that moving the mouse (or a // single finger) will not move the cursor CGAssociateMouseAndMouseCursorPosition(0) // Hide the mouse cursor NSCursor.hide() // Remember that we detached and hid the // mouse cursor self.cursorIsHidden = true } } else { frameToScreen.origin.y = CGFloat(screenHeight) - frameToScreen.origin.y CGWarpMouseCursorPosition(frameToScreen.origin) // Attach the mouse cursor to the mouse // hardware so that moving the mouse (or a // single finger) will move the cursor CGAssociateMouseAndMouseCursorPosition(1) // Show the mouse cursor NSCursor.unhide() // Remember that we attached and unhid the // mouse cursor so that the next touch that // begins will detach and hide it self.cursorIsHidden = false } } } private var cursorIsHidden: Bool = false private var mouseIsInView: Bool = false private var activeTouch: NSTouch? weak var delegate: KMDrawViewDelegate? var drawBezierPath: NSBezierPath { return bezierPath } var touchEndCallback: ((Bool) -> Void)? var changeDrawCallback: ((Bool) -> Void)? // override func acceptsFirstResponder() -> Bool { // return true // } override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.drawImage = NSImage(size: self.frame.size) self.drawColor = NSColor(red: 0, green: 0, blue: 0, alpha: 1) self.strokeRadius = 0.3 self.wantsLayer = true self.layer?.borderWidth = 1.0 self.layer?.borderColor = NSColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.05).cgColor } required init?(coder: NSCoder) { super.init(coder: coder) } func clearImage() { self.drawImage = nil self.bezierPath.removeAllPoints() self.needsDisplay = true if let touchEndCallback = self.touchEndCallback { touchEndCallback(true) } } func signatureImage() -> NSImage? { var rect = CGRect.zero if bezierPath.isEmpty { return nil } else { rect = bezierPath.bounds } let size = CGSize(width: rect.size.width + bezierPath.lineWidth, height: rect.size.height + bezierPath.lineWidth) if size.width <= 0 && size.height <= 0 { return nil } let image = NSImage(size: self.bounds.size) image.lockFocus() drawColor.set() drawBezierPath.lineWidth = strokeRadius * 2 drawBezierPath.lineCapStyle = .round drawBezierPath.lineJoinStyle = .round drawBezierPath.stroke() image.unlockFocus() return image } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) NSGraphicsContext.saveGraphicsState() NSColor.clear.set() if window?.firstResponder == self && mouseIsInView { focusRingType = .`default` } NSBezierPath(rect: bounds).fill() NSGraphicsContext.restoreGraphicsState() if let drawImage = self.drawImage { let imageFrame = imageFrameInRect(rect: dirtyRect) drawImage.draw(in: imageFrame) } drawColor.set() bezierPath.lineWidth = strokeRadius * 2 bezierPath.lineCapStyle = .round bezierPath.lineJoinStyle = .round bezierPath.stroke() } private func imageFrameInRect(rect: CGRect) -> CGRect { var originX: CGFloat var originY: CGFloat var width: CGFloat var height: CGFloat if drawImage?.size.width ?? 0 < rect.size.width && drawImage?.size.height ?? 0 < rect.size.height { originX = (rect.size.width - (drawImage?.size.width ?? 0)) / 2.0 originY = (rect.size.height - (drawImage?.size.height ?? 0)) / 2.0 width = drawImage?.size.width ?? 0 height = drawImage?.size.height ?? 0 } else { if (drawImage?.size.width ?? 0) / (drawImage?.size.height ?? 0) > rect.size.width / rect.size.height { width = rect.size.width height = (rect.size.width * (drawImage?.size.height ?? 0)) / (drawImage?.size.width ?? 0) } else { height = rect.size.height width = (rect.size.height * (drawImage?.size.width ?? 0)) / (drawImage?.size.height ?? 0) } originX = (rect.size.width - width) / 2.0 originY = (rect.size.height - height) / 2.0 } let rect = NSMakeRect(originX, originY, width, height) return rect } override func viewDidMoveToWindow() { if window != nil { addTrackingRect(bounds, owner: self, userData: nil, assumeInside: false) } } override func touchesBegan(with event: NSEvent) { super.touchesBegan(with: event) let touches = event.touches(matching: .began, in: self) self.activeTouch = touches.first guard let activeTouch = self.activeTouch else { return } var point = activeTouch.normalizedPosition point.x *= self.bounds.size.width point.y *= self.bounds.size.height _index = 0 _points[0] = point self.needsDisplay = true if !self.cursorIsHidden { CGAssociateMouseAndMouseCursorPosition(0) NSCursor.hide() self.cursorIsHidden = true } if let changeDrawCallback = changeDrawCallback { changeDrawCallback(true) } } override func touchesMoved(with event: NSEvent) { super.touchesMoved(with: event) let touches = event.touches(matching: .moved, in: self) var isTouch = false for touch in touches { if touch.identity.isEqual(self.activeTouch?.identity) { isTouch = true self.activeTouch = touch } } guard isTouch else { return } var point = self.activeTouch!.normalizedPosition point.x *= self.bounds.size.width point.y *= self.bounds.size.height _index += 1 _points[_index] = point if _index == 4 { _points[3] = CGPoint(x: (_points[2].x + _points[4].x)/2.0, y: (_points[2].y + _points[4].y)/2.0) bezierPath.move(to: _points[0]) bezierPath.curve(to: _points[3], controlPoint1: _points[1], controlPoint2: _points[2]) _points[0] = _points[3] _points[1] = _points[4] _index = 1 self.needsDisplay = true } } override func touchesEnded(with event: NSEvent) { super.touchesEnded(with: event) let touches = event.touches(matching: .moved, in: self) for touch in touches { if touch.identity.isEqual(self.activeTouch?.identity) { self.activeTouch = nil } } if _index < 4 { for i in 0..<_index { bezierPath.move(to: _points[i]) } self.needsDisplay = true } // var image = signatureImage() // if let changeDrawCallback = changeDrawCallback { // changeDrawCallback(image) // } } override func touchesCancelled(with event: NSEvent) { super.touchesCancelled(with: event) for i in 0..<_index { bezierPath.move(to: _points[i]) } activeTouch = nil self.needsDisplay = true } override func mouseEntered(with event: NSEvent) { window?.makeFirstResponder(self) // self.mouseIsInView = true needsDisplay = true } override func mouseExited(with event: NSEvent) { // self.mouseIsInView = false needsDisplay = true } override func mouseDown(with event: NSEvent) { if acceptsTouchEvents { return } let point = convert(event.locationInWindow, from: nil) _index = 0 _points[0] = point } override func mouseDragged(with event: NSEvent) { if acceptsTouchEvents { return } let point = convert(event.locationInWindow, from: nil) _index += 1 _points[_index] = point if _index == 4 { _points[3] = CGPoint(x: (_points[2].x + _points[4].x) / 2.0, y: (_points[2].y + _points[4].y) / 2.0) bezierPath.move(to: _points[0]) bezierPath.curve(to: _points[3], controlPoint1: _points[1], controlPoint2: _points[2]) _points[0] = _points[3] _points[1] = _points[4] _index = 1 needsDisplay = true if let changeDrawCallback = self.changeDrawCallback { changeDrawCallback(true) } } } override func mouseUp(with event: NSEvent) { if acceptsTouchEvents { return } if _index < 4 { for i in 0..<_index { bezierPath.move(to: _points[i]) } needsDisplay = true } if let touchEndCallback = self.touchEndCallback { touchEndCallback(false) } } override func keyDown(with event: NSEvent) { if let characters = event.characters, characters.count > 0 { let character = characters.first! // if character == "\u{1B}" { // Check for the Escape key if isAcceptsTouch { isAcceptsTouch = false if let delegate = self.delegate { delegate.drawViewDidFinishTouchMode(self) } return } // } } super.keyDown(with: event) } }