X-Git-Url: https://git.r.bdr.sh/rbdr/captura/blobdiff_plain/7ee43fb83799abb89a69cfcd4e2146dd4eaa5045..47eb1128eb930279d0fcf2e836d78372ac7ef5c3:/Captura/Presentation/Windows/RecordingWindow.swift?ds=sidebyside diff --git a/Captura/Presentation/Windows/RecordingWindow.swift b/Captura/Presentation/Windows/RecordingWindow.swift index 370a47a..71e3a55 100644 --- a/Captura/Presentation/Windows/RecordingWindow.swift +++ b/Captura/Presentation/Windows/RecordingWindow.swift @@ -1,27 +1,42 @@ +/* + Copyright (C) 2024 Rubén Beltrán del Río + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see https://captura.tranquil.systems. + */ import Cocoa import Combine class RecordingWindow: NSWindow { - + var pixelDensity: CGFloat { self.screen?.backingScaleFactor ?? 1.0 } - + var recordingContentView: RecordingContentView { self.contentView as! RecordingContentView } - + init(_ configuration: CaptureSessionConfiguration, _ button: NSRect?) { - + let boundingBox = NSScreen.screenWithMouse?.frame ?? NSZeroRect - + super.init( contentRect: boundingBox, styleMask: [.borderless], backing: .buffered, defer: false) - self.isReleasedWhenClosed = false self.collectionBehavior = [.canJoinAllSpaces] self.isMovableByWindowBackground = false @@ -30,45 +45,41 @@ class RecordingWindow: NSWindow { self.titlebarAppearsTransparent = true self.setFrame(boundingBox, display: true) self.titleVisibility = .hidden - let recordingView = RecordingContentView(configuration, frame: boundingBox) + let recordingView = RecordingContentView( + configuration, frame: boundingBox, button: button ?? NSZeroRect) recordingView.frame = boundingBox - recordingView.button = button self.contentView = recordingView - //self.backgroundColor = NSColor(white: 1.0, alpha: 0.001) - self.backgroundColor = NSColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.5) + self.backgroundColor = NSColor(white: 1.0, alpha: 0.001) + // uncomment below to debug window placement visually + // self.backgroundColor = NSColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.5) self.level = .screenSaver self.isOpaque = false self.hasShadow = false - - print("AAAAH INIT CHANGE") - print("AAAAH FRAME X: \(recordingView.frame.minX) \(recordingView.frame.maxX) // Y: \(recordingView.frame.minY) \(recordingView.frame.maxY)") - print("AAAAH BOUNDS X: \(recordingView.bounds.minX) \(recordingView.bounds.maxX) // Y: \(recordingView.bounds.minY) \(recordingView.bounds.maxY)") - print("AAAAH WIN F X: \(self.frame.minX) \(self.frame.maxX) // Y: \(self.frame.minY) \(self.frame.maxY)") } - + // MARK: - Window Behavior Overrides - + override func resetCursorRects() { super.resetCursorRects() let cursor = NSCursor.crosshair self.contentView?.addCursorRect(self.contentView!.bounds, cursor: cursor) } - + override var canBecomeKey: Bool { return true } - + override var canBecomeMain: Bool { return true } - + override func resignMain() { super.resignMain() if (self.contentView as? RecordingContentView)?.state != .recording { self.ignoresMouseEvents = false } } - + override func becomeMain() { super.becomeMain() if (self.contentView as? RecordingContentView)?.state != .recording { @@ -78,18 +89,35 @@ class RecordingWindow: NSWindow { } enum RecordingWindowState { - case passthrough, idle, drawing, moving, resizing, recording; + case passthrough, idle, drawing, moving, resizing, recording } class RecordingContentView: NSView { - - init(_ configuration: CaptureSessionConfiguration, frame: NSRect) { + + init(_ configuration: CaptureSessionConfiguration, frame: NSRect, button: NSRect) { + self.buttonSize = button.size + var buttonOffset = NSPoint() + for screen in NSScreen.screens { + if screen.frame.intersects(button) { + let relativeY = screen.frame.height - (button.minY - screen.frame.minY) + let relativeX = screen.frame.width - (button.minX - screen.frame.minX) + buttonOffset = NSPoint(x: relativeX, y: relativeY) + } + } + self.buttonOffset = buttonOffset super.init(frame: frame) preventResize = configuration.preventResize preventMove = configuration.preventMove autoStart = configuration.autoStart - - if configuration.x != nil || configuration.y != nil || configuration.width != nil || configuration.height != nil { + + self.bounds = frame + self.button = NSRect( + x: frame.maxX - buttonOffset.x, y: frame.maxY - buttonOffset.y, width: buttonSize.width, + height: buttonSize.height) + + if configuration.x != nil || configuration.y != nil || configuration.width != nil + || configuration.height != nil + { box = NSRect( x: configuration.x ?? Int(frame.width / 2.0), y: configuration.y ?? Int(frame.height / 2.0), @@ -97,18 +125,20 @@ class RecordingContentView: NSView { height: configuration.height ?? 400 ) } - + if autoStart { DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { NotificationCenter.default.post(name: .startRecording, object: nil, userInfo: nil) } } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + private let buttonSize: NSSize + private let buttonOffset: NSPoint public var button: NSRect? = nil @Published public var box: NSRect? = nil public var state: RecordingWindowState = .idle @@ -118,68 +148,71 @@ class RecordingContentView: NSView { private var preventResize = false private var preventMove = false private var autoStart = false - + private var resizeBox: NSRect? { if let box { return NSRect(x: box.maxX - 5, y: box.minY - 5, width: 10, height: 10) } return nil } - + private var shouldPassthrough: Bool { state == .recording || state == .passthrough } - + // MARK: - State changing API - + public func startRecording() { state = .recording window?.ignoresMouseEvents = true } - + public func stopRecording() { - + } - + public func reset() { state = .idle window?.ignoresMouseEvents = false } - + public func startPassthrough() { state = .passthrough window?.ignoresMouseEvents = true } - + public func stopPassthrough() { state = .idle window?.ignoresMouseEvents = false } - + // MARK: - View Behavior Overrides - + override func updateTrackingAreas() { super.updateTrackingAreas() - + for trackingArea in self.trackingAreas { self.removeTrackingArea(trackingArea) } - let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeInKeyWindow, .cursorUpdate, .mouseMoved] - let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) + let options: NSTrackingArea.Options = [ + .mouseEnteredAndExited, .activeInKeyWindow, .cursorUpdate, .mouseMoved, + ] + let trackingArea = NSTrackingArea( + rect: self.bounds, options: options, owner: self, userInfo: nil) self.addTrackingArea(trackingArea) } - + override func mouseExited(with event: NSEvent) { if state == .idle && box == nil { self.moveWindow() } } - + override func mouseMoved(with event: NSEvent) { - + self.mouseLocation = self.convert(event.locationInWindow, from: nil) - + if shouldPassthrough { NSCursor.arrow.set() } else { @@ -205,7 +238,7 @@ class RecordingContentView: NSView { self.setNeedsDisplay(self.bounds) } - + override func mouseDragged(with event: NSEvent) { self.mouseLocation = self.convert(event.locationInWindow, from: nil) if state == .drawing { @@ -216,7 +249,7 @@ class RecordingContentView: NSView { height: round(abs(mouseLocation.y - origin.y)) ) } - + if box != nil { if state == .moving { NSCursor.closedHand.set() @@ -224,7 +257,7 @@ class RecordingContentView: NSView { x: self.boxOrigin.x - self.origin.x + self.mouseLocation.x, y: self.boxOrigin.y - self.origin.y + self.mouseLocation.y) } - + if state == .resizing { box = NSRect( x: round(min(origin.x, mouseLocation.x)), @@ -236,7 +269,7 @@ class RecordingContentView: NSView { } self.setNeedsDisplay(self.bounds) } - + override func cursorUpdate(with event: NSEvent) { NSCursor.crosshair.set() } @@ -244,7 +277,7 @@ class RecordingContentView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { return shouldPassthrough ? nil : self } - + override var acceptsFirstResponder: Bool { return true } @@ -252,14 +285,14 @@ class RecordingContentView: NSView { override func mouseDown(with event: NSEvent) { self.origin = self.convert(event.locationInWindow, from: nil) if let box { - + if let button { if button.contains(origin) { NotificationCenter.default.post(name: .startRecording, object: nil, userInfo: nil) return } } - + if resizeBox!.contains(origin) && !preventResize { self.origin = NSPoint(x: box.minX, y: box.maxY) state = .resizing @@ -271,11 +304,11 @@ class RecordingContentView: NSView { return } } - + if preventResize || preventMove { return } - + state = .drawing } @@ -284,16 +317,16 @@ class RecordingContentView: NSView { state = .idle } } - + override func keyDown(with event: NSEvent) { switch event.keyCode { - case 53: // Escape key - NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) - default: - super.keyDown(with: event) + case 53: // Escape key + NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) + default: + super.keyDown(with: event) } } - + override func flagsChanged(with event: NSEvent) { if state == .idle { if event.modifierFlags.contains(.shift) { @@ -303,7 +336,7 @@ class RecordingContentView: NSView { } } } - + override func draw(_ dirtyRect: NSRect) { if shouldPassthrough { NSColor.clear.setFill() @@ -311,53 +344,54 @@ class RecordingContentView: NSView { NSColor(white: 1.0, alpha: 0.001).setFill() } dirtyRect.fill() - + let dashLength: CGFloat = 5.0 let lineWidth = 0.5 - - if let button { - let buttonPath = NSBezierPath() - buttonPath.move(to: NSPoint(x: button.minX, y: button.minY)) - buttonPath.line(to: NSPoint(x: button.maxX, y: button.minY)) - buttonPath.line(to: NSPoint(x: button.maxX, y: button.maxY)) - buttonPath.line(to: NSPoint(x: button.minX, y: button.maxY)) - buttonPath.line(to: NSPoint(x: button.minX, y: button.minY)) - NSColor(red: 1, green: 0, blue: 1, alpha: 1).setFill() - buttonPath.fill() - } + + // Uncomment below to debug button placement visually + // if let button { + // let buttonPath = NSBezierPath() + // buttonPath.move(to: NSPoint(x: button.minX, y: button.minY)) + // buttonPath.line(to: NSPoint(x: button.maxX, y: button.minY)) + // buttonPath.line(to: NSPoint(x: button.maxX, y: button.maxY)) + // buttonPath.line(to: NSPoint(x: button.minX, y: button.maxY)) + // buttonPath.line(to: NSPoint(x: button.minX, y: button.minY)) + // NSColor(red: 1, green: 0, blue: 1, alpha: 1).setFill() + // buttonPath.fill() + // } if state == .idle && box == nil { let blackLine = NSBezierPath() blackLine.lineWidth = lineWidth blackLine.setLineDash([dashLength, dashLength], count: 2, phase: 0) - + // Vertical line (Black) blackLine.move(to: NSPoint(x: self.mouseLocation.x, y: NSMinY(self.bounds))) blackLine.line(to: NSPoint(x: self.mouseLocation.x, y: NSMaxY(self.bounds))) - + // Horizontal line (Black) blackLine.move(to: NSPoint(x: NSMinX(self.bounds), y: self.mouseLocation.y)) blackLine.line(to: NSPoint(x: NSMaxX(self.bounds), y: self.mouseLocation.y)) - + NSColor.black.setStroke() blackLine.stroke() - + let whiteLine = NSBezierPath() whiteLine.lineWidth = lineWidth whiteLine.setLineDash([dashLength, dashLength], count: 2, phase: dashLength) - + // Vertical line (White) whiteLine.move(to: NSPoint(x: self.mouseLocation.x, y: NSMinY(self.bounds))) whiteLine.line(to: NSPoint(x: self.mouseLocation.x, y: NSMaxY(self.bounds))) - + // Horizontal line (White) whiteLine.move(to: NSPoint(x: NSMinX(self.bounds), y: self.mouseLocation.y)) whiteLine.line(to: NSPoint(x: NSMaxX(self.bounds), y: self.mouseLocation.y)) - + NSColor.white.setStroke() whiteLine.stroke() } - + if let box { let blackBox = NSBezierPath() blackBox.lineWidth = lineWidth @@ -369,7 +403,7 @@ class RecordingContentView: NSView { blackBox.line(to: NSPoint(x: box.minX, y: box.minY)) NSColor.black.setStroke() blackBox.stroke() - + let whiteBox = NSBezierPath() whiteBox.lineWidth = lineWidth whiteBox.setLineDash([dashLength, dashLength], count: 2, phase: dashLength) @@ -380,11 +414,11 @@ class RecordingContentView: NSView { whiteBox.line(to: NSPoint(x: box.minX, y: box.minY)) NSColor.white.setStroke() whiteBox.stroke() - + if state == .recording { return } - + if let resizeBox { let clearBox = NSBezierPath() clearBox.move(to: NSPoint(x: resizeBox.minX, y: resizeBox.minY)) @@ -395,46 +429,53 @@ class RecordingContentView: NSView { NSColor(white: 0, alpha: 0.2).setFill() clearBox.fill() } - + if state == .moving { let string = "\(Int(box.minX)), \(Int(box.maxY))" as NSString - drawText(string, NSPoint( - x: box.minX, - y: box.maxY - ), true) + drawText( + string, + NSPoint( + x: box.minX, + y: box.maxY + ), true) } - + if state == .resizing { let string = "\(Int(mouseLocation.x)), \(Int(mouseLocation.y))" as NSString drawText(string, mouseLocation) } - + if box.contains(mouseLocation) && state != .resizing { - return; + return } } - + // Draw text let string = "\(Int(mouseLocation.x)), \(Int(mouseLocation.y))" as NSString drawText(string, mouseLocation) } - + // MARK: - Utilities - + private func drawText(_ text: NSString, _ location: NSPoint, _ isBottomRight: Bool = false) { - + let textAttributes = [ - NSAttributedString.Key.font: NSFont(name: "Hiragino Mincho ProN", size: 12) ?? NSFont.systemFont(ofSize: 12), + NSAttributedString.Key.font: NSFont(name: "Hiragino Mincho ProN", size: 12) + ?? NSFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: NSColor.white, ] let offset = NSPoint(x: 10, y: 10) let padding = NSPoint(x: 5, y: 2) let size = text.size(withAttributes: textAttributes) - var rect = NSRect(x: location.x + offset.x, y: location.y + offset.y, width: size.width + 2 * padding.x, height: size.height + 2 * padding.y) - var textRect = NSRect(x: location.x + offset.x + padding.x, y: location.y + offset.y + padding.y, width: size.width, height: size.height) - - if (isBottomRight) { + var rect = NSRect( + x: location.x + offset.x, y: location.y + offset.y, width: size.width + 2 * padding.x, + height: size.height + 2 * padding.y) + var textRect = NSRect( + x: location.x + offset.x + padding.x, y: location.y + offset.y + padding.y, width: size.width, + height: size.height) + + if isBottomRight { rect = rect.offsetBy(dx: -size.width - 2 * offset.x, dy: 0) textRect = textRect.offsetBy(dx: -size.width - 2 * offset.x, dy: 0) } @@ -444,29 +485,24 @@ class RecordingContentView: NSView { text.draw(in: textRect, withAttributes: textAttributes) } - + private func moveWindow() { - print("AAAAH BEFORE WE CHANGE") - print("AAAAH FRAME X: \(self.frame.minX) \(self.frame.maxX) // Y: \(self.frame.minY) \(self.frame.maxY)") - print("AAAAH BOUNDS X: \(self.bounds.minX) \(self.bounds.maxX) // Y: \(self.bounds.minY) \(self.bounds.maxY)") - print("AAAAH WIN F X: \(self.window?.frame.minX) \(self.window?.frame.maxX) // Y: \(self.window?.frame.minY) \(self.window?.frame.maxY)") let screen = NSScreen.screenWithMouse if let currentScreen = self.window?.screen { if currentScreen != screen { let frame = screen?.frame ?? NSZeroRect - self.frame = frame - self.bounds = frame + self.frame = CGRect(origin: NSZeroPoint, size: frame.size) + self.bounds = CGRect(origin: NSZeroPoint, size: frame.size) self.updateTrackingAreas() - + if let window = self.window { self.bounds = frame + self.button = NSRect( + x: frame.maxX - buttonOffset.x, y: frame.maxY - buttonOffset.y, width: buttonSize.width, + height: buttonSize.height) window.setFrame(frame, display: true, animate: false) window.makeKeyAndOrderFront(nil) window.orderFrontRegardless() - print("AAAAH AFTER CHANGE") - print("AAAAH FRAME X: \(self.frame.minX) \(self.frame.maxX) // Y: \(self.frame.minY) \(self.frame.maxY)") - print("AAAAH BOUNDS X: \(self.bounds.minX) \(self.bounds.maxX) // Y: \(self.bounds.minY) \(self.bounds.maxY)") - print("AAAAH WIN F X: \(self.window?.frame.minX) \(self.window?.frame.maxX) // Y: \(self.window?.frame.minY) \(self.window?.frame.maxY)") } } }