diff --git a/Aerial.xcodeproj/project.pbxproj b/Aerial.xcodeproj/project.pbxproj index fe62e0e0..ebcbb5da 100644 --- a/Aerial.xcodeproj/project.pbxproj +++ b/Aerial.xcodeproj/project.pbxproj @@ -49,6 +49,20 @@ 0395835621807D1F008E8F9C /* thumbnail.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835221807D1F008E8F9C /* thumbnail.png */; }; 03A2CB9C216BA9AF0061E8E8 /* VideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */; }; 03A2CB9D216BB1490061E8E8 /* VideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */; }; + 03D1E78722842FB300D10CF7 /* DisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E78622842FB300D10CF7 /* DisplayView.swift */; }; + 03D1E7882284367200D10CF7 /* DisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E78622842FB300D10CF7 /* DisplayView.swift */; }; + 03D1E78A2284471A00D10CF7 /* DisplayDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */; }; + 03D1E78B22844AFD00D10CF7 /* DisplayDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */; }; + 03D1E78C22844AFE00D10CF7 /* DisplayDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */; }; + 03D1E79122848F7F00D10CF7 /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; + 03D1E79222848F7F00D10CF7 /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; + 03D1E79322848F7F00D10CF7 /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; + 03D1E79422848F7F00D10CF7 /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; + 03D1E79522848F7F00D10CF7 /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; + 03D1E79622848F7F00D10CF7 /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; + 03D1E79722848F7F00D10CF7 /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; + 03D1E79822848F7F00D10CF7 /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; + 03D1E79922848F7F00D10CF7 /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; 03D37FD922145487005A146F /* es.json in Resources */ = {isa = PBXBuildFile; fileRef = 03D37FD722145487005A146F /* es.json */; }; 03D37FDA22145487005A146F /* es.json in Resources */ = {isa = PBXBuildFile; fileRef = 03D37FD722145487005A146F /* es.json */; }; 03D37FDB22145487005A146F /* fr.json in Resources */ = {isa = PBXBuildFile; fileRef = 03D37FD822145487005A146F /* fr.json */; }; @@ -128,6 +142,11 @@ 0395835121807D1F008E8F9C /* thumbnail@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "thumbnail@2x.png"; sourceTree = ""; }; 0395835221807D1F008E8F9C /* thumbnail.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = thumbnail.png; sourceTree = ""; }; 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoManager.swift; sourceTree = ""; }; + 03D1E78622842FB300D10CF7 /* DisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DisplayView.swift; path = Aerial/Source/Views/DisplayView.swift; sourceTree = SOURCE_ROOT; }; + 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayDetection.swift; sourceTree = ""; }; + 03D1E78E22848F7F00D10CF7 /* screen2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = screen2.jpg; sourceTree = ""; }; + 03D1E78F22848F7F00D10CF7 /* screen1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = screen1.jpg; sourceTree = ""; }; + 03D1E79022848F7F00D10CF7 /* screen0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = screen0.jpg; sourceTree = ""; }; 03D37FD722145487005A146F /* es.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = es.json; sourceTree = ""; }; 03D37FD822145487005A146F /* fr.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fr.json; sourceTree = ""; }; 03D3DAC3221F286700BDA52F /* pl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pl.json; sourceTree = ""; }; @@ -230,6 +249,16 @@ path = Time; sourceTree = ""; }; + 03D1E78D22848F6D00D10CF7 /* Screenshots */ = { + isa = PBXGroup; + children = ( + 03D1E79022848F7F00D10CF7 /* screen0.jpg */, + 03D1E78F22848F7F00D10CF7 /* screen1.jpg */, + 03D1E78E22848F7F00D10CF7 /* screen2.jpg */, + ); + path = Screenshots; + sourceTree = ""; + }; 03E8730D216501B3002B469B /* Downloads */ = { isa = PBXGroup; children = ( @@ -297,6 +326,7 @@ FAC36F361BE1756D007F2A20 /* Resources */ = { isa = PBXGroup; children = ( + 03D1E78D22848F6D00D10CF7 /* Screenshots */, 033192DF217B77E90073B580 /* Community */, 033D62AA216CADCD00F3AF83 /* icon-day-dark.pdf */, FAC36F371BE1756D007F2A20 /* icon-day.pdf */, @@ -340,6 +370,7 @@ FAC36F401BE1756D007F2A20 /* AerialVideo.swift */, FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */, 03893CB2217749F0008E7125 /* ErrorLog.swift */, + 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */, ); path = Models; sourceTree = ""; @@ -363,6 +394,7 @@ FAC36F431BE1756D007F2A20 /* AerialView.swift */, FAC36F441BE1756D007F2A20 /* CheckCellView.swift */, AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */, + 03D1E78622842FB300D10CF7 /* DisplayView.swift */, ); path = Views; sourceTree = ""; @@ -530,6 +562,7 @@ 036A34B622730A0700A49135 /* zh_CN.json in Resources */, 033192E2217B78240073B580 /* en.json in Resources */, FAC36F541BE1756D007F2A20 /* PreferencesWindow.xib in Resources */, + 03D1E79522848F7F00D10CF7 /* screen1.jpg in Resources */, FAC36F4E1BE1756D007F2A20 /* icon-day.pdf in Resources */, 03D37FDA22145487005A146F /* es.json in Resources */, 0369985E2196129C00E359D3 /* missingvideos.json in Resources */, @@ -539,11 +572,13 @@ 03D3DAC5221F286D00BDA52F /* pl.json in Resources */, FAC36F481BE1756D007F2A20 /* Assets.xcassets in Resources */, 033D62AC216CADCD00F3AF83 /* icon-day-dark.pdf in Resources */, + 03D1E79222848F7F00D10CF7 /* screen2.jpg in Resources */, FAC36F4A1BE1756D007F2A20 /* MainMenu.xib in Resources */, FAC36F501BE1756D007F2A20 /* icon-night.pdf in Resources */, 035A92A9225F8C480095AB85 /* he.json in Resources */, 035A92A7225F8C480095AB85 /* de.json in Resources */, 0395835421807D1F008E8F9C /* thumbnail@2x.png in Resources */, + 03D1E79822848F7F00D10CF7 /* screen0.jpg in Resources */, 03D37FDC22145487005A146F /* fr.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -552,8 +587,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03D1E79322848F7F00D10CF7 /* screen2.jpg in Resources */, 036A34B722730A0700A49135 /* zh_CN.json in Resources */, 033D62B1216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */, + 03D1E79622848F7F00D10CF7 /* screen1.jpg in Resources */, + 03D1E79922848F7F00D10CF7 /* screen0.jpg in Resources */, 033D62AD216CADCD00F3AF83 /* icon-day-dark.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -567,13 +605,16 @@ 03D37FDB22145487005A146F /* fr.json in Resources */, 035A92AC226754760095AB85 /* ar.json in Resources */, 036A34B5227309FB00A49135 /* zh_CN.json in Resources */, + 03D1E79122848F7F00D10CF7 /* screen2.jpg in Resources */, FAC36F4D1BE1756D007F2A20 /* icon-day.pdf in Resources */, 035A92A8225F8C480095AB85 /* he.json in Resources */, 03D3DAC4221F286700BDA52F /* pl.json in Resources */, 033D62AB216CADCD00F3AF83 /* icon-day-dark.pdf in Resources */, + 03D1E79722848F7F00D10CF7 /* screen0.jpg in Resources */, 0395835321807D1F008E8F9C /* thumbnail@2x.png in Resources */, 0395835521807D1F008E8F9C /* thumbnail.png in Resources */, 03D37FD922145487005A146F /* es.json in Resources */, + 03D1E79422848F7F00D10CF7 /* screen1.jpg in Resources */, 033D62AF216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */, 0369985D2196103300E359D3 /* missingvideos.json in Resources */, FAC36F4F1BE1756D007F2A20 /* icon-night.pdf in Resources */, @@ -720,9 +761,11 @@ 03233B692172762C0077D3F9 /* PoiStringProvider.swift in Sources */, 03A2CB9D216BB1490061E8E8 /* VideoManager.swift in Sources */, 03E87314216760B7002B469B /* TimeManagement.swift in Sources */, + 03D1E78B22844AFD00D10CF7 /* DisplayDetection.swift in Sources */, 03E8731021662AEB002B469B /* DownloadManager.swift in Sources */, 03E8731121662AEB002B469B /* AsynchronousOperation.swift in Sources */, 03510C7121834FC7008F74F2 /* IOBridge.m in Sources */, + 03D1E7882284367200D10CF7 /* DisplayView.swift in Sources */, 030D9B7C21551A8D00961E95 /* AerialPlayerItem.swift in Sources */, FAC36F5E1BE1756D007F2A20 /* CheckCellView.swift in Sources */, FAC36F5C1BE1756D007F2A20 /* AerialView.swift in Sources */, @@ -744,6 +787,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03D1E78C22844AFE00D10CF7 /* DisplayDetection.swift in Sources */, 0393857C2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */, FA7199711D94EC5A00FBC99B /* PreferencesTests.swift in Sources */, ); @@ -753,6 +797,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03D1E78A2284471A00D10CF7 /* DisplayDetection.swift in Sources */, FAC36F5D1BE1756D007F2A20 /* CheckCellView.swift in Sources */, FAC36F5B1BE1756D007F2A20 /* AerialView.swift in Sources */, 0393857A2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */, @@ -768,6 +813,7 @@ 03A2CB9C216BA9AF0061E8E8 /* VideoManager.swift in Sources */, FAF450211BE2B45D00C1F98A /* VideoLoader.swift in Sources */, 03E8731321675FE0002B469B /* TimeManagement.swift in Sources */, + 03D1E78722842FB300D10CF7 /* DisplayView.swift in Sources */, FAB22A7E1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */, 03958349217F4416008E8F9C /* Solar.swift in Sources */, 03233B68217272640077D3F9 /* PoiStringProvider.swift in Sources */, diff --git a/Aerial/Source/Controllers/Preferences.swift b/Aerial/Source/Controllers/Preferences.swift index 235526a3..d93c2cfc 100644 --- a/Aerial/Source/Controllers/Preferences.swift +++ b/Aerial/Source/Controllers/Preferences.swift @@ -74,6 +74,17 @@ final class Preferences { case updateWhileSaverMode = "updateWhileSaverMode" case allowBetas = "allowBetas" case betaCheckFrequency = "betaCheckFrequency" + case newDisplayMode = "newDisplayMode" + case newViewingMode = "newViewingMode" + case newDisplayDict = "newDisplayDict" + } + + enum NewDisplayMode: Int { + case allDisplays, mainOnly, secondaryOnly, selection + } + + enum NewViewingMode: Int { + case independant, mirrored, spanned } enum BetaCheckFrequency: Int { @@ -199,6 +210,9 @@ final class Preferences { defaultValues[.updateWhileSaverMode] = true defaultValues[.allowBetas] = false defaultValues[.betaCheckFrequency] = BetaCheckFrequency.daily + defaultValues[.newDisplayMode] = NewDisplayMode.allDisplays + defaultValues[.newViewingMode] = NewViewingMode.independant + defaultValues[.newDisplayDict] = [String: Bool]() // Set today's date as default let dateFormatter = DateFormatter() @@ -226,6 +240,33 @@ final class Preferences { } } + var newDisplayDict: [String: Bool] { + get { + return userDefaults.dictionary(forKey: "newDisplayDict") as! [String: Bool] + } + set { + setValue(forIdentifier: .newDisplayDict, value: newValue) + } + } + + var newDisplayMode: Int? { + get { + return optionalValue(forIdentifier: .newDisplayMode) + } + set { + setValue(forIdentifier: .newDisplayMode, value: newValue) + } + } + + var newViewingMode: Int? { + get { + return optionalValue(forIdentifier: .newViewingMode) + } + set { + setValue(forIdentifier: .newViewingMode, value: newValue) + } + } + var lastVideoCheck: String? { get { return optionalValue(forIdentifier: .lastVideoCheck) diff --git a/Aerial/Source/Controllers/PreferencesWindowController.swift b/Aerial/Source/Controllers/PreferencesWindowController.swift index c407d4ca..63f160d0 100644 --- a/Aerial/Source/Controllers/PreferencesWindowController.swift +++ b/Aerial/Source/Controllers/PreferencesWindowController.swift @@ -72,7 +72,6 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo @IBOutlet var overrideNightOnDarkMode: NSButton! - @IBOutlet var multiMonitorModePopup: NSPopUpButton! @IBOutlet var popupVideoFormat: NSPopUpButton! @IBOutlet var alternatePopupVideoFormat: NSPopUpButton! @IBOutlet var descriptionModePopup: NSPopUpButton! @@ -206,6 +205,12 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo @IBOutlet var addVideoSetConfirmButton: NSButton! @IBOutlet var addVideoSetCancelButton: NSButton! @IBOutlet var addVideoSetErrorLabel: NSTextField! + + // Display tab + @IBOutlet var newDisplayModePopup: NSPopUpButton! + @IBOutlet var newViewingModePopup: NSPopUpButton! + @IBOutlet var displayInstructionLabel: NSTextField! + var player: AVPlayer = AVPlayer() var videos: [AerialVideo]? @@ -226,6 +231,7 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo var locationManager: CLLocationManager? var sparkleUpdater: SUUpdater? + @IBOutlet var displayView: DisplayView! public var appMode: Bool = false private lazy var timeFormatter: DateFormatter = { @@ -566,8 +572,6 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo solarModePopup.selectItem(at: preferences.solarMode!) - multiMonitorModePopup.selectItem(at: preferences.multiMonitorMode!) - popupVideoFormat.selectItem(at: preferences.videoFormat!) alternatePopupVideoFormat.selectItem(at: preferences.alternateVideoFormat!) @@ -586,6 +590,14 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo lastCheckedVideosLabel.stringValue = "Last checked on " + preferences.lastVideoCheck! + // Displays Tab + newDisplayModePopup.selectItem(at: preferences.newDisplayMode!) + newViewingModePopup.selectItem(at: preferences.newViewingMode!) + + if preferences.newDisplayMode == Preferences.NewDisplayMode.selection.rawValue { + displayInstructionLabel.isHidden = false + } + // Format date if sparkleUpdater!.lastUpdateCheckDate != nil { let dateFormatter = DateFormatter() @@ -741,12 +753,6 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo popoverPower.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) } - @IBAction func multiMonitorModePopupChange(_ sender: NSPopUpButton) { - debugLog("UI multiMonitorMode: \(sender.indexOfSelectedItem)") - preferences.multiMonitorMode = sender.indexOfSelectedItem - preferences.synchronize() - } - @IBAction func fadeInOutModePopupChange(_ sender: NSPopUpButton) { debugLog("UI fadeInOutMode: \(sender.indexOfSelectedItem)") preferences.fadeMode = sender.indexOfSelectedItem @@ -839,6 +845,23 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo // Hackintosh/new SKUs may fail this test return .unsure } + // MARK: - Displays panel + @IBAction func newDisplayModeClick(_ sender: NSPopUpButton) { + debugLog("UI newDisplayModeClick: \(sender.indexOfSelectedItem)") + preferences.newDisplayMode = sender.indexOfSelectedItem + if preferences.newDisplayMode == Preferences.NewDisplayMode.selection.rawValue { + displayInstructionLabel.isHidden = false + } else { + displayInstructionLabel.isHidden = true + } + displayView.needsDisplay = true + } + + @IBAction func newViewingModeClick(_ sender: NSPopUpButton) { + debugLog("UI newViewingModeClick: \(sender.indexOfSelectedItem)") + preferences.newViewingMode = sender.indexOfSelectedItem + displayView.needsDisplay = true + } // MARK: - Text panel diff --git a/Aerial/Source/Models/DisplayDetection.swift b/Aerial/Source/Models/DisplayDetection.swift new file mode 100644 index 00000000..8cb06ac7 --- /dev/null +++ b/Aerial/Source/Models/DisplayDetection.swift @@ -0,0 +1,239 @@ +// +// DisplayDetection.swift +// Aerial +// +// Created by Guillaume Louel on 09/05/2019. +// Copyright © 2019 John Coates. All rights reserved. +// + +import Foundation +import Cocoa + +class Screen: NSObject { + var id: CGDirectDisplayID + var width: Int + var height: Int + var bottomLeftFrame: CGRect + var topRightCorner: CGPoint + var zeroedOrigin: CGPoint + var isMain: Bool + var backingScaleFactor: CGFloat + + init(id: CGDirectDisplayID, width: Int, height: Int, bottomLeftFrame: CGRect, isMain: Bool) { + self.id = id + self.width = width + self.height = height + self.bottomLeftFrame = bottomLeftFrame + // We precalculate the right corner too, as we will need this ! + self.topRightCorner = CGPoint(x: bottomLeftFrame.origin.x + CGFloat(width), + y: bottomLeftFrame.origin.y + CGFloat(height)) + self.zeroedOrigin = CGPoint(x: 0, y: 0) + self.isMain = isMain + self.backingScaleFactor = 1 + } + + override var description: String { + //swiftlint:disable:next line_length + return "[id=\(self.id), width=\(self.width), height=\(self.height), bottomLeftFrame=\(self.bottomLeftFrame), topRightCorner=\(self.topRightCorner), isMain=\(self.isMain), backingScaleFactor=\(self.backingScaleFactor)]" + } +} + +final class DisplayDetection: NSObject { + static let sharedInstance = DisplayDetection() + + var screens = [Screen]() + + // MARK: - Lifecycle + override init() { + super.init() + debugLog("Display Detection initialized") + _ = detectDisplays() + } + + // MARK: - Detection + func detectDisplays() { + // Display detection is done in two passes : + // - Through CGDisplay, we grab all online screens (connected, but + // may or may not be powered on !) and get most information needed + // - Through NSScreen to get the backingScaleFactor (retinaness of a screen) + + debugLog("***Display Detection***") + // First pass + let maxDisplays: UInt32 = 32 + var onlineDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) + var displayCount: UInt32 = 0 + + _ = CGGetOnlineDisplayList(maxDisplays, &onlineDisplays, &displayCount) + debugLog("\(displayCount) display(s) detected") + + for currentDisplay in onlineDisplays[0.. Screen? { + for screen in screens where frame == screen.bottomLeftFrame { + return screen + } + + return nil + } + + func findScreenWith(id: CGDirectDisplayID) -> Screen? { + for screen in screens where screen.id == id { + return screen + } + + return nil + } + + // Calculate the size of the global screen (the composite of all the displays attached) + func getGlobalScreenRect() -> CGRect { + var minX: CGFloat = 0.0, minY: CGFloat = 0.0, maxX: CGFloat = 0.0, maxY: CGFloat = 0.0 + for screen in screens { + if screen.bottomLeftFrame.origin.x < minX { + minX = screen.bottomLeftFrame.origin.x + } + if screen.bottomLeftFrame.origin.y < minY { + minY = screen.bottomLeftFrame.origin.y + } + if screen.topRightCorner.x > maxX { + maxX = screen.topRightCorner.x + } + if screen.topRightCorner.y > maxY { + maxY = screen.topRightCorner.y + } + } + + return CGRect(x: minX, y: minY, width: maxX-minX, height: maxY-minY) + } + + func getZeroedActiveSpannedRect() -> CGRect { + var minX: CGFloat = 0.0, minY: CGFloat = 0.0, maxX: CGFloat = 0.0, maxY: CGFloat = 0.0 + for screen in screens where isScreenActive(id: screen.id) { + if screen.bottomLeftFrame.origin.x < minX { + minX = screen.bottomLeftFrame.origin.x + } + if screen.bottomLeftFrame.origin.y < minY { + minY = screen.bottomLeftFrame.origin.y + } + if screen.topRightCorner.x > maxX { + maxX = screen.topRightCorner.x + } + if screen.topRightCorner.y > maxY { + maxY = screen.topRightCorner.y + } + } + + let width = maxX - minX + let height = maxY - minY + // Zero the origin to the global rect + let orect = getGlobalScreenRect() + minX -= orect.origin.x + minY -= orect.origin.y + return CGRect(x: minX, y: minY, width: width, height: height) + } + + // NSScreen coordinates are with a bottom left origin, whereas CGDisplay + // coordinates are top left origin, this function converts the origin.y value + func convertTopLeftToBottomLeft(rect: CGRect) -> CGRect { + let screenFrame = (NSScreen.main?.frame)! + let newY = 0 - (rect.origin.y - screenFrame.size.height + rect.height) + return CGRect(x: rect.origin.x, y: newY, width: rect.width, height: rect.height) + } + + // MARK: - Public utility fuctions + func isScreenActive(id: CGDirectDisplayID) -> Bool { + let preferences = Preferences.sharedInstance + let screen = findScreenWith(id: id) + + switch preferences.newDisplayMode { + case Preferences.NewDisplayMode.allDisplays.rawValue: + // This one is easy + return true + case Preferences.NewDisplayMode.mainOnly.rawValue: + if let scr = screen { + if scr.isMain { + return true + } + } + return false + case Preferences.NewDisplayMode.secondaryOnly.rawValue: + if let scr = screen { + if scr.isMain { + return false + } + } + return true + case Preferences.NewDisplayMode.selection.rawValue: + if isScreenSelected(id: id) { + return true + } + return false + default: + return true // Will never get called + } + } + + func isScreenSelected(id: CGDirectDisplayID) -> Bool { + let preferences = Preferences.sharedInstance + + // If we have it in the dictionnary, then return that + if preferences.newDisplayDict.keys.contains(String(id)) { + return preferences.newDisplayDict[String(id)]! + } + return false // Unknown screens will not be considered selected + } + + func selectScreen(id: CGDirectDisplayID) { + let preferences = Preferences.sharedInstance + preferences.newDisplayDict[String(id)] = true + } + + func unselectScreen(id: CGDirectDisplayID) { + let preferences = Preferences.sharedInstance + preferences.newDisplayDict[String(id)] = false + } + +} diff --git a/Aerial/Source/Models/ErrorLog.swift b/Aerial/Source/Models/ErrorLog.swift index c2aa56e7..1730bb31 100644 --- a/Aerial/Source/Models/ErrorLog.swift +++ b/Aerial/Source/Models/ErrorLog.swift @@ -77,10 +77,8 @@ func Log(level: ErrorLevel, message: String) { if preferences.logToDisk { DispatchQueue.main.async { let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .medium + dateFormatter.dateFormat = "yyyy-MM-dd' 'HH:mm:ss" let string = dateFormatter.string(from: Date()) + " : " + message + "\n" - //let string = message + "\n" // tmpOverride //if var cacheFileUrl = try? FileManager.default.url(for: .desktopDirectory, in: .userDomainMask, appropriateFor: nil, create: false) { diff --git a/Aerial/Source/Views/AerialView.swift b/Aerial/Source/Views/AerialView.swift index bccd7772..3d682a97 100644 --- a/Aerial/Source/Views/AerialView.swift +++ b/Aerial/Source/Views/AerialView.swift @@ -71,19 +71,19 @@ final class AerialView: ScreenSaverView { } } + // Mirrored viewing mode and Spanned viewing mode share the same player for sync & ressource saving static var sharingPlayers: Bool { let preferences = Preferences.sharedInstance - return (preferences.multiMonitorMode == Preferences.MultiMonitorMode.mirrored.rawValue) + return (preferences.newViewingMode == Preferences.NewViewingMode.mirrored.rawValue) || + (preferences.newViewingMode == Preferences.NewViewingMode.spanned.rawValue) } static var sharedViews: [AerialView] = [] - // because of lifecycle in Preview, we may pile up old/no longer + // Because of lifecycle in Preview, we may pile up old/no longer // shared instanciated views that we need to track to not reuse static var instanciatedViews: [AerialView] = [] - //var instanciatedIndex: Int // MARK: - Shared Player - static var singlePlayerAlreadySetup: Bool = false static var sharedPlayerIndex: Int? static var didSkipMain: Bool = false @@ -110,7 +110,7 @@ final class AerialView: ScreenSaverView { // This is the one used by System Preferences override init?(frame: NSRect, isPreview: Bool) { super.init(frame: frame, isPreview: isPreview) - debugLog("avInit1") + debugLog("avInit1 \(frame)") self.animationTimeInterval = 1.0 / 30.0 setup() } @@ -137,13 +137,11 @@ final class AerialView: ScreenSaverView { } // Remove from player index - let indexMaybe = AerialView.players.firstIndex(of: player) guard let index = indexMaybe else { return } - AerialView.players.remove(at: index) } @@ -184,9 +182,6 @@ final class AerialView: ScreenSaverView { let preferences = Preferences.sharedInstance let timeManagement = TimeManagement.sharedInstance - debugLog("isOnBattery : \(timeManagement.isOnBattery())") - debugLog("isBatteryLow : \(timeManagement.isBatteryLow())") - // Initialize Sparkle updater if !isPreview && preferences.updateWhileSaverMode { let suup = SUUpdater.init(for: Bundle(for: AerialView.self)) @@ -255,34 +250,29 @@ final class AerialView: ScreenSaverView { } } + let displayDetection = DisplayDetection.sharedInstance + // We look for the screen in our detected list. In case of preview or unknown screen + // result will be nil + let thisScreen = displayDetection.findScreenWith(frame: self.frame) var localPlayer: AVPlayer? - - let notPreview = !isPreview debugLog("\(self.description) isPreview : \(isPreview)") + debugLog("Using : \(String(describing: thisScreen))") - if notPreview { - debugLog("\(self.description) singlePlayerAlreadySetup \(AerialView.singlePlayerAlreadySetup)") - if AerialView.singlePlayerAlreadySetup && preferences.multiMonitorMode == Preferences.MultiMonitorMode.mainOnly.rawValue { - isDisabled = true - return - } - - if preferences.multiMonitorMode == Preferences.MultiMonitorMode.secondaryOnly.rawValue { - if !AerialView.didSkipMain { - AerialView.didSkipMain = true + if !isPreview { + if let screen = thisScreen { + // Is the screen active according to user settings or not ? + if !displayDetection.isScreenActive(id: screen.id) { + // Then we disable and exit + debugLog("This display is not active, disabling") isDisabled = true return } + } else { + // If we don't know this screen, we disable + debugLog("This is an unknown display, disabling") + isDisabled = true + return } - - // check if we should share preview's player - //let noPlayers = (AerialView.players.count == 0) - let previewPlayerExists = (AerialView.previewPlayer != nil) - debugLog("\(self.description) nbPlayers \(AerialView.players.count) previewPlayerExists \(previewPlayerExists)") - /*if noPlayers && previewPlayerExists { - - localPlayer = AerialView.previewPlayer - }*/ } else { AerialView.previewView = self } @@ -298,12 +288,7 @@ final class AerialView: ScreenSaverView { debugLog("\(self.description) no local player") if AerialView.sharingPlayers { - /*if AerialView.previewPlayer != nil { - localPlayer = AerialView.previewPlayer - } else {*/ - localPlayer = AerialView.sharedPlayer - //} } else { localPlayer = AVPlayer() } @@ -325,6 +310,7 @@ final class AerialView: ScreenSaverView { setupPlayerLayer(withPlayer: player) + // In mirror mode we use the main instance player if AerialView.sharingPlayers && AerialView.singlePlayerAlreadySetup { self.playerLayer.player = AerialView.instanciatedViews[AerialView.sharedPlayerIndex!].player self.playerLayer.opacity = 0 @@ -355,6 +341,8 @@ final class AerialView: ScreenSaverView { func setupPlayerLayer(withPlayer player: AVPlayer) { debugLog("\(self.description) setupPlayerLayer") + let displayDetection = DisplayDetection.sharedInstance + let preferences = Preferences.sharedInstance self.layer = CALayer() guard let layer = self.layer else { @@ -365,15 +353,32 @@ final class AerialView: ScreenSaverView { layer.backgroundColor = NSColor.black.cgColor layer.needsDisplayOnBoundsChange = true layer.frame = self.bounds - - debugLog("\(self.description) setting up player layer with frame: \(self.bounds) / \(self.frame)") + debugLog("\(self.description) setting up player layer with bounds/frame: \(layer.bounds) / \(layer.frame)") playerLayer = AVPlayerLayer(player: player) if #available(OSX 10.10, *) { playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill } playerLayer.autoresizingMask = [CAAutoresizingMask.layerWidthSizable, CAAutoresizingMask.layerHeightSizable] - playerLayer.frame = layer.bounds + + // In case of span mode we need to compute the size of our layer + if preferences.newViewingMode == Preferences.NewViewingMode.spanned.rawValue && !isPreview { + let zRect = displayDetection.getZeroedActiveSpannedRect() + let screen = displayDetection.findScreenWith(frame: self.frame) + if let scr = screen { + let tRect = CGRect(x: zRect.origin.x - scr.zeroedOrigin.x, + y: zRect.origin.y - scr.zeroedOrigin.y, + width: zRect.width, + height: zRect.height) + playerLayer.frame = tRect + } else { + errorLog("This is an unknown screen in span mode, this is not good") + playerLayer.frame = layer.bounds + } + } else { + playerLayer.frame = layer.bounds + } + layer.addSublayer(playerLayer) textLayer = CATextLayer() diff --git a/Aerial/Source/Views/DisplayView.swift b/Aerial/Source/Views/DisplayView.swift new file mode 100644 index 00000000..bbe7a48a --- /dev/null +++ b/Aerial/Source/Views/DisplayView.swift @@ -0,0 +1,237 @@ +// +// 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 + } +} + +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) + let preferences = Preferences.sharedInstance + + // 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 + + let timeManagement = TimeManagement.sharedInstance + if timeManagement.isDarkModeEnabled() { + 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 preferences.newViewingMode == Preferences.NewViewingMode.spanned.rawValue { + 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: PreferencesWindowController.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 + // 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 preferences.newViewingMode == Preferences.NewViewingMode.independant.rawValue || + preferences.newViewingMode == Preferences.NewViewingMode.mirrored.rawValue { + if displayDetection.isScreenActive(id: screen.id) { + let bundle = Bundle(for: PreferencesWindowController.self) + if let imagePath = bundle.path(forResource: "screen"+String(idx), ofType: "jpg") { + let image = NSImage(contentsOfFile: imagePath) + //image!.draw(in: sInRect) + 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 preferences.newViewingMode == Preferences.NewViewingMode.independant.rawValue { + 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: PreferencesWindowController.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 + let preferences = Preferences.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 preferences.newDisplayMode == Preferences.NewDisplayMode.selection.rawValue { + 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 + } + } + } + } +} diff --git a/Resources/Info.plist b/Resources/Info.plist index 9cf15a28..3c5dbb35 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.10beta3 + 1.4.98beta1 CFBundleSignature ???? CFBundleVersion - 1.4.10beta3 + 1.4.98beta1 LSApplicationCategoryType LSMinimumSystemVersion diff --git a/Resources/PreferencesWindow.xib b/Resources/PreferencesWindow.xib index 00c7b8b2..72c339ff 100644 --- a/Resources/PreferencesWindow.xib +++ b/Resources/PreferencesWindow.xib @@ -44,6 +44,8 @@ + + @@ -85,10 +87,11 @@ - + + @@ -320,7 +323,7 @@ - + @@ -338,7 +341,7 @@ - + @@ -415,38 +418,8 @@ is disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -466,7 +439,7 @@ is disabled - + @@ -487,7 +460,7 @@ is disabled - + @@ -519,7 +492,7 @@ is disabled