VMDisplayAppleWindowController.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. //
  2. // Copyright © 2021 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. class VMDisplayAppleWindowController: VMDisplayWindowController {
  18. var mainView: NSView?
  19. var isInstalling: Bool = false
  20. var appleVM: UTMAppleVirtualMachine! {
  21. vm as? UTMAppleVirtualMachine
  22. }
  23. var appleConfig: UTMAppleConfiguration! {
  24. vm?.config.appleConfig
  25. }
  26. var defaultTitle: String {
  27. appleConfig.information.name
  28. }
  29. var defaultSubtitle: String {
  30. ""
  31. }
  32. private var isSharePathAlertShownOnce = false
  33. // MARK: - User preferences
  34. @Setting("SharePathAlertShown") private var isSharePathAlertShownPersistent: Bool = false
  35. override func windowDidLoad() {
  36. mainView!.translatesAutoresizingMaskIntoConstraints = false
  37. displayView.addSubview(mainView!)
  38. NSLayoutConstraint.activate(mainView!.constraintsForAnchoringTo(boundsOf: displayView))
  39. appleVM.screenshotDelegate = self
  40. window!.recalculateKeyViewLoop()
  41. if #available(macOS 12, *) {
  42. shouldAutoStartVM = appleConfig.system.boot.macRecoveryIpswURL == nil
  43. }
  44. super.windowDidLoad()
  45. if #available(macOS 12, *), let ipswUrl = appleConfig.system.boot.macRecoveryIpswURL {
  46. showConfirmAlert(NSLocalizedString("Would you like to install macOS? If an existing operating system is already installed on the primary drive of this VM, then it will be erased.", comment: "VMDisplayAppleWindowController")) {
  47. self.isInstalling = true
  48. self.appleVM.requestInstallVM(with: ipswUrl)
  49. }
  50. }
  51. if !isSecondary {
  52. // create remaining serial windows
  53. for i in appleConfig.serials.indices {
  54. if i == 0 && self is VMDisplayAppleTerminalWindowController {
  55. continue
  56. }
  57. if appleConfig.serials[i].mode != .builtin || appleConfig.serials[i].terminal == nil {
  58. continue
  59. }
  60. let vc = VMDisplayAppleTerminalWindowController(secondaryForIndex: i, vm: appleVM)
  61. showSecondaryWindow(vc)
  62. }
  63. }
  64. }
  65. override func enterLive() {
  66. window!.title = defaultTitle
  67. window!.subtitle = defaultSubtitle
  68. updateWindowFrame()
  69. super.enterLive()
  70. captureMouseToolbarItem.isEnabled = false
  71. drivesToolbarItem.isEnabled = false
  72. usbToolbarItem.isEnabled = false
  73. startPauseToolbarItem.isEnabled = true
  74. if #available(macOS 12, *) {
  75. isPowerForce = false
  76. sharedFolderToolbarItem.isEnabled = appleConfig.system.boot.operatingSystem == .linux
  77. } else {
  78. // stop() not available on macOS 11 for some reason
  79. restartToolbarItem.isEnabled = false
  80. sharedFolderToolbarItem.isEnabled = false
  81. isPowerForce = true
  82. }
  83. }
  84. override func enterSuspended(isBusy busy: Bool) {
  85. isPowerForce = true
  86. super.enterSuspended(isBusy: busy)
  87. }
  88. override func virtualMachine(_ vm: UTMVirtualMachine, didTransitionTo state: UTMVMState) {
  89. super.virtualMachine(vm, didTransitionTo: state)
  90. if #available(macOS 12, *), state == .vmStopped && isInstalling {
  91. didFinishInstallation()
  92. }
  93. }
  94. func updateWindowFrame() {
  95. // implement in subclass
  96. }
  97. override func stopButtonPressed(_ sender: Any) {
  98. if isPowerForce {
  99. super.stopButtonPressed(sender)
  100. } else {
  101. appleVM.requestVmStop(force: false)
  102. isPowerForce = true
  103. }
  104. }
  105. override func resizeConsoleButtonPressed(_ sender: Any) {
  106. // implement in subclass
  107. }
  108. @IBAction override func sharedFolderButtonPressed(_ sender: Any) {
  109. guard #available(macOS 12, *) else {
  110. return
  111. }
  112. if !isSharePathAlertShownOnce && !isSharePathAlertShownPersistent {
  113. let alert = NSAlert()
  114. alert.messageText = NSLocalizedString("Directory sharing", comment: "VMDisplayAppleWindowController")
  115. alert.informativeText = NSLocalizedString("To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path.", comment: "VMDisplayAppleWindowController")
  116. alert.showsSuppressionButton = true
  117. alert.beginSheetModal(for: window!) { _ in
  118. if alert.suppressionButton?.state ?? .off == .on {
  119. self.isSharePathAlertShownPersistent = true
  120. }
  121. self.isSharePathAlertShownOnce = true
  122. }
  123. } else {
  124. openShareMenu(sender)
  125. }
  126. }
  127. }
  128. @available(macOS 12, *)
  129. extension VMDisplayAppleWindowController {
  130. func openShareMenu(_ sender: Any) {
  131. let menu = NSMenu()
  132. for i in appleConfig.sharedDirectories.indices {
  133. let item = NSMenuItem()
  134. let sharedDirectory = appleConfig.sharedDirectories[i]
  135. guard let name = sharedDirectory.directoryURL?.lastPathComponent else {
  136. continue
  137. }
  138. item.title = name
  139. let submenu = NSMenu()
  140. let ro = NSMenuItem(title: NSLocalizedString("Read Only", comment: "VMDisplayAppleController"),
  141. action: #selector(flipReadOnlyShare),
  142. keyEquivalent: "")
  143. ro.target = self
  144. ro.tag = i
  145. ro.state = sharedDirectory.isReadOnly ? .on : .off
  146. submenu.addItem(ro)
  147. let change = NSMenuItem(title: NSLocalizedString("Change…", comment: "VMDisplayAppleController"),
  148. action: #selector(changeShare),
  149. keyEquivalent: "")
  150. change.target = self
  151. change.tag = i
  152. submenu.addItem(change)
  153. let remove = NSMenuItem(title: NSLocalizedString("Remove…", comment: "VMDisplayAppleController"),
  154. action: #selector(removeShare),
  155. keyEquivalent: "")
  156. remove.target = self
  157. remove.tag = i
  158. submenu.addItem(remove)
  159. item.submenu = submenu
  160. menu.addItem(item)
  161. }
  162. let add = NSMenuItem(title: NSLocalizedString("Add…", comment: "VMDisplayAppleController"),
  163. action: #selector(addShare),
  164. keyEquivalent: "")
  165. add.target = self
  166. menu.addItem(add)
  167. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  168. }
  169. @objc func addShare(sender: AnyObject) {
  170. pickShare { url in
  171. let sharedDirectory = UTMAppleConfigurationSharedDirectory(directoryURL: url)
  172. self.appleConfig.sharedDirectories.append(sharedDirectory)
  173. }
  174. }
  175. @objc func changeShare(sender: AnyObject) {
  176. guard let menu = sender as? NSMenuItem else {
  177. logger.error("wrong sender for changeShare")
  178. return
  179. }
  180. let i = menu.tag
  181. let isReadOnly = appleConfig.sharedDirectories[i].isReadOnly
  182. pickShare { url in
  183. let sharedDirectory = UTMAppleConfigurationSharedDirectory(directoryURL: url, isReadOnly: isReadOnly)
  184. self.appleConfig.sharedDirectories[i] = sharedDirectory
  185. }
  186. }
  187. @objc func flipReadOnlyShare(sender: AnyObject) {
  188. guard let menu = sender as? NSMenuItem else {
  189. logger.error("wrong sender for changeShare")
  190. return
  191. }
  192. let i = menu.tag
  193. let isReadOnly = appleConfig.sharedDirectories[i].isReadOnly
  194. appleConfig.sharedDirectories[i].isReadOnly = !isReadOnly
  195. }
  196. @objc func removeShare(sender: AnyObject) {
  197. guard let menu = sender as? NSMenuItem else {
  198. logger.error("wrong sender for removeShare")
  199. return
  200. }
  201. let i = menu.tag
  202. appleConfig.sharedDirectories.remove(at: i)
  203. }
  204. func pickShare(_ onComplete: @escaping (URL) -> Void) {
  205. let openPanel = NSOpenPanel()
  206. openPanel.title = NSLocalizedString("Select Shared Folder", comment: "VMDisplayAppleWindowController")
  207. openPanel.canChooseDirectories = true
  208. openPanel.canChooseFiles = false
  209. openPanel.beginSheetModal(for: window!) { response in
  210. guard response == .OK else {
  211. return
  212. }
  213. guard let url = openPanel.url else {
  214. logger.debug("no directory selected")
  215. return
  216. }
  217. onComplete(url)
  218. }
  219. }
  220. }
  221. extension VMDisplayAppleWindowController {
  222. func didFinishInstallation() {
  223. DispatchQueue.main.async {
  224. self.isInstalling = false
  225. // delete IPSW setting
  226. self.enterSuspended(isBusy: true)
  227. self.appleConfig.system.boot.macRecoveryIpswURL = nil
  228. // start VM
  229. self.vm.requestVmStart()
  230. }
  231. }
  232. func virtualMachine(_ vm: UTMVirtualMachine, didUpdateInstallationProgress progress: Double) {
  233. DispatchQueue.main.async {
  234. if progress >= 1 {
  235. self.window!.subtitle = ""
  236. } else {
  237. let installationFormat = NSLocalizedString("Installation: %@", comment: "VMDisplayAppleWindowController")
  238. let percentString = NumberFormatter.localizedString(from: progress as NSNumber, number: .percent)
  239. self.window!.subtitle = String.localizedStringWithFormat(installationFormat, percentString)
  240. }
  241. }
  242. }
  243. }
  244. extension VMDisplayAppleWindowController: UTMScreenshotProvider {
  245. var screenshot: CSScreenshot? {
  246. if let image = mainView?.image() {
  247. return CSScreenshot(image: image)
  248. } else {
  249. return nil
  250. }
  251. }
  252. }
  253. // https://www.avanderlee.com/swift/auto-layout-programmatically/
  254. fileprivate extension NSView {
  255. /// Returns a collection of constraints to anchor the bounds of the current view to the given view.
  256. ///
  257. /// - Parameter view: The view to anchor to.
  258. /// - Returns: The layout constraints needed for this constraint.
  259. func constraintsForAnchoringTo(boundsOf view: NSView) -> [NSLayoutConstraint] {
  260. return [
  261. topAnchor.constraint(equalTo: view.topAnchor),
  262. leadingAnchor.constraint(equalTo: view.leadingAnchor),
  263. view.bottomAnchor.constraint(equalTo: bottomAnchor),
  264. view.trailingAnchor.constraint(equalTo: trailingAnchor)
  265. ]
  266. }
  267. }
  268. // https://stackoverflow.com/a/41387514/13914748
  269. fileprivate extension NSView {
  270. /// Get `NSImage` representation of the view.
  271. ///
  272. /// - Returns: `NSImage` of view
  273. func image() -> NSImage {
  274. let imageRepresentation = bitmapImageRepForCachingDisplay(in: bounds)!
  275. cacheDisplay(in: bounds, to: imageRepresentation)
  276. return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)
  277. }
  278. }