JohnCoates/Aerial

View on GitHub
Aerial/Source/Views/PrefPanel/DisplayView.swift

Summary

Maintainability
D
2 days
Test Coverage
//
//  DisplayView.swift
//  Aerial
//
//  Created by Guillaume Louel on 09/05/2019.
//  Copyright © 2019 John Coates. All rights reserved.
//

import Foundation
import Cocoa

class DisplayPreview: NSObject {
    var screen: Screen
    var previewRect: CGRect

    init(screen: Screen, previewRect: CGRect) {
        self.screen = screen
        self.previewRect = previewRect
    }
}

extension NSImage {
    func flipped(flipHorizontally: Bool = false, flipVertically: Bool = false) -> NSImage {
        let flippedImage = NSImage(size: size)

        flippedImage.lockFocus()

        NSGraphicsContext.current?.imageInterpolation = .high

        let transform = NSAffineTransform()
        transform.translateX(by: flipHorizontally ? size.width : 0, yBy: flipVertically ? size.height : 0)
        transform.scaleX(by: flipHorizontally ? -1 : 1, yBy: flipVertically ? -1 : 1)
        transform.concat()

        draw(at: .zero, from: NSRect(origin: .zero, size: size), operation: .sourceOver, fraction: 1)

        flippedImage.unlockFocus()

        return flippedImage
    }
}

class DisplayView: NSView {
    // We store our computed previews here
    var displayPreviews = [DisplayPreview]()

    // MARK: - Lifecycle
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    // MARK: - Drawing
    // swiftlint:disable:next cyclomatic_complexity
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        // We need to handle dark mode
        var backgroundColor = NSColor.init(white: 0.9, alpha: 1.0)
        var borderColor = NSColor.init(white: 0.8, alpha: 1.0)

        // let screenColor = NSColor.init(red: 0.38, green: 0.60, blue: 0.85, alpha: 1.0)
        let screenBorderColor = NSColor.black

        if DarkMode.isEnabled() {
            backgroundColor = NSColor.init(white: 0.2, alpha: 1.0)
            borderColor = NSColor.init(white: 0.6, alpha: 1.0)
        }

        // Draw background with a 1pt border
        borderColor.setFill()
        __NSRectFill(dirtyRect)

        let path = NSBezierPath(rect: dirtyRect.insetBy(dx: 1, dy: 1))
        backgroundColor.setFill()
        path.fill()

        let displayDetection = DisplayDetection.sharedInstance
        displayPreviews = [DisplayPreview]()    // Empty the array in case we redraw

        // In order to draw the screen we need to know the total size of all
        // the displays together
        let globalRect = displayDetection.getGlobalScreenRect()

        var minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat, scaleFactor: CGFloat
        if (frame.width / frame.height) > (globalRect.width / globalRect.height) {
            // We fill vertically then
            maxY = frame.height - 60
            minY = 30
            scaleFactor = globalRect.height / maxY
            maxX = globalRect.width / scaleFactor
            minX = (frame.width - maxX)/2
        } else {
            // We fill horizontally
            maxX = frame.width - 60
            minX = 30
            scaleFactor = globalRect.width / maxX
            maxY = globalRect.height / scaleFactor
            minY = (frame.height - maxY)/2
        }

        // In spanned mode, we start by a faint full view of the span
        if PrefsDisplays.viewingMode == .spanned {
            let activeRect = displayDetection.getZeroedActiveSpannedRect()
            debugLog("spanned active rect \(activeRect)")
            let activeSRect = NSRect(x: minX + (activeRect.origin.x/scaleFactor),
                               y: minY + (activeRect.origin.y/scaleFactor),
                               width: activeRect.width/scaleFactor,
                               height: activeRect.height/scaleFactor)

            let bundle = Bundle(for: PanelWindowController.self)
            if let imagePath = bundle.path(forResource: "screen0", ofType: "jpg") {
                let image = NSImage(contentsOfFile: imagePath)
                image!.draw(in: activeSRect, from: calcScreenshotRect(src: activeSRect), operation: NSCompositingOperation.copy, fraction: 0.1)
            } else {
                errorLog("\(#file) screenshot is missing!!!")
            }
        }

        var idx = 0
        var shouldFlip = true
        // Now we draw each individual screen
        for screen in displayDetection.screens {
            let sRect = NSRect(x: minX + (screen.zeroedOrigin.x/scaleFactor),
                               y: minY + (screen.zeroedOrigin.y/scaleFactor),
                               width: screen.bottomLeftFrame.width/scaleFactor,
                               height: screen.bottomLeftFrame.height/scaleFactor)

            let sPath = NSBezierPath(rect: sRect)
            screenBorderColor.setFill()
            sPath.fill()

            let sInRect = sRect.insetBy(dx: 1, dy: 1)

            if PrefsDisplays.viewingMode != .spanned {
                if displayDetection.isScreenActive(id: screen.id) {
                    let bundle = Bundle(for: PanelWindowController.self)
                    if let imagePath = bundle.path(forResource: "screen"+String(idx), ofType: "jpg") {
                        var image = NSImage(contentsOfFile: imagePath)

                        if PrefsDisplays.viewingMode == .mirrored && shouldFlip {
                            image = image?.flipped(flipHorizontally: true, flipVertically: false)
                        }

                        shouldFlip = !shouldFlip

                        image!.draw(in: sInRect, from: calcScreenshotRect(src: sInRect), operation: NSCompositingOperation.copy, fraction: 1.0)
                    } else {
                        errorLog("\(#file) screenshot is missing!!!")
                    }

                    // Show difference images in independant mode to simulate
                    if PrefsDisplays.viewingMode == .independent {
                        if idx < 2 {
                            idx += 1
                        } else {
                            idx = 0
                        }
                    }
                } else {
                    // If the screen is innactive we fill it with a near black color
                    let sInPath = NSBezierPath(rect: sInRect)
                    let grey = NSColor(white: 0.1, alpha: 1.0)
                    grey.setFill()
                    sInPath.fill()
                }
            } else {
                // Spanned mode
                if displayDetection.isScreenActive(id: screen.id) {
                    // Calculate which portion of the image to display
                    let activeRect = displayDetection.getZeroedActiveSpannedRect()
                    let activeSRect = NSRect(x: minX + (activeRect.origin.x/scaleFactor),
                                             y: minY + (activeRect.origin.y/scaleFactor),
                                             width: activeRect.width/scaleFactor,
                                             height: activeRect.height/scaleFactor)
                    let ssRect = calcScreenshotRect(src: activeSRect)
                    let xFactor = ssRect.width / activeSRect.width
                    let yFactor = ssRect.height / activeSRect.height
                    // ...
                    let sFRect = CGRect(x: (sInRect.origin.x - activeSRect.origin.x) * xFactor + ssRect.origin.x,
                                        y: (sInRect.origin.y - activeSRect.origin.y) * yFactor + ssRect.origin.y,
                                        width: sInRect.width*xFactor,
                                        height: sInRect.height*yFactor)

                    let bundle = Bundle(for: PanelWindowController.self)
                    if let imagePath = bundle.path(forResource: "screen0", ofType: "jpg") {
                        let image = NSImage(contentsOfFile: imagePath)
                        // image!.draw(in: sInRect)
                        image!.draw(in: sInRect, from: sFRect, operation: NSCompositingOperation.copy, fraction: 1.0)
                    } else {
                        errorLog("\(#file) screenshot is missing!!!")
                    }
                }
            }

            // We preserve those calculations to handle our clicking logic
            displayPreviews.append(DisplayPreview(screen: screen, previewRect: sInRect))

            // We put a white bar on the main screen
            if screen.isMain {
                let mainRect = CGRect(x: sRect.origin.x, y: sRect.origin.y + sRect.height-8, width: sRect.width, height: 8)
                let sMainPath = NSBezierPath(rect: mainRect)
                NSColor.black.setFill()
                sMainPath.fill()
                let sMainInPath = NSBezierPath(rect: mainRect.insetBy(dx: 1, dy: 1))
                NSColor.white.setFill()
                sMainInPath.fill()
            }
        }
    }

    // Helper to keep aspect ratio of screenshots to be displayed
    func calcScreenshotRect(src: CGRect) -> CGRect {
        var minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat, scaleFactor: CGFloat

        let imgw: CGFloat = 720
        let imgh: CGFloat = 400

        if (imgw/imgh) < (src.width/src.height) {
            minX = 0
            maxX = imgw
            scaleFactor = src.width / maxX
            maxY = src.height / scaleFactor
            minY = (imgh - maxY)/2
        } else {
            minY = 0
            maxY = imgh
            scaleFactor = src.height / maxY
            maxX = src.width / scaleFactor
            minX = (imgw - maxX)/2
        }

        return CGRect(x: minX, y: minY, width: maxX, height: maxY)
    }

    // MARK: - Clicking
    override func mouseDown(with event: NSEvent) {
        let displayDetection = DisplayDetection.sharedInstance

        // Grab relative location of the click in view
        let point = convert(event.locationInWindow, from: nil)

        // If in selection mode, toggle the screen & redraw
        if PrefsDisplays.displayMode == .selection {
            for displayPreview in displayPreviews {
                if displayPreview.previewRect.contains(point) {
                    if displayDetection.isScreenActive(id: displayPreview.screen.id) {
                        displayDetection.unselectScreen(id: displayPreview.screen.id)
                    } else {
                        displayDetection.selectScreen(id: displayPreview.screen.id)
                    }
                    debugLog("Clicked on \(displayPreview.screen.id)")
                    self.needsDisplay = true
                }
            }
        }
    }
}