]> git.r.bdr.sh - rbdr/captura/blobdiff - Captura/Presentation/Windows/RecordingWindow.swift
Format the code
[rbdr/captura] / Captura / Presentation / Windows / RecordingWindow.swift
index 3395c549c65ba6a7734772354de906da45442f1e..71e3a5577605ffd75735886b562b9f1518315ac9 100644 (file)
@@ -1,24 +1,36 @@
+/*
+ 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(_ button: NSRect?) {
-    
-    let screens = NSScreen.screens
-    var boundingBox = NSZeroRect
-    for screen in screens {
-        boundingBox = NSUnionRect(boundingBox, screen.frame)
-    }
-    
+
+  init(_ configuration: CaptureSessionConfiguration, _ button: NSRect?) {
+
+    let boundingBox = NSScreen.screenWithMouse?.frame ?? NSZeroRect
+
     super.init(
       contentRect: boundingBox,
       styleMask: [.borderless],
@@ -33,39 +45,41 @@ class RecordingWindow: NSWindow {
     self.titlebarAppearsTransparent = true
     self.setFrame(boundingBox, display: true)
     self.titleVisibility = .hidden
-    let recordingView = RecordingContentView()
+    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)
+    // 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
   }
-  
+
   // 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 {
@@ -75,73 +89,130 @@ 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, 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
+
+    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),
+        width: configuration.width ?? 400,
+        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
   private var mouseLocation: NSPoint = NSPoint()
   private var origin: NSPoint = NSPoint()
   private var boxOrigin: NSPoint = NSPoint()
-  
+  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 {
@@ -167,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 {
@@ -178,7 +249,7 @@ class RecordingContentView: NSView {
         height: round(abs(mouseLocation.y - origin.y))
       )
     }
-    
+
     if box != nil {
       if state == .moving {
         NSCursor.closedHand.set()
@@ -186,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)),
@@ -198,7 +269,7 @@ class RecordingContentView: NSView {
     }
     self.setNeedsDisplay(self.bounds)
   }
-  
+
   override func cursorUpdate(with event: NSEvent) {
     NSCursor.crosshair.set()
   }
@@ -206,7 +277,7 @@ class RecordingContentView: NSView {
   override func hitTest(_ point: NSPoint) -> NSView? {
     return shouldPassthrough ? nil : self
   }
-      
+
   override var acceptsFirstResponder: Bool {
     return true
   }
@@ -214,26 +285,30 @@ 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) {
+
+      if resizeBox!.contains(origin) && !preventResize {
         self.origin = NSPoint(x: box.minX, y: box.maxY)
         state = .resizing
         return
       }
-      if box.contains(origin) {
+      if box.contains(origin) && !preventMove {
         state = .moving
         self.boxOrigin = NSPoint(x: box.origin.x, y: box.origin.y)
         return
       }
     }
-    
+
+    if preventResize || preventMove {
+      return
+    }
+
     state = .drawing
   }
 
@@ -242,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) {
@@ -261,7 +336,7 @@ class RecordingContentView: NSView {
       }
     }
   }
-  
+
   override func draw(_ dirtyRect: NSRect) {
     if shouldPassthrough {
       NSColor.clear.setFill()
@@ -269,42 +344,54 @@ class RecordingContentView: NSView {
       NSColor(white: 1.0, alpha: 0.001).setFill()
     }
     dirtyRect.fill()
-    
+
     let dashLength: CGFloat = 5.0
     let lineWidth = 0.5
 
+    // 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
@@ -316,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)
@@ -327,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))
@@ -342,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)
     }
@@ -391,4 +485,26 @@ class RecordingContentView: NSView {
 
     text.draw(in: textRect, withAttributes: textAttributes)
   }
+
+  private func moveWindow() {
+    let screen = NSScreen.screenWithMouse
+    if let currentScreen = self.window?.screen {
+      if currentScreen != screen {
+        let frame = screen?.frame ?? NSZeroRect
+        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()
+        }
+      }
+    }
+  }
 }