mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
ios: open new windows through SwiftUI and hide the item when unsupported
"New Window…" called requestSceneSessionActivation with no error handler whenever UIApplication reports multiple scenes. On iPhone Duo that stays true although new scenes are only allowed on the inner display, so the request could fail silently on the outer one. Give the window group an identifier and use openWindow from iOS 16, gated on the supportsMultipleWindows environment value which follows the system's current answer; the iOS 15 path keeps the UIKit call but now reports a refusal in an alert. Two windows of one session could also pick the same display, and both would then request its resolution with the last one winning. The window and external monitor pickers only offer devices that no other window already shows. Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
@@ -36,8 +36,11 @@ struct UTMApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies the window group for `openWindow`.
|
||||
static let windowID = "main"
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
WindowGroup(id: Self.windowID) {
|
||||
UTMSingleWindowView(data: data)
|
||||
.externalDisplayAccessory()
|
||||
}.commands {
|
||||
|
||||
@@ -23,18 +23,22 @@ struct VMToolbarDisplayMenuView: View {
|
||||
#if os(visionOS)
|
||||
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
#else
|
||||
private var supportsMultipleWindows: Bool {
|
||||
UIApplication.shared.supportsMultipleScenes
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Two views of one display would both request its resolution, so a window only offers what no
|
||||
/// other window shows, and what it shows itself so that its selection is always in the list.
|
||||
private func availableDevices(for windowID: VMSessionState.WindowID?, current: VMWindowState.Device?) -> [VMWindowState.Device] {
|
||||
session.devices.filter { device in
|
||||
device == current || !session.windowDeviceMap.contains { $0.key != windowID && $0.value == device }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Menu {
|
||||
Picker("", selection: $state.device) {
|
||||
MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
|
||||
ForEach(session.devices) { device in
|
||||
ForEach(availableDevices(for: state.id, current: state.device)) { device in
|
||||
switch device {
|
||||
case .serial(_, let index):
|
||||
MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "rectangle.connected.to.line.below").tag(device as VMWindowState.Device?)
|
||||
@@ -55,7 +59,7 @@ struct VMToolbarDisplayMenuView: View {
|
||||
}
|
||||
Picker("", selection: $externalDevice) {
|
||||
MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
|
||||
ForEach(session.devices) { device in
|
||||
ForEach(availableDevices(for: externalWindowBinding.wrappedValue.id, current: externalWindowBinding.wrappedValue.device)) { device in
|
||||
switch device {
|
||||
case .serial(_, let index):
|
||||
MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "rectangle.connected.to.line.below").tag(device as VMWindowState.Device?)
|
||||
@@ -68,19 +72,29 @@ struct VMToolbarDisplayMenuView: View {
|
||||
MenuLabel("External Monitor", systemImage: "rectangle.on.rectangle")
|
||||
}
|
||||
}
|
||||
#if os(visionOS)
|
||||
if supportsMultipleWindows {
|
||||
Divider()
|
||||
Button {
|
||||
#if os(visionOS)
|
||||
openWindow(value: session.newWindow())
|
||||
#else
|
||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: nil, options: nil, errorHandler: nil)
|
||||
#endif
|
||||
} label: {
|
||||
MenuLabel("New Window…", systemImage: "plus.rectangle.on.rectangle")
|
||||
NewWindowLabel()
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
if #available(iOS 16, *) {
|
||||
NewWindowMenuItem()
|
||||
} else if UIApplication.shared.supportsMultipleScenes {
|
||||
Divider()
|
||||
Button {
|
||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { error in
|
||||
state.alert = .nonfatalError(error.localizedDescription)
|
||||
}
|
||||
} label: {
|
||||
NewWindowLabel()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} label: {
|
||||
Label("Display", systemImage: "rectangle.on.rectangle")
|
||||
}.overlay(Badge(count: session.devices.count), alignment: .topTrailing)
|
||||
@@ -90,6 +104,35 @@ struct VMToolbarDisplayMenuView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct NewWindowLabel: View {
|
||||
var body: some View {
|
||||
MenuLabel("New Window…", systemImage: "plus.rectangle.on.rectangle")
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
/// Opens another scene through SwiftUI, which also knows when the system will not allow one.
|
||||
///
|
||||
/// `UIApplication.supportsMultipleScenes` stays true on iPhone Duo even though new scenes can
|
||||
/// only be created on the inner display, so the item hides itself from the environment instead.
|
||||
@available(iOS 16, *)
|
||||
private struct NewWindowMenuItem: View {
|
||||
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
|
||||
var body: some View {
|
||||
if supportsMultipleWindows {
|
||||
Divider()
|
||||
Button {
|
||||
openWindow(id: UTMApp.windowID)
|
||||
} label: {
|
||||
NewWindowLabel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private struct Badge: View {
|
||||
let count: Int
|
||||
|
||||
|
||||
Reference in New Issue
Block a user