691 lines
26 KiB
Swift
691 lines
26 KiB
Swift
//
|
|
// Copyright © 2022 osy. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
class VMDisplayQemuWindowController: VMDisplayWindowController {
|
|
private(set) var id: Int = 0
|
|
private weak var vmUsbManager: CSUSBManager?
|
|
private var allUsbDevices: [CSUSBDevice] = []
|
|
private var connectedUsbDevices: [CSUSBDevice] = []
|
|
@Setting("NoUsbPrompt") private var isNoUsbPrompt: Bool = false
|
|
|
|
var qemuVM: UTMQemuVirtualMachine! {
|
|
vm as? UTMQemuVirtualMachine
|
|
}
|
|
|
|
var vmQemuConfig: UTMQemuConfiguration! {
|
|
vm?.config.qemuConfig
|
|
}
|
|
|
|
var defaultTitle: String {
|
|
vmQemuConfig.information.name
|
|
}
|
|
|
|
var defaultSubtitle: String {
|
|
if qemuVM.isRunningAsSnapshot {
|
|
return NSLocalizedString("Disposable Mode", comment: "VMDisplayQemuDisplayController")
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
convenience init(vm: UTMQemuVirtualMachine, id: Int) {
|
|
self.init(vm: vm, onClose: nil)
|
|
self.id = id
|
|
}
|
|
|
|
override var shouldSaveOnPause: Bool {
|
|
!qemuVM.isRunningAsSnapshot
|
|
}
|
|
|
|
override func enterLive() {
|
|
if !isSecondary {
|
|
qemuVM.ioDelegate = self
|
|
}
|
|
startPauseToolbarItem.isEnabled = true
|
|
#if arch(x86_64)
|
|
if vmQemuConfig.qemu.hasHypervisor {
|
|
// currently x86_64 HVF doesn't support suspending
|
|
startPauseToolbarItem.isEnabled = false
|
|
}
|
|
#endif
|
|
drivesToolbarItem.isEnabled = vmQemuConfig.drives.count > 0
|
|
sharedFolderToolbarItem.isEnabled = vmQemuConfig.sharing.directoryShareMode == .webdav // virtfs cannot dynamically change
|
|
usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection
|
|
window!.title = defaultTitle
|
|
window!.subtitle = defaultSubtitle
|
|
super.enterLive()
|
|
}
|
|
|
|
override func enterSuspended(isBusy busy: Bool) {
|
|
if vm.state == .vmStopped {
|
|
connectedUsbDevices.removeAll()
|
|
allUsbDevices.removeAll()
|
|
if isSecondary {
|
|
close()
|
|
}
|
|
}
|
|
super.enterSuspended(isBusy: busy)
|
|
}
|
|
|
|
override func windowDidLoad() {
|
|
setupStopButtonMenu()
|
|
super.windowDidLoad()
|
|
}
|
|
}
|
|
|
|
// MARK: - Stop menu
|
|
extension VMDisplayQemuWindowController {
|
|
private func setupStopButtonMenu() {
|
|
let menu = NSMenu()
|
|
menu.autoenablesItems = false
|
|
let item1 = NSMenuItem()
|
|
item1.title = NSLocalizedString("Request power down", comment: "VMDisplayQemuDisplayController")
|
|
item1.toolTip = NSLocalizedString("Sends power down request to the guest. This simulates pressing the power button on a PC.", comment: "VMDisplayQemuDisplayController")
|
|
item1.target = self
|
|
item1.action = #selector(requestPowerDown)
|
|
menu.addItem(item1)
|
|
let item2 = NSMenuItem()
|
|
item2.title = NSLocalizedString("Force shut down", comment: "VMDisplayQemuDisplayController")
|
|
item2.toolTip = NSLocalizedString("Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC.", comment: "VMDisplayQemuDisplayController")
|
|
item2.target = self
|
|
item2.action = #selector(qmpShutDown)
|
|
menu.addItem(item2)
|
|
let item3 = NSMenuItem()
|
|
item3.title = NSLocalizedString("Force kill", comment: "VMDisplayQemuDisplayController")
|
|
item3.toolTip = NSLocalizedString("Force kill the VM process with high risk of data corruption.", comment: "VMDisplayQemuDisplayController")
|
|
item3.target = self
|
|
item3.action = #selector(qmpForceKill)
|
|
menu.addItem(item3)
|
|
stopToolbarItem.menu = menu
|
|
}
|
|
|
|
@MainActor @objc private func requestPowerDown(sender: AnyObject) {
|
|
qemuVM.requestGuestPowerDown()
|
|
}
|
|
|
|
@MainActor @objc private func qmpShutDown(sender: AnyObject) {
|
|
let prev = isPowerForce
|
|
isPowerForce = false
|
|
stopButtonPressed(sender)
|
|
isPowerForce = prev
|
|
}
|
|
|
|
@MainActor @objc private func qmpForceKill(sender: AnyObject) {
|
|
let prev = isPowerForce
|
|
isPowerForce = true
|
|
stopButtonPressed(sender)
|
|
isPowerForce = prev
|
|
}
|
|
}
|
|
|
|
// MARK: - Removable drives
|
|
|
|
@objc extension VMDisplayQemuWindowController {
|
|
@IBAction override func drivesButtonPressed(_ sender: Any) {
|
|
let menu = NSMenu()
|
|
menu.autoenablesItems = false
|
|
let item = NSMenuItem()
|
|
item.title = NSLocalizedString("Querying drives status...", comment: "VMDisplayWindowController")
|
|
item.isEnabled = false
|
|
menu.addItem(item)
|
|
updateDrivesMenu(menu, drives: vmQemuConfig.drives)
|
|
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
|
|
}
|
|
|
|
@nonobjc func updateDrivesMenu(_ menu: NSMenu, drives: [UTMQemuConfigurationDrive]) {
|
|
menu.removeAllItems()
|
|
if drives.count == 0 {
|
|
let item = NSMenuItem()
|
|
item.title = NSLocalizedString("No drives connected.", comment: "VMDisplayWindowController")
|
|
item.isEnabled = false
|
|
menu.addItem(item)
|
|
} else {
|
|
let item = NSMenuItem()
|
|
item.title = NSLocalizedString("Install Windows Guest Tools…", comment: "VMDisplayWindowController")
|
|
item.isEnabled = !qemuVM.isGuestToolsInstallRequested
|
|
item.target = self
|
|
item.action = #selector(installWindowsGuestTools)
|
|
menu.addItem(item)
|
|
}
|
|
for i in drives.indices {
|
|
let drive = drives[i]
|
|
if drive.imageType != .disk && drive.imageType != .cd && !drive.isExternal {
|
|
continue // skip non-disks
|
|
}
|
|
let item = NSMenuItem()
|
|
item.title = label(for: drive)
|
|
if !drive.isExternal {
|
|
item.isEnabled = false
|
|
} else {
|
|
let submenu = NSMenu()
|
|
submenu.autoenablesItems = false
|
|
let eject = NSMenuItem(title: NSLocalizedString("Eject", comment: "VMDisplayWindowController"),
|
|
action: #selector(ejectDrive),
|
|
keyEquivalent: "")
|
|
eject.target = self
|
|
eject.tag = i
|
|
eject.isEnabled = qemuVM.externalImageURL(for: drive) != nil
|
|
submenu.addItem(eject)
|
|
let change = NSMenuItem(title: NSLocalizedString("Change", comment: "VMDisplayWindowController"),
|
|
action: #selector(changeDriveImage),
|
|
keyEquivalent: "")
|
|
change.target = self
|
|
change.tag = i
|
|
change.isEnabled = true
|
|
submenu.addItem(change)
|
|
item.submenu = submenu
|
|
}
|
|
menu.addItem(item)
|
|
}
|
|
menu.update()
|
|
}
|
|
|
|
func ejectDrive(sender: AnyObject) {
|
|
guard let menu = sender as? NSMenuItem else {
|
|
logger.error("wrong sender for ejectDrive")
|
|
return
|
|
}
|
|
let drive = vmQemuConfig.drives[menu.tag]
|
|
Task.detached(priority: .background) { [self] in
|
|
do {
|
|
try await qemuVM.eject(drive)
|
|
} catch {
|
|
Task { @MainActor in
|
|
showErrorAlert(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func openDriveImage(forDriveIndex index: Int) {
|
|
let drive = vmQemuConfig.drives[index]
|
|
let openPanel = NSOpenPanel()
|
|
openPanel.title = NSLocalizedString("Select Drive Image", comment: "VMDisplayWindowController")
|
|
openPanel.allowedContentTypes = [.data]
|
|
openPanel.beginSheetModal(for: window!) { response in
|
|
guard response == .OK else {
|
|
return
|
|
}
|
|
guard let url = openPanel.url else {
|
|
logger.debug("no file selected")
|
|
return
|
|
}
|
|
Task.detached(priority: .background) { [self] in
|
|
do {
|
|
try await qemuVM.changeMedium(drive, to: url)
|
|
} catch {
|
|
Task { @MainActor in
|
|
showErrorAlert(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func changeDriveImage(sender: AnyObject) {
|
|
guard let menu = sender as? NSMenuItem else {
|
|
logger.error("wrong sender for ejectDrive")
|
|
return
|
|
}
|
|
openDriveImage(forDriveIndex: menu.tag)
|
|
}
|
|
|
|
@nonobjc private func label(for drive: UTMQemuConfigurationDrive) -> String {
|
|
let imageURL = qemuVM.externalImageURL(for: drive) ?? drive.imageURL
|
|
return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMDisplayQemuDisplayController"),
|
|
drive.imageType.prettyValue,
|
|
drive.interface.prettyValue,
|
|
imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMDisplayQemuDisplayController"))
|
|
}
|
|
|
|
@MainActor private func installWindowsGuestTools(sender: AnyObject) {
|
|
qemuVM.isGuestToolsInstallRequested = true
|
|
}
|
|
}
|
|
|
|
// MARK: - Shared folders
|
|
|
|
extension VMDisplayQemuWindowController {
|
|
@IBAction override func sharedFolderButtonPressed(_ sender: Any) {
|
|
let openPanel = NSOpenPanel()
|
|
openPanel.title = NSLocalizedString("Select Shared Folder", comment: "VMDisplayWindowController")
|
|
openPanel.canChooseDirectories = true
|
|
openPanel.canChooseFiles = false
|
|
openPanel.beginSheetModal(for: window!) { response in
|
|
guard response == .OK else {
|
|
return
|
|
}
|
|
guard let url = openPanel.url else {
|
|
logger.debug("no directory selected")
|
|
return
|
|
}
|
|
Task.detached(priority: .background) { [self] in
|
|
do {
|
|
try await self.qemuVM.changeSharedDirectory(to: url)
|
|
} catch {
|
|
Task { @MainActor in
|
|
self.showErrorAlert(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SPICE base implementation
|
|
|
|
extension VMDisplayQemuWindowController: UTMSpiceIODelegate {
|
|
private func configIdForSerial(_ serial: CSPort) -> Int? {
|
|
let prefix = "com.utmapp.terminal."
|
|
guard serial.name?.hasPrefix(prefix) ?? false else {
|
|
return nil
|
|
}
|
|
return Int(serial.name!.dropFirst(prefix.count))
|
|
}
|
|
|
|
func spiceDidCreateInput(_ input: CSInput) {
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDidCreateInput(input)
|
|
}
|
|
}
|
|
|
|
func spiceDidDestroyInput(_ input: CSInput) {
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDidDestroyInput(input)
|
|
}
|
|
}
|
|
|
|
func spiceDidCreateDisplay(_ display: CSDisplay) {
|
|
guard !isSecondary else {
|
|
return
|
|
}
|
|
Task { @MainActor in
|
|
findWindow(for: display)
|
|
}
|
|
}
|
|
|
|
func spiceDidUpdateDisplay(_ display: CSDisplay) {
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDidUpdateDisplay(display)
|
|
}
|
|
}
|
|
|
|
func spiceDidDestroyDisplay(_ display: CSDisplay) {
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDidDestroyDisplay(display)
|
|
}
|
|
}
|
|
|
|
func spiceDidChangeUsbManager(_ usbManager: CSUSBManager?) {
|
|
if usbManager != vmUsbManager {
|
|
connectedUsbDevices.removeAll()
|
|
allUsbDevices.removeAll()
|
|
vmUsbManager = usbManager
|
|
if let usbManager = usbManager {
|
|
usbManager.delegate = self
|
|
}
|
|
}
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDidChangeUsbManager(usbManager)
|
|
}
|
|
}
|
|
|
|
func spiceDynamicResolutionSupportDidChange(_ supported: Bool) {
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDynamicResolutionSupportDidChange(supported)
|
|
}
|
|
}
|
|
|
|
func spiceDidCreateSerial(_ serial: CSPort) {
|
|
guard !isSecondary else {
|
|
return
|
|
}
|
|
Task { @MainActor in
|
|
findWindow(for: serial)
|
|
}
|
|
}
|
|
|
|
func spiceDidDestroySerial(_ serial: CSPort) {
|
|
for subwindow in secondaryWindows {
|
|
(subwindow as! VMDisplayQemuWindowController).spiceDidDestroySerial(serial)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - USB handling
|
|
|
|
extension VMDisplayQemuWindowController: CSUSBManagerDelegate {
|
|
func spiceUsbManager(_ usbManager: CSUSBManager, deviceError error: String, for device: CSUSBDevice) {
|
|
logger.debug("USB device error: (\(device)) \(error)")
|
|
DispatchQueue.main.async {
|
|
self.showErrorAlert(error)
|
|
}
|
|
}
|
|
|
|
func spiceUsbManager(_ usbManager: CSUSBManager, deviceAttached device: CSUSBDevice) {
|
|
logger.debug("USB device attached: \(device)")
|
|
if !isNoUsbPrompt {
|
|
DispatchQueue.main.async {
|
|
if self.window!.isKeyWindow && self.vm.state == .vmStarted {
|
|
self.showConnectPrompt(for: device)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func spiceUsbManager(_ usbManager: CSUSBManager, deviceRemoved device: CSUSBDevice) {
|
|
logger.debug("USB device removed: \(device)")
|
|
if let i = connectedUsbDevices.firstIndex(of: device) {
|
|
connectedUsbDevices.remove(at: i)
|
|
}
|
|
}
|
|
|
|
func showConnectPrompt(for usbDevice: CSUSBDevice) {
|
|
guard let usbManager = vmUsbManager else {
|
|
logger.error("cannot get usb manager")
|
|
return
|
|
}
|
|
let alert = NSAlert()
|
|
alert.alertStyle = .informational
|
|
alert.messageText = NSLocalizedString("USB Device", comment: "VMQemuDisplayMetalWindowController")
|
|
alert.informativeText = String.localizedStringWithFormat(NSLocalizedString("Would you like to connect '%@' to this virtual machine?", comment: "VMQemuDisplayMetalWindowController"), usbDevice.name ?? usbDevice.description)
|
|
alert.showsSuppressionButton = true
|
|
alert.addButton(withTitle: NSLocalizedString("Confirm", comment: "VMQemuDisplayMetalWindowController"))
|
|
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMQemuDisplayMetalWindowController"))
|
|
alert.beginSheetModal(for: window!) { response in
|
|
if let suppressionButton = alert.suppressionButton,
|
|
suppressionButton.state == .on {
|
|
self.isNoUsbPrompt = true
|
|
}
|
|
guard response == .alertFirstButtonReturn else {
|
|
return
|
|
}
|
|
DispatchQueue.global(qos: .background).async {
|
|
usbManager.connectUsbDevice(usbDevice) { (result, message) in
|
|
DispatchQueue.main.async {
|
|
if let msg = message {
|
|
self.showErrorAlert(msg)
|
|
}
|
|
if result {
|
|
self.connectedUsbDevices.append(usbDevice)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// These devices cannot be captured as enforced by macOS. Capturing results in an error. App Store Review requests that we block out the option.
|
|
let usbBlockList = [
|
|
(0x05ac, 0x8102), // Apple Touch Bar Backlight
|
|
(0x05ac, 0x8103), // Apple Headset
|
|
(0x05ac, 0x8233), // Apple T2 Controller
|
|
(0x05ac, 0x8262), // Apple Ambient Light Sensor
|
|
(0x05ac, 0x8263),
|
|
(0x05ac, 0x8302), // Apple Touch Bar Display
|
|
(0x05ac, 0x8514), // Apple FaceTime HD Camera (Built-in)
|
|
(0x05ac, 0x8600), // Apple iBridge
|
|
]
|
|
|
|
extension VMDisplayQemuWindowController {
|
|
|
|
@IBAction override func usbButtonPressed(_ sender: Any) {
|
|
let menu = NSMenu()
|
|
menu.autoenablesItems = false
|
|
let item = NSMenuItem()
|
|
item.title = NSLocalizedString("Querying USB devices...", comment: "VMQemuDisplayMetalWindowController")
|
|
item.isEnabled = false
|
|
menu.addItem(item)
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
let devices = self.vmUsbManager?.usbDevices ?? []
|
|
DispatchQueue.main.async {
|
|
self.updateUsbDevicesMenu(menu, devices: devices)
|
|
}
|
|
}
|
|
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
|
|
}
|
|
|
|
func updateUsbDevicesMenu(_ menu: NSMenu, devices: [CSUSBDevice]) {
|
|
allUsbDevices = devices
|
|
menu.removeAllItems()
|
|
if devices.count == 0 {
|
|
let item = NSMenuItem()
|
|
item.title = NSLocalizedString("No USB devices detected.", comment: "VMQemuDisplayMetalWindowController")
|
|
item.isEnabled = false
|
|
menu.addItem(item)
|
|
}
|
|
for (i, device) in devices.enumerated() {
|
|
let item = NSMenuItem()
|
|
let canRedirect = vmUsbManager?.canRedirectUsbDevice(device, errorMessage: nil) ?? false
|
|
let isConnected = vmUsbManager?.isUsbDeviceConnected(device) ?? false
|
|
let isConnectedToSelf = connectedUsbDevices.contains(device)
|
|
item.title = device.name ?? device.description
|
|
let blocked = usbBlockList.contains { (usbVid, usbPid) in usbVid == device.usbVendorId && usbPid == device.usbProductId }
|
|
item.isEnabled = !blocked && canRedirect && (isConnectedToSelf || !isConnected)
|
|
item.state = isConnectedToSelf ? .on : .off;
|
|
item.tag = i
|
|
item.target = self
|
|
item.action = isConnectedToSelf ? #selector(disconnectUsbDevice) : #selector(connectUsbDevice)
|
|
menu.addItem(item)
|
|
}
|
|
menu.update()
|
|
}
|
|
|
|
@objc func connectUsbDevice(sender: AnyObject) {
|
|
guard let menu = sender as? NSMenuItem else {
|
|
logger.error("wrong sender for connectUsbDevice")
|
|
return
|
|
}
|
|
guard let usbManager = vmUsbManager else {
|
|
logger.error("cannot get usb manager")
|
|
return
|
|
}
|
|
let device = allUsbDevices[menu.tag]
|
|
DispatchQueue.global(qos: .background).async {
|
|
usbManager.connectUsbDevice(device) { (result, message) in
|
|
DispatchQueue.main.async {
|
|
if let msg = message {
|
|
self.showErrorAlert(msg)
|
|
}
|
|
if result {
|
|
self.connectedUsbDevices.append(device)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func disconnectUsbDevice(sender: AnyObject) {
|
|
guard let menu = sender as? NSMenuItem else {
|
|
logger.error("wrong sender for disconnectUsbDevice")
|
|
return
|
|
}
|
|
guard let usbManager = vmUsbManager else {
|
|
logger.error("cannot get usb manager")
|
|
return
|
|
}
|
|
let device = allUsbDevices[menu.tag]
|
|
DispatchQueue.global(qos: .background).async {
|
|
usbManager.disconnectUsbDevice(device) { (result, message) in
|
|
DispatchQueue.main.async {
|
|
if let msg = message {
|
|
self.showErrorAlert(msg)
|
|
}
|
|
if result {
|
|
self.connectedUsbDevices.removeAll(where: { $0 == device })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Window management
|
|
|
|
extension VMDisplayQemuWindowController {
|
|
@IBAction override func windowsButtonPressed(_ sender: Any) {
|
|
let menu = NSMenu()
|
|
menu.autoenablesItems = false
|
|
for display in qemuVM.ioService!.displays {
|
|
let id = display.monitorID
|
|
guard id < vmQemuConfig.displays.count else {
|
|
continue
|
|
}
|
|
let config = vmQemuConfig.displays[id]
|
|
let item = NSMenuItem()
|
|
let format = NSLocalizedString("Display %lld: %@", comment: "VMDisplayQemuDisplayController")
|
|
let title = String.localizedStringWithFormat(format, id + 1, config.hardware.prettyValue)
|
|
let isCurrent = self is VMDisplayQemuMetalWindowController && self.id == id
|
|
item.title = title
|
|
item.isEnabled = !isCurrent
|
|
item.state = isCurrent ? .on : .off
|
|
item.tag = id
|
|
item.target = self
|
|
item.action = #selector(showWindowFromDisplay)
|
|
menu.addItem(item)
|
|
}
|
|
for serial in qemuVM.ioService!.serials {
|
|
guard let id = configIdForSerial(serial) else {
|
|
continue
|
|
}
|
|
let item = NSMenuItem()
|
|
let format = NSLocalizedString("Serial %lld", comment: "VMDisplayQemuDisplayController")
|
|
let title = String.localizedStringWithFormat(format, id + 1)
|
|
let isCurrent = self is VMDisplayQemuTerminalWindowController && self.id == id
|
|
item.title = title
|
|
item.isEnabled = !isCurrent
|
|
item.state = isCurrent ? .on : .off
|
|
item.tag = id
|
|
item.target = self
|
|
item.action = #selector(showWindowFromSerial)
|
|
menu.addItem(item)
|
|
}
|
|
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
|
|
}
|
|
|
|
@objc private func showWindowFromDisplay(sender: AnyObject) {
|
|
let item = sender as! NSMenuItem
|
|
let id = item.tag
|
|
if self is VMDisplayQemuMetalWindowController && self.id == id {
|
|
return
|
|
}
|
|
guard let display = qemuVM.ioService?.displays.first(where: { $0.monitorID == id}) else {
|
|
return
|
|
}
|
|
if let window = findWindow(for: display) {
|
|
window.showWindow(self)
|
|
}
|
|
}
|
|
|
|
@objc private func showWindowFromSerial(sender: AnyObject) {
|
|
let item = sender as! NSMenuItem
|
|
let id = item.tag
|
|
if self is VMDisplayQemuTerminalWindowController && self.id == id {
|
|
return
|
|
}
|
|
guard let serial = qemuVM.ioService?.serials.first(where: { id == configIdForSerial($0) }) else {
|
|
return
|
|
}
|
|
if let window = findWindow(for: serial) {
|
|
window.showWindow(self)
|
|
}
|
|
}
|
|
|
|
@MainActor private func findWindow(for display: CSDisplay) -> VMDisplayQemuWindowController? {
|
|
let id = display.monitorID
|
|
let secondaryWindows: [VMDisplayWindowController]
|
|
if self is VMDisplayQemuMetalWindowController && self.id == id {
|
|
return self
|
|
}
|
|
if let window = primaryWindow {
|
|
if (window as? VMDisplayQemuMetalWindowController)?.id == id {
|
|
return window as? VMDisplayQemuWindowController
|
|
}
|
|
secondaryWindows = window.secondaryWindows
|
|
} else {
|
|
secondaryWindows = self.secondaryWindows
|
|
}
|
|
for window in secondaryWindows {
|
|
if let window = window as? VMDisplayQemuMetalWindowController {
|
|
if window.id == id {
|
|
// found existing window
|
|
return window
|
|
}
|
|
}
|
|
}
|
|
if let newWindow = newWindow(from: display) {
|
|
return newWindow
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@MainActor private func newWindow(from display: CSDisplay) -> VMDisplayQemuMetalWindowController? {
|
|
let id = display.monitorID
|
|
guard id < vmQemuConfig.displays.count else {
|
|
return nil
|
|
}
|
|
guard let primary = (primaryWindow ?? self) as? VMDisplayQemuMetalWindowController else {
|
|
return nil
|
|
}
|
|
let secondary = VMDisplayQemuMetalWindowController(secondaryFromDisplay: display, primary: primary, vm: qemuVM, id: id)
|
|
registerSecondaryWindow(secondary)
|
|
return secondary
|
|
}
|
|
|
|
@MainActor private func findWindow(for serial: CSPort) -> VMDisplayQemuWindowController? {
|
|
let id = configIdForSerial(serial)!
|
|
let secondaryWindows: [VMDisplayWindowController]
|
|
if self is VMDisplayQemuTerminalWindowController && self.id == id {
|
|
return self
|
|
}
|
|
if let window = primaryWindow {
|
|
if (window as? VMDisplayQemuTerminalWindowController)?.id == id {
|
|
return window as? VMDisplayQemuWindowController
|
|
}
|
|
secondaryWindows = window.secondaryWindows
|
|
} else {
|
|
secondaryWindows = self.secondaryWindows
|
|
}
|
|
for window in secondaryWindows {
|
|
if let window = window as? VMDisplayQemuTerminalWindowController {
|
|
if window.id == id {
|
|
// found existing window
|
|
return window
|
|
}
|
|
}
|
|
}
|
|
if let newWindow = newWindow(from: serial) {
|
|
return newWindow
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@MainActor private func newWindow(from serial: CSPort) -> VMDisplayQemuTerminalWindowController? {
|
|
guard let id = configIdForSerial(serial) else {
|
|
return nil
|
|
}
|
|
guard id < vmQemuConfig.serials.count else {
|
|
return nil
|
|
}
|
|
let secondary = VMDisplayQemuTerminalWindowController(secondaryFromSerialPort: serial, vm: qemuVM, id: id)
|
|
registerSecondaryWindow(secondary)
|
|
return secondary
|
|
}
|
|
}
|