VMDisplayQemuDisplayController.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. //
  2. // Copyright © 2022 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. class VMDisplayQemuWindowController: VMDisplayWindowController {
  17. private(set) var id: Int = 0
  18. private weak var vmUsbManager: CSUSBManager?
  19. private var allUsbDevices: [CSUSBDevice] = []
  20. private var connectedUsbDevices: [CSUSBDevice] = []
  21. @Setting("NoUsbPrompt") private var isNoUsbPrompt: Bool = false
  22. var qemuVM: UTMQemuVirtualMachine! {
  23. vm as? UTMQemuVirtualMachine
  24. }
  25. var vmQemuConfig: UTMQemuConfiguration! {
  26. vm?.config.qemuConfig
  27. }
  28. var defaultTitle: String {
  29. vmQemuConfig.information.name
  30. }
  31. var defaultSubtitle: String {
  32. if qemuVM.isRunningAsSnapshot {
  33. return NSLocalizedString("Disposable Mode", comment: "VMDisplayQemuDisplayController")
  34. } else {
  35. return ""
  36. }
  37. }
  38. private var defaultPauseTooltip: String?
  39. var suspendDisabledReason: String? {
  40. #if arch(x86_64)
  41. if vmQemuConfig.qemu.hasHypervisor && vmQemuConfig.system.architecture == .x86_64 {
  42. return NSLocalizedString("Suspend is not supported for virtualization.", comment: "VMDisplayQemuWindowController")
  43. }
  44. #endif
  45. for display in vmQemuConfig.displays {
  46. if display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl") {
  47. return NSLocalizedString("Suspend is not supported when GPU acceleration is enabled.", comment: "VMDisplayQemuWindowController")
  48. }
  49. }
  50. for drive in vmQemuConfig.drives {
  51. if drive.interface == .nvme {
  52. return NSLocalizedString("Suspend is not supported when an emulated NVMe device is active.", comment: "VMDisplayQemuWindowController")
  53. }
  54. }
  55. return nil
  56. }
  57. convenience init(vm: UTMQemuVirtualMachine, id: Int) {
  58. self.init(vm: vm, onClose: nil)
  59. self.id = id
  60. }
  61. override var shouldSaveOnPause: Bool {
  62. !qemuVM.isRunningAsSnapshot
  63. }
  64. override func enterLive() {
  65. if !isSecondary {
  66. qemuVM.ioServiceDelegate = self
  67. }
  68. startPauseToolbarItem.isEnabled = true
  69. if defaultPauseTooltip == nil {
  70. defaultPauseTooltip = startPauseToolbarItem.toolTip
  71. }
  72. if let suspendDisabledReason = suspendDisabledReason {
  73. startPauseToolbarItem.isEnabled = false
  74. startPauseToolbarItem.toolTip = suspendDisabledReason
  75. }
  76. drivesToolbarItem.isEnabled = vmQemuConfig.drives.count > 0
  77. sharedFolderToolbarItem.isEnabled = vmQemuConfig.sharing.directoryShareMode == .webdav // virtfs cannot dynamically change
  78. usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection
  79. window!.title = defaultTitle
  80. window!.subtitle = defaultSubtitle
  81. super.enterLive()
  82. }
  83. override func enterSuspended(isBusy busy: Bool) {
  84. if vm.state == .vmStopped {
  85. connectedUsbDevices.removeAll()
  86. allUsbDevices.removeAll()
  87. if isSecondary {
  88. close()
  89. }
  90. }
  91. if defaultPauseTooltip != nil {
  92. startPauseToolbarItem.toolTip = defaultPauseTooltip
  93. }
  94. super.enterSuspended(isBusy: busy)
  95. }
  96. }
  97. // MARK: - Removable drives
  98. @objc extension VMDisplayQemuWindowController {
  99. @IBAction override func drivesButtonPressed(_ sender: Any) {
  100. let menu = NSMenu()
  101. menu.autoenablesItems = false
  102. let item = NSMenuItem()
  103. item.title = NSLocalizedString("Querying drives status...", comment: "VMDisplayWindowController")
  104. item.isEnabled = false
  105. menu.addItem(item)
  106. updateDrivesMenu(menu, drives: vmQemuConfig.drives)
  107. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  108. }
  109. @nonobjc func updateDrivesMenu(_ menu: NSMenu, drives: [UTMQemuConfigurationDrive]) {
  110. menu.removeAllItems()
  111. if drives.count == 0 {
  112. let item = NSMenuItem()
  113. item.title = NSLocalizedString("No drives connected.", comment: "VMDisplayWindowController")
  114. item.isEnabled = false
  115. menu.addItem(item)
  116. } else {
  117. let item = NSMenuItem()
  118. item.title = NSLocalizedString("Install Windows Guest Tools…", comment: "VMDisplayWindowController")
  119. item.isEnabled = !qemuVM.isGuestToolsInstallRequested
  120. item.target = self
  121. item.action = #selector(installWindowsGuestTools)
  122. menu.addItem(item)
  123. }
  124. for i in drives.indices {
  125. let drive = drives[i]
  126. if drive.imageType != .disk && drive.imageType != .cd && !drive.isExternal {
  127. continue // skip non-disks
  128. }
  129. let item = NSMenuItem()
  130. item.title = label(for: drive)
  131. if !drive.isExternal {
  132. item.isEnabled = false
  133. } else {
  134. let submenu = NSMenu()
  135. submenu.autoenablesItems = false
  136. let eject = NSMenuItem(title: NSLocalizedString("Eject", comment: "VMDisplayWindowController"),
  137. action: #selector(ejectDrive),
  138. keyEquivalent: "")
  139. eject.target = self
  140. eject.tag = i
  141. eject.isEnabled = qemuVM.externalImageURL(for: drive) != nil
  142. submenu.addItem(eject)
  143. let change = NSMenuItem(title: NSLocalizedString("Change", comment: "VMDisplayWindowController"),
  144. action: #selector(changeDriveImage),
  145. keyEquivalent: "")
  146. change.target = self
  147. change.tag = i
  148. change.isEnabled = true
  149. submenu.addItem(change)
  150. item.submenu = submenu
  151. }
  152. menu.addItem(item)
  153. }
  154. menu.update()
  155. }
  156. func ejectDrive(sender: AnyObject) {
  157. guard let menu = sender as? NSMenuItem else {
  158. logger.error("wrong sender for ejectDrive")
  159. return
  160. }
  161. let drive = vmQemuConfig.drives[menu.tag]
  162. Task.detached(priority: .background) { [self] in
  163. do {
  164. try await qemuVM.eject(drive)
  165. } catch {
  166. Task { @MainActor in
  167. showErrorAlert(error.localizedDescription)
  168. }
  169. }
  170. }
  171. }
  172. func openDriveImage(forDriveIndex index: Int) {
  173. let drive = vmQemuConfig.drives[index]
  174. let openPanel = NSOpenPanel()
  175. openPanel.title = NSLocalizedString("Select Drive Image", comment: "VMDisplayWindowController")
  176. openPanel.allowedContentTypes = [.data]
  177. openPanel.beginSheetModal(for: window!) { response in
  178. guard response == .OK else {
  179. return
  180. }
  181. guard let url = openPanel.url else {
  182. logger.debug("no file selected")
  183. return
  184. }
  185. Task.detached(priority: .background) { [self] in
  186. do {
  187. try await qemuVM.changeMedium(drive, to: url)
  188. } catch {
  189. Task { @MainActor in
  190. showErrorAlert(error.localizedDescription)
  191. }
  192. }
  193. }
  194. }
  195. }
  196. func changeDriveImage(sender: AnyObject) {
  197. guard let menu = sender as? NSMenuItem else {
  198. logger.error("wrong sender for ejectDrive")
  199. return
  200. }
  201. openDriveImage(forDriveIndex: menu.tag)
  202. }
  203. @nonobjc private func label(for drive: UTMQemuConfigurationDrive) -> String {
  204. let imageURL = qemuVM.externalImageURL(for: drive) ?? drive.imageURL
  205. return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMDisplayQemuDisplayController"),
  206. drive.imageType.prettyValue,
  207. drive.interface.prettyValue,
  208. imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMDisplayQemuDisplayController"))
  209. }
  210. @MainActor private func installWindowsGuestTools(sender: AnyObject) {
  211. qemuVM.isGuestToolsInstallRequested = true
  212. }
  213. }
  214. // MARK: - Shared folders
  215. extension VMDisplayQemuWindowController {
  216. @IBAction override func sharedFolderButtonPressed(_ sender: Any) {
  217. let openPanel = NSOpenPanel()
  218. openPanel.title = NSLocalizedString("Select Shared Folder", comment: "VMDisplayWindowController")
  219. openPanel.canChooseDirectories = true
  220. openPanel.canChooseFiles = false
  221. openPanel.beginSheetModal(for: window!) { response in
  222. guard response == .OK else {
  223. return
  224. }
  225. guard let url = openPanel.url else {
  226. logger.debug("no directory selected")
  227. return
  228. }
  229. Task.detached(priority: .background) { [self] in
  230. do {
  231. try await self.qemuVM.changeSharedDirectory(to: url)
  232. } catch {
  233. Task { @MainActor in
  234. self.showErrorAlert(error.localizedDescription)
  235. }
  236. }
  237. }
  238. }
  239. }
  240. }
  241. // MARK: - SPICE base implementation
  242. extension VMDisplayQemuWindowController: UTMSpiceIODelegate {
  243. private func configIdForSerial(_ serial: CSPort) -> Int? {
  244. let prefix = "com.utmapp.terminal."
  245. guard serial.name?.hasPrefix(prefix) ?? false else {
  246. return nil
  247. }
  248. return Int(serial.name!.dropFirst(prefix.count))
  249. }
  250. func spiceDidCreateInput(_ input: CSInput) {
  251. for subwindow in secondaryWindows {
  252. (subwindow as! VMDisplayQemuWindowController).spiceDidCreateInput(input)
  253. }
  254. }
  255. func spiceDidDestroyInput(_ input: CSInput) {
  256. for subwindow in secondaryWindows {
  257. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroyInput(input)
  258. }
  259. }
  260. func spiceDidCreateDisplay(_ display: CSDisplay) {
  261. guard !isSecondary else {
  262. return
  263. }
  264. Task { @MainActor in
  265. findWindow(for: display)
  266. }
  267. }
  268. func spiceDidUpdateDisplay(_ display: CSDisplay) {
  269. for subwindow in secondaryWindows {
  270. (subwindow as! VMDisplayQemuWindowController).spiceDidUpdateDisplay(display)
  271. }
  272. }
  273. func spiceDidDestroyDisplay(_ display: CSDisplay) {
  274. for subwindow in secondaryWindows {
  275. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroyDisplay(display)
  276. }
  277. }
  278. func spiceDidChangeUsbManager(_ usbManager: CSUSBManager?) {
  279. if usbManager != vmUsbManager {
  280. connectedUsbDevices.removeAll()
  281. allUsbDevices.removeAll()
  282. vmUsbManager = usbManager
  283. if let usbManager = usbManager {
  284. usbManager.delegate = self
  285. }
  286. }
  287. for subwindow in secondaryWindows {
  288. (subwindow as! VMDisplayQemuWindowController).spiceDidChangeUsbManager(usbManager)
  289. }
  290. }
  291. func spiceDynamicResolutionSupportDidChange(_ supported: Bool) {
  292. for subwindow in secondaryWindows {
  293. (subwindow as! VMDisplayQemuWindowController).spiceDynamicResolutionSupportDidChange(supported)
  294. }
  295. }
  296. func spiceDidCreateSerial(_ serial: CSPort) {
  297. guard !isSecondary else {
  298. return
  299. }
  300. Task { @MainActor in
  301. findWindow(for: serial)
  302. }
  303. }
  304. func spiceDidDestroySerial(_ serial: CSPort) {
  305. for subwindow in secondaryWindows {
  306. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroySerial(serial)
  307. }
  308. }
  309. }
  310. // MARK: - USB handling
  311. extension VMDisplayQemuWindowController: CSUSBManagerDelegate {
  312. func spiceUsbManager(_ usbManager: CSUSBManager, deviceError error: String, for device: CSUSBDevice) {
  313. logger.debug("USB device error: (\(device)) \(error)")
  314. DispatchQueue.main.async {
  315. self.showErrorAlert(error)
  316. }
  317. }
  318. func spiceUsbManager(_ usbManager: CSUSBManager, deviceAttached device: CSUSBDevice) {
  319. logger.debug("USB device attached: \(device)")
  320. if !isNoUsbPrompt {
  321. DispatchQueue.main.async {
  322. if self.window!.isKeyWindow && self.vm.state == .vmStarted {
  323. self.showConnectPrompt(for: device)
  324. }
  325. }
  326. }
  327. }
  328. func spiceUsbManager(_ usbManager: CSUSBManager, deviceRemoved device: CSUSBDevice) {
  329. logger.debug("USB device removed: \(device)")
  330. if let i = connectedUsbDevices.firstIndex(of: device) {
  331. connectedUsbDevices.remove(at: i)
  332. }
  333. }
  334. func showConnectPrompt(for usbDevice: CSUSBDevice) {
  335. guard let usbManager = vmUsbManager else {
  336. logger.error("cannot get usb manager")
  337. return
  338. }
  339. let alert = NSAlert()
  340. alert.alertStyle = .informational
  341. alert.messageText = NSLocalizedString("USB Device", comment: "VMQemuDisplayMetalWindowController")
  342. alert.informativeText = String.localizedStringWithFormat(NSLocalizedString("Would you like to connect '%@' to this virtual machine?", comment: "VMQemuDisplayMetalWindowController"), usbDevice.name ?? usbDevice.description)
  343. alert.showsSuppressionButton = true
  344. alert.addButton(withTitle: NSLocalizedString("Confirm", comment: "VMQemuDisplayMetalWindowController"))
  345. alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMQemuDisplayMetalWindowController"))
  346. alert.beginSheetModal(for: window!) { response in
  347. if let suppressionButton = alert.suppressionButton,
  348. suppressionButton.state == .on {
  349. self.isNoUsbPrompt = true
  350. }
  351. guard response == .alertFirstButtonReturn else {
  352. return
  353. }
  354. DispatchQueue.global(qos: .utility).async {
  355. usbManager.connectUsbDevice(usbDevice) { (result, message) in
  356. DispatchQueue.main.async {
  357. if let msg = message {
  358. self.showErrorAlert(msg)
  359. }
  360. if result {
  361. self.connectedUsbDevices.append(usbDevice)
  362. }
  363. }
  364. }
  365. }
  366. }
  367. }
  368. }
  369. /// These devices cannot be captured as enforced by macOS. Capturing results in an error. App Store Review requests that we block out the option.
  370. let usbBlockList = [
  371. (0x05ac, 0x8102), // Apple Touch Bar Backlight
  372. (0x05ac, 0x8103), // Apple Headset
  373. (0x05ac, 0x8233), // Apple T2 Controller
  374. (0x05ac, 0x8262), // Apple Ambient Light Sensor
  375. (0x05ac, 0x8263),
  376. (0x05ac, 0x8302), // Apple Touch Bar Display
  377. (0x05ac, 0x8514), // Apple FaceTime HD Camera (Built-in)
  378. (0x05ac, 0x8600), // Apple iBridge
  379. ]
  380. extension VMDisplayQemuWindowController {
  381. @IBAction override func usbButtonPressed(_ sender: Any) {
  382. let menu = NSMenu()
  383. menu.autoenablesItems = false
  384. let item = NSMenuItem()
  385. item.title = NSLocalizedString("Querying USB devices...", comment: "VMQemuDisplayMetalWindowController")
  386. item.isEnabled = false
  387. menu.addItem(item)
  388. DispatchQueue.global(qos: .userInitiated).async {
  389. let devices = self.vmUsbManager?.usbDevices ?? []
  390. DispatchQueue.main.async {
  391. self.updateUsbDevicesMenu(menu, devices: devices)
  392. }
  393. }
  394. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  395. }
  396. func updateUsbDevicesMenu(_ menu: NSMenu, devices: [CSUSBDevice]) {
  397. allUsbDevices = devices
  398. menu.removeAllItems()
  399. if devices.count == 0 {
  400. let item = NSMenuItem()
  401. item.title = NSLocalizedString("No USB devices detected.", comment: "VMQemuDisplayMetalWindowController")
  402. item.isEnabled = false
  403. menu.addItem(item)
  404. }
  405. for (i, device) in devices.enumerated() {
  406. let item = NSMenuItem()
  407. let canRedirect = vmUsbManager?.canRedirectUsbDevice(device, errorMessage: nil) ?? false
  408. let isConnected = vmUsbManager?.isUsbDeviceConnected(device) ?? false
  409. let isConnectedToSelf = connectedUsbDevices.contains(device)
  410. item.title = device.name ?? device.description
  411. let blocked = usbBlockList.contains { (usbVid, usbPid) in usbVid == device.usbVendorId && usbPid == device.usbProductId }
  412. item.isEnabled = !blocked && canRedirect && (isConnectedToSelf || !isConnected)
  413. item.state = isConnectedToSelf ? .on : .off;
  414. item.tag = i
  415. item.target = self
  416. item.action = isConnectedToSelf ? #selector(disconnectUsbDevice) : #selector(connectUsbDevice)
  417. menu.addItem(item)
  418. }
  419. menu.update()
  420. }
  421. @objc func connectUsbDevice(sender: AnyObject) {
  422. guard let menu = sender as? NSMenuItem else {
  423. logger.error("wrong sender for connectUsbDevice")
  424. return
  425. }
  426. guard let usbManager = vmUsbManager else {
  427. logger.error("cannot get usb manager")
  428. return
  429. }
  430. let device = allUsbDevices[menu.tag]
  431. DispatchQueue.global(qos: .utility).async {
  432. usbManager.connectUsbDevice(device) { (result, message) in
  433. DispatchQueue.main.async {
  434. if let msg = message {
  435. self.showErrorAlert(msg)
  436. }
  437. if result {
  438. self.connectedUsbDevices.append(device)
  439. }
  440. }
  441. }
  442. }
  443. }
  444. @objc func disconnectUsbDevice(sender: AnyObject) {
  445. guard let menu = sender as? NSMenuItem else {
  446. logger.error("wrong sender for disconnectUsbDevice")
  447. return
  448. }
  449. guard let usbManager = vmUsbManager else {
  450. logger.error("cannot get usb manager")
  451. return
  452. }
  453. let device = allUsbDevices[menu.tag]
  454. DispatchQueue.global(qos: .utility).async {
  455. usbManager.disconnectUsbDevice(device) { (result, message) in
  456. DispatchQueue.main.async {
  457. if let msg = message {
  458. self.showErrorAlert(msg)
  459. }
  460. if result {
  461. self.connectedUsbDevices.removeAll(where: { $0 == device })
  462. }
  463. }
  464. }
  465. }
  466. }
  467. }
  468. // MARK: - Window management
  469. extension VMDisplayQemuWindowController {
  470. @IBAction override func windowsButtonPressed(_ sender: Any) {
  471. let menu = NSMenu()
  472. menu.autoenablesItems = false
  473. for display in qemuVM.ioService!.displays {
  474. let id = display.monitorID
  475. guard id < vmQemuConfig.displays.count else {
  476. continue
  477. }
  478. let config = vmQemuConfig.displays[id]
  479. let item = NSMenuItem()
  480. let format = NSLocalizedString("Display %lld: %@", comment: "VMDisplayQemuDisplayController")
  481. let title = String.localizedStringWithFormat(format, id + 1, config.hardware.prettyValue)
  482. let isCurrent = self is VMDisplayQemuMetalWindowController && self.id == id
  483. item.title = title
  484. item.isEnabled = !isCurrent
  485. item.state = isCurrent ? .on : .off
  486. item.tag = id
  487. item.target = self
  488. item.action = #selector(showWindowFromDisplay)
  489. menu.addItem(item)
  490. }
  491. for serial in qemuVM.ioService!.serials {
  492. guard let id = configIdForSerial(serial) else {
  493. continue
  494. }
  495. let item = NSMenuItem()
  496. let format = NSLocalizedString("Serial %lld", comment: "VMDisplayQemuDisplayController")
  497. let title = String.localizedStringWithFormat(format, id + 1)
  498. let isCurrent = self is VMDisplayQemuTerminalWindowController && self.id == id
  499. item.title = title
  500. item.isEnabled = !isCurrent
  501. item.state = isCurrent ? .on : .off
  502. item.tag = id
  503. item.target = self
  504. item.action = #selector(showWindowFromSerial)
  505. menu.addItem(item)
  506. }
  507. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  508. }
  509. @objc private func showWindowFromDisplay(sender: AnyObject) {
  510. let item = sender as! NSMenuItem
  511. let id = item.tag
  512. if self is VMDisplayQemuMetalWindowController && self.id == id {
  513. return
  514. }
  515. guard let display = qemuVM.ioService?.displays.first(where: { $0.monitorID == id}) else {
  516. return
  517. }
  518. if let window = findWindow(for: display) {
  519. window.showWindow(self)
  520. }
  521. }
  522. @objc private func showWindowFromSerial(sender: AnyObject) {
  523. let item = sender as! NSMenuItem
  524. let id = item.tag
  525. if self is VMDisplayQemuTerminalWindowController && self.id == id {
  526. return
  527. }
  528. guard let serial = qemuVM.ioService?.serials.first(where: { id == configIdForSerial($0) }) else {
  529. return
  530. }
  531. if let window = findWindow(for: serial) {
  532. window.showWindow(self)
  533. }
  534. }
  535. @MainActor private func findWindow(for display: CSDisplay) -> VMDisplayQemuWindowController? {
  536. let id = display.monitorID
  537. let secondaryWindows: [VMDisplayWindowController]
  538. if self is VMDisplayQemuMetalWindowController && self.id == id {
  539. return self
  540. }
  541. if let window = primaryWindow {
  542. if (window as? VMDisplayQemuMetalWindowController)?.id == id {
  543. return window as? VMDisplayQemuWindowController
  544. }
  545. secondaryWindows = window.secondaryWindows
  546. } else {
  547. secondaryWindows = self.secondaryWindows
  548. }
  549. for window in secondaryWindows {
  550. if let window = window as? VMDisplayQemuMetalWindowController {
  551. if window.id == id {
  552. // found existing window
  553. return window
  554. }
  555. }
  556. }
  557. if let newWindow = newWindow(from: display) {
  558. return newWindow
  559. } else {
  560. return nil
  561. }
  562. }
  563. @MainActor private func newWindow(from display: CSDisplay) -> VMDisplayQemuMetalWindowController? {
  564. let id = display.monitorID
  565. guard id < vmQemuConfig.displays.count else {
  566. return nil
  567. }
  568. guard let primary = (primaryWindow ?? self) as? VMDisplayQemuMetalWindowController else {
  569. return nil
  570. }
  571. let secondary = VMDisplayQemuMetalWindowController(secondaryFromDisplay: display, primary: primary, vm: qemuVM, id: id)
  572. registerSecondaryWindow(secondary)
  573. return secondary
  574. }
  575. @MainActor private func findWindow(for serial: CSPort) -> VMDisplayQemuWindowController? {
  576. let id = configIdForSerial(serial)!
  577. let secondaryWindows: [VMDisplayWindowController]
  578. if self is VMDisplayQemuTerminalWindowController && self.id == id {
  579. return self
  580. }
  581. if let window = primaryWindow {
  582. if (window as? VMDisplayQemuTerminalWindowController)?.id == id {
  583. return window as? VMDisplayQemuWindowController
  584. }
  585. secondaryWindows = window.secondaryWindows
  586. } else {
  587. secondaryWindows = self.secondaryWindows
  588. }
  589. for window in secondaryWindows {
  590. if let window = window as? VMDisplayQemuTerminalWindowController {
  591. if window.id == id {
  592. // found existing window
  593. return window
  594. }
  595. }
  596. }
  597. if let newWindow = newWindow(from: serial) {
  598. return newWindow
  599. } else {
  600. return nil
  601. }
  602. }
  603. @MainActor private func newWindow(from serial: CSPort) -> VMDisplayQemuTerminalWindowController? {
  604. guard let id = configIdForSerial(serial) else {
  605. return nil
  606. }
  607. guard id < vmQemuConfig.serials.count else {
  608. return nil
  609. }
  610. let secondary = VMDisplayQemuTerminalWindowController(secondaryFromSerialPort: serial, vm: qemuVM, id: id)
  611. registerSecondaryWindow(secondary)
  612. return secondary
  613. }
  614. }