From a67f096f1521657d08892cb5913a9517322c93f4 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 5 May 2023 15:58:24 +0200 Subject: [PATCH] Add option for setting shared folder and hard disk location Needs testing when distributed by Mac app store --- virtualOS.xcodeproj/project.pbxproj | 12 +- virtualOS/Extension/URL+Paths.swift | 38 ++++- .../Extension/UserDefaults+Settings.swift | 15 +- virtualOS/Model/ApplicationDelegate.swift | 4 + virtualOS/Model/Bookmark.swift | 66 +++++++++ virtualOS/Model/MainViewModel.swift | 47 ++++-- virtualOS/Model/VirtualMac.swift | 4 +- virtualOS/Model/VirtualMacConfiguration.swift | 17 +++ virtualOS/View/ConfigurationView.swift | 134 ++++++++++++++---- virtualOS/View/SettingsView.swift | 130 ++++++++++++----- virtualOS/virtualOS.entitlements | 6 +- 11 files changed, 379 insertions(+), 94 deletions(-) create mode 100644 virtualOS/Model/Bookmark.swift diff --git a/virtualOS.xcodeproj/project.pbxproj b/virtualOS.xcodeproj/project.pbxproj index 36a72c6..6b6c096 100644 --- a/virtualOS.xcodeproj/project.pbxproj +++ b/virtualOS.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C9927E238930048776B /* VirtualMacConfiguration.swift */; }; 00A4FFE8283E3D6F004DD9B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A4FFE7283E3D6F004DD9B3 /* SettingsView.swift */; }; 0114C02629AA2416004159AF /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114C02529AA2416004159AF /* MenuCommands.swift */; }; + 01B042F229CD9F6A003CD5C2 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */; }; 01FCAD8429AB707C00F12689 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FCAD8329AB707C00F12689 /* ApplicationDelegate.swift */; }; /* End PBXBuildFile section */ @@ -69,6 +70,7 @@ 00A4FFE7283E3D6F004DD9B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 00BA26AC2826DAF200E80B76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 0114C02529AA2416004159AF /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; + 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; 01FCAD8329AB707C00F12689 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -175,6 +177,7 @@ 0044A65427F601E60007988A /* MainViewModel.swift */, 007987B027E24A8400960D74 /* VirtualMac.swift */, 00989C9927E238930048776B /* VirtualMacConfiguration.swift */, + 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */, ); path = Model; sourceTree = ""; @@ -333,6 +336,7 @@ 006504E727F9D59300723BCA /* ConfigurationView.swift in Sources */, 0114C02629AA2416004159AF /* MenuCommands.swift in Sources */, 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */, + 01B042F229CD9F6A003CD5C2 /* Bookmark.swift in Sources */, 0090AF6127E25F6F0077D35F /* UInt64+Byte.swift in Sources */, 0044A65527F601E60007988A /* MainViewModel.swift in Sources */, ); @@ -496,7 +500,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; DEVELOPMENT_TEAM = 2AD47BTDQ6; @@ -511,7 +515,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -529,7 +533,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; DEVELOPMENT_TEAM = 2AD47BTDQ6; @@ -544,7 +548,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/virtualOS/Extension/URL+Paths.swift b/virtualOS/Extension/URL+Paths.swift index 1378f7b..f9483a9 100644 --- a/virtualOS/Extension/URL+Paths.swift +++ b/virtualOS/Extension/URL+Paths.swift @@ -8,13 +8,37 @@ import Foundation extension URL { static let basePath = NSHomeDirectory() + "/Documents" static let restoreImageURL = URL(fileURLWithPath: basePath + "/RestoreImage.ipsw") + static let bundleName = "virtualOS.bundle/" + static let defaultVmBundlePath = basePath + "/\(bundleName)" + + static var vmBundleURL: URL { + return URL(fileURLWithPath: vmBundlePath) + } + static var diskImageURL: URL { + return URL(fileURLWithPath: vmBundlePath + "/Disk.img") + } + static var auxiliaryStorageURL: URL { + return URL(fileURLWithPath: vmBundlePath + "/AuxiliaryStorage") + } + static var machineIdentifierURL: URL { + return URL(fileURLWithPath: vmBundlePath + "/MachineIdentifier") + } + static var hardwareModelURL: URL { + return URL(fileURLWithPath: vmBundlePath + "/HardwareModel") + } + static var parametersURL: URL { + return URL(fileURLWithPath: vmBundlePath + "/Parameters.txt") + } - static let vmBundlePath = basePath + "/virtualOS.bundle/" - static let vmBundleURL = URL(fileURLWithPath: vmBundlePath) - static let diskImageURL = URL(fileURLWithPath: vmBundlePath + "Disk.img") - static let auxiliaryStorageURL = URL(fileURLWithPath: vmBundlePath + "AuxiliaryStorage") - static let machineIdentifierURL = URL(fileURLWithPath: vmBundlePath + "MachineIdentifier") - static let hardwareModelURL = URL(fileURLWithPath: vmBundlePath + "HardwareModel") - static let parametersURL = URL(fileURLWithPath: vmBundlePath + "Parameters.txt") + static var vmBundlePath: String { + if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData, + let hardDiskDirectoryURL = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) + { + let vmBundlePath = hardDiskDirectoryURL.appendingPathComponent(bundleName).path + return vmBundlePath + } else { + return URL.defaultVmBundlePath + } + } } diff --git a/virtualOS/Extension/UserDefaults+Settings.swift b/virtualOS/Extension/UserDefaults+Settings.swift index 70aef75..88743f1 100644 --- a/virtualOS/Extension/UserDefaults+Settings.swift +++ b/virtualOS/Extension/UserDefaults+Settings.swift @@ -8,8 +8,9 @@ import Foundation extension UserDefaults { - fileprivate static let diskSizeKey = "diskSize" - + fileprivate static let diskSizeKey = "diskSize" + fileprivate static let hardDiskBookmarkKey = "hardDiskBookmark" + var diskSize: Int { get { if object(forKey: Self.diskSizeKey) != nil { @@ -22,4 +23,14 @@ extension UserDefaults { synchronize() } } + + var hardDiskDirectoryBookmarkData: Data? { + get { + return data(forKey: Self.hardDiskBookmarkKey) + } + set { + set(newValue, forKey: Self.hardDiskBookmarkKey) + synchronize() + } + } } diff --git a/virtualOS/Model/ApplicationDelegate.swift b/virtualOS/Model/ApplicationDelegate.swift index 7c0c122..92b42fc 100644 --- a/virtualOS/Model/ApplicationDelegate.swift +++ b/virtualOS/Model/ApplicationDelegate.swift @@ -11,4 +11,8 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + func applicationWillTerminate(_ notification: Notification) { + Bookmark.stopAllAccess() + } } diff --git a/virtualOS/Model/Bookmark.swift b/virtualOS/Model/Bookmark.swift new file mode 100644 index 0000000..e9ce4ee --- /dev/null +++ b/virtualOS/Model/Bookmark.swift @@ -0,0 +1,66 @@ +// +// Data+Bookmark.swift +// virtualOS +// +// Created by Jahn Bertsch on 24.03.23. +// + +import Foundation + +struct Bookmark { + enum BookmarkType { + case hardDisk + case sharedFolder + } + + fileprivate static var accessedURLs: [BookmarkType: URL] = [:] + + static func createBookmarkData(fromUrl url: URL) -> Data? { + if let bookmarkData = try? url.bookmarkData(options: .withSecurityScope, relativeTo: nil) { + return bookmarkData + } + return nil + } + + static func startAccess(data: Data?, forType key: BookmarkType) -> URL? { + var bookmarkDataIsStale = false + if let bookmarkData = data, + let bookmarkURL = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale), + !bookmarkDataIsStale + { + // stop accessing previous resource + if let previousURL = accessedURLs[key], + previousURL != bookmarkURL + { + previousURL.stopAccessingSecurityScopedResource() + } + + if accessedURLs[key] == bookmarkURL { + // resource already accessed, do nothing + } else { + // start access resource + if !bookmarkURL.startAccessingSecurityScopedResource() { + // access failed + bookmarkURL.stopAccessingSecurityScopedResource() + return nil + } + accessedURLs[key] = bookmarkURL + } + return bookmarkURL + } + + return nil + } + + static func stopAccess(url: URL, forKey key: BookmarkType) { + url.stopAccessingSecurityScopedResource() + Self.accessedURLs[key] = nil + } + + static func stopAllAccess() { + for (_, accessedURL) in accessedURLs { + accessedURL.stopAccessingSecurityScopedResource() + } + Self.accessedURLs = [:] + } +} diff --git a/virtualOS/Model/MainViewModel.swift b/virtualOS/Model/MainViewModel.swift index 1371f80..d6b096d 100644 --- a/virtualOS/Model/MainViewModel.swift +++ b/virtualOS/Model/MainViewModel.swift @@ -38,6 +38,7 @@ final class MainViewModel: NSObject, ObservableObject { @Published var virtualMac = VirtualMac() @Published var virtualMachine: VZVirtualMachine? @Published var customRestoreImageURL: URL? + @Published var customHardDiskURL: URL? @Published var diskSize = UserDefaults.standard.diskSize { didSet { UserDefaults.standard.diskSize = diskSize @@ -58,20 +59,31 @@ final class MainViewModel: NSObject, ObservableObject { static var restoreImageExists: Bool { return FileManager.default.fileExists(atPath: URL.restoreImageURL.path) } - + var sharedFolderExists: Bool { + if let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: virtualMac.parameters.sharedFolder, forType: .hardDisk) { + var isDirectory = ObjCBool(false) + if FileManager.default.fileExists(atPath: hardDiskDirectoryBookmarkData.path, isDirectory: &isDirectory), + isDirectory.boolValue == true + { + return true + } + } + return false + } var showConfigurationView: Bool { return (Self.diskImageExists || Self.restoreImageExists) && state == .Stopped } var showSettingsInfo: Bool { return !Self.diskImageExists && state == .Stopped } + + // MARK: - Public override init() { super.init() updateLabels(for: state) readParametersFromDisk() loadLicenseInformationFromBundle() - moveFilesAfterUpdate() handleCommandLineArguments() } @@ -126,7 +138,7 @@ final class MainViewModel: NSObject, ObservableObject { } else { licenseInformationString = "License information not found" } - + licenseInformationTitleString = "virtualOS" if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String @@ -134,10 +146,25 @@ final class MainViewModel: NSObject, ObservableObject { licenseInformationTitleString += " \(version) (Build \(build))" } } + + func set(sharedFolderUrl: URL) { + if let sharedFolderData = Bookmark.createBookmarkData(fromUrl: sharedFolderUrl) { + _ = Bookmark.startAccess(data: sharedFolderData, forType: .sharedFolder) + virtualMac.parameters.sharedFolder = sharedFolderData + objectWillChange.send() + if let errorString = virtualMac.writeParametersToDisk() { + display(errorString: errorString) + } + } + } // MARK: - Private fileprivate func readParametersFromDisk() { + if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData { + _ = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) + } + if Self.diskImageExists { // read previous vm settings if let errorString = virtualMac.readFromDisk(delegate: self) { @@ -176,7 +203,9 @@ final class MainViewModel: NSObject, ObservableObject { self.display(errorString: "Download of restore image failed: \(errorString)") } else { virtualOSApp.debugLog("Download of restore image completed") - self.install(virtualMac: self.virtualMac) + DispatchQueue.main.async { + self.install(virtualMac: self.virtualMac) + } } } } @@ -306,16 +335,6 @@ final class MainViewModel: NSObject, ObservableObject { statusLabel = statusText } - fileprivate func moveFilesAfterUpdate() { - let oldRestoreImageLocation = URL(fileURLWithPath: NSHomeDirectory() + "/RestoreImage.ipsw") - let newRestoreImageLocation = URL(fileURLWithPath: NSHomeDirectory() + "/Documents/RestoreImage.ipsw") - try? FileManager.default.moveItem(at: oldRestoreImageLocation, to: newRestoreImageLocation) - - let oldVirtualMachineLocation = URL(fileURLWithPath: NSHomeDirectory() + "/virtualOS.bundle") - let newVirtualMachineLocation = URL(fileURLWithPath: NSHomeDirectory() + "/Documents/virtualOS.bundle") - try? FileManager.default.moveItem(at: oldVirtualMachineLocation, to: newVirtualMachineLocation) - } - fileprivate func handleCommandLineArguments() { for arg in CommandLine.arguments where arg == "start" { start() diff --git a/virtualOS/Model/VirtualMac.swift b/virtualOS/Model/VirtualMac.swift index e6e4dd4..79ab1ec 100644 --- a/virtualOS/Model/VirtualMac.swift +++ b/virtualOS/Model/VirtualMac.swift @@ -27,6 +27,7 @@ final class VirtualMac: ObservableObject { screenHeight = try container.decode(Int.self, forKey: .screenHeight) pixelsPerInch = try container.decode(Int.self, forKey: .pixelsPerInch) microphoneEnabled = try container.decode(Bool.self, forKey: .microphoneEnabled) + sharedFolder = try container.decodeIfPresent(Data.self, forKey: .sharedFolder) ?? nil // optional macAddress = try container.decodeIfPresent(String.self, forKey: .macAddress) ?? VZMACAddress.randomLocallyAdministered().string // optional } @@ -42,6 +43,7 @@ final class VirtualMac: ObservableObject { var screenHeight = 900 var pixelsPerInch = 250 var microphoneEnabled = false + var sharedFolder: Data? var macAddress = VZMACAddress.randomLocallyAdministered().string } @@ -166,7 +168,7 @@ final class VirtualMac: ObservableObject { let virtualMachine = VZVirtualMachine(configuration: virtualMacConfiguration, queue: .main) virtualMachine.delegate = delegate - virtualOSApp.debugLog("Using \(virtualMacConfiguration.cpuCount) cores, \(virtualMacConfiguration.memorySize.bytesToGigabytes()) GB RAM and screen size \(parameters.screenWidth)x\(parameters.screenHeight) px at \(parameters.pixelsPerInch) ppi") + virtualOSApp.debugLog("Using \(virtualMacConfiguration.cpuCount) cores, \(virtualMacConfiguration.memorySize.bytesToGigabytes()) GB RAM, screen size \(parameters.screenWidth)x\(parameters.screenHeight) px at \(parameters.pixelsPerInch) ppi, shared folder: \(Bookmark.startAccess(data: parameters.sharedFolder, forType: .sharedFolder)?.absoluteString ?? "none")") return virtualMachine } diff --git a/virtualOS/Model/VirtualMacConfiguration.swift b/virtualOS/Model/VirtualMacConfiguration.swift index c9f895c..6bcb99f 100644 --- a/virtualOS/Model/VirtualMacConfiguration.swift +++ b/virtualOS/Model/VirtualMacConfiguration.swift @@ -58,6 +58,9 @@ final class VirtualMacConfiguration: VZVirtualMachineConfiguration { configureGraphicsDevice(parameters: parameters) configureStorageDevice(parameters: parameters) configureNetworkDevices(parameters: parameters) + if #available(macOS 13.0, *) { + configureSharedFolder(parameters: parameters) + } } // MARK: - Private @@ -139,6 +142,20 @@ final class VirtualMacConfiguration: VZVirtualMachineConfiguration { virtualOSApp.debugLog("Error: could not create storage device") } } + + @available(macOS 13.0, *) + fileprivate func configureSharedFolder(parameters: VirtualMac.Parameters) { + guard let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: parameters.sharedFolder, forType: .hardDisk) else { + return + } + + let sharedDirectory = VZSharedDirectory(url: hardDiskDirectoryBookmarkData, readOnly: false) + let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory) + let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag) + sharingConfiguration.share = singleDirectoryShare + + directorySharingDevices = [sharingConfiguration] + } fileprivate func computeCPUCount() -> Int { let totalAvailableCPUs = ProcessInfo.processInfo.processorCount diff --git a/virtualOS/View/ConfigurationView.swift b/virtualOS/View/ConfigurationView.swift index 997bbe3..ce5f82b 100644 --- a/virtualOS/View/ConfigurationView.swift +++ b/virtualOS/View/ConfigurationView.swift @@ -10,8 +10,16 @@ import SwiftUI struct ConfigurationView: View { + enum ScreenSize: Int, Codable { + case custom = 1 + case mainScreen = 2 + } + public enum SharedFolderType: Int, Codable { + case none = 1 + case custom = 2 + } + @ObservedObject var viewModel: MainViewModel - fileprivate let sliderTextWidth = CGFloat(150) @State fileprivate var cpuCountSliderValue: Float = 0 { didSet { viewModel.virtualMac.parameters.cpuCount = Int(cpuCountSliderValue) @@ -32,15 +40,34 @@ struct ConfigurationView: View { viewModel.virtualMac.parameters.screenHeight = Int(screenHeightValue) } } - @State fileprivate var useCustomScreenSize = true - @State fileprivate var useMainScreenSize = false + @State fileprivate var screenSize: ScreenSize = .mainScreen + @State var sharedFolderType: SharedFolderType = .none + + fileprivate var sharedFolderInfo: String { + if #available(macOS 13.0, *) { + if sharedFolderType == .custom, + let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: viewModel.virtualMac.parameters.sharedFolder, forType: .sharedFolder) + { + if viewModel.sharedFolderExists { + return "Using \(hardDiskDirectoryBookmarkData.path)" + } else { + return "Shared folder not found." + } + } else { + return "No shared folder selected." + } + } else { + return "Shared folders require macOS 13 or newer." + } + } + fileprivate let textWidth = CGFloat(150) var body: some View { VStack { Spacer() VStack { let parameters = viewModel.virtualMac.parameters - Text("Virtual Machine Configuration").font(.title) + Text("Virtual Machine Configuration").font(.headline) Slider(value: Binding(get: { cpuCountSliderValue @@ -48,7 +75,7 @@ struct ConfigurationView: View { cpuCountSliderValue = newValue }), in: Float(parameters.cpuCountMin) ... Float(parameters.cpuCountMax), step: 1) { Text("CPU Count: \(viewModel.virtualMac.parameters.cpuCount)") - .frame(minWidth: sliderTextWidth, alignment: .leading) + .frame(minWidth: textWidth, alignment: .leading) } Slider(value: Binding(get: { @@ -57,23 +84,23 @@ struct ConfigurationView: View { memorySliderValue = newValue }), in: Float(parameters.memorySizeInGBMin) ... Float(parameters.memorySizeInGBMax), step: 1) { Text("RAM: \(viewModel.virtualMac.parameters.memorySizeInGB) GB") - .frame(minWidth: sliderTextWidth, alignment: .leading) + .frame(minWidth: textWidth, alignment: .leading) } HStack() { - Text("Screen Size:") - Spacer() - Toggle("Custom", isOn: $useCustomScreenSize).onChange(of: useCustomScreenSize) { newValue in - useMainScreenSize = !newValue - } - Toggle("Main Screen", isOn: $useMainScreenSize).onChange(of: useMainScreenSize) { newValue in - useCustomScreenSize = !newValue - viewModel.virtualMac.parameters.useMainScreenSize = newValue - if let mainScreen = NSScreen.main { - screenWidthValue = Float(mainScreen.frame.width) - screenHeightValue = Float(mainScreen.frame.height) + Text("Screen Size").frame(minWidth: textWidth, alignment: .leading) + Picker("", selection: $screenSize) { + Text("Main Screen").tag(ScreenSize.mainScreen) + Text("Custom").tag(ScreenSize.custom) + }.pickerStyle(.inline) + .onChange(of: screenSize) { newValue in + viewModel.virtualMac.parameters.useMainScreenSize = newValue == .mainScreen + if let mainScreen = NSScreen.main { + screenWidthValue = Float(mainScreen.frame.width) + screenHeightValue = Float(mainScreen.frame.height) + } } - } + Spacer() } Slider(value: Binding(get: { @@ -82,8 +109,8 @@ struct ConfigurationView: View { screenWidthValue = newValue }), in: 800 ... Float(NSScreen.main?.frame.width ?? CGFloat(parameters.screenWidth)), step: 100) { Text("Screen Width: \(viewModel.virtualMac.parameters.screenWidth) px") - .frame(minWidth: sliderTextWidth, alignment: .leading) - }.disabled(useMainScreenSize) + .frame(minWidth: textWidth, alignment: .leading) + }.disabled(screenSize == .mainScreen) Slider(value: Binding(get: { screenHeightValue @@ -91,12 +118,32 @@ struct ConfigurationView: View { screenHeightValue = newValue }), in: 600 ... Float(NSScreen.main?.frame.height ?? CGFloat(parameters.screenHeight)), step: 50) { Text("Screen Height: \(viewModel.virtualMac.parameters.screenHeight) px") - .frame(minWidth: sliderTextWidth, alignment: .leading) - }.disabled(useMainScreenSize) + .frame(minWidth: textWidth, alignment: .leading) + }.disabled(screenSize == .mainScreen) + + HStack() { + Text("Shared Folder").frame(minWidth: textWidth, alignment: .leading) + VStack(alignment: .leading, content: { + Picker("", selection: $sharedFolderType) { + Text("None").tag(SharedFolderType.none) + Text("Custom").tag(SharedFolderType.custom) + }.pickerStyle(.inline) + Button("Select Shared Folder") { + selectSharedFolder() + } .disabled(sharedFolderType == .none) + .padding(.top, 7) + Text(sharedFolderInfo) + .font(.caption) + .frame(maxWidth: 270, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(nil) + .disabled(sharedFolderType == .none) + }) + } } .padding() .overlay { - RoundedRectangle(cornerRadius: 5) + RoundedRectangle(cornerRadius: 3) .stroke(.tertiary, lineWidth: 1) } @@ -105,12 +152,41 @@ struct ConfigurationView: View { .padding() .frame(maxWidth: 400) .onAppear { - let parameters = viewModel.virtualMac.parameters - cpuCountSliderValue = Float(parameters.cpuCount) - memorySliderValue = Float(parameters.memorySizeInGB) - useMainScreenSize = parameters.useMainScreenSize - screenWidthValue = Float(parameters.screenWidth) - screenHeightValue = Float(parameters.screenHeight) + onAppear() + } + } + + // MARK: - Private + + fileprivate func selectSharedFolder() { + let openPanel = NSOpenPanel() + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = true + openPanel.canChooseFiles = false + openPanel.prompt = "Select" + if openPanel.runModal() == .OK, + let selectedURL = openPanel.url + { + viewModel.set(sharedFolderUrl: selectedURL) + } + } + + fileprivate func onAppear() { + let parameters = viewModel.virtualMac.parameters + cpuCountSliderValue = Float(parameters.cpuCount) + memorySliderValue = Float(parameters.memorySizeInGB) + screenWidthValue = Float(parameters.screenWidth) + screenHeightValue = Float(parameters.screenHeight) + if parameters.useMainScreenSize { + screenSize = .mainScreen + } else { + screenSize = .custom + } + if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData { + _ = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) + } + if Bookmark.startAccess(data: parameters.sharedFolder, forType: .sharedFolder) != nil { + sharedFolderType = .custom } } } diff --git a/virtualOS/View/SettingsView.swift b/virtualOS/View/SettingsView.swift index bce88ae..9a4ba4c 100644 --- a/virtualOS/View/SettingsView.swift +++ b/virtualOS/View/SettingsView.swift @@ -13,12 +13,30 @@ import UniformTypeIdentifiers struct SettingsView: View { @ObservedObject var viewModel: MainViewModel - enum RestoreImageType: String { + enum HardDiskLocation: String, CaseIterable, Identifiable { + case sandbox = "Sandbox" + case custom = "Select location where VM hard disk images will be stored." + var id: Self { self } + } + enum RestoreImageType: String, CaseIterable, Identifiable { case latest = "Downloads latest restore image from Apple." case custom = "Select custom restore image (.ipsw)\nFor example, download from [https://ipsw.me](https://ipsw.me/product/Mac)" + var id: Self { self } } + @State var diskSize = String(UserDefaults.standard.diskSize) + @State var hardDisk = HardDiskLocation.sandbox @State var restoreImageType = RestoreImageType.latest + + var hardDiskLocationInfo: String { + if let customHardDiskURL = viewModel.customHardDiskURL { + return "Using \(customHardDiskURL.path)" + } else if hardDisk == .sandbox { + return URL.basePath + } else { + return HardDiskLocation.custom.rawValue + } + } var restoreImageInfo: String { if let restoreImageURL = viewModel.customRestoreImageURL { return "Using \(restoreImageURL.path)" @@ -27,21 +45,6 @@ struct SettingsView: View { } } - fileprivate func selectRestoreImage() { - guard let ipswContentType = UTType(filenameExtension: "ipsw") else { - return - } - let openPanel = NSOpenPanel() - openPanel.allowsMultipleSelection = false - openPanel.canChooseFiles = true - openPanel.allowedContentTypes = [ipswContentType] - if openPanel.runModal() == .OK, - let selectedURL = openPanel.url - { - viewModel.customRestoreImageURL = selectedURL - } - } - var body: some View { VStack { Text("Settings") @@ -59,47 +62,104 @@ struct SettingsView: View { Text("(in GB)") } - Picker("Restore Image:", selection: $restoreImageType) { - Text("Latest").tag(RestoreImageType.latest) - Text("Custom").tag(RestoreImageType.custom) + Picker("Hard Disk Location:", selection: $hardDisk) { + Text("Default").tag(HardDiskLocation.sandbox) + Text("Custom").tag(HardDiskLocation.custom) } .pickerStyle(.inline) + .onChange(of: hardDisk) { newValue in + if newValue == .sandbox { + UserDefaults.standard.hardDiskDirectoryBookmarkData = nil + } + } HStack { - Button("Select Restore Image") { - selectRestoreImage() - } - .disabled(restoreImageType == .latest) + Button("Select Hard Disk Location") { + selectCustomHardDiskLocation() + }.disabled(hardDisk == .sandbox) } + + Text(.init("hardDiskLocationInfo")) + .font(.caption) + .frame(maxWidth: 270, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(nil) + .disabled(hardDisk == .sandbox) - Text(.init(restoreImageInfo)) + Picker("Restore Image:", selection: $restoreImageType) { + Text("Latest").tag(RestoreImageType.latest) + Text("Custom").tag(RestoreImageType.custom) + }.pickerStyle(.inline) + Button("Select Restore Image") { + selectRestoreImage() + }.disabled(restoreImageType == .latest) + Text(restoreImageInfo) .font(.caption) .frame(maxWidth: 270, alignment: .leading) .fixedSize(horizontal: false, vertical: true) .lineLimit(nil) .disabled(restoreImageType == .latest) - } - .padding(.bottom) + }.padding(.bottom) - Text("Virtual machine and restore image location:\n \(URL.basePath)\n\nTo open this directory:\nIn Finder, in the 'Go' menu, select 'Go to Folder' and enter the above URL.") - .frame(maxWidth: 370, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(nil) - .padding(.bottom) - .textSelection(.enabled) - .font(.caption) + Text("To open the hard disk location directory:\nIn Finder, in the 'Go' menu, select 'Go to Folder' and enter the path shown above.") + .frame(maxWidth: 370, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(nil) + .padding(.bottom) + .textSelection(.enabled) + .font(.caption) Button("OK") { viewModel.showSettings = !viewModel.showSettings - } - .keyboardShortcut(.defaultAction) + }.keyboardShortcut(.defaultAction) } .padding() .frame(minWidth: 420) .onAppear() { diskSize = String(viewModel.diskSize) + if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData, + let hardDiskURL = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) + { + hardDisk = .custom + viewModel.customHardDiskURL = hardDiskURL + } + } + } + + // MARK: - Private + + fileprivate func selectCustomHardDiskLocation() { + let openPanel = NSOpenPanel() + openPanel.directoryURL = URL(fileURLWithPath: URL.basePath, isDirectory: true) + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = true + openPanel.canChooseFiles = false + if openPanel.runModal() == .OK, + let selectedURL = openPanel.url + { + let hardDiskDirectoryBookmarkData = Bookmark.createBookmarkData(fromUrl: selectedURL) + _ = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) + + viewModel.customHardDiskURL = selectedURL + UserDefaults.standard.hardDiskDirectoryBookmarkData = hardDiskDirectoryBookmarkData } } + + fileprivate func selectRestoreImage() { + guard let ipswContentType = UTType(filenameExtension: "ipsw") else { + return + } + let openPanel = NSOpenPanel() + openPanel.allowsMultipleSelection = false + openPanel.canChooseFiles = true + openPanel.allowedContentTypes = [ipswContentType] + if openPanel.runModal() == .OK, + let selectedURL = openPanel.url + { + viewModel.customRestoreImageURL = selectedURL + } + } + } struct SettingsViewProvider_Previews: PreviewProvider { diff --git a/virtualOS/virtualOS.entitlements b/virtualOS/virtualOS.entitlements index f1c6161..38d08b4 100644 --- a/virtualOS/virtualOS.entitlements +++ b/virtualOS/virtualOS.entitlements @@ -4,11 +4,13 @@ com.apple.security.app-sandbox - com.apple.security.files.user-selected.read-only + com.apple.security.virtualization com.apple.security.network.client - com.apple.security.virtualization + com.apple.security.files.user-selected.read-write + + com.apple.security.files.bookmarks.document-scope