mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Add Xcode command menu
This commit is contained in:
parent
912ac0a28e
commit
4d2600f821
7 changed files with 254 additions and 18 deletions
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; };
|
||||
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
|
||||
CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; };
|
||||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
|
||||
|
|
@ -59,6 +60,8 @@
|
|||
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A52449575000113D76 /* Assets.xcassets */; };
|
||||
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A82449575000113D76 /* Preview Assets.xcassets */; };
|
||||
CAD2E7B82449575100113D76 /* XcodesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7B72449575100113D76 /* XcodesTests.swift */; };
|
||||
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */; };
|
||||
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB942598FE96003DCC5A /* FocusedValues.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -73,6 +76,7 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = "<group>"; };
|
||||
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
|
||||
CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = "<group>"; };
|
||||
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -129,6 +133,8 @@
|
|||
CAD2E7B32449575100113D76 /* XcodesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XcodesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CAD2E7B72449575100113D76 /* XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesTests.swift; sourceTree = "<group>"; };
|
||||
CAD2E7B92449575100113D76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedXcode.swift; sourceTree = "<group>"; };
|
||||
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -221,17 +227,20 @@
|
|||
CABFA9B22592EEEA00380FEE /* Entry+.swift */,
|
||||
CABFA9A92592EEE900380FEE /* Environment.swift */,
|
||||
CABFA9B82592EEEA00380FEE /* FileManager+.swift */,
|
||||
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */,
|
||||
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
|
||||
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
|
||||
CABFA9AE2592EEE900380FEE /* Path+.swift */,
|
||||
CABFA9B42592EEEA00380FEE /* Process.swift */,
|
||||
CABFA9B02592EEEA00380FEE /* Promise+.swift */,
|
||||
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */,
|
||||
CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */,
|
||||
CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */,
|
||||
CABFA9A82592EEE900380FEE /* Version+.swift */,
|
||||
CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */,
|
||||
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */,
|
||||
CA61A6DF259835580008926E /* Xcode.swift */,
|
||||
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */,
|
||||
);
|
||||
path = Backend;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -462,6 +471,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */,
|
||||
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */,
|
||||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
||||
|
|
@ -481,11 +491,13 @@
|
|||
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
|
||||
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
|
||||
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */,
|
||||
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
|
||||
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
|
||||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
|
||||
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
|
||||
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
|
||||
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
|
||||
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ class AppState: ObservableObject {
|
|||
@Published var presentingSignInAlert = false
|
||||
@Published var isProcessingAuthRequest = false
|
||||
@Published var secondFactorData: SecondFactorData?
|
||||
// Selected in the Xcode list, not in the xcode-select sense
|
||||
// This probably belongs as private @State in XcodeListView,
|
||||
// but we need it here instead so that it can be a focusedValue at the top level in XcodesApp instead of in a list row. The latter seems more like how the focusedValue API is supposed to work, but currently doesn't.
|
||||
@Published var selectedXcodeID: Xcode.ID?
|
||||
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
|
||||
|
||||
init() {
|
||||
|
|
|
|||
16
Xcodes/Backend/FocusedValues.swift
Normal file
16
Xcodes/Backend/FocusedValues.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import SwiftUI
|
||||
|
||||
// MARK: - FocusedXcodeKey
|
||||
|
||||
struct FocusedXcodeKey : FocusedValueKey {
|
||||
typealias Value = SelectedXcode
|
||||
}
|
||||
|
||||
// MARK: - FocusedValues
|
||||
|
||||
extension FocusedValues {
|
||||
var selectedXcode: FocusedXcodeKey.Value? {
|
||||
get { self[FocusedXcodeKey.self] }
|
||||
set { self[FocusedXcodeKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
35
Xcodes/Backend/SelectedXcode.swift
Normal file
35
Xcodes/Backend/SelectedXcode.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import Foundation
|
||||
|
||||
/// As part of the unexpected way we have to use focusedValue in XcodesApp, we need to provide an `Optional<Xcode>` because there isn't always a selected Xcode in the focused window.
|
||||
/// But FocusedValueKey.Value is already optional, because there might not be a focused UI element to begin with, so the type ends up being `Optional<Optional<Xcode>>`.
|
||||
/// This is weird enough, but I wasn't able to find a way to have FocusedXcodeKey.Value be `Optional<Optional<Xcode>>` and still compile.
|
||||
/// There was always an error somewhere in either the use of @FocusedValue or FocusedValues.xcode or .focusedValue, as if it is only ever expecting a single level of optionality.
|
||||
/// But! If we make our own Optional replica like SelectedXcode, it _does_ compile, and there's some more noise required to turn it back into an `Optional<Xcode>`.
|
||||
/// All this to say, maybe one day we don't need to have this type at all.
|
||||
enum SelectedXcode {
|
||||
case none
|
||||
case some(Xcode)
|
||||
|
||||
init(_ optional: Optional<Xcode>) {
|
||||
switch optional {
|
||||
case .none: self = .none
|
||||
case let .some(xcode): self = .some(xcode)
|
||||
}
|
||||
}
|
||||
|
||||
var asOptional: Xcode? {
|
||||
switch self {
|
||||
case .none: return .none
|
||||
case let .some(xcode): return .some(xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional where Wrapped == SelectedXcode {
|
||||
var unwrapped: Xcode? {
|
||||
switch self {
|
||||
case Optional<SelectedXcode>.none: return Optional<Xcode>.none
|
||||
case let .some(selectedXcode): return selectedXcode.asOptional
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Xcodes/Backend/XcodeCommands.swift
Normal file
172
Xcodes/Backend/XcodeCommands.swift
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import SwiftUI
|
||||
|
||||
// MARK: - CommandMenu
|
||||
|
||||
struct XcodeCommands: Commands {
|
||||
// CommandMenus don't participate in the environment hierarchy, so we need to shuffle AppState along to the individual Commands manually.
|
||||
let appState: AppState
|
||||
|
||||
var body: some Commands {
|
||||
CommandMenu("Xcode") {
|
||||
Group {
|
||||
InstallCommand()
|
||||
|
||||
Divider()
|
||||
|
||||
SelectCommand()
|
||||
LaunchCommand()
|
||||
RevealCommand()
|
||||
CopyPathCommand()
|
||||
}
|
||||
.environmentObject(appState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Buttons
|
||||
// These are used for both context menus and commands
|
||||
|
||||
struct InstallButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: uninstallOrInstall) {
|
||||
if let xcode = xcode {
|
||||
Text(xcode.installed == true ? "Uninstall" : "Install")
|
||||
} else {
|
||||
Text("Install")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uninstallOrInstall() {
|
||||
guard let xcode = xcode else { return }
|
||||
if xcode.installed {
|
||||
appState.xcodeBeingConfirmedForUninstallation = xcode
|
||||
} else {
|
||||
appState.install(id: xcode.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: select) {
|
||||
Text("Select")
|
||||
}
|
||||
}
|
||||
|
||||
private func select() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.select(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct LaunchButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: launch) {
|
||||
Text("Launch")
|
||||
}
|
||||
}
|
||||
|
||||
private func launch() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.launch(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct RevealButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: reveal) {
|
||||
Text("Reveal in Finder")
|
||||
}
|
||||
}
|
||||
|
||||
private func reveal() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.reveal(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct CopyPathButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: copyPath) {
|
||||
Text("Copy Path")
|
||||
}
|
||||
}
|
||||
|
||||
private func copyPath() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.copyPath(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Commands
|
||||
|
||||
struct InstallCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
InstallButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped == nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
SelectButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut("s", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
||||
struct LaunchCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
LaunchButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut(KeyboardShortcut(.downArrow, modifiers: .command))
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
||||
struct RevealCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
RevealButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut("r", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
||||
struct CopyPathCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
CopyPathButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut("c", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ struct XcodeListView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
List(visibleXcodes, selection: $selection) { xcode in
|
||||
List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(xcode.description)
|
||||
|
|
@ -54,31 +54,23 @@ struct XcodeListView: View {
|
|||
print("Installing...")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(installed: xcode.installed,
|
||||
highlighted: selection == xcode.id))
|
||||
highlighted: appState.selectedXcodeID == xcode.id))
|
||||
.disabled(xcode.installed)
|
||||
}
|
||||
Text(verbatim: xcode.path ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(selection == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
|
||||
.foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { xcode.installed ? appState.xcodeBeingConfirmedForUninstallation = xcode : self.appState.install(id: xcode.id) }) {
|
||||
Text(xcode.installed ? "Uninstall" : "Install")
|
||||
}
|
||||
InstallButton(xcode: xcode)
|
||||
|
||||
Divider()
|
||||
|
||||
if xcode.installed {
|
||||
Button(action: { self.appState.select(id: xcode.id) }) {
|
||||
Text("Select")
|
||||
}
|
||||
Button(action: { self.appState.launch(id: xcode.id) }) {
|
||||
Text("Launch")
|
||||
}
|
||||
Button(action: { self.appState.reveal(id: xcode.id) }) {
|
||||
Text("Reveal in Finder")
|
||||
}
|
||||
Button(action: { self.appState.copyPath(id: xcode.id) }) {
|
||||
Text("Copy Path")
|
||||
}
|
||||
SelectButton(xcode: xcode)
|
||||
LaunchButton(xcode: xcode)
|
||||
RevealButton(xcode: xcode)
|
||||
CopyPathButton(xcode: xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ struct XcodesApp: App {
|
|||
appState.updateIfNeeded()
|
||||
}
|
||||
}
|
||||
// I'm expecting to be able to use this modifier on a List row, but using it at the top level here is the only way that has made XcodeCommands work so far.
|
||||
// FB8954571 focusedValue(_:_:) on List row doesn't propagate value to @FocusedValue
|
||||
.focusedValue(\.selectedXcode, SelectedXcode(appState.allXcodes.first { $0.id == appState.selectedXcodeID }))
|
||||
}
|
||||
.commands {
|
||||
CommandGroup(replacing: .appInfo) {
|
||||
|
|
@ -34,6 +37,8 @@ struct XcodesApp: App {
|
|||
.keyboardShortcut(KeyEquivalent("r"))
|
||||
.disabled(appState.isUpdating)
|
||||
}
|
||||
|
||||
XcodeCommands(appState: appState)
|
||||
}
|
||||
|
||||
Settings {
|
||||
|
|
|
|||
Loading…
Reference in a new issue