Skip to content

Commit 06d86e2

Browse files
authored
Merge pull request #185 from ikorich/feature/context-menu
added power action and open in Finder to context menu
2 parents b2b2811 + ccfb2a2 commit 06d86e2

File tree

5 files changed

+157
-99
lines changed

5 files changed

+157
-99
lines changed

ControlRoom.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@
108108
511BA58F23F4030D00E3E660 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
109109
511BA59123F4031F00E3E660 /* ControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlView.swift; sourceTree = "<group>"; };
110110
511BA59523F408F800E3E660 /* LoadingFailedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingFailedView.swift; sourceTree = "<group>"; };
111-
511BA59723F4096800E3E660 /* Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulator.swift; sourceTree = "<group>"; };
112-
511BA59B23F4172400E3E660 /* SystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemView.swift; sourceTree = "<group>"; };
111+
511BA59723F4096800E3E660 /* Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulator.swift; sourceTree = "<group>"; wrapsLines = 1; };
112+
511BA59B23F4172400E3E660 /* SystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemView.swift; sourceTree = "<group>"; usesTabs = 1; };
113113
511BA59F23F4197200E3E660 /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = "<group>"; };
114114
511BA5A123F41A5900E3E660 /* Binding-OnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding-OnChange.swift"; sourceTree = "<group>"; };
115115
511BA5A723F42EE400E3E660 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };

ControlRoom/Controllers/Simulator.swift

+111-76
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ typealias Runtime = SimCtl.Runtime
1313
typealias DeviceType = SimCtl.DeviceType
1414

1515
/// Stores one simulator and its identifier.
16-
struct Simulator: Identifiable, Comparable, Hashable {
16+
struct Simulator: Identifiable, Comparable {
1717
enum State {
1818
case unknown
1919
case creating
@@ -37,6 +37,28 @@ struct Simulator: Identifiable, Comparable, Hashable {
3737
self = .unknown
3838
}
3939
}
40+
41+
var menuActionName: String {
42+
switch self {
43+
case .unknown: ""
44+
case .creating: "Creating..."
45+
case .booting: "Booting..."
46+
case .booted: "Shutdown"
47+
case .shuttingDown: "Shutting Down..."
48+
case .shutdown: "Boot"
49+
}
50+
}
51+
52+
var isActionAllowed: Bool {
53+
switch self {
54+
case .unknown: false
55+
case .creating: false
56+
case .booting: false
57+
case .booted: true
58+
case .shuttingDown: false
59+
case .shutdown: true
60+
}
61+
}
4062
}
4163

4264
/// The user-facing name for this simulator, e.g. iPhone 11 Pro Max.
@@ -64,7 +86,7 @@ struct Simulator: Identifiable, Comparable, Hashable {
6486
let deviceType: DeviceType?
6587

6688
/// The current state of the simulator
67-
let state: State
89+
private(set) var state: State
6890

6991
/// Wheter this simulator is the `Default` one or not
7092
var isDefault: Bool {
@@ -100,81 +122,91 @@ struct Simulator: Identifiable, Comparable, Hashable {
100122
self.image = typeIdentifier.icon
101123
}
102124

103-
func urlForFilePath(_ filePath: FilePathKind) -> URL {
104-
105-
if filePath == .root {
106-
return URL(fileURLWithPath: dataPath)
107-
}
108-
109-
let containerPath = dataPath + "/Containers/Shared/AppGroup/"
110-
111-
guard let containerContents = try? FileManager.default.contentsOfDirectory(atPath: containerPath) else {
112-
print("could not find any subfolders in '\(containerPath)'")
113-
return URL(fileURLWithPath: "")
114-
}
115-
116-
for content in containerContents {
117-
118-
if content.hasSuffix("DS_Store") { continue }
119-
120-
let subDirectoryPath = containerPath + content
121-
let plistUrl = URL(fileURLWithPath: subDirectoryPath)
122-
123-
guard let subDirectoryContents = try? FileManager.default.contentsOfDirectory(atPath: subDirectoryPath),
124-
let plistFile = subDirectoryContents.first(where: { $0.hasSuffix("plist")}),
125-
let plistData = try? Data(contentsOf: plistUrl.appendingPathComponent(plistFile)),
126-
let plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? NSDictionary else {
127-
print("could not find or decode the plist file in '\(subDirectoryPath)'")
128-
return URL(fileURLWithPath: "")
129-
}
130-
131-
for value in plist.allValues {
132-
if let value = value as? String {
133-
if value.hasSuffix(filePath.storageType) {
134-
return URL(fileURLWithPath: subDirectoryPath).appendingPathComponent("File Provider Storage", isDirectory: true)
135-
}
136-
}
137-
}
138-
}
139-
print("could not find folder of type '\(filePath)' in '\(containerPath)'")
140-
return URL(fileURLWithPath: "")
141-
}
142-
143-
func copyFilesFromProviders(_ providers: [NSItemProvider], toFilePath filePath: FilePathKind) -> Bool {
144-
for provider in providers {
145-
provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
146-
if let urlData = urlData as? Data {
147-
let sourceUrl = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
148-
do {
149-
try FileManager.default.copyItem(at: sourceUrl, to: urlForFilePath(filePath).appendingPathComponent(sourceUrl.lastPathComponent))
150-
NSSound(named: "Glass")?.play()
151-
} catch {
152-
NSSound(named: "Sosumi")?.play()
153-
print(error.localizedDescription)
154-
}
155-
} else {
156-
NSSound(named: "Sosumi")?.play()
157-
}
158-
sleep(1) // if multiple files are dropped, allow user to distinguish success/error sounds
159-
}
160-
}
161-
return true
162-
}
163-
164-
enum FilePathKind {
165-
case root, files // photos is complicated, and you can't just drop files there anyway
166-
167-
var storageType: String {
168-
switch self {
169-
case .root:
170-
print("Storage type is not applicable to the root path")
171-
return ""
172-
case .files:
173-
return "LocalStorage"
174-
}
175-
}
176-
}
125+
mutating func update(state: State) {
126+
self.state = state
127+
}
128+
129+
func open(_ filePath: FilePathKind) {
130+
NSWorkspace.shared.activateFileViewerSelecting([urlForFilePath(filePath)])
131+
}
132+
133+
func urlForFilePath(_ filePath: FilePathKind) -> URL {
134+
135+
if filePath == .root {
136+
return URL(fileURLWithPath: dataPath)
137+
}
138+
139+
let containerPath = dataPath + "/Containers/Shared/AppGroup/"
140+
141+
guard let containerContents = try? FileManager.default.contentsOfDirectory(atPath: containerPath) else {
142+
print("could not find any subfolders in '\(containerPath)'")
143+
return URL(fileURLWithPath: "")
144+
}
145+
146+
for content in containerContents {
147+
148+
if content.hasSuffix("DS_Store") { continue }
149+
150+
let subDirectoryPath = containerPath + content
151+
let plistUrl = URL(fileURLWithPath: subDirectoryPath)
152+
153+
guard let subDirectoryContents = try? FileManager.default.contentsOfDirectory(atPath: subDirectoryPath),
154+
let plistFile = subDirectoryContents.first(where: { $0.hasSuffix("plist")}),
155+
let plistData = try? Data(contentsOf: plistUrl.appendingPathComponent(plistFile)),
156+
let plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? NSDictionary else {
157+
print("could not find or decode the plist file in '\(subDirectoryPath)'")
158+
return URL(fileURLWithPath: "")
159+
}
160+
161+
for value in plist.allValues {
162+
if let value = value as? String {
163+
if value.hasSuffix(filePath.storageType) {
164+
return URL(fileURLWithPath: subDirectoryPath).appendingPathComponent("File Provider Storage", isDirectory: true)
165+
}
166+
}
167+
}
168+
}
169+
print("could not find folder of type '\(filePath)' in '\(containerPath)'")
170+
return URL(fileURLWithPath: "")
171+
}
172+
173+
func copyFilesFromProviders(_ providers: [NSItemProvider], toFilePath filePath: FilePathKind) -> Bool {
174+
for provider in providers {
175+
provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
176+
if let urlData = urlData as? Data {
177+
let sourceUrl = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
178+
do {
179+
try FileManager.default.copyItem(at: sourceUrl, to: urlForFilePath(filePath).appendingPathComponent(sourceUrl.lastPathComponent))
180+
NSSound(named: "Glass")?.play()
181+
} catch {
182+
NSSound(named: "Sosumi")?.play()
183+
print(error.localizedDescription)
184+
}
185+
} else {
186+
NSSound(named: "Sosumi")?.play()
187+
}
188+
sleep(1) // if multiple files are dropped, allow user to distinguish success/error sounds
189+
}
190+
}
191+
return true
192+
}
193+
194+
enum FilePathKind {
195+
case root, files // photos is complicated, and you can't just drop files there anyway
177196

197+
var storageType: String {
198+
switch self {
199+
case .root:
200+
print("Storage type is not applicable to the root path")
201+
return ""
202+
case .files:
203+
return "LocalStorage"
204+
}
205+
}
206+
}
207+
}
208+
209+
extension Simulator: Hashable {
178210
/// Sort simulators alphabetically, and then by OS version.
179211
static func < (lhs: Simulator, rhs: Simulator) -> Bool {
180212
if lhs.name == rhs.name,
@@ -184,7 +216,10 @@ struct Simulator: Identifiable, Comparable, Hashable {
184216
}
185217
return lhs.name < rhs.name
186218
}
219+
}
187220

221+
/// Preview
222+
extension Simulator {
188223
/// An example simulator for Xcode preview purposes
189224
static let example = Simulator(name: "iPhone 11 Pro max", udid: UUID().uuidString, state: .booted, runtime: .unknown, deviceType: nil, dataPath: "<example data path>")
190225

ControlRoom/Main Window/SimulatorAction.swift

+17-9
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,41 @@
99
import struct SwiftUI.LocalizedStringKey
1010

1111
enum Action: Int, Identifiable {
12+
case power
1213
case rename
1314
case clone
1415
case delete
16+
case openRoot
1517

1618
var id: Int { rawValue }
1719

1820
var sheetTitle: LocalizedStringKey {
1921
switch self {
20-
case .rename: return "Rename Simulator"
21-
case .clone: return "Clone Simulator"
22-
case .delete: return "Delete Simulator"
22+
case .power: ""
23+
case .rename: "Rename Simulator"
24+
case .clone: "Clone Simulator"
25+
case .delete: "Delete Simulator"
26+
case .openRoot: ""
2327
}
2428
}
2529

2630
var sheetMessage: LocalizedStringKey {
2731
switch self {
28-
case .rename: return "Enter a new name for this simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
29-
case .clone: return "Enter a name for the new simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
30-
case .delete: return "Are you sure you want to delete this simulator? You will not be able to undo this action."
32+
case .power: ""
33+
case .rename: "Enter a new name for this simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
34+
case .clone: "Enter a name for the new simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
35+
case .delete: "Are you sure you want to delete this simulator? You will not be able to undo this action."
36+
case .openRoot: ""
3137
}
3238
}
3339

3440
var saveActionTitle: LocalizedStringKey {
3541
switch self {
36-
case .rename: return "Rename"
37-
case .clone: return "Clone"
38-
case .delete: return "Delete"
42+
case .power: "Power"
43+
case .rename: "Rename"
44+
case .clone: "Clone"
45+
case .delete: "Delete"
46+
case .openRoot: ""
3947
}
4048
}
4149
}

ControlRoom/Main Window/SimulatorSidebarView.swift

+26-11
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import KeyboardShortcuts
1111

1212
/// Shows one simulator in the sidebar.
1313
struct SimulatorSidebarView: View {
14-
let simulator: Simulator
14+
var simulator: Simulator
1515
let canShowContextualMenu: Bool
1616

1717
@State private var action: Action?
@@ -56,26 +56,26 @@ struct SimulatorSidebarView: View {
5656
.padding(.top, 2)
5757
.shadow(color: .primary, radius: 1)
5858
Text(simulatorSummary)
59-
Spacer()
6059
}
60+
.frame(alignment: .leading)
6161
.contextMenu(
6262
ContextMenu(shouldDisplay: canShowContextualMenu) {
63+
Button("\(simulator.state.menuActionName)") { performAction(.power) }
64+
.disabled(!simulator.state.isActionAllowed)
65+
Divider()
6366
Button("Rename...") { action = .rename }
6467
Button("Clone...") { action = .clone }
6568
.disabled(simulator.state == .booted)
6669
Button("Delete...") { action = .delete }
70+
Divider()
71+
Button("Open in Finder") { performAction(.openRoot) }
6772
}
6873
)
6974
.sheet(item: $action) { action in
70-
if action == .delete {
71-
SimulatorActionSheet(
72-
icon: simulator.image,
73-
message: action.sheetTitle,
74-
informativeText: action.sheetMessage,
75-
confirmationTitle: action.saveActionTitle,
76-
confirm: { performAction(action) }
77-
)
78-
} else {
75+
switch action {
76+
case .power, .openRoot:
77+
EmptyView()
78+
case .rename, .clone:
7979
SimulatorActionSheet(
8080
icon: simulator.image,
8181
message: action.sheetTitle,
@@ -87,6 +87,13 @@ struct SimulatorSidebarView: View {
8787
TextField("Name", text: $newName)
8888
}
8989
)
90+
case .delete:
91+
SimulatorActionSheet(
92+
icon: simulator.image,
93+
message: action.sheetTitle,
94+
informativeText: action.sheetMessage,
95+
confirmationTitle: action.saveActionTitle,
96+
confirm: { performAction(action) })
9097
}
9198
}
9299
}
@@ -98,6 +105,14 @@ struct SimulatorSidebarView: View {
98105
case .rename: SimCtl.rename(simulator.udid, name: newName)
99106
case .clone: SimCtl.clone(simulator.udid, name: newName)
100107
case .delete: SimCtl.delete([simulator.udid])
108+
case .power:
109+
if simulator.state == .booted {
110+
SimCtl.shutdown(simulator.udid)
111+
} else if simulator.state == .shutdown {
112+
SimCtl.boot(simulator)
113+
}
114+
case .openRoot:
115+
simulator.open(.root)
101116
}
102117
}
103118
}

ControlRoom/Simulator UI/ControlScreens/SystemView/SystemView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ struct SystemView: View {
205205
}
206206

207207
func openInFinder(_ filePath: Simulator.FilePathKind) {
208-
NSWorkspace.shared.activateFileViewerSelecting([simulator.urlForFilePath(filePath)])
208+
simulator.open(filePath)
209209
}
210210

211211
func openInTerminal(_ filePath: Simulator.FilePathKind) {

0 commit comments

Comments
 (0)